Skip to content
NEW BENCHMARKS

SurrealDB 3.x by the numbers

View

1/3

LATEST STABLE

Release 3.1

6 patch releases · Latest 3.1.4 on Jun 10, 2026

SurrealDB 3.1.4 is a patch release on the 3.1 line. It addresses a permission-enforcement vulnerability in array element-level SELECT permissions, alongside a…

3.1.4

LATEST STABLE

Released on Jun 10, 2026

SurrealDB 3.1.4 is a patch release on the 3.1 line. It addresses a permission-enforcement vulnerability in array element-level SELECT permissions, alongside a query-planner correctness fix, a permission fix for the experimental file buckets feature, and a build fix for asynchronous Surrealism modules.

The security fix addresses array element-level SELECT permissions that could leak denied elements. We recommend that all users on the 3.1 line upgrade.

v3.1.4 is a drop-in upgrade from any earlier 3.1.x release, with no SurrealQL surface changes and no catalog or on-disk layout changes.

v3.1.4 is a patch release with 1 security fix and 3 bug fixes across the 3.1 series.

Security

A field permission on an array's elements (for example PERMISSIONS FOR select WHERE ... on items[*]) expanded the array in ascending index order and removed each denied element with an in-place Vec::remove, which shifts every later element down. Cutting forward therefore invalidated the pending indices: a deny-all permission leaked the odd-indexed elements, and a per-element predicate kept or dropped the wrong elements. All affected paths now cut elements in reverse index order, so denied elements are removed by identity and permitted siblings are preserved.

Bug fixes

  • Fixed type::field('id') equality not using the record-id point lookup. A query such as SELECT * FROM item WHERE type::field('id') = item:1 (and the reversed-operand form) is now planned as a RecordIdScan point lookup, like a bare id, instead of falling back to a full table scan when no user-defined index on id exists. Residual conjuncts such as AND score > 15 are still applied after the lookup.

  • Fixed bucket PERMISSIONS WHERE predicates not being enforced in the streaming file executor. The streaming executor previously could not evaluate a bucket's Permission::Specific expression and fell back to a role-only check, allowing record and guest users to bypass DEFINE BUCKET ... PERMISSIONS WHERE clauses on file::get, file::put, file::copy and related functions. The executor now evaluates the bucket predicate with the $action, $file and $target parameters bound, matching the legacy compute path, and list operations are checked against the List operation rather than Exists.

  • Fixed building asynchronous Surrealism modules with surreal module build. The Surrealism SDK relies on Tokio's net feature on the wasm32-wasip2 target, which currently requires the tokio_unstable cfg flag. The build command now generates a .cargo/config.toml with the required build flags (leaving any existing file untouched) and sets RUSTFLAGS accordingly. Failed cargo build and cargo metadata invocations now surface the underlying compiler output instead of a generic error message.

Upgrade or install

Get SurrealDB v3.1.4

Pick how you want to install or upgrade. Surrealist can update connected instances in place, or choose a platform below to copy a CLI command for v3.1.4.

You can upgrade your SurrealDB Cloud instance to v3.1.4 effortlessly through the Surrealist app.

  1. Select your organisation and instance
  2. On the dashboard, click on the "Upgrade" button
  3. Your instance will be updated and restarted automatically

3.1.3

Released on Jun 3, 2026

SurrealDB 3.1.3 is a patch release on the 3.1 line. It focuses on connection and SDK robustness, query-engine correctness, and build fixes across more platforms.

Notable fixes include a cold-start "Session not found" race in the engine router, a graph traversal carrying an inline filter that returned no rows, permission predicates that run their own subqueries, and embedded scripting fetch() Blob types. It also restores 32-bit builds and makes indexed k-nearest-neighbour query plans clearer in EXPLAIN.

v3.1.3 is a drop-in upgrade from any earlier 3.1.x release, with no SurrealQL surface changes and no catalog or on-disk layout changes. The one platform note is that DiskANN vector indexes now require a 64-bit target.

v3.1.3 is a patch release with 3 minor improvements and 10 bug fixes across the 3.1 series.

Improvements

  • Clearer kNN query plans. EXPLAIN now reports the filter pushed into an indexed k-nearest-neighbour search as a predicate attribute on the KnnScan operator, mirroring how TableScan reports its own predicate. Previously an indexed kNN query with a pushed-down filter was indistinguishable in the plan from one without.

  • Embedded surrealmx storage engine upgraded to 0.22.0. The upgrade is API-compatible with no query-surface change, and drops a transitive dependency (ferntree) as a precaution against a theoretical race-condition risk.

  • Looser wasm-bindgen constraint. wasm-bindgen-futures is no longer pinned to an exact version, so projects that depend on the SDK alongside a newer wasm-bindgen resolve cleanly.

Bug fixes

  • Fixed a cold-start race in the engine router that could surface a spurious "Session not found" error on the first request after connecting.

  • Idempotent USE entries are now coalesced in the remote replay log, so reconnecting no longer replays redundant namespace and database selections.

  • In-flight queries are now cancelled when their WebSocket connection closes, instead of continuing to run after the client has disconnected.

  • Fixed a graph traversal carrying an inline filter returning no results. A query such as SELECT * FROM person:1->(knows WHERE since > 2020) now returns the matching edges instead of an empty array.

  • Permission predicates now evaluate as the system. A permission WHERE clause that runs its own subquery or graph traversal no longer recursively re-triggers row-level permission checks. Denial behaviour is unchanged.

  • In embedded scripting, the type of a fetch() response Blob is now derived from the Content-Type header, matching the Fetch specification.

  • The GraphQL schema cache now evicts entries deterministically, in first-in, first-out order.

  • Restored 32-bit builds. i686, armv7, and other 32-bit targets compile again, with DiskANN now gated to 64-bit targets.

  • Fixed a build failure when compiling with only the protocol-http feature, without WebSocket support.

  • Fixed a panic when running with tokio-console enabled. Release artifacts are now built with the configuration that tokio-console requires.

Newer patch available

Upgrade to 3.1.4

You are viewing the 3.1.3 changelog. A newer patch in this release line is available - we recommend running 3.1.4 for the latest fixes and improvements.

View 3.1.4 release notes

3.1.0

Released on May 26, 2026

SurrealDB 3.1.0 is the first minor release on the 3.0 line. It is a stability-focused update with a thorough security pass, many correctness fixes, faster read and scan paths, and a single unified observability pipeline.

It also adds a first-party MCP server for AI agents, a DiskANN vector index alongside HNSW, async functions in Surrealism plugins, full ALTER coverage for every DEFINE statement, distributed trace propagation, a GraphQL overhaul (Apollo naming, cursor pagination, GRAPHQL_ALIAS, and expanded filters), and value::expect for assertions in SurrealQL. In SurrealDB Enterprise, you also get durable audit logging and slow-query telemetry.

The 3.0 → 3.1 catalog and on-disk layouts are unchanged, so existing 3.0.x deployments can upgrade in place. A handful of operational defaults have been tightened, notably RocksDB readahead, which is now 256 KiB. See Improvements for details.

v3.1.0 lands 10 highlight features, 46 broader improvements, 108 bug fixes, and 22 security fixes across the 3.1 series.

Highlights

SurrealDB now exposes a typed tool surface for AI agents. You can run it locally as a surreal mcp stdio subcommand for IDE integrations, or over HTTP at /mcp behind the existing authentication middleware.

  • Twelve typed tools: query, select, create, insert, upsert, update, delete, relate, info, list, use, and run.

  • read_only_hint / destructive_hint / idempotent_hint annotations so MCP clients can prompt before mutating operations.

  • Self-describing schema resources at surrealdb://schema/ns/{ns}/db/{db}[/table/{table}].

  • New environment variables: SURREAL_HTTP_MAX_MCP_BODY_SIZE (default 4 MiB), SURREAL_MCP_QUERY_TIMEOUT_SECS (default 60 s), SURREAL_MCP_MAX_RESULT_BYTES (default 256 KiB), SURREAL_MCP_RUN_MAX_ARGS (default 64), and SURREAL_MCP_PARAMS_MAX_KEYS (default 256).

Observability is now a single OpenTelemetry pipeline instead of separate community Prometheus and SemConv paths.

  • Every signal is recorded once and routed to many exporters: Prometheus text on /metrics, OTLP metrics push, OTLP logs push.

  • Metric families are reorganised under the surrealdb.* scope (statement, query, transaction, RPC, auth, session, network, HTTP, live query, process, storage).

  • The /metrics endpoint enforces a render-time PUBLIC_METRICS allow-list. Anonymous scrapers see only aggregate process gauges, while root-authenticated operators see the full surface.

  • Tenant context (namespace / database / user) is carried on every labelled family but filtered out of the public view by family name.

  • New surrealdb.transaction.retries / surrealdb.transaction.conflicts counters fire from the commit path when the storage engine returns a retryable error.

  • MCP tool dispatch now emits per-tool request metrics alongside GraphQL, HTTP, and WebSocket surfaces.

  • New environment variable: SURREAL_PROCESS_METRICS_REFRESH_INTERVAL (default 5 s). Retired: SURREAL_TELEMETRY_NAMESPACE, SURREAL_TELEMETRY_RPC_LIVE_ID. Repurposed: SURREAL_METRICS_ENABLED.

  • This is a breaking change for existing dashboards. See the observability documentation for the migration table and the full new surface.

