package transform import ( "fmt" "reflect" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" ) // LowercaseIdentifiers copies the model and lowercases names and structured // references. SQL expressions, defaults, enum values, comments and metadata are // preserved. Callers must adjust identifiers inside their own SQL expressions. // Collisions are rejected before any output is written. func LowercaseIdentifiers(db *models.Database) (*models.Database, error) { if db == nil { return nil, fmt.Errorf("database is required") } seen := map[any]reflect.Value{} nameFields := map[string]bool{} for _, name := range strings.Fields("Name Schema Table Tablespace Collation TableName SchemaName FromTable FromSchema ToTable ToSchema ThroughTable ThroughSchema ForeignKey ReferencedTable ReferencedSchema OwnedByTable OwnedByColumn") { nameFields[name] = true } listFields := map[string]bool{"Columns": true, "Include": true, "ReferencedColumns": true, "FromColumns": true, "ToColumns": true, "RunAfter": true} var copyValue func(reflect.Value) (reflect.Value, error) copyValue = func(v reflect.Value) (reflect.Value, error) { if !v.IsValid() { return v, nil } switch v.Kind() { case reflect.Pointer: if v.IsNil() || v.Type().Elem().PkgPath() != reflect.TypeOf(models.Database{}).PkgPath() { return v, nil } if prior, ok := seen[v.Interface()]; ok { return prior, nil } out := reflect.New(v.Type().Elem()) seen[v.Interface()] = out copied, err := copyValue(v.Elem()) if err != nil { return reflect.Value{}, err } out.Elem().Set(copied) return out, nil case reflect.Struct: out := reflect.New(v.Type()).Elem() out.Set(v) for i := 0; i < v.NumField(); i++ { field := v.Type().Field(i).Name value := v.Field(i) if nameFields[field] && value.Kind() == reflect.String { out.Field(i).SetString(strings.ToLower(value.String())) continue } if (v.Type() != reflect.TypeOf(models.Index{}) || field != "Columns") && listFields[field] && value.Kind() == reflect.Slice && value.Type().Elem().Kind() == reflect.String { list := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) for j := 0; j < value.Len(); j++ { list.Index(j).SetString(strings.ToLower(value.Index(j).String())) } out.Field(i).Set(list) continue } if field == "Metadata" || field == "Default" || field == "Properties" || field == "Permissions" { continue } copied, err := copyValue(value) if err != nil { return reflect.Value{}, err } out.Field(i).Set(copied) } if v.Type() == reflect.TypeOf(models.Table{}) { original := v.Interface().(models.Table) copied := out.Addr().Interface().(*models.Table) for originalKey, index := range original.Indexes { if index == nil { continue } target := copied.Indexes[strings.ToLower(originalKey)] target.Columns = append([]string(nil), index.Columns...) for i, reference := range index.Columns { for _, column := range original.Columns { if column != nil && (reference == column.Name || reference == `"`+strings.ReplaceAll(column.Name, `"`, `""`)+`"`) { target.Columns[i] = strings.ToLower(column.Name) break } } } } } return out, nil case reflect.Slice: if v.IsNil() { return v, nil } out := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) names := map[string]bool{} for i := 0; i < v.Len(); i++ { copied, err := copyValue(v.Index(i)) if err != nil { return reflect.Value{}, err } out.Index(i).Set(copied) if copied.Kind() == reflect.Pointer && !copied.IsNil() && copied.Elem().Kind() == reflect.Struct { name := copied.Elem().FieldByName("Name") if name.IsValid() { n := name.String() if names[n] { return reflect.Value{}, fmt.Errorf("lowercase identifier collision: %q", n) } names[n] = true } } } return out, nil case reflect.Map: if v.IsNil() || v.Type().Elem().Kind() != reflect.Pointer { return v, nil } out := reflect.MakeMap(v.Type()) names := map[string]bool{} iter := v.MapRange() for iter.Next() { key := iter.Key() if key.Kind() == reflect.String { key = reflect.ValueOf(strings.ToLower(key.String())).Convert(key.Type()) } if out.MapIndex(key).IsValid() { return reflect.Value{}, fmt.Errorf("lowercase identifier collision: %v", key.Interface()) } copied, err := copyValue(iter.Value()) if err != nil { return reflect.Value{}, err } if copied.Kind() == reflect.Pointer && !copied.IsNil() && copied.Elem().Kind() == reflect.Struct { name := copied.Elem().FieldByName("Name") if name.IsValid() { n := name.String() if names[n] { return reflect.Value{}, fmt.Errorf("lowercase identifier collision: %q", n) } names[n] = true } } out.SetMapIndex(key, copied) } return out, nil } return v, nil } value, err := copyValue(reflect.ValueOf(db)) if err != nil { return nil, err } return value.Interface().(*models.Database), nil }