Skip to content

Commit 03c362e

Browse files
Improve support for SQLite (#61)
* Update SQLize for the new Sqlite parser * Upgrade the build pipeline * Add a unit test for parsing sqlite schema * Fix the nil error in sqlite parse * FIx the sqlite scripts * Add a unit test for migrations generation * Fix some parse errors, that prevented migration from being generated * Add autoincrement keyword * Add support for unique constraint * Add support for check constraints * Add mention of sqlite constraints * Fix potential panic * Add edge cases for default values to ensure unquote stability for sqlite * Add some migration verification scripts * Refactor tests, extract file read boilerplate * Apply more CodeRabbit suggestions, fix some nitpicks * Just a couple more finishing improvements by CodeRabbit * Fix quote/unquote logic for default values
1 parent 0a20d9c commit 03c362e

File tree

11 files changed

+221
-27
lines changed

11 files changed

+221
-27
lines changed

.github/workflows/go.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Go
1+
name: Build pipeline
22

33
on:
44
push:
@@ -11,12 +11,12 @@ jobs:
1111
build:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v2
14+
- uses: actions/checkout@v4
1515

1616
- name: Set up Go
17-
uses: actions/setup-go@v2
17+
uses: actions/setup-go@v5
1818
with:
19-
go-version: 1.19
19+
go-version-file: go.mod
2020

2121
- name: Build
2222
run: go build -v ./...

element/column.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/md5"
66
"encoding/hex"
77
"fmt"
8+
"strconv"
89
"strings"
910

1011
ptypes "github.com/auxten/postgresql-parser/pkg/sql/types"
@@ -204,6 +205,40 @@ func (c Column) pkDefinition(isPrev bool) (string, bool) {
204205
continue
205206
}
206207

208+
if sql.IsSqlite() {
209+
// SQLite overrides, that pingcap doesn't support
210+
if opt.Tp == ast.ColumnOptionDefaultValue {
211+
// Parsed StrValue may be quoted in single quotes, which breaks SQL expression.
212+
// We need to unquote it and, if it's a TEXT column. quote it again with double quotes.
213+
expression, err := strconv.Unquote(opt.StrValue)
214+
if err != nil {
215+
expression = opt.StrValue
216+
}
217+
if len(expression) >= 2 && expression[0] == '\'' && expression[len(expression)-1] == '\'' {
218+
// remove single quotes. strconv may not detect it
219+
expression = expression[1 : len(expression)-1]
220+
}
221+
if c.typeDefinition(isPrev) == "TEXT" {
222+
expression = strconv.Quote(expression)
223+
}
224+
strSql += " DEFAULT " + expression
225+
continue
226+
}
227+
228+
if opt.Tp == ast.ColumnOptionAutoIncrement {
229+
strSql += " AUTOINCREMENT"
230+
continue
231+
}
232+
if opt.Tp == ast.ColumnOptionUniqKey {
233+
strSql += " UNIQUE"
234+
continue
235+
}
236+
if opt.Tp == ast.ColumnOptionCheck {
237+
strSql += " CHECK (" + opt.StrValue + ")"
238+
continue
239+
}
240+
}
241+
207242
if opt.Tp == ast.ColumnOptionReference && opt.Refer == nil { // manual add
208243
continue
209244
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/auxten/postgresql-parser v1.0.1
77
github.com/pingcap/parser v0.0.0-20200623164729-3a18f1e5dceb
88
github.com/pkg/errors v0.9.1
9-
github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd
9+
github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196
1010
)
1111

1212
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
214214
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
215215
github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd h1:wW6BtayFoKaaDeIvXRE3SZVPOscSKlYD+X3bB749+zk=
216216
github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg=
217+
github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196 h1:SjRKMwKLTEE3STO6unJlz4VlMjMv5NZgIdI9HikBeAc=
218+
github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg=
217219
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
218220
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
219221
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=

sql-parser/sqlite.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ func (p *Parser) ParserSqlite(sql string) error {
1717
return err
1818
}
1919

20-
return sqlite.Walk(p, node)
20+
_, err = sqlite.Walk(p, node)
21+
return err
2122
}
2223

