Skip to content

Commit 0ba5f14

Browse files
authored
Merge pull request #14 from CloverHealth/joined-table-inheritance
Joined table inheritance
2 parents f14273b + 7d63bbd commit 0ba5f14

File tree

7 files changed

+151
-25
lines changed

7 files changed

+151
-25
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
sqlalchemy==1.0.15
1+
sqlalchemy==1.1.2
22
psycopg2==2.6.2

temporal_sqlalchemy/clock.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def make_temporal(cls: nine.Type[Clocked]):
124124
clock_table_name = _truncate_identifier("%s_clock" % entity_table_name)
125125

126126
history_tables = {
127-
p: build_history_class(p, schema)
127+
p: build_history_class(cls, p, schema)
128128
for p in local_props | relationship_props
129129
}
130130

@@ -221,12 +221,12 @@ def build_clock_class(
221221

222222

223223
def build_history_class(
224+
cls: declarative.DeclarativeMeta,
224225
prop: T_PROPS,
225226
schema: str = None) -> nine.Type[TemporalProperty]:
226227
"""build a sql alchemy table for given prop"""
227-
cls = prop.parent.class_
228228
class_name = "%s%s_%s" % (cls.__name__, 'History', prop.key)
229-
table = build_history_table(prop, schema)
229+
table = build_history_table(cls, prop, schema)
230230
base_classes = (
231231
TemporalProperty,
232232
declarative.declarative_base(metadata=cls.metadata),
@@ -252,7 +252,10 @@ def build_history_class(
252252
return model
253253

254254

255-
def build_history_table(prop: T_PROPS, schema: str = None) -> sa.Table:
255+
def build_history_table(
256+
cls: declarative.DeclarativeMeta,
257+
prop: T_PROPS,
258+
schema: str = None) -> sa.Table:
256259
"""build a sql alchemy table for given prop"""
257260

258261
if isinstance(prop, orm.RelationshipProperty):
@@ -267,7 +270,7 @@ def build_history_table(prop: T_PROPS, schema: str = None) -> sa.Table:
267270
property_key = prop.key
268271
columns = (_copy_column(col) for col in prop.columns)
269272

270-
local_table = prop.parent.local_table
273+
local_table = cls.__table__
271274
table_name = _truncate_identifier(
272275
'%s_%s_%s' % (local_table.name, 'history', property_key))
273276
index_name = _truncate_identifier('%s_effective_idx' % table_name)
@@ -289,7 +292,7 @@ def build_history_table(prop: T_PROPS, schema: str = None) -> sa.Table:
289292
]
290293

291294
# TODO: make this support different shape pks
292-
foreign_key = getattr(prop.parent.class_, 'id')
295+
foreign_key = getattr(cls, 'id')
293296
return sa.Table(
294297
table_name,
295298
prop.parent.class_.metadata,

temporal_sqlalchemy/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Version information."""
2-
__version__ = '0.1.0'
2+
__version__ = '0.2.0'

tests/models.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,42 @@ class HugeIndices(temporal_sqlalchemy.Clocked, EdgeCaseBase):
165165

166166
id = auto_uuid()
167167
really_really_really_really_really_long_column = sa.Column(sa.Integer)
168+
169+
170+
class JoinedEnumBase(Base):
171+
__tablename__ = 'joined_enum_base'
172+
173+
id = auto_uuid()
174+
kind = sa.Column(
175+
sap.ENUM('default', 'enum_a', 'enum_b', name='joined_enum_kind'))
176+
is_deleted = sa.Column(sa.Boolean, default=False)
177+
178+
__mapper_args__ = {
179+
'polymorphic_on': kind,
180+
'polymorphic_identity': 'default'
181+
}
182+
183+
184+
@temporal_sqlalchemy.add_clock(
185+
'val',
186+
'is_deleted',
187+
temporal_schema=TEMPORAL_SCHEMA)
188+
class JoinedEnumA(temporal_sqlalchemy.Clocked, JoinedEnumBase):
189+
__tablename__ = 'joined_enum_a'
190+
191+
id = sa.Column(sa.ForeignKey(JoinedEnumBase.id), primary_key=True)
192+
val = sa.Column(sap.ENUM('foo', 'foobar', name='joined_enum_a_val'))
193+
194+
__mapper_args__ = {'polymorphic_identity': 'enum_a'}
195+
196+
197+
@temporal_sqlalchemy.add_clock(
198+
'val',
199+
'is_deleted', temporal_schema=TEMPORAL_SCHEMA)
200+
class JoinedEnumB(temporal_sqlalchemy.Clocked, JoinedEnumBase):
201+
__tablename__ = 'joined_enum_b'
202+
203+
id = sa.Column(sa.ForeignKey(JoinedEnumBase.id), primary_key=True)
204+
val = sa.Column(sap.ENUM('bar', 'barfoo', name='joined_enum_b_val'))
205+
206+
__mapper_args__ = {'polymorphic_identity': 'enum_b'}

tests/test_builders.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ def test_build_history_table():
1111
rel_id_prop = sa.inspect(models.RelationalTemporalModel.rel_id).property
1212
rel_prop = sa.inspect(models.RelationalTemporalModel.rel).property
1313

14-
history_table = build_history_table(rel_id_prop, models.TEMPORAL_SCHEMA)
14+
history_table = build_history_table(
15+
models.RelationalTemporalModel, rel_id_prop, models.TEMPORAL_SCHEMA)
1516

1617
assert history_table == build_history_table(
17-
rel_prop, models.TEMPORAL_SCHEMA)
18+
models.RelationalTemporalModel,
19+
rel_prop,
20+
models.TEMPORAL_SCHEMA)
1821
assert history_table.name == 'relational_temporal_history_rel_id'
1922
assert history_table.schema == models.TEMPORAL_SCHEMA
2023
assert history_table.c.keys() == [
@@ -26,8 +29,8 @@ def test_build_history_class():
2629
rel_id_prop = sa.inspect(models.SimpleTable.rel_id).property
2730
rel_prop = sa.inspect(models.SimpleTable.rel).property
2831

29-
rel_id_prop_class = build_history_class(rel_id_prop)
30-
rel_prop_class = build_history_class(rel_prop)
32+
rel_id_prop_class = build_history_class(models.SimpleTable, rel_id_prop)
33+
rel_prop_class = build_history_class(models.SimpleTable, rel_prop)
3134

3235
assert rel_id_prop_class.__table__ == rel_prop_class.__table__
3336
assert rel_id_prop_class.__name__ == 'SimpleTableHistory_rel_id'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import pytest
2+
import sqlalchemy as sa
3+
4+
import temporal_sqlalchemy as temporal
5+
6+
from . import models
7+
8+
9+
@pytest.fixture(autouse=True)
10+
def setup(session):
11+
"""Generate schema and tables from a list of model bases."""
12+
for schema in models.basic_metadata._schemas:
13+
session.execute('CREATE SCHEMA IF NOT EXISTS %s' % schema)
14+
15+
models.basic_metadata.create_all(session.bind)
16+
17+
18+
def test_joined_enums_create(session):
19+
session.add_all([
20+
models.JoinedEnumA(val='foo'),
21+
models.JoinedEnumA(val='foobar'),
22+
models.JoinedEnumB(val='bar'),
23+
models.JoinedEnumB(val='barfoo'),
24+
models.JoinedEnumB(val='barfoo'),
25+
])
26+
27+
session.commit()
28+
29+
assert session.query(models.JoinedEnumBase).count() == 5
30+
31+
enum_a_val_history = temporal.get_history_model(models.JoinedEnumA.val)
32+
assert session.query(enum_a_val_history).count() == 2
33+
34+
enum_b_val_history = temporal.get_history_model(models.JoinedEnumB.val)
35+
assert session.query(enum_b_val_history).count() == 3
36+
37+
38+
@pytest.mark.parametrize("model,first_val,second_val", (
39+
(models.JoinedEnumA, 'foo', 'foobar'),
40+
(models.JoinedEnumA, 'foobar', 'foo'),
41+
(models.JoinedEnumB, 'bar', 'barfoo'),
42+
))
43+
def test_joined_enums_edit(session, model, first_val, second_val):
44+
kind = sa.inspect(model).polymorphic_identity
45+
46+
session.add(model(val=first_val, is_deleted=False))
47+
session.commit()
48+
49+
entity = session.query(model).first()
50+
entity_id = entity.id
51+
with entity.clock_tick():
52+
entity.is_deleted = True
53+
entity.val = second_val
54+
55+
session.commit()
56+
session.expunge_all() # empty the session
57+
58+
# query directly from the model
59+
entity = session.query(model).get(entity_id)
60+
assert entity.vclock == 2
61+
assert entity.val == second_val
62+
assert entity.kind == kind
63+
assert entity.is_deleted is True
64+
65+
# query via with_polymorphic
66+
entity = session.query(models.JoinedEnumBase)\
67+
.with_polymorphic(model).first()
68+
assert entity.vclock == 2
69+
assert entity.val == second_val
70+
assert entity.kind == kind
71+
assert entity.is_deleted is True

tests/test_temporal_multi_entity_with_activity.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import pytest
2-
import sqlalchemy.exc as exc
3-
import sqlalchemy.orm.attributes as orm_attr
42

53
import temporal_sqlalchemy as temporal
64

@@ -11,7 +9,7 @@ class TestTemporalMultiEntityWithActivity(shared.DatabaseTest):
119
@pytest.fixture(autouse=True)
1210
def setup(self, session):
1311
models.activity_metadata.create_all(session.bind)
14-
12+
1513
def test_activity_on_multi_entity_create(self, session):
1614
activity = models.Activity(description='Create temps')
1715
session.add(activity)
@@ -42,9 +40,11 @@ def test_activity_on_multi_entity_create(self, session):
4240
assert t2.vclock == 1
4341
assert t2.clock.count() == 1
4442

45-
clock1_query = session.query(models.FirstTemporalWithActivity.temporal_options.clock_table)
43+
clock1_query = session.query(
44+
models.FirstTemporalWithActivity.temporal_options.clock_table)
4645
assert clock1_query.count() == 1
47-
clock2_query = session.query(models.SecondTemporalWithActivity.temporal_options.clock_table)
46+
clock2_query = session.query(
47+
models.SecondTemporalWithActivity.temporal_options.clock_table)
4848
assert clock2_query.count() == 1
4949

5050
clock1_result = clock1_query.first()
@@ -57,8 +57,10 @@ def test_activity_on_multi_entity_edit(self, session):
5757
create_activity = models.Activity(description='Create temp')
5858
session.add(create_activity)
5959

60-
t1 = models.FirstTemporalWithActivity(column=1234, activity=create_activity)
61-
t2 = models.SecondTemporalWithActivity(column=4567, activity=create_activity)
60+
t1 = models.FirstTemporalWithActivity(
61+
column=1234, activity=create_activity)
62+
t2 = models.SecondTemporalWithActivity(
63+
column=4567, activity=create_activity)
6264
session.add(t1)
6365
session.add(t2)
6466
session.commit()
@@ -77,7 +79,9 @@ def test_activity_on_multi_entity_edit(self, session):
7779
activity_query = session.query(models.Activity)
7880
assert activity_query.count() == 2
7981

80-
create_activity_result = activity_query.order_by(models.Activity.date_created).first()
82+
create_activity_result = activity_query\
83+
.order_by(models.Activity.date_created)\
84+
.first()
8185
assert create_activity_result.description == 'Create temp'
8286

8387
activity_clock2_backref = temporal.get_activity_clock_backref(
@@ -88,7 +92,9 @@ def test_activity_on_multi_entity_edit(self, session):
8892
assert getattr(create_activity_result, activity_clock2_backref.key)
8993
assert getattr(create_activity_result, activity_clock1_backref.key)
9094

91-
edit_activity_result = activity_query.order_by(models.Activity.date_created.desc()).first()
95+
edit_activity_result = activity_query\
96+
.order_by(models.Activity.date_created.desc())\
97+
.first()
9298
assert edit_activity_result.description == 'Edit temp'
9399

94100
assert getattr(edit_activity, activity_clock2_backref.key)
@@ -102,15 +108,19 @@ def test_activity_on_multi_entity_edit(self, session):
102108
assert t2.vclock == 2
103109
assert t2.clock.count() == 2
104110

105-
clock1_query = session.query(models.FirstTemporalWithActivity.temporal_options.clock_table)\
106-
.order_by(models.FirstTemporalWithActivity.temporal_options.clock_table.tick).all()
111+
clock_model = models.FirstTemporalWithActivity.temporal_options\
112+
.clock_table
113+
clock1_query = session.query(clock_model)\
114+
.order_by(clock_model.tick).all()
107115
assert len(clock1_query) == 2
108116

109117
assert clock1_query[0].activity_id == create_activity_result.id
110118
assert clock1_query[1].activity_id == edit_activity_result.id
111119

112-
clock2_query = session.query(models.SecondTemporalWithActivity.temporal_options.clock_table)\
113-
.order_by(models.SecondTemporalWithActivity.temporal_options.clock_table.tick).all()
120+
clock_model = models.SecondTemporalWithActivity.temporal_options\
121+
.clock_table
122+
clock2_query = session.query(clock_model)\
123+
.order_by(clock_model.tick).all()
114124
assert len(clock2_query) == 2
115125

116126
assert clock2_query[0].activity_id == create_activity_result.id

0 commit comments

Comments
 (0)