Retain structured query predicates at lowering (insert-membership for live updates)
Component of #44 (write/invalidation graph as a lowering), surfaced by the capture-seam audit in #44's comment. Filing it separately because it's a concrete, self-contained lowering change with a clear before/after.
Problem
For live updates, a live fragment's subscription needs to know not just which rows it rendered (handled by runtime read-capture at the from_stmt funnel) but which future rows would enter its result set — the insert-membership question ("would a new Comment with article_id=7 belong in this fragment?").
Today that's unanswerable at runtime, because the query lowering flattens structured queries to opaque concatenated SQL before emit, and inlines the query into the call site (no relation object to introspect):
# generated articles_controller#index — predicate is a string by the time it runs
stmt = Db.prepare("SELECT id, body, created_at, title, updated_at FROM articles" +
" ORDER BY created_at DESC")
# generated Article#comments — association predicate is also opaque SQL
stmt = Db.prepare("SELECT ... FROM comments" + " WHERE " + "article_id = " + Db.escape_int(@id))
Testing membership against a SQL string means parsing SQL at runtime — a thicket.
Key insight: retain, don't recover
The structured predicate (table, where-columns, ops, values, scope chain) existed in the AST at lowering time and was discarded during emit. So this is a retention problem, not a recovery problem. When a fragment is marked live, the query lowering should preserve the structured predicate as IR metadata alongside (not instead of) the emitted SQL.
This sits naturally in #44: rows are knowable only at runtime (capture at from_stmt); predicates are knowable at lowering (retain in IR before SQL concat). The two halves compose into the subscription's read-set.
Worked case
fixtures/real-blog/app/models/comment.rb:13-14:
after_create_commit { article.broadcast_replace_to("articles") rescue nil }
after_destroy_commit { article.broadcast_replace_to("articles") rescue nil }
The index preloads comments WHERE article_id IN (...). A comment update/delete already invalidates precisely (those rows were in the captured read-set). A new comment is the insert-membership case — no row was captured, so the dev had to hand-wire this rescue nil callback. Retaining the article_id IN (...) predicate is exactly what would derive it instead.
Scope / concrete targets
Non-goals
- Runtime SQL parsing (the whole point is to avoid it).
- Column-level precision (separate, optional layer via accessor capture).
🤖 Generated with Claude Code
Retain structured query predicates at lowering (insert-membership for live updates)
Component of #44 (write/invalidation graph as a lowering), surfaced by the capture-seam audit in #44's comment. Filing it separately because it's a concrete, self-contained lowering change with a clear before/after.
Problem
For live updates, a
livefragment's subscription needs to know not just which rows it rendered (handled by runtime read-capture at thefrom_stmtfunnel) but which future rows would enter its result set — the insert-membership question ("would a newCommentwitharticle_id=7belong in this fragment?").Today that's unanswerable at runtime, because the query lowering flattens structured queries to opaque concatenated SQL before emit, and inlines the query into the call site (no relation object to introspect):
Testing membership against a SQL string means parsing SQL at runtime — a thicket.
Key insight: retain, don't recover
The structured predicate (table, where-columns, ops, values, scope chain) existed in the AST at lowering time and was discarded during emit. So this is a retention problem, not a recovery problem. When a fragment is marked
live, the query lowering should preserve the structured predicate as IR metadata alongside (not instead of) the emitted SQL.This sits naturally in #44: rows are knowable only at runtime (capture at
from_stmt); predicates are knowable at lowering (retain in IR before SQL concat). The two halves compose into the subscription's read-set.Worked case
fixtures/real-blog/app/models/comment.rb:13-14:The index preloads
comments WHERE article_id IN (...). A comment update/delete already invalidates precisely (those rows were in the captured read-set). A new comment is the insert-membership case — no row was captured, so the dev had to hand-wire thisrescue nilcallback. Retaining thearticle_id IN (...)predicate is exactly what would derive it instead.Scope / concrete targets
live-marked fragments at the query-lowering site (finders, scopes, associations, and the inlined controller queries).liveannotation so non-live actions pay nothing._adapter_{insert,update,delete}), decide intersect / maybe-intersect / no.Non-goals
🤖 Generated with Claude Code