1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-02-13 00:16:19 +02:00

added create index sql parser

This commit is contained in:
Gani Georgiev 2023-03-19 10:15:26 +02:00
parent 5fd103481c
commit 44f5172db7
2 changed files with 248 additions and 0 deletions

106
tools/dbutils/index.go Normal file
View File

@ -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
}

142
tools/dbutils/index_test.go Normal file
View File

@ -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)
}
}
}