Skip to content

OUTPUT $action is counted as a placeholder: extra argument and a false placeholder-style error on MERGE #231

Description

@tiagolauer

$action is a MERGE pseudo-column, not a parameter — the row type already handles it correctly ('INSERT' | 'UPDATE' | 'DELETE', README Limitations). But IsPlaceholder matches any token starting with $, so the parameter scanner and the placeholder-style check both treat $action as a $ placeholder. The documented MERGE example therefore does not compile against the mssql adapter.

Reproduction

const db = createTypedDb<DB, { placeholders: 'at' }>(createMssqlExecutor(pool), { placeholders: 'at' });

await db.query(
  'merge into users using orders on users.id = orders.user_id when matched then update set name = @name output $action',
  'x',
);

Two independent errors:

  1. Extra argument.

    type P = Params<DB, '…output $action'>;
    //   ^? [unknown, string]     (expected: [string] — only @name is a parameter)

    With the right arity the call reports Expected 3 arguments, but got 2.

  2. Bogus dialect error. With the arity satisfied:

    Argument of type '"merge into … output $action"' is not assignable to parameter of type
    '"merge into … output $action" & QueryTypeError<"the query placeholder style does not match the executor dialect">'
    

    because UsedPlaceholderStyles<Q> sees the $ in $action and reports 'dollar' for a query whose only real placeholder is @name. MERGE is SQL-Server-only, so the executor is always 'at' — the check fires on every MERGE that outputs $action.

Root cause

src/params.ts:

  • IsPlaceholder — excludes $$…, '$', @@…, but not $action.
  • UsedPlaceholderStylesText extends \${string}$${string}` ? 'dollar' : never`, no exclusion at all.

ResolveColumnType already special-cases the pseudo-column via IsMergeActionPseudoColumn (src/parse.ts); the same exclusion is missing on the parameter side. Excluding a case-insensitive $action in IsPlaceholder and stripping it before the 'dollar' test in UsedPlaceholderStyles (the way StripDoubledAt already handles @@) covers both symptoms.

Found in an audit of master @ dc1afdb (v0.1.8).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions