Skip to content

Commit

Permalink
Fix issue #26: Remove compound primary keys on temporal clock tables (#…
Browse files Browse the repository at this point in the history
…31)

* Fix issue #26: Remove compound primary keys on temporal clock tables (#31)
* Bump version to 0.3.3
  • Loading branch information
bijanvakili authored Jun 22, 2017
1 parent 3b44746 commit 426b303
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 21 deletions.
8 changes: 5 additions & 3 deletions temporal_sqlalchemy/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import contextlib
import datetime as dt
import typing
import uuid
import warnings

import sqlalchemy as sa
import sqlalchemy.dialects.postgresql as sap
import sqlalchemy.orm as orm
import sqlalchemy.orm.attributes as attributes
import psycopg2.extras as psql_extras
Expand All @@ -21,9 +23,9 @@


class EntityClock(object):
tick = sa.Column(sa.Integer, primary_key=True, autoincrement=False)
timestamp = sa.Column(sa.DateTime(True),
server_default=sa.func.current_timestamp())
id = sa.Column(sap.UUID(as_uuid=True), default=uuid.uuid4, primary_key=True)
tick = sa.Column(sa.Integer, nullable=False)
timestamp = sa.Column(sa.DateTime(True), server_default=sa.func.current_timestamp())


class TemporalProperty(object):
Expand Down
22 changes: 14 additions & 8 deletions temporal_sqlalchemy/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,24 +131,30 @@ def make_temporal(cls: nine.Type[Clocked]):

clock_properties = dict(
__tablename__=clock_table_name,
# todo support different shape PKs
entity_id=sa.Column(sa.ForeignKey(cls.id), primary_key=True),

entity_id=sa.Column(sa.ForeignKey(cls.id), nullable=False),
entity=orm.relationship(
cls, backref=orm.backref("clock", lazy='dynamic')),
)

tick_entity_constraint_name = truncate_identifier(
'%s_tick_entity_id_key' % clock_table_name
)
table_args = [
sa.UniqueConstraint('tick', 'entity_id',
name=tick_entity_constraint_name)
]

if activity_cls is not None:
backref_name = '%s_clock' % entity_table_name
clock_properties['activity_id'] = sa.Column(
sa.ForeignKey(activity_cls.id), nullable=False)
clock_properties['activity'] = orm.relationship(
activity_cls, backref=backref_name)
clock_properties['__table_args__'] = (
sa.UniqueConstraint('entity_id', 'activity_id'),
{'schema': schema}
)
else:
clock_properties['__table_args__'] = {'schema': schema}
table_args.append(sa.UniqueConstraint('entity_id', 'activity_id'))

table_args.append({'schema': schema})
clock_properties['__table_args__'] = tuple(table_args)

clock_table = build_clock_class(
cls.__name__, cls.metadata, clock_properties)
Expand Down
19 changes: 17 additions & 2 deletions temporal_sqlalchemy/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import uuid

import sqlalchemy as sa
import sqlalchemy.dialects.postgresql as sap
import sqlalchemy.ext.declarative as declarative
import sqlalchemy.orm as orm
import sqlalchemy.event as event
Expand All @@ -17,12 +20,16 @@ def build_clock_table(entity_table: sa.Table,
clock_table = sa.Table(
clock_table_name,
metadata,
sa.Column('id',
sap.UUID(as_uuid=True),
default=uuid.uuid4,
primary_key=True),
sa.Column('tick',
sa.Integer,
primary_key=True,
autoincrement=False),
nullable=False),
sa.Column('timestamp',
sa.DateTime(True),
nullable=False,
server_default=sa.func.current_timestamp()),
schema=schema)

