Table.Columns/Constraints/Indexes/Relationships are Go maps, and every writer, reader, diff, inspector, and merge code path that iterated them directly was subject to Go's randomized map order, so identical input could produce different output (or a different in-report violation/diff order) on every run. Most visibly this showed up as bun/gorm `unique:` struct tags changing order across consecutive `make models` runs with no source change. Fixed by sorting map iteration (by Sequence then Name, or alphabetically for string-keyed maps) everywhere the order affects generated output or first-match tie-break logic, across the bun, gorm, sqlite, dbml, drawdb, pgsql, prisma, graphql, typeorm, drizzle, and dctx writers; the dctx, prisma, and typeorm readers; the shared models.GetPrimaryKey/ GetForeignKeys helpers; pkg/diff, pkg/inspector, and pkg/merge; and the TUI column/relationship pickers in pkg/ui.
805 lines
22 KiB
Go
805 lines
22 KiB
Go
package gorm
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
)
|
|
|
|
func TestWriter_WriteTable(t *testing.T) {
|
|
// Create a simple table
|
|
table := models.InitTable("users", "public")
|
|
table.Columns["id"] = &models.Column{
|
|
Name: "id",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
AutoIncrement: true,
|
|
Sequence: 1,
|
|
}
|
|
table.Columns["email"] = &models.Column{
|
|
Name: "email",
|
|
Type: "varchar",
|
|
Length: 255,
|
|
NotNull: false,
|
|
Sequence: 2,
|
|
}
|
|
table.Columns["created_at"] = &models.Column{
|
|
Name: "created_at",
|
|
Type: "timestamp",
|
|
NotNull: true,
|
|
Sequence: 3,
|
|
}
|
|
|
|
// Create writer
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
Metadata: map[string]interface{}{
|
|
"generate_table_name": true,
|
|
"generate_get_id": true,
|
|
},
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
|
|
// Write to temporary file
|
|
tmpDir := t.TempDir()
|
|
opts.OutputPath = filepath.Join(tmpDir, "test.go")
|
|
|
|
err := writer.WriteTable(table)
|
|
if err != nil {
|
|
t.Fatalf("WriteTable failed: %v", err)
|
|
}
|
|
|
|
// Read the generated file
|
|
content, err := os.ReadFile(opts.OutputPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read generated file: %v", err)
|
|
}
|
|
|
|
generated := string(content)
|
|
|
|
// Verify key elements are present
|
|
expectations := []string{
|
|
"package models",
|
|
"type ModelPublicUsers struct",
|
|
"ID",
|
|
"int64",
|
|
"Email",
|
|
"*string",
|
|
"CreatedAt",
|
|
"time.Time",
|
|
"gorm:\"column:id",
|
|
"gorm:\"column:email",
|
|
"func (m ModelPublicUsers) TableName() string",
|
|
"return \"public.users\"",
|
|
"func (m ModelPublicUsers) GetID() int64",
|
|
}
|
|
|
|
for _, expected := range expectations {
|
|
if !strings.Contains(generated, expected) {
|
|
t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriter_WriteDatabase_MultiFile(t *testing.T) {
|
|
// Create a database with two tables
|
|
db := models.InitDatabase("testdb")
|
|
schema := models.InitSchema("public")
|
|
|
|
// Table 1: users
|
|
users := models.InitTable("users", "public")
|
|
users.Columns["id"] = &models.Column{
|
|
Name: "id",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
schema.Tables = append(schema.Tables, users)
|
|
|
|
// Table 2: posts
|
|
posts := models.InitTable("posts", "public")
|
|
posts.Columns["id"] = &models.Column{
|
|
Name: "id",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
posts.Columns["user_id"] = &models.Column{
|
|
Name: "user_id",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
}
|
|
posts.Constraints["fk_user"] = &models.Constraint{
|
|
Name: "fk_user",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"user_id"},
|
|
ReferencedTable: "users",
|
|
ReferencedSchema: "public",
|
|
ReferencedColumns: []string{"id"},
|
|
OnDelete: "CASCADE",
|
|
}
|
|
schema.Tables = append(schema.Tables, posts)
|
|
|
|
db.Schemas = append(db.Schemas, schema)
|
|
|
|
// Create writer with multi-file mode
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: tmpDir,
|
|
Metadata: map[string]interface{}{
|
|
"multi_file": true,
|
|
},
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
|
|
err := writer.WriteDatabase(db)
|
|
if err != nil {
|
|
t.Fatalf("WriteDatabase failed: %v", err)
|
|
}
|
|
|
|
// Verify two files were created
|
|
expectedFiles := []string{
|
|
"sql_public_users.go",
|
|
"sql_public_posts.go",
|
|
}
|
|
|
|
for _, filename := range expectedFiles {
|
|
filepath := filepath.Join(tmpDir, filename)
|
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
|
t.Errorf("Expected file not created: %s", filename)
|
|
}
|
|
}
|
|
|
|
// Check posts file contains relationship
|
|
postsContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_public_posts.go"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to read posts file: %v", err)
|
|
}
|
|
|
|
postsStr := string(postsContent)
|
|
|
|
// Verify relationship is present with new naming convention
|
|
// Should now be RelUserID (belongs-to) instead of USE
|
|
if !strings.Contains(postsStr, "RelUserID") {
|
|
t.Errorf("Missing relationship field RelUserID (new naming convention)")
|
|
}
|
|
|
|
// Check users file contains has-many relationship
|
|
usersContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_public_users.go"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to read users file: %v", err)
|
|
}
|
|
|
|
usersStr := string(usersContent)
|
|
|
|
// Should have RelUserIDPublicPosts (has-many) field - includes schema prefix
|
|
if !strings.Contains(usersStr, "RelUserIDPublicPosts") {
|
|
t.Errorf("Missing has-many relationship field RelUserIDPublicPosts")
|
|
}
|
|
}
|
|
|
|
func TestWriter_MultipleReferencesToSameTable(t *testing.T) {
|
|
// Test scenario: api_event table with multiple foreign keys to filepointer table
|
|
db := models.InitDatabase("testdb")
|
|
schema := models.InitSchema("org")
|
|
|
|
// Filepointer table
|
|
filepointer := models.InitTable("filepointer", "org")
|
|
filepointer.Columns["id_filepointer"] = &models.Column{
|
|
Name: "id_filepointer",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
schema.Tables = append(schema.Tables, filepointer)
|
|
|
|
// API event table with two foreign keys to filepointer
|
|
apiEvent := models.InitTable("api_event", "org")
|
|
apiEvent.Columns["id_api_event"] = &models.Column{
|
|
Name: "id_api_event",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
apiEvent.Columns["rid_filepointer_request"] = &models.Column{
|
|
Name: "rid_filepointer_request",
|
|
Type: "bigint",
|
|
NotNull: false,
|
|
}
|
|
apiEvent.Columns["rid_filepointer_response"] = &models.Column{
|
|
Name: "rid_filepointer_response",
|
|
Type: "bigint",
|
|
NotNull: false,
|
|
}
|
|
|
|
// Add constraints
|
|
apiEvent.Constraints["fk_request"] = &models.Constraint{
|
|
Name: "fk_request",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_filepointer_request"},
|
|
ReferencedTable: "filepointer",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_filepointer"},
|
|
}
|
|
apiEvent.Constraints["fk_response"] = &models.Constraint{
|
|
Name: "fk_response",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_filepointer_response"},
|
|
ReferencedTable: "filepointer",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_filepointer"},
|
|
}
|
|
|
|
schema.Tables = append(schema.Tables, apiEvent)
|
|
db.Schemas = append(db.Schemas, schema)
|
|
|
|
// Create writer
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: tmpDir,
|
|
Metadata: map[string]interface{}{
|
|
"multi_file": true,
|
|
},
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
err := writer.WriteDatabase(db)
|
|
if err != nil {
|
|
t.Fatalf("WriteDatabase failed: %v", err)
|
|
}
|
|
|
|
// Read the api_event file
|
|
apiEventContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_api_event.go"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to read api_event file: %v", err)
|
|
}
|
|
|
|
contentStr := string(apiEventContent)
|
|
|
|
// Verify both relationships have unique names based on column names
|
|
expectations := []struct {
|
|
fieldName string
|
|
tag string
|
|
}{
|
|
{"RelRIDFilepointerRequest", "foreignKey:RIDFilepointerRequest"},
|
|
{"RelRIDFilepointerResponse", "foreignKey:RIDFilepointerResponse"},
|
|
}
|
|
|
|
for _, exp := range expectations {
|
|
if !strings.Contains(contentStr, exp.fieldName) {
|
|
t.Errorf("Missing relationship field: %s\nGenerated:\n%s", exp.fieldName, contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, exp.tag) {
|
|
t.Errorf("Missing relationship tag: %s\nGenerated:\n%s", exp.tag, contentStr)
|
|
}
|
|
}
|
|
|
|
// Verify NO duplicate field names (old behavior would create duplicate "FIL" fields)
|
|
if strings.Contains(contentStr, "FIL *ModelFilepointer") {
|
|
t.Errorf("Found old prefix-based naming (FIL), should use column-based naming")
|
|
}
|
|
|
|
// Also verify has-many relationships on filepointer table
|
|
filepointerContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_filepointer.go"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to read filepointer file: %v", err)
|
|
}
|
|
|
|
filepointerStr := string(filepointerContent)
|
|
|
|
// Should have two different has-many relationships with unique names
|
|
hasManyExpectations := []string{
|
|
"RelRIDFilepointerRequestOrgAPIEvents", // Has many via rid_filepointer_request
|
|
"RelRIDFilepointerResponseOrgAPIEvents", // Has many via rid_filepointer_response
|
|
}
|
|
|
|
for _, exp := range hasManyExpectations {
|
|
if !strings.Contains(filepointerStr, exp) {
|
|
t.Errorf("Missing has-many relationship field: %s\nGenerated:\n%s", exp, filepointerStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriter_MultipleHasManyRelationships(t *testing.T) {
|
|
// Test scenario: api_provider table referenced by multiple tables via rid_api_provider
|
|
db := models.InitDatabase("testdb")
|
|
schema := models.InitSchema("org")
|
|
|
|
// Owner table
|
|
owner := models.InitTable("owner", "org")
|
|
owner.Columns["id_owner"] = &models.Column{
|
|
Name: "id_owner",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
schema.Tables = append(schema.Tables, owner)
|
|
|
|
// API Provider table
|
|
apiProvider := models.InitTable("api_provider", "org")
|
|
apiProvider.Columns["id_api_provider"] = &models.Column{
|
|
Name: "id_api_provider",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
apiProvider.Columns["rid_owner"] = &models.Column{
|
|
Name: "rid_owner",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
}
|
|
apiProvider.Constraints["fk_owner"] = &models.Constraint{
|
|
Name: "fk_owner",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_owner"},
|
|
ReferencedTable: "owner",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_owner"},
|
|
}
|
|
schema.Tables = append(schema.Tables, apiProvider)
|
|
|
|
// Login table
|
|
login := models.InitTable("login", "org")
|
|
login.Columns["id_login"] = &models.Column{
|
|
Name: "id_login",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
login.Columns["rid_api_provider"] = &models.Column{
|
|
Name: "rid_api_provider",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
}
|
|
login.Constraints["fk_api_provider"] = &models.Constraint{
|
|
Name: "fk_api_provider",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_api_provider"},
|
|
ReferencedTable: "api_provider",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_api_provider"},
|
|
}
|
|
schema.Tables = append(schema.Tables, login)
|
|
|
|
// Filepointer table
|
|
filepointer := models.InitTable("filepointer", "org")
|
|
filepointer.Columns["id_filepointer"] = &models.Column{
|
|
Name: "id_filepointer",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
filepointer.Columns["rid_api_provider"] = &models.Column{
|
|
Name: "rid_api_provider",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
}
|
|
filepointer.Constraints["fk_api_provider"] = &models.Constraint{
|
|
Name: "fk_api_provider",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_api_provider"},
|
|
ReferencedTable: "api_provider",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_api_provider"},
|
|
}
|
|
schema.Tables = append(schema.Tables, filepointer)
|
|
|
|
// API Event table
|
|
apiEvent := models.InitTable("api_event", "org")
|
|
apiEvent.Columns["id_api_event"] = &models.Column{
|
|
Name: "id_api_event",
|
|
Type: "bigserial",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
apiEvent.Columns["rid_api_provider"] = &models.Column{
|
|
Name: "rid_api_provider",
|
|
Type: "bigint",
|
|
NotNull: true,
|
|
}
|
|
apiEvent.Constraints["fk_api_provider"] = &models.Constraint{
|
|
Name: "fk_api_provider",
|
|
Type: models.ForeignKeyConstraint,
|
|
Columns: []string{"rid_api_provider"},
|
|
ReferencedTable: "api_provider",
|
|
ReferencedSchema: "org",
|
|
ReferencedColumns: []string{"id_api_provider"},
|
|
}
|
|
schema.Tables = append(schema.Tables, apiEvent)
|
|
|
|
db.Schemas = append(db.Schemas, schema)
|
|
|
|
// Create writer
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: tmpDir,
|
|
Metadata: map[string]interface{}{
|
|
"multi_file": true,
|
|
},
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
err := writer.WriteDatabase(db)
|
|
if err != nil {
|
|
t.Fatalf("WriteDatabase failed: %v", err)
|
|
}
|
|
|
|
// Read the api_provider file
|
|
apiProviderContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_api_provider.go"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to read api_provider file: %v", err)
|
|
}
|
|
|
|
contentStr := string(apiProviderContent)
|
|
|
|
// Verify all has-many relationships have unique names
|
|
hasManyExpectations := []string{
|
|
"RelRIDAPIProviderOrgLogins", // Has many via Login
|
|
"RelRIDAPIProviderOrgFilepointers", // Has many via Filepointer
|
|
"RelRIDAPIProviderOrgAPIEvents", // Has many via APIEvent
|
|
"RelRIDOwner", // Belongs to via rid_owner
|
|
}
|
|
|
|
for _, exp := range hasManyExpectations {
|
|
if !strings.Contains(contentStr, exp) {
|
|
t.Errorf("Missing relationship field: %s\nGenerated:\n%s", exp, contentStr)
|
|
}
|
|
}
|
|
|
|
// Verify NO duplicate field names
|
|
// Count occurrences of "RelRIDAPIProvider" fields - should have 3 unique ones
|
|
count := strings.Count(contentStr, "RelRIDAPIProvider")
|
|
if count != 3 {
|
|
t.Errorf("Expected 3 RelRIDAPIProvider* fields, found %d\nGenerated:\n%s", count, contentStr)
|
|
}
|
|
|
|
// Verify no duplicate declarations (would cause compilation error)
|
|
duplicatePattern := "RelRIDAPIProviders []*Model"
|
|
if strings.Contains(contentStr, duplicatePattern) {
|
|
t.Errorf("Found duplicate field declaration pattern, fields should be unique")
|
|
}
|
|
}
|
|
|
|
func TestWriter_FieldNameCollision(t *testing.T) {
|
|
// Test scenario: table with columns that would conflict with generated method names
|
|
table := models.InitTable("audit_table", "audit")
|
|
table.Columns["id_audit_table"] = &models.Column{
|
|
Name: "id_audit_table",
|
|
Type: "smallint",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
Sequence: 1,
|
|
}
|
|
table.Columns["table_name"] = &models.Column{
|
|
Name: "table_name",
|
|
Type: "varchar",
|
|
Length: 100,
|
|
NotNull: true,
|
|
Sequence: 2,
|
|
}
|
|
table.Columns["table_schema"] = &models.Column{
|
|
Name: "table_schema",
|
|
Type: "varchar",
|
|
Length: 100,
|
|
NotNull: true,
|
|
Sequence: 3,
|
|
}
|
|
|
|
// Create writer
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: filepath.Join(tmpDir, "test.go"),
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
|
|
err := writer.WriteTable(table)
|
|
if err != nil {
|
|
t.Fatalf("WriteTable failed: %v", err)
|
|
}
|
|
|
|
// Read the generated file
|
|
content, err := os.ReadFile(opts.OutputPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read generated file: %v", err)
|
|
}
|
|
|
|
generated := string(content)
|
|
|
|
// Verify that TableName field was renamed to TableName_ to avoid collision
|
|
if !strings.Contains(generated, "TableName_") {
|
|
t.Errorf("Expected field 'TableName_' (with underscore) but not found\nGenerated:\n%s", generated)
|
|
}
|
|
|
|
// Verify the struct tag still references the correct database column
|
|
if !strings.Contains(generated, `gorm:"column:table_name;`) {
|
|
t.Errorf("Expected gorm tag to reference 'table_name' column\nGenerated:\n%s", generated)
|
|
}
|
|
|
|
// Verify the TableName() method still exists and doesn't conflict
|
|
if !strings.Contains(generated, "func (m ModelAuditAuditTable) TableName() string") {
|
|
t.Errorf("TableName() method should still be generated\nGenerated:\n%s", generated)
|
|
}
|
|
|
|
// Verify NO field named just "TableName" (without underscore)
|
|
if strings.Contains(generated, "TableName sql_types") || strings.Contains(generated, "TableName string") {
|
|
t.Errorf("Field 'TableName' without underscore should not exist (would conflict with method)\nGenerated:\n%s", generated)
|
|
}
|
|
}
|
|
|
|
func TestWriter_UpdateIDTypeSafety(t *testing.T) {
|
|
// Test scenario: tables with different primary key types
|
|
tests := []struct {
|
|
name string
|
|
pkType string
|
|
expectedPK string
|
|
castType string
|
|
}{
|
|
{"int32_pk", "int", "int32", "int32(newid)"},
|
|
{"int16_pk", "smallint", "int16", "int16(newid)"},
|
|
{"int64_pk", "bigint", "int64", "int64(newid)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
table := models.InitTable("test_table", "public")
|
|
table.Columns["id"] = &models.Column{
|
|
Name: "id",
|
|
Type: tt.pkType,
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: filepath.Join(tmpDir, "test.go"),
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
err := writer.WriteTable(table)
|
|
if err != nil {
|
|
t.Fatalf("WriteTable failed: %v", err)
|
|
}
|
|
|
|
content, err := os.ReadFile(opts.OutputPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read generated file: %v", err)
|
|
}
|
|
|
|
generated := string(content)
|
|
|
|
// Verify UpdateID method has correct type cast
|
|
if !strings.Contains(generated, tt.castType) {
|
|
t.Errorf("Expected UpdateID to cast to %s\nGenerated:\n%s", tt.castType, generated)
|
|
}
|
|
|
|
// Verify no invalid int32(newid) for non-int32 types
|
|
if tt.expectedPK != "int32" && strings.Contains(generated, "int32(newid)") {
|
|
t.Errorf("UpdateID should not cast to int32 for %s type\nGenerated:\n%s", tt.pkType, generated)
|
|
}
|
|
|
|
// Verify UpdateID parameter is int64 (for consistency)
|
|
if !strings.Contains(generated, "UpdateID(newid int64)") {
|
|
t.Errorf("UpdateID should accept int64 parameter\nGenerated:\n%s", generated)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriter_StringPrimaryKeyHelpers_Gorm(t *testing.T) {
|
|
table := models.InitTable("accounts", "public")
|
|
table.Columns["id"] = &models.Column{
|
|
Name: "id",
|
|
Type: "uuid",
|
|
NotNull: true,
|
|
IsPrimaryKey: true,
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
opts := &writers.WriterOptions{
|
|
PackageName: "models",
|
|
OutputPath: filepath.Join(tmpDir, "test.go"),
|
|
}
|
|
|
|
writer := NewWriter(opts)
|
|
err := writer.WriteTable(table)
|
|
if err != nil {
|
|
t.Fatalf("WriteTable failed: %v", err)
|
|
}
|
|
|
|
content, err := os.ReadFile(opts.OutputPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read generated file: %v", err)
|
|
}
|
|
|
|
generated := string(content)
|
|
|
|
expectations := []string{
|
|
"ID string",
|
|
"func (m ModelPublicAccounts) GetID() string",
|
|
"return m.ID",
|
|
"func (m ModelPublicAccounts) GetIDStr() string",
|
|
"func (m ModelPublicAccounts) SetID(newid string)",
|
|
"func (m *ModelPublicAccounts) UpdateID(newid string)",
|
|
"m.ID = newid",
|
|
}
|
|
|
|
for _, expected := range expectations {
|
|
if !strings.Contains(generated, expected) {
|
|
t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated)
|
|
}
|
|
}
|
|
|
|
if strings.Contains(generated, "GetID() int64") || strings.Contains(generated, "UpdateID(newid int64)") {
|
|
t.Errorf("String primary keys should not use int64 helper signatures\nGenerated:\n%s", generated)
|
|
}
|
|
}
|
|
|
|
func TestNameConverter_SnakeCaseToPascalCase(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"user_id", "UserID"},
|
|
{"http_request", "HTTPRequest"},
|
|
{"user_profiles", "UserProfiles"},
|
|
{"guid", "GUID"},
|
|
{"rid_process", "RIDProcess"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := SnakeCaseToPascalCase(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("SnakeCaseToPascalCase(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNameConverter_Pluralize(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"user", "users"},
|
|
{"process", "processes"},
|
|
{"child", "children"},
|
|
{"person", "people"},
|
|
{"status", "statuses"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := Pluralize(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("Pluralize(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
|
mapper := NewTypeMapper("")
|
|
|
|
tests := []struct {
|
|
sqlType string
|
|
notNull bool
|
|
want string
|
|
}{
|
|
{"bigint", true, "int64"},
|
|
{"bigint", false, "*int64"},
|
|
{"varchar", true, "string"},
|
|
{"varchar", false, "*string"},
|
|
{"timestamp", true, "time.Time"},
|
|
{"timestamp", false, "*time.Time"},
|
|
{"boolean", true, "bool"},
|
|
{"boolean", false, "*bool"},
|
|
{"text[]", true, "[]string"},
|
|
{"text[]", false, "[]string"},
|
|
{"integer[]", true, "[]int32"},
|
|
{"bigint[]", false, "[]int64"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.sqlType, func(t *testing.T) {
|
|
result := mapper.SQLTypeToGoType(tt.sqlType, tt.notNull)
|
|
if result != tt.want {
|
|
t.Errorf("SQLTypeToGoType(%q, %v) = %q, want %q", tt.sqlType, tt.notNull, result, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypeMapper_BuildGormTag_ArrayType(t *testing.T) {
|
|
mapper := NewTypeMapper("")
|
|
|
|
col := &models.Column{
|
|
Name: "tags",
|
|
Type: "text[]",
|
|
NotNull: false,
|
|
}
|
|
|
|
tag := mapper.BuildGormTag(col, nil)
|
|
if !strings.Contains(tag, "type:text[]") {
|
|
t.Fatalf("expected array type to be preserved, got %q", tag)
|
|
}
|
|
}
|
|
|
|
func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
|
mapper := NewTypeMapper("")
|
|
|
|
col := &models.Column{
|
|
Name: "embedding",
|
|
Type: "vector(1536)",
|
|
Length: 1536,
|
|
Precision: 0,
|
|
Scale: 0,
|
|
}
|
|
|
|
tag := mapper.BuildGormTag(col, nil)
|
|
if !strings.Contains(tag, "type:vector(1536)") {
|
|
t.Fatalf("expected explicit modifier to be preserved, got %q", tag)
|
|
}
|
|
if strings.Contains(tag, ")(") {
|
|
t.Fatalf("type modifier appears duplicated in %q", tag)
|
|
}
|
|
}
|
|
|
|
// TestTypeMapper_BuildGormTag_MultipleUniqueIndexesDeterministic verifies
|
|
// that when a column belongs to a unique constraint and more than one
|
|
// unique index, the "uniqueIndex:" tag fragments always appear in the same
|
|
// order across repeated calls, instead of following Go's randomized map
|
|
// iteration order over Table.Constraints and Table.Indexes.
|
|
func TestTypeMapper_BuildGormTag_MultipleUniqueIndexesDeterministic(t *testing.T) {
|
|
mapper := NewTypeMapper("")
|
|
table := &models.Table{
|
|
Name: "accounts",
|
|
Constraints: map[string]*models.Constraint{
|
|
"uq_z_accounts_email": {
|
|
Name: "uq_z_accounts_email",
|
|
Type: models.UniqueConstraint,
|
|
Columns: []string{"email"},
|
|
},
|
|
},
|
|
Indexes: map[string]*models.Index{
|
|
"idx_z_accounts_email_tenant": {
|
|
Name: "idx_z_accounts_email_tenant",
|
|
Columns: []string{"email", "tenant_id"},
|
|
Unique: true,
|
|
},
|
|
"idx_a_accounts_email_region": {
|
|
Name: "idx_a_accounts_email_region",
|
|
Columns: []string{"email", "region_id"},
|
|
Unique: true,
|
|
},
|
|
},
|
|
}
|
|
column := &models.Column{Name: "email", Type: "varchar", Length: 255, NotNull: true}
|
|
|
|
first := mapper.BuildGormTag(column, table)
|
|
for i := 0; i < 50; i++ {
|
|
got := mapper.BuildGormTag(column, table)
|
|
if got != first {
|
|
t.Fatalf("BuildGormTag() is non-deterministic across calls: %q vs %q", first, got)
|
|
}
|
|
}
|
|
|
|
wantOrder := "uniqueIndex:uq_z_accounts_email;uniqueIndex:idx_a_accounts_email_region;uniqueIndex:idx_z_accounts_email_tenant"
|
|
if !strings.Contains(first, wantOrder) {
|
|
t.Errorf("BuildGormTag() = %q, want uniqueIndex tags sorted (constraint before indexes, indexes by name): %q", first, wantOrder)
|
|
}
|
|
}
|