mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
BASE_DB_TIER=sql opened PostgreSQL and then spoke to it in SQLite: strftime, sqlite_master, json_extract, json_each, an INTEGER column holding an int64, an outer join with no ON, backticked identifiers, and a rowid that only one engine has. The tier picked a backend the queries could not reach. Every one of those now comes from hanzoai/orm/dialect, resolved once from the driver the data DB was opened with and reachable as app.Dialect(). Base holds no switch on the engine — it asks for the JSON accessors, the schema catalog, the generated instant and identifier, and the column types, and it asks whether a feature exists at all where one engine has nothing to offer: an insertion-order column, a write-ahead log to fold, a function that renders an instant. Empty means the caller falls back or refuses rather than emitting something the engine will reject. Some differences turned out not to need a dialect at all. An instant is stored as text in a fixed layout, so bucketing the log rollup to the hour is a substring rather than a calendar function, which every engine spells the same way and an index can be built on. Null-safe equality is IS NOT DISTINCT FROM. An int64 column is BIGINT. A join with nothing of its own to match still states a condition. Case-insensitive comparison stays COLLATE NOCASE, because the schema now asks the engine for the collation that name refers to. The proof is a Postgres 18 running the data plane: migrations apply, a collection created over /v1 lands as a table with text, numeric and jsonb columns and its declared index, and records insert, page, sort, filter across scalars, multi-value elements and JSON paths, update and delete. A unique index declared COLLATE NOCASE rejects the second spelling of a handle. Comparing a JSON sub-path to a number is refused there: the extraction yields text, the literal binds as a number, and the filter language does not say which the comparison meant. Compared as text it works.
225 lines
5.4 KiB
Go
225 lines
5.4 KiB
Go
package dbutils
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/base/tools/tokenizer"
|
|
"github.com/hanzoai/orm/dialect"
|
|
)
|
|
|
|
var (
|
|
// the optional USING clause names the access method, which an engine that
|
|
// has more than one reports back in the index it hands out
|
|
indexRegex = regexp.MustCompile(`(?im)create\s+(unique\s+)?\s*index\s*(if\s+not\s+exists\s+)?(\S*)\s+on\s+(\S*)(?:\s+using\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 {
|
|
SchemaName string `json:"schemaName"`
|
|
IndexName string `json:"indexName"`
|
|
TableName string `json:"tableName"`
|
|
Where string `json:"where"`
|
|
Columns []IndexColumn `json:"columns"`
|
|
Unique bool `json:"unique"`
|
|
Optional bool `json:"optional"`
|
|
}
|
|
|
|
// 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(d dialect.Dialect) 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(d.Quote(idx.SchemaName))
|
|
str.WriteString(".")
|
|
}
|
|
|
|
str.WriteString(d.Quote(idx.IndexName))
|
|
str.WriteString(" ")
|
|
|
|
str.WriteString("ON ")
|
|
str.WriteString(d.Quote(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(d.Quote(trimmedColName))
|
|
}
|
|
|
|
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
|
|
// ---
|
|
// a qualifier here names the schema the table is in; Base works in one
|
|
// schema and carries it on the index name, so only the table survives
|
|
tableTk := tokenizer.NewFromString(matches[4])
|
|
tableTk.Separators('.')
|
|
|
|
tableParts, _ := tableTk.ScanAll()
|
|
if len(tableParts) > 0 {
|
|
result.TableName = strings.Trim(tableParts[len(tableParts)-1], 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
|
|
}
|
|
|
|
// FindSingleColumnUniqueIndex returns the first matching single column unique index.
|
|
func FindSingleColumnUniqueIndex(indexes []string, column string) (Index, bool) {
|
|
var index Index
|
|
|
|
for _, idx := range indexes {
|
|
index := ParseIndex(idx)
|
|
if index.Unique && len(index.Columns) == 1 && strings.EqualFold(index.Columns[0].Name, column) {
|
|
return index, true
|
|
}
|
|
}
|
|
|
|
return index, false
|
|
}
|
|
|
|
// Deprecated: Use `_, ok := FindSingleColumnUniqueIndex(indexes, column)` instead.
|
|
//
|
|
// HasColumnUniqueIndex loosely checks whether the specified column has
|
|
// a single column unique index (WHERE statements are ignored).
|
|
func HasSingleColumnUniqueIndex(column string, indexes []string) bool {
|
|
_, ok := FindSingleColumnUniqueIndex(indexes, column)
|
|
return ok
|
|
}
|