fix(r): make table_name optional for deferred QueryChat$new(NULL) - #305
Merged
Conversation
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.
cpsievert
force-pushed
the
fix/r-server-deferred-table-name
branch
from
September 12, 2026 16:17
fb26d0e to
866311f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
QueryChat$new(NULL)— constructing without a data source, to register one later per Shiny session via$server(data_source = )— currently requirestable_nameup front too:That's stricter than it needs to be. The only thing
table_nameis 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 oneQueryChatinstance. For a single-instance app, a generic default ID works fine, and Python'sQueryChat()already allows omitting it for exactly that reason.The fix
table_nameis now optional whendata_sourceisNULL. If omitted (or explicitlyNULL),$idfalls back to the generic"querychat", matching Python's behavior. You can still passid =yourself if you want a custom one.$server()gains atable_nameparameter, so a name can be supplied per-session instead of only at construction time:If
data_sourceis 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$idtoquerychat_<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_pinnedflag.Test plan
test-QueryChat.R:QueryChat$new(NULL)no longer errors and falls back to the generic ID; explicittable_name = NULLbehaves 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), andmake 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.