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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Thumbs.db

# Build output
pgschema
docs/plans/
dist/
build/

Expand Down
95 changes: 95 additions & 0 deletions internal/diff/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func generateCreateFunctionsSQL(functions []*ir.Function, targetSchema string, c
}

collector.collect(context, sql)

// Generate COMMENT ON FUNCTION if the function has a comment
if function.Comment != "" {
generateFunctionComment(function, targetSchema, DiffTypeFunction, DiffOperationCreate, collector)
}
}
}

Expand All @@ -39,6 +44,15 @@ func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, coll
oldFunc := diff.Old
newFunc := diff.New

// Check if only comment changed (no body/attribute changes)
onlyCommentChanged := functionsEqualExceptComment(oldFunc, newFunc) && oldFunc.Comment != newFunc.Comment

if onlyCommentChanged {
// Only the comment changed - generate just COMMENT ON FUNCTION
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
continue
}

// Check if only LEAKPROOF or PARALLEL attributes changed (not the function body/definition)
onlyAttributesChanged := functionsEqualExceptAttributes(oldFunc, newFunc)

Expand Down Expand Up @@ -83,6 +97,11 @@ func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, coll
}
collector.collect(context, stmt)
}

// Check if comment also changed alongside attributes
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 All @@ -97,6 +116,11 @@ func generateModifyFunctionsSQL(diffs []*functionDiff, targetSchema string, coll
}

collector.collect(context, sql)

// Check if comment also changed alongside body changes
if oldFunc.Comment != newFunc.Comment {
generateFunctionComment(newFunc, targetSchema, DiffTypeFunction, DiffOperationAlter, collector)
}
}
}
}
Expand Down Expand Up @@ -369,6 +393,9 @@ func functionsEqual(old, new *ir.Function) bool {
if old.Parallel != new.Parallel {
return false
}
if old.Comment != new.Comment {
return false
}

// Compare using normalized Parameters array
// This ensures type aliases like "character varying" vs "varchar" are treated as equal
Expand All @@ -379,6 +406,46 @@ func functionsEqual(old, new *ir.Function) bool {
return parametersEqual(oldInputParams, newInputParams)
}

// functionsEqualExceptComment compares two functions ignoring comment differences
// Used to determine if only the comment changed (no body/attribute changes needed)
func functionsEqualExceptComment(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
}
if old.IsLeakproof != new.IsLeakproof {
return false
}
if old.Parallel != new.Parallel {
return false
}
// Note: We intentionally do NOT compare Comment here

oldInputParams := filterNonTableParameters(old.Parameters)
newInputParams := filterNonTableParameters(new.Parameters)
return parametersEqual(oldInputParams, newInputParams)
}

// 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 Expand Up @@ -433,3 +500,31 @@ func parameterEqual(old, new *ir.Parameter) bool {

return true
}

// generateFunctionComment generates COMMENT ON FUNCTION statement
func generateFunctionComment(
function *ir.Function,
targetSchema string,
diffType DiffType,
operation DiffOperation,
collector *diffCollector,
) {
functionName := qualifyEntityName(function.Schema, function.Name, targetSchema)
argsList := function.GetArguments()

var sql string
if function.Comment == "" {
sql = fmt.Sprintf("COMMENT ON FUNCTION %s(%s) IS NULL;", functionName, argsList)
} else {
sql = fmt.Sprintf("COMMENT ON FUNCTION %s(%s) IS %s;", functionName, argsList, quoteString(function.Comment))
}

context := &diffContext{
Type: diffType,
Operation: operation,
Path: fmt.Sprintf("%s.%s", function.Schema, function.Name),
Source: function,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}
90 changes: 86 additions & 4 deletions internal/diff/procedure.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,50 @@ func generateCreateProceduresSQL(procedures []*ir.Procedure, targetSchema string
}

collector.collect(context, sql)

// Generate COMMENT ON PROCEDURE if the procedure has a comment
if procedure.Comment != "" {
generateProcedureComment(procedure, targetSchema, DiffTypeProcedure, DiffOperationCreate, collector)
}
}
}

// generateModifyProceduresSQL generates DROP and CREATE PROCEDURE statements for modified procedures
func generateModifyProceduresSQL(diffs []*procedureDiff, targetSchema string, collector *diffCollector) {
for _, diff := range diffs {
oldProc := diff.Old
newProc := diff.New

// Check if only comment changed (no body changes)
onlyCommentChanged := proceduresEqualExceptComment(oldProc, newProc) && oldProc.Comment != newProc.Comment

if onlyCommentChanged {
// Only the comment changed - generate just COMMENT ON PROCEDURE
generateProcedureComment(newProc, targetSchema, DiffTypeProcedure, DiffOperationAlter, collector)
continue
}

// Drop the old procedure first
procedureName := qualifyEntityName(diff.Old.Schema, diff.Old.Name, targetSchema)
procedureName := qualifyEntityName(oldProc.Schema, oldProc.Name, targetSchema)
var dropSQL string

// For DROP statements, we need the full parameter signature including modes and names
paramSignature := formatProcedureParametersForDrop(diff.Old)
paramSignature := formatProcedureParametersForDrop(oldProc)
if paramSignature != "" {
dropSQL = fmt.Sprintf("DROP PROCEDURE IF EXISTS %s(%s);", procedureName, paramSignature)
} else {
dropSQL = fmt.Sprintf("DROP PROCEDURE IF EXISTS %s();", procedureName)
}

// Create the new procedure
createSQL := generateProcedureSQL(diff.New, targetSchema)
createSQL := generateProcedureSQL(newProc, targetSchema)

// Create a single context with ALTER operation and multiple statements
// This represents the modification as a single operation in the summary
alterContext := &diffContext{
Type: DiffTypeProcedure,
Operation: DiffOperationAlter,
Path: fmt.Sprintf("%s.%s", diff.New.Schema, diff.New.Name),
Path: fmt.Sprintf("%s.%s", newProc.Schema, newProc.Name),
Source: diff,
CanRunInTransaction: true,
}
Expand All @@ -68,6 +85,11 @@ func generateModifyProceduresSQL(diffs []*procedureDiff, targetSchema string, co
}

collector.collectStatements(alterContext, statements)

// Check if comment also changed alongside body changes
if oldProc.Comment != newProc.Comment {
generateProcedureComment(newProc, targetSchema, DiffTypeProcedure, DiffOperationAlter, collector)
}
}
}

Expand Down Expand Up @@ -240,6 +262,9 @@ func proceduresEqual(old, new *ir.Procedure) bool {
if old.Language != new.Language {
return false
}
if old.Comment != new.Comment {
return false
}

// Compare using normalized Parameters array instead of Signature
// This ensures proper comparison regardless of how parameters are specified
Expand All @@ -258,6 +283,35 @@ func proceduresEqual(old, new *ir.Procedure) bool {
return true
}

// proceduresEqualExceptComment compares two procedures ignoring comment differences
// Used to determine if only the comment changed (no body changes needed)
func proceduresEqualExceptComment(old, new *ir.Procedure) bool {
if old.Schema != new.Schema {
return false
}
if old.Name != new.Name {
return false
}
if old.Definition != new.Definition {
return false
}
if old.Language != new.Language {
return false
}
// Note: We intentionally do NOT compare Comment here

hasOldParams := len(old.Parameters) > 0
hasNewParams := len(new.Parameters) > 0

if hasOldParams && hasNewParams {
return parametersEqual(old.Parameters, new.Parameters)
} else if hasOldParams || hasNewParams {
return false
}

return true
}

// formatProcedureParametersForDrop formats procedure parameters for DROP PROCEDURE statements
// Returns the full parameter signature including mode and name (e.g., "IN order_id integer, IN amount numeric")
// This is necessary for proper procedure identification in PostgreSQL
Expand All @@ -271,3 +325,31 @@ func formatProcedureParametersForDrop(procedure *ir.Procedure) string {
}
return strings.Join(paramParts, ", ")
}

