Skip to content

Commit 042ddec

Browse files
fix: preserve length modifier in varchar(n)[] array columns (#420) (#438)
Co-authored-by: vytautas.karpavicius <vytautas.karpavicius@cloudkitchens.com>
1 parent 8dc3100 commit 042ddec

8 files changed

Lines changed: 111 additions & 19 deletions

File tree

cmd/dump/dump_integration_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ func TestDumpCommand_Issue191FunctionProcedureOverload(t *testing.T) {
151151
runExactMatchTest(t, "issue_191_function_procedure_overload")
152152
}
153153

154+
func TestDumpCommand_Issue420VarcharArrayLengthModifier(t *testing.T) {
155+
if testing.Short() {
156+
t.Skip("Skipping integration test in short mode")
157+
}
158+
runExactMatchTest(t, "issue_420_varchar_array_length_modifier")
159+
}
160+
154161
// Reproduces a bug where a column declared as `name` is dumped as `char[]`.
155162
// The inspector classifies any base type with pg_type.typelem <> 0 as an array,
156163
// but the `name` type has typelem = 18 (the OID of "char") despite not being an array.

ir/normalize.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,9 +1135,15 @@ func normalizePostgreSQLType(input string) string {
11351135
// Handle direct type names
11361136
typeName := input
11371137

1138+
// Decompose into base name and optional modifier+suffix (e.g., "bpchar(10)[]" -> "bpchar" + "(10)[]").
1139+
// When there's no modifier, rest is empty and this is equivalent to a plain exact-match lookup.
1140+
baseName, rest := typeName, ""
1141+
if idx := strings.Index(typeName, "("); idx != -1 {
1142+
baseName, rest = typeName[:idx], typeName[idx:]
1143+
}
11381144
// Check if we have a direct mapping
1139-
if normalized, exists := postgresTypeNormalization[typeName]; exists {
1140-
return normalized
1145+
if normalized, exists := postgresTypeNormalization[baseName]; exists {
1146+
return normalized + rest
11411147
}
11421148

11431149
// Remove pg_catalog prefix for unmapped types
@@ -1480,4 +1486,3 @@ func findArrayClose(expr string, startIdx int) int {
14801486

14811487
return -1 // Not found
14821488
}
1483-

ir/queries/queries.sql

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ WITH column_base AS (
8585
-- Array types: apply same schema qualification logic to element type
8686
-- Use typcategory = 'A' rather than typelem <> 0; the latter is true
8787
-- for non-array fixed-length types like name (typelem points to char).
88+
-- Use format_type to preserve typmod for element types (e.g., varchar(128)[] for character varying(128)[])
8889
CASE
89-
WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]'
90-
WHEN en.nspname = c.table_schema THEN et.typname || '[]'
91-
ELSE en.nspname || '.' || et.typname || '[]'
92-
END
90+
WHEN en.nspname = 'pg_catalog' THEN et.typname
91+
WHEN en.nspname = c.table_schema THEN et.typname
92+
ELSE en.nspname || '.' || et.typname
93+
END || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') || '[]'
9394
WHEN dt.typtype = 'b' THEN
9495
-- Non-array base types: qualify if not in pg_catalog or table's schema
9596
-- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector)
@@ -203,11 +204,12 @@ WITH column_base AS (
203204
-- Array types: apply same schema qualification logic to element type
204205
-- Use typcategory = 'A' rather than typelem <> 0; the latter is true
205206
-- for non-array fixed-length types like name (typelem points to char).
207+
-- Use format_type to preserve typmod for element types (e.g., varchar(128)[] for character varying(128)[])
206208
CASE
207-
WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]'
208-
WHEN en.nspname = c.table_schema THEN et.typname || '[]'
209-
ELSE en.nspname || '.' || et.typname || '[]'
210-
END
209+
WHEN en.nspname = 'pg_catalog' THEN et.typname
210+
WHEN en.nspname = c.table_schema THEN et.typname
211+
ELSE en.nspname || '.' || et.typname
212+
END || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') || '[]'
211213
WHEN dt.typtype = 'b' THEN
212214
-- Non-array base types: qualify if not in pg_catalog or table's schema
213215
-- Use format_type to preserve typmod for extension types (e.g., vector(384) for pgvector)

ir/queries/queries.sql.go

Lines changed: 10 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "issue_420_varchar_array_length_modifier",
3+
"description": "pgschema dump silently drops the length modifier from varchar(n)[] array columns, emitting varchar[] instead of varchar(128)[]",
4+
"source": "https://github.com/pgplex/pgschema/issues/420",
5+
"notes": [
6+
"The SQL query for array types used et.typname || '[]' which discards the atttypmod",
7+
"The fix uses format_type(a.atttypid, a.atttypmod) to extract the modifier (e.g., (128))",
8+
"Also covers character(n)[] which had the same bug (bpchar typname)"
9+
]
10+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--
2+
-- PostgreSQL database dump
3+
--
4+
5+
SET statement_timeout = 0;
6+
SET lock_timeout = 0;
7+
SET client_encoding = 'UTF8';
8+
SET standard_conforming_strings = on;
9+
SET check_function_bodies = false;
10+
SET client_min_messages = warning;
11+
SET row_security = off;
12+
13+
--
14+
-- Name: items; Type: TABLE; Schema: public; Owner: -
15+
--
16+
17+
CREATE TABLE public.items (
18+
id integer NOT NULL,
19+
name character varying(64),
20+
tags character varying(128)[],
21+
codes character(10)[]
22+
);
23+
24+
--
25+
-- Name: items items_pkey; Type: CONSTRAINT; Schema: public; Owner: -
26+
--
27+
28+
ALTER TABLE ONLY public.items
29+
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
30+
31+
--
32+
-- PostgreSQL database dump complete
33+
--
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--
2+
-- pgschema database dump
3+
--
4+
5+
-- Dumped from database version PostgreSQL 18.0
6+
-- Dumped by pgschema version 1.9.0
7+
8+
9+
--
10+
-- Name: items; Type: TABLE; Schema: -; Owner: -
11+
--
12+
13+
CREATE TABLE IF NOT EXISTS items (
14+
id integer,
15+
name varchar(64),
16+
tags varchar(128)[],
17+
codes character(10)[],
18+
CONSTRAINT items_pkey PRIMARY KEY (id)
19+
);
20+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--
2+
-- Test case for GitHub issue #420: varchar(n)[] length modifier silently dropped in dump
3+
--
4+
-- The length modifier is lost when dumping array columns of character types
5+
-- with a length constraint: varchar(128)[] is emitted as varchar[].
6+
--
7+
8+
CREATE TABLE items (
9+
id integer PRIMARY KEY,
10+
name varchar(64),
11+
tags varchar(128)[],
12+
codes character(10)[]
13+
);

0 commit comments

Comments
 (0)