Skip to content

Build a name bind as a NameData, not as text - #41

Open
sfc-gh-okalaci wants to merge 1 commit into
up/09-exact-number-stringsfrom
up/10-name-bind
Open

Build a name bind as a NameData, not as text#41
sfc-gh-okalaci wants to merge 1 commit into
up/09-exact-number-stringsfrom
up/10-name-bind

Conversation

@sfc-gh-okalaci

Copy link
Copy Markdown
Collaborator

Stacked on #40.

NAMEOID was handled in the same case group as TEXTOID, VARCHAROID and
BPCHAROID, all of which construct a varlena. name is not a varlena — it is a
fixed-length NameData of NAMEDATALEN bytes with no length word. Building one as text
writes a varlena header into the first bytes of the value, and anything comparing it
against a real name reads that header as characters.

Nothing raises, which is what makes it expensive to find. The value simply never
matches:

pljs.execute('SELECT nspname FROM pg_namespace WHERE nspname = $1', ['pg_catalog'])
// 0 rows

pljs.execute('SELECT $1::name::text AS t', ['hello_name'])[0].t
// '8'

Catalog introspection by name — nspname, relname, typname, attname — is how
most of it is written, so the blast radius is wider than the round trip suggests: any
such query silently returns nothing.

NAMEOID now gets its own case and goes through namein(), which lays the value out
correctly and applies the truncation rule at NAMEDATALEN - 1 instead of raising. The
QuickJS string is released on the error path as well, so a value namein() rejects does
not leak it.

Test plan

sql/pg_name_bind.sql covers a catalog lookup by name, the round trip, the empty name,
truncation of an over-long value, and returning a name from a function.

It discriminates: with the conversion reverted, both catalog lookups return 0 instead
of 1, and the round trip returns 8 and \x10 instead of the names. Full suite green
on PostgreSQL 17; builds clean on 16, 18 and 19beta3.

NAMEOID was handled in the same case group as TEXTOID, VARCHAROID and BPCHAROID,
all of which construct a varlena. `name` is not a varlena: it is a fixed-length
NameData of NAMEDATALEN bytes with no length word. Building one as text writes a
varlena header into the first bytes of the value, and anything that compares it
against a real name reads that header as characters.

Nothing raises, which is what makes it costly. The value simply never matches, so
a catalog lookup driven by a JavaScript string finds nothing:

    pljs.execute('SELECT nspname FROM pg_namespace WHERE nspname = $1',
                 ['pg_catalog'])        -- 0 rows

    pljs.execute('SELECT $1::name::text AS t', ['hello_name'])[0].t
                                        -- '8'

Catalog introspection by name -- nspname, relname, typname, attname -- is how most
of it is written, so this affects a lot more than the round trip.

NAMEOID now gets its own case and goes through namein(), which lays the value out
correctly and applies the truncation rule at NAMEDATALEN - 1 rather than raising.
The QuickJS string is released on the error path too, so a rejected value does not
leak it.

Adds sql/pg_name_bind.sql, covering a catalog lookup by name, the round trip, the
empty name, truncation of an over-long value, and returning a name from a function.
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