package pgsql import ( "strconv" "strings" ) // Index access-method storage parameters, the WITH (...) clause of CREATE INDEX. RelSpec // carries them through the model in Index.Comment, so the parsing here is deliberately // strict: only well-formed "key = value" pairs survive, and comment prose is discarded. // // Value forms accepted: // - bare tokens: lists=100, m=16, deduplicate_items=true // - quoted strings: key_field='id' (pg_search bm25) // - dollar-quoted blocks: options=$$ [build.internal] lists=[4096] $$ (vchord) // ExtractWithClause returns the contents of the first WITH (...) clause in s, without the // surrounding parentheses. Parentheses inside quoted and dollar-quoted values are ignored, // so a vchord TOML block survives intact. Returns "" when there is no WITH clause. func ExtractWithClause(s string) string { lower := strings.ToLower(s) for offset := 0; ; { idx := strings.Index(lower[offset:], "with") if idx < 0 { return "" } start := offset + idx offset = start + 4 // "with" must stand as its own word. if start > 0 && isSQLIdentifierByte(s[start-1]) { continue } pos := offset for pos < len(s) && isSQLSpace(s[pos]) { pos++ } if pos >= len(s) || s[pos] != '(' { continue } if end, ok := matchClosingParen(s, pos); ok { return s[pos+1 : end] } return "" } } // matchClosingParen returns the index of the ')' matching the '(' at open, skipping over // quoted and dollar-quoted spans. func matchClosingParen(s string, open int) (int, bool) { depth := 0 for i := open; i < len(s); i++ { switch s[i] { case '\'': end, ok := skipQuoted(s, i) if !ok { return 0, false } i = end case '$': if end, ok := skipDollarQuoted(s, i); ok { i = end } case '(': depth++ case ')': depth-- if depth == 0 { return i, true } } } return 0, false } // skipQuoted returns the index of the closing quote of the single-quoted string starting // at start, treating ” as an escaped quote. func skipQuoted(s string, start int) (int, bool) { for i := start + 1; i < len(s); i++ { if s[i] != '\'' { continue } if i+1 < len(s) && s[i+1] == '\'' { i++ continue } return i, true } return 0, false } // skipDollarQuoted returns the index of the last byte of the dollar-quoted block starting // at start ($tag$ … $tag$). Reports false when start does not open one. func skipDollarQuoted(s string, start int) (int, bool) { tagEnd := strings.IndexByte(s[start+1:], '$') if tagEnd < 0 { return 0, false } tag := s[start : start+1+tagEnd+1] for i := start + 1; i < len(tag); i++ { if !isSQLIdentifierByte(tag[i]) && tag[i] != '$' { return 0, false } } closing := strings.Index(s[start+len(tag):], tag) if closing < 0 { return 0, false } return start + len(tag) + closing + len(tag) - 1, true } // SplitStorageParameters splits a WITH clause body on top-level commas, leaving quoted and // dollar-quoted values untouched. func SplitStorageParameters(clause string) []string { parts := make([]string, 0, 4) depth := 0 start := 0 for i := 0; i < len(clause); i++ { switch clause[i] { case '\'': if end, ok := skipQuoted(clause, i); ok { i = end } case '$': if end, ok := skipDollarQuoted(clause, i); ok { i = end } case '(', '[': depth++ case ')', ']': depth-- case ',': if depth == 0 { parts = append(parts, clause[start:i]) start = i + 1 } } } parts = append(parts, clause[start:]) trimmed := make([]string, 0, len(parts)) for _, part := range parts { if part = strings.TrimSpace(part); part != "" { trimmed = append(trimmed, part) } } return trimmed } // ParseStorageParameter splits one "key = value" storage parameter. It reports false for // anything that is not a well-formed parameter, which is how comment prose is filtered out. func ParseStorageParameter(part string) (key, value string, ok bool) { key, value, found := strings.Cut(part, "=") if !found { return "", "", false } key = strings.ToLower(strings.TrimSpace(key)) value = strings.TrimSpace(value) if key == "" || value == "" || !isBareIdentifier(key) { return "", "", false } if !isStorageParameterValue(value) { return "", "", false } return key, value, true } // FormatStorageParameters renders a WITH clause body as a canonical "key = value" list, // dropping anything malformed. Returns "" when nothing survives. func FormatStorageParameters(clause string) string { params := make([]string, 0, 4) for _, part := range SplitStorageParameters(clause) { key, value, ok := ParseStorageParameter(part) if !ok { continue } params = append(params, key+" = "+value) } return strings.Join(params, ", ") } // NormalizeStorageParameterValue unquotes a value that PostgreSQL rendered as a string but // that is really a number, so that pg_indexes output (lists='100') and hand-written models // (lists=100) normalize identically. Non-numeric quoted values keep their quotes because // some access methods require a string (pg_search's key_field='id'). func NormalizeStorageParameterValue(value string) string { value = strings.TrimSpace(value) if len(value) < 2 || value[0] != '\'' || value[len(value)-1] != '\'' { return value } inner := strings.ReplaceAll(value[1:len(value)-1], "''", "'") if _, err := strconv.ParseFloat(inner, 64); err == nil { return inner } if strings.EqualFold(inner, "true") || strings.EqualFold(inner, "false") { return strings.ToLower(inner) } return value } func isBareIdentifier(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { b := s[i] switch { case b >= 'a' && b <= 'z', b >= 'A' && b <= 'Z', b == '_': case b >= '0' && b <= '9' && i > 0: default: return false } } return true } // isStorageParameterValue reports whether value is a bare token, a complete quoted string, // or a complete dollar-quoted block. func isStorageParameterValue(value string) bool { switch { case value == "": return false case value[0] == '\'': end, ok := skipQuoted(value, 0) return ok && end == len(value)-1 case value[0] == '$': end, ok := skipDollarQuoted(value, 0) return ok && end == len(value)-1 } for i := 0; i < len(value); i++ { b := value[i] switch { case b >= 'a' && b <= 'z', b >= 'A' && b <= 'Z', b >= '0' && b <= '9': case b == '_', b == '.', b == '-', b == '+': default: return false } } return true }