From 69adab1ac53c5789830be6f577ed3dc554eae6e7 Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Fri, 8 May 2026 14:03:27 -0600 Subject: [PATCH 1/6] initial implementation --- src/column-parser.ts | 9 +- src/defines.ts | 1 + src/parser.ts | 35 ++- src/tokenizer.ts | 9 +- test/index.spec.ts | 2 +- test/parser/snowflake.spec.ts | 447 ++++++++++++++++++++++++++++++++++ 6 files changed, 494 insertions(+), 9 deletions(-) create mode 100644 test/parser/snowflake.spec.ts diff --git a/src/column-parser.ts b/src/column-parser.ts index 0d1a432..2b84da5 100644 --- a/src/column-parser.ts +++ b/src/column-parser.ts @@ -349,7 +349,14 @@ export class ColumnParser { private maybeIdent(token: Token): boolean { const ch = token.value[0]; - const startChars = this.dialect === 'mssql' ? ['"', '['] : ['"', '`']; + let startChars: string[]; + if (this.dialect === 'mssql') { + startChars = ['"', '[']; + } else if (this.dialect === 'snowflake') { + startChars = ['"']; + } else { + startChars = ['"', '`']; + } return token.type !== 'string' && (startChars.includes(ch) || /[a-zA-Z_]/.test(ch)); } } diff --git a/src/defines.ts b/src/defines.ts index 5fc2f15..799009f 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -5,6 +5,7 @@ export const DIALECTS = [ 'oracle', 'psql', 'bigquery', + 'snowflake', 'generic', ] as const; export type Dialect = (typeof DIALECTS)[number]; diff --git a/src/parser.ts b/src/parser.ts index c180c78..b22503a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -113,6 +113,7 @@ const blockOpeners: Record = { sqlite: ['BEGIN', 'CASE'], oracle: ['DECLARE', 'BEGIN', 'CASE'], bigquery: ['BEGIN', 'CASE', 'IF', 'LOOP', 'REPEAT', 'WHILE', 'FOR'], + snowflake: ['DECLARE', 'BEGIN', 'CASE', 'LOOP', 'IF', 'WHILE', 'FOR'], }; interface ParseOptions { @@ -348,7 +349,12 @@ function createStatementParserByToken( case 'TRUNCATE': return createTruncateStatementParser(options); case 'BEGIN': - if (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value !== 'TRANSACTION') { + if ( + (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value !== 'TRANSACTION') || + (options.dialect === 'snowflake' && + nextToken.value !== 'WORK' && + nextToken.value !== 'TRANSACTION') + ) { return createBlockStatementParser(options); } return createBeginTransactionStatementParser(options); @@ -362,7 +368,7 @@ function createStatementParserByToken( case 'ROLLBACK': return createRollbackStatementParser(options); case 'DECLARE': - if (options.dialect === 'oracle') { + if (['oracle', 'snowflake'].includes(options.dialect)) { return createBlockStatementParser(options); } break; @@ -912,12 +918,13 @@ function stateMachineStatementParser( (token.value.toUpperCase() !== 'BEGIN' || (token.value.toUpperCase() === 'BEGIN' && nextToken.value.toUpperCase() !== 'TRANSACTION' && + (dialect !== 'snowflake' || nextToken.value.toUpperCase() !== 'WORK') && (dialect !== 'sqlite' || (dialect === 'sqlite' && !['DEFERRED', 'IMMEDIATE', 'EXCLUSIVE'].includes(nextToken.value.toUpperCase()))))) ) { if ( - dialect === 'oracle' && + ['oracle', 'snowflake'].includes(dialect) && lastBlockOpener?.value === 'DECLARE' && token.value.toUpperCase() === 'BEGIN' ) { @@ -975,13 +982,22 @@ function stateMachineStatementParser( } if ( - ['psql', 'mssql', 'bigquery'].includes(dialect) && + ['psql', 'mssql', 'bigquery', 'snowflake'].includes(dialect) && token.value.toUpperCase() === 'MATERIALIZED' ) { setPrevToken(token); return; } + let afterOrTokens: string[]; + if (dialect === 'mssql') { + afterOrTokens = ['ALTER']; + } else if (dialect === 'snowflake') { + afterOrTokens = ['ALTER', 'REPLACE']; + } else { + afterOrTokens = ['REPLACE']; + } + // technically these dialects don't allow "OR REPLACE" or "OR ALTER" between all statement // types, but we'll allow it for now. // For "ALTER", we need to make sure we only catch it here if it directly follows "OR", so @@ -990,7 +1006,7 @@ function stateMachineStatementParser( dialect !== 'sqlite' && (token.value.toUpperCase() === 'OR' || (prevNonWhitespaceToken?.value.toUpperCase() === 'OR' && - token.value.toUpperCase() === (dialect === 'mssql' ? 'ALTER' : 'REPLACE'))) + afterOrTokens.includes(token.value.toUpperCase()))) ) { setPrevToken(token); return; @@ -1000,7 +1016,9 @@ function stateMachineStatementParser( if ( (dialect === 'psql' && ['TEMP', 'TEMPORARY'].includes(token.value.toUpperCase())) || (dialect === 'sqlite' && - ['TEMP', 'TEMPORARY', 'VIRTUAL'].includes(token.value.toUpperCase())) + ['TEMP', 'TEMPORARY', 'VIRTUAL'].includes(token.value.toUpperCase())) || + (dialect === 'snowflake' && + ['TEMP', 'TEMPORARY', 'TRANSIENT', 'VOLATILE'].includes(token.value.toUpperCase())) ) { setPrevToken(token); return; @@ -1153,6 +1171,11 @@ export function defaultParamTypesFor(dialect: Dialect): ParamTypes { numbered: ['?'], named: [':', '@'], }; + case 'snowflake': + return { + positional: true, + named: [':'], + }; default: return { positional: true, diff --git a/src/tokenizer.ts b/src/tokenizer.ts index e558749..f0c9994 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -520,7 +520,14 @@ function isDollarQuotedString(state: State): boolean { } function isQuotedIdentifier(ch: Char, dialect: Dialect): boolean { - const startQuoteChars: Char[] = dialect === 'mssql' ? ['"', '['] : ['"', '`']; + let startQuoteChars: Char[]; + if (dialect === 'mssql') { + startQuoteChars = ['"', '[']; + } else if (dialect === 'snowflake') { + startQuoteChars = ['"']; + } else { + startQuoteChars = ['"', '`']; + } return startQuoteChars.includes(ch); } diff --git a/test/index.spec.ts b/test/index.spec.ts index 861a2c5..4e79ac9 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -5,7 +5,7 @@ import { ParamTypes } from '../src/defines'; describe('identify', () => { it('should throw error for invalid dialect', () => { expect(() => identify('SELECT * FROM foo', { dialect: 'invalid' as Dialect })).to.throw( - 'Unknown dialect. Allowed values: mssql, sqlite, mysql, oracle, psql, bigquery, generic', + 'Unknown dialect. Allowed values: mssql, sqlite, mysql, oracle, psql, bigquery, snowflake, generic', ); }); diff --git a/test/parser/snowflake.spec.ts b/test/parser/snowflake.spec.ts new file mode 100644 index 0000000..69e0393 --- /dev/null +++ b/test/parser/snowflake.spec.ts @@ -0,0 +1,447 @@ +import { parse, defaultParamTypesFor } from '../../src/parser'; +import { expect } from 'chai'; + +describe('Parser for snowflake', () => { + // Anonymous blocks + describe('anonymous blocks', () => { + it('should parse bare BEGIN as ANON_BLOCK', () => { + const result = parse('BEGIN SELECT 1; END;', false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should parse bare BEGIN...END followed by another statement', () => { + const result = parse('BEGIN SELECT 1; END; SELECT 2;', false, 'snowflake'); + expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + expect(result.body[1].type).to.eql('SELECT'); + }); + + it('should parse DECLARE...BEGIN...END as a single ANON_BLOCK', () => { + const sql = `DECLARE + x INTEGER; + BEGIN + x := 1; + SELECT x; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should parse DECLARE...BEGIN...END followed by another statement', () => { + const sql = `DECLARE + x INTEGER; + BEGIN + SELECT x; + END; + + SELECT * FROM foo;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + expect(result.body[1].type).to.eql('SELECT'); + }); + + it('should handle nested BEGIN...END blocks', () => { + const sql = `BEGIN + BEGIN + SELECT 1; + END; + SELECT 2; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle a block with multiple internal statements', () => { + const sql = `BEGIN + INSERT INTO t1 VALUES (1); + INSERT INTO t1 VALUES (2); + INSERT INTO t1 VALUES (3); + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle CASE inside a block', () => { + const sql = `BEGIN + SELECT CASE WHEN a = 1 THEN 'yes' ELSE 'no' END CASE FROM t; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle IF inside a block', () => { + const sql = `BEGIN + IF (x > 0) THEN + SELECT 1; + ELSE + SELECT 2; + END IF; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle WHILE inside a block', () => { + const sql = `BEGIN + WHILE (x < 10) DO + SELECT x; + END WHILE; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle FOR inside a block', () => { + const sql = `BEGIN + FOR i IN 1 TO 10 DO + SELECT i; + END FOR; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should handle LOOP inside a block', () => { + const sql = `BEGIN + LOOP + SELECT 1; + END LOOP; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + }); + + // Transactions + describe('transactions', () => { + it('should parse BEGIN TRANSACTION as BEGIN_TRANSACTION', () => { + const result = parse('BEGIN TRANSACTION; SELECT 1; COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + + it('should parse BEGIN WORK as BEGIN_TRANSACTION', () => { + const result = parse('BEGIN WORK; SELECT 1; COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + + it('should parse START TRANSACTION as BEGIN_TRANSACTION', () => { + const result = parse('START TRANSACTION; SELECT 1; COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + + it('should parse BEGIN TRANSACTION with NAME', () => { + const result = parse( + 'BEGIN TRANSACTION NAME T1; INSERT INTO t VALUES (1); COMMIT;', + false, + 'snowflake', + ); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('INSERT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + + it('should parse COMMIT and ROLLBACK', () => { + const result = parse('COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('COMMIT'); + + const result2 = parse('ROLLBACK;', false, 'snowflake'); + expect(result2.body.length).to.eql(1); + expect(result2.body[0].type).to.eql('ROLLBACK'); + }); + + it('should not treat BEGIN WORK inside a block as a block opener', () => { + const sql = `BEGIN + BEGIN WORK; + INSERT INTO t VALUES (1); + COMMIT; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + }); + + // Standard statement identification + describe('standard statements', () => { + it('should identify SELECT', () => { + const result = parse('SELECT * FROM foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('SELECT'); + }); + + it('should identify INSERT', () => { + const result = parse('INSERT INTO foo VALUES (1);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('INSERT'); + }); + + it('should identify UPDATE', () => { + const result = parse('UPDATE foo SET bar = 1;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('UPDATE'); + }); + + it('should identify DELETE', () => { + const result = parse('DELETE FROM foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DELETE'); + }); + + it('should identify TRUNCATE', () => { + const result = parse('TRUNCATE TABLE foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('TRUNCATE'); + }); + }); + + // CREATE statements with modifiers + describe('CREATE statements', () => { + it('should identify CREATE TABLE', () => { + const result = parse('CREATE TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE OR REPLACE TABLE', () => { + const result = parse('CREATE OR REPLACE TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE OR ALTER VIEW', () => { + const result = parse('CREATE OR ALTER VIEW v AS SELECT 1;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_VIEW'); + }); + + it('should identify CREATE TEMPORARY TABLE', () => { + const result = parse('CREATE TEMPORARY TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE TEMP TABLE', () => { + const result = parse('CREATE TEMP TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE TRANSIENT TABLE', () => { + const result = parse('CREATE TRANSIENT TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE VOLATILE TABLE', () => { + const result = parse('CREATE VOLATILE TABLE foo (id INTEGER);', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + }); + + it('should identify CREATE MATERIALIZED VIEW', () => { + const result = parse('CREATE MATERIALIZED VIEW v AS SELECT 1;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_VIEW'); + }); + + it('should identify CREATE VIEW', () => { + const result = parse('CREATE VIEW v AS SELECT 1;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_VIEW'); + }); + + it('should identify CREATE SCHEMA', () => { + const result = parse('CREATE SCHEMA myschema;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_SCHEMA'); + }); + + it('should identify CREATE DATABASE', () => { + const result = parse('CREATE DATABASE mydb;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_DATABASE'); + }); + + it('should identify CREATE FUNCTION', () => { + const result = parse( + 'CREATE FUNCTION myfunc() RETURNS INTEGER AS $$ SELECT 1 $$;', + true, + 'snowflake', + ); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_FUNCTION'); + }); + + it('should identify CREATE PROCEDURE', () => { + const result = parse( + 'CREATE PROCEDURE myproc() RETURNS INTEGER AS $$ SELECT 1 $$;', + true, + 'snowflake', + ); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_PROCEDURE'); + }); + + it('should identify CREATE OR REPLACE FUNCTION', () => { + const result = parse( + 'CREATE OR REPLACE FUNCTION myfunc() RETURNS INTEGER AS $$ SELECT 1 $$;', + true, + 'snowflake', + ); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('CREATE_FUNCTION'); + }); + }); + + // DROP statements + describe('DROP statements', () => { + it('should identify DROP TABLE', () => { + const result = parse('DROP TABLE foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DROP_TABLE'); + }); + + it('should identify DROP VIEW', () => { + const result = parse('DROP VIEW v;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DROP_VIEW'); + }); + + it('should identify DROP DATABASE', () => { + const result = parse('DROP DATABASE mydb;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DROP_DATABASE'); + }); + + it('should identify DROP SCHEMA', () => { + const result = parse('DROP SCHEMA myschema;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DROP_SCHEMA'); + }); + }); + + // ALTER statements + describe('ALTER statements', () => { + it('should identify ALTER TABLE', () => { + const result = parse('ALTER TABLE foo ADD COLUMN bar INTEGER;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ALTER_TABLE'); + }); + + it('should identify ALTER VIEW', () => { + const result = parse("ALTER VIEW v SET COMMENT = 'test';", true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ALTER_VIEW'); + }); + }); + + // Multiple statements / splitting + describe('statement splitting', () => { + it('should split multiple simple statements', () => { + const result = parse('SELECT 1; SELECT 2; SELECT 3;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('SELECT'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('SELECT'); + }); + + it('should split mixed statement types', () => { + const result = parse( + 'CREATE TABLE foo (id INTEGER); INSERT INTO foo VALUES (1); SELECT * FROM foo;', + true, + 'snowflake', + ); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + expect(result.body[1].type).to.eql('INSERT'); + expect(result.body[2].type).to.eql('SELECT'); + }); + + it('should not split on semicolons inside BEGIN...END blocks', () => { + const sql = `BEGIN + INSERT INTO t1 VALUES (1); + INSERT INTO t2 VALUES (2); + END; + SELECT * FROM t1;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + expect(result.body[1].type).to.eql('SELECT'); + }); + + it('should not split on semicolons inside DECLARE...BEGIN...END blocks', () => { + const sql = `DECLARE + x INTEGER; + BEGIN + INSERT INTO t1 VALUES (1); + INSERT INTO t2 VALUES (2); + END; + SELECT * FROM t1;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + expect(result.body[1].type).to.eql('SELECT'); + }); + + it('should handle a block after a CREATE TABLE', () => { + const sql = `CREATE TABLE foo (id INTEGER); + BEGIN + INSERT INTO foo VALUES (1); + INSERT INTO foo VALUES (2); + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eql('CREATE_TABLE'); + expect(result.body[1].type).to.eql('ANON_BLOCK'); + }); + }); + + // Parameters + describe('parameters', () => { + it('should identify positional parameters', () => { + const result = parse( + 'SELECT * FROM foo WHERE id = ?;', + true, + 'snowflake', + false, + false, + defaultParamTypesFor('snowflake'), + ); + expect(result.body[0].parameters).to.eql(['?']); + }); + + it('should identify named parameters with colon', () => { + const result = parse( + 'SELECT * FROM foo WHERE id = :id AND name = :name;', + true, + 'snowflake', + false, + false, + defaultParamTypesFor('snowflake'), + ); + expect(result.body[0].parameters).to.include(':id'); + expect(result.body[0].parameters).to.include(':name'); + }); + }); +}); From 685e8881e6457d3fde957c4bab9e51f68385c1b8 Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Wed, 13 May 2026 16:25:12 -0600 Subject: [PATCH 2/6] fix repeat, declare/begin/end, and transactions --- src/parser.ts | 10 ++++----- test/parser/snowflake.spec.ts | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index b22503a..eb783d9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -113,7 +113,7 @@ const blockOpeners: Record = { sqlite: ['BEGIN', 'CASE'], oracle: ['DECLARE', 'BEGIN', 'CASE'], bigquery: ['BEGIN', 'CASE', 'IF', 'LOOP', 'REPEAT', 'WHILE', 'FOR'], - snowflake: ['DECLARE', 'BEGIN', 'CASE', 'LOOP', 'IF', 'WHILE', 'FOR'], + snowflake: ['DECLARE', 'BEGIN', 'CASE', 'LOOP', 'IF', 'WHILE', 'FOR', 'REPEAT'], }; interface ParseOptions { @@ -350,10 +350,10 @@ function createStatementParserByToken( return createTruncateStatementParser(options); case 'BEGIN': if ( - (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value !== 'TRANSACTION') || + (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value.toUpperCase() !== 'TRANSACTION') || (options.dialect === 'snowflake' && - nextToken.value !== 'WORK' && - nextToken.value !== 'TRANSACTION') + nextToken.value.toUpperCase() !== 'WORK' && + nextToken.value.toUpperCase() !== 'TRANSACTION') ) { return createBlockStatementParser(options); } @@ -417,7 +417,7 @@ function createBlockStatementParser(options: ParseOptions) { preCanGoToNext: () => false, validation: { acceptTokens: [ - ...(options.dialect === 'oracle' ? [{ type: 'keyword', value: 'DECLARE' }] : []), + ...(['oracle', 'snowflake'].includes(options.dialect) ? [{ type: 'keyword', value: 'DECLARE' }] : []), { type: 'keyword', value: 'BEGIN' }, ], }, diff --git a/test/parser/snowflake.spec.ts b/test/parser/snowflake.spec.ts index 69e0393..e43beac 100644 --- a/test/parser/snowflake.spec.ts +++ b/test/parser/snowflake.spec.ts @@ -120,6 +120,29 @@ describe('Parser for snowflake', () => { expect(result.body.length).to.eql(1); expect(result.body[0].type).to.eql('ANON_BLOCK'); }); + + it('should handle REPEAT inside a block', () => { + const sql = `BEGIN + REPEAT + SELECT 1; + UNTIL (x > 10) END REPEAT; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + + it('should parse DECLARE...BEGIN...END as ANON_BLOCK in strict mode', () => { + const sql = `DECLARE + x INTEGER; + BEGIN + x := 1; + SELECT x; + END;`; + const result = parse(sql, true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); }); // Transactions @@ -170,6 +193,22 @@ describe('Parser for snowflake', () => { expect(result2.body[0].type).to.eql('ROLLBACK'); }); + it('should parse lowercase "BEGIN transaction" as BEGIN_TRANSACTION', () => { + const result = parse('BEGIN transaction; SELECT 1; COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + + it('should parse lowercase "BEGIN work" as BEGIN_TRANSACTION', () => { + const result = parse('BEGIN work; SELECT 1; COMMIT;', false, 'snowflake'); + expect(result.body.length).to.eql(3); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); + expect(result.body[1].type).to.eql('SELECT'); + expect(result.body[2].type).to.eql('COMMIT'); + }); + it('should not treat BEGIN WORK inside a block as a block opener', () => { const sql = `BEGIN BEGIN WORK; From 58fc12bb4094f92cae2ef2cf915d93c3815225f8 Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Fri, 15 May 2026 13:27:27 -0600 Subject: [PATCH 3/6] add some more snowflake keywords and ansi keywords that were missing --- src/defines.ts | 22 ++++ src/parser.ts | 149 +++++++++++++++++++++++++- src/tokenizer.ts | 25 +++++ test/parser/single-statements.spec.ts | 58 +++++++++- test/parser/snowflake.spec.ts | 115 ++++++++++++++++++++ 5 files changed, 362 insertions(+), 7 deletions(-) diff --git a/src/defines.ts b/src/defines.ts index 799009f..a9ef689 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -54,6 +54,16 @@ export type StatementType = | 'SHOW_KEYS' | 'SHOW_INDEX' | 'SHOW_TABLE' + | 'SHOW_WAREHOUSES' + | 'SHOW_USERS' + | 'SHOW_ROLES' + | 'SHOW_SCHEMAS' + | 'SHOW_STAGES' + | 'SHOW_INTEGRATIONS' + | 'SHOW_STREAMS' + | 'SHOW_TASKS' + | 'SHOW_PIPES' + | 'SHOW_SEQUENCES' | 'SHOW_TABLES' | 'SHOW_COLUMNS' | 'DROP_DATABASE' @@ -76,6 +86,18 @@ export type StatementType = | 'COMMIT' | 'ROLLBACK' | 'ANON_BLOCK' + | 'MERGE' + | 'CALL' + | 'GRANT' + | 'REVOKE' + | 'EXPLAIN' + | 'DESCRIBE' + | 'USE' + | 'COPY' + | 'PUT' + | 'GET' + | 'LIST' + | 'REMOVE' | 'UNKNOWN'; export type ExecutionType = diff --git a/src/parser.ts b/src/parser.ts index eb783d9..bf545ea 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -93,6 +93,28 @@ export const EXECUTION_TYPES: Record = { BEGIN_TRANSACTION: 'TRANSACTION', COMMIT: 'TRANSACTION', ROLLBACK: 'TRANSACTION', + MERGE: 'MODIFICATION', + CALL: 'MODIFICATION', + GRANT: 'MODIFICATION', + REVOKE: 'MODIFICATION', + EXPLAIN: 'INFORMATION', + DESCRIBE: 'INFORMATION', + USE: 'INFORMATION', + COPY: 'MODIFICATION', + PUT: 'MODIFICATION', + GET: 'LISTING', + LIST: 'LISTING', + REMOVE: 'MODIFICATION', + SHOW_WAREHOUSES: 'LISTING', + SHOW_USERS: 'LISTING', + SHOW_ROLES: 'LISTING', + SHOW_SCHEMAS: 'LISTING', + SHOW_STAGES: 'LISTING', + SHOW_INTEGRATIONS: 'LISTING', + SHOW_STREAMS: 'LISTING', + SHOW_TASKS: 'LISTING', + SHOW_PIPES: 'LISTING', + SHOW_SEQUENCES: 'LISTING', UNKNOWN: 'UNKNOWN', ANON_BLOCK: 'ANON_BLOCK', }; @@ -332,7 +354,7 @@ function createStatementParserByToken( case 'CREATE': return createCreateStatementParser(options); case 'SHOW': - if (['mysql', 'generic'].includes(options.dialect)) { + if (['mysql', 'generic', 'snowflake'].includes(options.dialect)) { return createShowStatementParser(options); } break; @@ -350,7 +372,8 @@ function createStatementParserByToken( return createTruncateStatementParser(options); case 'BEGIN': if ( - (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value.toUpperCase() !== 'TRANSACTION') || + (['bigquery', 'oracle'].includes(options.dialect) && + nextToken.value.toUpperCase() !== 'TRANSACTION') || (options.dialect === 'snowflake' && nextToken.value.toUpperCase() !== 'WORK' && nextToken.value.toUpperCase() !== 'TRANSACTION') @@ -372,6 +395,40 @@ function createStatementParserByToken( return createBlockStatementParser(options); } break; + case 'MERGE': + return createMergeStatementParser(options); + case 'CALL': + return createCallStatementParser(options); + case 'GRANT': + return createGrantStatementParser(options); + case 'REVOKE': + return createRevokeStatementParser(options); + case 'EXPLAIN': + return createExplainStatementParser(options); + case 'DESCRIBE': + case 'DESC': + return createDescribeStatementParser(options); + case 'USE': + if (['mssql', 'mysql', 'snowflake'].includes(options.dialect)) { + return createUseStatementParser(options); + } + break; + case 'COPY': + return createCopyStatementParser(options); + case 'PUT': + if (options.dialect === 'snowflake') return createPutStatementParser(options); + break; + case 'GET': + if (options.dialect === 'snowflake') return createGetStatementParser(options); + break; + case 'LIST': + case 'LS': + if (options.dialect === 'snowflake') return createListStatementParser(options); + break; + case 'REMOVE': + case 'RM': + if (options.dialect === 'snowflake') return createRemoveStatementParser(options); + break; default: break; } @@ -417,7 +474,9 @@ function createBlockStatementParser(options: ParseOptions) { preCanGoToNext: () => false, validation: { acceptTokens: [ - ...(['oracle', 'snowflake'].includes(options.dialect) ? [{ type: 'keyword', value: 'DECLARE' }] : []), + ...(['oracle', 'snowflake'].includes(options.dialect) + ? [{ type: 'keyword', value: 'DECLARE' }] + : []), { type: 'keyword', value: 'BEGIN' }, ], }, @@ -665,6 +724,80 @@ function createTruncateStatementParser(options: ParseOptions) { return stateMachineStatementParser(statement, steps, options); } +function createSingleKeywordStatementParser( + options: ParseOptions, + type: StatementType, + keywords: string[], +) { + const statement = createInitialStatement(); + + const steps: Step[] = [ + { + preCanGoToNext: () => false, + validation: { + acceptTokens: keywords.map((value) => ({ type: 'keyword', value })), + }, + add: (token) => { + statement.type = type; + if (statement.start < 0) { + statement.start = token.start; + } + }, + postCanGoToNext: () => true, + }, + ]; + + return stateMachineStatementParser(statement, steps, options); +} + +function createMergeStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'MERGE', ['MERGE']); +} + +function createCallStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'CALL', ['CALL']); +} + +function createGrantStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'GRANT', ['GRANT']); +} + +function createRevokeStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'REVOKE', ['REVOKE']); +} + +function createExplainStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'EXPLAIN', ['EXPLAIN']); +} + +function createDescribeStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'DESCRIBE', ['DESCRIBE', 'DESC']); +} + +function createUseStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'USE', ['USE']); +} + +function createCopyStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'COPY', ['COPY']); +} + +function createPutStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'PUT', ['PUT']); +} + +function createGetStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'GET', ['GET']); +} + +function createListStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'LIST', ['LIST', 'LS']); +} + +function createRemoveStatementParser(options: ParseOptions) { + return createSingleKeywordStatementParser(options, 'REMOVE', ['REMOVE', 'RM']); +} + function createShowStatementParser(options: ParseOptions) { const statement = createInitialStatement(); @@ -721,6 +854,16 @@ function createShowStatementParser(options: ParseOptions) { { type: 'keyword', value: 'TRIGGERS' }, { type: 'keyword', value: 'VARIABLES' }, { type: 'keyword', value: 'WARNINGS' }, + { type: 'keyword', value: 'WAREHOUSES' }, + { type: 'keyword', value: 'USERS' }, + { type: 'keyword', value: 'ROLES' }, + { type: 'keyword', value: 'SCHEMAS' }, + { type: 'keyword', value: 'STAGES' }, + { type: 'keyword', value: 'INTEGRATIONS' }, + { type: 'keyword', value: 'STREAMS' }, + { type: 'keyword', value: 'TASKS' }, + { type: 'keyword', value: 'PIPES' }, + { type: 'keyword', value: 'SEQUENCES' }, ], }, add: (token) => { diff --git a/src/tokenizer.ts b/src/tokenizer.ts index f0c9994..10dbb74 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -72,6 +72,31 @@ const KEYWORDS = [ 'TRIGGERS', 'VARIABLES', 'WARNINGS', + 'MERGE', + 'CALL', + 'GRANT', + 'REVOKE', + 'EXPLAIN', + 'DESCRIBE', + 'DESC', + 'USE', + 'COPY', + 'PUT', + 'GET', + 'LIST', + 'LS', + 'REMOVE', + 'RM', + 'WAREHOUSES', + 'USERS', + 'ROLES', + 'SCHEMAS', + 'STAGES', + 'INTEGRATIONS', + 'STREAMS', + 'TASKS', + 'PIPES', + 'SEQUENCES', ]; const INDIVIDUALS: Record = { diff --git a/test/parser/single-statements.spec.ts b/test/parser/single-statements.spec.ts index 37a4208..70b4ffc 100644 --- a/test/parser/single-statements.spec.ts +++ b/test/parser/single-statements.spec.ts @@ -12,16 +12,16 @@ describe('parser', () => { describe('with strict disabled', () => { it('should parse if first token is unknown', () => { - const actual = parse('LIST * FROM foo', false); + const actual = parse('FOOBAR * FROM foo', false); actual.tokens = aggregateUnknownTokens(actual.tokens); expect(actual).to.eql({ type: 'QUERY', start: 0, - end: 14, + end: 16, body: [ { start: 0, - end: 14, + end: 16, parameters: [], tables: [], columns: [], @@ -29,7 +29,7 @@ describe('parser', () => { executionType: 'UNKNOWN', }, ], - tokens: [{ type: 'unknown', value: 'LIST * FROM foo', start: 0, end: 14 }], + tokens: [{ type: 'unknown', value: 'FOOBAR * FROM foo', start: 0, end: 16 }], }); }); @@ -710,6 +710,56 @@ describe('parser', () => { expect(actual).to.eql(expected); }); + it('should parse "MERGE" statement', () => { + const actual = parse( + 'MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v;', + ); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('MERGE'); + expect(actual.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should parse "CALL" statement', () => { + const actual = parse('CALL my_proc(1);'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('CALL'); + expect(actual.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should parse "GRANT" statement', () => { + const actual = parse('GRANT SELECT ON t TO bob;'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('GRANT'); + expect(actual.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should parse "REVOKE" statement', () => { + const actual = parse('REVOKE SELECT ON t FROM bob;'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('REVOKE'); + expect(actual.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should parse "EXPLAIN" statement as EXPLAIN, not SELECT', () => { + const actual = parse('EXPLAIN SELECT * FROM t;'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('EXPLAIN'); + expect(actual.body[0].executionType).to.eql('INFORMATION'); + }); + + it('should parse psql "COPY" statement', () => { + const actual = parse("COPY t FROM '/tmp/x.csv';", true, 'psql'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('COPY'); + expect(actual.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should still parse SELECT with ORDER BY DESC as SELECT (not DESCRIBE)', () => { + const actual = parse('SELECT * FROM t ORDER BY a DESC;'); + expect(actual.body.length).to.eql(1); + expect(actual.body[0].type).to.eql('SELECT'); + }); + describe('with parameters', () => { it('should extract the parameters', () => { const actual = parse('select x from a where x = ?'); diff --git a/test/parser/snowflake.spec.ts b/test/parser/snowflake.spec.ts index e43beac..e1d3e85 100644 --- a/test/parser/snowflake.spec.ts +++ b/test/parser/snowflake.spec.ts @@ -454,6 +454,121 @@ describe('Parser for snowflake', () => { expect(result.body[0].type).to.eql('CREATE_TABLE'); expect(result.body[1].type).to.eql('ANON_BLOCK'); }); + + it('should not split on MERGE inside a BEGIN...END block', () => { + const sql = `BEGIN + MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v; + SELECT 1; + END;`; + const result = parse(sql, false, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('ANON_BLOCK'); + }); + }); + + describe('DESCRIBE / DESC', () => { + it('should identify DESCRIBE TABLE', () => { + const result = parse('DESCRIBE TABLE foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DESCRIBE'); + expect(result.body[0].executionType).to.eql('INFORMATION'); + }); + + it('should identify DESC TABLE as DESCRIBE', () => { + const result = parse('DESC TABLE foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('DESCRIBE'); + expect(result.body[0].executionType).to.eql('INFORMATION'); + }); + }); + + describe('USE', () => { + ['WAREHOUSE', 'DATABASE', 'ROLE'].forEach((target) => { + it(`should identify USE ${target}`, () => { + const result = parse(`USE ${target} foo;`, true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('USE'); + expect(result.body[0].executionType).to.eql('INFORMATION'); + }); + }); + }); + + describe('stage commands', () => { + it('should identify PUT', () => { + const result = parse('PUT file:///tmp/x.csv @stage;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('PUT'); + expect(result.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should identify GET', () => { + const result = parse('GET @stage file:///tmp/x.csv;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('GET'); + expect(result.body[0].executionType).to.eql('LISTING'); + }); + + it('should identify LIST', () => { + const result = parse('LIST @stage;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('LIST'); + expect(result.body[0].executionType).to.eql('LISTING'); + }); + + it('should identify LS as LIST', () => { + const result = parse('LS @stage;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('LIST'); + }); + + it('should identify REMOVE', () => { + const result = parse('REMOVE @stage/f.csv;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('REMOVE'); + expect(result.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should identify RM as REMOVE', () => { + const result = parse('RM @stage/f.csv;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('REMOVE'); + }); + }); + + describe('COPY', () => { + it('should identify COPY INTO table FROM stage (load)', () => { + const result = parse('COPY INTO foo FROM @stage;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('COPY'); + expect(result.body[0].executionType).to.eql('MODIFICATION'); + }); + + it('should identify COPY INTO stage FROM table (unload)', () => { + const result = parse('COPY INTO @stage FROM foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('COPY'); + expect(result.body[0].executionType).to.eql('MODIFICATION'); + }); + }); + + describe('EXPLAIN', () => { + it('should identify EXPLAIN SELECT', () => { + const result = parse('EXPLAIN SELECT * FROM foo;', true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql('EXPLAIN'); + expect(result.body[0].executionType).to.eql('INFORMATION'); + }); + }); + + describe('SHOW', () => { + ['WAREHOUSES', 'USERS', 'ROLES', 'SCHEMAS', 'STAGES'].forEach((target) => { + it(`should identify SHOW ${target}`, () => { + const result = parse(`SHOW ${target};`, true, 'snowflake'); + expect(result.body.length).to.eql(1); + expect(result.body[0].type).to.eql(`SHOW_${target}`); + expect(result.body[0].executionType).to.eql('LISTING'); + }); + }); }); // Parameters From 7321479c9767bd7247427ceeb83668bb90d78880 Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Mon, 1 Jun 2026 12:53:18 -0600 Subject: [PATCH 4/6] move snowflake only keywords to separate list --- src/tokenizer.ts | 33 +++++++++++++++++++-------- test/parser/single-statements.spec.ts | 8 +++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 10dbb74..30c0352 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -81,6 +81,13 @@ const KEYWORDS = [ 'DESC', 'USE', 'COPY', + 'USERS', + 'ROLES', + 'SCHEMAS', + 'SEQUENCES' +]; + +const SNOWFLAKE_KEYWORDS = [ 'PUT', 'GET', 'LIST', @@ -88,16 +95,12 @@ const KEYWORDS = [ 'REMOVE', 'RM', 'WAREHOUSES', - 'USERS', - 'ROLES', - 'SCHEMAS', 'STAGES', 'INTEGRATIONS', 'STREAMS', 'TASKS', 'PIPES', - 'SEQUENCES', -]; +] const INDIVIDUALS: Record = { ';': 'semicolon', @@ -146,7 +149,7 @@ export function scanToken( } if (isLetter(ch)) { - return scanWord(state); + return scanWord(state, dialect); } const individual = scanIndividualCharacter(state); @@ -189,8 +192,18 @@ function peek(state: State): Char { return state.input[state.position + 1]; } -function isKeyword(word: string): boolean { - return KEYWORDS.includes(word.toUpperCase()); +function getDialectSpecificKeywords(dialect: Dialect): string[] { + switch (dialect) { + case 'snowflake': + return SNOWFLAKE_KEYWORDS; + default: + return []; + } +} + +function isKeyword(word: string, dialect: Dialect): boolean { + const dialectSpecific = getDialectSpecificKeywords(dialect); + return KEYWORDS.includes(word.toUpperCase()) || dialectSpecific.includes(word.toUpperCase()); } function resolveIndividualTokenType(ch: string): Token['type'] | undefined { @@ -429,7 +442,7 @@ function scanQuotedIdentifier(state: State, endToken: Char): Token { }; } -function scanWord(state: State): Token { +function scanWord(state: State, dialect: Dialect): Token { let nextChar: Char; do { @@ -441,7 +454,7 @@ function scanWord(state: State): Token { } const value = state.input.slice(state.start, state.position + 1); - if (!isKeyword(value)) { + if (!isKeyword(value, dialect)) { return skipWord(state, value); } diff --git a/test/parser/single-statements.spec.ts b/test/parser/single-statements.spec.ts index 70b4ffc..33a65de 100644 --- a/test/parser/single-statements.spec.ts +++ b/test/parser/single-statements.spec.ts @@ -12,16 +12,16 @@ describe('parser', () => { describe('with strict disabled', () => { it('should parse if first token is unknown', () => { - const actual = parse('FOOBAR * FROM foo', false); + const actual = parse('LIST * FROM foo', false); actual.tokens = aggregateUnknownTokens(actual.tokens); expect(actual).to.eql({ type: 'QUERY', start: 0, - end: 16, + end: 14, body: [ { start: 0, - end: 16, + end: 14, parameters: [], tables: [], columns: [], @@ -29,7 +29,7 @@ describe('parser', () => { executionType: 'UNKNOWN', }, ], - tokens: [{ type: 'unknown', value: 'FOOBAR * FROM foo', start: 0, end: 16 }], + tokens: [{ type: 'unknown', value: 'LIST * FROM foo', start: 0, end: 14 }], }); }); From 0bd73c144126957b8d4b6688911818ad63bffebb Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Mon, 1 Jun 2026 12:53:37 -0600 Subject: [PATCH 5/6] fix lint --- src/tokenizer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 30c0352..2ec48a1 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -84,7 +84,7 @@ const KEYWORDS = [ 'USERS', 'ROLES', 'SCHEMAS', - 'SEQUENCES' + 'SEQUENCES', ]; const SNOWFLAKE_KEYWORDS = [ @@ -100,7 +100,7 @@ const SNOWFLAKE_KEYWORDS = [ 'STREAMS', 'TASKS', 'PIPES', -] +]; const INDIVIDUALS: Record = { ';': 'semicolon', From 535a736ead8402defb9235a608acf88f92709ae0 Mon Sep 17 00:00:00 2001 From: Day Matchullis Date: Thu, 4 Jun 2026 13:59:22 -0600 Subject: [PATCH 6/6] weird merge issue --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index aa8c77f..1495159 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import { Dialect, Token } from './defines'; export function getStartQuotes(dialect: Dialect): string[] { if (dialect === 'mssql') { return ['"', '[']; - } else if (dialect) { + } else if (dialect === 'snowflake') { return ['"']; } else { return ['"', '`'];