diff --git a/src/types.js b/src/types.js index 7c7c2b9..f0cf15d 100644 --- a/src/types.js +++ b/src/types.js @@ -173,7 +173,10 @@ const builders = Object.entries({ return '(' + escapeIdentifiers(columns, options) + ')values' + valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) } -}).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) +}).map(([x, fn]) => { + const keyword = '(?:^|[\\s(])' + x + '(?:$|[\\s(])' + return [new RegExp(keyword + '(?![\\s\\S]*' + keyword + ')', 'i'), fn] +}) function notTagged() { throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') diff --git a/tests/index.js b/tests/index.js index 845c6ce..89f04f5 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2615,6 +2615,105 @@ t('Last keyword used even with duplicate keywords', async() => { return [x, true, await sql`drop table test`] }) +t('Last keyword used even when an earlier keyword is followed by (', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in(select x from test where x in ${ sql([1, 2]) })` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used even when it ends the string', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in (select x from test where x in${ sql([1, 2]) })` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used when nested keywords are all spaced', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in (select x from test where x in ${ sql([1, 2]) })` + + return [1, x, await sql`drop table test`] +}) + +t('Single keyword followed by ( is still used', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where (x,x) in(${ sql([[1, 1]]) })` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used for keywords other than in', async() => { + await sql`create table test (x int)` + const [{ x }] = await sql`with a as (insert into test values(2) returning(x)) insert into test values(3) returning ${ sql(['x']) }` + + return [3, x, await sql`drop table test`] +}) + +t('Last keyword used when the spaced keyword comes first', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in (select x from test where x in(select x from test where x in ${ sql([1, 2]) }))` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used with three occurrences and three delimiters', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in(select x from test where x in (select x from test where x in${ sql([1, 2]) }))` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used case insensitively', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`SELECT x FROM test WHERE x IN(SELECT x FROM test WHERE x IN ${ sql([1, 2]) })` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used when the earlier keyword is in a nested fragment', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x ${ sql`in(select x from test where x in ${ sql([1, 2]) })` }` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used when an earlier keyword is newline delimited', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x +in(select x from test where x in ${ sql([1, 2]) })` + + return [1, x, await sql`drop table test`] +}) + +// Control: identical `(` delimiters were never affected by the backreference, +// so this passes with and without the fix. It pins that the repeated-pattern +// lookahead does not regress the same-delimiter case. +t('Repeated keyword with identical ( delimiters is unchanged (control)', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql`select x from test where x in(select x from test where (x,x) in(${ sql([[1, 1]]) }))` + + return [1, x, await sql`drop table test`] +}) + +t('Last keyword used with an empty array', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const rows = await sql`select x from test where x in(select x from test where x in ${ sql([]) })` + + return [0, rows.length, await sql`drop table test`] +}) + t('Insert array with null', async() => { await sql`create table test (x int[])` await sql`insert into test ${ sql({ x: [1, null, 3] }) }`