Bugs Fixed
1. pkg/models/models.go:184 - Fixed typo in ForeignKeyConstraint constant from "foreign_Key" to "foreign_key" 2. pkg/readers/drawdb/reader.go:62-68 - Fixed ReadSchema() to properly detect schema name from tables instead of hardcoding "default" 3. pkg/writers/dbml/writer.go:128 - Changed primary key attribute from "primary key" to DBML standard "pk" 4. pkg/writers/dbml/writer.go:208-221 - Fixed foreign key reference format to use "table.column" syntax for single columns instead of "table.(column)" Test Results All reader and writer tests are now passing: Readers: - DBML: 74.4% coverage (2 tests skipped due to missing parser features for Ref statements) - DCTX: 77.6% coverage - DrawDB: 83.6% coverage - JSON: 82.1% coverage - YAML: 82.1% coverage Writers: - Bun: 68.5% coverage - DBML: 91.5% coverage - DCTX: 100.0% coverage - DrawDB: 83.8% coverage - GORM: 69.2% coverage - JSON: 82.4% coverage - YAML: 82.4% coverage
This commit is contained in:
@@ -2,6 +2,7 @@ package bun
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
@@ -16,23 +17,24 @@ type TemplateData struct {
|
||||
|
||||
// ModelData represents a single model/struct in the template
|
||||
type ModelData struct {
|
||||
Name string
|
||||
TableName string // schema.table format
|
||||
SchemaName string
|
||||
TableNameOnly string // just table name without schema
|
||||
Comment string
|
||||
Fields []*FieldData
|
||||
Config *MethodConfig
|
||||
PrimaryKeyField string // Name of the primary key field
|
||||
IDColumnName string // Name of the ID column in database
|
||||
Prefix string // 3-letter prefix
|
||||
Name string
|
||||
TableName string // schema.table format
|
||||
SchemaName string
|
||||
TableNameOnly string // just table name without schema
|
||||
Comment string
|
||||
Fields []*FieldData
|
||||
Config *MethodConfig
|
||||
PrimaryKeyField string // Name of the primary key field
|
||||
PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call)
|
||||
IDColumnName string // Name of the ID column in database
|
||||
Prefix string // 3-letter prefix
|
||||
}
|
||||
|
||||
// FieldData represents a single field in a struct
|
||||
type FieldData struct {
|
||||
Name string // Go field name (PascalCase)
|
||||
Type string // Go type
|
||||
GormTag string // Complete gorm tag
|
||||
BunTag string // Complete bun tag
|
||||
JSONTag string // JSON tag
|
||||
Comment string // Field comment
|
||||
}
|
||||
@@ -133,6 +135,9 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M
|
||||
if col.IsPrimaryKey {
|
||||
model.PrimaryKeyField = SnakeCaseToPascalCase(col.Name)
|
||||
model.IDColumnName = col.Name
|
||||
// Check if PK type is a SQL type (contains resolvespec_common or sql_types)
|
||||
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
||||
model.PrimaryKeyIsSQL = strings.Contains(goType, "resolvespec_common") || strings.Contains(goType, "sql_types")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -151,13 +156,13 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper) *M
|
||||
func columnToField(col *models.Column, table *models.Table, typeMapper *TypeMapper) *FieldData {
|
||||
fieldName := SnakeCaseToPascalCase(col.Name)
|
||||
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
||||
gormTag := typeMapper.BuildGormTag(col, table)
|
||||
bunTag := typeMapper.BuildBunTag(col, table)
|
||||
jsonTag := col.Name // Use column name for JSON tag
|
||||
|
||||
return &FieldData{
|
||||
Name: fieldName,
|
||||
Type: goType,
|
||||
GormTag: gormTag,
|
||||
BunTag: bunTag,
|
||||
JSONTag: jsonTag,
|
||||
Comment: formatComment(col.Description, col.Comment),
|
||||
}
|
||||
|
||||
376
pkg/writers/bun/writer.go
Normal file
376
pkg/writers/bun/writer.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// Writer implements the writers.Writer interface for Bun models
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
typeMapper *TypeMapper
|
||||
templates *Templates
|
||||
config *MethodConfig
|
||||
}
|
||||
|
||||
// NewWriter creates a new Bun writer with the given options
|
||||
func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
w := &Writer{
|
||||
options: options,
|
||||
typeMapper: NewTypeMapper(),
|
||||
config: LoadMethodConfigFromMetadata(options.Metadata),
|
||||
}
|
||||
|
||||
// Initialize templates
|
||||
tmpl, err := NewTemplates()
|
||||
if err != nil {
|
||||
// Should not happen with embedded templates
|
||||
panic(fmt.Sprintf("failed to initialize templates: %v", err))
|
||||
}
|
||||
w.templates = tmpl
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// WriteDatabase writes a complete database as Bun models
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
// Check if multi-file mode is enabled
|
||||
multiFile := false
|
||||
if w.options.Metadata != nil {
|
||||
if mf, ok := w.options.Metadata["multi_file"].(bool); ok {
|
||||
multiFile = mf
|
||||
}
|
||||
}
|
||||
|
||||
if multiFile {
|
||||
return w.writeMultiFile(db)
|
||||
}
|
||||
|
||||
return w.writeSingleFile(db)
|
||||
}
|
||||
|
||||
// WriteSchema writes a schema as Bun models
|
||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
// Create a temporary database with just this schema
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// WriteTable writes a single table as a Bun model
|
||||
func (w *Writer) WriteTable(table *models.Table) error {
|
||||
// Create a temporary schema and database
|
||||
schema := models.InitSchema(table.Schema)
|
||||
schema.Tables = []*models.Table{table}
|
||||
|
||||
db := models.InitDatabase(schema.Name)
|
||||
db.Schemas = []*models.Schema{schema}
|
||||
|
||||
return w.WriteDatabase(db)
|
||||
}
|
||||
|
||||
// writeSingleFile writes all models to a single file
|
||||
func (w *Writer) writeSingleFile(db *models.Database) error {
|
||||
packageName := w.getPackageName()
|
||||
templateData := NewTemplateData(packageName, w.config)
|
||||
|
||||
// Add bun import (always needed)
|
||||
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
||||
|
||||
// Add resolvespec_common import (always needed for nullable types)
|
||||
templateData.AddImport(fmt.Sprintf("resolvespec_common \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
||||
|
||||
// Collect all models
|
||||
for _, schema := range db.Schemas {
|
||||
for _, table := range schema.Tables {
|
||||
modelData := NewModelData(table, schema.Name, w.typeMapper)
|
||||
|
||||
// Add relationship fields
|
||||
w.addRelationshipFields(modelData, table, schema, db)
|
||||
|
||||
templateData.AddModel(modelData)
|
||||
|
||||
// Check if we need time import
|
||||
for _, field := range modelData.Fields {
|
||||
if w.typeMapper.NeedsTimeImport(field.Type) {
|
||||
templateData.AddImport("\"time\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add fmt import if GetIDStr is enabled
|
||||
if w.config.GenerateGetIDStr {
|
||||
templateData.AddImport("\"fmt\"")
|
||||
}
|
||||
|
||||
// Finalize imports
|
||||
templateData.FinalizeImports()
|
||||
|
||||
// Generate code
|
||||
code, err := w.templates.GenerateCode(templateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate code: %w", err)
|
||||
}
|
||||
|
||||
// Format code
|
||||
formatted, err := w.formatCode(code)
|
||||
if err != nil {
|
||||
// Return unformatted code with warning
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to format code: %v\n", err)
|
||||
formatted = code
|
||||
}
|
||||
|
||||
// Write output
|
||||
return w.writeOutput(formatted)
|
||||
}
|
||||
|
||||
// writeMultiFile writes each table to a separate file
|
||||
func (w *Writer) writeMultiFile(db *models.Database) error {
|
||||
packageName := w.getPackageName()
|
||||
|
||||
// Check if populate_refs is enabled
|
||||
populateRefs := false
|
||||
if w.options.Metadata != nil {
|
||||
if pr, ok := w.options.Metadata["populate_refs"].(bool); ok {
|
||||
populateRefs = pr
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure output path is a directory
|
||||
if w.options.OutputPath == "" {
|
||||
return fmt.Errorf("output path is required for multi-file mode")
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(w.options.OutputPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate a file for each table
|
||||
for _, schema := range db.Schemas {
|
||||
// Populate RefDatabase for schema if enabled
|
||||
if populateRefs && schema.RefDatabase == nil {
|
||||
schema.RefDatabase = w.createDatabaseRef(db)
|
||||
}
|
||||
|
||||
for _, table := range schema.Tables {
|
||||
// Populate RefSchema for table if enabled
|
||||
if populateRefs && table.RefSchema == nil {
|
||||
table.RefSchema = w.createSchemaRef(schema, db)
|
||||
}
|
||||
// Create template data for this single table
|
||||
templateData := NewTemplateData(packageName, w.config)
|
||||
|
||||
// Add bun import
|
||||
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
||||
|
||||
// Add resolvespec_common import
|
||||
templateData.AddImport(fmt.Sprintf("resolvespec_common \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
||||
|
||||
// Create model data
|
||||
modelData := NewModelData(table, schema.Name, w.typeMapper)
|
||||
|
||||
// Add relationship fields
|
||||
w.addRelationshipFields(modelData, table, schema, db)
|
||||
|
||||
templateData.AddModel(modelData)
|
||||
|
||||
// Check if we need time import
|
||||
for _, field := range modelData.Fields {
|
||||
if w.typeMapper.NeedsTimeImport(field.Type) {
|
||||
templateData.AddImport("\"time\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Add fmt import if GetIDStr is enabled
|
||||
if w.config.GenerateGetIDStr {
|
||||
templateData.AddImport("\"fmt\"")
|
||||
}
|
||||
|
||||
// Finalize imports
|
||||
templateData.FinalizeImports()
|
||||
|
||||
// Generate code
|
||||
code, err := w.templates.GenerateCode(templateData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate code for table %s: %w", table.Name, err)
|
||||
}
|
||||
|
||||
// Format code
|
||||
formatted, err := w.formatCode(code)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to format code for %s: %v\n", table.Name, err)
|
||||
formatted = code
|
||||
}
|
||||
|
||||
// Generate filename: sql_{schema}_{table}.go
|
||||
filename := fmt.Sprintf("sql_%s_%s.go", schema.Name, table.Name)
|
||||
filepath := filepath.Join(w.options.OutputPath, filename)
|
||||
|
||||
// Write file
|
||||
if err := os.WriteFile(filepath, []byte(formatted), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addRelationshipFields adds relationship fields to the model based on foreign keys
|
||||
func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table, schema *models.Schema, db *models.Database) {
|
||||
// For each foreign key in this table, add a belongs-to/has-one relationship
|
||||
for _, constraint := range table.Constraints {
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the referenced table
|
||||
refTable := w.findTable(constraint.ReferencedSchema, constraint.ReferencedTable, db)
|
||||
if refTable == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create relationship field (has-one in Bun, similar to belongs-to in GORM)
|
||||
refModelName := w.getModelName(constraint.ReferencedTable)
|
||||
fieldName := w.generateRelationshipFieldName(constraint.ReferencedTable)
|
||||
relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-one")
|
||||
|
||||
modelData.AddRelationshipField(&FieldData{
|
||||
Name: fieldName,
|
||||
Type: "*" + refModelName, // Pointer type
|
||||
BunTag: relationTag,
|
||||
JSONTag: strings.ToLower(fieldName) + ",omitempty",
|
||||
Comment: fmt.Sprintf("Has one %s", refModelName),
|
||||
})
|
||||
}
|
||||
|
||||
// For each table that references this table, add a has-many relationship
|
||||
for _, otherSchema := range db.Schemas {
|
||||
for _, otherTable := range otherSchema.Tables {
|
||||
if otherTable.Name == table.Name && otherSchema.Name == schema.Name {
|
||||
continue // Skip self
|
||||
}
|
||||
|
||||
for _, constraint := range otherTable.Constraints {
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this constraint references our table
|
||||
if constraint.ReferencedTable == table.Name && constraint.ReferencedSchema == schema.Name {
|
||||
// Add has-many relationship
|
||||
otherModelName := w.getModelName(otherTable.Name)
|
||||
fieldName := w.generateRelationshipFieldName(otherTable.Name) + "s" // Pluralize
|
||||
relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-many")
|
||||
|
||||
modelData.AddRelationshipField(&FieldData{
|
||||
Name: fieldName,
|
||||
Type: "[]*" + otherModelName, // Slice of pointers
|
||||
BunTag: relationTag,
|
||||
JSONTag: strings.ToLower(fieldName) + ",omitempty",
|
||||
Comment: fmt.Sprintf("Has many %s", otherModelName),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findTable finds a table by schema and name in the database
|
||||
func (w *Writer) findTable(schemaName, tableName string, db *models.Database) *models.Table {
|
||||
for _, schema := range db.Schemas {
|
||||
if schema.Name != schemaName {
|
||||
continue
|
||||
}
|
||||
for _, table := range schema.Tables {
|
||||
if table.Name == tableName {
|
||||
return table
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getModelName generates the model name from a table name
|
||||
func (w *Writer) getModelName(tableName string) string {
|
||||
singular := Singularize(tableName)
|
||||
modelName := SnakeCaseToPascalCase(singular)
|
||||
|
||||
if !hasModelPrefix(modelName) {
|
||||
modelName = "Model" + modelName
|
||||
}
|
||||
|
||||
return modelName
|
||||
}
|
||||
|
||||
// generateRelationshipFieldName generates a field name for a relationship
|
||||
func (w *Writer) generateRelationshipFieldName(tableName string) string {
|
||||
// Use just the prefix (3 letters) for relationship fields
|
||||
return GeneratePrefix(tableName)
|
||||
}
|
||||
|
||||
// getPackageName returns the package name from options or defaults to "models"
|
||||
func (w *Writer) getPackageName() string {
|
||||
if w.options.PackageName != "" {
|
||||
return w.options.PackageName
|
||||
}
|
||||
return "models"
|
||||
}
|
||||
|
||||
// formatCode formats Go code using gofmt
|
||||
func (w *Writer) formatCode(code string) (string, error) {
|
||||
formatted, err := format.Source([]byte(code))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("format error: %w", err)
|
||||
}
|
||||
return string(formatted), nil
|
||||
}
|
||||
|
||||
// writeOutput writes the content to file or stdout
|
||||
func (w *Writer) writeOutput(content string) error {
|
||||
if w.options.OutputPath != "" {
|
||||
return os.WriteFile(w.options.OutputPath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// Print to stdout
|
||||
fmt.Print(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDatabaseRef creates a shallow copy of database without schemas to avoid circular references
|
||||
func (w *Writer) createDatabaseRef(db *models.Database) *models.Database {
|
||||
return &models.Database{
|
||||
Name: db.Name,
|
||||
Description: db.Description,
|
||||
Comment: db.Comment,
|
||||
DatabaseType: db.DatabaseType,
|
||||
DatabaseVersion: db.DatabaseVersion,
|
||||
SourceFormat: db.SourceFormat,
|
||||
Schemas: nil, // Don't include schemas to avoid circular reference
|
||||
}
|
||||
}
|
||||
|
||||
// createSchemaRef creates a shallow copy of schema without tables to avoid circular references
|
||||
func (w *Writer) createSchemaRef(schema *models.Schema, db *models.Database) *models.Schema {
|
||||
return &models.Schema{
|
||||
Name: schema.Name,
|
||||
Description: schema.Description,
|
||||
Owner: schema.Owner,
|
||||
Permissions: schema.Permissions,
|
||||
Comment: schema.Comment,
|
||||
Metadata: schema.Metadata,
|
||||
Scripts: schema.Scripts,
|
||||
Sequence: schema.Sequence,
|
||||
RefDatabase: w.createDatabaseRef(db), // Include database ref
|
||||
Tables: nil, // Don't include tables to avoid circular reference
|
||||
}
|
||||
}
|
||||
267
pkg/writers/bun/writer_test.go
Normal file
267
pkg/writers/bun/writer_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package bun
|
||||
|
||||
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 ModelUser struct",
|
||||
"bun.BaseModel",
|
||||
"table:public.users",
|
||||
"alias:users",
|
||||
"ID",
|
||||
"int64",
|
||||
"Email",
|
||||
"resolvespec_common.SqlString",
|
||||
"CreatedAt",
|
||||
"resolvespec_common.SqlTime",
|
||||
"bun:\"id",
|
||||
"bun:\"email",
|
||||
"func (m ModelUser) TableName() string",
|
||||
"return \"public.users\"",
|
||||
"func (m ModelUser) GetID() int64",
|
||||
}
|
||||
|
||||
for _, expected := range expectations {
|
||||
if !strings.Contains(generated, expected) {
|
||||
t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Bun-specific elements
|
||||
if !strings.Contains(generated, "bun:\"id,type:bigint,pk,") {
|
||||
t.Errorf("Missing Bun-style primary key tag")
|
||||
}
|
||||
}
|
||||
|
||||
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 Bun format
|
||||
if !strings.Contains(postsStr, "USE") {
|
||||
t.Errorf("Missing relationship field USE")
|
||||
}
|
||||
if !strings.Contains(postsStr, "rel:has-one") {
|
||||
t.Errorf("Missing Bun relationship tag: %s", postsStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
||||
mapper := NewTypeMapper()
|
||||
|
||||
tests := []struct {
|
||||
sqlType string
|
||||
notNull bool
|
||||
want string
|
||||
}{
|
||||
{"bigint", true, "int64"},
|
||||
{"bigint", false, "resolvespec_common.SqlInt64"},
|
||||
{"varchar", true, "resolvespec_common.SqlString"}, // Bun uses sql types even for NOT NULL strings
|
||||
{"varchar", false, "resolvespec_common.SqlString"},
|
||||
{"timestamp", true, "resolvespec_common.SqlTime"},
|
||||
{"timestamp", false, "resolvespec_common.SqlTime"},
|
||||
{"date", false, "resolvespec_common.SqlDate"},
|
||||
{"boolean", true, "bool"},
|
||||
{"boolean", false, "resolvespec_common.SqlBool"},
|
||||
{"uuid", false, "resolvespec_common.SqlUUID"},
|
||||
{"jsonb", false, "resolvespec_common.SqlJSONB"},
|
||||
}
|
||||
|
||||
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_BuildBunTag(t *testing.T) {
|
||||
mapper := NewTypeMapper()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
want []string // Parts that should be in the tag
|
||||
}{
|
||||
{
|
||||
name: "primary key",
|
||||
column: &models.Column{
|
||||
Name: "id",
|
||||
Type: "bigint",
|
||||
IsPrimaryKey: true,
|
||||
NotNull: true,
|
||||
},
|
||||
want: []string{"id,", "type:bigint,", "pk,"},
|
||||
},
|
||||
{
|
||||
name: "nullable varchar",
|
||||
column: &models.Column{
|
||||
Name: "email",
|
||||
Type: "varchar",
|
||||
Length: 255,
|
||||
NotNull: false,
|
||||
},
|
||||
want: []string{"email,", "type:varchar(255),", "nullzero,"},
|
||||
},
|
||||
{
|
||||
name: "with default",
|
||||
column: &models.Column{
|
||||
Name: "status",
|
||||
Type: "text",
|
||||
NotNull: true,
|
||||
Default: "active",
|
||||
},
|
||||
want: []string{"status,", "type:text,", "default:active,"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := mapper.BuildBunTag(tt.column, nil)
|
||||
for _, part := range tt.want {
|
||||
if !strings.Contains(result, part) {
|
||||
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user