Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions dialect/sql/schema/integration/versioned_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,3 +479,136 @@ func TestCheck(t *testing.T) {
x.Empty(tableNames(ctx, t, db))
})
}

// TestExtra is the directory beside the ent one: what the schema cannot state,
// written by hand, applied after the ent directory and with a history of its
// own.
//
// The case it exists for is the second one. Sharing the ent directory's
// history, a file written on one branch before a plan made on another is out of
// order once that plan has run, however unrelated the two are. Kept apart, it
// runs.
func TestExtra(t *testing.T) {
ctx := t.Context()

dir, err := entschema.OpenDir(t.TempDir())
require.NoError(t, err)
m := entschema.Migrations{Dir: dir, Dialect: dialect.SQLite, Tables: testSchema()}

extraDir, err := entschema.OpenDir(t.TempDir())
require.NoError(t, err)
extra := m.Extra(extraDir)

db := open(t)
_, err = m.Plan(ctx, open(t), "init")
require.NoError(t, err)
_, err = m.Apply(ctx, db, dialect.SQLite)
require.NoError(t, err)

// A partial unique index the ent schema here does not declare, which is
// the kind of thing this directory is for and one SQLite can hold.
const index = "CREATE UNIQUE INDEX `holder_one_per_tenant` ON `holder` (`holder_tenant`) WHERE `holder_tenant` IS NOT NULL;\n"

versions := func(t *testing.T, table string) []string {
t.Helper()

rows, err := db.QueryContext(ctx, `SELECT version FROM `+table+` ORDER BY version`)
require.NoError(t, err)
defer rows.Close()

vs := []string{}
for rows.Next() {
var v string
require.NoError(t, rows.Scan(&v))
vs = append(vs, v)
}
require.NoError(t, rows.Err())

return vs
}

t.Run("a file written by hand is in the sum, so the directory opens", func(t *testing.T) {
x := require.New(t)

// Older than the file planned above: written before that plan, on a
// branch of its own.
f, err := extra.Write("20000101000000", "holder_one_per_tenant", []byte(index))
x.NoError(err)
x.Equal("20000101000000_holder_one_per_tenant.sql", f.Name())

_, err = entschema.OpenDir(extraDir.Path())
x.NoError(err)
})

t.Run("a version that is already there is refused", func(t *testing.T) {
x := require.New(t)

_, err := extra.Write("20000101000000", "again", []byte("SELECT 1;\n"))
x.ErrorContains(err, "cannot be ordered")
})

t.Run("it runs atop the ent schema, older than the plan though it is", func(t *testing.T) {
x := require.New(t)

fs, err := extra.Apply(ctx, db, dialect.SQLite)
x.NoError(err)
x.Len(fs, 1)

// Two histories, each holding only what it ran.
x.Len(versions(t, entschema.RevisionTable), 1)
x.Equal([]string{"20000101000000"}, versions(t, entschema.ExtraRevisionTable))

fs, err = m.Pending(ctx, db, dialect.SQLite)
x.NoError(err)
x.Empty(fs)
fs, err = extra.Pending(ctx, db, dialect.SQLite)
x.NoError(err)
x.Empty(fs)
})

t.Run("without Atop, a database that holds tables is refused", func(t *testing.T) {
x := require.New(t)

// Which is what keeps the ent directory from being applied over a
// database somebody built another way -- and why this one says Atop.
other := open(t)
_, err := m.Apply(ctx, other, dialect.SQLite)
x.NoError(err)

bare := extra
bare.Atop = false
_, err = bare.Apply(ctx, other, dialect.SQLite)
x.ErrorContains(err, "not clean")
})

t.Run("the next plan of the ent directory leaves what it made alone", func(t *testing.T) {
x := require.New(t)

nextSecond(t)

u := m
u.Tables = moved()
fs, err := u.Plan(ctx, open(t), "add_holder_email")
x.NoError(err)
x.Len(fs, 1)
x.NotContains(string(fs[0].Bytes()), "holder_one_per_tenant")
})

t.Run("and so does Check", func(t *testing.T) {
x := require.New(t)

x.NoError(entschema.Check(ctx, db, dialect.SQLite, testSchema()))
})

t.Run("there is nothing to plan it from", func(t *testing.T) {
x := require.New(t)

// Planned against no tables it would be a file that drops every one.
_, err := extra.Plan(ctx, open(t), "anything")
x.ErrorIs(err, entschema.ErrNoSchema)

fs, err := extraDir.Files()
x.NoError(err)
x.Len(fs, 1)
})
}
46 changes: 37 additions & 9 deletions dialect/sql/schema/revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"regexp"
"time"

