From 999275902dbc1a9502fc5ded27ac1c68466371fb Mon Sep 17 00:00:00 2001 From: Peter Brodsky Date: Fri, 7 Nov 2025 22:04:47 -0500 Subject: [PATCH] fix: normalize function argument types to prevent spurious replacements This fixes a bug where function replacements would fail because PostgreSQL stores parameter types with schema qualifications (e.g., 'public.user_status') while the source SQL might use unqualified names (e.g., 'user_status'). When comparing function signatures, pgschema would treat these as different functions and try to DROP the old one before CREATE OR REPLACE, causing failures when other objects referenced the function. Changes: - Added stripSchemaPrefixFromType() helper to normalize type names in function arguments by removing schema prefixes that match the function's schema - Modified Function.GetArguments() to strip schema prefixes from all parameter types before building the argument signature - This ensures 'public.user_status' and 'user_status' are treated as identical when comparing function signatures Fixes #155 (partially - this is the third of three fixes) --- ir/ir.go | 92 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/ir/ir.go b/ir/ir.go index ac33bd6a..e430fa09 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -1,6 +1,7 @@ package ir import ( + "fmt" "strings" "sync" ) @@ -43,7 +44,7 @@ type LikeClause struct { type Table struct { Schema string `json:"schema"` Name string `json:"name"` - Type TableType `json:"type"` // BASE_TABLE, VIEW, etc. + Type TableType `json:"type"` // BASE_TABLE, VIEW, etc. IsExternal bool `json:"is_external,omitempty"` // True if table is externally managed (e.g., in ignored schemas) Columns []*Column `json:"columns"` Constraints map[string]*Constraint `json:"constraints"` // constraint_name -> Constraint @@ -61,18 +62,18 @@ type Table struct { // Column represents a table column type Column struct { - Name string `json:"name"` - Position int `json:"position"` // ordinal_position - DataType string `json:"data_type"` - IsNullable bool `json:"is_nullable"` - DefaultValue *string `json:"default_value,omitempty"` - MaxLength *int `json:"max_length,omitempty"` - Precision *int `json:"precision,omitempty"` - Scale *int `json:"scale,omitempty"` - Comment string `json:"comment,omitempty"` - Identity *Identity `json:"identity,omitempty"` - GeneratedExpr *string `json:"generated_expr,omitempty"` // Expression for generated columns - IsGenerated bool `json:"is_generated,omitempty"` // True if this is a generated column + Name string `json:"name"` + Position int `json:"position"` // ordinal_position + DataType string `json:"data_type"` + IsNullable bool `json:"is_nullable"` + DefaultValue *string `json:"default_value,omitempty"` + MaxLength *int `json:"max_length,omitempty"` + Precision *int `json:"precision,omitempty"` + Scale *int `json:"scale,omitempty"` + Comment string `json:"comment,omitempty"` + Identity *Identity `json:"identity,omitempty"` + GeneratedExpr *string `json:"generated_expr,omitempty"` // Expression for generated columns + IsGenerated bool `json:"is_generated,omitempty"` // True if this is a generated column } // Identity represents PostgreSQL identity column configuration @@ -148,13 +149,42 @@ func (f *Function) GetArguments() string { // Include only input parameter modes for DROP FUNCTION compatibility // Exclude OUT and TABLE mode parameters (they're part of return signature) if param.Mode == "" || param.Mode == "IN" || param.Mode == "INOUT" || param.Mode == "VARIADIC" { - argTypes = append(argTypes, param.DataType) + argTypes = append(argTypes, stripSchemaPrefixFromType(param.DataType, f.Schema)) } } return strings.Join(argTypes, ", ") } +// stripSchemaPrefixFromType removes the schema qualification from a type name +// when it matches the provided schema. This ensures function identity arguments +// remain stable even if PostgreSQL stores parameter types as schema-qualified +// names (e.g., public.member_status). +func stripSchemaPrefixFromType(typeName, schema string) string { + if typeName == "" || schema == "" { + return typeName + } + + unquotedSchema := schema + if strings.HasPrefix(unquotedSchema, "\"") && strings.HasSuffix(unquotedSchema, "\"") { + unquotedSchema = unquotedSchema[1 : len(unquotedSchema)-1] + } + + // Handle quoted schema qualification: "schema".type + quotedPrefix := fmt.Sprintf("\"%s\".", unquotedSchema) + if strings.HasPrefix(typeName, quotedPrefix) { + return typeName[len(quotedPrefix):] + } + + // Handle unquoted schema qualification: schema.type + unquotedPrefix := unquotedSchema + "." + if strings.HasPrefix(typeName, unquotedPrefix) { + return typeName[len(unquotedPrefix):] + } + + return typeName +} + // Parameter represents a function parameter type Parameter struct { Name string `json:"name"` @@ -258,11 +288,11 @@ type Trigger struct { Function string `json:"function"` Condition string `json:"condition,omitempty"` // WHEN condition Comment string `json:"comment,omitempty"` - IsConstraint bool `json:"is_constraint,omitempty"` // Whether this is a constraint trigger - Deferrable bool `json:"deferrable,omitempty"` // Can be deferred until end of transaction - InitiallyDeferred bool `json:"initially_deferred,omitempty"` // Whether deferred by default - OldTable string `json:"old_table,omitempty"` // REFERENCING OLD TABLE AS name - NewTable string `json:"new_table,omitempty"` // REFERENCING NEW TABLE AS name + IsConstraint bool `json:"is_constraint,omitempty"` // Whether this is a constraint trigger + Deferrable bool `json:"deferrable,omitempty"` // Can be deferred until end of transaction + InitiallyDeferred bool `json:"initially_deferred,omitempty"` // Whether deferred by default + OldTable string `json:"old_table,omitempty"` // REFERENCING OLD TABLE AS name + NewTable string `json:"new_table,omitempty"` // REFERENCING NEW TABLE AS name } // TriggerTiming represents the timing of trigger execution @@ -292,7 +322,6 @@ const ( TriggerLevelStatement TriggerLevel = "STATEMENT" ) - // RLSPolicy represents a Row Level Security policy type RLSPolicy struct { Schema string `json:"schema"` @@ -377,8 +406,6 @@ type Procedure struct { Comment string `json:"comment,omitempty"` } - - // NewIR creates a new empty catalog IR func NewIR() *IR { return &IR{ @@ -538,15 +565,14 @@ func (s *Schema) SetType(name string, typ *Type) { } // DiffSource interface implementations for IR types -func (t *Table) IsDiffSource() {} -func (c *Column) IsDiffSource() {} +func (t *Table) IsDiffSource() {} +func (c *Column) IsDiffSource() {} func (c *Constraint) IsDiffSource() {} -func (i *Index) IsDiffSource() {} -func (t *Trigger) IsDiffSource() {} -func (p *RLSPolicy) IsDiffSource() {} -func (f *Function) IsDiffSource() {} -func (p *Procedure) IsDiffSource() {} -func (v *View) IsDiffSource() {} -func (s *Sequence) IsDiffSource() {} -func (t *Type) IsDiffSource() {} - +func (i *Index) IsDiffSource() {} +func (t *Trigger) IsDiffSource() {} +func (p *RLSPolicy) IsDiffSource() {} +func (f *Function) IsDiffSource() {} +func (p *Procedure) IsDiffSource() {} +func (v *View) IsDiffSource() {} +func (s *Sequence) IsDiffSource() {} +func (t *Type) IsDiffSource() {}