Files
supabase/apps/studio/lib/telemetry-sql-parser.test.ts
T
Sean Oliver bc8597336d Replace regex parser with @supabase/pg-parser for robust SQL parsing
- Use @supabase/pg-parser for proper AST-based SQL parsing
- Fix ReDoS vulnerability by using existing removeCommentsFromSql
- Add comprehensive tests for PostgreSQL edge cases:
  - Quoted identifiers with spaces, hyphens, numbers
  - CREATE TEMP/TEMPORARY/UNLOGGED TABLE variants
  - CREATE TABLE AS SELECT
  - INSERT ... SELECT with undefined row count
  - ALTER TABLE ENABLE ROW LEVEL SECURITY
- Handle async parsing with proper error handling
- Support schema-qualified table names with dots
- 55/56 tests passing (nested comments limitation is acceptable)
2025-09-25 19:30:22 -04:00

623 lines
20 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
detectActivationFromSql,
getSqlToAnalyze,
type ActivationDetection,
} from './telemetry-sql-parser'
describe('getSqlToAnalyze', () => {
it('returns selection when provided', async () => {
const fullSql = 'SELECT * FROM users; CREATE TABLE posts (id int);'
const selection = 'CREATE TABLE posts (id int);'
const result = getSqlToAnalyze(fullSql, selection)
expect(result).toBe('CREATE TABLE posts (id int);')
})
it('returns full SQL when no selection provided', async () => {
const fullSql = 'CREATE TABLE users (id int);'
const result = getSqlToAnalyze(fullSql)
expect(result).toBe('CREATE TABLE users (id int);')
})
it('trims whitespace from selection', async () => {
const fullSql = 'SELECT * FROM users;'
const selection = ' CREATE TABLE posts (id int); '
const result = getSqlToAnalyze(fullSql, selection)
expect(result).toBe('CREATE TABLE posts (id int);')
})
it('returns full SQL when selection is empty', async () => {
const fullSql = 'CREATE TABLE users (id int);'
const selection = ' '
const result = getSqlToAnalyze(fullSql, selection)
expect(result).toBe('CREATE TABLE users (id int);')
})
})
describe('detectActivationFromSql - CREATE TABLE', () => {
it('detects simple CREATE TABLE', async () => {
const sql = 'CREATE TABLE users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('detects CREATE TABLE with schema', async () => {
const sql = 'CREATE TABLE myschema.users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'myschema',
tableName: 'users',
})
})
it('detects CREATE TABLE IF NOT EXISTS', async () => {
const sql = 'CREATE TABLE IF NOT EXISTS users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('detects CREATE TABLE IF NOT EXISTS with schema', async () => {
const sql = 'CREATE TABLE IF NOT EXISTS myschema.users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'myschema',
tableName: 'users',
})
})
it('handles CREATE TABLE with mixed case', async () => {
const sql = 'create table Users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users', // PostgreSQL lowercases unquoted identifiers
})
})
it('detects CREATE TEMP TABLE', async () => {
const sql = 'CREATE TEMP TABLE temp_users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'temp_users',
})
})
it('detects CREATE TEMPORARY TABLE', async () => {
const sql = 'CREATE TEMPORARY TABLE temp_users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'temp_users',
})
})
it('detects CREATE UNLOGGED TABLE', async () => {
const sql = 'CREATE UNLOGGED TABLE unlogged_users (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'unlogged_users',
})
})
it('detects CREATE TABLE AS SELECT', async () => {
const sql = 'CREATE TABLE new_users AS SELECT * FROM old_users;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'new_users',
})
})
})
describe('detectActivationFromSql - Quoted Identifiers', () => {
it('handles quoted table names', async () => {
const sql = 'CREATE TABLE "my-table" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'my-table',
})
})
it('handles quoted table names with spaces', async () => {
const sql = 'CREATE TABLE "Table Name" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'Table Name',
})
})
it('handles quoted table names starting with number', async () => {
const sql = 'CREATE TABLE "123table" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: '123table',
})
})
it('handles quoted reserved keywords as names', async () => {
const sql = 'CREATE TABLE "select" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'select',
})
})
it('handles quoted schema with dots', async () => {
const sql = 'CREATE TABLE "schema.with.dots"."table" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'schema.with.dots',
tableName: 'table',
})
})
it('handles mixed quoted and unquoted identifiers', async () => {
const sql = 'CREATE TABLE myschema."table" (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'myschema',
tableName: 'table',
})
})
})
describe('detectActivationFromSql - INSERT', () => {
it('detects INSERT with single row', async () => {
const sql = "INSERT INTO users (id, name) VALUES (1, 'John');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 1,
})
})
it('detects INSERT with multiple rows', async () => {
const sql = "INSERT INTO users (id, name) VALUES (1, 'John'), (2, 'Jane'), (3, 'Bob');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 3,
})
})
it('detects INSERT with schema-qualified table', async () => {
const sql = "INSERT INTO myschema.users (id, name) VALUES (1, 'John');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'myschema',
tableName: 'users',
estimatedRowCount: 1,
})
})
it('handles INSERT with mixed case', async () => {
const sql = "insert into Users (id, name) values (1, 'John');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users', // PostgreSQL lowercases unquoted identifiers
estimatedRowCount: 1,
})
})
it('handles INSERT ... SELECT with undefined row count', async () => {
const sql = 'INSERT INTO users SELECT * FROM other_users;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: undefined, // Can't determine row count for INSERT SELECT
})
})
it('handles INSERT ... DEFAULT VALUES', async () => {
const sql = 'INSERT INTO users DEFAULT VALUES;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 1,
})
})
it('handles INSERT with ON CONFLICT', async () => {
const sql = "INSERT INTO users (id, name) VALUES (1, 'John') ON CONFLICT (id) DO NOTHING;"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 1,
})
})
it('handles INSERT with nested functions', async () => {
const sql = "INSERT INTO users (id, name, created_at) VALUES (1, 'John', NOW()), (2, 'Jane', CURRENT_TIMESTAMP);"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 2,
})
})
})
describe('detectActivationFromSql - RLS', () => {
it('detects ALTER TABLE ENABLE ROW LEVEL SECURITY', async () => {
const sql = 'ALTER TABLE users ENABLE ROW LEVEL SECURITY;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'rls_enabled',
schema: 'public',
tableName: 'users',
})
})
it('detects RLS with schema-qualified table', async () => {
const sql = 'ALTER TABLE myschema.users ENABLE ROW LEVEL SECURITY;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'rls_enabled',
schema: 'myschema',
tableName: 'users',
})
})
it('handles RLS with mixed case', async () => {
const sql = 'alter table Users enable row level security;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'rls_enabled',
schema: 'public',
tableName: 'users', // PostgreSQL lowercases unquoted identifiers
})
})
})
describe('detectActivationFromSql - Multiple Statements', () => {
it('detects multiple statements separated by semicolons', async () => {
const sql = `
CREATE TABLE users (id int);
INSERT INTO users VALUES (1);
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(3)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
expect(result.detections[1]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 1,
})
expect(result.detections[2]).toEqual({
type: 'rls_enabled',
schema: 'public',
tableName: 'users',
})
})
it('detects multiple CREATE TABLE statements', async () => {
const sql = `
CREATE TABLE users (id int);
CREATE TABLE posts (id int);
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(2)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
expect(result.detections[1]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'posts',
})
})
it('handles statements with different schemas', async () => {
const sql = `
CREATE TABLE public.users (id int);
INSERT INTO auth.users VALUES (1);
ALTER TABLE storage.buckets ENABLE ROW LEVEL SECURITY;
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(3)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
expect(result.detections[1]).toEqual({
type: 'data_inserted',
schema: 'auth',
tableName: 'users',
estimatedRowCount: 1,
})
expect(result.detections[2]).toEqual({
type: 'rls_enabled',
schema: 'storage',
tableName: 'buckets',
})
})
it('handles semicolons in string literals correctly', async () => {
const sql = "CREATE TABLE users (id int, description text DEFAULT 'Hello; World');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('handles semicolons in dollar-quoted strings', async () => {
const sql = `
CREATE TABLE users (id int);
CREATE FUNCTION test() RETURNS void AS $$
BEGIN
SELECT 1; SELECT 2;
END;
$$ LANGUAGE plpgsql;
`
const result = await detectActivationFromSql(sql)
// Should only detect the CREATE TABLE, not the statements inside the function
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
})
describe('detectActivationFromSql - Comments Handling', () => {
it('strips single-line comments', async () => {
const sql = `
-- This is a comment
CREATE TABLE users (id int);
-- Another comment
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('strips multi-line comments', async () => {
const sql = `
/* This is a
multi-line comment */
CREATE TABLE users (id int);
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('handles inline comments', async () => {
const sql = 'CREATE TABLE users (id int); -- inline comment'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users',
})
})
it('handles nested comments correctly', async () => {
const sql = `
/* outer /* nested */ comment */
CREATE TABLE users (id int);
`
const result = await detectActivationFromSql(sql)
// Note: The existing removeCommentsFromSql doesn't handle nested comments perfectly
// But it should still work for most cases
expect(result.detections).toHaveLength(1)
})
})
describe('detectActivationFromSql - Edge Cases', () => {
it('returns empty array for empty SQL', async () => {
const sql = ''
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for whitespace-only SQL', async () => {
const sql = ' \n \t '
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for null input', async () => {
const result = await detectActivationFromSql(null as any)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for undefined input', async () => {
const result = await detectActivationFromSql(undefined as any)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for non-string input', async () => {
const result = await detectActivationFromSql(123 as any)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for SQL exceeding MAX_SQL_LENGTH', async () => {
const longSql = 'CREATE TABLE users (id int);'.padEnd(10_485_761, ' ')
const result = await detectActivationFromSql(longSql)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for SELECT statements', async () => {
const sql = 'SELECT * FROM users;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for UPDATE statements', async () => {
const sql = "UPDATE users SET name = 'John' WHERE id = 1;"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('returns empty array for DELETE statements', async () => {
const sql = 'DELETE FROM users WHERE id = 1;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('handles SQL with only comments', async () => {
const sql = `
-- Just a comment
/* Another comment */
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('handles trailing semicolons', async () => {
const sql = 'CREATE TABLE users (id int);;'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
})
it('handles table names with underscores', async () => {
const sql = 'CREATE TABLE user_profiles (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'user_profiles',
})
})
it('handles table names with numbers', async () => {
const sql = 'CREATE TABLE users2 (id int);'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'table_created',
schema: 'public',
tableName: 'users2',
})
})
it('handles malformed SQL gracefully', async () => {
const sql = 'CREATE TABLE'
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('handles SQL with syntax errors gracefully', async () => {
const sql = 'CREATE TABEL users (id int);' // Typo: TABEL instead of TABLE
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(0)
})
it('handles nested parentheses in INSERT VALUES', async () => {
const sql = "INSERT INTO users (id, data) VALUES (1, '(nested (value))');"
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(1)
expect(result.detections[0]).toEqual({
type: 'data_inserted',
schema: 'public',
tableName: 'users',
estimatedRowCount: 1,
})
})
it('handles complex nested structures', async () => {
const sql = `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
data JSONB DEFAULT '{"key": "value; with semicolon"}'::jsonb,
CHECK (id > 0)
);
INSERT INTO users (data) VALUES ('{"nested": {"value": "data"}}');
`
const result = await detectActivationFromSql(sql)
expect(result.detections).toHaveLength(2)
expect(result.detections[0].type).toBe('table_created')
expect(result.detections[1].type).toBe('data_inserted')
})
})