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

Support creating different index types #417

Merged
merged 17 commits into from
Oct 29, 2024
7 changes: 6 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ Example **add unique** migrations:

### Create index

A create index operation creates a new btree index on a set of columns.
A create index operation creates a new index on a set of columns.

**create index** operations have this structure:

Expand All @@ -966,14 +966,19 @@ A create index operation creates a new btree index on a set of columns.
"name": "index name",
"columns": [ "names of columns on which to define the index" ]
"predicate": "conditional expression for defining a partial index",
"method": "btree"
}
}
```

The field `method` can be `btree`, `hash`, `gist`, `spgist`, `gin`, `brin`.
You can also specify storage parameters for the index in `storage_parameters`.

Example **create index** migrations:

* [10_create_index.json](../examples/10_create_index.json)
* [37_create_partial_index.json](../examples/37_create_partial_index.json)
* [38_create_hash_index_with_fillfactor.json](../examples/38_create_hash_index_with_fillfactor.json)

### Create table

Expand Down
16 changes: 16 additions & 0 deletions examples/38_create_hash_index_with_fillfactor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "38_create_hash_index_with_fillfactor",
"operations": [
{
"create_index": {
"name": "idx_fruits_name",
"table": "fruits",
"columns": [
"name"
],
"method": "hash",
"storage_parameters": "fillfactor = 70"
}
}
]
}
20 changes: 20 additions & 0 deletions internal/jsonschema/testdata/create-invalid-index.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
This is an invalid 'create_index' migration.

-- create_table.json --
{
"name": "migration_name",
"operations": [
{
"create_index": {
"name": "reviews_index",
"columns": [
"my-column"
],
"method": "no_such_index_method"
}
}
]
}

-- valid --
false
21 changes: 21 additions & 0 deletions pkg/migrations/op_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,27 @@ func indexExists(t *testing.T, db *sql.DB, schema, table, index string) bool {
return exists
}

func CheckIndexDefinition(t *testing.T, db *sql.DB, schema, table, index, expectedDefinition string) {
kvch marked this conversation as resolved.
Show resolved Hide resolved
t.Helper()

var actualDef string
err := db.QueryRow(`
SELECT indexdef
FROM pg_indexes
WHERE schemaname = $1
AND tablename = $2
AND indexname = $3
`,
schema, table, index).Scan(&actualDef)
if err != nil {
t.Fatal(err)
}

if expectedDefinition != actualDef {
t.Fatalf("Expected index %q to have definition %q, got %q", index, expectedDefinition, actualDef)
}
}

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

Expand Down
15 changes: 12 additions & 3 deletions pkg/migrations/op_create_index.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ var _ Operation = (*OpCreateIndex)(nil)

func (o *OpCreateIndex) Start(ctx context.Context, conn db.DB, latestSchema string, tr SQLTransformer, s *schema.Schema, cbs ...CallbackFn) (*schema.Table, error) {
// create index concurrently
stmt := fmt.Sprintf("CREATE INDEX CONCURRENTLY %s ON %s (%s)",
stmt := fmt.Sprintf("CREATE INDEX CONCURRENTLY %s ON %s",
pq.QuoteIdentifier(o.Name),
pq.QuoteIdentifier(o.Table),
strings.Join(quoteColumnNames(o.Columns), ", "))
pq.QuoteIdentifier(o.Table))

if o.Method != nil {
stmt += fmt.Sprintf(" USING %s", string(*o.Method))
}

stmt += fmt.Sprintf(" (%s)", strings.Join(quoteColumnNames(o.Columns), ", "))

if o.StorageParameters != nil {
stmt += fmt.Sprintf(" WITH (%s)", *o.StorageParameters)
}

if o.Predicate != nil {
stmt += fmt.Sprintf(" WHERE %s", *o.Predicate)
Expand Down
52 changes: 52 additions & 0 deletions pkg/migrations/op_create_index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package migrations_test

import (
"database/sql"
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -150,6 +151,7 @@ func TestCreateIndex(t *testing.T) {
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// The index has been created on the underlying table.
IndexMustExist(t, db, schema, "users", "idx_users_name_after_2019")
CheckIndexDefinition(t, db, schema, "users", "idx_users_name_after_2019", fmt.Sprintf("CREATE INDEX idx_users_name_after_2019 ON %s.users USING btree (registered_at_year) WHERE (registered_at_year > 2019)", schema))
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
// The index has been dropped from the the underlying table.
Expand Down Expand Up @@ -203,6 +205,56 @@ func TestCreateIndex(t *testing.T) {
afterRollback: func(t *testing.T, db *sql.DB, schema string) {},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {},
},
{
name: "create hash index with option",
migrations: []migrations.Migration{
{
Name: "01_add_table",
Operations: migrations.Operations{
&migrations.OpCreateTable{
Name: "users",
Columns: []migrations.Column{
{
Name: "id",
Type: "serial",
Pk: ptr(true),
},
{
Name: "name",
Type: "varchar(255)",
Nullable: ptr(false),
},
},
},
},
},
{
Name: "02_create_hash_index",
Operations: migrations.Operations{
&migrations.OpCreateIndex{
Name: "idx_users_name_hash",
Table: "users",
Columns: []string{"name"},
Method: ptr(migrations.OpCreateIndexMethodHash),
StorageParameters: ptr("fillfactor = 70"),
},
},
},
},
afterStart: func(t *testing.T, db *sql.DB, schema string) {
// The index has been created on the underlying table.
IndexMustExist(t, db, schema, "users", "idx_users_name_hash")
// Check the index definition.
CheckIndexDefinition(t, db, schema, "users", "idx_users_name_hash", fmt.Sprintf("CREATE INDEX idx_users_name_hash ON %s.users USING hash (name) WITH (fillfactor='70')", schema))
},
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
// The index has been dropped from the the underlying table.
IndexMustNotExist(t, db, schema, "users", "idx_users_name_hash")
},
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
// Complete is a no-op.
},
},
})
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/migrations/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ type Index struct {

// Predicate is the optional predicate for the index
Predicate *string `json:"predicate,omitempty"`

// Method is the method for the index
Method string `json:"method,omitempty"`

// Definition is statement to construct the index
Definition string `json:"definition"`
}

// ForeignKey represents a foreign key on a table
Expand Down
14 changes: 11 additions & 3 deletions pkg/state/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,27 @@ BEGIN
'name', ix_details.name,
'unique', ix_details.indisunique,
'columns', ix_details.columns,
'predicate', ix_details.predicate
'predicate', ix_details.predicate,
'method', ix_details.method,
'definition', ix_details.definition
)), '{}'::json)
FROM (SELECT replace(
reverse(split_part(reverse(pi.indexrelid::regclass::text), '.', 1)),
'"', '') as name,
pi.indisunique,
array_agg(a.attname) AS columns,
pg_get_expr(pi.indpred, t.oid) AS predicate
pg_get_expr(pi.indpred, t.oid) AS predicate,
am.amname AS method,
pg_get_indexdef(pi.indexrelid) AS definition
FROM pg_index pi
JOIN pg_attribute a
ON a.attrelid = pi.indrelid AND a.attnum = ANY (pi.indkey)
JOIN pg_class cls
ON cls.oid = pi.indexrelid
JOIN pg_am am
ON am.oid = cls.relam
WHERE indrelid = t.oid::regclass
GROUP BY pi.indexrelid, pi.indisunique) as ix_details),
GROUP BY pi.indexrelid, pi.indisunique, am.amname) as ix_details),
'checkConstraints', (SELECT COALESCE(json_object_agg(cc_details.conname, json_build_object(
'name', cc_details.conname,
'columns', cc_details.columns,
Expand Down
Loading