Skip to content
NEW

SurrealDB 3.1 is here! Stability, DiskANN, and a new release process

Learn more

1/3

LATEST STABLE

Release v3.1.0

Released on May 26, 2026

SurrealDB 3.1.0 lands as the first minor on top of the 3.0 line, and it is deliberately a stability release. The themes are a thorough security pass against…

SurrealDB 3.1.0 lands as the first minor on top of the 3.0 line, and it is deliberately a stability release. The themes are a thorough security pass against the engine, a long list of correctness fixes, measurable performance work on the read and scan paths, and a unified observability surface that is finally one pipeline instead of three.

On top of that, the release adds enough new surface to be worth the version bump on its own - a first-party MCP server for AI agents, a DiskANN approximate-nearest-neighbour index alongside HNSW, async functions in Surrealism plugins, full ALTER coverage for every DEFINE statement, distributed trace-context propagation on every endpoint, a GraphQL overhaul (Apollo naming, cursor pagination, GRAPHQL_ALIAS, and expanded filters), value::expect for assertions in SurrealQL chains, and - for SurrealDB Enterprise - a durable audit logging and slow-query pipeline with hash-chained, optionally fsync'd file sinks.

The 3.0 → 3.1 catalog and KV on-disk layouts are unchanged, so an existing 3.0.x deployment can roll forward in place. Internally, Value / Object / Array / Set adopt an optimised revision 0.23.0 encoding on the planner hot path (indexed prologues for O(1) field access without a migration step). A handful of operational defaults have been tightened (the most notable being RocksDB readahead, which is now 256 KiB on every deployment); see the Improvements section for the full list and how to override.

This entry covers everything that landed across v3.1.0-beta.1, v3.1.0-beta.2, v3.1.0-beta.3, and the final stretch into stable. Items marked 🆕 first shipped between the final beta and 3.1.0 stable.

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

🚀 Highlights

A typed, structured tool surface for AI agents, exposed as a surreal mcp stdio subcommand for local IDE integrations and as an HTTP /mcp endpoint guarded by 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 the user 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).

The observability and monitoring layer is now one 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 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 honoured on every transport, so an OTEL-aware client tracing db.query("…") sees the trace tree extend continuously through the server.

  • HTTP: traceparent extracted from request headers and parented onto the existing request span. 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 serves many independent SDK operations under their own traces rather than 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 wanting deep nested core instrumentation 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 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.

  • 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 process-local 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. If you used DiskANN on a 3.1 beta and saw intermittent DiskANN KNN search failed, that class of failure is addressed (#7318).

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

  • Predicate prefilter on every KV scan. 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, IN / NOT IN, byte-level CONTAINS / CONTAINSANY / CONTAINSALL, simple unary NOT, conjunctions/disjunctions, and shared-prefix nested accesses. 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 cancel after ~LIMIT rows globally instead of materialising every matching row through a heap. On the crud-bench combined-workload scan against 5M rows this turns ~2M intermediate rows into ~LIMIT rows and removes the OOM that drove this work.

  • 🆕 Composite array-element indexes for CONTAINS / INSIDE and CONTAINSANY / ANYINSIDE. On indexes such as DEFINE INDEX tag_age ON t FIELDS tags.*, age, WHERE tags CONTAINS 'x' ORDER BY age DESC LIMIT N (and the CONTAINSANY / ANYINSIDE multi-value forms) now plan as bounded IndexScan / UnionIndexScan walks with LIMIT pushed into the scan instead of a full-range index read plus heap sort. CONTAINSALL / ALLINSIDE still keep a residual filter above the union (intersection semantics) 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 instead of scanning edges then chasing each edge's adjacency. Legacy keys remain readable; re-RELATE lazily upgrades 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 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 was 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.

  • 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, cutting allocator pressure on read-heavy workloads.

  • Hash-keyed GROUP BY aggregation. The aggregate state map is now hash-keyed and probes buckets in place - for 1M rows × 100 groups that is roughly ~100 group-key allocations instead of ~1M, while still preserving the previous key-sorted output ordering.

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.

  • Strand is 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.

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 closes a class of issues where COUNT indexes could overcount when the initial build ran on multiple nodes concurrently.

  • No SurrealQL surface change.

A new audit-logging and slow-query pipeline ships in SurrealDB Enterprise, 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 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.

  • 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.

  • 🆕 Full-keyspace compaction to bottommost on ALTER SYSTEM COMPACT. ALTER SYSTEM COMPACT and the per-resource ALTER {DATABASE,NAMESPACE,TABLE} COMPACT verbs now land outputs at L6 with bottommost-level Zstd compaction forced, instead of leaving compactions at L1/L2 with bottommost SSTs skipped. Tightened auto-compaction trigger defaults (L0 file-trigger 4 → 2; new level0_slowdown_writes_trigger default 8 and level0_stop_writes_trigger default 12; deletion-factory window 1000 → 500 and count 50 → 25). 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 universal_size_ratio / min_merge_width / max_merge_width / max_size_amplification_percent / compression_size_percent / stop_style 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 via cancel_all_background_work. Errors in the shutdown path are logged-and-continued rather than propagated - a partial shutdown beats panicking on the way out.

  • 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.

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

  • SurrealKV updated to 0.21.2, picking up the upstream fixes from surrealdb/surrealdb#7303; raised the maximum memtable size for large transactions, allowing larger batches to be inserted without flushing mid-transaction.

  • 🆕 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, and the spurious Could not parse configuration value for key DATASTORE_PERSIST warning on plain in-memory start is gone.

  • 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.

  • 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 and simplifies test setup.

  • 🆕 Configurable scan batch size. Promoted the scan BATCH_SIZE constant to SURREAL_SCAN_BATCH_SIZE (default 1000). Memory-constrained deployments can lower it; throughput-focused workloads stay on the default.

  • In-memory performance improvements via SurrealMX (SurrealDB's in-memory engine) switching to ferntree, a concurrent B+tree with optimistic lock coupling.

  • 🆕 Optimised internal Value wire encoding (revision 0.23.0). Value, Object, Array, and Set use indexed collection prologues and tagged variants so planner walks (pre-decode filter, projection, field lookup) can skip or seek without fully deserialising large records. User-visible serialisation through RPC and export is unchanged; this is a hot-path storage-encoding improvement, not a database migration.

  • 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 rather than the previous binary form, matching the OTel rpc.* conventions. 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 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.

  • Bounded shutdown background work to mirror the bounded-startup retry behaviour. 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.

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

  • 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, fixing garbled output in Docker containers, Kubernetes pods, and log shippers like Kibana and GKE Log Explorer.

  • 🆕 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. The default LocalMessageBroker preserves single-node behaviour when unset.

  • 🆕 Added value::expect method. Assert a predicate on a value via a closure and pass the original value through on success - 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, 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 GraphQL Subscriptions and root-level field comments.

  • 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.

  • 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.

  • 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 - previously the analyzer would be dropped unconditionally, leaving the index pointing at nothing.

  • 🆕 REMOVE MODEL now deletes the model file from the configured object store rather than leaving the .surml artifact orphaned. The delete is idempotent and invalidates the in-process model cache.

  • 🆕 Tightened numeric input handling on standard library functions. rand::id, rand::string, array::repeat, string::repeat, string::semver::set_{major,minor,patch}, duration::from_{nanos,micros,millis,secs,mins,hours,days,weeks}, and the <bytes> […] cast / type::bytes now reject negative or out-of-range inputs with a clear ArithmeticNegativeOverflow error rather than silently wrapping i64 to usize/u64. The previous behaviour produced confusing outputs like string::semver::set::major("1.1.3", -1) → "18446744073709551615.1.3" and could trigger multi-exabyte allocations in rand::*.

  • 🆕 Strict array-index conversion. Array indexing ([1,2,3][i] and the equivalent idiom paths) now treats negatives, NaN, infinities, fractional values, and out-of-range magnitudes as out-of-bounds and returns NONE, rather than wrapping or saturating. Integer-valued floats and decimals (1.0, 1dec) remain valid indices.

  • 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.

  • 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. The language-test harness has been overhauled in the same change.

  • Added 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. User-facing semantics are unchanged.

  • 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.

  • 🆕 SDK tolerates servers denying the version RPC during connect (--deny-rpc version). Operators who explicitly opt out of exposing the version no longer break new SDK connections.

  • 🆕 Improved error communication. Type errors in arithmetic / extend operators now report only the type name rather than the raw operand, and propagated anyhow source chains are no longer copied onto wire errors.

A consolidated GraphQL pass standardises the auto-generated schema on Apollo conventions, adds Relay-style cursor pagination, and closes 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) - 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 (first_name stays first_name) 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 (maps to vector::similarity::* / vector::distance::* with a comparison threshold) and nearest (maps to SurrealQL <|k, distance|>). Distance metrics include cosine, Euclidean, Manhattan, Hamming, Jaccard, Chebyshev, and Pearson. Closes the vector-similarity portion of #7312.

  • Full-text search filter via matches on string fields, translating to SurrealQL @@ (requires DEFINE INDEX … SEARCH ANALYZER …). Example: articles(filter: { title: { matches: { query: "fox" } } }) { id title }.

  • Table aggregation via <plural>_aggregate(filter, groupBy), exposing count, per-numeric-field {field}_min / {field}_max / {field}_sum / {field}_avg, and optional groupBy. Example: products_aggregate(groupBy: [category]) { category count price_avg }.

  • Function-call filters via call on every field filter input (string::len, vector::*, user fn::…). List queries still accept order alongside these predicates.

  • id filters: id: { in: […] } (expanded to OR for SurrealQL IN semantics), gt / gte / lt / lte, and range: { from, to, inclusive }. Closes #4555.

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

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

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

  • array<record<T>> filters and mutation inputs use valid GraphQL names (e.g. _filter_list_child); nested-object fields no longer leak dots; mutation inputs for record arrays map to [ID!]. Closes #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 (#5078).

  • Closes #4537 (aliases), #4552 (naming convention).

🐛 Bug fixes

  • The issue causing much longer compile times for surrealdb-core since SurrealDB 3.0 has been identified. 3.1 includes a number of fixes that have vastly reduced compile times when compiling surrealdb-core or adding it as a dependency. More fixes to further reduce this time are already planned out and will continue throughout 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. 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).

  • 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. The streaming source operator now resolves RecordId values to full documents so downstream Filter, Sort, Group, and Split operators have the fields they need.

  • 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.

  • 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.

  • 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; the legacy FrozenContext is now refreshed after a planned LET body runs.

  • 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.

  • 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.

  • 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 - both the executor projection and GROUP BY alias paths previously stringified the alias and lost the backtick distinction.

  • 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.

  • 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.

  • 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.

  • 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 now thread the element index through the error.

  • 🆕 Improved error message when a LET statement appears in a non-block expression position (e.g. inside an array literal or function argument). The previous unreachable error read like an internal panic; the new message clearly states the syntactic constraint.

  • 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.

  • 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).

  • 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.

  • 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 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.

  • 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.

  • 🆕 New-executor index analyzer: detects contradictory range bounds (a > 10 AND a < 5) and short-circuits to a zero-row scan; merges same-side bounds (a > 5 AND a > 10); short-circuits WHERE field IN [] instead of running a full scan with a filter; recognises single-element field IN [v] as compound-prefix-joinable; cleanly pushes single-column WHERE field != NULL / != NONE to an exclusive-lower-bound range scan; caps the compound-prefix bonus so a 12-column prefix no longer outscores a unique equality; warns when a WITH INDEX hint matches no candidate instead of silently falling back.

  • 🆕 Indexed count() / IndexCountScan and legacy COUNT-index fast paths now refuse plans when the WHERE clause touches a field governed by non-Full SELECT permissions, so record users cannot infer cardinality of values they cannot read (fixes the indexed-path leak; WITH NOINDEX already behaved correctly).

  • 🆕 Composite CONTAINSANY / ANYINSIDE UnionIndexScan plans no longer strip predicates on fields with restricted SELECT permissions; the residual filter re-evaluates after permission reduction so hidden array values cannot be inferred from index membership.

  • 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.

  • Fixed concurrent index conflicts and reduced overhead during full-text concurrent builds. The initial-build write transaction no longer reads 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). 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.

  • 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.

  • 🆕 Graph and versioned-read permission enforcement. Vertex-delete cascades now run edge deletes with the caller's permissions (edge-table DELETE is no longer bypassed). The ->edge->vertex single-scan fast path is declined whenever a VERSION clause is present or the edge table's SELECT is not FULL, so row-level edge filters are not skipped on point-in-time reads. Versioned SELECT now applies historical field permissions and params (processor, planner, and scan operators that previously used the current catalog). Graph-edge traversal also no longer hardcodes the current version when SELECT … WITH VERSION traverses edges.

  • 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.

  • 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.

  • 🆕 Fixed a memory leak where DEFINE INDEX would pin the entire Datastore (and its underlying RocksDB / SurrealKV file descriptors) until process exit. The background IndexBuilder was forming an Arc cycle through the build context; dropping the user's Surreal / Datastore handle now reliably closes the storage handles (fixes surrealdb/surrealdb#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 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.

  • 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.

  • 🆕 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 (matching LIVE / KILL), so subscriptions cannot survive a drop and DEFINE TABLE with the same name; previously stale Lvs entries could notify old live-query ids against the recreated table.

  • 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).

  • Empty-group sentinels for aggregate functions are now consistent with the corresponding scalar functions: time::min / time::max return NONE for empty groups (previously surfaced placeholders like d'+262142-12-31T23:59:59.999999999Z'); math::variance / 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. (Fixes surrealdb/surrealdb#7301.)

  • 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).

  • 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. A previous semantic change had repurposed arg2 from a table constraint to a record key, so type::record($id, "user") silently accepted any $id regardless of its table. The newer type::record(table, key) form is unaffected.

  • 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. Fields like r#type: String now serialise as "type" without needing an explicit #[surreal(rename = "type")] attribute.

  • Fixed a copy-paste bug in prepare_array that made the lookup-optimisation branch dead code, so edge traversals inside array FROM clauses (SELECT * FROM [a:1->edge->table]) now reach the optimised path.

  • 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 explicitly short-circuiting; arr[-1] in a FETCH path now correctly returns NONE.

  • Local engine db.run now validates the function name as an identifier inside the IntoFn trait. The embedded engine had been constructing SurrealQL via format!("{name}({args})") and passing the result to Datastore::execute, so non-identifier names were forwarded verbatim.

  • 🆕 Memory ?versioned= enforcement. VERSION / point-in-time reads on memory:// without ?versioned=true now return The underlying datastore does not support versioned queries, matching RocksDB and SurrealKV. Plain surreal start no longer treats the memory backend as versioned by default.

  • 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.

  • 🆕 Filesystem bucket keys are now preserved exactly as supplied. The lowercase_paths URL option for filesystem buckets defaulted to true, silently folding every file::put key to lowercase when writing to disk while reads kept the user-supplied case - producing a "split brain" between SurrealQL and external tooling. Default is now false; explicit opt-in (?lowercase_paths=true) is preserved for callers that genuinely want case-folding (fixes surrealdb/surrealdb#7309).

  • 🆕 file::list({ limit: -1 }) now rejects negative limits with Expected "non-negative int" instead of silently treating -1 as usize::MAX and returning an unbounded listing.

  • Tightened the filesystem bucket allowlist (experimental files feature). is_path_allowed now requires UTF-8 validity and a component-boundary prefix match - previously a bytewise prefix match admitted /srv/data-evil against an allowlist of /srv/data. Object keys containing .. segments are now rejected at the bucket entry points.

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

  • 🆕 Propagate CBOR encode errors instead of panicking on the RPC response path, hardening the connection task against future ciborium changes.

  • /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. Behaviour change for callers that relied on body evaluation (e.g. embedding time::now() in the body).

  • 🆕 /key bodies must be an inert value expression (literals, $param, constants such as math::PI only). A single executable form - bare CREATE …, fn::…(), or a parenthesised statement - is rejected even when it is only one statement, 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 (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.

  • 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.

  • 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.

  • 🆕 Live-query computed-field permissions are now applied after the computed fields are populated. A computed field marked PERMISSIONS FOR select NONE (or a conditional permission that returns false) was being shipped to subscribers on the LIVE delivery path because permission reduction ran before computed-field population; both the live doc and the LIVE … DIFF initial state are now filtered correctly.

  • ALTER API now recomputes the caller's IAM envelope from current privileges. Legacy API records written by v3.0.0-beta.1 previously carried a permissive stored auth_limit that the check trusted.

  • GraphQL fn_* resolvers now read the session from the per-request GraphQL context instead of closing over the schema-generation-time session. The cached resolvers captured session at schema build time, so subsequent fn_* calls ran under that captured session rather than the caller's. Brings them in line with the other resolvers in the crate.

  • 🆕 USE NS / USE DB no longer auto-creates namespaces or databases without the same authorization as DEFINE NAMESPACE / DEFINE DATABASE. Anonymous guests and under-privileged sessions can no longer materialise new namespaces through USE alone; a stale namespace-level token can no longer recreate a dropped parent namespace as a side effect of USE NS dropped_db ….

  • 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.

  • Fixed JSON log format being unused even when configured.

  • 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.

  • 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.

  • 🆕 --client-ip=forwarded. Parses the RFC 7239 Forwarded header and uses the for= identifier from the first forwarded-element as the request client IP (complements the existing X-Forwarded-For mode).

  • FIPS build option (Enterprise). surrealdb-server gains a fips Cargo feature that routes TLS through the AWS-LC FIPS module (rustls/fips) and installs the FIPS crypto provider at startup via surrealdb_server::tls::install_default_crypto_provider().

  • 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.

  • Reserved-word and backtick-quoted field names no longer leak into the generated schema. Fields such as value or type are exposed as valid GraphQL names (value, type, …), which fixes introspection failures in strict clients (Postman, GraphiQL) and restores runtime field resolution for table fetch, filter, and aggregate lookups.

  • Nested array types deeper than three levels now collapse to the JSON scalar in the generated schema instead of building a float list chain that exceeds the standard seven-hop introspection limit. Runtime values are unchanged; only the declared GraphQL type differs (for example array<array<array<array<float>>>> is typed as JSON! rather than [[[[Float!]!]!]!]!).

  • Pre-execution HTTP errors (missing surreal-ns / surreal-db, GraphQL not configured, schema generation failures) now return a spec-compliant JSON body ({"data": null, "errors": [{"message": "…"}]}) with Content-Type: application/json, instead of a plain-text 400 that clients reported as “Received an invalid GraphQL response”.

🔒 Security

This release closes 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 server runs fixed statements such as CREATE type::table($table) CONTENT $data or … MERGE $data). There is no $body parameter on /key - if you reference $body in custom logic expecting the HTTP payload, use $data instead. The value must be inert: literals, $param references, and constants only. Function calls, idioms, statements, and parenthesised executable forms (for example a bare CREATE … or fn::evil()) are rejected even when they are a single top-level statement.

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.

This aligns /key with its documented contract of accepting a single inert value rather than arbitrary SurrealQL.

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 (also summarised under Unified observability and monitoring pipeline below).

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 (see Predicate prefilter and scan-path performance below).

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 (cursor pagination, batched HTTP, new filters).

GeoJSON output and input geometry types now expose coordinates as the JSON scalar instead of deeply nested Float list types (for example [[[Float!]!]!]!). 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 (strict introspection tools, codegen) must be regenerated. This applies to GeometryPoint, GeometryLineString, GeometryPolygon, and the other variant types, plus their *Input counterparts.

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 - e.g. --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, unspecified; IPv6 loopback (::1), unspecified (::), unique-local (fc00::/7), link-local (fe80::/10). IPv4-mapped IPv6 is canonicalised first

Upgrade or install

Get SurrealDB v3.1.0

Pick your platform to copy a command that installs or upgrades SurrealDB to v3.1.0. Already have the CLI? Use surreal upgrade to swap in place.

Install via Homebrew, then upgrade in place to v3.1.0.

brew install surrealdb/tap/surreal
surreal upgrade --version 3.1.0

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 - in one transaction, one query, one deployment.

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