Skip to content

Commit dd5122c

Browse files
tianzhouclaude
andcommitted
fix: DROP function before CREATE when return type or param names change (#326)
PostgreSQL does not allow CREATE OR REPLACE FUNCTION to change the return type, parameter names, or OUT/INOUT parameter types of an existing function. When these change (but input parameter types stay the same, keeping the same function signature key), pgschema now generates DROP FUNCTION followed by CREATE FUNCTION instead of just CREATE OR REPLACE FUNCTION. Also extracts generateDropFunctionSQL helper to share DROP statement generation between the recreate path and the existing drop path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d6dd938 commit dd5122c

19 files changed

Lines changed: 298 additions & 11 deletions

File tree

internal/diff/function.go

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,31 @@ func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, coll
100100
if oldFunc.Comment != newFunc.Comment {
101101
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
102102
}
103+
} else if functionRequiresRecreate(oldFunc, newFunc) {
104+
// Return type, OUT parameters, or parameter names changed - must DROP then CREATE
105+
// PostgreSQL does not allow CREATE OR REPLACE to change these.
106+
// See https://github.com/pgplex/pgschema/issues/326
107+
dropSQL := generateDropFunctionSQL(oldFunc, targetSchema)
108+
createSQL := generateFunctionSQL(newFunc, targetSchema)
109+
110+
alterContext := &diffContext{
111+
Type: DiffTypeFunction,
112+
Operation: DiffOperationAlter,
113+
Path: fmt.Sprintf("%s.%s", newFunc.Schema, newFunc.Name),
114+
Source: diff,
115+
CanRunInTransaction: true,
116+
}
117+
118+
statements := []SQLStatement{
119+
{SQL: dropSQL, CanRunInTransaction: true},
120+
{SQL: createSQL, CanRunInTransaction: true},
121+
}
122+
collector.collectStatements(alterContext, statements)
123+
124+
// Check if comment also changed alongside body changes
125+
if oldFunc.Comment != newFunc.Comment {
126+
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
127+
}
103128
} else {
104129
// Function body or other attributes changed - use CREATE OR REPLACE
105130
sql := generateFunctionSQL(newFunc, targetSchema)
@@ -129,17 +154,7 @@ func generateDropFunctionsSQL(functions []*ir.Function, targetSchema string, col
129154
sortedFunctions := reverseSlice(topologicallySortFunctions(functions))
130155

131156
for _, function := range sortedFunctions {
132-
functionName := qualifyEntityName(function.Schema, function.Name, targetSchema)
133-
var sql string
134-
135-
// Build argument list for DROP statement using GetArguments()
136-
argsList := function.GetArguments()
137-
138-
if argsList != "" {
139-
sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, argsList)
140-
} else {
141-
sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s();", functionName)
142-
}
157+
sql := generateDropFunctionSQL(function, targetSchema)
143158

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

172+
// generateDropFunctionSQL generates a DROP FUNCTION IF EXISTS statement
173+
func generateDropFunctionSQL(function *ir.Function, targetSchema string) string {
174+
functionName := qualifyEntityName(function.Schema, function.Name, targetSchema)
175+
argsList := function.GetArguments()
176+
if argsList != "" {
177+
return fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, argsList)
178+
}
179+
return fmt.Sprintf("DROP FUNCTION IF EXISTS %s();", functionName)
180+
}
181+
157182
// generateFunctionSQL generates CREATE OR REPLACE FUNCTION SQL for a function
158183
func generateFunctionSQL(function *ir.Function, targetSchema string) string {
159184
var stmt strings.Builder
@@ -460,6 +485,36 @@ func functionsEqualExceptComment(old, new *ir.Function) bool {
460485
return parametersEqual(oldInputParams, newInputParams)
461486
}
462487

488+
// functionRequiresRecreate checks if a function modification requires DROP+CREATE
489+
// instead of CREATE OR REPLACE. PostgreSQL does not allow CREATE OR REPLACE to change
490+
// the return type or parameter names of an existing function.
491+
func functionRequiresRecreate(old, new *ir.Function) bool {
492+
if old.ReturnType != new.ReturnType {
493+
return true
494+
}
495+
// Check parameter changes that CREATE OR REPLACE cannot handle.
496+
// Input parameter types are the same (same map key), but names, OUT/INOUT
497+
// parameter types/modes, or parameter count differences require DROP+CREATE.
498+
oldParams := filterNonTableParameters(old.Parameters)
499+
newParams := filterNonTableParameters(new.Parameters)
500+
if len(oldParams) != len(newParams) {
501+
return true
502+
}
503+
for i := range oldParams {
504+
if oldParams[i].Name != newParams[i].Name {
505+
return true
506+
}
507+
// OUT/INOUT parameter type or mode changes also require DROP+CREATE
508+
if oldParams[i].Mode == "OUT" || oldParams[i].Mode == "INOUT" ||
509+
newParams[i].Mode == "OUT" || newParams[i].Mode == "INOUT" {
510+
if !parameterEqual(oldParams[i], newParams[i]) {
511+
return true
512+
}
513+
}
514+
}
515+
return false
516+
}
517+
463518
// filterNonTableParameters filters out TABLE mode parameters
464519
// TABLE parameters are output columns in RETURNS TABLE() and shouldn't be compared as input parameters
465520
func filterNonTableParameters(params []*ir.Parameter) []*ir.Parameter {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
DROP FUNCTION IF EXISTS somefunction(text);
2+
3+
CREATE OR REPLACE FUNCTION somefunction(
4+
new_name text
5+
)
6+
RETURNS text
7+
LANGUAGE sql
8+
VOLATILE
9+
AS $$ SELECT new_name;
10+
$$;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE OR REPLACE FUNCTION somefunction(
2+
new_name text
3+
) RETURNS text
4+
LANGUAGE sql
5+
AS $$ SELECT new_name; $$;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE OR REPLACE FUNCTION somefunction(
2+
old_name text
3+
) RETURNS text
4+
LANGUAGE sql
5+
AS $$ SELECT old_name; $$;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "1.0.0",
3+
"pgschema_version": "1.7.2",
4+
"created_at": "1970-01-01T00:00:00Z",
5+
"source_fingerprint": {
6+
"hash": "d87f2cfffc1d1273ca588466e14557b2698607c55dc7c8a0e44317046e3c95a9"
7+
},
8+
"groups": [
9+
{
10+
"steps": [
11+
{
12+
"sql": "DROP FUNCTION IF EXISTS somefunction(text);",
13+
"type": "function",
14+
"operation": "alter",
15+
"path": "public.somefunction"
16+
},
17+
{
18+
"sql": "CREATE OR REPLACE FUNCTION somefunction(\n new_name text\n)\nRETURNS text\nLANGUAGE sql\nVOLATILE\nAS $$ SELECT new_name;\n$$;",
19+
"type": "function",
20+
"operation": "alter",
21+
"path": "public.somefunction"
22+
}
23+
]
24+
}
25+
]
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
DROP FUNCTION IF EXISTS somefunction(text);
2+
3+
CREATE OR REPLACE FUNCTION somefunction(
4+
new_name text
5+
)
6+
RETURNS text
7+
LANGUAGE sql
8+
VOLATILE
9+
AS $$ SELECT new_name;
10+
$$;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Plan: 1 to modify.
2+
3+
Summary by type:
4+
functions: 1 to modify
5+
6+
Functions:
7+
~ somefunction
8+
9+
DDL to be executed:
10+
--------------------------------------------------
11+
12+
DROP FUNCTION IF EXISTS somefunction(text);
13+
14+
CREATE OR REPLACE FUNCTION somefunction(
15+
new_name text
16+
)
17+
RETURNS text
18+
LANGUAGE sql
19+
VOLATILE
20+
AS $$ SELECT new_name;
21+
$$;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
DROP FUNCTION IF EXISTS somefunction(text);
2+
3+
CREATE OR REPLACE FUNCTION somefunction(
4+
param2 uuid
5+
)
6+
RETURNS uuid
7+
LANGUAGE sql
8+
VOLATILE
9+
AS $$ SELECT param2;
10+
$$;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE OR REPLACE FUNCTION somefunction(
2+
param2 uuid
3+
) RETURNS uuid
4+
LANGUAGE sql
5+
AS $$ SELECT param2; $$;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE OR REPLACE FUNCTION somefunction(
2+
param1 text
3+
) RETURNS text
4+
LANGUAGE sql
5+
AS $$ SELECT param1; $$;

0 commit comments

Comments
 (0)