Skip to content

Commit e6e4523

Browse files
authored
Convert CREATE INDEX SQL into pgroll operation (#551)
Convert the majority of `CREATE INDEX` statements into `OpCreateIndex` The following cases are covered: ```sql CREATE INDEX idx_name ON foo (bar) CREATE INDEX idx_name ON foo (bar ASC) CREATE INDEX idx_name ON foo USING btree (bar) CREATE INDEX idx_name ON foo USING brin (bar) CREATE INDEX idx_name ON foo USING gin (bar) CREATE INDEX idx_name ON foo USING gist (bar) CREATE INDEX idx_name ON foo USING hash (bar) CREATE INDEX idx_name ON foo USING spgist (bar) CREATE INDEX CONCURRENTLY idx_name ON foo (bar) CREATE INDEX idx_name ON schema.foo (bar) CREATE INDEX idx_name ON foo (bar, baz) CREATE UNIQUE INDEX idx_name ON foo (bar) CREATE INDEX idx_name ON foo (bar) WHERE (foo > 0) CREATE INDEX idx_name ON foo (bar) WHERE foo > 0 CREATE INDEX idx_name ON foo (bar) WITH (fillfactor = 70) CREATE INDEX idx_name ON foo (bar) WITH (deduplicate_items = true) CREATE INDEX idx_name ON foo (bar) WITH (buffering = ON) CREATE INDEX idx_name ON foo (bar) WITH (buffering = OFF) CREATE INDEX idx_name ON foo (bar) WITH (buffering = AUTO) CREATE INDEX idx_name ON foo (bar) WITH (fastupdate = true) CREATE INDEX idx_name ON foo (bar) WITH (pages_per_range = 100) CREATE INDEX idx_name ON foo (bar) WITH (autosummarize = true) CREATE INDEX idx_name ON foo (bar) WITH (fillfactor = 70, deduplicate_items = true) ``` And the following unsupported cases fall back to RAW SQL: ```sql CREATE INDEX idx_name ON foo (bar) TABLESPACE baz CREATE INDEX idx_name ON foo (bar COLLATE en_US) CREATE INDEX idx_name ON foo (bar DESC) CREATE INDEX idx_name ON foo (bar NULLS FIRST) CREATE INDEX idx_name ON foo (bar NULLS LAST) CREATE INDEX idx_name ON foo (bar) INCLUDE (baz) CREATE INDEX idx_name ON foo (bar opclass (test = test)) CREATE INDEX idx_name ON foo (bar opclass) CREATE INDEX idx_name ON ONLY foo (bar) CREATE INDEX idx_name ON foo(a) NULLS NOT DISTINCT CREATE INDEX IF NOT EXISTS idx_name ON foo(a) CREATE INDEX idx_name ON foo(LOWER(a)) CREATE INDEX idx_name ON foo(a, LOWER(b)) ```
1 parent 97ff17d commit e6e4523

File tree

5 files changed

+381
-0
lines changed

5 files changed

+381
-0
lines changed

pkg/migrations/op_create_index.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,23 @@ func quoteColumnNames(columns []string) (quoted []string) {
9797
}
9898
return quoted
9999
}
100+
101+
// ParseCreateIndexMethod parsed index methods into OpCreateIndexMethod
102+
func ParseCreateIndexMethod(method string) (OpCreateIndexMethod, error) {
103+
switch method {
104+
case "btree":
105+
return OpCreateIndexMethodBtree, nil
106+
case "hash":
107+
return OpCreateIndexMethodHash, nil
108+
case "gist":
109+
return OpCreateIndexMethodGist, nil
110+
case "spgist":
111+
return OpCreateIndexMethodSpgist, nil
112+
case "gin":
113+
return OpCreateIndexMethodGin, nil
114+
case "brin":
115+
return OpCreateIndexMethodBrin, nil
116+
default:
117+
return OpCreateIndexMethodBtree, fmt.Errorf("unknown method: %s", method)
118+
}
119+
}

pkg/sql2pgroll/convert.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ func convert(sql string) (migrations.Operations, error) {
4747
return convertRenameStmt(node.RenameStmt)
4848
case *pgq.Node_DropStmt:
4949
return convertDropStatement(node.DropStmt)
50+
case *pgq.Node_IndexStmt:
51+
return convertCreateIndexStmt(node.IndexStmt)
5052
default:
5153
return makeRawSQLOperation(sql), nil
5254
}

