Skip to content

Support WHERE filtering in selectLive()#194

Open
welpie21 wants to merge 4 commits into
surrealdb:mainfrom
welpie21:feature/live-select-where
Open

Support WHERE filtering in selectLive()#194
welpie21 wants to merge 4 commits into
surrealdb:mainfrom
welpie21:feature/live-select-where

Conversation

@welpie21

Copy link
Copy Markdown

JS-parity gap

The JS SDK's live queries support server-side filtering: live(table).where(expr) compiles to LIVE SELECT * FROM <table> WHERE <cond> (packages/sdk/src/query/live.ts). The Java SDK's Surreal.selectLive(String table) is table-only — the JNI runs a hardcoded LIVE SELECT * FROM type::table($tb), so users cannot receive filtered live notifications through the typed API. Running query("LIVE SELECT ... WHERE ...") returns the live UUID but no LiveStream, so there is no workaround.

Reproduction

On the embedded memory engine (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 generated LIVE SELECT * FROM type::table($tb) WHERE ... statement (same trust model as query(); documented).
  • Surreal.selectLive(String table, String where, Map<String, ?> params) — additionally binds parameters exactly like query(String, Map), the safe way to inject values (e.g. "age > $minAge"). The binding name tb is reserved (it binds the table) and rejected rather than silently overridden.
  • Rust JNI: the existing selectLive body is factored into a shared start_live_select() helper used by both the unfiltered entry point and the new selectLiveWhere entry point, preserving the existing behavior: eager subscription-error surfacing (invalid WHERE, missing table), live-query UUID available up front via LiveStream.getQueryId(), and the dedicated notification thread. The existing single-arg selectLive is untouched behaviorally.

Tests

New LiveSelectWhereTests (memory engine, timeout-safe consumer threads in the LiveQueryTests style):

  • only matching CREATEs notify (non-matching record created first to prove ordering)
  • UPDATE across the filter boundary: entering the filter delivers an UPDATE notification; leaving it delivers nothing (asserted per observed engine semantics)
  • literal (two-arg) condition variant
  • getQueryId() available up front; kill() stops notifications; close() unblocks a blocked next() — same contracts as the unfiltered variant
  • eager errors: invalid WHERE, null/blank WHERE, reserved tb binding, non-existent table
  • typed POJO mapping through a filtered stream

Full ./gradlew test suite passes.

🤖 Generated with Claude Code

welpie21 and others added 3 commits July 10, 2026 14:53
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>
@welpie21 welpie21 force-pushed the feature/live-select-where branch from f2daccc to 1ebbe73 Compare July 10, 2026 14:10
@welpie21 welpie21 marked this pull request as ready for review July 10, 2026 14:29

@emmanuel-keller emmanuel-keller left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread src/main/rust/surreal.rs
.exception(env, null_mut);
}
params.insert("tb".to_string(), Value::String(table));
let sql = format!("LIVE SELECT * FROM type::table($tb) WHERE {}", where_clause);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 the format! happens, noting the trust model and that the positional arg keeps the clause inert to format! itself;
  • the Rust doc comment on Java_com_surrealdb_Surreal_selectLiveWhere;
  • the Javadoc of both selectLive(table, where) and selectLive(table, where, params), which state the clause is appended verbatim with the same trust model as query(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>
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