Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sqlc): Support custom generic nullable types (e.g. sql.Null) #3279

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ The `gen` mapping supports the following keys:
- If true, generated methods will accept a DBTX argument instead of storing a DBTX on the `*Queries` struct. Defaults to `false`.
- `emit_pointers_for_null_types`:
- If true, generated types for nullable columns are emitted as pointers (ie. `*string`) instead of `database/sql` null types (ie. `NullString`). Currently only supported for PostgreSQL if `sql_package` is `pgx/v4` or `pgx/v5`, and for SQLite. Defaults to `false`.
- `generic_null_type`:
- A fully qualified name to a Go type that is used to wrap nullable fields; for example, `database/sql.Null` would represent a nullable `string` as `sql.Null[string]`. For more complicated import paths, `generic_null_type` can also be an object with the same keys as `go_type` below.
- `emit_enum_valid_method`:
- If true, generate a Valid method on enum types,
indicating whether a string is a valid enum value.
Expand Down
13 changes: 13 additions & 0 deletions internal/codegen/golang/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,19 @@ func buildImports(options *opts.Options, queries []Query, uses func(string) bool
}
}

// Ensure custom null type is imported
if options.GenericNullType != nil {
t := options.GenericNullType.Parsed

if !(t.BasicType && t.TypeName == "") {
_, alreadyImported := std[t.ImportPath]
hasPackageAlias := t.Package != ""
if (!alreadyImported || hasPackageAlias) && uses(t.TypeName) {
pkg[ImportSpec{Path: t.ImportPath, ID: t.Package}] = struct{}{}
}
}
}

return std, pkg
}

Expand Down
75 changes: 59 additions & 16 deletions internal/codegen/golang/mysql_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.C
notNull := col.NotNull || col.IsArray
unsigned := col.Unsigned

var genericNull *opts.ParsedGoType
if options.GenericNullType != nil {
genericNull = &options.GenericNullType.Parsed
}

switch columnType {

case "varchar", "text", "char", "tinytext", "mediumtext", "longtext":
if notNull {
return "string"
}
if genericNull != nil {
return genericNull.TypeName + "[string]"
}
return "sql.NullString"

case "tinyint":
Expand All @@ -29,58 +37,87 @@ func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.C
}
return "sql.NullBool"
} else {
baseType := "int32"
if unsigned {
baseType = "uint32"
}

if notNull {
if unsigned {
return "uint32"
}
return "int32"
return baseType
}
if genericNull != nil {
return genericNull.TypeName + "[" + baseType + "]"
}
return "sql.NullInt32"
}

case "smallint", "year":
baseType := "int16"
if unsigned {
baseType = "uint16"
}

if notNull {
if unsigned {
return "uint16"
}
return "int16"
return baseType
}
if genericNull != nil {
return genericNull.TypeName + "[" + baseType + "]"
}
return "sql.NullInt16"

case "int", "integer", "mediumint":
baseType := "int32"
if unsigned {
baseType = "uint32"
}

if notNull {
if unsigned {
return "uint32"
}
return "int32"
return baseType
}
if genericNull != nil {
return genericNull.TypeName + "[" + baseType + "]"
}
return "sql.NullInt32"

case "bigint":
baseType := "int64"
if unsigned {
baseType = "uint64"
}

if notNull {
if unsigned {
return "uint64"
}
return "int64"
return baseType
}
if genericNull != nil {
return genericNull.TypeName + "[" + baseType + "]"
}
return "sql.NullInt64"

case "blob", "binary", "varbinary", "tinyblob", "mediumblob", "longblob":
if notNull {
return "[]byte"
}
if genericNull != nil {
return genericNull.TypeName + "[[]byte]"
}
return "sql.NullString"

case "double", "double precision", "real", "float":
if notNull {
return "float64"
}
if genericNull != nil {
return genericNull.TypeName + "[float64]"
}
return "sql.NullFloat64"

case "decimal", "dec", "fixed":
if notNull {
return "string"
}
if genericNull != nil {
return genericNull.TypeName + "[string]"
}
return "sql.NullString"

case "enum":
Expand All @@ -91,12 +128,18 @@ func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.C
if notNull {
return "time.Time"
}
if genericNull != nil {
return genericNull.TypeName + "[time.Time]"
}
return "sql.NullTime"

