Skip to content

Commit 93579c8

Browse files
authored
fix: support storing columns for indices (#485)
* test: add system tests * test: run system tests on prod * build: allow any Python version for sys tests * build: keep instance and create new databases instead * chore: format code * fix: do not use static fallback config * fix: cleanup job * fix: search until end of string * build: only run system tests on the emulator for presubmits * build: skip system tests when skipping conformance tests * test: run tests with Python 3.8 * test: try this * test: no tests * fix: support storing columns for indices * fix: split key and non-key columns * build: run system tests on real Spanner * fix: add include_columns as none * fix: remove unused test * fix: use lower case for asc/desc * test: expect lower case desc
1 parent 2106987 commit 93579c8

File tree

3 files changed

+99
-15
lines changed

3 files changed

+99
-15
lines changed

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

+56-9
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,26 @@ def post_create_table(self, table):
492492

493493
return post_cmds
494494

495+
def visit_create_index(
496+
self, create, include_schema=False, include_table_schema=True, **kw
497+
):
498+
text = super().visit_create_index(
499+
create, include_schema, include_table_schema, **kw
500+
)
501+
index = create.element
502+
if "spanner" in index.dialect_options:
503+
options = index.dialect_options["spanner"]
504+
if "storing" in options:
505+
storing = options["storing"]
506+
storing_columns = [
507+
index.table.c[col] if isinstance(col, str) else col
508+
for col in storing
509+
]
510+
text += " STORING (%s)" % ", ".join(
511+
[self.preparer.quote(c.name) for c in storing_columns]
512+
)
513+
return text
514+
495515
def get_identity_options(self, identity_options):
496516
text = ["sequence_kind = 'bit_reversed_positive'"]
497517
if identity_options.start is not None:
@@ -997,15 +1017,35 @@ def get_multi_indexes(
9971017
i.table_schema,
9981018
i.table_name,
9991019
i.index_name,
1000-
ARRAY_AGG(ic.column_name),
1020+
(
1021+
SELECT ARRAY_AGG(ic.column_name)
1022+
FROM information_schema.index_columns ic
1023+
WHERE ic.index_name = i.index_name
1024+
AND ic.table_catalog = i.table_catalog
1025+
AND ic.table_schema = i.table_schema
1026+
AND ic.table_name = i.table_name
1027+
AND ic.column_ordering is not null
1028+
) as columns,
10011029
i.is_unique,
1002-
ARRAY_AGG(ic.column_ordering)
1030+
(
1031+
SELECT ARRAY_AGG(ic.column_ordering)
1032+
FROM information_schema.index_columns ic
1033+
WHERE ic.index_name = i.index_name
1034+
AND ic.table_catalog = i.table_catalog
1035+
AND ic.table_schema = i.table_schema
1036+
AND ic.table_name = i.table_name
1037+
AND ic.column_ordering is not null
1038+
) as column_orderings,
1039+
(
1040+
SELECT ARRAY_AGG(storing.column_name)
1041+
FROM information_schema.index_columns storing
1042+
WHERE storing.index_name = i.index_name
1043+
AND storing.table_catalog = i.table_catalog
1044+
AND storing.table_schema = i.table_schema
1045+
AND storing.table_name = i.table_name
1046+
AND storing.column_ordering is null
1047+
) as storing_columns,
10031048
FROM information_schema.indexes as i
1004-
JOIN information_schema.index_columns AS ic
1005-
ON ic.index_name = i.index_name
1006-
AND ic.table_catalog = i.table_catalog
1007-
AND ic.table_schema = i.table_schema
1008-
AND ic.table_name = i.table_name
10091049
JOIN information_schema.tables AS t
10101050
ON i.table_catalog = t.table_catalog
10111051
AND i.table_schema = t.table_schema
@@ -1016,7 +1056,8 @@ def get_multi_indexes(
10161056
{schema_filter_query}
10171057
i.index_type != 'PRIMARY_KEY'
10181058
AND i.spanner_is_managed = FALSE
1019-
GROUP BY i.table_schema, i.table_name, i.index_name, i.is_unique
1059+
GROUP BY i.table_catalog, i.table_schema, i.table_name,
1060+
i.index_name, i.is_unique
10201061
ORDER BY i.index_name
10211062
""".format(
10221063
table_filter_query=table_filter_query,
@@ -1029,13 +1070,19 @@ def get_multi_indexes(
10291070
result_dict = {}
10301071

10311072
for row in rows:
1073+
dialect_options = {}
1074+
include_columns = row[6]
1075+
if include_columns:
1076+
dialect_options["spanner_storing"] = include_columns
10321077
index_info = {
10331078
"name": row[2],
10341079
"column_names": row[3],
10351080
"unique": row[4],
10361081
"column_sorting": {
1037-
col: order for col, order in zip(row[3], row[5])
1082+
col: order.lower() for col, order in zip(row[3], row[5])
10381083
},
1084+
"include_columns": include_columns if include_columns else [],
1085+
"dialect_options": dialect_options,
10391086
}
10401087
row[0] = row[0] or None
10411088
table_info = result_dict.get((row[0], row[1]), [])

test/system/test_basics.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,66 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from sqlalchemy import text, Table, Column, Integer, PrimaryKeyConstraint, String
14+
15+
from sqlalchemy import (
16+
text,
17+
Table,
18+
Column,
19+
Integer,
20+
PrimaryKeyConstraint,
21+
String,
22+
Index,
23+
MetaData,
24+
Boolean,
25+
)
1526
from sqlalchemy.testing import eq_
1627
from sqlalchemy.testing.plugin.plugin_base import fixtures
1728

1829

1930
class TestBasics(fixtures.TablesTest):
2031
@classmethod
2132
def define_tables(cls, metadata):
22-
Table(
33+
numbers = Table(
2334
"numbers",
2435
metadata,
2536
Column("number", Integer),
2637
Column("name", String(20)),
38+
Column("alternative_name", String(20)),
39+
Column("prime", Boolean),
2740
PrimaryKeyConstraint("number"),
2841
)
42+
Index(
43+
"idx_numbers_name",
44+
numbers.c.name,
45+
numbers.c.prime.desc(),
46+
spanner_storing=[numbers.c.alternative_name],
47+
)
2948

3049
def test_hello_world(self, connection):
3150
greeting = connection.execute(text("select 'Hello World'"))
3251
eq_("Hello World", greeting.fetchone()[0])
3352

3453
def test_insert_number(self, connection):
3554
connection.execute(
36-
text("insert or update into numbers(number, name) values (1, 'One')")
55+
text(
56+
"""insert or update into numbers (number, name, prime)
57+
values (1, 'One', false)"""
58+
)
3759
)
3860
name = connection.execute(text("select name from numbers where number=1"))
3961
eq_("One", name.fetchone()[0])
62+
63+
def test_reflect(self, connection):
64+
engine = connection.engine
65+
meta: MetaData = MetaData()
66+
meta.reflect(bind=engine)
67+
eq_(1, len(meta.tables))
68+
table = meta.tables["numbers"]
69+
eq_(1, len(table.indexes))
70+
index = next(iter(table.indexes))
71+
eq_(2, len(index.columns))
72+
eq_("name", index.columns[0].name)
73+
eq_("prime", index.columns[1].name)
74+
dialect_options = index.dialect_options["spanner"]
75+
eq_(1, len(dialect_options["storing"]))
76+
eq_("alternative_name", dialect_options["storing"][0])

test/test_suite_20.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ def idx(
14141414
"include_columns": [],
14151415
}
14161416
if column_sorting:
1417-
res["column_sorting"] = {"q": "DESC"}
1417+
res["column_sorting"] = {"q": "desc"}
14181418
if duplicates:
14191419
res["duplicates_constraint"] = name
14201420
return [res]
@@ -1458,11 +1458,11 @@ def idx(
14581458
*idx(
14591459
"q",
14601460
name="noncol_idx_nopk",
1461-
column_sorting={"q": "DESC"},
1461+
column_sorting={"q": "desc"},
14621462
)
14631463
],
14641464
(schema, "noncol_idx_test_pk"): [
1465-
*idx("q", name="noncol_idx_pk", column_sorting={"q": "DESC"})
1465+
*idx("q", name="noncol_idx_pk", column_sorting={"q": "desc"})
14661466
],
14671467
(schema, self.temp_table_name()): [
14681468
*idx("foo", name="user_tmp_ix"),

0 commit comments

Comments
 (0)