Changelog¶
All notable changes to this project will be documented in this file.
The format follows Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.33.0] - 2026-08-12¶
Added¶
- DISKANN vector indexes and the F16 element type (SurrealDB 3.2).
IndexType::Diskannand thediskann_index(name, column, dimension, distance, vector_type)builder define the on-disk ANN graph the 3.2 engine parses, withwith_degree/with_l_build/with_alpha/with_hashed_vectorfor the tuning tail andDiskAnnDistanceTypefor the metric — its own enum (EUCLIDEAN/COSINE/INNER_PRODUCT/COSINE_NORMALIZED) because the engine's DISKANN set neither contains nor is contained by the HNSW one.MTreeVectorTypegainedF16,I8, andU8, which HNSW also accepts. The<|k,ef|>KNN operator reaches a DISKANN index through the sameKnnScanplan HNSW gets. This unblocks downstream F16/DiskANN adoption (copal roadmap item 6).
The engine echoes a DISKANN index back with DIST / TYPE / DEGREE / L_BUILD / ALPHA always spelled — defaults EUCLIDEAN / F32 / 64 / 100 / 1.2 filled in even when the definition never stated them, and a float ALPHA carrying a trailing f suffix (ALPHA 1.2f). The same lesson as the sequence BATCH/START echo applies: the renderer and builder spell the defaults explicitly, and the parser strips the f suffix, so a definition compares equal to its own echo instead of re-applying on every reconcile boot.
IndexDefinition::validate refuses what the probed engine refuses, by name: a DISKANN element type outside F32 / F16 / I8 / U8, an MTREE element type among the new F16 / I8 / U8 (MTREE still parses only its historical five), and an MTREE/HNSW metric aimed at a DISKANN index, which only diskann_distance can carry. The new IndexDefinition members (diskann_distance, degree, l_build, alpha, hashed_vector) all default off in serde, so stored snapshots and old contracts deserialise unchanged. The vector-index vocabulary moved to schema::index_vector to keep schema::index under the 1000-LOC budget; every existing path re-resolves through the old re-exports.
[0.32.0] - 2026-08-11¶
Security¶
surrealdbis required at 3.1.5 or later. The 3.0 line carries twenty-five published advisories (five high), all patched by 3.1.0 or 3.1.5, and the old"3.0"requirement let a consumer resolve a vulnerable engine and let this repository's own lock sit on one. The requirement now names the first fully patched version, so every downstream resolution is forced past the set; the lock moves to the current 3.2 line with it.
Fixed¶
-
The default branch compiles again. The ulid 3 and comfy-table 8 major bumps landed with APIs this crate no longer had:
Ulid::new()becameUlid::generate(), and comfy-table's string presets becameTableStylevalues loaded withload_style. Two call sites (the streaming subscription id and the CLI table renderer) moved to the new names; behaviour is unchanged. -
parse_table_full,parse_table_info, andparse_edge_inforefused the response shapequeryactually returns.queryanswers one result per statement, so anINFO FOR TABLEobject arrives wrapped in a one-element array that onlyparse_db_infohad learned to unwrap; every other caller had to remember to index[0]first. The table and edge parsers now accept either shape by the same argument — an INFO response is never itself an array, so the two cannot be confused — and callers that already index the wrapper keep working, since indexing yields the bare object. No other public parser takes the response value, so none can carry the same footgun. -
global_helpers_configure_and_invalidateflaked against its own binary. The cache integration tests run concurrently in one process, andis_cached_returns_false_when_no_managercallsclose_cache(), which empties the process-global manager slot; landing between another test'sconfigure_cacheand its reads of that slot, it madeinvalidateandclear_cachereport zero. The two tests that touch the global slot now serialize on a shared lock, the pattern the cache module's own unit tests already use. Test-only; no library change. -
A field that gained
REFERENCEsilently tracked nothing for its existing rows. The engine backfills nothing when the clause is added, and a self-assignment registers nothing either; only an actual value change does. Applying the DDL a diff renders therefore left<~blind to every row that predated the clause, and whatever consumed the reverse references undercounted with no error anywhere.schema::reference_backfill_sql(table, field)renders the rewrite that makes the tracking true (NONE-and-back per row, shaped by two probedFORquirks:SELECT VALUE idbecauseFORrefuses object rows,?? []because it refuses an empty selection). The field diff carries it indetailswith aSchemaDiff::reference_backfill_sql()accessor rather than insideforward_sql, because it is DML an application's own events may refuse and a live reconciler must choose where it runs; the migration generator, whose files a person reviews, writes it into the file right after the DDL, and registration inside the same transaction is probed behaviour.
Getting the rewrite through a migration file exposed a second bug: the statement splitter cut on every semicolon, which shattered any statement with a braced body — the backfill's FOR loop, and equally a DEFINE FUNCTION with more than one statement in it. The splitter now respects brace and parenthesis nesting and string literals, so a statement ends only at a top-level semicolon.
surql schema tables/export/validateandsurql bucket listread every database as empty. The CLI passed the rawclient.query("INFO FOR DB;")response toparse_db_info, butqueryanswers one result per statement, so the INFO object arrives wrapped in a one-element array the parser refused;unwrap_or_default()at all four call sites turned that refusal into an empty database report.parse_db_infonow accepts either shape (an INFO response is never itself an array, so the two cannot be confused), and the call sites surface parse errors instead of defaulting them, so an echo the parser cannot read is now a message rather than a silent nothing.
Added¶
- Record references (
DEFINE FIELD ... REFERENCE).FieldDefinitiongainedreference: Option<ReferenceAction>(IGNORE/REJECT/CASCADE/UNSET) andcomputed: Option<String>, withFieldBuilder::reference/::computedand thereverse_reference_field(name, source)constructor for the reverse half (COMPUTED <~source).Query::reverse_traverseandquery::references::reverse_reference_queryrender the<~table/<~table.{ a, b }projection that reads incoming links back.
The REFERENCE clause always spells out its ON DELETE action, because a bare REFERENCE is what INFO FOR TABLE echoes as ON DELETE IGNORE; emitting the short form would diff against the database forever. The parser reads both forms, and the assertion / default / value extractors now stop at REFERENCE and COMPUTED, which the engine emits after ASSERT.
FieldDefinition::validate rejects what v3.0.5 rejects: REFERENCE on a nested field (metadata.comics) or on anything that is not a record<table> / array<record<table>> link, and COMPUTED beside READONLY, VALUE, or DEFAULT. Union types (array<record<x>> | string), which the engine also rejects, are not expressible here.
- Background index builds (
DEFINE INDEX ... CONCURRENTLY).IndexDefinition::with_concurrentlyappends the directive, which lets a large index populate without blocking the statement.info_for_index_surql(name, table)renders the progress query andIndexBuildStatus::from_inforeads the{ building: { status, initial, pending, updated } }answer, including through the arrayDatabaseClient::querywraps results in.
v3.0.5 accepts CONCURRENTLY and then echoes the index back without it, so the parser deliberately reports concurrently: false and no comparison looks at the member. Storing it as parsed would make every background-built index diff forever.
- Table change feeds (
DEFINE TABLE ... CHANGEFEED).ChangeFeedandTableDefinition::with_changefeedrenderCHANGEFEED <duration> [INCLUDE ORIGINAL]between the mode and thePERMISSIONSclause,parse_changefeedreads it back out of theINFO FOR DBecho, anddiff_tablesreports a change as the newDiffOperation::ModifyTablecarrying the fullDEFINE TABLE OVERWRITEform (a bareCHANGEFEEDstatement would reset the table's mode and permissions).
The read side is query::changes: show_changes_surql(table, since, limit) renders SHOW CHANGES FOR TABLE <t> SINCE <versionstamp|d'...'> [LIMIT n], and ChangeSet::from_response pulls the versionstamp / changes pairs out of the answer so a consumer can resume where it stopped.
- Pre-computed view tables (
DEFINE TABLE ... TYPE NORMAL AS SELECT).ViewDefinition/ViewGroupandTableDefinition::with_viewrenderTYPE NORMAL <mode> AS SELECT <projections> FROM <tables> [WHERE ...] [GROUP BY ...|GROUP ALL],parser::parse_viewreads it back, and a changed body reports asDiffOperation::ModifyTable.
The parser splits on top-level commas and keywords only, so math::max([a, b]) stays one projection and a table literally named comment is not mistaken for a COMMENT clause. Views compare on the whitespace-normalised clause, because the engine reformats what it stores.
TableDefinition::validate rejects a view that declares fields: the engine computes a view's contents and stores no field definitions for one, so a reconciler would drop the declared fields on every boot.
- ID sequences (
DEFINE SEQUENCE).SequenceDefinition/sequence_schemarenderBATCH/START/TIMEOUTand theREMOVE SEQUENCE IF EXISTSandsequence::nextval("<name>")statements;parse_sequencereads thesequencesmap ofINFO FOR DBback;DatabaseInfo::sequencesandSchemaSnapshot::sequencescarry them; anddiff_objects::diff_sequencesreportsAdd/Modify/DropSequence.
BATCH and START are always rendered, including at their defaults (1000 and 0), because the engine echoes them either way.
- Custom functions (
DEFINE FUNCTION fn::<name>).FunctionDefinition/function_schemarender the signature, return type, body,COMMENT, andPERMISSIONS;parse_functionreads thefunctionsmap ofINFO FOR DBback (splitting arguments on top-level commas soarray<record<x>>survives, and taking the body from the outermost brace pair so a nested block does not truncate it); anddiff_objects::diff_functionsreportsAdd/Modify/DropFunction.
The engine rewrites what it stores — option<T> becomes none | T, the body loses its trailing ;, and an omitted PERMISSIONS comes back as PERMISSIONS FULL. FunctionDefinition::normalized applies the same rewrites and the diff compares canonical forms, so a function does not report as modified on every reconcile.
- Database params (
DEFINE PARAM $<name>).ParamDefinition/param_schemarender the value,COMMENT, andPERMISSIONS;parse_paramreads theparamsmap ofINFO FOR DBback; anddiff_objects::diff_paramsreportsAdd/Modify/DropParam. As with functions,normalizedfills in thePERMISSIONS FULLthe engine echoes.
The parser locates clause keywords outside quoted runs, so a value like 'leave a comment about permissions' is not cut at its own words.
-
migration::diff_objects. Every database-level object diff now lives here:diff_bucketsanddiff_analyzersmoved across (re-exported frommigration::diff, so no path changes), anddiff_namedcaptures the add / drop / modify shape they share, so a new kind is three lines rather than another copy of the walk. Net effect:migration::diffis smaller than it was before this release despite gaining change-feed and view diffing.SchemaDiffgained oneobject: Option<String>field naming the object such a diff targets. -
array<record<table>>field types. An ARRAY field with atarget_tablenow rendersarray<record<{target}>>instead of a barearray, which is the shape a to-many reference needs. Atarget_tableon an ARRAY field was previously carried but never rendered, so this changes emitted DDL for any definition that set both.
Changed¶
-
BREAKING:
SchemaSnapshotandSchemaDiffgained fields.SchemaSnapshot::sequencesandSchemaDiff::objectboth default on deserialize, so stored snapshots still load, but a struct literal that names every field stops compiling. Use the constructors (SchemaSnapshot::new/from_parts/from_all_parts) or..Default::default(), as the shipped examples now do. -
Module splits to stay inside the 1000-LOC budget.
FieldTypemoved toschema::field_typeandIndexDefinition(withIndexType, the distance and vector-type enums, and theindex/unique_index/search_index/bm25_index/mtree_index/hnsw_indexbuilders) moved toschema::index. Both are re-exported from their previous homes (schema::fields,schema::table), so no consumer path changes.
[0.31.0] - 2026-08-07¶
Added¶
-
The diff engine serves live-database reconciliation. Analyzers join
DatabaseInfo,SchemaSnapshot, anddiff_schemaswith their own parser, so the full schema a consumer declares can be compared against what a database actually holds.parse_table_fullnames the two-level composition (INFO FOR DBfor mode and permissions,INFO FOR TABLEfor fields, indexes, and events), because the database level alone yields fieldless tables, which is a trap for anyone diffing against it. -
Nullable fields (
TYPE option<...>).FieldBuilder::nullable(bool)/FieldDefinition::with_nullable(bool)wrap the rendered type inoption<...>(including record targets:option<record<blob>>) so a SCHEMAFULL column can acceptNONE. TheINFO FOR TABLEparser round-trips the wrapper, keeping migration diffing stable for nullable columns. Restores parity with surql-py (nullable=True, 1.5.8+) and the TS port; rendering for non-nullable fields is byte-identical to before. -
Row-level filtering on the graph helpers (
conditions).query::graphgained aconditionsargument ontraverse,traverse_raw,traverse_with_depth,get_outgoing_edges,get_incoming_edges,get_related_records, andshortest_path. Each entry renders throughQuery::where_and multiple entries combine withAND, so a traversal can carry a tenant guard or any other row-level predicate. Restores parity with the sibling ports, which have acceptedconditionson their graph helpers since surql-py 1.6.0.
Previously these helpers emitted a bare SELECT * FROM record->edge with no filtering hook at all. A caller needing row-level isolation, a mandatory WHERE tenant_id = ... alongside engine-enforced PERMISSIONS for instance, could not express it, and had to abandon the helpers for a hand-rolled equality-filtered edge table.
query::Condition. An ownedRaw(String) | Op(Operator)carrier withFromimpls for&str,String,&String,Operator, and&Operator, plusWhereConditionfor bothConditionand&Condition.WhereConditiontakesselfby value and so cannot be used behind a trait object;Conditionis what lets one slice mix raw fragments and operators, matching thestr | Operatorunion the sibling ports accept.
Changed¶
-
DatabaseClientclones now share ONE engine session. The SDK mints a session perSurrealclone and announces it with lifecycle events the remote router can lose under concurrency, which surfaced as intermittentSession not foundfailures in any service that clones its client per request (an axum state extraction does exactly that). The inner handle now rides anArc, so clones share the service session and session churn stops entirely. Code that wants an independent session asks for one:caller_sessionfor a caller-bound session,client.inner().clone()for a raw one. Auth calls (signin,authenticate,invalidate) now act on the shared session, which is what a service almost always means; the previous per-clone isolation was an accident of the SDK'sClone. -
BREAKING: graph helper signatures. The seven helpers above take a new trailing
conditions: Option<&[Condition]>parameter. Rust has no default arguments, so this follows the existing convention in this module of rendering a Python default argument as a requiredOption<T>parameter (ascreate_relation'sdata: Option<Value>already does). Existing call sites migrate by passingNone, which leaves the emitted SurrealQL unchanged.count_relatedis deliberately not included, because surql-py does not filter it either, and diverging would break the 1:1 contract. -
Graph helpers compose through
Queryinstead offormat!. EverySELECT-shaped helper now builds its statement withQuery::new().select(…).from_table(…).traverse(…)rather than interpolating identifiers into a string. Statement construction moved into pure sync functions (select_traversal_surql,count_related_surql,shortest_path_surql,depth_path) that are unit-testable without a live client.
create_relation and remove_relation are intentionally left hand-composed: Query::relate inlines its payload via render_data_object, whereas create_relation binds CONTENT $data as a variable, and routing it through the builder would inline caller payloads into the statement.
shortest_pathemits parenthesised predicates. Now that the identity check goes through the builder, the rendered clause isWHERE (id = <to>) [AND (…)]rather thanWHERE id = <to>. Semantically identical; noted because it changes the exact statement text.
Fixed¶
-
The guides document what this release adds. Nullable fields and
OVERWRITErendering are in the schema guide, reading a live database back throughparse_db_infoandparse_table_fullis in the migrations guide. Two examples that predate this release are corrected while passing:SchemaSnapshot::from_registrydoes not exist and never did, and aSchemaSnapshotstruct literal stops compiling every time the type gains a kind of definition, so both now use the constructors. Every example in the changed pages was compiled against the crate rather than read over. -
The dependency audit carries its two unfixable advisories in one place.
.cargo/audit.tomlnames each, what would have to change upstream for it to come out, and why it is safe to carry meanwhile. Both workflows now read that file instead of passing a flag with the reasoning written somewhere else. The file governs this repository's own audit and is not published with the crate: a consumer runningcargo auditsees both advisories and makes their own call. -
Diff results are now safe to apply to a live database. Grouped permission actions (
FOR select, create ...) compare equal to the engine's split echo instead of reporting a permanent false modification. Modify-class diffs renderOVERWRITEforms: a modified field re-defines withOVERWRITE, and a permissions change carries the owning table's FULL definition, because a permissions-onlyDEFINE TABLEwould silently reset the table's mode. -
OVERWRITErendering across the schema layer. Tables, fields, indexes, events, analyzers, and access methods gainto_surql_overwrite, andgenerate_table_sql_overwriterenders a table's full statement set with it.IF NOT EXISTScreates and then never updates, so a consumer whose definitions evolve needs the replacing form to bring an existing database up to the code's schema; data is untouched, only definitions are replaced. -
DatabaseClient::caller_sessionopens per-caller engine sessions. A cloned SDK handle is its own session, so one connection can hold a root session and record-authenticated caller sessions side by side, with the engine applyingPERMISSIONSto each session's actor. The method authenticates a record access token on a fresh clone, verifies the engine bound a record identity (refusing database-level tokens thatPERMISSIONSwould not filter), and returns a client whose drop ends the session. -
Connection credentials reach embedded engines at build time.
username/passwordnow construct embedded datastores with the root user in place, so anonymous sessions stop acting as owner and the engine is lockable from configuration alone. Remote engines keep the existing signin path. -
Query::set/set_expraccept dotted paths.SET metadata.processing = {...}is native SurrealDB nested assignment, and the schema layer already accepts dot notation for field definitions, but the update builder rejected any dotted target as an invalid identifier. Targets now validate per segment. -
FLEXIBLEnow renders immediately after theTYPEclause. The previous trailing position (... READONLY FLEXIBLE;) is a parse error on SurrealDB v3 ("FLEXIBLE must be specified after TYPE"), so any schema combining a flexible object field withREADONLY,DEFAULT,VALUE, orASSERTfailed to apply. Verified against v3.0.5: the after-TYPE position is accepted in every combination, includingoption<object> FLEXIBLE.
[0.30.0] - 2026-07-29¶
Added¶
- Files & buckets (SurrealDB v3 object storage), full code-first depth.
- Field types:
FieldType::File/FieldType::Bytes(emitTYPE file/TYPE bytes) plusfile_field(name)/bytes_field(name)builders; theINFO FOR ...field parser round-trips both. - Schema: new
schema::bucketmodule —BucketDefinitionwithbucket_schema(name, backend)/memory_bucket(name)/file_bucket(name, path)builders, renderingDEFINE BUCKET [IF NOT EXISTS|OVERWRITE] … BACKEND "…" [READONLY] [PERMISSIONS …] [COMMENT "…"],REMOVE BUCKET …, andALTER BUCKET [IF EXISTS] …(READONLY/DROP READONLY,BACKEND/DROP BACKEND,PERMISSIONS,COMMENT/DROP COMMENT). Exposed viagenerate_bucket_sql[_with_options]. - Migrations:
DiffOperation::{AddBucket, DropBucket, ModifyBucket}, abucketfield onSchemaDiff,diff_buckets(code, db), and bucket support threaded throughSchemaSnapshot,VersionedSnapshot, theSchemaRegistry(register_bucket/get_registered_buckets), the initial-migration generator, drift detection, andschema generate. - Parser:
parse_bucketreconstructsBucketDefinitionfromINFO FOR DB(bu/buckets) intoDatabaseInfo::buckets. FileRefvalue type (types::file):{ bucket, key }exposing SurrealDB's canonical key form — the key is stored verbatim (including the server's leading slash, e.g./a.txt), whileDisplayalways renders a single-slash pointer (bucket:/a.txt) for any input. serde round-trips the structured form ({ bucket, key: "/a.txt" }) and accepts thef"bucket:/key"literal the SDK emits. The Rust SDK decodesfilevalues (inhead/file::list/record fields) straight to that literal, so — unlike the Python port — nofile::bucket/file::keyprojection is needed.- Runtime API:
DatabaseClient::bucket(name)returns aBuckethandle withput/put_if_not_exists/get/get_text/exists/head/delete/copy/copy_if_not_exists/rename/rename_if_not_exists/list. Every op uses the parameterisedtype::file($bucket, $key)constructor with bound params (never string-interpolated). Binary payloads bind as a nativesurrealdb::types::Value::Bytesvia the newDatabaseClient::query_with_surreal_vars(no base64) — the JSON bind path cannot carry raw bytes. Data is accepted as aFileData::Text|Bytesenum. - CLI: new
surql bucketgroup —define/list/rmplus file opsput/get/delete/exists/files. -
Buckets require the server's
SURREAL_CAPS_ALLOW_EXPERIMENTAL=filesenvironment variable (the feature is hidden and not enabled by--allow-all; the--allow-experimental filesflag form is broken). Live round-trip coverage is intests/integration_files.rs(embedded probe + an#[ignore]d server test gated onSURREAL_FILES_URL), verified against SurrealDB 3.1.3. -
Sessions documented as unsupported. The Rust
surrealdbcrate has no multiplexed-session API, sosurql-rsdeliberately ships none (unlike the Python / TypeScript ports). The newconnection::sessionmodule documents this and advises a separateDatabaseClientper isolated namespace/auth context.
[0.29.0] - 2026-06-17¶
Added¶
- Full-text search (BM25) is now first-class — the sparse leg of hybrid retrieval. Define a
DEFINE ANALYZERin code withanalyzer(name)/standard_analyzer(name)(AnalyzerDefinition+Tokenizer+TokenFilter, rendered viagenerate_analyzer_sql/generate_analyzer_sql_with_options); build a BM25-scored full-text index withbm25_index(name, columns, analyzer)(orsearch_index(...).with_analyzer(...).with_bm25().with_highlights()); and run the lexical query withQuery::fulltext_search(field, reference, query)+Query::search_score(reference, alias), or thefulltext_search_query(...)helper. Pair it withvector_searchand fuse the two result orders by rank (Reciprocal Rank Fusion). Verified end-to-end against an embedded SurrealDB engine intests/integration_fulltext.rs.
Fixed¶
- Full-text index now emits the SurrealDB 3.x
FULLTEXTkeyword. The full- text index keyword was renamed fromSEARCHtoFULLTEXTin SurrealDB 3.0, so the previous output (... SEARCH ANALYZER ascii) was a parse error on v3.IndexType::Search/search_index/IndexDefinition::to_surql*and the migration diff now emitFULLTEXT, and theINFO FOR TABLEindex parser recognises both spellings. Seedocs/v3-patterns.md§9 — including the note that the v3 streaming executor's full-text scan returns rows in BM25 relevance order butsearch::scoreis not plumbed through it (returns 0), so rank by the scan's natural order.
[0.28.1] - 2026-06-12¶
Fixed¶
- Connect retries no longer mask the real failure behind "Already connected". The SDK engine connects once per handle and rejects a second
connect; after a partially-successful attempt (engine up, then credential signin or namespace selection failed), every retry died on that rejection, so the surfaced error wasAlready connectedinstead of the actual failure (e.g.There was a problem with authentication), and retries 2..n never re-attempted the failing step at all.DatabaseClientnow tracks engine-level connection state: a retry — or aconnecton an already-connected client (reconnect), which failed the same way — skips the engine connect and resumes at the step that failed.
[0.28.0] - 2026-06-06¶
Fixed¶
- Table-level
PERMISSIONSnow render correctly.TableDefinitionemitted a malformedDEFINE FIELD PERMISSIONS FOR {action} ON TABLE ...per action, which SurrealDB rejects (Unexpected token FOR). Table permissions now render inline on theDEFINE TABLEstatement (... PERMISSIONS FOR select WHERE ... FOR create WHERE ...), the only valid placement. Affectsto_surql_with_options/to_surql_all_with_optionsandgenerate_table_sql. - Edge table
PERMISSIONSwere silently dropped.EdgeDefinitionignored itspermissionsentirely; they now render inline on theDEFINE TABLE ... TYPE RELATIONstatement. - Migration diff renders a permissions change as valid SurrealQL. A
ModifyPermissionsdiff emitted the same malformedDEFINE FIELD PERMISSIONSform; it now emits a singleDEFINE TABLE <t> PERMISSIONS ...statement. (ASCHEMAFULLtable re-defined this way falls back toSCHEMALESS; full-mode fidelity is a follow-up once the diff carries the table mode.)
Added¶
- Expression-valued
UPDATE ... SETfor atomic read-modify-writes.Query::update_setbegins anUPDATE <target> SET ...whose assignments are supplied viaset(literal) orset_expr(expression-valued), combinable withwhere_andRETURN.Expressionnow implements the standard arithmetic operators (+ - * /over anythingInto<Expression>, withFrom<i64|i32|f64>for numeric literals), so aSETvalue can reference the row's current fields — e.g.UPDATE t SET n = n + 1 WHERE ...collapses a read-modify-write into one statement. is_none/is_not_noneoperators (field IS NONE) — the correct guard for an absent optional field, which SurrealDB reports asNONE, notNULL.AccessDefinition::to_surql_with_options(if_not_exists)andgenerate_access_sql_with_options(access, if_not_exists)to emitDEFINE ACCESS IF NOT EXISTS ...for idempotent re-application (e.g. a persistent store applying its schema on every connect).
[0.2.7] - 2026-05-30¶
Added¶
- Typed
record<table>field emission.FieldDefinitiongains atarget_tablefield, andrecord_field(name, Some("user")), the newtarget_table(...)builder setter, andwith_target_table(...)all renderTYPE record<user>. A canonicaltype::record("X", $value)coercion on a RECORD field is auto-lifted intotarget_tableat build time, dropping the now redundant VALUE clause. TheDEFINE FIELDparser readsrecord<table>back intotarget_tableso typed records round-trip.
[0.2.6] - 2026-05-22¶
Maintenance release focused on closing open security, dependency, and CI-hygiene work. No public-API breaking changes.
Security¶
- Bumped
opensslfrom0.10.79to0.10.80viacargo update, closing CVE-2026-45784 (medium severity, potential out-of-bounds write inCipherCtxRef::cipher_update_inplacefor AES-KW-PAD ciphers). The crate's defaultclient-rustlsbackend never linksopenssl; the bump only affects consumers that opt into theclient/client-tlsfeature.
Fixed¶
- Daily Security Audit workflow no longer fails on every scheduled run. Replaced the deprecated Node.js 20
rustsec/audit-check@v2.0.0action with a directtaiki-e/install-action+cargo auditinvocation. The new step exits non-zero only on actual vulnerabilities; informational unmaintained warnings (atomic-polyfill, bincode 2.x) are surfaced as logs because they reach the dep graph transitively throughsurrealdband are not actionable from this repo.
Changed¶
- CI workflow (
ci.yml) now runs thestableRust toolchain only on push and pull-request triggers.betatoolchain coverage moved to the daily Nightly workflow so regressions still surface within 24 hours without paying for two parallel jobs on every PR rev. - Added
paths-ignorefilters toci.ymlandcoverage.ymlso pure documentation, LICENSE, or.editorconfig/.gitignorechanges no longer trigger a full compile + clippy + test run.docs.ymlalready handles documentation rebuilds. - Dependabot auto-merge workflow now uses
dependabot/fetch-metadata@v3andlewagon/wait-on-check-action@v1.7.0, the latest stable majors of both actions. docs/features.mdanddocs/migration.mdcorrected: the default feature has beenclient-rustlssince0.2.3, notclient.
Added¶
docs/connection-management.mddocuments the task-scoped current client,ConnectionRegistry,AuthManager,StreamingManager/LiveQuery, andTransaction.docs/caching.mddocumentsCacheManager, theMemoryCacheandRedisCachebackends, thecached/cached_with/cache_key_forhelpers, and the invalidation surface.docs/orchestration.mddocumentsEnvironmentConfig/EnvironmentRegistry,DeploymentPlan,DeploymentCoordinator, the four built-inDeploymentStrategyimplementations (Sequential, Parallel, Rolling, Canary),DeploymentResult, andcheck_environment_health/verify_connectivity.docs/migration.mdnow carries anUpgrading 0.2.5 -> 0.2.6section.mkdocs.ymlnavigation surfaces the three new module pages under Guides.
[0.2.5] - 2026-05-19¶
Brings the parser, RecordID, and batch surfaces to feature parity with the surql-py 1.6.4 / 1.7.0 release window (and the sibling surql v1.5.0 TypeScript port). Also hardens the CI workflow set so PRs do not double- run and the docs build no longer serialises every ref behind a single queue.
Added¶
-
parse_edge_info(edge_name, info, define_table)insurql::schema::parser— counterpart to [parse_table_info] for graph-edge tables defined viaedge_schema/ [EdgeDefinition]. Edge mode is detected from theDEFINE TABLEstatement:TYPE RELATIONresolves toEdgeMode::Relation,SCHEMAFULLtoEdgeMode::Schemafull, anything else toEdgeMode::Schemaless.FROM <table>andTO <table>are extracted independently so a malformed live definition that lost one clause surfaces as missing-endpoint drift instead of a parse failure. OnRelation-mode edges the auto-emittedinandoutfield declarations SurrealDB stores are stripped on parse — they are implicit whenTYPE RELATIONis set, so the code-sideEdgeDefinitiondoes not declare them and round-trip diffs were flagging them as orphan additions. Per-actionPERMISSIONSround-trip via the newparse_table_permissionshelper. -
parse_table_permissions(definition)insurql::schema::parser— extracts the per-actionPERMISSIONSrules from aDEFINE TABLEstatement string. ReturnsNonefor the trivialNONE/FULLpostures (the code-side helpers have no representation for those) and for definitions without aPERMISSIONSclause. Recognises the expanded form (FOR select WHERE r1 FOR create WHERE r2 …), the comma-joined form v3 emits when several actions share a rule (FOR select, create, update, delete WHERE r), and arbitrary mixes of both. The Rustregexcrate does not support lookahead, so the body is split onFORboundaries before applying the per-clause matcher — same per-action map shape the surql-py port produces, no lookahead. -
parse_table_info(name, info, define_table)— the optional third argument is theDEFINE TABLE <name> ...statement string, fetched fromINFO FOR DB'stables.<name>entry. SurrealDB v3'sINFO FOR TABLEdoes not include the table-levelDEFINE TABLEstatement, so table mode andPERMISSIONScannot be recovered from it alone. Withoutdefine_tablethe parser falls back to the legacytbkey inside the response (the v1 / v2 shape) and table mode defaults toSchemalesson v3. -
strip_brackets(value)insurql::types, re-exported from the crate root. SurrealDB v3 wraps record-id keys that contain anything other than[A-Za-z_][A-Za-z0-9_]*or pure digits in unicode angle brackets⟨ … ⟩(U+27E8 / U+27E9). Downstream consumers that wanted the baretable:idshape were callingvalue.replace('⟨', "").replace('⟩', "")themselves at every API boundary;strip_bracketscentralises that strip and also accepts the legacy ASCII< … >form.Noneis passed through untouched so the helper is safe to apply unconditionally. -
upsert_many_in_tx(txn, table, items, conflict_fields)— atomic counterpart to [upsert_many]. Queues oneUPSERT <target> CONTENT { … }statement per item on the supplied [Transaction] buffer; the per-record statements inherit the surroundingBEGIN TRANSACTION/COMMIT TRANSACTIONframing so a single bad record rolls back the entire batch on commit instead of leaving the database half-seeded.Transaction::executequeues raw SQL without param bindings, so the CONTENT payload is rendered as a SurrealQL object literal (rather than$data-bound as it is in autocommit mode). Bothupsert_manyandupsert_many_in_txaccept an optionalconflict_fieldsslice that emits an inline-valueWHERE … AND …clause appended to each UPSERT.
Fixed¶
-
build_upsert_queryemittedUPSERT INTO <table> [ {…}, {…} ], which SurrealDB v3 rejects with a parse error — v3 wants a single record-id or table target afterUPSERT, not an array literal. The renderer now emits oneUPSERT <target> CONTENT { … }statement per item, joined by;, matching the surql-py 1.7.0 / surql 1.5.0 shape that is portable across the sibling ports. The pre-0.2.5 source comment acknowledged the bug ("not valid SurrealDB v3 SurrealQL") but kept the broken shape for byte-for-byte parity with the older surql-py renderer; that parity bridge is no longer needed. -
build_upsert_queryconflict_fieldsemittedWHERE field = $item.field, which has no$itembinding in scope at the call site (and the rendered string is also fed verbatim toTransaction::execute, which queues raw SQL without binding params). The renderer now inlines the conflict values (WHERE email = 'a@b.com' AND tenant = 'BFS'), matching the surql 1.5.0 fix. -
RecordID::Displayemitted ASCII<id>brackets for ids that could not be rendered bare. SurrealDB v3 rejects ASCII</>in record-id positions withUnexpected token '<', expected a record-id key; the output now uses the v3-correct unicode escape syntax⟨id⟩(U+27E8 / U+27E9).RecordID::parseaccepts both forms on input so legacy wire payloads still round-trip cleanly. Breaking for callers that asserted on the exactDisplayoutput; the SQL shape is identical otherwise. -
RecordID::needs_angle_bracketsaccepted leading-digit ids bare (chunk:1abc). The pre-0.2.5simple_id_patternwas[A-Za-z0-9_]+, which let1abcslip through and produced a literal v3 rejects withUnexpected token. The newidentifier_id_patternis[A-Za-z_][A-Za-z0-9_]*, with a separate allow-list for pure- digit strings (which v3 parses as integer-key ids and round-trips bare). Matches surql-py 1.7.0.
Changed¶
-
upsert_manyno longer routes throughUPSERT <table> CONTENT $datafor items that lack anidfield. The autocommit path always pins the target —data.idwhen present,<table>otherwise — and stripsidfrom the bound payload so v3 does not reject the duplicate field. -
CI workflow set hardened against runaway runs:
docs.ymlswitched from the globalgroup: pagesconcurrency queue (which serialised every build + deploy across all refs and caused multi-day stalls when a long-running deploy held the queue) to a per-ref group withcancel-in-progress: true.ci.ymlandcoverage.ymlgained per-ref concurrency groups so rapid pushes to a PR cancel the in-progress run. The redundantpush: branches: ['release/**']triggers were dropped — release branches only ever receive PRs that already fired the workflow viapull_request, so the push trigger was pure duplicated work.audit.yml,dep-review.yml, andpr-title.ymlgained per-ref concurrency groups so a sequence of PR edits cancels the in-progress lint and only the latest revision is checked.
Verified¶
cargo fmt --all -- --check— clean.cargo clippy --lib --all-features --tests -- -D warnings— clean.cargo test --lib --no-default-features— 927 passed, 0 failed.cargo test --lib --all-features— 1088 passed, 0 failed (baseline was 1066 on 0.2.4; +22 regression tests coveringparse_edge_info,parse_table_permissions,strip_brackets, unicode-bracketRecordID::Display, and the per-recordbuild_upsert_queryshape).- All integration tests compile.
[0.2.4] - 2026-05-02¶
Added¶
client-wasmfeature (Oneiriq/surql-rs#115). Wasm-friendly client surface that compiles cleanly towasm32-unknown-unknown. Pullssurrealdbwithprotocol-ws+kv-memonly -- norustls/native-tls/reqwest, since browsers terminate TLS at the WebSocket layer andkv-memlets wasm callers run an embedded engine for local state and tests. Exposes the sameDatabaseClient/executor/crud/graph/batchAPI asclient-rustls.[target.'cfg(target_arch = "wasm32")'.dependencies]block inCargo.tomlthat overridestokioto the wasm-buildable subset (sync,macros,rt,time) and pullsgetrandom 0.3with thewasm_jsfeature soulid/rand_corelink on wasm..cargo/config.tomlwith the--cfg=getrandom_backend="wasm_js"rustflag required bygetrandom 0.3onwasm32-unknown-unknown(the feature flag alone is insufficient -- see https://docs.rs/getrandom/0.3/#webassembly-support).scripts/check-wasm.sh-- canonical local + CI gate for the wasm build. On macOS auto-detects Homebrew LLVM socc-rscan handring 0.17's build script a wasm-capable clang (Apple's/usr/bin/clanghas no wasm32 backend).
Changed¶
- The optional
tokiodependency moved from a top-level[dependencies]declaration withfeatures = ["full"]to two target-specific declarations: native targets keep the historical["full"]feature set, whilewasm32-*targets get["sync", "macros", "rt", "time"]. No source-level API changes.
Fixed¶
cargo build --target wasm32-unknown-unknown -p oneiriq-surql --no-default-features --features client-wasmnow succeeds on a system with a wasm-capable clang in scope. Unblocks Oneiriq/pixel-stroke#236 (web-build ofpixel-stroke-persistence).
[0.2.3] - 2026-05-02¶
Changed¶
- The default feature set is now
["client-rustls"](pure-Rust TLS). Previously the default was["client"], which pulledsurrealdb/native-tlsandreqwest/default-tlsand thereforeopenssl-sysinto the dependency graph. The historical native-tls backend is still available via theclientfeature (now also exposed under theclient-tlsalias) for consumers that need the system OpenSSL stack. - The
cliandorchestrationfeatures now depend onclient-rustlsinstead ofclientso thatcargo install oneiriq-surql --features cliand other typical builds no longer compile againstopenssl-sys.
Security¶
- Drops the
openssl-systransitive dependency from the default dependency graph, clearing the following Dependabot advisories on this crate's published default build: - rust-openssl: incorrect bounds assertion in AES key wrap (HIGH)
- rust-openssl: unchecked callback length in PSK / cookie trampolines leaks adjacent memory to peer (HIGH)
- rust-openssl:
MdCtxRef::digest_final()writes past caller buffer with no length check (HIGH) - Consumers who explicitly opt into
--features client(or theclient-tlsalias) still link the system OpenSSL stack and remain subject to upstreamrust-openssladvisories.
[0.2.2] - 2026-04-21¶
Added¶
client-rustlsfeature (Oneiriq/surql-rs#97). Same surface as the defaultclientfeature, but with a pure-Rust TLS stack (rustls+webpki-roots) instead ofnative-tls. Enables building on runners that do not havelibssl-dev/ the system OpenSSL headers installed. See docs/features.md for the trade-offs and docs/migration.md for a switching guide.
Changed¶
- The
clientfeature now explicitly selectssurrealdb/native-tlsandreqwest/default-tls. Behaviour is unchanged for existing consumers (the implicit TLS stack was alreadynative-tls), but the TLS backend is no longer inherited from upstream defaults -- it is pinned by the feature flag. No API changes. - Optional
surrealdbandreqwestdependencies are declared withdefault-features = falseso the TLS backend is selected exclusively byclient/client-rustls.
[0.2.1] - 2026-04-18¶
Documentation¶
docs/features.md-- full feature-flag reference.docs/query-ux.md-- before / after walkthroughs for the 0.2 crate-root helpers (type_record,type_thing,extract_many,has_result,select_expr,execute,aggregate_records).docs/v3-patterns.md-- SurrealDB v3-specific SurrealQL shapes (subprotocol handshake,type::recordrename, datetime coercion, unrolled graph depth, rejectedUPSERT INTO [...], buffered transactions,SurrealValueavoidance).docs/cli.md-- full subcommand reference (replaces the pre-0.1 "planned" placeholder).docs/migration.md-- 0.1.x -> 0.2.x upgrade notes.- Updated README top-level example with
type_record,Query::select_expr,Query::execute,aggregate_records. - Updated
mkdocs.ymlnav with the new pages anddocs.rs/oneiriq-surqlreference link. - Fixed pre-existing rustdoc intra-doc link warnings so
cargo doc --no-deps --all-featuressucceeds underRUSTDOCFLAGS="-D warnings".
No API changes.
[0.1.0 - 0.2.0] see releases¶
Added¶
migration::versioning--VersionedSnapshot,VersionGraph, andcompare_snapshotsfor DAG-based migration history.migration::generator-- generate migration files (generate_migration,generate_initial_migration,create_blank_migration,generate_migration_from_diffs) with atomic writes and round-trip load.migration::diff-- schema diff engine (diff_tables,diff_fields,diff_indexes,diff_events,diff_permissions,diff_edges,diff_schemas).migration::{models, discovery}--.surqlfile-format migrations with-- @metadata/-- @up/-- @downsection markers and SHA-256 checksum.schema::{visualize, themes, utils}-- Mermaid / GraphViz / ASCII diagrams with modern / dark / forest / minimal themes.schema::parser-- parses SurrealDBINFO FOR DB/INFO FOR TABLEresponses back into schema definitions.schema::{validator, validator_utils}-- cross-schema validation with severity-filtered reports.schema::{sql, registry}-- full DEFINE-statement composition and a thread-safeSchemaRegistry.schema::{fields, table, edge, access}-- code-first schema DSL.query::{builder, helpers}-- immutableQuerywith fluent chaining.query::expressions-- 25+ function builders and typed expression kinds.query::{hints, results}-- query optimization hints + typed result wrappers with raw-response extraction helpers.connection::{config, auth}-- connection configuration (URL / ns / db / timeouts / retry / live-queries gate) + auth credential types.types::{operators, record_id, record_ref, surreal_fn, reserved, coerce}-- operator enum +RecordID<T>with angle-bracket syntax + reserved-word checks + ISO-8601 datetime coercion.error::SurqlError-- unified error enum withContextchaining trait.
Notes¶
This is a pre-release port of surql-py targeting 1:1 feature parity. The runtime async client, CRUD executor, and CLI land in the 0.1 -> 0.2 window.