-
Notifications
You must be signed in to change notification settings - Fork 30
fix: varchar normalization #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,8 +63,27 @@ func generateDropFunctionsSQL(functions []*ir.Function, targetSchema string, col | |
| for _, function := range sortedFunctions { | ||
| functionName := qualifyEntityName(function.Schema, function.Name, targetSchema) | ||
| var sql string | ||
| if function.Arguments != "" { | ||
| sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, function.Arguments) | ||
|
|
||
| // Build argument list for DROP statement using normalized Parameters array | ||
| var argsList string | ||
| if len(function.Parameters) > 0 { | ||
| // Format parameters for DROP (omit names and defaults, include only types) | ||
| // Per PostgreSQL docs, DROP FUNCTION only needs input arguments (IN, INOUT, VARIADIC) | ||
| // Exclude OUT and TABLE mode parameters as they're part of the return signature | ||
| var argTypes []string | ||
| for _, param := range function.Parameters { | ||
| // Include only input parameter modes: IN (empty/implicit), INOUT, VARIADIC | ||
| if param.Mode == "" || param.Mode == "IN" || param.Mode == "INOUT" || param.Mode == "VARIADIC" { | ||
| argTypes = append(argTypes, param.DataType) | ||
| } | ||
| } | ||
| argsList = strings.Join(argTypes, ", ") | ||
| } else if function.Arguments != "" { | ||
| argsList = function.Arguments | ||
| } | ||
|
|
||
| if argsList != "" { | ||
| sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s(%s);", functionName, argsList) | ||
| } else { | ||
| sql = fmt.Sprintf("DROP FUNCTION IF EXISTS %s();", functionName) | ||
| } | ||
|
|
@@ -90,8 +109,22 @@ func generateFunctionSQL(function *ir.Function, targetSchema string) string { | |
| functionName := qualifyEntityName(function.Schema, function.Name, targetSchema) | ||
| stmt.WriteString(fmt.Sprintf("CREATE OR REPLACE FUNCTION %s", functionName)) | ||
|
|
||
| // Add parameters using detailed signature if available | ||
| if function.Signature != "" { | ||
| // Add parameters - prefer structured Parameters array for normalized types | ||
| if len(function.Parameters) > 0 { | ||
| // Build parameter list from structured Parameters array | ||
| // Exclude TABLE mode parameters as they're part of RETURNS clause | ||
| var paramParts []string | ||
| for _, param := range function.Parameters { | ||
| if param.Mode != "TABLE" { | ||
| paramParts = append(paramParts, formatFunctionParameter(param, true)) | ||
| } | ||
| } | ||
| if len(paramParts) > 0 { | ||
| stmt.WriteString(fmt.Sprintf("(\n %s\n)", strings.Join(paramParts, ",\n "))) | ||
| } else { | ||
| stmt.WriteString("()") | ||
| } | ||
| } else if function.Signature != "" { | ||
| stmt.WriteString(fmt.Sprintf("(\n %s\n)", strings.ReplaceAll(function.Signature, ", ", ",\n "))) | ||
| } else if function.Arguments != "" { | ||
| stmt.WriteString(fmt.Sprintf("(%s)", function.Arguments)) | ||
|
|
@@ -184,6 +217,32 @@ func containsParameterReferences(body string) bool { | |
| return false | ||
| } | ||
|
|
||
| // formatFunctionParameter formats a single function parameter with name, type, and optional default value | ||
| // For functions, mode is typically omitted (unlike procedures) unless it's OUT/INOUT | ||
| // includeDefault controls whether DEFAULT clauses are included in the output | ||
| func formatFunctionParameter(param *ir.Parameter, includeDefault bool) string { | ||
| var part string | ||
|
|
||
| // For functions, only include mode if it's OUT or INOUT (IN is implicit) | ||
| if param.Mode == "OUT" || param.Mode == "INOUT" || param.Mode == "VARIADIC" { | ||
| part = param.Mode + " " | ||
| } | ||
|
|
||
| // Add parameter name and type | ||
| if param.Name != "" { | ||
| part += param.Name + " " + param.DataType | ||
| } else { | ||
| part += param.DataType | ||
| } | ||
|
Comment on lines
+231
to
+236
|
||
|
|
||
| // Add DEFAULT value if present and requested | ||
| if includeDefault && param.DefaultValue != nil { | ||
| part += " DEFAULT " + *param.DefaultValue | ||
| } | ||
|
|
||
| return part | ||
| } | ||
|
|
||
| // functionsEqual compares two functions for equality | ||
| func functionsEqual(old, new *ir.Function) bool { | ||
| if old.Schema != new.Schema { | ||
|
|
@@ -201,11 +260,76 @@ func functionsEqual(old, new *ir.Function) bool { | |
| if old.Language != new.Language { | ||
| return false | ||
| } | ||
|
|
||
| // For RETURNS TABLE functions, the Parameters array includes TABLE output columns | ||
| // which can cause comparison issues. In this case, rely on ReturnType comparison instead. | ||
| isTableReturn := strings.HasPrefix(old.ReturnType, "TABLE(") || strings.HasPrefix(new.ReturnType, "TABLE(") | ||
|
|
||
| if !isTableReturn { | ||
| // For non-TABLE functions, compare using normalized Parameters array | ||
| // This ensures type aliases like "character varying" vs "varchar" are treated as equal | ||
| hasOldParams := len(old.Parameters) > 0 | ||
| hasNewParams := len(new.Parameters) > 0 | ||
|
|
||
| if hasOldParams && hasNewParams { | ||
| // Both have Parameters - compare them | ||
| return parametersEqual(old.Parameters, new.Parameters) | ||
| } else if hasOldParams || hasNewParams { | ||
| // One has Parameters, one doesn't - they're different | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // For TABLE functions or functions without Parameters, fall back to Arguments/Signature | ||
| if old.Arguments != new.Arguments { | ||
| return false | ||
| } | ||
| if old.Signature != new.Signature { | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // parametersEqual compares two parameter arrays for equality | ||
| func parametersEqual(oldParams, newParams []*ir.Parameter) bool { | ||
| if len(oldParams) != len(newParams) { | ||
| return false | ||
| } | ||
|
|
||
| for i := range oldParams { | ||
| if !parameterEqual(oldParams[i], newParams[i]) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // parameterEqual compares two parameters for equality | ||
| func parameterEqual(old, new *ir.Parameter) bool { | ||
| if old.Name != new.Name { | ||
| return false | ||
| } | ||
|
|
||
| // Compare data types (already normalized by ir.normalizeFunction) | ||
| if old.DataType != new.DataType { | ||
| return false | ||
| } | ||
|
|
||
| if old.Mode != new.Mode { | ||
| return false | ||
| } | ||
|
|
||
| // Compare default values | ||
| if (old.DefaultValue == nil) != (new.DefaultValue == nil) { | ||
| return false | ||
| } | ||
| if old.DefaultValue != nil && new.DefaultValue != nil { | ||
| if *old.DefaultValue != *new.DefaultValue { | ||
| return false | ||
| } | ||
| } | ||
tianzhou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return true | ||
tianzhou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,6 +245,27 @@ func normalizeFunction(function *Function) { | |
| param.DataType = normalizePostgreSQLType(param.DataType) | ||
| } | ||
| } | ||
| // Normalize function body to handle whitespace differences | ||
| function.Definition = normalizeFunctionDefinition(function.Definition) | ||
| } | ||
|
|
||
| // normalizeFunctionDefinition normalizes function body whitespace | ||
| // PostgreSQL stores function bodies with specific whitespace that may differ from source | ||
| func normalizeFunctionDefinition(def string) string { | ||
| if def == "" { | ||
| return def | ||
| } | ||
|
|
||
| // Only trim trailing whitespace from each line, preserving the line structure | ||
| // This ensures leading/trailing blank lines are preserved (matching PostgreSQL storage) | ||
| lines := strings.Split(def, "\n") | ||
| var normalized []string | ||
| for _, line := range lines { | ||
| // Trim trailing whitespace but preserve leading whitespace for indentation | ||
| normalized = append(normalized, strings.TrimRight(line, " \t")) | ||
| } | ||
|
Comment on lines
+259
to
+266
|
||
|
|
||
| return strings.Join(normalized, "\n") | ||
| } | ||
|
|
||
| // normalizeProcedure normalizes procedure representation | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -741,7 +741,7 @@ ORDER BY s.schemaname, s.sequencename; | |||||
| SELECT | ||||||
| r.routine_schema, | ||||||
| r.routine_name, | ||||||
| p.prosrc AS routine_definition, | ||||||
| CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END AS routine_definition, | ||||||
|
||||||
| CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END AS routine_definition, | |
| COALESCE(CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END, '') AS routine_definition, |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above for procedures: wrap with COALESCE to avoid NULL and retain a plain string type in generated code: COALESCE(CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END, '') AS routine_definition, then re-generate.
| CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END AS routine_definition, | |
| COALESCE(CASE WHEN p.prosrc ~ E'\n$' THEN p.prosrc ELSE p.prosrc || E'\n' END, '') AS routine_definition, |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| DROP FUNCTION IF EXISTS get_user_stats(integer); | ||
| DROP FUNCTION IF EXISTS process_order(integer, numeric); | ||
| DROP FUNCTION IF EXISTS process_payment(integer, text); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,5 @@ | ||
| DROP FUNCTION IF EXISTS get_user_stats(integer); | ||
|
|
||
| DROP FUNCTION IF EXISTS process_order(integer, numeric); | ||
|
|
||
| DROP FUNCTION IF EXISTS process_payment(integer, text); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,18 @@ | ||
| Plan: 1 to drop. | ||
| Plan: 3 to drop. | ||
|
|
||
| Summary by type: | ||
| functions: 1 to drop | ||
| functions: 3 to drop | ||
|
|
||
| Functions: | ||
| - get_user_stats | ||
| - process_order | ||
| - process_payment | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| DROP FUNCTION IF EXISTS get_user_stats(integer); | ||
|
|
||
| DROP FUNCTION IF EXISTS process_order(integer, numeric); | ||
|
|
||
| DROP FUNCTION IF EXISTS process_payment(integer, text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prioritizing synthesized Parameters over the original Signature can produce invalid DDL when parameter names require quoting (e.g., reserved words) or when original quoting/formatting is significant. Prefer using function.Signature when present, falling back to structured Parameters only when Signature is empty, or ensure all parameter identifiers are properly quoted.