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 }