Files
relspecgo/pkg/writers/pgsql/identifiers.go
T

117 lines
2.9 KiB
Go

package pgsql
import (
"fmt"
"reflect"
"strings"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/transform"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
// prepareModel rejects NUL before creating files or executing SQL. SQL names
// cannot contain NUL; pgx's identifier sanitizer would otherwise remove it.
func prepareModel(db *models.Database, lower bool) (*models.Database, error) {
if db == nil {
return nil, fmt.Errorf("database is required")
}
seen := map[any]bool{}
var validate func(reflect.Value) error
validate = func(v reflect.Value) error {
switch v.Kind() {
case reflect.Pointer:
if v.IsNil() || v.Type().Elem().PkgPath() != reflect.TypeOf(models.Database{}).PkgPath() {
return nil
}
if seen[v.Interface()] {
return nil
}
seen[v.Interface()] = true
return validate(v.Elem())
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if v.Type().Field(i).Name == "Metadata" {
continue
}
if err := validate(v.Field(i)); err != nil {
return err
}
}
case reflect.String:
if strings.ContainsRune(v.String(), 0) {
return fmt.Errorf("PostgreSQL model contains a NUL byte")
}
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
if err := validate(v.Index(i)); err != nil {
return err
}
}
case reflect.Map:
if v.Type().Elem().Kind() == reflect.Pointer {
iter := v.MapRange()
for iter.Next() {
if err := validate(iter.Value()); err != nil {
return err
}
}
}
}
return nil
}
if err := validate(reflect.ValueOf(db)); err != nil {
return nil, err
}
if lower {
return transform.LowercaseIdentifiers(db)
}
return db, nil
}
func (w *Writer) prepareSchema(schema *models.Schema) (*models.Schema, error) {
db, err := prepareModel(&models.Database{Schemas: []*models.Schema{schema}}, w.options.LowercaseIdentifiers)
if err != nil {
return nil, err
}
return db.Schemas[0], nil
}
// regclassLastName decodes quoted identifiers in a qualified regclass spelling.
func regclassLastName(text string) string {
var name strings.Builder
quoted := false
for i := 0; i < len(text); i++ {
c := text[i]
if c == '"' {
if quoted && i+1 < len(text) && text[i+1] == '"' {
name.WriteByte('"')
i++
} else {
quoted = !quoted
}
continue
}
if c == '.' && !quoted {
name.Reset()
continue
}
name.WriteByte(c)
}
return name.String()
}
// quoteDefaultValue keeps explicit SQL expressions intact and uses the same
// backslash-safe literal quoting as other PostgreSQL value contexts.
func quoteDefaultValue(value, sqlType string) string {
trimmed := strings.TrimSpace(value)
if len(trimmed) >= 3 && (trimmed[0] == 'E' || trimmed[0] == 'e') && trimmed[1] == '\'' && trimmed[len(trimmed)-1] == '\'' {
return trimmed
}
formatted := writers.QuoteDefaultValue(value, sqlType)
if formatted == "'"+strings.ReplaceAll(trimmed, "'", "''")+"'" {
return quote(trimmed)
}
return formatted
}