Skip to content

fix(r): make table_name optional for deferred QueryChat$new(NULL) - #305

Merged
cpsievert merged 7 commits into
mainfrom
fix/r-server-deferred-table-name
Sep 12, 2026
Merged

fix(r): make table_name optional for deferred QueryChat$new(NULL)#305
cpsievert merged 7 commits into
mainfrom
fix/r-server-deferred-table-name

Conversation

@cpsievert

@cpsievert cpsievert commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The problem

QueryChat$new(NULL) — constructing without a data source, to register one later per Shiny session via $server(data_source = ) — currently requires table_name up front too:

QueryChat$new(NULL)
#> Error: `table_name` is required when `data_source` is `NULL`.

That's stricter than it needs to be. The only thing table_name is used for in this deferred case is computing a default Shiny module ID (querychat_<table_name>) so $ui()/$sidebar() — which have to be built once, before any session (and therefore any real table) exists — get a readable, collision-free namespace when an app has more than one QueryChat instance. For a single-instance app, a generic default ID works fine, and Python's QueryChat() already allows omitting it for exactly that reason.

The fix

  • table_name is now optional when data_source is NULL. If omitted (or explicitly NULL), $id falls back to the generic "querychat", matching Python's behavior. You can still pass id = yourself if you want a custom one.

  • $server() gains a table_name parameter, so a name can be supplied per-session instead of only at construction time:

    qc <- QueryChat$new(NULL)  # no table_name needed just to build the UI
    
    server <- function(input, output, session) {
      conn <- get_per_user_connection(session)
      qc$server(data_source = conn, table_name = "orders")
    }
  • If data_source is given to $server() and no name can be resolved from any of $server(table_name = ), $new(table_name = ), or an already-registered table, you now get a clear error instead of a cryptic base-R "subscript out of bounds".

  • As part of this, $add_table()'s single-table id auto-rename (rewrite $id to querychat_<table> when one table is registered) is removed rather than worked around: the id is now fixed at construction time, matching Python. The rename was nearly unreachable — $remove_table() refuses to remove the last table, and the constructor registers inline — and in its one live path (registering a table between $ui() and $server()) it could itself desync the module namespace from the already-rendered UI. This replaces an earlier revision of this PR that pinned the id via a private .id_pinned flag.

Test plan

  • Updated/added tests in test-QueryChat.R: QueryChat$new(NULL) no longer errors and falls back to the generic ID; explicit table_name = NULL behaves the same as omitting it; $server(data_source = ) gives a clear error when no name can be inferred; $server(data_source = , table_name = ) registers under the given name.
  • make r-check-format, make r-check-tests (2083 passed), and make r-check-package (0 errors, 0 warnings; the one pre-existing NOTE is unrelated to this change) all pass.

Related to #300, but independent of it — this is about deferred-construction ergonomics, not the concurrent-session safety issues that issue tracks.

This comment was marked as resolved.

This comment was marked as resolved.

@cpsievert

This comment was marked as resolved.

This comment was marked as resolved.

QueryChat$new(NULL) (the deferred, per-session data source pattern)
required table_name up front purely to compute a Shiny module $id --
but that's only needed for uniqueness across multiple instances in one
app, not a real dependency of the deferred pattern itself. Python's
QueryChat() already allows omitting it in this case, falling back to a
generic module id.

table_name is now optional when data_source is NULL: $id falls back to
"querychat" if omitted (matching Python), and an explicit
table_name = NULL is treated the same as omitting it.

$server() gains a table_name parameter so a name can still be supplied
per-session rather than only at $new() time, with a clear error
(instead of a cryptic "subscript out of bounds") if data_source is
given and no table name can be inferred at all -- from $server(),
$new(), or an already-registered table.
Two real bugs in the previous commit, both caught by review:

- add_table()'s existing single-table auto-rename
  (self$id <- sprintf("querychat_%s", table_name) when
  is.null(self$id_override)) fired again when $server(data_source=)
  registered the deferred table, changing self$id after $ui()/$sidebar()
  had already rendered with the old one -- desyncing the Shiny module
  namespace between UI and server. Pin id_override once the deferred
  branch resolves self$id, so add_table() never renames it afterward.

- $server()'s new table_name parameter was inserted right after
  data_source, shifting the meaning of existing positional calls like
  $server(data_source, client). Moved it after `...`/session so it's
  named-only, like id already is.
- $server(data_source=, table_name=): reject a mismatch between an
  already-built DataSource's own table_name and the resolved
  table_name, instead of silently registering it under the wrong key
  (normalize_data_source() returns DataSource objects unchanged, so a
  mismatch would otherwise desync the registered name from the name
  the source actually queries/describes).
- Stop reusing the public id_override field (documented as "the user
  explicitly set id") to pin the deferred-fallback id -- that field's
  nullness is also add_table()'s auto-rename guard, so writing to it
  for an auto-generated id blurred its contract. Added a private
  .id_pinned flag instead; add_table()'s guard now checks both.
- Updated the @PARAM id roxygen text, which still described the old
  always-inferred-from-table_name behavior.
The check added in 8829386 only guarded $server(data_source=,
table_name=), but the identical mismatch (a DataSource's own
table_name differing from the dict key it's registered under) was
still reachable unguarded through $add_table() called directly.
Move the check into add_table() itself so all callers (including
$server(), which delegates to it) get the same protection.
Also drop the now-stale breaking-change note claiming $server()'s
data_source parameter was removed in #195 -- it is re-blessed here for
the deferred per-session pattern, and will never have been removed in a
released version.
Rather than pinning $id in the deferred case (.id_pinned), drop the
rename altogether: $add_table() no longer rewrites the Shiny module $id
when the registered table is the only one. The id is now fixed at
construction time, matching Python's set-once semantics.

The rename was nearly unreachable (remove_table() refuses to remove the
last table, and the constructor registers inline), and in its one live
path -- registering a table between $ui() and $server() -- it could
itself cause the namespace desync the pin was guarding against.
Still named-only (after ...), so existing positional calls are unaffected;
this just groups it with the other registration-related args.

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.

🟢 Approval recommended

No unresolved issues blocking approval were identified.

Review details

Files not reviewed (1)

  • pkg-r/man/QueryChat.Rd: Generated file
  • Files reviewed: 3/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@cpsievert
cpsievert merged commit 92844f0 into main Sep 12, 2026
12 checks passed
@cpsievert
cpsievert deleted the fix/r-server-deferred-table-name branch September 12, 2026 23:07
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