diff --git a/docs/README.md b/docs/README.md index 2bea182..891af50 100644 --- a/docs/README.md +++ b/docs/README.md @@ -698,6 +698,7 @@ An add column operation creates a new column on an existing table. "column": { "name": "name of column", "type": "postgres type", + "comment": "postgres comment for the column", "nullable": true|false, "unique": true|false, "pk": true|false, @@ -911,6 +912,7 @@ where each `column` is defined as: { "name": "column name", "type": "postgres type", + "comment": "postgres comment for the column", "nullable": true|false, "unique": true|false, "pk": true|false, diff --git a/examples/12_create_employees_table.json b/examples/12_create_employees_table.json index 727ff7d..0fc773d 100644 --- a/examples/12_create_employees_table.json +++ b/examples/12_create_employees_table.json @@ -4,6 +4,7 @@ { "create_table": { "name": "employees", + "comment": "This is a comment for the employees table", "columns": [ { "name": "id", @@ -12,7 +13,8 @@ }, { "name": "role", - "type": "varchar(255)" + "type": "varchar(255)", + "comment": "This is a comment for the role column" } ] } diff --git a/examples/30_add_column_simple_up.json b/examples/30_add_column_simple_up.json index 0e438b7..ddf31f7 100644 --- a/examples/30_add_column_simple_up.json +++ b/examples/30_add_column_simple_up.json @@ -8,7 +8,8 @@ "column": { "name": "description", "type": "varchar(255)", - "nullable": false + "nullable": false, + "comment": "This is a comment for the description column" } } } diff --git a/pkg/migrations/comment.go b/pkg/migrations/comment.go new file mode 100644 index 0000000..a8d9f55 --- /dev/null +++ b/pkg/migrations/comment.go @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/lib/pq" +) + +func addCommentToColumn(ctx context.Context, conn *sql.DB, tableName, columnName, comment string) error { + _, err := conn.ExecContext(ctx, fmt.Sprintf(`COMMENT ON COLUMN %s.%s IS %s`, + pq.QuoteIdentifier(tableName), + pq.QuoteIdentifier(columnName), + pq.QuoteLiteral(comment))) + + return err +} + +func addCommentToTable(ctx context.Context, conn *sql.DB, tableName, comment string) error { + _, err := conn.ExecContext(ctx, fmt.Sprintf(`COMMENT ON TABLE %s IS %s`, + pq.QuoteIdentifier(tableName), + pq.QuoteLiteral(comment))) + + return err +} diff --git a/pkg/migrations/op_add_column.go b/pkg/migrations/op_add_column.go index ef2c866..72253ae 100644 --- a/pkg/migrations/op_add_column.go +++ b/pkg/migrations/op_add_column.go @@ -21,6 +21,12 @@ func (o *OpAddColumn) Start(ctx context.Context, conn *sql.DB, stateSchema strin return fmt.Errorf("failed to start add column operation: %w", err) } + if o.Column.Comment != nil { + if err := addCommentToColumn(ctx, conn, o.Table, TemporaryName(o.Column.Name), *o.Column.Comment); err != nil { + return fmt.Errorf("failed to add comment to column: %w", err) + } + } + if !o.Column.Nullable && o.Column.Default == nil { if err := addNotNullConstraint(ctx, conn, o.Table, o.Column.Name, TemporaryName(o.Column.Name)); err != nil { return fmt.Errorf("failed to add not null constraint: %w", err) diff --git a/pkg/migrations/op_add_column_test.go b/pkg/migrations/op_add_column_test.go index b179755..b9b0854 100644 --- a/pkg/migrations/op_add_column_test.go +++ b/pkg/migrations/op_add_column_test.go @@ -47,6 +47,7 @@ func TestAddColumn(t *testing.T) { Type: "integer", Nullable: false, Default: ptr("0"), + Comment: ptr("the age of the user"), }, }, }, @@ -751,3 +752,59 @@ func TestAddColumnWithCheckConstraint(t *testing.T) { }, }}) } + +func TestAddColumnWithComment(t *testing.T) { + t.Parallel() + + ExecuteTests(t, TestCases{{ + name: "add column", + migrations: []migrations.Migration{ + { + Name: "01_add_table", + Operations: migrations.Operations{ + &migrations.OpCreateTable{ + Name: "users", + Columns: []migrations.Column{ + { + Name: "id", + Type: "serial", + Pk: true, + }, + { + Name: "name", + Type: "varchar(255)", + Unique: true, + }, + }, + }, + }, + }, + { + Name: "02_add_column", + Operations: migrations.Operations{ + &migrations.OpAddColumn{ + Table: "users", + Column: migrations.Column{ + Name: "age", + Type: "integer", + Nullable: false, + Default: ptr("0"), + Comment: ptr("the age of the user"), + }, + }, + }, + }, + }, + afterStart: func(t *testing.T, db *sql.DB) { + // The comment has been added to the underlying column. + columnName := migrations.TemporaryName("age") + ColumnMustHaveComment(t, db, "public", "users", columnName, "the age of the user") + }, + afterRollback: func(t *testing.T, db *sql.DB) { + }, + afterComplete: func(t *testing.T, db *sql.DB) { + // The comment is still present on the underlying column. + ColumnMustHaveComment(t, db, "public", "users", "age", "the age of the user") + }, + }}) +} diff --git a/pkg/migrations/op_common_test.go b/pkg/migrations/op_common_test.go index 7e3d41d..164511b 100644 --- a/pkg/migrations/op_common_test.go +++ b/pkg/migrations/op_common_test.go @@ -146,6 +146,20 @@ func ColumnMustHaveType(t *testing.T, db *sql.DB, schema, table, column, expecte } } +func ColumnMustHaveComment(t *testing.T, db *sql.DB, schema, table, column, expectedComment string) { + t.Helper() + if !columnHasComment(t, db, schema, table, column, expectedComment) { + t.Fatalf("Expected column %q to have comment %q", column, expectedComment) + } +} + +func TableMustHaveComment(t *testing.T, db *sql.DB, schema, table, expectedComment string) { + t.Helper() + if !tableHasComment(t, db, schema, table, expectedComment) { + t.Fatalf("Expected table %q to have comment %q", table, expectedComment) + } +} + func TableMustHaveColumnCount(t *testing.T, db *sql.DB, schema, table string, n int) { t.Helper() if !tableMustHaveColumnCount(t, db, schema, table, n) { @@ -400,6 +414,40 @@ func columnHasType(t *testing.T, db *sql.DB, schema, table, column, expectedType return expectedType == actualType } +func columnHasComment(t *testing.T, db *sql.DB, schema, table, column, expectedComment string) bool { + t.Helper() + + var actualComment string + err := db.QueryRow(fmt.Sprintf(` + SELECT col_description( + %[1]s::regclass, + (SELECT attnum FROM pg_attribute WHERE attname=%[2]s and attrelid=%[1]s::regclass) + )`, + pq.QuoteLiteral(fmt.Sprintf("%s.%s", schema, table)), + pq.QuoteLiteral(column)), + ).Scan(&actualComment) + if err != nil { + t.Fatal(err) + } + + return expectedComment == actualComment +} + +func tableHasComment(t *testing.T, db *sql.DB, schema, table, expectedComment string) bool { + t.Helper() + + var actualComment string + err := db.QueryRow(fmt.Sprintf(` + SELECT obj_description(%[1]s::regclass, 'pg_class')`, + pq.QuoteLiteral(fmt.Sprintf("%s.%s", schema, table))), + ).Scan(&actualComment) + if err != nil { + t.Fatal(err) + } + + return expectedComment == actualComment +} + func MustInsert(t *testing.T, db *sql.DB, schema, version, table string, record map[string]string) { t.Helper() diff --git a/pkg/migrations/op_create_table.go b/pkg/migrations/op_create_table.go index d2b5561..8441b51 100644 --- a/pkg/migrations/op_create_table.go +++ b/pkg/migrations/op_create_table.go @@ -22,6 +22,22 @@ func (o *OpCreateTable) Start(ctx context.Context, conn *sql.DB, stateSchema str return err } + // Add comments to any columns that have them + for _, col := range o.Columns { + if col.Comment != nil { + if err := addCommentToColumn(ctx, conn, tempName, col.Name, *col.Comment); err != nil { + return fmt.Errorf("failed to add comment to column: %w", err) + } + } + } + + // Add comment to the table itself + if o.Comment != nil { + if err := addCommentToTable(ctx, conn, tempName, *o.Comment); err != nil { + return fmt.Errorf("failed to add comment to table: %w", err) + } + } + columns := make(map[string]schema.Column, len(o.Columns)) for _, col := range o.Columns { columns[col.Name] = schema.Column{ diff --git a/pkg/migrations/op_create_table_test.go b/pkg/migrations/op_create_table_test.go index ae7dc50..c336404 100644 --- a/pkg/migrations/op_create_table_test.go +++ b/pkg/migrations/op_create_table_test.go @@ -230,6 +230,48 @@ func TestCreateTable(t *testing.T) { }) }, }, + { + name: "create table with column and table comments", + migrations: []migrations.Migration{ + { + Name: "01_create_table", + Operations: migrations.Operations{ + &migrations.OpCreateTable{ + Name: "users", + Comment: ptr("the users table"), + Columns: []migrations.Column{ + { + Name: "id", + Type: "serial", + Pk: true, + }, + { + Name: "name", + Type: "varchar(255)", + Unique: true, + Comment: ptr("the username"), + }, + }, + }, + }, + }, + }, + afterStart: func(t *testing.T, db *sql.DB) { + tableName := migrations.TemporaryName("users") + // The comment has been added to the underlying table. + TableMustHaveComment(t, db, "public", tableName, "the users table") + // The comment has been added to the underlying column. + ColumnMustHaveComment(t, db, "public", tableName, "name", "the username") + }, + afterRollback: func(t *testing.T, db *sql.DB) { + }, + afterComplete: func(t *testing.T, db *sql.DB) { + // The comment is still present on the underlying table. + TableMustHaveComment(t, db, "public", "users", "the users table") + // The comment is still present on the underlying column. + ColumnMustHaveComment(t, db, "public", "users", "name", "the username") + }, + }, }) } diff --git a/pkg/migrations/types.go b/pkg/migrations/types.go index 4d4214e..8ed25fe 100644 --- a/pkg/migrations/types.go +++ b/pkg/migrations/types.go @@ -17,6 +17,9 @@ type Column struct { // Check constraint for the column Check *CheckConstraint `json:"check,omitempty"` + // Postgres comment for the column + Comment *string `json:"comment,omitempty"` + // Default value for the column Default *string `json:"default,omitempty"` @@ -113,6 +116,9 @@ type OpCreateTable struct { // Columns corresponds to the JSON schema field "columns". Columns []Column `json:"columns"` + // Postgres comment for the table + Comment *string `json:"comment,omitempty"` + // Name of the table Name string `json:"name"` } diff --git a/schema.json b/schema.json index ba3fa1b..f0a3bb5 100644 --- a/schema.json +++ b/schema.json @@ -56,6 +56,10 @@ "unique": { "description": "Indicates if the column values must be unique", "type": "boolean" + }, + "comment": { + "description": "Postgres comment for the column", + "type": "string" } }, "required": ["name", "nullable", "pk", "type", "unique"], @@ -186,6 +190,10 @@ "name": { "description": "Name of the table", "type": "string" + }, + "comment": { + "description": "Postgres comment for the table", + "type": "string" } }, "required": ["columns", "name"],