fix: contain generated files and assets and harden SQL rendering
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLowercaseIdentifiersReferencesAndData(t *testing.T) {
|
||||
db := models.InitDatabase("DB")
|
||||
s := models.InitSchema("App")
|
||||
tbl := models.InitTable("Users", "App")
|
||||
col := models.InitColumn("ID", "Users", "App")
|
||||
tbl.Tablespace = "FastStore"
|
||||
col.Collation = "MyCollation"
|
||||
col.Default = "Keep CASE"
|
||||
col.GenerationExpression = `"ID" + 1`
|
||||
tbl.Columns[col.Name] = col
|
||||
tbl.Constraints["FK"] = &models.Constraint{Name: "FK", Columns: []string{"ID"}, ReferencedSchema: "Other", ReferencedTable: "Target", ReferencedColumns: []string{"Key"}}
|
||||
tbl.Indexes["IDX"] = &models.Index{Name: "IDX", Columns: []string{"ID", "lower('VALUE')"}, Include: []string{"ID"}, Where: `"ID" > 0`}
|
||||
tbl.RefSchema = s
|
||||
s.Tables = append(s.Tables, tbl)
|
||||
db.Schemas = append(db.Schemas, s)
|
||||
got, err := LowercaseIdentifiers(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
table := got.Schemas[0].Tables[0]
|
||||
if table.Tablespace != "faststore" || table.Columns["id"].Collation != "mycollation" || got.Name != "db" || table.Name != "users" || table.Schema != "app" || table.Columns["id"].Name != "id" || table.Constraints["fk"].ReferencedColumns[0] != "key" || table.Indexes["idx"].Include[0] != "id" || table.RefSchema != got.Schemas[0] {
|
||||
t.Fatalf("bad normalized model: %+v", table)
|
||||
}
|
||||
if table.Columns["id"].Default != "Keep CASE" || table.Columns["id"].GenerationExpression != `"ID" + 1` || table.Indexes["idx"].Where != `"ID" > 0` || table.Indexes["idx"].Columns[1] != "lower('VALUE')" {
|
||||
t.Fatal("data/expression changed")
|
||||
}
|
||||
if tbl.Name != "Users" || tbl.Columns["ID"] != col {
|
||||
t.Fatal("input mutated")
|
||||
}
|
||||
}
|
||||
func TestLowercaseIdentifierCollisions(t *testing.T) {
|
||||
db := models.InitDatabase("db")
|
||||
s := models.InitSchema("app")
|
||||
db.Schemas = append(db.Schemas, s)
|
||||
s.Tables = []*models.Table{models.InitTable("Users", "app"), models.InitTable("users", "app")}
|
||||
if _, err := LowercaseIdentifiers(db); err == nil {
|
||||
t.Fatal("expected table collision")
|
||||
}
|
||||
s.Tables = s.Tables[:1]
|
||||
s.Tables[0].Columns = map[string]*models.Column{"ID": {Name: "ID"}, "id": {Name: "id"}}
|
||||
if _, err := LowercaseIdentifiers(db); err == nil {
|
||||
t.Fatal("expected column collision")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user