Create dbutil package for sqlite (#1311)
This commit is contained in:
parent
df20597f67
commit
84c2f991d2
@ -47,7 +47,7 @@ func QuerySchema(db dbschema.Queryer) (*dbschema.Schema, error) {
|
||||
return rows.Err()
|
||||
}()
|
||||
if err != nil {
|
||||
return schema, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find constraints
|
||||
|
78
internal/dbutil/sqliteutil/db.go
Normal file
78
internal/dbutil/sqliteutil/db.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package sqliteutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/internal/dbutil/dbschema"
|
||||
)
|
||||
|
||||
// LoadSchemaFromSQL inserts script into connstr and loads schema.
|
||||
func LoadSchemaFromSQL(script string) (_ *dbschema.Schema, err error) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, db.Close()) }()
|
||||
|
||||
_, err = db.Exec(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return QuerySchema(db)
|
||||
}
|
||||
|
||||
// LoadSnapshotFromSQL inserts script into connstr and loads schema.
|
||||
func LoadSnapshotFromSQL(script string) (_ *dbschema.Snapshot, err error) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, db.Close()) }()
|
||||
|
||||
_, err = db.Exec(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot, err := QuerySnapshot(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot.Script = script
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
// QuerySnapshot loads snapshot from database
|
||||
func QuerySnapshot(db dbschema.Queryer) (*dbschema.Snapshot, error) {
|
||||
schema, err := QuerySchema(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := QueryData(db, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dbschema.Snapshot{
|
||||
Version: -1,
|
||||
Schema: schema,
|
||||
Data: data,
|
||||
}, err
|
||||
}
|
||||
|
||||
// QueryData loads all data from tables
|
||||
func QueryData(db dbschema.Queryer, schema *dbschema.Schema) (*dbschema.Data, error) {
|
||||
return dbschema.QueryData(db, schema, func(columnName string) string {
|
||||
quoted := strconv.Quote(columnName)
|
||||
return `quote(` + quoted + `) as ` + quoted
|
||||
})
|
||||
}
|
188
internal/dbutil/sqliteutil/query.go
Normal file
188
internal/dbutil/sqliteutil/query.go
Normal file
@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package sqliteutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/internal/dbutil/dbschema"
|
||||
)
|
||||
|
||||
type definition struct {
|
||||
name string
|
||||
sql string
|
||||
}
|
||||
|
||||
// QuerySchema loads the schema from sqlite database.
|
||||
func QuerySchema(db dbschema.Queryer) (*dbschema.Schema, error) {
|
||||
schema := &dbschema.Schema{}
|
||||
|
||||
tableDefinitions := make([]*definition, 0)
|
||||
indexDefinitions := make([]*definition, 0)
|
||||
|
||||
// find tables and indexes
|
||||
err := func() error {
|
||||
rows, err := db.Query(`
|
||||
SELECT name, type, sql FROM sqlite_master WHERE sql NOT NULL AND name NOT LIKE 'sqlite_%'
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, rows.Close()) }()
|
||||
|
||||
for rows.Next() {
|
||||
var defName, defType, defSQL string
|
||||
err := rows.Scan(&defName, &defType, &defSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if defType == "table" {
|
||||
tableDefinitions = append(tableDefinitions, &definition{name: defName, sql: defSQL})
|
||||
} else if defType == "index" {
|
||||
indexDefinitions = append(indexDefinitions, &definition{name: defName, sql: defSQL})
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = discoverTables(db, schema, tableDefinitions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = discoverIndexes(db, schema, indexDefinitions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schema.Sort()
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func discoverTables(db dbschema.Queryer, schema *dbschema.Schema, tableDefinitions []*definition) (err error) {
|
||||
for _, definition := range tableDefinitions {
|
||||
table := schema.EnsureTable(definition.name)
|
||||
|
||||
tableRows, err := db.Query(`PRAGMA table_info(` + definition.name + `)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, tableRows.Close()) }()
|
||||
|
||||
for tableRows.Next() {
|
||||
var defaultValue sql.NullString
|
||||
var index, name, columnType string
|
||||
var pk int
|
||||
var notNull bool
|
||||
err := tableRows.Scan(&index, &name, &columnType, ¬Null, &defaultValue, &pk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
column := &dbschema.Column{
|
||||
Name: name,
|
||||
Type: columnType,
|
||||
IsNullable: !notNull && pk == 0,
|
||||
}
|
||||
table.AddColumn(column)
|
||||
if pk > 0 {
|
||||
if table.PrimaryKey == nil {
|
||||
table.PrimaryKey = make([]string, 0)
|
||||
}
|
||||
table.PrimaryKey = append(table.PrimaryKey, name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
matches := rxUnique.FindAllStringSubmatch(definition.sql, -1)
|
||||
for _, match := range matches {
|
||||
// TODO feel this can be done easier
|
||||
var columns []string
|
||||
for _, name := range strings.Split(match[1], ",") {
|
||||
columns = append(columns, strings.TrimSpace(name))
|
||||
}
|
||||
|
||||
table.Unique = append(table.Unique, columns)
|
||||
}
|
||||
|
||||
keysRows, err := db.Query(`PRAGMA foreign_key_list(` + definition.name + `)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, keysRows.Close()) }()
|
||||
|
||||
for keysRows.Next() {
|
||||
var id, sec int
|
||||
var tableName, from, to, onUpdate, onDelete, match string
|
||||
err := keysRows.Scan(&id, &sec, &tableName, &from, &to, &onUpdate, &onDelete, &match)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
column, found := table.FindColumn(from)
|
||||
if found {
|
||||
if onDelete == "NO ACTION" {
|
||||
onDelete = ""
|
||||
}
|
||||
if onUpdate == "NO ACTION" {
|
||||
onUpdate = ""
|
||||
}
|
||||
column.Reference = &dbschema.Reference{
|
||||
Table: tableName,
|
||||
Column: to,
|
||||
OnUpdate: onUpdate,
|
||||
OnDelete: onDelete,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func discoverIndexes(db dbschema.Queryer, schema *dbschema.Schema, indexDefinitions []*definition) (err error) {
|
||||
// TODO improve indexes discovery
|
||||
for _, definition := range indexDefinitions {
|
||||
index := &dbschema.Index{
|
||||
Name: definition.name,
|
||||
}
|
||||
schema.Indexes = append(schema.Indexes, index)
|
||||
|
||||
indexRows, err := db.Query(`PRAGMA index_info(` + definition.name + `)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, indexRows.Close()) }()
|
||||
|
||||
for indexRows.Next() {
|
||||
var name string
|
||||
var seqno, cid int
|
||||
err := indexRows.Scan(&seqno, &cid, &name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index.Columns = append(index.Columns, name)
|
||||
}
|
||||
|
||||
matches := rxIndexTable.FindStringSubmatch(definition.sql)
|
||||
index.Table = strings.TrimSpace(matches[1])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
// matches UNIQUE (a,b)
|
||||
rxUnique = regexp.MustCompile(`UNIQUE\s*\((.*?)\)`)
|
||||
|
||||
// matches ON (a,b)
|
||||
rxIndexTable = regexp.MustCompile(`ON\s*(.*)\(`)
|
||||
)
|
104
internal/dbutil/sqliteutil/query_test.go
Normal file
104
internal/dbutil/sqliteutil/query_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package sqliteutil_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/storj/internal/dbutil/dbschema"
|
||||
"storj.io/storj/internal/dbutil/sqliteutil"
|
||||
"storj.io/storj/internal/testcontext"
|
||||
)
|
||||
|
||||
func TestQuery(t *testing.T) {
|
||||
ctx := testcontext.New(t)
|
||||
defer ctx.Cleanup()
|
||||
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer ctx.Check(db.Close)
|
||||
|
||||
emptySchema, err := sqliteutil.QuerySchema(db)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &dbschema.Schema{}, emptySchema)
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
a integer NOT NULL,
|
||||
b integer NOT NULL,
|
||||
c text,
|
||||
UNIQUE (c),
|
||||
PRIMARY KEY (a)
|
||||
);
|
||||
CREATE TABLE names (
|
||||
users_a integer REFERENCES users( a ) ON DELETE CASCADE,
|
||||
a text NOT NULL,
|
||||
x text,
|
||||
b text,
|
||||
PRIMARY KEY (a, x),
|
||||
UNIQUE ( x ),
|
||||
UNIQUE ( a, b )
|
||||
);
|
||||
CREATE INDEX names_a ON names (a, b);
|
||||
`)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
schema, err := sqliteutil.QuerySchema(db)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := &dbschema.Schema{
|
||||
Tables: []*dbschema.Table{
|
||||
{
|
||||
Name: "users",
|
||||
Columns: []*dbschema.Column{
|
||||
{Name: "a", Type: "integer", IsNullable: false, Reference: nil},
|
||||
{Name: "b", Type: "integer", IsNullable: false, Reference: nil},
|
||||
{Name: "c", Type: "text", IsNullable: true, Reference: nil},
|
||||
},
|
||||
PrimaryKey: []string{"a"},
|
||||
Unique: [][]string{
|
||||
{"c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "names",
|
||||
Columns: []*dbschema.Column{
|
||||
{Name: "users_a", Type: "integer", IsNullable: true,
|
||||
Reference: &dbschema.Reference{
|
||||
Table: "users",
|
||||
Column: "a",
|
||||
OnDelete: "CASCADE",
|
||||
}},
|
||||
{Name: "a", Type: "text", IsNullable: false, Reference: nil},
|
||||
{Name: "x", Type: "text", IsNullable: false, Reference: nil}, // not null, because primary key
|
||||
{Name: "b", Type: "text", IsNullable: true, Reference: nil},
|
||||
},
|
||||
PrimaryKey: []string{"a", "x"},
|
||||
Unique: [][]string{
|
||||
{"a", "b"},
|
||||
{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Indexes: []*dbschema.Index{
|
||||
{
|
||||
Name: "names_a",
|
||||
Table: "names",
|
||||
Columns: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected.Sort()
|
||||
schema.Sort()
|
||||
assert.Equal(t, expected, schema)
|
||||
}
|
Loading…
Reference in New Issue
Block a user