import { describe, expect, test } from 'vitest'; import { analyzeDestructiveMigrations } from './destructiveMigrations.js'; describe('analyzeDestructiveMigrations', () => { test('returns empty when SQL has no destructive statements', () => { expect( analyzeDestructiveMigrations([ { fileName: '0001_init.sql', sql: 'CREATE TABLE users (id uuid PRIMARY KEY);\nALTER TABLE users ADD COLUMN email text;' } ]) ).toEqual([]); }); test('flags DROP TABLE', () => { const findings = analyzeDestructiveMigrations([{ fileName: '0002_drop_users.sql', sql: 'DROP TABLE users;' }]); expect(findings).toEqual([ { fileName: '0002_drop_users.sql', kind: 'drop_table', excerpt: expect.stringMatching(/DROP TABLE users/i) } ]); }); test('flags DROP DATABASE, DROP SCHEMA, TRUNCATE, and DROP COLUMN', () => { const findings = analyzeDestructiveMigrations([ { fileName: '0003_multi.sql', sql: ` DROP DATABASE IF EXISTS old_db; DROP SCHEMA public CASCADE; TRUNCATE TABLE events; ALTER TABLE users DROP COLUMN email; ` } ]); expect(findings.map((f) => f.kind).sort()).toEqual(['drop_column', 'drop_database', 'drop_schema', 'truncate']); }); test('flags ALTER TABLE … DROP col without the COLUMN keyword', () => { // Postgres treats COLUMN as optional: ALTER TABLE t DROP c; const findings = analyzeDestructiveMigrations([{ fileName: '0003b_drop_col_short.sql', sql: 'ALTER TABLE users DROP email;' }]); expect(findings).toHaveLength(1); expect(findings[0]?.kind).toBe('drop_column'); }); test('flags standalone DROP COLUMN without ALTER prefix', () => { const findings = analyzeDestructiveMigrations([{ fileName: '0004_drop_col.sql', sql: 'DROP COLUMN orphaned;' }]); expect(findings).toHaveLength(1); expect(findings[0]?.kind).toBe('drop_column'); }); test('ignores DROP INDEX, DROP VIEW, DROP CONSTRAINT, and non-column ALTER', () => { expect( analyzeDestructiveMigrations([ { fileName: '0005_safe.sql', sql: ` DROP INDEX users_email_idx; DROP VIEW user_summary; ALTER TABLE users DROP CONSTRAINT users_email_key; ALTER TABLE users DROP INDEX users_email_idx; ALTER TABLE users ADD COLUMN name text; ` } ]) ).toEqual([]); }); test('ignores ALTER COLUMN DROP NOT NULL / DROP DEFAULT / DROP EXPRESSION (not column drops)', () => { expect( analyzeDestructiveMigrations([ { fileName: '0005b_safe_alter.sql', sql: ` ALTER TABLE users ALTER COLUMN email DROP NOT NULL; ALTER TABLE users ALTER COLUMN status DROP DEFAULT; ALTER TABLE users ALTER COLUMN full_name DROP EXPRESSION; ` } ]) ).toEqual([]); }); test('does not flag DELETE FROM (data DML is out of the schema-destroying set)', () => { expect(analyzeDestructiveMigrations([{ fileName: '0005c_delete.sql', sql: 'DELETE FROM users WHERE id = 1;' }])).toEqual([]); }); test('strips line and block comments before matching', () => { expect( analyzeDestructiveMigrations([ { fileName: '0006_commented.sql', sql: ` -- DROP TABLE users; /* DROP DATABASE doomed; */ CREATE TABLE users (id int); ` } ]) ).toEqual([]); }); test('is case-insensitive and finds multiple statements across files', () => { const findings = analyzeDestructiveMigrations([ { fileName: 'a.sql', sql: 'drop table IF EXISTS foo;' }, { fileName: 'b.sql', sql: 'Truncate events;' } ]); expect(findings).toHaveLength(2); expect(findings[0]).toMatchObject({ fileName: 'a.sql', kind: 'drop_table' }); expect(findings[1]).toMatchObject({ fileName: 'b.sql', kind: 'truncate' }); }); });