feat: add --types flag and stdlib nullable type support for bun/gorm writers
* Fix pgsql reader double-quoting defaults: normalizePostgresDefault strips surrounding SQL string literal quotes from column_default before storing, matching the convention used by every other reader. * Add NullableTypes field to WriterOptions with NullableTypeResolveSpec (default) and NullableTypeStdlib constants. * Both bun and gorm TypeMappers now accept a typeStyle parameter. stdlib mode produces sql.NullString/NullInt32/NullTime etc. for nullable scalars, plain Go slices for arrays, and time.Time for NOT NULL timestamps. Default resolvespec behaviour is unchanged. * Add --types flag to convert and split commands. * Update bun/README.md and gorm/README.md with side-by-side generated code examples, updated type mapping tables, and Writer Options documentation.
This commit is contained in:
@@ -52,6 +52,7 @@ var (
|
|||||||
convertPackageName string
|
convertPackageName string
|
||||||
convertSchemaFilter string
|
convertSchemaFilter string
|
||||||
convertFlattenSchema bool
|
convertFlattenSchema bool
|
||||||
|
convertNullableTypes string
|
||||||
)
|
)
|
||||||
|
|
||||||
var convertCmd = &cobra.Command{
|
var convertCmd = &cobra.Command{
|
||||||
@@ -175,6 +176,7 @@ func init() {
|
|||||||
convertCmd.Flags().StringVar(&convertPackageName, "package", "", "Package name (for code generation formats like gorm/bun)")
|
convertCmd.Flags().StringVar(&convertPackageName, "package", "", "Package name (for code generation formats like gorm/bun)")
|
||||||
convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single schemas)")
|
convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single 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().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): 'resolvespec' (default) or 'stdlib' (database/sql)")
|
||||||
|
|
||||||
err := convertCmd.MarkFlagRequired("from")
|
err := convertCmd.MarkFlagRequired("from")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,7 +243,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); err != nil {
|
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes); err != nil {
|
||||||
return fmt.Errorf("failed to write target: %w", err)
|
return fmt.Errorf("failed to write target: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,13 +383,14 @@ 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) error {
|
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string) error {
|
||||||
var writer writers.Writer
|
var writer writers.Writer
|
||||||
|
|
||||||
writerOpts := &writers.WriterOptions{
|
writerOpts := &writers.WriterOptions{
|
||||||
OutputPath: outputPath,
|
OutputPath: outputPath,
|
||||||
PackageName: packageName,
|
PackageName: packageName,
|
||||||
FlattenSchema: flattenSchema,
|
FlattenSchema: flattenSchema,
|
||||||
|
NullableTypes: nullableTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.ToLower(dbType) {
|
switch strings.ToLower(dbType) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var (
|
|||||||
splitDatabaseName string
|
splitDatabaseName string
|
||||||
splitExcludeSchema string
|
splitExcludeSchema string
|
||||||
splitExcludeTables string
|
splitExcludeTables string
|
||||||
|
splitNullableTypes string
|
||||||
)
|
)
|
||||||
|
|
||||||
var splitCmd = &cobra.Command{
|
var splitCmd = &cobra.Command{
|
||||||
@@ -110,6 +111,7 @@ func init() {
|
|||||||
splitCmd.Flags().StringVar(&splitTables, "tables", "", "Comma-separated list of table names to include (case-insensitive)")
|
splitCmd.Flags().StringVar(&splitTables, "tables", "", "Comma-separated list of table names to include (case-insensitive)")
|
||||||
splitCmd.Flags().StringVar(&splitExcludeSchema, "exclude-schema", "", "Comma-separated list of schema names to exclude")
|
splitCmd.Flags().StringVar(&splitExcludeSchema, "exclude-schema", "", "Comma-separated list of schema names to exclude")
|
||||||
splitCmd.Flags().StringVar(&splitExcludeTables, "exclude-tables", "", "Comma-separated list of table names to exclude (case-insensitive)")
|
splitCmd.Flags().StringVar(&splitExcludeTables, "exclude-tables", "", "Comma-separated list of table names to exclude (case-insensitive)")
|
||||||
|
splitCmd.Flags().StringVar(&splitNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'resolvespec' (default) or 'stdlib' (database/sql)")
|
||||||
|
|
||||||
err := splitCmd.MarkFlagRequired("from")
|
err := splitCmd.MarkFlagRequired("from")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -185,6 +187,7 @@ func runSplit(cmd *cobra.Command, args []string) error {
|
|||||||
splitPackageName,
|
splitPackageName,
|
||||||
"", // no schema filter for split
|
"", // no schema filter for split
|
||||||
false, // no flatten-schema for split
|
false, // no flatten-schema for split
|
||||||
|
splitNullableTypes,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write output: %w", err)
|
return fmt.Errorf("failed to write output: %w", err)
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models.
|
|||||||
column.AutoIncrement = true
|
column.AutoIncrement = true
|
||||||
column.Default = defaultVal
|
column.Default = defaultVal
|
||||||
} else {
|
} else {
|
||||||
column.Default = defaultVal
|
column.Default = normalizePostgresDefault(defaultVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,3 +613,30 @@ func (r *Reader) parseIndexDefinition(indexName, tableName, schema, indexDef str
|
|||||||
|
|
||||||
return index, nil
|
return index, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizePostgresDefault converts a raw PostgreSQL column_default expression into the
|
||||||
|
// unquoted string value that the model convention expects. PostgreSQL stores string
|
||||||
|
// literal defaults as 'value' or 'value'::type (e.g. '{}'::text[]), while every other
|
||||||
|
// reader stores the bare value so the writer can re-quote it correctly.
|
||||||
|
func normalizePostgresDefault(defaultVal string) string {
|
||||||
|
if !strings.HasPrefix(defaultVal, "'") {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
// Decode the SQL string literal: skip the leading quote, unescape '' → ', stop at
|
||||||
|
// the first unescaped closing quote (any trailing ::cast is ignored).
|
||||||
|
rest := defaultVal[1:]
|
||||||
|
var buf strings.Builder
|
||||||
|
for i := 0; i < len(rest); i++ {
|
||||||
|
if rest[i] == '\'' {
|
||||||
|
if i+1 < len(rest) && rest[i+1] == '\'' {
|
||||||
|
buf.WriteByte('\'')
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(rest[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,54 +46,67 @@ func main() {
|
|||||||
### CLI Examples
|
### CLI Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate Bun models from PostgreSQL database
|
# Generate Bun models from a DBML schema (default: resolvespec types)
|
||||||
relspec --input pgsql \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--conn "postgres://localhost/mydb" \
|
--to bun --to-path models.go --package models
|
||||||
--output bun \
|
|
||||||
--out-file models.go \
|
|
||||||
--package models
|
|
||||||
|
|
||||||
# Convert GORM models to Bun
|
# Use standard library database/sql nullable types instead of resolvespec
|
||||||
relspec --input gorm --in-file gorm_models.go --output bun --out-file bun_models.go
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
|
--to bun --to-path models.go --package models \
|
||||||
|
--types stdlib
|
||||||
|
|
||||||
# Multi-file output
|
# Explicitly select resolvespec types (same as omitting --types)
|
||||||
relspec --input json --in-file schema.json --output bun --out-file models/
|
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
||||||
|
--to bun --to-path models.go --package models \
|
||||||
|
--types resolvespec
|
||||||
|
|
||||||
|
# Multi-file output (one file per table)
|
||||||
|
relspec convert --from json --from-path schema.json \
|
||||||
|
--to bun --to-path models/ --package models
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generated Code Example
|
## Generated Code Examples
|
||||||
|
|
||||||
|
### Default — resolvespec types (`--types resolvespec`)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
"database/sql"
|
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
bun.BaseModel `bun:"table:users,alias:u"`
|
bun.BaseModel `bun:"table:users,alias:u"`
|
||||||
|
|
||||||
ID int64 `bun:"id,pk,autoincrement" json:"id"`
|
ID int64 `bun:"id,type:uuid,pk," json:"id"`
|
||||||
Username string `bun:"username,notnull,unique" json:"username"`
|
Username string `bun:"username,type:text,notnull," json:"username"`
|
||||||
Email string `bun:"email,notnull" json:"email"`
|
Email resolvespec_common.SqlString `bun:"email,type:text,nullzero," json:"email"`
|
||||||
Bio sql.NullString `bun:"bio" json:"bio,omitempty"`
|
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
CreatedAt time.Time `bun:"created_at,notnull,default:now()" json:"created_at"`
|
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
|
|
||||||
// Relationships
|
|
||||||
Posts []*Post `bun:"rel:has-many,join:id=user_id" json:"posts,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
type Post struct {
|
### Standard library — `--types stdlib`
|
||||||
bun.BaseModel `bun:"table:posts,alias:p"`
|
|
||||||
|
|
||||||
ID int64 `bun:"id,pk" json:"id"`
|
```go
|
||||||
UserID int64 `bun:"user_id,notnull" json:"user_id"`
|
package models
|
||||||
Title string `bun:"title,notnull" json:"title"`
|
|
||||||
Content sql.NullString `bun:"content" json:"content,omitempty"`
|
|
||||||
|
|
||||||
// Belongs to
|
import (
|
||||||
User *User `bun:"rel:belongs-to,join:user_id=id" json:"user,omitempty"`
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
bun.BaseModel `bun:"table:users,alias:u"`
|
||||||
|
|
||||||
|
ID string `bun:"id,type:uuid,pk," json:"id"`
|
||||||
|
Username string `bun:"username,type:text,notnull," json:"username"`
|
||||||
|
Email sql.NullString `bun:"email,type:text,nullzero," json:"email"`
|
||||||
|
Tags []string `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
|
CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,19 +124,68 @@ type Post struct {
|
|||||||
|
|
||||||
## Type Mapping
|
## Type Mapping
|
||||||
|
|
||||||
| SQL Type | Go Type | Nullable Type |
|
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
||||||
|----------|---------|---------------|
|
|
||||||
| bigint | int64 | sql.NullInt64 |
|
| SQL Type | NOT NULL (both) | Nullable — resolvespec | Nullable — stdlib |
|
||||||
| integer | int | sql.NullInt32 |
|
|---|---|---|---|
|
||||||
| varchar, text | string | sql.NullString |
|
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
||||||
| boolean | bool | sql.NullBool |
|
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
||||||
| timestamp | time.Time | sql.NullTime |
|
| `smallint` | `int16` | `SqlInt16` | `sql.NullInt16` |
|
||||||
| numeric | float64 | sql.NullFloat64 |
|
| `text`, `varchar` | `string` | `SqlString` | `sql.NullString` |
|
||||||
|
| `boolean` | `bool` | `SqlBool` | `sql.NullBool` |
|
||||||
|
| `timestamp`, `timestamptz` | `time.Time`* | `SqlTimeStamp` | `sql.NullTime` |
|
||||||
|
| `numeric`, `decimal` | `float64` | `SqlFloat64` | `sql.NullFloat64` |
|
||||||
|
| `uuid` | `string` | `SqlUUID` | `sql.NullString` |
|
||||||
|
| `jsonb` | `string` | `SqlJSONB` | `sql.NullString` |
|
||||||
|
| `text[]` | `SqlStringArray` | `SqlStringArray` | `[]string` |
|
||||||
|
| `integer[]` | `SqlInt32Array` | `SqlInt32Array` | `[]int32` |
|
||||||
|
| `uuid[]` | `SqlUUIDArray` | `SqlUUIDArray` | `[]string` |
|
||||||
|
| `vector` | `SqlVector` | `SqlVector` | `[]float32` |
|
||||||
|
|
||||||
|
\* In resolvespec mode, NOT NULL timestamps use `SqlTimeStamp` (not `time.Time`) unless the base type is a simple integer or boolean. In stdlib mode, NOT NULL timestamps use `time.Time`.
|
||||||
|
|
||||||
|
## Writer Options
|
||||||
|
|
||||||
|
### NullableTypes
|
||||||
|
|
||||||
|
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use resolvespec types (default — omit NullableTypes or set to "resolvespec")
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
NullableTypes: writers.NullableTypeResolveSpec,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standard library database/sql types
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
NullableTypes: writers.NullableTypeStdlib,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"multi_file": true, // Enable multi-file mode
|
||||||
|
"populate_refs": true, // Populate RefDatabase/RefSchema
|
||||||
|
"generate_get_id_str": true, // Generate GetIDStr() methods
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Model names are derived from table names (singularized, PascalCase)
|
- Model names are derived from table names (singularized, PascalCase)
|
||||||
- Table aliases are auto-generated from table names
|
- Table aliases are auto-generated from table names
|
||||||
|
- Nullable columns use `resolvespec_common.SqlString`, `resolvespec_common.SqlTimeStamp`, etc. by default; pass `--types stdlib` to use `sql.NullString`, `sql.NullTime`, etc. instead
|
||||||
|
- Array columns use `resolvespec_common.SqlStringArray`, `resolvespec_common.SqlInt32Array`, etc. by default; `--types stdlib` produces plain Go slices (`[]string`, `[]int32`, …)
|
||||||
- Multi-file mode: one file per table named `sql_{schema}_{table}.go`
|
- Multi-file mode: one file per table named `sql_{schema}_{table}.go`
|
||||||
- Generated code is auto-formatted
|
- Generated code is auto-formatted
|
||||||
- JSON tags are automatically added
|
- JSON tags are automatically added
|
||||||
|
|||||||
@@ -11,30 +11,43 @@ import (
|
|||||||
|
|
||||||
// TypeMapper handles type conversions between SQL and Go types for Bun
|
// TypeMapper handles type conversions between SQL and Go types for Bun
|
||||||
type TypeMapper struct {
|
type TypeMapper struct {
|
||||||
// Package alias for sql_types import
|
|
||||||
sqlTypesAlias string
|
sqlTypesAlias string
|
||||||
|
typeStyle string // writers.NullableTypeResolveSpec | writers.NullableTypeStdlib
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTypeMapper creates a new TypeMapper with default settings
|
// NewTypeMapper creates a new TypeMapper.
|
||||||
func NewTypeMapper() *TypeMapper {
|
// typeStyle should be writers.NullableTypeResolveSpec or writers.NullableTypeStdlib;
|
||||||
|
// an empty string defaults to resolvespec.
|
||||||
|
func NewTypeMapper(typeStyle string) *TypeMapper {
|
||||||
|
if typeStyle == "" {
|
||||||
|
typeStyle = writers.NullableTypeResolveSpec
|
||||||
|
}
|
||||||
return &TypeMapper{
|
return &TypeMapper{
|
||||||
sqlTypesAlias: "resolvespec_common",
|
sqlTypesAlias: "resolvespec_common",
|
||||||
|
typeStyle: typeStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQLTypeToGoType converts a SQL type to its Go equivalent
|
// SQLTypeToGoType converts a SQL type to its Go equivalent.
|
||||||
// Uses ResolveSpec common package types (all are nullable by default in Bun)
|
|
||||||
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||||
// Normalize SQL type (lowercase, remove length/precision)
|
// Array types are handled separately for both styles.
|
||||||
|
if pgsql.IsArrayType(sqlType) {
|
||||||
|
return tm.arrayGoType(tm.extractBaseType(sqlType))
|
||||||
|
}
|
||||||
|
|
||||||
baseType := tm.extractBaseType(sqlType)
|
baseType := tm.extractBaseType(sqlType)
|
||||||
|
|
||||||
// For Bun, we typically use resolvespec_common types for most fields
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
// unless they're explicitly NOT NULL and we want to avoid null handling
|
if notNull {
|
||||||
|
return tm.rawGoType(baseType)
|
||||||
|
}
|
||||||
|
return tm.stdlibNullableGoType(baseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvespec (default): use base Go types only for simple NOT NULL fields.
|
||||||
if notNull && tm.isSimpleType(baseType) {
|
if notNull && tm.isSimpleType(baseType) {
|
||||||
return tm.baseGoType(baseType)
|
return tm.baseGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use resolvespec_common types for nullable fields
|
|
||||||
return tm.bunGoType(baseType)
|
return tm.bunGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +167,9 @@ func (tm *TypeMapper) bunGoType(sqlType string) string {
|
|||||||
|
|
||||||
// Other
|
// Other
|
||||||
"money": tm.sqlTypesAlias + ".SqlFloat64",
|
"money": tm.sqlTypesAlias + ".SqlFloat64",
|
||||||
|
|
||||||
|
// pgvector
|
||||||
|
"vector": tm.sqlTypesAlias + ".SqlVector",
|
||||||
}
|
}
|
||||||
|
|
||||||
if goType, ok := typeMap[sqlType]; ok {
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
@@ -164,6 +180,123 @@ func (tm *TypeMapper) bunGoType(sqlType string) string {
|
|||||||
return tm.sqlTypesAlias + ".SqlString"
|
return tm.sqlTypesAlias + ".SqlString"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arrayGoType returns the Go type for a PostgreSQL array column.
|
||||||
|
// The baseElemType is the canonical base type (e.g. "text", "integer").
|
||||||
|
func (tm *TypeMapper) arrayGoType(baseElemType string) string {
|
||||||
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
return tm.stdlibArrayGoType(baseElemType)
|
||||||
|
}
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"text": tm.sqlTypesAlias + ".SqlStringArray", "varchar": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"char": tm.sqlTypesAlias + ".SqlStringArray", "character": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"citext": tm.sqlTypesAlias + ".SqlStringArray", "bpchar": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"inet": tm.sqlTypesAlias + ".SqlStringArray", "cidr": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"macaddr": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"json": tm.sqlTypesAlias + ".SqlStringArray", "jsonb": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"integer": tm.sqlTypesAlias + ".SqlInt32Array", "int": tm.sqlTypesAlias + ".SqlInt32Array",
|
||||||
|
"int4": tm.sqlTypesAlias + ".SqlInt32Array", "serial": tm.sqlTypesAlias + ".SqlInt32Array",
|
||||||
|
"smallint": tm.sqlTypesAlias + ".SqlInt16Array", "int2": tm.sqlTypesAlias + ".SqlInt16Array",
|
||||||
|
"smallserial": tm.sqlTypesAlias + ".SqlInt16Array",
|
||||||
|
"bigint": tm.sqlTypesAlias + ".SqlInt64Array", "int8": tm.sqlTypesAlias + ".SqlInt64Array",
|
||||||
|
"bigserial": tm.sqlTypesAlias + ".SqlInt64Array",
|
||||||
|
"real": tm.sqlTypesAlias + ".SqlFloat32Array", "float4": tm.sqlTypesAlias + ".SqlFloat32Array",
|
||||||
|
"double precision": tm.sqlTypesAlias + ".SqlFloat64Array", "float8": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"numeric": tm.sqlTypesAlias + ".SqlFloat64Array", "decimal": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"money": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"boolean": tm.sqlTypesAlias + ".SqlBoolArray", "bool": tm.sqlTypesAlias + ".SqlBoolArray",
|
||||||
|
"uuid": tm.sqlTypesAlias + ".SqlUUIDArray",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[baseElemType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return tm.sqlTypesAlias + ".SqlStringArray"
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawGoType returns the plain Go type for a NOT NULL column in stdlib mode.
|
||||||
|
func (tm *TypeMapper) rawGoType(sqlType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"integer": "int32", "int": "int32", "int4": "int32", "serial": "int32",
|
||||||
|
"smallint": "int16", "int2": "int16", "smallserial": "int16",
|
||||||
|
"bigint": "int64", "int8": "int64", "bigserial": "int64",
|
||||||
|
"boolean": "bool", "bool": "bool",
|
||||||
|
"real": "float32", "float4": "float32",
|
||||||
|
"double precision": "float64", "float8": "float64",
|
||||||
|
"numeric": "float64", "decimal": "float64", "money": "float64",
|
||||||
|
"text": "string", "varchar": "string", "char": "string",
|
||||||
|
"character": "string", "citext": "string", "bpchar": "string",
|
||||||
|
"inet": "string", "cidr": "string", "macaddr": "string",
|
||||||
|
"uuid": "string", "json": "string", "jsonb": "string",
|
||||||
|
"timestamp": "time.Time",
|
||||||
|
"timestamp without time zone": "time.Time",
|
||||||
|
"timestamp with time zone": "time.Time",
|
||||||
|
"timestamptz": "time.Time",
|
||||||
|
"date": "time.Time",
|
||||||
|
"time": "time.Time",
|
||||||
|
"time without time zone": "time.Time",
|
||||||
|
"time with time zone": "time.Time",
|
||||||
|
"timetz": "time.Time",
|
||||||
|
"bytea": "[]byte",
|
||||||
|
"vector": "[]float32",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdlibNullableGoType returns the database/sql nullable type for a column.
|
||||||
|
func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"integer": "sql.NullInt32", "int": "sql.NullInt32", "int4": "sql.NullInt32", "serial": "sql.NullInt32",
|
||||||
|
"smallint": "sql.NullInt16", "int2": "sql.NullInt16", "smallserial": "sql.NullInt16",
|
||||||
|
"bigint": "sql.NullInt64", "int8": "sql.NullInt64", "bigserial": "sql.NullInt64",
|
||||||
|
"boolean": "sql.NullBool", "bool": "sql.NullBool",
|
||||||
|
"real": "sql.NullFloat64", "float4": "sql.NullFloat64",
|
||||||
|
"double precision": "sql.NullFloat64", "float8": "sql.NullFloat64",
|
||||||
|
"numeric": "sql.NullFloat64", "decimal": "sql.NullFloat64", "money": "sql.NullFloat64",
|
||||||
|
"text": "sql.NullString", "varchar": "sql.NullString", "char": "sql.NullString",
|
||||||
|
"character": "sql.NullString", "citext": "sql.NullString", "bpchar": "sql.NullString",
|
||||||
|
"inet": "sql.NullString", "cidr": "sql.NullString", "macaddr": "sql.NullString",
|
||||||
|
"uuid": "sql.NullString", "json": "sql.NullString", "jsonb": "sql.NullString",
|
||||||
|
"timestamp": "sql.NullTime",
|
||||||
|
"timestamp without time zone": "sql.NullTime",
|
||||||
|
"timestamp with time zone": "sql.NullTime",
|
||||||
|
"timestamptz": "sql.NullTime",
|
||||||
|
"date": "sql.NullTime",
|
||||||
|
"time": "sql.NullTime",
|
||||||
|
"time without time zone": "sql.NullTime",
|
||||||
|
"time with time zone": "sql.NullTime",
|
||||||
|
"timetz": "sql.NullTime",
|
||||||
|
"bytea": "[]byte",
|
||||||
|
"vector": "[]float32",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "sql.NullString"
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode.
|
||||||
|
func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"text": "[]string", "varchar": "[]string", "char": "[]string",
|
||||||
|
"character": "[]string", "citext": "[]string", "bpchar": "[]string",
|
||||||
|
"inet": "[]string", "cidr": "[]string", "macaddr": "[]string",
|
||||||
|
"uuid": "[]string", "json": "[]string", "jsonb": "[]string",
|
||||||
|
"integer": "[]int32", "int": "[]int32", "int4": "[]int32", "serial": "[]int32",
|
||||||
|
"smallint": "[]int16", "int2": "[]int16", "smallserial": "[]int16",
|
||||||
|
"bigint": "[]int64", "int8": "[]int64", "bigserial": "[]int64",
|
||||||
|
"real": "[]float32", "float4": "[]float32",
|
||||||
|
"double precision": "[]float64", "float8": "[]float64",
|
||||||
|
"numeric": "[]float64", "decimal": "[]float64", "money": "[]float64",
|
||||||
|
"boolean": "[]bool", "bool": "[]bool",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[baseElemType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "[]string"
|
||||||
|
}
|
||||||
|
|
||||||
// BuildBunTag generates a complete Bun tag string for a column
|
// BuildBunTag generates a complete Bun tag string for a column
|
||||||
// Bun format: bun:"column_name,type:type_name,pk,default:value"
|
// Bun format: bun:"column_name,type:type_name,pk,default:value"
|
||||||
func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) string {
|
func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) string {
|
||||||
@@ -286,11 +419,20 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool {
|
|||||||
return generateGetIDStr
|
return generateGetIDStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSQLTypesImport returns the import path for sql_types (ResolveSpec common)
|
// GetSQLTypesImport returns the import path for the ResolveSpec spectypes package.
|
||||||
func (tm *TypeMapper) GetSQLTypesImport() string {
|
func (tm *TypeMapper) GetSQLTypesImport() string {
|
||||||
return "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
return "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
||||||
|
// package (ready to pass to AddImport). Returns empty string when no import is needed.
|
||||||
|
func (tm *TypeMapper) GetNullableTypeImportLine() string {
|
||||||
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
return "\"database/sql\""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport())
|
||||||
|
}
|
||||||
|
|
||||||
// GetBunImport returns the import path for Bun
|
// GetBunImport returns the import path for Bun
|
||||||
func (tm *TypeMapper) GetBunImport() string {
|
func (tm *TypeMapper) GetBunImport() string {
|
||||||
return "github.com/uptrace/bun"
|
return "github.com/uptrace/bun"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Writer struct {
|
|||||||
func NewWriter(options *writers.WriterOptions) *Writer {
|
func NewWriter(options *writers.WriterOptions) *Writer {
|
||||||
w := &Writer{
|
w := &Writer{
|
||||||
options: options,
|
options: options,
|
||||||
typeMapper: NewTypeMapper(),
|
typeMapper: NewTypeMapper(options.NullableTypes),
|
||||||
config: LoadMethodConfigFromMetadata(options.Metadata),
|
config: LoadMethodConfigFromMetadata(options.Metadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +80,8 @@ func (w *Writer) writeSingleFile(db *models.Database) error {
|
|||||||
// Add bun import (always needed)
|
// Add bun import (always needed)
|
||||||
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
||||||
|
|
||||||
// Add resolvespec_common import (always needed for nullable types)
|
// Add nullable types import (resolvespec or stdlib depending on options)
|
||||||
templateData.AddImport(fmt.Sprintf("resolvespec_common \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Collect all models
|
// Collect all models
|
||||||
for _, schema := range db.Schemas {
|
for _, schema := range db.Schemas {
|
||||||
@@ -177,8 +177,8 @@ func (w *Writer) writeMultiFile(db *models.Database) error {
|
|||||||
// Add bun import
|
// Add bun import
|
||||||
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
templateData.AddImport(fmt.Sprintf("\"%s\"", w.typeMapper.GetBunImport()))
|
||||||
|
|
||||||
// Add resolvespec_common import
|
// Add nullable types import (resolvespec or stdlib depending on options)
|
||||||
templateData.AddImport(fmt.Sprintf("resolvespec_common \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Create model data
|
// Create model data
|
||||||
modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema)
|
modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema)
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ func TestWriter_FieldNameCollision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
||||||
mapper := NewTypeMapper()
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sqlType string
|
sqlType string
|
||||||
@@ -587,7 +587,7 @@ func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTypeMapper_BuildBunTag(t *testing.T) {
|
func TestTypeMapper_BuildBunTag(t *testing.T) {
|
||||||
mapper := NewTypeMapper()
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -700,7 +700,7 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTypeMapper_BuildBunTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
func TestTypeMapper_BuildBunTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||||
mapper := NewTypeMapper()
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
col := &models.Column{
|
col := &models.Column{
|
||||||
Name: "embedding",
|
Name: "embedding",
|
||||||
|
|||||||
@@ -48,22 +48,23 @@ func main() {
|
|||||||
### CLI Examples
|
### CLI Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate GORM models from PostgreSQL database (single file)
|
# Generate GORM models from a DBML schema (default: resolvespec types)
|
||||||
relspec --input pgsql \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--conn "postgres://localhost/mydb" \
|
--to gorm --to-path models.go --package models
|
||||||
--output gorm \
|
|
||||||
--out-file models.go \
|
|
||||||
--package models
|
|
||||||
|
|
||||||
# Generate GORM models with multi-file output (one file per table)
|
# Use standard library database/sql nullable types instead of resolvespec
|
||||||
relspec --input json \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--in-file schema.json \
|
--to gorm --to-path models.go --package models \
|
||||||
--output gorm \
|
--types stdlib
|
||||||
--out-file models/ \
|
|
||||||
--package models
|
|
||||||
|
|
||||||
# Convert DBML to GORM models
|
# Explicitly select resolvespec types (same as omitting --types)
|
||||||
relspec --input dbml --in-file schema.dbml --output gorm --out-file models.go
|
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
||||||
|
--to gorm --to-path models.go --package models \
|
||||||
|
--types resolvespec
|
||||||
|
|
||||||
|
# Multi-file output (one file per table)
|
||||||
|
relspec convert --from json --from-path schema.json \
|
||||||
|
--to gorm --to-path models/ --package models
|
||||||
```
|
```
|
||||||
|
|
||||||
## Output Modes
|
## Output Modes
|
||||||
@@ -86,58 +87,86 @@ relspec --input pgsql --conn "..." --output gorm --out-file models/
|
|||||||
|
|
||||||
Files are named: `sql_{schema}_{table}.go`
|
Files are named: `sql_{schema}_{table}.go`
|
||||||
|
|
||||||
## Generated Code Example
|
## Generated Code Examples
|
||||||
|
|
||||||
|
### Default — resolvespec types (`--types resolvespec`)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
sql_types "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
sql_types "git.warky.dev/wdevs/sql_types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelUser struct {
|
type ModelUser struct {
|
||||||
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement" json:"id"`
|
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
|
||||||
Username string `gorm:"column:username;type:varchar(50);not null;uniqueIndex" json:"username"`
|
Username string `gorm:"column:username;type:text;not null" json:"username"`
|
||||||
Email string `gorm:"column:email;type:varchar(100);not null" json:"email"`
|
Email sql_types.SqlString `gorm:"column:email;type:text" json:"email,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:now()" json:"created_at"`
|
Tags sql_types.SqlStringArray `gorm:"column:tags;type:text[];not null;default:'{}'" json:"tags"`
|
||||||
|
CreatedAt sql_types.SqlTimeStamp `gorm:"column:created_at;type:timestamptz;not null;default:now()" json:"created_at"`
|
||||||
// Relationships
|
|
||||||
Pos []*ModelPost `gorm:"foreignKey:UserID;references:ID;constraint:OnDelete:CASCADE" json:"pos,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ModelUser) TableName() string {
|
func (ModelUser) TableName() string {
|
||||||
return "public.users"
|
return "public.users"
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
type ModelPost struct {
|
### Standard library — `--types stdlib`
|
||||||
ID int64 `gorm:"column:id;type:bigint;primaryKey" json:"id"`
|
|
||||||
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
|
|
||||||
Title string `gorm:"column:title;type:varchar(200);not null" json:"title"`
|
|
||||||
Content sql_types.SqlString `gorm:"column:content;type:text" json:"content,omitempty"`
|
|
||||||
|
|
||||||
// Belongs to
|
```go
|
||||||
Use *ModelUser `gorm:"foreignKey:UserID;references:ID" json:"use,omitempty"`
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelUser struct {
|
||||||
|
ID string `gorm:"column:id;type:uuid;primaryKey" json:"id"`
|
||||||
|
Username string `gorm:"column:username;type:text;not null" json:"username"`
|
||||||
|
Email sql.NullString `gorm:"column:email;type:text" json:"email,omitempty"`
|
||||||
|
Tags []string `gorm:"column:tags;type:text[];not null;default:'{}'" json:"tags"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;default:now()" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ModelPost) TableName() string {
|
func (ModelUser) TableName() string {
|
||||||
return "public.posts"
|
return "public.users"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Writer Options
|
## Writer Options
|
||||||
|
|
||||||
|
### NullableTypes
|
||||||
|
|
||||||
|
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use resolvespec types (default — omit NullableTypes or set to "resolvespec")
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
NullableTypes: writers.NullableTypeResolveSpec,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standard library database/sql types
|
||||||
|
options := &writers.WriterOptions{
|
||||||
|
OutputPath: "models.go",
|
||||||
|
PackageName: "models",
|
||||||
|
NullableTypes: writers.NullableTypeStdlib,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Metadata Options
|
### Metadata Options
|
||||||
|
|
||||||
Configure the writer behavior using metadata in `WriterOptions`:
|
Configure additional writer behavior using metadata in `WriterOptions`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
options := &writers.WriterOptions{
|
options := &writers.WriterOptions{
|
||||||
OutputPath: "models.go",
|
OutputPath: "models.go",
|
||||||
PackageName: "models",
|
PackageName: "models",
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]any{
|
||||||
"multi_file": true, // Enable multi-file mode
|
"multi_file": true, // Enable multi-file mode
|
||||||
"populate_refs": true, // Populate RefDatabase/RefSchema
|
"populate_refs": true, // Populate RefDatabase/RefSchema
|
||||||
"generate_get_id_str": true, // Generate GetIDStr() methods
|
"generate_get_id_str": true, // Generate GetIDStr() methods
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -145,18 +174,23 @@ options := &writers.WriterOptions{
|
|||||||
|
|
||||||
## Type Mapping
|
## Type Mapping
|
||||||
|
|
||||||
| SQL Type | Go Type | Notes |
|
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
||||||
|----------|---------|-------|
|
|
||||||
| bigint, int8 | int64 | - |
|
| SQL Type | NOT NULL — both | Nullable — resolvespec | Nullable — stdlib |
|
||||||
| integer, int, int4 | int | - |
|
|---|---|---|---|
|
||||||
| smallint, int2 | int16 | - |
|
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
||||||
| varchar, text | string | Not nullable |
|
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
||||||
| varchar, text (nullable) | sql_types.SqlString | Nullable |
|
| `smallint` | `int16` | `SqlInt16` | `sql.NullInt16` |
|
||||||
| boolean, bool | bool | - |
|
| `text`, `varchar` | `string` | `SqlString` | `sql.NullString` |
|
||||||
| timestamp, timestamptz | time.Time | - |
|
| `boolean` | `bool` | `SqlBool` | `sql.NullBool` |
|
||||||
| numeric, decimal | float64 | - |
|
| `timestamp`, `timestamptz` | `time.Time` | `SqlTimeStamp` | `sql.NullTime` |
|
||||||
| uuid | string | - |
|
| `numeric`, `decimal` | `float64` | `SqlFloat64` | `sql.NullFloat64` |
|
||||||
| json, jsonb | string | - |
|
| `uuid` | `string` | `SqlUUID` | `sql.NullString` |
|
||||||
|
| `jsonb` | `string` | `SqlString` | `sql.NullString` |
|
||||||
|
| `text[]` | `SqlStringArray` | `SqlStringArray` | `[]string` |
|
||||||
|
| `integer[]` | `SqlInt32Array` | `SqlInt32Array` | `[]int32` |
|
||||||
|
| `uuid[]` | `SqlUUIDArray` | `SqlUUIDArray` | `[]string` |
|
||||||
|
| `vector` | `SqlVector` | `SqlVector` | `[]float32` |
|
||||||
|
|
||||||
## Relationship Generation
|
## Relationship Generation
|
||||||
|
|
||||||
@@ -170,7 +204,8 @@ The writer automatically generates relationship fields:
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Model names are prefixed with "Model" (e.g., `ModelUser`)
|
- Model names are prefixed with "Model" (e.g., `ModelUser`)
|
||||||
- Nullable columns use `sql_types.SqlString`, `sql_types.SqlInt64`, etc.
|
- Nullable columns use `sql_types.SqlString`, `sql_types.SqlInt64`, etc. by default; pass `--types stdlib` to use `sql.NullString`, `sql.NullInt64`, etc. instead
|
||||||
|
- Array columns use `sql_types.SqlStringArray`, `sql_types.SqlInt32Array`, etc. by default; `--types stdlib` produces plain Go slices (`[]string`, `[]int32`, …)
|
||||||
- Generated code is auto-formatted with `go fmt`
|
- Generated code is auto-formatted with `go fmt`
|
||||||
- JSON tags are automatically added
|
- JSON tags are automatically added
|
||||||
- Supports schema-qualified table names in `TableName()` method
|
- Supports schema-qualified table names in `TableName()` method
|
||||||
|
|||||||
@@ -11,29 +11,43 @@ import (
|
|||||||
|
|
||||||
// TypeMapper handles type conversions between SQL and Go types
|
// TypeMapper handles type conversions between SQL and Go types
|
||||||
type TypeMapper struct {
|
type TypeMapper struct {
|
||||||
// Package alias for sql_types import
|
|
||||||
sqlTypesAlias string
|
sqlTypesAlias string
|
||||||
|
typeStyle string // writers.NullableTypeResolveSpec | writers.NullableTypeStdlib
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTypeMapper creates a new TypeMapper with default settings
|
// NewTypeMapper creates a new TypeMapper.
|
||||||
func NewTypeMapper() *TypeMapper {
|
// typeStyle should be writers.NullableTypeResolveSpec or writers.NullableTypeStdlib;
|
||||||
|
// an empty string defaults to resolvespec.
|
||||||
|
func NewTypeMapper(typeStyle string) *TypeMapper {
|
||||||
|
if typeStyle == "" {
|
||||||
|
typeStyle = writers.NullableTypeResolveSpec
|
||||||
|
}
|
||||||
return &TypeMapper{
|
return &TypeMapper{
|
||||||
sqlTypesAlias: "sql_types",
|
sqlTypesAlias: "sql_types",
|
||||||
|
typeStyle: typeStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQLTypeToGoType converts a SQL type to its Go equivalent
|
// SQLTypeToGoType converts a SQL type to its Go equivalent.
|
||||||
// Handles nullable types using ResolveSpec sql_types package
|
|
||||||
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||||
// Normalize SQL type (lowercase, remove length/precision)
|
// Array types are handled separately for both styles.
|
||||||
|
if pgsql.IsArrayType(sqlType) {
|
||||||
|
return tm.arrayGoType(tm.extractBaseType(sqlType))
|
||||||
|
}
|
||||||
|
|
||||||
baseType := tm.extractBaseType(sqlType)
|
baseType := tm.extractBaseType(sqlType)
|
||||||
|
|
||||||
// If not null, use base Go types
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
if notNull {
|
||||||
|
return tm.rawGoType(baseType)
|
||||||
|
}
|
||||||
|
return tm.stdlibNullableGoType(baseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvespec (default)
|
||||||
if notNull {
|
if notNull {
|
||||||
return tm.baseGoType(baseType)
|
return tm.baseGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For nullable fields, use sql_types
|
|
||||||
return tm.nullableGoType(baseType)
|
return tm.nullableGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +120,9 @@ func (tm *TypeMapper) baseGoType(sqlType string) string {
|
|||||||
|
|
||||||
// Other
|
// Other
|
||||||
"money": "float64",
|
"money": "float64",
|
||||||
|
|
||||||
|
// pgvector — always uses SqlVector even when NOT NULL
|
||||||
|
"vector": tm.sqlTypesAlias + ".SqlVector",
|
||||||
}
|
}
|
||||||
|
|
||||||
if goType, ok := typeMap[sqlType]; ok {
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
@@ -179,6 +196,9 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string {
|
|||||||
|
|
||||||
// Other
|
// Other
|
||||||
"money": tm.sqlTypesAlias + ".SqlFloat64",
|
"money": tm.sqlTypesAlias + ".SqlFloat64",
|
||||||
|
|
||||||
|
// pgvector
|
||||||
|
"vector": tm.sqlTypesAlias + ".SqlVector",
|
||||||
}
|
}
|
||||||
|
|
||||||
if goType, ok := typeMap[sqlType]; ok {
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
@@ -189,6 +209,123 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string {
|
|||||||
return tm.sqlTypesAlias + ".SqlString"
|
return tm.sqlTypesAlias + ".SqlString"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arrayGoType returns the Go type for a PostgreSQL array column.
|
||||||
|
// The baseElemType is the canonical base type (e.g. "text", "integer").
|
||||||
|
func (tm *TypeMapper) arrayGoType(baseElemType string) string {
|
||||||
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
return tm.stdlibArrayGoType(baseElemType)
|
||||||
|
}
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"text": tm.sqlTypesAlias + ".SqlStringArray", "varchar": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"char": tm.sqlTypesAlias + ".SqlStringArray", "character": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"citext": tm.sqlTypesAlias + ".SqlStringArray", "bpchar": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"inet": tm.sqlTypesAlias + ".SqlStringArray", "cidr": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"macaddr": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"json": tm.sqlTypesAlias + ".SqlStringArray", "jsonb": tm.sqlTypesAlias + ".SqlStringArray",
|
||||||
|
"integer": tm.sqlTypesAlias + ".SqlInt32Array", "int": tm.sqlTypesAlias + ".SqlInt32Array",
|
||||||
|
"int4": tm.sqlTypesAlias + ".SqlInt32Array", "serial": tm.sqlTypesAlias + ".SqlInt32Array",
|
||||||
|
"smallint": tm.sqlTypesAlias + ".SqlInt16Array", "int2": tm.sqlTypesAlias + ".SqlInt16Array",
|
||||||
|
"smallserial": tm.sqlTypesAlias + ".SqlInt16Array",
|
||||||
|
"bigint": tm.sqlTypesAlias + ".SqlInt64Array", "int8": tm.sqlTypesAlias + ".SqlInt64Array",
|
||||||
|
"bigserial": tm.sqlTypesAlias + ".SqlInt64Array",
|
||||||
|
"real": tm.sqlTypesAlias + ".SqlFloat32Array", "float4": tm.sqlTypesAlias + ".SqlFloat32Array",
|
||||||
|
"double precision": tm.sqlTypesAlias + ".SqlFloat64Array", "float8": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"numeric": tm.sqlTypesAlias + ".SqlFloat64Array", "decimal": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"money": tm.sqlTypesAlias + ".SqlFloat64Array",
|
||||||
|
"boolean": tm.sqlTypesAlias + ".SqlBoolArray", "bool": tm.sqlTypesAlias + ".SqlBoolArray",
|
||||||
|
"uuid": tm.sqlTypesAlias + ".SqlUUIDArray",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[baseElemType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return tm.sqlTypesAlias + ".SqlStringArray"
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawGoType returns the plain Go type for a NOT NULL column in stdlib mode.
|
||||||
|
func (tm *TypeMapper) rawGoType(sqlType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"integer": "int32", "int": "int32", "int4": "int32", "serial": "int32",
|
||||||
|
"smallint": "int16", "int2": "int16", "smallserial": "int16",
|
||||||
|
"bigint": "int64", "int8": "int64", "bigserial": "int64",
|
||||||
|
"boolean": "bool", "bool": "bool",
|
||||||
|
"real": "float32", "float4": "float32",
|
||||||
|
"double precision": "float64", "float8": "float64",
|
||||||
|
"numeric": "float64", "decimal": "float64", "money": "float64",
|
||||||
|
"text": "string", "varchar": "string", "char": "string",
|
||||||
|
"character": "string", "citext": "string", "bpchar": "string",
|
||||||
|
"inet": "string", "cidr": "string", "macaddr": "string",
|
||||||
|
"uuid": "string", "json": "string", "jsonb": "string",
|
||||||
|
"timestamp": "time.Time",
|
||||||
|
"timestamp without time zone": "time.Time",
|
||||||
|
"timestamp with time zone": "time.Time",
|
||||||
|
"timestamptz": "time.Time",
|
||||||
|
"date": "time.Time",
|
||||||
|
"time": "time.Time",
|
||||||
|
"time without time zone": "time.Time",
|
||||||
|
"time with time zone": "time.Time",
|
||||||
|
"timetz": "time.Time",
|
||||||
|
"bytea": "[]byte",
|
||||||
|
"vector": "[]float32",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdlibNullableGoType returns the database/sql nullable type for a column.
|
||||||
|
func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"integer": "sql.NullInt32", "int": "sql.NullInt32", "int4": "sql.NullInt32", "serial": "sql.NullInt32",
|
||||||
|
"smallint": "sql.NullInt16", "int2": "sql.NullInt16", "smallserial": "sql.NullInt16",
|
||||||
|
"bigint": "sql.NullInt64", "int8": "sql.NullInt64", "bigserial": "sql.NullInt64",
|
||||||
|
"boolean": "sql.NullBool", "bool": "sql.NullBool",
|
||||||
|
"real": "sql.NullFloat64", "float4": "sql.NullFloat64",
|
||||||
|
"double precision": "sql.NullFloat64", "float8": "sql.NullFloat64",
|
||||||
|
"numeric": "sql.NullFloat64", "decimal": "sql.NullFloat64", "money": "sql.NullFloat64",
|
||||||
|
"text": "sql.NullString", "varchar": "sql.NullString", "char": "sql.NullString",
|
||||||
|
"character": "sql.NullString", "citext": "sql.NullString", "bpchar": "sql.NullString",
|
||||||
|
"inet": "sql.NullString", "cidr": "sql.NullString", "macaddr": "sql.NullString",
|
||||||
|
"uuid": "sql.NullString", "json": "sql.NullString", "jsonb": "sql.NullString",
|
||||||
|
"timestamp": "sql.NullTime",
|
||||||
|
"timestamp without time zone": "sql.NullTime",
|
||||||
|
"timestamp with time zone": "sql.NullTime",
|
||||||
|
"timestamptz": "sql.NullTime",
|
||||||
|
"date": "sql.NullTime",
|
||||||
|
"time": "sql.NullTime",
|
||||||
|
"time without time zone": "sql.NullTime",
|
||||||
|
"time with time zone": "sql.NullTime",
|
||||||
|
"timetz": "sql.NullTime",
|
||||||
|
"bytea": "[]byte",
|
||||||
|
"vector": "[]float32",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[sqlType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "sql.NullString"
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode.
|
||||||
|
func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string {
|
||||||
|
typeMap := map[string]string{
|
||||||
|
"text": "[]string", "varchar": "[]string", "char": "[]string",
|
||||||
|
"character": "[]string", "citext": "[]string", "bpchar": "[]string",
|
||||||
|
"inet": "[]string", "cidr": "[]string", "macaddr": "[]string",
|
||||||
|
"uuid": "[]string", "json": "[]string", "jsonb": "[]string",
|
||||||
|
"integer": "[]int32", "int": "[]int32", "int4": "[]int32", "serial": "[]int32",
|
||||||
|
"smallint": "[]int16", "int2": "[]int16", "smallserial": "[]int16",
|
||||||
|
"bigint": "[]int64", "int8": "[]int64", "bigserial": "[]int64",
|
||||||
|
"real": "[]float32", "float4": "[]float32",
|
||||||
|
"double precision": "[]float64", "float8": "[]float64",
|
||||||
|
"numeric": "[]float64", "decimal": "[]float64", "money": "[]float64",
|
||||||
|
"boolean": "[]bool", "bool": "[]bool",
|
||||||
|
}
|
||||||
|
if goType, ok := typeMap[baseElemType]; ok {
|
||||||
|
return goType
|
||||||
|
}
|
||||||
|
return "[]string"
|
||||||
|
}
|
||||||
|
|
||||||
// BuildGormTag generates a complete GORM tag string for a column
|
// BuildGormTag generates a complete GORM tag string for a column
|
||||||
func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) string {
|
func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
@@ -330,7 +467,16 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool {
|
|||||||
return generateGetIDStr
|
return generateGetIDStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSQLTypesImport returns the import path for sql_types
|
// GetSQLTypesImport returns the import path for the ResolveSpec spectypes package.
|
||||||
func (tm *TypeMapper) GetSQLTypesImport() string {
|
func (tm *TypeMapper) GetSQLTypesImport() string {
|
||||||
return "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
return "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
||||||
|
// package (ready to pass to AddImport). Returns empty string when no import is needed.
|
||||||
|
func (tm *TypeMapper) GetNullableTypeImportLine() string {
|
||||||
|
if tm.typeStyle == writers.NullableTypeStdlib {
|
||||||
|
return "\"database/sql\""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport())
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type Writer struct {
|
|||||||
func NewWriter(options *writers.WriterOptions) *Writer {
|
func NewWriter(options *writers.WriterOptions) *Writer {
|
||||||
w := &Writer{
|
w := &Writer{
|
||||||
options: options,
|
options: options,
|
||||||
typeMapper: NewTypeMapper(),
|
typeMapper: NewTypeMapper(options.NullableTypes),
|
||||||
config: LoadMethodConfigFromMetadata(options.Metadata),
|
config: LoadMethodConfigFromMetadata(options.Metadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +77,8 @@ func (w *Writer) writeSingleFile(db *models.Database) error {
|
|||||||
packageName := w.getPackageName()
|
packageName := w.getPackageName()
|
||||||
templateData := NewTemplateData(packageName, w.config)
|
templateData := NewTemplateData(packageName, w.config)
|
||||||
|
|
||||||
// Add sql_types import (always needed for nullable types)
|
// Add nullable types import (resolvespec or stdlib depending on options)
|
||||||
templateData.AddImport(fmt.Sprintf("sql_types \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Collect all models
|
// Collect all models
|
||||||
for _, schema := range db.Schemas {
|
for _, schema := range db.Schemas {
|
||||||
@@ -171,8 +171,8 @@ func (w *Writer) writeMultiFile(db *models.Database) error {
|
|||||||
// Create template data for this single table
|
// Create template data for this single table
|
||||||
templateData := NewTemplateData(packageName, w.config)
|
templateData := NewTemplateData(packageName, w.config)
|
||||||
|
|
||||||
// Add sql_types import
|
// Add nullable types import (resolvespec or stdlib depending on options)
|
||||||
templateData.AddImport(fmt.Sprintf("sql_types \"%s\"", w.typeMapper.GetSQLTypesImport()))
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Create model data
|
// Create model data
|
||||||
modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema)
|
modelData := NewModelData(table, schema.Name, w.typeMapper, w.options.FlattenSchema)
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ func TestNameConverter_Pluralize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
||||||
mapper := NewTypeMapper()
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
sqlType string
|
sqlType string
|
||||||
@@ -671,7 +671,7 @@ func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
func TestTypeMapper_BuildGormTag_PreservesExplicitTypeModifiers(t *testing.T) {
|
||||||
mapper := NewTypeMapper()
|
mapper := NewTypeMapper("")
|
||||||
|
|
||||||
col := &models.Column{
|
col := &models.Column{
|
||||||
Name: "embedding",
|
Name: "embedding",
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ type Writer interface {
|
|||||||
WriteTable(table *models.Table) error
|
WriteTable(table *models.Table) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NullableType constants control which Go package is used for nullable column types
|
||||||
|
// in code-generation writers (Bun, GORM).
|
||||||
|
const (
|
||||||
|
// NullableTypeResolveSpec uses github.com/bitechdev/ResolveSpec/pkg/spectypes
|
||||||
|
// (SqlString, SqlInt32, SqlVector, SqlStringArray, …). This is the default.
|
||||||
|
NullableTypeResolveSpec = "resolvespec"
|
||||||
|
|
||||||
|
// NullableTypeStdlib uses the standard library database/sql nullable types
|
||||||
|
// (sql.NullString, sql.NullInt32, …) and plain Go slices for arrays.
|
||||||
|
NullableTypeStdlib = "stdlib"
|
||||||
|
)
|
||||||
|
|
||||||
// WriterOptions contains common options for writers
|
// WriterOptions contains common options for writers
|
||||||
type WriterOptions struct {
|
type WriterOptions struct {
|
||||||
// OutputPath is the path where the output should be written
|
// OutputPath is the path where the output should be written
|
||||||
@@ -33,6 +45,12 @@ type WriterOptions struct {
|
|||||||
// Useful for databases like SQLite that do not support schemas.
|
// Useful for databases like SQLite that do not support schemas.
|
||||||
FlattenSchema bool
|
FlattenSchema bool
|
||||||
|
|
||||||
|
// NullableTypes selects the Go type package used for nullable columns in
|
||||||
|
// code-generation writers (bun, gorm). Accepted values:
|
||||||
|
// "resolvespec" (default) — github.com/bitechdev/ResolveSpec/pkg/spectypes
|
||||||
|
// "stdlib" — database/sql (sql.NullString, sql.NullInt32, …)
|
||||||
|
NullableTypes string
|
||||||
|
|
||||||
// Additional options can be added here as needed
|
// Additional options can be added here as needed
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user