Context
After #636, every read site that wants prediction review state has to repeat the same incantation:
LEFT JOIN prediction_review pr_rev
ON pr_rev.prediction_id = pr.id AND pr_rev.workspace_id = ?
WHERE COALESCE(pr_rev.status, 'pending') != 'rejected'
#644 is sweeping remaining sites for the related (detector_confidence, labels_fingerprint) filters, but #636 already needed 19 review rounds to find them all, and #644 is at least one more pass. The cost is structural: any future caller that forgets the join + COALESCE will silently surface rejected rows from another workspace.
Fix
Two options:
Option A — SQL view.
CREATE VIEW predictions_for_workspace AS
SELECT pr.*,
COALESCE(rv.status, 'pending') AS review_status,
rv.individual, rv.group_id, rv.vote_count, rv.total_votes
FROM predictions pr
LEFT JOIN prediction_review rv
ON rv.prediction_id = pr.id AND rv.workspace_id = :ws
Tradeoff: SQLite doesn't support parameterized views, so callers still substitute workspace_id — half the win.
Option B — db.predictions_query(ws_id, ...) helper that returns a callable / pre-built Cursor, used everywhere review-aware reads happen. Easier to test and refactor.
Either way, add a regression test that an intentional-bad query (missing the join) fails so the pattern can't drift back.
Sequencing
Best timed after #644 closes, so the sweep happens once and we centralize a single converged shape.
Context
After #636, every read site that wants prediction review state has to repeat the same incantation:
#644 is sweeping remaining sites for the related
(detector_confidence, labels_fingerprint)filters, but #636 already needed 19 review rounds to find them all, and #644 is at least one more pass. The cost is structural: any future caller that forgets the join + COALESCE will silently surface rejected rows from another workspace.Fix
Two options:
Option A — SQL view.
Tradeoff: SQLite doesn't support parameterized views, so callers still substitute
workspace_id— half the win.Option B —
db.predictions_query(ws_id, ...)helper that returns a callable / pre-builtCursor, used everywhere review-aware reads happen. Easier to test and refactor.Either way, add a regression test that an intentional-bad query (missing the join) fails so the pattern can't drift back.
Sequencing
Best timed after #644 closes, so the sweep happens once and we centralize a single converged shape.