mirror of
https://github.com/pocketbase/pocketbase.git
synced 2024-11-21 13:35:49 +02:00
208 lines
4.7 KiB
Go
208 lines
4.7 KiB
Go
package dbutils
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/tokenizer"
|
|
)
|
|
|
|
var (
|
|
indexRegex = regexp.MustCompile(`(?im)create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)\s*\(([\s\S]*)\)(?:\s*where\s+([\s\S]*))?`)
|
|
indexColumnRegex = regexp.MustCompile(`(?im)^([\s\S]+?)(?:\s+collate\s+([\w]+))?(?:\s+(asc|desc))?$`)
|
|
)
|
|
|
|
// IndexColumn represents a single parsed SQL index column.
|
|
type IndexColumn struct {
|
|
Name string `json:"name"` // identifier or expression
|
|
Collate string `json:"collate"`
|
|
Sort string `json:"sort"`
|
|
}
|
|
|
|
// Index represents a single parsed SQL CREATE INDEX expression.
|
|
type Index struct {
|
|
Unique bool `json:"unique"`
|
|
Optional bool `json:"optional"`
|
|
SchemaName string `json:"schemaName"`
|
|
IndexName string `json:"indexName"`
|
|
TableName string `json:"tableName"`
|
|
Columns []IndexColumn `json:"columns"`
|
|
Where string `json:"where"`
|
|
}
|
|
|
|
// IsValid checks if the current Index contains the minimum required fields to be considered valid.
|
|
func (idx Index) IsValid() bool {
|
|
return idx.IndexName != "" && idx.TableName != "" && len(idx.Columns) > 0
|
|
}
|
|
|
|
// Build returns a "CREATE INDEX" SQL string from the current index parts.
|
|
//
|
|
// Returns empty string if idx.IsValid() is false.
|
|
func (idx Index) Build() string {
|
|
if !idx.IsValid() {
|
|
return ""
|
|
}
|
|
|
|
var str strings.Builder
|
|
|
|
str.WriteString("CREATE ")
|
|
|
|
if idx.Unique {
|
|
str.WriteString("UNIQUE ")
|
|
}
|
|
|
|
str.WriteString("INDEX ")
|
|
|
|
if idx.Optional {
|
|
str.WriteString("IF NOT EXISTS ")
|
|
}
|
|
|
|
if idx.SchemaName != "" {
|
|
str.WriteString("`")
|
|
str.WriteString(idx.SchemaName)
|
|
str.WriteString("`.")
|
|
}
|
|
|
|
str.WriteString("`")
|
|
str.WriteString(idx.IndexName)
|
|
str.WriteString("` ")
|
|
|
|
str.WriteString("ON `")
|
|
str.WriteString(idx.TableName)
|
|
str.WriteString("` (")
|
|
|
|
if len(idx.Columns) > 1 {
|
|
str.WriteString("\n ")
|
|
}
|
|
|
|
var hasCol bool
|
|
for _, col := range idx.Columns {
|
|
trimmedColName := strings.TrimSpace(col.Name)
|
|
if trimmedColName == "" {
|
|
continue
|
|
}
|
|
|
|
if hasCol {
|
|
str.WriteString(",\n ")
|
|
}
|
|
|
|
if strings.Contains(col.Name, "(") || strings.Contains(col.Name, " ") {
|
|
// most likely an expression
|
|
str.WriteString(trimmedColName)
|
|
} else {
|
|
// regular identifier
|
|
str.WriteString("`")
|
|
str.WriteString(trimmedColName)
|
|
str.WriteString("`")
|
|
}
|
|
|
|
if col.Collate != "" {
|
|
str.WriteString(" COLLATE ")
|
|
str.WriteString(col.Collate)
|
|
}
|
|
|
|
if col.Sort != "" {
|
|
str.WriteString(" ")
|
|
str.WriteString(strings.ToUpper(col.Sort))
|
|
}
|
|
|
|
hasCol = true
|
|
}
|
|
|
|
if hasCol && len(idx.Columns) > 1 {
|
|
str.WriteString("\n")
|
|
}
|
|
|
|
str.WriteString(")")
|
|
|
|
if idx.Where != "" {
|
|
str.WriteString(" WHERE ")
|
|
str.WriteString(idx.Where)
|
|
}
|
|
|
|
return str.String()
|
|
}
|
|
|
|
// ParseIndex parses the provided "CREATE INDEX" SQL string into Index struct.
|
|
func ParseIndex(createIndexExpr string) Index {
|
|
result := Index{}
|
|
|
|
matches := indexRegex.FindStringSubmatch(createIndexExpr)
|
|
if len(matches) != 7 {
|
|
return result
|
|
}
|
|
|
|
trimChars := "`\"'[]\r\n\t\f\v "
|
|
|
|
// Unique
|
|
// ---
|
|
result.Unique = strings.TrimSpace(matches[1]) != ""
|
|
|
|
// Optional (aka. "IF NOT EXISTS")
|
|
// ---
|
|
result.Optional = strings.TrimSpace(matches[2]) != ""
|
|
|
|
// SchemaName and IndexName
|
|
// ---
|
|
nameTk := tokenizer.NewFromString(matches[3])
|
|
nameTk.Separators('.')
|
|
|
|
nameParts, _ := nameTk.ScanAll()
|
|
if len(nameParts) == 2 {
|
|
result.SchemaName = strings.Trim(nameParts[0], trimChars)
|
|
result.IndexName = strings.Trim(nameParts[1], trimChars)
|
|
} else {
|
|
result.IndexName = strings.Trim(nameParts[0], trimChars)
|
|
}
|
|
|
|
// TableName
|
|
// ---
|
|
result.TableName = strings.Trim(matches[4], trimChars)
|
|
|
|
// Columns
|
|
// ---
|
|
columnsTk := tokenizer.NewFromString(matches[5])
|
|
columnsTk.Separators(',')
|
|
|
|
rawColumns, _ := columnsTk.ScanAll()
|
|
|
|
result.Columns = make([]IndexColumn, 0, len(rawColumns))
|
|
|
|
for _, col := range rawColumns {
|
|
colMatches := indexColumnRegex.FindStringSubmatch(col)
|
|
if len(colMatches) != 4 {
|
|
continue
|
|
}
|
|
|
|
trimmedName := strings.Trim(colMatches[1], trimChars)
|
|
if trimmedName == "" {
|
|
continue
|
|
}
|
|
|
|
result.Columns = append(result.Columns, IndexColumn{
|
|
Name: trimmedName,
|
|
Collate: strings.TrimSpace(colMatches[2]),
|
|
Sort: strings.ToUpper(colMatches[3]),
|
|
})
|
|
}
|
|
|
|
// WHERE expression
|
|
// ---
|
|
result.Where = strings.TrimSpace(matches[6])
|
|
|
|
return result
|
|
}
|
|
|
|
// HasColumnUniqueIndex loosely checks whether the specified column has
|
|
// a single column unique index (WHERE statements are ignored).
|
|
func HasSingleColumnUniqueIndex(column string, indexes []string) bool {
|
|
for _, idx := range indexes {
|
|
parsed := ParseIndex(idx)
|
|
if parsed.Unique && len(parsed.Columns) == 1 && strings.EqualFold(parsed.Columns[0].Name, column) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|