Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/dump/dump_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ func TestDumpCommand_Issue191FunctionProcedureOverload(t *testing.T) {
runExactMatchTest(t, "issue_191_function_procedure_overload")
}

func TestDumpCommand_Issue420VarcharArrayLengthModifier(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
runExactMatchTest(t, "issue_420_varchar_array_length_modifier")
}

// Reproduces a bug where a column declared as `name` is dumped as `char[]`.
// The inspector classifies any base type with pg_type.typelem <> 0 as an array,
// but the `name` type has typelem = 18 (the OID of "char") despite not being an array.
Expand Down
11 changes: 8 additions & 3 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -1135,9 +1135,15 @@ func normalizePostgreSQLType(input string) string {
// Handle direct type names
typeName := input

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

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

return -1 // Not found
}

18 changes: 10 additions & 8 deletions ir/queries/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ WITH column_base AS (
-- Array types: apply same schema qualification logic to element type
-- Use typcategory = 'A' rather than typelem <> 0; the latter is true
-- for non-array fixed-length types like name (typelem points to char).
-- Use format_type to preserve typmod for element types (e.g., varchar(128)[] for character varying(128)[])
CASE
WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]'
WHEN en.nspname = c.table_schema THEN et.typname || '[]'
ELSE en.nspname || '.' || et.typname || '[]'
END
WHEN en.nspname = 'pg_catalog' THEN et.typname
WHEN en.nspname = c.table_schema THEN et.typname
ELSE en.nspname || '.' || et.typname
END || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') || '[]'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve interval fields When an array column uses an interval field range, such as INTERVAL DAY TO SECOND(3)[], format_type(a.atttypid, a.atttypmod) includes both the fields and the precision. This substring only keeps the parenthesized precision, so the dumped type becomes interval(3)[] and drops DAY TO SECOND. That changes the column type semantics in the dump.

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)
Expand Down Expand Up @@ -203,11 +204,12 @@ WITH column_base AS (
-- Array types: apply same schema qualification logic to element type
-- Use typcategory = 'A' rather than typelem <> 0; the latter is true
-- for non-array fixed-length types like name (typelem points to char).
-- Use format_type to preserve typmod for element types (e.g., varchar(128)[] for character varying(128)[])
CASE
WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]'
WHEN en.nspname = c.table_schema THEN et.typname || '[]'
ELSE en.nspname || '.' || et.typname || '[]'
END
WHEN en.nspname = 'pg_catalog' THEN et.typname
WHEN en.nspname = c.table_schema THEN et.typname
ELSE en.nspname || '.' || et.typname
END || COALESCE(substring(format_type(a.atttypid, a.atttypmod) FROM '\([^)]*\)'), '') || '[]'
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)
Expand Down
18 changes: 10 additions & 8 deletions ir/queries/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "issue_420_varchar_array_length_modifier",
"description": "pgschema dump silently drops the length modifier from varchar(n)[] array columns, emitting varchar[] instead of varchar(128)[]",
"source": "https://github.com/pgplex/pgschema/issues/420",
"notes": [
"The SQL query for array types used et.typname || '[]' which discards the atttypmod",
"The fix uses format_type(a.atttypid, a.atttypmod) to extract the modifier (e.g., (128))",
"Also covers character(n)[] which had the same bug (bpchar typname)"
]
}
33 changes: 33 additions & 0 deletions testdata/dump/issue_420_varchar_array_length_modifier/pgdump.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--
-- PostgreSQL database dump
--

SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: items; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.items (
id integer NOT NULL,
name character varying(64),
tags character varying(128)[],
codes character(10)[]
);

--
-- Name: items items_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);

--
-- PostgreSQL database dump complete
--
20 changes: 20 additions & 0 deletions testdata/dump/issue_420_varchar_array_length_modifier/pgschema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--
-- pgschema database dump
--

-- Dumped from database version PostgreSQL 18.0
-- Dumped by pgschema version 1.9.0


--
-- Name: items; Type: TABLE; Schema: -; Owner: -
--

CREATE TABLE IF NOT EXISTS items (
id integer,
name varchar(64),
tags varchar(128)[],
codes character(10)[],
CONSTRAINT items_pkey PRIMARY KEY (id)
);

13 changes: 13 additions & 0 deletions testdata/dump/issue_420_varchar_array_length_modifier/raw.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--
-- Test case for GitHub issue #420: varchar(n)[] length modifier silently dropped in dump
--
-- The length modifier is lost when dumping array columns of character types
-- with a length constraint: varchar(128)[] is emitted as varchar[].
--

CREATE TABLE items (
id integer PRIMARY KEY,
name varchar(64),
tags varchar(128)[],
codes character(10)[]
);