2324
/*
@@ -31,7 +32,7 @@ AlterTableStatement
3132
3233
sqlite does not support drop column
3334
*/
34-
func (p *Parser) Visit(node sqlite.Node) (w sqlite.Visitor, err error) {
35+
func (p *Parser) Visit(node sqlite.Node) (w sqlite.Visitor, n sqlite.Node, err error) {
3536
switch n := node.(type) {
3637
case *sqlite.CreateTableStatement:
3738
tbName := n.Name.String()
@@ -48,7 +49,7 @@ func (p *Parser) Visit(node sqlite.Node) (w sqlite.Visitor, err error) {
4849
},
4950
CurrentAttr: element.SqlAttr{
5051
LiteType: n.Columns[i].Type,
51-
Options: p.parseSqliteConstrains(tbName, n.Columns[i].Constraints),
52+
Options: p.parseSqliteConstrains(tbName, n.Columns[i]),
5253
},
5354
}
5455

@@ -122,52 +123,51 @@ func (p *Parser) Visit(node sqlite.Node) (w sqlite.Visitor, err error) {
122123
},
123124
CurrentAttr: element.SqlAttr{
124125
LiteType: n.ColumnDef.Type,
125-
Options: p.parseSqliteConstrains(tbName, n.ColumnDef.Constraints),
126+
Options: p.parseSqliteConstrains(tbName, n.ColumnDef),
126127
},
127128
})
128129
}
129130
}
130131

131-
return nil, nil
132+
return p, nil, nil
132133
}
133134

134-
func (p *Parser) parseSqliteConstrains(tbName string, conss []sqlite.Constraint) []*ast.ColumnOption {
135+
func (p *Parser) parseSqliteConstrains(tbName string, columnDefinition *sqlite.ColumnDefinition) []*ast.ColumnOption {
136+
// https://www.sqlite.org/syntax/column-constraint.html
137+
// Also, Sqlite does not support dropping constraints, so we safely can add them here
138+
conss := columnDefinition.Constraints
135139
opts := []*ast.ColumnOption{}
136140
for _, cons := range conss {
137141
switch cons := cons.(type) {
138142
case *sqlite.PrimaryKeyConstraint:
139143
opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionPrimaryKey})
144+
if cons.Autoincrement.IsValid() {
145+
opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionAutoIncrement})
146+
}
140147

141148
case *sqlite.NotNullConstraint:
142149
opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionNotNull})
143150

144151
case *sqlite.UniqueConstraint:
145-
indexCol := make([]string, len(cons.Columns))
146-
for i := range cons.Columns {
147-
indexCol[i] = cons.Columns[i].Collation.Name
148-
}
149-
150-
p.Migration.AddIndex(tbName, element.Index{
151-
Node: element.Node{
152-
Name: cons.Name.Name,
153-
Action: element.MigrateAddAction,
154-
},
155-
Columns: indexCol,
156-
Typ: ast.IndexKeyTypeUnique,
157-
})
152+
opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionUniqKey})
158153

159154
case *sqlite.CheckConstraint:
155+
opts = append(opts, &ast.ColumnOption{
156+
Tp: ast.ColumnOptionCheck,
157+
StrValue: cons.Expr.String(),
158+
})
160159

161160
case *sqlite.DefaultConstraint:
162161
opts = append(opts, &ast.ColumnOption{
163162
Tp: ast.ColumnOptionDefaultValue,
164-
StrValue: cons.Default.String()})
163+
StrValue: cons.Expr.String(),
164+
})
165165
}
166166
}
167167

168168
return opts
169169
}
170170