pkg/sql2pgroll/create_index.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package sql2pgroll
4+
5+
import (
6+
"fmt"
7+
8+
pgq "github.com/xataio/pg_query_go/v6"
9+
10+
"github.com/xataio/pgroll/pkg/migrations"
11+
)
12+
13+
// convertCreateIndexStmt converts CREATE INDEX statements into pgroll operations.
14+
func convertCreateIndexStmt(stmt *pgq.IndexStmt) (migrations.Operations, error) {
15+
if !canConvertCreateIndexStmt(stmt) {
16+
return nil, nil
17+
}
18+
19+
// Get the qualified table name
20+
tableName := getQualifiedRelationName(stmt.GetRelation())
21+
var columns []string
22+
23+
// Get the columns on which the index is defined
24+
for _, param := range stmt.GetIndexParams() {
25+
if colName := param.GetIndexElem().GetName(); colName != "" {
26+
columns = append(columns, colName)
27+
}
28+
}
29+
30+
// Parse the access method
31+
method, err := migrations.ParseCreateIndexMethod(stmt.GetAccessMethod())
32+
if err != nil {
33+
return nil, fmt.Errorf("parse create index method: %w", err)
34+
}
35+
36+
// Get index uniqueness
37+
unique := false
38+
if stmt.GetUnique() {
39+
unique = true
40+
}
41+
42+
// Deparse WHERE clause
43+
var predicate string
44+
if where := stmt.GetWhereClause(); where != nil {
45+
predicate, err = pgq.DeparseExpr(where)
46+
if err != nil {
47+
return nil, fmt.Errorf("parsing where clause: %w", err)
48+
}
49+
}
50+
51+
// Deparse storage parameters
52+
var storageParams string
53+
if len(stmt.GetOptions()) > 0 {
54+
storageParams, err = pgq.DeparseRelOptions(stmt.GetOptions())
55+
if err != nil {
56+
return nil, fmt.Errorf("parsing options: %w", err)
57+
}
58+
// strip outer parentheses
59+
storageParams = storageParams[1 : len(storageParams)-1]
60+
}
61+
62+
return migrations.Operations{
63+
&migrations.OpCreateIndex{
64+
Table: tableName,
65+
Columns: columns,
66+
Name: stmt.GetIdxname(),
67+
Method: method,
68+
Unique: unique,
69+
Predicate: predicate,
70+
StorageParameters: storageParams,
71+
},
72+
}, nil
73+
}
74+
75+
func canConvertCreateIndexStmt(stmt *pgq.IndexStmt) bool {
76+
// Tablespaces are not supported
77+
if stmt.GetTableSpace() != "" {
78+
return false
79+
}
80+
// Indexes with INCLUDE are not supported
81+
if stmt.GetIndexIncludingParams() != nil {
82+
return false
83+
}
84+
// Indexes created with ONLY are not supported
85+
if !stmt.GetRelation().GetInh() {
86+
return false
87+
}
88+
// Indexes with NULLS NOT DISTINCT are not supported
89+
if stmt.GetNullsNotDistinct() {
90+
return false
91+
}
92+
// IF NOT EXISTS is unsupported
93+
if stmt.GetIfNotExists() {
94+
return false
95+
}
96+
// Indexes defined on expressions are not supported
97+
for _, node := range stmt.GetIndexParams() {
98+
if node.GetIndexElem().GetExpr() != nil {
99+
return false
100+
}
101+
}
102+
103+
for _, param := range stmt.GetIndexParams() {
104+
// Indexes with non-default collations are not supported
105+
if param.GetIndexElem().GetCollation() != nil {
106+
return false
107+
}
108+
// Indexes with non-default ordering are not supported
109+
ordering := param.GetIndexElem().GetOrdering()
110+
if ordering != pgq.SortByDir_SORTBY_DEFAULT && ordering != pgq.SortByDir_SORTBY_ASC {
111+
return false
112+
}
113+
// Indexes with non-default nulls ordering are not supported
114+
if param.GetIndexElem().GetNullsOrdering() != pgq.SortByNulls_SORTBY_NULLS_DEFAULT {
115+
return false
116+
}
117+
// Indexes with opclasses are not supported
118+
if param.GetIndexElem().GetOpclass() != nil || param.GetIndexElem().GetOpclassopts() != nil {
119+
return false
120+
}
121+
}
122+
123+
return true
124+
}

