Let a null carry no type so postgres infers it #148

Merged
PerishFire merged 2 commits from pg-null-type into main 2026-07-30 13:47:24 +00:00
Owner

wire: let a null carry no type so postgres infers it

Val::Null bound None::<i64>, so sqlx declared that parameter int8
in the Parse message. Postgres caches the prepared statement with those
types, so the first row to leave a column null fixed it as int8 for
every later execution — and the next row that supplied text sent four
bytes where eight were declared:

INSERT INTO "@unit_@field" (...) VALUES ($1..$14)
ERROR:  insufficient data left in message
CONTEXT:  unnamed portal parameter $6

A null now binds a Void that produces OID zero, which leaves the type
unspecified and lets Postgres infer it from the target column. The first
prepare then reads the type off the schema rather than off whichever row
happened to arrive first.

Sqlite is untouched: it declares no parameter types, so its null binding
was never able to poison a column.

The binding is as old as the adapter. What changed is that 0.10 writes
the manifest as rows, so @field.serial — null for most fields, text for
a scoped serial — puts the poisoning sequence directly in the boot path.
Two tests pin both halves: scoped covers bootstrap with a serial field,
spare covers an ordinary model whose optional field is absent in the
first row and present in the second. Both failed before this.

release: advance keel to 0.10.1

The pg null binding fix, with its changelog in en and zh. Nothing is asked
of a caller: the public surface is unchanged and the estate format stays
at eleven.

wire: let a null carry no type so postgres infers it `Val::Null` bound `None::<i64>`, so sqlx declared that parameter `int8` in the Parse message. Postgres caches the prepared statement with those types, so the first row to leave a column null fixed it as `int8` for every later execution — and the next row that supplied text sent four bytes where eight were declared: INSERT INTO "@unit_@field" (...) VALUES ($1..$14) ERROR: insufficient data left in message CONTEXT: unnamed portal parameter $6 A null now binds a `Void` that produces OID zero, which leaves the type unspecified and lets Postgres infer it from the target column. The first prepare then reads the type off the schema rather than off whichever row happened to arrive first. Sqlite is untouched: it declares no parameter types, so its null binding was never able to poison a column. The binding is as old as the adapter. What changed is that 0.10 writes the manifest as rows, so `@field.serial` — null for most fields, text for a scoped serial — puts the poisoning sequence directly in the boot path. Two tests pin both halves: `scoped` covers bootstrap with a serial field, `spare` covers an ordinary model whose optional field is absent in the first row and present in the second. Both failed before this. release: advance keel to 0.10.1 The pg null binding fix, with its changelog in en and zh. Nothing is asked of a caller: the public surface is unchanged and the estate format stays at eleven.
`Val::Null` bound `None::<i64>`, so sqlx declared that parameter `int8`
in the Parse message. Postgres caches the prepared statement with those
types, so the first row to leave a column null fixed it as `int8` for
every later execution — and the next row that supplied text sent four
bytes where eight were declared:

    INSERT INTO "@unit_@field" (...) VALUES ($1..$14)
    ERROR:  insufficient data left in message
    CONTEXT:  unnamed portal parameter $6

A null now binds a `Void` that produces OID zero, which leaves the type
unspecified and lets Postgres infer it from the target column. The first
prepare then reads the type off the schema rather than off whichever row
happened to arrive first.

Sqlite is untouched: it declares no parameter types, so its null binding
was never able to poison a column.

The binding is as old as the adapter. What changed is that 0.10 writes
the manifest as rows, so `@field.serial` — null for most fields, text for
a scoped serial — puts the poisoning sequence directly in the boot path.
Two tests pin both halves: `scoped` covers bootstrap with a serial field,
`spare` covers an ordinary model whose optional field is absent in the
first row and present in the second. Both failed before this.
release: advance keel to 0.10.1
All checks were successful
guard / guard (pull_request) Successful in 1m42s
1f3e3059f9
The pg null binding fix, with its changelog in en and zh. Nothing is asked
of a caller: the public surface is unchanged and the estate format stays
at eleven.
PerishFire deleted branch pg-null-type 2026-07-30 13:47:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
PerishLab/keel!148
No description provided.