import { describe, it, expect, beforeAll } from 'vitest'; import { analyzeQuery, initSqlParser } from '../../src/utils/sql-parser.js'; beforeAll(async () => { await initSqlParser(); }); describe('analyzeQuery', () => { // =================== // RECORDS (DML) - with table name // =================== describe('records - INSERT', () => { it('simple insert', () => { expect(analyzeQuery('INSERT INTO users (name) VALUES ("john")')).toEqual([ { type: 'records', name: 'users' }, ]); }); it('insert with schema', () => { expect(analyzeQuery('INSERT INTO public.users (name) VALUES ("john")')).toEqual([ { type: 'records', name: 'users' }, ]); }); it('insert multiple rows', () => { expect(analyzeQuery("INSERT INTO products (name) VALUES ('a'), ('b'), ('c')")).toEqual([ { type: 'records', name: 'products' }, ]); }); it('insert with returning', () => { expect(analyzeQuery('INSERT INTO orders (total) VALUES (100) RETURNING id')).toEqual([ { type: 'records', name: 'orders' }, ]); }); it('insert with on conflict', () => { expect( analyzeQuery( 'INSERT INTO users (id, name) VALUES (1, "john") ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name' ) ).toEqual([{ type: 'records', name: 'users' }]); }); it('insert from select', () => { expect( analyzeQuery('INSERT INTO archive SELECT * FROM logs WHERE created_at < NOW()') ).toEqual([{ type: 'records', name: 'archive' }]); }); }); describe('records - UPDATE', () => { it('simple update', () => { expect(analyzeQuery('UPDATE users SET name = "jane" WHERE id = 1')).toEqual([ { type: 'records', name: 'users' }, ]); }); it('update with schema', () => { expect(analyzeQuery('UPDATE public.posts SET title = "new" WHERE id = 1')).toEqual([ { type: 'records', name: 'posts' }, ]); }); it('update multiple columns', () => { expect( analyzeQuery( 'UPDATE users SET name = "jane", email = "jane@test.com", updated_at = NOW() WHERE id = 1' ) ).toEqual([{ type: 'records', name: 'users' }]); }); it('update with subquery', () => { expect( analyzeQuery( 'UPDATE orders SET status = "shipped" WHERE id IN (SELECT order_id FROM shipments)' ) ).toEqual([{ type: 'records', name: 'orders' }]); }); it('update with returning', () => { expect(analyzeQuery('UPDATE products SET price = price * 1.1 RETURNING id, price')).toEqual([ { type: 'records', name: 'products' }, ]); }); }); describe('records - DELETE', () => { it('simple delete', () => { expect(analyzeQuery('DELETE FROM users WHERE id = 1')).toEqual([ { type: 'records', name: 'users' }, ]); }); it('delete with schema', () => { expect(analyzeQuery('DELETE FROM public.logs WHERE created_at < NOW()')).toEqual([ { type: 'records', name: 'logs' }, ]); }); it('delete all', () => { expect(analyzeQuery('DELETE FROM temp_table')).toEqual([ { type: 'records', name: 'temp_table' }, ]); }); it('delete with subquery', () => { expect( analyzeQuery( 'DELETE FROM orders WHERE user_id IN (SELECT id FROM users WHERE banned = true)' ) ).toEqual([{ type: 'records', name: 'orders' }]); }); it('delete with returning', () => { expect(analyzeQuery('DELETE FROM sessions WHERE expired = true RETURNING id')).toEqual([ { type: 'records', name: 'sessions' }, ]); }); }); // =================== // TABLES (CREATE/DROP) - no name // =================== describe('tables - CREATE/DROP TABLE', () => { it('create table', () => { expect(analyzeQuery('CREATE TABLE users (id INT)')).toEqual([{ type: 'tables' }]); }); it('create table if not exists', () => { expect(analyzeQuery('CREATE TABLE IF NOT EXISTS posts (id UUID PRIMARY KEY)')).toEqual([ { type: 'tables' }, ]); }); it('create table with schema', () => { expect(analyzeQuery('CREATE TABLE public.comments (id SERIAL PRIMARY KEY)')).toEqual([ { type: 'tables' }, ]); }); it('create table with constraints', () => { expect( analyzeQuery( 'CREATE TABLE orders (id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), total DECIMAL NOT NULL)' ) ).toEqual([{ type: 'tables' }]); }); it('drop table', () => { expect(analyzeQuery('DROP TABLE users')).toEqual([{ type: 'tables' }]); }); it('drop table if exists', () => { expect(analyzeQuery('DROP TABLE IF EXISTS temp_data')).toEqual([{ type: 'tables' }]); }); it('drop table cascade', () => { expect(analyzeQuery('DROP TABLE orders CASCADE')).toEqual([{ type: 'tables' }]); }); }); // =================== // TABLE (ALTER) - with name // =================== describe('table - ALTER TABLE', () => { it('add column', () => { expect(analyzeQuery('ALTER TABLE users ADD COLUMN email VARCHAR(255)')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('drop column', () => { expect(analyzeQuery('ALTER TABLE posts DROP COLUMN temp_field')).toEqual([ { type: 'table', name: 'posts' }, ]); }); it('rename column', () => { expect(analyzeQuery('ALTER TABLE users RENAME COLUMN name TO full_name')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('alter column type', () => { expect(analyzeQuery('ALTER TABLE products ALTER COLUMN price TYPE NUMERIC(10,2)')).toEqual([ { type: 'table', name: 'products' }, ]); }); it('add constraint', () => { expect( analyzeQuery( 'ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)' ) ).toEqual([{ type: 'table', name: 'orders' }]); }); it('enable RLS', () => { expect(analyzeQuery('ALTER TABLE users ENABLE ROW LEVEL SECURITY')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('disable RLS', () => { expect(analyzeQuery('ALTER TABLE users DISABLE ROW LEVEL SECURITY')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('force RLS', () => { expect(analyzeQuery('ALTER TABLE users FORCE ROW LEVEL SECURITY')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('set not null', () => { expect(analyzeQuery('ALTER TABLE users ALTER COLUMN email SET NOT NULL')).toEqual([ { type: 'table', name: 'users' }, ]); }); it('set default', () => { expect(analyzeQuery('ALTER TABLE users ALTER COLUMN created_at SET DEFAULT NOW()')).toEqual([ { type: 'table', name: 'users' }, ]); }); }); // =================== // INDEX - no name // =================== describe('index', () => { it('create index', () => { expect(analyzeQuery('CREATE INDEX idx_users_email ON users (email)')).toEqual([ { type: 'index' }, ]); }); it('create unique index', () => { expect(analyzeQuery('CREATE UNIQUE INDEX idx_users_email ON users (email)')).toEqual([ { type: 'index' }, ]); }); it('create index concurrently', () => { expect( analyzeQuery('CREATE INDEX CONCURRENTLY idx_orders_date ON orders (created_at)') ).toEqual([{ type: 'index' }]); }); it('create partial index', () => { expect( analyzeQuery('CREATE INDEX idx_active_users ON users (id) WHERE active = true') ).toEqual([{ type: 'index' }]); }); it('create index with expression', () => { expect(analyzeQuery('CREATE INDEX idx_users_lower_email ON users (LOWER(email))')).toEqual([ { type: 'index' }, ]); }); it('drop index', () => { expect(analyzeQuery('DROP INDEX idx_users_email')).toEqual([{ type: 'index' }]); }); it('drop index if exists', () => { expect(analyzeQuery('DROP INDEX IF EXISTS idx_old')).toEqual([{ type: 'index' }]); }); }); // =================== // TRIGGER - no name // =================== describe('trigger', () => { it('create trigger before insert', () => { expect( analyzeQuery( 'CREATE TRIGGER set_timestamp BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION update_modified()' ) ).toEqual([{ type: 'trigger' }]); }); it('create trigger after update', () => { expect( analyzeQuery( 'CREATE TRIGGER audit_log AFTER UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION log_changes()' ) ).toEqual([{ type: 'trigger' }]); }); it('create trigger before delete', () => { expect( analyzeQuery( 'CREATE TRIGGER prevent_delete BEFORE DELETE ON users FOR EACH ROW EXECUTE FUNCTION check_delete()' ) ).toEqual([{ type: 'trigger' }]); }); it('create or replace trigger', () => { expect( analyzeQuery( 'CREATE OR REPLACE TRIGGER update_ts BEFORE UPDATE ON posts FOR EACH ROW EXECUTE FUNCTION update_modified()' ) ).toEqual([{ type: 'trigger' }]); }); it('drop trigger', () => { expect(analyzeQuery('DROP TRIGGER update_timestamp ON users')).toEqual([{ type: 'trigger' }]); }); it('drop trigger if exists', () => { expect(analyzeQuery('DROP TRIGGER IF EXISTS old_trigger ON users')).toEqual([ { type: 'trigger' }, ]); }); }); // =================== // POLICY - no name // =================== describe('policy', () => { it('create policy for select', () => { expect( analyzeQuery('CREATE POLICY user_select ON users FOR SELECT USING (id = current_user_id())') ).toEqual([{ type: 'policy' }]); }); it('create policy for insert', () => { expect( analyzeQuery('CREATE POLICY user_insert ON users FOR INSERT WITH CHECK (true)') ).toEqual([{ type: 'policy' }]); }); it('create policy for update', () => { expect( analyzeQuery( 'CREATE POLICY user_update ON users FOR UPDATE USING (id = current_user_id()) WITH CHECK (id = current_user_id())' ) ).toEqual([{ type: 'policy' }]); }); it('create policy for delete', () => { expect( analyzeQuery('CREATE POLICY user_delete ON users FOR DELETE USING (id = current_user_id())') ).toEqual([{ type: 'policy' }]); }); it('create policy for all', () => { expect( analyzeQuery( 'CREATE POLICY full_access ON orders FOR ALL USING (user_id = current_user_id())' ) ).toEqual([{ type: 'policy' }]); }); it('create permissive policy', () => { expect( analyzeQuery( 'CREATE POLICY admin_access ON users AS PERMISSIVE FOR ALL TO admin USING (true)' ) ).toEqual([{ type: 'policy' }]); }); it('create restrictive policy', () => { expect( analyzeQuery( 'CREATE POLICY tenant_isolation ON orders AS RESTRICTIVE FOR ALL USING (tenant_id = current_tenant())' ) ).toEqual([{ type: 'policy' }]); }); it('alter policy', () => { expect( analyzeQuery( 'ALTER POLICY user_select ON users USING (id = current_user_id() OR is_admin())' ) ).toEqual([{ type: 'policy' }]); }); it('drop policy', () => { expect(analyzeQuery('DROP POLICY user_policy ON users')).toEqual([{ type: 'policy' }]); }); it('drop policy if exists', () => { expect(analyzeQuery('DROP POLICY IF EXISTS old_policy ON users')).toEqual([ { type: 'policy' }, ]); }); }); // =================== // FUNCTION - no name // =================== describe('function', () => { it('create function sql', () => { expect( analyzeQuery( 'CREATE FUNCTION get_user(id INT) RETURNS TEXT AS $$ SELECT name FROM users WHERE id = $1 $$ LANGUAGE sql' ) ).toEqual([{ type: 'function' }]); }); it('create function plpgsql', () => { expect( analyzeQuery( 'CREATE FUNCTION update_modified() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql' ) ).toEqual([{ type: 'function' }]); }); it('create or replace function', () => { expect( analyzeQuery( 'CREATE OR REPLACE FUNCTION current_user_id() RETURNS UUID AS $$ SELECT auth.uid() $$ LANGUAGE sql' ) ).toEqual([{ type: 'function' }]); }); it('create function with security definer', () => { expect( analyzeQuery( 'CREATE FUNCTION admin_action() RETURNS VOID AS $$ UPDATE users SET role = "admin" $$ LANGUAGE sql SECURITY DEFINER' ) ).toEqual([{ type: 'function' }]); }); it('drop function', () => { expect(analyzeQuery('DROP FUNCTION get_user')).toEqual([{ type: 'function' }]); }); it('drop function with args', () => { expect(analyzeQuery('DROP FUNCTION get_user(INT)')).toEqual([{ type: 'function' }]); }); it('drop function if exists', () => { expect(analyzeQuery('DROP FUNCTION IF EXISTS old_func')).toEqual([{ type: 'function' }]); }); }); // =================== // EXTENSION - no name // =================== describe('extension', () => { it('create extension', () => { expect(analyzeQuery('CREATE EXTENSION "uuid-ossp"')).toEqual([{ type: 'extension' }]); }); it('create extension if not exists', () => { expect(analyzeQuery('CREATE EXTENSION IF NOT EXISTS pgcrypto')).toEqual([ { type: 'extension' }, ]); }); it('create extension with schema', () => { expect(analyzeQuery('CREATE EXTENSION hstore WITH SCHEMA public')).toEqual([ { type: 'extension' }, ]); }); it('drop extension', () => { expect(analyzeQuery('DROP EXTENSION "uuid-ossp"')).toEqual([{ type: 'extension' }]); }); it('drop extension cascade', () => { expect(analyzeQuery('DROP EXTENSION pgcrypto CASCADE')).toEqual([{ type: 'extension' }]); }); }); // =================== // SELECT - ignored // =================== describe('SELECT (ignored)', () => { it('simple select', () => { expect(analyzeQuery('SELECT * FROM users')).toEqual([]); }); it('select with joins', () => { expect( analyzeQuery('SELECT u.*, p.title FROM users u JOIN posts p ON u.id = p.user_id') ).toEqual([]); }); it('select with subquery', () => { expect(analyzeQuery('SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)')).toEqual( [] ); }); it('select with cte', () => { expect( analyzeQuery( 'WITH active_users AS (SELECT * FROM users WHERE active) SELECT * FROM active_users' ) ).toEqual([]); }); it('select for update (still ignored)', () => { expect(analyzeQuery('SELECT * FROM users WHERE id = 1 FOR UPDATE')).toEqual([]); }); }); // =================== // MULTI-STATEMENT // =================== describe('multi-statement', () => { it('multiple inserts', () => { const result = analyzeQuery( "INSERT INTO users (name) VALUES ('a'); INSERT INTO posts (title) VALUES ('b');" ); expect(result).toEqual([ { type: 'records', name: 'users' }, { type: 'records', name: 'posts' }, ]); }); it('create table + insert', () => { const result = analyzeQuery('CREATE TABLE temp (id INT); INSERT INTO temp VALUES (1);'); expect(result).toEqual([{ type: 'tables' }, { type: 'records', name: 'temp' }]); }); it('full table setup with RLS', () => { const result = analyzeQuery(` CREATE TABLE orders (id UUID PRIMARY KEY, user_id UUID NOT NULL); ALTER TABLE orders ENABLE ROW LEVEL SECURITY; CREATE POLICY orders_select ON orders FOR SELECT USING (user_id = auth.uid()); CREATE INDEX idx_orders_user ON orders (user_id); `); expect(result).toHaveLength(4); expect(result[0]).toEqual({ type: 'tables' }); expect(result[1]).toEqual({ type: 'table', name: 'orders' }); expect(result[2]).toEqual({ type: 'policy' }); expect(result[3]).toEqual({ type: 'index' }); }); it('migration: drop and recreate', () => { const result = analyzeQuery(` DROP TABLE IF EXISTS old_data; DROP INDEX IF EXISTS idx_old; DROP POLICY IF EXISTS old_policy ON users; CREATE TABLE new_data (id INT); ALTER TABLE users ADD COLUMN new_col TEXT; `); // tables is deduplicated (DROP + CREATE both emit 'tables') expect(result).toHaveLength(4); expect(result).toContainEqual({ type: 'tables' }); expect(result).toContainEqual({ type: 'index' }); expect(result).toContainEqual({ type: 'policy' }); expect(result).toContainEqual({ type: 'table', name: 'users' }); }); it('batch DML operations', () => { const result = analyzeQuery(` INSERT INTO products (name) VALUES ('new'); UPDATE products SET price = 100 WHERE name = 'new'; DELETE FROM products WHERE stock = 0; `); // Deduplicated to single entry expect(result).toEqual([{ type: 'records', name: 'products' }]); }); it('mixed with selects (selects ignored)', () => { const result = analyzeQuery(` SELECT * FROM users; INSERT INTO logs (action) VALUES ('viewed'); SELECT COUNT(*) FROM logs; `); expect(result).toEqual([{ type: 'records', name: 'logs' }]); }); it('complete app setup', () => { const result = analyzeQuery(` CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE users (id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, email TEXT UNIQUE); CREATE TABLE posts (id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), title TEXT); CREATE INDEX idx_posts_user ON posts (user_id); ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; CREATE POLICY users_policy ON users FOR ALL USING (id = auth.uid()); CREATE POLICY posts_policy ON posts FOR ALL USING (user_id = auth.uid()); CREATE FUNCTION auth.uid() RETURNS UUID AS $$ SELECT current_setting('app.user_id')::UUID $$ LANGUAGE sql; `); // Deduplicated: 2 CREATE TABLEs -> 1 'tables', 2 CREATE POLICYs -> 1 'policy' expect(result).toHaveLength(7); expect(result.filter((r) => r.type === 'extension')).toHaveLength(1); expect(result.filter((r) => r.type === 'tables')).toHaveLength(1); expect(result.filter((r) => r.type === 'index')).toHaveLength(1); expect(result.filter((r) => r.type === 'table')).toHaveLength(2); // ALTER users + ALTER posts (different names) expect(result.filter((r) => r.type === 'policy')).toHaveLength(1); expect(result.filter((r) => r.type === 'function')).toHaveLength(1); }); }); // =================== // EDGE CASES // =================== describe('edge cases', () => { it('empty string', () => { expect(analyzeQuery('')).toEqual([]); }); it('whitespace only', () => { expect(analyzeQuery(' \n\t ')).toEqual([]); }); it('comment only', () => { expect(analyzeQuery('-- this is a comment')).toEqual([]); }); it('block comment only', () => { expect(analyzeQuery('/* block comment */')).toEqual([]); }); it('invalid SQL', () => { expect(analyzeQuery('THIS IS NOT SQL')).toEqual([]); }); it('table name with underscore', () => { expect(analyzeQuery('INSERT INTO user_profiles (name) VALUES ("test")')).toEqual([ { type: 'records', name: 'user_profiles' }, ]); }); it('table name with quotes', () => { expect(analyzeQuery('INSERT INTO "user-data" (name) VALUES ("test")')).toEqual([ { type: 'records', name: 'user-data' }, ]); }); it('schema qualified table', () => { expect(analyzeQuery('UPDATE myschema.mytable SET x = 1')).toEqual([ { type: 'records', name: 'mytable' }, ]); }); it('semicolon in string literal', () => { expect(analyzeQuery("INSERT INTO logs (msg) VALUES ('hello; world')")).toEqual([ { type: 'records', name: 'logs' }, ]); }); }); // =================== // DEDUPLICATION // =================== describe('deduplication', () => { it('deduplicates multiple inserts to same table', () => { const result = analyzeQuery(` INSERT INTO users (name) VALUES ('a'); INSERT INTO users (name) VALUES ('b'); INSERT INTO users (name) VALUES ('c'); `); expect(result).toEqual([{ type: 'records', name: 'users' }]); }); it('deduplicates multiple updates to same table', () => { const result = analyzeQuery(` UPDATE products SET price = 10 WHERE id = 1; UPDATE products SET price = 20 WHERE id = 2; `); expect(result).toEqual([{ type: 'records', name: 'products' }]); }); it('keeps different tables separate', () => { const result = analyzeQuery(` INSERT INTO users (name) VALUES ('a'); INSERT INTO posts (title) VALUES ('b'); INSERT INTO users (name) VALUES ('c'); `); expect(result).toEqual([ { type: 'records', name: 'users' }, { type: 'records', name: 'posts' }, ]); }); it('keeps different types separate', () => { const result = analyzeQuery(` INSERT INTO users (name) VALUES ('a'); ALTER TABLE users ADD COLUMN foo TEXT; `); expect(result).toEqual([ { type: 'records', name: 'users' }, { type: 'table', name: 'users' }, ]); }); it('deduplicates DDL without names', () => { const result = analyzeQuery(` CREATE INDEX idx1 ON users (a); CREATE INDEX idx2 ON users (b); CREATE INDEX idx3 ON posts (c); `); expect(result).toEqual([{ type: 'index' }]); }); }); });