From 480038d51d3cc5a209fff189edec465cfc724328 Mon Sep 17 00:00:00 2001 From: Hein Date: Fri, 20 Feb 2026 16:03:50 +0200 Subject: [PATCH] feat(writers): quote default values based on SQL column type Bun and GORM struct tags now emit quoted defaults for string/date/time/UUID columns (e.g. default:'disconnected') and unquoted defaults for numeric and boolean columns (e.g. default:0, default:true). Function-call expressions such as now() or gen_random_uuid() are never quoted regardless of type. Adds QuoteDefaultValue(value, sqlType) helper in pkg/writers and updates both type mappers and the bun writer tests accordingly. --- pkg/writers/bun/type_mapper.go | 4 +-- pkg/writers/bun/writer_test.go | 42 ++++++++++++++++++++---- pkg/writers/gorm/type_mapper.go | 4 +-- pkg/writers/writer.go | 58 +++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/pkg/writers/bun/type_mapper.go b/pkg/writers/bun/type_mapper.go index d0cf404..5767849 100644 --- a/pkg/writers/bun/type_mapper.go +++ b/pkg/writers/bun/type_mapper.go @@ -208,8 +208,8 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st // Default value if column.Default != nil { - // Sanitize default value to remove backticks - safeDefault := writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)) + // Sanitize default value to remove backticks, then quote based on column type + safeDefault := writers.QuoteDefaultValue(writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)), column.Type) parts = append(parts, fmt.Sprintf("default:%s", safeDefault)) } diff --git a/pkg/writers/bun/writer_test.go b/pkg/writers/bun/writer_test.go index d97e75c..2a7862a 100644 --- a/pkg/writers/bun/writer_test.go +++ b/pkg/writers/bun/writer_test.go @@ -312,7 +312,7 @@ func TestWriter_MultipleReferencesToSameTable(t *testing.T) { fieldName string tag string }{ - {"RelRIDFilepointerRequestOrgAPIEvents", "join:id_filepointer=rid_filepointer_request"}, // Has many via rid_filepointer_request + {"RelRIDFilepointerRequestOrgAPIEvents", "join:id_filepointer=rid_filepointer_request"}, // Has many via rid_filepointer_request {"RelRIDFilepointerResponseOrgAPIEvents", "join:id_filepointer=rid_filepointer_response"}, // Has many via rid_filepointer_response } @@ -461,10 +461,10 @@ func TestWriter_MultipleHasManyRelationships(t *testing.T) { // Verify all has-many relationships have unique names hasManyExpectations := []string{ - "RelRIDAPIProviderOrgLogins", // Has many via Login + "RelRIDAPIProviderOrgLogins", // Has many via Login "RelRIDAPIProviderOrgFilepointers", // Has many via Filepointer - "RelRIDAPIProviderOrgAPIEvents", // Has many via APIEvent - "RelRIDOwner", // Has one via rid_owner + "RelRIDAPIProviderOrgAPIEvents", // Has many via APIEvent + "RelRIDOwner", // Has one via rid_owner } for _, exp := range hasManyExpectations { @@ -615,14 +615,44 @@ func TestTypeMapper_BuildBunTag(t *testing.T) { want: []string{"email,", "type:varchar(255),", "nullzero,"}, }, { - name: "with default", + name: "with default string", column: &models.Column{ Name: "status", Type: "text", NotNull: true, Default: "active", }, - want: []string{"status,", "type:text,", "default:active,"}, + want: []string{"status,", "type:text,", "default:'active',"}, + }, + { + name: "with default integer", + column: &models.Column{ + Name: "retries", + Type: "integer", + NotNull: true, + Default: "0", + }, + want: []string{"retries,", "type:integer,", "default:0,"}, + }, + { + name: "with default boolean", + column: &models.Column{ + Name: "active", + Type: "boolean", + NotNull: true, + Default: "true", + }, + want: []string{"active,", "type:boolean,", "default:true,"}, + }, + { + name: "with default function call", + column: &models.Column{ + Name: "created_at", + Type: "timestamp", + NotNull: true, + Default: "now()", + }, + want: []string{"created_at,", "type:timestamp,", "default:now(),"}, }, { name: "auto increment with AutoIncrement flag", diff --git a/pkg/writers/gorm/type_mapper.go b/pkg/writers/gorm/type_mapper.go index a8a0eba..097d503 100644 --- a/pkg/writers/gorm/type_mapper.go +++ b/pkg/writers/gorm/type_mapper.go @@ -238,8 +238,8 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s // Default value if column.Default != nil { - // Sanitize default value to remove backticks - safeDefault := writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)) + // Sanitize default value to remove backticks, then quote based on column type + safeDefault := writers.QuoteDefaultValue(writers.SanitizeStructTagValue(fmt.Sprintf("%v", column.Default)), column.Type) parts = append(parts, fmt.Sprintf("default:%s", safeDefault)) } diff --git a/pkg/writers/writer.go b/pkg/writers/writer.go index c29eb9c..78b5183 100644 --- a/pkg/writers/writer.go +++ b/pkg/writers/writer.go @@ -81,6 +81,64 @@ func SanitizeFilename(name string) string { return name } +// QuoteDefaultValue wraps a sanitized default value in single quotes when the SQL +// column type requires it (strings, dates, times, UUIDs, enums). Numeric types +// (integers, floats, serials) and boolean types are left unquoted. Function-call +// expressions such as now() or gen_random_uuid() are always left unquoted regardless +// of type, because they contain parentheses. +// +// Examples (varchar): "disconnected" → "'disconnected'" +// Examples (boolean): "true" → "true" +// Examples (bigint): "0" → "0" +// Examples (timestamp): "now()" → "now()" (function call – never quoted) +func QuoteDefaultValue(value, sqlType string) string { + // Function calls are never quoted regardless of column type. + if strings.Contains(value, "(") || strings.Contains(value, ")") { + return value + } + + // Normalise the SQL type: lowercase, strip length/precision suffix. + baseType := strings.ToLower(strings.TrimSpace(sqlType)) + if idx := strings.Index(baseType, "("); idx > 0 { + baseType = baseType[:idx] + } + + // Types whose default values must NOT be quoted. + unquotedTypes := map[string]bool{ + // Integer types + "integer": true, + "int": true, + "int2": true, + "int4": true, + "int8": true, + "smallint": true, + "bigint": true, + "serial": true, + "smallserial": true, + "bigserial": true, + // Float / numeric types + "real": true, + "float": true, + "float4": true, + "float8": true, + "double precision": true, + "numeric": true, + "decimal": true, + "money": true, + // Boolean + "boolean": true, + "bool": true, + } + + if unquotedTypes[baseType] { + return value + } + + // Everything else (text, varchar, char, uuid, date, time, timestamp, json, …) + // is treated as a quoted literal. + return "'" + value + "'" +} + // SanitizeStructTagValue sanitizes a value to be safely used inside Go struct tags. // Go struct tags are delimited by backticks, so any backtick in the value would break the syntax. // This function: