Skip to content

fix(params): strip $action as a whole token, not a substring - #329

Merged
tiagolauer merged 1 commit into
masterfrom
fix/298-dollar-action-token
Aug 2, 2026
Merged

fix(params): strip $action as a whole token, not a substring#329
tiagolauer merged 1 commit into
masterfrom
fix/298-dollar-action-token

Conversation

@tiagolauer

Copy link
Copy Markdown
Owner

Fixes #298.

The bug

const db = createTypedDb<DB, { placeholders: 'question' }>(executor);
db.query('select id from users where kind = $actionType', 'x'); // compiles

UsedPlaceholderStyles came back never for that query, so the brand check passed for any dialect, while Params still assigned the token a [string] slot. A mysql or sqlite driver can never bind a $name, so this is exactly the guaranteed runtime failure the brand exists to prevent. Same for $action_id, $actionable, and friends.

The fix

The two layers disagreed about what $action means. StripDollarAction removed the substring anywhere, so $actiontype became type before the dollar scan ran; IsPlaceholder excludes only the exact token. The strip now follows IsPlaceholder's rule: keep $action when an identifier character follows it.

MERGE is unaffected — a real output $action, inserted.id has a comma after it, and tests/dialect-mssql.test-d.ts pins that (the pseudo-column takes no parameter slot and does not make the statement look dollar-styled).

Verification

tests/dialect-brand.test-d.ts, four new cases. The three @ts-expect-error ones are red on master, which tsc reports precisely because the directive is unused there:

tests/dialect-brand.test-d.ts(58,3): error TS2578: Unused '@ts-expect-error' directive.
tests/dialect-brand.test-d.ts(61,3): error TS2578: Unused '@ts-expect-error' directive.
tests/dialect-brand.test-d.ts(64,3): error TS2578: Unused '@ts-expect-error' directive.
  • a question-style client rejects $actionType, $action_id and $actionable
  • a control: a dollar-style client still accepts $actionType

tsc --noEmit clean, budget 192,568 (+82).

    const db = createTypedDb<DB, { placeholders: 'question' }>(executor);
    db.query('select id from users where kind = $actionType', 'x'); // compiled

Any `$name` starting with "action" escaped the placeholder-style check.
StripDollarAction removed the substring `$action` anywhere, so `$actiontype`
became `type` before the dollar scan ran, UsedPlaceholderStyles came back
`never`, and the brand accepted every dialect - while Params still assigned
the token a slot. A mysql or sqlite driver cannot bind a `$name`, so this is
exactly the guaranteed runtime failure the brand exists to prevent.

The strip now keeps `$action` when an identifier character follows it, which
is the rule IsPlaceholder already applies on the other side (it excludes only
the exact token). A real `output $action, inserted.id` is followed by a
comma, so MERGE is unaffected.

Fixes #298

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tiagolauer
tiagolauer force-pushed the fix/298-dollar-action-token branch from 5ee0cea to 6ad5f73 Compare August 2, 2026 13:03
@tiagolauer
tiagolauer merged commit 7300771 into master Aug 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$action-prefixed named placeholders escape the dialect brand check

1 participant