Skip to content

fix(xpq): render an empty BitsValue as NULL, not B'' - #164

Open
loganintech wants to merge 1 commit into
mainfrom
logan/bits-value-empty-renders-null
Open

fix(xpq): render an empty BitsValue as NULL, not B''#164
loganintech wants to merge 1 commit into
mainfrom
logan/bits-value-empty-renders-null

Conversation

@loganintech

Copy link
Copy Markdown
Contributor

What

BitsValue.Value() hardcoded Valid: true, so pgtype.Bits.Value() could never reach its if !src.Valid { return nil, nil } early return. An empty input encoded to the empty string and arrived at Postgres as a zero-length bit.

 	bits := &pgtype.Bits{
 		Bytes: b,
-		Valid: true,
+		Valid: len(b) > 0,
 		Len:   bitsLen,
 	}

Why it matters

BytesToBitVector has three callers:

  • proto_format_bits.tmpl — guarded, only invoked when the value is exactly ExpectedBytesLen. Unaffected.
  • query_builder.tmpl:146 and :364 — the generated Distance() helper, which calls it unconditionally with no length check.

So a nil or empty []byte passed to Distance() renders as B''. Against a sized bit(N) column, pgvector's distance operators reject a zero-length operand:

ERROR: different bit lengths 4096 and 0 (SQLSTATE 22000)

Emitting NULL instead lets the comparison propagate NULL, which the COALESCE the generated query already wraps around the distance expression absorbs correctly.

Found while investigating a ConductorOne sandbox incident. That incident turned out to have a different root cause (a malformed entry in an HNSW index aborting autovacuum), but this is a real, independently reachable bug on the read path with the same signature.

Why here and not in the template

This is a runtime fix, so a version bump repairs every already-generated .pb.pgdb.go without regeneration. A guard in query_builder.tmpl would only take effect after every consumer regenerates.

Test

  • New pgdb/v1/xpq/bits_value_test.go covering nil, zero-length, and populated inputs, plus a case pinning that an empty operand does not render as B''.
  • go build ./..., gofmt clean, go test ./pgdb/v1/xpq/ passes.
  • Pre-existing unrelated failures in internal/pgtest and pgdb/v1 (TestSearchPathsFull) — both did not find PostgreSQL executables installed; verified they fail identically on main.

🤖 Generated with Claude Code

BitsValue.Value() hardcoded Valid: true, so pgtype.Bits.Value() could never
take its `if !src.Valid { return nil, nil }` path. An empty input therefore
encoded to the empty string and reached Postgres as a zero-length bit.

That is only reachable through the generated Distance() helper, which calls
BytesToBitVector unconditionally with no length check. Against a sized bit
column, a zero-length operand makes pgvector's distance operators raise
"different bit lengths N and 0" (SQLSTATE 22000). Record() is unaffected --
it only calls BytesToBitVector when the value is exactly ExpectedBytesLen.

Emitting NULL instead lets the comparison propagate NULL, which the
generated COALESCE around the distance expression already absorbs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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