diff --git a/docs/docs/04-api/01-postgres/01-constructive.md b/docs/docs/04-api/01-postgres/01-constructive.md index b62cb89..8fc66ad 100644 --- a/docs/docs/04-api/01-postgres/01-constructive.md +++ b/docs/docs/04-api/01-postgres/01-constructive.md @@ -10,6 +10,8 @@ They are the operations that create, alter, or drop tables, columns, indexes, co - [AddColumnComment(tableName schema.TableName, columnName string, comment *string, opts ...schema.ColumnCommentOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddColumnComment) +- [AddTableComment(tableName schema.TableName, comment *string, opts ...schema.TableCommentOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddTableComment) + - [AddCheckConstraint(tableName schema.TableName, constraintName string, expression string, opts ...schema.CheckConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddCheckConstraint) - [AddExtension(name string, option ...schema.ExtensionOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddExtension) diff --git a/docs/docs/04-api/01-postgres/04-transformative.md b/docs/docs/04-api/01-postgres/04-transformative.md index 807793b..f859baa 100644 --- a/docs/docs/04-api/01-postgres/04-transformative.md +++ b/docs/docs/04-api/01-postgres/04-transformative.md @@ -7,3 +7,5 @@ They are the operations that change the data in the database. - [RenameTable(tableName schema.TableName, newTableName string)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.RenameTable) - [ChangeColumnType(tableName schema.TableName, columnName string, columnType schema.ColumnType, opts ...schema.ChangeColumnTypeOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.ChangeColumnType) + +- [ChangeColumnDefault(tableName schema.TableName, columnName string, defaultValue string, opts ...schema.ChangeColumnDefaultOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.ChangeColumnDefault) diff --git a/docs/docs/index.md b/docs/docs/index.md index a244f6f..ec04b27 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -3,7 +3,7 @@ sidebar_position: 1 --- [![Go Report Card](https://goreportcard.com/badge/github.com/alexisvisco/amigo)](https://goreportcard.com/report/github.com/alexisvisco/amigo) -[![GoDoc](https://pkg.go.dev/badge/alexisvisco/mig)](https://pkg.go.dev/alexisvisco/mig) +[![GoDoc](https://pkg.go.dev/badge/alexisvisco/amigo)](https://pkg.go.dev/alexisvisco/mig) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![GitHub release](https://img.shields.io/github/v/release/alexisvisco/amigo.svg)](https://github.com/alexisvisco/amigo/releases) [![Tests](https://github.com/alexisvisco/amigo/actions/workflows/tests.yml/badge.svg)](https://github.com/alexisvisco/amigo/actions/workflows/tests.yml) diff --git a/pkg/schema/migrator_context.go b/pkg/schema/migrator_context.go index 8a570c8..a791a96 100644 --- a/pkg/schema/migrator_context.go +++ b/pkg/schema/migrator_context.go @@ -30,10 +30,12 @@ type MigrationEvents struct { tablesCreated []TableOptions versionCreated []string - columnsRenamed []RenameColumnOptions - tablesRenamed []RenameTableOptions - changeColumnTypes []ChangeColumnTypeOptions - columnComments []ColumnCommentOptions + columnsRenamed []RenameColumnOptions + tablesRenamed []RenameTableOptions + changeColumnTypes []ChangeColumnTypeOptions + columnComments []ColumnCommentOptions + tableComments []TableCommentOptions + changeColumnDefaults []ChangeColumnDefaultOptions extensionsDropped []DropExtensionOptions tablesDropped []DropTableOptions @@ -183,3 +185,13 @@ func (m *MigratorContext) AddChangeColumnType(options ChangeColumnTypeOptions) { m.MigrationEvents.changeColumnTypes = append(m.MigrationEvents.changeColumnTypes, options) logger.Info(options) } + +func (m *MigratorContext) AddChangeColumnDefault(options ChangeColumnDefaultOptions) { + m.MigrationEvents.changeColumnDefaults = append(m.MigrationEvents.changeColumnDefaults, options) + logger.Info(options) +} + +func (m *MigratorContext) AddTableComment(options TableCommentOptions) { + m.MigrationEvents.tableComments = append(m.MigrationEvents.tableComments, options) + logger.Info(options) +} diff --git a/pkg/schema/options.go b/pkg/schema/options.go index 1734dcf..9a33578 100644 --- a/pkg/schema/options.go +++ b/pkg/schema/options.go @@ -468,7 +468,7 @@ func (c ColumnCommentOptions) EventName() string { } func (c ColumnCommentOptions) String() string { - cmt := "nil" + cmt := "NULL" if c.Comment != nil { cmt = fmt.Sprintf("%q", *c.Comment) } @@ -580,6 +580,41 @@ func (c *ChangeColumnTypeOptions) IsArray() bool { return c.Array } +type ChangeColumnDefaultOptions struct { + Table TableName + ColumnName string + Value string + + Reversible *ChangeColumnDefaultOptions +} + +func (c *ChangeColumnDefaultOptions) EventName() string { + return "ChangeColumnDefaultEvent" +} + +func (c *ChangeColumnDefaultOptions) String() string { + return fmt.Sprintf("-- change_column_default(table: %s, column: %s, value: %s)", c.Table, c.ColumnName, c.Value) +} + +type TableCommentOptions struct { + Table TableName + Comment *string + + Reversible *TableCommentOptions +} + +func (t TableCommentOptions) EventName() string { + return "TableCommentEvent" +} + +func (t TableCommentOptions) String() string { + cmt := "NULL" + if t.Comment != nil { + cmt = fmt.Sprintf("%q", *t.Comment) + } + return fmt.Sprintf("-- comment_table(table: %s, comment: %s)", t.Table, cmt) +} + type PrimaryKeyConstraintOptions struct { Table TableName @@ -661,6 +696,8 @@ type TableOptions struct { // Option is at the end of the table creation. Option string + Comment *string + // TableDefinition is the definition of the table. Usually a struct that implements TableDef will allow you to // define the columns and other options. TableDefinition TableDef diff --git a/pkg/schema/pg/postgres_column.go b/pkg/schema/pg/postgres_column.go index a68c807..955491a 100644 --- a/pkg/schema/pg/postgres_column.go +++ b/pkg/schema/pg/postgres_column.go @@ -194,7 +194,7 @@ func (p *Schema) AddTimestamps(tableName schema.TableName) { // // Example: // -// p.AddColumnComment("users", "name", schema.utils.Ptr("The name of the User")) +// p.AddColumnComment("users", "name", utils.Ptr("The name of the User")) // // Generates: // @@ -397,6 +397,52 @@ func (p *Schema) ChangeColumnType(tableName schema.TableName, columnName string, p.Context.AddChangeColumnType(options) } +// ChangeColumnDefault changes the default value of a column. +// +// Example: +// +// p.ChangeColumnDefault("users", "status", "'draft'") +// +// Generates: +// +// ALTER TABLE "users" ALTER COLUMN "status" SET DEFAULT 'draft' +func (p *Schema) ChangeColumnDefault(tableName schema.TableName, columnName, defaultValue string, opts ...schema.ChangeColumnDefaultOptions) { + options := schema.ChangeColumnDefaultOptions{} + if len(opts) > 0 { + options = opts[0] + } + + options.Table = tableName + options.ColumnName = columnName + options.Value = defaultValue + + if p.Context.MigrationDirection == types.MigrationDirectionDown { + if options.Reversible != nil { + p.rollbackMode().ChangeColumnDefault(tableName, columnName, options.Reversible.Value, *options.Reversible) + } else { + logger.Warn(events.MessageEvent{ + Message: fmt.Sprintf("unable to recreate the column %s.%s", tableName, columnName), + }) + } + return + } + + query := "ALTER TABLE {table_name} ALTER COLUMN {column_name} SET DEFAULT {default}" + replacer := utils.Replacer{ + "table_name": utils.StrFunc(options.Table.String()), + "column_name": utils.StrFunc(options.ColumnName), + "default": utils.StrFunc(options.Value), + } + + _, err := p.DB.ExecContext(p.Context.Context, replacer.Replace(query)) + if err != nil { + p.Context.RaiseError(fmt.Errorf("error while changing column default: %w", err)) + return + } + + p.Context.AddChangeColumnDefault(options) +} + func (p *Schema) toType(c schema.ColumnType, co schema.ColumnData) string { p.modifyColumnOptionFromType(c, co) diff --git a/pkg/schema/pg/postgres_column_test.go b/pkg/schema/pg/postgres_column_test.go index eca3a68..9d2f550 100644 --- a/pkg/schema/pg/postgres_column_test.go +++ b/pkg/schema/pg/postgres_column_test.go @@ -220,6 +220,32 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() } +func TestPostgres_ChangeColumnDefault(t *testing.T) { + t.Parallel() + + sc := "tst_pg_add_column_with_default" + + base := `create table {schema}.articles(id integer);` + + t.Run("simple change", func(t *testing.T) { + t.Parallel() + p, r, sc := baseTest(t, base, sc, 0) + + p.ChangeColumnDefault(schema.Table("articles", sc), "id", "4") + + testutils.AssertSnapshotDiff(t, r.FormatRecords(), true) + }) + + t.Run("null change", func(t *testing.T) { + t.Parallel() + p, r, sc := baseTest(t, base, sc, 1) + + p.ChangeColumnDefault(schema.Table("articles", sc), "id", "null") + + testutils.AssertSnapshotDiff(t, r.FormatRecords(), true) + }) +} + func TestPostgres_DropColumn(t *testing.T) { t.Parallel() diff --git a/pkg/schema/pg/postgres_table.go b/pkg/schema/pg/postgres_table.go index 8026db8..96ec685 100644 --- a/pkg/schema/pg/postgres_table.go +++ b/pkg/schema/pg/postgres_table.go @@ -125,6 +125,10 @@ func (p *Schema) CreateTable(tableName schema.TableName, f func(*PostgresTableDe return } + if options.Comment != nil { + p.AddTableComment(tableName, options.Comment) + } + p.Context.AddTableCreated(options) for _, afterCreate := range td.deferCreationAction { @@ -470,3 +474,55 @@ func (p *Schema) RenameTable(oldTableName, newTableName schema.TableName) { p.Context.AddTableRenamed(schema.RenameTableOptions{OldTable: oldTableName, NewTable: newTableName}) } + +// AddTableComment adds a comment to a table in the database. +// +// Example: +// +// p.AddTableComment("users", utils.Ptr("This table contains the users")) +// +// Generates: +// +// COMMENT ON TABLE "users" IS 'This table contains the users' +// +// To remove a comment from a table: +// +// p.AddTableComment("users", nil) +// +// Generates: +// +// COMMENT ON TABLE "users" IS NULL +func (p *Schema) AddTableComment(tableName schema.TableName, comment *string, opts ...schema.TableCommentOptions) { + options := schema.TableCommentOptions{} + if len(opts) > 0 { + options = opts[0] + } + + options.Table = tableName + options.Comment = comment + + if p.Context.MigrationDirection == types.MigrationDirectionDown && options.Reversible != nil { + p.rollbackMode().AddTableComment(tableName, options.Reversible.Comment) + return + } + + sql := `COMMENT ON TABLE {table_name} IS {comment}` + + replacer := utils.Replacer{ + "table_name": utils.StrFunc(options.Table.String()), + "comment": func() string { + if options.Comment == nil { + return "NULL" + } + return fmt.Sprintf("'%s'", *options.Comment) + }, + } + + _, err := p.DB.ExecContext(p.Context.Context, replacer.Replace(sql)) + if err != nil { + p.Context.RaiseError(fmt.Errorf("error while adding column comment: %w", err)) + return + } + + p.Context.AddTableComment(options) +} diff --git a/pkg/schema/pg/postgres_table_test.go b/pkg/schema/pg/postgres_table_test.go index a097498..03be9d9 100644 --- a/pkg/schema/pg/postgres_table_test.go +++ b/pkg/schema/pg/postgres_table_test.go @@ -2,6 +2,7 @@ package pg import ( "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils" "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/stretchr/testify/require" "testing" @@ -118,6 +119,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.Serial("id") t.String("title") }, schema.TableOptions{ + Comment: utils.Ptr("This is a table without primary key"), WithoutPrimaryKey: true, }) @@ -126,7 +128,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.String("title") }) - testutils.AssertSnapshotDiff(t, r.FormatRecords(), true) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) assertTableExist(t, p, schema.Table("articles_without_id", sc)) }) @@ -176,6 +178,38 @@ func TestPostgres_DropTable(t *testing.T) { }) } +func TestPostgres_AddTableComment(t *testing.T) { + t.Parallel() + + sc := "tst_pg_add_table_comment" + + t.Run("add table comment", func(t *testing.T) { + t.Parallel() + p, r, sc := baseTest(t, "select 1;", sc, 0) + + p.CreateTable(schema.Table("articles", sc), func(t *PostgresTableDef) { + t.Serial("id") + }) + + p.AddTableComment(schema.Table("articles", sc), utils.Ptr("This is a table of articles")) + + testutils.AssertSnapshotDiff(t, r.FormatRecords()) + }) + + t.Run("add table comment null", func(t *testing.T) { + t.Parallel() + p, r, sc := baseTest(t, "select 1;", sc, 1) + + p.CreateTable(schema.Table("articles", sc), func(t *PostgresTableDef) { + t.Serial("id") + }) + + p.AddTableComment(schema.Table("articles", sc), nil) + + testutils.AssertSnapshotDiff(t, r.FormatRecords()) + }) +} + func TestPostgres_RenameTable(t *testing.T) { t.Parallel() diff --git a/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment.snap.txt b/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment.snap.txt new file mode 100644 index 0000000..49ebcc7 --- /dev/null +++ b/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment.snap.txt @@ -0,0 +1,4 @@ +CREATE TABLE tst_pg_add_table_comment_0.articles ( +"id" SERIAL NOT NULL PRIMARY KEY +) +COMMENT ON TABLE tst_pg_add_table_comment_0.articles IS 'This is a table of articles' diff --git a/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment_null.snap.txt b/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment_null.snap.txt new file mode 100644 index 0000000..3eb6e66 --- /dev/null +++ b/pkg/schema/pg/testdata/TestPostgres_AddTableComment/add_table_comment_null.snap.txt @@ -0,0 +1,4 @@ +CREATE TABLE tst_pg_add_table_comment_1.articles ( +"id" SERIAL NOT NULL PRIMARY KEY +) +COMMENT ON TABLE tst_pg_add_table_comment_1.articles IS NULL diff --git a/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/null_change.snap.txt b/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/null_change.snap.txt new file mode 100644 index 0000000..ab3f0b5 --- /dev/null +++ b/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/null_change.snap.txt @@ -0,0 +1 @@ +ALTER TABLE tst_pg_add_column_with_default_1.articles ALTER COLUMN id SET DEFAULT null diff --git a/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/simple_change.snap.txt b/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/simple_change.snap.txt new file mode 100644 index 0000000..57009c6 --- /dev/null +++ b/pkg/schema/pg/testdata/TestPostgres_ChangeColumnDefault/simple_change.snap.txt @@ -0,0 +1 @@ +ALTER TABLE tst_pg_add_column_with_default_0.articles ALTER COLUMN id SET DEFAULT 4 diff --git a/pkg/schema/pg/testdata/TestPostgres_CreateTable/without_primary_key.snap.txt b/pkg/schema/pg/testdata/TestPostgres_CreateTable/without_primary_key.snap.txt index 8e61b79..029fb84 100644 --- a/pkg/schema/pg/testdata/TestPostgres_CreateTable/without_primary_key.snap.txt +++ b/pkg/schema/pg/testdata/TestPostgres_CreateTable/without_primary_key.snap.txt @@ -2,6 +2,7 @@ CREATE TABLE tst_pg_create_table_5.articles ( "id" SERIAL, "title" TEXT ) +COMMENT ON TABLE tst_pg_create_table_5.articles IS 'This is a table without primary key' CREATE TABLE tst_pg_create_table_5.articles_without_id ( "title" TEXT ) diff --git a/readme.md b/readme.md index 4750503..0b14842 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![AMIGO Logo](docs/static/img/logo.svg)](https://amigo.alexisvis.co) [![Go Report Card](https://goreportcard.com/badge/github.com/alexisvisco/amigo)](https://goreportcard.com/report/github.com/alexisvisco/amigo) -[![GoDoc](https://pkg.go.dev/badge/alexisvisco/mig)](https://pkg.go.dev/alexisvisco/mig) +[![GoDoc](https://pkg.go.dev/badge/alexisvisco/amigo)](https://pkg.go.dev/alexisvisco/mig) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![GitHub release](https://img.shields.io/github/v/release/alexisvisco/amigo.svg)](https://github.com/alexisvisco/amigo/releases) [![Tests](https://github.com/alexisvisco/amigo/actions/workflows/tests.yml/badge.svg)](https://github.com/alexisvisco/amigo/actions/workflows/tests.yml)