If you absolutely cannot alter your schema, then you could unpivot performance data and join with users. Sample mappings for relations, since none were provided:
In [2]: class User(Base):
...: __tablename__ = "users"
...: username = Column(Unicode(255), primary_key=True)
...: firstname = Column(Unicode(255))
...: lastname = Column(Unicode(255))
...:
In [3]: class Performance(Base):
...: __tablename__ = "performance"
...: timestamp = Column(Date, primary_key=True)
...: # NOTE: this works **only** in a class body context,
...: # and your schema shouldn't be like this anyway.
...: for name in ['alice', 'bob']:
...: locals()[name] = Column(Integer, nullable=False)
...:
Get usernames that have performance data using inspection. You could also hold on to a static list of usernames:
In [11]: users = inspect(Performance).attrs.keys()[1:]
In [12]: users
Out[12]: ['alice', 'bob']
Form the unpivot query – just one way to do it:
In [15]: from sqlalchemy.dialects import postgresql
...: performance = session.query(
...: func.unnest(postgresql.array(users)).label('username'),
...: func.unnest(postgresql.array(
...: [getattr(Performance, name)
...: for name in users])).label('value')).\
...: filter(Performance.timestamp == '2017-11-2').\
...: subquery()
...:
Join users with their performance values:
In [24]: session.query(User, performance.c.value).\
...: join(performance, performance.c.username == User.username).\
...: all()
Out[24]:
[(<__main__.User at 0x7f79eb5d2c88>, 1),
(<__main__.User at 0x7f79eb5d2cf8>, 5)]
Were you to alter your schema such that performance data is stored as (timestamp, username, value)
tuples, you could simply do this:
In [2]: class User(Base):
...: __tablename__ = "users"
...: ...
...: performance = relationship("BetterPerformance")
...:
In [25]: class BetterPerformance(Base):
...: __tablename__ = "better_performance"
...: timestamp = Column(Date, primary_key=True)
...: username = Column(ForeignKey('users.username'), primary_key=True)
...: value = Column(Integer, nullable=False)
...:
In [13]: session.query(User, BetterPerformance.value).\
...: join(User.performance).\
...: filter(BetterPerformance.timestamp == '2017-11-2').\
...: all()
Out[13]:
[(<__main__.User at 0x7f6ae3282c18>, 1),
(<__main__.User at 0x7f6ae3282ba8>, 5)]
or even:
In [17]: session.query(User).\
...: join(User.performance).\
...: options(contains_eager(User.performance)).\
...: filter(BetterPerformance.timestamp == '2017-11-2').\
...: all()
Out[17]: [<__main__.User at 0x7f6ae3282c18>, <__main__.User at 0x7f6ae3282ba8>]
In [18]: [u.performance[0].value for u in _]
Out[18]: [1, 5]