Files
MyGoNavi/frontend/src/utils/sqlStatementSelection.test.ts

218 lines
7.2 KiB
TypeScript

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();
});
});