Hy, For a long time ago I manage versioned data's into relational database. You must be careful with the way you deal with timeable information. Membership table represent information with a validity period specified by start and stop attribute. The first change that I propose is to replace your 'Integer' column type for yours start and top attribute by a DateTime
class Membership(Base): __tablename__ = 'membership' group_id=Column(ForeignKey(GroupInfo.group_id, onupdate="cascade",ondelete='cascade')) employee_id = Column(Integer, primary_key=True, nullable=False) _start = Column(DateTime, nullable=False, primary_key=True, autoincrement=False) _stop = Column(DateTime, nullable=False, primary_key=True,autoincrement=False) When dealing with period information (_start, _stop) you have to make the difference between the value of yours data in the code and their values into the database. A common mistake is to save null value in the database for one of their attributes. Another mistake is the meaning of each limit. When you give me a value for _start, _end do you mean that the limit is included or excluded? If a membershipA start at 2pm, stop at 3pm and membershipB start at 3pm and stop at 4pw do you mean that they are an overlap between these 2 memberships. In period management, a common pattern is to work with include, excluded limit so a period is represented by [_start, _end[ meaning that at _end, your are no more membership To hide the whole complexity behind period, I use a composite property to encapsulate _start and _end import datetime from sqlalchemy.orm import composite from sqlalchemy import sql from sqlalchemy import DateTime from sqlalchemy.sql import or_ from sqlalchemy.sql import and_ from sqlalchemy.sql.expression import ClauseList from sqlalchemy.sql.expression import _BinaryExpression from sqlalchemy.sql.expression import _literal_as_binds from sqlalchemy.sql.expression import cast from sqlalchemy.orm.properties import CompositeProperty from sqlalchemy.databases.postgres import PGDialect class Membership(Base): __tablename__ = 'membership' group_id=Column(ForeignKey(GroupInfo.group_id, onupdate="cascade",ondelete='cascade')) employee_id = Column(Integer, primary_key=True, nullable=False) validity_period = composite(Period, Column("start", DateTime, nullable=False, primary_key=True, autoincrement=False), Column("end", DateTime, nullable=False, primary_key=True,autoincrement=False) , comparator_factory = PeriodComparator) the code for Period and PeriodComparator -*- coding: utf-8 -*- # Copyright (c) 2008 # Authors: Laurent Mignon (Software AG) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or higher # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. class Period(object): """Period datetype This class implement a custom datetype for sqlalchemy where values are provided by the composition of two columns. The requirement for the custom this class are that it have a constructor which accepts positional arguments corresponding to its column format. In our case, two DATETIME columns definition corresponding to the start and the end of the period. It also provides a method __composite_values__() which returns the state of the object as a list or tuple, in order of its column-based attributes. It also should supply adequate __eq__() and __ne__() methods which test equality of two instances. """ PRECISION = datetime.timedelta(minutes=1) endMax = datetime.datetime.max - PRECISION def __init__(self, start=None, end=None): """Constructor """ self.__start = None self.__end = None self.start = start self.end = end def __set_start(self, value): if value is None: value = datetime.datetime.min self.__start = value def __get_start(self): if self.__start == datetime.datetime.min: return None return self.__start start = property(__get_start, __set_start) def __set_end(self, value): if value is None or value == datetime.datetime.max: value = self.endMax self.__end = value def __get_end(self): if self.__end == self.endMax: return None return self.__end end = property(__get_end, __set_end) def __composite_values__(self): """ returns the state of the object as a list or tuple, in order of its column-based attribute """ self.start = self.start self.end = self.end return [self.__start, self.__end] def __eq__(self, other): if other is None: return False if not isinstance(other, Period) : return False return other.start == self.start and \ other.end == self.end def __ne__(self, other): return not self.__eq__(other) def __cmp__(self, other): if self.start == other.start: return cmp(self.end, other.end) return cmp(self.start, other.start) def __contains__(self, other): if other is None: return False other_start = other.start or datetime.datetime.min other_end = other.end or datetime.datetime.max start = self.start or datetime.datetime.min end = self.end or datetime.datetime.max # Zero-length periods are excluded of search results when occuring at # the end of a period, since upper bound is not inclusive # TODO include zero length periods that have the same bounds => see # includes operator if other_start == other_end and \ end == other_end: return False elif start <= other_start and \ end >= other_end: return True return False class PeriodComparator(CompositeProperty.Comparator): """Specific operators used to request datas based on period criteria The most important operator is the OVERLAPS operator. It's probably the only one required to efficiently retrieve period based datas since we always store no null value to qualify a period. """ def overlaps(self, other): """ Constrains the property to overlap the given [beginDate,endDate[ period When the backend is a postgres database, the OVERLAPS operator is used otherwhise a complex sql statement is issued """ beginColumn = self.prop.columns[0] endColumn = self.prop.columns[1] other_start = other.start or datetime.datetime.min other_end = other.end or datetime.datetime.max dialect = self.mapper.tables[0].metadata.bind.dialect.name if dialect == PGDialect.name: #SELECT * #FROM PERIODE #WHERE # (P1_DEBUT, P1_FIN) OVERLAPS (P2_DEBUT, P2_FIN) leftClause = ClauseList(beginColumn, endColumn) rightClause = ClauseList( cast(_literal_as_binds(other_start), DateTime), cast(_literal_as_binds(other_end), DateTime)) expr = _BinaryExpression( leftClause, rightClause, 'OVERLAPS') return expr else: #SELECT * #FROM PERIODE #WHERE #WHERE (P1_DEBUT > P2_DEBUT AND # (P1_DEBUT < P2_FIN OR P1_FIN < P2_FIN)) OR # (P2_DEBUT > P1_DEBUT AND # (P2_DEBUT < P1_FIN OR P2_FIN < P1_FIN)) OR # (P1_DEBUT = P2_DEBUT AND # (P1_FIN IS NOT NULL AND P2_FIN IS NOT NULL)) return or_( and_(beginColumn > other_start, or_(beginColumn < other_end, endColumn < other_end), ), and_(other_start > beginColumn, or_(other_start < endColumn, other_end < endColumn), ), beginColumn == other_start, ) def partially_overlaps(self, other): """ Returns a query which selects the records where the period * include(other.start) AND exclude other.end OR * include(other.end) AND exclude other.start For example, the two period p1 and p2 will overlap in cases (a), (b), and (c), but not in cases (d) and (e): p2 [ [ (a) p1 [ [ (b) p1 [ [ (c) p1 [ [ (d) p1 [ [ (e) p1 [ [ ------------+----------------+------------------------- > time p1.start p1.end """ if other is None: return True column_start = self.prop.columns[0] column_end = self.prop.columns[1] other_start = other.start or datetime.datetime.min other_end = other.end or datetime.datetime.max ret = sql.or_( sql.and_( sql.and_(column_start > other_start, column_start < other_end), column_end > other_end), sql.and_( sql.and_(column_end > other_start, column_end < other_end), column_start < other_start)) return ret def includes(self, other): """ Returns a query which selects the records where the period includes the specified period. For example, the period p2 is included in period p1 in case (c), but not in other cases: p2 [ [ (a) p1 [ [ (b) p1 [ [ (c) p1 [ [ (d) p1 [ [ (e) p1 [ [ ------------+----------------+------------------------- > time p1.start p1.end """ if other is None: return True column_start = self.prop.columns[0] column_end = self.prop.columns[1] other_start = other.start or datetime.datetime.min other_end = other.end or Period.endMax # If both periods have the same end date, we have to exclude null # length periods, since upper bound is exclusive. # In addition to that, we have to include periods with the same bounds # (even if they have null length) if other_start == other_end: ret = sql.or_( sql.and_(column_start == other_start, column_end == other_end), sql.and_( sql.and_(column_start <= other_start, column_end >= other_end), sql.not_(other_end == column_end))) else: ret = sql.or_( sql.and_(column_start == other_start, column_end == other_end), sql.and_(column_start <= other_start, column_end >= other_end)) return ret def isincluded(self, other): """ Returns a query which selects the records where the period is included into the specified period. """ if other is None: return True column_start = self.prop.columns[0] column_end = self.prop.columns[1] other_start = other.start or datetime.datetime.min other_end = other.end or datetime.datetime.max ret = sql.and_(column_start >= other_start, column_end <= other_end) return ret Now you can initialize your membership with: mebership = Membership() membership.validity_period = Period(datetime.datetime.now()) And simply query using the 'overlap' operator query = session.query(Membership) query = query.filter( Membership.validity_period.comparator.overlaps( Period(datetime.datetime.now(), datetime.datetime.now() + datetime.timedelate(days=1)))) I give you the code without guaranty since it's used in non declarative way but I think that I've made (on the fly and without testing) the required change to your sample... Laurent Mignon Senior Software Engineer Software AG Belgium If you want to query the membership whit an overlap operator or an other dedicated operator On 16 juil, 18:36, Gregg Lind <gregg.l...@gmail.com> wrote: > That's a totally fair answer! Mostly, I wish some sense of relational > change over time was built into SQL, the way it is in BigTable style > systems. > > Maybe you could shed a little light on how to use the overlap > operator? I'm having trouble getting the multiple fields into the > clause statement. > > In [43]: > session.query(Membership).filter(Membership.start.op('OVERLAPS',[1,100])) > > TypeError: op() takes exactly 2 arguments (3 given) > > It would also be have / emulate a .when(ts) method in queries (perhaps > via a query subclass) that would take the "timings" into account, but > this does smack of magic as you suggest! > > Thanks for the advice! > > Gregg > > On Thu, Jul 16, 2009 at 10:17 AM, Michael Bayer<mike...@zzzcomputing.com> > wrote: > > > Gregg Lind wrote: > > >> Questions: > > >> 1. Is there a "SQLAlchemical" way to write group_snapshot_ts into > >> a declarative class, such that the joins and loader respect the > >> time > >> constraints? (Read-only is fine as well on the loaded attributes) > >> a. eager_loader? > >> b. subclassing query? > > > im really not in favor of trying to build "magic" awareness of things like > > this into mappers and query subclasses. If you have complex conditions > > to be queried upon I am a fan of abstracting the common kinds of queries > > you need into Python functions and/or class methods. no magic, just > > regular old application architecture. --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To post to this group, send email to sqlalchemy@googlegroups.com To unsubscribe from this group, send email to sqlalchemy+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/sqlalchemy?hl=en -~----------~----~----~----~------~----~------~--~---