0

I have the following models in my DB (Flask-SQLALchemy, declarative approach, simplified):

class Player(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    ...

class Game(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    creator_id = db.Column(db.Integer, db.ForeignKey('player.id'))
    creator = db.relationship(Player, foreign_keys='Game.creator_id')
    opponent_id = db.Column(db.Integer, db.ForeignKey('player.id'))
    opponent = db.relationship(Player, foreign_keys='Game.opponent_id')
    winner = db.Column(db.Enum('creator', 'opponent'))

Each game may be either won, lost or ended in draw. I need to get players sorting them by "win rate" - i.e.:

  • if player created a game and that game's winner is creator, it is considered win;
  • if the player was invited to game as opponent and game's winner is opponent, it is also considered win;
  • other games where this player participated are considered lost games.

So my algorithm is as follows:

@hybrid_property
def winrate(self):
    games = Game.query.filter(or_(
        Game.creator_id == self.id,
        Game.opponent_id == self.id,
    ))
    count = 0
    wins = 0
    for game in games:
        count += 1
        if game.creator_id == self.id and game.winner == 'creator':
            wins += 1
        elif game.opponent_id == self.id and game.winner == 'opponent':
            wins += 1
    if count == 0:
        return 0
    return wins / count

This approach works when I want to determine win rate for particular player; but it fails when I want to sort players by win rate. I tried to rewrite it in SQL and got something like this:

SELECT * FROM player
ORDER BY ((SELECT count(g1.id) FROM game g1
    WHERE g1.creator_id = player.id AND g1.winner = 'creator'
) + (SELECT count(g2.id) FROM game g2
    WHERE g2.opponent_id = player.id AND g2.winner = 'opponent'
)) / (SELECT count(g3.id) FROM game g3
    WHERE player.id IN (g3.creator_id, g3.opponent_id)
)

This doesn't handle players without games but should work in general. Players without games can be probably handled with MySQL CASE statement.

But the problem is that I cannot figure how do I encode this SQL using SQLAlchemy. Here is a (simplified) code I try to use:

@winrate.expression
def winrate(cls):
    cnt = Game.query.filter(
        cls.id.in_(Game.creator_id, Game.opponent_id)
    ).with_entities(func.count(Game.id))
    won = Game.query.filter(
        or_(
            and_(
                Game.creator_id == cls.id,
                Game.winner == 'creator',
            ),
            and_(
                Game.opponent_id == cls.id,
                Game.winner == 'opponent',
            ),
        )
    )
    return case([
        (count == 0, 0),
    ], else_ = (
        won / count
    ))

This code fails when it comes to won / count line telling me that Query cannot be divided by Query. I tried using subqueries but without any success.

How should I implement it? Or maybe I should use some kind of joins/whatever? (DB scheme cannot be changed.)

3
  • 2
    without actually looking or thinking too hard about this: won.scalar() / count.scalar()? Query.scalar() produces a scalar subquery, which is a clause element Commented Sep 6, 2015 at 3:08
  • 1
    Query.scalar() returns number, not subquery. Commented Sep 7, 2015 at 19:42
  • 1
    ah, sorry, i meant Query.as_scalar() :) Commented Sep 8, 2015 at 22:28

1 Answer 1

1

Try working with core expressions instead of orm queries:

class Player(..):
    # ...
    @winrate.expression
    def _winrate(cls):
        cnt = (
            select([db.func.count(Game.id)])
            .where(
                db.or_(
                    Game.creator_id == cls.id,
                    Game.opponent_id == cls.id,
                ))
            .label("cnt")
        )
        won = (
            select([db.func.count(Game.id)])
            .where(
                db.or_(
                    db.and_(Game.creator_id == cls.id,
                            Game.winner == 'creator'),
                    db.and_(Game.opponent_id == cls.id,
                            Game.winner == 'opponent'),
                ))
            .label("cnt")
        )

        return db.case(
            [(cnt == 0, 0)],
            else_ = db.cast(won, db.Numeric) / cnt
        )
# ...
q = session.query(Player).order_by(Player.winrate.desc())
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks very much! That worked. My only change was to use db.select instead of just select, but should not be critical.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.