Upgrade ArcadeDB
ArcadeDB automatically upgrades a database to a newer on-disk format when a newer version of ArcadeDB opens it. The migration is transparent and happens lazily on first open, so the only operator step is to point the new binaries at your existing data.
Breaking changes
This section lists behavior changes that may require action when upgrading.
26.11.1 - Float and double arrays are stored fixed-width, which older builds cannot read
An ARRAY_OF_FLOATS or ARRAY_OF_DOUBLES value (a vector embedding, for example) is now written as its element count followed by the raw little-endian IEEE-754 elements, 4 bytes per float and 8 per double, where each element used to be a zigzag varint of 5 bytes per float and 9 to 10 per double (issue #10856). A record holding a 1536-dimension float embedding shrinks from about 7,695 to about 6,150 bytes, about 22% less disk and page cache. Records written before keep the old form and are still read; an update rewrites the value in the new form. In an HA cluster no node writes the new form until every peer advertises the fixed-width-float-arrays capability, so a rolling upgrade keeps writing the old form until the last node is upgraded.
Impact: once a database holds records in the new form, a build older than 26.11.1 cannot read them.
Action: none to upgrade, and no migration is needed. To go back to an older build afterwards, export the database with 26.11.1 and re-import it with the older version. See Data Types.
26.11.1 - A Graph Analytical View rebuilds once after the upgrade, and a failed build reads FAILED
The CSR a view persists at a clean close is written in a new format (version 5), streamed in frames so that writing and reading it need no more heap than the view itself (issue #11011). A file written by an earlier version is not read: the first open after the upgrade rebuilds the view from a full scan, as an open after an unclean stop does, and the next clean close writes the new format. A build that fails, an OutOfMemoryError included, now ends in the new FAILED status with a buildError column in schema:graphAnalyticalViews, where it used to read BUILDING for good after an out-of-memory error. The default of arcadedb.cypher.algoMaxWorkingMemory is now 0, sized from the heap at each call and never below the eighth of the heap it used to be.
Impact: one full rebuild of each persisted view on the first open. Code that compares GraphAnalyticalView.getStatus() against every value of Status meets a new one, FAILED.
Action: none. See Sizing the Heap for a View.
26.11.1 - The rows of the vector search functions no longer list the record twice
Each row of vector.neighbors(), vector.sparseNeighbors(), vector.fuse(), vector.rerank() and the other vector functions holds the record’s properties, @rid, @type and its score, and used to hold the whole record again under a record key. SELECT expand(…), SELECT * and the JSON of the rows serialized every property twice, embeddings and passages included (issue #10898). The record key is no longer listed: a path such as record.name, a projection of record, and a Java caller that reads the key from the row still get the record.
Impact: a client that read the nested record object from the JSON of an expanded or unprojected row no longer finds it.
Action: read the properties from the row itself, or project record explicitly (SELECT record, distance FROM (SELECT expand(vector.neighbors(…)))). See vector.neighbors().
26.11.1 - A vector index object held across REBUILD INDEX refuses to answer
A Java caller that kept the bucket-level LSMVectorIndex or LSMSparseVectorIndex from before a REBUILD INDEX or a drop was answered as if the index were empty (0 neighbors, 0 entries) by the dense index, and with an IllegalStateException ("engine is closed") by the sparse one (issue #10897). Both now throw an IndexException ("Index '…' is not valid. Probably has been drop or rebuilt") on their searches, get() and countEntries(), as the TypeIndex over them and an LSM_TREE bucket index already did. getStats() still answers: it describes that instance.
Impact: code that held such an object and read an empty answer now gets the exception.
Action: fetch the index again by name from the schema after a rebuild.
26.11.1 - Function options refuse a value of the wrong kind
A boolean option of a function’s options map (parallel, tieBreaker and emptyIfMaxDepth of astar() and dijkstra(), edge of shortestPath(), and every other boolean option read through the same option reader) read any value other than a boolean or 'true' as false, so a typo such as 'ture', 'yes' or 1 silently ran another query. It now accepts only a boolean or the strings 'true' and 'false' (in any case), and refuses any other value with an error that names the option (issue #10788). maxDepth must be a non-negative whole number: 2.5, 'abc' and -1 are refused the same way. For shortestPath(), maxDepth: null now means no bound, where it was read as 0.
Impact: a query that passed such a value fails where it used to answer.
Action: pass true or false, and a whole maxDepth. See astar(), dijkstra() and shortestPath().
26.11.1 - The default-named indexes of a renamed type answer to the new name
After ALTER TYPE Foo NAME Bar, the index Foo[a] was listed as Bar[a] but resolved only by Foo[a], so DROP INDEX and REBUILD INDEX with the listed name failed, and an index created with a name of its own lost it to the default name. A default-named index now answers to Bar[a] at once and no longer to Foo[a], and an index with a name of its own keeps it across the rename and a restart (issue #10751).
Impact: a script that reached a renamed type’s index by its old name fails, since that name no longer resolves.
Action: use the name the schema lists. See ALTER TYPE.
26.11.1 - SQL refuses positional values that do not match the placeholders
A call that binds positional values (query/command with Object… for sql and sqlscript, the HTTP params given as a JSON array or as an object keyed "0", "1", …, and the remote Java client) dropped a value no placeholder read and bound a placeholder with no value to null, without a word. A list spread into several values for one IN ? answered fewer rows than the caller meant. The values are now counted against the placeholders before the statement runs, and a mismatch is refused with a CommandSQLParsingException (HTTP 400) that names both counts (issue #10721). Each ? and each positionally bound :name takes exactly one value, a script counts the placeholders of all its statements, and a Postgres-style $N may leave lower positions unread. Values bound by name (a map) are not counted.
Impact: code that passed a surplus value, or relied on a missing value being bound to null, fails where it used to answer.
Action: pass one value per placeholder, null included, and a list meant for one IN ? as one value (a List or a Java array). See positional parameters over HTTP.
26.11.1 - A plain CHECK DATABASE FIX relinks orphan edge records
An orphan edge record, whose @out and @in are live vertices but that no vertex’s edge list references, is reported as ORPHAN RECORD under unreachableEdgeRecords. A plain CHECK DATABASE FIX left it untouched, and only FIX DELETE ORPHANS repaired it, by deleting it. It now relinks it into the outgoing list of its @out vertex and, for a bidirectional edge type, the incoming list of its @in vertex (issue #10741). The result counts them under relinkedOrphanEdgeRecords. The usual source is a GraphBatch whose process died before close(), and CHECK DATABASE FIX is now the supported recovery for it.
Impact: edges that no traversal reached before the repair are traversed after it, and counted by every query that walks those vertices.
Action: none if those edges are wanted. To remove them instead, run CHECK DATABASE FIX DELETE ORPHANS, which takes precedence. See CHECK DATABASE and GraphBatch.
26.11.1 - count(*) repeats inside a REPEATABLE_READ transaction
count(*) on a type and countType() followed the commits of other transactions at every isolation level, so inside one REPEATABLE_READ transaction two counts could differ, and a count could disagree with a scan. Under REPEATABLE_READ the committed counter of each bucket is now read at the first count of the transaction and kept, plus the transaction’s own inserts and deletes (issue #10712). READ_COMMITTED is unchanged.
Impact: a REPEATABLE_READ transaction that polled count(*) to watch other transactions' inserts no longer sees them change.
Action: poll with separate transactions, or at READ_COMMITTED. See Isolation.
26.11.1 - The HTTP server’s IO thread count is arcadedb.server.httpIoThreads
The setting that sets the number of IO threads of the HTTP server was keyed arcadedb.server.httpsIoThreads, which read as the HTTPS-only setting declared beside it, so the natural spelling -Darcadedb.server.httpIoThreads was ignored without a word (issue #10599). Its key is now arcadedb.server.httpIoThreads. The old key is still read from a system property, an environment variable, the server configuration file and the settings commands, and the first use of it is reported in the log.
Impact: none for a deployment that set the old key. A deployment that set arcadedb.server.httpIoThreads before, and saw no effect, now gets the value it asked for. When both are given, the new key wins.
Action: none. Switch to the new key when convenient, the old one may be removed in a later release. See server settings.
26.11.1 - A MongoDB deleteMany over a TIMESERIES type no longer accepts a number for the timestamp
find and count over a TIMESERIES type hand the timestamp to a MongoDB client as a date and, as MongoDB does, do not match a number against it. A deleteMany with a number on the timestamp ({ts: 1792396800000}, {ts: {$gte: <number>, $lt: <number>}}) was accepted and removed the samples at those epoch milliseconds, so the usual preview with find or count and then delete of the same filter removed samples the preview had shown none of (issue #10598). It is now refused with an error that names the type and asks for a date.
Impact: a client that deleted time-series samples by a number of milliseconds gets an error instead of a deletion. Nothing is removed by a refused delete.
Action: compare the timestamp with a date (a Date, a datetime, a BSON date): deleteMany({ts: {$gte: ISODate("2026-10-19T08:00:00Z")}}). On a type restored from an export written before MILLISECOND became the only precision, and still declaring SECOND, MICROSECOND or NANOSECOND, no condition on the timestamp is accepted, a date or a number, because the unit of its timestamps is not known: the error names ALTER TIMESERIES TYPE <name> PRECISION MILLISECOND, which confirms the unit and opens the date form (issue #10740). See Query Languages.
26.11.1 - A DATETIME string with an offset is stored as the UTC wall clock on every path, whatever the database time zone
The conversion the gRPC service and the other callers of DateUtils.dateTimeToTimestamp apply to a string moved the instant onto the database’s time zone before storing it as a UTC wall clock, so a database whose zone is not UTC stored 2020-01-02T03:04:05+02:00 hours away from the same instant written through SQL, and 2020-01-02T03:04:05Z shifted by the zone’s offset (issue #10498). Every path now stores the UTC wall clock of the instant, as SQL, HTTP and Bolt already did.
Impact: none for a database whose zone is UTC. In a database with another zone, a DATETIME written from a string with an offset or a Z through the gRPC service (or another bulk path) before the upgrade holds a value shifted by the zone’s offset; values written after the upgrade are right. Stored values are not changed.
Action: none for a UTC database. Otherwise rewrite the values loaded that way, by reloading them or by correcting them with an UPDATE on the records those loads wrote. See Managing Dates.
26.11.1 - A pattern named in asDateTime(), date() and date.parse() converts the offset of the text, and the time zone argument of date() is used
'2020-01-02T03:04:05+02:00'.asDateTime("yyyy-MM-dd’T’HH:mm:ssXXX") and date('2020-01-02T03:04:05+02:00', "yyyy-MM-dd’T’HH:mm:ssXXX") answered 2020-01-02T03:04:05, the offset the pattern had read dropped, where the same text read with the schema’s own pattern, a bound parameter or an OffsetDateTime gave 2020-01-02T01:04:05 (issue #10596). A named pattern is now read under the same rule: the instant the text names is the UTC wall clock. The third argument of date(), which was ignored, is the zone a text that names none was written in. date.parse() and date.fields() read the offset of the text, and so does the datetimeFormat of the importer.
Impact: a query or an import that names a pattern with an offset or zone field and a text with a non-zero offset gets the converted value, two hours earlier in the example; one that passes a time zone to date() for a text without an offset gets the instant of that zone in UTC. Stored values are not changed.
Action: none if the converted value is what the query meant. To keep the wall clock written, drop the offset from the text, or read it with a pattern that has no offset field. See Managing Dates.
26.11.1 - SQL equality on a list or a map compares the elements as it compares two scalars
[<Date>] = v, where v holds the LocalDateTime of the same instant, was false and <> was true; a Timestamp on the left of = was never equal to the same instant (issue #10595). Two values = reports equal are now equal as the elements of a list or a map too, for =, <>, IN and CONTAINS.
Impact: a filter on a list or map property compared with a parameter that holds the same instants in another Java class (Date, Instant, Calendar, Timestamp) now matches the row. A list of one kind compared with a list of another that converts matches as the scalars do: [1] = ['1'] is true, as 1 = '1' is.
Action: none. Compare with a typed parameter if a list of strings must not equal a list of numbers.
26.11.1 - Redis commands on in-memory keys are no longer refused with TRYAGAIN
GET, SET, GETDEL, EXISTS, INCR, INCRBY, INCRBYFLOAT, DECR and DECRBY no longer wait for the query admission control, and are no longer listed by list queries / SHOW TRANSACTIONS (issue #10499). Each is a single in-memory map operation.
Impact: when the server is busy these commands are answered instead of refused with TRYAGAIN. A client that used TRYAGAIN on them as a back-pressure signal no longer receives it. The commands that read or write records (HSET, HGET…) are still gated.
Action: none for most installations.
26.11.1 - A database that a restore holds or a close database closed is answered as unavailable on every protocol
A database that exists but cannot serve now (an overwrite restore holds its name, a close database closed it, it is being dropped) used to reach a Redis or MongoDB client as a generic server error, and a Redis write to <database>.<key> was kept as a plain key of the connection and acknowledged with OK (issue #10553). Every protocol now answers that the database is not available: LOADING for Redis, error 365 TemporarilyUnavailable for MongoDB, UNAVAILABLE for gRPC, 57P03 for Postgres, DatabaseUnavailable for BOLT. A name that is not a database keeps its answer.
Impact: a client that treated the old Redis or MongoDB error as a server fault now sees a retryable "not available" and can try again later. A Redis write to a held database is refused, where it used to be acknowledged and lost once the database came back.
Action: none for most installations. See Opening a database on demand.
26.11.1 - A name that is not a database is the client’s error, not a server fault
A client that names a database the server does not have (a typo in the connection’s database, or a database dropped while a connection still names it) used to be answered as a server fault by every protocol but HTTP and BOLT: SQLSTATE XX000 for a statement on Postgres (HV00Q when the connection opened), status INTERNAL for gRPC, its admin calls included, an uncoded "Unknown error" for MongoDB, and the server logged it with a stack trace (issue #10795). It is now answered as the client’s to correct: SQLSTATE 3D000 invalid_catalog_name for Postgres, the code PostgreSQL itself gives, status NOT_FOUND for gRPC (the Java gRPC client raises a DatabaseNotFoundException), error 26 NamespaceNotFound for MongoDB, Neo.ClientError.Database.DatabaseNotFound for BOLT also when the database is lost under a statement, and ERR Database '<name>' not found for Redis, the reply its SELECT already gave. HTTP keeps its 404.
Impact: a Postgres client that matched HV00Q to detect a missing database now receives 3D000. A gRPC client that read INTERNAL as a server fault now receives NOT_FOUND, and does not retry a request that can never succeed.
Action: update any client code that matched the old codes.
26.11.1 - -Darcadedb.profile=low-ram keeps its 16MB page cache
The profile given as a -D system property or as an environment variable (the way server.sh, the Docker image and Kubernetes configure it) set every one of its settings except arcadedb.maxPageRAM: the page cache stayed at its computed default, a quarter of the heap (issue #10446). Any other setting the profile lists that also has a computed default, such as arcadedb.server.httpIoThreads or arcadedb.timeSeriesDecodedBlockCacheRAM, lost its profile value too on a machine where the computed default differs. The first embedded database of a JVM also logged Configuration profile PARTIALLY applied for a profile that was set before any database was open.
Impact: a server started with the low-ram profile and no explicit arcadedb.maxPageRAM now has a page cache of 16MB instead of a quarter of the heap, as the profile always documented. A database larger than 16MB is read from disk more often.
Action: none if the profile was chosen to save memory. If the larger page cache was relied upon, set arcadedb.maxPageRAM explicitly, which wins over the profile. See RAM Configuration.
26.11.1 - An update of a record the transaction already updated, refused by a constraint or a listener, makes the commit fail
When a transaction updates a record and a later statement updates it again, a MAX, MIN, REGEXP or other constraint (or a before-update listener that throws) can refuse the second update after its values were set on the copy of the record the transaction writes at commit. That copy kept the refused values and the commit stored them (issue #10482). The statement still fails with the constraint’s error, and commit() now throws a TransactionException ("Transaction cannot be committed: a statement failed after part of its records were written …"), like any other statement that fails after writing. A before-update listener that returns false to skip the record, which was a silent veto, now makes the commit fail too when the record was already updated by the transaction, since the skipped values would otherwise be committed.
Impact: code that caught the error of such an update, or relied on a listener’s veto of an already updated record, and went on to commit, now gets a TransactionException at commit(). A refused update of a record the transaction had not updated yet is unchanged: nothing of it is left. An openCypher statement whose existence-constraint check takes back every record it created, with the edges it connected to them, no longer makes the commit fail.
Action: roll the transaction back and retry, or avoid refusing an update after the same record was updated in the transaction. See Transactions.
26.11.1 - DELETE FROM and TRUNCATE TYPE remove the samples of a TIMESERIES type
DELETE FROM <timeseries type> WHERE <time range> [AND <tag> = <value> …] and TRUNCATE TYPE <timeseries type> now remove samples, from the mutable pages and the sealed blocks of every shard, replicated under HA as a compaction is (issue #10342). Before, #10336 refused them, and before that they reported rows deleted or success and kept every sample. A WHERE that is not exactly a time range and tag equalities (a predicate on a FIELD column, an OR), a LIMIT, a BATCH and RETURN BEFORE are refused. UPDATE stays refused. The refusal of ALTER PROPERTY … MAX/MIN/REGEXP/MANDATORY over a stored sample that violates it now names the DELETE that removes the sample.
Impact: a delete is not part of the transaction it runs in, like the append of an INSERT: the samples are gone when it returns and a ROLLBACK does not bring them back. It needs the deleteRecord permission on the type. A read that straddles a delete fails with the retryable error a downsample gives (HTTP 503). A delete does not recompute a continuous aggregate over the type (issue #10428): the buckets it already holds keep the deleted samples until they are rebuilt.
Action: none for installations that did not delete. See Deleting Samples.
26.11.1 - database.async().appendSamples() refuses what the other ingestion surfaces refuse
The embedded async API converts every value as INSERT does and holds it to the MANDATORY, NOTNULL, MIN, MAX and REGEXP constraints of the type’s columns before it returns, on the calling thread (issue #10381). A value no column can store, or a sample a constraint refuses, raises a ValidationException naming the column, and the whole batch is refused. Before, such a sample was stored (and then blocked the same constraint from being declared again), and a value no column could store was lost inside the async worker while waitCompletion() returned normally.
Impact: application code that fed appendSamples() values a constraint refuses now fails at the call. Valid batches behave as before, including text for a numeric column, which is converted. The public constructor of DatabaseAsyncAppendSamples that took the boxed arrays is gone, since it built a grid of its own: code that created the task directly must go through database.async().appendSamples().
Action: handle the ValidationException, or fix the values. See Deleting Samples for removing a sample that was stored before.
The form that takes a TimeSeriesBatch is held to the same rules (issue #10487): a TAG or STRING text of more than 256 UTF-8 bytes is refused on the calling thread with the whole batch, where it used to be lost inside the worker. TimeSeriesBatch.setValue() converts as INSERT does and raises an IllegalArgumentException for a value its column cannot store, where it stored a Long past the INTEGER range modulo 2^32 and any text of a BOOLEAN column as false. The typed setters (setLong(), setString(), …) are unchanged.
26.11.1 - SQL split() keeps the empty pieces at the end of the list
SQL split(), the split() function and text.split() answer the same list as the openCypher split() (issue #10485): 'a,'.split(',') is ['a', ''] and ','.split(',') is ['', ''], where the empty pieces at the end were dropped (['a'] and []). An empty delimiter splits a string into its characters, a character outside the Basic Multilingual Plane (an emoji) being one, and replace() with an empty search puts the replacement between characters and never inside one.
Impact: a query that counted the pieces of a text ending with the delimiter, or compared the last one, sees one more piece.
Action: remove the empty last piece in the query if it is not wanted, for example by trimming the delimiter from the end of the text before splitting it.
26.11.1 - The JSON of a record writes a DATETIME_MICROS and a DATETIME_NANOS property as text
record.toJSON() wrote a DATETIME_SECOND, DATETIME_MICROS or DATETIME_NANOS property as a count of seconds, microseconds or nanoseconds, which fromJSON() and the server read as milliseconds (issue #10486). It now writes epoch milliseconds for a DATETIME_SECOND property, as for DATETIME, and the text with all its digits (for example "2020-01-02 03:04:05.123456789") for DATETIME_MICROS and DATETIME_NANOS, which milliseconds would truncate. The remote client sends a record as this JSON, so it now stores those properties right on the server.
Impact: a reader of toJSON() (the change events of a WebSocket subscription carry it) that took those numbers as seconds, microseconds or nanoseconds gets milliseconds, or text, instead.
Action: read a number as epoch milliseconds, and parse the text of a DATETIME_MICROS or DATETIME_NANOS property.
26.11.1 - A declared LIST OF / MAP OF property survives a JSONL export and import
EXPORT DATABASE and IMPORT DATABASE in the jsonl format now round-trip a declared LIST OF or MAP OF property of any element type (issue #10391). Before, a declared LIST OF DATE, MAP OF DATE, LIST OF BINARY, MAP OF BINARY, MAP OF DOUBLE or MAP OF FLOAT holding a non-finite number aborted the import of the whole database, and a MAP OF DECIMAL lost the scale of its values.
Impact: the export of such a property carries the type tree (y) it did not carry before, so the file is slightly larger. Files exported by earlier versions import where their data allows (see the JSONL round trip note).
Action: none.
26.11.1 - gRPC BeginTransaction refuses SERIALIZABLE
A gRPC BeginTransaction that asks for SERIALIZABLE isolation is now refused with INVALID_ARGUMENT, as is an isolation value the server does not know, a newer client’s enum constant (issue #10386). Before, the server began the transaction at REPEATABLE_READ (or, for an unknown value, at READ_COMMITTED) and answered success, so a caller that needed write-skew prevention was told its transaction started and got a weaker guarantee. HTTP, SQL, openCypher and PostgreSQL already refused a level the engine cannot provide, and SQL BEGIN ISOLATION and openCypher START TRANSACTION ISOLATION now say so in the same words as HTTP: Invalid isolationLevel 'SERIALIZABLE': accepted values are [READ_COMMITTED, REPEATABLE_READ] (SQL raised the JVM’s No enum constant …).
Impact: a hand-written gRPC client that sends SERIALIZABLE now fails at BeginTransaction. The first-party RemoteGrpcDatabase never sent it. READ_UNCOMMITTED, the proto default, still begins, at the stronger READ_COMMITTED, and the response reports it in effective_isolation. The server no longer logs a throttled warning for SERIALIZABLE, because there is nothing left to warn about.
Action: ask for REPEATABLE_READ, the strongest level the engine provides. See Transactions.
26.11.1 - A database an overwrite restore replaces refuses writes until the restore ends
While restore backup … overwrite downloads and extracts the backup, the database it replaces stays open for reads and refuses every write, on every protocol, including connections and transactions opened before the restore started (issue #10689). The write is answered as "not available": 57P03 for Postgres, DatabaseUnavailable for BOLT, UNAVAILABLE for gRPC. A database setting change is refused the same way. The restore first waits for the commits already running. Before, such writes were acknowledged and then lost when the restored copy replaced the database.
Impact: an application writing to a database while it is restored now gets an error instead of a success that is silently lost. Reads are unchanged until the swap. When the restore fails, writes are accepted again.
Action: none for most installations. Retry the writes after the restore ends.
On an HA cluster the same holds for writes made through the other servers (issues #10965, #10974): while the leader restores a database, a commit on a replica is refused with a retryable error, and the commits the leader had already accepted from replicas are completed before the download starts (at most 10 seconds, otherwise the restore is refused and can be retried). A restore database or restore backup is refused, retryable, when the server running it is not the leader, or when the leadership moves before the restored copy replaces the database: a new leader may have accepted writes meanwhile, and the restore would destroy them. Requests sent to a follower are forwarded to the leader, as before.
26.11.1 - BOLT: a message sent out of turn fails the connection until RESET
A BOLT connection that receives a message its current state does not allow, for example a second HELLO on an authenticated connection, now answers with a protocol error and moves to the failed state, as it already did for RUN, PULL or COMMIT sent out of turn (issue #10771). Until the client sends RESET, every request is answered IGNORED, HELLO and LOGON included. Before, HELLO and LOGON answered the error but left the connection serving, and answered an error instead of IGNORED on a failed connection.
Impact: none for the Neo4j drivers, which never send these messages out of turn. A hand-written client that did must send RESET after the error.
Action: none for most installations.
26.11.1 - Postgres, Bolt, gRPC, Redis, MongoDB, MCP and Gremlin clients no longer open a database a restore holds or close database closed
These protocols resolved the database a client names through the server’s own entry point, which sees past the marks an operator puts on a name, so a connection could open the copy an overwrite restore was about to replace, or a database closed for a cold backup (issue #10533). They now resolve it through the entry point HTTP uses, and the client is told that the database is not available.
Impact: a connection that names such a database while the restore runs, or after close database and before open database, is refused instead of opening it. A database that is already open is served for reads as before and refuses writes while the restore runs (see the note above), and a restore that targets a name nobody has open no longer loses the writes a client made to the copy it replaces.
Action: none for most installations. Retry the connection once the restore ends, or run open database after the cold backup.
26.11.1 - A plugin route that another plugin or the server already holds fails the server start
The routes a plugin declares through registerAPI(HttpServer, PluginRoutes) are now one namespace shared by every plugin (issue #10385). Declaring another method on a path a different plugin declared merges into the same entry. Declaring a method a plugin already declared on that path, a path that fills a route the server registers for itself (for example GET /api/v1/server or /api/v1/query/mydb), or a prefix equal to one of the server’s own mounts (/api/v1, /api/v1/ai, /ws), is refused with an IllegalStateException naming both parties, and the server does not start. So is an exact path on the path of a prefix another party holds, and a prefix on the path of an exact path another party holds (issue #10532): an exact path is matched before any prefix, so one of the two would answer everything under the path but not the path itself. One plugin may declare both on one path, to handle the root in one handler and everything below it in another. Before, the second registration silently replaced the first plugin’s route, which then answered 405 naming the other plugin’s method, a plugin could shadow a core route, and a plugin declaring the /api/v1 prefix replaced the whole core router, with no log line. The same check applies to a plugin still implementing the deprecated registerAPI(HttpServer, PathHandler).
Impact: a third-party plugin that relied on replacing another route no longer starts the server. The plugins shipped with ArcadeDB are unaffected.
Action: give the route another path, or remove the plugin that also declares it. A route declared GET now also answers HEAD with the same handler, as the core health routes do, so container healthchecks such as wget --spider work against it (issue #10384). Every core GET route does the same, and a 405 from a core route names the methods the route answers in an Allow header, as a plugin route does (issue #10531).
26.11.1 - A taken schema object name is a conflict, not a server error
Creating, or renaming onto, a name already taken by a type, property, index, constraint, bucket, trigger, view or function now raises com.arcadedb.exception.AlreadyExistsException, a SchemaException (issue #10153). Before, the SQL statements raised CommandExecutionException, and defining a function or a library twice, or renaming or copying a type onto a taken name, raised IllegalArgumentException.
Impact: over HTTP these answer 409 instead of 500 (or 400 for a rename); over PostgreSQL 42P07, 42701 or 42710 instead of XX000 or 42P01; over Bolt a Neo.ClientError.Schema.*AlreadyExists code; over gRPC ALREADY_EXISTS. Embedded or remote Java code that caught IllegalArgumentException or CommandExecutionException for this case no longer catches it.
Action: catch AlreadyExistsException (or SchemaException) where you handle a name that is already taken. Clients that keyed on the old status codes should expect the new ones.
26.11.1 - REFRESH INCREMENTAL views track every type their query reads
An INCREMENTAL materialized view now refreshes on changes to every type its query names, at any depth (subqueries in FROM, WHERE, projection, LET), and to all their subtypes (issue #10205). Before, only the first FROM type was tracked, without its subtypes, and the view went stale while staying VALID. REFRESH INCREMENTAL is refused for a query that reads a TRAVERSE, a MATCH, a function, a bucket, an index, schema: metadata, record ids or parameters in FROM.
Impact: existing views have their source list completed from their query on open, so they refresh more often, and DROP TYPE of any type they read is refused while the view exists. Creating such a view, or switching one to INCREMENTAL, with an untrackable source fails.
Action: use REFRESH MANUAL or REFRESH EVERY for a view over an untrackable source. See INCREMENTAL.
26.11.1 - CREATE PROPERTY … STRING over stored non-string values is refused
CREATE PROPERTY T.n STRING now fails when records of T or its subtypes already hold n as a number, a date, a boolean or another non-string value, as CREATE PROPERTY … INTEGER over a stored 'abc' always did (issue #10204). Before, it was accepted without converting the values, and an index on n compared them as text while a scan compared them natively.
Impact: a schema script that declares such a property fails, naming the first offending record.
Action: convert the values first, for example UPDATE T SET n = n.asString(), then create the property. See CREATE PROPERTY.
26.11.1 - SQL replace() and length() on NULL return NULL
s.replace('a', 'b') on a NULL s returns NULL (it returned 'a', its first argument), and so does replace() with a NULL argument. s.length() on NULL returns NULL (it returned 0), as SQL’s REPLACE() and LENGTH() do (issue #10193). ''.length() is still 0.
Impact: WHERE s.length() = 0 no longer matches records where s is NULL.
26.11.1 - JSONL import clears links to records outside the export, and restores value types and bucket counts
A link whose target is not in a jsonl export (deleted before the export, or excluded by includeTypes, excludeTypes or includeRecords) is set to null on import, or dropped from a LIST OF / MAP OF, and counted as danglingLinks in the import result (issue #10201). Before, it kept its old RID, which after the import named another record. Values with no declaration now keep their type (DECIMAL scale, LONG, SHORT, BYTE, FLOAT, full date precision, BINARY, arrays, links), and each type is recreated with its exported bucket count.
Impact: a record whose MANDATORY or NOT NULL link points outside the export fails to import like any other record error. The export is format version 2 (exporterVersion 2); files exported by older versions import as before.
Action: none for most installations. Check danglingLinks in the import result. See the JSONL round trip note.
26.11.1 - GraphSON and GraphML exports write every value type, and Gremlin accepts primitive arrays
A graphson export no longer fails for the whole database on a BYTE, SHORT, DECIMAL, date or BINARY property: it writes TinkerPop’s gx: types and ArcadeDB’s own arcadedb: types (issue #10202). A graphml export declares a key with a GraphML type only when all its values share it, and writes the other values as text with an arcadedb:type attribute that ArcadeDB’s GraphML import reads back. The Gremlin API now accepts byte[], short[], int[], long[], float[] and double[] property values and stores them natively; the graph features report Integer, Long, Float and Double array values as supported. String and boolean arrays are still refused.
Impact: a GraphSON file with gx: types needs TinkerPop’s GraphSONXModuleV3 to be read outside ArcadeDB, and the arcadedb: types are readable by ArcadeDB only. Links are written as plain RID text in both formats.
Action: none for ArcadeDB to ArcadeDB transfers. See the graph formats note.
26.10.2 - Client requests wait in a queue when the server is busy
Every request of a remote client (HTTP, Postgres, BOLT, Redis, gRPC, Gremlin Server, MCP, MongoDB) now goes through a query admission gate: at most twice the number of cores run at once, and a request that arrives when they are all taken, or when the running queries hold more than 80% of the query heap budget, waits in a queue and starts in arrival order (issue #9518). Before, every request started at once, and under a burst of heavy queries the last ones were refused by the heap budget or exhausted the heap.
Impact: under load a request can wait before it starts, up to arcadedb.queryQueueTimeout (30 seconds by default), and is refused as retryable once that runs out or when arcadedb.queryQueueMaxSize requests already wait (HTTP 503). A MongoDB request is refused at once instead of waiting. A BOLT result stream that is not read to the end keeps its slot until it is.
Action: none for most installations. Size the gate with arcadedb.queryMaxConcurrent, arcadedb.queryAdmissionHeapWatermark, arcadedb.queryQueueTimeout and arcadedb.queryQueueMaxSize (see Settings), or set arcadedb.queryMaxConcurrent=0 for the previous behavior.
26.10.2 - The Gremlin plugin fails the server start when its port is busy
If the Gremlin port (gremlin.port, default 8182) is already in use, the server now stops with an error. Before, it started without a Gremlin listener and still advertised the port in GET /api/v1/server (issue #9319). With gremlin.port=0 a free port is chosen at startup and advertised.
Impact: a server that used to start while another process held the Gremlin port no longer starts.
Action: free the port or set gremlin.port to a free one.
26.10.2 - A plain = / IN on a COLLATE CI index is case sensitive
The planner used to return every case variant for WHERE Name = 'JOHN' when a COLLATE CI index served the query, while the same predicate evaluated by a scan was case sensitive (issue #9403). The index no longer changes the answer: a plain = or IN is case sensitive with or without it.
Impact: a query that relied on Name = 'john' finding "John" through a COLLATE CI index now returns fewer rows.
Action: write the lookup on Name.toLowerCase(), for example WHERE Name.toLowerCase() = 'john', which the index serves. See Case-insensitive indexes.
26.10.2 - Cypher: a path variable over a quantified group is one path, and ignored GQL constructs are refused
MATCH p = m)-[r]→(n{2} used to bind p to a list with one path per repetition. It now binds one path running through every repetition, as any other path variable does and as Neo4j Cypher 25 does: valueType(p) is PATH NOT NULL and length(p) is 2 (issue #9940). Variables declared inside the group (m, r, n) are still lists.
GQL constructs that the parser accepted and the engine ignored now fail with a FeatureNotImplemented error that names the construct:
-
the path search prefixes
ANY SHORTEST,ALL SHORTEST,SHORTEST k,SHORTEST k GROUPS,ANYandANY k -
the match mode
REPEATABLE ELEMENTS -
x IS DIRECTED -
USEof a graph other than the current database -
a quantified group nested inside another one
An ORDER BY, SKIP, OFFSET or LIMIT written after RETURN (RETURN … LIMIT 2 OFFSET 1) is now applied after the RETURN’s sort, as one page: sort, then skip, then limit.
Impact: a query that read such a p as a list (size(p), p[0]) gets a path instead. A query that used one of the refused constructs fails instead of returning rows the construct did not select.
Action: use length(p), nodes(p) and relationships(p) on the path. Replace a shortest-path prefix with shortestPath() or allShortestPaths(), and drop REPEATABLE ELEMENTS, IS DIRECTED and USE. See ISO GQL Coverage Statement.
26.10.2 - Cypher DURATION arithmetic is exact, and date * duration is an error
Durations stored in a property are read back as durations, including short ones such as P1D, so ORDER BY and comparisons on them now work (issue #9338). Duration arithmetic raises an arithmetic error when the result does not fit, instead of returning a wrong value. A date or time combined with a duration using *, / or % is now a type error, and duration + date is accepted.
Impact: a query that relied on a stored duration being plain text, or on date * duration returning a date, changes.
Action: none for normal use. A range index on a duration property still orders by its text form (issue #9348).
26.10.2 - Sparse-vector top-K is split across workers from 50,000 postings
vector.sparseNeighbors on an LSM_SPARSE_VECTOR index now splits a query across the scoring workers once its terms hold 50,000 postings in total (setting arcadedb.sparseVectorScoringMinPostingsForPartitioning, default 50000, was 200000). Queries between 50,000 and 200,000 postings used to run on one thread and set the slowest latencies. A query just past the threshold uses two ranges and leaves the other workers free; wider queries use more (issue #9482).
Impact: results are the same. Queries in that size range are faster, and use more CPU when the server is otherwise idle.
Action: none. On a small or shared server where CPU matters more than latency, raise arcadedb.sparseVectorScoringMinPostingsForPartitioning (for example back to 200000).
26.10.2 - Production mode conceals error text on Bolt, Redis, /ws and MCP
In production server mode the /ws insert session, /ws change-event subscriptions, Bolt, Redis and MCP now answer an engine failure with "The request failed. Check the server log for the details" and write the detail to the server log, as HTTP, gRPC, PostgreSQL, MongoDB and Gremlin Server already did (issue #8749). Before, they returned the engine’s message as is, including the stored values of a duplicated key.
Impact: a client of these protocols that reads the error text in production gets the generic message. Error codes, Neo4j status codes, RESP prefixes and exception class names are unchanged, and so is text the protocol writes about the request itself.
Action: branch on the code rather than on the message, and read the detail from the server log. See Error details in production mode.
26.10.2 - list queries names the client on every protocol
Every entry of list queries and SHOW TRANSACTIONS now carries the client that asked for the work: clientAddress for MCP tool calls (stdio for the stdio transport), /ws insert sessions, the Gremlin Server and gRPC, which listed none before, plus a connectionId for the Gremlin Server and /ws (issue #9962). A client whose address the transport cannot report has no clientAddress field, where it used to be listed as "null".
Impact: tools that read the listing see the new fields. Plugins that open entries through RunningQueryRegistry.register(…) or open(…) now pass a RunningQuery.Client (RunningQuery.Client.NONE when there is no client).
Action: none for clients. Plugin code: add the client argument. See list queries.
26.10.2 - Graph algorithms answer the incoming side of unidirectional edge types
The algo. and path. procedures, the path-finding SQL functions (dijkstra(), astar(), bellmanFord(), cchShortestPath(), duanSSSP()) and the node.degree* / node.relationship.* functions now follow the edges of a UNIDIRECTIONAL type from their target too, as a Graph Analytical View already did (issue #8629). Before, they saw such an edge from its source only, so for example a walk with direction IN or BOTH missed it. refactor.mergeNodes, refactor.cloneNodesWithRelationships and changing a node’s labels in Cypher no longer drop the incoming unidirectional edges of the node.
Impact: results over unidirectional edge types can change (more paths, higher in-degrees, different scores), and now match the results with a Graph Analytical View.
Action: none. See CREATE TYPE for the heap a walk from the target takes.
26.10.2 - Cypher counts match an undirected self loop once
An undirected relationship pattern such as (n)-[:KNOWS]-(m) matches a self loop (an edge from a vertex to itself) once, as the openCypher specification and Neo4j do. The rows of such a pattern always did, but the counts computed without materializing the rows counted it twice: MATCH (a)-[:KNOWS]-(b) RETURN count(), a count() grouped by the start node, OPTIONAL MATCH … WITH n, count(m) and the COUNT { } subquery, with and without a Graph Analytical View (issues #8750, #9540). The same release corrects the count of a chain with an inequality such as WHERE a <> c when the chain has a labelled or multi-hop part before or after the two positions the inequality compares.
Impact: those counts can be lower than before on graphs with self loops, or on such chains, and now equal the number of rows the same MATCH returns.
Action: none.
26.10.2 - Cypher count(*) of an OPTIONAL MATCH with several arms, and of a Cartesian product
A count(*) over a star pattern no longer counts an OPTIONAL MATCH that reaches the central node from two sides, or holds two patterns, as two independent optional matches: when one side finds nothing the clause contributes one row, not the number of matches of the other side. A star next to a node pattern that shares no variable with it, MATCH (a)-[:R]→(b), (a)-[:S]→(c), (t:Tag), now multiplies by the Tag count, which it used to leave out. Both were answered by the count push-down without building the rows (issue #9596).
count(*) over parts of a MATCH that share no variable is now the product of the parts' counts and builds no row, so a count whose result does not fit a 64-bit integer raises an error instead of running until the command timeout.
Impact: the counts of those two star shapes change, and now equal the number of rows the same MATCH returns.
Action: none.
26.10.2 - A Postgres cancel request stops the statement and keeps the connection
A CancelRequest (what JDBC Statement.cancel(), pgjdbc’s setQueryTimeout() and libpq’s PQcancel send) now stops
the statement the connection is running with SQLSTATE 57014 and leaves the connection open, as PostgreSQL does
(issue #9689). Before, it closed the whole connection, and since every connection had the backend process id 0, a
cancel could reach a different connection than the one it named.
Impact: after a cancel or a query timeout the connection stays usable instead of being closed.
Action: a client that relied on a cancel request closing the connection must call pg_terminate_backend(pid)
instead. See Cancelling a statement.
26.10.2 - An HTTP client that disconnects terminates its statement
A client that closes its connection while its statement runs (its own timeout, a killed process, curl --max-time)
now terminates the statement, and what it wrote is rolled back unless it had already committed, instead of leaving the server working on it until it completes or
reaches arcadedb.command.timeout (issue #9689). This applies to plain HTTP/1.x connections, not to TLS or HTTP/2.
Likewise a gRPC client that cancels a call, or whose deadline expires, terminates the server-side work.
Impact: a client that sends a write and closes the connection without waiting for the answer can find the write rolled back.
Action: wait for the response of a write before closing the connection. See List and terminate running queries.
26.10.2 - openCypher temporals are stored natively
openCypher used to store time(), localtime(), zoned datetime() and duration() values as Strings and guessed the type back from the text, so a String such as "09:00-17:00" read back as a time. They are now stored in four new property types, OFFSET_TIME, LOCAL_TIME, ZONED_DATETIME and DURATION, and a new database never reinterprets a String (issues #8572, #9462, #9636). A zoned datetime() written to a DATETIME property is stored as an instant and reads back as the UTC wall clock; before, it kept the local wall clock.
Impact: a database written by 26.10.1 or earlier keeps the old reading of undeclared properties until it is migrated, so existing queries keep working after the upgrade. The migration is one-way and converts every String with a temporal shape on an undeclared property, including one meant as text.
Action: take a backup, quiesce writers, and run CALL db.migrateLegacyTemporals() YIELD records, properties once per database (under HA, on the leader; the user needs updateDatabaseSettings). To convert only some properties, declare each with its new type and copy the values through a scratch property instead. See Migrating Cypher temporals stored as Strings.
26.10.2 - Weighted path finders share one edge-weight rule
Every weighted shortest-path finder now reads an edge weight the same way: an edge without the weight property, or with a value that is not a number, weighs 1, and an edge whose weight is negative, NaN or infinite is not walked (issue #9443). Before, dijkstra(), astar(), algo.dijkstra and algo.astar treated a missing weight as 0, and several finders walked negative weights. duanSSSP() now accepts an options map { direction, edgeTypeNames } as its fourth argument, answers through the same engine as cchShortestPath() and honors the command timeout. bellmanFord() and algo.bellmanford are unchanged.
Impact: on a graph where some edges have no weight, dijkstra(), astar(), algo.dijkstra and algo.astar return different paths and distances. A graph with negative weights is now routed around those edges. Among paths of equal weight duanSSSP() may return a different one. Embedded code: the MIN constant of SQLFunctionHeuristicPathFinderAbstract is gone.
Action: give every routed edge a weight if 0 was intended, and use bellmanFord() where negative weights must be walked. A custom subclass reads the rule from com.arcadedb.graph.EdgeWeight. See dijkstra() and duanSSSP().
26.10.2 - SQL UPSERT requires a UNIQUE index
UPDATE … UPSERT WHERE without a sub-query used to accept any index, so an upsert matched through a non-unique index could create duplicates. It now needs an equality on every property of a UNIQUE index, as the sub-query form already did (issues #9339, #9342).
Impact: an UPSERT served by a non-unique index, by part of a composite key, or by a range, IN or OR condition fails with an error stating the requirement.
Action: create a UNIQUE index on the properties of the WHERE condition, or rewrite the condition as equalities on all of its properties. See UPDATE.
26.10.2 - SQL sum() over an empty or all-NULL group returns NULL
SQL sum() now returns NULL for an empty or all-NULL group, like avg(), min() and max(). It used to return 0 (issues #9351, #9384). Cypher sum() keeps returning 0, as openCypher requires.
Impact: SQL queries that read sum() of such a group get NULL.
Action: wrap the aggregate where 0 is expected, for example ifnull(sum(amount), 0).
26.10.2 - SQL k = <list> no longer expands the list
An index lookup used to expand a list on the right of =, so k = [1, 2] behaved like k IN [1, 2], while a scan compared the field with the list. Only IN and the other multi-value operators expand a list now; = and the range operators compare with the list as one value, which matches nothing on a scalar field (issues #9370, #9385).
Impact: a query that relied on = with a list to match any element returns no rows.
Action: use IN.
26.10.2 - A statement that hides a write inside a read is refused on the query path
SQL decided whether a statement reads, writes or changes the schema from its top-level node only, so a write placed in a LET, a projection, a FROM sub-query, a CASE, a RETURN or a script block ran durably through the read-only paths (issues #9628, #9687). A statement now counts everything nested in it. PROFILE takes the classification of the statement it wraps, so PROFILE INSERT is a write (issue #9340). EXPLAIN stays read-only.
Impact: Database.query(), GET and POST /api/v1/query and MCP identities without write permission refuse such statements. A DDL inside IF, FOREACH or WHILE needs allowSchemaChange on MCP.
Action: send these statements as commands (Database.command(), POST /api/v1/command).
26.10.2 - SQL include() and exclude() return a map
include() and exclude() used to build a record carrying the source record identity, so calling save() on a projection overwrote the stored record. They now always return a map with no @rid, @type or @cat unless one of the names asks for it, and they work on edges (issues #9840, #9873). UPDATE V SET copy = @this.exclude('secret') now stores an embedded map instead of a link to the record itself.
Impact: code that treated the result as a record (saving it, reading its @rid, calling .out() on it) changes.
Action: apply traversals before the projection, and name @rid in include() when you need it. See include().
26.10.2 - SQL result rows: values() follows keys(), and a bare ORDER BY @rid on a MATCH with columns is refused
values() on a sub-query row or LET variable used to return the record metadata as extra unlabeled values. It now returns one value per key, in the order of keys() (issue #9843). A SQL MATCH that returns columns, $paths or $patterns builds rows with no @rid, so ORDER BY @rid sorted on null and kept the pattern order; it now fails at planning time (issue #9911). The same applies to other bare record attributes such as @type.
Impact: code that read the extra values of values() sees fewer, and a keyset pagination that used a bare ORDER BY @rid on such a MATCH fails.
Action: write ORDER BY <alias>.@rid, or return the RID as a column and order by it. See MATCH.
26.10.2 - Schema refuses a REGEXP no value can match, and constrained properties on LIGHTWEIGHT edges
CREATE PROPERTY, ALTER PROPERTY and Property.setRegexp() refuse a pattern that no value of the property type can match, such as [A-Z]+ on a LONG or a DATE, which used to be accepted and then refused every write (issues #9853, #9871). A pattern the check cannot decide in time is accepted with a WARNING. A MANDATORY or default-valued property can no longer reach a LIGHTWEIGHT edge type through a supertype, by CREATE EDGE TYPE … EXTENDS … LIGHTWEIGHT, ALTER TYPE … SUPERTYPE, ALTER TYPE … WITH lightweight = true or by declaring the property on the supertype (issue #9846).
Impact: such DDL fails with a SchemaException. A schema that already holds such a pattern or combination still opens.
Action: fix the pattern, or remove it with ALTER PROPERTY … REGEXP null. Make the inherited property optional, or keep the edge type record-backed. See ALTER PROPERTY and Lightweight edges.
26.10.2 - Cypher MERGE matches what MATCH matches, and declared properties compare in their declared type
MERGE used its own equality and disagreed with MATCH: it missed a stored FLOAT, creating a duplicate, and matched a temporal against the text of a STRING property (issues #9693, #9696). It now uses the comparison MATCH applies. On a property declared STRING, a stored text also equals a number, boolean or temporal whose written text it is, so MATCH (n:S) WHERE n.v = 1 finds the node holding the text '1'; on a property declared INTEGER, LONG, FLOAT, DOUBLE, BOOLEAN, DATE or DATETIME, a text equals the value it converts to (issues #9695, #9794). Undeclared properties keep strict Cypher typing.
Impact: a repeated MERGE no longer duplicates nodes in these cases, and =, <>, IN and joins on declared properties can return more rows than before.
Action: none, unless a query relied on the old mismatch. See MERGE.
26.10.2 - DROP CONSTRAINT <name> does not drop named constraints created by older releases
A named Cypher constraint now records what its CREATE CONSTRAINT created, and DROP CONSTRAINT <name> removes exactly that; before, it did nothing (issues #9646, #9707). Creating a constraint under a name in use and dropping an unknown name now raise an error, unless IF NOT EXISTS or IF EXISTS is given.
Impact: a named constraint created by 26.10.1 or earlier has no such record, so DROP CONSTRAINT <name> raises an error for it.
Action: drop an old constraint by its index name, or through SQL (DROP INDEX, ALTER PROPERTY … MANDATORY false). See DROP CONSTRAINT.
26.10.2 - The path. procedures answer as apoc.path. does
path.expandconfig now reads its uniqueness option, with the APOC default RELATIONSHIP_PATH, and path.expand is breadth-first with RELATIONSHIP_PATH (issue #10049). Before, the breadth-first walk was always NODE_GLOBAL and the depth-first walk and path.expand always NODE_PATH. path.subgraphall returns every relationship between the returned nodes, whatever the relationshipFilter (issue #10048). relationshipFilter and labelFilter read the APOC syntax, and an unknown relationship type or label is refused (issue #10025).
Impact: on a graph with cycles or several routes to a node these procedures return more paths, growing quickly with maxLevel. A walk too wide for arcadedb.queryMaxHeapElementsAllowedPerOp or arcadedb.queryMaxHeapRAM fails the query.
Action: to keep the old answers add uniqueness: 'NODE_GLOBAL' to a breadth-first path.expandconfig, uniqueness: 'NODE_PATH' to a depth-first one, and replace path.expand with the equivalent path.expandconfig call with bfs: false, uniqueness: 'NODE_PATH'. See path.expandconfig.
26.10.2 - An ISO duration text is parsed as Neo4j parses it
duration('<text>'), a DURATION property set from text and a gRPC value tagged duration now follow the Neo4j grammar (issue #10055). Refused: a text with no component (P, PT, P1YT), a malformed number (P.5Y, PT1.S) and a fraction on any component but the last one (P1.5Y2M). Newly accepted: a sign before the P or on a component, and a decimal comma.
Impact: writes and queries that used such texts fail instead of storing a zero or truncated duration.
Action: correct the duration texts. See DURATION.
26.10.2 - Time-series writes refuse what they used to store silently
-
A line protocol, gRPC or remote write with an empty tag value (
cpu,host= value=1) is refused with HTTP400orINVALID_ARGUMENT, as InfluxDB does. Leave the tag out instead (issues #8647, #9364). -
A sample carrying a tag, field, property or label its type does not declare in that role is dropped and reported as a partial write (HTTP
400withundeclaredKeys, or gRPCundeclared_keys). SQLINSERTfails, and Prometheus remote write drops the series (issues #8646, #9365, #9366, #9384). A sample that omits a declared key is still accepted. A Prometheus label with an empty value is an absent label. -
A column codec that cannot encode the column type is refused at
CREATE; types already persisted keep loading (issues #9310, #9384). -
A line protocol line cut right after a tag or field
,separator is refused (issues #9323, #9392).
Impact: producers that send extra tags (Telegraf, for example), empty tag values or a misspelled key see refusals where they used to succeed.
Action: fix the producers, or set the database setting arcadedb.timeSeriesUndeclaredKeys=ignore for schema-less producers. See Undeclared keys.
26.10.2 - Unbounded time-series reads stream
SELECT FROM <timeseries type> without LIMIT, and the gRPC time-series stream without a row limit, now stream rows instead of loading the whole range first (issues #9420, #9421).
Impact: a retention pass that runs during such a read gives a shorter answer, since the removed rows are skipped. A downsampling pass that runs during it fails the read with a retryable error (HTTP 503, gRPC retry category), possibly after some rows were sent.
Action: retry a read that fails this way, and expect a client to have received partial rows before the error.
26.10.2 - PromQL refuses what Prometheus refuses
The PromQL engine now answers HTTP 400 bad_data on /prom/api/v1/query and /query_range, and an error in SQL promql(), for expressions it used to answer with NaN, an empty result or an unchanged value (issues #9723, #9743, #9788, #9883, #9922, #9924): a comparison between two scalars without bool (1 > 0), and, or and unless with a scalar operand, a range vector or a string as an operator operand, a function or aggregation argument of the wrong type or count (sum(m[5m]), abs(-1)), and a range query over a range vector or a string. bool right after an operator is now the comparison modifier, so a metric named bool cannot be the right operand.
Impact: dashboards and alert rules that used these expressions get an error instead of a value.
Action: rewrite them as Prometheus requires, for example 1 > bool 0. See Refused PromQL expressions.
26.10.2 - Time-series filters, Prometheus endpoints and ts.movingAvg change results
-
The PromQL label matchers
=,!=,=~and!~read a typed tag the same way on/query, remote read and/series, so=and!=select complementary sets, and=~"007"selects aLONGtag holding 7 (issues #9833, #9880). -
The JSON tag filter of
POST /api/v1/ts/{database}/query,GET /api/v1/ts/{database}/latest, the Grafana query and the gRPCTimeSeriesQueryandTimeSeriesLatestcalls reads aDATEorDATETIMEvalue in the database time zone instead of the JVM one (issues #9879, #9929). Embedded API:TimeSeriesGateway.andTag()andbuildTagFilter()now take theDatabaseas an argument. -
Prometheus remote write and read answer
415for aproto=content type they do not implement, so a remote-write 2.0 sender falls back to 1.0 instead of losing the batch, and400instead of204for a non-empty body with no remote-write 1.0 field (issues #9624, #9755). -
ts.movingAvg()keeps one element per row: a null sample keeps its slot instead of being dropped, which shifted every later average into an earlier row (issue #9958).
Impact: some matcher and tag filter results change, a misconfigured remote-write sender now gets an error, and ts.movingAvg() results line up with their rows.
Action: recompile embedded code that calls andTag() or buildTagFilter(); otherwise none.
26.10.2 - GraphBatch.withLightEdges(true) needs a LIGHTWEIGHT edge type
GraphBatch with the deprecated withLightEdges(true) flag now throws for a property-less edge into an edge type that does not declare LIGHTWEIGHT, instead of creating light edges in it (issues #9378, #9383). The batch HTTP endpoint answers 400.
Action: declare the edge type LIGHTWEIGHT. See Lightweight edges.
26.10.2 - A Graph Analytical View over some of the vertex types no longer accelerates traversals
A view created with VERTEX TYPES naming only some vertex types was chosen by edge type alone, so SQL out(), SQL MATCH and Cypher MATCH answered 0 for edges leaving the uncovered types (issues #9301, #9320). Such a partial view no longer serves count push-downs, SQL traversal, shortest paths or algo.* procedures. A view covering every vertex type is unaffected.
Impact: queries that used a partial view now return correct results through the OLTP path, which can be slower.
Action: to keep the acceleration, create the view over every vertex type. See Graph analytical views.
26.10.2 - HA snapshot ship compresses at DEFLATE level 1
The leader compresses a database snapshot shipped to a follower at the new arcadedb.ha.snapshotCompressionLevel, default 1 (fastest), instead of the JDK default level 6, and serves two followers concurrently (issues #9295, #9296).
Impact: a full resync is faster on a LAN, but sends more bytes over a WAN or metered link.
Action: set arcadedb.ha.snapshotCompressionLevel=6 (or -1) to restore the previous level. See SERVER settings.
26.10.2 - HA: a follower restarted in place is no longer reformatted automatically
A follower whose Raft layer was restarted in place, and that has since taken no replicated entry and seen no new leader, is no longer reformatted when it looks stuck at a stale term, because that signature also matches a dead replication path (issues #8901, #9001). A node that was never restarted in place keeps the automatic recovery of arcadedb.ha.divergedFollowerRecovery.
Impact: such a node keeps raising the follower-stuck-at-stale-term alert and logs a WARNING every 5 minutes instead of healing itself.
Action: restart the node by hand when the alert persists.
26.10.2 - HA rolling upgrade: type renames and database settings wait for every node
ALTER TYPE … NAME and ALTER DATABASE / set database setting now replicate to every node; before, they changed the node that ran them only (issues #9762, #9763). While any peer runs an older build or cannot be reached, a rename is refused up front with HTTP 409, and a database setting change with HTTP 409 / gRPC FAILED_PRECONDITION; nothing changes (issues #9891, #9904, #9919). Path-valued settings (arcadedb.externalPropertyBucketPath, arcadedb.pageSnapshotSpillPath, arcadedb.opencypher.loadCsv.importDirectory) now apply on every node. No setting overrides the rename refusal; arcadedb.ha.securityEntryCapabilityGate=false skips the database setting check, at the risk of halting a peer that cannot decode the entry (see how-to/operations/ha.adoc#ha-database-settings).
Impact: during a rolling upgrade, and while a node is down, these commands fail. Once a rename has been replicated, the cluster cannot be downgraded past 26.10.2 on its retained Raft log.
Action: finish the rolling upgrade before renaming types or changing database settings. To rename a type while a peer is gone for good, remove that peer from the Raft configuration first. Make sure path-valued settings point to a directory that exists on every node. See HA cluster upgrade.
26.10.2 - HA vector indexes that lost entries on a follower must be rebuilt
A vector insert committed through a follower could reuse a vector id the leader had already allocated and replace that vector on every node, so countEntries() stayed flat and the replaced vectors dropped out of search (issues #9428, #9507). The cause is fixed, but the lost vectors are not restored.
Impact: an HA vector index that received inserts through followers before the upgrade can be missing entries.
Action: run REBUILD INDEX <name> on such vector indexes after every node runs 26.10.2. See REBUILD INDEX.
26.10.2 - Vector graphs are rebuilt after a snapshot install or a restore
A snapshot install now ships a vector index graph file (.vecgraph) empty, and the follower builds and verifies its own graph on the first search (issues #9758, #9889). A full backup archives the graph with its side files only when it can prove they belong to the pages it captured; otherwise it archives the graph empty, and the restored index builds it on the first search (issues #9888, #9917, #9918). See Vector indexes.
Impact: the first search on a vector index after a snapshot install, or after a restore whose graph could not be archived, pays one graph build. Archives taken before 26.10.2 restore the old graph unverified (counted in unverifiedGraphReuses and logged at WARNING).
Action: after restoring an archive taken with 26.10.1 or earlier, run REBUILD INDEX on its vector indexes. See Restore.
26.10.2 - A follower’s write refusal is a redirect on every protocol
A write sent to an HA follower is refused with the leader address. Bolt, MongoDB and Redis reported it as a retryable conflict, so drivers retried against the same follower. A refusal that names the leader is now a redirect (issues #9845, #9868, #9869, #9930, #9931): Bolt Neo.ClientError.Cluster.NotALeader (was DeadlockDetected), MongoDB 10107 NotWritablePrimary (was 112 WriteConflict), Redis READONLY (was TRYAGAIN), the /ws insert endpoint code NOT_LEADER (was DB_ERROR) with a leaderAddress field, and the gRPC streaming inserts InsertError.code = NOT_LEADER (was DB_ERROR or COMMIT_FAILED) with leader_address. PostgreSQL keeps 40001. During an election, with no leader known, the old retryable answers stay.
Impact: a bolt:// (not neo4j://) or plain MongoDB client no longer retries a leader refusal, and clients that matched the old codes miss the new ones.
Action: send writes to the leader or use a routing driver, and handle the new codes. See /ws insert session.
26.10.2 - Gremlin Server in production mode refuses a channelizer that cannot hide error details
With arcadedb.server.mode=production, a Gremlin Server channelizer other than TinkerPop’s default WebSocketChannelizer (WsAndHttpChannelizer, HttpChannelizer, UnifiedChannelizer or a custom class), set by channelizer: in gremlin-server.yaml or by gremlin.channelizer, now fails the server start, because it would send engine error text, including stored values, to clients (issues #9639, #9750). Outside production mode the server starts with a WARNING.
Impact: a production server configured with such a channelizer does not start after the upgrade.
Action: remove the channelizer entry or set it to org.apache.tinkerpop.gremlin.server.channel.WebSocketChannelizer. See Gremlin Server.
26.10.2 - Security checks that now apply to open sessions, GraphQL and LOAD CSV settings
-
PostgreSQL, Bolt, MongoDB, Redis and
/wssessions re-check their user against the live security state on every request or frame (issues #9279, #9311, #9340). Deleting a user, changing a password or revoking a database grant now applies to open connections; a Bolt transaction or/wssession whose authorization is gone is rolled back. -
A GraphQL type definition replaces the database GraphQL schema, so it needs
UPDATE_SCHEMA(issue #9280). -
The settings that control what
LOAD CSVmay reach (remote URLs, local files, import directory, blocked IP ranges) needupdateSecurityto change throughALTER DATABASE(issue #9371).
Impact: long-lived clients can be disconnected or refused after a security change, and users without these permissions are refused.
Action: grant updateSchema to the users that define GraphQL types and updateSecurity to the users that change the LOAD CSV settings. See Security.
26.10.2 - Rotate passwords set through server.defaultDatabases if diagnostics were uploaded
Before 26.10.2, support diagnostics masked secrets by setting name only, so a password inside -Darcadedb.server.defaultDatabases=Universe[albert:einstein] was written in clear to diagnostics.json, uploaded to the portal and put in the support bundle (issues #9625, #9678). It is now masked there and in logs.
Action: on a server that uploaded support diagnostics before 26.10.2, rotate every password configured through arcadedb.server.defaultDatabases. See Support.
26.10.2 - Subtype records keep inherited EXTERNAL values inline until REBUILD TYPE
A subtype that inherits an EXTERNAL property now stores its values in its own _ext bucket; before, they were written inline (issues #9683, #9815).
Impact: subtype records written before the upgrade keep those values inline. They read normally, but do not get the storage benefits.
Action: run REBUILD TYPE <Subtype> to move them. See External property storage.
26.10.2 - Creating a user or a database under a taken name answers 409
Creating a user whose name exists used to answer 403 "Security error", and creating a database whose name exists 400 (issues #10073, #10074). Both now answer 409 over HTTP (REST routes and server commands, including import database) and ALREADY_EXISTS over gRPC. The Java remote clients raise RemoteAlreadyExistsException for a taken user, API token or database name (issue #10075). Restoring into a taken name keeps 400.
Action: clients that matched 403 or 400 for these cases must handle 409.
26.10.2 - BACKUP DATABASE applies the retention policy
The SQL BACKUP DATABASE statement now prunes the archive directory of the database after a successful backup, as the scheduler and the trigger backup command already did (issue #9844). With auto-backup disabled nothing is pruned.
Impact: with auto-backup enabled, archives beyond the maxFiles (or tiered) retention of the effective policy are deleted, also for a database absent from the auto-backup configuration. The archive just written, and archives that do not follow the <db>-backup-<timestamp>.zip naming, are never deleted.
Action: give archives you want to keep a custom name, for example BACKUP DATABASE 'before-the-upgrade.zip'. See Backup.
26.10.2 - gRPC settings are read from the configuration file and environment
Every arcadedb.grpc.* setting is now a registered server setting, so the server configuration file and environment variables reach it (issue #9316). Before, only arcadedb.grpc.port was, and the others worked as JVM system properties only: a grpc.tls.enabled=true written in the configuration file was ignored and the endpoint stayed in cleartext. An invalid grpc.mode or grpc.tls.enabled value now makes the server refuse to start, grpc.host is applied to the standard server, and grpc.compression.enabled=false is honored (issue #9782).
Impact: grpc.* values that sat unused in the configuration file now take effect at the next start, which can turn on TLS or change the bind address.
Action: review the grpc.* entries of the configuration file and environment before upgrading. See gRPC settings.
26.10.2 - Docker images run on Java 25, and Studio is served with a Content-Security-Policy
The Docker images are built on eclipse-temurin:25-alpine instead of Java 21, which enables compact object headers, and ARCADEDB_OPTS_GC drops the obsolete -XX:+ZGenerational (issue #9602). The jars still run on Java 21 for embedded use. Studio and the Swagger UI page are served with a Content-Security-Policy that refuses inline script (issue #9681), set by arcadedb.server.httpContentSecurityPolicy.
Impact: custom images or scripts that set JVM flags removed in Java 25 fail to start. A reverse proxy or extension that injects inline script into Studio is blocked.
Action: check custom JVM options against Java 25. To try a custom policy without blocking, set arcadedb.server.httpContentSecurityPolicyReportOnly=true. See Docker / Kubernetes.
26.10.2 - New and rebuilt hash indexes use page layout 3, which 26.10.1 cannot open
A NOTUNIQUE_HASH key whose record ids would take more than a quarter of a page now keeps them in a chain of pages of their own, so adding a record to a key costs the same whatever the key already holds (issue #9228). Before, the cost of an insert grew with the record ids the key already held: 49 pages were written per insert into a key with 20,000 of them. Every hash index created or rebuilt by 26.10.2, UNIQUE_HASH included, is written at page layout version 3. Hash indexes created by older versions keep working at their layout and move to version 3 only on REBUILD INDEX.
Impact: none on upgrade. A 26.10.1 server knows layouts 1 and 2 only, and refuses a hash index at layout 3 (see Downgrading from 26.10.2 to 26.10.1 with hash indexes).
Action: to get the new layout on an existing NOTUNIQUE_HASH index with large keys, run REBUILD INDEX on it. See Hash index.
26.10.2 - Cypher paths are sent as one path object on every protocol
Every Cypher path is now one path value, whatever built it: a MATCH p = … pattern, shortestPath(), allShortestPaths(), a path procedure, or apoc.path.create, apoc.path.slice and apoc.path.combine (issue #9913). Over HTTP (and Result.toJSON()), gRPC and PostgreSQL a path returned by a MATCH used to be sent as its text form; it is now the object {"_type": "path", "nodes": […], "relationships": […], "length": n}, with every node and relationship written whole whatever the JSON serializer settings. A Bolt driver receives a native Path for the paths of procedures and shortestPath() too, and Studio’s graph view draws a returned path.
Impact: a client that parsed the text form of a MATCH path gets an object instead.
Action: read the nodes, relationships and length fields of the object, or return nodes(p) and relationships(p) explicitly. See reference/graph-algorithms/path-finding.adoc#graph-algo-path-value.
26.10.2 - COMMIT RETRY and Database.transaction() do not retry a leader refusal
A NeedRetryException now tells whether retrying on the same server can succeed (isRetryableOnThisNode()). An HA follower that refuses leader-only work (a schema change, a security mutation) and names the leader answers false, so BEGIN … COMMIT RETRY <n> and Database.transaction(…, retries) raise its ServerIsNotTheLeaderException on the first attempt (issue #9995). Before, they retried it <n> times with the retry delay in between, and COMMIT RETRY … ELSE then ran the ELSE block. A refusal raised during an election, which names no leader, is still retried.
Impact: the caller gets the leader refusal at once. A COMMIT RETRY … ELSE { … } AND CONTINUE block no longer runs its ELSE or hides the error for such a refusal.
Action: send leader-only work to the leader the error names. See reference/sql/sql-script.adoc#sql-script-commit.
26.10.2 - GraphQL introspection reports real types, and Cypher typed constraints keep a property’s attributes
-
GraphQL introspection of a type with no
.gqldeclaration reportsMAPand untypedEMBEDDEDas the newJSONscalar,EMBEDDED OF <T>as an object,LINKasID,DECIMALas the newBigDecimalscalar andBINARYas a list ofInt; in 26.10.1 and earlier they were allString. A sub-selection on aLINKproperty now resolves the linked record (issues #8756, #9467, #9468). See GraphQL type mapping. -
CREATE CONSTRAINT … REQUIRE n.x IS :: <TYPE>keeps the property’s earlier attributes (MANDATORY,READONLY,REGEXP,MIN,MAX,DEFAULT) instead of dropping them, and checks the records already stored: when one fails, the constraint is refused and the old declaration stays (issue #9304). See reference/cypher/cypher-clauses.adoc#cypher-is-typed-retype.
Action: regenerate GraphQL client code built from introspection, and expect a typed constraint over existing data that does not fit to be refused.
26.10.2 - Embedded API changes
-
Schema.alterMaterializedView()refuses a null mode and a negative interval (IllegalArgumentException) and an interval on aMANUALorINCREMENTALview (SchemaException), which it used to store (issues #8726, #9381). SQLALTER MATERIALIZED VIEWis unaffected. -
Schema.unregisterFunctionLibrary()of a stored library is now saved toschema.jsonand replicated (issues #8879, #9423). -
Iterator.remove()on the iterators ofVertex.getEdges()andVertex.getVertices()deletes the edge through the database, asedge.delete()does, and a refused delete reaches the caller with the entry left in place; a secondremove()throwsIllegalStateException(issue #9779). Before, an iterator over given edge types only unlinked the walked side, and one over every edge type logged a refusal and unlinked anyway. See reference/java-api/java-ref-database.adoc#vertex-edge-iterator-remove. -
On an HA follower,
unregisterFunctionLibrary(),Propertysetters,Schema.setExtension(), the schema date settings and type setters such asLocalEdgeType.setLightweight()throwServerIsNotTheLeaderException("Changes to the schema must be executed on the leader server") instead of changing that follower alone (issues #9747, #9765).Schema.setZoneId()checksUPDATE_DATABASE_SETTINGS.
Action: call these methods on the leader, validate materialized view arguments before passing them, and expect remove() on an edge iterator to throw when the delete is refused.
26.10.2 - Client error handling: gRPC writes whose answer was lost, streamed error lines, Postgres transaction modes
-
The gRPC Java client throws a non-retryable
RemoteExceptionwhen an admin write throughRemoteGrpcServeror a streaming write (ingestStream,ingestBidi,timeSeriesWriteStream, graph batch loads) ends with a bareUNAVAILABLE, since the write may have been applied; reads andvalidate_onlydry runs stay retryable. A client-side deadline is aTimeoutExceptionwhose message says the write may already have been applied (issues #8822, #9470). -
In production mode, the
messageof theerrorline of a streamed query is the classified label, like theerrorfield the line now carries in every mode; the detail is in the server log (issues #8899, #9475). -
PostgreSQL:
SERIALIZABLE,READ ONLYandAND CHAINinBEGIN/START TRANSACTION/COMMIT/ROLLBACKare refused with SQLSTATE0A000instead of a syntax error (issues #9631, #9753).
Action: do not retry a write the client reports as possibly applied without checking its outcome, and branch on the error field or the status instead of the message text.
26.10.1 — Sparse-vector top-K scores a window of documents at a time
vector.sparseNeighbors on an LSM_SPARSE_VECTOR index now scores documents in windows (setting arcadedb.sparseVectorScoringWindow, default 16384). On wide queries this is about twice as fast, and queries no longer wait for a COMPACT INDEX or a memtable flush to finish (issues #9200, #9209, #9210).
Impact: scores can differ from earlier versions in the last bit, so documents with exactly equal scores may rank in a different order.
Action: none. Set arcadedb.sparseVectorScoringWindow to 0 to go back to the previous traversal.
26.10.1 — The remote ArcadeGraph connects to the Gremlin port the server is bound to
The remote ArcadeGraph client used to connect to Gremlin Server on the fixed port 8182. It now uses the port the server advertises (GET /api/v1/server?mode=cluster, ports.gremlin), and falls back to 8182 when the server advertises none (issue #8578).
Impact: only a client that reaches Gremlin Server through a port mapping (for example a container or load balancer publishing 8182 to a different internal port), which worked by accident before.
Action: set the client setting arcadedb.gremlin.client.port to the port the client must connect to.
26.10.1 — Negative zero equals zero, and a few query results change
-0.0 is now equal to 0.0 everywhere (SQL and Cypher, =, IN, ordering, indexed or not), as IEEE 754 and openCypher define it.
Action: a HASH index over a FLOAT or DOUBLE property that already stores -0.0, or an LSM index that stored both -0.0 and 0.0, should be rebuilt once after upgrading:
REBUILD INDEX `Measure[value]`
Other results that change:
-
vector.neighbors,vector.sparseNeighborsanddb.index.vector.queryNodeson a parent type spec (Parent[prop]) now also search the records of its sub-types. Use the sub-type name in the spec to search only that sub-type. -
A
FLOATproperty is equal to theDOUBLEbound that narrows to it, so<,⇐,>and>=agree with=and with an index (v = 16777217.0finds16777216f, andv < 16777217.0no longer does). -
An integer bound above 2^53 on a
DOUBLEproperty, or above 2^24 on aFLOATproperty, is answered by a scan instead of the index, so the result is exact.
26.10.1 — HASH indexes on a DECIMAL property need one REBUILD INDEX after upgrading
A DECIMAL keeps the scale it was written with, so 5 and 5.00 are the same number stored two different ways. The LSM_TREE index family already treated them as one key; the HASH family did not, because it settles key identity on the serialized bytes and the two spell out differently. A UNIQUE_HASH index on a DECIMAL property therefore accepted both as separate rows, and a lookup for one spelling could not find a row stored under the other. Fixed in 26.10.1 (issue #7767).
Impact: any HASH/UNIQUE_HASH/NOTUNIQUE_HASH index on a DECIMAL property created before 26.10.1. LSM_TREE/UNIQUE/NOTUNIQUE indexes are unaffected.
Action: run REBUILD INDEX once, after upgrading, on every HASH index over a DECIMAL property:
REBUILD INDEX `Invoice[amount]`
Until it is rebuilt, the index file still holds the keys in their original form and lookups stay unreliable. If duplicates slipped in while a UNIQUE_HASH constraint was not being enforced, the rebuild reports them as a duplicated key — correct the data, then rebuild again.
26.10.1 — Case-insensitive HASH indexes need one REBUILD INDEX after upgrading
A HASH/UNIQUE_HASH/NOTUNIQUE_HASH index created with COLLATE CI did not actually fold case: an existing key and its differently-cased duplicate could both be inserted into a UNIQUE_HASH index, and a case-insensitive query answered through such an index could return fewer rows than the same query without an index. Fixed in 26.10.1 (issue #7766).
Impact: any HASH-family index with COLLATE CI created before 26.10.1. LSM_TREE/UNIQUE/NOTUNIQUE indexes with COLLATE CI already folded case correctly and are unaffected.
Action: run REBUILD INDEX once, after upgrading, on every case-insensitive HASH index:
REBUILD INDEX `Product[name]`
This rewrites the index with the keys folded correctly. See Case-insensitive indexes.
26.10.1 - Writes that used to store NULL or a coerced value now fail
A value that the declared property type cannot take is refused with an error naming the property, instead of being stored silently as NULL or coerced (issues #8090, #9110). This covers:
-
an unparsable datetime string written to a
DATETIME/DATEproperty (the SQL timestamp form2024-02-29 13:45:10.123456, which PostgreSQL clients send, is now parsed instead of being stored asNULL); -
a number that overflows a
LONG,FLOATorDOUBLEproperty, an empty string written to a number, a fraction written to aBOOLEAN, and wrong-typed values written toDECIMAL,DATETIME,BINARYorLINKproperties; -
a typed property set from a bare sub-query, for example
UPDATE Order SET id = (SELECT max(id) FROM Order), which used to storeNULLand now fails withCannot convert type ArrayList to INTEGER.
Schemaless properties are unaffected.
Action: fix the statements that now fail. For a sub-query, extract the scalar, for example SET id = (SELECT max(id) AS m FROM Order)[0].m.
26.10.1 - A write computed from a stale read is refused
A property write on a record that another transaction changed after the current transaction read it is refused with a retryable ConcurrentModificationException, instead of silently overwriting the concurrent change (issue #8610). SQL UPDATE and Cypher SET/MERGE/REMOVE are not affected, since they compute their values from the latest data. See Isolation.
Action: retry the transaction on ConcurrentModificationException (the database.transaction(…, retries) API does). Set arcadedb.txStaleReadCheck=false to restore the previous last-writer-wins behavior.
26.10.1 - TRUNCATE TYPE refuses a vertex or edge type that holds records
The refusal was documented but never fired, so TRUNCATE TYPE silently emptied live vertex and edge types (issue #8042).
Action: add UNSAFE where the truncation is intended. See TRUNCATE TYPE.
26.10.1 - Postgres wire: ROLLBACK TO SAVEPOINT is refused
It used to answer success while keeping every write made since the savepoint, so the next COMMIT persisted them. It now fails with SQLSTATE 0A000 and the transaction must be rolled back. See Savepoints.
26.10.1 - Stricter authorization for schema DDL and Gremlin scripting
-
ALTER TYPE(including the edge-typelightweightanduniquesettings) andREBUILD TYPErequire theupdateSchemapermission (security advisory GHSA-9c7g-grf7-j2r5). -
Gremlin capabilities that run code or touch files on the host are reserved to
root: thegroovyengine (and theautofallback to it) over HTTP, Gremlin Server scripts in a language other thangremlin-lang, bytecode that carries a lambda, and theio()step on every engine (security advisory GHSA-h2j4-28h8-cj5v). Plaingremlin-langtraversals are unchanged. Embedded use without a bound user is unaffected.
Action: grant updateSchema to the users that manage types, and run Groovy scripts, lambdas and io() as root or rewrite them as gremlin-lang traversals. See Gremlin security.
26.10.1 - API tokens minted over plain HTTP are logged
POST /api/v1/server/api-tokens over plain HTTP from a non-loopback peer still works, but logs a WARNING. The new gRPC CreateApiToken RPC always requires TLS or a loopback peer. With arcadedb.server.apiTokenRequireSecureTransport=true it is refused with 412, and the default of that setting is scheduled to change to true in 27.1.1.
Action: mint tokens over HTTPS, from localhost, or through a reverse proxy listed in arcadedb.server.apiTokenTrustedProxies. See Create an API token and, for the Docker first-run flow on http://localhost:2480, Docker.
26.10.1 - HA defaults that changed
| Setting | Before 26.10.1 | From 26.10.1 |
|---|---|---|
|
no gate |
With |
|
n/a |
|
|
n/a |
|
|
shared |
separate window, default 20000. A deployment that raised the old setting to delay the reformat of a diverged follower must raise this one. |
|
unbounded |
300000: an idle inbound Raft gRPC connection is closed with a GOAWAY and reopened on the next call. |
|
Ratis closed the Raft division after a 60 s JVM pause |
|
See HA cluster upgrade for the rolling-upgrade procedure.
26.9.1 — Sparse-vector indexes need one REBUILD INDEX after upgrading
A partial compaction of an LSM_SPARSE_VECTOR index could give the merged segment a position ahead of a segment it had not merged. When that skipped segment held the tombstone for a document an older merged segment still had a live posting for, the delete was overruled and the document reappeared in vector.sparseNeighbors results — permanently, because the next full compaction then discarded the tombstone as the loser. The same inversion could restore a stale weight after an update. Fixed in 26.9.1 (issue #6379).
Impact: LSM_SPARSE_VECTOR indexes on databases created before 26.9.1 that have seen deletes or updates. Upgrading corrects the ordering for every new merge, but a segment already written under the old rule cannot be repaired automatically — the record of where it should have sat is gone.
How to tell: the server reports it once per affected index, when the index is opened:
Sparse-vector index 'Doc[tokens,weights]' holds 3 segment(s) whose precedence predates the
recency-epoch fix (issue #6379); a document deleted before those segments were merged may still
be returned by queries. Run 'REBUILD INDEX Doc[tokens,weights]' once to rewrite them in the
correct order.
The flag behind that message is stored in the segment and inherited by any later merge, so ordinary write traffic compacting the affected segments away does not silence it. Nothing else distinguishes an affected index: queries succeed and the data looks intact.
Action: run once per sparse-vector index, after upgrading:
REBUILD INDEX `Doc[tokens,weights]`
This collapses the existing segments into a single correctly ordered segment. Indexes that have only ever been appended to are unaffected, as are databases created on 26.9.1 or later. See Sparse Vector Search.
HA rolling upgrade: the corrected ordering is recorded in the segment file, but reading it correctly requires the fix. During a staged rollout, a node still on the older build that receives a segment merged by an upgraded leader falls back to ordering by segment id and can still return a deleted document, until it is upgraded too. Upgrade every node before running the REBUILD INDEX above, and treat sparse-vector query results from a mixed-version cluster as provisional.
Single-server upgrade
-
Download and extract the new ArcadeDB release in a separate directory (do not overwrite the old install).
-
Stop the running server (or close every open database via the HTTP
close databasecommand). -
Copy the directories listed in What to copy from the old install into the new install, preserving the original paths.
-
Start the new server.
When the server starts, every database is opened with the new code and any required format migration is applied in place.
What to copy
ArcadeDB’s root directory contains a fixed set of folders. Only a few of them hold user-owned state; the rest are recreated from the release archive on every install.
| Folder | Action | Notes |
|---|---|---|
|
Copy |
The actual database data. Every subdirectory is one database. This is the only folder strictly required for an upgrade. |
|
Copy selected files |
The release ships a default |
|
Copy if you want history |
Backup archives produced by manual or automatic backups. The server does not need them to start; copy them only if you want to keep the backup history reachable from the Studio Backup tab and from |
|
Skip |
Server log files. Safe to start with an empty |
|
Copy |
Per-node Raft log segments, created at runtime when HA is enabled. This storage is durable by default ( |
|
Skip |
Legacy directory from the pre-Raft replication implementation. Not used by the current HA stack. |
|
Use the new release as-is |
Java artifacts and start scripts. Always take them from the new release — do not copy from the old install. The only exception is custom JARs you have dropped into |
Files to carry over from config/
The config/ directory ships with a small set of defaults.
You only need to copy the files that you have actually changed:
| File | When to copy |
|---|---|
|
Always, unless you create users only via REST/SQL on every upgrade. This is where ArcadeDB persists local users and their per-database group assignments. |
|
If you have customised the security policy (custom groups, type-level permissions, result-set limits). See Security Policy. |
|
If you have configured the auto-backup scheduler (schedule, retention, target directory). See Automatic Backup. |
|
If you have tuned |
|
Only if you use the embedded Gremlin Server and have edited any of these. |
Everything else under config/ (for example arcadedb-log-all.properties) ships unchanged with each release; take the new version.
ArcadeDB does not read a config/server-configuration.json file in normal operation.
Server-wide settings come from JVM system properties, environment variables and the arcadedb-server.sh arguments — see Server Configuration.
|
HA cluster upgrade
An HA cluster (see High Availability) is upgraded one node at a time. The per-node procedure is the same as single-server above, with the following clarifications.
What to migrate on each node:
-
databases/— copy, exactly as in the single-server case. -
config/— copy the same files as in the single-server case. The cluster’s identity is derived fromarcadedb.ha.clusterNameand therootpassword (the inter-node token is computed from them at startup), so as long as every node keeps the same cluster name and the same root password, the upgraded cluster reattaches to itself without any extra step. -
raft-storage-<peerId>/— copy. The local Raft log is durable by default (arcadedb.ha.raftPersistStorage=true), so preserve this directory across the upgrade to let the node rejoin by replaying its own log. Whether or not it is preserved, after the upgraded node rejoins the cluster the leader replays any missing entries via Raft or, if the node has fallen behind the purge boundary, streams a full snapshot over HTTP automatically. WithraftPersistStorage=falsethis directory is ephemeral and can be skipped. -
backups/andlog/— same as single-server.
Recommended rolling upgrade flow (zero downtime, requires quorum at all times):
-
Pick a non-leader node first.
-
Stop the node, install the new release in a separate directory, copy the folders listed above, start the new binary.
-
Wait until
GET /api/v1/clusterreports the node as healthy and caught up (replication lag near zero).-
The Studio Cluster tab shows the same information visually.
-
-
Repeat for every remaining replica.
-
Step the leader down with
POST /api/v1/cluster/stepdown(or simply stop it — Raft will elect a new leader) and upgrade it last.
|
From 26.10.1, a node that receives replicated TIMESERIES data for a type it does not have — or for a type that exists locally but is not a TIMESERIES one — refuses the entry, which quarantines that database and triggers an automatic resync from the leader. Earlier releases logged the problem and carried on, which left the node’s copy of that type permanently behind the leader’s with nothing but a log line to say so. This should not occur during an ordinary rolling upgrade, since the entry that creates a type is always applied before data for it arrives. It is a safeguard for the case where it does — for instance a mixed-version window in which one node’s build cannot construct a type another node has. If you see a database quarantined with a message about a TimeSeries sealed store, the resync will restore it; complete the rollout so every node runs the same build. |
For first-time HA bring-up with a pre-existing database (e.g. when you scale a single-node install into a cluster), see Offline Cluster Bootstrap.
Docker / Kubernetes
When you run ArcadeDB from the official container image, the release is replaced atomically by changing the image tag. The only data that survives across container restarts is what lives on a persistent volume, so:
-
Mount
/home/arcadedb/databaseson a persistent volume — this is the equivalent of "copy thedatabases/folder". -
Mount
/home/arcadedb/configonly if you have edited the files listed in Files to carry over fromconfig/; otherwise let the image ship its defaults. -
Set
arcadedb.ha.raftStorageDirectoryto a static path (e.g./home/arcadedb/raft-storage) and mount that path on a persistent volume. The default per-node directory is namedraft-storage-<nodeName>dynamically, which a container runtime or Kubernetes cannot mount as a volume (a wildcard like/home/arcadedb/raft-storage-*is created literally, not expanded) — this is exactly whatraftStorageDirectoryis for. The Raft log is durable by default (arcadedb.ha.raftPersistStorage=true) so a restarted pod can replay its own log instead of resyncing from peers. WithraftPersistStorage=false, the log is ephemeral and this mount is unnecessary. -
Mount
/home/arcadedb/backupsif you want to retain backup archives across pod restarts.
For Kubernetes specifics (StatefulSet, headless service, init container for pre-staging a database), see Kubernetes.
Downgrade ArcadeDB
In case you need to downgrade to an older version of ArcadeDB, check the binary compatibility between the versions.
ArcadeDB uses calendar versioning in the format YY.M.S (year, month, serial: for example 26.10.1 is the first release of October 2026), with 100% on-disk compatibility for migration of databases up or down between serial releases of the same year and month (the S in YY.M.S).
For downgrades across months the safest path is to export the database with the newer version and re-import it with the older version.
See Versioning, Compatibility and Deprecation Policy for the full compatibility rules.
Downgrading a node of an HA cluster
A cluster running v26.10.1 or later replicates schema changes in a compact format that earlier builds cannot read. Those entries stay in the Raft log, so a node that is restarted on an older build replays them and ends up with a schema that no longer matches the rest of the cluster - silently.
A node of a cluster that has run v26.10.1 or later must not be rolled back below it by restarting it on its existing Raft log.
If you have to go back that far, rebuild the node instead: stop it, delete its raft-storage-<peerId>/ directory and its databases/ folder, and start the older build so it rejoins empty and takes a fresh copy from the leader.
| This applies only to the Raft log, not to your data. Database files themselves keep the usual serial-release compatibility described above. |
Downgrading from 26.10.2 to 26.10.1 with hash indexes
Since v26.10.2 every hash index (UNIQUE_HASH or NOTUNIQUE_HASH) that is created or rebuilt is written at page layout version 3, which 26.10.1 does not know.
A 26.10.1 server refuses such an index with:
Hash index '<name>' has the page layout version 3, which this server does not support (it knows 1 and 2). It was created by a newer version: open the database with that version, or drop and recreate the index.
Hash indexes that 26.10.2 opened but did not rebuild keep their older layout and still open on 26.10.1. Before going back, drop the hash indexes created or rebuilt by 26.10.2 (the layout version is part of the index file name) and recreate them once the database runs on 26.10.1, or export the database and re-import it. Do not open a database holding a hash index created or rebuilt by 26.10.1 or later with a server older than 26.10.1, which does not check the layout version: see Hash index.