feat(bun): generate native Go array slices for PostgreSQL array columns

Bun's pgdialect scans/appends native slices directly, so array columns
(text[], integer[], uuid[], ...) always generate as plain []string,
[]int32, etc. with an explicit "array" bun tag, regardless of --types
(sqltypes/stdlib/baselib). The SqlXxxArray wrapper types are no longer
used for Bun array columns (gorm is unaffected and keeps using them).

Adds --array-nullable pointer_slice to represent nullable array columns
as *[]T instead of []T, so callers can distinguish SQL NULL (nil) from
'{}' (pointer to an empty slice). Verified end-to-end against a live
PostgreSQL instance for NULL/{}/populated arrays in every --types mode.

Closes #13
This commit is contained in:
Hein
2026-07-21 12:41:38 +02:00
parent 2cecb4c11c
commit 5d9ff5df03
65 changed files with 9584 additions and 167 deletions
+5 -3
View File
@@ -54,6 +54,7 @@ var (
convertSchemaFilter string
convertFlattenSchema bool
convertNullableTypes string
convertNullableArrays string
convertContinueOnError bool
convertExtraFields string
)
@@ -180,6 +181,7 @@ func init() {
convertCmd.Flags().StringVar(&convertSchemaFilter, "schema", "", "Filter to a specific schema by name (required for formats like dctx that only support single schemas)")
convertCmd.Flags().BoolVar(&convertFlattenSchema, "flatten-schema", false, "Flatten schema.table names to schema_table (useful for databases like SQLite that do not support schemas)")
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
convertCmd.Flags().StringVar(&convertNullableArrays, "array-nullable", "", "Nullable PostgreSQL array representation for the Bun writer in stdlib/baselib --types mode: 'slice' (default, plain slice) or 'pointer_slice' (*[]T, distinguishes NULL from '{}')")
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
convertCmd.Flags().StringVar(&convertExtraFields, "extra-fields", "", "Path to JSON file containing extra Bun model fields to inject (bun output only); fields support target_table, name, type, bun_tag, json_tag, comment")
@@ -248,7 +250,7 @@ func runConvert(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, " Schema: %s\n", convertSchemaFilter)
}
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes, convertContinueOnError, convertExtraFields); err != nil {
if err := writeDatabase(db, convertTargetType, convertTargetPath, convertPackageName, convertSchemaFilter, convertFlattenSchema, convertNullableTypes, convertNullableArrays, convertContinueOnError, convertExtraFields); err != nil {
return fmt.Errorf("failed to write target: %w", err)
}
@@ -388,10 +390,10 @@ func readDatabaseForConvert(dbType, filePath, connString string) (*models.Databa
return db, nil
}
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes string, continueOnError bool, extraFields string) error {
func writeDatabase(db *models.Database, dbType, outputPath, packageName, schemaFilter string, flattenSchema bool, nullableTypes, nullableArrays string, continueOnError bool, extraFields string) error {
var writer writers.Writer
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, continueOnError)
writerOpts := newWriterOptions(outputPath, packageName, flattenSchema, nullableTypes, nullableArrays, continueOnError)
if extraFields != "" {
if !strings.EqualFold(dbType, "bun") {
return fmt.Errorf("--extra-fields is only supported for Bun output")
+13 -13
View File
@@ -323,31 +323,31 @@ func writeDatabaseForEdit(dbType, filePath, connString string, db *models.Databa
switch strings.ToLower(dbType) {
case "dbml":
writer = wdbml.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wdbml.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "dctx":
writer = wdctx.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wdctx.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "drawdb":
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "graphql":
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "json":
writer = wjson.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wjson.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "yaml":
writer = wyaml.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wyaml.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "gorm":
writer = wgorm.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wgorm.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "bun":
writer = wbun.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wbun.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "drizzle":
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "prisma":
writer = wprisma.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wprisma.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "typeorm":
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "sqlite", "sqlite3":
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
case "pgsql":
writer = wpgsql.NewWriter(newWriterOptions(filePath, "", false, "", false))
writer = wpgsql.NewWriter(newWriterOptions(filePath, "", false, "", "", false))
default:
return fmt.Errorf("%s: unsupported format: %s", label, dbType)
}
+13 -13
View File
@@ -375,61 +375,61 @@ func writeDatabaseForMerge(dbType, filePath, connString string, db *models.Datab
if filePath == "" {
return fmt.Errorf("%s: file path is required for DBML format", label)
}
writer = wdbml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wdbml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "dctx":
if filePath == "" {
return fmt.Errorf("%s: file path is required for DCTX format", label)
}
writer = wdctx.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wdctx.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "drawdb":
if filePath == "" {
return fmt.Errorf("%s: file path is required for DrawDB format", label)
}
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wdrawdb.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "graphql":
if filePath == "" {
return fmt.Errorf("%s: file path is required for GraphQL format", label)
}
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wgraphql.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "json":
if filePath == "" {
return fmt.Errorf("%s: file path is required for JSON format", label)
}
writer = wjson.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wjson.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "yaml":
if filePath == "" {
return fmt.Errorf("%s: file path is required for YAML format", label)
}
writer = wyaml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wyaml.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "gorm":
if filePath == "" {
return fmt.Errorf("%s: file path is required for GORM format", label)
}
writer = wgorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wgorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "bun":
if filePath == "" {
return fmt.Errorf("%s: file path is required for Bun format", label)
}
writer = wbun.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wbun.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "drizzle":
if filePath == "" {
return fmt.Errorf("%s: file path is required for Drizzle format", label)
}
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wdrizzle.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "prisma":
if filePath == "" {
return fmt.Errorf("%s: file path is required for Prisma format", label)
}
writer = wprisma.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wprisma.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "typeorm":
if filePath == "" {
return fmt.Errorf("%s: file path is required for TypeORM format", label)
}
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wtypeorm.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "sqlite", "sqlite3":
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", false))
writer = wsqlite.NewWriter(newWriterOptions(filePath, "", flattenSchema, "", "", false))
case "pgsql":
writerOpts := newWriterOptions(filePath, "", flattenSchema, "", false)
writerOpts := newWriterOptions(filePath, "", flattenSchema, "", "", false)
if connString != "" {
writerOpts.Metadata = map[string]interface{}{
"connection_string": connString,
+2 -1
View File
@@ -13,12 +13,13 @@ func newReaderOptions(filePath, connString string) *readers.ReaderOptions {
}
}
func newWriterOptions(outputPath, packageName string, flattenSchema bool, nullableTypes string, continueOnError bool) *writers.WriterOptions {
func newWriterOptions(outputPath, packageName string, flattenSchema bool, nullableTypes, nullableArrays string, continueOnError bool) *writers.WriterOptions {
return &writers.WriterOptions{
OutputPath: outputPath,
PackageName: packageName,
FlattenSchema: flattenSchema,
NullableTypes: nullableTypes,
NullableArrays: nullableArrays,
Prisma7: prisma7,
ContinueOnError: continueOnError,
}
+15 -12
View File
@@ -11,18 +11,19 @@ import (
)
var (
splitSourceType string
splitSourcePath string
splitSourceConn string
splitTargetType string
splitTargetPath string
splitSchemas string
splitTables string
splitPackageName string
splitDatabaseName string
splitExcludeSchema string
splitExcludeTables string
splitNullableTypes string
splitSourceType string
splitSourcePath string
splitSourceConn string
splitTargetType string
splitTargetPath string
splitSchemas string
splitTables string
splitPackageName string
splitDatabaseName string
splitExcludeSchema string
splitExcludeTables string
splitNullableTypes string
splitNullableArrays string
)
var splitCmd = &cobra.Command{
@@ -112,6 +113,7 @@ func init() {
splitCmd.Flags().StringVar(&splitExcludeSchema, "exclude-schema", "", "Comma-separated list of schema names to exclude")
splitCmd.Flags().StringVar(&splitExcludeTables, "exclude-tables", "", "Comma-separated list of table names to exclude (case-insensitive)")
splitCmd.Flags().StringVar(&splitNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
splitCmd.Flags().StringVar(&splitNullableArrays, "array-nullable", "", "Nullable PostgreSQL array representation for the Bun writer in stdlib/baselib --types mode: 'slice' (default, plain slice) or 'pointer_slice' (*[]T, distinguishes NULL from '{}')")
err := splitCmd.MarkFlagRequired("from")
if err != nil {
@@ -188,6 +190,7 @@ func runSplit(cmd *cobra.Command, args []string) error {
"", // no schema filter for split
false, // no flatten-schema for split
splitNullableTypes,
splitNullableArrays,
false, // no continue-on-error for split
"", // no extra fields for split
)