Skip to content

Commit 3af9e73

Browse files
authored
[POC] Schema migration API (go-rel#79)
* wip dsl * add more functionality to dsl * simplify struct and add tests for column * options * add index * add tests * wip query builder * more tests * reafactor map column type * wip migration manager and use functor to define migration * refactor dsl to root package * implement migrator * fix test * fix coverage * wip adapter integration for sqlite3 * special column type: id, wip mysql integration * wip postgres spec * fix test * wip migrate spec, cancel support for comment and binary * fix test and refactor map column * use strings.Builder * wip split index and key * wip index, fix tests * implement create index * add unique option * move config to global variable * fix comment * drop support for alter column on poc, bump sqlite3, more integration for migration * go mod itdy, fix test * bump sqlite3 on godep * fix rename table * add more test * add helper for optional create and drop table * postpone rename and rop key * add more tests * fix coverage * refactor NewName to Rename * accommodate timestamp version * unsigned version * add schema.Exec and schema.Do * add feature to readme * Update adapter_test.go * organize schema options into one file * remove unused codes
1 parent a3c0a8c commit 3af9e73

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2751
-384
lines changed

Gopkg.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

+4
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@
2828
[prune]
2929
go-tests = true
3030
unused-packages = true
31+
32+
[[constraint]]
33+
name = "github.com/mattn/go-sqlite3"
34+
version = "1.14.2"

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ REL is golang orm-ish database layer for layered architecture. It's testable and
2020
- Multi adapter.
2121
- Soft Deletion.
2222
- Pagination.
23-
23+
- Schema Migration.
2424

2525
## Install
2626

adapter.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ type Adapter interface {
1818
Begin(ctx context.Context) (Adapter, error)
1919
Commit(ctx context.Context) error
2020
Rollback(ctx context.Context) error
21+
22+
Apply(ctx context.Context, migration Migration) error
2123
}

adapter/mysql/mysql.go

+16-9
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,26 @@ type Adapter struct {
2525
*sql.Adapter
2626
}
2727

28-
var _ rel.Adapter = (*Adapter)(nil)
28+
var (
29+
_ rel.Adapter = (*Adapter)(nil)
2930

30-
// New is mysql adapter constructor.
31+
// Config for mysql adapter.
32+
Config = sql.Config{
33+
DropIndexOnTable: true,
34+
Placeholder: "?",
35+
EscapeChar: "`",
36+
IncrementFunc: incrementFunc,
37+
ErrorFunc: errorFunc,
38+
MapColumnFunc: sql.MapColumn,
39+
}
40+
)
41+
42+
// New mysql adapter using existing connection.
3143
func New(database *db.DB) *Adapter {
3244
return &Adapter{
3345
Adapter: &sql.Adapter{
34-
Config: &sql.Config{
35-
Placeholder: "?",
36-
EscapeChar: "`",
37-
IncrementFunc: incrementFunc,
38-
ErrorFunc: errorFunc,
39-
},
40-
DB: database,
46+
Config: Config,
47+
DB: database,
4148
},
4249
}
4350
}

adapter/mysql/mysql_test.go

+8-52
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,6 @@ import (
1515

1616
var ctx = context.TODO()
1717

18-
func init() {
19-
adapter, err := Open(dsn())
20-
paranoid.Panic(err, "failed to open database connection")
21-
22-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS extras;`, nil)
23-
paranoid.Panic(err, "failed dropping extras table")
24-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS addresses;`, nil)
25-
paranoid.Panic(err, "failed dropping addresses table")
26-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS users;`, nil)
27-
paranoid.Panic(err, "failed dropping users table")
28-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS composites;`, nil)
29-
paranoid.Panic(err, "failed dropping composites table")
30-
31-
_, _, err = adapter.Exec(ctx, `CREATE TABLE users (
32-
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
33-
name VARCHAR(30) NOT NULL DEFAULT '',
34-
gender VARCHAR(10) NOT NULL DEFAULT '',
35-
age INT NOT NULL DEFAULT 0,
36-
note varchar(50),
37-
created_at DATETIME,
38-
updated_at DATETIME
39-
);`, nil)
40-
paranoid.Panic(err, "failed creating users table")
41-
42-
_, _, err = adapter.Exec(ctx, `CREATE TABLE addresses (
43-
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
44-
user_id INT UNSIGNED,
45-
name VARCHAR(60) NOT NULL DEFAULT '',
46-
created_at DATETIME,
47-
updated_at DATETIME,
48-
FOREIGN KEY (user_id) REFERENCES users(id)
49-
);`, nil)
50-
paranoid.Panic(err, "failed creating addresses table")
51-
52-
_, _, err = adapter.Exec(ctx, `CREATE TABLE extras (
53-
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
54-
slug VARCHAR(30) DEFAULT NULL UNIQUE,
55-
user_id INT UNSIGNED,
56-
SCORE INT,
57-
CONSTRAINT extras_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id)
58-
);`, nil)
59-
paranoid.Panic(err, "failed creating extras table")
60-
61-
_, _, err = adapter.Exec(ctx, `CREATE TABLE composites (
62-
primary1 INT UNSIGNED,
63-
primary2 INT UNSIGNED,
64-
data VARCHAR(255) DEFAULT NULL,
65-
PRIMARY KEY (primary1, primary2)
66-
);`, nil)
67-
paranoid.Panic(err, "failed creating composites table")
68-
}
69-
7018
func dsn() string {
7119
if os.Getenv("MYSQL_DATABASE") != "" {
7220
return os.Getenv("MYSQL_DATABASE") + "?charset=utf8&parseTime=True&loc=Local"
@@ -82,6 +30,14 @@ func TestAdapter_specs(t *testing.T) {
8230

8331
repo := rel.New(adapter)
8432

33+
// Prepare tables
34+
teardown := specs.Setup(t, repo)
35+
defer teardown()
36+
37+
// Migration Specs
38+
// - Rename column is only supported by MySQL 8.0
39+
specs.Migrate(t, repo, specs.SkipRenameColumn)
40+
8541
// Query Specs
8642
specs.Query(t, repo)
8743
specs.QueryJoin(t, repo)

adapter/postgres/postgres.go

+47-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package postgres
1515
import (
1616
"context"
1717
db "database/sql"
18+
"time"
1819

1920
"github.com/Fs02/rel"
2021
"github.com/Fs02/rel/adapter/sql"
@@ -25,20 +26,26 @@ type Adapter struct {
2526
*sql.Adapter
2627
}
2728

28-
var _ rel.Adapter = (*Adapter)(nil)
29+
var (
30+
_ rel.Adapter = (*Adapter)(nil)
31+
32+
// Config for postgres adapter.
33+
Config = sql.Config{
34+
Placeholder: "$",
35+
EscapeChar: "\"",
36+
Ordinal: true,
37+
InsertDefaultValues: true,
38+
ErrorFunc: errorFunc,
39+
MapColumnFunc: mapColumnFunc,
40+
}
41+
)
2942

30-
// New is postgres adapter constructor.
43+
// New postgres adapter using existing connection.
3144
func New(database *db.DB) *Adapter {
3245
return &Adapter{
3346
Adapter: &sql.Adapter{
34-
Config: &sql.Config{
35-
Placeholder: "$",
36-
EscapeChar: "\"",
37-
Ordinal: true,
38-
InsertDefaultValues: true,
39-
ErrorFunc: errorFunc,
40-
},
41-
DB: database,
47+
Config: Config,
48+
DB: database,
4249
},
4350
}
4451
}
@@ -144,3 +151,33 @@ func errorFunc(err error) error {
144151
return err
145152
}
146153
}
154+
155+
func mapColumnFunc(column *rel.Column) (string, int, int) {
156+
var (
157+
typ string
158+
m, n int
159+
)
160+
161+
// postgres specific
162+
column.Unsigned = false
163+
if column.Default == "" {
164+
column.Default = nil
165+
}
166+
167+
switch column.Type {
168+
case rel.ID:
169+
typ = "SERIAL NOT NULL PRIMARY KEY"
170+
case rel.DateTime:
171+
typ = "TIMESTAMPTZ"
172+
if t, ok := column.Default.(time.Time); ok {
173+
column.Default = t.Format("2006-01-02 15:04:05")
174+
}
175+
case rel.Int, rel.BigInt, rel.Text:
176+
column.Limit = 0
177+
typ, m, n = sql.MapColumn(column)
178+
default:
179+
typ, m, n = sql.MapColumn(column)
180+
}
181+
182+
return typ, m, n
183+
}

adapter/postgres/postgres_test.go

+9-54
Original file line numberDiff line numberDiff line change
@@ -16,68 +16,16 @@ import (
1616
var ctx = context.TODO()
1717

1818
func init() {
19-
adapter, err := Open(dsn())
20-
paranoid.Panic(err, "failed to open database connection")
21-
defer adapter.Close()
22-
23-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS extras;`, nil)
24-
paranoid.Panic(err, "failed dropping extras table")
25-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS addresses;`, nil)
26-
paranoid.Panic(err, "failed dropping addresses table")
27-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS users;`, nil)
28-
paranoid.Panic(err, "failed dropping users table")
29-
_, _, err = adapter.Exec(ctx, `DROP TABLE IF EXISTS composites;`, nil)
30-
paranoid.Panic(err, "failed dropping composites table")
31-
32-
_, _, err = adapter.Exec(ctx, `CREATE TABLE users (
33-
id SERIAL NOT NULL PRIMARY KEY,
34-
slug VARCHAR(30) DEFAULT NULL,
35-
name VARCHAR(30) NOT NULL DEFAULT '',
36-
gender VARCHAR(10) NOT NULL DEFAULT '',
37-
age INT NOT NULL DEFAULT 0,
38-
note varchar(50),
39-
created_at TIMESTAMPTZ,
40-
updated_at TIMESTAMPTZ,
41-
UNIQUE(slug)
42-
);`, nil)
43-
paranoid.Panic(err, "failed creating users table")
44-
45-
_, _, err = adapter.Exec(ctx, `CREATE TABLE addresses (
46-
id SERIAL NOT NULL PRIMARY KEY,
47-
user_id INTEGER REFERENCES users(id),
48-
name VARCHAR(60) NOT NULL DEFAULT '',
49-
created_at TIMESTAMPTZ,
50-
updated_at TIMESTAMPTZ
51-
);`, nil)
52-
paranoid.Panic(err, "failed creating addresses table")
53-
54-
_, _, err = adapter.Exec(ctx, `CREATE TABLE extras (
55-
id SERIAL NOT NULL PRIMARY KEY,
56-
slug VARCHAR(30) DEFAULT NULL UNIQUE,
57-
user_id INTEGER REFERENCES users(id),
58-
score INTEGER DEFAULT 0 CHECK (score>=0 AND score<=100)
59-
);`, nil)
60-
paranoid.Panic(err, "failed creating extras table")
61-
62-
_, _, err = adapter.Exec(ctx, `CREATE TABLE composites (
63-
primary1 SERIAL NOT NULL,
64-
primary2 SERIAL NOT NULL,
65-
data VARCHAR(255) DEFAULT NULL,
66-
PRIMARY KEY (primary1, primary2)
67-
);`, nil)
68-
paranoid.Panic(err, "failed creating composites table")
69-
7019
// hack to make sure location it has the same location object as returned by pq driver.
71-
time.Local, err = time.LoadLocation("Asia/Jakarta")
72-
paranoid.Panic(err, "failed loading time location")
20+
time.Local, _ = time.LoadLocation("Asia/Jakarta")
7321
}
7422

7523
func dsn() string {
7624
if os.Getenv("POSTGRESQL_DATABASE") != "" {
7725
return os.Getenv("POSTGRESQL_DATABASE") + "?sslmode=disable&timezone=Asia/Jakarta"
7826
}
7927

80-
return "postgres://rel@localhost:9920/rel_test?sslmode=disable&timezone=Asia/Jakarta"
28+
return "postgres://rel@localhost:5432/rel_test?sslmode=disable&timezone=Asia/Jakarta"
8129
}
8230

8331
func TestAdapter_specs(t *testing.T) {
@@ -87,6 +35,13 @@ func TestAdapter_specs(t *testing.T) {
8735

8836
repo := rel.New(adapter)
8937

38+
// Prepare tables
39+
teardown := specs.Setup(t, repo)
40+
defer teardown()
41+
42+
// Migration Specs
43+
specs.Migrate(t, repo)
44+
9045
// Query Specs
9146
specs.Query(t, repo)
9247
specs.QueryJoin(t, repo)

0 commit comments

Comments
 (0)