fix: writer/reader correctness and determinism issues

- merge: cloneTable keeps relationships; skip-tables applies to new schemas
- diff: detect schema description/owner changes
- prisma: reader no longer turns relation fields into columns, enum
  detection via declared names; writer type mapping is ordered
- typeorm writer: keep explicit SQL types that cannot be inferred
- drizzle: enum columns call the enum constant; reader resolves them
- mysql/mssql/sqlite writers: honour OutputPath, deterministic column
  and constraint order; mssql live execute covers full schema
- template: ToYAML recovers from panics; Merge nil-pointer loop
- regenerate drizzle fixtures
This commit is contained in:
2026-10-03 21:33:59 +02:00
parent 08e1417393
commit a32647ee16
15 changed files with 301 additions and 180 deletions
+27
View File
@@ -15,6 +15,9 @@ import (
// Reader implements the readers.Reader interface for Drizzle schema format
type Reader struct {
options *readers.ReaderOptions
// enumVars maps the constant a pgEnum() is assigned to (e.g. "role") to the
// enum's SQL name (e.g. "Role"), so columns declared as role('col') resolve.
enumVars map[string]string
}
// NewReader creates a new Drizzle reader with the given options
@@ -29,6 +32,7 @@ func (r *Reader) ReadDatabase() (*models.Database, error) {
if r.options.FilePath == "" {
return nil, fmt.Errorf("file path is required for Drizzle reader")
}
r.enumVars = make(map[string]string)
// Check if it's a file or directory
info, err := os.Stat(r.options.FilePath)
@@ -100,6 +104,13 @@ func (r *Reader) readDirectory(dirPath string) (*models.Database, error) {
return nil, fmt.Errorf("failed to glob directory: %w", err)
}
// Enums may be declared in a different file than the tables using them
for _, file := range files {
if content, err := os.ReadFile(file); err == nil {
r.collectEnumVars(string(content))
}
}
// Parse each file
for _, file := range files {
content, err := os.ReadFile(file)
@@ -125,9 +136,22 @@ func (r *Reader) readDirectory(dirPath string) (*models.Database, error) {
return db, nil
}
var enumVarRegex = regexp.MustCompile(`export\s+const\s+(\w+)\s*=\s*pgEnum\s*\(\s*['"](\w+)['"]`)
// collectEnumVars records every pgEnum() constant declared in content.
func (r *Reader) collectEnumVars(content string) {
if r.enumVars == nil {
r.enumVars = make(map[string]string)
}
for _, m := range enumVarRegex.FindAllStringSubmatch(content, -1) {
r.enumVars[m[1]] = m[2]
}
}
// parseDrizzle parses Drizzle schema content and returns a Database model
func (r *Reader) parseDrizzle(content string) (*models.Database, error) {
db := models.InitDatabase("database")
r.collectEnumVars(content)
if r.options.Metadata != nil {
if name, ok := r.options.Metadata["name"].(string); ok {
@@ -375,6 +399,9 @@ func (r *Reader) parseColumnDefinition(line, fieldName, drizzleType string, tabl
// Map Drizzle type to SQL type
column.Type = r.drizzleTypeToSQL(drizzleType)
if enumName, ok := r.enumVars[drizzleType]; ok {
column.Type = enumName
}
// Default: columns are nullable unless specified
column.NotNull = false
+18 -14
View File
@@ -13,7 +13,8 @@ import (
// Reader implements the readers.Reader interface for Prisma schema format
type Reader struct {
options *readers.ReaderOptions
options *readers.ReaderOptions
enumNames map[string]bool // enum names declared in the schema being parsed
}
// NewReader creates a new Prisma reader with the given options
@@ -82,6 +83,8 @@ func (r *Reader) parsePrisma(content string) (*models.Database, error) {
schema := models.InitSchema("public")
schema.Enums = make([]*models.Enum, 0)
r.enumNames = collectEnumNames(content)
scanner := bufio.NewScanner(strings.NewReader(content))
// State tracking
@@ -600,20 +603,21 @@ func (r *Reader) isPrimitiveType(typeName string) bool {
return false
}
// isEnumType checks if a type name might be an enum
// Note: We can't definitively check against schema.Enums at parse time
// because enums might be defined after the model, so we just check
// if it starts with uppercase (Prisma convention for enums)
func (r *Reader) isEnumType(typeName string, table *models.Table) bool {
// Simple heuristic: enum types start with uppercase letter
// and are not known model names (though we can't check that yet)
if len(typeName) > 0 && typeName[0] >= 'A' && typeName[0] <= 'Z' {
// Additional check: primitive types are already handled above
// So if it's uppercase and not primitive, it's likely an enum or model
// We'll assume it's an enum if it's a single word
return !strings.Contains(typeName, "_")
// isEnumType reports whether typeName is an enum declared in the schema.
// Enum names are collected up front because enums may be declared after the
// models that use them.
func (r *Reader) isEnumType(typeName string, _ *models.Table) bool {
return r.enumNames[typeName]
}
var enumDeclRegex = regexp.MustCompile(`(?m)^\s*enum\s+(\w+)\s*{`)
func collectEnumNames(content string) map[string]bool {
names := make(map[string]bool)
for _, m := range enumDeclRegex.FindAllStringSubmatch(content, -1) {
names[m[1]] = true
}
return false
return names
}
// createConstraintFromRelation creates a FK constraint from a @relation attribute