CloverHealth / temporal-sqlalchemy

SQLAlchemy + Temporal
http://temporal-sqlalchemy.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
43 stars 3 forks source link

History Tables & Classes should reference clocked classes directly #13

Closed multigl closed 8 years ago

multigl commented 8 years ago

not through the prop.parent.class_

Fixes joined table inheritance.

def build_history_class(cls, prop, schema=None):
    """ build a sql alchemy table for given prop

    Args:
        cls (Clocked): class to refer back to
        prop (orm.RelationshipProperty|orm.ColumnProperty): property to build a history class for
        schema (Optional[str]): schema to use for history table

    Returns:
        _TemporalProperty: history class with table
    """
    class_name = "%s%s_%s" % (cls.__name__, 'History', prop.key)

    table = build_history_table(cls, prop, schema)
    model = type(
        class_name,
        (_TemporalProperty, declarative.declarative_base(metadata=cls.metadata)),
        dict(
            __table__=table,
            entity=orm.relationship(cls, backref=orm.backref('%s_history' % prop.key, lazy='dynamic')),
        )
    )

    if isinstance(prop, orm.RelationshipProperty):
        mapper = sa.inspect(model)
        rel = orm.relationship(
            prop.argument,
            primaryjoin=getattr(model, prop.info['temporal_on']) == prop.argument.id,  # todo different shaped FKs
            lazy="noload")  # write only rel
        mapper.add_property(prop.key, rel)

    return model

and

def build_history_table(cls, prop, schema=None):
    """ build a sql alchemy table for given prop

    Args:
        cls (Clocked): class to refer back to
        prop (orm.ColumnProperty|orm.RelationshipProperty): property to build a history table for
        schema (Optional[str]): schema to use for table, if None will use schema of given prop

    Returns:
        sa.Table: Table for property
    """

    if isinstance(prop, orm.RelationshipProperty):
        assert 'temporal_on' in prop.info, 'cannot temporal-ize a property without temporal_on=True'
        prop_ = prop.parent.get_property(prop.info['temporal_on'])  # converts rel prop to fk prop
        assert prop_.parent.local_table is prop.parent.local_table
        property_key = prop_.key
        columns = (_copy_column(col) for col in prop_.columns)
    else:
        property_key = prop.key
        columns = (_copy_column(col) for col in prop.columns)

    local_table = cls.__table__
    table_name = _truncate_identifier('%s_%s_%s' % (local_table.name, 'history', property_key))
    index_name = _truncate_identifier('%s_effective_idx' % table_name)
    effective_exclude_name = _truncate_identifier('%s_excl_effective' % table_name)
    vclock_exclude_name = _truncate_identifier('%s_excl_vclock' % table_name)
    constraints = [
        sa.Index(index_name, 'effective', postgresql_using='gist'),
        sap.ExcludeConstraint(
            (sa.cast(sa.text('entity_id'), sap.TEXT), '='), ('effective', '&&'),
            name=effective_exclude_name,
        ),
        sap.ExcludeConstraint(
            (sa.cast(sa.text('entity_id'), sap.TEXT), '='), ('vclock', '&&'),
            name=vclock_exclude_name
        ),
    ]

    foreign_key = getattr(cls, 'id')  # TODO make this support different shape pks
    return sa.Table(table_name, prop.parent.class_.metadata,
                    sa.Column('id', sap.UUID(as_uuid=True), default=uuid.uuid4, primary_key=True),
                    sa.Column('effective', sap.TSTZRANGE, default=effective_now, nullable=False),
                    sa.Column('vclock', sap.INT4RANGE, nullable=False),
                    sa.Column('entity_id', sa.ForeignKey(foreign_key)),
                    *columns,
                    *constraints,
                    schema=schema or local_table.schema,
                    keep_existing=True)  # memoization ftw