From b3a9697d069e694d82fc523fc9b199c1d5c987b8 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Fri, 8 Nov 2024 16:46:16 +0100 Subject: [PATCH] chore: add in-code documentation --- internal/dbtest/inspect_test.go | 10 ++++---- migrate/auto.go | 32 +++++++++++++++++++++++- migrate/diff.go | 10 +++++++- migrate/operations.go | 43 ++++++++++++++++++++++++++++++--- migrate/sqlschema/inspector.go | 31 ++++++++++++++---------- migrate/sqlschema/schema.go | 3 +++ 6 files changed, 106 insertions(+), 23 deletions(-) diff --git a/internal/dbtest/inspect_test.go b/internal/dbtest/inspect_test.go index f8f5fbdd7..f63f0b037 100644 --- a/internal/dbtest/inspect_test.go +++ b/internal/dbtest/inspect_test.go @@ -406,7 +406,7 @@ func formatType(c sqlschema.ColumnDefinition) string { return fmt.Sprintf("%s(%d)", c.SQLType, c.VarcharLen) } -func TestSchemaInspector_Inspect(t *testing.T) { +func TestBunModelInspector_Inspect(t *testing.T) { testEachDialect(t, func(t *testing.T, dialectName string, dialect schema.Dialect) { if _, ok := dialect.(sqlschema.InspectorDialect); !ok { t.Skip(dialectName + " is not sqlschema.InspectorDialect") @@ -420,7 +420,7 @@ func TestSchemaInspector_Inspect(t *testing.T) { tables := schema.NewTables(dialect) tables.Register((*Model)(nil)) - inspector := sqlschema.NewSchemaInspector(tables) + inspector := sqlschema.NewBunModelInspector(tables) want := map[string]sqlschema.ColumnDefinition{ "id": { @@ -453,7 +453,7 @@ func TestSchemaInspector_Inspect(t *testing.T) { tables := schema.NewTables(dialect) tables.Register((*Model)(nil)) - inspector := sqlschema.NewSchemaInspector(tables) + inspector := sqlschema.NewBunModelInspector(tables) want := map[string]sqlschema.ColumnDefinition{ "id": { @@ -488,7 +488,7 @@ func TestSchemaInspector_Inspect(t *testing.T) { tables := schema.NewTables(dialect) tables.Register((*Model)(nil)) - inspector := sqlschema.NewSchemaInspector(tables) + inspector := sqlschema.NewBunModelInspector(tables) want := sqlschema.TableDefinition{ Name: "models", @@ -517,7 +517,7 @@ func TestSchemaInspector_Inspect(t *testing.T) { tables := schema.NewTables(dialect) tables.Register((*Model)(nil)) - inspector := sqlschema.NewSchemaInspector(tables) + inspector := sqlschema.NewBunModelInspector(tables) want := sqlschema.NewColumns("id", "email") got, err := inspector.Inspect(context.Background()) diff --git a/migrate/auto.go b/migrate/auto.go index 10ab3f2a7..95b0da2fa 100644 --- a/migrate/auto.go +++ b/migrate/auto.go @@ -64,6 +64,36 @@ func WithMigrationsDirectoryAuto(directory string) AutoMigratorOption { } } +// AutoMigrator performs automated schema migrations. +// +// It is designed to be a drop-in replacement for some Migrator functionality and supports all existing +// configuration options. +// Similarly to Migrator, it has methods to create SQL migrations, write them to a file, and apply them. +// Unlike Migrator, it detects the differences between the state defined by bun models and the current +// database schema automatically. +// +// Usage: +// 1. Generate migrations and apply them au once with AutoMigrator.Migrate(). +// 2. Create up- and down-SQL migration files and apply migrations using Migrator.Migrate(). +// +// While both methods produce complete, reversible migrations (with entries in the database +// and SQL migration files), prefer creating migrations and applying them separately for +// any non-trivial cases to ensure AutoMigrator detects expected changes correctly. +// +// Limitations: +// - AutoMigrator only supports a subset of the possible ALTER TABLE modifications. +// - Some changes are not automatically reversible. For example, you would need to manually +// add a CREATE TABLE query to the .down migration file to revert a DROP TABLE migration. +// - Does not validate most dialect-specific constraints. For example, when changing column +// data type, make sure the data con be auto-casted to the new type. +// - Due to how the schema-state diff is calculated, it is not possible to rename a table and +// modify any of its columns' _data type_ in a single run. This will cause the AutoMigrator +// to drop and re-create the table under a different name; it is better to apply this change in 2 steps. +// Renaming a table and renaming its columns at the same time is possible. +// - Renaming table/column to an existing name, i.e. like this [A->B] [B->C], is not possible due to how +// AutoMigrator distinguishes "rename" and "unchanged" columns. +// +// Dialect must implement both sqlschema.Inspector and sqlschema.Migrator to be used with AutoMigrator. type AutoMigrator struct { db *bun.DB @@ -122,7 +152,7 @@ func NewAutoMigrator(db *bun.DB, opts ...AutoMigratorOption) (*AutoMigrator, err tables := schema.NewTables(db.Dialect()) tables.Register(am.includeModels...) - am.modelInspector = sqlschema.NewSchemaInspector(tables) + am.modelInspector = sqlschema.NewBunModelInspector(tables) return am, nil } diff --git a/migrate/diff.go b/migrate/diff.go index 0fd57a234..237a05000 100644 --- a/migrate/diff.go +++ b/migrate/diff.go @@ -58,7 +58,7 @@ RenameCreate: // If wantTable does not exist in the database and was not renamed // then we need to create this table in the database. - additional := wantTable.(sqlschema.ModelTable) + additional := wantTable.(sqlschema.BunTable) d.changes.Add(&CreateTableOp{ FQN: wantTable.GetFQN(), Model: additional.Model, @@ -379,6 +379,11 @@ func (s *signature) Equals(other signature) bool { return true } +// refMap is a utility for tracking superficial changes in foreign keys, +// which do not require any modificiation in the database. +// Modern SQL dialects automatically updated foreign key constraints whenever +// a column or a table is renamed. Detector can use refMap to ignore any +// differences in foreign keys which were caused by renamed column/table. type refMap map[*sqlschema.ForeignKey]string func newRefMap(fks map[sqlschema.ForeignKey]string) refMap { @@ -389,6 +394,7 @@ func newRefMap(fks map[sqlschema.ForeignKey]string) refMap { return rm } +// RenameT updates table name in all foreign key definions which depend on it. func (rm refMap) RenameTable(table schema.FQN, newName string) { for fk := range rm { switch table { @@ -400,6 +406,7 @@ func (rm refMap) RenameTable(table schema.FQN, newName string) { } } +// RenameColumn updates column name in all foreign key definions which depend on it. func (rm refMap) RenameColumn(table schema.FQN, column, newName string) { for fk := range rm { if table == fk.From.FQN { @@ -411,6 +418,7 @@ func (rm refMap) RenameColumn(table schema.FQN, column, newName string) { } } +// Deref returns copies of ForeignKey values to a map. func (rm refMap) Deref() map[sqlschema.ForeignKey]string { out := make(map[sqlschema.ForeignKey]string) for fk, name := range rm { diff --git a/migrate/operations.go b/migrate/operations.go index e9bf6383b..41f5bd6ef 100644 --- a/migrate/operations.go +++ b/migrate/operations.go @@ -9,11 +9,28 @@ import ( // Operation encapsulates the request to change a database definition // and knowns which operation can revert it. +// +// It is useful to define "monolith" Operations whenever possible, +// even though they a dialect may require several distinct steps to apply them. +// For example, changing a primary key involves first dropping the old constraint +// before generating the new one. Yet, this is only an implementation detail and +// passing a higher-level ChangePrimaryKeyOp will give the dialect more information +// about the applied change. +// +// Some operations might be irreversible due to technical limitations. Returning +// a *comment from GetReverse() will add an explanatory note to the generate migation file. +// +// To declare dependency on another Operation, operations should implement +// { DependsOn(Operation) bool } interface, which Changeset will use to resolve dependencies. type Operation interface { GetReverse() Operation } -// CreateTableOp +// CreateTableOp creates a new table in the schema. +// +// It does not report dependency on any other migration and may be executed first. +// Make sure the dialect does not include FOREIGN KEY constraints in the CREATE TABLE +// statement, as those may potentially reference not-yet-existing columns/tables. type CreateTableOp struct { FQN schema.FQN Model interface{} @@ -25,6 +42,7 @@ func (op *CreateTableOp) GetReverse() Operation { return &DropTableOp{FQN: op.FQN} } +// DropTableOp drops a database table. This operation is not reversible. type DropTableOp struct { FQN schema.FQN } @@ -43,6 +61,7 @@ func (op *DropTableOp) GetReverse() Operation { return &c } +// RenameTableOp renames the table. Note, that changing the "schema" part of the table's FQN is not allowed. type RenameTableOp struct { FQN schema.FQN NewName string @@ -57,7 +76,8 @@ func (op *RenameTableOp) GetReverse() Operation { } } -// RenameColumnOp. +// RenameColumnOp renames a column in the table. If the changeset includes a rename operation +// for the column's table, it should be executed first. type RenameColumnOp struct { FQN schema.FQN OldName string @@ -79,6 +99,7 @@ func (op *RenameColumnOp) DependsOn(another Operation) bool { return ok && op.FQN.Schema == rename.FQN.Schema && op.FQN.Table == rename.NewName } +// AddColumnOp adds a new column to the table. type AddColumnOp struct { FQN schema.FQN Column string @@ -95,6 +116,12 @@ func (op *AddColumnOp) GetReverse() Operation { } } +// DropColumnOp drop a column from the table. +// +// While some dialects allow DROP CASCADE to drop dependent constraints, +// explicit handling on constraints is preferred for transparency and debugging. +// DropColumnOp depends on DropForeignKeyOp, DropPrimaryKeyOp, and ChangePrimaryKeyOp +// if any of the constraints is defined on this table. type DropColumnOp struct { FQN schema.FQN Column string @@ -123,6 +150,7 @@ func (op *DropColumnOp) DependsOn(another Operation) bool { return false } +// AddForeignKey adds a new FOREIGN KEY constraint. type AddForeignKeyOp struct { ForeignKey sqlschema.ForeignKey ConstraintName string @@ -152,6 +180,7 @@ func (op *AddForeignKeyOp) GetReverse() Operation { } } +// DropForeignKeyOp drops a FOREIGN KEY constraint. type DropForeignKeyOp struct { ForeignKey sqlschema.ForeignKey ConstraintName string @@ -170,6 +199,7 @@ func (op *DropForeignKeyOp) GetReverse() Operation { } } +// AddUniqueConstraintOp adds new UNIQUE constraint to the table. type AddUniqueConstraintOp struct { FQN schema.FQN Unique sqlschema.Unique @@ -199,6 +229,7 @@ func (op *AddUniqueConstraintOp) DependsOn(another Operation) bool { } +// DropUniqueConstraintOp drops a UNIQUE constraint. type DropUniqueConstraintOp struct { FQN schema.FQN Unique sqlschema.Unique @@ -220,7 +251,10 @@ func (op *DropUniqueConstraintOp) GetReverse() Operation { } } -// Change column type. +// ChangeColumnTypeOp set a new data type for the column. +// The two types should be such that the data can be auto-casted from one to another. +// E.g. reducing VARCHAR lenght is not possible in most dialects. +// AutoMigrator does not enforce or validate these rules. type ChangeColumnTypeOp struct { FQN schema.FQN Column string @@ -239,6 +273,7 @@ func (op *ChangeColumnTypeOp) GetReverse() Operation { } } +// DropPrimaryKeyOp drops the table's PRIMARY KEY. type DropPrimaryKeyOp struct { FQN schema.FQN PrimaryKey sqlschema.PrimaryKey @@ -253,6 +288,7 @@ func (op *DropPrimaryKeyOp) GetReverse() Operation { } } +// AddPrimaryKeyOp adds a new PRIMARY KEY to the table. type AddPrimaryKeyOp struct { FQN schema.FQN PrimaryKey sqlschema.PrimaryKey @@ -275,6 +311,7 @@ func (op *AddPrimaryKeyOp) DependsOn(another Operation) bool { return false } +// ChangePrimaryKeyOp changes the PRIMARY KEY of the table. type ChangePrimaryKeyOp struct { FQN schema.FQN Old sqlschema.PrimaryKey diff --git a/migrate/sqlschema/inspector.go b/migrate/sqlschema/inspector.go index 616aea922..bb05c5b75 100644 --- a/migrate/sqlschema/inspector.go +++ b/migrate/sqlschema/inspector.go @@ -20,10 +20,12 @@ type InspectorDialect interface { EquivalentType(Column, Column) bool } +// Inspector reads schema state. type Inspector interface { Inspect(ctx context.Context) (Schema, error) } +// Schema is an abstract collection of database objects. type Schema interface { GetTables() []Table GetForeignKeys() map[ForeignKey]string @@ -48,10 +50,12 @@ type Column interface { GetIsIdentity() bool } +// inspector is opaque pointer to a databse inspector. type inspector struct { Inspector } +// NewInspector creates a new database inspector, if the dialect supports it. func NewInspector(db *bun.DB, excludeTables ...string) (Inspector, error) { dialect, ok := (db.Dialect()).(InspectorDialect) if !ok { @@ -62,24 +66,25 @@ func NewInspector(db *bun.DB, excludeTables ...string) (Inspector, error) { }, nil } -// SchemaInspector creates the current project state from the passed bun.Models. -// Do not recycle SchemaInspector for different sets of models, as older models will not be de-registerred before the next run. -type SchemaInspector struct { +// BunModelInspector creates the current project state from the passed bun.Models. +// Do not recycle BunModelInspector for different sets of models, as older models will not be de-registerred before the next run. +type BunModelInspector struct { tables *schema.Tables } -var _ Inspector = (*SchemaInspector)(nil) +var _ Inspector = (*BunModelInspector)(nil) -func NewSchemaInspector(tables *schema.Tables) *SchemaInspector { - return &SchemaInspector{ +func NewBunModelInspector(tables *schema.Tables) *BunModelInspector { + return &BunModelInspector{ tables: tables, } } +// BunModelSchema is the schema state derived from bun table models. type BunModelSchema struct { DatabaseSchema - ModelTables map[schema.FQN]ModelTable + ModelTables map[schema.FQN]BunTable } func (ms BunModelSchema) GetTables() []Table { @@ -90,22 +95,22 @@ func (ms BunModelSchema) GetTables() []Table { return tables } -// ModelTable provides additional table metadata that is only accessible from scanning Go models. -type ModelTable struct { +// BunTable provides additional table metadata that is only accessible from scanning bun models. +type BunTable struct { TableDefinition // Model stores the zero interface to the underlying Go struct. Model interface{} } -func (si *SchemaInspector) Inspect(ctx context.Context) (Schema, error) { +func (bmi *BunModelInspector) Inspect(ctx context.Context) (Schema, error) { state := BunModelSchema{ DatabaseSchema: DatabaseSchema{ ForeignKeys: make(map[ForeignKey]string), }, - ModelTables: make(map[schema.FQN]ModelTable), + ModelTables: make(map[schema.FQN]BunTable), } - for _, t := range si.tables.All() { + for _, t := range bmi.tables.All() { columns := make(map[string]ColumnDefinition) for _, f := range t.Fields { @@ -153,7 +158,7 @@ func (si *SchemaInspector) Inspect(ctx context.Context) (Schema, error) { } fqn := schema.FQN{Schema: t.Schema, Table: t.Name} - state.ModelTables[fqn] = ModelTable{ + state.ModelTables[fqn] = BunTable{ TableDefinition: TableDefinition{ Schema: t.Schema, Name: t.Name, diff --git a/migrate/sqlschema/schema.go b/migrate/sqlschema/schema.go index c833568ea..f9c22c5d0 100644 --- a/migrate/sqlschema/schema.go +++ b/migrate/sqlschema/schema.go @@ -8,6 +8,9 @@ import ( "github.com/uptrace/bun/schema" ) +// DatabaseSchema provides a default implementation of the Schema interface. +// Dialects which support schema inspection may return it directly from Inspect() +// or embed it in their custom schema structs. type DatabaseSchema struct { TableDefinitions map[schema.FQN]TableDefinition ForeignKeys map[ForeignKey]string