fix(writer): update nullable type handling to use baselib

* change default nullable type from resolvespec to baselib
* update type mappings in tests to reflect new nullable types
* adjust comments for clarity on nullable type options
This commit is contained in:
2026-06-25 23:25:36 +02:00
parent e29c7e31c1
commit ab735d1f3a
8 changed files with 128 additions and 40 deletions
+1 -1
View File
@@ -177,7 +177,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)") convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'resolvespec'")
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)")
err := convertCmd.MarkFlagRequired("from") err := convertCmd.MarkFlagRequired("from")
+1 -1
View File
@@ -111,7 +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)") splitCmd.Flags().StringVar(&splitNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'resolvespec'")
err := splitCmd.MarkFlagRequired("from") err := splitCmd.MarkFlagRequired("from")
if err != nil { if err != nil {
+45 -4
View File
@@ -20,7 +20,7 @@ type TypeMapper struct {
// an empty string defaults to resolvespec. // an empty string defaults to resolvespec.
func NewTypeMapper(typeStyle string) *TypeMapper { func NewTypeMapper(typeStyle string) *TypeMapper {
if typeStyle == "" { if typeStyle == "" {
typeStyle = writers.NullableTypeResolveSpec typeStyle = writers.NullableTypeBaselib
} }
return &TypeMapper{ return &TypeMapper{
sqlTypesAlias: "resolvespec_common", sqlTypesAlias: "resolvespec_common",
@@ -37,11 +37,17 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
baseType := tm.extractBaseType(sqlType) baseType := tm.extractBaseType(sqlType)
if tm.typeStyle == writers.NullableTypeStdlib { switch tm.typeStyle {
case writers.NullableTypeStdlib:
if notNull { if notNull {
return tm.rawGoType(baseType) return tm.rawGoType(baseType)
} }
return tm.stdlibNullableGoType(baseType) return tm.stdlibNullableGoType(baseType)
case writers.NullableTypeBaselib:
if notNull {
return tm.rawGoType(baseType)
}
return tm.baselibNullableGoType(baseType)
} }
// resolvespec (default): use base Go types only for simple NOT NULL fields. // resolvespec (default): use base Go types only for simple NOT NULL fields.
@@ -183,7 +189,7 @@ func (tm *TypeMapper) bunGoType(sqlType string) string {
// arrayGoType returns the Go type for a PostgreSQL array column. // arrayGoType returns the Go type for a PostgreSQL array column.
// The baseElemType is the canonical base type (e.g. "text", "integer"). // The baseElemType is the canonical base type (e.g. "text", "integer").
func (tm *TypeMapper) arrayGoType(baseElemType string) string { func (tm *TypeMapper) arrayGoType(baseElemType string) string {
if tm.typeStyle == writers.NullableTypeStdlib { if tm.typeStyle == writers.NullableTypeStdlib || tm.typeStyle == writers.NullableTypeBaselib {
return tm.stdlibArrayGoType(baseElemType) return tm.stdlibArrayGoType(baseElemType)
} }
typeMap := map[string]string{ typeMap := map[string]string{
@@ -276,6 +282,38 @@ func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string {
return "sql.NullString" return "sql.NullString"
} }
// baselibNullableGoType returns plain Go pointer types for nullable columns.
func (tm *TypeMapper) baselibNullableGoType(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"
}
// stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode. // stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode.
func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string { func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string {
typeMap := map[string]string{ typeMap := map[string]string{
@@ -431,8 +469,11 @@ func (tm *TypeMapper) GetSQLTypesImport() string {
// GetNullableTypeImportLine returns the full Go import line for the nullable type // 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. // package (ready to pass to AddImport). Returns empty string when no import is needed.
func (tm *TypeMapper) GetNullableTypeImportLine() string { func (tm *TypeMapper) GetNullableTypeImportLine() string {
if tm.typeStyle == writers.NullableTypeStdlib { switch tm.typeStyle {
case writers.NullableTypeStdlib:
return "\"database/sql\"" return "\"database/sql\""
case writers.NullableTypeBaselib:
return ""
} }
return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport()) return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport())
} }
+19 -19
View File
@@ -73,9 +73,9 @@ func TestWriter_WriteTable(t *testing.T) {
"ID", "ID",
"int64", "int64",
"Email", "Email",
"resolvespec_common.SqlString", "*string",
"CreatedAt", "CreatedAt",
"resolvespec_common.SqlTime", "time.Time",
"bun:\"id", "bun:\"id",
"bun:\"email", "bun:\"email",
"func (m ModelPublicUsers) TableName() string", "func (m ModelPublicUsers) TableName() string",
@@ -564,20 +564,20 @@ func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) {
want string want string
}{ }{
{"bigint", true, "int64"}, {"bigint", true, "int64"},
{"bigint", false, "resolvespec_common.SqlInt64"}, {"bigint", false, "*int64"},
{"varchar", true, "resolvespec_common.SqlString"}, // Bun uses sql types even for NOT NULL strings {"varchar", true, "string"},
{"varchar", false, "resolvespec_common.SqlString"}, {"varchar", false, "*string"},
{"timestamp", true, "resolvespec_common.SqlTimeStamp"}, {"timestamp", true, "time.Time"},
{"timestamp", false, "resolvespec_common.SqlTimeStamp"}, {"timestamp", false, "*time.Time"},
{"date", false, "resolvespec_common.SqlDate"}, {"date", false, "*time.Time"},
{"boolean", true, "bool"}, {"boolean", true, "bool"},
{"boolean", false, "resolvespec_common.SqlBool"}, {"boolean", false, "*bool"},
{"uuid", false, "resolvespec_common.SqlUUID"}, {"uuid", false, "*string"},
{"jsonb", false, "resolvespec_common.SqlJSONB"}, {"jsonb", false, "*string"},
{"text[]", true, "resolvespec_common.SqlStringArray"}, {"text[]", true, "[]string"},
{"text[]", false, "resolvespec_common.SqlStringArray"}, {"text[]", false, "[]string"},
{"integer[]", true, "resolvespec_common.SqlInt32Array"}, {"integer[]", true, "[]int32"},
{"bigint[]", false, "resolvespec_common.SqlInt64Array"}, {"bigint[]", false, "[]int64"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -599,7 +599,7 @@ func TestWriter_UpdateIDTypeSafety_Bun(t *testing.T) {
forbidInt32 bool forbidInt32 bool
}{ }{
{"int32_pk", "int", "int32", "m.ID = int32(newid)", false}, {"int32_pk", "int", "int32", "m.ID = int32(newid)", false},
{"sql_int16_pk", "smallint", "resolvespec_common.SqlInt16", "m.ID.FromString(fmt.Sprintf(\"%d\", newid))", true}, {"sql_int16_pk", "smallint", "int16", "m.ID = int16(newid)", true},
{"int64_pk", "bigint", "int64", "m.ID = int64(newid)", true}, {"int64_pk", "bigint", "int64", "m.ID = int64(newid)", true},
} }
@@ -680,13 +680,13 @@ func TestWriter_StringPrimaryKeyHelpers_Bun(t *testing.T) {
generated := string(content) generated := string(content)
expectations := []string{ expectations := []string{
"resolvespec_common.SqlUUID", "ID string",
"func (m ModelPublicAccounts) GetID() string", "func (m ModelPublicAccounts) GetID() string",
"return m.ID.String()", "return m.ID",
"func (m ModelPublicAccounts) GetIDStr() string", "func (m ModelPublicAccounts) GetIDStr() string",
"func (m ModelPublicAccounts) SetID(newid string)", "func (m ModelPublicAccounts) SetID(newid string)",
"func (m *ModelPublicAccounts) UpdateID(newid string)", "func (m *ModelPublicAccounts) UpdateID(newid string)",
"m.ID.FromString(newid)", "m.ID = newid",
} }
for _, expected := range expectations { for _, expected := range expectations {
+1 -1
View File
@@ -202,7 +202,7 @@ func formatComment(description, comment string) string {
func isStringLikePrimaryKeyType(goType string) bool { func isStringLikePrimaryKeyType(goType string) bool {
switch goType { switch goType {
case "string", "sql.NullString", "sql_types.SqlString", "sql_types.SqlUUID": case "string", "*string", "sql.NullString", "sql_types.SqlString", "sql_types.SqlUUID":
return true return true
default: default:
return false return false
+45 -4
View File
@@ -20,7 +20,7 @@ type TypeMapper struct {
// an empty string defaults to resolvespec. // an empty string defaults to resolvespec.
func NewTypeMapper(typeStyle string) *TypeMapper { func NewTypeMapper(typeStyle string) *TypeMapper {
if typeStyle == "" { if typeStyle == "" {
typeStyle = writers.NullableTypeResolveSpec typeStyle = writers.NullableTypeBaselib
} }
return &TypeMapper{ return &TypeMapper{
sqlTypesAlias: "sql_types", sqlTypesAlias: "sql_types",
@@ -37,11 +37,17 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
baseType := tm.extractBaseType(sqlType) baseType := tm.extractBaseType(sqlType)
if tm.typeStyle == writers.NullableTypeStdlib { switch tm.typeStyle {
case writers.NullableTypeStdlib:
if notNull { if notNull {
return tm.rawGoType(baseType) return tm.rawGoType(baseType)
} }
return tm.stdlibNullableGoType(baseType) return tm.stdlibNullableGoType(baseType)
case writers.NullableTypeBaselib:
if notNull {
return tm.rawGoType(baseType)
}
return tm.baselibNullableGoType(baseType)
} }
// resolvespec (default) // resolvespec (default)
@@ -212,7 +218,7 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string {
// arrayGoType returns the Go type for a PostgreSQL array column. // arrayGoType returns the Go type for a PostgreSQL array column.
// The baseElemType is the canonical base type (e.g. "text", "integer"). // The baseElemType is the canonical base type (e.g. "text", "integer").
func (tm *TypeMapper) arrayGoType(baseElemType string) string { func (tm *TypeMapper) arrayGoType(baseElemType string) string {
if tm.typeStyle == writers.NullableTypeStdlib { if tm.typeStyle == writers.NullableTypeStdlib || tm.typeStyle == writers.NullableTypeBaselib {
return tm.stdlibArrayGoType(baseElemType) return tm.stdlibArrayGoType(baseElemType)
} }
typeMap := map[string]string{ typeMap := map[string]string{
@@ -305,6 +311,38 @@ func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string {
return "sql.NullString" return "sql.NullString"
} }
// baselibNullableGoType returns plain Go pointer types for nullable columns.
func (tm *TypeMapper) baselibNullableGoType(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"
}
// stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode. // stdlibArrayGoType returns a plain Go slice type for array columns in stdlib mode.
func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string { func (tm *TypeMapper) stdlibArrayGoType(baseElemType string) string {
typeMap := map[string]string{ typeMap := map[string]string{
@@ -475,8 +513,11 @@ func (tm *TypeMapper) GetSQLTypesImport() string {
// GetNullableTypeImportLine returns the full Go import line for the nullable type // 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. // package (ready to pass to AddImport). Returns empty string when no import is needed.
func (tm *TypeMapper) GetNullableTypeImportLine() string { func (tm *TypeMapper) GetNullableTypeImportLine() string {
if tm.typeStyle == writers.NullableTypeStdlib { switch tm.typeStyle {
case writers.NullableTypeStdlib:
return "\"database/sql\"" return "\"database/sql\""
case writers.NullableTypeBaselib:
return ""
} }
return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport()) return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport())
} }
+9 -9
View File
@@ -70,7 +70,7 @@ func TestWriter_WriteTable(t *testing.T) {
"ID", "ID",
"int64", "int64",
"Email", "Email",
"sql_types.SqlString", "*string",
"CreatedAt", "CreatedAt",
"time.Time", "time.Time",
"gorm:\"column:id", "gorm:\"column:id",
@@ -700,17 +700,17 @@ func TestTypeMapper_SQLTypeToGoType(t *testing.T) {
want string want string
}{ }{
{"bigint", true, "int64"}, {"bigint", true, "int64"},
{"bigint", false, "sql_types.SqlInt64"}, {"bigint", false, "*int64"},
{"varchar", true, "string"}, {"varchar", true, "string"},
{"varchar", false, "sql_types.SqlString"}, {"varchar", false, "*string"},
{"timestamp", true, "time.Time"}, {"timestamp", true, "time.Time"},
{"timestamp", false, "sql_types.SqlTimeStamp"}, {"timestamp", false, "*time.Time"},
{"boolean", true, "bool"}, {"boolean", true, "bool"},
{"boolean", false, "sql_types.SqlBool"}, {"boolean", false, "*bool"},
{"text[]", true, "sql_types.SqlStringArray"}, {"text[]", true, "[]string"},
{"text[]", false, "sql_types.SqlStringArray"}, {"text[]", false, "[]string"},
{"integer[]", true, "sql_types.SqlInt32Array"}, {"integer[]", true, "[]int32"},
{"bigint[]", false, "sql_types.SqlInt64Array"}, {"bigint[]", false, "[]int64"},
} }
for _, tt := range tests { for _, tt := range tests {
+7 -1
View File
@@ -24,12 +24,17 @@ type Writer interface {
// in code-generation writers (Bun, GORM). // in code-generation writers (Bun, GORM).
const ( const (
// NullableTypeResolveSpec uses github.com/bitechdev/ResolveSpec/pkg/spectypes // NullableTypeResolveSpec uses github.com/bitechdev/ResolveSpec/pkg/spectypes
// (SqlString, SqlInt32, SqlVector, SqlStringArray, …). This is the default. // (SqlString, SqlInt32, SqlVector, SqlStringArray, …).
NullableTypeResolveSpec = "resolvespec" NullableTypeResolveSpec = "resolvespec"
// NullableTypeStdlib uses the standard library database/sql nullable types // NullableTypeStdlib uses the standard library database/sql nullable types
// (sql.NullString, sql.NullInt32, …) and plain Go slices for arrays. // (sql.NullString, sql.NullInt32, …) and plain Go slices for arrays.
NullableTypeStdlib = "stdlib" NullableTypeStdlib = "stdlib"
// NullableTypeBaselib uses plain Go pointer types for nullable columns
// (*string, *int32, *time.Time, …) and plain Go slices for arrays.
// No external imports are required beyond the standard library. This is the default.
NullableTypeBaselib = "baselib"
) )
// WriterOptions contains common options for writers // WriterOptions contains common options for writers
@@ -49,6 +54,7 @@ type WriterOptions struct {
// code-generation writers (bun, gorm). Accepted values: // code-generation writers (bun, gorm). Accepted values:
// "resolvespec" (default) — github.com/bitechdev/ResolveSpec/pkg/spectypes // "resolvespec" (default) — github.com/bitechdev/ResolveSpec/pkg/spectypes
// "stdlib" — database/sql (sql.NullString, sql.NullInt32, …) // "stdlib" — database/sql (sql.NullString, sql.NullInt32, …)
// "baselib" — plain Go pointer types (*string, *int32, …)
NullableTypes string NullableTypes string
// Prisma7 enables Prisma 7-specific output for Prisma writers. // Prisma7 enables Prisma 7-specific output for Prisma writers.