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
71 changes: 69 additions & 2 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,73 @@ func (i *Inspector) buildFunctions(ctx context.Context, schema *IR, targetSchema
return nil
}

// splitParameterString splits a parameter string by commas, but respects quotes,
// parentheses, and brackets. This handles complex defaults like '{1,2,3}' or '{"key": "value"}'
func splitParameterString(signature string) []string {
var params []string
var current strings.Builder
depth := 0 // Track nesting depth of (), [], {}
inQuote := false // Track if we're inside a string literal

i := 0
for i < len(signature) {
ch := rune(signature[i])

switch ch {
case '\'':
// Toggle quote state, but handle escaped quotes
if !inQuote {
inQuote = true
current.WriteRune(ch)
i++
} else {
// Check if this is an escaped quote (two single quotes)
if i+1 < len(signature) && signature[i+1] == '\'' {
current.WriteRune(ch)
current.WriteRune('\'')
i += 2 // Skip both quotes
} else {
inQuote = false
current.WriteRune(ch)
i++
}
}
case '(', '[', '{':
if !inQuote {
depth++
}
current.WriteRune(ch)
i++
case ')', ']', '}':
if !inQuote {
depth--
}
current.WriteRune(ch)
i++
case ',':
if !inQuote && depth == 0 {
// This comma is a parameter separator
params = append(params, strings.TrimSpace(current.String()))
current.Reset()
} else {
// This comma is inside quotes or nested structure
current.WriteRune(ch)
}
i++
default:
current.WriteRune(ch)
i++
}
}

// Add the last parameter
if current.Len() > 0 {
params = append(params, strings.TrimSpace(current.String()))
}

return params
}

// parseParametersFromSignature parses function signature string into Parameter structs
// Example signature: "order_id integer, discount_percent numeric DEFAULT 0"
// Or with modes: "IN order_id integer, OUT result integer"
Expand All @@ -1180,8 +1247,8 @@ func (i *Inspector) parseParametersFromSignature(signature string) []*Parameter
var parameters []*Parameter
position := 1

