package mysql import ( "os" "path/filepath" "strings" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/transform" "git.warky.dev/wdevs/relspecgo/pkg/writers" ) func TestStrictConversionPreflightsAllObjects(t *testing.T) { db := shopDB() unsupported := models.InitTable("unsupported", "shop") col := models.InitColumn("opaque", "unsupported", "shop") col.Type = "unknown_type" unsupported.Columns[col.Name] = col index := models.InitIndex("partial", "unsupported", "shop") index.Columns = []string{col.Name} index.Where = "opaque IS NOT NULL" index.Include = []string{"other"} unsupported.Indexes[index.Name] = index db.Schemas[0].Tables = append(db.Schemas[0].Tables, unsupported) out := filepath.Join(t.TempDir(), "existing.sql") if err := os.WriteFile(out, []byte("keep existing contents"), 0600); err != nil { t.Fatal(err) } opts := &writers.WriterOptions{StrictConversion: true, OutputPath: out} err := NewWriter(opts).WriteDatabase(db) for _, want := range []string{"lossy MySQL conversion rejected", "unknown type", "partial predicate", "INCLUDE"} { if err == nil || !strings.Contains(err.Error(), want) { t.Fatalf("expected %s: %v", want, err) } } bytes, err := os.ReadFile(out) if err != nil || string(bytes) != "keep existing contents" { t.Fatalf("destination changed: %q %v", bytes, err) } opts.Metadata = map[string]any{"connection_string": "invalid DSN"} if err := NewWriter(opts).WriteDatabase(db); err == nil || !strings.Contains(err.Error(), "lossy MySQL") { t.Fatalf("connected before strict validation: %v", err) } opts.StrictConversion = false opts.Metadata = nil var warnings []string opts.Warning = func(s string) { warnings = append(warnings, s) } if err := NewWriter(opts).WriteDatabase(db); err != nil { t.Fatal(err) } if len(warnings) != 3 { t.Fatalf("warnings: %v", warnings) } } func TestUnsupportedModelFeaturesDiagnosed(t *testing.T) { db := shopDB() s := db.Schemas[0] table := s.Tables[0] s.Metadata["mysql_unsupported"] = []any{"trigger shop.audit", "partitioned table shop.archive"} c := table.Columns["id"] c.Type = "timestamptz" c.Identity = true c.Metadata["mysql_srs_id"] = 4326 check := models.InitConstraint("deferred", models.CheckConstraint) check.Expression = "id > 0" yes := true check.Deferrable = &yes table.Constraints[check.Name] = check i := models.InitIndex("gin", "users", "shop") i.Type = "gin" i.Columns = []string{"id"} table.Indexes[i.Name] = i _, err := NewWriter(&writers.WriterOptions{StrictConversion: true}).Statements(db) for _, want := range []string{"trigger shop.audit", "partitioned table", "time zone", "identity", "mysql_srs_id", "deferrable", "unsupported index type gin"} { if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("missing diagnostic %q: %v", want, err) } } } func TestCanonicalDimensionsAndNativeEdit(t *testing.T) { db := shopDB() c := db.Schemas[0].Tables[0].Columns["age"] c.Type = "decimal" c.Precision = 12 c.Scale = 4 if got := columnType(c); got != "DECIMAL(12,4)" { t.Fatal(got) } c.Type = "uint64" c.Precision = 0 c.Scale = 0 if got := columnType(c); got != "BIGINT UNSIGNED" { t.Fatal(got) } c.Metadata["mysql_type"] = "bigint unsigned" c.Metadata["mysql_type_signature"] = signature(c) c.Type = "int64" _, err := NewWriter(&writers.WriterOptions{StrictConversion: true}).Statements(db) if err == nil || !strings.Contains(err.Error(), "override native declaration") { t.Fatalf("native metadata silently overrode edit: %v", err) } if got := columnType(c); got != "BIGINT" { t.Fatal(got) } } func TestLowercaseIndexPartsPreserveSQL(t *testing.T) { db := shopDB() table := db.Schemas[0].Tables[0] c := models.InitColumn("Mixed", "users", "shop") c.Type = "string" table.Columns[c.Name] = c i := models.InitIndex("MixedIndex", "users", "shop") i.KeyParts = []models.IndexPart{{Name: "Mixed", Length: 8, Descending: true}, {Expression: "lower(`Mixed`)"}} table.Indexes[i.Name] = i model, err := transform.LowercaseIdentifiers(db) if err != nil { t.Fatal(err) } parts := model.Schemas[0].Tables[0].Indexes["mixedindex"].KeyParts if parts[0].Name != "mixed" || parts[1].Expression != "lower(`Mixed`)" || i.KeyParts[0].Name != "Mixed" { t.Fatalf("lowercase parts: %+v", parts) } } func TestLowercasePrimaryIndexIsNotDuplicated(t *testing.T) { db := shopDB() table := db.Schemas[0].Tables[0] i := models.InitIndex("PRIMARY", table.Name, table.Schema) i.Unique = true i.Type = "btree" i.Columns = []string{"id"} i.KeyParts = []models.IndexPart{{Name: "id", Descending: true}} table.Indexes[i.Name] = i statements, err := NewWriter(&writers.WriterOptions{StrictConversion: true, LowercaseIdentifiers: true}).Statements(db) if err != nil { t.Fatal(err) } if !strings.Contains(statements[0], "PRIMARY KEY (`id` DESC)") || strings.Contains(statements[0], "KEY `primary`") { t.Fatalf("primary key duplicated or attributes lost: %s", statements[0]) } }