case "boolean", "bool":
if notNull {
return "bool"
}
if genericNull != nil {
return genericNull.TypeName + "[bool]"
}
return "sql.NullBool"

case "json":
Expand Down
84 changes: 51 additions & 33 deletions internal/codegen/golang/opts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,53 @@ import (
)

type Options struct {
EmitInterface bool `json:"emit_interface" yaml:"emit_interface"`
EmitJsonTags bool `json:"emit_json_tags" yaml:"emit_json_tags"`
JsonTagsIdUppercase bool `json:"json_tags_id_uppercase" yaml:"json_tags_id_uppercase"`
EmitDbTags bool `json:"emit_db_tags" yaml:"emit_db_tags"`
EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"`
EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"`
EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"`
EmitExportedQueries bool `json:"emit_exported_queries" yaml:"emit_exported_queries"`
EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"`
EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"`
EmitMethodsWithDbArgument bool `json:"emit_methods_with_db_argument,omitempty" yaml:"emit_methods_with_db_argument"`
EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"`
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
JsonTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
Package string `json:"package" yaml:"package"`
Out string `json:"out" yaml:"out"`
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
SqlPackage string `json:"sql_package" yaml:"sql_package"`
SqlDriver string `json:"sql_driver" yaml:"sql_driver"`
OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"`
OutputDbFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"`
OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"`
OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"`
OutputCopyfromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"`
OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"`
InflectionExcludeTableNames []string `json:"inflection_exclude_table_names,omitempty" yaml:"inflection_exclude_table_names"`
QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"`
OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"`
OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"`
BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"`
EmitInterface bool `json:"emit_interface" yaml:"emit_interface"`
EmitJsonTags bool `json:"emit_json_tags" yaml:"emit_json_tags"`
JsonTagsIdUppercase bool `json:"json_tags_id_uppercase" yaml:"json_tags_id_uppercase"`
EmitDbTags bool `json:"emit_db_tags" yaml:"emit_db_tags"`
EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"`
EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"`
EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"`
EmitExportedQueries bool `json:"emit_exported_queries" yaml:"emit_exported_queries"`
EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"`
EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"`
EmitMethodsWithDbArgument bool `json:"emit_methods_with_db_argument,omitempty" yaml:"emit_methods_with_db_argument"`
EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"`
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
JsonTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
Package string `json:"package" yaml:"package"`
Out string `json:"out" yaml:"out"`
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
SqlPackage string `json:"sql_package" yaml:"sql_package"`
SqlDriver string `json:"sql_driver" yaml:"sql_driver"`
OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"`
OutputDbFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"`
OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"`
OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"`
OutputCopyfromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"`
OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"`
InflectionExcludeTableNames []string `json:"inflection_exclude_table_names,omitempty" yaml:"inflection_exclude_table_names"`
QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"`
OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"`
OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"`
BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"`
GenericNullType *GenericNullOption `json:"generic_null_type" yaml:"generic_null_type"`
}

type GlobalOptions struct {
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
}

type GenericNullOption struct {
GoType

Parsed ParsedGoType `json:"-"`
}

func Parse(req *plugin.GenerateRequest) (*Options, error) {
options, err := parseOpts(req)
if err != nil {
Expand Down Expand Up @@ -106,6 +113,14 @@ func parseOpts(req *plugin.GenerateRequest) (*Options, error) {
}
}

if options.GenericNullType != nil {
parsed, err := options.GenericNullType.parse()
if err != nil {
return nil, err
}
options.GenericNullType.Parsed = *parsed
}

if options.QueryParameterLimit == nil {
options.QueryParameterLimit = new(int32)
*options.QueryParameterLimit = 1
Expand Down Expand Up @@ -137,6 +152,9 @@ func ValidateOpts(opts *Options) error {
if *opts.QueryParameterLimit < 0 {
return fmt.Errorf("invalid options: query parameter limit must not be negative")
}
if opts.EmitPointersForNullTypes && opts.GenericNullType != nil {
return fmt.Errorf("invalid options: emit_pointers_for_null_types and generic_null_type are mutually exclusive")
}

return nil
}