Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 39 additions & 33 deletions ddlmod.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,23 @@ import (
)

var (
sqliteSeparator = "`|\"|'|\t"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
sqliteSeparator = "`|\"|'"
sqliteColumnQuote = "`"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^(?:CONSTRAINT [%v]?[\w-]+[%v]? )?UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?(.*)$`, sqliteSeparator, sqliteSeparator))
checkRegexp = regexp.MustCompile(`^(?i)CHECK[\s]*\(`)
constraintRegexp = regexp.MustCompile(fmt.Sprintf(`^(?i)CONSTRAINT\s+%[1]s[\w\d_]+%[1]s[\s]+`, sqliteColumnQuote))
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
columnsRegexp = regexp.MustCompile(fmt.Sprintf(`[(,][%v]?(\w+)[%v]?`, sqliteSeparator, sqliteSeparator))
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
regRealDataType = regexp.MustCompile(`[^\d](\d+)[^\d]?`)
)

func getAllColumns(s string) []string {
allMatches := columnsRegexp.FindAllStringSubmatch(s, -1)
columns := make([]string, 0, len(allMatches))
for _, matches := range allMatches {
if len(matches) > 1 {
columns = append(columns, matches[1])
}
}
return columns
}

type ddl struct {
head string
fields []string
suffix string // table options after the column list, e.g. WITHOUT ROWID, STRICT
columns []migrator.ColumnType
}

Expand All @@ -54,6 +46,7 @@ func parseDDL(strs ...string) (*ddl, error) {
ddlBodyRunesLen := len(ddlBodyRunes)

result.head = sections[1]
result.suffix = sections[3]

for idx := 0; idx < ddlBodyRunesLen; idx++ {
var (
Expand Down Expand Up @@ -104,15 +97,15 @@ func parseDDL(strs ...string) (*ddl, error) {

for _, f := range result.fields {
fUpper := strings.ToUpper(f)
if strings.HasPrefix(fUpper, "CHECK") {
if checkRegexp.MatchString(f) || strings.HasPrefix(fUpper, "FOREIGN KEY") {
continue
}
if strings.HasPrefix(fUpper, "CONSTRAINT") {
matches := uniqueRegexp.FindStringSubmatch(f)
if matches := uniqueRegexp.FindStringSubmatch(f); matches != nil {
if len(matches) > 0 {
if columns := getAllColumns(matches[1]); len(columns) == 1 {
cols, err := parseAllColumns(matches[1])
if err == nil && len(cols) == 1 {
for idx, column := range result.columns {
if column.NameValue.String == columns[0] {
if column.NameValue.String == cols[0] {
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
result.columns[idx] = column
break
Expand All @@ -122,13 +115,19 @@ func parseDDL(strs ...string) (*ddl, error) {
}
continue
}
if constraintRegexp.MatchString(f) {
continue
}
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
for _, name := range getAllColumns(f) {
for idx, column := range result.columns {
if column.NameValue.String == name {
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
result.columns[idx] = column
break
cols, err := parseAllColumns(f)
if err == nil {
for _, name := range cols {
for idx, column := range result.columns {
if column.NameValue.String == name {
column.PrimaryKeyValue = sql.NullBool{Bool: true, Valid: true}
result.columns[idx] = column
break
}
}
}
}
Expand Down Expand Up @@ -196,10 +195,10 @@ func (d *ddl) clone() *ddl {

func (d *ddl) compile() string {
if len(d.fields) == 0 {
return d.head
return d.head + d.suffix
}

return fmt.Sprintf("%s (%s)", d.head, strings.Join(d.fields, ","))
return fmt.Sprintf("%s (%s)%s", d.head, strings.Join(d.fields, ","), d.suffix)
}

func (d *ddl) renameTable(dst, src string) error {
Expand All @@ -217,8 +216,12 @@ func (d *ddl) renameTable(dst, src string) error {
return nil
}

func compileConstraintRegexp(name string) *regexp.Regexp {
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"`]?" + regexp.QuoteMeta(name) + "[\"`\\s]")
}

func (d *ddl) addConstraint(name string, sql string) {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)

for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
Expand All @@ -231,7 +234,7 @@ func (d *ddl) addConstraint(name string, sql string) {
}

func (d *ddl) removeConstraint(name string) bool {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)

for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
Expand All @@ -243,7 +246,7 @@ func (d *ddl) removeConstraint(name string) bool {
}

func (d *ddl) hasConstraint(name string) bool {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
reg := compileConstraintRegexp(name)

for _, f := range d.fields {
if reg.MatchString(f) {
Expand All @@ -259,12 +262,15 @@ func (d *ddl) getColumns() []string {
for _, f := range d.fields {
fUpper := strings.ToUpper(f)
if strings.HasPrefix(fUpper, "PRIMARY KEY") ||
strings.HasPrefix(fUpper, "CHECK") ||
strings.HasPrefix(fUpper, "CONSTRAINT") ||
strings.HasPrefix(fUpper, "FOREIGN KEY") ||
strings.Contains(fUpper, "GENERATED ALWAYS AS") {
continue
}

if checkRegexp.MatchString(f) || constraintRegexp.MatchString(f) || uniqueRegexp.MatchString(f) {
continue
}

reg := regexp.MustCompile("^[\"`']?([\\w\\d]+)[\"`']?")
match := reg.FindStringSubmatch(f)

Expand Down
117 changes: 117 additions & 0 deletions ddlmod_parse_all_columns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package sqlite

import (
"errors"
"fmt"
)

type parseAllColumnsState int

const (
parseAllColumnsState_NONE parseAllColumnsState = iota
parseAllColumnsState_Beginning
parseAllColumnsState_ReadingRawName
parseAllColumnsState_ReadingQuotedName
parseAllColumnsState_EndOfName
parseAllColumnsState_State_End
)

func parseAllColumns(in string) ([]string, error) {
s := []rune(in)
columns := make([]string, 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, string(name))
name = make([]rune, 0)
continue
}
if s[i] == ')' {
state = parseAllColumnsState_State_End
columns = append(columns, string(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, string(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, string(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
}
}
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 == ','
}
48 changes: 48 additions & 0 deletions ddlmod_parse_all_columns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package sqlite

import "testing"

func TestParseAllColumns(t *testing.T) {
tc := []struct {
name string
input string
expected []string
}{
{
name: "Simple case",
input: "PRIMARY KEY (column1, column2)",
expected: []string{"column1", "column2"},
},
{
name: "Quoted column name",
input: "PRIMARY KEY (`column,xxx`, \"column 2\", \"column)3\", 'column''4', \"column\"\"5\")",
expected: []string{"column,xxx", "column 2", "column)3", "column'4", "column\"5"},
},
{
name: "Japanese column name",
input: "PRIMARY KEY (カラム1, `カラム2`)",
expected: []string{"カラム1", "カラム2"},
},
{
name: "Column name quoted with []",
input: "PRIMARY KEY ([column1], [column2])",
expected: []string{"column1", "column2"},
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
cols, err := parseAllColumns(tt.input)
if err != nil {
t.Errorf("Failed to parse columns: %s", err)
}
if len(cols) != len(tt.expected) {
t.Errorf("Expected %d columns, got %d", len(tt.expected), len(cols))
}
for i, col := range cols {
if col != tt.expected[i] {
t.Errorf("Expected %s, got %s", tt.expected[i], col)
}
}
})
}
}
Loading
Loading