171-
func (p Parser) VisitEnd(node sqlite.Node) error {
172-
return nil
171+
func (p Parser) VisitEnd(node sqlite.Node) (sqlite.Node, error) {
172+
return nil, nil
173173
}

test/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SQLize tests
2+
3+
This folder aims to align with the common guidelines of the [golang project layout](https://github.com/golang-standards/project-layout/tree/master/test).
4+
5+
## How to run?
6+
```bash
7+
go test ./test/...
8+
```

test/sqlite/common.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package sqlite
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
)
8+
9+
const (
10+
schemaWithOneTable = "./testdata/schema_one_table.sql"
11+
schemaWithTwoTables = "./testdata/schema_two_tables.sql"
12+
)
13+
14+
func assertContains(t *testing.T, str, substr, message string) {
15+
if !strings.Contains(str, substr) {
16+
t.Errorf("%s: expected to find '%s' in:\n%s", message, substr, str)
17+
}
18+
}
19+
20+
func readFile(t *testing.T, path string) string {
21+
t.Helper()
22+
data, err := os.ReadFile(path)
23+
if err != nil {
24+
t.Fatalf("Failed to read file %s: %v", path, err)
25+
}
26+
return string(data)
27+
}

test/sqlite/migration_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package sqlite
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/sunary/sqlize"
8+
)
9+
10+
// TestMigrationGeneratorSingleTable tests that Sqlize can generate a migration script for the simplest schema.
11+
func TestMigrationGeneratorSingleTable(t *testing.T) {
12+
s := sqlize.NewSqlize(
13+
sqlize.WithSqlite(),
14+
)
15+
16+
schemaSql := readFile(t, schemaWithOneTable)
17+
if err := s.FromString(schemaSql); err != nil {
18+
t.Fatalf("failed to parse schema: %v", err)
19+
}
20+
21+
upSQL := s.StringUp()
22+
downSQL := s.StringDown()
23+
24+
// Validate generated migration scripts
25+
assertContains(t, upSQL, "CREATE TABLE", "Up migration should create the table")
26+
assertContains(t, upSQL, "AUTOINCREMENT", "Up migration should include AUTOINCREMENT")
27+
assertContains(t, upSQL, "CHECK (\"age\" >= 18)", "Up migration should include CHECK constraint")
28+
assertContains(t, upSQL, "UNIQUE", "Up migration should include UNIQUE values")
29+
assertContains(t, upSQL, "DEFAULT", "Up migration should include DEFAULT values")
30+
31+
assertContains(t, downSQL, "DROP TABLE", "Down migration should drop the table")
32+
33+
upWithVersionSQL := s.StringUpWithVersion(0, false)
34+
downWithVersionSQL := s.StringDownWithVersion(0)
35+
36+
// Validate versioned migration scripts
37+
assertContains(t, upWithVersionSQL, "CREATE TABLE IF NOT EXISTS schema_migrations", "Initial migration should create the migrations table")
38+
assertContains(t, downWithVersionSQL, "DROP TABLE IF EXISTS schema_migrations", "Down migration from before initial should drop the migrations table")
39+
40+
version := s.HashValue()
41+
upWithVersionNumberSQL := s.StringUpWithVersion(version, false)
42+
43+
// Validate versioned migration scripts
44+
expectedVersionInsert := fmt.Sprintf("INSERT INTO schema_migrations (version, dirty) VALUES (%d, false);", version)
45+
46+
assertContains(t, upWithVersionNumberSQL, expectedVersionInsert, "Versioned up migration should include version comment")
47+
}

test/sqlite/parser_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package sqlite
2+
3+
import (
4+
"testing"
5+
6+
"github.com/sunary/sqlize"
7+
)
8+
9+
// TestParserSingleTable tests that Sqlize can parse a sqlite schema with one table.
10+
func TestParserSingleTable(t *testing.T) {
11+
sqlizeCurrent := sqlize.NewSqlize(
12+
sqlize.WithSqlite(),
13+
)
14+
15+
schemaSql := readFile(t, schemaWithOneTable)
16+
if err := sqlizeCurrent.FromString(schemaSql); err != nil {
17+
t.Fatalf("failed to parse schema: %v", err)
18+
}
19+
}
20+
21+
// TestParserMultipleTables tests that Sqlize can parse a sqlite schema with foreign keys.
22+
func TestParserMultipleTables(t *testing.T) {
23+
sqlizeCurrent := sqlize.NewSqlize(
24+
sqlize.WithSqlite(),
25+
)
26+
27+
schemaSql := readFile(t, schemaWithTwoTables)
28+
if err := sqlizeCurrent.FromString(schemaSql); err != nil {
29+
t.Fatalf("failed to parse schema: %v", err)
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
CREATE TABLE IF NOT EXISTS table_with_all_types (
2+
id INTEGER PRIMARY KEY AUTOINCREMENT,
3+
4+
name TEXT NOT NULL,
5+
6+
unique_number INTEGER UNIQUE,
7+
number_with_default INTEGER DEFAULT 123,
8+
9+
price REAL,
10+
price_with_default REAL DEFAULT 0.0,
11+
12+
is_active BOOLEAN DEFAULT TRUE,
13+
14+
binary_data BLOB,
15+
16+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
17+
18+
age INTEGER CHECK (age >= 18),
19+
20+
description TEXT DEFAULT "Some description",
21+
22+
empty_text TEXT DEFAULT "",
23+
single_char_text TEXT DEFAULT "x",
24+
single_quote_escape TEXT DEFAULT "It\'s a test",
25+
backslash_escape TEXT DEFAULT "C:\\Program Files",
26+
newline_escape TEXT DEFAULT "Line1\nLine2",
27+
tab_escape TEXT DEFAULT "Column1\tColumn2",
28+
unicode_escape TEXT DEFAULT "Unicode: \u263A"
29+
);
30+
31+
CREATE INDEX IF NOT EXISTS idx_name ON table_with_all_types (name);
32+
33+
CREATE INDEX IF NOT EXISTS idx_unique_number_is_active ON table_with_all_types (unique_number, is_active);

0 commit comments

Comments
 (0)