Conversation
…rsager#701 The builder keyword regexes used a backreference (\1) in their trailing negative lookahead, so the 'no later occurrence' assertion only rejected a repeat that matched the SAME delimiters as the first hit. ' in(' and ' in ' are different text, so a later ' in ' did not cancel an earlier ' in('. Builder.build picks the keyword with the highest index, so 'where x in(select ... where x in ${...}' kept the leading in(, ranked the '(select ' hit above it, and handed the array to the select helper, which threw TypeError: str.replace is not a function. Repeat the keyword pattern inside the lookahead instead of backreferencing the captured text, so any later occurrence counts regardless of which delimiters surround it.
The single-keyword test built `x in((1,2))` - the template supplies the ( and the in builder emits its own ( ) - so the operand was a row constructor and Postgres rejected it with "operator does not exist: integer = record" on both arms. Compare a row constructor on both sides instead, which keeps the in( delimiter the test exists to cover. Also cover the cases the fix changes that no test pinned: a keyword other than in, the reverse ordering, three occurrences with three delimiters, and uppercase.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #701
Summary
src/types.jscompiles one regex per helper keyword (values,in,select,as,returning,\(,update,insert). Each was((?:^|[\s(])KW(?:$|[\s(]))(?![\s\S]*\1)— a capture group plus a backreference\1in the trailing "no later occurrence" lookahead." in("and" in "are different strings, so a later" in "did not cancel an earlier" in(". The stale earlier hit survived, andBuilder.build(which picks the highest index) then ranked the"(select "hit above it.selecthelper →escapeIdentifiers→escape()→str.replaceon a number, throwingTypeError: str.replace is not a function.src/types.js: hoist the keyword pattern into aconstand repeat it inside the lookahead instead of backreferencing the captured text, so any later occurrence counts regardless of surrounding delimiters.Last keyword used even with duplicate keywordstest — 9 discriminate (fail on base), 3 are declared controls (pass on both arms), and 1 is the pre-existing test re-run as a baseline.The three rows that pass on both arms are deliberate controls: the same-delimiter spaced path, the single-keyword
in(path, and the case the backreference already handled correctly. Two base failures are notably not exceptions: one returns a wrong answer (expected 3 != got undefined) and one is rejected by the server (syntax error at or near ")", from the emittedwhere x in )) — both stronger arguments for the fix than a build-timeTypeError.Decisions
src/types.js, the.map()that compilesbuilders:The intent of the regex was "use the last occurrence of the keyword". Repeating the keyword pattern in the lookahead expresses that directly; the backreference expressed the narrower and unintended "no later occurrence with identical delimiters". The capture group is dropped because nothing else read it.
Minimality: one expression, no behaviour added, no new dependency, no version bump, no changelog. It preserves the existing structure — still one regex per keyword, still consumed unchanged by
Builder.build.Alternatives rejected:
Builder.buildre-scan for the last match instead of relying on the lookahead — larger change to the hot path, and the lookahead is already the designed mechanism.lastIndexOf-style scanning — changessearch()semantics for all eight keywords and riskslastIndexstatefulness on shared regex objects.in— the defect is generic to every keyword (valuesshows the same index shift); special-casing would leave the others wrong.(a,b,c) IN (${...})— rejected as out of scope: different root cause, a separate'\\(' : selectbuilder legitimately out-ranksinthere. That is a separate bug and belongs in its own PR.Not run:
npm run test:esm/test:cjs/test:denoas a whole (the bootstrap needscreatedb/psqlto provision roles, unavailable in my environment); the thirteen tests were instead run end-to-end against a real PostgreSQL-wire-protocol backend directly, so thet()harness wrapper itself and the transpiled cjs/deno copies are unverified here — the changed expression is plain ES2020 so no transpile-specific risk is expected, but CI should confirm.AI assistance: this bug was found and the fix and tests were drafted with AI tooling in my workflow; the tests and checks above were executed as pasted. I'm responsible for the change and will handle review feedback.