Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5edb004799 | |||
| 764d00c249 | |||
| 47b77763cb |
+25
-2
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
stdjson "encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,6 +55,7 @@ var (
|
|||||||
convertFlattenSchema bool
|
convertFlattenSchema bool
|
||||||
convertNullableTypes string
|
convertNullableTypes string
|
||||||
convertContinueOnError bool
|
convertContinueOnError bool
|
||||||
|
convertExtraFields string
|
||||||
)
|
)
|
||||||
|
|
||||||
var convertCmd = &cobra.Command{
|
var convertCmd = &cobra.Command{
|
||||||
@@ -179,6 +181,7 @@ func init() {
|
|||||||
convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)")
|
convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)")
|
||||||
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
|
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
|
||||||
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
|
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
|
||||||
|
convertCmd.Flags().StringVar(&convertExtraFields, "extra-fields", "", "Path to JSON file containing extra Bun model fields to inject (bun output only); fields support target_table, name, type, bun_tag, json_tag, comment")
|
||||||
|
|
||||||
err := convertCmd.MarkFlagRequired("from")
|
err := convertCmd.MarkFlagRequired("from")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,7 +248,7 @@ func runConvert(cmd *cobra.Command, args []string) error {
|
|||||||
fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter)
|
fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes, convertContinueOnError); err != nil {
|
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes, convertContinueOnError, convertExtraFields); err != nil {
|
||||||
return fmt.Errorf("failed to write target: %w", err)
|
return fmt.Errorf("failed to write target: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +388,30 @@ func readDatabaseForConvert(dbType, filePath, connString string) (*models.Databa
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string, continueOnError bool) error {
|
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string, continueOnError bool, extraFields string) error {
|
||||||
var writer writers.Writer
|
var writer writers.Writer
|
||||||
|
|
||||||
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError)
|
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError)
|
||||||
|
if extraFields != "" {
|
||||||
|
if strings.ToLower(dbType) != "bun" {
|
||||||
|
return fmt.Errorf("--extra-fields is only supported for Bun output")
|
||||||
|
}
|
||||||
|
extraFieldsJSON, err := os.ReadFile(extraFields)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read --extra-fields file %q: %w", extraFields, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed []wbun.ExtraFieldConfig
|
||||||
|
if err := stdjson.Unmarshal(extraFieldsJSON, &parsed); err != nil {
|
||||||
|
return fmt.Errorf("invalid --extra-fields JSON in %q: %w", extraFields, err)
|
||||||
|
}
|
||||||
|
if len(parsed) == 0 {
|
||||||
|
return fmt.Errorf("--extra-fields must contain at least one field")
|
||||||
|
}
|
||||||
|
writerOpts.Metadata = map[string]interface{}{
|
||||||
|
"extra_fields": string(extraFieldsJSON),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch strings.ToLower(dbType) {
|
switch strings.ToLower(dbType) {
|
||||||
case "dbml":
|
case "dbml":
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ func runSplit(cmd *cobra.Command, args []string) error {
|
|||||||
false, // no flatten-schema for split
|
false, // no flatten-schema for split
|
||||||
splitNullableTypes,
|
splitNullableTypes,
|
||||||
false, // no continue-on-error for split
|
false, // no continue-on-error for split
|
||||||
|
"", // no extra fields for split
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write output: %w", err)
|
return fmt.Errorf("failed to write output: %w", err)
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
|||||||
# Multi-file output (one file per table)
|
# Multi-file output (one file per table)
|
||||||
relspec convert --from json --from-path schema.json \
|
relspec convert --from json --from-path schema.json \
|
||||||
--to bun --to-path models/ --package models
|
--to bun --to-path models/ --package models
|
||||||
|
|
||||||
|
# Inject computed/scan-only fields that are not present in the database schema
|
||||||
|
# (extra-fields.json contains the JSON array shown below)
|
||||||
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
|
--to bun --to-path models.go --package models \
|
||||||
|
--extra-fields extra-fields.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generated Code Examples
|
## Generated Code Examples
|
||||||
@@ -182,6 +188,61 @@ options := &writers.WriterOptions{
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Extra fields
|
||||||
|
|
||||||
|
Use `Metadata["extra_fields"]` (or the CLI `--extra-fields <path>` flag) to inject
|
||||||
|
custom fields into generated Bun models without editing generated files. This is
|
||||||
|
intended for computed columns and scan-only query fields that Bun must scan into
|
||||||
|
but that are not real database table columns.
|
||||||
|
|
||||||
|
The CLI flag expects a path to a JSON file, not inline JSON. This avoids shell
|
||||||
|
quoting problems with struct tags and complex type names.
|
||||||
|
|
||||||
|
Each entry supports:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---:|---|
|
||||||
|
| `target_table` | No | Optional model scope. Accepts table name (`projects`), qualified table (`public.projects`), or model name (`ModelPublicProjects`). If omitted, the field is added to every generated model. |
|
||||||
|
| `name` | Yes | Go struct field name. |
|
||||||
|
| `type` | Yes | Go type to emit. |
|
||||||
|
| `bun_tag` | No | Raw Bun tag contents, e.g. `thought_count,scanonly`. |
|
||||||
|
| `json_tag` | No | Raw JSON tag contents. |
|
||||||
|
| `comment` | No | Optional line comment. |
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"extra_fields": `[
|
||||||
|
{
|
||||||
|
"target_table": "projects",
|
||||||
|
"name": "ThoughtCount",
|
||||||
|
"type": "sql_types.SqlInt64",
|
||||||
|
"bun_tag": "thought_count,scanonly",
|
||||||
|
"json_tag": "thought_count",
|
||||||
|
"comment": "Computed by ResolveSpec queries"
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example `extra-fields.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"target_table": "projects",
|
||||||
|
"name": "ThoughtCount",
|
||||||
|
"type": "sql_types.SqlInt64",
|
||||||
|
"bun_tag": "thought_count,scanonly",
|
||||||
|
"json_tag": "thought_count",
|
||||||
|
"comment": "Computed by ResolveSpec queries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Model names are derived from table names (singularized, PascalCase)
|
- Model names are derived from table names (singularized, PascalCase)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package bun
|
package bun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -32,6 +34,90 @@ type ModelData struct {
|
|||||||
PrimaryKeyIDType string // Helper method GetID/SetID/UpdateID type
|
PrimaryKeyIDType string // Helper method GetID/SetID/UpdateID type
|
||||||
IDColumnName string // Name of the ID column in database
|
IDColumnName string // Name of the ID column in database
|
||||||
Prefix string // 3-letter prefix
|
Prefix string // 3-letter prefix
|
||||||
|
|
||||||
|
// ExtraFields are user-defined fields added via generator config (issue #4)
|
||||||
|
ExtraFields []*FieldData `json:"extra_fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraFieldConfig represents a custom field to be added to generated models
|
||||||
|
type ExtraFieldConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
BunTag string `json:"bun_tag,omitempty"`
|
||||||
|
JSONTag string `json:"json_tag,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
TargetTable string `json:"target_table,omitempty"` // optional: if set, field only applies to this table
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadExtraFieldsFromMetadata loads custom field configurations from metadata map.
|
||||||
|
// Accepts either a JSON-encoded array of fields (for CLI use) or a structured list
|
||||||
|
// (for programmatic/sidecar file use). Each entry must have "name" and "type";
|
||||||
|
// an optional "target_table" scopes the field to one table only.
|
||||||
|
func LoadExtraFieldsFromMetadata(metadata map[string]interface{}) []ExtraFieldConfig {
|
||||||
|
if metadata == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
extraFieldsRaw, ok := metadata["extra_fields"]
|
||||||
|
if !ok || extraFieldsRaw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as JSON string first (from CLI flag)
|
||||||
|
if strVal, ok := extraFieldsRaw.(string); ok {
|
||||||
|
var fields []ExtraFieldConfig
|
||||||
|
if err := json.Unmarshal([]byte(strVal), &fields); err == nil && len(fields) > 0 {
|
||||||
|
return filterValidFields(fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as structured data (from sidecar file or programmatic API)
|
||||||
|
extraFieldsList, ok := extraFieldsRaw.([]interface{})
|
||||||
|
if !ok || len(extraFieldsList) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := make([]ExtraFieldConfig, 0, len(extraFieldsList))
|
||||||
|
for _, item := range extraFieldsList {
|
||||||
|
if fieldMap, ok := item.(map[string]interface{}); ok {
|
||||||
|
field := ExtraFieldConfig{
|
||||||
|
Name: toString(fieldMap["name"]),
|
||||||
|
Type: toString(fieldMap["type"]),
|
||||||
|
BunTag: toString(fieldMap["bun_tag"]),
|
||||||
|
JSONTag: toString(fieldMap["json_tag"]),
|
||||||
|
Comment: toString(fieldMap["comment"]),
|
||||||
|
TargetTable: toString(fieldMap["target_table"]),
|
||||||
|
}
|
||||||
|
if field.Name != "" && field.Type != "" {
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterValidFields(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterValidFields removes entries that are missing required fields.
|
||||||
|
func filterValidFields(fields []ExtraFieldConfig) []ExtraFieldConfig {
|
||||||
|
var valid []ExtraFieldConfig
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.Name != "" && f.Type != "" {
|
||||||
|
valid = append(valid, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FieldData represents a single field in a struct
|
// FieldData represents a single field in a struct
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ type {{.Name}} struct {
|
|||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{.Name}} {{.Type}} ` + "`bun:\"{{.BunTag}}\" json:\"{{.JSONTag}}\"`" + `{{if .Comment}} // {{.Comment}}{{end}}
|
{{.Name}} {{.Type}} ` + "`bun:\"{{.BunTag}}\" json:\"{{.JSONTag}}\"`" + `{{if .Comment}} // {{.Comment}}{{end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- if .ExtraFields}}
|
||||||
|
|
||||||
|
// --- Custom fields (managed by relspecgo generator config, issue #4) ---
|
||||||
|
{{- range .ExtraFields}}
|
||||||
|
{{.Name}} {{.Type}} ` + "`bun:\"{{.BunTag}}\" json:\"{{.JSONTag}}\"`" + `{{if .Comment}} // {{.Comment}}{{end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
}
|
}
|
||||||
{{if .Config.GenerateTableName}}
|
{{if .Config.GenerateTableName}}
|
||||||
// TableName returns the table name for {{.Name}}
|
// TableName returns the table name for {{.Name}}
|
||||||
|
|||||||
@@ -51,6 +51,43 @@ func (w *Writer) WriteDatabase(db *models.Database) error {
|
|||||||
return w.writeSingleFile(db)
|
return w.writeSingleFile(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addExtraFields appends user-defined extra fields to matching models.
|
||||||
|
// Fields may be scoped by target_table (table name, schema.table name, or model name).
|
||||||
|
// Unscoped fields apply to every generated model.
|
||||||
|
func (w *Writer) addExtraFields(templateData *TemplateData) {
|
||||||
|
extraFields := LoadExtraFieldsFromMetadata(w.options.Metadata)
|
||||||
|
if len(extraFields) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range templateData.Models {
|
||||||
|
for _, ef := range extraFields {
|
||||||
|
if !extraFieldMatchesModel(ef, model) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldData := &FieldData{
|
||||||
|
Name: resolveFieldNameCollision(ef.Name),
|
||||||
|
Type: ef.Type,
|
||||||
|
BunTag: ef.BunTag,
|
||||||
|
JSONTag: ef.JSONTag,
|
||||||
|
Comment: ef.Comment,
|
||||||
|
}
|
||||||
|
model.ExtraFields = append(model.ExtraFields, fieldData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extraFieldMatchesModel(field ExtraFieldConfig, model *ModelData) bool {
|
||||||
|
if field.TargetTable == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.TargetTable == model.TableNameOnly ||
|
||||||
|
field.TargetTable == model.TableName ||
|
||||||
|
field.TargetTable == model.Name
|
||||||
|
}
|
||||||
|
|
||||||
// WriteSchema writes a schema as Bun models
|
// WriteSchema writes a schema as Bun models
|
||||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||||
// Create a temporary database with just this schema
|
// Create a temporary database with just this schema
|
||||||
@@ -107,6 +144,9 @@ func (w *Writer) writeSingleFile(db *models.Database) error {
|
|||||||
templateData.AddImport("\"fmt\"")
|
templateData.AddImport("\"fmt\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply extra fields from generator config (issue #4)
|
||||||
|
w.addExtraFields(templateData)
|
||||||
|
|
||||||
// Finalize imports
|
// Finalize imports
|
||||||
templateData.FinalizeImports()
|
templateData.FinalizeImports()
|
||||||
|
|
||||||
@@ -200,6 +240,9 @@ func (w *Writer) writeMultiFile(db *models.Database) error {
|
|||||||
templateData.AddImport("\"fmt\"")
|
templateData.AddImport("\"fmt\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply extra fields from generator config (issue #4)
|
||||||
|
w.addExtraFields(templateData)
|
||||||
|
|
||||||
// Finalize imports
|
// Finalize imports
|
||||||
templateData.FinalizeImports()
|
templateData.FinalizeImports()
|
||||||
|
|
||||||
|
|||||||
@@ -855,6 +855,166 @@ func TestTypeMapper_BuildBunTag_StdlibArrayHasArrayTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtraFields_LoadFromMetadata(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
metadata map[string]interface{}
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil metadata",
|
||||||
|
metadata: nil,
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no extra_fields key",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"generate_table_name": true,
|
||||||
|
},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty array",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"extra_fields": []interface{}{},
|
||||||
|
},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON string format",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"extra_fields": `[{"name":"ThoughtCount","type":"int64","bun_tag":"thought_count,scanonly","json_tag":"thought_count"}]`,
|
||||||
|
},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "structured format",
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"extra_fields": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "ThoughtCount",
|
||||||
|
"type": "int64",
|
||||||
|
"bun_tag": "thought_count,scanonly",
|
||||||
|
"json_tag": "thought_count",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fields := LoadExtraFieldsFromMetadata(tt.metadata)
|
||||||
|
if len(fields) != tt.expected {
|
||||||
|
t.Errorf("expected %d fields, got %d", tt.expected, len(fields))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtraFields_InSingleFile(t *testing.T) {
|
||||||
|
table := models.InitTable("projects", "public")
|
||||||
|
table.Columns["id"] = &models.Column{
|
||||||
|
Name: "id",
|
||||||
|
Type: "bigint",
|
||||||
|
NotNull: true,
|
||||||
|
IsPrimaryKey: true,
|
||||||
|
}
|
||||||
|
table.Columns["name"] = &models.Column{
|
||||||
|
Name: "name",
|
||||||
|
Type: "varchar",
|
||||||
|
Length: 255,
|
||||||
|
Sequence: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &writers.WriterOptions{
|
||||||
|
PackageName: "models",
|
||||||
|
OutputPath: t.TempDir() + "/test.go",
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"extra_fields": `[{"name":"ThoughtCount","type":"int64","bun_tag":"thought_count,scanonly","json_tag":"thought_count"}]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := NewWriter(opts)
|
||||||
|
err := writer.WriteTable(table)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteTable failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ := os.ReadFile(opts.OutputPath)
|
||||||
|
generated := string(content)
|
||||||
|
|
||||||
|
// Verify extra field is present with separator comment
|
||||||
|
expectations := []string{
|
||||||
|
"// --- Custom fields (managed by relspecgo generator config, issue #4) ---",
|
||||||
|
"ThoughtCount int64 `bun:\"thought_count,scanonly\" json:\"thought_count\"`",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range expectations {
|
||||||
|
if !strings.Contains(generated, expected) {
|
||||||
|
t.Errorf("Generated code missing: %q\nGenerated:\n%s", expected, generated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtraFields_InMultiFile(t *testing.T) {
|
||||||
|
db := models.InitDatabase("testdb")
|
||||||
|
schema := models.InitSchema("public")
|
||||||
|
|
||||||
|
users := models.InitTable("users", "public")
|
||||||
|
users.Columns["id"] = &models.Column{
|
||||||
|
Name: "id",
|
||||||
|
Type: "bigint",
|
||||||
|
NotNull: true,
|
||||||
|
IsPrimaryKey: true,
|
||||||
|
}
|
||||||
|
schema.Tables = append(schema.Tables, users)
|
||||||
|
|
||||||
|
posts := models.InitTable("posts", "public")
|
||||||
|
posts.Columns["id"] = &models.Column{
|
||||||
|
Name: "id",
|
||||||
|
Type: "bigint",
|
||||||
|
NotNull: true,
|
||||||
|
IsPrimaryKey: true,
|
||||||
|
}
|
||||||
|
schema.Tables = append(schema.Tables, posts)
|
||||||
|
|
||||||
|
db.Schemas = append(db.Schemas, schema)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
opts := &writers.WriterOptions{
|
||||||
|
PackageName: "models",
|
||||||
|
OutputPath: tmpDir,
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"multi_file": true,
|
||||||
|
"extra_fields": `[{"target_table":"users","name":"PostCount","type":"int64","bun_tag":"post_count,scanonly","json_tag":"post_count"}]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := NewWriter(opts)
|
||||||
|
err := writer.WriteDatabase(db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteDatabase failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check users file has the extra field (matched by table name "users")
|
||||||
|
usersContent, _ := os.ReadFile(tmpDir + "/sql_public_users.go")
|
||||||
|
usersStr := string(usersContent)
|
||||||
|
|
||||||
|
if !strings.Contains(usersStr, "PostCount int64 `bun:\"post_count,scanonly\"") {
|
||||||
|
t.Errorf("Extra field not found in users file:\n%s", usersStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posts file should NOT have the extra field (name mismatch)
|
||||||
|
postsContent, _ := os.ReadFile(tmpDir + "/sql_public_posts.go")
|
||||||
|
postsStr := string(postsContent)
|
||||||
|
|
||||||
|
if strings.Contains(postsStr, "PostCount") {
|
||||||
|
t.Errorf("Extra field should not be in posts file:\n%s", postsStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTypeMapper_BuildBunTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
func TestTypeMapper_BuildBunTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||||
mapper := NewTypeMapper("")
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user