-
Notifications
You must be signed in to change notification settings - Fork 29
fix: preserve interval type casts in function parameter defaults (#216) #217
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,21 +8,55 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/pgschema/pgschema/ir" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // stripSchemaPrefix removes the schema prefix from a type name if it matches the target schema | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // stripSchemaPrefix removes the schema prefix from a type name if it matches the target schema. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // It handles both simple type names (e.g., "schema.typename") and type casts within expressions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (e.g., "'value'::schema.typename" -> "'value'::typename"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func stripSchemaPrefix(typeName, targetSchema string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if typeName == "" || targetSchema == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if the type has the target schema prefix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if the type has the target schema prefix at the beginning | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prefix := targetSchema + "." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if after, found := strings.CutPrefix(typeName, prefix); found { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return after | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Also handle type casts within expressions: ::schema.typename -> ::typename | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This is needed for function parameter default values like 'value'::schema.enum_type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| castPrefix := "::" + targetSchema + "." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.Contains(typeName, castPrefix) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return strings.ReplaceAll(typeName, castPrefix, "::") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // stripTempSchemaPrefix removes temporary embedded postgres schema prefixes (pgschema_tmp_*). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // These are used internally during plan generation and should not appear in output DDL. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func stripTempSchemaPrefix(value string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if value == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Pattern: ::pgschema_tmp_YYYYMMDD_HHMMSS_XXXXXXXX.typename -> ::typename | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We look for ::pgschema_tmp_ followed by anything until the next dot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| idx := strings.Index(value, "::pgschema_tmp_") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if idx == -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the dot after pgschema_tmp_* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dotIdx := strings.Index(value[idx+15:], ".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if dotIdx == -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace ::pgschema_tmp_XXX.typename with ::typename | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prefix := value[idx : idx+15+dotIdx+1] // includes the trailing dot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+56
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| idx := strings.Index(value, "::pgschema_tmp_") | |
| if idx == -1 { | |
| return value | |
| } | |
| // Find the dot after pgschema_tmp_* | |
| dotIdx := strings.Index(value[idx+15:], ".") | |
| if dotIdx == -1 { | |
| return value | |
| } | |
| // Replace ::pgschema_tmp_XXX.typename with ::typename | |
| prefix := value[idx : idx+15+dotIdx+1] // includes the trailing dot | |
| tempPrefix := "::pgschema_tmp_" | |
| idx := strings.Index(value, tempPrefix) | |
| if idx == -1 { | |
| return value | |
| } | |
| // Find the dot after pgschema_tmp_* | |
| dotIdx := strings.Index(value[idx+len(tempPrefix):], ".") | |
| if dotIdx == -1 { | |
| return value | |
| } | |
| // Replace ::pgschema_tmp_XXX.typename with ::typename | |
| prefix := value[idx : idx+len(tempPrefix)+dotIdx+1] // includes the trailing dot |
Copilot
AI
Dec 29, 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.
The same off-by-one error exists here. The calculation idx+15+dotIdx+1 uses 15 instead of the correct length of "::pgschema_tmp_" which is 16. This will cause the prefix extraction to include one extra character before the "::" or exclude one character that should be included.
The correct calculation should be idx + len("::pgschema_tmp_") + dotIdx + 1 or idx + 16 + dotIdx + 1.
| dotIdx := strings.Index(value[idx+15:], ".") | |
| if dotIdx == -1 { | |
| return value | |
| } | |
| // Replace ::pgschema_tmp_XXX.typename with ::typename | |
| prefix := value[idx : idx+15+dotIdx+1] // includes the trailing dot | |
| dotIdx := strings.Index(value[idx+len("::pgschema_tmp_"):], ".") | |
| if dotIdx == -1 { | |
| return value | |
| } | |
| // Replace ::pgschema_tmp_XXX.typename with ::typename | |
| prefix := value[idx : idx+len("::pgschema_tmp_")+dotIdx+1] // includes the trailing dot |
Copilot
AI
Dec 29, 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.
Similar to the issue in stripSchemaPrefix, using strings.ReplaceAll here could cause problems if there are multiple temporary schema prefixes in the value, or if the extracted prefix accidentally matches other parts of the expression.
Additionally, this function only handles the first occurrence it finds (since it searches from the beginning), but then uses ReplaceAll which will replace all occurrences. This creates an inconsistency: if there are multiple different temporary schema prefixes (e.g., ::pgschema_tmp_20231201_120000_ABC.type1 and ::pgschema_tmp_20231201_130000_XYZ.type2), the function will only extract the prefix for the first one but attempt to replace all occurrences of that prefix, potentially leaving other temporary prefixes in place.
Consider either using strings.Replace(value, prefix, "::", 1) to only replace the first occurrence, or looping to handle all temporary schema prefixes in the value.
| // We look for ::pgschema_tmp_ followed by anything until the next dot | |
| idx := strings.Index(value, "::pgschema_tmp_") | |
| if idx == -1 { | |
| return value | |
| } | |
| // Find the dot after pgschema_tmp_* | |
| dotIdx := strings.Index(value[idx+15:], ".") | |
| if dotIdx == -1 { | |
| return value | |
| } | |
| // Replace ::pgschema_tmp_XXX.typename with ::typename | |
| prefix := value[idx : idx+15+dotIdx+1] // includes the trailing dot | |
| return strings.ReplaceAll(value, prefix, "::") | |
| // We look for ::pgschema_tmp_ followed by anything until the next dot. | |
| const tmpPrefix = "::pgschema_tmp_" | |
| for { | |
| idx := strings.Index(value, tmpPrefix) | |
| if idx == -1 { | |
| break | |
| } | |
| // Find the dot after pgschema_tmp_* | |
| dotIdx := strings.Index(value[idx+len(tmpPrefix):], ".") | |
| if dotIdx == -1 { | |
| // No terminating dot; nothing more to strip safely. | |
| break | |
| } | |
| // Replace this specific ::pgschema_tmp_XXX.typename with ::typename | |
| prefixEnd := idx + len(tmpPrefix) + dotIdx + 1 // includes the trailing dot | |
| prefix := value[idx:prefixEnd] | |
| value = strings.Replace(value, prefix, "::", 1) | |
| } | |
| return value |
Copilot
AI
Dec 29, 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.
The new stripSchemaPrefix and stripTempSchemaPrefix helper functions lack unit tests. Given the complexity of these functions and the edge cases they need to handle (multiple occurrences, off-by-one errors, nested expressions), comprehensive unit tests would help ensure correctness and prevent regressions.
Consider adding unit tests that cover:
- Simple type name prefix stripping
- Type casts within expressions
- Multiple occurrences of schema prefixes
- Edge cases like empty strings, missing dots, etc.
- Temporary schema prefix patterns with various timestamp formats
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| ALTER TABLE users ADD COLUMN fqdn citext NOT NULL; | ||
| ALTER TABLE users ADD COLUMN metadata utils.hstore; | ||
| ALTER TABLE users ADD COLUMN description utils.custom_text; | ||
| ALTER TABLE users ADD COLUMN status utils.custom_enum DEFAULT 'active'; | ||
| ALTER TABLE users ADD COLUMN status utils.custom_enum DEFAULT 'active'::utils.custom_enum; |
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.
Using
strings.ReplaceAllwill replace all occurrences of the schema prefix in the type name, which could cause issues if the schema name appears in other parts of the expression. For example, iftargetSchemais "public" andtypeNameis"CASE WHEN public.status = 'active'::public.status_enum THEN 'yes'::public.yes_no END", this would replace all instances of"::public."with"::", resulting in malformed SQL.Consider using a more targeted approach that only replaces the specific occurrence related to the type cast, or add additional validation to ensure you're only replacing type cast contexts.