Skip to content

named: support PostgreSQL :: cast directly after a named param - #985

Open
c-tonneslan wants to merge 1 commit into
jmoiron:masterfrom
c-tonneslan:fix/named-param-followed-by-cast
Open

named: support PostgreSQL :: cast directly after a named param#985
c-tonneslan wants to merge 1 commit into
jmoiron:masterfrom
c-tonneslan:fix/named-param-followed-by-cast

Conversation

@c-tonneslan

Copy link
Copy Markdown

Closes #983.

sqlx.Named was bailing with `unexpected ':' while reading named param at N` on Postgres queries like:

```sql
SELECT :boundary::jsonb FROM ...
```

because the colon parser only knew about `::` as a way to escape a single colon — it had no story for the cast operator glued directly to the trailing edge of a named parameter.

When `:` lands while we've already built up a name, the new code emits the bindvar for that name and then peeks at the next byte. If it's another `:`, the whole `::` is treated as a Postgres cast and passed through to the rebound query. Otherwise the trailing `:` is written as literal text.

The bindvar-emission switch is now factored into an `emitName` helper so the new path and the existing end-of-name path stay in sync.

The `'a::b::c'` escape behavior inside SQL string literals is intentionally left alone — that case is ambiguous between Postgres cast and escaped colon and isn't what #983 is about.

Test plan

```
go test ./...
```

Added a case to TestCompileQuery covering `:boundary::jsonb, :id::text` across all bind types.

sqlx.Named was bailing with "unexpected ':' while reading named param"
on perfectly valid Postgres queries like:

    SELECT :boundary::jsonb FROM ...

because the colon parser only knew about :: as a way to escape a single
colon in a string literal — it had no story for the cast operator glued
directly to the trailing edge of a named parameter.

Closes jmoiron#983.

After this change, when ':' lands while we've already built up a name,
we emit the bindvar for the name and then look at the next byte. If
it's another ':' we treat the whole '::' as a Postgres cast and pass
both bytes through to the rebound query (and skip the second one).
Otherwise the trailing ':' is written as literal text.

Also pulled the bindvar-emission switch into an emitName helper so the
new path and the existing end-of-name path don't drift.

The 'a::b::c' escape behavior inside SQL string literals is unchanged —
that case is still ambiguous between Postgres cast and escaped colon
and isn't what jmoiron#983 is about.

Signed-off-by: Charlie Tonneslan <cst0520@gmail.com>
@mrj0

mrj0 commented May 26, 2026

Copy link
Copy Markdown

fyi I have a fix here https://github.com/Vinovest/sqlx/blob/main/bind.go#L191 using sqltokenizer that fixes this and other problems with the parser

@itxaiohanglover itxaiohanglover left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clean approach — extracting emitName avoids the duplication and the test cases cover the main scenarios. The sqltokenizer route would be more invasive for what's ultimately a 2-character lookahead. Any chance this could get reviewed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants