Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New operation: create_constraint and support unique constraints with multiple columns #459

Merged
merged 69 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9c2e6d2
support table references in foreign key definitions
kvch Oct 16, 2024
88ee25b
in progress
kvch Oct 17, 2024
4b36f1f
more
kvch Oct 17, 2024
127d75e
Merge remote-tracking branch 'upstream/main' into feature-improve-for…
kvch Oct 25, 2024
aa4279e
rename migration files
kvch Oct 25, 2024
d6afb8d
minifix
kvch Oct 25, 2024
cf80f2b
Merge remote-tracking branch 'upstream/main' into feature-improve-for…
kvch Oct 30, 2024
1fdab42
more changes
kvch Oct 30, 2024
03231e8
moremore
kvch Oct 30, 2024
b84e4b0
cleaner
kvch Oct 30, 2024
b74e8bf
Merge remote-tracking branch 'upstream/main' into feature-improve-for…
kvch Nov 6, 2024
2f35452
fix interface
kvch Nov 6, 2024
ea6452f
more
kvch Nov 6, 2024
a9ef71a
rename migrations
kvch Nov 6, 2024
e932cf5
updae
kvch Nov 6, 2024
0d5408b
format
kvch Nov 6, 2024
03ab28a
rm debug
kvch Nov 6, 2024
2c4c487
fix rewrite
kvch Nov 6, 2024
0ddf446
more rewrite fix
kvch Nov 6, 2024
6f41cb0
add new op for real
kvch Nov 6, 2024
244d16b
more fixes
kvch Nov 6, 2024
9fb5c5b
rm print
kvch Nov 6, 2024
3ff9476
fix formatting
kvch Nov 6, 2024
e6938a2
really fix
kvch Nov 6, 2024
66995ac
format
kvch Nov 7, 2024
df7e426
more cleaning
kvch Nov 8, 2024
1d7c900
add unique example
kvch Nov 8, 2024
8fee548
Allow adding a column to a table created in the same migration (#449)
andrew-farries Nov 6, 2024
8297c87
Allow adding an index to a table created in the same migration (#451)
andrew-farries Nov 6, 2024
5419656
Add a `healthcheck` to the `db` service in `docker-compose.yml` (#452)
andrew-farries Nov 7, 2024
f4d323b
Allow adding indexes to columns created in the same migration (#454)
andrew-farries Nov 7, 2024
15cafb3
Add missing group by (#456)
ryanslade Nov 8, 2024
24339b2
more update
kvch Nov 8, 2024
ff70be8
more
kvch Nov 8, 2024
2942d0c
minor fixes
kvch Nov 8, 2024
5679e45
add docs
kvch Nov 8, 2024
7c02974
add more docs
kvch Nov 8, 2024
3ad210c
add test for casing
kvch Nov 8, 2024
ca097be
pretty tests
kvch Nov 8, 2024
8706061
create_constraint: unique
kvch Nov 11, 2024
cd4ed33
Merge remote-tracking branch 'upstream/main' into feature-create-cons…
kvch Nov 11, 2024
56e1a5f
rm more
kvch Nov 11, 2024
a26d9e6
more changes
kvch Nov 11, 2024
76ed12f
more more
kvch Nov 11, 2024
d0e9994
more updates
kvch Nov 11, 2024
88ef0a6
quiet linter
kvch Nov 11, 2024
40de5d1
rm professional debug
kvch Nov 11, 2024
52363ad
Update pkg/migrations/op_create_constraint.go
kvch Nov 12, 2024
eb6d6f9
Update docs/README.md
kvch Nov 12, 2024
f351a80
add backfilling
kvch Nov 12, 2024
8bd20d1
update migrations
kvch Nov 12, 2024
d510a77
Merge remote-tracking branch 'upstream/main' into feature-create-cons…
kvch Nov 12, 2024
0598fed
add comma
kvch Nov 12, 2024
51573fd
update down
kvch Nov 12, 2024
fb7d517
update readme
kvch Nov 12, 2024
3d38aec
update toc
kvch Nov 12, 2024
58a7f60
Update examples/44_add_table_unique_constraint.json
kvch Nov 12, 2024
f50087f
Update examples/44_add_table_unique_constraint.json
kvch Nov 12, 2024
7c67e9c
Update pkg/migrations/op_create_constraint.go
kvch Nov 12, 2024
356e3d6
Update pkg/migrations/op_create_constraint.go
kvch Nov 12, 2024
14b19d8
address more review notes
kvch Nov 12, 2024
b4a832f
add more tests
kvch Nov 12, 2024
c6d72d5
happy linter
kvch Nov 12, 2024
f81b7b9
Update schema.json
kvch Nov 13, 2024
9f5aa94
Update schema.json
kvch Nov 13, 2024
15ead83
more ntoes
kvch Nov 13, 2024
32be848
add check and test
kvch Nov 13, 2024
8e81ec0
really quote
kvch Nov 13, 2024
031d3e8
update docs
kvch Nov 13, 2024
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
36 changes: 36 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* [Add unique constraint](#add-unique-constraint)
* [Create index](#create-index)
* [Create table](#create-table)
* [Create constraint](#create-constraint)
* [Drop column](#drop-column)
* [Drop constraint](#drop-constraint)
* [Drop index](#drop-index)
Expand Down Expand Up @@ -687,6 +688,7 @@ See the [examples](../examples) directory for examples of each kind of operation
* [Add unique constraint](#add-unique-constraint)
* [Create index](#create-index)
* [Create table](#create-table)
* [Create constraint](#create-constraint)
* [Drop column](#drop-column)
* [Drop constraint](#drop-constraint)
* [Drop index](#drop-index)
Expand Down Expand Up @@ -1037,6 +1039,40 @@ Example **create table** migrations:
* [25_add_table_with_check_constraint.json](../examples/25_add_table_with_check_constraint.json)
* [28_different_defaults.json](../examples/28_different_defaults.json)

### Create constraint

A create constraint operation adds a new constraint to an existing table.

Only `UNIQUE` constraints are supported.

Required fields: `name`, `table`, `type`, `up`, `down`.

**create constraint** operations have this structure:

```json
{
"create_constraint": {
"table": "name of table",
"name": "my_unique_constraint",
"columns": ["col1", "col2"],
"type": "unique"
"up": {
"col1": "col1 || random()",
"col2": "col2 || random()"
},
"down": {
"col1": "col1",
"col2": "col2"
}
}
}
```

Example **create constraint** migrations:

* [44_add_table_unique_constraint.json](../examples/44_add_table_unique_constraint.json)


### Drop column

A drop column operation drops a column from an existing table.
Expand Down
2 changes: 2 additions & 0 deletions examples/.ledger
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@
40_create_enum_type.json
41_add_enum_column.json
42_create_unique_index.json
43_create_tickets_table.json
44_add_table_unique_constraint.json
25 changes: 25 additions & 0 deletions examples/43_create_tickets_table.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "43_create_tickets_table",
"operations": [
{
"create_table": {
"name": "tickets",
"columns": [
{
"name": "ticket_id",
"type": "serial",
"pk": true
},
{
"name": "sellers_name",
"type": "varchar(255)"
},
{
"name": "sellers_zip",
"type": "integer"
}
]
}
}
]
}
24 changes: 24 additions & 0 deletions examples/44_add_table_unique_constraint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
kvch marked this conversation as resolved.
Show resolved Hide resolved
"name": "44_add_table_unique_constraint",
"operations": [
{
"create_constraint": {
"type": "unique",
"table": "tickets",
"name": "unique_zip_name",
"columns": [
"sellers_name",
"sellers_zip"
],
"up": {
"sellers_name": "sellers_name",
"sellers_zip": "sellers_zip"
},
"down": {
"sellers_name": "sellers_name",
"sellers_zip": "sellers_zip"
}
}
}
]
}
9 changes: 9 additions & 0 deletions pkg/migrations/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ func (e ColumnDoesNotExistError) Error() string {
return fmt.Sprintf("column %q does not exist on table %q", e.Name, e.Table)
}

type ColumnMigrationMissingError struct {
Table string
Name string
}

func (e ColumnMigrationMissingError) Error() string {
return fmt.Sprintf("migration for column %q in %q is missing", e.Name, e.Table)
}

type ColumnIsNotNullableError struct {
Table string
Name string
Expand Down
7 changes: 7 additions & 0 deletions pkg/migrations/op_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
OpNameDropConstraint OpName = "drop_constraint"
OpNameSetReplicaIdentity OpName = "set_replica_identity"
OpRawSQLName OpName = "sql"
OpCreateConstraintName OpName = "create_constraint"

// Internal operation types used by `alter_column`
OpNameRenameColumn OpName = "rename_column"
Expand Down Expand Up @@ -124,6 +125,9 @@ func (v *Operations) UnmarshalJSON(data []byte) error {
case OpRawSQLName:
item = &OpRawSQL{}

case OpCreateConstraintName:
item = &OpCreateConstraint{}

default:
return fmt.Errorf("unknown migration type: %v", opName)
}
Expand Down Expand Up @@ -210,6 +214,9 @@ func OperationName(op Operation) OpName {
case *OpRawSQL:
return OpRawSQLName

case *OpCreateConstraint:
return OpCreateConstraintName

}

panic(fmt.Errorf("unknown operation for %T", op))
Expand Down
216 changes: 216 additions & 0 deletions pkg/migrations/op_create_constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: Apache-2.0

package migrations

import (
"context"
"fmt"
"strings"

"github.com/lib/pq"

"github.com/xataio/pgroll/pkg/db"
"github.com/xataio/pgroll/pkg/schema"
)

var _ Operation = (*OpCreateConstraint)(nil)

func (o *OpCreateConstraint) Start(ctx context.Context, conn db.DB, latestSchema string, tr SQLTransformer, s *schema.Schema, cbs ...CallbackFn) (*schema.Table, error) {
var err error
var table *schema.Table
for _, col := range o.Columns {
if table, err = o.duplicateColumnBeforeStart(ctx, conn, latestSchema, tr, col, s); err != nil {
return nil, err
}
}

switch o.Type { //nolint:gocritic // more cases will be added
case OpCreateConstraintTypeUnique:
return table, o.addUniqueIndex(ctx, conn)
}

return table, nil
}

func (o *OpCreateConstraint) duplicateColumnBeforeStart(ctx context.Context, conn db.DB, latestSchema string, tr SQLTransformer, colName string, s *schema.Schema) (*schema.Table, error) {
table := s.GetTable(o.Table)
column := table.GetColumn(colName)

d := NewColumnDuplicator(conn, table, column)
if err := d.Duplicate(ctx); err != nil {
return nil, fmt.Errorf("failed to duplicate column for new constraint: %w", err)
}

upSQL, ok := o.Up[colName]
if !ok {
return nil, fmt.Errorf("up migration is missing for column %s", colName)
}
physicalColumnName := TemporaryName(colName)
err := createTrigger(ctx, conn, tr, triggerConfig{
Name: TriggerName(o.Table, colName),
Direction: TriggerDirectionUp,
Columns: table.Columns,
SchemaName: s.Name,
LatestSchema: latestSchema,
TableName: o.Table,
PhysicalColumn: physicalColumnName,
SQL: upSQL,
})
if err != nil {
return nil, fmt.Errorf("failed to create up trigger: %w", err)
}

table.AddColumn(colName, schema.Column{
Name: physicalColumnName,
})

downSQL, ok := o.Down[colName]
if !ok {
return nil, fmt.Errorf("down migration is missing for column %s", colName)
}
err = createTrigger(ctx, conn, tr, triggerConfig{
Name: TriggerName(o.Table, physicalColumnName),
Direction: TriggerDirectionDown,
Columns: table.Columns,
LatestSchema: latestSchema,
SchemaName: s.Name,
TableName: o.Table,
PhysicalColumn: colName,
SQL: downSQL,
})
if err != nil {
return nil, fmt.Errorf("failed to create down trigger: %w", err)
}
return table, nil
}

func (o *OpCreateConstraint) Complete(ctx context.Context, conn db.DB, tr SQLTransformer, s *schema.Schema) error {
switch o.Type { //nolint:gocritic // more cases will be added
case OpCreateConstraintTypeUnique:
uniqueOp := &OpSetUnique{
Table: o.Table,
Name: o.Name,
}
err := uniqueOp.Complete(ctx, conn, tr, s)
if err != nil {
return err
}
}

// remove old columns
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s %s",
pq.QuoteIdentifier(o.Table),
dropMultipleColumns(quoteColumnNames(o.Columns)),
))
if err != nil {
return err
}

// rename new columns to old name
table := s.GetTable(o.Table)
for _, col := range o.Columns {
column := table.GetColumn(col)
if err := RenameDuplicatedColumn(ctx, conn, table, column); err != nil {
return err
}
}

return o.removeTriggers(ctx, conn)
}

func (o *OpCreateConstraint) Rollback(ctx context.Context, conn db.DB, tr SQLTransformer, s *schema.Schema) error {
andrew-farries marked this conversation as resolved.
Show resolved Hide resolved
_, err := conn.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s %s",
pq.QuoteIdentifier(o.Table),
dropMultipleColumns(quotedTemporaryNames(o.Columns)),
))
if err != nil {
return err
}

return o.removeTriggers(ctx, conn)
}

func (o *OpCreateConstraint) removeTriggers(ctx context.Context, conn db.DB) error {
dropFuncs := make([]string, len(o.Columns)*2)
for i, j := 0, 0; i < len(o.Columns); i, j = i+1, j+2 {
dropFuncs[j] = pq.QuoteIdentifier(TriggerFunctionName(o.Table, o.Columns[i]))
dropFuncs[j+1] = pq.QuoteIdentifier(TriggerFunctionName(o.Table, TemporaryName(o.Columns[i])))
}
_, err := conn.ExecContext(ctx, fmt.Sprintf("DROP FUNCTION IF EXISTS %s CASCADE",
strings.Join(dropFuncs, ", "),
))
return err
}

func dropMultipleColumns(columns []string) string {
for i, col := range columns {
columns[i] = "DROP COLUMN IF EXISTS " + col
}
return strings.Join(columns, ", ")
}

func (o *OpCreateConstraint) Validate(ctx context.Context, s *schema.Schema) error {
table := s.GetTable(o.Table)
if table == nil {
return TableDoesNotExistError{Name: o.Table}
}

if err := ValidateIdentifierLength(o.Name); err != nil {
return err
}

if table.ConstraintExists(o.Name) {
return ConstraintAlreadyExistsError{
Table: o.Table,
Constraint: o.Name,
}
}

for _, col := range o.Columns {
if table.GetColumn(col) == nil {
return ColumnDoesNotExistError{
Table: o.Table,
Name: col,
}
}
if _, ok := o.Up[col]; !ok {
return ColumnMigrationMissingError{
Table: o.Table,
Name: col,
}
}
if _, ok := o.Down[col]; !ok {
return ColumnMigrationMissingError{
Table: o.Table,
Name: col,
}
}
}

switch o.Type { //nolint:gocritic // more cases will be added
case OpCreateConstraintTypeUnique:
if len(o.Columns) == 0 {
return FieldRequiredError{Name: "columns"}
}
}

return nil
}

func (o *OpCreateConstraint) addUniqueIndex(ctx context.Context, conn db.DB) error {
_, err := conn.ExecContext(ctx, fmt.Sprintf("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS %s ON %s (%s)",
pq.QuoteIdentifier(o.Name),
pq.QuoteIdentifier(o.Table),
strings.Join(quotedTemporaryNames(o.Columns), ", "),
))

return err
}

func quotedTemporaryNames(columns []string) []string {
andrew-farries marked this conversation as resolved.
Show resolved Hide resolved
names := make([]string, len(columns))
for i, col := range columns {
names[i] = pq.QuoteIdentifier(TemporaryName(col))
}
return names
}
Loading