package dbml import ( "os" "path/filepath" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" ) func TestReader_ReadDatabase_Simple(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if db == nil { t.Fatal("ReadDatabase() returned nil database") } if len(db.Schemas) == 0 { t.Fatal("Expected at least one schema") } schema := db.Schemas[0] if schema.Name != "public" { t.Errorf("Expected schema name 'public', got '%s'", schema.Name) } if len(schema.Tables) != 1 { t.Fatalf("Expected 1 table, got %d", len(schema.Tables)) } table := schema.Tables[0] if table.Name != "users" { t.Errorf("Expected table name 'users', got '%s'", table.Name) } if len(table.Columns) != 4 { t.Errorf("Expected 4 columns, got %d", len(table.Columns)) } // Verify id column idCol, exists := table.Columns["id"] if !exists { t.Fatal("Column 'id' not found") } if !idCol.IsPrimaryKey { t.Error("Column 'id' should be primary key") } if !idCol.AutoIncrement { t.Error("Column 'id' should be auto-increment") } if !idCol.NotNull { t.Error("Column 'id' should be not null") } if idCol.Type != "bigint" { t.Errorf("Expected id type 'bigint', got '%s'", idCol.Type) } // Verify email column emailCol, exists := table.Columns["email"] if !exists { t.Fatal("Column 'email' not found") } if !emailCol.NotNull { t.Error("Column 'email' should be not null") } if emailCol.Type != "varchar(255)" { t.Errorf("Expected email type 'varchar(255)', got '%s'", emailCol.Type) } // Verify default value createdCol, exists := table.Columns["created_at"] if !exists { t.Fatal("Column 'created_at' not found") } if createdCol.Default == nil { t.Error("Expected default value for created_at") } } func TestReader_ReadDatabase_Complex(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "complex.dbml"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if db == nil { t.Fatal("ReadDatabase() returned nil database") } // Verify multiple schemas if len(db.Schemas) != 2 { t.Fatalf("Expected 2 schemas, got %d", len(db.Schemas)) } // Find public schema var publicSchema *models.Schema var adminSchema *models.Schema for _, schema := range db.Schemas { if schema.Name == "public" { publicSchema = schema } else if schema.Name == "admin" { adminSchema = schema } } if publicSchema == nil { t.Fatal("Public schema not found") } if adminSchema == nil { t.Fatal("Admin schema not found") } // Verify public schema has 3 tables if len(publicSchema.Tables) != 3 { t.Errorf("Expected 3 tables in public schema, got %d", len(publicSchema.Tables)) } // Find tables var usersTable, postsTable, commentsTable *models.Table for _, table := range publicSchema.Tables { switch table.Name { case "users": usersTable = table case "posts": postsTable = table case "comments": commentsTable = table } } if usersTable == nil { t.Fatal("Users table not found") } if postsTable == nil { t.Fatal("Posts table not found") } if commentsTable == nil { t.Fatal("Comments table not found") } // Verify users table has indexes if len(usersTable.Indexes) != 2 { t.Errorf("Expected 2 indexes on users table, got %d", len(usersTable.Indexes)) } // Verify named index emailIdx, exists := usersTable.Indexes["idx_users_email"] if !exists { t.Error("Index 'idx_users_email' not found") } else { if !emailIdx.Unique { t.Error("Email index should be unique") } if len(emailIdx.Columns) != 1 || emailIdx.Columns[0] != "email" { t.Error("Email index should have 'email' column") } } // Verify table description (DBML Note field maps to Description) // Note: The description is only set if the DBML reader properly parses the Note if usersTable.Description == "" { t.Log("Warning: users table description is empty (Note field may not be parsed)") } // Verify posts table columns if len(postsTable.Columns) != 9 { t.Errorf("Expected 9 columns in posts table, got %d", len(postsTable.Columns)) } // Verify slug column is unique slugCol, exists := postsTable.Columns["slug"] if !exists { t.Fatal("Column 'slug' not found in posts table") } if !slugCol.NotNull { t.Error("Slug column should be not null") } // Verify default values publishedCol, exists := postsTable.Columns["published"] if !exists { t.Fatal("Column 'published' not found") } if publishedCol.Default != "false" { t.Errorf("Expected published default 'false', got '%v'", publishedCol.Default) } viewCountCol, exists := postsTable.Columns["view_count"] if !exists { t.Fatal("Column 'view_count' not found") } if viewCountCol.Default != "0" { t.Errorf("Expected view_count default '0', got '%v'", viewCountCol.Default) } // Verify posts indexes if len(postsTable.Indexes) != 3 { t.Errorf("Expected 3 indexes on posts table, got %d", len(postsTable.Indexes)) } // Verify btree index type found := false for _, idx := range postsTable.Indexes { if idx.Type == "btree" { found = true break } } if !found { t.Error("Expected at least one btree index on posts table") } // Verify foreign key constraints // Note: Constraint names are generated by the reader if len(postsTable.Constraints) == 0 { t.Log("Warning: No constraints found on posts table - Ref statements may not be parsed correctly") t.Skip("Skipping constraint verification as none were found") } // Find any foreign key constraint (name may vary) var fkPostsUser *models.Constraint for _, c := range postsTable.Constraints { if c.Type == models.ForeignKeyConstraint && c.ReferencedTable == "users" { fkPostsUser = c break } } if fkPostsUser == nil { t.Fatal("Foreign key to users table not found") } if fkPostsUser.Type != models.ForeignKeyConstraint { t.Error("Expected foreign key constraint type") } if fkPostsUser.ReferencedTable != "users" { t.Errorf("Expected referenced table 'users', got '%s'", fkPostsUser.ReferencedTable) } if fkPostsUser.OnDelete != "CASCADE" { t.Errorf("Expected ON DELETE CASCADE, got '%s'", fkPostsUser.OnDelete) } if fkPostsUser.OnUpdate != "CASCADE" { t.Errorf("Expected ON UPDATE CASCADE, got '%s'", fkPostsUser.OnUpdate) } if len(fkPostsUser.Columns) != 1 || fkPostsUser.Columns[0] != "user_id" { t.Error("Expected FK column 'user_id'") } if len(fkPostsUser.ReferencedColumns) != 1 || fkPostsUser.ReferencedColumns[0] != "id" { t.Error("Expected FK referenced column 'id'") } // Verify comments table constraints if len(commentsTable.Constraints) == 0 { t.Log("Warning: No constraints found on comments table") t.Skip("Skipping constraint verification as none were found") } // Find foreign keys (names may vary) var fkCommentsPost, fkCommentsUser *models.Constraint for _, c := range commentsTable.Constraints { if c.Type == models.ForeignKeyConstraint { if c.ReferencedTable == "posts" { fkCommentsPost = c } else if c.ReferencedTable == "users" { fkCommentsUser = c } } } // Check foreign key to posts with CASCADE if fkCommentsPost == nil { t.Error("Foreign key to posts table not found") } else if fkCommentsPost.OnDelete != "CASCADE" { t.Errorf("Expected ON DELETE CASCADE for comments->posts FK, got '%s'", fkCommentsPost.OnDelete) } // Check foreign key to users with SET NULL if fkCommentsUser == nil { t.Error("Foreign key to users table not found") } else if fkCommentsUser.OnDelete != "SET NULL" { t.Errorf("Expected ON DELETE SET NULL for comments->users FK, got '%s'", fkCommentsUser.OnDelete) } // Verify admin schema if len(adminSchema.Tables) != 1 { t.Errorf("Expected 1 table in admin schema, got %d", len(adminSchema.Tables)) } auditTable := adminSchema.Tables[0] if auditTable.Name != "audit_logs" { t.Errorf("Expected table name 'audit_logs', got '%s'", auditTable.Name) } if auditTable.Schema != "admin" { t.Errorf("Expected table schema 'admin', got '%s'", auditTable.Schema) } // Verify cross-schema foreign key if len(auditTable.Constraints) == 0 { t.Log("Warning: No constraints found on audit_logs table") t.Skip("Skipping constraint verification as none were found") } // Find foreign key to users (name may vary) var fkAuditUser *models.Constraint for _, c := range auditTable.Constraints { if c.Type == models.ForeignKeyConstraint && c.ReferencedTable == "users" { fkAuditUser = c break } } if fkAuditUser == nil { t.Fatal("Foreign key to users table not found") } if fkAuditUser.ReferencedSchema != "public" { t.Errorf("Expected referenced schema 'public', got '%s'", fkAuditUser.ReferencedSchema) } if fkAuditUser.ReferencedTable != "users" { t.Errorf("Expected referenced table 'users', got '%s'", fkAuditUser.ReferencedTable) } } func TestReader_ReadDatabase_Minimal(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "minimal.dbml"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if len(db.Schemas) == 0 { t.Fatal("Expected at least one schema") } schema := db.Schemas[0] if len(schema.Tables) != 1 { t.Fatalf("Expected 1 table, got %d", len(schema.Tables)) } table := schema.Tables[0] if table.Name != "users" { t.Errorf("Expected table name 'users', got '%s'", table.Name) } if len(table.Columns) != 1 { t.Errorf("Expected 1 column, got %d", len(table.Columns)) } idCol, exists := table.Columns["id"] if !exists { t.Fatal("Column 'id' not found") } if !idCol.IsPrimaryKey { t.Error("Column 'id' should be primary key") } } func TestReader_ReadSchema(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), } reader := NewReader(opts) schema, err := reader.ReadSchema() if err != nil { t.Fatalf("ReadSchema() error = %v", err) } if schema == nil { t.Fatal("ReadSchema() returned nil schema") } if schema.Name != "public" { t.Errorf("Expected schema name 'public', got '%s'", schema.Name) } if len(schema.Tables) != 1 { t.Errorf("Expected 1 table, got %d", len(schema.Tables)) } } func TestReader_ReadTable(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), } reader := NewReader(opts) table, err := reader.ReadTable() if err != nil { t.Fatalf("ReadTable() error = %v", err) } if table == nil { t.Fatal("ReadTable() returned nil table") } if table.Name != "users" { t.Errorf("Expected table name 'users', got '%s'", table.Name) } if len(table.Columns) != 4 { t.Errorf("Expected 4 columns, got %d", len(table.Columns)) } } func TestReader_ReadDatabase_InvalidPath(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: "/nonexistent/file.dbml", } reader := NewReader(opts) _, err := reader.ReadDatabase() if err == nil { t.Error("Expected error for invalid file path") } } func TestReader_ReadDatabase_EmptyPath(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: "", } reader := NewReader(opts) _, err := reader.ReadDatabase() if err == nil { t.Error("Expected error for empty file path") } } func TestReader_ReadDatabase_WithMetadata(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), Metadata: map[string]interface{}{ "name": "custom_db_name", }, } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if db.Name != "custom_db_name" { t.Errorf("Expected database name 'custom_db_name', got '%s'", db.Name) } } func TestGetPrimaryKey(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), } reader := NewReader(opts) table, err := reader.ReadTable() if err != nil { t.Fatalf("ReadTable() error = %v", err) } pk := table.GetPrimaryKey() if pk == nil { t.Fatal("Expected primary key, got nil") } if pk.Name != "id" { t.Errorf("Expected primary key name 'id', got '%s'", pk.Name) } } func TestGetForeignKeys(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "complex.dbml"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } // Find posts table var postsTable *models.Table for _, schema := range db.Schemas { for _, table := range schema.Tables { if table.Name == "posts" { postsTable = table break } } } if postsTable == nil { t.Fatal("Posts table not found") } fks := postsTable.GetForeignKeys() if len(fks) == 0 { t.Skip("No foreign keys found - Ref statements may not be parsed correctly") } if fks[0].Type != models.ForeignKeyConstraint { t.Error("Expected foreign key constraint type") } } // Tests for multi-file directory loading func TestReadDirectory_MultipleFiles(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if db == nil { t.Fatal("ReadDatabase() returned nil database") } // Should have public schema if len(db.Schemas) == 0 { t.Fatal("Expected at least one schema") } var publicSchema *models.Schema for _, schema := range db.Schemas { if schema.Name == "public" { publicSchema = schema break } } if publicSchema == nil { t.Fatal("Public schema not found") } // Should have 3 tables: users, posts, comments if len(publicSchema.Tables) != 3 { t.Fatalf("Expected 3 tables, got %d", len(publicSchema.Tables)) } // Find tables var usersTable, postsTable, commentsTable *models.Table for _, table := range publicSchema.Tables { switch table.Name { case "users": usersTable = table case "posts": postsTable = table case "comments": commentsTable = table } } if usersTable == nil { t.Fatal("Users table not found") } if postsTable == nil { t.Fatal("Posts table not found") } if commentsTable == nil { t.Fatal("Comments table not found") } // Verify users table has merged columns from 1_users.dbml and 3_add_columns.dbml expectedUserColumns := []string{"id", "email", "name", "created_at"} if len(usersTable.Columns) != len(expectedUserColumns) { t.Errorf("Expected %d columns in users table, got %d", len(expectedUserColumns), len(usersTable.Columns)) } for _, colName := range expectedUserColumns { if _, exists := usersTable.Columns[colName]; !exists { t.Errorf("Expected column '%s' in users table", colName) } } // Verify posts table columns expectedPostColumns := []string{"id", "user_id", "title", "content", "created_at"} for _, colName := range expectedPostColumns { if _, exists := postsTable.Columns[colName]; !exists { t.Errorf("Expected column '%s' in posts table", colName) } } } func TestReadDirectory_TableMerging(t *testing.T) { opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } // Find users table var usersTable *models.Table for _, schema := range db.Schemas { for _, table := range schema.Tables { if table.Name == "users" && schema.Name == "public" { usersTable = table break } } } if usersTable == nil { t.Fatal("Users table not found") } // Verify columns from file 1 (id, email) if _, exists := usersTable.Columns["id"]; !exists { t.Error("Column 'id' from 1_users.dbml not found") } if _, exists := usersTable.Columns["email"]; !exists { t.Error("Column 'email' from 1_users.dbml not found") } // Verify columns from file 3 (name, created_at) if _, exists := usersTable.Columns["name"]; !exists { t.Error("Column 'name' from 3_add_columns.dbml not found") } if _, exists := usersTable.Columns["created_at"]; !exists { t.Error("Column 'created_at' from 3_add_columns.dbml not found") } // Verify column properties from file 1 emailCol := usersTable.Columns["email"] if !emailCol.NotNull { t.Error("Email column should be not null (from 1_users.dbml)") } if emailCol.Type != "varchar(255)" { t.Errorf("Expected email type 'varchar(255)', got '%s'", emailCol.Type) } } func TestReadDirectory_CommentedRefsLast(t *testing.T) { // This test verifies that files with commented refs are processed last // by checking that the file discovery returns them in the correct order dirPath := filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile") opts := &readers.ReaderOptions{ FilePath: dirPath, } reader := NewReader(opts) files, err := reader.discoverDBMLFiles(dirPath) if err != nil { t.Fatalf("discoverDBMLFiles() error = %v", err) } if len(files) < 2 { t.Skip("Not enough files to test ordering") } // Check that 9_refs.dbml (which has commented refs) comes last lastFile := filepath.Base(files[len(files)-1]) if lastFile != "9_refs.dbml" { t.Errorf("Expected last file to be '9_refs.dbml' (has commented refs), got '%s'", lastFile) } // Check that numbered files without commented refs come first firstFile := filepath.Base(files[0]) if firstFile != "1_users.dbml" { t.Errorf("Expected first file to be '1_users.dbml', got '%s'", firstFile) } } func TestReadDirectory_EmptyDirectory(t *testing.T) { // Create a temporary empty directory tmpDir := filepath.Join("..", "..", "..", "tests", "assets", "dbml", "empty_test_dir") err := os.MkdirAll(tmpDir, 0755) if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } defer os.RemoveAll(tmpDir) opts := &readers.ReaderOptions{ FilePath: tmpDir, } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() should not error on empty directory, got: %v", err) } if db == nil { t.Fatal("ReadDatabase() returned nil database") } // Empty directory should return empty database if len(db.Schemas) != 0 { t.Errorf("Expected 0 schemas for empty directory, got %d", len(db.Schemas)) } } func TestReadDatabase_BackwardCompat(t *testing.T) { // Test that single file loading still works opts := &readers.ReaderOptions{ FilePath: filepath.Join("..", "..", "..", "tests", "assets", "dbml", "simple.dbml"), } reader := NewReader(opts) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("ReadDatabase() error = %v", err) } if db == nil { t.Fatal("ReadDatabase() returned nil database") } if len(db.Schemas) == 0 { t.Fatal("Expected at least one schema") } schema := db.Schemas[0] if len(schema.Tables) != 1 { t.Fatalf("Expected 1 table, got %d", len(schema.Tables)) } table := schema.Tables[0] if table.Name != "users" { t.Errorf("Expected table name 'users', got '%s'", table.Name) } } func TestParseFilePrefix(t *testing.T) { tests := []struct { filename string wantPrefix int wantHas bool }{ {"1_schema.dbml", 1, true}, {"2_tables.dbml", 2, true}, {"10_relationships.dbml", 10, true}, {"99_data.dbml", 99, true}, {"schema.dbml", 0, false}, {"tables_no_prefix.dbml", 0, false}, {"/path/to/1_file.dbml", 1, true}, {"/path/to/file.dbml", 0, false}, {"1-file.dbml", 1, true}, {"2-another.dbml", 2, true}, } for _, tt := range tests { t.Run(tt.filename, func(t *testing.T) { gotPrefix, gotHas := parseFilePrefix(tt.filename) if gotPrefix != tt.wantPrefix { t.Errorf("parseFilePrefix(%s) prefix = %d, want %d", tt.filename, gotPrefix, tt.wantPrefix) } if gotHas != tt.wantHas { t.Errorf("parseFilePrefix(%s) hasPrefix = %v, want %v", tt.filename, gotHas, tt.wantHas) } }) } } func TestHasCommentedRefs(t *testing.T) { // Test with the actual multifile test fixtures tests := []struct { filename string wantHas bool }{ {filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile", "1_users.dbml"), false}, {filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile", "2_posts.dbml"), false}, {filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile", "3_add_columns.dbml"), false}, {filepath.Join("..", "..", "..", "tests", "assets", "dbml", "multifile", "9_refs.dbml"), true}, } for _, tt := range tests { t.Run(filepath.Base(tt.filename), func(t *testing.T) { gotHas, err := hasCommentedRefs(tt.filename) if err != nil { t.Fatalf("hasCommentedRefs() error = %v", err) } if gotHas != tt.wantHas { t.Errorf("hasCommentedRefs(%s) = %v, want %v", filepath.Base(tt.filename), gotHas, tt.wantHas) } }) } }