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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 66 additions & 11 deletions internal/diff/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,31 @@ func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, coll
if oldFunc.Comment != newFunc.Comment {
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
}
} else if functionRequiresRecreate(oldFunc, newFunc) {
// Return type, OUT parameters, or parameter names changed - must DROP then CREATE
// PostgreSQL does not allow CREATE OR REPLACE to change these.
// See https://github.com/pgplex/pgschema/issues/326
dropSQL := generateDropFunctionSQL(oldFunc, targetSchema)
Comment on lines +103 to +107
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The DROP+CREATE path for incompatible function changes can still fail at runtime if any existing objects depend on the function (e.g., views, policies, triggers on unchanged tables). PostgreSQL will reject DROP FUNCTION with dependent objects present, and this branch runs during the modify phase without any dependency pre-drop/recreate handling. Consider adding dependency handling similar to view recreation (drop/recreate dependents around the function recreation), or reordering so dependents are updated/dropped before the function is dropped; adding a regression test with a view depending on the function would help prevent plan failures.

Copilot uses AI. Check for mistakes.
createSQL := generateFunctionSQL(newFunc, targetSchema)

alterContext := &diffContext{
Type: DiffTypeFunction,
Operation: DiffOperationAlter,
Comment on lines +107 to +112
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Dropping and recreating a function inside the modify phase can fail if any existing objects depend on it (e.g., views or triggers), since PostgreSQL will block DROP FUNCTION ... under RESTRICT (the default). There doesn’t appear to be corresponding logic here to pre-drop/recreate dependent objects for this “recreate” path (unlike the drop phase which handles triggers for dropped functions). Consider extending dependency handling (or explicitly using CASCADE with controlled recreation) so this migration path is reliable when dependencies exist.

Copilot uses AI. Check for mistakes.
Path: fmt.Sprintf("%s.%s", newFunc.Schema, newFunc.Name),
Source: diff,
CanRunInTransaction: true,
}

statements := []SQLStatement{
{SQL: dropSQL, CanRunInTransaction: true},
{SQL: createSQL, CanRunInTransaction: true},
}
collector.collectStatements(alterContext, statements)

// Check if comment also changed alongside body changes
if oldFunc.Comment != newFunc.Comment {
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
}
} else {
// Function body or other attributes changed - use CREATE OR REPLACE
sql := generateFunctionSQL(newFunc, targetSchema)
Expand Down Expand Up @@ -129,17 +154,7 @@ func generateDropFunctionsSQL(functions []*ir.Function, targetSchema string, col
sortedFunctions := reverseSlice(topologicallySortFunctions(functions))

for _, function := range sortedFunctions {
functionName := qualifyEntityName(function.Schema, function.Name, targetSchema)
var sql string

// Build argument list for DROP statement using GetArguments()
argsList := function.GetArguments()

if argsList != "" {
sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, argsList)
} else {
sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s();", functionName)
}
sql := generateDropFunctionSQL(function, targetSchema)

// Create context for this statement
context := &diffContext{
Expand All @@ -154,6 +169,16 @@ func generateDropFunctionsSQL(functions []*ir.Function, targetSchema string, col
}
}

// generateDropFunctionSQL generates a DROP FUNCTION IF EXISTS statement
func generateDropFunctionSQL(function *ir.Function, targetSchema string) string {
functionName := qualifyEntityName(function.Schema, function.Name, targetSchema)
argsList := function.GetArguments()
if argsList != "" {
return fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, argsList)
}
return fmt.Sprintf("DROP FUNCTION IF EXISTS %s();", functionName)
}

// generateFunctionSQL generates CREATE OR REPLACE FUNCTION SQL for a function
func generateFunctionSQL(function *ir.Function, targetSchema string) string {
var stmt strings.Builder
Expand Down Expand Up @@ -460,6 +485,36 @@ func functionsEqualExceptComment(old, new *ir.Function) bool {
return parametersEqual(oldInputParams, newInputParams)
}

// functionRequiresRecreate checks if a function modification requires DROP+CREATE
// instead of CREATE OR REPLACE. PostgreSQL does not allow CREATE OR REPLACE to change
// the return type or parameter names of an existing function.
func functionRequiresRecreate(old, new *ir.Function) bool {
if old.ReturnType != new.ReturnType {
return true
}
// Check parameter changes that CREATE OR REPLACE cannot handle.
// Input parameter types are the same (same map key), but names, OUT/INOUT
// parameter types/modes, or parameter count differences require DROP+CREATE.
oldParams := filterNonTableParameters(old.Parameters)
newParams := filterNonTableParameters(new.Parameters)
if len(oldParams) != len(newParams) {
return true
}
for i := range oldParams {
if oldParams[i].Name != newParams[i].Name {
return true
}
Comment on lines +503 to +506
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

functionRequiresRecreate currently only compares parameter names (and count) after filtering out TABLE params. If an OUT parameter’s type or mode changes (which does not affect Function.GetArguments() and therefore can still show up as a “modify”), this will fall through to CREATE OR REPLACE FUNCTION and PostgreSQL will reject it. Consider extending this check to also compare OUT/INOUT parameter mode and data type (at least for non-input parameters), not just .Name.

Suggested change
for i := range oldParams {
if oldParams[i].Name != newParams[i].Name {
return true
}
for i := range oldParams {
// Name changes always require a DROP+CREATE
if oldParams[i].Name != newParams[i].Name {
return true
}
// For OUT/INOUT parameters, a change in mode or data type (or other
// signature-relevant attributes) also requires DROP+CREATE, since
// PostgreSQL does not allow CREATE OR REPLACE FUNCTION to alter them.
if (oldParams[i].Mode == "OUT" || oldParams[i].Mode == "INOUT") ||
(newParams[i].Mode == "OUT" || newParams[i].Mode == "INOUT") {
if !parameterEqual(oldParams[i], newParams[i]) {
return true
}
}

Copilot uses AI. Check for mistakes.
// OUT/INOUT parameter type or mode changes also require DROP+CREATE
if oldParams[i].Mode == "OUT" || oldParams[i].Mode == "INOUT" ||
newParams[i].Mode == "OUT" || newParams[i].Mode == "INOUT" {
if !parameterEqual(oldParams[i], newParams[i]) {
return true
}
}
}
return false
}

// filterNonTableParameters filters out TABLE mode parameters
// TABLE parameters are output columns in RETURNS TABLE() and shouldn't be compared as input parameters
func filterNonTableParameters(params []*ir.Parameter) []*ir.Parameter {
Expand Down
10 changes: 10 additions & 0 deletions testdata/diff/create_function/issue_326_param_name_change/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
new_name text
)
RETURNS text
LANGUAGE sql
VOLATILE
AS $$ SELECT new_name;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
new_name text
) RETURNS text
LANGUAGE sql
AS $$ SELECT new_name; $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
old_name text
) RETURNS text
LANGUAGE sql
AS $$ SELECT old_name; $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "1.0.0",
"pgschema_version": "1.7.2",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "d87f2cfffc1d1273ca588466e14557b2698607c55dc7c8a0e44317046e3c95a9"
},
"groups": [
{
"steps": [
{
"sql": "DROP FUNCTION IF EXISTS somefunction(text);",
"type": "function",
"operation": "alter",
"path": "public.somefunction"
},
{
"sql": "CREATE OR REPLACE FUNCTION somefunction(\n new_name text\n)\nRETURNS text\nLANGUAGE sql\nVOLATILE\nAS $$ SELECT new_name;\n$$;",
"type": "function",
"operation": "alter",
"path": "public.somefunction"
}
]
}
]
}
10 changes: 10 additions & 0 deletions testdata/diff/create_function/issue_326_param_name_change/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
new_name text
)
RETURNS text
LANGUAGE sql
VOLATILE
AS $$ SELECT new_name;
$$;
21 changes: 21 additions & 0 deletions testdata/diff/create_function/issue_326_param_name_change/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Plan: 1 to modify.

