Skip to content

Commit 4156bc6

Browse files
Multi-operation migration support for add_column operations (#590)
Ensure that `add_column` operations can be used as part of multi-operation migrations: Add testcases for: * **create table, add column** * **rename table, add column** * **add column, add column validation** Theses changes help ensure that `add_column` operations can be used as part of multi-operation migrations. Part of #239
1 parent 5ec28da commit 4156bc6

File tree

4 files changed

+192
-31
lines changed

4 files changed

+192
-31
lines changed

internal/testutils/error_codes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ const (
88
NotNullViolationErrorCode string = "not_null_violation"
99
UniqueViolationErrorCode string = "unique_violation"
1010
UndefinedColumnErrorCode string = "undefined_column"
11+
UndefinedTableErrorCode string = "undefined_table"
1112
)

pkg/migrations/op_add_column.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ func (o *OpAddColumn) Start(ctx context.Context, conn db.DB, latestSchema string
4646
err := createTrigger(ctx, conn, tr, triggerConfig{
4747
Name: TriggerName(o.Table, o.Column.Name),
4848
Direction: TriggerDirectionUp,
49-
Columns: s.GetTable(o.Table).Columns,
49+
Columns: table.Columns,
5050
SchemaName: s.Name,
5151
LatestSchema: latestSchema,
52-
TableName: o.Table,
52+
TableName: table.Name,
5353
PhysicalColumn: TemporaryName(o.Column.Name),
5454
SQL: o.Up,
5555
})
@@ -120,10 +120,11 @@ func (o *OpAddColumn) Complete(ctx context.Context, conn db.DB, tr SQLTransforme
120120
}
121121

122122
func (o *OpAddColumn) Rollback(ctx context.Context, conn db.DB, tr SQLTransformer, s *schema.Schema) error {
123+
table := s.GetTable(o.Table)
123124
tempName := TemporaryName(o.Column.Name)
124125

125126
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE IF EXISTS %s DROP COLUMN IF EXISTS %s",
126-
pq.QuoteIdentifier(o.Table),
127+
pq.QuoteIdentifier(table.Name),
127128
pq.QuoteIdentifier(tempName)))
128129
if err != nil {
129130
return err

pkg/migrations/op_add_column_test.go

Lines changed: 174 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,18 +1658,18 @@ func TestAddColumnDefaultTransformation(t *testing.T) {
16581658
}, roll.WithSQLTransformer(sqlTransformer))
16591659
}
16601660

