fix: add hstore support and decode arrays of custom-OID types - #71
Merged
Merged
Conversation
hstore has no well-known Postgres OID, so extract.rs's Type:: dispatch table never matched it and columns silently decoded as null (#68, #69). Dispatch on ty.name() == "hstore" instead, matching the builtin driver's extract/simple.rs pattern, and decode via tokio-postgres's native HashMap<String, Option<String>> support. Also ports the write side from tabularis#427: binding.rs rejected any JSON object bound to a non-JSON column, so editing an hstore cell failed outright. Resolves the real hstore OID per column via pg_type (extension types aren't well-known OIDs) and binds through the same HashMap shape.
Version suggestionBased on this PR's title (
This is informational only — no tag or release is created automatically yet. |
4 tasks
This was referenced Sep 14, 2026
extract.rs's dispatch only special-cased a handful of well-known array OIDs (int2/int4/int8/float4/float8/bool/text/varchar), so any array whose element type isn't one of those — enum[], hstore[], and in practice every other type without a hardcoded fast-path (numeric[], date[], json[], money[], inet[], etc.) — fell through to the generic string fallback and silently decoded to null (#72). Adds a generic 1-D array decoder (ArrayValue) that parses the array wire format directly and recurses per-element via extract_element_from_bytes, matching the builtin driver's generic Kind::Array dispatch (extract/mod.rs + extract/array.rs::try_extract_elem). Placed after the existing hardcoded array arms so their behavior is unchanged. Multi- dimensional arrays still fall back to null, consistent with this file's existing 1-D-only array handling. Verified against a live database that every previously-working scalar and array type is byte-identical to before, and that the newly-covered array types match the builtin driver's exact JSON shape for their scalar form. Filed #73 for an unrelated pre-existing bug found during this audit: hardcoded array types decode the whole column to null when any element is NULL (Vec<T> vs Vec<Option<T>>), unaffected by this fix.
rustls 0.23.43 incorrectly accepted TLS 1.3 handshake messages sent at the wrong encryption level when packed into the same record as a key-changing message (CVE-2025-61730) — fixed upstream in 0.23.45. CI's cargo-audit gate started failing on this PR once the advisory was published, unrelated to this branch's actual changes.
ArrayValue::from_sql called Vec::with_capacity(len) using the array header's claimed element count before validating the buffer actually contains that many elements. A truncated or corrupted array value can claim up to i32::MAX elements while carrying far fewer bytes, which would attempt a multi-gigabyte allocation on the first decode attempt, before the per-element truncation check ever runs. Vec::new() grows by amortized doubling as elements are actually read, so the allocation stays proportional to what's genuinely present in the buffer. Added a regression test with a claimed i32::MAX-element array and no element bytes following it.
aesslinger
added a commit
that referenced
this pull request
Sep 15, 2026
Ships PR #71 (hstore read/write support, plus decoding for arrays of any custom-OID element type — fixes #68, #69, #72), PR #75 (hardcoded array types no longer null out the whole array when one element is NULL — fixes #73), PR #76 (table/column comments exposed via get_tables/get_columns, closing #74), and PR #77 (SHOW/CALL with a limit param no longer produces a LIMIT syntax error, while SELECT/WITH/VALUES/TABLE/EXPLAIN keep real SQL-level pagination — fixes #70). Also carries the rustls 0.23.43 -> 0.23.45 security bump (RUSTSEC-2026-0285) merged as part of PR #71's branch. Verified: .tabularium re-validated as valid JSON with version matching the upcoming v1.0.0-rc.3 tag; cargo build --release/test (171 unit + 18 live-DB against the local pg-tabularis-test container)/clippy/fmt/ audit all pass; the release binary returns a valid initialize JSON-RPC response.
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
extract.rs'sType::dispatch table never matched it — columns silently decoded asnull([Bug]: HSTORE support in new postgreSQL plugin #68, Bug: HSTORE fields being shown as null even when containing data #69). Fixed by dispatching onty.name() == "hstore", matching the builtin driver'sextract/simple.rspattern, and decoding via tokio-postgres's nativeHashMap<String, Option<String>>support.tabularis#427(referenced from Phase 2.3: Extension-aware type system #27/Phase 2: PostgreSQL-specific features beyond the built-in driver (tabularis#16) #9):binding.rspreviously rejected any JSON object bound to a non-JSON column, so editing an hstore cell failed with "Cannot bind a JSON object to a non-JSON column". Now resolves the real hstore OID per column viapg_type(extension types aren't well-known OIDs) and binds through the sameHashMapshape tokio-postgres encodes natively.enum[],hstore[], and in practice every array type without a hardcoded fast-path (numeric[],date[],json[],money[],inet[], etc.) also silently decoded tonull(Arrays of custom-OID types (enum[], hstore[], etc.) decode to null #72). Fixed with a generic 1-D array decoder that recurses per-element, matching the builtin driver's genericKind::Arraydispatch. Existing hardcoded array fast-paths (int2/int4/int8/float4/float8/bool/text/varchar) are untouched.Vecusing the wire header's claimed element count, before validating the buffer actually contained that many elements — a truncated/corrupted array value could claim up toi32::MAXelements and trigger a multi-gigabyte allocation attempt. Fixed to grow incrementally instead, with a regression test.rustls0.23.43 → 0.23.45, fixing RUSTSEC-2026-0285 (CVE-2025-61730), a new advisory published while this PR was open that started failing CI's Security audit gate for every branch in the repo, including this one.Cargo.tomlalready allowed the patched version; the lockfile was just stale.Closes #68. Closes #69 (already closed as a duplicate of #68). Closes #72.
Also filed #73 for an unrelated pre-existing bug found while regression-testing this: the hardcoded array fast-paths decode the whole column to
nullwhen any element isNULL(Vec<T>vsVec<Option<T>>) — confirmed unchanged by this PR (same behavior onmain), left for separate follow-up.Test plan
cargo test --lib— 152 passing, including new unit tests for hstore decode/bind, the generic array decoder (hand-built wire bytes, following this repo's TDD-without-live-db convention), and the untrusted-length allocation-safety fixcargo clippy --all-targets -- -D warnings— cleancargo fmt --all— cleancargo audit --ignore RUSTSEC-2026-0235— clean (only the pre-existing, already-ignoredchacha20yanked-crate warning remains)execute_query_returns_a_real_hstore_value_not_null,execute_query_returns_real_array_values_for_custom_oid_element_typesinsert_record/update_recordwith hstore objects round-trip correctlymergeStateStatus: CLEAN, no conflicts withmain(branch is up to date withmain, which hasn't moved since this branch was cut)