import { TABLE_EVENT_ACTIONS } from 'common/telemetry-constants' import { describe, expect, it } from 'vitest' import { sqlEventParser } from './sql-event-parser' describe('SQL Event Parser', () => { describe('CREATE TABLE detection', () => { it('detects basic CREATE TABLE', () => { const results = sqlEventParser.getTableEvents('CREATE TABLE users (id INT PRIMARY KEY)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'users', }) }) it('detects CREATE TABLE with schema', () => { const results = sqlEventParser.getTableEvents('CREATE TABLE public.users (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: 'public', tableName: 'users', }) }) it('detects CREATE TABLE IF NOT EXISTS', () => { const results = sqlEventParser.getTableEvents('CREATE TABLE IF NOT EXISTS users (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'users', }) }) it('handles quoted identifiers', () => { const results = sqlEventParser.getTableEvents('CREATE TABLE "public"."user_table" (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: 'public', tableName: 'user_table', }) }) it('returns empty array for non-matching SQL', () => { const results = sqlEventParser.getTableEvents('SELECT * FROM users') expect(results).toHaveLength(0) }) it('detects CREATE TEMPORARY TABLE', () => { const results = sqlEventParser.getTableEvents('CREATE TEMPORARY TABLE temp_users (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'temp_users', }) }) it('detects CREATE TEMP TABLE', () => { const results = sqlEventParser.getTableEvents('CREATE TEMP TABLE temp_users (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'temp_users', }) }) it('detects CREATE UNLOGGED TABLE', () => { const results = sqlEventParser.getTableEvents('CREATE UNLOGGED TABLE fast_table (id INT)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'fast_table', }) }) it('detects CREATE TEMP TABLE IF NOT EXISTS', () => { const results = sqlEventParser.getTableEvents( 'CREATE TEMP TABLE IF NOT EXISTS temp_users (id INT)' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'temp_users', }) }) }) describe('INSERT detection', () => { it('detects basic INSERT INTO', () => { const results = sqlEventParser.getTableEvents("INSERT INTO users (name) VALUES ('John')") expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: undefined, tableName: 'users', }) }) it('detects INSERT with schema', () => { const results = sqlEventParser.getTableEvents( "INSERT INTO public.users (name) VALUES ('John')" ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: 'public', tableName: 'users', }) }) it('handles quoted identifiers', () => { const results = sqlEventParser.getTableEvents('INSERT INTO "auth"."users" (id) VALUES (1)') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: 'auth', tableName: 'users', }) }) it('returns empty array for non-matching SQL', () => { const results = sqlEventParser.getTableEvents('UPDATE users SET name = "John"') expect(results).toHaveLength(0) }) }) describe('COPY detection', () => { it('detects basic COPY FROM', () => { const results = sqlEventParser.getTableEvents("COPY users FROM '/tmp/users.csv'") expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: undefined, tableName: 'users', }) }) it('detects COPY with schema', () => { const results = sqlEventParser.getTableEvents( "COPY public.users FROM '/tmp/users.csv' WITH CSV HEADER" ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: 'public', tableName: 'users', }) }) it('handles quoted identifiers', () => { const results = sqlEventParser.getTableEvents('COPY "auth"."users" FROM STDIN') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableDataAdded, schema: 'auth', tableName: 'users', }) }) it('returns empty array for COPY TO', () => { const results = sqlEventParser.getTableEvents("COPY users TO '/tmp/users.csv'") expect(results).toHaveLength(0) }) it('returns empty array for non-matching SQL', () => { const results = sqlEventParser.getTableEvents('SELECT * FROM users') expect(results).toHaveLength(0) }) }) describe('SELECT INTO detection', () => { it('detects SELECT INTO', () => { const results = sqlEventParser.getTableEvents('SELECT * INTO new_users FROM users') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'new_users', }) }) it('detects SELECT INTO with schema', () => { const results = sqlEventParser.getTableEvents( 'SELECT id, name INTO public.new_users FROM users' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: 'public', tableName: 'new_users', }) }) it('detects CREATE TABLE AS SELECT', () => { const results = sqlEventParser.getTableEvents('CREATE TABLE new_users AS SELECT * FROM users') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'new_users', }) }) it('detects CREATE TABLE IF NOT EXISTS AS SELECT', () => { const results = sqlEventParser.getTableEvents( 'CREATE TABLE IF NOT EXISTS new_users AS SELECT * FROM users WHERE active = true' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'new_users', }) }) it('handles quoted identifiers', () => { const results = sqlEventParser.getTableEvents( 'SELECT * INTO "backup"."users_2024" FROM users' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: 'backup', tableName: 'users_2024', }) }) it('returns empty array for regular SELECT', () => { const results = sqlEventParser.getTableEvents('SELECT * FROM users') expect(results).toHaveLength(0) }) }) describe('RLS detection', () => { it('detects ALTER TABLE ENABLE ROW LEVEL SECURITY', () => { const results = sqlEventParser.getTableEvents('ALTER TABLE users ENABLE ROW LEVEL SECURITY') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: undefined, tableName: 'users', }) }) it('detects short form ENABLE RLS', () => { const results = sqlEventParser.getTableEvents('ALTER TABLE users ENABLE RLS') expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: undefined, tableName: 'users', }) }) it('detects with schema', () => { const results = sqlEventParser.getTableEvents( 'ALTER TABLE public.users ENABLE ROW LEVEL SECURITY' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: 'public', tableName: 'users', }) }) it('handles other ALTER TABLE statements in between', () => { const results = sqlEventParser.getTableEvents( 'ALTER TABLE users ADD COLUMN test INT, ENABLE ROW LEVEL SECURITY' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: undefined, tableName: 'users', }) }) it('returns empty array for disabling RLS', () => { const results = sqlEventParser.getTableEvents('ALTER TABLE users DISABLE ROW LEVEL SECURITY') expect(results).toHaveLength(0) }) it('detects ALTER TABLE IF EXISTS ENABLE ROW LEVEL SECURITY', () => { const results = sqlEventParser.getTableEvents( 'ALTER TABLE IF EXISTS public."Conversations" ENABLE ROW LEVEL SECURITY' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: 'public', tableName: 'Conversations', }) }) it('detects ALTER TABLE ONLY ENABLE ROW LEVEL SECURITY', () => { const results = sqlEventParser.getTableEvents( 'ALTER TABLE ONLY public.users ENABLE ROW LEVEL SECURITY' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: 'public', tableName: 'users', }) }) it('detects ALTER TABLE IF EXISTS ONLY ENABLE ROW LEVEL SECURITY', () => { const results = sqlEventParser.getTableEvents( 'ALTER TABLE IF EXISTS ONLY public.users ENABLE ROW LEVEL SECURITY' ) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: 'public', tableName: 'users', }) }) }) describe('ReDoS protection', () => { it('handles extremely long identifier names efficiently', () => { const longIdentifier = 'a'.repeat(10000) const sql = `CREATE TABLE ${longIdentifier} (id INT)` const startTime = Date.now() const results = sqlEventParser.getTableEvents(sql) const duration = Date.now() - startTime expect(duration).toBeLessThan(100) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: longIdentifier, }) }) it('handles nested dots in schema names without catastrophic backtracking', () => { const maliciousInput = 'a.'.repeat(1000) + 'table' const sql = `CREATE TABLE ${maliciousInput} (id INT)` const startTime = Date.now() const results = sqlEventParser.getTableEvents(sql) const duration = Date.now() - startTime expect(duration).toBeLessThan(100) expect(results.length).toBeGreaterThan(0) }) it('handles pathological SELECT INTO patterns', () => { const maliciousSQL = 'SELECT ' + 'a '.repeat(1000) + 'INTO table FROM users' const startTime = Date.now() const results = sqlEventParser.getTableEvents(maliciousSQL) const duration = Date.now() - startTime expect(duration).toBeLessThan(100) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'table', }) }) it('handles ALTER TABLE with many operations between', () => { const manyOperations = 'ADD COLUMN test INT, '.repeat(100) const sql = `ALTER TABLE users ${manyOperations} ENABLE ROW LEVEL SECURITY` const startTime = Date.now() const results = sqlEventParser.getTableEvents(sql) const duration = Date.now() - startTime expect(duration).toBeLessThan(100) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableRLSEnabled, schema: undefined, tableName: 'users', }) }) it('handles mixed quotes and backticks efficiently', () => { const mixedQuotes = '`"`.'.repeat(100) + 'tablename' const sql = `CREATE TABLE ${mixedQuotes} (id INT)` const startTime = Date.now() sqlEventParser.getTableEvents(sql) const duration = Date.now() - startTime expect(duration).toBeLessThan(100) }) }) describe('Edge cases and special characters', () => { it('handles Unicode identifiers', () => { const sql = 'CREATE TABLE 用户表 (id INT)' const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(0) }) it('handles identifiers with numbers', () => { const sql = 'CREATE TABLE table123 (id INT)' const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'table123', }) }) it('handles identifiers with underscores', () => { const sql = 'CREATE TABLE user_accounts (id INT)' const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'user_accounts', }) }) it('handles escaped quotes in identifiers', () => { const sql = 'CREATE TABLE "user""table" (id INT)' const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'usertable', }) }) it('does not scan inside dollar-quoted string literals', () => { // $$...$$ is a string literal in Postgres — its contents must not be // parsed as DDL, otherwise a user inserting SQL-shaped text into a log // table would trigger false-positive table-created events. const sql = ` CREATE TABLE users (id INT); INSERT INTO logs VALUES ($$CREATE TABLE fake$$); INSERT INTO users VALUES (1); ` const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(3) expect(results[0]).toMatchObject({ type: TABLE_EVENT_ACTIONS.TableCreated, tableName: 'users', }) expect(results[1]).toMatchObject({ type: TABLE_EVENT_ACTIONS.TableDataAdded, tableName: 'logs', }) expect(results[2]).toMatchObject({ type: TABLE_EVENT_ACTIONS.TableDataAdded, tableName: 'users', }) }) it('does not treat SELECT..INTO inside a plpgsql body as table creation', () => { // Regression for the RLS warning modal false-positive: variable // assignment inside a function body must not be reported as a new table. const sql = ` create or replace function schema_checks() returns jsonb language plpgsql as $$ declare ret jsonb; begin select jsonb_build_object('value', 'ok') into ret; return ret; end; $$; ` const results = sqlEventParser.getTableEvents(sql) expect(results).toEqual([]) }) it('does not leak nested dollar-quoted dynamic SQL through statement splitting', () => { // Regression: an inner $sql$...$sql$ tag used inside an outer $fn$...$fn$ // body was pairing with the outer opening tag (the splitStatements regex // doesn't enforce matching tags), which caused the inner semicolon to // split the statement and exposed `create table fake` to the detectors. // The fix is that stripDollarQuoteBodies runs before splitStatements and // uses a backreference to require matching tags. const sql = ` create function f() returns void language plpgsql as $fn$ begin execute $sql$create table fake(id int);$sql$; end; $fn$; ` const results = sqlEventParser.getTableEvents(sql) expect(results).toEqual([]) }) it('still detects a real top-level CREATE TABLE next to a function with nested dollar tags', () => { const sql = ` create table public.real_table(id int); create function f() returns void language plpgsql as $fn$ begin execute $sql$create table fake(id int);$sql$; end; $fn$; ` const results = sqlEventParser.getTableEvents(sql) expect(results).toEqual([ { type: TABLE_EVENT_ACTIONS.TableCreated, schema: 'public', tableName: 'real_table', }, ]) }) it('handles SQL injection attempts safely', () => { const sql = "CREATE TABLE users'; DROP TABLE users; -- (id INT)" const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(1) expect(results[0]).toEqual({ type: TABLE_EVENT_ACTIONS.TableCreated, schema: undefined, tableName: 'users', }) }) }) describe('getTableEvents', () => { it('filters only table-related events', () => { const sql = ` CREATE TABLE users (id INT); CREATE FUNCTION test() RETURNS INT AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql; INSERT INTO users (id) VALUES (1); ALTER TABLE users ENABLE RLS; CREATE VIEW user_view AS SELECT * FROM users; ` const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(3) expect(results.map((r) => r.type)).toEqual([ TABLE_EVENT_ACTIONS.TableCreated, TABLE_EVENT_ACTIONS.TableDataAdded, TABLE_EVENT_ACTIONS.TableRLSEnabled, ]) }) it('returns empty array for non-table SQL', () => { const sql = ` CREATE FUNCTION test() RETURNS INT AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql; CREATE VIEW user_view AS SELECT * FROM users; SELECT * FROM users; ` const results = sqlEventParser.getTableEvents(sql) expect(results).toHaveLength(0) }) }) })