diff --git a/cmd/relspec/convert.go b/cmd/relspec/convert.go index 4e6d19a..21541d3 100644 --- a/cmd/relspec/convert.go +++ b/cmd/relspec/convert.go @@ -177,7 +177,7 @@ func init() { 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().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)") err := convertCmd.MarkFlagRequired("from") diff --git a/cmd/relspec/split.go b/cmd/relspec/split.go index 04da013..acbe986 100644 --- a/cmd/relspec/split.go +++ b/cmd/relspec/split.go @@ -111,7 +111,7 @@ func init() { 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(&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") if err != nil { diff --git a/pkg/writers/bun/type_mapper.go b/pkg/writers/bun/type_mapper.go index 0ca1f57..26a47ab 100644 --- a/pkg/writers/bun/type_mapper.go +++ b/pkg/writers/bun/type_mapper.go @@ -20,7 +20,7 @@ type TypeMapper struct { // an empty string defaults to resolvespec. func NewTypeMapper(typeStyle string) *TypeMapper { if typeStyle == "" { - typeStyle = writers.NullableTypeResolveSpec + typeStyle = writers.NullableTypeBaselib } return &TypeMapper{ sqlTypesAlias: "resolvespec_common", @@ -37,11 +37,17 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string { baseType := tm.extractBaseType(sqlType) - if tm.typeStyle == writers.NullableTypeStdlib { + switch tm.typeStyle { + case writers.NullableTypeStdlib: if notNull { return tm.rawGoType(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. @@ -183,7 +189,7 @@ func (tm *TypeMapper) bunGoType(sqlType string) string { // 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 { + if tm.typeStyle == writers.NullableTypeStdlib || tm.typeStyle == writers.NullableTypeBaselib { return tm.stdlibArrayGoType(baseElemType) } typeMap := map[string]string{ @@ -276,6 +282,38 @@ func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string { 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. func (tm *TypeMapper) stdlibArrayGoType(baseElemType 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 // package (ready to pass to AddImport). Returns empty string when no import is needed. func (tm *TypeMapper) GetNullableTypeImportLine() string { - if tm.typeStyle == writers.NullableTypeStdlib { + switch tm.typeStyle { + case writers.NullableTypeStdlib: return "\"database/sql\"" + case writers.NullableTypeBaselib: + return "" } return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport()) } diff --git a/pkg/writers/bun/writer_test.go b/pkg/writers/bun/writer_test.go index e987ac1..0e66048 100644 --- a/pkg/writers/bun/writer_test.go +++ b/pkg/writers/bun/writer_test.go @@ -73,9 +73,9 @@ func TestWriter_WriteTable(t *testing.T) { "ID", "int64", "Email", - "resolvespec_common.SqlString", + "*string", "CreatedAt", - "resolvespec_common.SqlTime", + "time.Time", "bun:\"id", "bun:\"email", "func (m ModelPublicUsers) TableName() string", @@ -564,20 +564,20 @@ func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) { want string }{ {"bigint", true, "int64"}, - {"bigint", false, "resolvespec_common.SqlInt64"}, - {"varchar", true, "resolvespec_common.SqlString"}, // Bun uses sql types even for NOT NULL strings - {"varchar", false, "resolvespec_common.SqlString"}, - {"timestamp", true, "resolvespec_common.SqlTimeStamp"}, - {"timestamp", false, "resolvespec_common.SqlTimeStamp"}, - {"date", false, "resolvespec_common.SqlDate"}, + {"bigint", false, "*int64"}, + {"varchar", true, "string"}, + {"varchar", false, "*string"}, + {"timestamp", true, "time.Time"}, + {"timestamp", false, "*time.Time"}, + {"date", false, "*time.Time"}, {"boolean", true, "bool"}, - {"boolean", false, "resolvespec_common.SqlBool"}, - {"uuid", false, "resolvespec_common.SqlUUID"}, - {"jsonb", false, "resolvespec_common.SqlJSONB"}, - {"text[]", true, "resolvespec_common.SqlStringArray"}, - {"text[]", false, "resolvespec_common.SqlStringArray"}, - {"integer[]", true, "resolvespec_common.SqlInt32Array"}, - {"bigint[]", false, "resolvespec_common.SqlInt64Array"}, + {"boolean", false, "*bool"}, + {"uuid", false, "*string"}, + {"jsonb", false, "*string"}, + {"text[]", true, "[]string"}, + {"text[]", false, "[]string"}, + {"integer[]", true, "[]int32"}, + {"bigint[]", false, "[]int64"}, } for _, tt := range tests { @@ -599,7 +599,7 @@ func TestWriter_UpdateIDTypeSafety_Bun(t *testing.T) { forbidInt32 bool }{ {"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}, } @@ -680,13 +680,13 @@ func TestWriter_StringPrimaryKeyHelpers_Bun(t *testing.T) { generated := string(content) expectations := []string{ - "resolvespec_common.SqlUUID", + "ID string", "func (m ModelPublicAccounts) GetID() string", - "return m.ID.String()", + "return m.ID", "func (m ModelPublicAccounts) GetIDStr() string", "func (m ModelPublicAccounts) SetID(newid string)", "func (m *ModelPublicAccounts) UpdateID(newid string)", - "m.ID.FromString(newid)", + "m.ID = newid", } for _, expected := range expectations { diff --git a/pkg/writers/gorm/template_data.go b/pkg/writers/gorm/template_data.go index 2b54350..1d1d7bc 100644 --- a/pkg/writers/gorm/template_data.go +++ b/pkg/writers/gorm/template_data.go @@ -202,7 +202,7 @@ func formatComment(description, comment string) string { func isStringLikePrimaryKeyType(goType string) bool { 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 default: return false diff --git a/pkg/writers/gorm/type_mapper.go b/pkg/writers/gorm/type_mapper.go index 41c91e2..75e9d2d 100644 --- a/pkg/writers/gorm/type_mapper.go +++ b/pkg/writers/gorm/type_mapper.go @@ -20,7 +20,7 @@ type TypeMapper struct { // an empty string defaults to resolvespec. func NewTypeMapper(typeStyle string) *TypeMapper { if typeStyle == "" { - typeStyle = writers.NullableTypeResolveSpec + typeStyle = writers.NullableTypeBaselib } return &TypeMapper{ sqlTypesAlias: "sql_types", @@ -37,11 +37,17 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string { baseType := tm.extractBaseType(sqlType) - if tm.typeStyle == writers.NullableTypeStdlib { + switch tm.typeStyle { + case writers.NullableTypeStdlib: if notNull { return tm.rawGoType(baseType) } return tm.stdlibNullableGoType(baseType) + case writers.NullableTypeBaselib: + if notNull { + return tm.rawGoType(baseType) + } + return tm.baselibNullableGoType(baseType) } // resolvespec (default) @@ -212,7 +218,7 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string { // 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 { + if tm.typeStyle == writers.NullableTypeStdlib || tm.typeStyle == writers.NullableTypeBaselib { return tm.stdlibArrayGoType(baseElemType) } typeMap := map[string]string{ @@ -305,6 +311,38 @@ func (tm *TypeMapper) stdlibNullableGoType(sqlType string) string { 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. func (tm *TypeMapper) stdlibArrayGoType(baseElemType 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 // package (ready to pass to AddImport). Returns empty string when no import is needed. func (tm *TypeMapper) GetNullableTypeImportLine() string { - if tm.typeStyle == writers.NullableTypeStdlib { + switch tm.typeStyle { + case writers.NullableTypeStdlib: return "\"database/sql\"" + case writers.NullableTypeBaselib: + return "" } return fmt.Sprintf("%s \"%s\"", tm.sqlTypesAlias, tm.GetSQLTypesImport()) } diff --git a/pkg/writers/gorm/writer_test.go b/pkg/writers/gorm/writer_test.go index 49851bf..52a2ec0 100644 --- a/pkg/writers/gorm/writer_test.go +++ b/pkg/writers/gorm/writer_test.go @@ -70,7 +70,7 @@ func TestWriter_WriteTable(t *testing.T) { "ID", "int64", "Email", - "sql_types.SqlString", + "*string", "CreatedAt", "time.Time", "gorm:\"column:id", @@ -700,17 +700,17 @@ func TestTypeMapper_SQLTypeToGoType(t *testing.T) { want string }{ {"bigint", true, "int64"}, - {"bigint", false, "sql_types.SqlInt64"}, + {"bigint", false, "*int64"}, {"varchar", true, "string"}, - {"varchar", false, "sql_types.SqlString"}, + {"varchar", false, "*string"}, {"timestamp", true, "time.Time"}, - {"timestamp", false, "sql_types.SqlTimeStamp"}, + {"timestamp", false, "*time.Time"}, {"boolean", true, "bool"}, - {"boolean", false, "sql_types.SqlBool"}, - {"text[]", true, "sql_types.SqlStringArray"}, - {"text[]", false, "sql_types.SqlStringArray"}, - {"integer[]", true, "sql_types.SqlInt32Array"}, - {"bigint[]", false, "sql_types.SqlInt64Array"}, + {"boolean", false, "*bool"}, + {"text[]", true, "[]string"}, + {"text[]", false, "[]string"}, + {"integer[]", true, "[]int32"}, + {"bigint[]", false, "[]int64"}, } for _, tt := range tests { diff --git a/pkg/writers/writer.go b/pkg/writers/writer.go index 0ada614..0a6cb41 100644 --- a/pkg/writers/writer.go +++ b/pkg/writers/writer.go @@ -24,12 +24,17 @@ type Writer interface { // in code-generation writers (Bun, GORM). const ( // NullableTypeResolveSpec uses github.com/bitechdev/ResolveSpec/pkg/spectypes - // (SqlString, SqlInt32, SqlVector, SqlStringArray, …). This is the default. + // (SqlString, SqlInt32, SqlVector, SqlStringArray, …). NullableTypeResolveSpec = "resolvespec" // NullableTypeStdlib uses the standard library database/sql nullable types // (sql.NullString, sql.NullInt32, …) and plain Go slices for arrays. 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 @@ -49,6 +54,7 @@ type WriterOptions struct { // code-generation writers (bun, gorm). Accepted values: // "resolvespec" (default) — github.com/bitechdev/ResolveSpec/pkg/spectypes // "stdlib" — database/sql (sql.NullString, sql.NullInt32, …) + // "baselib" — plain Go pointer types (*string, *int32, …) NullableTypes string // Prisma7 enables Prisma 7-specific output for Prisma writers.