Summary
GET /api/ops/auth-report returns HTTP 500 on the v0.6.1 (#1093 SQLAlchemy-Core db rewrite) release. The failure is a compile-time InvalidRequestError in db/subscriptions.py::get_agent_subscription, where a correlated agent-count scalar subquery loses its FROM clause to SQLAlchemy auto-correlation. Because it fails at statement-compile time it 500s for every call regardless of data or backend, and it reproduces identically on both SQLite and PostgreSQL. Token decrypt/inject (subscription auth) is unaffected — only paths that call get_agent_subscription (the fleet auth-report, per-agent subscription-status lookups) break.
Component
Backend / Subscriptions (db layer)
Priority
P1
Error
sqlalchemy.exc.InvalidRequestError: Select statement '<...Select object...>' returned no FROM clauses due to auto-correlation; specify correlate(<tables>) to control correlation manually.
Abbreviated stack:
routers/ops.py -> get_auth_report
database.py -> get_agent_subscription
db/subscriptions.py -> get_agent_subscription (conn.execute(stmt))
sqlalchemy ... compiler.visit_select -> _setup_select_stack -> selectable._get_display_froms
-> InvalidRequestError: returned no FROM clauses due to auto-correlation
Location
- File:
src/backend/db/subscriptions.py
- Function:
SubscriptionOperations.get_agent_subscription (+ helper _agent_count_subquery)
Root Cause
_agent_count_subquery() builds a scalar subquery whose only FROM is agent_ownership, correlated to the outer query via agent_ownership.c.subscription_id == subscription_credentials.c.id:
@staticmethod
def _agent_count_subquery():
return (
select(func.count())
.select_from(agent_ownership)
.where(and_(
agent_ownership.c.subscription_id == subscription_credentials.c.id,
agent_ownership.c.deleted_at.is_(None),
))
.scalar_subquery()
.label("agent_count")
)
This is fine in callers whose outer FROM is subscription_credentials JOIN users (e.g. list_subscriptions, get_subscription): only subscription_credentials is auto-correlated, agent_ownership stays in the subquery FROM.
But get_agent_subscription also joins agent_ownership in its outer query (to filter by agent_name):
select(*self._subscription_select_columns(), self._agent_count_subquery())
.select_from(
subscription_credentials
.join(users, subscription_credentials.c.owner_id == users.c.id)
.join(agent_ownership, agent_ownership.c.subscription_id == subscription_credentials.c.id)
)
.where(and_(agent_ownership.c.agent_name == <agent_name>, agent_ownership.c.deleted_at.is_(None)))
Now the enclosing query contains both subscription_credentials and agent_ownership, so SQLAlchemy auto-correlates both out of the subquery — removing agent_ownership, the subquery's only FROM — leaving "no FROM clauses" and raising at compile time.
Reproduction Steps
- Deploy Trinity at a commit including the
#1093 SQLAlchemy-Core db rewrite (v0.6.1).
- Have at least one agent registered (with or without a subscription).
- As an admin, call
GET /api/ops/auth-report (equivalently, invoke db.get_agent_subscription(<agent_name>)).
- Observe HTTP 500 with the
InvalidRequestError: returned no FROM clauses due to auto-correlation.
(Reproduces on both SQLite and PostgreSQL backends.)
Suggested Fix
Alias agent_ownership inside the count subquery so its FROM table is always distinct from any agent_ownership in the enclosing query — auto-correlation then only removes subscription_credentials (the intended correlation):
from sqlalchemy import and_, func, select # existing imports
@staticmethod
def _agent_count_subquery():
ao = agent_ownership.alias("ao_count")
return (
select(func.count())
.select_from(ao)
.where(and_(
ao.c.subscription_id == subscription_credentials.c.id,
ao.c.deleted_at.is_(None),
))
.scalar_subquery()
.label("agent_count")
)
Alternatively, pin correlation explicitly on the subquery with .correlate(subscription_credentials). The alias approach is the most robust because it makes the helper safe in every caller, including those that join agent_ownership.
Environment
- Trinity version: v0.6.1 (
bd11ea96)
- Backend: FastAPI + SQLAlchemy Core (
#1093)
- Reproduced on SQLite and PostgreSQL
Related
src/backend/db/subscriptions.py (get_agent_subscription, _agent_count_subquery)
src/backend/routers/ops.py (get_auth_report)
- Introduced by the
#1093 SQLAlchemy-Core database rewrite.
Summary
GET /api/ops/auth-reportreturns HTTP 500 on the v0.6.1 (#1093SQLAlchemy-Core db rewrite) release. The failure is a compile-timeInvalidRequestErrorindb/subscriptions.py::get_agent_subscription, where a correlated agent-count scalar subquery loses its FROM clause to SQLAlchemy auto-correlation. Because it fails at statement-compile time it 500s for every call regardless of data or backend, and it reproduces identically on both SQLite and PostgreSQL. Token decrypt/inject (subscription auth) is unaffected — only paths that callget_agent_subscription(the fleet auth-report, per-agent subscription-status lookups) break.Component
Backend / Subscriptions (db layer)
Priority
P1
Error
Abbreviated stack:
Location
src/backend/db/subscriptions.pySubscriptionOperations.get_agent_subscription(+ helper_agent_count_subquery)Root Cause
_agent_count_subquery()builds a scalar subquery whose only FROM isagent_ownership, correlated to the outer query viaagent_ownership.c.subscription_id == subscription_credentials.c.id:This is fine in callers whose outer FROM is
subscription_credentials JOIN users(e.g.list_subscriptions,get_subscription): onlysubscription_credentialsis auto-correlated,agent_ownershipstays in the subquery FROM.But
get_agent_subscriptionalso joinsagent_ownershipin its outer query (to filter byagent_name):Now the enclosing query contains both
subscription_credentialsandagent_ownership, so SQLAlchemy auto-correlates both out of the subquery — removingagent_ownership, the subquery's only FROM — leaving "no FROM clauses" and raising at compile time.Reproduction Steps
#1093SQLAlchemy-Core db rewrite (v0.6.1).GET /api/ops/auth-report(equivalently, invokedb.get_agent_subscription(<agent_name>)).InvalidRequestError: returned no FROM clauses due to auto-correlation.(Reproduces on both SQLite and PostgreSQL backends.)
Suggested Fix
Alias
agent_ownershipinside the count subquery so its FROM table is always distinct from anyagent_ownershipin the enclosing query — auto-correlation then only removessubscription_credentials(the intended correlation):Alternatively, pin correlation explicitly on the subquery with
.correlate(subscription_credentials). The alias approach is the most robust because it makes the helper safe in every caller, including those that joinagent_ownership.Environment
bd11ea96)#1093)Related
src/backend/db/subscriptions.py(get_agent_subscription,_agent_count_subquery)src/backend/routers/ops.py(get_auth_report)#1093SQLAlchemy-Core database rewrite.