// generateProcedureComment generates COMMENT ON PROCEDURE statement
func generateProcedureComment(
procedure *ir.Procedure,
targetSchema string,
diffType DiffType,
operation DiffOperation,
collector *diffCollector,
) {
procedureName := qualifyEntityName(procedure.Schema, procedure.Name, targetSchema)
argsList := procedure.GetArguments()

var sql string
if procedure.Comment == "" {
sql = fmt.Sprintf("COMMENT ON PROCEDURE %s(%s) IS NULL;", procedureName, argsList)
} else {
sql = fmt.Sprintf("COMMENT ON PROCEDURE %s(%s) IS %s;", procedureName, argsList, quoteString(procedure.Comment))
}

context := &diffContext{
Type: diffType,
Operation: operation,
Path: fmt.Sprintf("%s.%s", procedure.Schema, procedure.Name),
Source: procedure,
CanRunInTransaction: true,
}
collector.collect(context, sql)
}
1 change: 1 addition & 0 deletions testdata/diff/comment/add_function_comment/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMENT ON FUNCTION calculate_total(numeric, integer) IS 'Calculates total price from unit price and quantity';
6 changes: 6 additions & 0 deletions testdata/diff/comment/add_function_comment/new.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE FUNCTION public.calculate_total(price numeric, quantity integer)
RETURNS numeric
LANGUAGE sql
AS $$SELECT price * quantity$$;

COMMENT ON FUNCTION public.calculate_total(numeric, integer) IS 'Calculates total price from unit price and quantity';
4 changes: 4 additions & 0 deletions testdata/diff/comment/add_function_comment/old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE FUNCTION public.calculate_total(price numeric, quantity integer)
RETURNS numeric
LANGUAGE sql
AS $$SELECT price * quantity$$;
20 changes: 20 additions & 0 deletions testdata/diff/comment/add_function_comment/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"pgschema_version": "1.5.1",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "b6bab766c934e98996959773eed9f4a536ad858a300beac3fc74b5edc0359228"
},
"groups": [
{
"steps": [
{
"sql": "COMMENT ON FUNCTION calculate_total(numeric, integer) IS 'Calculates total price from unit price and quantity';",
"type": "function",
"operation": "alter",
"path": "public.calculate_total"
}
]
}
]
}
1 change: 1 addition & 0 deletions testdata/diff/comment/add_function_comment/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMENT ON FUNCTION calculate_total(numeric, integer) IS 'Calculates total price from unit price and quantity';
12 changes: 12 additions & 0 deletions testdata/diff/comment/add_function_comment/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Plan: 1 to modify.

Summary by type:
functions: 1 to modify

Functions:
~ calculate_total

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

COMMENT ON FUNCTION calculate_total(numeric, integer) IS 'Calculates total price from unit price and quantity';
1 change: 1 addition & 0 deletions testdata/diff/comment/add_procedure_comment/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMENT ON PROCEDURE process_order(integer) IS 'Processes a single order by ID';
5 changes: 5 additions & 0 deletions testdata/diff/comment/add_procedure_comment/new.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE PROCEDURE public.process_order(IN order_id integer)
LANGUAGE sql
AS $$SELECT 1$$;

COMMENT ON PROCEDURE public.process_order(integer) IS 'Processes a single order by ID';
3 changes: 3 additions & 0 deletions testdata/diff/comment/add_procedure_comment/old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE PROCEDURE public.process_order(IN order_id integer)
LANGUAGE sql
AS $$SELECT 1$$;
20 changes: 20 additions & 0 deletions testdata/diff/comment/add_procedure_comment/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"pgschema_version": "1.5.1",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "f2a1199280f3ed3bb4f7dad3602c7f0fcacfce9d79e40201b48b5bf7325f5c64"
},
"groups": [
{
"steps": [
{
"sql": "COMMENT ON PROCEDURE process_order(integer) IS 'Processes a single order by ID';",
"type": "procedure",
"operation": "alter",
"path": "public.process_order"
}
]
}
]
}
1 change: 1 addition & 0 deletions testdata/diff/comment/add_procedure_comment/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
COMMENT ON PROCEDURE process_order(integer) IS 'Processes a single order by ID';
12 changes: 12 additions & 0 deletions testdata/diff/comment/add_procedure_comment/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Plan: 1 to modify.

Summary by type:
procedures: 1 to modify

Procedures:
~ process_order

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

COMMENT ON PROCEDURE process_order(integer) IS 'Processes a single order by ID';