// Split by comma to get individual parameters
paramStrings := strings.Split(signature, ",")
// Split by comma to get individual parameters (smart split that respects quotes/brackets)
paramStrings := splitParameterString(signature)
for _, paramStr := range paramStrings {
paramStr = strings.TrimSpace(paramStr)
if paramStr == "" {
Expand Down
43 changes: 31 additions & 12 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,40 @@ func normalizeDefaultValue(value string) string {
}

// Handle type casting - remove explicit type casts that are semantically equivalent
// Use regex to properly handle type casts within complex expressions
// Pattern: 'literal'::type -> 'literal' (removes redundant casts from string literals)
if strings.Contains(value, "::") {
// Use regex to match and remove type casts from string literals
// This handles: 'text'::text, 'utc'::text, '{}'::jsonb, '{}'::text[], etc.
// Also handles multi-word types like 'value'::character varying
// Handle NULL::type -> NULL
// Example: NULL::text -> NULL
re := regexp.MustCompile(`\bNULL::(?:[a-zA-Z_][\w\s.]*)(?:\[\])?`)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

This regex is compiled on every call to normalizeDefaultValue(). Consider moving regex compilation to package-level variables to avoid repeated compilation overhead.

Copilot uses AI. Check for mistakes.
value = re.ReplaceAllString(value, "NULL")

// Handle numeric literals with type casts
// Example: '-1'::integer -> -1
// Example: '100'::bigint -> 100
// Note: PostgreSQL sometimes casts numeric literals to different types, e.g., -1::integer stored as numeric
re = regexp.MustCompile(`'(-?\d+(?:\.\d+)?)'::(?:integer|bigint|smallint|numeric|decimal|real|double precision|int2|int4|int8|float4|float8)`)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

This regex is compiled on every call to normalizeDefaultValue(). Consider moving regex compilation to package-level variables to avoid repeated compilation overhead.

Copilot uses AI. Check for mistakes.
value = re.ReplaceAllString(value, "$1")

// Handle string literals with type casts (including escaped quotes)
// Example: 'text'::text -> 'text'
// Example: 'O''Brien'::text -> 'O''Brien'
// Example: '{}'::jsonb -> '{}'
// Example: '{1,2,3}'::integer[] -> '{1,2,3}'
// Pattern explanation:
// '([^']*)' - matches a quoted string literal (capturing the content)
// '(?:[^']|'')*' - matches a quoted string literal, handling escaped quotes ''
// ::[a-zA-Z_][\w\s.]* - matches ::typename
// [a-zA-Z_] - type name must start with letter or underscore
// [\w\s.]* - followed by word chars, spaces, or dots (for "character varying" or "pg_catalog.text")
// (?:\[\])? - optionally followed by [] for array types (non-capturing group)
// (?:\b|(?=\[)|$) - followed by word boundary, opening bracket, or end of string
re := regexp.MustCompile(`'([^']*)'::(?:[a-zA-Z_][\w\s.]*)(?:\[\])?`)
value = re.ReplaceAllString(value, "'$1'")
// (?:\[\])? - optionally followed by [] for array types
re = regexp.MustCompile(`('(?:[^']|'')*')::(?:[a-zA-Z_][\w\s.]*)(?:\[\])?`)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

This regex is compiled on every call to normalizeDefaultValue(). Consider moving regex compilation to package-level variables to avoid repeated compilation overhead.

Copilot uses AI. Check for mistakes.
value = re.ReplaceAllString(value, "$1")

// Handle date/timestamp literals with type casts
// Example: '2024-01-01'::date -> '2024-01-01'
// Already handled by the string literal pattern above

// Handle parenthesized expressions with type casts - remove outer parentheses
// Example: (100)::bigint -> 100::bigint
// Pattern captures the number and the type cast separately
re = regexp.MustCompile(`\((\d+)\)(::(?:bigint|integer|smallint|numeric|decimal))`)
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

This regex is compiled on every call to normalizeDefaultValue(). Consider moving regex compilation to package-level variables to avoid repeated compilation overhead.

Copilot uses AI. Check for mistakes.
value = re.ReplaceAllString(value, "$1$2")
}

return value
Expand Down
6 changes: 5 additions & 1 deletion testdata/diff/create_function/add_function/diff.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
CREATE OR REPLACE FUNCTION process_order(
order_id integer,
discount_percent numeric DEFAULT 0,
note varchar DEFAULT ''
priority_level integer DEFAULT 1,
note varchar DEFAULT '',
status text DEFAULT 'pending',
apply_tax boolean DEFAULT true,
is_priority boolean DEFAULT false
)
RETURNS numeric
LANGUAGE plpgsql
Expand Down
9 changes: 8 additions & 1 deletion testdata/diff/create_function/add_function/new.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
CREATE FUNCTION process_order(
order_id integer,
-- Simple numeric defaults
discount_percent numeric DEFAULT 0,
note varchar DEFAULT ''
priority_level integer DEFAULT 1,
-- String defaults
note varchar DEFAULT '',
status text DEFAULT 'pending',
-- Boolean defaults
apply_tax boolean DEFAULT true,
is_priority boolean DEFAULT false
)
RETURNS numeric
LANGUAGE plpgsql
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_function/add_function/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"steps": [
{
"sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n note varchar DEFAULT ''\n)\nRETURNS numeric\nLANGUAGE plpgsql\nSECURITY DEFINER\nVOLATILE\nSTRICT\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;",
"sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false\n)\nRETURNS numeric\nLANGUAGE plpgsql\nSECURITY DEFINER\nVOLATILE\nSTRICT\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;",
"type": "function",
"operation": "create",
"path": "public.process_order"
Expand Down
6 changes: 5 additions & 1 deletion testdata/diff/create_function/add_function/plan.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
CREATE OR REPLACE FUNCTION process_order(
order_id integer,
discount_percent numeric DEFAULT 0,
note varchar DEFAULT ''
priority_level integer DEFAULT 1,
note varchar DEFAULT '',
status text DEFAULT 'pending',
apply_tax boolean DEFAULT true,
is_priority boolean DEFAULT false
)
RETURNS numeric
LANGUAGE plpgsql
Expand Down
6 changes: 5 additions & 1 deletion testdata/diff/create_function/add_function/plan.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ DDL to be executed:
CREATE OR REPLACE FUNCTION process_order(
order_id integer,
discount_percent numeric DEFAULT 0,
note varchar DEFAULT ''
priority_level integer DEFAULT 1,
note varchar DEFAULT '',
status text DEFAULT 'pending',
apply_tax boolean DEFAULT true,
is_priority boolean DEFAULT false
)
RETURNS numeric
LANGUAGE plpgsql
Expand Down