Files
relspecgo/pkg/transform/identifiers.go
T
warkanum 1d93255575 fix(assets): improve error reporting for asset failures
fix(embed): update embedDirectives to return result directly

fix(executor): clean up import order and formatting

fix(syntax_test): change file permission syntax to octal

fix(mysql): update file permission syntax to octal

fix(pgsql): update file permission syntax to octal

fix(sqlex): simplify Walk function parameter handling

fix(transform): adjust identifier comparison logic

fix(template): clean up function formatting and error handling
2026-10-12 22:04:40 +02:00

158 lines
5.0 KiB
Go

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
}