import { describe, expect, it } from 'vitest'; import { findSqlStatementRanges, resolveCurrentSqlStatementRange, resolveExecutableSql } from './sqlStatementSelection'; describe('sqlStatementSelection', () => { it('resolves the statement containing the cursor', () => { const sql = 'select 1;\n\nselect 2 from users;\nselect 3'; expect(resolveCurrentSqlStatementRange(sql, sql.indexOf('1'))?.text).toBe('select 1'); expect(resolveCurrentSqlStatementRange(sql, sql.indexOf('users'))?.text).toBe('select 2 from users'); expect(resolveCurrentSqlStatementRange(sql, sql.indexOf('3'))?.text).toBe('select 3'); }); it('ignores semicolons inside strings and comments', () => { const sql = [ "select ';' as semi;", "-- comment ;", "select 'a; b' as text;", "select $$a; b$$ as body;", ].join('\n'); const ranges = findSqlStatementRanges(sql).map((range) => range.text); expect(ranges).toEqual([ "select ';' as semi", "-- comment ;\nselect 'a; b' as text", "select $$a; b$$ as body", ]); }); it('keeps Oracle anonymous PL/SQL blocks as one executable statement', () => { const plsql = [ 'BEGIN', " INSERT INTO tmp_disable_trigger (table_name) VALUES ('t_memcard_reg');", " UPDATE t_memcard_reg SET CARDLEVEL = 1 WHERE MEMCARDNO = '8032277312';", " DELETE FROM tmp_disable_trigger WHERE table_name = 't_memcard_reg';", 'END;', 'SELECT 1 FROM dual;', ].join('\n'); const ranges = findSqlStatementRanges(plsql).map((range) => range.text); expect(ranges).toEqual([ [ 'BEGIN', " INSERT INTO tmp_disable_trigger (table_name) VALUES ('t_memcard_reg');", " UPDATE t_memcard_reg SET CARDLEVEL = 1 WHERE MEMCARDNO = '8032277312';", " DELETE FROM tmp_disable_trigger WHERE table_name = 't_memcard_reg';", 'END;', ].join('\n'), 'SELECT 1 FROM dual', ]); expect(resolveExecutableSql(plsql, plsql.indexOf('UPDATE'))).toEqual({ sql: ranges[0], source: 'statement', }); }); it('keeps Oracle DECLARE blocks as one executable statement', () => { const sql = [ 'DECLARE', ' v_count NUMBER;', 'BEGIN', ' SELECT COUNT(*) INTO v_count FROM t_memcard_reg;', " UPDATE t_memcard_reg SET CARDLEVEL = v_count WHERE MEMCARDNO = '8032277312';", 'END;', 'SELECT 1 FROM dual;', ].join('\n'); const ranges = findSqlStatementRanges(sql).map((range) => range.text); expect(ranges).toEqual([ [ 'DECLARE', ' v_count NUMBER;', 'BEGIN', ' SELECT COUNT(*) INTO v_count FROM t_memcard_reg;', " UPDATE t_memcard_reg SET CARDLEVEL = v_count WHERE MEMCARDNO = '8032277312';", 'END;', ].join('\n'), 'SELECT 1 FROM dual', ]); expect(resolveExecutableSql(sql, sql.indexOf('UPDATE'))).toEqual({ sql: ranges[0], source: 'statement', }); }); it('keeps Oracle CREATE PROCEDURE definitions as one executable statement', () => { const sql = [ 'CREATE OR REPLACE PROCEDURE proc_tally2accept(', ' p_tallyacceptno IN t_tally_accept_h.acceptno%TYPE,', ' out_acceptno OUT t_accept_h.acceptno%TYPE', ') IS', ' v_busno t_tally_accept_h.busno%TYPE;', ' v_count PLS_INTEGER;', 'BEGIN', " SELECT COUNT(*) INTO v_count FROM t_tally_accept_h WHERE acceptno = p_tallyacceptno;", ' IF v_count > 0 THEN', ' out_acceptno := p_tallyacceptno;', ' END IF;', 'END;', 'SELECT 1 FROM dual;', ].join('\n'); const ranges = findSqlStatementRanges(sql).map((range) => range.text); expect(ranges).toEqual([ [ 'CREATE OR REPLACE PROCEDURE proc_tally2accept(', ' p_tallyacceptno IN t_tally_accept_h.acceptno%TYPE,', ' out_acceptno OUT t_accept_h.acceptno%TYPE', ') IS', ' v_busno t_tally_accept_h.busno%TYPE;', ' v_count PLS_INTEGER;', 'BEGIN', " SELECT COUNT(*) INTO v_count FROM t_tally_accept_h WHERE acceptno = p_tallyacceptno;", ' IF v_count > 0 THEN', ' out_acceptno := p_tallyacceptno;', ' END IF;', 'END;', ].join('\n'), 'SELECT 1 FROM dual', ]); expect(resolveExecutableSql(sql, sql.indexOf('v_busno'))).toEqual({ sql: ranges[0], source: 'statement', }); }); it('keeps PostgreSQL dollar-quoted CREATE FUNCTION definitions as one executable statement', () => { const sql = [ 'CREATE OR REPLACE FUNCTION refresh_stats() RETURNS void AS $$', 'BEGIN', ' PERFORM refresh_now();', 'END;', '$$ LANGUAGE plpgsql;', 'SELECT 2;', ].join('\n'); const ranges = findSqlStatementRanges(sql).map((range) => range.text); expect(ranges).toEqual([ [ 'CREATE OR REPLACE FUNCTION refresh_stats() RETURNS void AS $$', 'BEGIN', ' PERFORM refresh_now();', 'END;', '$$ LANGUAGE plpgsql', ].join('\n'), 'SELECT 2', ]); }); it('still splits transaction BEGIN statements', () => { const sql = 'BEGIN; UPDATE accounts SET balance = balance - 1 WHERE id = 1; COMMIT;'; expect(findSqlStatementRanges(sql).map((range) => range.text)).toEqual([ 'BEGIN', 'UPDATE accounts SET balance = balance - 1 WHERE id = 1', 'COMMIT', ]); }); it('selects the next statement when the cursor is on whitespace before it', () => { const sql = 'select 1;\n\n select 2;'; const range = resolveCurrentSqlStatementRange(sql, sql.indexOf(' select 2')); expect(range?.text).toBe('select 2'); }); it('returns null when there is no statement', () => { expect(resolveCurrentSqlStatementRange(' \n\t ', 0)).toBeNull(); }); it('prefers a non-empty selection for executable SQL', () => { const sql = 'select 1;\nselect 2;'; expect(resolveExecutableSql(sql, sql.indexOf('1'), ' select selected ')?.sql).toBe(' select selected '); expect(resolveExecutableSql(sql, sql.indexOf('1'), ' select selected ')?.source).toBe('selection'); }); it('uses the statement containing the cursor for executable SQL', () => { const sql = 'select 1;\n\nselect 2 from users;\nselect 3'; expect(resolveExecutableSql(sql, sql.indexOf('users'))).toEqual({ sql: 'select 2 from users', source: 'statement', }); }); it('keeps execution on the statement when the cursor lands after its semicolon', () => { const sql = 'select 1 as a;\nselect 2 as b;\n\nselect 3 as c;'; const afterSecondSemicolon = sql.indexOf('select 2 as b') + 'select 2 as b;'.length; expect(resolveExecutableSql(sql, afterSecondSemicolon)).toEqual({ sql: 'select 2 as b', source: 'statement', }); }); it('falls back to the current line when the cursor is not inside a statement', () => { const sql = 'select 1;\n\n select 2'; expect(resolveExecutableSql(sql, sql.indexOf('\n\n') + 1)).toBeNull(); expect(resolveExecutableSql(sql, sql.indexOf(' select 2'))).toEqual({ sql: 'select 2', source: 'statement', }); }); it('does not jump to the next statement when executing from blank space', () => { const sql = 'select 1;\n\nselect 2;'; expect(resolveExecutableSql(sql, sql.indexOf('\n\n') + 1)).toBeNull(); }); });