From e8e4c460ef9c221f4871aace0146df41065cff42 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 10:02:48 -0800 Subject: [PATCH 01/14] docs: design for LEAKPROOF and PARALLEL function attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive design document for implementing PostgreSQL function attributes LEAKPROOF and PARALLEL (SAFE/UNSAFE/RESTRICTED) support. Covers: - IR structure changes - Database inspection from pg_catalog.pg_proc - Dump output formatting (hybrid approach) - Diff logic and ALTER FUNCTION generation - Test case strategy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...1-27-function-leakproof-parallel-design.md | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 docs/plans/2025-11-27-function-leakproof-parallel-design.md diff --git a/docs/plans/2025-11-27-function-leakproof-parallel-design.md b/docs/plans/2025-11-27-function-leakproof-parallel-design.md new file mode 100644 index 00000000..f3d584e6 --- /dev/null +++ b/docs/plans/2025-11-27-function-leakproof-parallel-design.md @@ -0,0 +1,233 @@ +# Function LEAKPROOF and PARALLEL Support Design + +**Date:** 2025-11-27 +**Status:** Approved +**Scope:** Add full support for LEAKPROOF and PARALLEL attributes on PostgreSQL functions + +## Overview + +Extend pgschema to properly handle PostgreSQL function attributes LEAKPROOF and PARALLEL (SAFE/UNSAFE/RESTRICTED) throughout the dump/plan/apply workflow. This includes IR representation, database inspection, dump formatting, and migration generation. + +## Requirements + +### Functional Requirements + +1. **Complete PARALLEL support**: All three PostgreSQL parallel safety levels + - `PARALLEL SAFE` - Function can run in parallel workers + - `PARALLEL UNSAFE` - Function cannot run in parallel (default) + - `PARALLEL RESTRICTED` - Can run in parallel but restricted to leader + +2. **LEAKPROOF support**: Boolean attribute indicating function won't leak argument information + - Important for row-level security contexts + - Defaults to false in PostgreSQL + +3. **Migration detection**: Generate ALTER FUNCTION statements when attributes change + - `ALTER FUNCTION ... LEAKPROOF` / `NOT LEAKPROOF` + - `ALTER FUNCTION ... PARALLEL {SAFE|UNSAFE|RESTRICTED}` + +4. **Hybrid output approach**: + - Store explicit values in IR (no ambiguity in comparisons) + - Output only non-default values in dumps (clean, readable) + - Matches PostgreSQL conventions and existing pgschema patterns + +## Design + +### 1. IR Structure Changes + +**File:** `ir/ir.go` + +Add two new fields to the `Function` struct: + +```go +type Function struct { + Schema string `json:"schema"` + Name string `json:"name"` + Definition string `json:"definition"` + ReturnType string `json:"return_type"` + Language string `json:"language"` + Parameters []*Parameter `json:"parameters,omitempty"` + Comment string `json:"comment,omitempty"` + Volatility string `json:"volatility,omitempty"` + IsStrict bool `json:"is_strict,omitempty"` + IsSecurityDefiner bool `json:"is_security_definer,omitempty"` + IsLeakproof bool `json:"is_leakproof,omitempty"` // NEW + Parallel string `json:"parallel,omitempty"` // NEW +} +``` + +**Field specifications:** +- `IsLeakproof`: Boolean, defaults to `false` (matches PostgreSQL default) +- `Parallel`: String with valid values `"SAFE"`, `"UNSAFE"`, `"RESTRICTED"`, defaults to `"UNSAFE"` +- Both use `omitempty` JSON tag for clean serialization + +### 2. Database Inspector Changes + +**File:** `ir/inspector.go` + +Update `inspectFunctions()` to extract attributes from `pg_catalog.pg_proc`: + +**System catalog columns:** +- `proleakproof` (boolean) → `IsLeakproof` +- `proparallel` (char) → `Parallel` + - `'s'` → `"SAFE"` + - `'u'` → `"UNSAFE"` + - `'r'` → `"RESTRICTED"` + +**Query addition:** +```sql +SELECT + ...existing columns..., + p.proleakproof, + p.proparallel +FROM pg_catalog.pg_proc p +... +``` + +**Mapping logic:** +```go +func.IsLeakproof = proleakproof + +switch proparallel { +case 's': + func.Parallel = "SAFE" +case 'r': + func.Parallel = "RESTRICTED" +case 'u': + func.Parallel = "UNSAFE" +default: + func.Parallel = "UNSAFE" // Defensive default +} +``` + +### 3. Dump Output Logic + +**File:** `internal/dump/dump.go` + +Update function formatting to output attributes only when non-default: + +**Output rules:** +- Output `LEAKPROOF` only when `IsLeakproof == true` +- Output `PARALLEL SAFE` or `PARALLEL RESTRICTED` only when `Parallel != "UNSAFE"` +- Never output `NOT LEAKPROOF` or `PARALLEL UNSAFE` (they're defaults) + +**Attribute ordering** (matching pg_dump): +1. `LANGUAGE` +2. Volatility (`IMMUTABLE`/`STABLE`/`VOLATILE`) +3. `PARALLEL {SAFE|RESTRICTED}` (if not UNSAFE) +4. `LEAKPROOF` (if true) +5. `STRICT` (if true) +6. `SECURITY DEFINER` (if true) + +**Example output:** +```sql +CREATE FUNCTION safe_add(a integer, b integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +LEAKPROOF +STRICT +AS $$ + SELECT a + b; +$$; +``` + +### 4. Diff and Migration Logic + +**File:** `internal/diff/function.go` + +Detect attribute changes and generate ALTER statements: + +**Detection:** +- Compare `IsLeakproof` between old and new function +- Compare `Parallel` between old and new function +- Only applies when function signature is unchanged + +**Migration SQL:** +```sql +-- LEAKPROOF changes +ALTER FUNCTION schema.function_name(arg_types) LEAKPROOF; +ALTER FUNCTION schema.function_name(arg_types) NOT LEAKPROOF; + +-- PARALLEL changes +ALTER FUNCTION schema.function_name(arg_types) PARALLEL SAFE; +ALTER FUNCTION schema.function_name(arg_types) PARALLEL UNSAFE; +ALTER FUNCTION schema.function_name(arg_types) PARALLEL RESTRICTED; +``` + +**Multiple changes:** +- Generate separate ALTER statements (PostgreSQL doesn't support combining) +- Order: PARALLEL first, then LEAKPROOF (alphabetical) + +**Edge cases:** +- If signature changes (parameters/return type), use DROP/CREATE (existing behavior) +- Attribute-only changes handled via ALTER (no dependencies broken) + +### 5. Test Cases + +#### Enhanced Test: `testdata/diff/create_function/add_function/` + +**old.sql:** Empty (no functions) + +**new.sql:** Functions with various attribute combinations: +1. `process_order()` - Add `LEAKPROOF` and `PARALLEL RESTRICTED` +2. `days_since_special_date()` - Keep `PARALLEL SAFE`, add `LEAKPROOF` +3. `safe_add()` - New simple function with `PARALLEL SAFE` and `LEAKPROOF` + +**expected.sql:** CREATE statements matching new.sql with proper attribute ordering + +#### New Test: `testdata/diff/create_function/alter_function_attributes/` + +Tests attribute-only changes (ALTER FUNCTION path): + +**old.sql:** +```sql +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +AS $$ ... $$; +``` + +**new.sql:** +```sql +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +PARALLEL SAFE +LEAKPROOF +AS $$ ... $$; +``` + +**expected.sql:** +```sql +ALTER FUNCTION process_data(text) PARALLEL SAFE; +ALTER FUNCTION process_data(text) LEAKPROOF; +``` + +## Implementation Checklist + +1. ☐ Update `Function` struct in `ir/ir.go` +2. ☐ Update `inspectFunctions()` in `ir/inspector.go` +3. ☐ Update dump formatting in `internal/dump/dump.go` +4. ☐ Update diff logic in `internal/diff/function.go` +5. ☐ Enhance `add_function` test case +6. ☐ Create `alter_function_attributes` test case +7. ☐ Run diff tests: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./internal/diff -run TestDiffFromFiles` +8. ☐ Run integration tests: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./cmd -run TestPlanAndApply` +9. ☐ Validate with live PostgreSQL (compare pg_dump vs pgschema output) + +## Success Criteria + +- All existing function tests continue to pass +- New test cases pass for both diff and integration +- pg_dump output matches pgschema output for LEAKPROOF/PARALLEL attributes +- ALTER FUNCTION migrations execute successfully on live PostgreSQL +- No regression in function signature detection or DROP/CREATE logic + +## References + +- PostgreSQL Documentation: [Function Volatility and Parallel Safety](https://www.postgresql.org/docs/current/xfunc-volatility.html) +- System Catalog: `pg_catalog.pg_proc` columns `proleakproof` and `proparallel` +- SQL Syntax: `ALTER FUNCTION` for attribute changes From 6db55083f3b79d88ae7755ceefa49b087042f725 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 10:06:33 -0800 Subject: [PATCH 02/14] feat: add IsLeakproof and Parallel fields to Function IR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ir/ir.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ir/ir.go b/ir/ir.go index ac33bd6a..26b6cb1d 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -133,6 +133,8 @@ type Function struct { Volatility string `json:"volatility,omitempty"` // IMMUTABLE, STABLE, VOLATILE IsStrict bool `json:"is_strict,omitempty"` // STRICT or null behavior IsSecurityDefiner bool `json:"is_security_definer,omitempty"` // SECURITY DEFINER + IsLeakproof bool `json:"is_leakproof,omitempty"` // LEAKPROOF + Parallel string `json:"parallel,omitempty"` // SAFE, UNSAFE, RESTRICTED } // GetArguments returns the function arguments string (types only) for function identification. From ba99ef772dde404de4a1028bca5706d68562c32b Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 10:11:03 -0800 Subject: [PATCH 03/14] feat: extract LEAKPROOF and PARALLEL from pg_catalog.pg_proc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add proleakproof and proparallel to GetFunctionsForSchema SQL query - Generate updated Go code with IsLeakproof (bool) and ParallelMode fields - Map proparallel values ('s', 'r', 'u') to Parallel field (SAFE, RESTRICTED, UNSAFE) - Map proleakproof to IsLeakproof field in Function IR - Verified code compiles successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ir/inspector.go | 19 +++++++++++++++++++ ir/queries/queries.sql | 4 +++- ir/queries/queries.sql.go | 8 +++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ir/inspector.go b/ir/inspector.go index a3b00b46..dc8ebf91 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -913,6 +913,23 @@ func (i *Inspector) buildFunctions(ctx context.Context, schema *IR, targetSchema // Handle security definer isSecurityDefiner := fn.IsSecurityDefiner + // Handle leakproof + isLeakproof := fn.IsLeakproof + + // Handle parallel mode + parallelMode := "" + proparallel := i.safeInterfaceToString(fn.ParallelMode) + switch proparallel { + case "s": + parallelMode = "SAFE" + case "r": + parallelMode = "RESTRICTED" + case "u": + parallelMode = "UNSAFE" + default: + parallelMode = "UNSAFE" // Defensive default + } + // Parse parameters from the complete signature provided by pg_get_function_arguments() // This signature includes all parameter information including modes, names, types, and defaults parameters := i.parseParametersFromSignature(signature, schemaName) @@ -928,6 +945,8 @@ func (i *Inspector) buildFunctions(ctx context.Context, schema *IR, targetSchema Volatility: volatility, IsStrict: isStrict, IsSecurityDefiner: isSecurityDefiner, + IsLeakproof: isLeakproof, + Parallel: parallelMode, } dbSchema.SetFunction(functionName, function) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 370e57a7..2f9c54cc 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -813,7 +813,9 @@ SELECT ELSE NULL END AS volatility, p.proisstrict AS is_strict, - p.prosecdef AS is_security_definer + p.prosecdef AS is_security_definer, + p.proleakproof AS is_leakproof, + p.proparallel AS parallel_mode FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 60c7a373..9f85a054 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -1236,7 +1236,9 @@ SELECT ELSE NULL END AS volatility, p.proisstrict AS is_strict, - p.prosecdef AS is_security_definer + p.prosecdef AS is_security_definer, + p.proleakproof AS is_leakproof, + p.proparallel AS parallel_mode FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) @@ -1261,6 +1263,8 @@ type GetFunctionsForSchemaRow struct { Volatility sql.NullString `db:"volatility" json:"volatility"` IsStrict bool `db:"is_strict" json:"is_strict"` IsSecurityDefiner bool `db:"is_security_definer" json:"is_security_definer"` + IsLeakproof bool `db:"is_leakproof" json:"is_leakproof"` + ParallelMode interface{} `db:"parallel_mode" json:"parallel_mode"` } // GetFunctionsForSchema retrieves all user-defined functions for a specific schema @@ -1286,6 +1290,8 @@ func (q *Queries) GetFunctionsForSchema(ctx context.Context, dollar_1 sql.NullSt &i.Volatility, &i.IsStrict, &i.IsSecurityDefiner, + &i.IsLeakproof, + &i.ParallelMode, ); err != nil { return nil, err } From 963ed0ad6e6d33b94b364734fa5e85f547079cf6 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 10:15:31 -0800 Subject: [PATCH 04/14] feat: output LEAKPROOF and PARALLEL in function dumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PARALLEL output logic (SAFE/RESTRICTED, skip UNSAFE default) - Add LEAKPROOF output logic (output when true, skip NOT LEAKPROOF default) - Reorder function attributes: LANGUAGE, Volatility, PARALLEL, LEAKPROOF, STRICT, SECURITY DEFINER Task 3 of function LEAKPROOF and PARALLEL support implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/diff/function.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/diff/function.go b/internal/diff/function.go index 014e7514..2eb6a3e6 100644 --- a/internal/diff/function.go +++ b/internal/diff/function.go @@ -120,23 +120,37 @@ func generateFunctionSQL(function *ir.Function, targetSchema string) string { stmt.WriteString(fmt.Sprintf("\nLANGUAGE %s", function.Language)) } - // Add security definer/invoker - PostgreSQL default is INVOKER - if function.IsSecurityDefiner { - stmt.WriteString("\nSECURITY DEFINER") - } else { - stmt.WriteString("\nSECURITY INVOKER") - } - // Add volatility if not default if function.Volatility != "" { stmt.WriteString(fmt.Sprintf("\n%s", function.Volatility)) } + // Add PARALLEL if not default (UNSAFE) + if function.Parallel == "SAFE" { + stmt.WriteString("\nPARALLEL SAFE") + } else if function.Parallel == "RESTRICTED" { + stmt.WriteString("\nPARALLEL RESTRICTED") + } + // Note: Don't output PARALLEL UNSAFE (it's the default) + + // Add LEAKPROOF if true + if function.IsLeakproof { + stmt.WriteString("\nLEAKPROOF") + } + // Note: Don't output NOT LEAKPROOF (it's the default) + // Add STRICT if specified if function.IsStrict { stmt.WriteString("\nSTRICT") } + // Add security definer/invoker - PostgreSQL default is INVOKER + if function.IsSecurityDefiner { + stmt.WriteString("\nSECURITY DEFINER") + } else { + stmt.WriteString("\nSECURITY INVOKER") + } + // Add the function body if function.Definition != "" { // Check if this uses RETURN clause syntax (PG14+) From 548d4136cdab62c2446ff159e445cf928401117b Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 10:27:49 -0800 Subject: [PATCH 05/14] fix: align function attribute order with pg_dump canonical format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Research of pg_dump source code (pg_dump.c lines 13686-13750) revealed the canonical ordering for function attributes. This commit updates pgschema to match pg_dump exactly. Changes: 1. Move STRICT before SECURITY DEFINER (was after) 2. Move SECURITY DEFINER before LEAKPROOF (was after) 3. Remove SECURITY INVOKER output (it's the default, pg_dump never outputs it) 4. Keep PARALLEL SAFE/RESTRICTED at the end (after LEAKPROOF) The correct pg_dump order is: - LANGUAGE (always) - VOLATILE/STABLE/IMMUTABLE (only non-default) - STRICT (only if true) - SECURITY DEFINER (only if true, INVOKER is default) - LEAKPROOF (only if true) - COST (only if non-default) - ROWS (only if non-default) - SUPPORT (only if set) - PARALLEL SAFE/RESTRICTED (only non-default) Updated internal/diff/function.go and all affected test fixtures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/diff/function.go | 30 +++++++++---------- .../create_function/add_function/diff.sql | 4 +-- .../create_function/add_function/plan.sql | 4 +-- .../diff.sql | 2 +- .../plan.sql | 2 +- .../alter_function_same_signature/diff.sql | 1 - .../alter_function_same_signature/plan.sql | 1 - .../dependency/function_to_table/diff.sql | 2 +- .../dependency/function_to_table/plan.sql | 2 +- .../dependency/function_to_trigger/diff.sql | 2 +- .../dependency/function_to_trigger/plan.sql | 2 +- .../dependency/table_to_function/diff.sql | 2 +- .../dependency/table_to_function/plan.sql | 2 +- testdata/diff/migrate/v3/diff.sql | 2 +- testdata/diff/migrate/v3/plan.sql | 2 +- testdata/diff/migrate/v4/diff.sql | 2 +- testdata/diff/migrate/v4/plan.sql | 2 +- 17 files changed, 30 insertions(+), 34 deletions(-) diff --git a/internal/diff/function.go b/internal/diff/function.go index 2eb6a3e6..112e3d48 100644 --- a/internal/diff/function.go +++ b/internal/diff/function.go @@ -125,13 +125,15 @@ func generateFunctionSQL(function *ir.Function, targetSchema string) string { stmt.WriteString(fmt.Sprintf("\n%s", function.Volatility)) } - // Add PARALLEL if not default (UNSAFE) - if function.Parallel == "SAFE" { - stmt.WriteString("\nPARALLEL SAFE") - } else if function.Parallel == "RESTRICTED" { - stmt.WriteString("\nPARALLEL RESTRICTED") + // Add STRICT if specified + if function.IsStrict { + stmt.WriteString("\nSTRICT") + } + + // Add SECURITY DEFINER if true (INVOKER is default and not output) + if function.IsSecurityDefiner { + stmt.WriteString("\nSECURITY DEFINER") } - // Note: Don't output PARALLEL UNSAFE (it's the default) // Add LEAKPROOF if true if function.IsLeakproof { @@ -139,17 +141,13 @@ func generateFunctionSQL(function *ir.Function, targetSchema string) string { } // Note: Don't output NOT LEAKPROOF (it's the default) - // Add STRICT if specified - if function.IsStrict { - stmt.WriteString("\nSTRICT") - } - - // Add security definer/invoker - PostgreSQL default is INVOKER - if function.IsSecurityDefiner { - stmt.WriteString("\nSECURITY DEFINER") - } else { - stmt.WriteString("\nSECURITY INVOKER") + // Add PARALLEL if not default (UNSAFE) + if function.Parallel == "SAFE" { + stmt.WriteString("\nPARALLEL SAFE") + } else if function.Parallel == "RESTRICTED" { + stmt.WriteString("\nPARALLEL RESTRICTED") } + // Note: Don't output PARALLEL UNSAFE (it's the default) // Add the function body if function.Definition != "" { diff --git a/testdata/diff/create_function/add_function/diff.sql b/testdata/diff/create_function/add_function/diff.sql index a4e635ba..fc67ab9a 100644 --- a/testdata/diff/create_function/add_function/diff.sql +++ b/testdata/diff/create_function/add_function/diff.sql @@ -1,8 +1,8 @@ CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql -SECURITY INVOKER STABLE +PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); CREATE OR REPLACE FUNCTION process_order( @@ -16,9 +16,9 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY DEFINER VOLATILE STRICT +SECURITY DEFINER AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/add_function/plan.sql b/testdata/diff/create_function/add_function/plan.sql index a4e635ba..fc67ab9a 100644 --- a/testdata/diff/create_function/add_function/plan.sql +++ b/testdata/diff/create_function/add_function/plan.sql @@ -1,8 +1,8 @@ CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql -SECURITY INVOKER STABLE +PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); CREATE OR REPLACE FUNCTION process_order( @@ -16,9 +16,9 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY DEFINER VOLATILE STRICT +SECURITY DEFINER AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/alter_function_different_signature/diff.sql b/testdata/diff/create_function/alter_function_different_signature/diff.sql index c65d0503..f86565e5 100644 --- a/testdata/diff/create_function/alter_function_different_signature/diff.sql +++ b/testdata/diff/create_function/alter_function_different_signature/diff.sql @@ -6,8 +6,8 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS TABLE(status text, processed_at timestamp) LANGUAGE plpgsql -SECURITY DEFINER STABLE +SECURITY DEFINER AS $$ BEGIN RETURN QUERY diff --git a/testdata/diff/create_function/alter_function_different_signature/plan.sql b/testdata/diff/create_function/alter_function_different_signature/plan.sql index c65d0503..f86565e5 100644 --- a/testdata/diff/create_function/alter_function_different_signature/plan.sql +++ b/testdata/diff/create_function/alter_function_different_signature/plan.sql @@ -6,8 +6,8 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS TABLE(status text, processed_at timestamp) LANGUAGE plpgsql -SECURITY DEFINER STABLE +SECURITY DEFINER AS $$ BEGIN RETURN QUERY diff --git a/testdata/diff/create_function/alter_function_same_signature/diff.sql b/testdata/diff/create_function/alter_function_same_signature/diff.sql index c86be39a..ee039c57 100644 --- a/testdata/diff/create_function/alter_function_same_signature/diff.sql +++ b/testdata/diff/create_function/alter_function_same_signature/diff.sql @@ -6,7 +6,6 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY INVOKER STABLE AS $$ DECLARE diff --git a/testdata/diff/create_function/alter_function_same_signature/plan.sql b/testdata/diff/create_function/alter_function_same_signature/plan.sql index c86be39a..ee039c57 100644 --- a/testdata/diff/create_function/alter_function_same_signature/plan.sql +++ b/testdata/diff/create_function/alter_function_same_signature/plan.sql @@ -6,7 +6,6 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY INVOKER STABLE AS $$ DECLARE diff --git a/testdata/diff/dependency/function_to_table/diff.sql b/testdata/diff/dependency/function_to_table/diff.sql index 5baeaa94..bc279d28 100644 --- a/testdata/diff/dependency/function_to_table/diff.sql +++ b/testdata/diff/dependency/function_to_table/diff.sql @@ -1,7 +1,7 @@ CREATE OR REPLACE FUNCTION get_default_status() RETURNS text LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_table/plan.sql b/testdata/diff/dependency/function_to_table/plan.sql index 5baeaa94..bc279d28 100644 --- a/testdata/diff/dependency/function_to_table/plan.sql +++ b/testdata/diff/dependency/function_to_table/plan.sql @@ -1,7 +1,7 @@ CREATE OR REPLACE FUNCTION get_default_status() RETURNS text LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_trigger/diff.sql b/testdata/diff/dependency/function_to_trigger/diff.sql index 30881a81..f0c8b113 100644 --- a/testdata/diff/dependency/function_to_trigger/diff.sql +++ b/testdata/diff/dependency/function_to_trigger/diff.sql @@ -4,7 +4,7 @@ DROP FUNCTION IF EXISTS update_modified_time(); CREATE OR REPLACE FUNCTION log_user_changes() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_trigger/plan.sql b/testdata/diff/dependency/function_to_trigger/plan.sql index 1f0084bd..6ae7e679 100644 --- a/testdata/diff/dependency/function_to_trigger/plan.sql +++ b/testdata/diff/dependency/function_to_trigger/plan.sql @@ -5,7 +5,7 @@ DROP FUNCTION IF EXISTS update_modified_time(); CREATE OR REPLACE FUNCTION log_user_changes() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/table_to_function/diff.sql b/testdata/diff/dependency/table_to_function/diff.sql index bf16996a..22faba8c 100644 --- a/testdata/diff/dependency/table_to_function/diff.sql +++ b/testdata/diff/dependency/table_to_function/diff.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS documents ( CREATE OR REPLACE FUNCTION get_document_count() RETURNS integer LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/table_to_function/plan.sql b/testdata/diff/dependency/table_to_function/plan.sql index 69753cb4..18a1e39c 100644 --- a/testdata/diff/dependency/table_to_function/plan.sql +++ b/testdata/diff/dependency/table_to_function/plan.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS documents ( CREATE OR REPLACE FUNCTION get_document_count() RETURNS integer LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v3/diff.sql b/testdata/diff/migrate/v3/diff.sql index 601646cc..33d277cd 100644 --- a/testdata/diff/migrate/v3/diff.sql +++ b/testdata/diff/migrate/v3/diff.sql @@ -12,7 +12,7 @@ CREATE INDEX IF NOT EXISTS idx_audit_changed_at ON audit (changed_at); CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v3/plan.sql b/testdata/diff/migrate/v3/plan.sql index 601646cc..33d277cd 100644 --- a/testdata/diff/migrate/v3/plan.sql +++ b/testdata/diff/migrate/v3/plan.sql @@ -12,7 +12,7 @@ CREATE INDEX IF NOT EXISTS idx_audit_changed_at ON audit (changed_at); CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v4/diff.sql b/testdata/diff/migrate/v4/diff.sql index d562a0b6..3de1328d 100644 --- a/testdata/diff/migrate/v4/diff.sql +++ b/testdata/diff/migrate/v4/diff.sql @@ -58,7 +58,7 @@ CREATE OR REPLACE TRIGGER salary_log_trigger CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ DECLARE diff --git a/testdata/diff/migrate/v4/plan.sql b/testdata/diff/migrate/v4/plan.sql index 6d23c874..43691ba7 100644 --- a/testdata/diff/migrate/v4/plan.sql +++ b/testdata/diff/migrate/v4/plan.sql @@ -82,7 +82,7 @@ CREATE OR REPLACE TRIGGER salary_log_trigger CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER + VOLATILE AS $$ DECLARE From a1c2ee73d603dd9bcea5b5cc64ddcda9bb64b611 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:06:45 -0800 Subject: [PATCH 06/14] test: update test fixtures for function attribute ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update integration test fixtures (plan.txt, plan.json) after fixing function attribute ordering to match PostgreSQL standards. Changes: - Regenerated function test fixtures using --generate flag - Regenerated trigger test fixtures (depend on functions) - Regenerated dependency test fixtures (function dependencies) - Regenerated migrate test fixtures (include functions) - Updated dump test expected outputs (employee, sakila, tenant, issue_125) - Updated include test expected outputs (modular function files) All tests now pass with correct attribute ordering that omits default SECURITY INVOKER attribute and properly positions PARALLEL SAFE. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- testdata/diff/create_function/add_function/plan.json | 6 +++--- testdata/diff/create_function/add_function/plan.txt | 4 ++-- .../alter_function_different_signature/plan.json | 6 +++--- .../alter_function_different_signature/plan.txt | 2 +- .../alter_function_same_signature/plan.json | 6 +++--- .../alter_function_same_signature/plan.txt | 1 - .../diff/create_function/drop_function/plan.json | 4 ++-- testdata/diff/create_trigger/add_trigger/plan.json | 4 ++-- .../create_trigger/add_trigger_constraint/plan.json | 4 ++-- .../create_trigger/add_trigger_old_table/plan.json | 4 ++-- .../add_trigger_system_catalog/plan.json | 2 +- .../add_trigger_when_distinct/plan.json | 4 ++-- testdata/diff/create_trigger/alter_trigger/plan.json | 4 ++-- testdata/diff/create_trigger/drop_trigger/plan.json | 4 ++-- testdata/diff/dependency/function_to_table/plan.json | 2 +- testdata/diff/dependency/function_to_table/plan.sql | 1 - testdata/diff/dependency/function_to_table/plan.txt | 1 - .../diff/dependency/function_to_trigger/plan.json | 6 +++--- .../diff/dependency/function_to_trigger/plan.sql | 1 - .../diff/dependency/function_to_trigger/plan.txt | 1 - testdata/diff/dependency/table_to_function/plan.json | 4 ++-- testdata/diff/dependency/table_to_function/plan.sql | 1 - testdata/diff/dependency/table_to_function/plan.txt | 1 - testdata/diff/dependency/table_to_table/plan.json | 2 +- testdata/diff/migrate/v1/plan.json | 2 +- testdata/diff/migrate/v2/plan.json | 2 +- testdata/diff/migrate/v3/plan.json | 4 ++-- testdata/diff/migrate/v3/plan.sql | 1 - testdata/diff/migrate/v3/plan.txt | 1 - testdata/diff/migrate/v4/plan.json | 6 +++--- testdata/diff/migrate/v4/plan.sql | 1 - testdata/diff/migrate/v4/plan.txt | 1 - testdata/diff/migrate/v5/plan.json | 4 ++-- testdata/dump/employee/pgschema.sql | 3 +-- .../dump/issue_125_function_default/pgschema.sql | 7 +------ testdata/dump/sakila/pgschema.sql | 12 ++---------- testdata/dump/tenant/pgschema.sql | 5 +---- testdata/include/expected_full_schema.sql | 3 --- testdata/include/functions/get_order_count.sql | 1 - testdata/include/functions/get_user_count.sql | 1 - testdata/include/functions/update_timestamp.sql | 1 - 41 files changed, 48 insertions(+), 82 deletions(-) diff --git a/testdata/diff/create_function/add_function/plan.json b/testdata/diff/create_function/add_function/plan.json index 0b22a107..1991af23 100644 --- a/testdata/diff/create_function/add_function/plan.json +++ b/testdata/diff/create_function/add_function/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" @@ -9,13 +9,13 @@ { "steps": [ { - "sql": "CREATE OR REPLACE FUNCTION days_since_special_date()\nRETURNS SETOF timestamp with time zone\nLANGUAGE sql\nSECURITY INVOKER\nSTABLE\nRETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval);", + "sql": "CREATE OR REPLACE FUNCTION days_since_special_date()\nRETURNS SETOF timestamp with time zone\nLANGUAGE sql\nSTABLE\nPARALLEL SAFE\nRETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval);", "type": "function", "operation": "create", "path": "public.days_since_special_date" }, { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nSECURITY DEFINER\nVOLATILE\nSTRICT\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.process_order" diff --git a/testdata/diff/create_function/add_function/plan.txt b/testdata/diff/create_function/add_function/plan.txt index cae2ecd3..061b292e 100644 --- a/testdata/diff/create_function/add_function/plan.txt +++ b/testdata/diff/create_function/add_function/plan.txt @@ -13,8 +13,8 @@ DDL to be executed: CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql -SECURITY INVOKER STABLE +PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); CREATE OR REPLACE FUNCTION process_order( @@ -28,9 +28,9 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY DEFINER VOLATILE STRICT +SECURITY DEFINER AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/alter_function_different_signature/plan.json b/testdata/diff/create_function/alter_function_different_signature/plan.json index 63da2859..77c7433f 100644 --- a/testdata/diff/create_function/alter_function_different_signature/plan.json +++ b/testdata/diff/create_function/alter_function_different_signature/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "6999cab9e41f75143c1f09a16bb229452a4a06cd1171eece2a7466ca8d1323d6" + "hash": "60f48278b090a2550f1634778976ec483638f67d06485a7e54b22a59d79f31b2" }, "groups": [ { @@ -15,7 +15,7 @@ "path": "public.process_order" }, { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n customer_email text,\n priority boolean\n)\nRETURNS TABLE(status text, processed_at timestamp)\nLANGUAGE plpgsql\nSECURITY DEFINER\nSTABLE\nAS $$\nBEGIN\n RETURN QUERY\n SELECT 'completed'::text, NOW()\n WHERE priority = true;\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n customer_email text,\n priority boolean\n)\nRETURNS TABLE(status text, processed_at timestamp)\nLANGUAGE plpgsql\nSTABLE\nSECURITY DEFINER\nAS $$\nBEGIN\n RETURN QUERY\n SELECT 'completed'::text, NOW()\n WHERE priority = true;\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.process_order" diff --git a/testdata/diff/create_function/alter_function_different_signature/plan.txt b/testdata/diff/create_function/alter_function_different_signature/plan.txt index c4caf7b2..533163d7 100644 --- a/testdata/diff/create_function/alter_function_different_signature/plan.txt +++ b/testdata/diff/create_function/alter_function_different_signature/plan.txt @@ -18,8 +18,8 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS TABLE(status text, processed_at timestamp) LANGUAGE plpgsql -SECURITY DEFINER STABLE +SECURITY DEFINER AS $$ BEGIN RETURN QUERY diff --git a/testdata/diff/create_function/alter_function_same_signature/plan.json b/testdata/diff/create_function/alter_function_same_signature/plan.json index b2fc2ee6..9ba5ace2 100644 --- a/testdata/diff/create_function/alter_function_same_signature/plan.json +++ b/testdata/diff/create_function/alter_function_same_signature/plan.json @@ -1,15 +1,15 @@ { "version": "1.0.0", - "pgschema_version": "1.4.1", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "6d0c7bd072cf940b17ecae1b64566b63a06bd9cd0aaf2c69a12af2fe77ae6d47" + "hash": "f8f674906f2aa0fee086c386c8ee9da81b6be4db116ef22b9e2e97411fd7e695" }, "groups": [ { "steps": [ { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n status order_status DEFAULT 'pending',\n priority utils.priority_level DEFAULT 'medium'\n)\nRETURNS numeric\nLANGUAGE plpgsql\nSECURITY INVOKER\nSTABLE\nAS $$\nDECLARE\n base_price numeric;\n tax_rate numeric := 0.08;\nBEGIN\n -- Different logic: calculate with tax instead of just discount\n -- Status and priority parameters are available but not used in this simplified version\n SELECT price INTO base_price FROM products WHERE id = order_id;\n RETURN base_price * (1 - discount_percent / 100) * (1 + tax_rate);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n status order_status DEFAULT 'pending',\n priority utils.priority_level DEFAULT 'medium'\n)\nRETURNS numeric\nLANGUAGE plpgsql\nSTABLE\nAS $$\nDECLARE\n base_price numeric;\n tax_rate numeric := 0.08;\nBEGIN\n -- Different logic: calculate with tax instead of just discount\n -- Status and priority parameters are available but not used in this simplified version\n SELECT price INTO base_price FROM products WHERE id = order_id;\n RETURN base_price * (1 - discount_percent / 100) * (1 + tax_rate);\nEND;\n$$;", "type": "function", "operation": "alter", "path": "public.process_order" diff --git a/testdata/diff/create_function/alter_function_same_signature/plan.txt b/testdata/diff/create_function/alter_function_same_signature/plan.txt index c5a872f0..88e4ac1d 100644 --- a/testdata/diff/create_function/alter_function_same_signature/plan.txt +++ b/testdata/diff/create_function/alter_function_same_signature/plan.txt @@ -17,7 +17,6 @@ CREATE OR REPLACE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY INVOKER STABLE AS $$ DECLARE diff --git a/testdata/diff/create_function/drop_function/plan.json b/testdata/diff/create_function/drop_function/plan.json index 5cc8fb2d..446c0d57 100644 --- a/testdata/diff/create_function/drop_function/plan.json +++ b/testdata/diff/create_function/drop_function/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "c34208400ed55b8e5d1ee74c1200d4e126ed191b12f9005e80d878e34885968e" + "hash": "b50e4d62ed25f92850157eef13f4f129a5f082f37da2fe5731342900892c263f" }, "groups": [ { diff --git a/testdata/diff/create_trigger/add_trigger/plan.json b/testdata/diff/create_trigger/add_trigger/plan.json index 0799344b..aa933bfa 100644 --- a/testdata/diff/create_trigger/add_trigger/plan.json +++ b/testdata/diff/create_trigger/add_trigger/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "5c096500ddbec5e8aca1b76512fd7076f787118fde4fae6d0a80d99c4335abc1" + "hash": "76b8935489231ab4bd0742f7f1273e9302474f4f6e73ef99b4e24e211058ce37" }, "groups": [ { diff --git a/testdata/diff/create_trigger/add_trigger_constraint/plan.json b/testdata/diff/create_trigger/add_trigger_constraint/plan.json index 810fa1d2..e110af09 100644 --- a/testdata/diff/create_trigger/add_trigger_constraint/plan.json +++ b/testdata/diff/create_trigger/add_trigger_constraint/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "08f4c7258568484a7571fb1a332a97279e6dcb5336ddbbf021d777cc1b73730d" + "hash": "40681940fc8b6f55df564abf77a173d29f0faf13cc2a6d3f346ee17f1727dae2" }, "groups": [ { diff --git a/testdata/diff/create_trigger/add_trigger_old_table/plan.json b/testdata/diff/create_trigger/add_trigger_old_table/plan.json index bbc24a43..20771fe9 100644 --- a/testdata/diff/create_trigger/add_trigger_old_table/plan.json +++ b/testdata/diff/create_trigger/add_trigger_old_table/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "087a74a25f6a817a556750d274e732048ca5281379e6c0bedd1f5a35176daa2e" + "hash": "c7d5d7ce56f814aa8bc8287c6054c7dbbe0b2df7bdc9563d709d074d7ba4b863" }, "groups": [ { diff --git a/testdata/diff/create_trigger/add_trigger_system_catalog/plan.json b/testdata/diff/create_trigger/add_trigger_system_catalog/plan.json index 33245736..bf57f24c 100644 --- a/testdata/diff/create_trigger/add_trigger_system_catalog/plan.json +++ b/testdata/diff/create_trigger/add_trigger_system_catalog/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "4620d5c78a7c107496877b2ce463eaaa25ad87fac2cecb93a987e0d9a8ea803a" diff --git a/testdata/diff/create_trigger/add_trigger_when_distinct/plan.json b/testdata/diff/create_trigger/add_trigger_when_distinct/plan.json index 484c3807..55c8cb0d 100644 --- a/testdata/diff/create_trigger/add_trigger_when_distinct/plan.json +++ b/testdata/diff/create_trigger/add_trigger_when_distinct/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "98db11096a7a86d2175ff6821924a2b64dddbf240681f23079c0d912d3ea22b5" + "hash": "9f531a58a8a6e5160d9c22fd497d42a0fadbb9324994494f819629b94e46b868" }, "groups": [ { diff --git a/testdata/diff/create_trigger/alter_trigger/plan.json b/testdata/diff/create_trigger/alter_trigger/plan.json index 28c76d72..2052a83c 100644 --- a/testdata/diff/create_trigger/alter_trigger/plan.json +++ b/testdata/diff/create_trigger/alter_trigger/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "3532aafb5a93b1bfbd03aab1db8e95d7c02b248ebfa893bfb4af4ab9ac32039a" + "hash": "ba218be2c63f4cf69ba70b4f8dca11b3832609c271cde6166f570aa47026b923" }, "groups": [ { diff --git a/testdata/diff/create_trigger/drop_trigger/plan.json b/testdata/diff/create_trigger/drop_trigger/plan.json index 1f02e3ef..07833b10 100644 --- a/testdata/diff/create_trigger/drop_trigger/plan.json +++ b/testdata/diff/create_trigger/drop_trigger/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "3532aafb5a93b1bfbd03aab1db8e95d7c02b248ebfa893bfb4af4ab9ac32039a" + "hash": "ba218be2c63f4cf69ba70b4f8dca11b3832609c271cde6166f570aa47026b923" }, "groups": [ { diff --git a/testdata/diff/dependency/function_to_table/plan.json b/testdata/diff/dependency/function_to_table/plan.json index 487d002e..478ff569 100644 --- a/testdata/diff/dependency/function_to_table/plan.json +++ b/testdata/diff/dependency/function_to_table/plan.json @@ -9,7 +9,7 @@ { "steps": [ { - "sql": "CREATE OR REPLACE FUNCTION get_default_status()\nRETURNS text\nLANGUAGE plpgsql\nSECURITY INVOKER\nVOLATILE\nAS $$\nBEGIN\n RETURN 'active';\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION get_default_status()\nRETURNS text\nLANGUAGE plpgsql\nVOLATILE\nAS $$\nBEGIN\n RETURN 'active';\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.get_default_status" diff --git a/testdata/diff/dependency/function_to_table/plan.sql b/testdata/diff/dependency/function_to_table/plan.sql index bc279d28..31d6e645 100644 --- a/testdata/diff/dependency/function_to_table/plan.sql +++ b/testdata/diff/dependency/function_to_table/plan.sql @@ -1,7 +1,6 @@ CREATE OR REPLACE FUNCTION get_default_status() RETURNS text LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_table/plan.txt b/testdata/diff/dependency/function_to_table/plan.txt index cdfd16b5..9807303d 100644 --- a/testdata/diff/dependency/function_to_table/plan.txt +++ b/testdata/diff/dependency/function_to_table/plan.txt @@ -16,7 +16,6 @@ DDL to be executed: CREATE OR REPLACE FUNCTION get_default_status() RETURNS text LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_trigger/plan.json b/testdata/diff/dependency/function_to_trigger/plan.json index 4aafeb21..bbb96a84 100644 --- a/testdata/diff/dependency/function_to_trigger/plan.json +++ b/testdata/diff/dependency/function_to_trigger/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "838be5bff5331655f93ff85bf9e620d007f6c664b57889a1efb31343310534c8" + "hash": "318032a6f9451c9ed14e90c4ff0557d9ecf9a5620e9305314a549d884da5f154" }, "groups": [ { @@ -21,7 +21,7 @@ "path": "public.update_modified_time" }, { - "sql": "CREATE OR REPLACE FUNCTION log_user_changes()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY INVOKER\nVOLATILE\nAS $$\nBEGIN\n RAISE NOTICE 'User record changed: %', NEW.id;\n RETURN NEW;\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION log_user_changes()\nRETURNS trigger\nLANGUAGE plpgsql\nVOLATILE\nAS $$\nBEGIN\n RAISE NOTICE 'User record changed: %', NEW.id;\n RETURN NEW;\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.log_user_changes" diff --git a/testdata/diff/dependency/function_to_trigger/plan.sql b/testdata/diff/dependency/function_to_trigger/plan.sql index 6ae7e679..e9f402cb 100644 --- a/testdata/diff/dependency/function_to_trigger/plan.sql +++ b/testdata/diff/dependency/function_to_trigger/plan.sql @@ -5,7 +5,6 @@ DROP FUNCTION IF EXISTS update_modified_time(); CREATE OR REPLACE FUNCTION log_user_changes() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_trigger/plan.txt b/testdata/diff/dependency/function_to_trigger/plan.txt index f3c34cf2..9a45b225 100644 --- a/testdata/diff/dependency/function_to_trigger/plan.txt +++ b/testdata/diff/dependency/function_to_trigger/plan.txt @@ -23,7 +23,6 @@ DROP FUNCTION IF EXISTS update_modified_time(); CREATE OR REPLACE FUNCTION log_user_changes() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/table_to_function/plan.json b/testdata/diff/dependency/table_to_function/plan.json index 14432a86..da534f54 100644 --- a/testdata/diff/dependency/table_to_function/plan.json +++ b/testdata/diff/dependency/table_to_function/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" @@ -15,7 +15,7 @@ "path": "public.documents" }, { - "sql": "CREATE OR REPLACE FUNCTION get_document_count()\nRETURNS integer\nLANGUAGE plpgsql\nSECURITY INVOKER\nVOLATILE\nAS $$\nBEGIN\n RETURN (SELECT COUNT(*) FROM documents);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION get_document_count()\nRETURNS integer\nLANGUAGE plpgsql\nVOLATILE\nAS $$\nBEGIN\n RETURN (SELECT COUNT(*) FROM documents);\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.get_document_count" diff --git a/testdata/diff/dependency/table_to_function/plan.sql b/testdata/diff/dependency/table_to_function/plan.sql index 18a1e39c..61ef271d 100644 --- a/testdata/diff/dependency/table_to_function/plan.sql +++ b/testdata/diff/dependency/table_to_function/plan.sql @@ -9,7 +9,6 @@ CREATE TABLE IF NOT EXISTS documents ( CREATE OR REPLACE FUNCTION get_document_count() RETURNS integer LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/table_to_function/plan.txt b/testdata/diff/dependency/table_to_function/plan.txt index 40591717..3a2c94b2 100644 --- a/testdata/diff/dependency/table_to_function/plan.txt +++ b/testdata/diff/dependency/table_to_function/plan.txt @@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS documents ( CREATE OR REPLACE FUNCTION get_document_count() RETURNS integer LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/table_to_table/plan.json b/testdata/diff/dependency/table_to_table/plan.json index 04fc9ece..6e9f5136 100644 --- a/testdata/diff/dependency/table_to_table/plan.json +++ b/testdata/diff/dependency/table_to_table/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" diff --git a/testdata/diff/migrate/v1/plan.json b/testdata/diff/migrate/v1/plan.json index 50cfbf53..6c85219d 100644 --- a/testdata/diff/migrate/v1/plan.json +++ b/testdata/diff/migrate/v1/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" diff --git a/testdata/diff/migrate/v2/plan.json b/testdata/diff/migrate/v2/plan.json index a0e2ce1f..cf14e13b 100644 --- a/testdata/diff/migrate/v2/plan.json +++ b/testdata/diff/migrate/v2/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "e989216806a1e82a6a01d6d5898b00b5d94d3a5888c6ef481fa5f931f649aab5" diff --git a/testdata/diff/migrate/v3/plan.json b/testdata/diff/migrate/v3/plan.json index 6e302e7e..8c558556 100644 --- a/testdata/diff/migrate/v3/plan.json +++ b/testdata/diff/migrate/v3/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "b07ab6318b6ff348aa5554a1f6e1a1ec9ad6b987a6d47e455fbdf97f1b0b96fb" @@ -21,7 +21,7 @@ "path": "public.audit.idx_audit_changed_at" }, { - "sql": "CREATE OR REPLACE FUNCTION log_dml_operations()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY INVOKER\nVOLATILE\nAS $$\nBEGIN\n IF (TG_OP = 'INSERT') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('INSERT', current_query(), current_user);\n RETURN NEW;\n ELSIF (TG_OP = 'UPDATE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('UPDATE', current_query(), current_user);\n RETURN NEW;\n ELSIF (TG_OP = 'DELETE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('DELETE', current_query(), current_user);\n RETURN OLD;\n END IF;\n RETURN NULL;\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION log_dml_operations()\nRETURNS trigger\nLANGUAGE plpgsql\nVOLATILE\nAS $$\nBEGIN\n IF (TG_OP = 'INSERT') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('INSERT', current_query(), current_user);\n RETURN NEW;\n ELSIF (TG_OP = 'UPDATE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('UPDATE', current_query(), current_user);\n RETURN NEW;\n ELSIF (TG_OP = 'DELETE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES ('DELETE', current_query(), current_user);\n RETURN OLD;\n END IF;\n RETURN NULL;\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.log_dml_operations" diff --git a/testdata/diff/migrate/v3/plan.sql b/testdata/diff/migrate/v3/plan.sql index 33d277cd..94cf41ed 100644 --- a/testdata/diff/migrate/v3/plan.sql +++ b/testdata/diff/migrate/v3/plan.sql @@ -12,7 +12,6 @@ CREATE INDEX IF NOT EXISTS idx_audit_changed_at ON audit (changed_at); CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v3/plan.txt b/testdata/diff/migrate/v3/plan.txt index cccb1741..a4bed376 100644 --- a/testdata/diff/migrate/v3/plan.txt +++ b/testdata/diff/migrate/v3/plan.txt @@ -30,7 +30,6 @@ CREATE INDEX IF NOT EXISTS idx_audit_changed_at ON audit (changed_at); CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v4/plan.json b/testdata/diff/migrate/v4/plan.json index f6a7d717..c1a3dce6 100644 --- a/testdata/diff/migrate/v4/plan.json +++ b/testdata/diff/migrate/v4/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "953c5263c9185d170429fab8c78931f7801f8479579faf963c0ac9635bf2a8ed" + "hash": "c21343c0165a56727a51734705b31278a07e32f8a88cd17fb12dd0a026756403" }, "groups": [ { @@ -97,7 +97,7 @@ "path": "public.salary.salary_log_trigger" }, { - "sql": "CREATE OR REPLACE FUNCTION log_dml_operations()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY INVOKER\nVOLATILE\nAS $$\nDECLARE\n table_category TEXT;\n log_level TEXT;\nBEGIN\n -- Get arguments passed from trigger (if any)\n -- TG_ARGV[0] is the first argument, TG_ARGV[1] is the second\n table_category := COALESCE(TG_ARGV[0], 'default');\n log_level := COALESCE(TG_ARGV[1], 'standard');\n\n IF (TG_OP = 'INSERT') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'INSERT [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN NEW;\n ELSIF (TG_OP = 'UPDATE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'UPDATE [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN NEW;\n ELSIF (TG_OP = 'DELETE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'DELETE [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN OLD;\n END IF;\n RETURN NULL;\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION log_dml_operations()\nRETURNS trigger\nLANGUAGE plpgsql\nVOLATILE\nAS $$\nDECLARE\n table_category TEXT;\n log_level TEXT;\nBEGIN\n -- Get arguments passed from trigger (if any)\n -- TG_ARGV[0] is the first argument, TG_ARGV[1] is the second\n table_category := COALESCE(TG_ARGV[0], 'default');\n log_level := COALESCE(TG_ARGV[1], 'standard');\n\n IF (TG_OP = 'INSERT') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'INSERT [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN NEW;\n ELSIF (TG_OP = 'UPDATE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'UPDATE [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN NEW;\n ELSIF (TG_OP = 'DELETE') THEN\n INSERT INTO audit (operation, query, user_name)\n VALUES (\n 'DELETE [' || table_category || ':' || log_level || ']',\n current_query(),\n current_user\n );\n RETURN OLD;\n END IF;\n RETURN NULL;\nEND;\n$$;", "type": "function", "operation": "alter", "path": "public.log_dml_operations" diff --git a/testdata/diff/migrate/v4/plan.sql b/testdata/diff/migrate/v4/plan.sql index 43691ba7..e6c6016b 100644 --- a/testdata/diff/migrate/v4/plan.sql +++ b/testdata/diff/migrate/v4/plan.sql @@ -82,7 +82,6 @@ CREATE OR REPLACE TRIGGER salary_log_trigger CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ DECLARE diff --git a/testdata/diff/migrate/v4/plan.txt b/testdata/diff/migrate/v4/plan.txt index 3a245ba7..4496e76b 100644 --- a/testdata/diff/migrate/v4/plan.txt +++ b/testdata/diff/migrate/v4/plan.txt @@ -121,7 +121,6 @@ CREATE OR REPLACE TRIGGER salary_log_trigger CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ DECLARE diff --git a/testdata/diff/migrate/v5/plan.json b/testdata/diff/migrate/v5/plan.json index 652aea01..49dbf0ab 100644 --- a/testdata/diff/migrate/v5/plan.json +++ b/testdata/diff/migrate/v5/plan.json @@ -1,9 +1,9 @@ { "version": "1.0.0", - "pgschema_version": "1.4.0", + "pgschema_version": "1.4.3", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "00cecda254bb0731ef1d20915ed24ba4cdfd550430d43bfb9e0d0815c4f1b738" + "hash": "9a12c55798891493337f5358504b1c58cddb3260d0d581698c849c6ae57a4359" }, "groups": [ { diff --git a/testdata/dump/employee/pgschema.sql b/testdata/dump/employee/pgschema.sql index c9aeb029..a2023f67 100644 --- a/testdata/dump/employee/pgschema.sql +++ b/testdata/dump/employee/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.4.1 +-- Dumped by pgschema version 1.4.3 -- @@ -154,7 +154,6 @@ CREATE TABLE IF NOT EXISTS title ( CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ DECLARE diff --git a/testdata/dump/issue_125_function_default/pgschema.sql b/testdata/dump/issue_125_function_default/pgschema.sql index 0ae6b165..51b53434 100644 --- a/testdata/dump/issue_125_function_default/pgschema.sql +++ b/testdata/dump/issue_125_function_default/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.4.0 +-- Dumped by pgschema version 1.4.3 -- @@ -18,7 +18,6 @@ CREATE OR REPLACE FUNCTION test_complex_defaults( ) RETURNS jsonb LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -42,7 +41,6 @@ CREATE OR REPLACE FUNCTION test_inout_params( ) RETURNS record LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -64,7 +62,6 @@ CREATE OR REPLACE FUNCTION test_mixed_params( ) RETURNS record LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -86,7 +83,6 @@ CREATE OR REPLACE FUNCTION test_simple_defaults( ) RETURNS text LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -104,7 +100,6 @@ CREATE OR REPLACE FUNCTION test_variadic_defaults( ) RETURNS text LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/dump/sakila/pgschema.sql b/testdata/dump/sakila/pgschema.sql index c8a90104..bfcb5efe 100644 --- a/testdata/dump/sakila/pgschema.sql +++ b/testdata/dump/sakila/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.4.0 +-- Dumped by pgschema version 1.4.3 -- @@ -601,7 +601,6 @@ CREATE OR REPLACE FUNCTION _group_concat( ) RETURNS text LANGUAGE sql -SECURITY INVOKER IMMUTABLE AS $_$ SELECT CASE @@ -622,7 +621,6 @@ CREATE OR REPLACE FUNCTION film_in_stock( ) RETURNS SETOF integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $_$ SELECT inventory_id @@ -643,7 +641,6 @@ CREATE OR REPLACE FUNCTION film_not_in_stock( ) RETURNS SETOF integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $_$ SELECT inventory_id @@ -663,7 +660,6 @@ CREATE OR REPLACE FUNCTION get_customer_balance( ) RETURNS numeric LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ --#OK, WE NEED TO CALCULATE THE CURRENT BALANCE GIVEN A CUSTOMER_ID AND A DATE @@ -710,7 +706,6 @@ CREATE OR REPLACE FUNCTION inventory_held_by_customer( ) RETURNS integer LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ DECLARE @@ -735,7 +730,6 @@ CREATE OR REPLACE FUNCTION inventory_in_stock( ) RETURNS boolean LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ DECLARE @@ -775,7 +769,6 @@ CREATE OR REPLACE FUNCTION last_day( ) RETURNS date LANGUAGE sql -SECURITY INVOKER IMMUTABLE STRICT AS $_$ @@ -794,7 +787,6 @@ $_$; CREATE OR REPLACE FUNCTION last_updated() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -813,8 +805,8 @@ CREATE OR REPLACE FUNCTION rewards_report( ) RETURNS SETOF customer LANGUAGE plpgsql -SECURITY DEFINER VOLATILE +SECURITY DEFINER AS $_$ DECLARE last_month_start DATE; diff --git a/testdata/dump/tenant/pgschema.sql b/testdata/dump/tenant/pgschema.sql index 87931cd1..2de303de 100644 --- a/testdata/dump/tenant/pgschema.sql +++ b/testdata/dump/tenant/pgschema.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version PostgreSQL 17.5 --- Dumped by pgschema version 1.4.0 +-- Dumped by pgschema version 1.4.3 -- @@ -100,7 +100,6 @@ CREATE OR REPLACE FUNCTION create_task_assignment( ) RETURNS task_assignment LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ DECLARE @@ -120,7 +119,6 @@ $$; CREATE OR REPLACE FUNCTION generate_task_id() RETURNS text LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN @@ -137,7 +135,6 @@ CREATE OR REPLACE FUNCTION set_task_priority( ) RETURNS void LANGUAGE plpgsql -SECURITY INVOKER VOLATILE AS $$ BEGIN diff --git a/testdata/include/expected_full_schema.sql b/testdata/include/expected_full_schema.sql index 8d444e55..d25fc315 100644 --- a/testdata/include/expected_full_schema.sql +++ b/testdata/include/expected_full_schema.sql @@ -59,7 +59,6 @@ CREATE SEQUENCE IF NOT EXISTS order_number_seq; CREATE OR REPLACE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER STABLE AS $$ BEGIN @@ -167,7 +166,6 @@ CREATE POLICY orders_policy ON orders TO PUBLIC USING (user_id = 1); CREATE OR REPLACE FUNCTION get_user_count() RETURNS integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $$ SELECT COUNT(*) FROM users; @@ -181,7 +179,6 @@ CREATE OR REPLACE FUNCTION get_order_count( ) RETURNS integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $$ SELECT COUNT(*) FROM orders WHERE user_id = user_id_param; diff --git a/testdata/include/functions/get_order_count.sql b/testdata/include/functions/get_order_count.sql index 81f22968..91d79ef0 100644 --- a/testdata/include/functions/get_order_count.sql +++ b/testdata/include/functions/get_order_count.sql @@ -7,7 +7,6 @@ CREATE OR REPLACE FUNCTION get_order_count( ) RETURNS integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $$ SELECT COUNT(*) FROM orders WHERE user_id = user_id_param; diff --git a/testdata/include/functions/get_user_count.sql b/testdata/include/functions/get_user_count.sql index 73f71229..34df79b0 100644 --- a/testdata/include/functions/get_user_count.sql +++ b/testdata/include/functions/get_user_count.sql @@ -5,7 +5,6 @@ CREATE OR REPLACE FUNCTION get_user_count() RETURNS integer LANGUAGE sql -SECURITY INVOKER VOLATILE AS $$ SELECT COUNT(*) FROM users; diff --git a/testdata/include/functions/update_timestamp.sql b/testdata/include/functions/update_timestamp.sql index c90dc720..773c4244 100644 --- a/testdata/include/functions/update_timestamp.sql +++ b/testdata/include/functions/update_timestamp.sql @@ -5,7 +5,6 @@ CREATE OR REPLACE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql -SECURITY INVOKER STABLE AS $$ BEGIN From 2bc407cd3eb06541941a32f760bae74ab3ff6f3b Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:20:17 -0800 Subject: [PATCH 07/14] test: add LEAKPROOF and PARALLEL to add_function test case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated test case to demonstrate new LEAKPROOF and PARALLEL function attributes: - process_order: VOLATILE, PARALLEL RESTRICTED, LEAKPROOF, SECURITY DEFINER, STRICT - days_since_special_date: STABLE, PARALLEL SAFE, LEAKPROOF - safe_add: IMMUTABLE, PARALLEL SAFE, LEAKPROOF, STRICT (new function) Updated all test fixture files (diff.sql, new.sql, plan.sql, plan.txt, plan.json) to match the correct attribute ordering from the dump formatter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../create_function/add_function/diff.sql | 17 ++++++++++++++ .../diff/create_function/add_function/new.sql | 23 ++++++++++++++++--- .../create_function/add_function/plan.json | 10 ++++++-- .../create_function/add_function/plan.sql | 14 +++++++++++ .../create_function/add_function/plan.txt | 22 ++++++++++++++++-- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/testdata/diff/create_function/add_function/diff.sql b/testdata/diff/create_function/add_function/diff.sql index fc67ab9a..f512f439 100644 --- a/testdata/diff/create_function/add_function/diff.sql +++ b/testdata/diff/create_function/add_function/diff.sql @@ -2,6 +2,7 @@ CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql STABLE +LEAKPROOF PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); @@ -19,6 +20,8 @@ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER +LEAKPROOF +PARALLEL RESTRICTED AS $$ DECLARE total numeric; @@ -27,3 +30,17 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; + +CREATE OR REPLACE FUNCTION safe_add( + a integer, + b integer +) +RETURNS integer +LANGUAGE sql +IMMUTABLE +STRICT +LEAKPROOF +PARALLEL SAFE +AS $$ + SELECT a + b; +$$; diff --git a/testdata/diff/create_function/add_function/new.sql b/testdata/diff/create_function/add_function/new.sql index a884a33f..149b4e5f 100644 --- a/testdata/diff/create_function/add_function/new.sql +++ b/testdata/diff/create_function/add_function/new.sql @@ -12,8 +12,10 @@ CREATE FUNCTION process_order( ) RETURNS numeric LANGUAGE plpgsql -SECURITY DEFINER VOLATILE +PARALLEL RESTRICTED +LEAKPROOF +SECURITY DEFINER STRICT AS $$ DECLARE @@ -26,5 +28,20 @@ $$; -- Table function with RETURN clause (bug report test case) CREATE FUNCTION days_since_special_date() RETURNS SETOF timestamptz - LANGUAGE sql STABLE PARALLEL SAFE - RETURN generate_series(date_trunc('day', '2025-01-01'::timestamp), date_trunc('day', NOW()), '1 day'::interval); \ No newline at end of file + LANGUAGE sql + STABLE + PARALLEL SAFE + LEAKPROOF + RETURN generate_series(date_trunc('day', '2025-01-01'::timestamp), date_trunc('day', NOW()), '1 day'::interval); + +-- Simple pure function demonstrating PARALLEL SAFE + LEAKPROOF +CREATE FUNCTION safe_add(a integer, b integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +LEAKPROOF +STRICT +AS $$ + SELECT a + b; +$$; \ No newline at end of file diff --git a/testdata/diff/create_function/add_function/plan.json b/testdata/diff/create_function/add_function/plan.json index 1991af23..1d57042d 100644 --- a/testdata/diff/create_function/add_function/plan.json +++ b/testdata/diff/create_function/add_function/plan.json @@ -9,16 +9,22 @@ { "steps": [ { - "sql": "CREATE OR REPLACE FUNCTION days_since_special_date()\nRETURNS SETOF timestamp with time zone\nLANGUAGE sql\nSTABLE\nPARALLEL SAFE\nRETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval);", + "sql": "CREATE OR REPLACE FUNCTION days_since_special_date()\nRETURNS SETOF timestamp with time zone\nLANGUAGE sql\nSTABLE\nLEAKPROOF\nPARALLEL SAFE\nRETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval);", "type": "function", "operation": "create", "path": "public.days_since_special_date" }, { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nLEAKPROOF\nPARALLEL RESTRICTED\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.process_order" + }, + { + "sql": "CREATE OR REPLACE FUNCTION safe_add(\n a integer,\n b integer\n)\nRETURNS integer\nLANGUAGE sql\nIMMUTABLE\nSTRICT\nLEAKPROOF\nPARALLEL SAFE\nAS $$\n SELECT a + b;\n$$;", + "type": "function", + "operation": "create", + "path": "public.safe_add" } ] } diff --git a/testdata/diff/create_function/add_function/plan.sql b/testdata/diff/create_function/add_function/plan.sql index fc67ab9a..360840b8 100644 --- a/testdata/diff/create_function/add_function/plan.sql +++ b/testdata/diff/create_function/add_function/plan.sql @@ -2,6 +2,7 @@ CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql STABLE +LEAKPROOF PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); @@ -19,6 +20,8 @@ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER +LEAKPROOF +PARALLEL RESTRICTED AS $$ DECLARE total numeric; @@ -27,3 +30,14 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; + +CREATE OR REPLACE FUNCTION safe_add(a integer, b integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +STRICT +LEAKPROOF +PARALLEL SAFE +AS $$ + SELECT a + b; +$$; diff --git a/testdata/diff/create_function/add_function/plan.txt b/testdata/diff/create_function/add_function/plan.txt index 061b292e..913de35a 100644 --- a/testdata/diff/create_function/add_function/plan.txt +++ b/testdata/diff/create_function/add_function/plan.txt @@ -1,11 +1,12 @@ -Plan: 2 to add. +Plan: 3 to add. Summary by type: - functions: 2 to add + functions: 3 to add Functions: + days_since_special_date + process_order + + safe_add DDL to be executed: -------------------------------------------------- @@ -14,6 +15,7 @@ CREATE OR REPLACE FUNCTION days_since_special_date() RETURNS SETOF timestamp with time zone LANGUAGE sql STABLE +LEAKPROOF PARALLEL SAFE RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); @@ -31,6 +33,8 @@ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER +LEAKPROOF +PARALLEL RESTRICTED AS $$ DECLARE total numeric; @@ -39,3 +43,17 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; + +CREATE OR REPLACE FUNCTION safe_add( + a integer, + b integer +) +RETURNS integer +LANGUAGE sql +IMMUTABLE +STRICT +LEAKPROOF +PARALLEL SAFE +AS $$ + SELECT a + b; +$$; From 299336bde2d81fadd150cf897172e566b1c29217 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:25:20 -0800 Subject: [PATCH 08/14] feat: generate ALTER FUNCTION for LEAKPROOF and PARALLEL changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IsLeakproof and Parallel to functionsEqual comparison - Create functionsEqualExceptAttributes helper to detect attribute-only changes - Generate ALTER FUNCTION PARALLEL {SAFE|UNSAFE|RESTRICTED} when parallel mode changes - Generate ALTER FUNCTION [NOT] LEAKPROOF when leakproof attribute changes - Use CREATE OR REPLACE only when function body or other attributes change This enables efficient migrations that use ALTER FUNCTION for attribute changes instead of recreating the entire function definition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/diff/function.go | 114 ++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/internal/diff/function.go b/internal/diff/function.go index 112e3d48..c3190b4b 100644 --- a/internal/diff/function.go +++ b/internal/diff/function.go @@ -36,18 +36,68 @@ func generateCreateFunctionsSQL(functions []*ir.Function, targetSchema string, c // generateModifyFunctionsSQL generates ALTER FUNCTION statements func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, collector *diffCollector) { for _, diff := range diffs { - sql := generateFunctionSQL(diff.New, targetSchema) - - // Create context for this statement - context := &diffContext{ - Type: DiffTypeFunction, - Operation: DiffOperationAlter, - Path: fmt.Sprintf("%s.%s", diff.New.Schema, diff.New.Name), - Source: diff, - CanRunInTransaction: true, + oldFunc := diff.Old + newFunc := diff.New + + // Check if only LEAKPROOF or PARALLEL attributes changed (not the function body/definition) + onlyAttributesChanged := functionsEqualExceptAttributes(oldFunc, newFunc) + + if onlyAttributesChanged { + // Generate ALTER FUNCTION statements for attribute-only changes + // Check PARALLEL changes + if oldFunc.Parallel != newFunc.Parallel { + stmt := fmt.Sprintf("ALTER FUNCTION %s(%s) PARALLEL %s;", + qualifyEntityName(newFunc.Schema, newFunc.Name, targetSchema), + newFunc.GetArguments(), + newFunc.Parallel) + + context := &diffContext{ + Type: DiffTypeFunction, + Operation: DiffOperationAlter, + Path: fmt.Sprintf("%s.%s", newFunc.Schema, newFunc.Name), + Source: diff, + CanRunInTransaction: true, + } + collector.collect(context, stmt) + } + + // Check LEAKPROOF changes + if oldFunc.IsLeakproof != newFunc.IsLeakproof { + var stmt string + if newFunc.IsLeakproof { + stmt = fmt.Sprintf("ALTER FUNCTION %s(%s) LEAKPROOF;", + qualifyEntityName(newFunc.Schema, newFunc.Name, targetSchema), + newFunc.GetArguments()) + } else { + stmt = fmt.Sprintf("ALTER FUNCTION %s(%s) NOT LEAKPROOF;", + qualifyEntityName(newFunc.Schema, newFunc.Name, targetSchema), + newFunc.GetArguments()) + } + + context := &diffContext{ + Type: DiffTypeFunction, + Operation: DiffOperationAlter, + Path: fmt.Sprintf("%s.%s", newFunc.Schema, newFunc.Name), + Source: diff, + CanRunInTransaction: true, + } + collector.collect(context, stmt) + } + } else { + // Function body or other attributes changed - use CREATE OR REPLACE + sql := generateFunctionSQL(newFunc, targetSchema) + + // Create context for this statement + context := &diffContext{ + Type: DiffTypeFunction, + Operation: DiffOperationAlter, + Path: fmt.Sprintf("%s.%s", newFunc.Schema, newFunc.Name), + Source: diff, + CanRunInTransaction: true, + } + + collector.collect(context, sql) } - - collector.collect(context, sql) } } @@ -244,6 +294,42 @@ func formatFunctionParameter(param *ir.Parameter, includeDefault bool, targetSch return part } +// functionsEqualExceptAttributes compares two functions ignoring LEAKPROOF and PARALLEL attributes +// Used to determine if ALTER FUNCTION can be used instead of CREATE OR REPLACE +func functionsEqualExceptAttributes(old, new *ir.Function) bool { + if old.Schema != new.Schema { + return false + } + if old.Name != new.Name { + return false + } + if old.Definition != new.Definition { + return false + } + if old.ReturnType != new.ReturnType { + return false + } + if old.Language != new.Language { + return false + } + if old.Volatility != new.Volatility { + return false + } + if old.IsStrict != new.IsStrict { + return false + } + if old.IsSecurityDefiner != new.IsSecurityDefiner { + return false + } + // Note: We intentionally do NOT compare IsLeakproof or Parallel here + // That's the whole point - we want to detect when only those attributes changed + + // Compare using normalized Parameters array + oldInputParams := filterNonTableParameters(old.Parameters) + newInputParams := filterNonTableParameters(new.Parameters) + return parametersEqual(oldInputParams, newInputParams) +} + // functionsEqual compares two functions for equality func functionsEqual(old, new *ir.Function) bool { if old.Schema != new.Schema { @@ -270,6 +356,12 @@ func functionsEqual(old, new *ir.Function) bool { if old.IsSecurityDefiner != new.IsSecurityDefiner { return false } + if old.IsLeakproof != new.IsLeakproof { + return false + } + if old.Parallel != new.Parallel { + return false + } // Compare using normalized Parameters array // This ensures type aliases like "character varying" vs "varchar" are treated as equal From a49c9719cb48ed594df9d2197e91a0d8557308a0 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:30:11 -0800 Subject: [PATCH 09/14] test: add alter_function_attributes test case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This test validates that ALTER FUNCTION statements are correctly generated when only LEAKPROOF and PARALLEL attributes change (no function body changes). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../alter_function_attributes/diff.sql | 7 +++++++ .../alter_function_attributes/new.sql | 21 +++++++++++++++++++ .../alter_function_attributes/old.sql | 17 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 testdata/diff/create_function/alter_function_attributes/diff.sql create mode 100644 testdata/diff/create_function/alter_function_attributes/new.sql create mode 100644 testdata/diff/create_function/alter_function_attributes/old.sql diff --git a/testdata/diff/create_function/alter_function_attributes/diff.sql b/testdata/diff/create_function/alter_function_attributes/diff.sql new file mode 100644 index 00000000..1125e2e6 --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/diff.sql @@ -0,0 +1,7 @@ +ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE; + +ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF; + +ALTER FUNCTION process_data(text) PARALLEL SAFE; + +ALTER FUNCTION process_data(text) LEAKPROOF; diff --git a/testdata/diff/create_function/alter_function_attributes/new.sql b/testdata/diff/create_function/alter_function_attributes/new.sql new file mode 100644 index 00000000..1fbb936b --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/new.sql @@ -0,0 +1,21 @@ +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +PARALLEL SAFE +LEAKPROOF +AS $$ +BEGIN + RETURN upper(input); +END; +$$; + +CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) +RETURNS numeric +LANGUAGE sql +STABLE +PARALLEL SAFE +LEAKPROOF +AS $$ + SELECT amount * (1 + tax_rate); +$$; diff --git a/testdata/diff/create_function/alter_function_attributes/old.sql b/testdata/diff/create_function/alter_function_attributes/old.sql new file mode 100644 index 00000000..5db2146d --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/old.sql @@ -0,0 +1,17 @@ +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +AS $$ +BEGIN + RETURN upper(input); +END; +$$; + +CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) +RETURNS numeric +LANGUAGE sql +STABLE +AS $$ + SELECT amount * (1 + tax_rate); +$$; From c8bfb75131d0b3b8f72176eae5695527d9b414b0 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:34:17 -0800 Subject: [PATCH 10/14] test: add alter_function_attributes test case and update fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create new test case for ALTER FUNCTION attribute changes - Regenerate add_function fixtures with LEAKPROOF and PARALLEL - Validates ALTER FUNCTION generation for attribute-only changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../create_function/add_function/plan.sql | 5 ++- .../alter_function_attributes/plan.json | 38 +++++++++++++++++++ .../alter_function_attributes/plan.sql | 7 ++++ .../alter_function_attributes/plan.txt | 21 ++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 testdata/diff/create_function/alter_function_attributes/plan.json create mode 100644 testdata/diff/create_function/alter_function_attributes/plan.sql create mode 100644 testdata/diff/create_function/alter_function_attributes/plan.txt diff --git a/testdata/diff/create_function/add_function/plan.sql b/testdata/diff/create_function/add_function/plan.sql index 360840b8..f512f439 100644 --- a/testdata/diff/create_function/add_function/plan.sql +++ b/testdata/diff/create_function/add_function/plan.sql @@ -31,7 +31,10 @@ BEGIN END; $$; -CREATE OR REPLACE FUNCTION safe_add(a integer, b integer) +CREATE OR REPLACE FUNCTION safe_add( + a integer, + b integer +) RETURNS integer LANGUAGE sql IMMUTABLE diff --git a/testdata/diff/create_function/alter_function_attributes/plan.json b/testdata/diff/create_function/alter_function_attributes/plan.json new file mode 100644 index 00000000..66d91aea --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/plan.json @@ -0,0 +1,38 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.4.3", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "547c251b85c0364b36816db73820a51c5980be1979e614ead337b3f93052cb1c" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE;", + "type": "function", + "operation": "alter", + "path": "public.calculate_total" + }, + { + "sql": "ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF;", + "type": "function", + "operation": "alter", + "path": "public.calculate_total" + }, + { + "sql": "ALTER FUNCTION process_data(text) PARALLEL SAFE;", + "type": "function", + "operation": "alter", + "path": "public.process_data" + }, + { + "sql": "ALTER FUNCTION process_data(text) LEAKPROOF;", + "type": "function", + "operation": "alter", + "path": "public.process_data" + } + ] + } + ] +} diff --git a/testdata/diff/create_function/alter_function_attributes/plan.sql b/testdata/diff/create_function/alter_function_attributes/plan.sql new file mode 100644 index 00000000..1125e2e6 --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/plan.sql @@ -0,0 +1,7 @@ +ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE; + +ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF; + +ALTER FUNCTION process_data(text) PARALLEL SAFE; + +ALTER FUNCTION process_data(text) LEAKPROOF; diff --git a/testdata/diff/create_function/alter_function_attributes/plan.txt b/testdata/diff/create_function/alter_function_attributes/plan.txt new file mode 100644 index 00000000..bff6b852 --- /dev/null +++ b/testdata/diff/create_function/alter_function_attributes/plan.txt @@ -0,0 +1,21 @@ +Plan: 4 to modify. + +Summary by type: + functions: 4 to modify + +Functions: + ~ calculate_total + ~ calculate_total + ~ process_data + ~ process_data + +DDL to be executed: +-------------------------------------------------- + +ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE; + +ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF; + +ALTER FUNCTION process_data(text) PARALLEL SAFE; + +ALTER FUNCTION process_data(text) LEAKPROOF; From c694cd0b7a6ec45ffe1bace24237fd0e8cb492eb Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Thu, 27 Nov 2025 11:39:13 -0800 Subject: [PATCH 11/14] docs: add implementation plan for LEAKPROOF and PARALLEL support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../2025-11-27-function-leakproof-parallel.md | 722 ++++++++++++++++++ 1 file changed, 722 insertions(+) create mode 100644 docs/plans/2025-11-27-function-leakproof-parallel.md diff --git a/docs/plans/2025-11-27-function-leakproof-parallel.md b/docs/plans/2025-11-27-function-leakproof-parallel.md new file mode 100644 index 00000000..05566f4b --- /dev/null +++ b/docs/plans/2025-11-27-function-leakproof-parallel.md @@ -0,0 +1,722 @@ +# Function LEAKPROOF and PARALLEL Support Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add full support for PostgreSQL function LEAKPROOF and PARALLEL attributes (SAFE/UNSAFE/RESTRICTED) throughout the dump/plan/apply workflow. + +**Architecture:** Extend Function IR with two new fields, update database inspector to query pg_catalog.pg_proc, modify dump formatter to output non-default attributes, and enhance diff logic to generate ALTER FUNCTION migrations for attribute changes. + +**Tech Stack:** Go 1.24+, pgx/v5 for database queries, embedded-postgres for testing, PostgreSQL 14-17 + +--- + +## Task 1: Update IR Structure + +**Files:** +- Modify: `ir/ir.go:124-136` (Function struct) + +**Step 1: Add new fields to Function struct** + +Add `IsLeakproof` and `Parallel` fields after `IsSecurityDefiner`: + +```go +// Function represents a database function +type Function struct { + Schema string `json:"schema"` + Name string `json:"name"` + Definition string `json:"definition"` + ReturnType string `json:"return_type"` + Language string `json:"language"` + Parameters []*Parameter `json:"parameters,omitempty"` + Comment string `json:"comment,omitempty"` + Volatility string `json:"volatility,omitempty"` // IMMUTABLE, STABLE, VOLATILE + IsStrict bool `json:"is_strict,omitempty"` // STRICT or null behavior + IsSecurityDefiner bool `json:"is_security_definer,omitempty"` // SECURITY DEFINER + IsLeakproof bool `json:"is_leakproof,omitempty"` // LEAKPROOF + Parallel string `json:"parallel,omitempty"` // SAFE, UNSAFE, RESTRICTED +} +``` + +**Step 2: Verify code compiles** + +Run: `go build -o pgschema .` +Expected: Successful compilation (new fields don't break anything) + +**Step 3: Commit IR changes** + +```bash +git add ir/ir.go +git commit -m "feat: add IsLeakproof and Parallel fields to Function IR" +``` + +--- + +## Task 2: Update Database Inspector + +**Files:** +- Modify: `ir/inspector.go` (inspectFunctions method) + +**Step 1: Locate the inspectFunctions query** + +Find the SELECT query in `inspectFunctions()` that queries `pg_catalog.pg_proc`. It should be around line 400-500. + +**Step 2: Add proleakproof and proparallel to SELECT** + +Add these columns to the existing SELECT statement: + +```go +p.proleakproof, +p.proparallel +``` + +The query should look like: +```go +query := ` +SELECT + n.nspname AS schema_name, + p.proname AS function_name, + pg_get_functiondef(p.oid) AS definition, + pg_get_function_result(p.oid) AS return_type, + l.lanname AS language, + p.provolatile, + p.proisstrict, + p.prosecdef, + p.proleakproof, + p.proparallel, + obj_description(p.oid, 'pg_proc') AS comment +FROM pg_catalog.pg_proc p +... +` +``` + +**Step 3: Add variables to scan into** + +In the scan section, add variables: + +```go +var ( + // ... existing variables ... + proleakproof bool + proparallel string +) +``` + +**Step 4: Add to Scan() call** + +Add to the existing `rows.Scan()`: + +```go +&proleakproof, +&proparallel, +``` + +**Step 5: Map proparallel to Parallel field** + +After the existing volatility mapping, add: + +```go +// Map LEAKPROOF +fn.IsLeakproof = proleakproof + +// Map PARALLEL +switch proparallel { +case "s": + fn.Parallel = "SAFE" +case "r": + fn.Parallel = "RESTRICTED" +case "u": + fn.Parallel = "UNSAFE" +default: + fn.Parallel = "UNSAFE" // Defensive default +} +``` + +**Step 6: Verify code compiles** + +Run: `go build -o pgschema .` +Expected: Successful compilation + +**Step 7: Commit inspector changes** + +```bash +git add ir/inspector.go +git commit -m "feat: extract LEAKPROOF and PARALLEL from pg_catalog.pg_proc" +``` + +--- + +## Task 3: Update Dump Formatter + +**Files:** +- Modify: `internal/dump/dump.go` (function formatting) + +**Step 1: Locate function dump logic** + +Find the function that formats function definitions for dump output. Look for code that builds the CREATE FUNCTION statement. Should be in a method like `dumpFunction()` or similar. + +**Step 2: Add PARALLEL output logic** + +After the LANGUAGE and volatility output, add: + +```go +// Add PARALLEL if not default (UNSAFE) +if fn.Parallel == "SAFE" { + fmt.Fprintf(buf, "PARALLEL SAFE\n") +} else if fn.Parallel == "RESTRICTED" { + fmt.Fprintf(buf, "PARALLEL RESTRICTED\n") +} +// Note: Don't output PARALLEL UNSAFE (it's the default) +``` + +**Step 3: Add LEAKPROOF output logic** + +After PARALLEL, add: + +```go +// Add LEAKPROOF if true +if fn.IsLeakproof { + fmt.Fprintf(buf, "LEAKPROOF\n") +} +// Note: Don't output NOT LEAKPROOF (it's the default) +``` + +The attribute order should be: +1. LANGUAGE +2. Volatility (IMMUTABLE/STABLE/VOLATILE) +3. PARALLEL (if not UNSAFE) +4. LEAKPROOF (if true) +5. STRICT (if true) +6. SECURITY DEFINER (if true) + +**Step 4: Test dump output manually** + +Run: +```bash +go build -o pgschema . +PGPASSWORD='testpwd1' ./pgschema dump -h localhost -p 5432 -U postgres -d postgres --schema public +``` + +Expected: Should compile and run (may not show new attributes yet if test DB doesn't have them) + +**Step 5: Commit dump formatter changes** + +```bash +git add internal/dump/dump.go +git commit -m "feat: output LEAKPROOF and PARALLEL in function dumps" +``` + +--- + +## Task 4: Create Test Case - add_function Enhancement + +**Files:** +- Modify: `testdata/diff/create_function/add_function/new.sql` +- Modify: `testdata/diff/create_function/add_function/plan.sql` + +**Step 1: Update new.sql with LEAKPROOF and PARALLEL** + +Modify the existing functions to add attributes: + +```sql +CREATE FUNCTION process_order( + order_id integer, + -- Simple numeric defaults + discount_percent numeric DEFAULT 0, + priority_level integer DEFAULT 1, + -- String defaults + note varchar DEFAULT '', + status text DEFAULT 'pending', + -- Boolean defaults + apply_tax boolean DEFAULT true, + is_priority boolean DEFAULT false +) +RETURNS numeric +LANGUAGE plpgsql +VOLATILE +PARALLEL RESTRICTED +LEAKPROOF +SECURITY DEFINER +STRICT +AS $$ +DECLARE + total numeric; +BEGIN + SELECT amount INTO total FROM orders WHERE id = order_id; + RETURN total - (total * discount_percent / 100); +END; +$$; + +-- Table function with RETURN clause (bug report test case) +CREATE FUNCTION days_since_special_date() RETURNS SETOF timestamptz + LANGUAGE sql + STABLE + PARALLEL SAFE + LEAKPROOF + RETURN generate_series(date_trunc('day', '2025-01-01'::timestamp), date_trunc('day', NOW()), '1 day'::interval); + +-- Simple pure function demonstrating PARALLEL SAFE + LEAKPROOF +CREATE FUNCTION safe_add(a integer, b integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +LEAKPROOF +STRICT +AS $$ + SELECT a + b; +$$; +``` + +**Step 2: Update plan.sql to match expected output** + +Update `plan.sql` to show the normalized CREATE FUNCTION statements that pgschema will generate (with proper attribute ordering): + +```sql +CREATE OR REPLACE FUNCTION days_since_special_date() +RETURNS SETOF timestamp with time zone +LANGUAGE sql +STABLE +PARALLEL SAFE +LEAKPROOF +RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); + +CREATE OR REPLACE FUNCTION process_order( + order_id integer, + discount_percent numeric DEFAULT 0, + priority_level integer DEFAULT 1, + note varchar DEFAULT '', + status text DEFAULT 'pending', + apply_tax boolean DEFAULT true, + is_priority boolean DEFAULT false +) +RETURNS numeric +LANGUAGE plpgsql +VOLATILE +PARALLEL RESTRICTED +LEAKPROOF +SECURITY DEFINER +STRICT +AS $$ +DECLARE + total numeric; +BEGIN + SELECT amount INTO total FROM orders WHERE id = order_id; + RETURN total - (total * discount_percent / 100); +END; +$$; + +CREATE OR REPLACE FUNCTION safe_add(a integer, b integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +LEAKPROOF +STRICT +AS $$ + SELECT a + b; +$$; +``` + +**Step 3: Run diff test to see current output** + +Run: `PGSCHEMA_TEST_FILTER="create_function/add_function" go test -v ./internal/diff -run TestDiffFromFiles` +Expected: Test may fail initially (dump output may not match yet due to formatting details) + +**Step 4: Commit test case updates** + +```bash +git add testdata/diff/create_function/add_function/new.sql testdata/diff/create_function/add_function/plan.sql +git commit -m "test: add LEAKPROOF and PARALLEL to add_function test case" +``` + +--- + +## Task 5: Update Diff Logic for ALTER FUNCTION + +**Files:** +- Modify: `internal/diff/function.go` + +**Step 1: Locate function comparison logic** + +Find the code that compares old vs new functions with the same signature. Look for logic that checks if functions differ. + +**Step 2: Add LEAKPROOF comparison** + +Add after existing attribute comparisons (like volatility, strict, security definer): + +```go +// Check LEAKPROOF changes +if oldFunc.IsLeakproof != newFunc.IsLeakproof { + var stmt string + if newFunc.IsLeakproof { + stmt = fmt.Sprintf("ALTER FUNCTION %s.%s(%s) LEAKPROOF;", + QuoteIdentifier(newFunc.Schema), + QuoteIdentifier(newFunc.Name), + newFunc.GetArguments()) + } else { + stmt = fmt.Sprintf("ALTER FUNCTION %s.%s(%s) NOT LEAKPROOF;", + QuoteIdentifier(newFunc.Schema), + QuoteIdentifier(newFunc.Name), + newFunc.GetArguments()) + } + steps = append(steps, MigrationStep{ + Type: "ALTER_FUNCTION_LEAKPROOF", + Schema: newFunc.Schema, + Name: newFunc.Name, + SQL: stmt, + Description: fmt.Sprintf("Alter function %s.%s LEAKPROOF", newFunc.Schema, newFunc.Name), + }) +} +``` + +**Step 3: Add PARALLEL comparison** + +Add after LEAKPROOF: + +```go +// Check PARALLEL changes +if oldFunc.Parallel != newFunc.Parallel { + stmt := fmt.Sprintf("ALTER FUNCTION %s.%s(%s) PARALLEL %s;", + QuoteIdentifier(newFunc.Schema), + QuoteIdentifier(newFunc.Name), + newFunc.GetArguments(), + newFunc.Parallel) + steps = append(steps, MigrationStep{ + Type: "ALTER_FUNCTION_PARALLEL", + Schema: newFunc.Schema, + Name: newFunc.Name, + SQL: stmt, + Description: fmt.Sprintf("Alter function %s.%s PARALLEL %s", newFunc.Schema, newFunc.Name, newFunc.Parallel), + }) +} +``` + +**Step 4: Verify code compiles** + +Run: `go build -o pgschema .` +Expected: Successful compilation + +**Step 5: Commit diff logic** + +```bash +git add internal/diff/function.go +git commit -m "feat: generate ALTER FUNCTION for LEAKPROOF and PARALLEL changes" +``` + +--- + +## Task 6: Create Test Case - alter_function_attributes + +**Files:** +- Create: `testdata/diff/create_function/alter_function_attributes/old.sql` +- Create: `testdata/diff/create_function/alter_function_attributes/new.sql` +- Create: `testdata/diff/create_function/alter_function_attributes/plan.sql` + +**Step 1: Create test directory** + +Run: `mkdir -p testdata/diff/create_function/alter_function_attributes` + +**Step 2: Write old.sql (function without attributes)** + +Create `testdata/diff/create_function/alter_function_attributes/old.sql`: + +```sql +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +AS $$ +BEGIN + RETURN upper(input); +END; +$$; + +CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) +RETURNS numeric +LANGUAGE sql +STABLE +AS $$ + SELECT amount * (1 + tax_rate); +$$; +``` + +**Step 3: Write new.sql (add LEAKPROOF and PARALLEL)** + +Create `testdata/diff/create_function/alter_function_attributes/new.sql`: + +```sql +CREATE FUNCTION process_data(input text) +RETURNS text +LANGUAGE plpgsql +VOLATILE +PARALLEL SAFE +LEAKPROOF +AS $$ +BEGIN + RETURN upper(input); +END; +$$; + +CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) +RETURNS numeric +LANGUAGE sql +STABLE +PARALLEL SAFE +LEAKPROOF +AS $$ + SELECT amount * (1 + tax_rate); +$$; +``` + +**Step 4: Write plan.sql (expected ALTER statements)** + +Create `testdata/diff/create_function/alter_function_attributes/plan.sql`: + +```sql +ALTER FUNCTION process_data(text) PARALLEL SAFE; + +ALTER FUNCTION process_data(text) LEAKPROOF; + +ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE; + +ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF; +``` + +**Step 5: Run diff test** + +Run: `PGSCHEMA_TEST_FILTER="create_function/alter_function_attributes" go test -v ./internal/diff -run TestDiffFromFiles` +Expected: Test should pass if diff logic is correct + +**Step 6: Commit new test case** + +```bash +git add testdata/diff/create_function/alter_function_attributes/ +git commit -m "test: add alter_function_attributes test case" +``` + +--- + +## Task 7: Run All Function Diff Tests + +**Files:** +- N/A (testing only) + +**Step 1: Run all function diff tests** + +Run: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./internal/diff -run TestDiffFromFiles` +Expected: All tests pass + +**Step 2: If tests fail, check test fixtures** + +Common issues: +- Attribute ordering doesn't match expected output +- Missing quote identifiers +- Incorrect normalization of function signatures + +Fix by adjusting dump formatter or test fixture files to match actual pg_dump behavior. + +**Step 3: Regenerate expected files if needed** + +If the logic is correct but expected files need updating, use the test framework's regeneration option (if available) or manually update `plan.sql` files to match actual output. + +--- + +## Task 8: Run Integration Tests + +**Files:** +- N/A (testing only) + +**Step 1: Run function integration tests** + +Run: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./cmd -run TestPlanAndApply -timeout 5m` +Expected: All tests pass (creates embedded postgres, applies migrations, validates) + +**Step 2: Check for transaction handling** + +Ensure ALTER FUNCTION statements execute correctly within transactions (they should - it's a DDL but doesn't require --pgschema-no-transaction). + +**Step 3: If tests fail, debug** + +Use @superpowers:systematic-debugging skill: +- Check embedded postgres logs +- Verify SQL syntax +- Confirm pg_catalog queries work on test postgres version + +**Step 4: Verify across postgres versions** + +Run tests against all supported versions: + +```bash +PGSCHEMA_POSTGRES_VERSION=14 go test -v ./cmd -run TestPlanAndApply/create_function +PGSCHEMA_POSTGRES_VERSION=15 go test -v ./cmd -run TestPlanAndApply/create_function +PGSCHEMA_POSTGRES_VERSION=16 go test -v ./cmd -run TestPlanAndApply/create_function +PGSCHEMA_POSTGRES_VERSION=17 go test -v ./cmd -run TestPlanAndApply/create_function +``` + +Expected: All pass (LEAKPROOF and PARALLEL supported in all versions 14+) + +--- + +## Task 9: Validate Against Live Database + +**Files:** +- N/A (manual validation) + +**Step 1: Use validate_db skill** + +Run @validate_db skill to connect to live PostgreSQL and compare outputs. + +**Step 2: Create test function in live database** + +Connect to test database and create a function with attributes: + +```sql +CREATE FUNCTION test_leakproof_parallel(x integer) +RETURNS integer +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +LEAKPROOF +STRICT +AS $$ + SELECT x * 2; +$$; +``` + +**Step 3: Compare pg_dump vs pgschema dump** + +Run both: +```bash +PGPASSWORD='testpwd1' pg_dump -h localhost -p 5432 -U postgres -d postgres --schema-only --schema=public | grep -A 10 "test_leakproof_parallel" + +PGPASSWORD='testpwd1' ./pgschema dump -h localhost -p 5432 -U postgres -d postgres --schema public | grep -A 10 "test_leakproof_parallel" +``` + +Expected: Output should be identical (attribute order and formatting match) + +**Step 4: Test ALTER FUNCTION migration** + +1. Create function without attributes +2. Run pgschema plan with new.sql that adds attributes +3. Verify plan shows ALTER FUNCTION statements +4. Run pgschema apply +5. Verify function has new attributes in database + +```bash +# Setup +PGPASSWORD='testpwd1' psql -h localhost -p 5432 -U postgres -d postgres -c " +CREATE OR REPLACE FUNCTION test_alter(x int) RETURNS int LANGUAGE sql AS 'SELECT x'; +" + +# Create desired state file +echo "CREATE FUNCTION test_alter(x int) RETURNS int LANGUAGE sql PARALLEL SAFE LEAKPROOF AS 'SELECT x';" > /tmp/test.sql + +# Plan (using embedded postgres by default) +./pgschema plan --schema public /tmp/test.sql + +# Should show: +# ALTER FUNCTION test_alter(integer) PARALLEL SAFE; +# ALTER FUNCTION test_alter(integer) LEAKPROOF; + +# Apply +./pgschema apply --schema public /tmp/test.sql -h localhost -p 5432 -U postgres -d postgres + +# Verify +PGPASSWORD='testpwd1' psql -h localhost -p 5432 -U postgres -d postgres -c " +SELECT proname, proparallel, proleakproof +FROM pg_proc +WHERE proname = 'test_alter'; +" +``` + +Expected: proparallel='s', proleakproof=true + +--- + +## Task 10: Run Full Test Suite + +**Files:** +- N/A (testing only) + +**Step 1: Run all tests** + +Run: `go test -v ./...` +Expected: All tests pass (no regressions) + +**Step 2: Check test coverage** + +Run: `go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out` +Expected: New code paths are covered by tests + +**Step 3: Fix any regressions** + +If any existing tests fail, use @superpowers:systematic-debugging to identify and fix issues. + +--- + +## Task 11: Final Commit and Cleanup + +**Files:** +- All modified files + +**Step 1: Review all changes** + +Run: `git status && git diff` +Expected: Only intended changes present + +**Step 2: Run final verification** + +```bash +go build -o pgschema . +go test -v ./... +``` + +Expected: Clean build, all tests pass + +**Step 3: Create final commit if needed** + +If there are uncommitted changes: + +```bash +git add -A +git commit -m "feat: complete LEAKPROOF and PARALLEL function support + +- Add IsLeakproof and Parallel fields to Function IR +- Extract attributes from pg_catalog.pg_proc +- Output attributes in dump (non-defaults only) +- Generate ALTER FUNCTION for attribute changes +- Add comprehensive test cases + +Supports all three parallel modes: SAFE, UNSAFE, RESTRICTED +Validated against PostgreSQL 14-17" +``` + +**Step 4: Verify git log** + +Run: `git log --oneline -10` +Expected: Clean commit history with descriptive messages + +--- + +## Success Criteria + +- ✅ All function diff tests pass +- ✅ All function integration tests pass +- ✅ Tests pass on PostgreSQL 14, 15, 16, 17 +- ✅ pg_dump output matches pgschema output for LEAKPROOF/PARALLEL +- ✅ ALTER FUNCTION migrations execute successfully +- ✅ No regressions in existing function tests +- ✅ Manual validation against live database successful + +## Skills Referenced + +- @superpowers:systematic-debugging - For debugging test failures +- @validate_db - For live database validation +- @run_tests - For running pgschema test suite +- @pg_dump - For comparing output with pg_dump reference + +## Notes + +- LEAKPROOF and PARALLEL are supported in PostgreSQL 9.2+ and 9.6+ respectively (well within our 14-17 support range) +- ALTER FUNCTION for these attributes is transactional (no special handling needed) +- The hybrid approach (store explicit, output non-defaults) ensures clean diffs and readable dumps From 81c786676275e6d73098e2984e07d7e2ad1a6b07 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Fri, 28 Nov 2025 08:54:32 -0800 Subject: [PATCH 12/14] refactor: improve add_function test case clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure test functions for better separation of concerns: 1. process_order - Complex case with all qualifiers (VOLATILE, STRICT, SECURITY DEFINER, LEAKPROOF, PARALLEL RESTRICTED) 2. calculate_tax - Tests PARALLEL SAFE only (IMMUTABLE, PARALLEL SAFE) 3. mask_sensitive_data - Tests LEAKPROOF only (STABLE, LEAKPROOF) This makes it clearer what each function is testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../create_function/add_function/diff.sql | 37 +++++++++-------- .../diff/create_function/add_function/new.sql | 33 +++++++-------- .../create_function/add_function/plan.json | 12 +++--- .../create_function/add_function/plan.sql | 37 +++++++++-------- .../create_function/add_function/plan.txt | 41 ++++++++++--------- 5 files changed, 82 insertions(+), 78 deletions(-) diff --git a/testdata/diff/create_function/add_function/diff.sql b/testdata/diff/create_function/add_function/diff.sql index f512f439..b1bf99f6 100644 --- a/testdata/diff/create_function/add_function/diff.sql +++ b/testdata/diff/create_function/add_function/diff.sql @@ -1,10 +1,25 @@ -CREATE OR REPLACE FUNCTION days_since_special_date() -RETURNS SETOF timestamp with time zone +CREATE OR REPLACE FUNCTION calculate_tax( + amount numeric, + rate numeric +) +RETURNS numeric +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +AS $$ + SELECT amount * rate; +$$; + +CREATE OR REPLACE FUNCTION mask_sensitive_data( + input text +) +RETURNS text LANGUAGE sql STABLE LEAKPROOF -PARALLEL SAFE -RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); +AS $$ + SELECT '***' || substring(input from 4); +$$; CREATE OR REPLACE FUNCTION process_order( order_id integer, @@ -30,17 +45,3 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; - -CREATE OR REPLACE FUNCTION safe_add( - a integer, - b integer -) -RETURNS integer -LANGUAGE sql -IMMUTABLE -STRICT -LEAKPROOF -PARALLEL SAFE -AS $$ - SELECT a + b; -$$; diff --git a/testdata/diff/create_function/add_function/new.sql b/testdata/diff/create_function/add_function/new.sql index 149b4e5f..f6d6bbc8 100644 --- a/testdata/diff/create_function/add_function/new.sql +++ b/testdata/diff/create_function/add_function/new.sql @@ -1,3 +1,4 @@ +-- Complex function demonstrating all qualifiers CREATE FUNCTION process_order( order_id integer, -- Simple numeric defaults @@ -13,10 +14,10 @@ CREATE FUNCTION process_order( RETURNS numeric LANGUAGE plpgsql VOLATILE -PARALLEL RESTRICTED -LEAKPROOF -SECURITY DEFINER STRICT +SECURITY DEFINER +LEAKPROOF +PARALLEL RESTRICTED AS $$ DECLARE total numeric; @@ -26,22 +27,22 @@ BEGIN END; $$; --- Table function with RETURN clause (bug report test case) -CREATE FUNCTION days_since_special_date() RETURNS SETOF timestamptz - LANGUAGE sql - STABLE - PARALLEL SAFE - LEAKPROOF - RETURN generate_series(date_trunc('day', '2025-01-01'::timestamp), date_trunc('day', NOW()), '1 day'::interval); - --- Simple pure function demonstrating PARALLEL SAFE + LEAKPROOF -CREATE FUNCTION safe_add(a integer, b integer) -RETURNS integer +-- Function testing PARALLEL SAFE only +CREATE FUNCTION calculate_tax(amount numeric, rate numeric) +RETURNS numeric LANGUAGE sql IMMUTABLE PARALLEL SAFE +AS $$ + SELECT amount * rate; +$$; + +-- Function testing LEAKPROOF only +CREATE FUNCTION mask_sensitive_data(input text) +RETURNS text +LANGUAGE sql +STABLE LEAKPROOF -STRICT AS $$ - SELECT a + b; + SELECT '***' || substring(input from 4); $$; \ No newline at end of file diff --git a/testdata/diff/create_function/add_function/plan.json b/testdata/diff/create_function/add_function/plan.json index 1d57042d..326ffd75 100644 --- a/testdata/diff/create_function/add_function/plan.json +++ b/testdata/diff/create_function/add_function/plan.json @@ -9,22 +9,22 @@ { "steps": [ { - "sql": "CREATE OR REPLACE FUNCTION days_since_special_date()\nRETURNS SETOF timestamp with time zone\nLANGUAGE sql\nSTABLE\nLEAKPROOF\nPARALLEL SAFE\nRETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval);", + "sql": "CREATE OR REPLACE FUNCTION calculate_tax(\n amount numeric,\n rate numeric\n)\nRETURNS numeric\nLANGUAGE sql\nIMMUTABLE\nPARALLEL SAFE\nAS $$\n SELECT amount * rate;\n$$;", "type": "function", "operation": "create", - "path": "public.days_since_special_date" + "path": "public.calculate_tax" }, { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nLEAKPROOF\nPARALLEL RESTRICTED\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION mask_sensitive_data(\n input text\n)\nRETURNS text\nLANGUAGE sql\nSTABLE\nLEAKPROOF\nAS $$\n SELECT '***' || substring(input from 4);\n$$;", "type": "function", "operation": "create", - "path": "public.process_order" + "path": "public.mask_sensitive_data" }, { - "sql": "CREATE OR REPLACE FUNCTION safe_add(\n a integer,\n b integer\n)\nRETURNS integer\nLANGUAGE sql\nIMMUTABLE\nSTRICT\nLEAKPROOF\nPARALLEL SAFE\nAS $$\n SELECT a + b;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nLEAKPROOF\nPARALLEL RESTRICTED\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", "type": "function", "operation": "create", - "path": "public.safe_add" + "path": "public.process_order" } ] } diff --git a/testdata/diff/create_function/add_function/plan.sql b/testdata/diff/create_function/add_function/plan.sql index f512f439..b1bf99f6 100644 --- a/testdata/diff/create_function/add_function/plan.sql +++ b/testdata/diff/create_function/add_function/plan.sql @@ -1,10 +1,25 @@ -CREATE OR REPLACE FUNCTION days_since_special_date() -RETURNS SETOF timestamp with time zone +CREATE OR REPLACE FUNCTION calculate_tax( + amount numeric, + rate numeric +) +RETURNS numeric +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +AS $$ + SELECT amount * rate; +$$; + +CREATE OR REPLACE FUNCTION mask_sensitive_data( + input text +) +RETURNS text LANGUAGE sql STABLE LEAKPROOF -PARALLEL SAFE -RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); +AS $$ + SELECT '***' || substring(input from 4); +$$; CREATE OR REPLACE FUNCTION process_order( order_id integer, @@ -30,17 +45,3 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; - -CREATE OR REPLACE FUNCTION safe_add( - a integer, - b integer -) -RETURNS integer -LANGUAGE sql -IMMUTABLE -STRICT -LEAKPROOF -PARALLEL SAFE -AS $$ - SELECT a + b; -$$; diff --git a/testdata/diff/create_function/add_function/plan.txt b/testdata/diff/create_function/add_function/plan.txt index 913de35a..e8bed1ad 100644 --- a/testdata/diff/create_function/add_function/plan.txt +++ b/testdata/diff/create_function/add_function/plan.txt @@ -4,20 +4,35 @@ Summary by type: functions: 3 to add Functions: - + days_since_special_date + + calculate_tax + + mask_sensitive_data + process_order - + safe_add DDL to be executed: -------------------------------------------------- -CREATE OR REPLACE FUNCTION days_since_special_date() -RETURNS SETOF timestamp with time zone +CREATE OR REPLACE FUNCTION calculate_tax( + amount numeric, + rate numeric +) +RETURNS numeric +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +AS $$ + SELECT amount * rate; +$$; + +CREATE OR REPLACE FUNCTION mask_sensitive_data( + input text +) +RETURNS text LANGUAGE sql STABLE LEAKPROOF -PARALLEL SAFE -RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); +AS $$ + SELECT '***' || substring(input from 4); +$$; CREATE OR REPLACE FUNCTION process_order( order_id integer, @@ -43,17 +58,3 @@ BEGIN RETURN total - (total * discount_percent / 100); END; $$; - -CREATE OR REPLACE FUNCTION safe_add( - a integer, - b integer -) -RETURNS integer -LANGUAGE sql -IMMUTABLE -STRICT -LEAKPROOF -PARALLEL SAFE -AS $$ - SELECT a + b; -$$; From 1e3c1d2eafa20a775567bfa9382ce3fb2ed80ffd Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Fri, 28 Nov 2025 09:10:56 -0800 Subject: [PATCH 13/14] chore: remove plan --- ...1-27-function-leakproof-parallel-design.md | 233 ------ .../2025-11-27-function-leakproof-parallel.md | 722 ------------------ 2 files changed, 955 deletions(-) delete mode 100644 docs/plans/2025-11-27-function-leakproof-parallel-design.md delete mode 100644 docs/plans/2025-11-27-function-leakproof-parallel.md diff --git a/docs/plans/2025-11-27-function-leakproof-parallel-design.md b/docs/plans/2025-11-27-function-leakproof-parallel-design.md deleted file mode 100644 index f3d584e6..00000000 --- a/docs/plans/2025-11-27-function-leakproof-parallel-design.md +++ /dev/null @@ -1,233 +0,0 @@ -# Function LEAKPROOF and PARALLEL Support Design - -**Date:** 2025-11-27 -**Status:** Approved -**Scope:** Add full support for LEAKPROOF and PARALLEL attributes on PostgreSQL functions - -## Overview - -Extend pgschema to properly handle PostgreSQL function attributes LEAKPROOF and PARALLEL (SAFE/UNSAFE/RESTRICTED) throughout the dump/plan/apply workflow. This includes IR representation, database inspection, dump formatting, and migration generation. - -## Requirements - -### Functional Requirements - -1. **Complete PARALLEL support**: All three PostgreSQL parallel safety levels - - `PARALLEL SAFE` - Function can run in parallel workers - - `PARALLEL UNSAFE` - Function cannot run in parallel (default) - - `PARALLEL RESTRICTED` - Can run in parallel but restricted to leader - -2. **LEAKPROOF support**: Boolean attribute indicating function won't leak argument information - - Important for row-level security contexts - - Defaults to false in PostgreSQL - -3. **Migration detection**: Generate ALTER FUNCTION statements when attributes change - - `ALTER FUNCTION ... LEAKPROOF` / `NOT LEAKPROOF` - - `ALTER FUNCTION ... PARALLEL {SAFE|UNSAFE|RESTRICTED}` - -4. **Hybrid output approach**: - - Store explicit values in IR (no ambiguity in comparisons) - - Output only non-default values in dumps (clean, readable) - - Matches PostgreSQL conventions and existing pgschema patterns - -## Design - -### 1. IR Structure Changes - -**File:** `ir/ir.go` - -Add two new fields to the `Function` struct: - -```go -type Function struct { - Schema string `json:"schema"` - Name string `json:"name"` - Definition string `json:"definition"` - ReturnType string `json:"return_type"` - Language string `json:"language"` - Parameters []*Parameter `json:"parameters,omitempty"` - Comment string `json:"comment,omitempty"` - Volatility string `json:"volatility,omitempty"` - IsStrict bool `json:"is_strict,omitempty"` - IsSecurityDefiner bool `json:"is_security_definer,omitempty"` - IsLeakproof bool `json:"is_leakproof,omitempty"` // NEW - Parallel string `json:"parallel,omitempty"` // NEW -} -``` - -**Field specifications:** -- `IsLeakproof`: Boolean, defaults to `false` (matches PostgreSQL default) -- `Parallel`: String with valid values `"SAFE"`, `"UNSAFE"`, `"RESTRICTED"`, defaults to `"UNSAFE"` -- Both use `omitempty` JSON tag for clean serialization - -### 2. Database Inspector Changes - -**File:** `ir/inspector.go` - -Update `inspectFunctions()` to extract attributes from `pg_catalog.pg_proc`: - -**System catalog columns:** -- `proleakproof` (boolean) → `IsLeakproof` -- `proparallel` (char) → `Parallel` - - `'s'` → `"SAFE"` - - `'u'` → `"UNSAFE"` - - `'r'` → `"RESTRICTED"` - -**Query addition:** -```sql -SELECT - ...existing columns..., - p.proleakproof, - p.proparallel -FROM pg_catalog.pg_proc p -... -``` - -**Mapping logic:** -```go -func.IsLeakproof = proleakproof - -switch proparallel { -case 's': - func.Parallel = "SAFE" -case 'r': - func.Parallel = "RESTRICTED" -case 'u': - func.Parallel = "UNSAFE" -default: - func.Parallel = "UNSAFE" // Defensive default -} -``` - -### 3. Dump Output Logic - -**File:** `internal/dump/dump.go` - -Update function formatting to output attributes only when non-default: - -**Output rules:** -- Output `LEAKPROOF` only when `IsLeakproof == true` -- Output `PARALLEL SAFE` or `PARALLEL RESTRICTED` only when `Parallel != "UNSAFE"` -- Never output `NOT LEAKPROOF` or `PARALLEL UNSAFE` (they're defaults) - -**Attribute ordering** (matching pg_dump): -1. `LANGUAGE` -2. Volatility (`IMMUTABLE`/`STABLE`/`VOLATILE`) -3. `PARALLEL {SAFE|RESTRICTED}` (if not UNSAFE) -4. `LEAKPROOF` (if true) -5. `STRICT` (if true) -6. `SECURITY DEFINER` (if true) - -**Example output:** -```sql -CREATE FUNCTION safe_add(a integer, b integer) -RETURNS integer -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -LEAKPROOF -STRICT -AS $$ - SELECT a + b; -$$; -``` - -### 4. Diff and Migration Logic - -**File:** `internal/diff/function.go` - -Detect attribute changes and generate ALTER statements: - -**Detection:** -- Compare `IsLeakproof` between old and new function -- Compare `Parallel` between old and new function -- Only applies when function signature is unchanged - -**Migration SQL:** -```sql --- LEAKPROOF changes -ALTER FUNCTION schema.function_name(arg_types) LEAKPROOF; -ALTER FUNCTION schema.function_name(arg_types) NOT LEAKPROOF; - --- PARALLEL changes -ALTER FUNCTION schema.function_name(arg_types) PARALLEL SAFE; -ALTER FUNCTION schema.function_name(arg_types) PARALLEL UNSAFE; -ALTER FUNCTION schema.function_name(arg_types) PARALLEL RESTRICTED; -``` - -**Multiple changes:** -- Generate separate ALTER statements (PostgreSQL doesn't support combining) -- Order: PARALLEL first, then LEAKPROOF (alphabetical) - -**Edge cases:** -- If signature changes (parameters/return type), use DROP/CREATE (existing behavior) -- Attribute-only changes handled via ALTER (no dependencies broken) - -### 5. Test Cases - -#### Enhanced Test: `testdata/diff/create_function/add_function/` - -**old.sql:** Empty (no functions) - -**new.sql:** Functions with various attribute combinations: -1. `process_order()` - Add `LEAKPROOF` and `PARALLEL RESTRICTED` -2. `days_since_special_date()` - Keep `PARALLEL SAFE`, add `LEAKPROOF` -3. `safe_add()` - New simple function with `PARALLEL SAFE` and `LEAKPROOF` - -**expected.sql:** CREATE statements matching new.sql with proper attribute ordering - -#### New Test: `testdata/diff/create_function/alter_function_attributes/` - -Tests attribute-only changes (ALTER FUNCTION path): - -**old.sql:** -```sql -CREATE FUNCTION process_data(input text) -RETURNS text -LANGUAGE plpgsql -VOLATILE -AS $$ ... $$; -``` - -**new.sql:** -```sql -CREATE FUNCTION process_data(input text) -RETURNS text -LANGUAGE plpgsql -VOLATILE -PARALLEL SAFE -LEAKPROOF -AS $$ ... $$; -``` - -**expected.sql:** -```sql -ALTER FUNCTION process_data(text) PARALLEL SAFE; -ALTER FUNCTION process_data(text) LEAKPROOF; -``` - -## Implementation Checklist - -1. ☐ Update `Function` struct in `ir/ir.go` -2. ☐ Update `inspectFunctions()` in `ir/inspector.go` -3. ☐ Update dump formatting in `internal/dump/dump.go` -4. ☐ Update diff logic in `internal/diff/function.go` -5. ☐ Enhance `add_function` test case -6. ☐ Create `alter_function_attributes` test case -7. ☐ Run diff tests: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./internal/diff -run TestDiffFromFiles` -8. ☐ Run integration tests: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./cmd -run TestPlanAndApply` -9. ☐ Validate with live PostgreSQL (compare pg_dump vs pgschema output) - -## Success Criteria - -- All existing function tests continue to pass -- New test cases pass for both diff and integration -- pg_dump output matches pgschema output for LEAKPROOF/PARALLEL attributes -- ALTER FUNCTION migrations execute successfully on live PostgreSQL -- No regression in function signature detection or DROP/CREATE logic - -## References - -- PostgreSQL Documentation: [Function Volatility and Parallel Safety](https://www.postgresql.org/docs/current/xfunc-volatility.html) -- System Catalog: `pg_catalog.pg_proc` columns `proleakproof` and `proparallel` -- SQL Syntax: `ALTER FUNCTION` for attribute changes diff --git a/docs/plans/2025-11-27-function-leakproof-parallel.md b/docs/plans/2025-11-27-function-leakproof-parallel.md deleted file mode 100644 index 05566f4b..00000000 --- a/docs/plans/2025-11-27-function-leakproof-parallel.md +++ /dev/null @@ -1,722 +0,0 @@ -# Function LEAKPROOF and PARALLEL Support Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add full support for PostgreSQL function LEAKPROOF and PARALLEL attributes (SAFE/UNSAFE/RESTRICTED) throughout the dump/plan/apply workflow. - -**Architecture:** Extend Function IR with two new fields, update database inspector to query pg_catalog.pg_proc, modify dump formatter to output non-default attributes, and enhance diff logic to generate ALTER FUNCTION migrations for attribute changes. - -**Tech Stack:** Go 1.24+, pgx/v5 for database queries, embedded-postgres for testing, PostgreSQL 14-17 - ---- - -## Task 1: Update IR Structure - -**Files:** -- Modify: `ir/ir.go:124-136` (Function struct) - -**Step 1: Add new fields to Function struct** - -Add `IsLeakproof` and `Parallel` fields after `IsSecurityDefiner`: - -```go -// Function represents a database function -type Function struct { - Schema string `json:"schema"` - Name string `json:"name"` - Definition string `json:"definition"` - ReturnType string `json:"return_type"` - Language string `json:"language"` - Parameters []*Parameter `json:"parameters,omitempty"` - Comment string `json:"comment,omitempty"` - Volatility string `json:"volatility,omitempty"` // IMMUTABLE, STABLE, VOLATILE - IsStrict bool `json:"is_strict,omitempty"` // STRICT or null behavior - IsSecurityDefiner bool `json:"is_security_definer,omitempty"` // SECURITY DEFINER - IsLeakproof bool `json:"is_leakproof,omitempty"` // LEAKPROOF - Parallel string `json:"parallel,omitempty"` // SAFE, UNSAFE, RESTRICTED -} -``` - -**Step 2: Verify code compiles** - -Run: `go build -o pgschema .` -Expected: Successful compilation (new fields don't break anything) - -**Step 3: Commit IR changes** - -```bash -git add ir/ir.go -git commit -m "feat: add IsLeakproof and Parallel fields to Function IR" -``` - ---- - -## Task 2: Update Database Inspector - -**Files:** -- Modify: `ir/inspector.go` (inspectFunctions method) - -**Step 1: Locate the inspectFunctions query** - -Find the SELECT query in `inspectFunctions()` that queries `pg_catalog.pg_proc`. It should be around line 400-500. - -**Step 2: Add proleakproof and proparallel to SELECT** - -Add these columns to the existing SELECT statement: - -```go -p.proleakproof, -p.proparallel -``` - -The query should look like: -```go -query := ` -SELECT - n.nspname AS schema_name, - p.proname AS function_name, - pg_get_functiondef(p.oid) AS definition, - pg_get_function_result(p.oid) AS return_type, - l.lanname AS language, - p.provolatile, - p.proisstrict, - p.prosecdef, - p.proleakproof, - p.proparallel, - obj_description(p.oid, 'pg_proc') AS comment -FROM pg_catalog.pg_proc p -... -` -``` - -**Step 3: Add variables to scan into** - -In the scan section, add variables: - -```go -var ( - // ... existing variables ... - proleakproof bool - proparallel string -) -``` - -**Step 4: Add to Scan() call** - -Add to the existing `rows.Scan()`: - -```go -&proleakproof, -&proparallel, -``` - -**Step 5: Map proparallel to Parallel field** - -After the existing volatility mapping, add: - -```go -// Map LEAKPROOF -fn.IsLeakproof = proleakproof - -// Map PARALLEL -switch proparallel { -case "s": - fn.Parallel = "SAFE" -case "r": - fn.Parallel = "RESTRICTED" -case "u": - fn.Parallel = "UNSAFE" -default: - fn.Parallel = "UNSAFE" // Defensive default -} -``` - -**Step 6: Verify code compiles** - -Run: `go build -o pgschema .` -Expected: Successful compilation - -**Step 7: Commit inspector changes** - -```bash -git add ir/inspector.go -git commit -m "feat: extract LEAKPROOF and PARALLEL from pg_catalog.pg_proc" -``` - ---- - -## Task 3: Update Dump Formatter - -**Files:** -- Modify: `internal/dump/dump.go` (function formatting) - -**Step 1: Locate function dump logic** - -Find the function that formats function definitions for dump output. Look for code that builds the CREATE FUNCTION statement. Should be in a method like `dumpFunction()` or similar. - -**Step 2: Add PARALLEL output logic** - -After the LANGUAGE and volatility output, add: - -```go -// Add PARALLEL if not default (UNSAFE) -if fn.Parallel == "SAFE" { - fmt.Fprintf(buf, "PARALLEL SAFE\n") -} else if fn.Parallel == "RESTRICTED" { - fmt.Fprintf(buf, "PARALLEL RESTRICTED\n") -} -// Note: Don't output PARALLEL UNSAFE (it's the default) -``` - -**Step 3: Add LEAKPROOF output logic** - -After PARALLEL, add: - -```go -// Add LEAKPROOF if true -if fn.IsLeakproof { - fmt.Fprintf(buf, "LEAKPROOF\n") -} -// Note: Don't output NOT LEAKPROOF (it's the default) -``` - -The attribute order should be: -1. LANGUAGE -2. Volatility (IMMUTABLE/STABLE/VOLATILE) -3. PARALLEL (if not UNSAFE) -4. LEAKPROOF (if true) -5. STRICT (if true) -6. SECURITY DEFINER (if true) - -**Step 4: Test dump output manually** - -Run: -```bash -go build -o pgschema . -PGPASSWORD='testpwd1' ./pgschema dump -h localhost -p 5432 -U postgres -d postgres --schema public -``` - -Expected: Should compile and run (may not show new attributes yet if test DB doesn't have them) - -**Step 5: Commit dump formatter changes** - -```bash -git add internal/dump/dump.go -git commit -m "feat: output LEAKPROOF and PARALLEL in function dumps" -``` - ---- - -## Task 4: Create Test Case - add_function Enhancement - -**Files:** -- Modify: `testdata/diff/create_function/add_function/new.sql` -- Modify: `testdata/diff/create_function/add_function/plan.sql` - -**Step 1: Update new.sql with LEAKPROOF and PARALLEL** - -Modify the existing functions to add attributes: - -```sql -CREATE FUNCTION process_order( - order_id integer, - -- Simple numeric defaults - discount_percent numeric DEFAULT 0, - priority_level integer DEFAULT 1, - -- String defaults - note varchar DEFAULT '', - status text DEFAULT 'pending', - -- Boolean defaults - apply_tax boolean DEFAULT true, - is_priority boolean DEFAULT false -) -RETURNS numeric -LANGUAGE plpgsql -VOLATILE -PARALLEL RESTRICTED -LEAKPROOF -SECURITY DEFINER -STRICT -AS $$ -DECLARE - total numeric; -BEGIN - SELECT amount INTO total FROM orders WHERE id = order_id; - RETURN total - (total * discount_percent / 100); -END; -$$; - --- Table function with RETURN clause (bug report test case) -CREATE FUNCTION days_since_special_date() RETURNS SETOF timestamptz - LANGUAGE sql - STABLE - PARALLEL SAFE - LEAKPROOF - RETURN generate_series(date_trunc('day', '2025-01-01'::timestamp), date_trunc('day', NOW()), '1 day'::interval); - --- Simple pure function demonstrating PARALLEL SAFE + LEAKPROOF -CREATE FUNCTION safe_add(a integer, b integer) -RETURNS integer -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -LEAKPROOF -STRICT -AS $$ - SELECT a + b; -$$; -``` - -**Step 2: Update plan.sql to match expected output** - -Update `plan.sql` to show the normalized CREATE FUNCTION statements that pgschema will generate (with proper attribute ordering): - -```sql -CREATE OR REPLACE FUNCTION days_since_special_date() -RETURNS SETOF timestamp with time zone -LANGUAGE sql -STABLE -PARALLEL SAFE -LEAKPROOF -RETURN generate_series((date_trunc('day'::text, '2025-01-01 00:00:00'::timestamp without time zone))::timestamp with time zone, date_trunc('day'::text, now()), '1 day'::interval); - -CREATE OR REPLACE FUNCTION process_order( - order_id integer, - discount_percent numeric DEFAULT 0, - priority_level integer DEFAULT 1, - note varchar DEFAULT '', - status text DEFAULT 'pending', - apply_tax boolean DEFAULT true, - is_priority boolean DEFAULT false -) -RETURNS numeric -LANGUAGE plpgsql -VOLATILE -PARALLEL RESTRICTED -LEAKPROOF -SECURITY DEFINER -STRICT -AS $$ -DECLARE - total numeric; -BEGIN - SELECT amount INTO total FROM orders WHERE id = order_id; - RETURN total - (total * discount_percent / 100); -END; -$$; - -CREATE OR REPLACE FUNCTION safe_add(a integer, b integer) -RETURNS integer -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -LEAKPROOF -STRICT -AS $$ - SELECT a + b; -$$; -``` - -**Step 3: Run diff test to see current output** - -Run: `PGSCHEMA_TEST_FILTER="create_function/add_function" go test -v ./internal/diff -run TestDiffFromFiles` -Expected: Test may fail initially (dump output may not match yet due to formatting details) - -**Step 4: Commit test case updates** - -```bash -git add testdata/diff/create_function/add_function/new.sql testdata/diff/create_function/add_function/plan.sql -git commit -m "test: add LEAKPROOF and PARALLEL to add_function test case" -``` - ---- - -## Task 5: Update Diff Logic for ALTER FUNCTION - -**Files:** -- Modify: `internal/diff/function.go` - -**Step 1: Locate function comparison logic** - -Find the code that compares old vs new functions with the same signature. Look for logic that checks if functions differ. - -**Step 2: Add LEAKPROOF comparison** - -Add after existing attribute comparisons (like volatility, strict, security definer): - -```go -// Check LEAKPROOF changes -if oldFunc.IsLeakproof != newFunc.IsLeakproof { - var stmt string - if newFunc.IsLeakproof { - stmt = fmt.Sprintf("ALTER FUNCTION %s.%s(%s) LEAKPROOF;", - QuoteIdentifier(newFunc.Schema), - QuoteIdentifier(newFunc.Name), - newFunc.GetArguments()) - } else { - stmt = fmt.Sprintf("ALTER FUNCTION %s.%s(%s) NOT LEAKPROOF;", - QuoteIdentifier(newFunc.Schema), - QuoteIdentifier(newFunc.Name), - newFunc.GetArguments()) - } - steps = append(steps, MigrationStep{ - Type: "ALTER_FUNCTION_LEAKPROOF", - Schema: newFunc.Schema, - Name: newFunc.Name, - SQL: stmt, - Description: fmt.Sprintf("Alter function %s.%s LEAKPROOF", newFunc.Schema, newFunc.Name), - }) -} -``` - -**Step 3: Add PARALLEL comparison** - -Add after LEAKPROOF: - -```go -// Check PARALLEL changes -if oldFunc.Parallel != newFunc.Parallel { - stmt := fmt.Sprintf("ALTER FUNCTION %s.%s(%s) PARALLEL %s;", - QuoteIdentifier(newFunc.Schema), - QuoteIdentifier(newFunc.Name), - newFunc.GetArguments(), - newFunc.Parallel) - steps = append(steps, MigrationStep{ - Type: "ALTER_FUNCTION_PARALLEL", - Schema: newFunc.Schema, - Name: newFunc.Name, - SQL: stmt, - Description: fmt.Sprintf("Alter function %s.%s PARALLEL %s", newFunc.Schema, newFunc.Name, newFunc.Parallel), - }) -} -``` - -**Step 4: Verify code compiles** - -Run: `go build -o pgschema .` -Expected: Successful compilation - -**Step 5: Commit diff logic** - -```bash -git add internal/diff/function.go -git commit -m "feat: generate ALTER FUNCTION for LEAKPROOF and PARALLEL changes" -``` - ---- - -## Task 6: Create Test Case - alter_function_attributes - -**Files:** -- Create: `testdata/diff/create_function/alter_function_attributes/old.sql` -- Create: `testdata/diff/create_function/alter_function_attributes/new.sql` -- Create: `testdata/diff/create_function/alter_function_attributes/plan.sql` - -**Step 1: Create test directory** - -Run: `mkdir -p testdata/diff/create_function/alter_function_attributes` - -**Step 2: Write old.sql (function without attributes)** - -Create `testdata/diff/create_function/alter_function_attributes/old.sql`: - -```sql -CREATE FUNCTION process_data(input text) -RETURNS text -LANGUAGE plpgsql -VOLATILE -AS $$ -BEGIN - RETURN upper(input); -END; -$$; - -CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) -RETURNS numeric -LANGUAGE sql -STABLE -AS $$ - SELECT amount * (1 + tax_rate); -$$; -``` - -**Step 3: Write new.sql (add LEAKPROOF and PARALLEL)** - -Create `testdata/diff/create_function/alter_function_attributes/new.sql`: - -```sql -CREATE FUNCTION process_data(input text) -RETURNS text -LANGUAGE plpgsql -VOLATILE -PARALLEL SAFE -LEAKPROOF -AS $$ -BEGIN - RETURN upper(input); -END; -$$; - -CREATE FUNCTION calculate_total(amount numeric, tax_rate numeric) -RETURNS numeric -LANGUAGE sql -STABLE -PARALLEL SAFE -LEAKPROOF -AS $$ - SELECT amount * (1 + tax_rate); -$$; -``` - -**Step 4: Write plan.sql (expected ALTER statements)** - -Create `testdata/diff/create_function/alter_function_attributes/plan.sql`: - -```sql -ALTER FUNCTION process_data(text) PARALLEL SAFE; - -ALTER FUNCTION process_data(text) LEAKPROOF; - -ALTER FUNCTION calculate_total(numeric, numeric) PARALLEL SAFE; - -ALTER FUNCTION calculate_total(numeric, numeric) LEAKPROOF; -``` - -**Step 5: Run diff test** - -Run: `PGSCHEMA_TEST_FILTER="create_function/alter_function_attributes" go test -v ./internal/diff -run TestDiffFromFiles` -Expected: Test should pass if diff logic is correct - -**Step 6: Commit new test case** - -```bash -git add testdata/diff/create_function/alter_function_attributes/ -git commit -m "test: add alter_function_attributes test case" -``` - ---- - -## Task 7: Run All Function Diff Tests - -**Files:** -- N/A (testing only) - -**Step 1: Run all function diff tests** - -Run: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./internal/diff -run TestDiffFromFiles` -Expected: All tests pass - -**Step 2: If tests fail, check test fixtures** - -Common issues: -- Attribute ordering doesn't match expected output -- Missing quote identifiers -- Incorrect normalization of function signatures - -Fix by adjusting dump formatter or test fixture files to match actual pg_dump behavior. - -**Step 3: Regenerate expected files if needed** - -If the logic is correct but expected files need updating, use the test framework's regeneration option (if available) or manually update `plan.sql` files to match actual output. - ---- - -## Task 8: Run Integration Tests - -**Files:** -- N/A (testing only) - -**Step 1: Run function integration tests** - -Run: `PGSCHEMA_TEST_FILTER="create_function/" go test -v ./cmd -run TestPlanAndApply -timeout 5m` -Expected: All tests pass (creates embedded postgres, applies migrations, validates) - -**Step 2: Check for transaction handling** - -Ensure ALTER FUNCTION statements execute correctly within transactions (they should - it's a DDL but doesn't require --pgschema-no-transaction). - -**Step 3: If tests fail, debug** - -Use @superpowers:systematic-debugging skill: -- Check embedded postgres logs -- Verify SQL syntax -- Confirm pg_catalog queries work on test postgres version - -**Step 4: Verify across postgres versions** - -Run tests against all supported versions: - -```bash -PGSCHEMA_POSTGRES_VERSION=14 go test -v ./cmd -run TestPlanAndApply/create_function -PGSCHEMA_POSTGRES_VERSION=15 go test -v ./cmd -run TestPlanAndApply/create_function -PGSCHEMA_POSTGRES_VERSION=16 go test -v ./cmd -run TestPlanAndApply/create_function -PGSCHEMA_POSTGRES_VERSION=17 go test -v ./cmd -run TestPlanAndApply/create_function -``` - -Expected: All pass (LEAKPROOF and PARALLEL supported in all versions 14+) - ---- - -## Task 9: Validate Against Live Database - -**Files:** -- N/A (manual validation) - -**Step 1: Use validate_db skill** - -Run @validate_db skill to connect to live PostgreSQL and compare outputs. - -**Step 2: Create test function in live database** - -Connect to test database and create a function with attributes: - -```sql -CREATE FUNCTION test_leakproof_parallel(x integer) -RETURNS integer -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -LEAKPROOF -STRICT -AS $$ - SELECT x * 2; -$$; -``` - -**Step 3: Compare pg_dump vs pgschema dump** - -Run both: -```bash -PGPASSWORD='testpwd1' pg_dump -h localhost -p 5432 -U postgres -d postgres --schema-only --schema=public | grep -A 10 "test_leakproof_parallel" - -PGPASSWORD='testpwd1' ./pgschema dump -h localhost -p 5432 -U postgres -d postgres --schema public | grep -A 10 "test_leakproof_parallel" -``` - -Expected: Output should be identical (attribute order and formatting match) - -**Step 4: Test ALTER FUNCTION migration** - -1. Create function without attributes -2. Run pgschema plan with new.sql that adds attributes -3. Verify plan shows ALTER FUNCTION statements -4. Run pgschema apply -5. Verify function has new attributes in database - -```bash -# Setup -PGPASSWORD='testpwd1' psql -h localhost -p 5432 -U postgres -d postgres -c " -CREATE OR REPLACE FUNCTION test_alter(x int) RETURNS int LANGUAGE sql AS 'SELECT x'; -" - -# Create desired state file -echo "CREATE FUNCTION test_alter(x int) RETURNS int LANGUAGE sql PARALLEL SAFE LEAKPROOF AS 'SELECT x';" > /tmp/test.sql - -# Plan (using embedded postgres by default) -./pgschema plan --schema public /tmp/test.sql - -# Should show: -# ALTER FUNCTION test_alter(integer) PARALLEL SAFE; -# ALTER FUNCTION test_alter(integer) LEAKPROOF; - -# Apply -./pgschema apply --schema public /tmp/test.sql -h localhost -p 5432 -U postgres -d postgres - -# Verify -PGPASSWORD='testpwd1' psql -h localhost -p 5432 -U postgres -d postgres -c " -SELECT proname, proparallel, proleakproof -FROM pg_proc -WHERE proname = 'test_alter'; -" -``` - -Expected: proparallel='s', proleakproof=true - ---- - -## Task 10: Run Full Test Suite - -**Files:** -- N/A (testing only) - -**Step 1: Run all tests** - -Run: `go test -v ./...` -Expected: All tests pass (no regressions) - -**Step 2: Check test coverage** - -Run: `go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out` -Expected: New code paths are covered by tests - -**Step 3: Fix any regressions** - -If any existing tests fail, use @superpowers:systematic-debugging to identify and fix issues. - ---- - -## Task 11: Final Commit and Cleanup - -**Files:** -- All modified files - -**Step 1: Review all changes** - -Run: `git status && git diff` -Expected: Only intended changes present - -**Step 2: Run final verification** - -```bash -go build -o pgschema . -go test -v ./... -``` - -Expected: Clean build, all tests pass - -**Step 3: Create final commit if needed** - -If there are uncommitted changes: - -```bash -git add -A -git commit -m "feat: complete LEAKPROOF and PARALLEL function support - -- Add IsLeakproof and Parallel fields to Function IR -- Extract attributes from pg_catalog.pg_proc -- Output attributes in dump (non-defaults only) -- Generate ALTER FUNCTION for attribute changes -- Add comprehensive test cases - -Supports all three parallel modes: SAFE, UNSAFE, RESTRICTED -Validated against PostgreSQL 14-17" -``` - -**Step 4: Verify git log** - -Run: `git log --oneline -10` -Expected: Clean commit history with descriptive messages - ---- - -## Success Criteria - -- ✅ All function diff tests pass -- ✅ All function integration tests pass -- ✅ Tests pass on PostgreSQL 14, 15, 16, 17 -- ✅ pg_dump output matches pgschema output for LEAKPROOF/PARALLEL -- ✅ ALTER FUNCTION migrations execute successfully -- ✅ No regressions in existing function tests -- ✅ Manual validation against live database successful - -## Skills Referenced - -- @superpowers:systematic-debugging - For debugging test failures -- @validate_db - For live database validation -- @run_tests - For running pgschema test suite -- @pg_dump - For comparing output with pg_dump reference - -## Notes - -- LEAKPROOF and PARALLEL are supported in PostgreSQL 9.2+ and 9.6+ respectively (well within our 14-17 support range) -- ALTER FUNCTION for these attributes is transactional (no special handling needed) -- The hybrid approach (store explicit, output non-defaults) ensures clean diffs and readable dumps From 59970933ad2475eba46aab9f4b21b02932a144e8 Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Fri, 28 Nov 2025 09:18:53 -0800 Subject: [PATCH 14/14] chore: remove newline --- testdata/diff/dependency/function_to_table/diff.sql | 1 - testdata/diff/dependency/function_to_trigger/diff.sql | 1 - testdata/diff/migrate/v3/diff.sql | 1 - testdata/diff/migrate/v4/diff.sql | 1 - 4 files changed, 4 deletions(-) diff --git a/testdata/diff/dependency/function_to_table/diff.sql b/testdata/diff/dependency/function_to_table/diff.sql index bc279d28..31d6e645 100644 --- a/testdata/diff/dependency/function_to_table/diff.sql +++ b/testdata/diff/dependency/function_to_table/diff.sql @@ -1,7 +1,6 @@ CREATE OR REPLACE FUNCTION get_default_status() RETURNS text LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/dependency/function_to_trigger/diff.sql b/testdata/diff/dependency/function_to_trigger/diff.sql index f0c8b113..9f601547 100644 --- a/testdata/diff/dependency/function_to_trigger/diff.sql +++ b/testdata/diff/dependency/function_to_trigger/diff.sql @@ -4,7 +4,6 @@ DROP FUNCTION IF EXISTS update_modified_time(); CREATE OR REPLACE FUNCTION log_user_changes() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v3/diff.sql b/testdata/diff/migrate/v3/diff.sql index 33d277cd..94cf41ed 100644 --- a/testdata/diff/migrate/v3/diff.sql +++ b/testdata/diff/migrate/v3/diff.sql @@ -12,7 +12,6 @@ CREATE INDEX IF NOT EXISTS idx_audit_changed_at ON audit (changed_at); CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ BEGIN diff --git a/testdata/diff/migrate/v4/diff.sql b/testdata/diff/migrate/v4/diff.sql index 3de1328d..505054e4 100644 --- a/testdata/diff/migrate/v4/diff.sql +++ b/testdata/diff/migrate/v4/diff.sql @@ -58,7 +58,6 @@ CREATE OR REPLACE TRIGGER salary_log_trigger CREATE OR REPLACE FUNCTION log_dml_operations() RETURNS trigger LANGUAGE plpgsql - VOLATILE AS $$ DECLARE