Inbound traceparent / tracestate headers are now respected on every transport, so OTEL-aware clients see traces extend continuously through the server.

  • HTTP: traceparent is extracted from request headers and parented onto the existing request span. This applies to /rpc, /sql, /key/*, /graphql, /health, and every other route.

  • WebSocket: per-message trace context lives in the RPC envelope (trace_context field, matching the W3C name), so a long-lived connection can serve many independent SDK operations under their own traces instead of bucketing them all under the first one.

  • When SURREAL_LOG_OTEL_LEVEL is unset, the four user-facing spans (request, rpc/call, rpc.execute, executor) surface out of the box. Operators who want deep nested core instrumentation can set SURREAL_LOG_OTEL_LEVEL=trace.

  • Behaviour is unchanged for SDKs that don't yet emit propagation context. Requests without a traceparent continue to produce fresh root spans.

A new vector index type sits alongside HNSW under the same KNN query operator, with an overhauled warm-lookup path that benefits both implementations.

  • New index syntax:

      DEFINE INDEX pts_embedding_diskann ON pts FIELDS embedding
    DISKANN DIMENSION 4 DIST EUCLIDEAN TYPE F32 DEGREE 16 L_BUILD 64;
    SELECT id, label FROM pts
    WHERE embedding <|2,64|> [0f, 0f, 0f, 0f];

    <|K, EF|> queries DiskANN through the same operator used for HNSW.

  • Both DiskANN and HNSW gain TYPE F16, TYPE U8, and TYPE I8 element types, plus DISTANCE INNER_PRODUCT and DISTANCE COSINE_NORMALIZED metrics. Add HASHED_VECTOR to the index definition if vector-to-document mappings should be stored through vector hashes instead of full vectors.

  • The ANN lookup path was reworked with process-local caches for graph payloads, vector-to-document mappings, and doc-id lookups. KV remains the source of truth. Cache misses fall back to storage, and uncommitted writes never poison the cache.

  • DEFINE INDEX on an ANN index now drains pending vectors before returning, so a freshly-defined index is immediately usable.

  • Concurrent builds and kNN at release. DiskANN compactions coordinate the cache and graph lock through commit so concurrent DEFINE INDEX … CONCURRENTLY and KNN queries do not race. HNSW steady-state kNN avoids serialising on graph reload when the in-memory graph already matches KV. Intermittent DiskANN KNN search failed errors from 3.1 betas are addressed (PR #7318).

A series of changes lift the new executor's scan throughput well above 3.0, on both cold and warm data.

  • Predicate prefilter on every KV scan. Supported WHERE predicates are evaluated against raw key/value bytes before the record is decoded, skipping rows that are provably false. EXPLAIN reports whether the prefilter applied and which predicates were pushed down.

  • K-way merge for WHERE col IN [...] ORDER BY other LIMIT N. UnionIndexScan now merges per-IN-list branches by the composite-index suffix column, so an outer LIMIT can stop after ~LIMIT rows globally instead of materialising every match.

  • Composite array-element indexes for CONTAINS / INSIDE and CONTAINSANY / ANYINSIDE. Queries like WHERE tags CONTAINS 'x' ORDER BY age DESC LIMIT N now plan as bounded index scans with LIMIT pushed into the scan instead of a full-range read plus heap sort. CONTAINSALL / ALLINSIDE still keep a residual filter above the union until a follow-up lands.

  • Single-scan ->edge->vertex graph traversals. Vertex-side adjacency keys now embed the target (table, id) after RELATE, so plain SELECT ->likes->person FROM person:… can be served by one range scan. Legacy keys remain readable, and re-RELATE lazily upgrades the format. Edge tables with non-FULL SELECT permissions stay on the two-scan path.

  • Lower default RocksDB readahead for better NVMe scan throughput. SURREAL_ROCKSDB_MAX_AUTO_READAHEAD_SIZE is now 256 KiB on every deployment, replacing RAM-tiered defaults that reached 4 MiB. Larger readahead was often split into multiple IOs with per-IO overhead on typical NVMe devices.

  • Per-row clone elimination on the projection hot path. The streaming executor now moves row payloads through Compute and projection instead of deep-cloning them per row.

  • Hash-keyed GROUP BY aggregation. The aggregate state map is now hash-keyed, cutting group-key allocations on large grouped queries while preserving key-sorted output ordering.

This release includes memory work on the value layer: a small-string-optimised Strand type, a new surrealdb-collections crate with VecMap/VecSet, and reduced per-transaction overhead on the hot path.

  • Strand is a 24-byte small-string-optimised immutable string type, routed through core hot-path strings: Value::String, Object keys, TableName, and RecordIdKey::String.

  • Strand packs into the same 24 bytes as String via a repr(C) SmolStr-style tagged union, with an inline variant storing strings up to 23 bytes with zero heap allocation.

  • A heap variant uses Arc<str> so clones are a refcount bump rather than a copy.

  • Object entries and Value::String shrink by 8 bytes each, which compounds at document scale.

  • The wire format is unchanged. Strand serialises identically to String through serde, Revisioned, and storekey.

  • A companion Strand::new_static constructor lets compile-time-known strings in core skip allocation entirely.

Plugin authors can mark functions exported through #[surrealism] as async fn and .await host imports directly (storage, network, queries).

  • Modules can now use async crates like reqwest and sqlx that internally depend on tokio::spawn for connection pooling and timers.

  • The host runtime is initialised once per module instance and persists across invocations on pooled controllers, so there's no per-call setup cost.

  • The accompanying surrealism::demo crate gains an async fn fetch_pokemon example that exercises WASI socket access under allow_net = ["127.0.0.1"].

The broader Surrealism overhaul (WASI Preview 2, WIT-typed host/guest contract, FlatBuffers boundary, attached read-only filesystem, two-engine epoch model) is included in 3.1. See the original beta.1 entry for the full surface change.

Every DEFINE statement now has a matching ALTER counterpart.

  • Added ALTER support for EVENT, PARAM, BUCKET, ANALYZER, FUNCTION, USER, ACCESS, CONFIG, and API.

  • Each follows the same property-by-property pattern as the existing ALTER TABLE / FIELD / INDEX statements.

  • Individual properties on a definition can be modified without re-specifying the whole definition via DEFINE … OVERWRITE.

  • ALTER resource names and table targets accept bound parameters, matching the DEFINE / REMOVE parameterisation story.

  • ALTER PARAM does not support DROP VALUE (a param without a value is meaningless). ALTER EVENT uses DROP ASYNC to revert to synchronous mode. ALTER ACCESS does not allow changing the access type itself.

Concurrently built indexes now keep their build state in the catalog rather than in process memory, and every node coordinates through it.

  • On multi-node deployments sharing a storage engine, all nodes now agree on which node owns the build, which writes the in-progress index admits, when replay is complete, and when the index becomes query-eligible.

  • INFO FOR INDEX reflects the durable state.

  • The same change fixes a class of issues where COUNT indexes could overcount when the initial build ran on multiple nodes concurrently.

  • No SurrealQL surface change.

SurrealDB Enterprise ships a new audit-logging and slow-query pipeline alongside the unified observability surface.

  • A queued, worker-backed audit_log module records authenticated operations to a configurable sink, with a redaction layer that strips sensitive fields before serialisation.

  • Slow-query detection runs alongside the audit pipeline and emits structured records for queries that exceed the configured threshold.

  • Both pipelines share a size-rotated, SHA-256 hash-chained, optionally fsync'd file sink in append-only 0600 mode. Slow-query is essentially the audit pipeline with a duration gate at the front of the observer.

  • Configuration via 13 SURREAL_AUDIT_* and matching SURREAL_SLOW_QUERY_* environment variables: sink kind, file path, rotation, fsync cadence, hash chaining, SQL inclusion, redaction (tables / regex / literals), queue capacity, overflow strategy, OTLP export.

  • See the observability documentation for the full operator surface.

Improvements

  • Upgraded RocksDB to 11.0.0 and tuned the engine. Enabled prefix extractor for keyspace-aware bloom filters, aligned compaction readahead to disk block size, and enabled blob-file separation by default for large-value workloads. Scaled defaults for constrained environments while large deployments (≥16 GiB memory) keep the previous defaults via tiered scaling.

  • Tuned RocksDB scans and grouped-commit. Read-only transactions now bypass the merge layer in scan and count paths, removing overhead on every step. Large scans are offloaded to the storage threadpool using improved byte-based sizing. Fixed shutdown and lost-wakeup edge cases in grouped-commit coordinators.

  • Full-keyspace compaction to bottommost on ALTER SYSTEM COMPACT. ALTER SYSTEM COMPACT and per-resource ALTER {DATABASE,NAMESPACE,TABLE} COMPACT now land outputs at L6 with bottommost-level Zstd compaction. Tightened auto-compaction trigger defaults. New SURREAL_ROCKSDB_PERIODIC_COMPACTION_SECONDS (default 3600, 0 disables) bounds version overhang and tombstone accumulation on cold ranges. Universal compaction is now a first-class option (still off by default) with configurable knobs.

  • Cleaner RocksDB shutdown sequence. Optional full-keyspace compaction (SURREAL_ROCKSDB_COMPACT_ON_SHUTDOWN, default false), bounded wait_for_compact (SURREAL_ROCKSDB_SHUTDOWN_WAIT_FOR_COMPACT_SECONDS, default 60 s), and deterministic background-thread teardown. Errors in the shutdown path are logged and continued rather than propagated.

  • Optional RocksDB scan checksum verification. New SURREAL_ROCKSDB_SCAN_VERIFY_CHECKSUMS flag, default true (existing behaviour). Setting it to false skips CRC32C verification on first-read blocks during scans and counts, trading a small amount of integrity checking for cold-scan throughput on trusted storage.

  • RocksDB user-defined timestamps for versioned reads. SELECT … VERSION, INFO FOR DB / TABLE VERSION …, graph and reference traversals, the FETCH clause, and inherited inner subqueries are isolated from the LRU transaction cache so historical reads cannot pollute current-time reads. INFO FOR ROOT and INFO FOR NS now also accept VERSION.

  • SurrealKV updated to 0.21.2, picking up fixes from (PR #7303). It also raises the maximum memtable size for large transactions.

  • Memory backend versioning parity. memory://?versioned=true (and mem://) is parsed correctly at startup. SELECT … VERSION on a non-versioned memory datastore returns the same error as RocksDB and SurrealKV. Default surreal start no longer enables versioning implicitly.

  • Reduced per-transaction overhead on the hot path. Built-in function registry is now shared across transactions instead of rebuilt on every query. Change-feed writer is allocated lazily. Per-transaction cache sizing is tuned for transaction scope rather than defaulting to a large multi-shard allocation.

  • Capped the scanner's initial batch size using the query LIMIT so that small-LIMIT queries don't fetch more rows than they will ever return.

  • Added a sync fast-path for PhysicalExpr evaluation in scan filters, avoiding async overhead when no IO is required.

  • Avoided cloning the document on SELECT * projection where the source row can be passed through unchanged.

  • Replaced the global HTTP client cache with a single HttpClient instance per Datastore, exposed via Context::http_client.

  • Configurable scan batch size. Promoted the scan BATCH_SIZE constant to SURREAL_SCAN_BATCH_SIZE (default 1000).

  • In-memory performance improvements via SurrealMX switching to ferntree (PR #74), a concurrent B+tree with optimistic lock coupling.

  • Optimised internal Value wire encoding (revision 0.23.0). Planner walks can skip or seek without fully deserialising large records. User-visible serialisation through RPC and export is unchanged.

  • OTLP exporters now build with TLS (tls + tls-roots features), using the platform's native root store via rustls-native-certs. rpc.request_id is emitted as a string identifier. The OTLP runtime is driven by an async runtime so exporters no longer block on a dedicated thread.

  • Made datastore initialisation retry resilient to hung attempts. Each retry attempt is wrapped in its own timeout that increases linearly. The global retry budget is raised from 60 s to 120 s. Failures are logged with a per-task label, and exhaustion returns a descriptive error.

  • Bounded shutdown background work to mirror the bounded-startup retry behaviour, so a stuck task can no longer indefinitely delay datastore shutdown.

  • MCP request instrumentation added to the community OpenTelemetry pipeline, with error classification distinguishing client errors from server errors.

  • Auto-detect TTY for the console log output and respect the NO_COLOR environment variable. ANSI escape codes are now only emitted when both stdout and stderr are TTYs.

  • Pluggable live-query broker for clustered deployments. Datastore accepts an optional MessageBroker so a composer can forward live-query notifications to the node that owns each subscription when writes commit on a different node than the subscriber's WebSocket.

  • Added value::expect method. Assert a predicate on a value via a closure and pass the original value through on success. This is useful in method chains alongside value::chain. On failure, throws with an optional message.

  • Added REMOVE CONFIG [IF EXISTS] GRAPHQL | API | DEFAULT to mirror DEFINE CONFIG.

  • Extended GraphQL schema generation to support fields whose type is a literal (record / object / array literal types). Added GraphQL Subscriptions and root-level field comments.

  • Added encoding::json::encode and encoding::json::decode for serialising SurrealDB values to and from JSON strings, and added support for UTF-16 surrogate-pair escape sequences in the JSON parser (gated on a json_string_escapes parser setting).

  • Use serde_json to parse external JSON in http::* functions so valid JSON responses containing escaped forward slashes (\/) are no longer rejected.

  • Added --tables-exclude flag to surreal export for excluding specific tables from an export.

  • Allow surreal validate to read input from stdin, so SurrealQL can be piped in for syntax checking without writing to a file first.

  • Configurable CORS allow list for the HTTP server.

  • Reject REMOVE ANALYZER while a full-text index uses it, with a clear error naming the offending index and table.

  • REMOVE MODEL now deletes the model file from the configured object store rather than leaving the .surml artifact orphaned.

  • Tightened numeric input handling on standard library functions. Functions such as rand::id, array::repeat, string::repeat, duration::from_*, and <bytes> […] now reject negative or out-of-range inputs with a clear error rather than silently wrapping.

  • Strict array-index conversion. Array indexing now treats negatives, NaN, infinities, fractional values, and out-of-range magnitudes as out-of-bounds and returns NONE. Integer-valued floats and decimals remain valid indices.

  • Expanded the surrealdb-types SDK and derive surface. Added #[surreal(rename_all = "...")] on enums and structs, #[surreal(wrap)] for transparent wrapper types, more SurrealValue implementations, serde interop on the Value type, restored query chaining on the Query builder, and several missing crate exports.

  • Added a builder pattern to Datastore for properties that cannot be changed after construction, replacing post-construction setters.

  • Added surrealdb-collections workspace crate with VecMap and VecSet. These are vec-backed, ordered map and set types for small-collection workloads. Object and Set in surrealdb-core now use these types.

  • Removed several process-wide configuration globals in surrealdb-core so configuration is now carried through Datastore / Context.

  • SDK tolerates servers denying the version RPC during connect (--deny-rpc version).

  • Improved error communication. Type errors in arithmetic / extend operators now report only the type name rather than the raw operand.

This release includes a consolidated GraphQL pass that standardises the auto-generated schema on Apollo conventions, adds Relay-style cursor pagination, and fixes a batch of schema-generation and filter bugs. See GraphQL overview and DEFINE CONFIG GRAPHQL.

There is no longer a NAMING DEFAULT|APOLLO switch. Apollo-style names are the only generated shape:

  • Fetch one record: person(id: ID!) (was _get_person(id:)).

  • List records: people(filter, where, order, limit, start, version) with a pluralised table name (was person(...) on the list query).

  • Aggregate: people_aggregate(filter, groupBy, …) (was person_aggregate).

  • Mutations: createPerson / updatePerson / upsertPerson / deletePerson, with bulk forms createPeople / updatePeople / etc. (was createManyPerson and similar).

  • Object and input type names keep the SurrealQL source name so record<table> references stay stable. Field names mirror SurrealQL idioms unless overridden.

  • Tables whose names are already plural (likes, follows) keep the legacy _get_<name> / <verb>Many<Cap> shape so singular and bulk mutation names stay distinct.

  • Two tables that collapse to the same query name abort schema generation with a clear error suggesting GRAPHQL_ALIAS.

New optional clauses on DEFINE FIELD, DEFINE TABLE, and DEFINE FUNCTION:

DEFINE FIELD first_name ON person TYPE string
GRAPHQL_ALIAS "firstName";

DEFINE TABLE customer_account
GRAPHQL_ALIAS "Customer";

DEFINE FUNCTION fn::get_user($id: string) -> user { ... }
GRAPHQL_ALIAS "getUser";

DEFINE FIELD old_score ON player TYPE int
GRAPHQL_DEPRECATED "Use `rank` instead";

Aliases must be valid GraphQL Name tokens. Invalid aliases fall back to the auto-derived name. INFO FOR DB and export round-trip the clauses. Deprecation reasons surface in field descriptions as [Deprecated: …] until the schema builder can emit the @deprecated directive on dynamic fields.

Each table gains a Relay-style connection field, for example peopleConnection(first, after, last, before, filter, where, order, version), returning { edges { cursor node { … } }, pageInfo, totalCount }. Cursors are URL-safe base64 of the record id. first / last default to 20 and cap at 1000. totalCount runs only when the client selects it. Offset pagination via limit / start on the list query remains available.

Posting a JSON array of { query, variables, … } envelopes to the GraphQL HTTP endpoint returns a parallel array of responses (batched requests).

  • Vector similarity and KNN filters on numeric array fields (for example array<float> embeddings). Per-field filter inputs accept similarity and nearest (maps to SurrealQL <|k, distance|>). Distance metrics include cosine, Euclidean, Manhattan, Hamming, Jaccard, Chebyshev, and Pearson. Includes the vector-similarity portion of (PR #7312).

  • Full-text search filter via matches on string fields, translating to SurrealQL @@ (requires DEFINE INDEX … SEARCH ANALYZER …).

  • Table aggregation via <plural>_aggregate(filter, groupBy), exposing count, per-numeric-field min/max/sum/avg, and optional groupBy.

  • Function-call filters via call on every field filter input (string::len, vector::*, user fn::…).

  • id filters: id: { in: […] }, gt / gte / lt / lte, and range: { from, to, inclusive }. (PR #4555).

  • Relation count in WHERE: relation fields expose count on the table filter. For example, people(filter: { sent: { count: { gt: 5 } } }) compiles to count(->sent) > 5. (PR #4554).

  • Schema cache now keys on a content hash of catalog entries, so DDL changes appear on the next GraphQL request. (PR #6942).

  • TYPE { bar: record<foo> } object literals generate nested GraphQL objects instead of failing at schema build. (PR #7034).

  • array<record<T>> filters and mutation inputs use valid GraphQL names. Nested-object fields no longer leak dots, and mutation inputs for record arrays map to [ID!]. (PR #4999).

  • record<T> mutation inputs are ID everywhere. Output object types no longer leak into input position.

  • COMPUTED and READONLY fields are omitted from Create* / Update* / Upsert* inputs. Pre-computed DEFINE TABLE … AS SELECT … views no longer expose create/update/delete mutations.

  • Regex string filters regression-tested (PR #5078).

  • Added support for aliases for tables, functions, and fields (PR #4537)

  • Updated the naming convention for GraphQL names (PR #4552).

Bug fixes

  • The issue causing much longer compile times for surrealdb-core since SurrealDB 3.0 has been identified. 3.1 includes fixes that have vastly reduced compile times when compiling surrealdb-core or adding it as a dependency. More fixes are planned for upcoming 3.1.x releases.

  • NONE.*, NULL.*, scalar .*, and geometry .* now return NONE in the new streaming planner, matching the legacy executor, instead of wrapping the value in a single-element array. This fixes the Go SDK cannot decode array into <StructType> error when an option<record<…>> field was NONE and the query expanded it with .*. Mixed arrays also now pass non-record elements through unchanged (PR #7143).

  • Fixed $parent resolving incorrectly inside graph WHERE clauses in nested subqueries. $parent now refers to the current SELECT's row rather than an outer subquery's parent.

  • Fixed FROM ONLY not being propagated through fused graph lookup chains, which previously produced incorrect results from constrained graph traversals.

  • Fixed ORDER BY (and WHERE) being silently ignored in subqueries with graph traversal sources like FROM $parent->edge->target.

  • Fixed bare field paths in a subquery's FROM clause evaluating to NONE because the source operator had no current value in correlated-subquery context.

  • Fixed $parent not being bound during iterator.prepare, which caused correlated subqueries using FROM $parent->… to see $parent unset while select targets were being planned.

  • Fixed $parent resolution in materialised view definitions on the streaming path so view materialisation now sees the same parent context as the legacy executor.

  • Fixed LET bindings in planned bodies not being visible to IF / FOR blocks that fall back to the legacy compute path.

  • Fixed $auth and $session returning NONE in queries executed inside a client-side transaction.

  • Fixed duplicate constant values being omitted in the streaming executor (e.g. false AS isLiked, false AS isLocked collapsing to a single column).

  • Fixed nested .* destructure returning NONE for record links. DestructureField::All now fetches the linked record before expanding.

  • Fixed SELECT alias shadowing the source field of the same name on the projection fast path.

  • Preserved alias idiom structure in streaming-executor projections so that AS foo.bar continues to nest into { foo: { bar: _ } } while AS foo.bar stays as a flat key.

  • Fixed ?? (null coalescing) and ?: (ternary condition) operator precedence binding tighter than arithmetic. $ctx.limit ?? 2 ** 31 - 1 now parses as $ctx.limit ?? (2 ** 31 - 1).

  • Fixed value::diff and value::patch returning Function 'value::…' requires async execution when invoked via method syntax.

  • Fixed view materialisation crashing when the source SELECT aliased the id field.

  • Fixed coercion error messages omitting the position of the offending element on large inputs.

  • Improved error message when a LET statement appears in a non-block expression position.

  • Fixed WHERE filters on indexed fields silently omitting COMPUTED fields from evaluation. Indexed scans now resolve computed fields through their pipelines, matching table scans.

  • Fixed records with NONE / NULL fields being silently excluded from ORDER BY results when the sort key was on a unique compound index (REBUILD INDEX is required to fix existing indexes).

  • Fixed SELECT VALUE with ORDER BY ignoring the sort order when querying from an array source.

  • Fixed ORDER BY DESC with a variable LIMIT inside nested IF / LET blocks inside a function producing wrong results.

  • Fixed ORDER BY on edge-table queries with record-link source fields returning unsorted output.

  • Fixed KNN <|K, DISTANCE|> queries bypassing the HNSW index when the distance matches the index configuration.

  • New-executor index analyzer: detects contradictory range bounds and short-circuits to a zero-row scan, merges same-side bounds, short-circuits WHERE field IN [], and warns when a WITH INDEX hint matches no candidate instead of silently falling back.

  • Indexed count() / IndexCountScan now refuse plans when the WHERE clause touches a field governed by non-Full SELECT permissions.

  • Composite CONTAINSANY / ANYINSIDE UnionIndexScan plans no longer strip predicates on fields with restricted SELECT permissions.

  • Made full-text, COUNT and HNSW compaction SSI-safe so concurrent index activity no longer drops work committed after the compaction snapshot. Compaction plans are bounded to fixed-size batches.

  • Fixed concurrent index conflicts and reduced overhead during full-text concurrent builds.

  • Fixed cascade-delete index bugs that caused COUNT-index drift, ghost UNIQUE-index entries on relation tables, and phantom UNIQUE-index entries when IN / OUT records were deleted before the relation.

  • Graph and versioned-read permission enforcement. Vertex-delete cascades now run edge deletes with the caller's permissions. The ->edge->vertex single-scan fast path is declined when a VERSION clause is present or the edge table's SELECT is not FULL. Versioned SELECT now applies historical field permissions and params.

  • Fixed transient transaction conflicts at commit time dropping in-flight background index work.

  • Catalog ID sequences now seed safely on the first boot after upgrade, preventing ID collisions with existing catalog entries.

  • Fixed a memory leak where DEFINE INDEX would pin the entire Datastore until process exit (PR #7304).

  • Grouped view writes now emit changefeed deltas and trigger LIVE-query notifications. Previously, writes to a DEFINE TABLE … AS SELECT … GROUP BY … view skipped these hooks.

  • Terminate live queries on session invalidation and TTL expiry. The dispatcher also skips notifications for expired sessions.

  • Remove live-query registrations when the authenticated principal changes on a session (see GHSA-4m82-p8cx-f94j under Security).

  • REMOVE TABLE now bumps the datastore live-query cache version, so subscriptions cannot survive a drop and DEFINE TABLE with the same name.

  • value::diff followed by value::patch now round-trips correctly on top-level scalars (PR #7239).

  • Empty-group sentinels for aggregate functions are now consistent with the corresponding scalar functions. time::min / time::max return NONE for empty groups, and math::variance / math::stddev return NaN for empty groups (PR #7301).

  • Materialised aggregate views now compute math::pow against the sum accumulator and time::min via the correct aggregate.

  • Fixed infinity output and added a non-abbreviated constant path so that the constant is preserved on round-trip.

  • Fixed user-duration structure so that durations defined and stored on user definitions round-trip correctly.

  • Restored the table-membership check on type::record(rid, table) when arg1 is a RecordId and arg2 is a String.

  • Fixed stack overflow on recursive SurrealValue types so deeply nested or self-referential type definitions no longer crash on encode / decode.

  • Fixed SurrealValue derive macro emitting r#type instead of type for raw-identifier struct fields.

  • Fixed a copy-paste bug in prepare_array that made the lookup-optimisation branch dead code for edge traversals inside array FROM clauses.

  • Fixed JSON-patch remove operation on lists.

  • Fixed CREATE failing for fields typed as set<…>.

  • Fixed negative FETCH array indices wrapping to a huge usize instead of returning NONE.

  • Local engine db.run now validates the function name as an identifier, preventing non-identifier names from being forwarded verbatim.

  • Memory ?versioned= enforcement. VERSION / point-in-time reads on memory:// without ?versioned=true now return the same error as RocksDB and SurrealKV.

  • Fixed long server non-responsiveness when a client cancels surreal export mid-stream.

  • Filesystem bucket keys are now preserved exactly as supplied. The lowercase_paths URL option for filesystem buckets defaulted to true, silently folding keys to lowercase on write. The default is now false (PR #7309).

  • file::list({ limit: -1 }) now rejects negative limits instead of silently treating -1 as unbounded.

  • Tightened the filesystem bucket allowlist. is_path_allowed now requires UTF-8 validity and a component-boundary prefix match. Object keys containing .. segments are rejected.

  • Respond with a failure when an in-flight WebSocket request is cancelled rather than silently dropping it.

  • Propagate CBOR encode errors instead of panicking on the RPC response path.

  • /key POST / PUT / PATCH handlers now parse the request body as a single SurrealQL value and bind it to $data instead of running it through Datastore::execute ahead of the fixed CRUD statement.

  • /key bodies must be an inert value expression (literals, $param, constants such as math::PI only). Executable forms are rejected, closing arbitrary SurrealQL execution on deployments that expose only /key.

  • Surface unified Cannot COMMIT: … errors when an explicit COMMIT after a previous statement abort fails, instead of returning only the first statement error.

  • Fixed id resolving to NONE inside a table's FOR select WHERE … permission clause when a record was created and selected in the same statement.

  • Fixed user-defined function PERMISSIONS clauses being evaluated for system-level users (root, namespace, database).

  • Live-query computed-field permissions are now applied after the computed fields are populated.

  • ALTER API now recomputes the caller's IAM envelope from current privileges.

  • GraphQL fn_* resolvers now read the session from the per-request GraphQL context instead of closing over the schema-generation-time session.

  • USE NS / USE DB no longer auto-creates namespaces or databases without the same authorization as DEFINE NAMESPACE / DEFINE DATABASE.

  • Fixed events defined on a vertex table incorrectly firing for related edge-table records during cascade deletion.

  • Fixed JSON log format being unused even when configured.

  • Fixed ANSI escape codes leaking into JSON log output and added per-stream TTY detection for mixed-redirect scenarios.

  • Prevent panic on systems with restricted /proc access where System::total_memory() returns 0.

  • --client-ip=forwarded. Parses the RFC 7239 Forwarded header and uses the for= identifier from the first forwarded-element as the request client IP.

  • FIPS build option (Enterprise). surrealdb-server gains a fips Cargo feature that routes TLS through the AWS-LC FIPS module.

  • Fixed re-importing an exported database failing with "field already exists" errors. Export now emits DEFINE FIELD OVERWRITE.

  • Reserved-word and backtick-quoted field names no longer leak into the generated schema, fixing introspection failures in strict clients.

  • Nested array types deeper than three levels now collapse to the JSON scalar in the generated schema instead of exceeding the standard introspection limit.

  • Pre-execution HTTP errors now return a spec-compliant JSON body with Content-Type: application/json, instead of a plain-text 400.

Security

This release addresses a substantial batch of issues surfaced by SurrealDB's internal security review process and external reviewers. Most are reachable only by an authenticated caller and require existing privileges. A couple are pre-auth DoS or capability-bypass conditions. The fix for each of these issues ships in v3.1.0 and is enabled by default. No configuration is required.

Breaking changes

Catalog and KV on-disk layouts are unchanged from 3.0.x, so you can upgrade in place. The items below are behaviour or default changes that can affect clients, dashboards, or SurrealQL that relied on previous (often undocumented) semantics.

POST, PUT, and PATCH on /key/:table and /key/:table/:id no longer treat the request body as SurrealQL to execute. The body is parsed as a single SurrealQL value and bound to $data only. The value must be inert: literals, $param references, and constants only. Function calls, idioms, statements, and parenthesised executable forms are rejected.

If you previously sent bodies such as time::now(), fn::…(), or multi-statement SurrealQL through /key expecting server-side evaluation, move that logic to POST /sql or RPC instead, or compute values in the client and send JSON literals.

The community metrics pipeline was reorganised under the surrealdb.* scope with a new /metrics allow-list. Existing Prometheus dashboards and alerts that scrape metric names from 3.0.x need updating. See the observability documentation for the migration table.

SURREAL_ROCKSDB_MAX_AUTO_READAHEAD_SIZE now defaults to 256 KiB on every deployment instead of RAM-tiered values up to 4 MiB. Override the variable if you tuned scan behaviour around the old defaults.

For filesystem-backed buckets, the lowercase_paths URL option now defaults to false. Keys are stored with the casing you supply. Set ?lowercase_paths=true explicitly if you depend on the previous default of folding paths to lowercase on write.

The auto-generated GraphQL schema now follows a single Apollo-style naming convention (there is no NAMING config). Existing clients must update query and mutation names:

Before (3.0.x)After (3.1.0)
_get_person(id: …)person(id: …)
person(filter: …) (list)people(filter: …) (pluralised)
person_aggregate(…)people_aggregate(…)
createManyPerson(…)createPeople(…)

Regenerate introspection, client stubs, and saved queries after upgrade. Use GRAPHQL_ALIAS on DEFINE TABLE, DEFINE FIELD, or DEFINE FUNCTION when the default pluralisation collides or when camelCase names are required. See GraphQL under Improvements for the full surface.

GeoJSON output and input geometry types now expose coordinates as the JSON scalar instead of deeply nested Float list types. The wire shape at query time is still nested arrays of numbers. Only the introspected GraphQL type changes. Clients that generated queries or validators from the old nested-float schema must be regenerated.

When a hostname permitted by --allow-net resolves to a private or special-use IP, the resolved IP must also be covered by an --allow-net entry. Public IPs are unaffected and the check is skipped under --allow-net all. To allow internal traffic by hostname, for example --allow-net internal.example.com resolving to 10.0.5.42, add the range: --allow-net internal.example.com,10.0.5.0/24.

Ranges checked: IPv4 loopback (127.0.0.0/8), RFC 1918 (10/8, 172.16/12, 192.168/16), link-local (169.254.0.0/16), shared (100.64.0.0/10), broadcast, and unspecified. For IPv6: loopback (::1), unspecified (::), unique-local (fc00::/7), and link-local (fe80::/10). IPv4-mapped IPv6 is canonicalised first.

Newer patch available

Upgrade to 3.1.4

You are viewing the 3.1.0 changelog. A newer patch in this release line is available - we recommend running 3.1.4 for the latest fixes and improvements.

View 3.1.4 release notes

3.1.0-beta.3

PRE-RELEASE

Released on May 14, 2026

This is the third beta in the 3.1 series. It adds DiskANN as a second approximate-nearest-neighbour index type alongside HNSW (with an end-to-end overhaul of the ANN warm-lookup path that benefits both implementations), brings the new executor's WHERE-clause prefilter to every KV scan with EXPLAIN visibility, and lets Surrealism plugins write async fn directly. The full cumulative scope from v3.0.5 is documented in the v3.1.0-beta.1 entry; the items below describe only what changed between beta.2 and beta.3.

Highlights

A new vector index type sits alongside HNSW under the same KNN query operator, with an overhauled warm-lookup path that benefits both implementations.

  • New index syntax:

      DEFINE INDEX pts_embedding_diskann ON pts FIELDS embedding
    DISKANN DIMENSION 4 DIST EUCLIDEAN TYPE F32 DEGREE 16 L_BUILD 64;
    SELECT id, label FROM pts
    WHERE embedding <|2,64|> [0f, 0f, 0f, 0f];

    <|K, EF|> queries DiskANN through the same operator used for HNSW.

  • Both DiskANN and HNSW gain TYPE F16, TYPE U8, and TYPE I8 element types, plus DISTANCE INNER_PRODUCT and DISTANCE COSINE_NORMALIZED metrics. Add HASHED_VECTOR to the index definition if vector-to-document mappings should be stored through vector hashes instead of full vectors.

  • The RocksDB-backed ANN lookup path was reworked end-to-end. The dominant cost on warm-lookup profiles - scanning empty pending-update ranges - is gone; in its place sit process-local caches for graph payloads, vector-to-document doc sets, and doc-id-to-record-id mappings. KV stays the source of truth: cache misses fall back to existing reads, write paths evict, and uncommitted writes never poison the positive doc-id cache.

  • Lookup result materialisation is batched, and graph candidates resolve by ElementId first so warm lookups skip the previous re-read of candidate vectors.

  • DEFINE INDEX on an ANN index now drains pending vectors before returning, so a freshly-defined index is immediately usable.

  • The pending-state skip is distributed-safe: new !dp (DiskANN) and !ht (HNSW) guard keys are written conditionally by compaction, so on multi-node deployments lookups only skip pending scans when KV state confirms the range is empty.

The new executor's KV scan operator evaluates supported WHERE predicates against the raw RocksDB key/value pair before the record is decoded. Rows whose predicate is provably false from raw bytes are rejected before revision decode, schema enforcement, projection, and downstream operators run.

  • Supported predicate shapes: root-level equality (a = …), IN / NOT IN against literal arrays, byte-level CONTAINS / CONTAINSANY / CONTAINSALL, simple unary NOT, conjunctions/disjunctions of the above, and nested shared-prefix accesses such as outer.x AND outer.y.

  • Previously gated behind the SURREAL_PREDICATE_PREFILTER environment variable; now always on, with the legacy parallel path removed.

  • EXPLAIN reports whether the prefilter applied and which predicates were pushed down, so operators can confirm the optimisation is firing.

  • Applies on top of every scan operator that touches RocksDB - base table, indexed, unique compound, and full-text - without configuration.

Plugin authors can mark functions exported through #[surrealism] as async fn and .await host imports directly (storage, network, queries).

  • Modules can now use async crates like reqwest and sqlx that internally depend on tokio::spawn for connection pooling and timers.

  • The host runtime is initialised once per module instance and persists across invocations on pooled controllers, so there's no per-call setup cost.

  • The accompanying surrealism::demo crate gains an async fn fetch_pokemon example that exercises WASI socket access under allow_net = ["127.0.0.1"].

  • This is a stopgap until WASM Preview 3 / component-model async lands upstream, at which point the embedded executor becomes unnecessary.

Improvements

  • Durable, cluster-wide index build coordination. Concurrently built indexes now keep their build state in the catalog rather than in process memory, and every node coordinates through it. On multi-node deployments sharing a storage engine, all nodes now agree on which node owns the build, which writes the in-progress index admits, when replay is complete, and when the index becomes query-eligible. INFO FOR INDEX reflects the durable state. The same change closes a class of issues where COUNT indexes could overcount when the initial build ran on multiple nodes concurrently. No SurrealQL surface change.

  • Lower default RocksDB readahead for better NVMe scan throughput. The default SURREAL_ROCKSDB_MAX_AUTO_READAHEAD_SIZE is now 256 KiB on every deployment, replacing the previous RAM-tiered defaults that reached 4 MiB. Most NVMe block layers cap a single device IO around /sys/block/<device>/queue/max_sectors_kb (typically 128–512 KiB), so larger readahead values were being split into multiple IOs with per-IO overhead and over-prefetching. On a representative NVMe seek-random benchmark the per-machine optimum was 128 KiB at 578 ops/sec / 640 MB/s; 4 MiB readahead was ~60% slower. Operators with non-standard storage can still override via the environment variable.

  • Optional RocksDB scan checksum verification. New SURREAL_ROCKSDB_SCAN_VERIFY_CHECKSUMS flag, default true (existing behaviour). Setting it to false skips CRC32C verification on first-read blocks during scans and counts; cached blocks were never re-checksummed either way. Trades a small amount of integrity checking for cold-scan throughput on trusted storage.

  • SurrealKV updated to 0.21.2 picking up the upstream fixes from surrealdb/surrealdb#7303.

Bug fixes

  • [View] Grouped view writes now emit changefeed deltas and trigger LIVE-query notifications. Previously, writes to a DEFINE TABLE … AS SELECT … GROUP BY … view skipped the changefeed-emit and live-query-notify hooks because the aggregate metadata is written directly through tx.set_record - so SHOW CHANGES and live-query subscribers missed updates to grouped views. The view-write lifecycle now matches the regular document pipeline.

  • [Query Engine] NONE.*, NULL.*, scalar .*, and geometry .* now return NONE in the new streaming planner - matching the legacy executor - instead of wrapping the value in a single-element array. Fixes the Go SDK cannot decode array into <StructType> error that appeared when an option<record<…>> field was NONE and the query expanded it with .*. Mixed arrays also now pass non-record elements through unchanged, so [1, record:r, "x"].* returns [1, {fetched}, "x"] rather than [[1], {fetched}, ["x"]] (fixes surrealdb/surrealdb#7143).

  • [Functions] value::diff followed by value::patch now round-trips correctly on top-level scalars. value::diff of two scalars produced a JSON Patch with the empty pointer "" as its path; the patch parser was treating that as a single-empty-segment path rather than the document root, so value::patch silently no-op'd and returned the original value (fixes surrealdb/surrealdb#7239).

  • [Functions] Empty-group sentinels for aggregate functions are now consistent with the corresponding scalar functions:

    • time::min and time::max return NONE for empty groups (previously returned the chrono MAX_UTC / MIN_UTC placeholders, which surfaced to clients as d'+262142-12-31T23:59:59.999999999Z').

    • math::variance and math::stddev return NaN for empty groups (previously returned 0.0, which conflated "no data" with "all values identical"). A single-element group still returns 0.0.

  • [Functions] Materialised aggregate views now compute math::pow against the sum accumulator (previously used count, returning the wrong magnitude) and time::min via the correct DatetimeMin aggregate (previously used DatetimeMax, returning the maximum).

  • [Indexes] Catalog ID sequences now seed safely on the first boot after upgrade. The new per-node batch-based ID generator initialised at 0 when its state key was missing, so the first namespace, database, table, or index created after an upgrade could reuse an existing ID and collide with the live catalog entry. The sequence loader now scans the existing catalog and starts from max(existing_id) + 1 whenever the state key isn't present.

Newer patch available

Upgrade to 3.1.4

You are viewing the 3.1.0-beta.3 changelog. A newer patch in this release line is available - we recommend running 3.1.4 for the latest fixes and improvements.

View 3.1.4 release notes

3.1.0-beta.2

PRE-RELEASE

Released on May 8, 2026

This is the second beta in the 3.1 series and continues the stabilisation focus, with a new audit-logging and slow-query pipeline in the Enterprise product. The full cumulative scope from v3.0.5 is documented in the v3.1.0-beta.1 entry; the items below describe only what changed between beta.1 and beta.2.

Items tagged (Enterprise) on a section heading or [Enterprise] on a bullet refer to the SurrealDB Enterprise product. Everything else applies to community.

Highlights

A new audit-logging pipeline with configurable sinks, redaction, and slow-query detection has been added to Enterprise.

  • A queued, worker-backed audit_log module records authenticated operations to a configurable sink, with a redaction layer that strips sensitive fields before serialisation

  • Slow-query detection runs alongside the audit pipeline and emits structured records for queries that exceed the configured threshold

  • Configuration is exposed through new SURREAL_AUDIT_* and SURREAL_SLOW_QUERY_* environment variables (see crates/enterprise/src/cnf/audit.rs and crates/enterprise/src/cnf/slow_query.rs)

  • The audit queue, observer, record, redact, sink, and worker modules sit under crates/enterprise/src/observe/audit_log/

Improvements

  • Extended the community OpenTelemetry pipeline with MCP request instrumentation, error classification, and additional protocol-level metrics (#67)

    • A new observe/error_class.rs module categorises errors emitted by the executor, KV transactions, and the RPC and WebSocket layers, so dashboards can distinguish client errors from server errors without parsing message strings

    • The MCP server now emits per-tool request metrics alongside the GraphQL and HTTP/WebSocket surfaces via a new observe/mcp_adapter.rs

    • GraphQL, HTTP-tower, and WebSocket instrumentation surfaces are widened to record more of the request lifecycle

  • Bounded shutdown background work to mirror the bounded-startup retry behaviour introduced in beta.1 (#41)

    • Shutdown of long-running background tasks now uses tokio::time::timeout_at against the global deadline, so a stuck task can no longer indefinitely delay datastore shutdown

    • Added is_query_cancelled and is_query_timedout helpers in err/mod.rs so the shutdown path can distinguish expected cancellation from real errors

  • Enabled TLS for the OTLP exporters and aligned rpc.request_id with the OpenTelemetry semantic conventions (#53)

    • The OTLP push exporter now builds with the tls and tls-roots features and uses the platform's native root store via rustls-native-certs

    • rpc.request_id is emitted as a string identifier rather than the previous binary form, matching the OTel rpc.* conventions used by external collectors

    • The OTLP runtime is now driven by an async runtime so exporters no longer block on a dedicated thread

  • Removed LazyLock from the hot path to eliminate a small but measurable per-request synchronisation cost (#48)

  • Avoided emitting empty reference range deletes during the reference purge so the storage layer no longer pays for a no-op range delete on tables with no references (#49)

  • Upgraded the Rust toolchain to 1.95 across community (#57) and [Enterprise] (#81).

Bug fixes

  • [Query Engine] Fixed coercion error messages omitting the position of the offending element, which made expected array<int> / expected tuple errors unactionable on large inputs. Coerce and cast paths in val/value/convert/{coerce,cast}.rs now thread the element index through the error (#52, resolves #5677).

  • [Indexes] Fixed transient transaction conflicts at commit time dropping in-flight background index work. The background index runner now distinguishes recoverable conflicts from fatal errors and retries against fresh state, with the retry harness shared across kvs/index.rs and kvs/ds.rs (#69).

  • [Indexes] Fixed VecMap::append extending the map in a way that could leave duplicate keys behind. The surrealdb-collections crate's VecMap now de-duplicates and re-sorts on append (#59).

Newer patch available

Upgrade to 3.1.4

You are viewing the 3.1.0-beta.2 changelog. A newer patch in this release line is available - we recommend running 3.1.4 for the latest fixes and improvements.

View 3.1.4 release notes

3.1.0-beta.1

PRE-RELEASE

Released on May 1, 2026

This is the first beta in the 3.1 series. The focus is bug fixing and stabilisation of the 3.0 line, with a number of substantial additions on top of the fixes that already shipped in v3.0.1v3.0.5.

Highlights

This release fixes over 40 community-reported bugs.

Exposed as a surreal mcp stdio subcommand for local IDE integrations, and as an HTTP /mcp endpoint guarded by the existing authentication middleware.

  • The tool surface includes typed query, select, create, insert, upsert, update, delete, relate, info, list, use, and run tools

  • read_only_hint / destructive_hint annotations so MCP clients can prompt the user before mutating operations

  • Request bodies are bound as typed Variables rather than string-interpolated

  • Identifiers are validated against a strict allow-list

  • Per-request subject verification rejects cross-session credential reuse

  • New environment variables: SURREAL_HTTP_MAX_MCP_BODY_SIZE (default 4 MiB), SURREAL_MCP_QUERY_TIMEOUT_SECS (default 60 s), SURREAL_MCP_MAX_RESULT_BYTES (default 256 KiB), SURREAL_MCP_RUN_MAX_ARGS (default 64), and SURREAL_MCP_PARAMS_MAX_KEYS (default 256)

Overhauled the observability and monitoring layer onto a single OpenTelemetry pipeline.

  • The previous community Prometheus path and the parallel OtelObserver SemConv pipeline have been collapsed onto one SdkMeterProvider and one SdkLoggerProvider

  • Every signal is recorded once and routed to many exporters: Prometheus text on /metrics, OTLP metrics push, OTLP logs push

  • Metric families are reorganised under the surrealdb.* scope (statement, query, transaction, RPC, auth, session, network, HTTP, live query, process, storage)

  • The /metrics endpoint enforces a render-time PUBLIC_METRICS allow-list: anonymous scrapers see only aggregate process gauges, while root-authenticated operators see the full surface

  • Tenant context (namespace / database / user) is carried on every labelled family but filtered out of the public view by family name

  • New environment variable: SURREAL_PROCESS_METRICS_REFRESH_INTERVAL (default 5 s)

  • Retired: SURREAL_TELEMETRY_NAMESPACE, SURREAL_TELEMETRY_RPC_LIVE_ID

  • Repurposed: SURREAL_METRICS_ENABLED

  • This is a breaking change for existing dashboards; see doc/OBSERVABILITY.md for the migration table

A round of memory work on the value layer including a 24-byte small-string-optimised Strand type, a new surrealdb-collections crate with VecMap/VecSet, and a per-transaction overhead reduction on the hot path.

  • Introduced Strand, a 24-byte small-string-optimised immutable string type, routed through every core hot-path string: Value::String, Object keys, TableName, and RecordIdKey::String

  • Strand packs into the same 24 bytes as String via a repr(C) SmolStr-style tagged union, with an inline variant storing strings up to 23 bytes with zero heap allocation

  • A heap variant uses Arc<str> so clones are a refcount bump rather than a copy

  • Object entries and Value::String shrink by 8 bytes each, which compounds at document scale

  • The wire format is unchanged: Strand serialises identically to String through serde, Revisioned, and storekey

  • A companion Strand::new_static constructor lets compile-time-known strings in core skip allocation entirely

Substantial performance improvements through engine tuning including RocksDB 11.0.0 upgrade, prefix extractor, blob-file separation by default, and scaled defaults for constrained environments.

  • RocksDB upgraded to 11.0.0 with enabled prefix extractor for keyspace-aware bloom filters

  • Aligned the compaction readahead size to max_sector_kb on disk (currently 256 KiB on Cloud)

  • Enabled blob-file separation by default for large-value workloads

  • Scaled defaults for constrained environments (the SurrealDB Cloud free tier sees disproportionate memory and disk pressure on the previous mid-sized defaults)

  • Large deployments (≥16 GiB memory) keep the previous defaults via tiered scaling; small deployments get appropriately sized buffers, caches, and compaction settings out of the box

Every DEFINE statement now has a matching ALTER counterpart.

  • Added ALTER support for EVENT, PARAM, BUCKET, ANALYZER, FUNCTION, USER, ACCESS, CONFIG, and API

  • Each follows the same property-by-property pattern as the existing ALTER TABLE / FIELD / INDEX statements

  • Individual properties on a definition can be modified without re-specifying the whole definition via DEFINE … OVERWRITE

  • ALTER PARAM does not support DROP VALUE (a param without a value is meaningless)

  • ALTER EVENT uses DROP ASYNC to revert to synchronous mode

  • ALTER ACCESS does not allow changing the access type itself (record / JWT / bearer), only durations, the AUTHENTICATE clause, and comment

Improvements

  • Added REMOVE CONFIG [IF EXISTS] GRAPHQL | API | DEFAULT to mirror DEFINE CONFIG, providing a way to delete GRAPHQL, API, and DEFAULT configurations.

  • Extended GraphQL schema generation to support fields whose type is a literal (record / object / array literal types), so they now appear in the generated GraphQL schema rather than being silently omitted.

  • Added the surrealdb-collections workspace crate with VecMap and VecSet

    • Vec-backed, ordered, insertion-order-preserving map and set types

    • Tuned for the small-collection workloads that dominate SurrealDB's value layer

    • Object and Set in surrealdb-core now use these types

    • Linear scan for small slices, skipped re-sort when inputs are already ordered, and a tuned linear-vs-binary search threshold

    • User-facing semantics are unchanged

  • Upgraded RocksDB to 11.0.0 and tuned the engine

    • Enabled prefix extractor for keyspace-aware bloom filters

    • Aligned the compaction readahead size to max_sector_kb on disk (currently 256 KiB on Cloud)

    • Enabled blob-file separation by default for large-value workloads

    • Scaled defaults for constrained environments (the SurrealDB Cloud free tier sees disproportionate memory and disk pressure on the previous mid-sized defaults)

    • Large deployments (≥16 GiB memory) keep the previous defaults via tiered scaling; small deployments get appropriately sized buffers, caches, and compaction settings out of the box

  • Reduced per-transaction overhead on the hot path

    • FunctionRegistry::with_builtins is now built once per datastore and shared across transactions instead of being rebuilt on every query (was ~22% of CPU on a CREATE-heavy profile)

    • The change-feed Writer is allocated lazily so transactions that touch no changefeed-enabled tables no longer pay for a DashMap<ChangeKey, TableMutations> and its drop

    • The per-transaction quick_cache now uses a single shard sized for transaction-scope rather than the available_parallelism() * 4 default, eliminating the 256 CacheShard allocations that used to happen on every Transaction::new on a 64-core host

  • Updated SurrealKV to 0.21.1 and raised the maximum memtable size for large transactions, allowing larger batches to be inserted without flushing mid-transaction.

  • Tuned RocksDB scans and grouped-commit

    • Read-only transactions now bypass the BaseDeltaIterator merge layer in count, keys, keysr, scan, and scanr, which removes overhead on every step

    • The offload heuristic for moving large scans to the storage threadpool now uses byte-based sizing (including skip work) and handles BytesOrCount correctly

    • The grouped-commit coordinators (RocksDB and SurrealKV) had shutdown and lost-wakeup edge cases that could stall threads or callers; both are fixed

  • Capped the scanner's initial batch size using the query LIMIT so that small-LIMIT queries don't fetch more rows than they will ever return.

  • Added a sync fast-path for PhysicalExpr evaluation in scan filters, avoiding the async overhead for the common case where no IO is required.

  • Avoided cloning the document on SELECT * projection where the source row can be passed through unchanged, reducing allocator pressure on read-heavy workloads.

  • Replaced the global HTTP client cache (which keyed on a hash of capabilities) with a single HttpClient instance per Datastore, exposed via Context::http_client

    • Removes process-wide global state

    • Simplifies test setup

    • Removes per-request hashing

    • Context::background is renamed to Context::new_test and is now test-only

    • Context::new is renamed to Context::new_child to make its sub-context role explicit

  • Made datastore initialisation retry resilient to hung attempts

    • Each retry attempt is now wrapped in its own per-attempt tokio::time::timeout that increases linearly (10 s, 20 s, 30 s, …)

    • A single hung attempt against a slow or unresponsive storage backend can no longer silently consume the entire retry budget

    • Per-attempt timeouts and back-off sleeps are capped to the remaining global budget

    • The global retry budget is raised from 60 s to 120 s

    • Failures are now logged with a per-task label so the failed operation is identifiable

    • Exhaustion returns a descriptive Internal error instead of re-raising the last TransactionConflict

  • Added RocksDB user-defined timestamps and threaded the VERSION clause through every internal schema lookup

    • Versioned reads (SELECT … VERSION d'…', INFO FOR DB / TABLE VERSION …) are now isolated from the LRU transaction cache so historical reads cannot pollute current-time reads

    • The VERSION clause is now propagated through graph and reference traversals, the FETCH clause, automatic record dereferences during field access, and inherited by inner subqueries when the outer SELECT carries a version

    • INFO FOR ROOT and INFO FOR NS now also accept VERSION

    • RocksDB gains a custom comparator that stores 8-byte LE timestamps as key suffixes (newer versions ordered before older), with HLC commit timestamps assigned at commit time and ReadOptions::set_timestamp for point-in-time reads

  • Overhauled the Surrealism (WASM plugin) stack to v0.2.0 and migrated to WASI Preview 2 (Component Model)

    • Modules now target wasm32-wasip2 and a WIT interface (surrealism/wit/surrealism.wit) defines the host/guest contract

    • New #[surrealism] attribute options: writeable (lets the planner choose appropriate transaction modes), comment (description visible in surreal module info and INFO FOR DB STRUCTURE), init (one-time init hook with access to host imports), and namespaced module exports that produce prefixed names like math::add

    • Data crossing the host/guest boundary now serialises via FlatBuffers

    • Modules can declare an attached read-only filesystem ([attach] fs = "fs" in surrealism.toml) bundled into the .surli archive and mounted at / inside the WASM sandbox

    • The host runtime gets a two-engine epoch model - a guarded engine with epoch interruption for timeout enforcement, and a fast engine for trusted compute-heavy modules - selected via strict_timeout in surrealism.toml

  • Added a builder pattern to Datastore for properties that cannot be changed after construction, replacing the previous post-construction setters

    • Makes invalid configurations harder to express

    • Lets the language-test suite exercise more configuration surfaces

    • The language-test harness has been overhauled in the same change

  • Expanded the surrealdb-types SDK and derive surface

    • Added #[surreal(rename_all = "...")] on enums and structs and honoured per-variant #[surreal(rename = "...")] (previously parsed only for fields)

    • Added #[surreal(wrap)] for transparent wrapper types

    • Added more SurrealValue implementations for standard-library types and a NaiveDate implementation

    • Generalised the Cow and &str SurrealValue implementations

    • Added serde interop on the Value type

    • Restored query chaining on the Query builder

    • Added several missing crate exports

  • Auto-detect TTY for the console log output and respect the NO_COLOR environment variable

    • ANSI escape codes are now only emitted when both stdout and stderr are TTYs

    • Fixes garbled output in Docker containers, Kubernetes pods, and log shippers like Kibana and GKE Log Explorer

  • Allow surreal validate to read input from stdin, so SurrealQL can be piped in for syntax checking without writing to a file first.

  • Removed several process-wide configuration globals in surrealdb-core so configuration is now carried through Datastore / Context rather than read from static state, simplifying embedding and testing.

Bug fixes

  • [Query Engine] Fixed $parent resolving incorrectly inside graph WHERE clauses in nested subqueries; $parent now refers to the current SELECT's row rather than an outer subquery's parent.

  • [Query Engine] Fixed FROM ONLY not being propagated through fused graph lookup chains, which previously produced incorrect results from constrained graph traversals.

  • [Query Engine] Fixed ORDER BY (and WHERE) being silently ignored in subqueries with graph traversal sources like FROM $parent->edge->target. The streaming source operator now resolves RecordId values to full documents so downstream Filter, Sort, Group, and Split operators have the fields they need.

  • [Query Engine] Fixed bare field paths in a subquery's FROM clause (e.g. FROM data.files where data is a record link on the outer document) evaluating to NONE because the source operator had no current value in correlated-subquery context.

  • [Query Engine] Fixed $parent not being bound during iterator.prepare, which caused correlated subqueries using FROM $parent->… (including nested CREATE, UPDATE, and DELETE) to see $parent unset while select targets were being planned.

  • [Query Engine] Fixed $parent resolution in materialised view definitions on the streaming path so view materialisation now sees the same parent context as the legacy executor.

  • [Query Engine] Fixed LET bindings in planned bodies not being visible to IF / FOR blocks that fall back to the legacy compute path; the legacy FrozenContext is now refreshed after a planned LET body runs.

  • [Query Engine] Fixed $auth and $session returning NONE in queries executed inside a client-side transaction; the session is now correctly attached to the per-statement context.

  • [Query Engine] Fixed duplicate constant values being omitted in the streaming executor (e.g. false AS isLiked, false AS isLocked collapsing to a single column). The expression registry's de-duplication key now includes the alias.

  • [Query Engine] Fixed nested .* destructure returning NONE for record links; DestructureField::All now fetches the linked record before expanding.

  • [Query Engine] Fixed SELECT alias shadowing the source field of the same name on the projection fast path.

  • [Query Engine] Preserved alias idiom structure in streaming-executor projections so that AS foo.bar continues to nest into { foo: { bar: _ } } while AS foo.bar stays as a flat key - both the executor projection and GROUP BY alias paths previously stringified the alias and lost the backtick distinction.

  • [Query Engine] Fixed ?? (null coalescing) and ?: (ternary condition) operator precedence binding tighter than arithmetic. $ctx.limit ?? 2 ** 31 - 1 now parses as $ctx.limit ?? (2 ** 31 - 1), matching JavaScript / C# / Kotlin / Swift / PHP semantics; previously the operators bound above **, *, /, +, and -, which made ?? nearly useless without explicit parentheses.

  • [Query Engine] Fixed value::diff and value::patch returning Function 'value::…' requires async execution when invoked via method syntax ({a: 1}.diff({a: 2})). The async-vs-sync check now matches the behaviour of the full-path call.

  • [Query Engine] Fixed view materialisation crashing when the source SELECT aliased the id field; initialisation now falls back to scanning for any RecordId when the "id" key is absent.

  • [Query Engine] Fixed references being created against a target table even when the originating CREATE/INSERT/RELATE/UPSERT/UPDATE failed its CREATE permission check; references are now cleaned up on the permission-denied error path.

  • [Query Planner] Fixed WHERE filters on indexed fields silently omitting COMPUTED fields from evaluation. IndexScan, FullTextScan, and KnnScan now resolve field state and process computed fields through their pipelines, matching TableScan and UnionIndexScan.

  • [Query Planner] Fixed records with NONE / NULL fields being silently excluded from ORDER BY results when the sort key was on a unique compound index; unique indexes now store NONE / NULL tuples in the non-unique key format so they remain visible to scans (REBUILD INDEX is required to fix existing indexes).

  • [Query Planner] Fixed SELECT VALUE with ORDER BY ignoring the sort order when querying from an array source; the VALUE projection is now applied after sorting and pagination instead of before. ORDER BY can also reference a SELECT VALUE alias.

  • [Query Planner] Fixed ORDER BY DESC with a variable LIMIT inside nested IF / LET blocks inside a function producing wrong results.

  • [Query Planner] Fixed ORDER BY on edge-table queries with record-link source fields like in.creationDate returning unsorted output. The sort path now routes resolved alias expressions through the Compute operator (which can fetch linked records) and restricts the synchronous FieldPath shortcut to single-part paths where record-link traversal is impossible.

  • [Query Planner] Fixed KNN <|K, DISTANCE|> queries bypassing the HNSW index; eval_hnsw_knn now matches the K(k, d) variant when the distance matches the index configuration.

  • [Indexes] Made full-text, COUNT and HNSW compaction SSI-safe so concurrent index activity no longer drops work that was committed after the compaction snapshot. Compaction is now split-phase and generation-guarded, with conditional writes that advance an internal generation key and delete only the exact delta keys captured in the read phase. Full-text and COUNT compaction plans are bounded to COUNT_BATCH_SIZE (50,000) delta keys per batch, so a long-stalled delta backlog is processed in fixed-size batches instead of being held entirely in memory. HNSW pending updates now use record-keyed entries so sequential writes from multiple SurrealDB nodes against the same remote backend no longer depend on local append-key ordering.

  • [Indexes] Fixed concurrent index conflicts and reduced overhead during full-text concurrent builds. The initial-build write transaction no longer reads !ip / !ig queue handoff records (those are now read through a separate read-only transaction, so the initial-build write transaction stays out of active queue contention). maybe_consume no longer writes !ip keys after the initial build phase. The append-phase commit retries on transient transaction conflicts instead of treating them as fatal. Full-text concurrent builds now reuse FullTextIndex across documents in a batch instead of recreating it per document.

  • [Indexes] Fixed cascade-delete index bugs that caused COUNT-index drift, ghost UNIQUE-index entries on the relation table after deleting a vertex with edges, and phantom UNIQUE-index entries when IN / OUT records were deleted before the relation. Also fixed process_lookup hardcoding None for the version parameter, which caused SELECT … WITH VERSION queries that traverse graph edges to silently return current-state records instead of the requested historical snapshot.

  • [Live Queries] Terminate live queries on session invalidation and TTL expiry. invalidate() now calls cleanup_lqs(), mirroring reset(), so all live-query registrations for the session are removed from the notification engine. The dispatcher also compares the session expiry timestamp against the current time before sending each notification and silently skips expired ones, closing a class of bug where revoked sessions continued receiving CREATE / UPDATE / DELETE events indefinitely.

  • [Live Queries] Prevent ignorable evaluation errors in a LIVE SELECT's WHERE clause or SELECT projection (type mismatches, invalid function arguments) from propagating back through try_join_all and aborting the enclosing write transaction. Such errors now silently skip the notification.

  • [Storage] Fixed long server non-responsiveness when a client cancels surreal export mid-stream. The HTTP export handler now exits the proxy task when the body stream channel fails (indicating client disconnect), which causes the next channel send to fail and aborts the export task - releasing the read transaction promptly. On large databases this previously blocked endpoints like /health while the cancelled export ran to completion.

  • [Indexes/SurrealKV] Allow large transactions to insert without flushing mid-transaction by raising the maximum memtable size for SurrealKV.

  • [RPC/WebSocket] Respond with a failure when an in-flight WebSocket request is cancelled rather than silently dropping it, so clients always observe an explicit response.

  • [Transactions] Surface unified Cannot COMMIT: … errors when an explicit COMMIT after a previous statement abort fails (e.g. unique constraint), instead of returning only the first statement error. txn.commit() failures and the post-abort COMMIT fast-forward now share the same error wording so clients no longer treat a missing row as success.

  • [Auth] Fixed id resolving to NONE inside a table's FOR select WHERE … permission clause when a record was created and selected in the same statement (SELECT * FROM ONLY (CREATE ONLY a)), which previously caused the record to be filtered out by its own SELECT permission. RecordId::select_document now re-injects the id into the returned object when it's missing on a transaction-cache hit.

  • [Auth] Fixed user-defined function PERMISSIONS clauses being evaluated for system-level users (root, namespace, database). PERMISSIONS are intended only for record-level users; system-level users with Action::View now correctly bypass the check and can invoke functions defined with PERMISSIONS NONE. Also removed a hardcoded fn:: prefix from the FunctionPermissions error message that produced double-prefixed names like fn::fn::my_function.

  • [Functions] Added encoding::json::encode and encoding::json::decode for serialising SurrealDB values to / from JSON strings, and added support for UTF-16 surrogate-pair escape sequences (\uD800\uDFFF) in the JSON parser. The surrogate-pair support is gated on a json_string_escapes parser setting so SurrealQL string parsing is unaffected.

  • [Functions] Use serde_json to parse external JSON in http::* functions so valid JSON responses containing escaped forward slashes (\/) - produced by default json_encode() in PHP and other systems - are no longer rejected by SurrealQL's own JSON parser.

  • [Functions] Fixed infinity output and added a non-abbreviated constant path so that the constant is preserved on round-trip.

  • [Functions] Fixed user-duration structure so that durations defined and stored on user definitions round-trip correctly.

  • [Types/SDK] Fixed stack overflow on recursive SurrealValue types so deeply nested or self-referential type definitions no longer crash on encode / decode.

  • [Types/SDK] Fixed SurrealValue derive macro emitting r#type instead of type for raw-identifier struct fields. Fields like r#type: String now serialise as "type" without needing an explicit #[surreal(rename = "type")] attribute.

  • [Types/SDK] Fixed a copy-paste bug in prepare_array (in iterator.rs) that checked x[0] for Part::Lookup after already matching it as Part::Start, making the lookup-optimisation branch dead code. All edge traversals inside array FROM clauses (SELECT * FROM [a:1->edge->table]) previously fell through to the slower prepare_computed path; the optimised prepare_lookup path is now reached.

  • [Types/SDK] Fixed JSON-patch remove operation on lists.

  • [Types/SDK] Fixed CREATE failing for fields typed as set<…>.

  • [Imports/Exports] Fixed re-importing an exported database failing with "field already exists" errors. Export now emits DEFINE FIELD OVERWRITE so a clean re-import succeeds without manual intervention.

  • [Logging] Fixed JSON log format being unused even when configured.

  • [Logging] Fixed ANSI escape codes leaking into JSON log output (which previously produced corrupt records like "level": "\x1b[32mINFO\x1b[0m") and added per-stream TTY detection so mixed-redirect scenarios like surreal start > app.log no longer strip ANSI from the stream that is still an interactive terminal.

  • [CLI] Prevent panic on systems with restricted /proc access (e.g. NixOS with ProcSubset=pid hardening) where sysinfo cannot read /proc/meminfo and System::total_memory() returns 0. The previous cgroup_limits() assertion put the systemd service into an infinite restart loop on both RocksDB and SurrealKV backends.

  • [Events] Fixed events defined on a vertex table incorrectly firing for related edge-table records during cascade deletion. When deleting a vertex with graph edges (via RELATE), the edge deletes now run with the edge table's document context so the vertex table's events, views, live queries, changefeeds, and field validation are no longer applied to edge-record mutations.

Newer patch available

Upgrade to 3.1.4

You are viewing the 3.1.0-beta.1 changelog. A newer patch in this release line is available - we recommend running 3.1.4 for the latest fixes and improvements.

View 3.1.4 release notes

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks

SurrealDB

The context layer for AI agents.

Documents, graphs, vectors, time-series, and memory.
One transaction, one query, one deployment.

Explore with AI

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom