You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Some deployments can only create a table's data source inside the Shiny server function — for example, on Posit Connect, a per-user database connection (e.g. Snowflake via PositAuthenticator) depends on that user's session-scoped OAuth credentials, which aren't available until the server function runs. Both R and Python have a server(data_source = ) pattern meant to support exactly this: register a table lazily, once the session starts, instead of at app-startup time.
That pattern is currently broken in both languages — not just missing in Python, but genuinely unsafe for the concurrent-session case it exists for.
Example
qc<-QueryChat$new(NULL, table_name="orders") # no data source yetserver<-function(input, output, session) {
conn<-DBI::dbConnect(..., token= session_user_token(session)) # per-user connectionqc$server(data_source=conn)
}
User A opens the app. Session A's server() runs and registers conn_A under "orders". Works.
User B opens the app — a second browser tab, or simply a second person hitting the same running process. Session B's server() runs and registers conn_B under the same name.
Depending on the language, and how far you dig, different things go wrong here:
It can outright fail for the second session. Registering a table checks a guard meant to stop you from reconfiguring tables once a conversation is already underway — but that guard trips on any second session, not just a manual reconfiguration. In R, this means session B's qc$server(data_source = conn_B) call aborts with "Cannot add tables after server initialization." (Python's server() sidesteps this today only because it dropped the data_source parameter entirely — see History below.)
Even once that's bypassed, it can corrupt session A. "Registering a table under a name that's already taken" is implemented as "replace the old one," which includes cleaning up the old table's resources — closing a database connection, disposing an engine, etc. If session A is still running when session B registers conn_B under "orders", that cleanup closes conn_A — session A's own connection — out from under it. Session A's next query then fails, even though session A did nothing wrong and has no relationship to session B.
The welcome greeting can also leak between sessions. When no explicit greeting is provided, one is generated by asking the LLM to describe the registered table(s) — asynchronously, after the server function has already returned. If session B registers its table in the gap between session A's server function returning and session A's greeting actually being generated, session A's greeting can end up describing session B's table instead of its own.
R kept $server(data_source = ) through the same redesign (including greeting integration in ad0d6952) — but it was never actually usable for more than one session, because of issue (1) above: R's add_table() (which $server() calls internally) has the identical "no changes after the server has initialized" guard, and that guard flips to TRUE on the very first session.
Digging into why that guard matters at all surfaced issue (2): "replace" always assumes the old value is safe to throw away — true when reconfiguring a single shared table before anyone's connected, false when a second session is quietly swapping in its own resource under the same name.
Investigating the greeting path surfaced issue (3): greeting generation reads shared, mutable state asynchronously, well after the point where it was last known to be correct for a particular session.
What "fixed" means here
For both R and Python, per-session table registration via server(data_source = ) needs to:
Not be blocked by the fact that a previous session already registered a table.
Not tear down a resource that a different, still-running session is using — that becomes the responsibility of whoever created that resource (e.g. closing it themselves when their own session ends), not something querychat does automatically.
Generate any state that depends on "which table is registered" (like the auto-generated greeting) from what was true for the session that triggered it, not from whatever happens to be live by the time async work actually runs.
Summary
Some deployments can only create a table's data source inside the Shiny server function — for example, on Posit Connect, a per-user database connection (e.g. Snowflake via
PositAuthenticator) depends on that user's session-scoped OAuth credentials, which aren't available until the server function runs. Both R and Python have aserver(data_source = )pattern meant to support exactly this: register a table lazily, once the session starts, instead of at app-startup time.That pattern is currently broken in both languages — not just missing in Python, but genuinely unsafe for the concurrent-session case it exists for.
Example
server()runs and registersconn_Aunder"orders". Works.server()runs and registersconn_Bunder the same name.Depending on the language, and how far you dig, different things go wrong here:
It can outright fail for the second session. Registering a table checks a guard meant to stop you from reconfiguring tables once a conversation is already underway — but that guard trips on any second session, not just a manual reconfiguration. In R, this means session B's
qc$server(data_source = conn_B)call aborts with "Cannot add tables after server initialization." (Python'sserver()sidesteps this today only because it dropped thedata_sourceparameter entirely — see History below.)Even once that's bypassed, it can corrupt session A. "Registering a table under a name that's already taken" is implemented as "replace the old one," which includes cleaning up the old table's resources — closing a database connection, disposing an engine, etc. If session A is still running when session B registers
conn_Bunder"orders", that cleanup closesconn_A— session A's own connection — out from under it. Session A's next query then fails, even though session A did nothing wrong and has no relationship to session B.The welcome greeting can also leak between sessions. When no explicit greeting is provided, one is generated by asking the LLM to describe the registered table(s) — asynchronously, after the server function has already returned. If session B registers its table in the gap between session A's server function returning and session A's greeting actually being generated, session A's greeting can end up describing session B's table instead of its own.
History / how this was found
server()had adata_sourceparameter for exactly this deferred pattern (added in feat: Allow deferred data_source initialization #202), which the multi-table redesign (feat: multi-table support and data dictionary #195) removed — the breaking-changes table maps it toadd_table()beforeserver(), which doesn't support the per-session case at all.$server(data_source = )through the same redesign (including greeting integration inad0d6952) — but it was never actually usable for more than one session, because of issue (1) above: R'sadd_table()(which$server()calls internally) has the identical "no changes after the server has initialized" guard, and that guard flips toTRUEon the very first session.What "fixed" means here
For both R and Python, per-session table registration via
server(data_source = )needs to: