Skip to content

Commit 5f6c969

Browse files
committed
feat: CREATE domain
1 parent 02aec12 commit 5f6c969

14 files changed

Lines changed: 271 additions & 16 deletions

File tree

internal/diff/type.go

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,24 @@ func generateCreateTypesSQL(w *SQLWriter, types []*ir.Type, targetSchema string)
4949
// generateModifyTypesSQL generates ALTER TYPE statements
5050
func generateModifyTypesSQL(w *SQLWriter, diffs []*TypeDiff, targetSchema string) {
5151
for _, diff := range diffs {
52-
// Only ENUM types can be modified by adding values
53-
if diff.Old.Kind == ir.TypeKindEnum && diff.New.Kind == ir.TypeKindEnum {
54-
// Generate ALTER TYPE ... ADD VALUE statements for new enum values
55-
alterStatements := generateAlterTypeEnumStatements(diff.Old, diff.New, targetSchema)
56-
for _, stmt := range alterStatements {
57-
w.WriteDDLSeparator()
58-
w.WriteString(stmt) // No comments for diff scenarios
52+
switch diff.Old.Kind {
53+
case ir.TypeKindEnum:
54+
// ENUM types can be modified by adding values
55+
if diff.New.Kind == ir.TypeKindEnum {
56+
alterStatements := generateAlterTypeEnumStatements(diff.Old, diff.New, targetSchema)
57+
for _, stmt := range alterStatements {
58+
w.WriteDDLSeparator()
59+
w.WriteString(stmt) // No comments for diff scenarios
60+
}
61+
}
62+
case ir.TypeKindDomain:
63+
// Domain types can be modified with ALTER DOMAIN
64+
if diff.New.Kind == ir.TypeKindDomain {
65+
alterStatements := generateAlterDomainStatements(diff.Old, diff.New, targetSchema)
66+
for _, stmt := range alterStatements {
67+
w.WriteDDLSeparator()
68+
w.WriteString(stmt + "\n")
69+
}
5970
}
6071
}
6172
}
@@ -73,8 +84,18 @@ func generateDropTypesSQL(w *SQLWriter, types []*ir.Type, targetSchema string) {
7384
for _, typeObj := range sortedTypes {
7485
w.WriteDDLSeparator()
7586
typeName := qualifyEntityName(typeObj.Schema, typeObj.Name, targetSchema)
76-
sql := fmt.Sprintf("DROP TYPE IF EXISTS %s CASCADE;", typeName)
77-
w.WriteStatementWithComment("TYPE", typeObj.Name, typeObj.Schema, "", sql, targetSchema)
87+
88+
var sql string
89+
var objectType string
90+
if typeObj.Kind == ir.TypeKindDomain {
91+
sql = fmt.Sprintf("DROP DOMAIN IF EXISTS %s RESTRICT;", typeName)
92+
objectType = "DOMAIN"
93+
} else {
94+
sql = fmt.Sprintf("DROP TYPE IF EXISTS %s RESTRICT;", typeName)
95+
objectType = "TYPE"
96+
}
97+
98+
w.WriteStatementWithComment(objectType, typeObj.Name, typeObj.Schema, "", sql, targetSchema)
7899
}
79100
}
80101

@@ -112,6 +133,80 @@ func generateAlterTypeEnumStatements(oldType, newType *ir.Type, targetSchema str
112133
return statements
113134
}
114135

136+
// generateAlterDomainStatements generates ALTER DOMAIN statements for domain changes
137+
func generateAlterDomainStatements(oldDomain, newDomain *ir.Type, targetSchema string) []string {
138+
var statements []string
139+
domainName := qualifyEntityName(newDomain.Schema, newDomain.Name, targetSchema)
140+
141+
// Check if default value changed
142+
if oldDomain.Default != newDomain.Default {
143+
if newDomain.Default == "" {
144+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s DROP DEFAULT;", domainName))
145+
} else {
146+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s SET DEFAULT %s;", domainName, newDomain.Default))
147+
}
148+
}
149+
150+
// Check if NOT NULL changed
151+
if oldDomain.NotNull != newDomain.NotNull {
152+
if newDomain.NotNull {
153+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s SET NOT NULL;", domainName))
154+
} else {
155+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s DROP NOT NULL;", domainName))
156+
}
157+
}
158+
159+
// Check constraints changes
160+
// Create maps for easier comparison
161+
oldConstraints := make(map[string]*ir.DomainConstraint)
162+
for _, c := range oldDomain.Constraints {
163+
key := c.Name
164+
if key == "" {
165+
key = c.Definition
166+
}
167+
oldConstraints[key] = c
168+
}
169+
170+
newConstraints := make(map[string]*ir.DomainConstraint)
171+
for _, c := range newDomain.Constraints {
172+
key := c.Name
173+
if key == "" {
174+
key = c.Definition
175+
}
176+
newConstraints[key] = c
177+
}
178+
179+
// Drop removed constraints
180+
for key, oldConstraint := range oldConstraints {
181+
if newConstraint, exists := newConstraints[key]; !exists {
182+
// Constraint was removed
183+
if oldConstraint.Name != "" {
184+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s DROP CONSTRAINT %s;", domainName, oldConstraint.Name))
185+
}
186+
// Note: unnamed constraints cannot be dropped individually
187+
} else if oldConstraint.Name != "" && oldConstraint.Definition != newConstraint.Definition {
188+
// Constraint exists but definition changed - need to drop and recreate
189+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s DROP CONSTRAINT %s;", domainName, oldConstraint.Name))
190+
}
191+
}
192+
193+
// Add new constraints
194+
for key, newConstraint := range newConstraints {
195+
oldConstraint, exists := oldConstraints[key]
196+
if !exists || (exists && oldConstraint.Definition != newConstraint.Definition) {
197+
// Either new constraint or definition changed
198+
constraintDef := newConstraint.Definition
199+
if newConstraint.Name != "" {
200+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s ADD CONSTRAINT %s %s;", domainName, newConstraint.Name, constraintDef))
201+
} else {
202+
statements = append(statements, fmt.Sprintf("ALTER DOMAIN %s ADD %s;", domainName, constraintDef))
203+
}
204+
}
205+
}
206+
207+
return statements
208+
}
209+
115210
// generateTypeSQL generates CREATE TYPE statement
116211
func generateTypeSQL(typeObj *ir.Type, targetSchema string) string {
117212
// Only include type name without schema if it's in the target schema
@@ -144,22 +239,35 @@ func generateTypeSQL(typeObj *ir.Type, targetSchema string) string {
144239
}
145240
return fmt.Sprintf("CREATE TYPE %s AS (%s);", typeName, strings.Join(attributes, ", "))
146241
case ir.TypeKindDomain:
147-
stmt := fmt.Sprintf("CREATE DOMAIN %s AS %s", typeName, typeObj.BaseType)
242+
// Use multi-line format for better readability if there are constraints
243+
hasConstraints := len(typeObj.Constraints) > 0 || typeObj.NotNull || typeObj.Default != ""
244+
245+
if !hasConstraints {
246+
return fmt.Sprintf("CREATE DOMAIN %s AS %s;", typeName, typeObj.BaseType)
247+
}
248+
249+
// Multi-line format
250+
lines := []string{fmt.Sprintf("CREATE DOMAIN %s AS %s", typeName, typeObj.BaseType)}
251+
148252
if typeObj.Default != "" {
149-
stmt += fmt.Sprintf(" DEFAULT %s", typeObj.Default)
253+
lines = append(lines, fmt.Sprintf(" DEFAULT %s", typeObj.Default))
150254
}
151255
if typeObj.NotNull {
152-
stmt += " NOT NULL"
256+
lines = append(lines, " NOT NULL")
153257
}
258+
154259
// Add domain constraints (CHECK constraints)
260+
// Normalize VALUE to uppercase for consistency
155261
for _, constraint := range typeObj.Constraints {
262+
constraintDef := constraint.Definition
156263
if constraint.Name != "" {
157-
stmt += fmt.Sprintf(" CONSTRAINT %s %s", constraint.Name, constraint.Definition)
264+
lines = append(lines, fmt.Sprintf(" CONSTRAINT %s %s", constraint.Name, constraintDef))
158265
} else {
159-
stmt += fmt.Sprintf(" %s", constraint.Definition)
266+
lines = append(lines, fmt.Sprintf(" %s", constraintDef))
160267
}
161268
}
162-
return stmt + ";"
269+
270+
return strings.Join(lines, "\n") + ";"
163271
default:
164272
return fmt.Sprintf("CREATE TYPE %s;", typeName)
165273
}

internal/ir/inspector.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,11 @@ func (i *Inspector) buildTypes(ctx context.Context, schema *IR, targetSchema str
15071507
constraintName := i.safeInterfaceToString(constraint.ConstraintName)
15081508
constraintDef := i.safeInterfaceToString(constraint.ConstraintDefinition)
15091509

1510+
// Skip NOT NULL constraints as they are already captured in the NotNull boolean field
1511+
if constraintDef == "NOT NULL" {
1512+
continue
1513+
}
1514+
15101515
domainConstraint := &DomainConstraint{
15111516
Name: constraintName,
15121517
Definition: constraintDef,

internal/ir/normalize.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func normalizeSchema(schema *Schema) {
4343
for _, procedure := range schema.Procedures {
4444
normalizeProcedure(procedure)
4545
}
46+
47+
// Normalize types (including domains)
48+
for _, typeObj := range schema.Types {
49+
normalizeType(typeObj)
50+
}
4651
}
4752

4853
// normalizeTable normalizes table-related objects
@@ -610,6 +615,78 @@ func isComplexExpression(expr string) bool {
610615
return false
611616
}
612617

618+
// normalizeType normalizes type-related objects, including domain constraints
619+
func normalizeType(typeObj *Type) {
620+
if typeObj == nil || typeObj.Kind != TypeKindDomain {
621+
return
622+
}
623+
624+
// Normalize domain default value
625+
if typeObj.Default != "" {
626+
typeObj.Default = normalizeDomainDefault(typeObj.Default)
627+
}
628+
629+
// Normalize domain constraints
630+
for _, constraint := range typeObj.Constraints {
631+
normalizeDomainConstraint(constraint)
632+
}
633+
}
634+
635+
// normalizeDomainDefault normalizes domain default values
636+
func normalizeDomainDefault(defaultValue string) string {
637+
if defaultValue == "" {
638+
return defaultValue
639+
}
640+
641+
// Remove redundant type casts from string literals
642+
// e.g., 'example@acme.com'::text -> 'example@acme.com'
643+
defaultValue = regexp.MustCompile(`'([^']+)'::text\b`).ReplaceAllString(defaultValue, "'$1'")
644+
645+
return defaultValue
646+
}
647+
648+
// normalizeDomainConstraint normalizes domain constraint definitions
649+
func normalizeDomainConstraint(constraint *DomainConstraint) {
650+
if constraint == nil || constraint.Definition == "" {
651+
return
652+
}
653+
654+
def := constraint.Definition
655+
656+
// Normalize VALUE keyword to uppercase in domain constraints
657+
// Use word boundaries to ensure we only match the identifier, not parts of other words
658+
def = regexp.MustCompile(`\bvalue\b`).ReplaceAllStringFunc(def, func(match string) string {
659+
return strings.ToUpper(match)
660+
})
661+
662+
// Handle CHECK constraints
663+
if strings.HasPrefix(def, "CHECK ") {
664+
// Extract the expression inside CHECK (...)
665+
checkMatch := regexp.MustCompile(`^CHECK\s*\((.*)\)$`).FindStringSubmatch(def)
666+
if len(checkMatch) > 1 {
667+
expr := checkMatch[1]
668+
669+
// Remove outer parentheses if they wrap the entire expression
670+
expr = strings.TrimSpace(expr)
671+
if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") {
672+
inner := expr[1 : len(expr)-1]
673+
if isBalancedParentheses(inner) {
674+
expr = inner
675+
}
676+
}
677+
678+
// Remove redundant type casts
679+
// e.g., '...'::text -> '...'
680+
expr = regexp.MustCompile(`'([^']+)'::text\b`).ReplaceAllString(expr, "'$1'")
681+
682+
// Reconstruct the CHECK constraint
683+
def = fmt.Sprintf("CHECK (%s)", expr)
684+
}
685+
}
686+
687+
constraint.Definition = def
688+
}
689+
613690
// normalizePostgreSQLType normalizes PostgreSQL internal type names to standard SQL types.
614691
// This function handles both expressions (with type casts) and direct type names.
615692
func normalizePostgreSQLType(input string) string {

internal/ir/parser.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,46 @@ func (p *Parser) parseCreateDomain(domainStmt *pg_query.CreateDomainStmt) error
22682268
BaseType: baseType,
22692269
}
22702270

2271+
// Parse domain constraints from the AST
2272+
if domainStmt.Constraints != nil {
2273+
for _, constraintNode := range domainStmt.Constraints {
2274+
if constraint := constraintNode.GetConstraint(); constraint != nil {
2275+
// Handle different constraint types
2276+
switch constraint.Contype {
2277+
case pg_query.ConstrType_CONSTR_NOTNULL:
2278+
// Set NOT NULL flag for domain
2279+
domainType.NotNull = true
2280+
case pg_query.ConstrType_CONSTR_DEFAULT:
2281+
// Extract default value from the constraint
2282+
if constraint.RawExpr != nil {
2283+
domainType.Default = p.extractExpressionText(constraint.RawExpr)
2284+
}
2285+
case pg_query.ConstrType_CONSTR_CHECK:
2286+
// Extract CHECK constraint
2287+
constraintDef := ""
2288+
if constraint.RawExpr != nil {
2289+
exprText := p.extractExpressionText(constraint.RawExpr)
2290+
constraintDef = fmt.Sprintf("CHECK %s", exprText)
2291+
}
2292+
2293+
if constraintDef != "" {
2294+
constraintName := constraint.Conname
2295+
// Auto-generate constraint name if not provided (matching PostgreSQL behavior)
2296+
if constraintName == "" {
2297+
constraintName = fmt.Sprintf("%s_check", domainName)
2298+
}
2299+
2300+
domainConstraint := &DomainConstraint{
2301+
Name: constraintName,
2302+
Definition: constraintDef,
2303+
}
2304+
domainType.Constraints = append(domainType.Constraints, domainConstraint)
2305+
}
2306+
}
2307+
}
2308+
}
2309+
}
2310+
22712311
// Add type to schema
22722312
dbSchema.Types[domainName] = domainType
22732313

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE DOMAIN email_address AS text
2+
DEFAULT 'example@acme.com'
3+
NOT NULL
4+
CONSTRAINT email_address_check CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE DOMAIN email_address AS text
2+
DEFAULT 'example@acme.com'
3+
NOT NULL
4+
CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-- Empty schema (no domains)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ALTER DOMAIN user_rating SET DEFAULT 3;
2+
3+
ALTER DOMAIN user_rating DROP CONSTRAINT user_rating_check;
4+
5+
ALTER DOMAIN user_rating ADD CONSTRAINT user_rating_check CHECK ((VALUE >= 1) AND (VALUE <= 10));
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE DOMAIN user_rating AS integer
2+
DEFAULT 3
3+
CHECK (VALUE >= 1 AND VALUE <= 10);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE DOMAIN user_rating AS integer
2+
CHECK (VALUE >= 1 AND VALUE <= 5);

0 commit comments

Comments
 (0)