From 071946f3131995e08b50d7f326378ba2e6216df7 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Thu, 26 Feb 2026 12:34:07 -0600 Subject: [PATCH 1/3] Fix incorrect default param types for mssql, oracle, and sqlite - mssql: change named prefix from ':' to '@' (T-SQL uses @name, not :name) - oracle: add named [':'] and numbered [':'] (OCI/node-oracledb use :name and :N) - sqlite: add '$' to named prefixes ($name is natively supported by SQLite) Update all affected tests to match the corrected syntax. Fixes #55 Co-Authored-By: Claude Sonnet 4.6 --- src/parser.ts | 7 ++- test/identifier/single-statement.spec.ts | 74 +++++++++++++++++++++--- test/index.spec.ts | 3 +- test/parser/single-statements.spec.ts | 14 ++--- test/tokenizer/index.spec.ts | 38 ++++++++++-- 5 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index b185904..f8288ef 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1112,8 +1112,13 @@ export function defaultParamTypesFor(dialect: Dialect): ParamTypes { numbered: ['$'], }; case 'mssql': + return { + named: ['@'], + }; + case 'oracle': return { named: [':'], + numbered: [':'], }; case 'bigquery': return { @@ -1125,7 +1130,7 @@ export function defaultParamTypesFor(dialect: Dialect): ParamTypes { return { positional: true, numbered: ['?'], - named: [':', '@'], + named: [':', '@', '$'], }; default: return { diff --git a/test/identifier/single-statement.spec.ts b/test/identifier/single-statement.spec.ts index f86c1d8..037a532 100644 --- a/test/identifier/single-statement.spec.ts +++ b/test/identifier/single-statement.spec.ts @@ -791,7 +791,7 @@ describe('identifier', () => { text: query, type: 'CREATE_FUNCTION', executionType: 'MODIFICATION', - parameters: [], + parameters: ['@DATE', '@ISOweek'], tables: [], }, ]; @@ -1445,7 +1445,7 @@ describe('identifier', () => { }); it('Should extract named Parameters', () => { - const actual = identify('SELECT * FROM Persons where x = :one and y = :two and a = :one', { + const actual = identify('SELECT * FROM Persons where x = @one and y = @two and a = @one', { dialect: 'mssql', strict: true, }); @@ -1453,10 +1453,10 @@ describe('identifier', () => { { start: 0, end: 61, - text: 'SELECT * FROM Persons where x = :one and y = :two and a = :one', + text: 'SELECT * FROM Persons where x = @one and y = @two and a = @one', type: 'SELECT', executionType: 'LISTING', - parameters: [':one', ':two'], + parameters: ['@one', '@two'], tables: [], }, ]; @@ -1465,7 +1465,7 @@ describe('identifier', () => { }); it('Should extract named Parameters with trailing commas', () => { - const actual = identify('SELECT * FROM Persons where x in (:one, :two, :three)', { + const actual = identify('SELECT * FROM Persons where x in (@one, @two, @three)', { dialect: 'mssql', strict: true, }); @@ -1473,10 +1473,70 @@ describe('identifier', () => { { start: 0, end: 52, - text: 'SELECT * FROM Persons where x in (:one, :two, :three)', + text: 'SELECT * FROM Persons where x in (@one, @two, @three)', + type: 'SELECT', + executionType: 'LISTING', + parameters: ['@one', '@two', '@three'], + tables: [], + }, + ]; + + expect(actual).to.eql(expected); + }); + + it('Should extract oracle named parameters', () => { + const actual = identify('SELECT * FROM persons WHERE id = :one AND status = :two', { + dialect: 'oracle', + strict: true, + }); + const expected = [ + { + start: 0, + end: 54, + text: 'SELECT * FROM persons WHERE id = :one AND status = :two', + type: 'SELECT', + executionType: 'LISTING', + parameters: [':one', ':two'], + tables: [], + }, + ]; + + expect(actual).to.eql(expected); + }); + + it('Should extract oracle numbered parameters', () => { + const actual = identify('SELECT * FROM persons WHERE id = :1 AND status = :2', { + dialect: 'oracle', + strict: true, + }); + const expected = [ + { + start: 0, + end: 50, + text: 'SELECT * FROM persons WHERE id = :1 AND status = :2', + type: 'SELECT', + executionType: 'LISTING', + parameters: [':1', ':2'], + tables: [], + }, + ]; + + expect(actual).to.eql(expected); + }); + + it('Should extract sqlite $name parameters', () => { + const actual = identify('SELECT * FROM persons WHERE id = $myid', { + dialect: 'sqlite', + strict: true, + }); + const expected = [ + { + start: 0, + end: 37, + text: 'SELECT * FROM persons WHERE id = $myid', type: 'SELECT', executionType: 'LISTING', - parameters: [':one', ':two', ':three'], + parameters: ['$myid'], tables: [], }, ]; diff --git a/test/index.spec.ts b/test/index.spec.ts index 557cd86..a6b3490 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -148,7 +148,8 @@ describe('Regression tests', () => { { strict: false, dialect: 'mssql' as Dialect }, ); result.forEach((res) => { - expect(res.parameters.length).to.equal(0); + // :: cast syntax should not produce colon-prefixed parameters + expect(res.parameters.every((p) => !p.startsWith(':'))).to.equal(true); }); }); diff --git a/test/parser/single-statements.spec.ts b/test/parser/single-statements.spec.ts index 4fa5b0b..b2f142a 100644 --- a/test/parser/single-statements.spec.ts +++ b/test/parser/single-statements.spec.ts @@ -804,7 +804,7 @@ describe('parser', () => { it('should extract mssql parameters', () => { const actual = parse( - 'select x from a where x = :foo', + 'select x from a where x = @foo', true, 'mssql', false, @@ -826,13 +826,13 @@ describe('parser', () => { }, { type: 'parameter', - value: ':foo', + value: '@foo', start: 26, end: 29, }, ]; expect(actual.tokens).to.eql(expected); - expect(actual.body[0].parameters).to.eql([':foo']); + expect(actual.body[0].parameters).to.eql(['@foo']); }); it('should not identify params in a comment', () => { @@ -875,7 +875,7 @@ describe('parser', () => { it('should extract multiple mssql parameters', () => { const actual = parse( - 'select x from a where x = :foo and y = :bar', + 'select x from a where x = @foo and y = @bar', true, 'mssql', false, @@ -897,7 +897,7 @@ describe('parser', () => { }, { type: 'parameter', - value: ':foo', + value: '@foo', start: 26, end: 29, }, @@ -909,13 +909,13 @@ describe('parser', () => { }, { type: 'parameter', - value: ':bar', + value: '@bar', start: 39, end: 42, }, ]; expect(actual.tokens).to.eql(expected); - expect(actual.body[0].parameters).to.eql([':foo', ':bar']); + expect(actual.body[0].parameters).to.eql(['@foo', '@bar']); }); }); }); diff --git a/test/tokenizer/index.spec.ts b/test/tokenizer/index.spec.ts index 7195735..90ee47f 100644 --- a/test/tokenizer/index.spec.ts +++ b/test/tokenizer/index.spec.ts @@ -271,7 +271,7 @@ describe('scan', () => { ['?', 'generic'], ['?', 'mysql'], ['?', 'sqlite'], - [':', 'mssql'], + ['@', 'mssql'], ].forEach(([ch, dialect]) => { it(`scans just ${ch} as parameter for ${dialect}`, () => { const input = `${ch}`; @@ -324,7 +324,6 @@ describe('scan', () => { }); [ ['$', 'psql'], - [':', 'mssql'], ].forEach(([ch, dialect]) => { it(`should scan ${ch}1 for ${dialect}`, () => { const input = `${ch}1`; @@ -370,19 +369,34 @@ describe('scan', () => { const paramTypes = defaultParamTypesFor('mssql'); [ { - actual: scanToken(initState(':one,'), 'mssql', paramTypes), + actual: scanToken(initState('@one,'), 'mssql', paramTypes), expected: { type: 'parameter', - value: ':one', + value: '@one', + start: 0, + end: 3, + }, + }, + { + actual: scanToken(initState('@two)'), 'mssql', paramTypes), + expected: { + type: 'parameter', + value: '@two', start: 0, end: 3, }, }, + ].forEach(({ actual, expected }) => expect(actual).to.eql(expected)); + }); + + it('should not include trailing non-alphanumerics for oracle', () => { + const paramTypes = defaultParamTypesFor('oracle'); + [ { - actual: scanToken(initState(':two)'), 'mssql', paramTypes), + actual: scanToken(initState(':one,'), 'oracle', paramTypes), expected: { type: 'parameter', - value: ':two', + value: ':one', start: 0, end: 3, }, @@ -390,6 +404,18 @@ describe('scan', () => { ].forEach(({ actual, expected }) => expect(actual).to.eql(expected)); }); + it('should recognize $name for sqlite', () => { + const paramTypes = defaultParamTypesFor('sqlite'); + const actual = scanToken(initState('$myvar'), 'sqlite', paramTypes); + const expected = { + type: 'parameter', + value: '$myvar', + start: 0, + end: 5, + }; + expect(actual).to.eql(expected); + }); + describe('custom parameters', () => { describe('positional parameters', () => { const paramTypes = { From 9475da56f19b3271593f9d6fa3a0bfe076357115 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Thu, 26 Feb 2026 14:09:04 -0600 Subject: [PATCH 2/3] Add :name as a supported parameter style for mssql T-SQL supports both @name (native) and :name (common in ORMs/drivers). Add ':' to mssql named prefixes and add tests for colon-style params. Co-Authored-By: Claude Sonnet 4.6 --- src/parser.ts | 2 +- test/identifier/single-statement.spec.ts | 20 ++++++++++++++++++++ test/tokenizer/index.spec.ts | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index f8288ef..700b10c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1113,7 +1113,7 @@ export function defaultParamTypesFor(dialect: Dialect): ParamTypes { }; case 'mssql': return { - named: ['@'], + named: ['@', ':'], }; case 'oracle': return { diff --git a/test/identifier/single-statement.spec.ts b/test/identifier/single-statement.spec.ts index 037a532..6a99cd4 100644 --- a/test/identifier/single-statement.spec.ts +++ b/test/identifier/single-statement.spec.ts @@ -1484,6 +1484,26 @@ describe('identifier', () => { expect(actual).to.eql(expected); }); + it('Should extract mssql colon-prefixed named parameters', () => { + const actual = identify('SELECT * FROM Persons where x = :one and y = :two and a = :one', { + dialect: 'mssql', + strict: true, + }); + const expected = [ + { + start: 0, + end: 61, + text: 'SELECT * FROM Persons where x = :one and y = :two and a = :one', + type: 'SELECT', + executionType: 'LISTING', + parameters: [':one', ':two'], + tables: [], + }, + ]; + + expect(actual).to.eql(expected); + }); + it('Should extract oracle named parameters', () => { const actual = identify('SELECT * FROM persons WHERE id = :one AND status = :two', { dialect: 'oracle', diff --git a/test/tokenizer/index.spec.ts b/test/tokenizer/index.spec.ts index 90ee47f..1d182ca 100644 --- a/test/tokenizer/index.spec.ts +++ b/test/tokenizer/index.spec.ts @@ -386,6 +386,24 @@ describe('scan', () => { end: 3, }, }, + { + actual: scanToken(initState(':one,'), 'mssql', paramTypes), + expected: { + type: 'parameter', + value: ':one', + start: 0, + end: 3, + }, + }, + { + actual: scanToken(initState(':two)'), 'mssql', paramTypes), + expected: { + type: 'parameter', + value: ':two', + start: 0, + end: 3, + }, + }, ].forEach(({ actual, expected }) => expect(actual).to.eql(expected)); }); From d093958f609c70a4797a688dcaf006aa60a0a1ea Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Thu, 26 Feb 2026 14:19:07 -0600 Subject: [PATCH 3/3] Fix lint errors: rename short var and reformat single-element array Co-Authored-By: Claude Sonnet 4.6 --- test/index.spec.ts | 2 +- test/tokenizer/index.spec.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index a6b3490..b5c6723 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -149,7 +149,7 @@ describe('Regression tests', () => { ); result.forEach((res) => { // :: cast syntax should not produce colon-prefixed parameters - expect(res.parameters.every((p) => !p.startsWith(':'))).to.equal(true); + expect(res.parameters.every((param) => !param.startsWith(':'))).to.equal(true); }); }); diff --git a/test/tokenizer/index.spec.ts b/test/tokenizer/index.spec.ts index 1d182ca..c0b7805 100644 --- a/test/tokenizer/index.spec.ts +++ b/test/tokenizer/index.spec.ts @@ -322,9 +322,7 @@ describe('scan', () => { expect(actual).to.eql(expected); }); }); - [ - ['$', 'psql'], - ].forEach(([ch, dialect]) => { + [['$', 'psql']].forEach(([ch, dialect]) => { it(`should scan ${ch}1 for ${dialect}`, () => { const input = `${ch}1`; const actual = scanToken(