feat(postgres): support pgvector columns (vector, halfvec, sparsevec)#450
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (8 files)
Previous Review Summary (commit 5030586)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 5030586)Status: No Issues Found | Recommendation: Merge Files Reviewed (7 files)
Reviewed by step-3.7-flash · Input: 84.5K · Output: 36.4K · Cached: 1M |
NewtTheWolf
left a comment
There was a problem hiding this comment.
Really nice work — the *_send binary decoders are careful (the f16 zero/subnormal/inf/nan branches and the 1-based sparsevec index printing are all correct and well-tested), and inlining behind a strict character allow-list is the right way to handle the missing text -> vector cast. 🙏
One blocking issue inline: the write path never reaches bind_pg_vector_string, so editing a vector value doesn't actually work yet (reading does). Details + fix inline.
Non-blocking:
- Plain views still show
USER-DEFINED:get_view_columns(mod.rs:1112) selects barec.data_type, so avectorcolumn in a regular view won't get the CASE fallback. Materialized views are fine (they useformat_type). Consistency nit. - Cross-PR heads-up: #427 (hstore) also branches on
data_type == "USER-DEFINED"; once this CASE lands that value becomes"hstore", so whichever of the two merges second will need to reconcile that seam. Flagging since they touch the same spot.
Also: the branch currently has a merge conflict against main (mergeStateStatus: DIRTY) — could you rebase/resolve that when you get a chance? 🙏
pgvector columns showed up as USER-DEFINED with null values because the type is an extension base type the Postgres driver did not recognize on either the value or metadata path. - extract: decode the vector/halfvec/sparsevec binary send formats to their canonical text representation instead of falling through to null - metadata: fall back to udt_name in get_columns/get_all_columns_batch so columns report "vector" instead of "USER-DEFINED" - binding: inline vector literals as '<value>'::vector on write, since pgvector registers no text->vector cast for a bound parameter (value is validated against a strict character allow-list first) - frontend: treat vector columns as long-text targets so cells render a readable, expandable preview (and a multiline editor) instead of a bare ellipsis Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review on TabularisDB#450: - get_column_data_type now applies the same USER-DEFINED -> udt_name fallback as get_columns, so update_record/insert_record resolve a pgvector column as its real type ("vector"/"halfvec"/"sparsevec") instead of the literal "USER-DEFINED". Without this the write path never reached bind_pg_vector_string and editing a vector value failed. - get_view_columns applies the same CASE fallback so vector columns in plain views report their real type instead of "USER-DEFINED". - Add round-trip binding tests through bind_pg_value covering vector, halfvec, sparsevec and the invalid-literal rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5030586 to
9e2ed25
Compare
NewtTheWolf
left a comment
There was a problem hiding this comment.
Re-reviewed after the latest push (9e2ed25). All blocking feedback is addressed:
- Write path now reaches the vector binding:
get_column_data_typeapplies theUSER-DEFINED→udt_namefallback (mod.rs:529), so update/insert bind typed vector literals. - Plain views resolve the real type via the CASE in
get_view_columns(mod.rs:1194). - Round-trip binding tests through
bind_pg_valueadded, including rejection of non-numeric/injection literals. - Cleanly rebased on current main.
Follow-up (non-blocking): #427 (hstore) also branches on data_type == 'USER-DEFINED' — whichever of the two merges second needs to reconcile that seam. LGTM.
Resolve tests.rs conflict: keep both new test modules (build_connection_url_tests from this PR, pg_vector_binding_tests from TabularisDB#450).
What
Columns created with the pgvector extension were unusable in Tabularis: they showed up with type
USER-DEFINEDand every value rendered asnull. This adds first-class support forvector,halfvecandsparsevecacross reading, metadata, writing and the data-grid UI.Why it was broken
pgvector types are extension base types with dynamic OIDs, so they matched none of the built-in
Type::*arms in the value extractor and fell through toJsonValue::Null. Separately,information_schema.columns.data_typereports any extension type as the literal string"USER-DEFINED"(the real name lives inudt_name), so the column header showed that instead ofvector.How
Backend (
src-tauri/src/drivers/postgres)extract/advanced_types.rs— decoders forvector,halfvecandsparsevecfollowing pgvector's*_sendbinary layouts (incl. an IEEE-754 half-precision decode forhalfvec). Values render as their canonical text form ([1,2,3],{1:1.5,3:2}/5), so a 1536-dim embedding round-trips exactly.extract/simple.rs— name-guarded match arms route the three types to the new decoders instead of thenullfallthrough. Arrays (vector[]) work automatically via the existing element dispatch.mod.rs—get_columns/get_all_columns_batchfall back toudt_name(CASE WHEN data_type = 'USER-DEFINED' THEN udt_name … END) so columns reportvector.binding.rs— on write, vector values are inlined as'<value>'::vector. pgvector registers notext -> vectorcast, so a bound TEXT parameter is rejected; the value must reach the type's input function as an unknown-typed literal. Input is validated against a strict numeric/bracket character allow-list before inlining.Frontend
src/utils/text.ts— newisVectorColumn();isLongTextCellTargetnow treats vector columns as long-text so the grid routes them to the readable, expandableTextCellpreview instead of dumping the raw string into a CSS-clipped<td>(which collapsed to a bare…).src/components/ui/FieldEditor.tsx— vector columns get the multiline text editor.Testing
extract/simple.rs(vector, empty vector, halfvec, sparsevec). Fulldrivers::postgressuite: 250 passed.isVectorColumn/ vectorisLongTextCellTargetcases intests/utils/text.test.ts(23 passed);tsc --noEmitand ESLint clean.CREATE EXTENSION vector— avector(1536)embedding column now shows typevectorand a readable, expandable preview instead ofnull.Notes
0.000019073486) rather than scientific notation (1.9073486e-05).Cargo.lockleft untouched.