pkg/sql2pgroll/create_index_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package sql2pgroll_test
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/xataio/pgroll/pkg/migrations"
12+
"github.com/xataio/pgroll/pkg/sql2pgroll"
13+
"github.com/xataio/pgroll/pkg/sql2pgroll/expect"
14+
)
15+
16+
func TestConvertCreateIndexStatements(t *testing.T) {
17+
t.Parallel()
18+
19+
tests := []struct {
20+
sql string
21+
expectedOp migrations.Operation
22+
}{
23+
{
24+
sql: "CREATE INDEX idx_name ON foo (bar)",
25+
expectedOp: expect.CreateIndexOp1,
26+
},
27+
{
28+
sql: "CREATE INDEX idx_name ON foo (bar ASC)",
29+
expectedOp: expect.CreateIndexOp1,
30+
},
31+
{
32+
sql: "CREATE INDEX idx_name ON foo USING btree (bar)",
33+
expectedOp: expect.CreateIndexOp1,
34+
},
35+
{
36+
sql: "CREATE INDEX idx_name ON foo USING brin (bar)",
37+
expectedOp: expect.CreateIndexOp1WithMethod("brin"),
38+
},
39+
{
40+
sql: "CREATE INDEX idx_name ON foo USING gin (bar)",
41+
expectedOp: expect.CreateIndexOp1WithMethod("gin"),
42+
},
43+
{
44+
sql: "CREATE INDEX idx_name ON foo USING gist (bar)",
45+
expectedOp: expect.CreateIndexOp1WithMethod("gist"),
46+
},
47+
{
48+
sql: "CREATE INDEX idx_name ON foo USING hash (bar)",
49+
expectedOp: expect.CreateIndexOp1WithMethod("hash"),
50+
},
51+
{
52+
sql: "CREATE INDEX idx_name ON foo USING spgist (bar)",
53+
expectedOp: expect.CreateIndexOp1WithMethod("spgist"),
54+
},
55+
{
56+
sql: "CREATE INDEX CONCURRENTLY idx_name ON foo (bar)",
57+
expectedOp: expect.CreateIndexOp1,
58+
},
59+
{
60+
sql: "CREATE INDEX idx_name ON schema.foo (bar)",
61+
expectedOp: expect.CreateIndexOp2,
62+
},
63+
{
64+
sql: "CREATE INDEX idx_name ON foo (bar, baz)",
65+
expectedOp: expect.CreateIndexOp3,
66+
},
67+
{
68+
sql: "CREATE UNIQUE INDEX idx_name ON foo (bar)",
69+
expectedOp: expect.CreateIndexOp4,
70+
},
71+
{
72+
sql: "CREATE INDEX idx_name ON foo (bar) WHERE (foo > 0)",
73+
expectedOp: expect.CreateIndexOp5,
74+
},
75+
{
76+
sql: "CREATE INDEX idx_name ON foo (bar) WHERE foo > 0",
77+
expectedOp: expect.CreateIndexOp5,
78+
},
79+
{
80+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (fillfactor = 70)",
81+
expectedOp: expect.CreateIndexOpWithStorageParam("fillfactor=70"),
82+
},
83+
{
84+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (deduplicate_items = true)",
85+
expectedOp: expect.CreateIndexOpWithStorageParam("deduplicate_items=true"),
86+
},
87+
{
88+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (buffering = ON)",
89+
expectedOp: expect.CreateIndexOpWithStorageParam("buffering=on"),
90+
},
91+
{
92+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (buffering = OFF)",
93+
expectedOp: expect.CreateIndexOpWithStorageParam("buffering=off"),
94+
},
95+
{
96+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (buffering = AUTO)",
97+
expectedOp: expect.CreateIndexOpWithStorageParam("buffering=auto"),
98+
},
99+
{
100+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (fastupdate = true)",
101+
expectedOp: expect.CreateIndexOpWithStorageParam("fastupdate=true"),
102+
},
103+
{
104+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (pages_per_range = 100)",
105+
expectedOp: expect.CreateIndexOpWithStorageParam("pages_per_range=100"),
106+
},
107+
{
108+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (autosummarize = true)",
109+
expectedOp: expect.CreateIndexOpWithStorageParam("autosummarize=true"),
110+
},
111+
{
112+
sql: "CREATE INDEX idx_name ON foo (bar) WITH (fillfactor = 70, deduplicate_items = true)",
113+
expectedOp: expect.CreateIndexOpWithStorageParam("fillfactor=70, deduplicate_items=true"),
114+
},
115+
}
116+
117+
for _, tc := range tests {
118+
t.Run(tc.sql, func(t *testing.T) {
119+
ops, err := sql2pgroll.Convert(tc.sql)
120+
require.NoError(t, err)
121+
122+
require.Len(t, ops, 1)
123+
124+
assert.Equal(t, tc.expectedOp, ops[0])
125+
})
126+
}
127+
}
128+
129+
func TestUnconvertableCreateIndexStatements(t *testing.T) {
130+
t.Parallel()
131+
132+
tests := []string{
133+
// Tablespaces are not supported
134+
"CREATE INDEX idx_name ON foo (bar) TABLESPACE baz",
135+
// Index collations are not supported
136+
"CREATE INDEX idx_name ON foo (bar COLLATE en_US)",
137+
// Index ordering other than the default ASC is not supported
138+
"CREATE INDEX idx_name ON foo (bar DESC)",
139+
// Index nulls ordering is not supported
140+
"CREATE INDEX idx_name ON foo (bar NULLS FIRST)",
141+
"CREATE INDEX idx_name ON foo (bar NULLS LAST)",
142+
// Included columns are not supported
143+
"CREATE INDEX idx_name ON foo (bar) INCLUDE (baz)",
144+
// opclasses with or without options are not supported
145+
"CREATE INDEX idx_name ON foo (bar opclass (test = test))",
146+
"CREATE INDEX idx_name ON foo (bar opclass)",
147+
// Indexes created with ONLY are not supported
148+
"CREATE INDEX idx_name ON ONLY foo (bar)",
149+
// Indexes with NULLS NOT DISTINCT are not supported
150+
"CREATE INDEX idx_name ON foo(a) NULLS NOT DISTINCT",
151+
// IF NOT EXISTS is unsupported
152+
"CREATE INDEX IF NOT EXISTS idx_name ON foo(a)",
153+
// Indexes defined on expressions are not supported
154+
"CREATE INDEX idx_name ON foo(LOWER(a))",
155+
"CREATE INDEX idx_name ON foo(a, LOWER(b))",
156+
}
157+
158+
for _, sql := range tests {
159+
t.Run(sql, func(t *testing.T) {
160+
ops, err := sql2pgroll.Convert(sql)
161+
require.NoError(t, err)
162+
163+
require.Len(t, ops, 1)
164+
165+
assert.Equal(t, expect.RawSQLOp(sql), ops[0])
166+
})
167+
}
168+
}

pkg/sql2pgroll/expect/create_index.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package expect
4+
5+
import (
6+
"github.com/xataio/pgroll/pkg/migrations"
7+
)
8+
9+
var CreateIndexOp1 = &migrations.OpCreateIndex{
10+
Name: "idx_name",
11+
Table: "foo",
12+
Columns: []string{"bar"},
13+
Method: migrations.OpCreateIndexMethodBtree,
14+
}
15+
16+
func CreateIndexOp1WithMethod(method string) *migrations.OpCreateIndex {
17+
parsed, err := migrations.ParseCreateIndexMethod(method)
18+
if err != nil {
19+
panic(err)
20+
}
21+
return &migrations.OpCreateIndex{
22+
Name: "idx_name",
23+
Table: "foo",
24+
Columns: []string{"bar"},
25+
Method: parsed,
26+
}
27+
}
28+
29+
var CreateIndexOp2 = &migrations.OpCreateIndex{
30+
Name: "idx_name",
31+
Table: "schema.foo",
32+
Columns: []string{"bar"},
33+
Method: migrations.OpCreateIndexMethodBtree,
34+
}
35+
36+
var CreateIndexOp3 = &migrations.OpCreateIndex{
37+
Name: "idx_name",
38+
Table: "foo",
39+
Columns: []string{"bar", "baz"},
40+
Method: migrations.OpCreateIndexMethodBtree,
41+
}
42+
43+
var CreateIndexOp4 = &migrations.OpCreateIndex{
44+
Name: "idx_name",
45+
Table: "foo",
46+
Columns: []string{"bar"},
47+
Method: migrations.OpCreateIndexMethodBtree,
48+
Unique: true,
49+
}
50+
51+
var CreateIndexOp5 = &migrations.OpCreateIndex{
52+
Name: "idx_name",
53+
Table: "foo",
54+
Columns: []string{"bar"},
55+
Method: migrations.OpCreateIndexMethodBtree,
56+
Predicate: "foo > 0",
57+
}
58+
59+
func CreateIndexOpWithStorageParam(param string) *migrations.OpCreateIndex {
60+
return &migrations.OpCreateIndex{
61+
Name: "idx_name",
62+
Table: "foo",
63+
Columns: []string{"bar"},
64+
Method: migrations.OpCreateIndexMethodBtree,
65+
StorageParameters: param,
66+
}
67+
}

0 commit comments

Comments
 (0)