refactor: ♻️ change resolvspec types to build in sqltypes
This commit is contained in:
@@ -151,8 +151,21 @@ pkg/merge/ Schema merging
|
|||||||
pkg/models/ Internal data models
|
pkg/models/ Internal data models
|
||||||
pkg/transform/ Transformation logic
|
pkg/transform/ Transformation logic
|
||||||
pkg/pgsql/ PostgreSQL utilities
|
pkg/pgsql/ PostgreSQL utilities
|
||||||
|
pkg/sqltypes/ Nullable SQL types for generated/hand-written models (see below)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Nullable Types (`pkg/sqltypes`)
|
||||||
|
|
||||||
|
The `bun` and `gorm` writers can generate model structs using
|
||||||
|
[`pkg/sqltypes`](./pkg/sqltypes/README.md) — nullable types (`SqlString`,
|
||||||
|
`SqlInt32`, `SqlTimeStamp`, `SqlStringArray`, …) that implement
|
||||||
|
`database/sql.Scanner`, `driver.Valuer`, and JSON/YAML/XML marshalling in one
|
||||||
|
type, selected via `--types sqltypes`. See the
|
||||||
|
[`pkg/sqltypes` README](./pkg/sqltypes/README.md) for the full type
|
||||||
|
reference, or the [`bun`](./pkg/writers/bun/README.md) /
|
||||||
|
[`gorm`](./pkg/writers/gorm/README.md) writer docs for the `--types` flag
|
||||||
|
(`sqltypes`, `stdlib`, or `baselib`).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Register or sign in with GitHub at [git.warky.dev](https://git.warky.dev)
|
1. Register or sign in with GitHub at [git.warky.dev](https://git.warky.dev)
|
||||||
|
|||||||
@@ -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): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'resolvespec'")
|
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
|
||||||
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
|
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
|
||||||
|
|
||||||
err := convertCmd.MarkFlagRequired("from")
|
err := convertCmd.MarkFlagRequired("from")
|
||||||
|
|||||||
@@ -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): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'resolvespec'")
|
splitCmd.Flags().StringVar(&splitNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
|
||||||
|
|
||||||
err := splitCmd.MarkFlagRequired("from")
|
err := splitCmd.MarkFlagRequired("from")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package models_bun
|
|||||||
// //ModelCoreMasterprocess - Generated Table for Schema core
|
// //ModelCoreMasterprocess - Generated Table for Schema core
|
||||||
// type ModelCoreMasterprocess struct {
|
// type ModelCoreMasterprocess struct {
|
||||||
// bun.BaseModel `bun:"table:core.masterprocess,alias:masterprocess"`
|
// bun.BaseModel `bun:"table:core.masterprocess,alias:masterprocess"`
|
||||||
// Description resolvespec_common.SqlString `json:"description" bun:"description,type:citext,"`
|
// Description sql_types.SqlString `json:"description" bun:"description,type:citext,"`
|
||||||
// GUID resolvespec_common.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
// GUID sql_types.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
||||||
// Inactive resolvespec_common.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
// Inactive sql_types.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
||||||
// Jsonvalue resolvespec_common.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
// Jsonvalue sql_types.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
||||||
// Ridjsonschema resolvespec_common.SqlInt32 `json:"rid_jsonschema" bun:"rid_jsonschema,type:integer,"`
|
// Ridjsonschema sql_types.SqlInt32 `json:"rid_jsonschema" bun:"rid_jsonschema,type:integer,"`
|
||||||
// Ridmasterprocess resolvespec_common.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,pk,default:nextval('core.identity_masterprocess_rid_masterprocess'::regclass),"`
|
// Ridmasterprocess sql_types.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,pk,default:nextval('core.identity_masterprocess_rid_masterprocess'::regclass),"`
|
||||||
// Ridmastertypehubtype resolvespec_common.SqlInt32 `json:"rid_mastertype_hubtype" bun:"rid_mastertype_hubtype,type:integer,"`
|
// Ridmastertypehubtype sql_types.SqlInt32 `json:"rid_mastertype_hubtype" bun:"rid_mastertype_hubtype,type:integer,"`
|
||||||
// Ridmastertypeprocesstype resolvespec_common.SqlInt32 `json:"rid_mastertype_processtype" bun:"rid_mastertype_processtype,type:integer,"`
|
// Ridmastertypeprocesstype sql_types.SqlInt32 `json:"rid_mastertype_processtype" bun:"rid_mastertype_processtype,type:integer,"`
|
||||||
// Ridprogrammodule resolvespec_common.SqlInt32 `json:"rid_programmodule" bun:"rid_programmodule,type:integer,"`
|
// Ridprogrammodule sql_types.SqlInt32 `json:"rid_programmodule" bun:"rid_programmodule,type:integer,"`
|
||||||
// Sequenceno resolvespec_common.SqlInt32 `json:"sequenceno" bun:"sequenceno,type:integer,"`
|
// Sequenceno sql_types.SqlInt32 `json:"sequenceno" bun:"sequenceno,type:integer,"`
|
||||||
// Singleprocess resolvespec_common.SqlInt16 `json:"singleprocess" bun:"singleprocess,type:smallint,"`
|
// Singleprocess sql_types.SqlInt16 `json:"singleprocess" bun:"singleprocess,type:smallint,"`
|
||||||
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
||||||
// JSON *ModelCoreJsonschema `json:"JSON,omitempty" bun:"rel:has-one,join:rid_jsonschema=rid_jsonschema"`
|
// JSON *ModelCoreJsonschema `json:"JSON,omitempty" bun:"rel:has-one,join:rid_jsonschema=rid_jsonschema"`
|
||||||
// MTT_RID_MASTERTYPE_HUBTYPE *ModelCoreMastertype `json:"MTT_RID_MASTERTYPE_HUBTYPE,omitempty" bun:"rel:has-one,join:rid_mastertype_hubtype=rid_mastertype"`
|
// MTT_RID_MASTERTYPE_HUBTYPE *ModelCoreMastertype `json:"MTT_RID_MASTERTYPE_HUBTYPE,omitempty" bun:"rel:has-one,join:rid_mastertype_hubtype=rid_mastertype"`
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ package models_bun
|
|||||||
// //ModelCoreMastertask - Generated Table for Schema core
|
// //ModelCoreMastertask - Generated Table for Schema core
|
||||||
// type ModelCoreMastertask struct {
|
// type ModelCoreMastertask struct {
|
||||||
// bun.BaseModel `bun:"table:core.mastertask,alias:mastertask"`
|
// bun.BaseModel `bun:"table:core.mastertask,alias:mastertask"`
|
||||||
// Allactionsmustcomplete resolvespec_common.SqlInt16 `json:"allactionsmustcomplete" bun:"allactionsmustcomplete,type:smallint,"`
|
// Allactionsmustcomplete sql_types.SqlInt16 `json:"allactionsmustcomplete" bun:"allactionsmustcomplete,type:smallint,"`
|
||||||
// Condition resolvespec_common.SqlString `json:"condition" bun:"condition,type:citext,"`
|
// Condition sql_types.SqlString `json:"condition" bun:"condition,type:citext,"`
|
||||||
// Description resolvespec_common.SqlString `json:"description" bun:"description,type:citext,"`
|
// Description sql_types.SqlString `json:"description" bun:"description,type:citext,"`
|
||||||
// Dueday resolvespec_common.SqlInt16 `json:"dueday" bun:"dueday,type:smallint,"`
|
// Dueday sql_types.SqlInt16 `json:"dueday" bun:"dueday,type:smallint,"`
|
||||||
// Dueoption resolvespec_common.SqlString `json:"dueoption" bun:"dueoption,type:citext,"`
|
// Dueoption sql_types.SqlString `json:"dueoption" bun:"dueoption,type:citext,"`
|
||||||
// Escalation resolvespec_common.SqlInt32 `json:"escalation" bun:"escalation,type:integer,"`
|
// Escalation sql_types.SqlInt32 `json:"escalation" bun:"escalation,type:integer,"`
|
||||||
// Escalationoption resolvespec_common.SqlString `json:"escalationoption" bun:"escalationoption,type:citext,"`
|
// Escalationoption sql_types.SqlString `json:"escalationoption" bun:"escalationoption,type:citext,"`
|
||||||
// GUID resolvespec_common.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
// GUID sql_types.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
||||||
// Inactive resolvespec_common.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
// Inactive sql_types.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
||||||
// Jsonvalue resolvespec_common.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
// Jsonvalue sql_types.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
||||||
// Mastertasknote resolvespec_common.SqlString `json:"mastertasknote" bun:"mastertasknote,type:citext,"`
|
// Mastertasknote sql_types.SqlString `json:"mastertasknote" bun:"mastertasknote,type:citext,"`
|
||||||
// Repeatinterval resolvespec_common.SqlInt16 `json:"repeatinterval" bun:"repeatinterval,type:smallint,"`
|
// Repeatinterval sql_types.SqlInt16 `json:"repeatinterval" bun:"repeatinterval,type:smallint,"`
|
||||||
// Repeattype resolvespec_common.SqlString `json:"repeattype" bun:"repeattype,type:citext,"`
|
// Repeattype sql_types.SqlString `json:"repeattype" bun:"repeattype,type:citext,"`
|
||||||
// Ridjsonschema resolvespec_common.SqlInt32 `json:"rid_jsonschema" bun:"rid_jsonschema,type:integer,"`
|
// Ridjsonschema sql_types.SqlInt32 `json:"rid_jsonschema" bun:"rid_jsonschema,type:integer,"`
|
||||||
// Ridmasterprocess resolvespec_common.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,"`
|
// Ridmasterprocess sql_types.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,"`
|
||||||
// Ridmastertask resolvespec_common.SqlInt32 `json:"rid_mastertask" bun:"rid_mastertask,type:integer,pk,default:nextval('core.identity_mastertask_rid_mastertask'::regclass),"`
|
// Ridmastertask sql_types.SqlInt32 `json:"rid_mastertask" bun:"rid_mastertask,type:integer,pk,default:nextval('core.identity_mastertask_rid_mastertask'::regclass),"`
|
||||||
// Ridmastertypetasktype resolvespec_common.SqlInt32 `json:"rid_mastertype_tasktype" bun:"rid_mastertype_tasktype,type:integer,"`
|
// Ridmastertypetasktype sql_types.SqlInt32 `json:"rid_mastertype_tasktype" bun:"rid_mastertype_tasktype,type:integer,"`
|
||||||
// Sequenceno resolvespec_common.SqlInt32 `json:"sequenceno" bun:"sequenceno,type:integer,"`
|
// Sequenceno sql_types.SqlInt32 `json:"sequenceno" bun:"sequenceno,type:integer,"`
|
||||||
// Singletask resolvespec_common.SqlInt16 `json:"singletask" bun:"singletask,type:smallint,"`
|
// Singletask sql_types.SqlInt16 `json:"singletask" bun:"singletask,type:smallint,"`
|
||||||
// Startday resolvespec_common.SqlInt16 `json:"startday" bun:"startday,type:smallint,"`
|
// Startday sql_types.SqlInt16 `json:"startday" bun:"startday,type:smallint,"`
|
||||||
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
||||||
// JSON *ModelCoreJsonschema `json:"JSON,omitempty" bun:"rel:has-one,join:rid_jsonschema=rid_jsonschema"`
|
// JSON *ModelCoreJsonschema `json:"JSON,omitempty" bun:"rel:has-one,join:rid_jsonschema=rid_jsonschema"`
|
||||||
// MPR *ModelCoreMasterprocess `json:"MPR,omitempty" bun:"rel:has-one,join:rid_masterprocess=rid_masterprocess"`
|
// MPR *ModelCoreMasterprocess `json:"MPR,omitempty" bun:"rel:has-one,join:rid_masterprocess=rid_masterprocess"`
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package models_bun
|
|||||||
// //ModelCoreMastertype - Generated Table for Schema core
|
// //ModelCoreMastertype - Generated Table for Schema core
|
||||||
// type ModelCoreMastertype struct {
|
// type ModelCoreMastertype struct {
|
||||||
// bun.BaseModel `bun:"table:core.mastertype,alias:mastertype"`
|
// bun.BaseModel `bun:"table:core.mastertype,alias:mastertype"`
|
||||||
// Category resolvespec_common.SqlString `json:"category" bun:"category,type:citext,"`
|
// Category sql_types.SqlString `json:"category" bun:"category,type:citext,"`
|
||||||
// Description resolvespec_common.SqlString `json:"description" bun:"description,type:citext,"`
|
// Description sql_types.SqlString `json:"description" bun:"description,type:citext,"`
|
||||||
// Disableedit resolvespec_common.SqlInt16 `json:"disableedit" bun:"disableedit,type:smallint,"`
|
// Disableedit sql_types.SqlInt16 `json:"disableedit" bun:"disableedit,type:smallint,"`
|
||||||
// Forprefix resolvespec_common.SqlString `json:"forprefix" bun:"forprefix,type:citext,"`
|
// Forprefix sql_types.SqlString `json:"forprefix" bun:"forprefix,type:citext,"`
|
||||||
// GUID resolvespec_common.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
// GUID sql_types.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
||||||
// Hidden resolvespec_common.SqlInt16 `json:"hidden" bun:"hidden,type:smallint,"`
|
// Hidden sql_types.SqlInt16 `json:"hidden" bun:"hidden,type:smallint,"`
|
||||||
// Inactive resolvespec_common.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
// Inactive sql_types.SqlInt16 `json:"inactive" bun:"inactive,type:smallint,"`
|
||||||
// Jsonvalue resolvespec_common.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
// Jsonvalue sql_types.SqlJSONB `json:"jsonvalue" bun:"jsonvalue,type:jsonb,"`
|
||||||
// Mastertype resolvespec_common.SqlString `json:"mastertype" bun:"mastertype,type:citext,"`
|
// Mastertype sql_types.SqlString `json:"mastertype" bun:"mastertype,type:citext,"`
|
||||||
// Note resolvespec_common.SqlString `json:"note" bun:"note,type:citext,"`
|
// Note sql_types.SqlString `json:"note" bun:"note,type:citext,"`
|
||||||
// Ridmastertype resolvespec_common.SqlInt32 `json:"rid_mastertype" bun:"rid_mastertype,type:integer,pk,default:nextval('core.identity_mastertype_rid_mastertype'::regclass),"`
|
// Ridmastertype sql_types.SqlInt32 `json:"rid_mastertype" bun:"rid_mastertype,type:integer,pk,default:nextval('core.identity_mastertype_rid_mastertype'::regclass),"`
|
||||||
// Ridparent resolvespec_common.SqlInt32 `json:"rid_parent" bun:"rid_parent,type:integer,"`
|
// Ridparent sql_types.SqlInt32 `json:"rid_parent" bun:"rid_parent,type:integer,"`
|
||||||
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
||||||
// MTT *ModelCoreMastertype `json:"MTT,omitempty" bun:"rel:has-one,join:rid_mastertype=rid_parent"`
|
// MTT *ModelCoreMastertype `json:"MTT,omitempty" bun:"rel:has-one,join:rid_mastertype=rid_parent"`
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package models_bun
|
|||||||
// //ModelCoreProcess - Generated Table for Schema core
|
// //ModelCoreProcess - Generated Table for Schema core
|
||||||
// type ModelCoreProcess struct {
|
// type ModelCoreProcess struct {
|
||||||
// bun.BaseModel `bun:"table:core.process,alias:process"`
|
// bun.BaseModel `bun:"table:core.process,alias:process"`
|
||||||
// Completedate resolvespec_common.SqlDate `json:"completedate" bun:"completedate,type:date,"`
|
// Completedate sql_types.SqlDate `json:"completedate" bun:"completedate,type:date,"`
|
||||||
// Completetime types.CustomIntTime `json:"completetime" bun:"completetime,type:integer,"`
|
// Completetime types.CustomIntTime `json:"completetime" bun:"completetime,type:integer,"`
|
||||||
// Description resolvespec_common.SqlString `json:"description" bun:"description,type:citext,"`
|
// Description sql_types.SqlString `json:"description" bun:"description,type:citext,"`
|
||||||
// GUID resolvespec_common.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
// GUID sql_types.SqlUUID `json:"guid" bun:"guid,type:uuid,default:newid(),"`
|
||||||
// Ridcompleteuser resolvespec_common.SqlInt32 `json:"rid_completeuser" bun:"rid_completeuser,type:integer,"`
|
// Ridcompleteuser sql_types.SqlInt32 `json:"rid_completeuser" bun:"rid_completeuser,type:integer,"`
|
||||||
// Ridhub resolvespec_common.SqlInt32 `json:"rid_hub" bun:"rid_hub,type:integer,"`
|
// Ridhub sql_types.SqlInt32 `json:"rid_hub" bun:"rid_hub,type:integer,"`
|
||||||
// Ridmasterprocess resolvespec_common.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,"`
|
// Ridmasterprocess sql_types.SqlInt32 `json:"rid_masterprocess" bun:"rid_masterprocess,type:integer,"`
|
||||||
// Ridprocess resolvespec_common.SqlInt32 `json:"rid_process" bun:"rid_process,type:integer,pk,default:nextval('core.identity_process_rid_process'::regclass),"`
|
// Ridprocess sql_types.SqlInt32 `json:"rid_process" bun:"rid_process,type:integer,pk,default:nextval('core.identity_process_rid_process'::regclass),"`
|
||||||
// Status resolvespec_common.SqlString `json:"status" bun:"status,type:citext,"`
|
// Status sql_types.SqlString `json:"status" bun:"status,type:citext,"`
|
||||||
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
// Updatecnt int64 `json:"updatecnt" bun:"updatecnt,type:integer,default:0,"`
|
||||||
// HUB *ModelCoreHub `json:"HUB,omitempty" bun:"rel:has-one,join:rid_hub=rid_hub"`
|
// HUB *ModelCoreHub `json:"HUB,omitempty" bun:"rel:has-one,join:rid_hub=rid_hub"`
|
||||||
// MPR *ModelCoreMasterprocess `json:"MPR,omitempty" bun:"rel:has-one,join:rid_masterprocess=rid_masterprocess"`
|
// MPR *ModelCoreMasterprocess `json:"MPR,omitempty" bun:"rel:has-one,join:rid_masterprocess=rid_masterprocess"`
|
||||||
|
|||||||
@@ -746,7 +746,7 @@ func (r *Reader) goTypeToSQL(expr ast.Expr) string {
|
|||||||
if t.Sel.Name == "Time" {
|
if t.Sel.Name == "Time" {
|
||||||
return "timestamp"
|
return "timestamp"
|
||||||
}
|
}
|
||||||
case "resolvespec_common", "sql_types":
|
case "sql_types":
|
||||||
return r.sqlTypeToSQL(t.Sel.Name)
|
return r.sqlTypeToSQL(t.Sel.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -787,7 +787,7 @@ func (r *Reader) isNullableGoType(expr ast.Expr) bool {
|
|||||||
case *ast.SelectorExpr:
|
case *ast.SelectorExpr:
|
||||||
// Check for sql_types nullable types
|
// Check for sql_types nullable types
|
||||||
if ident, ok := t.X.(*ast.Ident); ok {
|
if ident, ok := t.X.(*ast.Ident); ok {
|
||||||
if ident.Name == "resolvespec_common" || ident.Name == "sql_types" {
|
if ident.Name == "sql_types" {
|
||||||
return strings.HasPrefix(t.Sel.Name, "Sql")
|
return strings.HasPrefix(t.Sel.Name, "Sql")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# sqltypes
|
||||||
|
|
||||||
|
Nullable SQL types for hand-written or generated Go models. Each type wraps a
|
||||||
|
value with a `Valid` flag and implements `database/sql.Scanner`,
|
||||||
|
`driver.Valuer`, `encoding/json`, `gopkg.in/yaml.v3`, and `encoding/xml`
|
||||||
|
marshalling — so a single struct field can be scanned from a database row,
|
||||||
|
round-tripped through JSON/YAML/XML, and written back to the database without
|
||||||
|
any per-format glue code.
|
||||||
|
|
||||||
|
This package is what the `bun` and `gorm` writers emit when generating models
|
||||||
|
with `--types sqltypes` (see [`pkg/writers/bun`](../writers/bun/README.md) and
|
||||||
|
[`pkg/writers/gorm`](../writers/gorm/README.md)). It can also be imported
|
||||||
|
directly in hand-written models.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scalar types
|
||||||
|
|
||||||
|
All scalar types are instantiations of the generic `SqlNull[T]`:
|
||||||
|
|
||||||
|
| Type | Underlying | Typical SQL type |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlInt16` | `int16` | `smallint` |
|
||||||
|
| `SqlInt32` | `int32` | `integer` |
|
||||||
|
| `SqlInt64` | `int64` | `bigint` |
|
||||||
|
| `SqlFloat32` | `float32` | `real`, `float4` |
|
||||||
|
| `SqlFloat64` | `float64` | `double precision`, `numeric`, `decimal`, `money` |
|
||||||
|
| `SqlBool` | `bool` | `boolean` |
|
||||||
|
| `SqlString` | `string` | `text`, `varchar`, `char`, `citext`, `inet`, `cidr`, `macaddr` |
|
||||||
|
| `SqlByteArray` | `[]byte` | `bytea` (base64-encoded in JSON/YAML/XML) |
|
||||||
|
| `SqlUUID` | `uuid.UUID` (`github.com/google/uuid`) | `uuid` |
|
||||||
|
|
||||||
|
You can also instantiate `SqlNull[T]` directly for any type not covered
|
||||||
|
above, e.g. `SqlNull[MyEnum]`.
|
||||||
|
|
||||||
|
### Date/time types
|
||||||
|
|
||||||
|
Plain `time.Time` doesn't distinguish date-only, time-only, and timestamp
|
||||||
|
semantics, and its zero value marshals to a confusing `0001-01-01T00:00:00Z`.
|
||||||
|
These wrapper types fix both problems:
|
||||||
|
|
||||||
|
| Type | Format | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlTimeStamp` | `2006-01-02T15:04:05` | Full timestamp |
|
||||||
|
| `SqlDate` | `2006-01-02` | Date only |
|
||||||
|
| `SqlTime` | `15:04:05` | Time only |
|
||||||
|
|
||||||
|
Zero/pre-epoch values (`time.Time{}` or anything before `0002-01-01`) marshal
|
||||||
|
to `null` and `Value()` returns `nil`, instead of leaking Go's zero-time
|
||||||
|
sentinel into the database or API responses.
|
||||||
|
|
||||||
|
### JSON types
|
||||||
|
|
||||||
|
| Type | Underlying | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlJSONB` | `[]byte` | Raw JSON bytes; `MarshalYAML` decodes to native YAML mappings/sequences instead of an embedded JSON string |
|
||||||
|
| `SqlJSON` | `= SqlJSONB` | Alias — PostgreSQL's `json` and `jsonb` share the same Go representation |
|
||||||
|
|
||||||
|
`SqlJSONB` has `AsMap()` / `AsSlice()` helpers for pulling out
|
||||||
|
`map[string]any` / `[]any` without a separate `json.Unmarshal` call.
|
||||||
|
|
||||||
|
### Vector type (pgvector)
|
||||||
|
|
||||||
|
`SqlVector` wraps `[]float32` for the `vector` column type ([pgvector](https://github.com/pgvector/pgvector)),
|
||||||
|
scanning/writing the `[1,2,3]` literal format pgvector uses over the wire.
|
||||||
|
|
||||||
|
## Array types
|
||||||
|
|
||||||
|
PostgreSQL array columns (`text[]`, `integer[]`, …) map to `SqlXxxArray`
|
||||||
|
types, each wrapping `Val []T` + `Valid bool` and handling PostgreSQL's
|
||||||
|
`{a,b,c}` array literal format on `Scan`/`Value`:
|
||||||
|
|
||||||
|
`SqlStringArray`, `SqlInt16Array`, `SqlInt32Array`, `SqlInt64Array`,
|
||||||
|
`SqlFloat32Array`, `SqlFloat64Array`, `SqlBoolArray`, `SqlUUIDArray`.
|
||||||
|
|
||||||
|
## Constructing values
|
||||||
|
|
||||||
|
Every type has a `NewSqlXxx(v)` constructor that sets `Valid: true`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
name := sql_types.NewSqlString("Ada Lovelace")
|
||||||
|
age := sql_types.NewSqlInt32(36)
|
||||||
|
tags := sql_types.NewSqlStringArray([]string{"engineer", "mathematician"})
|
||||||
|
```
|
||||||
|
|
||||||
|
The zero value of any type (`sql_types.SqlString{}`) is null/invalid — use it
|
||||||
|
directly for a `NULL` field instead of a separate constructor.
|
||||||
|
|
||||||
|
Generic helpers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sql_types.Null(v, valid) // SqlNull[T]{Val: v, Valid: valid}
|
||||||
|
sql_types.NewSql[T](anyValue) // best-effort conversion from any Go value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading values back
|
||||||
|
|
||||||
|
Each scalar type has typed accessors that return the zero value instead of
|
||||||
|
panicking when `Valid` is false:
|
||||||
|
|
||||||
|
```go
|
||||||
|
n.Int64() // SqlInt16/32/64, SqlFloat32/64, SqlBool, SqlString → int64
|
||||||
|
n.Float64() // → float64
|
||||||
|
n.Bool() // → bool
|
||||||
|
n.Time() // SqlNull[time.Time]-based types → time.Time
|
||||||
|
n.UUID() // SqlUUID → uuid.UUID
|
||||||
|
n.String() // fmt.Stringer — empty string when invalid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID sql_types.SqlUUID `json:"id"`
|
||||||
|
Name sql_types.SqlString `json:"name"`
|
||||||
|
Tags sql_types.SqlStringArray `json:"tags"`
|
||||||
|
Metadata sql_types.SqlJSONB `json:"metadata"`
|
||||||
|
CreatedAt sql_types.SqlTimeStamp `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
u := User{
|
||||||
|
ID: sql_types.NewSqlUUID(uuid.New()),
|
||||||
|
Name: sql_types.NewSqlString("Ada Lovelace"),
|
||||||
|
Tags: sql_types.NewSqlStringArray([]string{"engineer"}),
|
||||||
|
CreatedAt: sql_types.SqlTimeStampNow(),
|
||||||
|
}
|
||||||
|
// Metadata left as the zero value → serializes as null, scans as NULL.
|
||||||
|
```
|
||||||
|
|
||||||
|
Every type implements `sql.Scanner` and `driver.Valuer`, so these fields can
|
||||||
|
be used directly as struct fields with `database/sql`, `bun`, or `gorm`
|
||||||
|
without additional tags or hooks.
|
||||||
@@ -1,15 +1,85 @@
|
|||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// marshalYAMLSlice returns the value used by yaml.Marshaler implementations
|
||||||
|
// for the nullable array types below: nil when invalid, the slice otherwise.
|
||||||
|
func marshalYAMLSlice[T any](valid bool, vals []T) (any, error) {
|
||||||
|
if !valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalYAMLSlice returns the value used by yaml.Unmarshaler
|
||||||
|
// implementations for the nullable array types below.
|
||||||
|
func unmarshalYAMLSlice[T any](value *yaml.Node) (vals []T, valid bool, err error) {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
if err := value.Decode(&vals); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return vals, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// xmlArrayItemName is the element name used for each item when a nullable
|
||||||
|
// array type is encoded as XML, since XML has no native list type.
|
||||||
|
var xmlArrayItemName = xml.Name{Local: "item"}
|
||||||
|
|
||||||
|
// marshalXMLSlice writes a nullable array type as XML: an empty element when
|
||||||
|
// invalid, otherwise the start tag followed by one <item> child per element.
|
||||||
|
func marshalXMLSlice[T any](e *xml.Encoder, start xml.StartElement, valid bool, vals []T) error {
|
||||||
|
if !valid {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, v := range vals {
|
||||||
|
if err := e.EncodeElement(v, xml.StartElement{Name: xmlArrayItemName}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalXMLSlice reads back the XML written by marshalXMLSlice.
|
||||||
|
//
|
||||||
|
// XML has no native null representation: an element with no <item> children
|
||||||
|
// unmarshals to a valid, empty slice rather than an invalid one.
|
||||||
|
func unmarshalXMLSlice[T any](d *xml.Decoder, start xml.StartElement) ([]T, error) {
|
||||||
|
var vals []T
|
||||||
|
for {
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
var v T
|
||||||
|
if err := d.DecodeElement(&v, &t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vals = append(vals, v)
|
||||||
|
case xml.EndElement:
|
||||||
|
if t.Name == start.Name {
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parsePostgresArrayElements parses a PostgreSQL array literal (e.g. `{a,"b,c",d}`)
|
// parsePostgresArrayElements parses a PostgreSQL array literal (e.g. `{a,"b,c",d}`)
|
||||||
// into a slice of raw string elements. Each element retains its unquoted/unescaped value.
|
// into a slice of raw string elements. Each element retains its unquoted/unescaped value.
|
||||||
func parsePostgresArrayElements(s string) ([]string, error) {
|
func parsePostgresArrayElements(s string) ([]string, error) {
|
||||||
@@ -140,6 +210,32 @@ func (a *SqlStringArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[string](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[string](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlStringArray(v []string) SqlStringArray {
|
func NewSqlStringArray(v []string) SqlStringArray {
|
||||||
return SqlStringArray{Val: v, Valid: true}
|
return SqlStringArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -215,6 +311,32 @@ func (a *SqlInt16Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int16](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int16](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt16Array(v []int16) SqlInt16Array {
|
func NewSqlInt16Array(v []int16) SqlInt16Array {
|
||||||
return SqlInt16Array{Val: v, Valid: true}
|
return SqlInt16Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -290,6 +412,32 @@ func (a *SqlInt32Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt32Array(v []int32) SqlInt32Array {
|
func NewSqlInt32Array(v []int32) SqlInt32Array {
|
||||||
return SqlInt32Array{Val: v, Valid: true}
|
return SqlInt32Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -365,6 +513,32 @@ func (a *SqlInt64Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int64](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int64](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt64Array(v []int64) SqlInt64Array {
|
func NewSqlInt64Array(v []int64) SqlInt64Array {
|
||||||
return SqlInt64Array{Val: v, Valid: true}
|
return SqlInt64Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -440,6 +614,32 @@ func (a *SqlFloat32Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat32Array(v []float32) SqlFloat32Array {
|
func NewSqlFloat32Array(v []float32) SqlFloat32Array {
|
||||||
return SqlFloat32Array{Val: v, Valid: true}
|
return SqlFloat32Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -515,6 +715,32 @@ func (a *SqlFloat64Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float64](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float64](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat64Array(v []float64) SqlFloat64Array {
|
func NewSqlFloat64Array(v []float64) SqlFloat64Array {
|
||||||
return SqlFloat64Array{Val: v, Valid: true}
|
return SqlFloat64Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -591,6 +817,32 @@ func (a *SqlBoolArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[bool](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[bool](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlBoolArray(v []bool) SqlBoolArray {
|
func NewSqlBoolArray(v []bool) SqlBoolArray {
|
||||||
return SqlBoolArray{Val: v, Valid: true}
|
return SqlBoolArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -666,6 +918,32 @@ func (a *SqlUUIDArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[uuid.UUID](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[uuid.UUID](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlUUIDArray(v []uuid.UUID) SqlUUIDArray {
|
func NewSqlUUIDArray(v []uuid.UUID) SqlUUIDArray {
|
||||||
return SqlUUIDArray{Val: v, Valid: true}
|
return SqlUUIDArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -750,6 +1028,32 @@ func (v *SqlVector) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(v.Valid, v.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Val, v.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, v.Valid, v.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Val, v.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlVector(val []float32) SqlVector {
|
func NewSqlVector(val []float32) SqlVector {
|
||||||
return SqlVector{Val: val, Valid: true}
|
return SqlVector{Val: val, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
package sqltypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePostgresArrayElements(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"simple", "{a,b,c}", []string{"a", "b", "c"}, false},
|
||||||
|
{"empty array", "{}", []string{}, false},
|
||||||
|
{"null", "NULL", nil, false},
|
||||||
|
{"lowercase null", "null", nil, false},
|
||||||
|
{"empty string", "", nil, false},
|
||||||
|
{"quoted with comma", `{a,"b,c",d}`, []string{"a", "b,c", "d"}, false},
|
||||||
|
{"escaped quote", `{"a""b"}`, []string{`a"b`}, false},
|
||||||
|
{"escaped backslash", `{"a\\b"}`, []string{`a\b`}, false},
|
||||||
|
{"not an array", "abc", nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := parsePostgresArrayElements(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("expected %v, got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("index %d: expected %q, got %q", i, tt.want[i], got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatPostgresStringArray(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"nil", nil, "NULL"},
|
||||||
|
{"empty", []string{}, "{}"},
|
||||||
|
{"simple", []string{"a", "b"}, "{a,b}"},
|
||||||
|
{"needs quoting comma", []string{"a,b"}, `{"a,b"}`},
|
||||||
|
{"needs quoting empty elem", []string{""}, `{""}`},
|
||||||
|
{"needs quoting quote", []string{`a"b`}, `{"a""b"}`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := formatPostgresStringArray(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("expected %q, got %q", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlStringArray(t *testing.T) {
|
||||||
|
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
if err := a.Scan(`{a,"b,c",d}`); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []string{"a", "b,c", "d"}
|
||||||
|
if len(a.Val) != len(want) {
|
||||||
|
t.Fatalf("expected %v, got %v", want, a.Val)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if a.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %q, got %q", i, want[i], a.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlStringArray
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("re-scan failed: %v", err)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if b.Val[i] != want[i] {
|
||||||
|
t.Errorf("round-trip index %d: expected %q, got %q", i, want[i], b.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan nil", func(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
if err := a.Scan(nil); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
if a.Valid {
|
||||||
|
t.Error("expected invalid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("value invalid", func(t *testing.T) {
|
||||||
|
a := SqlStringArray{Valid: false}
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
if val != nil {
|
||||||
|
t.Errorf("expected nil, got %v", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan wrong type", func(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
if err := a.Scan(42); err == nil {
|
||||||
|
t.Error("expected error for unsupported scan type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json round-trip", func(t *testing.T) {
|
||||||
|
a := NewSqlStringArray([]string{"x", "y", "z"})
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != `["x","y","z"]` {
|
||||||
|
t.Errorf("unexpected JSON: %s", data)
|
||||||
|
}
|
||||||
|
var a2 SqlStringArray
|
||||||
|
if err := json.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if !a2.Valid || len(a2.Val) != 3 {
|
||||||
|
t.Fatalf("expected 3 valid elements, got %v", a2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json null", func(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "null" {
|
||||||
|
t.Errorf("expected null, got %s", data)
|
||||||
|
}
|
||||||
|
var a2 SqlStringArray
|
||||||
|
if err := json.Unmarshal([]byte("null"), &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if a2.Valid {
|
||||||
|
t.Error("expected invalid after unmarshaling null")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json unmarshal invalid type errors", func(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
if err := json.Unmarshal([]byte(`42`), &a); err == nil {
|
||||||
|
t.Error("expected error unmarshaling non-array JSON")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlInt16Array(t *testing.T) {
|
||||||
|
t.Run("scan and value", func(t *testing.T) {
|
||||||
|
var a SqlInt16Array
|
||||||
|
if err := a.Scan("{1,2,-3}"); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []int16{1, 2, -3}
|
||||||
|
for i := range want {
|
||||||
|
if a.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, want[i], a.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
if val != "{1,2,-3}" {
|
||||||
|
t.Errorf("expected {1,2,-3}, got %v", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan invalid element", func(t *testing.T) {
|
||||||
|
var a SqlInt16Array
|
||||||
|
if err := a.Scan("{1,abc}"); err == nil {
|
||||||
|
t.Error("expected error for non-numeric element")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json round-trip", func(t *testing.T) {
|
||||||
|
a := NewSqlInt16Array([]int16{5, 10, 15})
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlInt16Array
|
||||||
|
if err := json.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if a2.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, v, a2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlInt32Array(t *testing.T) {
|
||||||
|
a := NewSqlInt32Array([]int32{100000, -200000})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlInt32Array
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if b.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, v, b.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var c SqlInt32Array
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if c.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, v, c.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlInt64Array(t *testing.T) {
|
||||||
|
a := NewSqlInt64Array([]int64{9223372036854775807, -9223372036854775808})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlInt64Array
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if b.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, v, b.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("null json", func(t *testing.T) {
|
||||||
|
var n SqlInt64Array
|
||||||
|
data, _ := json.Marshal(n)
|
||||||
|
if string(data) != "null" {
|
||||||
|
t.Errorf("expected null, got %s", data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlFloat32Array(t *testing.T) {
|
||||||
|
a := NewSqlFloat32Array([]float32{1.5, -2.25, 0})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlFloat32Array
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if b.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, v, b.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var c SqlFloat32Array
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if c.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, v, c.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlFloat64Array(t *testing.T) {
|
||||||
|
a := NewSqlFloat64Array([]float64{3.14159, -2.71828})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlFloat64Array
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if b.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, v, b.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlBoolArray(t *testing.T) {
|
||||||
|
t.Run("scan various truthy forms", func(t *testing.T) {
|
||||||
|
var a SqlBoolArray
|
||||||
|
if err := a.Scan("{t,f,true,false,1,0,yes}"); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []bool{true, false, true, false, true, false, true}
|
||||||
|
for i := range want {
|
||||||
|
if a.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, want[i], a.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("value formatting", func(t *testing.T) {
|
||||||
|
a := NewSqlBoolArray([]bool{true, false})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
if val != "{t,f}" {
|
||||||
|
t.Errorf("expected {t,f}, got %v", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json round-trip", func(t *testing.T) {
|
||||||
|
a := NewSqlBoolArray([]bool{true, false, true})
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlBoolArray
|
||||||
|
if err := json.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if a2.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, v, a2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlUUIDArray(t *testing.T) {
|
||||||
|
u1, u2 := uuid.New(), uuid.New()
|
||||||
|
|
||||||
|
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||||
|
a := NewSqlUUIDArray([]uuid.UUID{u1, u2})
|
||||||
|
val, err := a.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
var b SqlUUIDArray
|
||||||
|
if err := b.Scan(val); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
if b.Val[0] != u1 || b.Val[1] != u2 {
|
||||||
|
t.Errorf("expected [%v %v], got %v", u1, u2, b.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan invalid uuid element", func(t *testing.T) {
|
||||||
|
var a SqlUUIDArray
|
||||||
|
if err := a.Scan("{not-a-uuid}"); err == nil {
|
||||||
|
t.Error("expected error for invalid uuid element")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json round-trip", func(t *testing.T) {
|
||||||
|
a := NewSqlUUIDArray([]uuid.UUID{u1, u2})
|
||||||
|
data, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlUUIDArray
|
||||||
|
if err := json.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if a2.Val[0] != u1 || a2.Val[1] != u2 {
|
||||||
|
t.Errorf("expected [%v %v], got %v", u1, u2, a2.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlVector(t *testing.T) {
|
||||||
|
t.Run("scan and value round-trip", func(t *testing.T) {
|
||||||
|
var v SqlVector
|
||||||
|
if err := v.Scan("[1,2.5,-3]"); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []float32{1, 2.5, -3}
|
||||||
|
for i := range want {
|
||||||
|
if v.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, want[i], v.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val, err := v.Value()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Value failed: %v", err)
|
||||||
|
}
|
||||||
|
if val != "[1,2.5,-3]" {
|
||||||
|
t.Errorf("expected [1,2.5,-3], got %v", val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan empty vector", func(t *testing.T) {
|
||||||
|
var v SqlVector
|
||||||
|
if err := v.Scan("[]"); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
if !v.Valid || len(v.Val) != 0 {
|
||||||
|
t.Errorf("expected valid empty vector, got %v", v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scan invalid literal", func(t *testing.T) {
|
||||||
|
var v SqlVector
|
||||||
|
if err := v.Scan("not-a-vector"); err == nil {
|
||||||
|
t.Error("expected error for invalid vector literal")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json round-trip", func(t *testing.T) {
|
||||||
|
v := NewSqlVector([]float32{0.1, 0.2, 0.3})
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var v2 SqlVector
|
||||||
|
if err := json.Unmarshal(data, &v2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, f := range v.Val {
|
||||||
|
if v2.Val[i] != f {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, f, v2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json null", func(t *testing.T) {
|
||||||
|
var v SqlVector
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "null" {
|
||||||
|
t.Errorf("expected null, got %s", data)
|
||||||
|
}
|
||||||
|
var v2 SqlVector
|
||||||
|
if err := json.Unmarshal([]byte("null"), &v2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if v2.Valid {
|
||||||
|
t.Error("expected invalid after unmarshaling null")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
// Package spectypes provides nullable SQL types with automatic casting and conversion methods.
|
// Package sqltypes provides nullable SQL types with automatic casting and conversion methods.
|
||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tryParseDT attempts to parse a string into a time.Time using various formats.
|
// tryParseDT attempts to parse a string into a time.Time using various formats.
|
||||||
@@ -120,15 +122,22 @@ func (n *SqlNull[T]) FromString(s string) error {
|
|||||||
|
|
||||||
var zero T
|
var zero T
|
||||||
switch any(zero).(type) {
|
switch any(zero).(type) {
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
case int, int8, int16, int32, int64:
|
||||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||||
reflect.ValueOf(&n.Val).Elem().SetInt(i)
|
reflect.ValueOf(&n.Val).Elem().SetInt(i)
|
||||||
n.Valid = true
|
n.Valid = true
|
||||||
}
|
} else if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
|
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
|
||||||
n.Valid = true
|
n.Valid = true
|
||||||
}
|
}
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
if u, err := strconv.ParseUint(s, 10, 64); err == nil {
|
||||||
|
reflect.ValueOf(&n.Val).Elem().SetUint(u)
|
||||||
|
n.Valid = true
|
||||||
|
} else if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 {
|
||||||
|
reflect.ValueOf(&n.Val).Elem().SetUint(uint64(f))
|
||||||
|
n.Valid = true
|
||||||
|
}
|
||||||
case float32, float64:
|
case float32, float64:
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
reflect.ValueOf(&n.Val).Elem().SetFloat(f)
|
reflect.ValueOf(&n.Val).Elem().SetFloat(f)
|
||||||
@@ -232,6 +241,104 @@ func (n *SqlNull[T]) UnmarshalJSON(b []byte) error {
|
|||||||
return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val)
|
return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements yaml.Marshaler.
|
||||||
|
func (n SqlNull[T]) MarshalYAML() (any, error) {
|
||||||
|
if !n.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||||
|
if b, ok := any(n.Val).([]byte); ok {
|
||||||
|
return base64.StdEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||||
|
func (n *SqlNull[T]) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
n.Valid = false
|
||||||
|
n.Val = *new(T)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and decode from base64.
|
||||||
|
var zero T
|
||||||
|
if _, ok := any(zero).([]byte); ok {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err == nil {
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||||
|
n.Val = any(decoded).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n.Val = any([]byte(s)).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var val T
|
||||||
|
if err := value.Decode(&val); err == nil {
|
||||||
|
n.Val = val
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: decode as string and parse.
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err == nil {
|
||||||
|
return n.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot unmarshal %q into SqlNull[%T]", value.Value, n.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML implements xml.Marshaler.
|
||||||
|
func (n SqlNull[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !n.Valid {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||||
|
if b, ok := any(n.Val).([]byte); ok {
|
||||||
|
return e.EncodeElement(base64.StdEncoding.EncodeToString(b), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.EncodeElement(n.Val, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements xml.Unmarshaler.
|
||||||
|
//
|
||||||
|
// XML has no native null representation, so an empty element unmarshals to
|
||||||
|
// an invalid (null) value rather than a zero-value-but-valid one.
|
||||||
|
func (n *SqlNull[T]) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
n.Valid = false
|
||||||
|
n.Val = *new(T)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero T
|
||||||
|
if _, ok := any(zero).([]byte); ok {
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||||
|
n.Val = any(decoded).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n.Val = any([]byte(s)).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
// String implements fmt.Stringer.
|
||||||
func (n SqlNull[T]) String() string {
|
func (n SqlNull[T]) String() string {
|
||||||
if !n.Valid {
|
if !n.Valid {
|
||||||
@@ -329,6 +436,7 @@ type (
|
|||||||
SqlInt16 = SqlNull[int16]
|
SqlInt16 = SqlNull[int16]
|
||||||
SqlInt32 = SqlNull[int32]
|
SqlInt32 = SqlNull[int32]
|
||||||
SqlInt64 = SqlNull[int64]
|
SqlInt64 = SqlNull[int64]
|
||||||
|
SqlFloat32 = SqlNull[float32]
|
||||||
SqlFloat64 = SqlNull[float64]
|
SqlFloat64 = SqlNull[float64]
|
||||||
SqlBool = SqlNull[bool]
|
SqlBool = SqlNull[bool]
|
||||||
SqlString = SqlNull[string]
|
SqlString = SqlNull[string]
|
||||||
@@ -343,7 +451,7 @@ func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
|||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, t.Val.Format("2006-01-02T15:04:05"))), nil
|
return fmt.Appendf(nil, `"%s"`, t.Val.Format("2006-01-02T15:04:05")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
||||||
@@ -363,6 +471,49 @@ func (t SqlTimeStamp) Value() (driver.Value, error) {
|
|||||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
||||||
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
||||||
|
t.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t SqlTimeStamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(t.Val.Format("2006-01-02T15:04:05"), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
t.Valid = false
|
||||||
|
t.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Val = tm
|
||||||
|
t.Valid = !tm.IsZero() && tm.Format("2006-01-02T15:04:05") != "0001-01-01T00:00:00"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlTimeStampNow() SqlTimeStamp {
|
func SqlTimeStampNow() SqlTimeStamp {
|
||||||
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -378,7 +529,7 @@ func (d SqlDate) MarshalJSON() ([]byte, error) {
|
|||||||
if strings.HasPrefix(s, "0001-01-01") {
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, s)), nil
|
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SqlDate) UnmarshalJSON(b []byte) error {
|
func (d *SqlDate) UnmarshalJSON(b []byte) error {
|
||||||
@@ -413,6 +564,57 @@ func (d SqlDate) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d SqlDate) MarshalYAML() (any, error) {
|
||||||
|
if !d.Valid || d.Val.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s := d.Val.Format("2006-01-02")
|
||||||
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqlDate) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := d.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" {
|
||||||
|
d.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !d.Valid || d.Val.IsZero() {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
s := d.Val.Format("2006-01-02")
|
||||||
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(s, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqlDate) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := dec.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
d.Valid = false
|
||||||
|
d.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Val = tm
|
||||||
|
d.Valid = !tm.IsZero() && tm.Format("2006-01-02") > "0001-01-01"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlDateNow() SqlDate {
|
func SqlDateNow() SqlDate {
|
||||||
return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -428,7 +630,7 @@ func (t SqlTime) MarshalJSON() ([]byte, error) {
|
|||||||
if s == "00:00:00" {
|
if s == "00:00:00" {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, s)), nil
|
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTime) UnmarshalJSON(b []byte) error {
|
func (t *SqlTime) UnmarshalJSON(b []byte) error {
|
||||||
@@ -455,6 +657,57 @@ func (t SqlTime) String() string {
|
|||||||
return t.Val.Format("15:04:05")
|
return t.Val.Format("15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t SqlTime) MarshalYAML() (any, error) {
|
||||||
|
if !t.Valid || t.Val.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s := t.Val.Format("15:04:05")
|
||||||
|
if s == "00:00:00" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTime) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Valid && t.Val.Format("15:04:05") == "00:00:00" {
|
||||||
|
t.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t SqlTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !t.Valid || t.Val.IsZero() {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
s := t.Val.Format("15:04:05")
|
||||||
|
if s == "00:00:00" {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(s, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
t.Valid = false
|
||||||
|
t.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Val = tm
|
||||||
|
t.Valid = !tm.IsZero() && tm.Format("15:04:05") != "00:00:00"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlTimeNow() SqlTime {
|
func SqlTimeNow() SqlTime {
|
||||||
return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -462,6 +715,11 @@ func SqlTimeNow() SqlTime {
|
|||||||
// SqlJSONB - Nullable JSONB as []byte.
|
// SqlJSONB - Nullable JSONB as []byte.
|
||||||
type SqlJSONB []byte
|
type SqlJSONB []byte
|
||||||
|
|
||||||
|
// SqlJSON - Nullable JSON as []byte. PostgreSQL's json and jsonb types share
|
||||||
|
// the same textual representation and Go marshalling behavior, differing only
|
||||||
|
// in server-side storage, so SqlJSON is an alias of SqlJSONB.
|
||||||
|
type SqlJSON = SqlJSONB
|
||||||
|
|
||||||
// Scan implements sql.Scanner.
|
// Scan implements sql.Scanner.
|
||||||
func (n *SqlJSONB) Scan(value any) error {
|
func (n *SqlJSONB) Scan(value any) error {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
@@ -518,6 +776,67 @@ func (n *SqlJSONB) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements yaml.Marshaler. The underlying JSON is decoded into
|
||||||
|
// a generic value first so it renders as native YAML mappings/sequences
|
||||||
|
// rather than an embedded JSON string.
|
||||||
|
func (n SqlJSONB) MarshalYAML() (any, error) {
|
||||||
|
if len(n) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(n, &v); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||||
|
func (n *SqlJSONB) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
*n = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := value.Decode(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*n = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML implements xml.Marshaler. JSON has no clean structural mapping
|
||||||
|
// to XML, so the raw JSON text is emitted as the element's text content.
|
||||||
|
func (n SqlJSONB) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if len(n) == 0 {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
var obj any
|
||||||
|
if err := json.Unmarshal(n, &obj); err != nil {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(string(n), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements xml.Unmarshaler, reading back the raw JSON text
|
||||||
|
// written by MarshalXML.
|
||||||
|
func (n *SqlJSONB) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
*n = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*n = []byte(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (n SqlJSONB) AsMap() (map[string]any, error) {
|
func (n SqlJSONB) AsMap() (map[string]any, error) {
|
||||||
if len(n) == 0 {
|
if len(n) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -625,6 +944,10 @@ func NewSqlInt64(v int64) SqlInt64 {
|
|||||||
return SqlInt64{Val: v, Valid: true}
|
return SqlInt64{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSqlFloat32(v float32) SqlFloat32 {
|
||||||
|
return SqlFloat32{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat64(v float64) SqlFloat64 {
|
func NewSqlFloat64(v float64) SqlFloat64 {
|
||||||
return SqlFloat64{Val: v, Valid: true}
|
return SqlFloat64{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package sqltypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSqlNull_FromString_UnsignedInt guards against a regression where
|
||||||
|
// FromString called reflect.Value.SetInt on unsigned-kind fields, which
|
||||||
|
// panics since SetInt only accepts Int/Int8/.../Int64 kinds.
|
||||||
|
func TestSqlNull_FromString_UnsignedInt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint32
|
||||||
|
}{
|
||||||
|
{"simple", "123", 123},
|
||||||
|
{"zero", "0", 0},
|
||||||
|
{"large", "4000000000", 4000000000},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var n SqlNull[uint32]
|
||||||
|
if err := n.FromString(tt.input); err != nil {
|
||||||
|
t.Fatalf("FromString failed: %v", err)
|
||||||
|
}
|
||||||
|
if !n.Valid {
|
||||||
|
t.Fatalf("expected valid=true")
|
||||||
|
}
|
||||||
|
if n.Val != tt.expected {
|
||||||
|
t.Errorf("expected %d, got %d", tt.expected, n.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSqlNull_FromString_Int64Precision guards against a regression where
|
||||||
|
// FromString unconditionally re-parsed the string as float64 after a
|
||||||
|
// successful ParseInt, corrupting large int64 values due to float rounding
|
||||||
|
// (e.g. math.MaxInt64 became negative).
|
||||||
|
func TestSqlNull_FromString_Int64Precision(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int64
|
||||||
|
}{
|
||||||
|
{"max int64", "9223372036854775807", 9223372036854775807},
|
||||||
|
{"min int64", "-9223372036854775808", -9223372036854775808},
|
||||||
|
{"large safe value", "1234567890123456", 1234567890123456},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var n SqlNull[int64]
|
||||||
|
if err := n.FromString(tt.input); err != nil {
|
||||||
|
t.Fatalf("FromString failed: %v", err)
|
||||||
|
}
|
||||||
|
if !n.Valid {
|
||||||
|
t.Fatalf("expected valid=true")
|
||||||
|
}
|
||||||
|
if n.Val != tt.expected {
|
||||||
|
t.Errorf("expected %d, got %d", tt.expected, n.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSqlNull_FromString_FloatFallback verifies that non-integral strings
|
||||||
|
// still parse via the float fallback path for integer-kind fields.
|
||||||
|
func TestSqlNull_FromString_FloatFallback(t *testing.T) {
|
||||||
|
var n SqlNull[int32]
|
||||||
|
if err := n.FromString("42.9"); err != nil {
|
||||||
|
t.Fatalf("FromString failed: %v", err)
|
||||||
|
}
|
||||||
|
if !n.Valid || n.Val != 42 {
|
||||||
|
t.Errorf("expected valid int32=42, got valid=%v val=%d", n.Valid, n.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
var u SqlNull[uint32]
|
||||||
|
if err := u.FromString("42.9"); err != nil {
|
||||||
|
t.Fatalf("FromString failed: %v", err)
|
||||||
|
}
|
||||||
|
if !u.Valid || u.Val != 42 {
|
||||||
|
t.Errorf("expected valid uint32=42, got valid=%v val=%d", u.Valid, u.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
var neg SqlNull[uint32]
|
||||||
|
if err := neg.FromString("-1.5"); err != nil {
|
||||||
|
t.Fatalf("FromString failed: %v", err)
|
||||||
|
}
|
||||||
|
if neg.Valid {
|
||||||
|
t.Errorf("expected invalid for negative value into unsigned type, got %v", neg.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSqlNull_UnsignedTypes_ScanAndJSON exercises Scan and JSON round-trip
|
||||||
|
// for every unsigned alias to make sure none of them panic.
|
||||||
|
func TestSqlNull_UnsignedTypes_ScanAndJSON(t *testing.T) {
|
||||||
|
t.Run("uint8 scan from string", func(t *testing.T) {
|
||||||
|
var n SqlNull[uint8]
|
||||||
|
if err := n.Scan("200"); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
if !n.Valid || n.Val != 200 {
|
||||||
|
t.Errorf("expected valid uint8=200, got valid=%v val=%d", n.Valid, n.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uint64 json round-trip", func(t *testing.T) {
|
||||||
|
n := Null(uint64(18446744073709551615), true)
|
||||||
|
data, err := json.Marshal(n)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var n2 SqlNull[uint64]
|
||||||
|
if err := json.Unmarshal(data, &n2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if n2.Val != n.Val {
|
||||||
|
t.Errorf("expected %d, got %d", n.Val, n2.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uint scan from numeric string fallback", func(t *testing.T) {
|
||||||
|
var n SqlNull[uint]
|
||||||
|
if err := n.Scan([]byte("77")); err != nil {
|
||||||
|
t.Fatalf("Scan failed: %v", err)
|
||||||
|
}
|
||||||
|
if !n.Valid || n.Val != 77 {
|
||||||
|
t.Errorf("expected valid uint=77, got valid=%v val=%d", n.Valid, n.Val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
@@ -0,0 +1,678 @@
|
|||||||
|
package sqltypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── SqlNull: YAML ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSqlNull_YAML_Int(t *testing.T) {
|
||||||
|
n := NewSqlInt32(42)
|
||||||
|
data, err := yaml.Marshal(n)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "42\n" {
|
||||||
|
t.Errorf("expected \"42\\n\", got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var n2 SqlInt32
|
||||||
|
if err := yaml.Unmarshal(data, &n2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if n2.Int64() != 42 {
|
||||||
|
t.Errorf("expected 42, got %d", n2.Int64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_YAML_Null(t *testing.T) {
|
||||||
|
var n SqlInt32
|
||||||
|
data, err := yaml.Marshal(n)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "null\n" {
|
||||||
|
t.Errorf("expected \"null\\n\", got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var n2 SqlInt32
|
||||||
|
if err := yaml.Unmarshal([]byte("null\n"), &n2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if n2.Valid {
|
||||||
|
t.Error("expected invalid after unmarshaling null")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~ is also a YAML null.
|
||||||
|
var n3 SqlInt32
|
||||||
|
if err := yaml.Unmarshal([]byte("~\n"), &n3); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if n3.Valid {
|
||||||
|
t.Error("expected invalid after unmarshaling ~")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_YAML_String(t *testing.T) {
|
||||||
|
s := NewSqlString("hello world")
|
||||||
|
data, err := yaml.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var s2 SqlString
|
||||||
|
if err := yaml.Unmarshal(data, &s2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if s2.String() != "hello world" {
|
||||||
|
t.Errorf("expected %q, got %q", "hello world", s2.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_YAML_UUID(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
u := NewSqlUUID(id)
|
||||||
|
data, err := yaml.Marshal(u)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != id.String()+"\n" {
|
||||||
|
t.Errorf("expected %q, got %q", id.String()+"\n", string(data))
|
||||||
|
}
|
||||||
|
var u2 SqlUUID
|
||||||
|
if err := yaml.Unmarshal(data, &u2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if u2.UUID() != id {
|
||||||
|
t.Errorf("expected %v, got %v", id, u2.UUID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_YAML_ByteArray_Base64(t *testing.T) {
|
||||||
|
orig := []byte{0x01, 0x02, 0xFF}
|
||||||
|
b := NewSqlByteArray(orig)
|
||||||
|
data, err := yaml.Marshal(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var b2 SqlByteArray
|
||||||
|
if err := yaml.Unmarshal(data, &b2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(b2.Val) != string(orig) {
|
||||||
|
t.Errorf("expected %v, got %v", orig, b2.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlNull: XML ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type xmlIntWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Value SqlInt32 `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_XML_Int(t *testing.T) {
|
||||||
|
w := xmlIntWrapper{Value: NewSqlInt32(99)}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlIntWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.Value.Int64() != 99 {
|
||||||
|
t.Errorf("expected 99, got %d", w2.Value.Int64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_XML_Null(t *testing.T) {
|
||||||
|
w := xmlIntWrapper{}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlIntWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.Value.Valid {
|
||||||
|
t.Errorf("expected invalid, got %v", w2.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlUUIDWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
ID SqlUUID `xml:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_XML_UUID(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
w := xmlUUIDWrapper{ID: NewSqlUUID(id)}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlUUIDWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.ID.UUID() != id {
|
||||||
|
t.Errorf("expected %v, got %v", id, w2.ID.UUID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlByteWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Data SqlByteArray `xml:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlNull_XML_ByteArray_Base64(t *testing.T) {
|
||||||
|
orig := []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||||
|
w := xmlByteWrapper{Data: NewSqlByteArray(orig)}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlByteWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(w2.Data.Val) != string(orig) {
|
||||||
|
t.Errorf("expected %v, got %v", orig, w2.Data.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlTimeStamp / SqlDate / SqlTime: YAML + XML ────────────────────────────
|
||||||
|
|
||||||
|
func TestSqlTimeStamp_YAML(t *testing.T) {
|
||||||
|
ts := NewSqlTimeStamp(time.Date(2024, 6, 15, 9, 30, 0, 0, time.UTC))
|
||||||
|
data, err := yaml.Marshal(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "2024-06-15T09:30:00\n" {
|
||||||
|
t.Errorf("unexpected YAML: %q", string(data))
|
||||||
|
}
|
||||||
|
var ts2 SqlTimeStamp
|
||||||
|
if err := yaml.Unmarshal(data, &ts2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if ts2.Time().Format("2006-01-02T15:04:05") != "2024-06-15T09:30:00" {
|
||||||
|
t.Errorf("expected 2024-06-15T09:30:00, got %v", ts2.Time())
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero SqlTimeStamp
|
||||||
|
zdata, err := yaml.Marshal(zero)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(zdata) != "null\n" {
|
||||||
|
t.Errorf("expected null, got %q", zdata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlTimeStampWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
At SqlTimeStamp `xml:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlTimeStamp_XML(t *testing.T) {
|
||||||
|
w := xmlTimeStampWrapper{At: NewSqlTimeStamp(time.Date(2024, 6, 15, 9, 30, 0, 0, time.UTC))}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlTimeStampWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.At.Time().Format("2006-01-02T15:04:05") != "2024-06-15T09:30:00" {
|
||||||
|
t.Errorf("expected 2024-06-15T09:30:00, got %v", w2.At.Time())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlDate_YAML(t *testing.T) {
|
||||||
|
d := NewSqlDate(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC))
|
||||||
|
data, err := yaml.Marshal(d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "\"2024-06-15\"\n" {
|
||||||
|
t.Errorf("unexpected YAML: %q", string(data))
|
||||||
|
}
|
||||||
|
var d2 SqlDate
|
||||||
|
if err := yaml.Unmarshal(data, &d2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if d2.String() != "2024-06-15" {
|
||||||
|
t.Errorf("expected 2024-06-15, got %q", d2.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlDateWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Day SqlDate `xml:"day"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlDate_XML(t *testing.T) {
|
||||||
|
w := xmlDateWrapper{Day: NewSqlDate(time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC))}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlDateWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.Day.String() != "2024-06-15" {
|
||||||
|
t.Errorf("expected 2024-06-15, got %q", w2.Day.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlTime_YAML(t *testing.T) {
|
||||||
|
tm := NewSqlTime(time.Date(0, 1, 1, 14, 5, 9, 0, time.UTC))
|
||||||
|
data, err := yaml.Marshal(tm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "\"14:05:09\"\n" {
|
||||||
|
t.Errorf("unexpected YAML: %q", string(data))
|
||||||
|
}
|
||||||
|
var tm2 SqlTime
|
||||||
|
if err := yaml.Unmarshal(data, &tm2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if tm2.String() != "14:05:09" {
|
||||||
|
t.Errorf("expected 14:05:09, got %q", tm2.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlTimeWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
At SqlTime `xml:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlTime_XML(t *testing.T) {
|
||||||
|
w := xmlTimeWrapper{At: NewSqlTime(time.Date(0, 1, 1, 14, 5, 9, 0, time.UTC))}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlTimeWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if w2.At.String() != "14:05:09" {
|
||||||
|
t.Errorf("expected 14:05:09, got %q", w2.At.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SqlJSONB: YAML + XML ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSqlJSONB_YAML(t *testing.T) {
|
||||||
|
j := SqlJSONB(`{"name":"test","count":42}`)
|
||||||
|
data, err := yaml.Marshal(j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded map[string]any
|
||||||
|
if err := yaml.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("failed decoding produced YAML: %v", err)
|
||||||
|
}
|
||||||
|
if decoded["name"] != "test" {
|
||||||
|
t.Errorf("expected name=test, got %v", decoded["name"])
|
||||||
|
}
|
||||||
|
|
||||||
|
var j2 SqlJSONB
|
||||||
|
if err := yaml.Unmarshal(data, &j2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
m, err := j2.AsMap()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AsMap failed: %v", err)
|
||||||
|
}
|
||||||
|
if m["name"] != "test" {
|
||||||
|
t.Errorf("expected name=test, got %v", m["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlJSONB_YAML_Null(t *testing.T) {
|
||||||
|
var j SqlJSONB
|
||||||
|
data, err := yaml.Marshal(j)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "null\n" {
|
||||||
|
t.Errorf("expected null, got %q", data)
|
||||||
|
}
|
||||||
|
var j2 SqlJSONB
|
||||||
|
if err := yaml.Unmarshal([]byte("null\n"), &j2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if j2 != nil {
|
||||||
|
t.Errorf("expected nil, got %v", j2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlJSONBWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Meta SqlJSONB `xml:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlJSONB_XML(t *testing.T) {
|
||||||
|
w := xmlJSONBWrapper{Meta: SqlJSONB(`{"key":"value"}`)}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlJSONBWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
m, err := w2.Meta.AsMap()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AsMap failed: %v", err)
|
||||||
|
}
|
||||||
|
if m["key"] != "value" {
|
||||||
|
t.Errorf("expected key=value, got %v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Array types: YAML ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func TestSqlStringArray_YAML(t *testing.T) {
|
||||||
|
a := NewSqlStringArray([]string{"a", "b", "c"})
|
||||||
|
data, err := yaml.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlStringArray
|
||||||
|
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(a2.Val) != 3 || a2.Val[1] != "b" {
|
||||||
|
t.Errorf("unexpected value %v", a2.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlStringArray_YAML_Null(t *testing.T) {
|
||||||
|
var a SqlStringArray
|
||||||
|
data, err := yaml.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "null\n" {
|
||||||
|
t.Errorf("expected null, got %q", data)
|
||||||
|
}
|
||||||
|
var a2 SqlStringArray
|
||||||
|
if err := yaml.Unmarshal([]byte("null\n"), &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if a2.Valid {
|
||||||
|
t.Error("expected invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlInt32Array_YAML(t *testing.T) {
|
||||||
|
a := NewSqlInt32Array([]int32{1, 2, 3})
|
||||||
|
data, err := yaml.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlInt32Array
|
||||||
|
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range a.Val {
|
||||||
|
if a2.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, v, a2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlUUIDArray_YAML(t *testing.T) {
|
||||||
|
ids := []uuid.UUID{uuid.New(), uuid.New()}
|
||||||
|
a := NewSqlUUIDArray(ids)
|
||||||
|
data, err := yaml.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var a2 SqlUUIDArray
|
||||||
|
if err := yaml.Unmarshal(data, &a2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, v := range ids {
|
||||||
|
if a2.Val[i] != v {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, v, a2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlVector_YAML(t *testing.T) {
|
||||||
|
v := NewSqlVector([]float32{0.1, 0.2, 0.3})
|
||||||
|
data, err := yaml.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var v2 SqlVector
|
||||||
|
if err := yaml.Unmarshal(data, &v2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, f := range v.Val {
|
||||||
|
if v2.Val[i] != f {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, f, v2.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Array types: XML ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type xmlStringArrayWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Tags SqlStringArray `xml:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlStringArray_XML(t *testing.T) {
|
||||||
|
w := xmlStringArrayWrapper{Tags: NewSqlStringArray([]string{"x", "y", "z"})}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlStringArrayWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []string{"x", "y", "z"}
|
||||||
|
if len(w2.Tags.Val) != len(want) {
|
||||||
|
t.Fatalf("expected %v, got %v", want, w2.Tags.Val)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if w2.Tags.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %q, got %q", i, want[i], w2.Tags.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlStringArray_XML_Empty(t *testing.T) {
|
||||||
|
w := xmlStringArrayWrapper{}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlStringArrayWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(w2.Tags.Val) != 0 {
|
||||||
|
t.Errorf("expected empty slice, got %v", w2.Tags.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlInt64ArrayWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Scores SqlInt64Array `xml:"scores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlInt64Array_XML(t *testing.T) {
|
||||||
|
w := xmlInt64ArrayWrapper{Scores: NewSqlInt64Array([]int64{10, 20, 30})}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlInt64ArrayWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []int64{10, 20, 30}
|
||||||
|
for i := range want {
|
||||||
|
if w2.Scores.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %d, got %d", i, want[i], w2.Scores.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlUUIDArrayWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
IDs SqlUUIDArray `xml:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlUUIDArray_XML(t *testing.T) {
|
||||||
|
ids := []uuid.UUID{uuid.New(), uuid.New()}
|
||||||
|
w := xmlUUIDArrayWrapper{IDs: NewSqlUUIDArray(ids)}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlUUIDArrayWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, id := range ids {
|
||||||
|
if w2.IDs.Val[i] != id {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, id, w2.IDs.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlVectorWrapper struct {
|
||||||
|
XMLName xml.Name `xml:"root"`
|
||||||
|
Embedding SqlVector `xml:"embedding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqlVector_XML(t *testing.T) {
|
||||||
|
w := xmlVectorWrapper{Embedding: NewSqlVector([]float32{1.5, -2.5})}
|
||||||
|
data, err := xml.Marshal(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var w2 xmlVectorWrapper
|
||||||
|
if err := xml.Unmarshal(data, &w2); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
want := []float32{1.5, -2.5}
|
||||||
|
for i := range want {
|
||||||
|
if w2.Embedding.Val[i] != want[i] {
|
||||||
|
t.Errorf("index %d: expected %v, got %v", i, want[i], w2.Embedding.Val[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Combined struct round-trip ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
type yamlXMLRecord struct {
|
||||||
|
XMLName xml.Name `xml:"record" yaml:"-" json:"-"`
|
||||||
|
ID SqlUUID `xml:"id" yaml:"id"`
|
||||||
|
Name SqlString `xml:"name" yaml:"name"`
|
||||||
|
Age SqlInt32 `xml:"age" yaml:"age"`
|
||||||
|
Active SqlBool `xml:"active" yaml:"active"`
|
||||||
|
Created SqlTimeStamp `xml:"created" yaml:"created"`
|
||||||
|
Tags SqlStringArray `xml:"tags" yaml:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCombinedRecord_YAML_RoundTrip(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
original := yamlXMLRecord{
|
||||||
|
ID: NewSqlUUID(id),
|
||||||
|
Name: NewSqlString("Ada"),
|
||||||
|
Age: NewSqlInt32(36),
|
||||||
|
Active: NewSqlBool(true),
|
||||||
|
Created: NewSqlTimeStamp(time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)),
|
||||||
|
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded yamlXMLRecord
|
||||||
|
if err := yaml.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.ID.UUID() != id {
|
||||||
|
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||||
|
}
|
||||||
|
if decoded.Name.String() != "Ada" {
|
||||||
|
t.Errorf("Name: expected Ada, got %q", decoded.Name.String())
|
||||||
|
}
|
||||||
|
if decoded.Age.Int64() != 36 {
|
||||||
|
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||||
|
}
|
||||||
|
if !decoded.Active.Bool() {
|
||||||
|
t.Error("Active: expected true")
|
||||||
|
}
|
||||||
|
if decoded.Created.Time().Format("2006-01-02T15:04:05") != "2024-03-10T12:30:00" {
|
||||||
|
t.Errorf("Created: expected 2024-03-10T12:30:00, got %v", decoded.Created.Time())
|
||||||
|
}
|
||||||
|
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||||
|
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCombinedRecord_XML_RoundTrip(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
original := yamlXMLRecord{
|
||||||
|
ID: NewSqlUUID(id),
|
||||||
|
Name: NewSqlString("Ada"),
|
||||||
|
Age: NewSqlInt32(36),
|
||||||
|
Active: NewSqlBool(true),
|
||||||
|
Created: NewSqlTimeStamp(time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)),
|
||||||
|
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := xml.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded yamlXMLRecord
|
||||||
|
if err := xml.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.ID.UUID() != id {
|
||||||
|
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||||
|
}
|
||||||
|
if decoded.Name.String() != "Ada" {
|
||||||
|
t.Errorf("Name: expected Ada, got %q", decoded.Name.String())
|
||||||
|
}
|
||||||
|
if decoded.Age.Int64() != 36 {
|
||||||
|
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||||
|
}
|
||||||
|
if !decoded.Active.Bool() {
|
||||||
|
t.Error("Active: expected true")
|
||||||
|
}
|
||||||
|
if decoded.Created.Time().Format("2006-01-02T15:04:05") != "2024-03-10T12:30:00" {
|
||||||
|
t.Errorf("Created: expected 2024-03-10T12:30:00, got %v", decoded.Created.Time())
|
||||||
|
}
|
||||||
|
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||||
|
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package sqltypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// record mimics a typical DB model composed of sqltypes fields, exercising
|
||||||
|
// marshalling/unmarshalling of the whole set together as encoding/json would
|
||||||
|
// when used on a real struct (not just the individual types in isolation).
|
||||||
|
type record struct {
|
||||||
|
ID SqlUUID `json:"id"`
|
||||||
|
Name SqlString `json:"name"`
|
||||||
|
Bio SqlString `json:"bio"`
|
||||||
|
Age SqlInt32 `json:"age"`
|
||||||
|
Score SqlFloat64 `json:"score"`
|
||||||
|
Active SqlBool `json:"active"`
|
||||||
|
CreatedAt SqlTimeStamp `json:"created_at"`
|
||||||
|
BirthDate SqlDate `json:"birth_date"`
|
||||||
|
Avatar SqlByteArray `json:"avatar"`
|
||||||
|
Tags SqlStringArray `json:"tags"`
|
||||||
|
Scores SqlInt32Array `json:"scores"`
|
||||||
|
Metadata SqlJSONB `json:"metadata"`
|
||||||
|
Embedding SqlVector `json:"embedding"`
|
||||||
|
Extras []uuid.UUID `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStruct_JSON_RoundTrip_AllFieldsPresent(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
createdAt := time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC)
|
||||||
|
birthDate := time.Date(1990, 5, 20, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
original := record{
|
||||||
|
ID: NewSqlUUID(id),
|
||||||
|
Name: NewSqlString("Ada Lovelace"),
|
||||||
|
Bio: SqlString{}, // intentionally null
|
||||||
|
Age: NewSqlInt32(36),
|
||||||
|
Score: NewSqlFloat64(98.6),
|
||||||
|
Active: NewSqlBool(true),
|
||||||
|
CreatedAt: NewSqlTimeStamp(createdAt),
|
||||||
|
BirthDate: NewSqlDate(birthDate),
|
||||||
|
Avatar: NewSqlByteArray([]byte{0xDE, 0xAD, 0xBE, 0xEF}),
|
||||||
|
Tags: NewSqlStringArray([]string{"engineer", "mathematician"}),
|
||||||
|
Scores: NewSqlInt32Array([]int32{10, 20, 30}),
|
||||||
|
Metadata: SqlJSONB(`{"role":"admin"}`),
|
||||||
|
Embedding: NewSqlVector([]float32{0.1, 0.2, 0.3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded record
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.ID.UUID() != id {
|
||||||
|
t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID())
|
||||||
|
}
|
||||||
|
if decoded.Name.String() != "Ada Lovelace" {
|
||||||
|
t.Errorf("Name: expected Ada Lovelace, got %q", decoded.Name.String())
|
||||||
|
}
|
||||||
|
if decoded.Bio.Valid {
|
||||||
|
t.Errorf("Bio: expected invalid/null, got %v", decoded.Bio)
|
||||||
|
}
|
||||||
|
if decoded.Age.Int64() != 36 {
|
||||||
|
t.Errorf("Age: expected 36, got %d", decoded.Age.Int64())
|
||||||
|
}
|
||||||
|
if decoded.Score.Float64() != 98.6 {
|
||||||
|
t.Errorf("Score: expected 98.6, got %v", decoded.Score.Float64())
|
||||||
|
}
|
||||||
|
if !decoded.Active.Bool() {
|
||||||
|
t.Errorf("Active: expected true")
|
||||||
|
}
|
||||||
|
if decoded.CreatedAt.Time().Format("2006-01-02T15:04:05") != createdAt.Format("2006-01-02T15:04:05") {
|
||||||
|
t.Errorf("CreatedAt: expected %v, got %v", createdAt, decoded.CreatedAt.Time())
|
||||||
|
}
|
||||||
|
if decoded.BirthDate.String() != "1990-05-20" {
|
||||||
|
t.Errorf("BirthDate: expected 1990-05-20, got %q", decoded.BirthDate.String())
|
||||||
|
}
|
||||||
|
if string(decoded.Avatar.Val) != string(original.Avatar.Val) {
|
||||||
|
t.Errorf("Avatar: expected %v, got %v", original.Avatar.Val, decoded.Avatar.Val)
|
||||||
|
}
|
||||||
|
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" {
|
||||||
|
t.Errorf("Tags: unexpected value %v", decoded.Tags.Val)
|
||||||
|
}
|
||||||
|
if len(decoded.Scores.Val) != 3 || decoded.Scores.Val[2] != 30 {
|
||||||
|
t.Errorf("Scores: unexpected value %v", decoded.Scores.Val)
|
||||||
|
}
|
||||||
|
m, err := decoded.Metadata.AsMap()
|
||||||
|
if err != nil || m["role"] != "admin" {
|
||||||
|
t.Errorf("Metadata: expected role=admin, got %v (err=%v)", m, err)
|
||||||
|
}
|
||||||
|
if len(decoded.Embedding.Val) != 3 || decoded.Embedding.Val[1] != 0.2 {
|
||||||
|
t.Errorf("Embedding: unexpected value %v", decoded.Embedding.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStruct_JSON_RoundTrip_AllNull(t *testing.T) {
|
||||||
|
var original record
|
||||||
|
|
||||||
|
data, err := json.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded record
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.ID.Valid || decoded.Name.Valid || decoded.Age.Valid || decoded.Score.Valid ||
|
||||||
|
decoded.Active.Valid || decoded.CreatedAt.Valid || decoded.BirthDate.Valid ||
|
||||||
|
decoded.Avatar.Valid || decoded.Tags.Valid || decoded.Scores.Valid || decoded.Embedding.Valid {
|
||||||
|
t.Errorf("expected all fields invalid/null after round-trip, got %+v", decoded)
|
||||||
|
}
|
||||||
|
if decoded.Metadata != nil {
|
||||||
|
t.Errorf("expected nil Metadata, got %v", decoded.Metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStruct_JSON_UnmarshalFromRawJSON(t *testing.T) {
|
||||||
|
raw := `{
|
||||||
|
"id": "3e843d4e-6b3c-4f2e-9a1e-6f0b2f3c9d10",
|
||||||
|
"name": "Grace Hopper",
|
||||||
|
"bio": null,
|
||||||
|
"age": 85,
|
||||||
|
"score": 100,
|
||||||
|
"active": false,
|
||||||
|
"created_at": "2023-12-01T08:00:00",
|
||||||
|
"birth_date": "1906-12-09",
|
||||||
|
"avatar": "AQIDBA==",
|
||||||
|
"tags": ["navy", "compiler"],
|
||||||
|
"scores": [1, 2, 3],
|
||||||
|
"metadata": {"key": "value"},
|
||||||
|
"embedding": [1.0, 2.0]
|
||||||
|
}`
|
||||||
|
|
||||||
|
var decoded record
|
||||||
|
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded.Name.String() != "Grace Hopper" {
|
||||||
|
t.Errorf("expected Grace Hopper, got %q", decoded.Name.String())
|
||||||
|
}
|
||||||
|
if decoded.Age.Int64() != 85 {
|
||||||
|
t.Errorf("expected age 85, got %d", decoded.Age.Int64())
|
||||||
|
}
|
||||||
|
if decoded.Active.Valid && decoded.Active.Bool() {
|
||||||
|
t.Errorf("expected active=false")
|
||||||
|
}
|
||||||
|
if string(decoded.Avatar.Val) != string([]byte{1, 2, 3, 4}) {
|
||||||
|
t.Errorf("expected avatar bytes [1 2 3 4], got %v", decoded.Avatar.Val)
|
||||||
|
}
|
||||||
|
if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[1] != "compiler" {
|
||||||
|
t.Errorf("unexpected tags %v", decoded.Tags.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
+17
-15
@@ -6,6 +6,8 @@ Generates Go source files with Bun model definitions from database schema inform
|
|||||||
|
|
||||||
The Bun Writer converts RelSpec's internal database model representation into Go source code with Bun struct definitions, complete with proper tags, relationships, and table configuration.
|
The Bun Writer converts RelSpec's internal database model representation into Go source code with Bun struct definitions, complete with proper tags, relationships, and table configuration.
|
||||||
|
|
||||||
|
With `--types sqltypes`, nullable fields use the [`pkg/sqltypes`](../../sqltypes/README.md) package.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Generates Bun-compatible Go structs
|
- Generates Bun-compatible Go structs
|
||||||
@@ -46,19 +48,19 @@ func main() {
|
|||||||
### CLI Examples
|
### CLI Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate Bun models from a DBML schema (default: resolvespec types)
|
# Generate Bun models from a DBML schema (default: baselib pointer types)
|
||||||
relspec convert --from dbml --from-path schema.dbml \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--to bun --to-path models.go --package models
|
--to bun --to-path models.go --package models
|
||||||
|
|
||||||
# Use standard library database/sql nullable types instead of resolvespec
|
# Use standard library database/sql nullable types instead
|
||||||
relspec convert --from dbml --from-path schema.dbml \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--to bun --to-path models.go --package models \
|
--to bun --to-path models.go --package models \
|
||||||
--types stdlib
|
--types stdlib
|
||||||
|
|
||||||
# Explicitly select resolvespec types (same as omitting --types)
|
# Select sqltypes package types (git.warky.dev/wdevs/relspecgo/pkg/sqltypes)
|
||||||
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
||||||
--to bun --to-path models.go --package models \
|
--to bun --to-path models.go --package models \
|
||||||
--types resolvespec
|
--types sqltypes
|
||||||
|
|
||||||
# Multi-file output (one file per table)
|
# Multi-file output (one file per table)
|
||||||
relspec convert --from json --from-path schema.json \
|
relspec convert --from json --from-path schema.json \
|
||||||
@@ -67,13 +69,13 @@ relspec convert --from json --from-path schema.json \
|
|||||||
|
|
||||||
## Generated Code Examples
|
## Generated Code Examples
|
||||||
|
|
||||||
### Default — resolvespec types (`--types resolvespec`)
|
### sqltypes package types (`--types sqltypes`)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,9 +84,9 @@ type User struct {
|
|||||||
|
|
||||||
ID int64 `bun:"id,type:uuid,pk," json:"id"`
|
ID int64 `bun:"id,type:uuid,pk," json:"id"`
|
||||||
Username string `bun:"username,type:text,notnull," json:"username"`
|
Username string `bun:"username,type:text,notnull," json:"username"`
|
||||||
Email resolvespec_common.SqlString `bun:"email,type:text,nullzero," json:"email"`
|
Email sql_types.SqlString `bun:"email,type:text,nullzero," json:"email"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ type User struct {
|
|||||||
|
|
||||||
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
||||||
|
|
||||||
| SQL Type | NOT NULL (both) | Nullable — resolvespec | Nullable — stdlib |
|
| SQL Type | NOT NULL (both) | Nullable — sqltypes | Nullable — stdlib |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
||||||
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
||||||
@@ -142,7 +144,7 @@ The nullable type package is selected with `--types` (or `WriterOptions.Nullable
|
|||||||
| `uuid[]` | `SqlUUIDArray` | `SqlUUIDArray` | `[]string` |
|
| `uuid[]` | `SqlUUIDArray` | `SqlUUIDArray` | `[]string` |
|
||||||
| `vector` | `SqlVector` | `SqlVector` | `[]float32` |
|
| `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`.
|
\* In sqltypes 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
|
## Writer Options
|
||||||
|
|
||||||
@@ -151,11 +153,11 @@ The nullable type package is selected with `--types` (or `WriterOptions.Nullable
|
|||||||
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Use resolvespec types (default — omit NullableTypes or set to "resolvespec")
|
// Use sqltypes package types
|
||||||
options := &writers.WriterOptions{
|
options := &writers.WriterOptions{
|
||||||
OutputPath: "models.go",
|
OutputPath: "models.go",
|
||||||
PackageName: "models",
|
PackageName: "models",
|
||||||
NullableTypes: writers.NullableTypeResolveSpec,
|
NullableTypes: writers.NullableTypeSqlTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use standard library database/sql types
|
// Use standard library database/sql types
|
||||||
@@ -184,8 +186,8 @@ options := &writers.WriterOptions{
|
|||||||
|
|
||||||
- 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
|
- Nullable columns use plain Go pointer types (`*string`, `*time.Time`, …) by default; pass `--types sqltypes` to use `sql_types.SqlString`, `sql_types.SqlTimeStamp`, etc., or `--types stdlib` to use `sql.NullString`, `sql.NullTime`, etc.
|
||||||
- Array columns use `resolvespec_common.SqlStringArray`, `resolvespec_common.SqlInt32Array`, etc. by default; `--types stdlib` produces plain Go slices (`[]string`, `[]int32`, …)
|
- Array columns use plain Go slices (`[]string`, `[]int32`, …) by default; `--types sqltypes` uses `sql_types.SqlStringArray`, `sql_types.SqlInt32Array`, etc.
|
||||||
- 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
|
||||||
|
|||||||
@@ -141,10 +141,10 @@ func NewModelData(table *models.Table, schema string, typeMapper *TypeMapper, fl
|
|||||||
safeName := writers.SanitizeStructTagValue(col.Name)
|
safeName := writers.SanitizeStructTagValue(col.Name)
|
||||||
model.PrimaryKeyField = SnakeCaseToPascalCase(safeName)
|
model.PrimaryKeyField = SnakeCaseToPascalCase(safeName)
|
||||||
model.IDColumnName = safeName
|
model.IDColumnName = safeName
|
||||||
// Check if PK type is a SQL type (contains resolvespec_common or sql_types)
|
// Check if PK type is a SQL type (contains sql_types)
|
||||||
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
goType := typeMapper.SQLTypeToGoType(col.Type, col.NotNull)
|
||||||
model.PrimaryKeyType = goType
|
model.PrimaryKeyType = goType
|
||||||
model.PrimaryKeyIsSQL = strings.Contains(goType, "resolvespec_common") || strings.Contains(goType, "sql_types")
|
model.PrimaryKeyIsSQL = strings.Contains(goType, "sql_types")
|
||||||
model.PrimaryKeyIsStr = isStringLikePrimaryKeyType(goType)
|
model.PrimaryKeyIsStr = isStringLikePrimaryKeyType(goType)
|
||||||
model.PrimaryKeyIDType = "int64"
|
model.PrimaryKeyIDType = "int64"
|
||||||
if model.PrimaryKeyIsStr {
|
if model.PrimaryKeyIsStr {
|
||||||
@@ -203,7 +203,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", "resolvespec_common.SqlString", "resolvespec_common.SqlUUID":
|
case "string", "sql.NullString", "sql_types.SqlString", "sql_types.SqlUUID":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ 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 {
|
||||||
sqlTypesAlias string
|
sqlTypesAlias string
|
||||||
typeStyle string // writers.NullableTypeResolveSpec | writers.NullableTypeStdlib
|
typeStyle string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTypeMapper creates a new TypeMapper.
|
// NewTypeMapper creates a new TypeMapper.
|
||||||
// typeStyle should be writers.NullableTypeResolveSpec or writers.NullableTypeStdlib;
|
// typeStyle should be writers.NullableTypeSqlTypes, writers.NullableTypeStdlib, or
|
||||||
// an empty string defaults to resolvespec.
|
// writers.NullableTypeBaselib; an empty string defaults to baselib.
|
||||||
func NewTypeMapper(typeStyle string) *TypeMapper {
|
func NewTypeMapper(typeStyle string) *TypeMapper {
|
||||||
if typeStyle == "" {
|
if typeStyle == "" {
|
||||||
typeStyle = writers.NullableTypeBaselib
|
typeStyle = writers.NullableTypeBaselib
|
||||||
}
|
}
|
||||||
return &TypeMapper{
|
return &TypeMapper{
|
||||||
sqlTypesAlias: "resolvespec_common",
|
sqlTypesAlias: "sql_types",
|
||||||
typeStyle: typeStyle,
|
typeStyle: typeStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
|||||||
return tm.baselibNullableGoType(baseType)
|
return tm.baselibNullableGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvespec (default): use base Go types only for simple NOT NULL fields.
|
// sqltypes: 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)
|
||||||
}
|
}
|
||||||
@@ -106,11 +106,11 @@ func (tm *TypeMapper) baseGoType(sqlType string) string {
|
|||||||
return goType
|
return goType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to resolvespec type
|
// Default to sqltypes type
|
||||||
return tm.bunGoType(sqlType)
|
return tm.bunGoType(sqlType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bunGoType returns the Bun/ResolveSpec common type
|
// bunGoType returns the Bun/sqltypes common type
|
||||||
func (tm *TypeMapper) bunGoType(sqlType string) string {
|
func (tm *TypeMapper) bunGoType(sqlType string) string {
|
||||||
typeMap := map[string]string{
|
typeMap := map[string]string{
|
||||||
// Integer types
|
// Integer types
|
||||||
@@ -461,9 +461,9 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool {
|
|||||||
return generateGetIDStr
|
return generateGetIDStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSQLTypesImport returns the import path for the spectypes package.
|
// GetSQLTypesImport returns the import path for the sqltypes package.
|
||||||
func (tm *TypeMapper) GetSQLTypesImport() string {
|
func (tm *TypeMapper) GetSQLTypesImport() string {
|
||||||
return "git.warky.dev/wdevs/relspecgo/pkg/spectypes"
|
return "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ 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 nullable types import (resolvespec or stdlib depending on options)
|
// Add nullable types import (sqltypes or stdlib depending on options)
|
||||||
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Collect all models
|
// Collect all models
|
||||||
@@ -177,7 +177,7 @@ 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 nullable types import (resolvespec or stdlib depending on options)
|
// Add nullable types import (sqltypes or stdlib depending on options)
|
||||||
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Create model data
|
// Create model data
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ func TestWriter_FieldNameCollision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify NO field named just "TableName" (without underscore)
|
// Verify NO field named just "TableName" (without underscore)
|
||||||
if strings.Contains(generated, "TableName resolvespec_common") || strings.Contains(generated, "TableName string") {
|
if strings.Contains(generated, "TableName sql_types") || strings.Contains(generated, "TableName string") {
|
||||||
t.Errorf("Field 'TableName' without underscore should not exist (would conflict with method)\nGenerated:\n%s", generated)
|
t.Errorf("Field 'TableName' without underscore should not exist (would conflict with method)\nGenerated:\n%s", generated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,9 +827,9 @@ func TestTypeMapper_BuildBunTag(t *testing.T) {
|
|||||||
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
t.Errorf("BuildBunTag() = %q, missing %q", result, part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// resolvespec mode must NOT add "array" — SqlXxxArray uses sql.Scanner
|
// sqltypes mode must NOT add "array" — SqlXxxArray uses sql.Scanner
|
||||||
if strings.Contains(result, ",array,") || strings.HasSuffix(result, ",array,") {
|
if strings.Contains(result, ",array,") || strings.HasSuffix(result, ",array,") {
|
||||||
t.Errorf("BuildBunTag() = %q, must not contain 'array' in resolvespec mode", result)
|
t.Errorf("BuildBunTag() = %q, must not contain 'array' in sqltypes mode", result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Generates Go source files with GORM model definitions from database schema infor
|
|||||||
|
|
||||||
The GORM Writer converts RelSpec's internal database model representation into Go source code with GORM struct definitions, complete with proper tags, relationships, and methods.
|
The GORM Writer converts RelSpec's internal database model representation into Go source code with GORM struct definitions, complete with proper tags, relationships, and methods.
|
||||||
|
|
||||||
|
With `--types sqltypes`, nullable fields use the [`pkg/sqltypes`](../../sqltypes/README.md) package.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Generates GORM-compatible Go structs
|
- Generates GORM-compatible Go structs
|
||||||
@@ -48,19 +50,19 @@ func main() {
|
|||||||
### CLI Examples
|
### CLI Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate GORM models from a DBML schema (default: resolvespec types)
|
# Generate GORM models from a DBML schema (default: baselib pointer types)
|
||||||
relspec convert --from dbml --from-path schema.dbml \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--to gorm --to-path models.go --package models
|
--to gorm --to-path models.go --package models
|
||||||
|
|
||||||
# Use standard library database/sql nullable types instead of resolvespec
|
# Use standard library database/sql nullable types instead
|
||||||
relspec convert --from dbml --from-path schema.dbml \
|
relspec convert --from dbml --from-path schema.dbml \
|
||||||
--to gorm --to-path models.go --package models \
|
--to gorm --to-path models.go --package models \
|
||||||
--types stdlib
|
--types stdlib
|
||||||
|
|
||||||
# Explicitly select resolvespec types (same as omitting --types)
|
# Select sqltypes package types (git.warky.dev/wdevs/relspecgo/pkg/sqltypes)
|
||||||
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
relspec convert --from pgsql --from-conn "postgres://localhost/mydb" \
|
||||||
--to gorm --to-path models.go --package models \
|
--to gorm --to-path models.go --package models \
|
||||||
--types resolvespec
|
--types sqltypes
|
||||||
|
|
||||||
# Multi-file output (one file per table)
|
# Multi-file output (one file per table)
|
||||||
relspec convert --from json --from-path schema.json \
|
relspec convert --from json --from-path schema.json \
|
||||||
@@ -89,13 +91,13 @@ Files are named: `sql_{schema}_{table}.go`
|
|||||||
|
|
||||||
## Generated Code Examples
|
## Generated Code Examples
|
||||||
|
|
||||||
### Default — resolvespec types (`--types resolvespec`)
|
### sqltypes package types (`--types sqltypes`)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
sql_types "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelUser struct {
|
type ModelUser struct {
|
||||||
@@ -141,11 +143,11 @@ func (ModelUser) TableName() string {
|
|||||||
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
Controls which Go package is used for nullable column types. Set via the `--types` CLI flag or `WriterOptions.NullableTypes`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Use resolvespec types (default — omit NullableTypes or set to "resolvespec")
|
// Use sqltypes package types
|
||||||
options := &writers.WriterOptions{
|
options := &writers.WriterOptions{
|
||||||
OutputPath: "models.go",
|
OutputPath: "models.go",
|
||||||
PackageName: "models",
|
PackageName: "models",
|
||||||
NullableTypes: writers.NullableTypeResolveSpec,
|
NullableTypes: writers.NullableTypeSqlTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use standard library database/sql types
|
// Use standard library database/sql types
|
||||||
@@ -176,7 +178,7 @@ options := &writers.WriterOptions{
|
|||||||
|
|
||||||
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
The nullable type package is selected with `--types` (or `WriterOptions.NullableTypes`).
|
||||||
|
|
||||||
| SQL Type | NOT NULL — both | Nullable — resolvespec | Nullable — stdlib |
|
| SQL Type | NOT NULL — both | Nullable — sqltypes | Nullable — stdlib |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
| `bigint` | `int64` | `SqlInt64` | `sql.NullInt64` |
|
||||||
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
| `integer` | `int32` | `SqlInt32` | `sql.NullInt32` |
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ 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 {
|
||||||
sqlTypesAlias string
|
sqlTypesAlias string
|
||||||
typeStyle string // writers.NullableTypeResolveSpec | writers.NullableTypeStdlib
|
typeStyle string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTypeMapper creates a new TypeMapper.
|
// NewTypeMapper creates a new TypeMapper.
|
||||||
// typeStyle should be writers.NullableTypeResolveSpec or writers.NullableTypeStdlib;
|
// typeStyle should be writers.NullableTypeSqlTypes, writers.NullableTypeStdlib, or
|
||||||
// an empty string defaults to resolvespec.
|
// writers.NullableTypeBaselib; an empty string defaults to baselib.
|
||||||
func NewTypeMapper(typeStyle string) *TypeMapper {
|
func NewTypeMapper(typeStyle string) *TypeMapper {
|
||||||
if typeStyle == "" {
|
if typeStyle == "" {
|
||||||
typeStyle = writers.NullableTypeBaselib
|
typeStyle = writers.NullableTypeBaselib
|
||||||
@@ -50,7 +50,7 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
|||||||
return tm.baselibNullableGoType(baseType)
|
return tm.baselibNullableGoType(baseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvespec (default)
|
// sqltypes
|
||||||
if notNull {
|
if notNull {
|
||||||
return tm.baseGoType(baseType)
|
return tm.baseGoType(baseType)
|
||||||
}
|
}
|
||||||
@@ -505,9 +505,9 @@ func (tm *TypeMapper) NeedsFmtImport(generateGetIDStr bool) bool {
|
|||||||
return generateGetIDStr
|
return generateGetIDStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSQLTypesImport returns the import path for the spectypes package.
|
// GetSQLTypesImport returns the import path for the sqltypes package.
|
||||||
func (tm *TypeMapper) GetSQLTypesImport() string {
|
func (tm *TypeMapper) GetSQLTypesImport() string {
|
||||||
return "git.warky.dev/wdevs/relspecgo/pkg/spectypes"
|
return "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
// GetNullableTypeImportLine returns the full Go import line for the nullable type
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ 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 nullable types import (resolvespec or stdlib depending on options)
|
// Add nullable types import (sqltypes or stdlib depending on options)
|
||||||
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Collect all models
|
// Collect all models
|
||||||
@@ -171,7 +171,7 @@ 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 nullable types import (resolvespec or stdlib depending on options)
|
// Add nullable types import (sqltypes or stdlib depending on options)
|
||||||
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
templateData.AddImport(w.typeMapper.GetNullableTypeImportLine())
|
||||||
|
|
||||||
// Create model data
|
// Create model data
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ type Writer interface {
|
|||||||
// NullableType constants control which Go package is used for nullable column types
|
// NullableType constants control which Go package is used for nullable column types
|
||||||
// in code-generation writers (Bun, GORM).
|
// in code-generation writers (Bun, GORM).
|
||||||
const (
|
const (
|
||||||
// NullableTypeResolveSpec uses git.warky.dev/wdevs/relspecgo/pkg/spectypes
|
// NullableTypeSqlTypes uses git.warky.dev/wdevs/relspecgo/pkg/sqltypes
|
||||||
// (SqlString, SqlInt32, SqlVector, SqlStringArray, …).
|
// (SqlString, SqlInt32, SqlVector, SqlStringArray, …).
|
||||||
NullableTypeResolveSpec = "resolvespec"
|
NullableTypeSqlTypes = "sqltypes"
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -52,9 +52,9 @@ type WriterOptions struct {
|
|||||||
|
|
||||||
// NullableTypes selects the Go type package used for nullable columns in
|
// NullableTypes selects the Go type package used for nullable columns in
|
||||||
// code-generation writers (bun, gorm). Accepted values:
|
// code-generation writers (bun, gorm). Accepted values:
|
||||||
// "resolvespec" (default) — git.warky.dev/wdevs/relspecgo/pkg/spectypes
|
// "sqltypes" — git.warky.dev/wdevs/relspecgo/pkg/sqltypes
|
||||||
// "stdlib" — database/sql (sql.NullString, sql.NullInt32, …)
|
// "stdlib" — database/sql (sql.NullString, sql.NullInt32, …)
|
||||||
// "baselib" — plain Go pointer types (*string, *int32, …)
|
// "baselib" (default) — 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user