Skip to content

[Pg-kit]: Bind positional parameters over the AWS Data API - #6148

Open
the-simian wants to merge 1 commit into
drizzle-team:betafrom
the-simian:pg-kit-aws-data-api-positional-params
Open

[Pg-kit]: Bind positional parameters over the AWS Data API#6148
the-simian wants to merge 1 commit into
drizzle-team:betafrom
the-simian:pg-kit-aws-data-api-positional-params

Conversation

@the-simian

@the-simian the-simian commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #6147.

The problem

The RDS Data API binds named parameters (:1) only. It never binds Postgres positional placeholders ($1), and reports the mismatch as:

bind message supplies 0 parameters, but prepared statement "sqlx_s_35" requires 1; SQLState: 08P01

SQL that drizzle-orm generates already arrives in named form, because AwsPgDialect.escapeParam() emits :${index + 1}. Studio does not go through the dialect: it forwards raw SQL carrying $N to proxy, and nothing translates it before session.prepareQuery.

The result is that drizzle-kit studio cannot run any parameterized query against a Data API connection.

The change

prepareAwsDataApiSql(sql: string, parameterCount: number): string in drizzle-kit/src/utils/aws-data-api-placeholders.ts, applied at both Data API adapter entry points in cli/connections.ts. proxy is the path studio uses and the one that is broken today; query gets the same treatment for consistency, since the two are the same adapter and introspection on this branch happens to be parameter-free.

It takes the parameter count rather than the parameter array because the count is its entire dependency: nothing in the rewrite inspects a value. That keeps the signature free of any and unknown.

The rewrite is literal aware, because a blind $N replace corrupts valid SQL. These spans are copied through untouched:

  • line comments, ending at CR as well as LF, and nestable block comments;
  • string constants, including '' escapes, E'...\'...', and continuations across a newline (line comments included), which keep the escape-string property of the first segment;
  • dollar-quoted bodies, tagged or not ($$ ... $$, $tag$ ... $tag$), including an unterminated body, which runs to end of input the way Postgres would read it;
  • quoted identifiers ("a $1 b");
  • identifiers, including non-ASCII ones, since Postgres treats every non-ASCII byte as an identifier character and permits $ after the first character. foo$1 and é$1 are each a single identifier, and $é$ is a valid dollar-quote tag.

Two deliberately conservative choices:

  • Only $N where 1 <= N <= parameterCount is rewritten. A stray $9 in a two-parameter query is left for the server to reject rather than silently renamed.
  • With no parameters, the input is returned unchanged, so the scanner never runs on the introspection path.

The one documented limitation is that it assumes standard_conforming_strings is on, which is the default and what Aurora ships.

Tests

drizzle-kit/tests/other/aws-data-api-placeholders.test.ts, 30 cases covering every span type above plus the boundaries: $0, a lone $, a two-digit index, an index beyond the parameter count, a repeated placeholder, a placeholder at end of input, and a quote following an identifier ending in e (which is not an E-string).

The lexical edge cases are not guesses. I checked each one against real Postgres via PGlite, the same engine this package already tests against, and the tests encode the observed behavior:

Construct Postgres
SELECT E'a'\n'it\'s' ait's, so a continuation keeps escape-string semantics
SELECT E'a' -- c\n'it\'s' ait's, so a line comment is continuation whitespace, and a block comment there is a syntax error
SELECT 'a' 'b' on one line syntax error, so a continuation requires a newline
SELECT 1 AS a --c<CR>, 2 AS b two columns, so CR ends a line comment
SELECT 1 AS é$1 one column named é$1
SELECT $é$ hi $é$ valid dollar-quoted string
SELECT $1abc trailing junk after parameter, so it is not a placeholder to preserve

The function is pure, so these need no database and no AWS account; they run in tests/other with the rest of the Docker-free suite.

What I verified, and what I did not

  • The 30 tests pass inside this package, on Node 24.19.0 with pnpm 10.15.0:

    $ cd drizzle-kit && pnpm vitest run tests/other/aws-data-api-placeholders.test.ts
     ✓ tests/other/aws-data-api-placeholders.test.ts (30 tests) 3ms
       Test Files  1 passed (1)
            Tests  30 passed (30)
    
  • The change adds no new failures to the Docker-free tests/other subset. Run on beta at 748058e and on this branch, the same 17 tests fail in the same files on both sides; the only difference is the passing tests this PR adds.

  • dprint check is clean on all three files.

  • The module typechecks under --strict --noUncheckedIndexedAccess, which is stricter than drizzle-kit/tsconfig.json. No any, no casts, no non-null assertions.

  • Behavior was verified end to end against Aurora Serverless v2 (PostgreSQL 15.12) over the Data API, using a standalone reproduction: https://github.com/simiancraft/drizzle-kit-rds-data-api-repro. Its probe sends the same query twice, once without parameters and once with one, so the control isolates the failure to binding rather than to the query or the connection.

  • I did not run the full drizzle-kit suite, since the rest of it stands up Docker-backed databases I cannot run here. Happy to iterate if CI surfaces anything.

I put this in drizzle-kit because escapeParam() is already correct and AwsDataApiSession is not the thing that is wrong. If you would rather the translation live in drizzle-orm's session so every consumer inherits it, say so and I will move it.

