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..aa61db79 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,12 @@ 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 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") 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..60c321c1 --- /dev/null +++ b/testdata/dump/issue_345_array_cast/manifest.json @@ -0,0 +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": "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" + ] +} 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');