From 071946f3131995e08b50d7f326378ba2e6216df7 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Thu, 26 Feb 2026 12:34:07 -0600 Subject: [PATCH 1/4] 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/4] 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/4] 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( From 42e753b3b697505a6b1442dc8a39fa6c71227bc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 23:11:27 +0000 Subject: [PATCH 4/4] Exclude DECLARE'd local variables from parameters Local variables declared via DECLARE inside a function or procedure body aren't user-supplied parameters, so they shouldn't appear in the identified parameters list. Track variables introduced by a DECLARE keyword and skip them on subsequent parameter-token occurrences. Also simplifies the mssql double-colon regression test to a single SET statement focused on verifying that :: cast syntax doesn't produce colon-prefixed parameters. Addresses review feedback on PR #87. --- src/parser.ts | 18 +++++++++++++----- test/identifier/single-statement.spec.ts | 2 +- test/index.spec.ts | 8 +------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 700b10c..482fd12 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -823,6 +823,8 @@ function stateMachineStatementParser( let openBlocks = 0; + const declaredVariables = new Set(); + /* eslint arrow-body-style: 0, no-extra-parens: 0 */ const isValidToken = (step: Step, token: Token) => { if (!step.validation) { @@ -924,11 +926,17 @@ function stateMachineStatementParser( } } - if ( - token.type === 'parameter' && - (token.value === '?' || !statement.parameters.includes(token.value)) - ) { - statement.parameters.push(token.value); + if (token.type === 'parameter') { + // Variables declared via DECLARE inside a function/procedure body are + // local — they aren't user-supplied parameters, so exclude them. + if (prevNonWhitespaceToken?.value.toUpperCase() === 'DECLARE') { + declaredVariables.add(token.value); + } else if ( + !declaredVariables.has(token.value) && + (token.value === '?' || !statement.parameters.includes(token.value)) + ) { + statement.parameters.push(token.value); + } } if (statement.type && statement.start >= 0) { diff --git a/test/identifier/single-statement.spec.ts b/test/identifier/single-statement.spec.ts index 6a99cd4..5bdaccd 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: ['@DATE', '@ISOweek'], + parameters: ['@DATE'], tables: [], }, ]; diff --git a/test/index.spec.ts b/test/index.spec.ts index b5c6723..bbc4443 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -138,13 +138,7 @@ describe('Regression tests', () => { // Regression test: https://github.com/beekeeper-studio/beekeeper-studio/issues/2560 it('Double colon should not be recognized as a param for mssql', () => { const result = identify( - ` - DECLARE @g geometry; - DECLARE @h geometry; - SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0); - set @h = geometry::STGeomFromText('POLYGON((1 1, 3 1, 3 3, 1 3, 1 1))', 0); - SELECT @g.STWithin(@h); - `, + "SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0);", { strict: false, dialect: 'mssql' as Dialect }, ); result.forEach((res) => {