From de8995e3f32203ff5a7b43846902143fdd136e62 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sun, 8 Mar 2026 06:38:44 -0700 Subject: [PATCH 1/2] fix: preserve array type casts in policy expressions (#345) The normalizeExpressionParentheses function was stripping ::text from array literal casts like '{nested,key}'::text[], leaving invalid SQL '{nested,key}'[]. Fix the regex to not match ::text when followed by []. Co-Authored-By: Claude Opus 4.6 --- cmd/dump/dump_integration_test.go | 7 +++ ir/normalize.go | 8 +++- .../dump/issue_345_array_cast/manifest.json | 6 +++ testdata/dump/issue_345_array_cast/pgdump.sql | 43 +++++++++++++++++++ .../dump/issue_345_array_cast/pgschema.sql | 30 +++++++++++++ testdata/dump/issue_345_array_cast/raw.sql | 13 ++++++ 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 testdata/dump/issue_345_array_cast/manifest.json create mode 100644 testdata/dump/issue_345_array_cast/pgdump.sql create mode 100644 testdata/dump/issue_345_array_cast/pgschema.sql create mode 100644 testdata/dump/issue_345_array_cast/raw.sql diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index db25b0e0..9517c9ff 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -123,6 +123,13 @@ func TestDumpCommand_Issue320PlpgsqlReservedKeywordType(t *testing.T) { runExactMatchTest(t, "issue_320_plpgsql_reserved_keyword_type") } +func TestDumpCommand_Issue345ArrayCast(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + runExactMatchTest(t, "issue_345_array_cast") +} + func TestDumpCommand_Issue318CrossSchemaComment(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") diff --git a/ir/normalize.go b/ir/normalize.go index b9eb9d7d..0f1dafff 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -175,6 +175,7 @@ func normalizeDefaultValue(value string, tableSchema string) string { // - '2024-01-01'::date (date literals need the cast in expressions) // // Pattern matches redundant text/varchar/char/json casts (including arrays) + // For column defaults, these casts are redundant because the column type provides context // Note: jsonb must come before json to avoid partial match // Note: (?:\[\])* handles multi-dimensional arrays like text[][] re = regexp.MustCompile(`('(?:[^']|'')*')::(text|character varying|character|bpchar|varchar|jsonb|json)(?:\[\])*`) @@ -743,8 +744,11 @@ func normalizeExpressionParentheses(expr string) string { // Step 3: Normalize redundant type casts in function arguments // Pattern: 'text'::text -> 'text' (removing redundant text cast from literals) - redundantTextCastRegex := regexp.MustCompile(`'([^']+)'::text`) - expr = redundantTextCastRegex.ReplaceAllString(expr, "'$1'") + // IMPORTANT: Do NOT match when followed by [] (array cast is semantically significant) + // e.g., '{nested,key}'::text[] must be preserved as-is + // Since Go regex doesn't support lookahead, we match a trailing non-[ char or end-of-string + redundantTextCastRegex := regexp.MustCompile(`'([^']+)'::text([^[\w]|$)`) + expr = redundantTextCastRegex.ReplaceAllString(expr, "'$1'$2") return expr } diff --git a/testdata/dump/issue_345_array_cast/manifest.json b/testdata/dump/issue_345_array_cast/manifest.json new file mode 100644 index 00000000..24f93a62 --- /dev/null +++ b/testdata/dump/issue_345_array_cast/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "issue_345_array_cast", + "description": "pgschema dump strips type name from array literal casts (e.g., '{nested,key}'::text[] becomes '{nested,key}'[])", + "source_url": "https://github.com/pgplex/pgschema/issues/345", + "notes": "Verifies that explicit array type casts are preserved in policy expressions and default values" +} diff --git a/testdata/dump/issue_345_array_cast/pgdump.sql b/testdata/dump/issue_345_array_cast/pgdump.sql new file mode 100644 index 00000000..31b5198f --- /dev/null +++ b/testdata/dump/issue_345_array_cast/pgdump.sql @@ -0,0 +1,43 @@ +-- +-- 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: repro; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.repro ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + data jsonb DEFAULT '{}'::jsonb +); + +-- +-- Name: repro repro_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.repro + ADD CONSTRAINT repro_pkey PRIMARY KEY (id); + +-- +-- Name: repro; Type: ROW SECURITY; Schema: public; Owner: - +-- + +ALTER TABLE public.repro ENABLE ROW LEVEL SECURITY; + +-- +-- Name: repro p; Type: POLICY; Schema: public; Owner: - +-- + +CREATE POLICY p ON public.repro USING (((data #>> ('{nested,key}'::text[])) = 'x'::text)); + +-- +-- PostgreSQL database dump complete +-- diff --git a/testdata/dump/issue_345_array_cast/pgschema.sql b/testdata/dump/issue_345_array_cast/pgschema.sql new file mode 100644 index 00000000..4394fe0b --- /dev/null +++ b/testdata/dump/issue_345_array_cast/pgschema.sql @@ -0,0 +1,30 @@ +-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 18.0 +-- Dumped by pgschema version 1.7.3 + + +-- +-- Name: repro; Type: TABLE; Schema: -; Owner: - +-- + +CREATE TABLE IF NOT EXISTS repro ( + id uuid DEFAULT gen_random_uuid(), + data jsonb DEFAULT '{}', + CONSTRAINT repro_pkey PRIMARY KEY (id) +); + +-- +-- Name: repro; Type: RLS; Schema: -; Owner: - +-- + +ALTER TABLE repro ENABLE ROW LEVEL SECURITY; + +-- +-- Name: p; Type: POLICY; Schema: -; Owner: - +-- + +CREATE POLICY p ON repro TO PUBLIC USING ((data #>> '{nested,key}'::text[]) = 'x'); + diff --git a/testdata/dump/issue_345_array_cast/raw.sql b/testdata/dump/issue_345_array_cast/raw.sql new file mode 100644 index 00000000..818d91ce --- /dev/null +++ b/testdata/dump/issue_345_array_cast/raw.sql @@ -0,0 +1,13 @@ +-- +-- Test case for GitHub issue #345: pgschema dump strips type name from array literal casts +-- +-- This reproduces the bug where explicit array type casts like ::text[] are +-- stripped, leaving invalid SQL like '{nested,key}'[] +-- + +CREATE TABLE repro ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + data jsonb DEFAULT '{}' +); + +CREATE POLICY p ON repro USING ((data #>> '{nested,key}'::text[]) = 'x'); From a1ffde4d89ad62be4947f4c2f7b744315a8f72c6 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sun, 8 Mar 2026 06:46:57 -0700 Subject: [PATCH 2/2] fix: address review feedback on comment accuracy and manifest schema - Fix regex comment to accurately describe [^[\w] excluding both '[' and word characters, not just '[' - Use 'source' field instead of 'source_url' and 'notes' as array to match other dump fixture manifests Co-Authored-By: Claude Opus 4.6 --- ir/normalize.go | 3 ++- testdata/dump/issue_345_array_cast/manifest.json | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ir/normalize.go b/ir/normalize.go index 0f1dafff..aa61db79 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -746,7 +746,8 @@ func normalizeExpressionParentheses(expr string) string { // Pattern: 'text'::text -> 'text' (removing redundant text cast from literals) // IMPORTANT: Do NOT match when followed by [] (array cast is semantically significant) // e.g., '{nested,key}'::text[] must be preserved as-is - // Since Go regex doesn't support lookahead, we match a trailing non-[ char or end-of-string + // Since Go regex doesn't support lookahead, we use [^[\w] which excludes both '[' + // and word characters (letters/digits/_), correctly preventing matches like ::text[] or ::textual redundantTextCastRegex := regexp.MustCompile(`'([^']+)'::text([^[\w]|$)`) expr = redundantTextCastRegex.ReplaceAllString(expr, "'$1'$2") diff --git a/testdata/dump/issue_345_array_cast/manifest.json b/testdata/dump/issue_345_array_cast/manifest.json index 24f93a62..60c321c1 100644 --- a/testdata/dump/issue_345_array_cast/manifest.json +++ b/testdata/dump/issue_345_array_cast/manifest.json @@ -1,6 +1,9 @@ { "name": "issue_345_array_cast", "description": "pgschema dump strips type name from array literal casts (e.g., '{nested,key}'::text[] becomes '{nested,key}'[])", - "source_url": "https://github.com/pgplex/pgschema/issues/345", - "notes": "Verifies that explicit array type casts are preserved in policy expressions and default values" + "source": "https://github.com/pgplex/pgschema/issues/345", + "notes": [ + "Verifies that explicit array type casts are preserved in policy expressions", + "Reproduces the bug where '{nested,key}'::text[] becomes '{nested,key}'[] in dump output" + ] }