Summary by type:
functions: 1 to modify

Functions:
~ somefunction

DDL to be executed:
--------------------------------------------------

DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
new_name text
)
RETURNS text
LANGUAGE sql
VOLATILE
AS $$ SELECT new_name;
$$;
10 changes: 10 additions & 0 deletions testdata/diff/create_function/issue_326_param_type_change/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param2 uuid
)
RETURNS uuid
LANGUAGE sql
VOLATILE
AS $$ SELECT param2;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
param2 uuid
) RETURNS uuid
LANGUAGE sql
AS $$ SELECT param2; $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
param1 text
) RETURNS text
LANGUAGE sql
AS $$ SELECT param1; $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "1.0.0",
"pgschema_version": "1.7.2",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "59a96fc0ed0cbfa32f92a9d869bbbc6e38359d50afcd5ea6eb4f388717c48135"
},
"groups": [
{
"steps": [
{
"sql": "DROP FUNCTION IF EXISTS somefunction(text);",
"type": "function",
"operation": "drop",
"path": "public.somefunction"
},
{
"sql": "CREATE OR REPLACE FUNCTION somefunction(\n param2 uuid\n)\nRETURNS uuid\nLANGUAGE sql\nVOLATILE\nAS $$ SELECT param2;\n$$;",
"type": "function",
"operation": "create",
"path": "public.somefunction"
}
]
}
]
}
10 changes: 10 additions & 0 deletions testdata/diff/create_function/issue_326_param_type_change/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param2 uuid
)
RETURNS uuid
LANGUAGE sql
VOLATILE
AS $$ SELECT param2;
$$;
22 changes: 22 additions & 0 deletions testdata/diff/create_function/issue_326_param_type_change/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Plan: 1 to add, 1 to drop.

Summary by type:
functions: 1 to add, 1 to drop

Functions:
- somefunction
+ somefunction

DDL to be executed:
--------------------------------------------------

DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param2 uuid
)
RETURNS uuid
LANGUAGE sql
VOLATILE
AS $$ SELECT param2;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param1 text
)
RETURNS integer
LANGUAGE sql
VOLATILE
AS $$ SELECT length(param1);
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
param1 text
) RETURNS integer
LANGUAGE sql
AS $$ SELECT length(param1); $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE FUNCTION somefunction(
param1 text
) RETURNS text
LANGUAGE sql
AS $$ SELECT param1; $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "1.0.0",
"pgschema_version": "1.7.2",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "59a96fc0ed0cbfa32f92a9d869bbbc6e38359d50afcd5ea6eb4f388717c48135"
},
"groups": [
{
"steps": [
{
"sql": "DROP FUNCTION IF EXISTS somefunction(text);",
"type": "function",
"operation": "alter",
"path": "public.somefunction"
},
{
"sql": "CREATE OR REPLACE FUNCTION somefunction(\n param1 text\n)\nRETURNS integer\nLANGUAGE sql\nVOLATILE\nAS $$ SELECT length(param1);\n$$;",
"type": "function",
"operation": "alter",
"path": "public.somefunction"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param1 text
)
RETURNS integer
LANGUAGE sql
VOLATILE
AS $$ SELECT length(param1);
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Plan: 1 to modify.

Summary by type:
functions: 1 to modify

Functions:
~ somefunction

DDL to be executed:
--------------------------------------------------

DROP FUNCTION IF EXISTS somefunction(text);

CREATE OR REPLACE FUNCTION somefunction(
param1 text
)
RETURNS integer
LANGUAGE sql
VOLATILE
AS $$ SELECT length(param1);
$$;