Skip to content

Commit

Permalink
Merge pull request #49 from k1LoW/sqlite3
Browse files Browse the repository at this point in the history
Support SQLite
  • Loading branch information
k1LoW authored Aug 5, 2018
2 parents b854419 + 492051d commit ff46ff8
Show file tree
Hide file tree
Showing 66 changed files with 230,646 additions and 10 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
dist/
testdb.sqlite3
9 changes: 9 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test:
usql pg://postgres:pgpass@localhost:55432/testdb?sslmode=disable -f test/pg.sql
usql my://root:mypass@localhost:33306/testdb -f test/my.sql
usql my://root:mypass@localhost:33308/testdb -f test/my.sql
sqlite3 $(CURDIR)/test/testdb.sqlite3 < test/sqlite.sql
go test -cover -v $(shell go list ./... | grep -v vendor)
make testdoc

Expand All @@ -29,6 +30,7 @@ doc: build
./tbls doc pg://postgres:pgpass@localhost:55432/testdb?sslmode=disable -a test/additional_data.yml -f sample/postgres
./tbls doc my://root:mypass@localhost:33306/testdb -a test/additional_data.yml -f sample/mysql
./tbls doc my://root:mypass@localhost:33308/testdb -a test/additional_data.yml -f sample/mysql8
./tbls doc sq://$(CURDIR)/test/testdb.sqlite3 -a test/additional_data.yml -f sample/sqlite
./tbls doc pg://postgres:pgpass@localhost:55432/testdb?sslmode=disable -a test/additional_data.yml -j -f sample/adjust