"ariga.io/atlas/sql/migrate"
Expand All @@ -19,8 +20,21 @@ import (
// RevisionTable is where a database records which migrations were applied to
// it. It is not part of the ent schema; it belongs to the database, not to the
// app.
//
// It is the ent directory's. A directory of its own beside it keeps its history
// in another table -- see [Migrations.Revisions] -- and [ExtraRevisionTable] is
// the one [Migrations.Extra] names.
const RevisionTable = "schema_revisions"

// ExtraRevisionTable is where the directory beside the ent one keeps its
// history; see [Migrations.Extra].
const ExtraRevisionTable = "schema_revisions_extra"

// tableName is what a revision table may be called. It is written into SQL as
// it is, so it is held to the one shape every database reads the same way
// unquoted.
var tableName = regexp.MustCompile(`^[a-z_][a-z0-9_]*$`)

// revisionColumns is the order the columns are read and written in.
var revisionColumns = []string{
"version",
Expand All @@ -40,7 +54,8 @@ var revisionColumns = []string{
// revisionSchema holds no type that is spelled differently by the databases
// this package migrates, so it is the same statement everywhere. Times are text
// in RFC 3339 rather than a timestamp, which every database stores its own way.
const revisionSchema = `CREATE TABLE IF NOT EXISTS ` + RevisionTable + ` (
func revisionSchema(table string) string {
return `CREATE TABLE IF NOT EXISTS ` + table + ` (
version TEXT NOT NULL PRIMARY KEY,
description TEXT NOT NULL,
type BIGINT NOT NULL,
Expand All @@ -54,6 +69,7 @@ const revisionSchema = `CREATE TABLE IF NOT EXISTS ` + RevisionTable + ` (
partial_hashes TEXT NOT NULL,
operator_version TEXT NOT NULL
)`
}

var _ migrate.RevisionReadWriter = (*Revisions)(nil)

Expand All @@ -63,6 +79,10 @@ type Revisions struct {
db *sql.DB
dialect string

// table is where the history is kept: [RevisionTable] unless a directory
// said otherwise.
table string

// schema is where the table ended up, for the databases that have more
// than one place to put it.
schema string
Expand All @@ -71,7 +91,15 @@ type Revisions struct {
// NewRevisions reads the history of `db`, creating it if this is the first
// time the database is migrated.
func NewRevisions(ctx context.Context, db *sql.DB, d string) (*Revisions, error) {
v := &Revisions{db: db, dialect: d}
return newRevisions(ctx, db, d, RevisionTable)
}

func newRevisions(ctx context.Context, db *sql.DB, d string, table string) (*Revisions, error) {
if !tableName.MatchString(table) {
return nil, fmt.Errorf("%q cannot name a revision table: it is written into SQL unquoted, so it is lowercase letters, digits and underscores", table)
}

v := &Revisions{db: db, dialect: d, table: table}

if d == dialect.Postgres {
// The table is created wherever the search path points. Atlas has to
Expand All @@ -86,21 +114,21 @@ func NewRevisions(ctx context.Context, db *sql.DB, d string) (*Revisions, error)
v.schema = s.String
}

if _, err := db.ExecContext(ctx, revisionSchema); err != nil {
return nil, fmt.Errorf("create %s: %w", RevisionTable, err)
if _, err := db.ExecContext(ctx, revisionSchema(table)); err != nil {
return nil, fmt.Errorf("create %s: %w", table, err)
}

return v, nil
}

func (r *Revisions) Ident() *migrate.TableIdent {
return &migrate.TableIdent{Name: RevisionTable, Schema: r.schema}
return &migrate.TableIdent{Name: r.table, Schema: r.schema}
}

func (r *Revisions) ReadRevisions(ctx context.Context) ([]*migrate.Revision, error) {
q, args := entsql.Dialect(r.dialect).
Select(revisionColumns...).
From(entsql.Table(RevisionTable)).
From(entsql.Table(r.table)).
OrderBy(entsql.Asc("version")).
Query()

Expand Down Expand Up @@ -129,7 +157,7 @@ func (r *Revisions) ReadRevisions(ctx context.Context) ([]*migrate.Revision, err
func (r *Revisions) ReadRevision(ctx context.Context, version string) (*migrate.Revision, error) {
q, args := entsql.Dialect(r.dialect).
Select(revisionColumns...).
From(entsql.Table(RevisionTable)).
From(entsql.Table(r.table)).
Where(entsql.EQ("version", version)).
Query()

Expand Down Expand Up @@ -157,7 +185,7 @@ func (r *Revisions) WriteRevision(ctx context.Context, v *migrate.Revision) erro
}

q, args := entsql.Dialect(r.dialect).
Insert(RevisionTable).
Insert(r.table).
Columns(revisionColumns...).
Values(
v.Version,
Expand Down Expand Up @@ -190,7 +218,7 @@ func (r *Revisions) WriteRevision(ctx context.Context, v *migrate.Revision) erro

func (r *Revisions) DeleteRevision(ctx context.Context, version string) error {
q, args := entsql.Dialect(r.dialect).
Delete(RevisionTable).
Delete(r.table).
Where(entsql.EQ("version", version)).
Query()

Expand Down
Loading
Loading