The Data API binds named parameters only, so `$N` placeholders forwarded to
`ExecuteStatement` are never bound and Postgres reports `bind message supplies
0 parameters, but prepared statement requires N`. SQL that drizzle-orm
generates already arrives in named form via `AwsPgDialect.escapeParam()`, but
SQL reaching `query` and `proxy` does not come from the dialect; studio sends
raw `$N`.

Rewrite `$N` to `:N` before `prepareQuery`, skipping comments, strings,
dollar-quoted bodies, and identifiers so dollar signs that are not placeholders
survive untouched.

Fixes drizzle-team#6147
@the-simian
the-simian force-pushed the pg-kit-aws-data-api-positional-params branch from d6cea52 to 7306909 Compare August 20, 2026 16:03
@the-simian

the-simian commented Aug 20, 2026

Copy link
Copy Markdown
Author

@AleksandrSherman / @AndriiSherman following up from #2982: you asked for a minimal reproducible example and suggested trying 1.0.0-rc.4. I did both, and the rc turned out to be the answer for almost all of it, so I have closed that issue.

This PR is the one defect that survives on 1.0.0-rc.4. Details and an isolated reproduction are in #6147; the short version is that the Data API binds named parameters only, and studio's raw SQL still arrives carrying $N.

What I did to keep this cheap to review:

  • The integration is three lines in cli/connections.ts. Everything else is one new pure function and its tests.
  • 30 unit tests in tests/other, passing in-package on Node 24.19.0 with your pinned vitest. No database and no AWS account needed to run them.
  • The lexical edge cases are pinned against real Postgres via PGlite, the same engine this package tests with, rather than against my own reading of the lexer. Escape-string continuations, non-ASCII identifiers and dollar-quote tags, and CR-terminated line comments each have an observed-behavior test behind them.
  • No new failures in the Docker-free tests/other subset: the same 17 tests fail in the same files on beta at 748058e and on this branch.
  • Verified end to end against Aurora Serverless v2 (PostgreSQL 15.12) over the Data API: https://github.com/simiancraft/drizzle-kit-rds-data-api-repro

I have not run the Docker-backed parts of the suite, and CI has not run here since workflow approval is gated for first-time contributors, so that is the gap in my own verification.

Anything you want changed, I will turn around quickly. With no attachment to my choices: the fix could live in AwsDataApiSession in drizzle-orm instead of in kit, the function or file could be renamed or moved, and I can add or trim coverage. Just say which.

@the-simian

the-simian commented Aug 20, 2026

Copy link
Copy Markdown
Author

Here's the before/after proof to make this easier to thumb up:

Verified against real Aurora Serverless v2 over the RDS Data API, since that is the surface this fixes. Same cluster, same database, same query, same probe, on 1.0.0-rc.4 with and without this change.

Before, published drizzle-kit@1.0.0-rc.4, unmodified:

### CONTROL: no parameters
sql:    select count(*)::text as n from regions
params: []
result: [{"n":"0"}]

### TEST: one bound parameter
sql:    select count(*)::text as n from regions where country_code = $1
params: ["US"]
result: {"status":"error","error":"ERROR: bind message supplies 0 parameters,
         but prepared statement \"sqlx_s_5\" requires 1; SQLState: 08P01"}

After, with this PR's rewrite applied at the two cli/connections.ts call sites:

### CONTROL: no parameters
result: [{"n":"0"}]

### TEST: one bound parameter
sql:    select count(*)::text as n from regions where country_code = $1
params: ["US"]
result: [{"n":"0"}]

The control passes in both runs, so the connection and the query were never the variable; only the binding was.

An empty result is weak evidence on its own, so two more probes through the same studio proxy confirm the parameters are genuinely bound rather than merely tolerated:

sql:    select $1::text as echoed, $2::int + 1 as incremented
params: ["round-trip", 41]
result: [{"echoed":"round-trip","incremented":42}]

Values arrive intact and in the right positions. And the case the literal-aware scan exists for:

sql:    select 'costs $5 and $1' as literal, $1::text as bound
params: ["bound-value"]
result: [{"literal":"costs $5 and $1","bound":"bound-value"}]

The $5 and $1 inside the string constant come back untouched, while the real $1 outside it binds. A naive replace would have rewritten all three and corrupted the literal.

Reproduction, including the probe used above: https://github.com/simiancraft/drizzle-kit-rds-data-api-repro (rc branch pins 1.0.0-rc.4).

More rigorous testing: behavior is now covered by a differential fuzz I ran locally against PGlite: 400 generated statements mixing real placeholders with decoy $N hidden inside strings, E-strings, dollar-quoted bodies, comments, quoted and non-ASCII identifiers, and multi-segment string continuations. Postgres PREPAREs each statement and reports how many parameters it actually sees, which is checked against the number the generator intended, and the rewrite is checked against the expected output. 400 checked, 0 oracle disagreements, 0 rewrite mismatches.

That 400-statement harness is not part of this PR, (I assumed it would be overkill) but I am happy to contribute it if you would want it in the suite. Point is, this is the fix, I'm sure of it.

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.

1 participant