Skip to content

Commit e2f1740

Browse files
authored
Support creating different index types (#417)
This PR adds new options to `create_index` operation: * `method`: enum for setting different index types. Default: `btree`. Available options: `btree`, `hash`, `gist`, `spgist`, `gin`, `brin`. * `storage_parameters`: optional storage parameters for the index. Example operation to create a new hash index with custom fillfactor: ```json { "name": "10_create_index", "operations": [ { "create_index": { "name": "idx_fruits_name", "table": "fruits", "columns": [ "name" ], "method": "hash", "storage_parameters": "fillfactor = 70" } } ] } ``` Furthermore, `analyze` now return the index method under the name `method` and the definition under `definition`. Please note that the definition `pgroll` returns is not the SQL statement `pgroll` runs when running `create_index` migrations. ```json { "idx_fruits_name": { "name": "idx_fruits_name", "unique": false, "columns": [ "name" ], "method": "hash", "definition": "CREATE INDEX idx_fruits_name ON public.fruits USING hash (name) WITH (fillfactor='70')" } } ``` Closes #378
1 parent 37a7232 commit e2f1740

File tree

11 files changed

+218
-37
lines changed

11 files changed

+218
-37
lines changed

docs/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,7 @@ Example **add unique** migrations:
955955

956956
### Create index
957957

958-
A create index operation creates a new btree index on a set of columns.
958+
A create index operation creates a new index on a set of columns.
959959

960960
**create index** operations have this structure:
961961

@@ -966,14 +966,19 @@ A create index operation creates a new btree index on a set of columns.
966966
"name": "index name",
967967
"columns": [ "names of columns on which to define the index" ]
968968
"predicate": "conditional expression for defining a partial index",
969+
"method": "btree"
969970
}
970971
}
971972
```
972973

974+
The field `method` can be `btree`, `hash`, `gist`, `spgist`, `gin`, `brin`.
975+
You can also specify storage parameters for the index in `storage_parameters`.
976+
973977
Example **create index** migrations:
974978

975979
* [10_create_index.json](../examples/10_create_index.json)
976980
* [37_create_partial_index.json](../examples/37_create_partial_index.json)
981+
* [38_create_hash_index_with_fillfactor.json](../examples/38_create_hash_index_with_fillfactor.json)
977982

978983
### Create table
979984

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "38_create_hash_index_with_fillfactor",
3+
"operations": [
4+
{
5+
"create_index": {
6+
"name": "idx_fruits_name",
7+
"table": "fruits",
8+
"columns": [
9+
"name"
10+
],
11+
"method": "hash",
12+
"storage_parameters": "fillfactor = 70"
13+
}
14+
}
15+
]
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
This is an invalid 'create_index' migration.
2+
3+
-- create_table.json --
4+
{
5+
"name": "migration_name",
6+
"operations": [
7+
{
8+
"create_index": {
9+
"name": "reviews_index",
10+
"columns": [
11+
"my-column"
12+
],
13+
"method": "no_such_index_method"
14+
}
15+
}
16+
]
17+
}
18+
19+
-- valid --
20+
false

pkg/migrations/op_common_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,27 @@ func indexExists(t *testing.T, db *sql.DB, schema, table, index string) bool {
304304
return exists
305305
}
306306

307+
func CheckIndexDefinition(t *testing.T, db *sql.DB, schema, table, index, expectedDefinition string) {
308+
t.Helper()
309+
310+
var actualDef string
311+
err := db.QueryRow(`
312+
SELECT indexdef
313+
FROM pg_indexes
314+
WHERE schemaname = $1
315+
AND tablename = $2
316+
AND indexname = $3
317+
`,
318+
schema, table, index).Scan(&actualDef)
319+
if err != nil {
320+
t.Fatal(err)
321+
}
322+
323+
if expectedDefinition != actualDef {
324+
t.Fatalf("Expected index %q to have definition %q, got %q", index, expectedDefinition, actualDef)
325+
}
326+
}
327+
307328
func checkConstraintExists(t *testing.T, db *sql.DB, schema, table, constraint string) bool {
308329
t.Helper()
309330

pkg/migrations/op_create_index.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ var _ Operation = (*OpCreateIndex)(nil)
1717

1818
func (o *OpCreateIndex) Start(ctx context.Context, conn db.DB, latestSchema string, tr SQLTransformer, s *schema.Schema, cbs ...CallbackFn) (*schema.Table, error) {
1919
// create index concurrently
20-
stmt := fmt.Sprintf("CREATE INDEX CONCURRENTLY %s ON %s (%s)",
20+
stmt := fmt.Sprintf("CREATE INDEX CONCURRENTLY %s ON %s",
2121
pq.QuoteIdentifier(o.Name),
22-
pq.QuoteIdentifier(o.Table),
23-
strings.Join(quoteColumnNames(o.Columns), ", "))
22+
pq.QuoteIdentifier(o.Table))
23+
24+
if o.Method != nil {
25+
stmt += fmt.Sprintf(" USING %s", string(*o.Method))
26+
}
27+
28+
stmt += fmt.Sprintf(" (%s)", strings.Join(quoteColumnNames(o.Columns), ", "))
29+
30+
if o.StorageParameters != nil {
31+
stmt += fmt.Sprintf(" WITH (%s)", *o.StorageParameters)
32+
}
2433

2534
if o.Predicate != nil {
2635
stmt += fmt.Sprintf(" WHERE %s", *o.Predicate)

pkg/migrations/op_create_index_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package migrations_test
44

55
import (
66
"database/sql"
7+
"fmt"
78
"strings"
89
"testing"
910

@@ -150,6 +151,7 @@ func TestCreateIndex(t *testing.T) {
150151
afterStart: func(t *testing.T, db *sql.DB, schema string) {
151152
// The index has been created on the underlying table.
152153
IndexMustExist(t, db, schema, "users", "idx_users_name_after_2019")
154+
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))
153155
},
154156
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
155157
// The index has been dropped from the the underlying table.
@@ -203,6 +205,56 @@ func TestCreateIndex(t *testing.T) {
203205
afterRollback: func(t *testing.T, db *sql.DB, schema string) {},
204206
afterComplete: func(t *testing.T, db *sql.DB, schema string) {},
205207
},
208+
{
209+
name: "create hash index with option",
210+
migrations: []migrations.Migration{
211+
{
212+
Name: "01_add_table",
213+
Operations: migrations.Operations{
214+
&migrations.OpCreateTable{
215+
Name: "users",
216+
Columns: []migrations.Column{
217+
{
218+
Name: "id",
219+
Type: "serial",
220+
Pk: ptr(true),
221+
},
222+
{
223+
Name: "name",
224+
Type: "varchar(255)",
225+
Nullable: ptr(false),
226+
},
227+
},
228+
},
229+
},
230+
},
231+
{
232+
Name: "02_create_hash_index",
233+
Operations: migrations.Operations{
234+
&migrations.OpCreateIndex{
235+
Name: "idx_users_name_hash",
236+
Table: "users",
237+
Columns: []string{"name"},
238+
Method: ptr(migrations.OpCreateIndexMethodHash),
239+
StorageParameters: ptr("fillfactor = 70"),
240+
},
241+
},
242+
},
243+
},
244+
afterStart: func(t *testing.T, db *sql.DB, schema string) {
245+
// The index has been created on the underlying table.
246+
IndexMustExist(t, db, schema, "users", "idx_users_name_hash")
247+
// Check the index definition.
248+
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))
249+
},
250+
afterRollback: func(t *testing.T, db *sql.DB, schema string) {
251+
// The index has been dropped from the the underlying table.
252+
IndexMustNotExist(t, db, schema, "users", "idx_users_name_hash")
253+
},
254+
afterComplete: func(t *testing.T, db *sql.DB, schema string) {
255+
// Complete is a no-op.
256+
},
257+
},
206258
})
207259
}
208260

pkg/migrations/types.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/schema/schema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ type Index struct {
8888

8989
// Predicate is the optional predicate for the index
9090
Predicate *string `json:"predicate,omitempty"`
91+
92+
// Method is the method for the index
93+
Method string `json:"method,omitempty"`
94+
95+
// Definition is statement to construct the index
96+
Definition string `json:"definition"`
9197
}
9298

9399
// ForeignKey represents a foreign key on a table

pkg/state/init.sql

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,19 +144,27 @@ BEGIN
144144
'name', ix_details.name,
145145
'unique', ix_details.indisunique,
146146
'columns', ix_details.columns,
147-
'predicate', ix_details.predicate
147+
'predicate', ix_details.predicate,
148+
'method', ix_details.method,
149+
'definition', ix_details.definition
148150
)), '{}'::json)
149151
FROM (SELECT replace(
150152
reverse(split_part(reverse(pi.indexrelid::regclass::text), '.', 1)),
151153
'"', '') as name,
152154
pi.indisunique,
153155
array_agg(a.attname) AS columns,
154-
pg_get_expr(pi.indpred, t.oid) AS predicate
156+
pg_get_expr(pi.indpred, t.oid) AS predicate,
157+
am.amname AS method,
158+
pg_get_indexdef(pi.indexrelid) AS definition
155159
FROM pg_index pi
156160
JOIN pg_attribute a
157161
ON a.attrelid = pi.indrelid AND a.attnum = ANY (pi.indkey)
162+
JOIN pg_class cls
163+
ON cls.oid = pi.indexrelid
164+
JOIN pg_am am
165+
ON am.oid = cls.relam
158166
WHERE indrelid = t.oid::regclass
159-
GROUP BY pi.indexrelid, pi.indisunique) as ix_details),
167+
GROUP BY pi.indexrelid, pi.indisunique, am.amname) as ix_details),
160168
'checkConstraints', (SELECT COALESCE(json_object_agg(cc_details.conname, json_build_object(
161169
'name', cc_details.conname,
162170
'columns', cc_details.columns,

0 commit comments

Comments
 (0)