testdoc: build
Expand All @@ -38,6 +40,8 @@ testdoc: build
@test -z "$(DIFF)" || (echo "document does not match database." && ./tbls diff my://root:mypass@localhost:33306/testdb -a test/additional_data.yml sample/mysql && exit 1)
$(eval DIFF := $(shell ./tbls diff my://root:mypass@localhost:33308/testdb -a test/additional_data.yml sample/mysql8))
@test -z "$(DIFF)" || (echo "document does not match database." && ./tbls diff my://root:mypass@localhost:33308/testdb -a test/additional_data.yml sample/mysql8 && exit 1)
$(eval DIFF := $(shell ./tbls diff sq://$(CURDIR)/test/testdb.sqlite3 -a test/additional_data.yml sample/sqlite))
@test -z "$(DIFF)" || (echo "document does not match database." && ./tbls diff sq://$(CURDIR)/test/testdb.sqlite3 -a test/additional_data.yml sample/sqlite && exit 1)
$(eval DIFF := $(shell ./tbls diff pg://postgres:pgpass@localhost:55432/testdb?sslmode=disable -a test/additional_data.yml -j sample/adjust))
@test -z "$(DIFF)" || (echo "document does not match database." && ./tbls diff pg://postgres:pgpass@localhost:55432/testdb?sslmode=disable -a test/additional_data.yml -j sample/adjust && exit 1)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ $ brew install k1LoW/tbls/tbls

- PostgreSQL
- MySQL
- SQLite
9 changes: 7 additions & 2 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/k1LoW/tbls/drivers/mysql"
"github.com/k1LoW/tbls/drivers/postgres"
"github.com/k1LoW/tbls/drivers/sqlite"
"github.com/k1LoW/tbls/schema"
"github.com/pkg/errors"
"github.com/xo/dburl"
Expand All @@ -25,10 +26,9 @@ func Analyze(urlstr string) (*schema.Schema, error) {
return s, errors.WithStack(err)
}
splitted := strings.Split(u.Short(), "/")
if len(splitted) != 2 {
if len(splitted) < 2 {
return s, errors.WithStack(fmt.Errorf("Error: %s. parse %s -> %#v", "invalid DSN", urlstr, u))
}
s.Name = splitted[1]

db, err := dburl.Open(urlstr)
defer db.Close()
Expand All @@ -43,9 +43,14 @@ func Analyze(urlstr string) (*schema.Schema, error) {

switch u.Driver {
case "postgres":
s.Name = splitted[1]
driver = new(postgres.Postgres)
case "mysql":
s.Name = splitted[1]
driver = new(mysql.Mysql)
case "sqlite3":
s.Name = splitted[len(splitted)-1]
driver = new(sqlite.Sqlite)
default:
return s, errors.WithStack(fmt.Errorf("Error: %s", "unsupported driver"))
}
Expand Down
305 changes: 305 additions & 0 deletions drivers/sqlite/sqlite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
package sqlite

import (
"database/sql"
"fmt"
"sort"
"strings"

"github.com/k1LoW/tbls/schema"
"github.com/pkg/errors"
)

// Sqlite struct
type Sqlite struct{}

type fk struct {
ID string
ForeignTableName string
ColumnNames []string
ForeignColumnNames []string
OnUpdate string
OnDelete string
Match string
}

// Analyze SQLite database schema
func (l *Sqlite) Analyze(db *sql.DB, s *schema.Schema) error {
// tables
tableRows, err := db.Query(`
SELECT name, type, sql
FROM sqlite_master
WHERE name != 'sqlite_sequence' AND (type = 'table' OR type = 'view');`)
defer tableRows.Close()
if err != nil {
return errors.WithStack(err)
}

relations := []*schema.Relation{}

tables := []*schema.Table{}
for tableRows.Next() {
var (
tableName string
tableType string
tableDef string
)
err := tableRows.Scan(&tableName, &tableType, &tableDef)
if err != nil {
return errors.WithStack(err)
}

table := &schema.Table{
Name: tableName,
Type: tableType,
Def: tableDef,
}

// constraints
constraints := []*schema.Constraint{}

// columns
columnRows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
defer columnRows.Close()
if err != nil {
return errors.WithStack(err)
}

columns := []*schema.Column{}
for columnRows.Next() {
var (
columnID string
columnName string
dataType string
columnNotNull string
columnDefault sql.NullString
columnPk string
)
err = columnRows.Scan(&columnID, &columnName, &dataType, &columnNotNull, &columnDefault, &columnPk)
if err != nil {
return errors.WithStack(err)
}
column := &schema.Column{
Name: columnName,
Type: dataType,
Nullable: convertColumnNullable(columnNotNull),
Default: columnDefault,
}
columns = append(columns, column)

if columnPk != "0" {
constraintDef := fmt.Sprintf("PRIMARY KEY (%s)", columnName)
constraint := &schema.Constraint{
Name: columnName,
Type: "PRIMARY KEY",
Def: constraintDef,
}
constraints = append(constraints, constraint)
}
}

/// foreign keys
fkMap := map[string]*fk{}
fkSlice := []*fk{}

foreignKeyRows, err := db.Query(fmt.Sprintf("PRAGMA foreign_key_list(%s)", tableName))
defer foreignKeyRows.Close()
if err != nil {
return errors.WithStack(err)
}
for foreignKeyRows.Next() {
var (
foreignKeyID string
foreignKeySeq string
foreignKeyForeignTableName string
foreignKeyColumnName string
foreignKeyForeignColumnName string
foreignKeyOnUpdate string
foreignKeyOnDelete string
foreignKeyMatch string
)
err = foreignKeyRows.Scan(
&foreignKeyID,
&foreignKeySeq,
&foreignKeyForeignTableName,
&foreignKeyColumnName,
&foreignKeyForeignColumnName,
&foreignKeyOnUpdate,
&foreignKeyOnDelete,
&foreignKeyMatch,
)
if err != nil {
return errors.WithStack(err)
}

if f, ok := fkMap[foreignKeyID]; ok {
fkMap[foreignKeyID].ColumnNames = append(f.ColumnNames, foreignKeyColumnName)
fkMap[foreignKeyID].ForeignColumnNames = append(f.ForeignColumnNames, foreignKeyForeignColumnName)
} else {
f := &fk{
ID: foreignKeyID,
ForeignTableName: foreignKeyForeignTableName,
ColumnNames: []string{foreignKeyColumnName},
ForeignColumnNames: []string{foreignKeyForeignColumnName},
OnUpdate: foreignKeyOnUpdate,
OnDelete: foreignKeyOnDelete,
Match: foreignKeyMatch,
}
fkMap[foreignKeyID] = f
}
}
for _, f := range fkMap {
fkSlice = append(fkSlice, f)
}
sort.SliceStable(fkSlice, func(i, j int) bool {
return fkSlice[i].ID < fkSlice[j].ID
})

for _, f := range fkSlice {
foreignKeyDef := fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s MATCH %s",
strings.Join(f.ColumnNames, ", "), f.ForeignTableName, strings.Join(f.ForeignColumnNames, ", "), f.OnUpdate, f.OnDelete, f.Match)
constraint := &schema.Constraint{
Name: fmt.Sprintf("- (Foreign key ID: %s)", f.ID),
Type: "FOREIGN KEY",
Def: foreignKeyDef,
}
relation := &schema.Relation{
Table: table,
Def: foreignKeyDef,
}
relations = append(relations, relation)

constraints = append(constraints, constraint)
}

// indexes and constraints(UNIQUE, PRIMARY KEY)
indexRows, err := db.Query(fmt.Sprintf("PRAGMA index_list(%s)", tableName))
defer indexRows.Close()
if err != nil {
return errors.WithStack(err)
}

indexes := []*schema.Index{}
for indexRows.Next() {
var (
indexID string
indexName string
indexIsUnique string
indexCreatedBy string
indexPartial string
indexDef string
)
err = indexRows.Scan(
&indexID,
&indexName,
&indexIsUnique,
&indexCreatedBy,
&indexPartial,
)
if err != nil {
return errors.WithStack(err)
}

if indexCreatedBy == "c" {
row, err := db.Query(`SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name = ?;
`, tableName, indexName)
for row.Next() {
err = row.Scan(
&indexDef,
)
if err != nil {
return errors.WithStack(err)
}
}
} else {
var (
colRank string
colRankWithinTable string
col string
cols []string
)
row, err := db.Query(fmt.Sprintf("PRAGMA index_info(%s)", indexName))
for row.Next() {
err = row.Scan(
&colRank,
&colRankWithinTable,
&col,
)
if err != nil {
return errors.WithStack(err)
}
cols = append(cols, col)
}
switch indexCreatedBy {
case "u":
indexDef = fmt.Sprintf("UNIQUE (%s)", strings.Join(cols, ", "))
constraint := &schema.Constraint{
Name: indexName,
Type: "UNIQUE",
Def: indexDef,
}
constraints = append(constraints, constraint)
case "pk":
// MEMO: Does not work ?
indexDef = fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(cols, ", "))
constraint := &schema.Constraint{
Name: indexName,
Type: "PRIMARY KEY",
Def: indexDef,
}
constraints = append(constraints, constraint)
}
}

index := &schema.Index{
Name: indexName,
Def: indexDef,
}
indexes = append(indexes, index)
}

// triggers
triggerRows, err := db.Query(`
SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?;
`, tableName)
defer triggerRows.Close()
if err != nil {
return errors.WithStack(err)
}

triggers := []*schema.Trigger{}
for triggerRows.Next() {
var (
triggerName string
triggerDef string
)
err = triggerRows.Scan(&triggerName, &triggerDef)
if err != nil {
return errors.WithStack(err)
}
trigger := &schema.Trigger{
Name: triggerName,
Def: triggerDef,
}
triggers = append(triggers, trigger)
}

table.Columns = columns
table.Indexes = indexes
table.Constraints = constraints
table.Triggers = triggers

tables = append(tables, table)
}

s.Tables = tables

return nil
}

func convertColumnNullable(str string) bool {
if str == "1" {
return false
}
return true
}
Loading

0 comments on commit ff46ff8

Please sign in to comment.