224 lines
7.5 KiB
Go
224 lines
7.5 KiB
Go
package writers
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
|
||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||
)
|
||
|
||
// Writer defines the interface for writing database specifications
|
||
// to various output formats at different granularity levels
|
||
type Writer interface {
|
||
// WriteDatabase takes a Database model and writes it to the desired format
|
||
WriteDatabase(db *models.Database) error
|
||
|
||
// WriteSchema takes a Schema model and writes it to the desired format
|
||
WriteSchema(schema *models.Schema) error
|
||
|
||
// WriteTable takes a Table model and writes it to the desired format
|
||
WriteTable(table *models.Table) error
|
||
}
|
||
|
||
// NullableType constants control which Go package is used for nullable column types
|
||
// in code-generation writers (Bun, GORM).
|
||
const (
|
||
// NullableTypeResolveSpec uses github.com/bitechdev/ResolveSpec/pkg/spectypes
|
||
// (SqlString, SqlInt32, SqlVector, SqlStringArray, …). This is the default.
|
||
NullableTypeResolveSpec = "resolvespec"
|
||
|
||
// NullableTypeStdlib uses the standard library database/sql nullable types
|
||
// (sql.NullString, sql.NullInt32, …) and plain Go slices for arrays.
|
||
NullableTypeStdlib = "stdlib"
|
||
)
|
||
|
||
// WriterOptions contains common options for writers
|
||
type WriterOptions struct {
|
||
// OutputPath is the path where the output should be written
|
||
OutputPath string
|
||
|
||
// PackageName is the Go package name (for code generation)
|
||
PackageName string
|
||
|
||
// FlattenSchema disables schema.table dot notation and instead joins
|
||
// schema and table with an underscore (e.g., "public_users").
|
||
// Useful for databases like SQLite that do not support schemas.
|
||
FlattenSchema bool
|
||
|
||
// NullableTypes selects the Go type package used for nullable columns in
|
||
// code-generation writers (bun, gorm). Accepted values:
|
||
// "resolvespec" (default) — github.com/bitechdev/ResolveSpec/pkg/spectypes
|
||
// "stdlib" — database/sql (sql.NullString, sql.NullInt32, …)
|
||
NullableTypes string
|
||
|
||
// Prisma7 enables Prisma 7-specific output for Prisma writers.
|
||
Prisma7 bool
|
||
|
||
// Additional options can be added here as needed
|
||
Metadata map[string]interface{}
|
||
}
|
||
|
||
// QualifiedTableName returns a schema-qualified table name.
|
||
// When flatten is true, schema and table are joined with underscore (e.g., "schema_table").
|
||
// When flatten is false, they are dot-separated (e.g., "schema.table").
|
||
// If schema is empty, just the table name is returned regardless of flatten.
|
||
func QualifiedTableName(schema, table string, flatten bool) string {
|
||
if schema == "" {
|
||
return table
|
||
}
|
||
if flatten {
|
||
return schema + "_" + table
|
||
}
|
||
return schema + "." + table
|
||
}
|
||
|
||
// SanitizeFilename removes quotes, comments, and invalid characters from identifiers
|
||
// to make them safe for use in filenames. This handles:
|
||
// - Double and single quotes: "table_name" or 'table_name' -> table_name
|
||
// - DBML comments: table [note: 'description'] -> table
|
||
// - Invalid filename characters: replaced with underscores
|
||
func SanitizeFilename(name string) string {
|
||
// Remove DBML/DCTX style comments in brackets (e.g., [note: 'description'])
|
||
commentRegex := regexp.MustCompile(`\s*\[.*?\]\s*`)
|
||
name = commentRegex.ReplaceAllString(name, "")
|
||
|
||
// Remove quotes (both single and double)
|
||
name = strings.ReplaceAll(name, `"`, "")
|
||
name = strings.ReplaceAll(name, `'`, "")
|
||
|
||
// Remove backticks (MySQL style identifiers)
|
||
name = strings.ReplaceAll(name, "`", "")
|
||
|
||
// Replace invalid filename characters with underscores
|
||
// Invalid chars: / \ : * ? " < > | and control characters
|
||
invalidChars := regexp.MustCompile(`[/\\:*?"<>|\x00-\x1f\x7f]`)
|
||
name = invalidChars.ReplaceAllString(name, "_")
|
||
|
||
// Trim whitespace and consecutive underscores
|
||
name = strings.TrimSpace(name)
|
||
name = regexp.MustCompile(`_+`).ReplaceAllString(name, "_")
|
||
name = strings.Trim(name, "_")
|
||
|
||
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 {
|
||
value = strings.TrimSpace(value)
|
||
|
||
// Function calls are never quoted regardless of column type.
|
||
if strings.Contains(value, "(") || strings.Contains(value, ")") ||
|
||
strings.Contains(value, "::") ||
|
||
strings.HasPrefix(strings.ToUpper(value), "ARRAY[") {
|
||
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]
|
||
}
|
||
|
||
if isArraySQLType(baseType) {
|
||
if arrayLiteral, ok := normalizeArrayDefaultLiteral(value); ok {
|
||
return quoteSQLLiteral(arrayLiteral)
|
||
}
|
||
}
|
||
|
||
if isQuotedSQLLiteral(value) {
|
||
return value
|
||
}
|
||
|
||
// 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 quoteSQLLiteral(value)
|
||
}
|
||
|
||
func isArraySQLType(sqlType string) bool {
|
||
return strings.HasSuffix(sqlType, "[]")
|
||
}
|
||
|
||
func normalizeArrayDefaultLiteral(value string) (string, bool) {
|
||
switch {
|
||
case strings.HasPrefix(value, "''{") && strings.HasSuffix(value, "}''"):
|
||
return value[2 : len(value)-2], true
|
||
case strings.HasPrefix(value, "'{") && strings.HasSuffix(value, "}'"):
|
||
return value[1 : len(value)-1], true
|
||
case strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}"):
|
||
return value, true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func isQuotedSQLLiteral(value string) bool {
|
||
return len(value) >= 2 && strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")
|
||
}
|
||
|
||
func quoteSQLLiteral(value string) string {
|
||
return "'" + strings.ReplaceAll(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:
|
||
// - Removes DBML/DCTX comments in brackets
|
||
// - Removes all quotes (double, single, and backticks)
|
||
// - Returns a clean identifier safe for use in struct tags and field names
|
||
func SanitizeStructTagValue(value string) string {
|
||
// Remove DBML/DCTX style comments in brackets (e.g., [note: 'description'])
|
||
commentRegex := regexp.MustCompile(`\s*\[.*?\]\s*`)
|
||
value = commentRegex.ReplaceAllString(value, "")
|
||
|
||
// Trim whitespace
|
||
value = strings.TrimSpace(value)
|
||
|
||
// Remove all quotes: backticks, double quotes, and single quotes
|
||
// This ensures the value is clean for use as Go identifiers and struct tag values
|
||
value = strings.ReplaceAll(value, "`", "")
|
||
value = strings.ReplaceAll(value, `"`, "")
|
||
value = strings.ReplaceAll(value, `'`, "")
|
||
|
||
return value
|
||
}
|