Vibhu Pandey c9e48b6de9
feat(sqlschema): add sqlschema (#8384)
## 📄 Summary

- add sqlschema package
- add unique index on email,org_id in users and user_invite
2025-07-08 00:21:26 +05:30

327 lines
9.4 KiB
Go

package sqlitesqlschema
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/SigNoz/signoz/pkg/sqlschema"
)
// Inspired by https://github.com/go-gorm/sqlite
var (
sqliteSeparator = "`|\"|'|\t"
sqliteIdentQuote = "`|\"|'"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^(?:CONSTRAINT [%v]?[\w-]+[%v]? )?UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
tableNameRegexp = regexp.MustCompile(fmt.Sprintf(`CREATE TABLE [%v]?([\w-]+)[%v]?`, sqliteSeparator, sqliteSeparator))
checkRegexp = regexp.MustCompile(`^(?i)CHECK[\s]*\(`)
constraintRegexp = regexp.MustCompile(fmt.Sprintf(`CONSTRAINT\s+[%v]?[\w\d_]+[%v]?[\s]+`, sqliteSeparator, sqliteSeparator))
foreignKeyRegexp = regexp.MustCompile(fmt.Sprintf(`FOREIGN KEY\s*\(\s*[%v]?(\w+)[%v]?\s*\)\s*REFERENCES\s*[%v]?(\w+)[%v]?\s*\(\s*[%v]?(\w+)[%v]?\s*\)`, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator))
referencesRegexp = regexp.MustCompile(fmt.Sprintf(`(\w+)\s*(\w+)\s*REFERENCES\s*[%v]?(\w+)[%v]?\s*\(\s*[%v]?(\w+)[%v]?\s*\)`, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator))
identQuoteRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteIdentQuote))
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
)
type parseAllColumnsState int
const (
parseAllColumnsState_NONE parseAllColumnsState = iota
parseAllColumnsState_Beginning
parseAllColumnsState_ReadingRawName
parseAllColumnsState_ReadingQuotedName
parseAllColumnsState_EndOfName
parseAllColumnsState_State_End
)
func parseCreateTable(str string, fmter sqlschema.SQLFormatter) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
sections := tableRegexp.FindStringSubmatch(str)
if len(sections) == 0 {
return nil, nil, errors.New("invalid DDL")
}
tableNameSections := tableNameRegexp.FindStringSubmatch(str)
if len(tableNameSections) == 0 {
return nil, nil, errors.New("invalid DDL")
}
tableName := sqlschema.TableName(tableNameSections[1])
fields := make([]string, 0)
columns := make([]*sqlschema.Column, 0)
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
var (
ddlBody = sections[2]
ddlBodyRunes = []rune(ddlBody)
bracketLevel int
quote rune
buf string
)
ddlBodyRunesLen := len(ddlBodyRunes)
for idx := 0; idx < ddlBodyRunesLen; idx++ {
var (
next rune = 0
c = ddlBodyRunes[idx]
)
if idx+1 < ddlBodyRunesLen {
next = ddlBodyRunes[idx+1]
}
if sc := string(c); identQuoteRegexp.MatchString(sc) {
if c == next {
buf += sc // Skip escaped quote
idx++
} else if quote > 0 {
quote = 0
} else {
quote = c
}
} else if quote == 0 {
if c == '(' {
bracketLevel++
} else if c == ')' {
bracketLevel--
} else if bracketLevel == 0 {
if c == ',' {
fields = append(fields, strings.TrimSpace(buf))
buf = ""
continue
}
}
}
if bracketLevel < 0 {
return nil, nil, errors.New("invalid DDL, unbalanced brackets")
}
buf += string(c)
}
if bracketLevel != 0 {
return nil, nil, errors.New("invalid DDL, unbalanced brackets")
}
if buf != "" {
fields = append(fields, strings.TrimSpace(buf))
}
for _, f := range fields {
fUpper := strings.ToUpper(f)
if checkRegexp.MatchString(f) {
continue
}
if strings.Contains(fUpper, "FOREIGN KEY") {
matches := foreignKeyRegexp.FindStringSubmatch(f)
if len(matches) >= 4 {
foreignKeyConstraints = append(foreignKeyConstraints, &sqlschema.ForeignKeyConstraint{
ReferencingColumnName: sqlschema.ColumnName(matches[1]),
ReferencedTableName: sqlschema.TableName(matches[2]),
ReferencedColumnName: sqlschema.ColumnName(matches[3]),
})
}
// This can never be a column name, so we can skip it
continue
}
if strings.Contains(fUpper, "REFERENCES") && !strings.Contains(fUpper, "FOREIGN KEY") {
matches := referencesRegexp.FindStringSubmatch(f)
if len(matches) >= 4 {
foreignKeyConstraints = append(foreignKeyConstraints, &sqlschema.ForeignKeyConstraint{
ReferencingColumnName: sqlschema.ColumnName(matches[1]),
ReferencedTableName: sqlschema.TableName(matches[3]),
ReferencedColumnName: sqlschema.ColumnName(matches[4]),
})
}
}
// Match unique constraints
if matches := uniqueRegexp.FindStringSubmatch(f); matches != nil {
if len(matches) > 0 {
cols, err := parseAllColumns(matches[1])
if err == nil {
uniqueConstraints = append(uniqueConstraints, &sqlschema.UniqueConstraint{
ColumnNames: cols,
})
}
}
// This can never be a column name, so we can skip it
continue
}
if matches := constraintRegexp.FindStringSubmatch(f); len(matches) > 0 {
if strings.Contains(fUpper, "PRIMARY KEY") {
cols, err := parseAllColumns(f)
if err == nil {
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
ColumnNames: cols,
}
}
}
// This can never be a column name, so we can skip it
continue
}
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
cols, err := parseAllColumns(f)
if err == nil {
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
ColumnNames: cols,
}
}
} else if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 0 {
column := &sqlschema.Column{
Name: sqlschema.ColumnName(matches[1]),
DataType: fmter.DataTypeOf(matches[2]),
Nullable: true,
Default: "",
}
matchUpper := strings.ToUpper(matches[3])
if strings.Contains(matchUpper, " NOT NULL") {
column.Nullable = false
} else if strings.Contains(matchUpper, " NULL") {
column.Nullable = true
}
if strings.Contains(matchUpper, " UNIQUE") && !strings.Contains(matchUpper, " PRIMARY") {
uniqueConstraints = append(uniqueConstraints, &sqlschema.UniqueConstraint{
ColumnNames: []sqlschema.ColumnName{column.Name},
})
}
if strings.Contains(matchUpper, " PRIMARY") {
column.Nullable = false
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{column.Name},
}
}
if defaultMatches := defaultValueRegexp.FindStringSubmatch(matches[3]); len(defaultMatches) > 1 {
if strings.ToLower(defaultMatches[1]) != "null" {
column.Default = strings.Trim(defaultMatches[1], `"`)
}
}
columns = append(columns, column)
}
}
return &sqlschema.Table{
Name: tableName,
Columns: columns,
PrimaryKeyConstraint: primaryKeyConstraint,
ForeignKeyConstraints: foreignKeyConstraints,
}, uniqueConstraints, nil
}
func parseAllColumns(in string) ([]sqlschema.ColumnName, error) {
s := []rune(in)
columns := make([]sqlschema.ColumnName, 0)
state := parseAllColumnsState_NONE
quote := rune(0)
name := make([]rune, 0)
for i := 0; i < len(s); i++ {
switch state {
case parseAllColumnsState_NONE:
if s[i] == '(' {
state = parseAllColumnsState_Beginning
}
case parseAllColumnsState_Beginning:
if isSpace(s[i]) {
continue
}
if isQuote(s[i]) {
state = parseAllColumnsState_ReadingQuotedName
quote = s[i]
continue
}
if s[i] == '[' {
state = parseAllColumnsState_ReadingQuotedName
quote = ']'
continue
} else if s[i] == ')' {
return columns, fmt.Errorf("unexpected token: %s", string(s[i]))
}
state = parseAllColumnsState_ReadingRawName
name = append(name, s[i])
case parseAllColumnsState_ReadingRawName:
if isSeparator(s[i]) {
state = parseAllColumnsState_Beginning
columns = append(columns, sqlschema.ColumnName(name))
name = make([]rune, 0)
continue
}
if s[i] == ')' {
state = parseAllColumnsState_State_End
columns = append(columns, sqlschema.ColumnName(name))
}
if isQuote(s[i]) {
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
}
if isSpace(s[i]) {
state = parseAllColumnsState_EndOfName
columns = append(columns, sqlschema.ColumnName(name))
name = make([]rune, 0)
continue
}
name = append(name, s[i])
case parseAllColumnsState_ReadingQuotedName:
if s[i] == quote {
// check if quote character is escaped
if i+1 < len(s) && s[i+1] == quote {
name = append(name, quote)
i++
continue
}
state = parseAllColumnsState_EndOfName
columns = append(columns, sqlschema.ColumnName(name))
name = make([]rune, 0)
continue
}
name = append(name, s[i])
case parseAllColumnsState_EndOfName:
if isSpace(s[i]) {
continue
}
if isSeparator(s[i]) {
state = parseAllColumnsState_Beginning
continue
}
if s[i] == ')' {
state = parseAllColumnsState_State_End
continue
}
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
case parseAllColumnsState_State_End:
// break is automatic in Go switch statements
}
}
if state != parseAllColumnsState_State_End {
return nil, errors.New("unexpected end")
}
return columns, nil
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isQuote(r rune) bool {
return r == '`' || r == '"' || r == '\''
}
func isSeparator(r rune) bool {
return r == ','
}