snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
1226
dialect/sql/schema/atlas.go
Normal file
1226
dialect/sql/schema/atlas.go
Normal file
File diff suppressed because it is too large
Load Diff
160
dialect/sql/schema/migrate.go
Normal file
160
dialect/sql/schema/migrate.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeTable defines the table name holding the type information.
|
||||
TypeTable = "ent_types"
|
||||
|
||||
// MaxTypes defines the max number of types can be created when
|
||||
// defining universal ids. The left 16-bits are reserved.
|
||||
MaxTypes = math.MaxUint16
|
||||
)
|
||||
|
||||
// NewTypesTable returns a new table for holding the global-id information.
|
||||
func NewTypesTable() *Table {
|
||||
return NewTable(TypeTable).
|
||||
AddPrimary(&Column{Name: "id", Type: field.TypeUint, Increment: true}).
|
||||
AddColumn(&Column{Name: "type", Type: field.TypeString, Unique: true})
|
||||
}
|
||||
|
||||
// MigrateOption allows configuring Atlas using functional arguments.
|
||||
type MigrateOption func(*Atlas)
|
||||
|
||||
// WithGlobalUniqueID sets the universal ids options to the migration.
|
||||
// Defaults to false.
|
||||
func WithGlobalUniqueID(b bool) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.universalID = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithIndent sets Atlas to generate SQL statements with indentation.
|
||||
// An empty string indicates no indentation.
|
||||
func WithIndent(indent string) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.indent = indent
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrNoPlan sets Atlas to returns a migrate.ErrNoPlan in case
|
||||
// the migration plan is empty. Defaults to false.
|
||||
func WithErrNoPlan(b bool) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.errNoPlan = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithSchemaName sets the database schema for the migration.
|
||||
// If not set, the CURRENT_SCHEMA() is used.
|
||||
func WithSchemaName(ns string) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.schema = ns
|
||||
}
|
||||
}
|
||||
|
||||
// WithDropColumn sets the columns dropping option to the migration.
|
||||
// Defaults to false.
|
||||
func WithDropColumn(b bool) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.dropColumns = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithDropIndex sets the indexes dropping option to the migration.
|
||||
// Defaults to false.
|
||||
func WithDropIndex(b bool) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.dropIndexes = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithForeignKeys enables creating foreign-key in ddl. Defaults to true.
|
||||
func WithForeignKeys(b bool) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.withForeignKeys = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithHooks adds a list of hooks to the schema migration.
|
||||
func WithHooks(hooks ...Hook) MigrateOption {
|
||||
return func(a *Atlas) {
|
||||
a.hooks = append(a.hooks, hooks...)
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
// Creator is the interface that wraps the Create method.
|
||||
Creator interface {
|
||||
// Create creates the given tables in the database. See Migrate.Create for more details.
|
||||
Create(context.Context, ...*Table) error
|
||||
}
|
||||
|
||||
// The CreateFunc type is an adapter to allow the use of ordinary function as Creator.
|
||||
// If f is a function with the appropriate signature, CreateFunc(f) is a Creator that calls f.
|
||||
CreateFunc func(context.Context, ...*Table) error
|
||||
|
||||
// Hook defines the "create middleware". A function that gets a Creator and returns a Creator.
|
||||
// For example:
|
||||
//
|
||||
// hook := func(next schema.Creator) schema.Creator {
|
||||
// return schema.CreateFunc(func(ctx context.Context, tables ...*schema.Table) error {
|
||||
// fmt.Println("Tables:", tables)
|
||||
// return next.Create(ctx, tables...)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
Hook func(Creator) Creator
|
||||
)
|
||||
|
||||
// Create calls f(ctx, tables...).
|
||||
func (f CreateFunc) Create(ctx context.Context, tables ...*Table) error {
|
||||
return f(ctx, tables...)
|
||||
}
|
||||
|
||||
// exist checks if the given COUNT query returns a value >= 1.
|
||||
func exist(ctx context.Context, conn dialect.ExecQuerier, query string, args ...any) (bool, error) {
|
||||
rows := &sql.Rows{}
|
||||
if err := conn.Query(ctx, query, args, rows); err != nil {
|
||||
return false, fmt.Errorf("reading schema information %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
n, err := sql.ScanInt(rows)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
func indexOf(a []string, s string) int {
|
||||
for i := range a {
|
||||
if a[i] == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type sqlDialect interface {
|
||||
atBuilder
|
||||
dialect.Driver
|
||||
init(context.Context) error
|
||||
tableExist(context.Context, dialect.ExecQuerier, string) (bool, error)
|
||||
}
|
||||
|
||||
// verifyRanger wraps the method for verifying global-id range correctness.
|
||||
type verifyRanger interface {
|
||||
verifyRange(context.Context, dialect.ExecQuerier, *Table, int64) error
|
||||
}
|
||||
464
dialect/sql/schema/migrate_test.go
Normal file
464
dialect/sql/schema/migrate_test.go
Normal file
@@ -0,0 +1,464 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"ariga.io/atlas/sql/schema"
|
||||
"ariga.io/atlas/sql/sqlite"
|
||||
"ariga.io/atlas/sql/sqltool"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrate_SchemaName(t *testing.T) {
|
||||
db, mk, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
mk.ExpectQuery(escape("SHOW server_version_num")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"server_version_num"}).AddRow("130000"))
|
||||
mk.ExpectQuery(escape("SELECT current_setting('server_version_num'), current_setting('default_table_access_method', true), current_setting('crdb_version', true)")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"current_setting", "current_setting", "current_setting"}).AddRow("130000", "heap", ""))
|
||||
mk.ExpectQuery("SELECT nspname AS schema_name,.+").
|
||||
WithArgs("public"). // Schema "public" param is used.
|
||||
WillReturnRows(sqlmock.NewRows([]string{"schema_name", "comment"}).AddRow("public", "default schema"))
|
||||
mk.ExpectQuery("SELECT t3.oid, t1.table_schema,.+").
|
||||
WillReturnRows(sqlmock.NewRows([]string{}))
|
||||
m, err := NewMigrate(sql.OpenDB("postgres", db), WithSchemaName("public"), WithDiffHook(func(next Differ) Differ {
|
||||
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
|
||||
return nil, nil // Noop.
|
||||
})
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(context.Background()))
|
||||
require.NoError(t, mk.ExpectationsWereMet())
|
||||
|
||||
// Without schema name the CURRENT_SCHEMA is used.
|
||||
mk.ExpectQuery(escape("SHOW server_version_num")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"server_version_num"}).AddRow("130000"))
|
||||
mk.ExpectQuery(escape("SELECT current_setting('server_version_num'), current_setting('default_table_access_method', true), current_setting('crdb_version', true)")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"current_setting", "current_setting", "current_setting"}).AddRow("130000", "heap", ""))
|
||||
mk.ExpectQuery("SELECT nspname AS schema_name,.+CURRENT_SCHEMA().+").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"schema_name", "comment"}).AddRow("public", "default schema"))
|
||||
mk.ExpectQuery("SELECT t3.oid, t1.table_schema,.+").
|
||||
WillReturnRows(sqlmock.NewRows([]string{}))
|
||||
m, err = NewMigrate(sql.OpenDB("postgres", db), WithDiffHook(func(next Differ) Differ {
|
||||
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
|
||||
return nil, nil // Noop.
|
||||
})
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(context.Background()))
|
||||
}
|
||||
|
||||
func escape(query string) string {
|
||||
rows := strings.Split(query, "\n")
|
||||
for i := range rows {
|
||||
rows[i] = strings.TrimPrefix(rows[i], " ")
|
||||
}
|
||||
query = strings.Join(rows, " ")
|
||||
return strings.TrimSpace(regexp.QuoteMeta(query)) + "$"
|
||||
}
|
||||
|
||||
func TestMigrate_Formatter(t *testing.T) {
|
||||
db, _, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
|
||||
// If no formatter is given it will be set according to the given migration directory implementation.
|
||||
for _, tt := range []struct {
|
||||
dir migrate.Dir
|
||||
fmt migrate.Formatter
|
||||
}{
|
||||
{&migrate.LocalDir{}, sqltool.GolangMigrateFormatter},
|
||||
{&sqltool.GolangMigrateDir{}, sqltool.GolangMigrateFormatter},
|
||||
{&sqltool.GooseDir{}, sqltool.GooseFormatter},
|
||||
{&sqltool.DBMateDir{}, sqltool.DBMateFormatter},
|
||||
{&sqltool.FlywayDir{}, sqltool.FlywayFormatter},
|
||||
{&sqltool.LiquibaseDir{}, sqltool.LiquibaseFormatter},
|
||||
{struct{ migrate.Dir }{}, sqltool.GolangMigrateFormatter}, // default one if migration dir is unknown
|
||||
} {
|
||||
m, err := NewMigrate(sql.OpenDB("", db), WithDir(tt.dir))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.fmt, m.fmt)
|
||||
}
|
||||
|
||||
// If a formatter is given, it is not overridden.
|
||||
m, err := NewMigrate(sql.OpenDB("", db), WithDir(&migrate.LocalDir{}), WithFormatter(migrate.DefaultFormatter))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, migrate.DefaultFormatter, m.fmt)
|
||||
}
|
||||
|
||||
func TestMigrate_DiffJoinTableAllocationBC(t *testing.T) {
|
||||
// Due to a bug in previous versions, if the universal ID option was enabled and the schema did contain an M2M
|
||||
// relation, the join table would have had an entry for the join table in the types table. This test ensures,
|
||||
// that the PK range allocated for the join table stays in place, since it's removal would break existing projects
|
||||
// due to shifted ranges.
|
||||
|
||||
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mock an existing database with an allocation for a join table.
|
||||
for _, stmt := range []string{
|
||||
"CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
|
||||
"CREATE INDEX `short` ON `groups` (`id`);",
|
||||
"CREATE INDEX `long____________________________1cb2e7e47a309191385af4ad320875b1` ON `groups` (`id`);",
|
||||
"CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
|
||||
"INSERT INTO sqlite_sequence (name, seq) VALUES (\"users\", 4294967296);",
|
||||
"CREATE TABLE `user_groups` (`user_id` integer NOT NULL, `group_id` integer NOT NULL, PRIMARY KEY (`user_id`, `group_id`), CONSTRAINT `user_groups_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_groups_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE);",
|
||||
"INSERT INTO sqlite_sequence (name, seq) VALUES (\"user_groups\", 8589934592);",
|
||||
"CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);",
|
||||
"CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);",
|
||||
"INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users'), ('user_groups');",
|
||||
"INSERT INTO `groups` (`name`) VALUES ('seniors'), ('juniors')",
|
||||
"INSERT INTO `users` (`name`) VALUES ('masseelch'), ('a8m'), ('rotemtam')",
|
||||
"INSERT INTO `user_groups` (`user_id`, `group_id`) VALUES (4294967297, 1), (4294967298, 1), (4294967299, 2)",
|
||||
} {
|
||||
_, err := db.ExecContext(context.Background(), stmt)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Expect to have no changes when migration runs with fix.
|
||||
m, err := NewMigrate(db, WithGlobalUniqueID(true), WithDiffHook(func(next Differ) Differ {
|
||||
return DiffFunc(func(current, desired *schema.Schema) ([]schema.Change, error) {
|
||||
changes, err := next.Diff(current, desired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
require.Len(t, changes, 0)
|
||||
return changes, nil
|
||||
})
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(context.Background(), tables...))
|
||||
|
||||
// Expect to have no changes to the allocation when the join table is dropped.
|
||||
m, err = NewMigrate(db, WithGlobalUniqueID(true))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(context.Background(), groupsTable, usersTable))
|
||||
|
||||
rows, err := db.QueryContext(context.Background(), "SELECT `type` from `ent_types` ORDER BY `id` ASC")
|
||||
require.NoError(t, err)
|
||||
var types []string
|
||||
for rows.Next() {
|
||||
var typ string
|
||||
require.NoError(t, rows.Scan(&typ))
|
||||
types = append(types, typ)
|
||||
}
|
||||
require.NoError(t, rows.Err())
|
||||
require.Equal(t, []string{"groups", "users", "user_groups"}, types)
|
||||
}
|
||||
|
||||
var (
|
||||
groupsColumns = []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
}
|
||||
groupsTable = &Table{
|
||||
Name: "groups",
|
||||
Columns: groupsColumns,
|
||||
PrimaryKey: []*Column{groupsColumns[0]},
|
||||
Indexes: []*Index{
|
||||
{
|
||||
Name: "short",
|
||||
Columns: []*Column{groupsColumns[0]}},
|
||||
{
|
||||
Name: "long_" + strings.Repeat("_", 60),
|
||||
Columns: []*Column{groupsColumns[0]},
|
||||
},
|
||||
},
|
||||
}
|
||||
usersColumns = []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
}
|
||||
usersTable = &Table{
|
||||
Name: "users",
|
||||
Columns: usersColumns,
|
||||
PrimaryKey: []*Column{usersColumns[0]},
|
||||
}
|
||||
userGroupsColumns = []*Column{
|
||||
{Name: "user_id", Type: field.TypeInt},
|
||||
{Name: "group_id", Type: field.TypeInt},
|
||||
}
|
||||
userGroupsTable = &Table{
|
||||
Name: "user_groups",
|
||||
Columns: userGroupsColumns,
|
||||
PrimaryKey: []*Column{userGroupsColumns[0], userGroupsColumns[1]},
|
||||
ForeignKeys: []*ForeignKey{
|
||||
{
|
||||
Symbol: "user_groups_user_id",
|
||||
Columns: []*Column{userGroupsColumns[0]},
|
||||
RefColumns: []*Column{usersColumns[0]},
|
||||
OnDelete: Cascade,
|
||||
},
|
||||
{
|
||||
Symbol: "user_groups_group_id",
|
||||
Columns: []*Column{userGroupsColumns[1]},
|
||||
RefColumns: []*Column{groupsColumns[0]},
|
||||
OnDelete: Cascade,
|
||||
},
|
||||
},
|
||||
}
|
||||
tables = []*Table{
|
||||
groupsTable,
|
||||
usersTable,
|
||||
userGroupsTable,
|
||||
}
|
||||
petColumns = []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
}
|
||||
petsTable = &Table{
|
||||
Name: "pets",
|
||||
Columns: petColumns,
|
||||
PrimaryKey: petColumns,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
userGroupsTable.ForeignKeys[0].RefTable = usersTable
|
||||
userGroupsTable.ForeignKeys[1].RefTable = groupsTable
|
||||
}
|
||||
|
||||
func TestMigrate_Diff(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
|
||||
require.NoError(t, err)
|
||||
|
||||
p := t.TempDir()
|
||||
d, err := migrate.NewLocalDir(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := NewMigrate(db, WithDir(d))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Diff(ctx, &Table{Name: "users"}))
|
||||
v := time.Now().UTC().Format("20060102150405")
|
||||
requireFileEqual(t, filepath.Join(p, v+"_changes.up.sql"), "-- create \"users\" table\nCREATE TABLE `users` ();\n")
|
||||
requireFileEqual(t, filepath.Join(p, v+"_changes.down.sql"), "-- reverse: create \"users\" table\nDROP TABLE `users`;\n")
|
||||
require.FileExists(t, filepath.Join(p, migrate.HashFileName))
|
||||
|
||||
// Test integrity file.
|
||||
p = t.TempDir()
|
||||
d, err = migrate.NewLocalDir(p)
|
||||
require.NoError(t, err)
|
||||
m, err = NewMigrate(db, WithDir(d))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Diff(ctx, &Table{Name: "users"}))
|
||||
requireFileEqual(t, filepath.Join(p, v+"_changes.up.sql"), "-- create \"users\" table\nCREATE TABLE `users` ();\n")
|
||||
requireFileEqual(t, filepath.Join(p, v+"_changes.down.sql"), "-- reverse: create \"users\" table\nDROP TABLE `users`;\n")
|
||||
require.FileExists(t, filepath.Join(p, migrate.HashFileName))
|
||||
require.NoError(t, d.WriteFile("tmp.sql", nil))
|
||||
require.ErrorIs(t, m.Diff(ctx, &Table{Name: "users"}), migrate.ErrChecksumMismatch)
|
||||
|
||||
p = t.TempDir()
|
||||
d, err = migrate.NewLocalDir(p)
|
||||
require.NoError(t, err)
|
||||
f, err := migrate.NewTemplateFormatter(
|
||||
template.Must(template.New("").Parse("{{ .Name }}.sql")),
|
||||
template.Must(template.New("").Parse(
|
||||
`{{ range .Changes }}{{ printf "%s;\n" .Cmd }}{{ end }}`,
|
||||
)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Join tables (mapping between user and group) will not result in an entry to the types table.
|
||||
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Diff(ctx, tables...))
|
||||
changesSQL := strings.Join([]string{
|
||||
"CREATE TABLE `groups` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
|
||||
"CREATE INDEX `short` ON `groups` (`id`);",
|
||||
"CREATE INDEX `long____________________________1cb2e7e47a309191385af4ad320875b1` ON `groups` (`id`);",
|
||||
"CREATE TABLE `users` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `name` text NOT NULL);",
|
||||
fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"users\", %d);", 1<<32),
|
||||
"CREATE TABLE `user_groups` (`user_id` integer NOT NULL, `group_id` integer NOT NULL, PRIMARY KEY (`user_id`, `group_id`), CONSTRAINT `user_groups_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_groups_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE);",
|
||||
"CREATE TABLE `ent_types` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `type` text NOT NULL);",
|
||||
"CREATE UNIQUE INDEX `ent_types_type_key` ON `ent_types` (`type`);",
|
||||
"INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users');",
|
||||
"",
|
||||
}, "\n")
|
||||
requireFileEqual(t, filepath.Join(p, "changes.sql"), changesSQL)
|
||||
|
||||
// Skipping table creation should write only the ent_type insertion.
|
||||
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithDiffOptions(schema.DiffSkipChanges(&schema.AddTable{})))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Diff(ctx, tables...))
|
||||
requireFileEqual(t, filepath.Join(p, "changes.sql"), "INSERT INTO `ent_types` (`type`) VALUES ('groups'), ('users');\n")
|
||||
|
||||
// Enable indentations.
|
||||
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithIndent(" "))
|
||||
require.NoError(t, err)
|
||||
// Adding another node will result in a new entry to the TypeTable (without actually creating it).
|
||||
_, err = db.ExecContext(ctx, changesSQL, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.NamedDiff(ctx, "changes_2", petsTable))
|
||||
requireFileEqual(t,
|
||||
filepath.Join(p, "changes_2.sql"), strings.Join([]string{
|
||||
"CREATE TABLE `pets` (\n `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT\n);",
|
||||
fmt.Sprintf("INSERT INTO sqlite_sequence (name, seq) VALUES (\"pets\", %d);", 2<<32),
|
||||
"INSERT INTO `ent_types` (`type`) VALUES ('pets');", "",
|
||||
}, "\n"))
|
||||
|
||||
// Checksum will be updated as well.
|
||||
require.NoError(t, migrate.Validate(d))
|
||||
|
||||
require.NoError(t, m.NamedDiff(ctx, "no_changes"), "should not error if WithErrNoPlan is not set")
|
||||
// Enable WithErrNoPlan.
|
||||
m, err = NewMigrate(db, WithFormatter(f), WithDir(d), WithGlobalUniqueID(true), WithErrNoPlan(true))
|
||||
require.NoError(t, err)
|
||||
err = m.NamedDiff(ctx, "no_changes")
|
||||
require.ErrorIs(t, err, migrate.ErrNoPlan)
|
||||
}
|
||||
|
||||
func requireFileEqual(t *testing.T, name, contents string) {
|
||||
c, err := os.ReadFile(name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contents, string(c))
|
||||
}
|
||||
|
||||
func TestMigrateWithoutForeignKeys(t *testing.T) {
|
||||
tbl := &schema.Table{
|
||||
Name: "tbl",
|
||||
Columns: []*schema.Column{
|
||||
{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
|
||||
},
|
||||
}
|
||||
fk := &schema.ForeignKey{
|
||||
Symbol: "fk",
|
||||
Table: tbl,
|
||||
Columns: tbl.Columns[1:],
|
||||
RefTable: tbl,
|
||||
RefColumns: tbl.Columns[:1],
|
||||
OnUpdate: schema.NoAction,
|
||||
OnDelete: schema.Cascade,
|
||||
}
|
||||
tbl.ForeignKeys = append(tbl.ForeignKeys, fk)
|
||||
t.Run("AddTable", func(t *testing.T) {
|
||||
mdiff := DiffFunc(func(_, _ *schema.Schema) ([]schema.Change, error) {
|
||||
return []schema.Change{
|
||||
&schema.AddTable{
|
||||
T: tbl,
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
df, err := withoutForeignKeys(mdiff).Diff(nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, df, 1)
|
||||
actual, ok := df[0].(*schema.AddTable)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, actual.T.ForeignKeys)
|
||||
})
|
||||
t.Run("ModifyTable", func(t *testing.T) {
|
||||
mdiff := DiffFunc(func(_, _ *schema.Schema) ([]schema.Change, error) {
|
||||
return []schema.Change{
|
||||
&schema.ModifyTable{
|
||||
T: tbl,
|
||||
Changes: []schema.Change{
|
||||
&schema.AddIndex{
|
||||
I: &schema.Index{
|
||||
Name: "id_key",
|
||||
Parts: []*schema.IndexPart{
|
||||
{C: tbl.Columns[0]},
|
||||
},
|
||||
},
|
||||
},
|
||||
&schema.DropForeignKey{
|
||||
F: fk,
|
||||
},
|
||||
&schema.AddForeignKey{
|
||||
F: fk,
|
||||
},
|
||||
&schema.ModifyForeignKey{
|
||||
From: fk,
|
||||
To: fk,
|
||||
Change: schema.ChangeRefColumn,
|
||||
},
|
||||
&schema.AddColumn{
|
||||
C: &schema.Column{Name: "name", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
df, err := withoutForeignKeys(mdiff).Diff(nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, df, 1)
|
||||
actual, ok := df[0].(*schema.ModifyTable)
|
||||
require.True(t, ok)
|
||||
require.Len(t, actual.Changes, 2)
|
||||
addIndex, ok := actual.Changes[0].(*schema.AddIndex)
|
||||
require.True(t, ok)
|
||||
require.EqualValues(t, "id_key", addIndex.I.Name)
|
||||
addColumn, ok := actual.Changes[1].(*schema.AddColumn)
|
||||
require.True(t, ok)
|
||||
require.EqualValues(t, "name", addColumn.C.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAtlas_StateReader(t *testing.T) {
|
||||
db, err := sql.Open(dialect.SQLite, "file:test?mode=memory&_fk=1")
|
||||
require.NoError(t, err)
|
||||
m, err := NewMigrate(db)
|
||||
require.NoError(t, err)
|
||||
realm, err := m.StateReader(&Table{
|
||||
Name: "users",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "active", Type: field.TypeBool},
|
||||
},
|
||||
Annotation: &entsql.Annotation{
|
||||
IncrementStart: func(i int) *int { return &i }(100),
|
||||
},
|
||||
}).ReadState(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, realm)
|
||||
require.Len(t, realm.Schemas, 1)
|
||||
require.Len(t, realm.Schemas[0].Tables, 1)
|
||||
require.Equal(t, "users", realm.Schemas[0].Tables[0].Name)
|
||||
require.Equal(t, []schema.Attr{&sqlite.AutoIncrement{Seq: 100}}, realm.Schemas[0].Tables[0].Attrs)
|
||||
require.Equal(t,
|
||||
realm.Schemas[0].Tables[0].Columns,
|
||||
[]*schema.Column{
|
||||
schema.NewIntColumn("id", "integer").
|
||||
AddAttrs(&sqlite.AutoIncrement{}),
|
||||
schema.NewStringColumn("name", "text"),
|
||||
schema.NewBoolColumn("active", "bool"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAtlas_ParallelCreate(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(10)
|
||||
for i := 0; i < 10; i++ {
|
||||
db, err := sql.Open(dialect.SQLite, fmt.Sprintf("file:test-%d?mode=memory&_fk=1", i))
|
||||
require.NoError(t, err)
|
||||
m, err := NewMigrate(db)
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
require.NoError(t, m.Create(context.Background(), petsTable))
|
||||
require.NoError(t, db.Close())
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
333
dialect/sql/schema/mysql.go
Normal file
333
dialect/sql/schema/mysql.go
Normal file
@@ -0,0 +1,333 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"ariga.io/atlas/sql/mysql"
|
||||
"ariga.io/atlas/sql/schema"
|
||||
)
|
||||
|
||||
// MySQL adapter for Atlas migration engine.
|
||||
type MySQL struct {
|
||||
dialect.Driver
|
||||
schema string
|
||||
version string
|
||||
}
|
||||
|
||||
// init loads the MySQL version from the database for later use in the migration process.
|
||||
func (d *MySQL) init(ctx context.Context) error {
|
||||
if d.version != "" {
|
||||
return nil // already initialized.
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
if err := d.Query(ctx, "SHOW VARIABLES LIKE 'version'", []any{}, rows); err != nil {
|
||||
return fmt.Errorf("mysql: querying mysql version %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("mysql: version variable was not found")
|
||||
}
|
||||
version := make([]string, 2)
|
||||
if err := rows.Scan(&version[0], &version[1]); err != nil {
|
||||
return fmt.Errorf("mysql: scanning mysql version: %w", err)
|
||||
}
|
||||
d.version = version[1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *MySQL) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
|
||||
query, args := sql.Select(sql.Count("*")).From(sql.Table("TABLES").Schema("INFORMATION_SCHEMA")).
|
||||
Where(sql.And(
|
||||
d.matchSchema(),
|
||||
sql.EQ("TABLE_NAME", name),
|
||||
)).Query()
|
||||
return exist(ctx, conn, query, args...)
|
||||
}
|
||||
|
||||
// matchSchema returns the predicate for matching table schema.
|
||||
func (d *MySQL) matchSchema(columns ...string) *sql.Predicate {
|
||||
column := "TABLE_SCHEMA"
|
||||
if len(columns) > 0 {
|
||||
column = columns[0]
|
||||
}
|
||||
if d.schema != "" {
|
||||
return sql.EQ(column, d.schema)
|
||||
}
|
||||
return sql.EQ(column, sql.Raw("(SELECT DATABASE())"))
|
||||
}
|
||||
|
||||
func (d *MySQL) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
|
||||
return mysql.Open(&db{ExecQuerier: conn})
|
||||
}
|
||||
|
||||
func (d *MySQL) atTable(t1 *Table, t2 *schema.Table) {
|
||||
t2.SetCharset("utf8mb4").SetCollation("utf8mb4_bin")
|
||||
if t1.Annotation == nil {
|
||||
return
|
||||
}
|
||||
if charset := t1.Annotation.Charset; charset != "" {
|
||||
t2.SetCharset(charset)
|
||||
}
|
||||
if collate := t1.Annotation.Collation; collate != "" {
|
||||
t2.SetCollation(collate)
|
||||
}
|
||||
if opts := t1.Annotation.Options; opts != "" {
|
||||
t2.AddAttrs(&mysql.CreateOptions{
|
||||
V: opts,
|
||||
})
|
||||
}
|
||||
// Check if the connected database supports the CHECK clause.
|
||||
// For MySQL, is >= "8.0.16" and for MariaDB it is "10.2.1".
|
||||
v1, v2 := d.version, "8.0.16"
|
||||
if v, ok := d.mariadb(); ok {
|
||||
v1, v2 = v, "10.2.1"
|
||||
}
|
||||
if compareVersions(v1, v2) >= 0 {
|
||||
setAtChecks(t1, t2)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MySQL) supportsDefault(c *Column) bool {
|
||||
_, maria := d.mariadb()
|
||||
switch c.Default.(type) {
|
||||
case Expr, map[string]Expr:
|
||||
if maria {
|
||||
return compareVersions(d.version, "10.2.0") >= 0
|
||||
}
|
||||
return c.supportDefault() && compareVersions(d.version, "8.0.0") >= 0
|
||||
default:
|
||||
return c.supportDefault() || maria
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MySQL) supportsUUID() bool {
|
||||
_, maria := d.mariadb()
|
||||
return maria && compareVersions(d.version, "10.7.0") >= 0
|
||||
}
|
||||
|
||||
func (d *MySQL) atTypeC(c1 *Column, c2 *schema.Column) error {
|
||||
if c1.SchemaType != nil && c1.SchemaType[dialect.MySQL] != "" {
|
||||
t, err := mysql.ParseType(strings.ToLower(c1.SchemaType[dialect.MySQL]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
return nil
|
||||
}
|
||||
var t schema.Type
|
||||
switch c1.Type {
|
||||
case field.TypeBool:
|
||||
t = &schema.BoolType{T: "boolean"}
|
||||
case field.TypeBlob:
|
||||
return fmt.Errorf("blob fields are not stored in the database")
|
||||
case field.TypeInt8:
|
||||
t = &schema.IntegerType{T: mysql.TypeTinyInt}
|
||||
case field.TypeUint8:
|
||||
t = &schema.IntegerType{T: mysql.TypeTinyInt, Unsigned: true}
|
||||
case field.TypeInt16:
|
||||
t = &schema.IntegerType{T: mysql.TypeSmallInt}
|
||||
case field.TypeUint16:
|
||||
t = &schema.IntegerType{T: mysql.TypeSmallInt, Unsigned: true}
|
||||
case field.TypeInt32:
|
||||
t = &schema.IntegerType{T: mysql.TypeInt}
|
||||
case field.TypeUint32:
|
||||
t = &schema.IntegerType{T: mysql.TypeInt, Unsigned: true}
|
||||
case field.TypeInt, field.TypeInt64:
|
||||
t = &schema.IntegerType{T: mysql.TypeBigInt}
|
||||
case field.TypeUint, field.TypeUint64:
|
||||
t = &schema.IntegerType{T: mysql.TypeBigInt, Unsigned: true}
|
||||
case field.TypeBytes:
|
||||
size := int64(math.MaxUint16)
|
||||
if c1.Size > 0 {
|
||||
size = c1.Size
|
||||
}
|
||||
switch {
|
||||
case size <= math.MaxUint8:
|
||||
t = &schema.BinaryType{T: mysql.TypeTinyBlob}
|
||||
case size <= math.MaxUint16:
|
||||
t = &schema.BinaryType{T: mysql.TypeBlob}
|
||||
case size < 1<<24:
|
||||
t = &schema.BinaryType{T: mysql.TypeMediumBlob}
|
||||
case size <= math.MaxUint32:
|
||||
t = &schema.BinaryType{T: mysql.TypeLongBlob}
|
||||
}
|
||||
case field.TypeJSON:
|
||||
t = &schema.JSONType{T: mysql.TypeJSON}
|
||||
if compareVersions(d.version, "5.7.8") == -1 {
|
||||
t = &schema.BinaryType{T: mysql.TypeLongBlob}
|
||||
}
|
||||
case field.TypeString:
|
||||
size := c1.Size
|
||||
if size == 0 {
|
||||
size = d.defaultSize(c1)
|
||||
}
|
||||
switch {
|
||||
case c1.typ == "tinytext", c1.typ == "text":
|
||||
t = &schema.StringType{T: c1.typ}
|
||||
case size <= math.MaxUint16:
|
||||
t = &schema.StringType{T: mysql.TypeVarchar, Size: int(size)}
|
||||
case size == 1<<24-1:
|
||||
t = &schema.StringType{T: mysql.TypeMediumText}
|
||||
default:
|
||||
t = &schema.StringType{T: mysql.TypeLongText}
|
||||
}
|
||||
case field.TypeFloat32, field.TypeFloat64:
|
||||
t = &schema.FloatType{T: c1.scanTypeOr(mysql.TypeDouble)}
|
||||
case field.TypeTime:
|
||||
t = &schema.TimeType{T: c1.scanTypeOr(mysql.TypeTimestamp)}
|
||||
// In MariaDB or in MySQL < v8.0.2, the TIMESTAMP column has both `DEFAULT CURRENT_TIMESTAMP`
|
||||
// and `ON UPDATE CURRENT_TIMESTAMP` if neither is specified explicitly. this behavior is
|
||||
// suppressed if the column is defined with a `DEFAULT` clause or with the `NULL` attribute.
|
||||
if _, maria := d.mariadb(); maria || compareVersions(d.version, "8.0.2") == -1 && c1.Default == nil {
|
||||
c2.SetNull(c1.Attr == "")
|
||||
}
|
||||
case field.TypeEnum:
|
||||
t = &schema.EnumType{T: mysql.TypeEnum, Values: c1.Enums}
|
||||
case field.TypeUUID:
|
||||
if d.supportsUUID() {
|
||||
// Native support for the uuid type
|
||||
t = &schema.UUIDType{T: mysql.TypeUUID}
|
||||
} else {
|
||||
// "CHAR(X) BINARY" is treated as "CHAR(X) COLLATE latin1_bin", and in MySQL < 8,
|
||||
// and "COLLATE utf8mb4_bin" in MySQL >= 8. However we already set the table to
|
||||
t = &schema.StringType{T: mysql.TypeChar, Size: 36}
|
||||
c2.SetCollation("utf8mb4_bin")
|
||||
}
|
||||
default:
|
||||
t, err := mysql.ParseType(strings.ToLower(c1.typ))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
}
|
||||
c2.Type.Type = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *MySQL) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
|
||||
// For UNIQUE columns, MySQL create an implicit index
|
||||
// named as the column with an extra index in case the
|
||||
// name is already taken (<e.g. c>, <c_2>, <c_3>, ...).
|
||||
for _, idx := range t1.Indexes {
|
||||
// Index also defined explicitly, and will be add in atIndexes.
|
||||
if idx.Unique && d.atImplicitIndexName(idx, c1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t2.AddIndexes(schema.NewUniqueIndex(c1.Name).AddColumns(c2))
|
||||
}
|
||||
|
||||
func (d *MySQL) atIncrementC(t *schema.Table, c *schema.Column) {
|
||||
if c.Default != nil {
|
||||
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&mysql.AutoIncrement{}))
|
||||
} else {
|
||||
c.AddAttrs(&mysql.AutoIncrement{})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MySQL) atIncrementT(t *schema.Table, v int64) {
|
||||
if v >= 0 {
|
||||
t.AddAttrs(mysql.NewAutoIncrement(uint64(v)))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MySQL) atImplicitIndexName(idx *Index, c1 *Column) bool {
|
||||
if idx.Name == c1.Name {
|
||||
return true
|
||||
}
|
||||
if !strings.HasPrefix(idx.Name, c1.Name+"_") {
|
||||
return false
|
||||
}
|
||||
i, err := strconv.ParseInt(strings.TrimLeft(idx.Name, c1.Name+"_"), 10, 64)
|
||||
return err == nil && i > 1
|
||||
}
|
||||
|
||||
func (d *MySQL) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
|
||||
prefix := indexParts(idx1)
|
||||
for _, c1 := range idx1.Columns {
|
||||
c2, ok := t2.Column(c1.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
|
||||
}
|
||||
part := &schema.IndexPart{C: c2}
|
||||
if v, ok := prefix[c1.Name]; ok {
|
||||
part.AddAttrs(&mysql.SubPart{Len: int(v)})
|
||||
}
|
||||
idx2.AddParts(part)
|
||||
}
|
||||
if t, ok := indexType(idx1, dialect.MySQL); ok {
|
||||
idx2.AddAttrs(&mysql.IndexType{T: t})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*MySQL) atTypeRangeSQL(ts ...string) string {
|
||||
for i := range ts {
|
||||
ts[i] = fmt.Sprintf("('%s')", ts[i])
|
||||
}
|
||||
return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", "))
|
||||
}
|
||||
|
||||
// mariadb reports if the migration runs on MariaDB and returns the semver string.
|
||||
func (d *MySQL) mariadb() (string, bool) {
|
||||
idx := strings.Index(d.version, "MariaDB")
|
||||
if idx == -1 {
|
||||
return "", false
|
||||
}
|
||||
return d.version[:idx-1], true
|
||||
}
|
||||
|
||||
// defaultSize returns the default size for MySQL/MariaDB varchar type
|
||||
// based on column size, charset and table indexes, in order to avoid
|
||||
// index prefix key limit (767) for older versions of MySQL/MariaDB.
|
||||
func (d *MySQL) defaultSize(c *Column) int64 {
|
||||
size := DefaultStringLen
|
||||
version, checked := d.version, "5.7.0"
|
||||
if v, ok := d.mariadb(); ok {
|
||||
version, checked = v, "10.2.2"
|
||||
}
|
||||
switch {
|
||||
// Version is >= 5.7 for MySQL, or >= 10.2.2 for MariaDB.
|
||||
case compareVersions(version, checked) != -1:
|
||||
// Column is non-unique, or not part of any index (reaching
|
||||
// the error 1071).
|
||||
case !c.Unique && len(c.indexes) == 0 && !c.PrimaryKey():
|
||||
default:
|
||||
size = 191
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// indexParts returns a map holding the sub_part mapping if exists.
|
||||
func indexParts(idx *Index) map[string]uint {
|
||||
parts := make(map[string]uint)
|
||||
if idx.Annotation == nil {
|
||||
return parts
|
||||
}
|
||||
// If prefix (without a name) was defined on the
|
||||
// annotation, map it to the single column index.
|
||||
if idx.Annotation.Prefix > 0 && len(idx.Columns) == 1 {
|
||||
parts[idx.Columns[0].Name] = idx.Annotation.Prefix
|
||||
}
|
||||
for column, part := range idx.Annotation.PrefixColumns {
|
||||
parts[column] = part
|
||||
}
|
||||
return parts
|
||||
}
|
||||
260
dialect/sql/schema/postgres.go
Normal file
260
dialect/sql/schema/postgres.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"ariga.io/atlas/sql/postgres"
|
||||
"ariga.io/atlas/sql/schema"
|
||||
)
|
||||
|
||||
// Postgres adapter for Atlas migration engine.
|
||||
type Postgres struct {
|
||||
dialect.Driver
|
||||
schema string
|
||||
version string
|
||||
}
|
||||
|
||||
// init loads the Postgres version from the database for later use in the migration process.
|
||||
// It returns an error if the server version is lower than v10.
|
||||
func (d *Postgres) init(ctx context.Context) error {
|
||||
if d.version != "" {
|
||||
return nil // already initialized.
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
if err := d.Query(ctx, "SHOW server_version_num", []any{}, rows); err != nil {
|
||||
return fmt.Errorf("querying server version %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("server_version_num variable was not found")
|
||||
}
|
||||
var version string
|
||||
if err := rows.Scan(&version); err != nil {
|
||||
return fmt.Errorf("scanning version: %w", err)
|
||||
}
|
||||
if len(version) < 6 {
|
||||
return fmt.Errorf("malformed version: %s", version)
|
||||
}
|
||||
d.version = fmt.Sprintf("%s.%s.%s", version[:2], version[2:4], version[4:])
|
||||
if compareVersions(d.version, "10.0.0") == -1 {
|
||||
return fmt.Errorf("unsupported postgres version: %s", d.version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tableExist checks if a table exists in the database and current schema.
|
||||
func (d *Postgres) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
|
||||
query, args := sql.Dialect(dialect.Postgres).
|
||||
Select(sql.Count("*")).From(sql.Table("tables").Schema("information_schema")).
|
||||
Where(sql.And(
|
||||
d.matchSchema(),
|
||||
sql.EQ("table_name", name),
|
||||
)).Query()
|
||||
return exist(ctx, conn, query, args...)
|
||||
}
|
||||
|
||||
// matchSchema returns the predicate for matching table schema.
|
||||
func (d *Postgres) matchSchema(columns ...string) *sql.Predicate {
|
||||
column := "table_schema"
|
||||
if len(columns) > 0 {
|
||||
column = columns[0]
|
||||
}
|
||||
if d.schema != "" {
|
||||
return sql.EQ(column, d.schema)
|
||||
}
|
||||
return sql.EQ(column, sql.Raw("CURRENT_SCHEMA()"))
|
||||
}
|
||||
|
||||
// maxCharSize defines the maximum size of limited character types in Postgres (10 MB).
|
||||
const maxCharSize = 10 << 20
|
||||
|
||||
func (d *Postgres) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
|
||||
return postgres.Open(&db{ExecQuerier: conn})
|
||||
}
|
||||
|
||||
func (d *Postgres) atTable(t1 *Table, t2 *schema.Table) {
|
||||
if t1.Annotation != nil {
|
||||
setAtChecks(t1, t2)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Postgres) supportsDefault(*Column) bool {
|
||||
// PostgreSQL supports default values for all standard types.
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Postgres) atTypeC(c1 *Column, c2 *schema.Column) error {
|
||||
if c1.SchemaType != nil && c1.SchemaType[dialect.Postgres] != "" {
|
||||
t, err := postgres.ParseType(strings.ToLower(c1.SchemaType[dialect.Postgres]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
if s, ok := t.(*postgres.SerialType); c1.foreign != nil && ok {
|
||||
c2.Type.Type = s.IntegerType()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var t schema.Type
|
||||
switch c1.Type {
|
||||
case field.TypeBlob:
|
||||
return fmt.Errorf("blob fields are not stored in the database")
|
||||
case field.TypeBool:
|
||||
t = &schema.BoolType{T: postgres.TypeBoolean}
|
||||
case field.TypeUint8, field.TypeInt8, field.TypeInt16:
|
||||
t = &schema.IntegerType{T: postgres.TypeSmallInt}
|
||||
case field.TypeUint16, field.TypeInt32:
|
||||
t = &schema.IntegerType{T: postgres.TypeInt}
|
||||
case field.TypeUint32, field.TypeInt, field.TypeUint, field.TypeInt64, field.TypeUint64:
|
||||
t = &schema.IntegerType{T: postgres.TypeBigInt}
|
||||
case field.TypeFloat32:
|
||||
t = &schema.FloatType{T: c1.scanTypeOr(postgres.TypeReal)}
|
||||
case field.TypeFloat64:
|
||||
t = &schema.FloatType{T: c1.scanTypeOr(postgres.TypeDouble)}
|
||||
case field.TypeBytes:
|
||||
t = &schema.BinaryType{T: postgres.TypeBytea}
|
||||
case field.TypeUUID:
|
||||
t = &postgres.UUIDType{T: postgres.TypeUUID}
|
||||
case field.TypeJSON:
|
||||
t = &schema.JSONType{T: postgres.TypeJSONB}
|
||||
case field.TypeString:
|
||||
t = &schema.StringType{T: postgres.TypeVarChar}
|
||||
if c1.Size > maxCharSize {
|
||||
t = &schema.StringType{T: postgres.TypeText}
|
||||
}
|
||||
case field.TypeTime:
|
||||
t = &schema.TimeType{T: c1.scanTypeOr(postgres.TypeTimestampWTZ)}
|
||||
case field.TypeEnum:
|
||||
// Although atlas supports enum types, we keep backwards compatibility
|
||||
// with previous versions of ent and use varchar (see cType).
|
||||
t = &schema.StringType{T: postgres.TypeVarChar}
|
||||
case field.TypeOther:
|
||||
t = &schema.UnsupportedType{T: c1.typ}
|
||||
default:
|
||||
t, err := postgres.ParseType(strings.ToLower(c1.typ))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
}
|
||||
c2.Type.Type = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Postgres) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
|
||||
// For UNIQUE columns, PostgreSQL creates an implicit index named
|
||||
// "<table>_<column>_key<i>".
|
||||
for _, idx := range t1.Indexes {
|
||||
// Index also defined explicitly, and will be added in atIndexes.
|
||||
if idx.Unique && d.atImplicitIndexName(idx, t1, c1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t2.AddIndexes(schema.NewUniqueIndex(fmt.Sprintf("%s_%s_key", t1.Name, c1.Name)).AddColumns(c2))
|
||||
}
|
||||
|
||||
func (d *Postgres) atImplicitIndexName(idx *Index, t1 *Table, c1 *Column) bool {
|
||||
p := fmt.Sprintf("%s_%s_key", t1.Name, c1.Name)
|
||||
if idx.Name == p {
|
||||
return true
|
||||
}
|
||||
i, err := strconv.ParseInt(strings.TrimPrefix(idx.Name, p), 10, 64)
|
||||
return err == nil && i > 0
|
||||
}
|
||||
|
||||
func (d *Postgres) atIncrementC(t *schema.Table, c *schema.Column) {
|
||||
// Skip marking this column as an identity in case it is
|
||||
// serial type or a default was already defined for it.
|
||||
if _, ok := c.Type.Type.(*postgres.SerialType); ok || c.Default != nil {
|
||||
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&postgres.Identity{}))
|
||||
return
|
||||
}
|
||||
id := &postgres.Identity{}
|
||||
for _, a := range t.Attrs {
|
||||
if a, ok := a.(*postgres.Identity); ok {
|
||||
id = a
|
||||
}
|
||||
}
|
||||
c.AddAttrs(id)
|
||||
}
|
||||
|
||||
func (d *Postgres) atIncrementT(t *schema.Table, v int64) {
|
||||
t.AddAttrs(&postgres.Identity{Sequence: &postgres.Sequence{Start: v}})
|
||||
}
|
||||
|
||||
// indexOpClass returns a map holding the operator-class mapping if exists.
|
||||
func indexOpClass(idx *Index) map[string]string {
|
||||
opc := make(map[string]string)
|
||||
if idx.Annotation == nil {
|
||||
return opc
|
||||
}
|
||||
// If operator-class (without a name) was defined on
|
||||
// the annotation, map it to the single column index.
|
||||
if idx.Annotation.OpClass != "" && len(idx.Columns) == 1 {
|
||||
opc[idx.Columns[0].Name] = idx.Annotation.OpClass
|
||||
}
|
||||
for column, op := range idx.Annotation.OpClassColumns {
|
||||
opc[column] = op
|
||||
}
|
||||
return opc
|
||||
}
|
||||
|
||||
func (d *Postgres) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
|
||||
opc := indexOpClass(idx1)
|
||||
for _, c1 := range idx1.Columns {
|
||||
c2, ok := t2.Column(c1.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
|
||||
}
|
||||
part := &schema.IndexPart{C: c2}
|
||||
if v, ok := opc[c1.Name]; ok {
|
||||
var op postgres.IndexOpClass
|
||||
if err := op.UnmarshalText([]byte(v)); err != nil {
|
||||
return fmt.Errorf("unmarshalling operator-class %q for column %q: %v", v, c1.Name, err)
|
||||
}
|
||||
part.Attrs = append(part.Attrs, &op)
|
||||
}
|
||||
idx2.AddParts(part)
|
||||
}
|
||||
if t, ok := indexType(idx1, dialect.Postgres); ok {
|
||||
idx2.AddAttrs(&postgres.IndexType{T: t})
|
||||
}
|
||||
if ant, supportsInclude := idx1.Annotation, compareVersions(d.version, "11.0.0") >= 0; ant != nil && len(ant.IncludeColumns) > 0 && supportsInclude {
|
||||
columns := make([]*schema.Column, len(ant.IncludeColumns))
|
||||
for i, ic := range ant.IncludeColumns {
|
||||
c, ok := t2.Column(ic)
|
||||
if !ok {
|
||||
return fmt.Errorf("include column %q was not found for index %q", ic, idx1.Name)
|
||||
}
|
||||
columns[i] = c
|
||||
}
|
||||
idx2.AddAttrs(&postgres.IndexInclude{Columns: columns})
|
||||
}
|
||||
if idx1.Annotation != nil && idx1.Annotation.Where != "" {
|
||||
idx2.AddAttrs(&postgres.IndexPredicate{P: idx1.Annotation.Where})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Postgres) atTypeRangeSQL(ts ...string) string {
|
||||
for i := range ts {
|
||||
ts[i] = fmt.Sprintf("('%s')", ts[i])
|
||||
}
|
||||
return fmt.Sprintf(`INSERT INTO "%s" ("type") VALUES %s`, TypeTable, strings.Join(ts, ", "))
|
||||
}
|
||||
695
dialect/sql/schema/schema.go
Normal file
695
dialect/sql/schema/schema.go
Normal file
@@ -0,0 +1,695 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
// Package schema contains all schema migration logic for SQL dialects.
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"ariga.io/atlas/sql/mysql"
|
||||
"ariga.io/atlas/sql/postgres"
|
||||
"ariga.io/atlas/sql/schema"
|
||||
"ariga.io/atlas/sql/sqlite"
|
||||
entdialect "entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultStringLen describes the default length for string/varchar types.
|
||||
DefaultStringLen int64 = 255
|
||||
// Null is the string representation of NULL in SQL.
|
||||
Null = "NULL"
|
||||
// PrimaryKey is the string representation of PKs in SQL.
|
||||
PrimaryKey = "PRI"
|
||||
// UniqueKey is the string representation of PKs in SQL.
|
||||
UniqueKey = "UNI"
|
||||
)
|
||||
|
||||
// Table schema definition for SQL dialects.
|
||||
type Table struct {
|
||||
mu sync.Mutex
|
||||
Name string
|
||||
Schema string
|
||||
Columns []*Column
|
||||
columns map[string]*Column
|
||||
Indexes []*Index
|
||||
PrimaryKey []*Column
|
||||
ForeignKeys []*ForeignKey
|
||||
Annotation *entsql.Annotation
|
||||
Comment string
|
||||
View bool // Indicate the table is a view.
|
||||
Pos string // filename:line of the ent schema definition.
|
||||
}
|
||||
|
||||
// NewTable returns a new table with the given name.
|
||||
func NewTable(name string) *Table {
|
||||
return &Table{
|
||||
Name: name,
|
||||
columns: make(map[string]*Column),
|
||||
}
|
||||
}
|
||||
|
||||
// NewView returns a new view with the given name.
|
||||
func NewView(name string) *Table {
|
||||
t := NewTable(name)
|
||||
t.View = true
|
||||
return t
|
||||
}
|
||||
|
||||
// SetComment sets the table comment.
|
||||
func (t *Table) SetComment(c string) *Table {
|
||||
t.Comment = c
|
||||
return t
|
||||
}
|
||||
|
||||
// SetSchema sets the table schema.
|
||||
func (t *Table) SetSchema(s string) *Table {
|
||||
t.Schema = s
|
||||
return t
|
||||
}
|
||||
|
||||
// SetPos sets the table position.
|
||||
func (t *Table) SetPos(p string) *Table {
|
||||
t.Pos = p
|
||||
return t
|
||||
}
|
||||
|
||||
// AddPrimary adds a new primary key to the table.
|
||||
func (t *Table) AddPrimary(c *Column) *Table {
|
||||
c.Key = PrimaryKey
|
||||
t.AddColumn(c)
|
||||
t.PrimaryKey = append(t.PrimaryKey, c)
|
||||
return t
|
||||
}
|
||||
|
||||
// AddForeignKey adds a foreign key to the table.
|
||||
func (t *Table) AddForeignKey(fk *ForeignKey) *Table {
|
||||
t.ForeignKeys = append(t.ForeignKeys, fk)
|
||||
return t
|
||||
}
|
||||
|
||||
// AddColumn adds a new column to the table.
|
||||
func (t *Table) AddColumn(c *Column) *Table {
|
||||
t.columns[c.Name] = c
|
||||
t.Columns = append(t.Columns, c)
|
||||
return t
|
||||
}
|
||||
|
||||
// HasColumn reports if the table contains a column with the given name.
|
||||
func (t *Table) HasColumn(name string) bool {
|
||||
_, ok := t.Column(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Column returns the column with the given name. If exists.
|
||||
func (t *Table) Column(name string) (*Column, bool) {
|
||||
if c, ok := t.columns[name]; ok {
|
||||
return c, true
|
||||
}
|
||||
// In case the column was added
|
||||
// directly to the Columns field.
|
||||
for _, c := range t.Columns {
|
||||
if c.Name == name {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SetAnnotation the entsql.Annotation on the table.
|
||||
func (t *Table) SetAnnotation(ant *entsql.Annotation) *Table {
|
||||
t.Annotation = ant
|
||||
return t
|
||||
}
|
||||
|
||||
// AddIndex creates and adds a new index to the table from the given options.
|
||||
func (t *Table) AddIndex(name string, unique bool, columns []string) *Table {
|
||||
return t.addIndex(&Index{
|
||||
Name: name,
|
||||
Unique: unique,
|
||||
columns: columns,
|
||||
Columns: make([]*Column, 0, len(columns)),
|
||||
})
|
||||
}
|
||||
|
||||
// AddIndex creates and adds a new index to the table from the given options.
|
||||
func (t *Table) addIndex(idx *Index) *Table {
|
||||
for _, name := range idx.columns {
|
||||
c, ok := t.columns[name]
|
||||
if ok {
|
||||
c.indexes.append(idx)
|
||||
idx.Columns = append(idx.Columns, c)
|
||||
}
|
||||
}
|
||||
t.Indexes = append(t.Indexes, idx)
|
||||
return t
|
||||
}
|
||||
|
||||
// column returns a table column by its name.
|
||||
// faster than map lookup for most cases.
|
||||
func (t *Table) column(name string) (*Column, bool) {
|
||||
for _, c := range t.Columns {
|
||||
if c.Name == name {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Index returns a table index by its exact name.
|
||||
func (t *Table) Index(name string) (*Index, bool) {
|
||||
idx, ok := t.index(name)
|
||||
if ok && idx.Name == name {
|
||||
return idx, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// index returns a table index by its name.
|
||||
func (t *Table) index(name string) (*Index, bool) {
|
||||
for _, idx := range t.Indexes {
|
||||
if name == idx.Name || name == idx.realname {
|
||||
return idx, true
|
||||
}
|
||||
// Same as below, there are cases where the index name
|
||||
// is unknown (created automatically on column constraint).
|
||||
if len(idx.Columns) == 1 && idx.Columns[0].Name == name {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
// If it is an "implicit index" (unique constraint on
|
||||
// table creation) and it wasn't loaded in table scanning.
|
||||
c, ok := t.column(name)
|
||||
if !ok {
|
||||
// Postgres naming convention for unique constraint (<table>_<column>_key).
|
||||
name = strings.TrimPrefix(name, t.Name+"_")
|
||||
name = strings.TrimSuffix(name, "_key")
|
||||
c, ok = t.column(name)
|
||||
}
|
||||
if ok && c.Unique {
|
||||
return &Index{Name: name, Unique: c.Unique, Columns: []*Column{c}, columns: []string{c.Name}}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// CopyTables returns a deep-copy of the given tables. This utility function is
|
||||
// useful for copying the generated schema tables (i.e. migrate.Tables) before
|
||||
// running schema migration when there is a need for execute multiple migrations
|
||||
// concurrently. e.g. running parallel unit-tests using the generated enttest package.
|
||||
func CopyTables(tables []*Table) ([]*Table, error) {
|
||||
var (
|
||||
copyT = make([]*Table, len(tables))
|
||||
byName = make(map[string]*Table)
|
||||
)
|
||||
for i, t := range tables {
|
||||
copyT[i] = &Table{
|
||||
Name: t.Name,
|
||||
Columns: make([]*Column, len(t.Columns)),
|
||||
Indexes: make([]*Index, len(t.Indexes)),
|
||||
ForeignKeys: make([]*ForeignKey, len(t.ForeignKeys)),
|
||||
}
|
||||
for j, c := range t.Columns {
|
||||
cc := *c
|
||||
// SchemaType and Enums are read-only fields.
|
||||
cc.indexes = nil
|
||||
cc.foreign = nil
|
||||
copyT[i].Columns[j] = &cc
|
||||
}
|
||||
if at := t.Annotation; at != nil {
|
||||
cat := *at
|
||||
copyT[i].Annotation = &cat
|
||||
}
|
||||
byName[t.Name] = copyT[i]
|
||||
}
|
||||
for i, t := range tables {
|
||||
ct := copyT[i]
|
||||
for _, c := range t.PrimaryKey {
|
||||
cc, ok := ct.column(c.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sql/schema: missing primary key column %q", c.Name)
|
||||
}
|
||||
ct.PrimaryKey = append(ct.PrimaryKey, cc)
|
||||
}
|
||||
for j, idx := range t.Indexes {
|
||||
cidx := &Index{
|
||||
Name: idx.Name,
|
||||
Unique: idx.Unique,
|
||||
Columns: make([]*Column, len(idx.Columns)),
|
||||
}
|
||||
if at := idx.Annotation; at != nil {
|
||||
cat := *at
|
||||
cidx.Annotation = &cat
|
||||
}
|
||||
for k, c := range idx.Columns {
|
||||
cc, ok := ct.column(c.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sql/schema: missing index column %q", c.Name)
|
||||
}
|
||||
cidx.Columns[k] = cc
|
||||
}
|
||||
ct.Indexes[j] = cidx
|
||||
}
|
||||
for j, fk := range t.ForeignKeys {
|
||||
cfk := &ForeignKey{
|
||||
Symbol: fk.Symbol,
|
||||
OnUpdate: fk.OnUpdate,
|
||||
OnDelete: fk.OnDelete,
|
||||
Columns: make([]*Column, len(fk.Columns)),
|
||||
RefColumns: make([]*Column, len(fk.RefColumns)),
|
||||
}
|
||||
for k, c := range fk.Columns {
|
||||
cc, ok := ct.column(c.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sql/schema: missing foreign-key column %q", c.Name)
|
||||
}
|
||||
cfk.Columns[k] = cc
|
||||
}
|
||||
cref, ok := byName[fk.RefTable.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sql/schema: missing foreign-key ref-table %q", fk.RefTable.Name)
|
||||
}
|
||||
cfk.RefTable = cref
|
||||
for k, c := range fk.RefColumns {
|
||||
cc, ok := cref.column(c.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sql/schema: missing foreign-key ref-column %q", c.Name)
|
||||
}
|
||||
cfk.RefColumns[k] = cc
|
||||
}
|
||||
ct.ForeignKeys[j] = cfk
|
||||
}
|
||||
}
|
||||
return copyT, nil
|
||||
}
|
||||
|
||||
// Column schema definition for SQL dialects.
|
||||
type Column struct {
|
||||
Name string // column name.
|
||||
Type field.Type // column type.
|
||||
SchemaType map[string]string // optional schema type per dialect.
|
||||
Attr string // extra attributes.
|
||||
Size int64 // max size parameter for string, blob, etc.
|
||||
Key string // key definition (PRI, UNI or MUL).
|
||||
Unique bool // column with unique constraint.
|
||||
Increment bool // auto increment attribute.
|
||||
Nullable bool // null or not null attribute.
|
||||
Default any // default value.
|
||||
Enums []string // enum values.
|
||||
Collation string // collation type (utf8mb4_unicode_ci, utf8mb4_general_ci)
|
||||
typ string // row column type (used for Rows.Scan).
|
||||
indexes Indexes // linked indexes.
|
||||
foreign *ForeignKey // linked foreign-key.
|
||||
Comment string // optional column comment.
|
||||
}
|
||||
|
||||
// Expr represents a raw expression. It is used to distinguish between
|
||||
// literal values and raw expressions when defining default values.
|
||||
type Expr string
|
||||
|
||||
// UniqueKey returns boolean indicates if this column is a unique key.
|
||||
// Used by the migration tool when parsing the `DESCRIBE TABLE` output Go objects.
|
||||
func (c *Column) UniqueKey() bool { return c.Key == UniqueKey }
|
||||
|
||||
// PrimaryKey returns boolean indicates if this column is on of the primary key columns.
|
||||
// Used by the migration tool when parsing the `DESCRIBE TABLE` output Go objects.
|
||||
func (c *Column) PrimaryKey() bool { return c.Key == PrimaryKey }
|
||||
|
||||
// ConvertibleTo reports whether a column can be converted to the new column without altering its data.
|
||||
func (c *Column) ConvertibleTo(d *Column) bool {
|
||||
switch {
|
||||
case c.Type == d.Type:
|
||||
if c.Size != 0 && d.Size != 0 {
|
||||
// Types match and have a size constraint.
|
||||
return c.Size <= d.Size
|
||||
}
|
||||
return true
|
||||
case c.IntType() && d.IntType() || c.UintType() && d.UintType():
|
||||
return c.Type <= d.Type
|
||||
case c.UintType() && d.IntType():
|
||||
// uintX can not be converted to intY, when X > Y.
|
||||
return c.Type-field.TypeUint8 <= d.Type-field.TypeInt8
|
||||
case c.Type == field.TypeString && d.Type == field.TypeEnum ||
|
||||
c.Type == field.TypeEnum && d.Type == field.TypeString:
|
||||
return true
|
||||
case c.Type.Integer() && d.Type == field.TypeString:
|
||||
return true
|
||||
}
|
||||
return c.FloatType() && d.FloatType()
|
||||
}
|
||||
|
||||
// IntType reports whether the column is an int type (int8 ... int64).
|
||||
func (c Column) IntType() bool { return c.Type >= field.TypeInt8 && c.Type <= field.TypeInt64 }
|
||||
|
||||
// UintType reports of the given type is a uint type (int8 ... int64).
|
||||
func (c Column) UintType() bool { return c.Type >= field.TypeUint8 && c.Type <= field.TypeUint64 }
|
||||
|
||||
// FloatType reports of the given type is a float type (float32, float64).
|
||||
func (c Column) FloatType() bool { return c.Type == field.TypeFloat32 || c.Type == field.TypeFloat64 }
|
||||
|
||||
// ScanDefault scans the default value string to its interface type.
|
||||
func (c *Column) ScanDefault(value string) error {
|
||||
switch {
|
||||
case strings.ToUpper(value) == Null: // ignore.
|
||||
case c.IntType():
|
||||
v := &sql.NullInt64{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning int value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = v.Int64
|
||||
case c.UintType():
|
||||
v := &sql.NullInt64{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning uint value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = uint64(v.Int64)
|
||||
case c.FloatType():
|
||||
v := &sql.NullFloat64{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning float value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = v.Float64
|
||||
case c.Type == field.TypeBool:
|
||||
v := &sql.NullBool{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning bool value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = v.Bool
|
||||
case c.Type == field.TypeString || c.Type == field.TypeEnum:
|
||||
v := &sql.NullString{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning string value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = v.String
|
||||
case c.Type == field.TypeJSON:
|
||||
v := &sql.NullString{}
|
||||
if err := v.Scan(value); err != nil {
|
||||
return fmt.Errorf("scanning json value for column %q: %w", c.Name, err)
|
||||
}
|
||||
c.Default = v.String
|
||||
case c.Type == field.TypeBytes:
|
||||
c.Default = []byte(value)
|
||||
case c.Type == field.TypeUUID:
|
||||
// skip function
|
||||
if !strings.Contains(value, "()") {
|
||||
c.Default = value
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported default type: %v default to %q", c.Type, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// supportDefault reports if the column type supports default value.
|
||||
func (c Column) supportDefault() bool {
|
||||
switch t := c.Type; t {
|
||||
case field.TypeString, field.TypeEnum:
|
||||
return c.Size < 1<<16 // not a text.
|
||||
case field.TypeBool, field.TypeTime, field.TypeUUID:
|
||||
return true
|
||||
default:
|
||||
return t.Numeric()
|
||||
}
|
||||
}
|
||||
|
||||
// scanTypeOr returns the scanning type or the given value.
|
||||
func (c *Column) scanTypeOr(t string) string {
|
||||
if c.typ != "" {
|
||||
return strings.ToLower(c.typ)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ForeignKey definition for creation.
|
||||
type ForeignKey struct {
|
||||
Symbol string // foreign-key name. Generated if empty.
|
||||
Columns []*Column // table column
|
||||
RefTable *Table // referenced table.
|
||||
RefColumns []*Column // referenced columns.
|
||||
OnUpdate ReferenceOption // action on update.
|
||||
OnDelete ReferenceOption // action on delete.
|
||||
}
|
||||
|
||||
// ReferenceOption for constraint actions.
|
||||
type ReferenceOption string
|
||||
|
||||
// Reference options.
|
||||
const (
|
||||
NoAction ReferenceOption = "NO ACTION"
|
||||
Restrict ReferenceOption = "RESTRICT"
|
||||
Cascade ReferenceOption = "CASCADE"
|
||||
SetNull ReferenceOption = "SET NULL"
|
||||
SetDefault ReferenceOption = "SET DEFAULT"
|
||||
)
|
||||
|
||||
// ConstName returns the constant name of a reference option. It's used by entc for printing the constant name in templates.
|
||||
func (r ReferenceOption) ConstName() string {
|
||||
return strings.ReplaceAll(strings.Title(strings.ToLower(string(r))), " ", "")
|
||||
}
|
||||
|
||||
// Index definition for table index.
|
||||
type Index struct {
|
||||
Name string // index name.
|
||||
Unique bool // uniqueness.
|
||||
Columns []*Column // actual table columns.
|
||||
Annotation *entsql.IndexAnnotation // index annotation.
|
||||
columns []string // columns loaded from query scan.
|
||||
realname string // real name in the database (Postgres only).
|
||||
}
|
||||
|
||||
// Indexes used for scanning all sql.Rows into a list of indexes, because
|
||||
// multiple sql rows can represent the same index (multi-columns indexes).
|
||||
type Indexes []*Index
|
||||
|
||||
// append wraps the basic `append` function by filtering duplicates indexes.
|
||||
func (i *Indexes) append(idx1 *Index) {
|
||||
for _, idx2 := range *i {
|
||||
if idx2.Name == idx1.Name {
|
||||
return
|
||||
}
|
||||
}
|
||||
*i = append(*i, idx1)
|
||||
}
|
||||
|
||||
// compareVersions returns an integer comparing the 2 versions.
|
||||
func compareVersions(v1, v2 string) int {
|
||||
pv1, ok1 := parseVersion(v1)
|
||||
pv2, ok2 := parseVersion(v2)
|
||||
if !ok1 && !ok2 {
|
||||
return 0
|
||||
}
|
||||
if !ok1 {
|
||||
return -1
|
||||
}
|
||||
if !ok2 {
|
||||
return 1
|
||||
}
|
||||
if v := compare(pv1.major, pv2.major); v != 0 {
|
||||
return v
|
||||
}
|
||||
if v := compare(pv1.minor, pv2.minor); v != 0 {
|
||||
return v
|
||||
}
|
||||
return compare(pv1.patch, pv2.patch)
|
||||
}
|
||||
|
||||
// version represents a parsed MySQL version.
|
||||
type version struct {
|
||||
major int
|
||||
minor int
|
||||
patch int
|
||||
}
|
||||
|
||||
// parseVersion returns an integer comparing the 2 versions.
|
||||
func parseVersion(v string) (*version, bool) {
|
||||
parts := strings.Split(v, ".")
|
||||
if len(parts) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var (
|
||||
err error
|
||||
ver = &version{}
|
||||
)
|
||||
for i, e := range []*int{&ver.major, &ver.minor, &ver.patch} {
|
||||
if i == len(parts) {
|
||||
break
|
||||
}
|
||||
if *e, err = strconv.Atoi(strings.Split(parts[i], "-")[0]); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return ver, true
|
||||
}
|
||||
|
||||
func compare(v1, v2 int) int {
|
||||
if v1 == v2 {
|
||||
return 0
|
||||
}
|
||||
if v1 < v2 {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func indexType(idx *Index, d string) (string, bool) {
|
||||
ant := idx.Annotation
|
||||
if ant == nil {
|
||||
return "", false
|
||||
}
|
||||
if ant.Types != nil && ant.Types[d] != "" {
|
||||
return ant.Types[d], true
|
||||
}
|
||||
if ant.Type != "" {
|
||||
return ant.Type, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
type driver struct {
|
||||
sqlDialect
|
||||
schema.Differ
|
||||
migrate.PlanApplier
|
||||
}
|
||||
|
||||
var drivers = func(v string) map[string]driver {
|
||||
return map[string]driver{
|
||||
entdialect.SQLite: {
|
||||
&SQLite{
|
||||
WithForeignKeys: true,
|
||||
Driver: nopDriver{dialect: entdialect.SQLite},
|
||||
},
|
||||
sqlite.DefaultDiff,
|
||||
sqlite.DefaultPlan,
|
||||
},
|
||||
entdialect.MySQL: {
|
||||
&MySQL{
|
||||
version: v,
|
||||
Driver: nopDriver{dialect: entdialect.MySQL},
|
||||
},
|
||||
mysql.DefaultDiff,
|
||||
mysql.DefaultPlan,
|
||||
},
|
||||
entdialect.Postgres: {
|
||||
&Postgres{
|
||||
version: v,
|
||||
Driver: nopDriver{dialect: entdialect.Postgres},
|
||||
},
|
||||
postgres.DefaultDiff,
|
||||
postgres.DefaultPlan,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type DDLArgs struct {
|
||||
// Dialect and Version of the target database.
|
||||
Dialect, Version string
|
||||
// HashSymbols indicates whether to hash long symbols in the DDL.
|
||||
HashSymbols bool
|
||||
// Tables to dump.
|
||||
Tables []*Table
|
||||
// Options to pass to the migration plan engine.
|
||||
Options []migrate.PlanOption
|
||||
}
|
||||
|
||||
// Dump the schema DDL for the given tables.
|
||||
//
|
||||
// Deprecated: use DDL instead.
|
||||
func Dump(ctx context.Context, dialect, version string, tables []*Table, opts ...migrate.PlanOption) (string, error) {
|
||||
return DDL(ctx, DDLArgs{
|
||||
Dialect: dialect,
|
||||
Version: version,
|
||||
Tables: tables,
|
||||
Options: opts,
|
||||
})
|
||||
}
|
||||
|
||||
// DDL the schema DDL for the given tables.
|
||||
func DDL(ctx context.Context, args DDLArgs) (string, error) {
|
||||
args.Options = append([]migrate.PlanOption{func(o *migrate.PlanOptions) {
|
||||
o.Mode = migrate.PlanModeDump
|
||||
o.Indent = " "
|
||||
}}, args.Options...)
|
||||
d, ok := drivers(args.Version)[args.Dialect]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported dialect %q", args.Dialect)
|
||||
}
|
||||
a := &Atlas{
|
||||
sqlDialect: d,
|
||||
dialect: args.Dialect,
|
||||
hashSymbols: args.HashSymbols,
|
||||
}
|
||||
r, err := a.StateReader(args.Tables...).ReadState(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Since the Atlas version bundled with Ent does not support view management,
|
||||
// simply spit out the definition instead of letting Atlas plan them.
|
||||
var vs []*schema.View
|
||||
for _, s := range r.Schemas {
|
||||
vs = append(vs, s.Views...)
|
||||
s.Views = nil
|
||||
}
|
||||
var c schema.Changes
|
||||
if slices.ContainsFunc(args.Tables, func(t *Table) bool { return t.Schema != "" }) {
|
||||
c, err = d.RealmDiff(&schema.Realm{}, r)
|
||||
} else {
|
||||
c, err = d.SchemaDiff(&schema.Schema{}, r.Schemas[0])
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := d.PlanChanges(ctx, "dump", c, args.Options...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, v := range vs {
|
||||
q, _ := sql.Dialect(args.Dialect).
|
||||
CreateView(v.Name).
|
||||
Schema(v.Schema.Name).
|
||||
Columns(func(cols []*schema.Column) (bs []*sql.ColumnBuilder) {
|
||||
for _, c := range cols {
|
||||
bs = append(bs, sql.Dialect(args.Dialect).Column(c.Name).Type(c.Type.Raw))
|
||||
}
|
||||
return
|
||||
}(v.Columns)...).
|
||||
As(sql.Raw(v.Def)).
|
||||
Query()
|
||||
p.Changes = append(p.Changes, &migrate.Change{
|
||||
Cmd: q,
|
||||
Comment: fmt.Sprintf("Add %q view", v.Name),
|
||||
})
|
||||
}
|
||||
for _, t := range args.Tables {
|
||||
p.Directives = append(p.Directives, fmt.Sprintf(
|
||||
"-- atlas:pos %s%s[type=%s] %s",
|
||||
func() string {
|
||||
if t.Schema != "" {
|
||||
return t.Schema + "[type=schema]."
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
t.Name,
|
||||
func() string {
|
||||
if t.View {
|
||||
return "view"
|
||||
}
|
||||
return "table"
|
||||
}(),
|
||||
t.Pos,
|
||||
))
|
||||
}
|
||||
f, err := migrate.DefaultFormatter.FormatFile(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(f.Bytes()), nil
|
||||
}
|
||||
390
dialect/sql/schema/schema_test.go
Normal file
390
dialect/sql/schema/schema_test.go
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/entsql"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestColumn_ConvertibleTo(t *testing.T) {
|
||||
c1 := &Column{Type: field.TypeString, Size: 10}
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 10}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 255}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 9}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
|
||||
|
||||
c1 = &Column{Type: field.TypeFloat32}
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat64}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
|
||||
|
||||
c1 = &Column{Type: field.TypeFloat64}
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat32}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeFloat64}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
|
||||
|
||||
c1 = &Column{Type: field.TypeUint}
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt64}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeUint64}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt8}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint8}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint16}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint32}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 1}))
|
||||
|
||||
c1 = &Column{Type: field.TypeInt}
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeInt64}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt8}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeInt32}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint8}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint16}))
|
||||
require.False(t, c1.ConvertibleTo(&Column{Type: field.TypeUint32}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString}))
|
||||
require.True(t, c1.ConvertibleTo(&Column{Type: field.TypeString, Size: 1}))
|
||||
}
|
||||
|
||||
func TestColumn_ScanDefault(t *testing.T) {
|
||||
c1 := &Column{Type: field.TypeString, Size: 10}
|
||||
require.NoError(t, c1.ScanDefault("Hello World"))
|
||||
require.Equal(t, "Hello World", c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("1"))
|
||||
require.Equal(t, "1", c1.Default)
|
||||
|
||||
c1 = &Column{Type: field.TypeInt64}
|
||||
require.NoError(t, c1.ScanDefault("128"))
|
||||
require.Equal(t, int64(128), c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("1"))
|
||||
require.Equal(t, int64(1), c1.Default)
|
||||
require.Error(t, c1.ScanDefault("foo"))
|
||||
|
||||
c1 = &Column{Type: field.TypeUint64}
|
||||
require.NoError(t, c1.ScanDefault("128"))
|
||||
require.Equal(t, uint64(128), c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("1"))
|
||||
require.Equal(t, uint64(1), c1.Default)
|
||||
require.Error(t, c1.ScanDefault("foo"))
|
||||
|
||||
c1 = &Column{Type: field.TypeFloat64}
|
||||
require.NoError(t, c1.ScanDefault("128.1"))
|
||||
require.Equal(t, 128.1, c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("1"))
|
||||
require.Equal(t, float64(1), c1.Default)
|
||||
require.Error(t, c1.ScanDefault("foo"))
|
||||
|
||||
c1 = &Column{Type: field.TypeBool}
|
||||
require.NoError(t, c1.ScanDefault("1"))
|
||||
require.Equal(t, true, c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("true"))
|
||||
require.Equal(t, true, c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("0"))
|
||||
require.Equal(t, false, c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("false"))
|
||||
require.Equal(t, false, c1.Default)
|
||||
require.Error(t, c1.ScanDefault("foo"))
|
||||
|
||||
c1 = &Column{Type: field.TypeUUID}
|
||||
require.NoError(t, c1.ScanDefault("gen_random_uuid()"))
|
||||
require.Equal(t, nil, c1.Default)
|
||||
require.NoError(t, c1.ScanDefault("00000000-0000-0000-0000-000000000000"))
|
||||
require.Equal(t, "00000000-0000-0000-0000-000000000000", c1.Default)
|
||||
}
|
||||
|
||||
func TestCopyTables(t *testing.T) {
|
||||
users := &Table{
|
||||
Name: "users",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "spouse_id", Type: field.TypeInt},
|
||||
},
|
||||
}
|
||||
users.PrimaryKey = users.Columns[:1]
|
||||
users.Indexes = append(users.Indexes, &Index{
|
||||
Name: "name",
|
||||
Columns: users.Columns[1:2],
|
||||
})
|
||||
users.AddForeignKey(&ForeignKey{
|
||||
Columns: users.Columns[2:],
|
||||
RefTable: users,
|
||||
RefColumns: users.Columns[:1],
|
||||
OnUpdate: SetNull,
|
||||
})
|
||||
users.SetAnnotation(&entsql.Annotation{Table: "Users"})
|
||||
pets := &Table{
|
||||
Name: "pets",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "owner_id", Type: field.TypeInt},
|
||||
},
|
||||
}
|
||||
pets.Indexes = append(pets.Indexes, &Index{
|
||||
Name: "name",
|
||||
Unique: true,
|
||||
Columns: pets.Columns[1:2],
|
||||
Annotation: entsql.Desc(),
|
||||
})
|
||||
pets.AddForeignKey(&ForeignKey{
|
||||
Columns: pets.Columns[2:],
|
||||
RefTable: users,
|
||||
RefColumns: users.Columns[:1],
|
||||
OnDelete: SetDefault,
|
||||
})
|
||||
tables := []*Table{users, pets}
|
||||
copyT, err := CopyTables(tables)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tables, copyT)
|
||||
}
|
||||
|
||||
func TestDDL(t *testing.T) {
|
||||
const (
|
||||
hash = "249590215c5bc8be0106146e65d7fa7f"
|
||||
idx = "super_duper_mega_ultra_hyper_giga_colossal_unbelievably_long_index_name"
|
||||
hashMY = "super_duper_mega_ultra_hyper_gi_249590215c5bc8be0106146e65d7fa7f"
|
||||
hashPG = "super_duper_mega_ultra_hyper_g_249590215c5bc8be0106146e65d7fa7f"
|
||||
)
|
||||
tbls := func() []*Table {
|
||||
users := &Table{
|
||||
Name: "users",
|
||||
Pos: "users.go:15",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "spouse_id", Type: field.TypeInt},
|
||||
},
|
||||
}
|
||||
users.PrimaryKey = users.Columns[:1]
|
||||
users.Indexes = append(users.Indexes, &Index{
|
||||
Name: "name",
|
||||
Columns: users.Columns[1:2],
|
||||
})
|
||||
users.AddForeignKey(&ForeignKey{
|
||||
Columns: users.Columns[2:],
|
||||
RefTable: users,
|
||||
RefColumns: users.Columns[:1],
|
||||
OnUpdate: SetDefault,
|
||||
})
|
||||
users.SetAnnotation(&entsql.Annotation{Table: "Users"})
|
||||
pets := &Table{
|
||||
Name: "pets",
|
||||
Pos: "pets.go:15",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "fur_color", Type: field.TypeEnum, Enums: []string{"black", "white"}},
|
||||
{Name: "owner_id", Type: field.TypeInt},
|
||||
},
|
||||
}
|
||||
pets.Indexes = append(pets.Indexes, &Index{
|
||||
Name: idx,
|
||||
Unique: true,
|
||||
Columns: pets.Columns[1:2],
|
||||
Annotation: entsql.Desc(),
|
||||
})
|
||||
pets.AddForeignKey(&ForeignKey{
|
||||
Columns: pets.Columns[3:],
|
||||
RefTable: users,
|
||||
RefColumns: users.Columns[:1],
|
||||
OnDelete: SetDefault,
|
||||
})
|
||||
petsWithoutFur := &Table{
|
||||
Name: "pets_without_fur",
|
||||
Pos: "pets.go:30",
|
||||
View: true,
|
||||
Columns: append(pets.Columns[:2], pets.Columns[3]),
|
||||
Annotation: entsql.View("SELECT id, name, owner_id FROM pets"),
|
||||
}
|
||||
petNames := &Table{
|
||||
Name: "pet_names",
|
||||
Pos: "pets.go:45",
|
||||
View: true,
|
||||
Columns: pets.Columns[1:1],
|
||||
Annotation: entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) {
|
||||
s.Select("name").From(sql.Table("pets"))
|
||||
}),
|
||||
}
|
||||
return []*Table{users, pets, petsWithoutFur, petNames}
|
||||
}
|
||||
my := func(length int, idx string) string {
|
||||
return fmt.Sprintf(strings.ReplaceAll(`-- Add new schema named "s1"
|
||||
CREATE DATABASE $s1$;
|
||||
-- Add new schema named "s2"
|
||||
CREATE DATABASE $s2$;
|
||||
-- Add new schema named "s3"
|
||||
CREATE DATABASE $s3$;
|
||||
-- Create "users" table
|
||||
CREATE TABLE $s1$.$users$ (
|
||||
$id$ bigint NOT NULL,
|
||||
$name$ varchar(%d) NOT NULL,
|
||||
$spouse_id$ bigint NOT NULL,
|
||||
PRIMARY KEY ($id$),
|
||||
INDEX $name$ ($name$),
|
||||
FOREIGN KEY ($spouse_id$) REFERENCES $s1$.$users$ ($id$) ON UPDATE SET DEFAULT
|
||||
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
|
||||
-- Create "pets" table
|
||||
CREATE TABLE $s2$.$pets$ (
|
||||
$id$ bigint NOT NULL,
|
||||
$name$ varchar(%d) NOT NULL,
|
||||
$owner_id$ bigint NOT NULL,
|
||||
$owner_id$ bigint NOT NULL,
|
||||
UNIQUE INDEX $%s$ ($name$ DESC),
|
||||
FOREIGN KEY ($owner_id$) REFERENCES $s1$.$users$ ($id$) ON DELETE SET DEFAULT
|
||||
) CHARSET utf8mb4 COLLATE utf8mb4_bin;
|
||||
-- Add "pets_without_fur" view
|
||||
CREATE VIEW $s3$.$pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets;
|
||||
`, "$", "`"), length, length, idx)
|
||||
}
|
||||
|
||||
pg := func(idx string) string {
|
||||
return fmt.Sprintf(`-- Add new schema named "s1"
|
||||
CREATE SCHEMA "s1";
|
||||
-- Add new schema named "s2"
|
||||
CREATE SCHEMA "s2";
|
||||
-- Add new schema named "s3"
|
||||
CREATE SCHEMA "s3";
|
||||
-- Create "users" table
|
||||
CREATE TABLE "s1"."users" (
|
||||
"id" bigint NOT NULL,
|
||||
"name" character varying NOT NULL,
|
||||
"spouse_id" bigint NOT NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
FOREIGN KEY ("spouse_id") REFERENCES "s1"."users" ("id") ON UPDATE SET DEFAULT
|
||||
);
|
||||
-- Create index "name" to table: "users"
|
||||
CREATE INDEX "name" ON "s1"."users" ("name");
|
||||
-- Create "pets" table
|
||||
CREATE TABLE "s2"."pets" (
|
||||
"id" bigint NOT NULL,
|
||||
"name" character varying NOT NULL,
|
||||
"owner_id" bigint NOT NULL,
|
||||
"owner_id" bigint NOT NULL,
|
||||
FOREIGN KEY ("owner_id") REFERENCES "s1"."users" ("id") ON DELETE SET DEFAULT
|
||||
);
|
||||
-- Create index "%s" to table: "pets"
|
||||
CREATE UNIQUE INDEX "%s" ON "s2"."pets" ("name" DESC);
|
||||
-- Add "pets_without_fur" view
|
||||
CREATE VIEW "s3"."pets_without_fur" ("id", "name", "owner_id") AS SELECT id, name, owner_id FROM pets;
|
||||
-- Add "pet_names" view
|
||||
CREATE VIEW "s3"."pet_names" AS SELECT "name" FROM "pets";
|
||||
`, idx, idx)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
dialect, version, expected string
|
||||
hash bool
|
||||
}{
|
||||
{
|
||||
dialect.SQLite, "",
|
||||
fmt.Sprintf(strings.ReplaceAll(`-- Create "users" table
|
||||
CREATE TABLE $users$ (
|
||||
$id$ integer NOT NULL,
|
||||
$name$ text NOT NULL,
|
||||
$spouse_id$ integer NOT NULL,
|
||||
PRIMARY KEY ($id$),
|
||||
FOREIGN KEY ($spouse_id$) REFERENCES $users$ ($id$) ON UPDATE SET DEFAULT
|
||||
);
|
||||
-- Create index "name" to table: "users"
|
||||
CREATE INDEX $name$ ON $users$ ($name$);
|
||||
-- Create "pets" table
|
||||
CREATE TABLE $pets$ (
|
||||
$id$ integer NOT NULL,
|
||||
$name$ text NOT NULL,
|
||||
$owner_id$ integer NOT NULL,
|
||||
$owner_id$ integer NOT NULL,
|
||||
FOREIGN KEY ($owner_id$) REFERENCES $users$ ($id$) ON DELETE SET DEFAULT
|
||||
);
|
||||
-- Create index "%s" to table: "pets"
|
||||
CREATE UNIQUE INDEX $%s$ ON $pets$ ($name$ DESC);
|
||||
-- Add "pets_without_fur" view
|
||||
CREATE VIEW $pets_without_fur$ ($id$, $name$, $owner_id$) AS SELECT id, name, owner_id FROM pets;
|
||||
`, "$", "`"), idx, idx), false,
|
||||
},
|
||||
{dialect.MySQL, "5.6", my(255, idx), false},
|
||||
{dialect.MySQL, "5.6", my(191, hashMY), true},
|
||||
{dialect.MySQL, "5.7", my(255, idx), false},
|
||||
{dialect.MySQL, "8", my(255, hashMY), true},
|
||||
{dialect.Postgres, "12", pg(idx), false},
|
||||
{dialect.Postgres, "13", pg(idx), false},
|
||||
{dialect.Postgres, "14", pg(hashPG), true},
|
||||
{dialect.Postgres, "15", pg(hashPG), true},
|
||||
} {
|
||||
n := tt.dialect
|
||||
if tt.version != "" {
|
||||
n += ":" + tt.version
|
||||
}
|
||||
pos := `-- atlas:pos users[type=table] users.go:15
|
||||
-- atlas:pos pets[type=table] pets.go:15
|
||||
-- atlas:pos pets_without_fur[type=view] pets.go:30
|
||||
-- atlas:pos pet_names[type=view] pets.go:45
|
||||
|
||||
`
|
||||
tables := tbls()
|
||||
if tt.dialect != dialect.SQLite {
|
||||
tables[0].Schema = "s1"
|
||||
tables[1].Schema = "s2"
|
||||
tables[2].Schema = "s3"
|
||||
tables[3].Schema = "s3"
|
||||
pos = `-- atlas:pos s1[type=schema].users[type=table] users.go:15
|
||||
-- atlas:pos s2[type=schema].pets[type=table] pets.go:15
|
||||
-- atlas:pos s3[type=schema].pets_without_fur[type=view] pets.go:30
|
||||
-- atlas:pos s3[type=schema].pet_names[type=view] pets.go:45
|
||||
|
||||
`
|
||||
}
|
||||
t.Run(n, func(t *testing.T) {
|
||||
ac, err := DDL(context.Background(), DDLArgs{
|
||||
Dialect: tt.dialect,
|
||||
Version: tt.version,
|
||||
Tables: tables,
|
||||
HashSymbols: tt.hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pos+tt.expected, ac)
|
||||
})
|
||||
t.Run(n+" single schema", func(t *testing.T) {
|
||||
ac, err := DDL(context.Background(), DDLArgs{
|
||||
Dialect: tt.dialect,
|
||||
Version: tt.version,
|
||||
Tables: tables[0:1],
|
||||
HashSymbols: tt.hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if tt.dialect != dialect.SQLite {
|
||||
require.Contains(t, ac, "s1[type=schema].")
|
||||
require.NotContains(t, ac, "s2[type=schema].")
|
||||
require.Contains(t, ac, "-- Add new schema named \"s1\"")
|
||||
}
|
||||
})
|
||||
t.Run(n+" no schema", func(t *testing.T) {
|
||||
tables[0].Schema = ""
|
||||
ac, err := DDL(context.Background(), DDLArgs{
|
||||
Dialect: tt.dialect,
|
||||
Version: tt.version,
|
||||
Tables: tables[0:1],
|
||||
HashSymbols: tt.hash,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, ac, "[type=schema].")
|
||||
require.Contains(t, ac, "[type=table]")
|
||||
})
|
||||
}
|
||||
}
|
||||
227
dialect/sql/schema/sqlite.go
Normal file
227
dialect/sql/schema/sqlite.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdsql "database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"ariga.io/atlas/sql/schema"
|
||||
"ariga.io/atlas/sql/sqlite"
|
||||
)
|
||||
|
||||
type (
|
||||
// SQLite adapter for Atlas migration engine.
|
||||
SQLite struct {
|
||||
dialect.Driver
|
||||
WithForeignKeys bool
|
||||
}
|
||||
// SQLiteTx implements dialect.Tx.
|
||||
SQLiteTx struct {
|
||||
dialect.Tx
|
||||
commit func() error // Override Commit to toggle foreign keys back on after Commit.
|
||||
rollback func() error // Override Rollback to toggle foreign keys back on after Rollback.
|
||||
}
|
||||
)
|
||||
|
||||
// Tx implements opens a transaction.
|
||||
func (d *SQLite) Tx(ctx context.Context) (dialect.Tx, error) {
|
||||
db := &db{d}
|
||||
if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = off"); err != nil {
|
||||
return nil, fmt.Errorf("sqlite: set 'foreign_keys = off': %w", err)
|
||||
}
|
||||
t, err := d.Driver.Tx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx := &tx{t}
|
||||
cm, err := sqlite.CommitFunc(ctx, db, tx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SQLiteTx{Tx: t, commit: cm, rollback: sqlite.RollbackFunc(ctx, db, tx, true)}, nil
|
||||
}
|
||||
|
||||
// Commit ensures foreign keys are toggled back on after commit.
|
||||
func (tx *SQLiteTx) Commit() error {
|
||||
return tx.commit()
|
||||
}
|
||||
|
||||
// Rollback ensures foreign keys are toggled back on after rollback.
|
||||
func (tx *SQLiteTx) Rollback() error {
|
||||
return tx.rollback()
|
||||
}
|
||||
|
||||
// init makes sure that foreign_keys support is enabled.
|
||||
func (d *SQLite) init(ctx context.Context) error {
|
||||
on, err := exist(ctx, d, "PRAGMA foreign_keys")
|
||||
if err != nil {
|
||||
return fmt.Errorf("sqlite: check foreign_keys pragma: %w", err)
|
||||
}
|
||||
if !on {
|
||||
// foreign_keys pragma is off, either enable it by execute "PRAGMA foreign_keys=ON"
|
||||
// or add the following parameter in the connection string "_fk=1".
|
||||
return fmt.Errorf("sqlite: foreign_keys pragma is off: missing %q in the connection string", "_fk=1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SQLite) tableExist(ctx context.Context, conn dialect.ExecQuerier, name string) (bool, error) {
|
||||
query, args := sql.Select().Count().
|
||||
From(sql.Table("sqlite_master")).
|
||||
Where(sql.And(
|
||||
sql.EQ("type", "table"),
|
||||
sql.EQ("name", name),
|
||||
)).
|
||||
Query()
|
||||
return exist(ctx, conn, query, args...)
|
||||
}
|
||||
|
||||
func (d *SQLite) atOpen(conn dialect.ExecQuerier) (migrate.Driver, error) {
|
||||
return sqlite.Open(&db{ExecQuerier: conn})
|
||||
}
|
||||
|
||||
func (d *SQLite) atTable(t1 *Table, t2 *schema.Table) {
|
||||
if t1.Annotation != nil {
|
||||
setAtChecks(t1, t2)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SQLite) supportsDefault(*Column) bool {
|
||||
// SQLite supports default values for all standard types.
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *SQLite) atTypeC(c1 *Column, c2 *schema.Column) error {
|
||||
if c1.SchemaType != nil && c1.SchemaType[dialect.SQLite] != "" {
|
||||
t, err := sqlite.ParseType(strings.ToLower(c1.SchemaType[dialect.SQLite]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
return nil
|
||||
}
|
||||
var t schema.Type
|
||||
switch c1.Type {
|
||||
case field.TypeBlob:
|
||||
return fmt.Errorf("blob fields are not stored in the database")
|
||||
case field.TypeBool:
|
||||
t = &schema.BoolType{T: "bool"}
|
||||
case field.TypeInt8, field.TypeUint8, field.TypeInt16, field.TypeUint16, field.TypeInt32,
|
||||
field.TypeUint32, field.TypeUint, field.TypeInt, field.TypeInt64, field.TypeUint64:
|
||||
t = &schema.IntegerType{T: sqlite.TypeInteger}
|
||||
case field.TypeBytes:
|
||||
t = &schema.BinaryType{T: sqlite.TypeBlob}
|
||||
case field.TypeString, field.TypeEnum:
|
||||
// SQLite does not impose any length restrictions on
|
||||
// the length of strings, BLOBs or numeric values.
|
||||
t = &schema.StringType{T: sqlite.TypeText}
|
||||
case field.TypeFloat32, field.TypeFloat64:
|
||||
t = &schema.FloatType{T: sqlite.TypeReal}
|
||||
case field.TypeTime:
|
||||
t = &schema.TimeType{T: "datetime"}
|
||||
case field.TypeJSON:
|
||||
t = &schema.JSONType{T: "json"}
|
||||
case field.TypeUUID:
|
||||
t = &sqlite.UUIDType{T: "uuid"}
|
||||
case field.TypeOther:
|
||||
t = &schema.UnsupportedType{T: c1.typ}
|
||||
default:
|
||||
t, err := sqlite.ParseType(strings.ToLower(c1.typ))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c2.Type.Type = t
|
||||
}
|
||||
c2.Type.Type = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SQLite) atUniqueC(t1 *Table, c1 *Column, t2 *schema.Table, c2 *schema.Column) {
|
||||
// For UNIQUE columns, SQLite create an implicit index named
|
||||
// "sqlite_autoindex_<table>_<i>". Ent uses the PostgreSQL approach
|
||||
// in its migration, and name these indexes as "<table>_<column>_key".
|
||||
for _, idx := range t1.Indexes {
|
||||
// Index also defined explicitly, and will be add in atIndexes.
|
||||
if idx.Unique && d.atImplicitIndexName(idx, t1, c1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t2.AddIndexes(schema.NewUniqueIndex(fmt.Sprintf("%s_%s_key", t2.Name, c1.Name)).AddColumns(c2))
|
||||
}
|
||||
|
||||
func (d *SQLite) atImplicitIndexName(idx *Index, t1 *Table, c1 *Column) bool {
|
||||
if idx.Name == c1.Name {
|
||||
return true
|
||||
}
|
||||
p := fmt.Sprintf("sqlite_autoindex_%s_", t1.Name)
|
||||
if !strings.HasPrefix(idx.Name, p) {
|
||||
return false
|
||||
}
|
||||
i, err := strconv.ParseInt(strings.TrimPrefix(idx.Name, p), 10, 64)
|
||||
return err == nil && i > 0
|
||||
}
|
||||
|
||||
func (d *SQLite) atIncrementC(t *schema.Table, c *schema.Column) {
|
||||
if c.Default != nil {
|
||||
t.Attrs = removeAttr(t.Attrs, reflect.TypeOf(&sqlite.AutoIncrement{}))
|
||||
} else {
|
||||
c.AddAttrs(&sqlite.AutoIncrement{})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SQLite) atIncrementT(t *schema.Table, v int64) {
|
||||
t.AddAttrs(&sqlite.AutoIncrement{Seq: v})
|
||||
}
|
||||
|
||||
func (d *SQLite) atIndex(idx1 *Index, t2 *schema.Table, idx2 *schema.Index) error {
|
||||
for _, c1 := range idx1.Columns {
|
||||
c2, ok := t2.Column(c1.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected index %q column: %q", idx1.Name, c1.Name)
|
||||
}
|
||||
idx2.AddParts(&schema.IndexPart{C: c2})
|
||||
}
|
||||
if idx1.Annotation != nil && idx1.Annotation.Where != "" {
|
||||
idx2.AddAttrs(&sqlite.IndexPredicate{P: idx1.Annotation.Where})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*SQLite) atTypeRangeSQL(ts ...string) string {
|
||||
for i := range ts {
|
||||
ts[i] = fmt.Sprintf("('%s')", ts[i])
|
||||
}
|
||||
return fmt.Sprintf("INSERT INTO `%s` (`type`) VALUES %s", TypeTable, strings.Join(ts, ", "))
|
||||
}
|
||||
|
||||
type tx struct {
|
||||
dialect.Tx
|
||||
}
|
||||
|
||||
func (tx *tx) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error) {
|
||||
rows := &sql.Rows{}
|
||||
if err := tx.Query(ctx, query, args, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows.ColumnScanner.(*stdsql.Rows), nil
|
||||
}
|
||||
|
||||
func (tx *tx) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error) {
|
||||
var r stdsql.Result
|
||||
if err := tx.Exec(ctx, query, args, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
365
dialect/sql/schema/writer.go
Normal file
365
dialect/sql/schema/writer.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
)
|
||||
|
||||
type (
|
||||
// WriteDriver is a driver that writes all driver exec operations to its writer.
|
||||
// Note that this driver is used only for printing or writing statements to SQL
|
||||
// files, and may require manual changes to the generated SQL statements.
|
||||
WriteDriver struct {
|
||||
dialect.Driver // optional driver for query calls.
|
||||
io.Writer // target for exec statements.
|
||||
FormatFunc func(string) (string, error)
|
||||
}
|
||||
// DirWriter implements the io.Writer interface
|
||||
// for writing to an Atlas managed directory.
|
||||
DirWriter struct {
|
||||
Dir migrate.Dir // target directory.
|
||||
Formatter migrate.Formatter // optional formatter.
|
||||
b bytes.Buffer // working buffer.
|
||||
changes []*migrate.Change // changes to flush.
|
||||
}
|
||||
)
|
||||
|
||||
// Write implements the io.Writer interface.
|
||||
func (d *DirWriter) Write(p []byte) (int, error) {
|
||||
return d.b.Write(trimReturning(p))
|
||||
}
|
||||
|
||||
// Change converts all written statement so far into a migration
|
||||
// change with the given comment.
|
||||
func (d *DirWriter) Change(comment string) {
|
||||
// Trim semicolon and new line, because formatter adds it.
|
||||
d.changes = append(d.changes, &migrate.Change{Comment: comment, Cmd: strings.TrimRight(d.b.String(), ";\n")})
|
||||
d.b.Reset()
|
||||
}
|
||||
|
||||
// Flush flushes the written statements to the directory.
|
||||
func (d *DirWriter) Flush(name string) error {
|
||||
switch {
|
||||
case d.b.Len() != 0:
|
||||
return fmt.Errorf("writer has undocumented change. Use Change or FlushChange instead")
|
||||
case len(d.changes) == 0:
|
||||
return errors.New("writer has no changes to flush")
|
||||
default:
|
||||
return migrate.NewPlanner(nil, d.Dir, migrate.PlanFormat(d.Formatter)).
|
||||
WritePlan(&migrate.Plan{
|
||||
Name: name,
|
||||
Changes: d.changes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FlushChange combines Change and Flush.
|
||||
func (d *DirWriter) FlushChange(name, comment string) error {
|
||||
d.Change(comment)
|
||||
return d.Flush(name)
|
||||
}
|
||||
|
||||
// NewWriteDriver creates a dialect.Driver that writes all driver exec statement to its writer.
|
||||
func NewWriteDriver(dialect string, w io.Writer) *WriteDriver {
|
||||
return &WriteDriver{
|
||||
Writer: w,
|
||||
Driver: nopDriver{dialect: dialect},
|
||||
}
|
||||
}
|
||||
|
||||
// Exec implements the dialect.Driver.Exec method.
|
||||
func (w *WriteDriver) Exec(_ context.Context, query string, args, res any) error {
|
||||
if rr, ok := res.(*sql.Result); ok {
|
||||
*rr = noResult{}
|
||||
}
|
||||
if !strings.HasSuffix(query, ";") {
|
||||
query += ";"
|
||||
}
|
||||
if args != nil {
|
||||
args, ok := args.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected args type: %T", args)
|
||||
}
|
||||
query = w.expandArgs(query, args)
|
||||
}
|
||||
_, err := io.WriteString(w, query+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// Query implements the dialect.Driver.Query method.
|
||||
func (w *WriteDriver) Query(ctx context.Context, query string, args, res any) error {
|
||||
if strings.HasPrefix(query, "INSERT") || strings.HasPrefix(query, "UPDATE") {
|
||||
if err := w.Exec(ctx, query, args, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if rr, ok := res.(*sql.Rows); ok {
|
||||
cols := func() []string {
|
||||
// If the query has a RETURNING clause, mock the result.
|
||||
var clause string
|
||||
outer:
|
||||
for i := 0; i < len(query); i++ {
|
||||
switch q := query[i]; {
|
||||
case q == '\'', q == '"', q == '`': // string or identifier
|
||||
_, skip := skipQuoted(query, i)
|
||||
if skip == -1 {
|
||||
return nil // malformed SQL
|
||||
}
|
||||
i = skip
|
||||
continue
|
||||
case reReturning.MatchString(query[i:]):
|
||||
var j int
|
||||
inner:
|
||||
// Forward until next unquoted ';' appears, or we reach the end of the query.
|
||||
for j = i; j < len(query); j++ {
|
||||
switch query[j] {
|
||||
case '\'', '"', '`': // string or identifier
|
||||
_, skip := skipQuoted(query, j)
|
||||
if skip == -1 {
|
||||
return nil // malformed RETURNING clause
|
||||
}
|
||||
j = skip
|
||||
case ';':
|
||||
break inner
|
||||
}
|
||||
}
|
||||
clause = query[i:j]
|
||||
break outer
|
||||
}
|
||||
}
|
||||
cols := strings.Split(reReturning.ReplaceAllString(clause, ""), ",")
|
||||
for i := range cols {
|
||||
cols[i] = strings.TrimSpace(cols[i])
|
||||
}
|
||||
return cols
|
||||
}()
|
||||
*rr = sql.Rows{ColumnScanner: &noRows{cols: cols}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch w.Driver.(type) {
|
||||
case nil, nopDriver:
|
||||
return errors.New("query is not supported by the WriteDriver")
|
||||
default:
|
||||
return w.Driver.Query(ctx, query, args, res)
|
||||
}
|
||||
}
|
||||
|
||||
// expandArgs combines to arguments and statement into a single statement to
|
||||
// print or write into a file (before editing).
|
||||
// Note, the output may be incorrect or unsafe SQL and require manual changes.
|
||||
func (w *WriteDriver) expandArgs(query string, args []any) string {
|
||||
var (
|
||||
b strings.Builder
|
||||
p = w.placeholder()
|
||||
scan = w.scanPlaceholder()
|
||||
)
|
||||
for i := 0; i < len(query); i++ {
|
||||
Top:
|
||||
switch query[i] {
|
||||
case p:
|
||||
idx, size := scan(query[i+1:])
|
||||
// Unrecognized placeholder.
|
||||
if idx < 0 || idx >= len(args) {
|
||||
return query
|
||||
}
|
||||
i += size
|
||||
v, err := w.formatArg(args[idx])
|
||||
if err != nil {
|
||||
// Unexpected formatting error.
|
||||
return query
|
||||
}
|
||||
b.WriteString(v)
|
||||
// String or identifier.
|
||||
case '\'', '"', '`':
|
||||
for j := i + 1; j < len(query); j++ {
|
||||
switch query[j] {
|
||||
case '\\':
|
||||
j++
|
||||
case query[i]:
|
||||
b.WriteString(query[i : j+1])
|
||||
i = j
|
||||
break Top
|
||||
}
|
||||
}
|
||||
// Unexpected EOS.
|
||||
return query
|
||||
default:
|
||||
b.WriteByte(query[i])
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (w *WriteDriver) scanPlaceholder() func(string) (int, int) {
|
||||
switch w.Dialect() {
|
||||
case dialect.Postgres:
|
||||
return func(s string) (int, int) {
|
||||
var i int
|
||||
for i < len(s) && unicode.IsDigit(rune(s[i])) {
|
||||
i++
|
||||
}
|
||||
idx, err := strconv.ParseInt(s[:i], 10, 64)
|
||||
if err != nil {
|
||||
return -1, 0
|
||||
}
|
||||
// Placeholders are 1-based.
|
||||
return int(idx) - 1, i
|
||||
}
|
||||
default:
|
||||
idx := -1
|
||||
return func(string) (int, int) {
|
||||
idx++
|
||||
return idx, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WriteDriver) placeholder() byte {
|
||||
if w.Dialect() == dialect.Postgres {
|
||||
return '$'
|
||||
}
|
||||
return '?'
|
||||
}
|
||||
|
||||
func (w *WriteDriver) formatArg(v any) (string, error) {
|
||||
if w.FormatFunc != nil {
|
||||
return w.FormatFunc(fmt.Sprint(v))
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return "NULL", nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprintf("%d", v), nil
|
||||
case float32, float64:
|
||||
return fmt.Sprintf("%g", v), nil
|
||||
case bool:
|
||||
if v {
|
||||
return "1", nil
|
||||
} else {
|
||||
return "0", nil
|
||||
}
|
||||
case string:
|
||||
return "'" + strings.ReplaceAll(v, "'", "''") + "'", nil
|
||||
case json.RawMessage:
|
||||
return "'" + strings.ReplaceAll(string(v), "'", "''") + "'", nil
|
||||
case []byte:
|
||||
return "{{ BINARY_VALUE }}", nil
|
||||
case time.Time:
|
||||
return "{{ TIME_VALUE }}", nil
|
||||
case fmt.Stringer:
|
||||
return "'" + strings.ReplaceAll(v.String(), "'", "''") + "'", nil
|
||||
default:
|
||||
return "{{ VALUE }}", nil
|
||||
}
|
||||
}
|
||||
|
||||
var reReturning = regexp.MustCompile(`(?i)^\s?RETURNING`)
|
||||
|
||||
// trimReturning trims any RETURNING suffix from INSERT/UPDATE queries.
|
||||
// Note, that the output may be incorrect or unsafe SQL and require manual changes.
|
||||
func trimReturning(query []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
loop:
|
||||
for i := 0; i < len(query); i++ {
|
||||
switch q := query[i]; {
|
||||
case q == '\'', q == '"', q == '`': // string or identifier
|
||||
s, skip := skipQuoted(query, i)
|
||||
if skip == -1 {
|
||||
return query
|
||||
}
|
||||
b.Write(s)
|
||||
i = skip
|
||||
continue
|
||||
case reReturning.Match(query[i:]):
|
||||
// Forward until next unquoted ';' appears.
|
||||
for j := i; j < len(query); j++ { // skip "RETURNING"
|
||||
switch query[j] {
|
||||
case '\'', '"', '`': // string or identifier
|
||||
_, skip := skipQuoted(query, j)
|
||||
if skip == -1 {
|
||||
return query
|
||||
}
|
||||
j = skip
|
||||
case ';':
|
||||
b.WriteString(";")
|
||||
i += j
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte(query[i])
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func skipQuoted[T []byte | string](query T, idx int) (T, int) {
|
||||
for j := idx + 1; j < len(query); j++ {
|
||||
switch query[j] {
|
||||
case '\\':
|
||||
j++
|
||||
case query[idx]:
|
||||
return query[idx : j+1], j
|
||||
}
|
||||
}
|
||||
// Unexpected EOS.
|
||||
return query, -1
|
||||
}
|
||||
|
||||
// Tx writes the transaction start.
|
||||
func (w *WriteDriver) Tx(context.Context) (dialect.Tx, error) {
|
||||
return dialect.NopTx(w), nil
|
||||
}
|
||||
|
||||
// noResult represents a zero result.
|
||||
type noResult struct{}
|
||||
|
||||
func (noResult) LastInsertId() (int64, error) { return 0, nil }
|
||||
func (noResult) RowsAffected() (int64, error) { return 0, nil }
|
||||
|
||||
// noRows represents no rows.
|
||||
type noRows struct {
|
||||
sql.ColumnScanner
|
||||
cols []string
|
||||
done bool
|
||||
}
|
||||
|
||||
func (*noRows) Close() error { return nil }
|
||||
func (*noRows) Err() error { return nil }
|
||||
func (r *noRows) Next() bool {
|
||||
if !r.done {
|
||||
r.done = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (r *noRows) Columns() ([]string, error) { return r.cols, nil }
|
||||
func (*noRows) Scan(...any) error { return nil }
|
||||
|
||||
type nopDriver struct {
|
||||
dialect.Driver
|
||||
dialect string
|
||||
}
|
||||
|
||||
func (d nopDriver) Dialect() string { return d.dialect }
|
||||
|
||||
func (nopDriver) Query(context.Context, string, any, any) error {
|
||||
return nil
|
||||
}
|
||||
189
dialect/sql/schema/writer_test.go
Normal file
189
dialect/sql/schema/writer_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqljson"
|
||||
|
||||
"ariga.io/atlas/sql/migrate"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteDriver(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
w := NewWriteDriver(dialect.MySQL, b)
|
||||
ctx := context.Background()
|
||||
tx, err := w.Tx(ctx)
|
||||
require.NoError(t, err)
|
||||
err = tx.Query(ctx, "SELECT `name` FROM `users`", nil, nil)
|
||||
require.EqualError(t, err, "query is not supported by the WriteDriver")
|
||||
err = tx.Exec(ctx, "ALTER TABLE `users` ADD COLUMN `age` int", nil, nil)
|
||||
require.NoError(t, err)
|
||||
err = tx.Exec(ctx, "ALTER TABLE `users` ADD COLUMN `NAME` varchar(100);", nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, tx.Commit())
|
||||
lines := strings.Split(b.String(), "\n")
|
||||
require.Len(t, lines, 3)
|
||||
require.Equal(t, "ALTER TABLE `users` ADD COLUMN `age` int;", lines[0])
|
||||
require.Equal(t, "ALTER TABLE `users` ADD COLUMN `NAME` varchar(100);", lines[1])
|
||||
require.Empty(t, lines[2], "file ends with blank line")
|
||||
|
||||
b.Reset()
|
||||
query, args := sql.Update("users").Schema("test").Set("a", 1).Set("b", "a").Set("c", "'c'").Set("d", true).Where(sql.EQ("p", 0.2)).Query()
|
||||
err = w.Exec(ctx, query, args, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UPDATE `test`.`users` SET `a` = 1, `b` = 'a', `c` = '''c''', `d` = 1 WHERE `p` = 0.2;\n", b.String())
|
||||
|
||||
b.Reset()
|
||||
query, args = sql.Dialect(dialect.MySQL).Update("users").Schema("test").Set("a", "{}").Where(sqljson.ValueIsNull("a")).Query()
|
||||
err = w.Exec(ctx, query, args, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "UPDATE `test`.`users` SET `a` = '{}' WHERE JSON_CONTAINS(`a`, 'null', '$');\n", b.String())
|
||||
|
||||
b.Reset()
|
||||
w = NewWriteDriver(dialect.Postgres, b)
|
||||
query, args = sql.Dialect(dialect.Postgres).Update("users").Set("id", uuid.Nil).Set("a", 1).Set("b", time.Now()).Query()
|
||||
err = w.Exec(ctx, query, args, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `UPDATE "users" SET "id" = '00000000-0000-0000-0000-000000000000', "a" = 1, "b" = {{ TIME_VALUE }};`+"\n", b.String())
|
||||
|
||||
b.Reset()
|
||||
err = w.Exec(ctx, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id`, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id;`+"\n", b.String())
|
||||
|
||||
// batchCreator uses tx.Query when doing an insert
|
||||
b.Reset()
|
||||
err = w.Query(ctx, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id`, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `INSERT INTO "users" (name) VALUES("a8m") RETURNING id;`+"\n", b.String())
|
||||
|
||||
// correct columns are extracted from a returning clause and returned by sql.ColumnScanner.
|
||||
for q, cols := range map[string][]string{
|
||||
`INSERT INTO "users" (name) VALUES("a8m") RETURNING id`: {"id"},
|
||||
`INSERT INTO "users" (name) VALUES("a8m") RETURNING id, "name"`: {"id", `"name"`},
|
||||
`INSERT INTO "users" (name) VALUES("a8m") RETURNING "id", "name"`: {`"id"`, `"name"`},
|
||||
`INSERT INTO "users" (name) VALUES("a8m") RETURNING "id", "name"; DROP "groups"`: {`"id"`, `"name"`},
|
||||
} {
|
||||
var rows sql.Rows
|
||||
err = w.Query(ctx, q, nil, &rows)
|
||||
require.NoError(t, err)
|
||||
require.True(t, rows.Next())
|
||||
c, err := rows.Columns()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cols, c)
|
||||
require.NoError(t, rows.Scan())
|
||||
}
|
||||
b.Reset()
|
||||
}
|
||||
|
||||
func TestDirWriter(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
dialect string
|
||||
exec []string
|
||||
comments []string
|
||||
args [][]any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
dialect.MySQL,
|
||||
[]string{
|
||||
"UPDATE `test`.`users` SET `a` = ?",
|
||||
"UPDATE `test`.`users` SET `b` = ?",
|
||||
},
|
||||
[]string{
|
||||
"Comment 1.",
|
||||
"Comment 2.",
|
||||
},
|
||||
[][]any{
|
||||
{1},
|
||||
{2},
|
||||
},
|
||||
"-- Comment 1.\nUPDATE `test`.`users` SET `a` = 1;\n-- Comment 2.\nUPDATE `test`.`users` SET `b` = 2;\n",
|
||||
},
|
||||
{
|
||||
dialect.Postgres,
|
||||
[]string{
|
||||
"INSERT INTO \"users\" (\"name\", \"email\") VALUES ($1, $2) RETURNING \"id\"",
|
||||
"INSERT INTO \"groups\" (\"name\") VALUES ($1) RETURNING \"id\"",
|
||||
},
|
||||
[]string{
|
||||
"Seed users table",
|
||||
"Seed groups table",
|
||||
},
|
||||
[][]any{
|
||||
{"masseelch", "j@ariga.io"},
|
||||
{"admins"},
|
||||
},
|
||||
strings.Join([]string{
|
||||
"-- Seed users table\nINSERT INTO \"users\" (\"name\", \"email\") VALUES ('masseelch', 'j@ariga.io');\n",
|
||||
"-- Seed groups table\nINSERT INTO \"groups\" (\"name\") VALUES ('admins');\n",
|
||||
}, ""),
|
||||
},
|
||||
{
|
||||
dialect.SQLite,
|
||||
[]string{
|
||||
"INSERT INTO `users` (`name`, `email`) VALUES (?, ?) RETURNING `id`",
|
||||
"INSERT INTO `groups` (`name`) VALUES (?) RETURNING `id`",
|
||||
},
|
||||
[]string{
|
||||
"Seed users table",
|
||||
"Seed groups table",
|
||||
},
|
||||
[][]any{
|
||||
{"masseelch", "j@ariga.io"},
|
||||
{"admins"},
|
||||
},
|
||||
strings.Join([]string{
|
||||
"-- Seed users table\nINSERT INTO `users` (`name`, `email`) VALUES ('masseelch', 'j@ariga.io');\n",
|
||||
"-- Seed groups table\nINSERT INTO `groups` (`name`) VALUES ('admins');\n",
|
||||
}, ""),
|
||||
},
|
||||
{
|
||||
dialect.SQLite + " no space",
|
||||
[]string{"INSERT INTO `users` (`name`) VALUES (?)RETURNING `id`"},
|
||||
[]string{"Seed users table"},
|
||||
[][]any{{"masseelch"}},
|
||||
"-- Seed users table\nINSERT INTO `users` (`name`) VALUES ('masseelch');\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.dialect, func(t *testing.T) {
|
||||
var (
|
||||
p = t.TempDir()
|
||||
dir = func() migrate.Dir {
|
||||
d, err := migrate.NewLocalDir(p)
|
||||
require.NoError(t, err)
|
||||
return d
|
||||
}()
|
||||
w = &DirWriter{Dir: dir}
|
||||
drv = NewWriteDriver(tt.dialect, w)
|
||||
)
|
||||
for i := range tt.exec {
|
||||
require.NoError(t, drv.Exec(context.Background(), tt.exec[i], tt.args[i], nil))
|
||||
w.Change(tt.comments[i])
|
||||
}
|
||||
require.NoError(t, w.Flush("migration_file"))
|
||||
files, err := os.ReadDir(p)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, files, 2)
|
||||
require.Contains(t, files[0].Name(), "_migration_file.sql")
|
||||
buf, err := os.ReadFile(filepath.Join(p, files[0].Name()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, string(buf))
|
||||
require.Equal(t, "atlas.sum", files[1].Name())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user