diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 7b2b72f5..3e43f190 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -75,6 +75,13 @@ func TestDumpCommand_Issue83ExplicitConstraintName(t *testing.T) { runExactMatchTest(t, "issue_83_explicit_constraint_name") } +func TestDumpCommand_Issue91ColDefaultExpr(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + runExactMatchTest(t, "issue_91_col_default_expr") +} + func runExactMatchTest(t *testing.T, testDataDir string) { runExactMatchTestWithContext(t, context.Background(), testDataDir) } diff --git a/ir/normalize.go b/ir/normalize.go index 0f869b1a..26f685c8 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -129,10 +129,13 @@ func normalizeDefaultValue(value string) string { } // Pattern: 'G'::schema.type_name -> 'G' // Pattern: 'G'::type_name -> 'G' + // More precisely: strip only the ::type_name portion, preserving any content after it if strings.Contains(value, "'::") { - if idx := strings.Index(value, "'::"); idx != -1 { - value = value[:idx+1] - } + // Use regex to match and remove type casts after string literals + // Pattern: 'string'::type_name -> 'string' + // This preserves content after the type cast, e.g., (expr 'val'::type) -> (expr 'val') + re := regexp.MustCompile(`'::([a-zA-Z_][a-zA-Z0-9_]*\.)?[a-zA-Z_][a-zA-Z0-9_]*`) + value = re.ReplaceAllString(value, "'") } } diff --git a/testdata/dump/issue_91_col_default_expr/manifest.json b/testdata/dump/issue_91_col_default_expr/manifest.json new file mode 100644 index 00000000..5d26051c --- /dev/null +++ b/testdata/dump/issue_91_col_default_expr/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "issue_91_default_expr", + "description": "Test case for default expression with NOT NULL dumping (GitHub issue #91)", + "source": "https://github.com/pgschema/pgschema/issues/91", + "notes": [ + "Tests that complex default expressions with NOT NULL constraints are correctly formatted", + "Bug: Missing closing parenthesis when default expression contains parentheses and column has NOT NULL", + "Example: DEFAULT (now() AT TIME ZONE 'utc') NOT NULL should not become DEFAULT (now() AT TIME ZONE 'utc' NOT NULL" + ] +} diff --git a/testdata/dump/issue_91_col_default_expr/pgdump.sql b/testdata/dump/issue_91_col_default_expr/pgdump.sql new file mode 100644 index 00000000..5e88255e --- /dev/null +++ b/testdata/dump/issue_91_col_default_expr/pgdump.sql @@ -0,0 +1,59 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 17.5 (Debian 17.5-1.pgdg120+1) +-- Dumped by pg_dump version 17.5 (Homebrew) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: some_table; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.some_table ( + id integer NOT NULL, + created_at timestamp without time zone DEFAULT (now() AT TIME ZONE 'utc'::text) NOT NULL +); + + +-- +-- Name: some_table_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +ALTER TABLE public.some_table ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.some_table_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: some_table some_table_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.some_table + ADD CONSTRAINT some_table_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/testdata/dump/issue_91_col_default_expr/pgschema.sql b/testdata/dump/issue_91_col_default_expr/pgschema.sql new file mode 100644 index 00000000..88686947 --- /dev/null +++ b/testdata/dump/issue_91_col_default_expr/pgschema.sql @@ -0,0 +1,18 @@ +-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 17.5 +-- Dumped by pgschema version 1.4.0 + + +-- +-- Name: some_table; Type: TABLE; Schema: -; Owner: - +-- + +CREATE TABLE IF NOT EXISTS some_table ( + id integer GENERATED BY DEFAULT AS IDENTITY, + created_at timestamp DEFAULT (now() AT TIME ZONE 'utc') NOT NULL, + CONSTRAINT some_table_pkey PRIMARY KEY (id) +); + diff --git a/testdata/dump/issue_91_col_default_expr/raw.sql b/testdata/dump/issue_91_col_default_expr/raw.sql new file mode 100644 index 00000000..095246a9 --- /dev/null +++ b/testdata/dump/issue_91_col_default_expr/raw.sql @@ -0,0 +1,23 @@ +-- +-- Test case for GitHub issue #91: Default expression with NOT NULL dumping +-- +-- This test case reproduces a bug where column default expressions containing +-- parentheses combined with NOT NULL constraints are formatted incorrectly. +-- +-- The issue occurs when: +-- 1. A column has a default expression wrapped in parentheses +-- 2. The default expression contains complex SQL like `now() AT TIME ZONE 'utc'` +-- 3. The column also has a NOT NULL constraint +-- +-- Original bug: DEFAULT (now() AT TIME ZONE 'utc') NOT NULL +-- Gets corrupted to: DEFAULT (now() AT TIME ZONE 'utc' NOT NULL +-- (Missing closing parenthesis before NOT NULL!) +-- + +-- +-- Test table with complex default expressions +-- +CREATE TABLE some_table ( + id serial primary key, + created_at timestamp without time zone default (now() at time zone 'utc') not null +);