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
100 changes: 50 additions & 50 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,24 +250,24 @@ type Diff struct {
}

type ddlDiff struct {
addedSchemas []*ir.Schema
droppedSchemas []*ir.Schema
modifiedSchemas []*schemaDiff
addedTables []*ir.Table
droppedTables []*ir.Table
modifiedTables []*tableDiff
addedViews []*ir.View
droppedViews []*ir.View
modifiedViews []*viewDiff
addedFunctions []*ir.Function
droppedFunctions []*ir.Function
modifiedFunctions []*functionDiff
addedProcedures []*ir.Procedure
droppedProcedures []*ir.Procedure
modifiedProcedures []*procedureDiff
addedTypes []*ir.Type
droppedTypes []*ir.Type
modifiedTypes []*typeDiff
addedSchemas []*ir.Schema
droppedSchemas []*ir.Schema
modifiedSchemas []*schemaDiff
addedTables []*ir.Table
droppedTables []*ir.Table
modifiedTables []*tableDiff
addedViews []*ir.View
droppedViews []*ir.View
modifiedViews []*viewDiff
addedFunctions []*ir.Function
droppedFunctions []*ir.Function
modifiedFunctions []*functionDiff
addedProcedures []*ir.Procedure
droppedProcedures []*ir.Procedure
modifiedProcedures []*procedureDiff
addedTypes []*ir.Type
droppedTypes []*ir.Type
modifiedTypes []*typeDiff
addedSequences []*ir.Sequence
droppedSequences []*ir.Sequence
modifiedSequences []*sequenceDiff
Expand Down Expand Up @@ -412,38 +412,38 @@ type rlsChange struct {
// GenerateMigration compares two IR schemas and returns the SQL differences
func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff {
diff := &ddlDiff{
addedSchemas: []*ir.Schema{},
droppedSchemas: []*ir.Schema{},
modifiedSchemas: []*schemaDiff{},
addedTables: []*ir.Table{},
droppedTables: []*ir.Table{},
modifiedTables: []*tableDiff{},
addedViews: []*ir.View{},
droppedViews: []*ir.View{},
modifiedViews: []*viewDiff{},
addedFunctions: []*ir.Function{},
droppedFunctions: []*ir.Function{},
modifiedFunctions: []*functionDiff{},
addedProcedures: []*ir.Procedure{},
droppedProcedures: []*ir.Procedure{},
modifiedProcedures: []*procedureDiff{},
addedTypes: []*ir.Type{},
droppedTypes: []*ir.Type{},
modifiedTypes: []*typeDiff{},
addedSequences: []*ir.Sequence{},
droppedSequences: []*ir.Sequence{},
modifiedSequences: []*sequenceDiff{},
addedDefaultPrivileges: []*ir.DefaultPrivilege{},
droppedDefaultPrivileges: []*ir.DefaultPrivilege{},
modifiedDefaultPrivileges: []*defaultPrivilegeDiff{},
addedPrivileges: []*ir.Privilege{},
droppedPrivileges: []*ir.Privilege{},
modifiedPrivileges: []*privilegeDiff{},
addedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{},
droppedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{},
addedColumnPrivileges: []*ir.ColumnPrivilege{},
droppedColumnPrivileges: []*ir.ColumnPrivilege{},
modifiedColumnPrivileges: []*columnPrivilegeDiff{},
addedSchemas: []*ir.Schema{},
droppedSchemas: []*ir.Schema{},
modifiedSchemas: []*schemaDiff{},
addedTables: []*ir.Table{},
droppedTables: []*ir.Table{},
modifiedTables: []*tableDiff{},
addedViews: []*ir.View{},
droppedViews: []*ir.View{},
modifiedViews: []*viewDiff{},
addedFunctions: []*ir.Function{},
droppedFunctions: []*ir.Function{},
modifiedFunctions: []*functionDiff{},
addedProcedures: []*ir.Procedure{},
droppedProcedures: []*ir.Procedure{},
modifiedProcedures: []*procedureDiff{},
addedTypes: []*ir.Type{},
droppedTypes: []*ir.Type{},
modifiedTypes: []*typeDiff{},
addedSequences: []*ir.Sequence{},
droppedSequences: []*ir.Sequence{},
modifiedSequences: []*sequenceDiff{},
addedDefaultPrivileges: []*ir.DefaultPrivilege{},
droppedDefaultPrivileges: []*ir.DefaultPrivilege{},
modifiedDefaultPrivileges: []*defaultPrivilegeDiff{},
addedPrivileges: []*ir.Privilege{},
droppedPrivileges: []*ir.Privilege{},
modifiedPrivileges: []*privilegeDiff{},
addedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{},
droppedRevokedDefaultPrivs: []*ir.RevokedDefaultPrivilege{},
addedColumnPrivileges: []*ir.ColumnPrivilege{},
droppedColumnPrivileges: []*ir.ColumnPrivilege{},
modifiedColumnPrivileges: []*columnPrivilegeDiff{},
}

// Compare schemas first in deterministic order
Expand Down
3 changes: 3 additions & 0 deletions internal/diff/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (

// generateCreateFunctionsSQL generates CREATE FUNCTION statements
func generateCreateFunctionsSQL(functions []*ir.Function, targetSchema string, collector *diffCollector) {
// Build dependencies from function bodies (supplements pg_depend, which doesn't track SQL function body references)
buildFunctionBodyDependencies(functions)

// Sort functions by dependency order (topological sort)
sortedFunctions := topologicallySortFunctions(functions)

Expand Down
79 changes: 79 additions & 0 deletions internal/diff/topological.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package diff

import (
"sort"
"strings"

"github.com/pgschema/pgschema/ir"
)
Expand Down Expand Up @@ -653,3 +654,81 @@ func constraintMatchesFKReference(uniqueConstraint, fkConstraint *ir.Constraint)

return true
}

// buildFunctionBodyDependencies scans function bodies for function calls and populates
// the Dependencies field. This supplements dependencies from pg_depend, which doesn't
// track references inside SQL function bodies.
func buildFunctionBodyDependencies(functions []*ir.Function) {
if len(functions) <= 1 {
return
}

// Build lookup maps by function name (both qualified and unqualified)
// Map to the full key format used by Dependencies: schema.name(args)
type funcInfo struct {
fn *ir.Function
key string
}
functionLookup := make(map[string]funcInfo)

for _, fn := range functions {
key := fn.Schema + "." + fn.Name + "(" + fn.GetArguments() + ")"
name := strings.ToLower(fn.Name)

// Store under unqualified name
functionLookup[name] = funcInfo{fn: fn, key: key}

// Store under qualified name
if fn.Schema != "" {
qualified := strings.ToLower(fn.Schema) + "." + name
functionLookup[qualified] = funcInfo{fn: fn, key: key}
}
}

// For each function, scan its body for function calls
for _, fn := range functions {
if fn.Definition == "" {
continue
}

fnKey := fn.Schema + "." + fn.Name + "(" + fn.GetArguments() + ")"

matches := functionCallRegex.FindAllStringSubmatch(fn.Definition, -1)
for _, match := range matches {
if len(match) < 2 {
continue
}
identifier := strings.ToLower(match[1])
if identifier == "" {
continue
}

// Try to find the referenced function
var info funcInfo
var found bool

if info, found = functionLookup[identifier]; !found {
// Try with schema prefix if identifier is unqualified
if !strings.Contains(identifier, ".") && fn.Schema != "" {
qualified := strings.ToLower(fn.Schema) + "." + identifier
info, found = functionLookup[qualified]
}
}

// If found and not self-reference, add dependency
if found && info.key != fnKey {
// Check if dependency already exists
alreadyExists := false
for _, existing := range fn.Dependencies {
if existing == info.key {
alreadyExists = true
break
}
}
if !alreadyExists {
fn.Dependencies = append(fn.Dependencies, info.key)
}
}
}
}
}
Loading
Loading