Skip to content

fix(py): restore server(data_source=) for Shiny/R parity - #301

Merged
cpsievert merged 1 commit into
mainfrom
fix/py-server-data-source-parity
Sep 12, 2026
Merged

fix(py): restore server(data_source=) for Shiny/R parity#301
cpsievert merged 1 commit into
mainfrom
fix/py-server-data-source-parity

Conversation

@cpsievert

@cpsievert cpsievert commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Part 1 of 4

This is the first PR in a stack restoring safe per-session data source registration:

  1. fix(py): restore server(data_source=) for Shiny/R parity #301 (this PR): restore server(data_source=)
  2. fix(py): let server(data_source=) survive a second session #302: let it survive a second session
  3. fix(py): don't clean up a table replaced via server(data_source=) #303: don't corrupt an earlier session's still-in-use resource
  4. fix(py): greeting reflects the session that registered its table #304: fix the welcome greeting leaking between sessions

Each PR is reviewable independently; #302-#304 build on this one.

The problem

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 depends on that user's session-scoped OAuth credentials, which don't exist until the server function runs. querychat supports this "deferred" pattern: construct QueryChat without a data source, then register one per session via .server(data_source=...).

Python's server() used to accept data_source= for exactly this, but it was dropped in a multi-table redesign (#195), so this pattern currently requires building a second, fully-duplicate QueryChat instance per session, kept manually in sync with a UI-only global one:

# Before this PR: awkward workaround
qc_ui = QueryChat(None, table_name="orders")  # for .ui()/.sidebar() only

def server(input, output, session):
    conn = get_per_user_connection(session)  # depends on this session's OAuth token
    qc_session = QueryChat(conn, table_name="orders", client=chat_client, ...)  # duplicate config
    qc_session.server()

The fix

QueryChat.server() gains data_source and table_name:

qc = QueryChat(None, table_name="orders")

def server(input, output, session):
    conn = get_per_user_connection(session)
    qc.server(data_source=conn, client=chat_client)
  • table_name is optional — it falls back to the name given to the constructor, or to the first already-registered table, matching R's equivalent behavior.
  • Registered via the existing add_table(replace=True, include_in_greeting=True) machinery.

This PR intentionally matches R's current $server(data_source = ) behavior exactly, guard included: a second session's call still raises RuntimeError("Cannot add tables after server initialization."), same as R does today. That's a real limitation — R's version has never actually worked for more than one session either — but it's a separate, focused problem from "restore the parameter," and is fixed in #302.

Test plan

  • New unit tests cover: registering the deferred table by its constructor name, an explicit table_name override, falling back to the first registered table, a clear error when no name can be inferred, and greeting inclusion.
  • A test also locks in the current single-session limitation described above, so fix(py): let server(data_source=) survive a second session #302's fix has something concrete to flip.
  • make py-check-format, make py-check-types, make py-check-tests all pass.

Part of #300.

This comment was marked as resolved.

cpsievert added a commit that referenced this pull request Sep 12, 2026
Copilot review of #301 found that replacing a table via
server(data_source=...) always cleaned up the table it replaced and
invalidated the shared query executor -- correct for a config-time
add_table(replace=True), but for the per-session registration path this
tore down a still-in-use resource out from under an earlier, already-
running session (e.g. disposing its SQLAlchemy engine or closing its
DuckDB connection).

_add_or_replace_table() gains cleanup_replaced=False for this path, so
the caller (not querychat) remains responsible for closing a replaced
per-session data source, e.g. via session.on_ended().

Separately, greeting generation runs asynchronously after .server()
returns, and read the shared, mutable greeter.tables/_data_sources at
that later point -- so a second session's registration could clobber an
earlier session's in-flight greeting. QueryChatGreeter gains a private
_generate_async_snapshot() that mod_server() feeds an explicit
tables/data_sources snapshot captured synchronously at .server()-call
time, leaving the public greeter API unchanged.
@cpsievert

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Per-session registration can close an executor still used by an earlier session; two naming nits also remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

pkg-py/src/querychat/_querychat_base.py:474

  • This new helper is defined in _querychat_base.py, which is already a private module, so the leading underscore is redundant and violates the package's Python naming convention. Rename it to add_or_replace_table and update its callers.
    def _add_or_replace_table(

pkg-py/src/querychat/_querychat_greeter.py:74

  • This new method is defined in _querychat_greeter.py, which is already a private module, so the leading underscore is redundant and violates the package's Python naming convention. Rename it to generate_async_snapshot and update its callers.
    async def _generate_async_snapshot(
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread pkg-py/src/querychat/_querychat_base.py Outdated
Python's QueryChat.server() lost its deferred data_source registration
in the multi-table redesign (#195), forcing per-session data sources
(e.g. per-user OAuth-scoped connections on Posit Connect) to require a
second full QueryChat instance kept manually in sync with the UI-only
one. Restores server(data_source=, table_name=), registering via the
existing add_table(replace=True, include_in_greeting=True) -- matching
R's current $server(data_source=) behavior exactly, guard included: a
second session's call still raises, same as R today. Making per-session
registration actually survive a second session is a separate follow-up.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Broaden the server(data_source=...) annotation to include supported BaseBoard sources.

Review details

Suppressed comments (1)

pkg-py/src/querychat/_shiny.py:636

  • This new annotation is narrower than the data-source contract used by the rest of the API: QueryChatBase.add_table() accepts pins.boards.BaseBoard (_querychat_base.py:456-458), and the public docs show QueryChat(board, "my_pin") (docs/data-sources.qmd:341-354), but a typed deferred call such as qc.server(data_source=board, table_name="my_pin") is rejected by type checkers even though normalize_data_source() supports it at runtime. Include the same supported source type (ideally via a shared input alias) in the new server() annotation.
        data_source: IntoFrame | sqlalchemy.Engine | ibis.Table | None = None,
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@cpsievert
cpsievert added this pull request to stack #307 September 12, 2026 16:09
@cpsievert
cpsievert merged commit bd77303 into main Sep 12, 2026
8 checks passed
@cpsievert
cpsievert deleted the fix/py-server-data-source-parity branch September 12, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants