SurrealDB v3 patterns¶
The 0.1 -> 0.2 window rebased the crate on the surrealdb 3.x driver (previously 2.x). Several of the SurrealQL shapes that worked on v2 are parse errors on v3. This page documents every call-site where surql adapted, so consumers porting their own SurrealQL know what to watch for.
1. Subprotocol handshake¶
DatabaseClient wraps surrealdb::Surreal<surrealdb::engine::any::Any>. The Any engine picks the transport from the URL at runtime:
| URL scheme | Engine |
|---|---|
ws:// / wss:// | WebSocket + RPC subprotocol |
http:// / https:// | HTTP |
mem:// | In-process |
surrealkv:// | SurrealKV embedded |
file:// / rocksdb:// | Local file stores |
The v3 driver negotiates an RPC subprotocol on connect (v2 skipped the handshake). DatabaseClient::classify_surrealdb_error specifically recognises subprotocol in the error message and re-tags the failure as [SurqlError::Connection] so retries use the connection back-off schedule rather than the query back-off schedule.
let client = surql::DatabaseClient::new(config)?;
client.connect().await?; // handshake happens here, not in ::new.
2. type::thing -> type::record¶
type::thing(table, id) was renamed to type::record(table, id) in v3. The old name emits:
surql ships both helpers so existing queries keep working under either server version:
type_record- renderstype::record(...). Preferred on v3.type_thing- renderstype::thing(...)verbatim. Use when a query plan relies on the literal function name matching.
The migration history recorder uses type::record:
// src/migration/history.rs
let surql = format!(
"CREATE type::record('{table}', $id) SET {set};",
table = MIGRATION_TABLE_NAME,
);
3. Datetime coercion¶
v3 rejects bare ISO-8601 strings for datetime-typed fields with:
The fix is to keep the cast explicit in SurrealQL:
// src/migration/history.rs
let mut set = String::from(
"version = $version, description = $description, \
applied_at = <datetime> $applied_at, checksum = $checksum",
);
types::coerce provides the inverse - coerce arbitrary serde_json::Values into ISO-8601 strings suitable for this pattern.
4. Unrolled graph-traversal depth¶
v3 rejects the Python port's depth-templated traversal syntax:
The trailing -> leaves no target. shortest_path instead iterates depths and unrolls the arrow chain with SurrealDB's ? wildcard:
// src/query/graph.rs
for depth in 1..=max_depth {
let mut path = String::new();
for _ in 0..depth {
write!(path, "->{edge_table}->?").unwrap();
}
let surql = format!(
"SELECT * FROM {from_record}{path} WHERE id = {to_record} LIMIT 1"
);
// ...
}
Incoming edges use FROM <record><-<edge> rather than v2's FROM <-edge<-<record>:
5. UPSERT INTO <table> [...] rejected¶
v3 requires a single target after UPSERT, not an array literal. The Python port's bulk pattern:
is a parse error. batch::upsert_many iterates per record and emits:
per row, with the payload bound as a variable. build_upsert_query still emits the Python-compatible statement for logging / preview output that needs byte-for-byte parity.
6. Buffered transactions¶
The v3 driver does not stream BEGIN / COMMIT / CANCEL as separate query() calls - each query() is an isolated request, and the server rejects a bare COMMIT. Transaction::execute buffers statements client-side and Transaction::commit flushes them as a single atomic request:
Transaction::rollback simply drops the buffer without contacting the server. See crate::connection::transaction for the full API.
Usage is unchanged from the v2 port:
let mut tx = client.begin_transaction().await?;
tx.execute("CREATE user SET name = 'Alice'").await?;
tx.execute("CREATE user SET name = 'Bob'").await?;
tx.commit().await?;
7. Structured Token on signin¶
v2 returned an opaque Jwt; v3 returns a structured Token. surql transparently re-exports via the upstream surrealdb::opt::auth::Token, so the [AuthManager] cache, refresh loop, and the connection::auth credential types all operate on the new type without requiring any caller changes.
8. SurrealValue envelope avoided¶
The typed-call envelope on v3 is the SurrealValue trait, which would require T: SurrealValue + Serialize + DeserializeOwned bounds on every typed helper. surql deliberately routes typed CRUD through raw SurrealQL + serde_json::Value, keeping the public bound at just serde::Serialize + serde::de::DeserializeOwned:
pub async fn fetch_one<T: DeserializeOwned>(
client: &DatabaseClient,
query: &Query,
) -> Result<Option<T>> { /* ... */ }
This keeps caller code identical between v2 and v3 and avoids a SurrealValue derive on every schema record type.
What's next¶
- Query UX helpers - the 0.2 crate-root additions.
- Migration 0.1 -> 0.2 - upgrade notes.
- API reference - generated rustdoc.