Support WHERE filtering in selectLive()#194
Conversation
Add filtered live-query overloads for parity with the JS SDK's live(table).where(expr): - Surreal.selectLive(String table, String where) appends the raw SurrealQL condition to the generated LIVE SELECT * FROM type::table($tb) statement. - Surreal.selectLive(String table, String where, Map<String, ?> params) additionally binds parameters (like query(String, Map)) so condition values can be injected safely; the binding name "tb" is reserved and rejected. The Rust JNI shares one start_live_select() helper between the unfiltered and filtered entry points, keeping the existing behavior: eager subscription-error surfacing (invalid WHERE, missing table), live-query UUID available up front, and a dedicated notification thread. Tests cover server-side filtering of CREATEs, updates across the filter boundary (notify on entering, silent on leaving), queryId, kill/close, and eager errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f2daccc to
1ebbe73
Compare
emmanuel-keller
left a comment
There was a problem hiding this comment.
Review — approve with nits.
I looked hardest at injection and the refactor: no correctness bug, no regression, no API break. The table name stays safely bound via type::table($tb), condition values go through a bound params map (mirroring query(String, Map)), and the refactor extracting start_live_select is a faithful, behaviour-preserving extraction — the UUID-up-front / kill() work from #184 is untouched and still covered by the unchanged LiveQueryTests.
One test gap: the Javadoc says CREATE/UPDATE/DELETE are filtered, but there's no DELETE-under-filter test — please add one that deletes an in-filter record. Also the 3-arg overload throws NPE (not SurrealException) if params is null, consistent with query(String, Map) but worth a guard/note.
| .exception(env, null_mut); | ||
| } | ||
| params.insert("tb".to_string(), Value::String(table)); | ||
| let sql = format!("LIVE SELECT * FROM type::table($tb) WHERE {}", where_clause); |
There was a problem hiding this comment.
Flagging for an explicit maintainer sign-off (not a bug): the where string is concatenated into the query via format! here, while the table is safely bound and condition values go through params. This is a deliberate "same trust model as query(String)" decision and isn't a new injection class — and the positional format! arg means there's no format-string hazard either. Just want it to be a conscious contract, since it's raw-SQL-in for the predicate.
There was a problem hiding this comment.
Confirmed — this is the deliberate contract: the where predicate is raw SurrealQL trusted exactly like query(String) input, values go through params bindings, and only the table name is bound ($tb). To make it a conscious, visible contract rather than an implicit one, it's now documented in three places:
- an inline comment at the concatenation site in
surreal.rs(added in 0d7bf3f), right where theformat!happens, noting the trust model and that the positional arg keeps the clause inert toformat!itself; - the Rust doc comment on
Java_com_surrealdb_Surreal_selectLiveWhere; - the Javadoc of both
selectLive(table, where)andselectLive(table, where, params), which state the clause is appended verbatim with the same trust model asquery(String), warn never to concatenate untrusted values, and point to the params overload as the safe path (with an example).
Final sign-off on the contract is of course yours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JS-parity gap
The JS SDK's live queries support server-side filtering:
live(table).where(expr)compiles toLIVE SELECT * FROM <table> WHERE <cond>(packages/sdk/src/query/live.ts). The Java SDK'sSurreal.selectLive(String table)is table-only — the JNI runs a hardcodedLIVE SELECT * FROM type::table($tb), so users cannot receive filtered live notifications through the typed API. Runningquery("LIVE SELECT ... WHERE ...")returns the live UUID but noLiveStream, so there is no workaround.Reproduction
On the embedded
memoryengine (and server v3.x),query("LIVE SELECT * FROM person WHERE category > 1")and its parameterized variant both succeed and return a live-query UUID — the engine fully supports filtered live queries; only the Java API surface was missing.Fix
Surreal.selectLive(String table, String where)— appends the raw SurrealQL condition to the generatedLIVE SELECT * FROM type::table($tb) WHERE ...statement (same trust model asquery(); documented).Surreal.selectLive(String table, String where, Map<String, ?> params)— additionally binds parameters exactly likequery(String, Map), the safe way to inject values (e.g."age > $minAge"). The binding nametbis reserved (it binds the table) and rejected rather than silently overridden.selectLivebody is factored into a sharedstart_live_select()helper used by both the unfiltered entry point and the newselectLiveWhereentry point, preserving the existing behavior: eager subscription-error surfacing (invalid WHERE, missing table), live-query UUID available up front viaLiveStream.getQueryId(), and the dedicated notification thread. The existing single-argselectLiveis untouched behaviorally.Tests
New
LiveSelectWhereTests(memory engine, timeout-safe consumer threads in theLiveQueryTestsstyle):getQueryId()available up front;kill()stops notifications;close()unblocks a blockednext()— same contracts as the unfiltered varianttbbinding, non-existent tableFull
./gradlew testsuite passes.🤖 Generated with Claude Code