1661-
func TestAddColumnToATableCreatedInTheSameMigration(t *testing.T) {
1661+
func TestAddColumnInMultiOperationMigrations(t *testing.T) {
16621662
t.Parallel()
16631663

16641664
ExecuteTests(t, TestCases{
16651665
{
1666-
name: "add column to newly created table",
1666+
name: "create table, add column",
16671667
migrations: []migrations.Migration{
16681668
{
1669-
Name: "01_add_table",
1669+
Name: "01_multi_operation",
16701670
Operations: migrations.Operations{
16711671
&migrations.OpCreateTable{
1672-
Name: "users",
1672+
Name: "items",
16731673
Columns: []migrations.Column{
16741674
{
16751675
Name: "id",
@@ -1683,46 +1683,193 @@ func TestAddColumnToATableCreatedInTheSameMigration(t *testing.T) {
16831683
},
16841684
},
16851685
&migrations.OpAddColumn{
1686-
Table: "users",
1686+
Table: "items",
16871687
Column: migrations.Column{
1688-
Name: "age",
1689-
Type: "integer",
1690-
Nullable: false,
1691-
Default: ptr("18"),
1692-
Check: &migrations.CheckConstraint{
1693-
Name: "age_check",
1694-
Constraint: "age >= 18",
1688+
Name: "description",
1689+
Type: "text",
1690+
},
1691+
Up: "UPPER(name)",
1692+
},
1693+
},
1694+
},
1695+
},
1696+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
1697+
// Can insert into the view in the new schema (the only version schema)
1698+
MustInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
1699+
"name": "apples",
1700+
"description": "green",
1701+
})
1702+
1703+
// The table has the expected rows
1704+
rows := MustSelect(t, db, schema, "01_multi_operation", "items")
1705+
assert.Equal(t, []map[string]any{
1706+
{"id": 1, "name": "apples", "description": "green"},
1707+
}, rows)
1708+
},
1709+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
1710+
// The table no longer exists
1711+
TableMustNotExist(t, db, schema, "items")
1712+
},
1713+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
1714+
// Can insert into the view in the new schema (the only version schema)
1715+
MustInsert(t, db, schema, "01_multi_operation", "items", map[string]string{
1716+
"name": "bananas",
1717+
"description": "yellow",
1718+
})
1719+
1720+
// The table has the expected rows
1721+
rows := MustSelect(t, db, schema, "01_multi_operation", "items")
1722+
assert.Equal(t, []map[string]any{
1723+
{"id": 1, "name": "bananas", "description": "yellow"},
1724+
}, rows)
1725+
},
1726+
},
1727+
{
1728+
name: "rename table, add column",
1729+
migrations: []migrations.Migration{
1730+
{
1731+
Name: "01_create_table",
1732+
Operations: migrations.Operations{
1733+
&migrations.OpCreateTable{
1734+
Name: "items",
1735+
Columns: []migrations.Column{
1736+
{
1737+
Name: "id",
1738+
Type: "serial",
1739+
Pk: true,
1740+
},
1741+
{
1742+
Name: "name",
1743+
Type: "varchar(255)",
16951744
},
1696-
Comment: ptr("the age of the user"),
16971745
},
16981746
},
16991747
},
17001748
},
1749+
{
1750+
Name: "02_multi_operation",
1751+
Operations: migrations.Operations{
1752+
&migrations.OpRenameTable{
1753+
From: "items",
1754+
To: "products",
1755+
},
1756+
&migrations.OpAddColumn{
1757+
Table: "products",
1758+
Column: migrations.Column{
1759+
Name: "description",
1760+
Type: "text",
1761+
},
1762+
Up: "UPPER(name)",
1763+
},
1764+
},
1765+
},
17011766
},
17021767
afterStart: func(t *testing.T, db *sql.DB, schema string) {
1703-
// Inserting into the new column on the new table works.
1704-
MustInsert(t, db, schema, "01_add_table", "users", map[string]string{
1705-
"name": "Alice", "age": "30",
1768+
// Can insert into the new table in the new schema using its new name
1769+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
1770+
"name": "apples",
1771+
"description": "green",
17061772
})
17071773

1708-
// Inserting a value that doesn't meet the check constraint fails.
1709-
MustNotInsert(t, db, schema, "01_add_table", "users", map[string]string{
1710-
"name": "Bob", "age": "8",
1711-
}, testutils.CheckViolationErrorCode)
1774+
// Can't insert into the new table in the new schema using its old name
1775+
MustNotInsert(t, db, schema, "02_multi_operation", "items", map[string]string{
1776+
"name": "bananas",
1777+
"description": "yellow",
1778+
}, testutils.UndefinedTableErrorCode)
1779+
1780+
// The table has the expected rows in the old schema
1781+
rows := MustSelect(t, db, schema, "01_create_table", "items")
1782+
assert.Equal(t, []map[string]any{
1783+
{"id": 1, "name": "apples"},
1784+
}, rows)
1785+
1786+
// The table has the expected rows in the new schema
1787+
rows = MustSelect(t, db, schema, "02_multi_operation", "products")
1788+
assert.Equal(t, []map[string]any{
1789+
{"id": 1, "name": "apples", "description": "green"},
1790+
}, rows)
17121791
},
17131792
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
1793+
// Can insert into the old table in the old schema using its old name
1794+
MustInsert(t, db, schema, "01_create_table", "items", map[string]string{
1795+
"name": "bananas",
1796+
})
1797+
1798+
// The temporary column, functions and triggers have been cleaned up
1799+
TableMustBeCleanedUp(t, db, schema, "items", "description")
17141800
},
17151801
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
1716-
// Inserting into the new column on the new table works.
1717-
MustInsert(t, db, schema, "01_add_table", "users", map[string]string{
1718-
"name": "Bob", "age": "31",
1802+
// Can insert into the new table in the new schema using its new name
1803+
MustInsert(t, db, schema, "02_multi_operation", "products", map[string]string{
1804+
"name": "carrots",
1805+
"description": "crunchy",
17191806
})
17201807

1721-
// Inserting a value that doesn't meet the check constraint fails.
1722-
MustNotInsert(t, db, schema, "01_add_table", "users", map[string]string{
1723-
"name": "Carl", "age": "8",
1724-
}, testutils.CheckViolationErrorCode)
1808+
// The table has the new name in the new schema and has the expected
1809+
// rows
1810+
rows := MustSelect(t, db, schema, "02_multi_operation", "products")
1811+
assert.Equal(t, []map[string]any{
1812+
{"id": 1, "name": "apples", "description": "APPLES"},
1813+
{"id": 2, "name": "bananas", "description": "BANANAS"},
1814+
{"id": 3, "name": "carrots", "description": "crunchy"},
1815+
}, rows)
1816+
1817+
// The temporary column, functions and triggers have been cleaned up
1818+
TableMustBeCleanedUp(t, db, schema, "products", "description")
1819+
},
1820+
},
1821+
})
1822+
}
1823+
1824+
func TestAddColumnValidationInMultiOperationMigrations(t *testing.T) {
1825+
t.Parallel()
1826+
1827+
ExecuteTests(t, TestCases{
1828+
{
1829+
name: "adding a column with the same name twice fails to validate",
1830+
migrations: []migrations.Migration{
1831+
{
1832+
Name: "01_create_table",
1833+
Operations: migrations.Operations{
1834+
&migrations.OpCreateTable{
1835+
Name: "items",
1836+
Columns: []migrations.Column{
1837+
{
1838+
Name: "id",
1839+
Type: "serial",
1840+
Pk: true,
1841+
},
1842+
{
1843+
Name: "name",
1844+
Type: "varchar(255)",
1845+
},
1846+
},
1847+
},
1848+
},
1849+
},
1850+
{
1851+
Name: "02_multi_operation",
1852+
Operations: migrations.Operations{
1853+
&migrations.OpAddColumn{
1854+
Table: "items",
1855+
Column: migrations.Column{
1856+
Name: "description",
1857+
Type: "text",
1858+
},
1859+
Up: "UPPER(name)",
1860+
},
1861+
&migrations.OpAddColumn{
1862+
Table: "items",
1863+
Column: migrations.Column{
1864+
Name: "description",
1865+
Type: "varchar(255)",
1866+
},
1867+
Up: "UPPER(name)",
1868+
},
1869+
},
1870+
},
17251871
},
1872+
wantStartErr: migrations.ColumnAlreadyExistsError{Table: "items", Name: "description"},
17261873
},
17271874
})
17281875
}

pkg/migrations/op_create_table.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
137137
columns := make(map[string]*schema.Column, len(o.Columns))
138138
for _, col := range o.Columns {
139139
columns[col.Name] = &schema.Column{
140-
Name: col.Name,
140+
Name: col.Name,
141+
Unique: col.Unique,
142+
Nullable: col.Nullable,
141143
}
142144
}
143145
var uniqueConstraints map[string]*schema.UniqueConstraint
@@ -153,10 +155,20 @@ func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
153155
}
154156
}
155157
}
158+
159+
// Build the table's primary key from the columns that have the `Pk` flag set
160+
var primaryKey []string
161+
for _, col := range o.Columns {
162+
if col.Pk {
163+
primaryKey = append(primaryKey, col.Name)
164+
}
165+
}
166+
156167
s.AddTable(o.Name, &schema.Table{
157168
Name: o.Name,
158169
Columns: columns,
159170
UniqueConstraints: uniqueConstraints,
171+
PrimaryKey: primaryKey,
160172
})
161173

162174
return s

0 commit comments

Comments
 (0)