117 lines
2.9 KiB
Go
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
|
|
}
|