Skip to content

PostgreSQL String bind to timestamptz sends invalid binary parameter data #175

Description

@larimonious

Bug description

Binding an NTNT String to a PostgreSQL parameter whose inferred type is timestamptz sends UTF-8 text bytes using the TIMESTAMPTZ binary type OID. PostgreSQL rejects the bind before executing the query:

ERROR: incorrect binary data format in bind parameter 1
CONTEXT: unnamed portal parameter $1

This was observed in NTNT 0.5.2 at source revision 04db475d339851970d2c872c333021a4b10d11f3. The current main implementation at 79c61dd98b0f10e3f6c1bce4f1d6e4df2343a21f appears to retain the same behavior.

Minimal reproduction

With a reachable PostgreSQL database:

import { connect, query_one } from "std/db/postgres"

let db = unwrap(connect("postgres://user:password@localhost/example"))
let row = unwrap(query_one(
    db,
    "SELECT $1::timestamptz::text AS value",
    ["2026-07-29T12:54:21.000000Z"]
))
print(row)

Actual behavior

query_one returns an error originating from PostgreSQL:

incorrect binary data format in bind parameter 1

A production occurrence showed the same failure with a cursor predicate:

AND (
  $3::timestamptz IS NULL
  OR (received_at, probe_run_id) > ($3::timestamptz, $4::uuid)
)

The UUID string parameter was coerced correctly; the timestamptz string parameter was not.

Expected behavior

Preferably, NTNT should parse a String into the target PostgreSQL timestamp type and encode the correct binary representation, consistent with the existing string coercions for integer, float, boolean, JSON, and UUID parameters.

If timestamp coercion is intentionally unsupported, the client should reject the bind with a clear NTNT error rather than sending malformed binary data to PostgreSQL.

Confirmed workaround

Force PostgreSQL to infer the parameter as text, then cast the resulting text value:

SELECT $1::text::timestamptz

For optional cursor parameters, this works:

NULLIF($1::text, '')::timestamptz

The same live query that failed with $1::timestamptz succeeded after changing it to $1::text::timestamptz.

Likely source

In src/stdlib/postgres.rs, SqlParam::String handles several target types explicitly, then falls through to:

_ => v.to_sql(ty, out),

SqlParam::accepts returns true for every type. For TIMESTAMPTZ, the fallback appears to write string bytes while the prepared parameter still carries the timestamp binary OID.

A likely fix is to add explicit coercion for DATE, TIME, TIMESTAMP, and TIMESTAMPTZ using the corresponding chrono types, plus PostgreSQL integration tests for valid and invalid strings.

Environment

  • NTNT: 0.5.2
  • NTNT source revision: 04db475d339851970d2c872c333021a4b10d11f3
  • PostgreSQL: 18
  • Runtime: Debian Bookworm container on Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions