Skip to content
This repository was archived by the owner on Mar 13, 2026. It is now read-only.

Commit 02b4674

Browse files
author
Ilya Gurov
authored
feat: support interleaved tables feature (#85)
1 parent c0d39cc commit 02b4674

File tree

3 files changed

+95
-1
lines changed

3 files changed

+95
-1
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,33 @@ A migration script can produce a lot of DDL statements. If each of the statement
9797

9898
Features and limitations
9999
-----------
100+
**Interleaved tables**
101+
Cloud Spanner dialect includes two dialect-specific arguments for `Table` constructor, which help to define interleave relations:
102+
`spanner_interleave_in` - a parent table name
103+
`spanner_inverleave_on_delete_cascade` - a flag specifying if `ON DELETE CASCADE` statement must be used for the interleave relation
104+
An example of interleave relations definition:
105+
```python
106+
team = Table(
107+
"team",
108+
metadata,
109+
Column("team_id", Integer, primary_key=True),
110+
Column("team_name", String(16), nullable=False),
111+
)
112+
team.create(engine)
113+
114+
client = Table(
115+
"client",
116+
metadata,
117+
Column("team_id", Integer, primary_key=True),
118+
Column("client_id", Integer, primary_key=True),
119+
Column("client_name", String(16), nullable=False),
120+
spanner_interleave_in="team",
121+
spanner_interleave_on_delete_cascade=True,
122+
)
123+
124+
client.create(engine)
125+
```
126+
100127
**Unique constraints**
101128
Cloud Spanner doesn't support direct UNIQUE constraints creation. In order to achieve column values uniqueness UNIQUE indexes should be used.
102129

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,26 @@ def visit_unique_constraint(self, constraint):
287287
def post_create_table(self, table):
288288
"""Build statements to be executed after CREATE TABLE.
289289
290+
Includes "primary key" and "interleaved table" statements generation.
291+
290292
Args:
291293
table (sqlalchemy.schema.Table): Table to create.
292294
293295
Returns:
294296
str: primary key difinition to add to the table CREATE request.
295297
"""
296298
cols = [col.name for col in table.primary_key.columns]
299+
post_cmds = " PRIMARY KEY ({})".format(", ".join(cols))
300+
301+
if table.kwargs.get("spanner_interleave_in"):
302+
post_cmds += ",\nINTERLEAVE IN PARENT {}".format(
303+
table.kwargs["spanner_interleave_in"]
304+
)
305+
306+
if table.kwargs.get("spanner_inverleave_on_delete_cascade"):
307+
post_cmds += " ON DELETE CASCADE"
297308

298-
return " PRIMARY KEY ({})".format(", ".join(cols))
309+
return post_cmds
299310

300311

301312
class SpannerTypeCompiler(GenericTypeCompiler):

test/test_suite.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import pytest
1919
import decimal
2020
import pytz
21+
from unittest import mock
2122

2223
import sqlalchemy
2324
from sqlalchemy import create_engine
@@ -1436,3 +1437,58 @@ class Address(Base):
14361437
query = query.join(Address)
14371438

14381439
assert str(query.statement.compile(session.bind)) == EXPECTED_QUERY
1440+
1441+
1442+
class InterleavedTablesTest(fixtures.TestBase):
1443+
"""
1444+
Check that CREATE TABLE statements for interleaved tables are correctly
1445+
generated.
1446+
"""
1447+
1448+
def setUp(self):
1449+
self._engine = create_engine(
1450+
"spanner:///projects/appdev-soda-spanner-staging/instances/"
1451+
"sqlalchemy-dialect-test/databases/compliance-test"
1452+
)
1453+
self._metadata = MetaData(bind=self._engine)
1454+
1455+
def test_interleave(self):
1456+
EXP_QUERY = (
1457+
"\nCREATE TABLE client (\n\tteam_id INT64 NOT NULL, "
1458+
"\n\tclient_id INT64 NOT NULL, "
1459+
"\n\tclient_name STRING(16) NOT NULL"
1460+
"\n) PRIMARY KEY (team_id, client_id),"
1461+
"\nINTERLEAVE IN PARENT team\n\n"
1462+
)
1463+
client = Table(
1464+
"client",
1465+
self._metadata,
1466+
Column("team_id", Integer, primary_key=True),
1467+
Column("client_id", Integer, primary_key=True),
1468+
Column("client_name", String(16), nullable=False),
1469+
spanner_interleave_in="team",
1470+
)
1471+
with mock.patch("google.cloud.spanner_dbapi.cursor.Cursor.execute") as execute:
1472+
client.create(self._engine)
1473+
execute.assert_called_once_with(EXP_QUERY, [])
1474+
1475+
def test_interleave_on_delete_cascade(self):
1476+
EXP_QUERY = (
1477+
"\nCREATE TABLE client (\n\tteam_id INT64 NOT NULL, "
1478+
"\n\tclient_id INT64 NOT NULL, "
1479+
"\n\tclient_name STRING(16) NOT NULL"
1480+
"\n) PRIMARY KEY (team_id, client_id),"
1481+
"\nINTERLEAVE IN PARENT team ON DELETE CASCADE\n\n"
1482+
)
1483+
client = Table(
1484+
"client",
1485+
self._metadata,
1486+
Column("team_id", Integer, primary_key=True),
1487+
Column("client_id", Integer, primary_key=True),
1488+
Column("client_name", String(16), nullable=False),
1489+
spanner_interleave_in="team",
1490+
spanner_inverleave_on_delete_cascade=True,
1491+
)
1492+
with mock.patch("google.cloud.spanner_dbapi.cursor.Cursor.execute") as execute:
1493+
client.create(self._engine)
1494+
execute.assert_called_once_with(EXP_QUERY, [])

0 commit comments

Comments
 (0)