Skip to content

Commit

Permalink
Add support for primary key constraints in create_table operation (#…
Browse files Browse the repository at this point in the history
…594)

This PR adds support for setting primary key constraints in the
`constraints` option of `create_table`.

Please note that if you set primary keys in `columns`, you are not
allowed to configure `primary_key` constraints.

Example:

```json
{
  "name": "50_create_table_with_table_constraint",
  "operations": [
    {
      "create_table": {
        "name": "phonebook",
        "columns": [
          {
            "name": "id",
            "type": "serial",
          },
          {
            "name": "name",
            "type": "varchar(255)"
          }
        ],
        "constraints": [
          {
            "name": "my_pk",
            "type": "primary_key",
            "columns": [
              "id"
            ] 
          }
        ]
      }
    }
  ]
}
```
  • Loading branch information
kvch authored Jan 17, 2025
1 parent 936391f commit 5df19ff
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 17 deletions.
5 changes: 4 additions & 1 deletion docs/operations/create_table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ Each `constraint` is defined as:
},
```

Supported constraint types: `unique`, `check`.
Supported constraint types: `unique`, `check`, `primary_key`.

Please note that you can only configure primary keys in `columns` list or `constraints` list, but
not in both places.

## Examples

Expand Down
10 changes: 8 additions & 2 deletions examples/50_create_table_with_table_constraint.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"columns": [
{
"name": "id",
"type": "serial",
"pk": true
"type": "serial"
},
{
"name": "name",
Expand All @@ -24,6 +23,13 @@
}
],
"constraints": [
{
"name": "phonebook_pk",
"type": "primary_key",
"columns": [
"id"
]
},
{
"name": "unique_numbers",
"type": "unique",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
This is an invalid 'create_table' migration.
Primary key constraint must not constain a check expression.

-- create_table.json --
{
"name": "migration_name",
"operations": [
{
"create_table": {
"name": "posts",
"columns": [
{
"name": "title",
"type": "varchar(255)"
},
{
"name": "user_id",
"type": "integer",
"nullable": true
}
],
"constraints": [
{
"name": "my_invalid_pk",
"type": "primary_key",
"columns": [
"title"
],
"check": "this should not be set"
}
]
}
}
]
}

-- valid --
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
This is an invalid 'create_table' migration.
Primary key constraint must have columns set

-- create_table.json --
{
"name": "migration_name",
"operations": [
{
"create_table": {
"name": "posts",
"columns": [
{
"name": "title",
"type": "varchar(255)"
},
{
"name": "user_id",
"type": "integer",
"nullable": true
}
],
"constraints": [
{
"name": "my_invalid_pk",
"type": "primary_key"
}
]
}
}
]
}

-- valid --
false
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a valid 'create_table' migration.

-- create_table.json --
{
"name": "migration_name",
"operations": [
{
"create_table": {
"name": "posts",
"columns": [
{
"name": "title",
"type": "varchar(255)"
},
{
"name": "user_id",
"type": "integer"
}
],
"constraints": [
{
"name": "my_pk",
"type": "primary_key",
"columns": [
"title",
"user_id"
]
}
]
}
}
]
}

-- valid --
true
8 changes: 8 additions & 0 deletions pkg/migrations/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,11 @@ type MultiColumnConstraintsNotSupportedError struct {
func (e MultiColumnConstraintsNotSupportedError) Error() string {
return fmt.Sprintf("constraint %q on table %q applies to multiple columns", e.Constraint, e.Table)
}

type PrimaryKeysAreAlreadySetError struct {
Table string
}

func (e PrimaryKeysAreAlreadySetError) Error() string {
return fmt.Sprintf("table %q already has a primary key configuration in columns list", e.Table)
}
27 changes: 27 additions & 0 deletions pkg/migrations/op_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ func NotValidatedForeignKeyMustExist(t *testing.T, db *sql.DB, schema, table, co
}
}

func PrimaryKeyConstraintMustExist(t *testing.T, db *sql.DB, schema, table, constraint string) {
t.Helper()
if !primaryKeyConstraintExists(t, db, schema, table, constraint) {
t.Fatalf("Expected constraint %q to exist", constraint)
}
}

func IndexMustExist(t *testing.T, db *sql.DB, schema, table, index string) {
t.Helper()
if !indexExists(t, db, schema, table, index) {
Expand Down Expand Up @@ -403,6 +410,26 @@ func foreignKeyExists(t *testing.T, db *sql.DB, schema, table, constraint string
return exists
}

func primaryKeyConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint string) bool {
t.Helper()

var exists bool
err := db.QueryRow(`
SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_constraint
WHERE conrelid = $1::regclass
AND conname = $2
AND contype = 'p'
)`,
fmt.Sprintf("%s.%s", schema, table), constraint).Scan(&exists)
if err != nil {
t.Fatal(err)
}

return exists
}

func triggerExists(t *testing.T, db *sql.DB, schema, table, trigger string) bool {
t.Helper()

Expand Down
48 changes: 37 additions & 11 deletions pkg/migrations/op_create_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (o *OpCreateTable) Validate(ctx context.Context, s *schema.Schema) error {
return TableAlreadyExistsError{Name: o.Name}
}

hasPrimaryKeyColumns := false
for _, col := range o.Columns {
if err := ValidateIdentifierLength(col.Name); err != nil {
return fmt.Errorf("invalid column: %w", err)
Expand Down Expand Up @@ -106,6 +107,10 @@ func (o *OpCreateTable) Validate(ctx context.Context, s *schema.Schema) error {
}
}
}

if col.Pk {
hasPrimaryKeyColumns = true
}
}

for _, c := range o.Constraints {
Expand Down Expand Up @@ -146,6 +151,14 @@ func (o *OpCreateTable) Validate(ctx context.Context, s *schema.Schema) error {
Err: fmt.Errorf("CHECK constraints cannot have NULLS NOT DISTINCT"),
}
}
case ConstraintTypePrimaryKey:
// users can only set primary keys either in columns list or in constraint list
if hasPrimaryKeyColumns {
return PrimaryKeysAreAlreadySetError{Table: o.Name}
}
if len(c.Columns) == 0 {
return FieldRequiredError{Name: "columns"}
}
}
}

Expand All @@ -160,12 +173,16 @@ func (o *OpCreateTable) Validate(ctx context.Context, s *schema.Schema) error {
// the new table.
func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
columns := make(map[string]*schema.Column, len(o.Columns))
primaryKeys := make([]string, 0)
for _, col := range o.Columns {
columns[col.Name] = &schema.Column{
Name: col.Name,
Unique: col.Unique,
Nullable: col.Nullable,
}
if col.Pk {
primaryKeys = append(primaryKeys, col.Name)
}
}
uniqueConstraints := make(map[string]*schema.UniqueConstraint, 0)
checkConstraints := make(map[string]*schema.CheckConstraint, 0)
Expand All @@ -182,14 +199,8 @@ func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
Columns: c.Columns,
Definition: c.Check,
}
}
}

// Build the table's primary key from the columns that have the `Pk` flag set
var primaryKey []string
for _, col := range o.Columns {
if col.Pk {
primaryKey = append(primaryKey, col.Name)
case ConstraintTypePrimaryKey:
primaryKeys = c.Columns
}
}

Expand All @@ -198,7 +209,7 @@ func (o *OpCreateTable) updateSchema(s *schema.Schema) *schema.Schema {
Columns: columns,
UniqueConstraints: uniqueConstraints,
CheckConstraints: checkConstraints,
PrimaryKey: primaryKey,
PrimaryKey: primaryKeys,
})

return s
Expand All @@ -219,12 +230,14 @@ func columnsToSQL(cols []Column, tr SQLTransformer) (string, error) {
sql += colSQL

if col.IsPrimaryKey() {
primaryKeys = append(primaryKeys, pq.QuoteIdentifier(col.Name))
primaryKeys = append(primaryKeys, col.Name)
}
}

// Add primary key constraint if there are primary key columns.
if len(primaryKeys) > 0 {
sql += fmt.Sprintf(", PRIMARY KEY (%s)", strings.Join(primaryKeys, ", "))
writer := &ConstraintSQLWriter{Columns: primaryKeys}
sql += ", " + writer.WritePrimaryKey()
}
return sql, nil
}
Expand All @@ -249,6 +262,8 @@ func constraintsToSQL(constraints []Constraint) (string, error) {
constraintsSQL[i] = writer.WriteUnique(c.NullsNotDistinct)
case ConstraintTypeCheck:
constraintsSQL[i] = writer.WriteCheck(c.Check, c.NoInherit)
case ConstraintTypePrimaryKey:
constraintsSQL[i] = writer.WritePrimaryKey()
}
}
if len(constraintsSQL) == 0 {
Expand Down Expand Up @@ -296,6 +311,17 @@ func (w *ConstraintSQLWriter) WriteCheck(check string, noInherit bool) string {
return constraint
}

func (w *ConstraintSQLWriter) WritePrimaryKey() string {
constraint := ""
if w.Name != "" {
constraint = fmt.Sprintf("CONSTRAINT %s ", pq.QuoteIdentifier(w.Name))
}
constraint += fmt.Sprintf("PRIMARY KEY (%s)", strings.Join(quoteColumnNames(w.Columns), ", "))
constraint += w.addIndexParameters()
constraint += w.addDeferrable()
return constraint
}

func (w *ConstraintSQLWriter) addIndexParameters() string {
constraint := ""
if len(w.IncludeColumns) != 0 {
Expand Down
Loading

0 comments on commit 5df19ff

Please sign in to comment.