added create index sql parser
This commit is contained in:
parent
5fd103481c
commit
44f5172db7
|
@ -0,0 +1,106 @@
|
||||||
|
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+)?([\w\"\'\[\]\.]*)\s+on\s+([\w\"\'\[\]\.]*)\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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package dbutils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tools/dbutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseIndex(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
index string
|
||||||
|
expected dbutils.Index
|
||||||
|
}{
|
||||||
|
// invalid
|
||||||
|
{
|
||||||
|
`invalid`,
|
||||||
|
dbutils.Index{},
|
||||||
|
},
|
||||||
|
// simple
|
||||||
|
{
|
||||||
|
`create index indexname on tablename (col1)`,
|
||||||
|
dbutils.Index{
|
||||||
|
IndexName: "indexname",
|
||||||
|
TableName: "tablename",
|
||||||
|
Columns: []dbutils.IndexColumn{
|
||||||
|
{Name: "col1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// all fields
|
||||||
|
{
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS "schemaname".[indexname] on 'tablename' (
|
||||||
|
col1,
|
||||||
|
json_extract("col2", "$.a") asc,
|
||||||
|
"col3" collate NOCASE,
|
||||||
|
"col4" collate RTRIM desc
|
||||||
|
) where test = 1`,
|
||||||
|
dbutils.Index{
|
||||||
|
Unique: true,
|
||||||
|
Optional: true,
|
||||||
|
SchemaName: "schemaname",
|
||||||
|
IndexName: "indexname",
|
||||||
|
TableName: "tablename",
|
||||||
|
Columns: []dbutils.IndexColumn{
|
||||||
|
{Name: "col1"},
|
||||||
|
{Name: `json_extract("col2", "$.a")`, Sort: "ASC"},
|
||||||
|
{Name: `col3`, Collate: "NOCASE"},
|
||||||
|
{Name: `col4`, Collate: "RTRIM", Sort: "DESC"},
|
||||||
|
},
|
||||||
|
Where: "test = 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range scenarios {
|
||||||
|
result := dbutils.ParseIndex(s.index)
|
||||||
|
|
||||||
|
resultRaw, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRaw, err := json.Marshal(s.expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%d] %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(resultRaw, expectedRaw) {
|
||||||
|
t.Errorf("[%d] Expected \n%s \ngot \n%s", i, expectedRaw, resultRaw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexIsValid(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
index dbutils.Index
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
dbutils.Index{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no index name",
|
||||||
|
dbutils.Index{
|
||||||
|
TableName: "table",
|
||||||
|
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no table name",
|
||||||
|
dbutils.Index{
|
||||||
|
IndexName: "index",
|
||||||
|
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no columns",
|
||||||
|
dbutils.Index{
|
||||||
|
IndexName: "index",
|
||||||
|
TableName: "table",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"min valid",
|
||||||
|
dbutils.Index{
|
||||||
|
IndexName: "index",
|
||||||
|
TableName: "table",
|
||||||
|
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"all fields",
|
||||||
|
dbutils.Index{
|
||||||
|
Optional: true,
|
||||||
|
Unique: true,
|
||||||
|
SchemaName: "schema",
|
||||||
|
IndexName: "index",
|
||||||
|
TableName: "table",
|
||||||
|
Columns: []dbutils.IndexColumn{{Name: "col"}},
|
||||||
|
Where: "test = 1 OR test = 2",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
result := s.index.IsValid()
|
||||||
|
|
||||||
|
if result != s.expected {
|
||||||
|
t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue