2019-02-13 16:06:34 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
// Package dbschema package implements querying and comparing schemas for testing.
|
|
|
|
package dbschema
|
|
|
|
|
|
|
|
import (
|
2020-01-14 11:29:25 +00:00
|
|
|
"context"
|
2019-02-14 13:33:42 +00:00
|
|
|
"database/sql"
|
2020-01-17 00:30:39 +00:00
|
|
|
"fmt"
|
2019-02-13 16:06:34 +00:00
|
|
|
"sort"
|
2020-01-17 00:30:39 +00:00
|
|
|
"strings"
|
2020-01-19 13:42:08 +00:00
|
|
|
|
|
|
|
"storj.io/storj/private/tagsql"
|
2019-02-13 16:06:34 +00:00
|
|
|
)
|
|
|
|
|
2019-02-14 13:33:42 +00:00
|
|
|
// Queryer is a representation for something that can query.
|
|
|
|
type Queryer interface {
|
2020-01-16 20:53:22 +00:00
|
|
|
// QueryRowContext executes a query that returns a single row.
|
|
|
|
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
2020-01-19 13:42:08 +00:00
|
|
|
// QueryContext executes a query that returns rows, typically a SELECT.
|
|
|
|
QueryContext(ctx context.Context, query string, args ...interface{}) (tagsql.Rows, error)
|
2019-02-14 13:33:42 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// Schema is the database structure.
|
|
|
|
type Schema struct {
|
|
|
|
Tables []*Table
|
|
|
|
Indexes []*Index
|
|
|
|
}
|
|
|
|
|
2020-01-17 00:30:39 +00:00
|
|
|
func (schema Schema) String() string {
|
|
|
|
var tables []string
|
|
|
|
for _, table := range schema.Tables {
|
|
|
|
tables = append(tables, table.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
var indexes []string
|
|
|
|
for _, index := range schema.Indexes {
|
|
|
|
indexes = append(indexes, index.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("Tables:\n\t%s\nIndexes:\n\t%s",
|
|
|
|
indent(strings.Join(tables, "\n")),
|
|
|
|
indent(strings.Join(indexes, "\n")))
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// Table is a sql table.
|
|
|
|
type Table struct {
|
|
|
|
Name string
|
|
|
|
Columns []*Column
|
|
|
|
PrimaryKey []string
|
|
|
|
Unique [][]string
|
|
|
|
}
|
|
|
|
|
2020-01-17 00:30:39 +00:00
|
|
|
func (table Table) String() string {
|
|
|
|
var columns []string
|
|
|
|
for _, column := range table.Columns {
|
|
|
|
columns = append(columns, column.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
var uniques []string
|
|
|
|
for _, unique := range table.Unique {
|
|
|
|
uniques = append(uniques, strings.Join(unique, " "))
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("Name: %s\nColumns:\n\t%s\nPrimaryKey: %s\nUniques:\n\t%s",
|
|
|
|
table.Name,
|
|
|
|
indent(strings.Join(columns, "\n")),
|
|
|
|
strings.Join(table.PrimaryKey, " "),
|
|
|
|
indent(strings.Join(uniques, "\n")))
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// Column is a sql column.
|
|
|
|
type Column struct {
|
|
|
|
Name string
|
|
|
|
Type string
|
|
|
|
IsNullable bool
|
2020-01-17 00:30:39 +00:00
|
|
|
Default string
|
2019-02-13 16:06:34 +00:00
|
|
|
Reference *Reference
|
|
|
|
}
|
|
|
|
|
2020-01-17 00:30:39 +00:00
|
|
|
func (column Column) String() string {
|
|
|
|
return fmt.Sprintf("Name: %s\nType: %s\nNullable: %t\nDefault: %q\nReference: %s",
|
|
|
|
column.Name,
|
|
|
|
column.Type,
|
|
|
|
column.IsNullable,
|
|
|
|
column.Default,
|
|
|
|
column.Reference)
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// Reference is a column foreign key.
|
|
|
|
type Reference struct {
|
|
|
|
Table string
|
|
|
|
Column string
|
|
|
|
OnDelete string
|
|
|
|
OnUpdate string
|
|
|
|
}
|
|
|
|
|
2020-01-17 00:30:39 +00:00
|
|
|
func (reference *Reference) String() string {
|
|
|
|
if reference == nil {
|
|
|
|
return "nil"
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("Reference<Table: %s, Column: %s, OnDelete: %s, OnUpdate: %s>",
|
|
|
|
reference.Table,
|
|
|
|
reference.Column,
|
|
|
|
reference.OnDelete,
|
|
|
|
reference.OnUpdate)
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// Index is an index for a table.
|
|
|
|
type Index struct {
|
|
|
|
Name string
|
|
|
|
Table string
|
|
|
|
Columns []string
|
|
|
|
Unique bool
|
2019-07-12 20:29:09 +01:00
|
|
|
Partial string // partial expression
|
2019-02-13 16:06:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-17 00:30:39 +00:00
|
|
|
func (index Index) String() string {
|
|
|
|
return fmt.Sprintf("Index<Table: %s, Name: %s, Columns: %s, Unique: %t, Partial: %q>",
|
|
|
|
index.Table,
|
|
|
|
index.Name,
|
|
|
|
indent(strings.Join(index.Columns, " ")),
|
|
|
|
index.Unique,
|
|
|
|
index.Partial)
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// EnsureTable returns the table with the specified name and creates one if needed.
|
|
|
|
func (schema *Schema) EnsureTable(tableName string) *Table {
|
|
|
|
for _, table := range schema.Tables {
|
|
|
|
if table.Name == tableName {
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
}
|
|
|
|
table := &Table{Name: tableName}
|
|
|
|
schema.Tables = append(schema.Tables, table)
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// DropTable removes the specified table.
|
2019-02-14 13:33:42 +00:00
|
|
|
func (schema *Schema) DropTable(tableName string) {
|
|
|
|
for i, table := range schema.Tables {
|
|
|
|
if table.Name == tableName {
|
|
|
|
schema.Tables = append(schema.Tables[:i], schema.Tables[i+1:]...)
|
2020-01-17 00:30:39 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
j := 0
|
|
|
|
for _, index := range schema.Indexes {
|
|
|
|
if index.Table == tableName {
|
|
|
|
continue
|
2019-02-14 13:33:42 +00:00
|
|
|
}
|
2020-01-17 00:30:39 +00:00
|
|
|
schema.Indexes[j] = index
|
|
|
|
j++
|
2019-02-14 13:33:42 +00:00
|
|
|
}
|
2020-01-17 00:30:39 +00:00
|
|
|
schema.Indexes = schema.Indexes[:j:j]
|
2019-02-14 13:33:42 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// FindIndex finds index in the schema.
|
|
|
|
func (schema *Schema) FindIndex(name string) (*Index, bool) {
|
|
|
|
for _, idx := range schema.Indexes {
|
|
|
|
if idx.Name == name {
|
|
|
|
return idx, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// DropIndex removes the specified index.
|
|
|
|
func (schema *Schema) DropIndex(name string) {
|
|
|
|
for i, idx := range schema.Indexes {
|
|
|
|
if idx.Name == name {
|
|
|
|
schema.Indexes = append(schema.Indexes[:i], schema.Indexes[i+1:]...)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-13 16:06:34 +00:00
|
|
|
// AddColumn adds the column to the table.
|
|
|
|
func (table *Table) AddColumn(column *Column) {
|
|
|
|
table.Columns = append(table.Columns, column)
|
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// FindColumn finds a column in the table.
|
2019-02-13 16:06:34 +00:00
|
|
|
func (table *Table) FindColumn(columnName string) (*Column, bool) {
|
|
|
|
for _, column := range table.Columns {
|
|
|
|
if column.Name == columnName {
|
|
|
|
return column, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// ColumnNames returns column names.
|
2019-02-14 13:33:42 +00:00
|
|
|
func (table *Table) ColumnNames() []string {
|
|
|
|
columns := make([]string, len(table.Columns))
|
|
|
|
for i, column := range table.Columns {
|
|
|
|
columns[i] = column.Name
|
|
|
|
}
|
|
|
|
return columns
|
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// Sort sorts tables and indexes.
|
2019-02-13 16:06:34 +00:00
|
|
|
func (schema *Schema) Sort() {
|
|
|
|
sort.Slice(schema.Tables, func(i, k int) bool {
|
|
|
|
return schema.Tables[i].Name < schema.Tables[k].Name
|
|
|
|
})
|
|
|
|
for _, table := range schema.Tables {
|
|
|
|
table.Sort()
|
|
|
|
}
|
2019-07-12 20:29:09 +01:00
|
|
|
sort.Slice(schema.Indexes, func(i, k int) bool {
|
2020-01-17 00:30:39 +00:00
|
|
|
switch {
|
|
|
|
case schema.Indexes[i].Table < schema.Indexes[k].Table:
|
|
|
|
return true
|
|
|
|
case schema.Indexes[i].Table > schema.Indexes[k].Table:
|
|
|
|
return false
|
|
|
|
default:
|
|
|
|
return schema.Indexes[i].Name < schema.Indexes[k].Name
|
|
|
|
}
|
2019-07-12 20:29:09 +01:00
|
|
|
})
|
2019-02-13 16:06:34 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 15:15:59 +01:00
|
|
|
// Sort sorts columns, primary keys and unique.
|
2019-02-13 16:06:34 +00:00
|
|
|
func (table *Table) Sort() {
|
|
|
|
sort.Slice(table.Columns, func(i, k int) bool {
|
|
|
|
return table.Columns[i].Name < table.Columns[k].Name
|
|
|
|
})
|
|
|
|
|
|
|
|
sort.Strings(table.PrimaryKey)
|
|
|
|
|
|
|
|
sort.Slice(table.Unique, func(i, k int) bool {
|
|
|
|
return lessStrings(table.Unique[i], table.Unique[k])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func lessStrings(a, b []string) bool {
|
|
|
|
n := len(a)
|
|
|
|
if len(b) < n {
|
|
|
|
n = len(b)
|
|
|
|
}
|
|
|
|
for k := 0; k < n; k++ {
|
|
|
|
if a[k] < b[k] {
|
|
|
|
return true
|
|
|
|
} else if a[k] > b[k] {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return len(a) < len(b)
|
|
|
|
}
|
2020-01-17 00:30:39 +00:00
|
|
|
|
|
|
|
func indent(lines string) string {
|
|
|
|
return strings.TrimSpace(strings.ReplaceAll(lines, "\n", "\n\t"))
|
|
|
|
}
|