Fix keyless-table numeric/temporal WHERE binding (port of tabularis#618) - #4
Merged
Merged
Conversation
debba flagged (PR #3, comment) that the builtin PostgreSQL driver had a bug fixed upstream in TabularisDB/tabularis#618: updating or deleting a row in a table with no primary key failed with "operator does not exist: numeric = text" (SQLSTATE 42883). Keyless tables identify rows by every column, so the WHERE predicate can target numeric/temporal columns — whose values arrive as JSON strings (numeric serializes as string to preserve arbitrary precision) — but bind_pk_value bound them as plain TEXT with no coercion. This plugin shares the exact same bug shape: bind_pg_string (used for SET binding) already had the numeric/temporal coercion cascade, but bind_pk_value (used for WHERE predicates via build_pk_map_predicate — the shared path for update_record, delete_record, save_blob_to_file, and fetch_blob_as_data_url) never routed through it. TDD, per the project's standing instruction to keep following it for parity fixes: wrote 4 tests mirroring #618's own upstream test cases (numeric cast, double precision cast, unparsable-numeric rejection, timestamp cast-through-TEXT) against bind_pk_value first, confirmed all 4 failed (RED — bound as plain "$N" with no CAST, exactly the missing coercion). Fixed by factoring the numeric/temporal coercion out of bind_pg_string into two shared functions (bind_pg_numeric_string / bind_pg_temporal_string, matching the builtin's own extracted-helper naming) and calling them from both bind_pg_string and bind_pk_value. Confirmed GREEN: 82/82 unit tests (78 previous + 4 new). Functionally verified against a live PostgreSQL instance (the local Podman pg-tabularis-test container), not just unit tests: created a keyless table with a numeric column, confirmed update_record failed with the exact 42883 error on the pre-fix binary (git stash to rebuild main's current state), then confirmed the identical request succeeds and persists correctly on the fixed binary. Repeated for delete_record and a keyless temporal-column table (timestamp). Re-ran tabularis's unmodified 82-test cross-repo parity suite against the rebuilt release binary afterward — still 82/82 GREEN, zero regression.
This was referenced Aug 11, 2026
aesslinger
added a commit
that referenced
this pull request
Aug 11, 2026
Version scheme reset from 0.1.0 to 1.0.0-beta.1. The actual target is 1.0.0 -- Phase 1 byte-for-byte parity is complete per tabularis PR #577's own "CP-4 Gate Met" status -- not an independent 0.x development line, so SemVer's own prerelease mechanism (1.0.0-beta.1 -> -beta.2 -> -rc.1 -> 1.0.0) expresses that relationship natively, which a bare 0.1.0 cannot. Applied retroactively at this point in history (pre-#4) so the v1.0.0-beta.1 tag/release's own manifest matches the tag name, rather than tagging a commit that still says "0.1.0" inside its .tabularium/Cargo.toml. Verified: .tabularium re-validated clean against the live registry schema via @tabularium/cli validate; cargo build/test (78/78)/clippy/fmt all pass; markdownlint clean.
aesslinger
added a commit
that referenced
this pull request
Aug 11, 2026
…18) (#4) debba flagged (PR #3, comment) that the builtin PostgreSQL driver had a bug fixed upstream in TabularisDB/tabularis#618: updating or deleting a row in a table with no primary key failed with "operator does not exist: numeric = text" (SQLSTATE 42883). Keyless tables identify rows by every column, so the WHERE predicate can target numeric/temporal columns — whose values arrive as JSON strings (numeric serializes as string to preserve arbitrary precision) — but bind_pk_value bound them as plain TEXT with no coercion. This plugin shares the exact same bug shape: bind_pg_string (used for SET binding) already had the numeric/temporal coercion cascade, but bind_pk_value (used for WHERE predicates via build_pk_map_predicate — the shared path for update_record, delete_record, save_blob_to_file, and fetch_blob_as_data_url) never routed through it. TDD, per the project's standing instruction to keep following it for parity fixes: wrote 4 tests mirroring #618's own upstream test cases (numeric cast, double precision cast, unparsable-numeric rejection, timestamp cast-through-TEXT) against bind_pk_value first, confirmed all 4 failed (RED — bound as plain "$N" with no CAST, exactly the missing coercion). Fixed by factoring the numeric/temporal coercion out of bind_pg_string into two shared functions (bind_pg_numeric_string / bind_pg_temporal_string, matching the builtin's own extracted-helper naming) and calling them from both bind_pg_string and bind_pk_value. Confirmed GREEN: 82/82 unit tests (78 previous + 4 new). Functionally verified against a live PostgreSQL instance (the local Podman pg-tabularis-test container), not just unit tests: created a keyless table with a numeric column, confirmed update_record failed with the exact 42883 error on the pre-fix binary (git stash to rebuild main's current state), then confirmed the identical request succeeds and persists correctly on the fixed binary. Repeated for delete_record and a keyless temporal-column table (timestamp). Re-ran tabularis's unmodified 82-test cross-repo parity suite against the rebuilt release binary afterward — still 82/82 GREEN, zero regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports a parity fix from the built-in PostgreSQL driver, flagged by @debba on PR #3: TabularisDB/tabularis#618 fixed a bug where updating or deleting a row in a table with no primary key failed with:
Keyless tables identify rows by every column, so the WHERE predicate can target numeric/temporal columns. Their values reach the driver as JSON strings (numeric serializes as string to preserve arbitrary precision), and
bind_pk_valuebound them as plainTEXT— PostgreSQL has no implicitnumeric = text(ortimestamp = text) operator and rejects the statement.This plugin has the exact same bug shape:
bind_pg_string(used for SET binding oninsert_record/update_record) already had the numeric/temporal coercion cascade, butbind_pk_value(used for WHERE predicates viabuild_pk_map_predicate— shared byupdate_record,delete_record,save_blob_to_file, andfetch_blob_as_data_url) never routed through it.Fix
Factored the numeric/temporal coercion out of
bind_pg_stringinto two shared functions —bind_pg_numeric_string/bind_pg_temporal_string, matching the builtin driver's own extracted-helper naming — and call them from bothbind_pg_stringandbind_pk_value. Unknown/non-numeric/non-temporal column types keep the existing shape heuristics unchanged.TDD — full red→green trail
Per this project's standing discipline for parity fixes, wrote the tests before touching
binding.rs:src/binding_tests.rs'sbind_pk_value_testsmodule, mirroring #618's own upstream test cases exactly:numeric_column_string_value_casts_to_numeric—"1500.00"against anumericcolumndouble_precision_column_string_value_casts_to_double—"1.5"against adouble precisioncolumnnumeric_column_unparsable_string_is_rejected—"abc"against anumericcolumn must error, not silently bind as texttimestamp_column_string_value_casts_through_text—"2024-05-01 10:30:00"against atimestampcolumncargo test bind_pk_value_testsbefore the fix:CAST, or (for the unparsable case) silently accepted instead of rejected.bind_pk_valuethrough them).cargo test bind_pk_value_testsafter:test result: ok. 10 passed; 0 failed. Full suite:cargo test --lib --bins→82 passed; 0 failed(78 previous + 4 new),cargo clippy --all-targets -- -D warningsclean,cargo fmt --all -- --checkclean, noCargo.lockdrift.Functional verification against a live PostgreSQL instance
Unit tests prove the binding logic in isolation; this proves the fix closes the actual reported bug end-to-end, including a genuine before/after reproduction:
pg-tabularis-testcontainer,postgres:16):git stash'd the fix to rebuild the binary atmain's current (pre-fix) state, then sent the exact request a realupdate_recordcall would make, identifying the row by itsamountcolumn (no PK exists):{"method":"update_record","params":{...},"table":"keyless_numeric_test","schema":"public","pk_map":{"amount":"1500.00"},"col_name":"label","new_val":"should-fail"}{"error":{"code":-32603,"message":"Prepare failed: db error"}}— confirmed viapsqlthat this is preciselynumeric = text(ranSELECT 1500.00::numeric = '1500.00'::textdirectly, got the identicaloperator does not existerror PostgreSQL raises).git stash pop'd the fix, rebuilt, and sent the identical request:{"result":1}— andSELECT * FROM keyless_numeric_testconfirmed the row was actually updated (labelchanged to the new value), not just a fake success response.delete_recordagainst the same table (amount = 250.50) — row correctly removed.timestampcolumn (CREATE TABLE keyless_temporal_test (imported_at timestamp, label text)), identifying the row byimported_at—update_recordsucceeded and the row was correctly updated.Cross-repo parity gate
This change touches a shared binding path used by
update_record,delete_record,save_blob_to_file, andfetch_blob_as_data_url— every method that identifies a row by primary key — so a regression here could be broad. Rebuilt the release binary and re-rantabularis's existing, unmodified 82-test parity suite against it viaPOSTGRES_PLUGIN_BIN(same command used for every prior fix in this migration): 82/82 GREEN, confirming zero regression.Test plan
cargo test --lib --bins: 82/82 unit tests pass (78 previous + 4 new)cargo clippy --all-targets -- -D warnings,cargo fmt --all -- --check: cleangit diff --exit-code Cargo.lock: no drift42883/"operator does not exist: numeric = text" failure), confirmed fixed post-fix, for bothupdate_recordanddelete_record, across numeric and temporal keyless-identifying columnstabularis's unmodified parity suite