From 73f02447880527477e80c87f6c6ec678949bc882 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Wed, 18 Feb 2026 20:24:45 -0800 Subject: [PATCH 1/2] fix: preserve typmod for extension types in column type resolution (#295) pgvector types with dimensions (e.g., vector(384), halfvec(384)) were losing their typmod during schema inspection. The resolved_type query only returned dt.typname for non-pg_catalog base types, dropping the typmod. Now extracts the typmod from format_type() and appends it. Co-Authored-By: Claude Opus 4.6 --- ir/queries/queries.sql | 14 ++++++++++---- ir/queries/queries.sql.go | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index a8433f24..822df23f 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -88,10 +88,13 @@ WITH column_base AS ( END WHEN dt.typtype = 'b' THEN -- Non-array base types: qualify if not in pg_catalog or table's schema + -- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector) CASE WHEN dn.nspname = 'pg_catalog' THEN c.udt_name - WHEN dn.nspname = c.table_schema THEN dt.typname - ELSE dn.nspname || '.' || dt.typname + WHEN dn.nspname = c.table_schema THEN + dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') + ELSE + dn.nspname || '.' || dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') END ELSE c.udt_name END AS resolved_type, @@ -201,10 +204,13 @@ WITH column_base AS ( END WHEN dt.typtype = 'b' THEN -- Non-array base types: qualify if not in pg_catalog or table's schema + -- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector) CASE WHEN dn.nspname = 'pg_catalog' THEN c.udt_name - WHEN dn.nspname = c.table_schema THEN dt.typname - ELSE dn.nspname || '.' || dt.typname + WHEN dn.nspname = c.table_schema THEN + dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') + ELSE + dn.nspname || '.' || dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') END ELSE c.udt_name END AS resolved_type, diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 67e4d5fa..acd44bfe 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -287,10 +287,13 @@ WITH column_base AS ( END WHEN dt.typtype = 'b' THEN -- Non-array base types: qualify if not in pg_catalog or table's schema + -- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector) CASE WHEN dn.nspname = 'pg_catalog' THEN c.udt_name - WHEN dn.nspname = c.table_schema THEN dt.typname - ELSE dn.nspname || '.' || dt.typname + WHEN dn.nspname = c.table_schema THEN + dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') + ELSE + dn.nspname || '.' || dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') END ELSE c.udt_name END AS resolved_type, @@ -472,10 +475,13 @@ WITH column_base AS ( END WHEN dt.typtype = 'b' THEN -- Non-array base types: qualify if not in pg_catalog or table's schema + -- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector) CASE WHEN dn.nspname = 'pg_catalog' THEN c.udt_name - WHEN dn.nspname = c.table_schema THEN dt.typname - ELSE dn.nspname || '.' || dt.typname + WHEN dn.nspname = c.table_schema THEN + dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') + ELSE + dn.nspname || '.' || dt.typname || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') END ELSE c.udt_name END AS resolved_type, From aab0e2421b42bdc3a81107b89e827e704e2ed326 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Wed, 18 Feb 2026 20:42:00 -0800 Subject: [PATCH 2/2] test: add pgvector typmod test case for issue #295 Add diff test case with setup.sql, old.sql, new.sql, and expected outputs for pgvector halfvec(384) column with HNSW index. The test is automatically skipped in CI since embedded-postgres doesn't include pgvector. Add skipListRequiresExtension mechanism for tests requiring third-party extensions. Co-Authored-By: Claude Opus 4.6 --- .../issue_295_pgvector_typmod/diff.sql | 2 + .../issue_295_pgvector_typmod/new.sql | 9 ++++ .../issue_295_pgvector_typmod/old.sql | 4 ++ .../issue_295_pgvector_typmod/plan.json | 44 +++++++++++++++++++ .../issue_295_pgvector_typmod/plan.sql | 15 +++++++ .../issue_295_pgvector_typmod/plan.txt | 28 ++++++++++++ .../issue_295_pgvector_typmod/setup.sql | 4 ++ testutil/skip_list.go | 16 +++++++ 8 files changed, 122 insertions(+) create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/diff.sql create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/new.sql create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/old.sql create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/plan.json create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/plan.sql create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/plan.txt create mode 100644 testdata/diff/create_table/issue_295_pgvector_typmod/setup.sql diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/diff.sql b/testdata/diff/create_table/issue_295_pgvector_typmod/diff.sql new file mode 100644 index 00000000..15f79733 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/diff.sql @@ -0,0 +1,2 @@ +ALTER TABLE activity ADD COLUMN embedding halfvec(384); +CREATE INDEX IF NOT EXISTS activity_embedding_idx ON activity USING hnsw (embedding halfvec_cosine_ops); diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/new.sql b/testdata/diff/create_table/issue_295_pgvector_typmod/new.sql new file mode 100644 index 00000000..3724b290 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/new.sql @@ -0,0 +1,9 @@ +-- Desired state: add pgvector columns with dimensions (typmod) +-- Reproduces GitHub issue #295 where vector(384)/halfvec(384) dimensions were dropped +CREATE TABLE public.activity ( + id bigserial PRIMARY KEY, + embedding halfvec(384) +); + +CREATE INDEX activity_embedding_idx + ON activity USING hnsw (embedding halfvec_cosine_ops); diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/old.sql b/testdata/diff/create_table/issue_295_pgvector_typmod/old.sql new file mode 100644 index 00000000..869d853c --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/old.sql @@ -0,0 +1,4 @@ +-- Initial state: table without vector columns +CREATE TABLE public.activity ( + id bigserial PRIMARY KEY +); diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/plan.json b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.json new file mode 100644 index 00000000..d6330d56 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.json @@ -0,0 +1,44 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.7.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER TABLE activity ADD COLUMN embedding halfvec(384);", + "type": "table.column", + "operation": "create", + "path": "public.activity.embedding" + } + ] + }, + { + "steps": [ + { + "sql": "CREATE INDEX CONCURRENTLY IF NOT EXISTS activity_embedding_idx ON activity USING hnsw (embedding halfvec_cosine_ops);", + "type": "table.index", + "operation": "create", + "path": "public.activity.activity_embedding_idx" + } + ] + }, + { + "steps": [ + { + "sql": "SELECT \n COALESCE(i.indisvalid, false) as done,\n CASE \n WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total\n ELSE 0\n END as progress\nFROM pg_class c\nLEFT JOIN pg_index i ON c.oid = i.indexrelid\nLEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid\nWHERE c.relname = 'activity_embedding_idx';", + "directive": { + "type": "wait", + "message": "Creating index activity_embedding_idx" + }, + "type": "table.index", + "operation": "create", + "path": "public.activity.activity_embedding_idx" + } + ] + } + ] +} diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/plan.sql b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.sql new file mode 100644 index 00000000..6fe86066 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.sql @@ -0,0 +1,15 @@ +ALTER TABLE activity ADD COLUMN embedding halfvec(384); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS activity_embedding_idx ON activity USING hnsw (embedding halfvec_cosine_ops); + +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 'activity_embedding_idx'; diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/plan.txt b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.txt new file mode 100644 index 00000000..99553987 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/plan.txt @@ -0,0 +1,28 @@ +Plan: 1 to modify. + +Summary by type: + tables: 1 to modify + +Tables: + ~ activity + + embedding (column) + + activity_embedding_idx (index) + +DDL to be executed: +-------------------------------------------------- + +ALTER TABLE activity ADD COLUMN embedding halfvec(384); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS activity_embedding_idx ON activity USING hnsw (embedding halfvec_cosine_ops); + +-- pgschema:wait +SELECT + COALESCE(i.indisvalid, false) as done, + CASE + WHEN p.blocks_total > 0 THEN p.blocks_done * 100 / p.blocks_total + ELSE 0 + END as progress +FROM pg_class c +LEFT JOIN pg_index i ON c.oid = i.indexrelid +LEFT JOIN pg_stat_progress_create_index p ON c.oid = p.index_relid +WHERE c.relname = 'activity_embedding_idx'; diff --git a/testdata/diff/create_table/issue_295_pgvector_typmod/setup.sql b/testdata/diff/create_table/issue_295_pgvector_typmod/setup.sql new file mode 100644 index 00000000..50453440 --- /dev/null +++ b/testdata/diff/create_table/issue_295_pgvector_typmod/setup.sql @@ -0,0 +1,4 @@ +-- Setup: Requires pgvector extension +-- This test is skipped in CI (embedded-postgres doesn't include pgvector). +-- To run manually, install pgvector and remove from skipListRequiresExtension. +CREATE EXTENSION IF NOT EXISTS vector; diff --git a/testutil/skip_list.go b/testutil/skip_list.go index fb1a4d0d..e1181826 100644 --- a/testutil/skip_list.go +++ b/testutil/skip_list.go @@ -51,6 +51,14 @@ var skipListPG14_15 = []string{ "TestIncludeIntegration", } +// skipListRequiresExtension defines test cases that require third-party extensions +// not available in embedded-postgres (e.g., pgvector, PostGIS). +// These tests are skipped on all PostgreSQL versions in CI but can be run manually +// against a database with the required extensions installed. +var skipListRequiresExtension = []string{ + "create_table/issue_295_pgvector_typmod", +} + // skipListForVersion maps PostgreSQL major versions to their skip lists. var skipListForVersion = map[int][]string{ 14: skipListPG14_15, @@ -70,6 +78,14 @@ var skipListForVersion = map[int][]string{ func ShouldSkipTest(t *testing.T, testName string, majorVersion int) { t.Helper() + // Check extension-required skip list (applies to all versions) + for _, pattern := range skipListRequiresExtension { + patternNormalized := strings.ReplaceAll(pattern, "/", "_") + if testName == patternNormalized || testName == pattern { + t.Skipf("Skipping test %q: requires third-party extension not available in embedded-postgres", testName) + } + } + // Get skip patterns for this version skipPatterns, exists := skipListForVersion[majorVersion] if !exists {