Expand All @@ -32,6 +39,14 @@ def build_clock_table(entity_table: sa.Table,
clock_table.append_column(fk)
entity_keys.add(fk.key)

# ensure we have DB constraint on tick <> entity uniqueness
tick_entity_constraint_name = clock.truncate_identifier(
'%s_tick_entity_id_key' % clock_table_name
)
clock_table.append_constraint(
sa.UniqueConstraint(*(set(['tick']) | entity_keys), name=tick_entity_constraint_name)
)

if activity_class:
activity_keys = set()
# support arbitrary shaped activity primary keys
Expand Down
2 changes: 1 addition & 1 deletion temporal_sqlalchemy/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Version information."""
__version__ = '0.3.2'
__version__ = '0.3.3'
17 changes: 17 additions & 0 deletions tests/test_builders.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sqlalchemy as sa
from sqlalchemy.inspection import inspect as sa_inspect

import temporal_sqlalchemy as temporal
from temporal_sqlalchemy.clock import (
build_history_table, build_history_class, build_clock_class)
from temporal_sqlalchemy.core import TemporalModel

from . import models

Expand Down Expand Up @@ -37,9 +39,24 @@ def test_build_history_class():
assert hasattr(rel_id_prop_class, 'entity')


def test_build_clock_table():
clock_table = TemporalModel.build_clock_table(
models.RelationalTemporalModel.__table__,
sa.MetaData(),
models.TEMPORAL_SCHEMA
)

assert clock_table.name == 'relational_temporal_clock'
assert clock_table.schema == models.TEMPORAL_SCHEMA
assert clock_table.c.keys() == ['id', 'tick', 'timestamp', 'entity_id']


def test_build_clock_class():
clock = build_clock_class(
'Testing', sa.MetaData(), {'__tablename__': 'test'})

assert clock.__name__ == 'TestingClock'
assert issubclass(clock, temporal.EntityClock)

actual_primary_keys = [k.name for k in sa_inspect(clock).primary_key]
assert actual_primary_keys == ['id']
31 changes: 24 additions & 7 deletions tests/test_temporal_model_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
import sqlalchemy as sa
import sqlalchemy.exc as sa_exc
import temporal_sqlalchemy as temporal

from . import shared, models
Expand Down Expand Up @@ -37,7 +38,7 @@ def test_create_temporal_options():
schema='bare_table_test_schema'
),
'bare_table_single_pk_no_activity_clock',
{'tick', 'timestamp', 'entity_id'},
{'id', 'tick', 'timestamp', 'entity_id'},
None
),
(
Expand All @@ -50,7 +51,7 @@ def test_create_temporal_options():
schema='bare_table_test_schema'
),
'bare_table_compositve_pk_no_activity_clock',
{'tick', 'timestamp', 'entity_num_id', 'entity_text_id'},
{'id', 'tick', 'timestamp', 'entity_num_id', 'entity_text_id'},
None
),
(
Expand All @@ -62,7 +63,7 @@ def test_create_temporal_options():
schema='bare_table_test_schema'
),
'bare_table_single_pk_with_activity_clock',
{'tick', 'timestamp', 'entity_id', 'activity_id'},
{'id', 'tick', 'timestamp', 'entity_id', 'activity_id'},
models.Activity
),
(
Expand All @@ -75,7 +76,7 @@ def test_create_temporal_options():
schema='bare_table_test_schema'
),
'bare_table_compositve_pk_with_activity_clock',
{'tick', 'timestamp', 'entity_num_id', 'entity_text_id', 'activity_id'},
{'id', 'tick', 'timestamp', 'entity_num_id', 'entity_text_id', 'activity_id'},
models.Activity
)
))
Expand Down Expand Up @@ -189,7 +190,7 @@ def test_date_created(self, session, newstylemodel):
session.add(newstylemodel)
session.commit()

tick = session.query(clock_model).get((1, newstylemodel.id))
tick = session.query(clock_model).filter_by(tick=1, entity_id=newstylemodel.id).one()
assert newstylemodel.vclock == 1
assert newstylemodel.clock.count() == 1
assert newstylemodel.date_created == tick.timestamp
Expand All @@ -199,7 +200,7 @@ def test_date_modified(self, session, newstylemodel):
session.add(newstylemodel)
session.commit()

first_tick = session.query(clock_model).get((1, newstylemodel.id))
first_tick = session.query(clock_model).filter_by(tick=1, entity_id=newstylemodel.id).one()
assert newstylemodel.vclock == 1
assert newstylemodel.clock.count() == 1
assert newstylemodel.date_created == first_tick.timestamp
Expand All @@ -210,7 +211,7 @@ def test_date_modified(self, session, newstylemodel):

session.commit()

second_tick = session.query(clock_model).get((1, newstylemodel.id))
second_tick = session.query(clock_model).filter_by(tick=1, entity_id=newstylemodel.id).one()
assert newstylemodel.vclock == 2
assert newstylemodel.clock.count() == 2
assert newstylemodel.date_created == first_tick.timestamp
Expand Down Expand Up @@ -257,3 +258,19 @@ def test_clock_tick_editing(self, session, newstylemodel):
session.query(history_model)
.order_by(history_model.vclock.desc()).first())
assert clock.tick in history.vclock

def test_disallaw_same_tick_for_same_entity(self, session, newstylemodel):
clock_model = models.NewStyleModel.temporal_options.clock_model

session.add(newstylemodel)
session.commit()

first_tick = session.query(clock_model).first()
duplicate_tick = clock_model(
tick=first_tick.tick,
entity_id=first_tick.entity_id,
activity=models.Activity(description="Inserting a duplicate"),
)
session.add(duplicate_tick)
with pytest.raises(sa_exc.IntegrityError):
session.commit()

0 comments on commit 426b303

Please sign in to comment.