Skip to content

Breaking changes ​

This page lists each change that makes correct code or stored data stop working: a node that no longer starts, a statement that now fails, or a client and a server that no longer fit together. Read it before you update kinedb. Each entry tells what breaks and what you must do.

A new feature, a new field, a new log line, a changed error text and a fix of a defect are not breaking changes, and they are not on this page.

kinedb has no release versions yet. Each entry has the date of the change and the issue id (B-id). The page lists the breaking changes since the container image of 2026-10-17 (build 12f6333). The newest entry is first.

kinedb does not keep the old behaviour next to the new behaviour. Update the server and every client together.

2026-10-29 — B3842: rights on users, credentials and passwords ​

State: Planned.

What breaks:

  • ALTER USER u ADD PUBLIC KEY, CREATE API SECRET, ADD API SECRET HASH, DROP CREDENTIAL, ADD FEDERATION PROVIDER, DROP FEDERATION PROVIDER and SHOW CREDENTIALS FOR u now need CONTROL on u, not the superuser right. A caller that cannot see u gets user '<u>' not found. A caller that sees u with no CONTROL gets permission denied: CONTROL on user '<u>' is needed for user '<caller>'. Before, both got permission denied: … requires superuser for user '<u>'.
  • A new user gets a stored CONTROL rule on himself, so he adds and drops his own keys and API secrets. SHOW GRANTS lists the rule as a row of kind user, for example ["alice", "control", "USER alice", null, "user"]. Revoke it with REVOKE CONTROL ON USER u FROM USER u.
  • One credential belongs to one user. The same public key or API secret on a second user is refused with credential '<id>' belongs to another user. Before, both users held it, and each logged in with it. A store of before the update keeps its shared credentials, and the start logs their count ([B3842_SHARED_CREDENTIALS] n).
  • SHOW USERS has two new last columns: home (default for a user with no home), then rights_version (0 for a user that the upgrade of B4032 did not reach yet, else 1).
  • GRANT or REVOKE on a user with rights_version 0 is refused with the upgrade of user <u> is not applied yet; run the statement again. Run it again after the upgrade.
  • ALTER USER u SET PASSWORD … of a user that is not root on another user now needs a password right on u (SET PASSWORD or RESET PASSWORD). With no right the answer is user '<u>' not found or permission denied: SET PASSWORD or RESET PASSWORD on user '<u>' is needed for user '<caller>', not permission denied: this statement requires superuser. A holder of RESET PASSWORD only must give REPLACE '<old>'. KEEP SESSIONS on another user needs a member of global_admins. Only a member of global_admins changes the password of a member of global_admins.
  • GRANT|REVOKE SET PASSWORD|RESET PASSWORD ON USERS IN DATABASE d is now accepted and stored. Before, the server refused it with is not supported by this build yet.
  • The run-as rule. A RUN AS user of a function or a ticker must have the home default and must not be a member of global_admins. So RUN AS 'root' is refused, also for root, with a function may not run as 'root': its home is not 'default' or it is a global admin. A stored function RUN AS 'root' stops: each CALL fails with function '<f>' may not run as 'root': …. The same applies to a function whose run-as user moves to another home or becomes a global admin, and to a conflict function.
  • The creator of a function with RUN AS u needs CONTROL on u. Before, a user that was not root could name only himself. A user made by this build holds CONTROL on himself; a user made before it gets the rule with the upgrade of B4032.
  • A ticker with no RUN AS runs as its creator, who must be eligible. So CREATE TICKER of root or of a global admin with no RUN AS is refused, and a stored ticker of root stops: the server skips its ticks and writes one warning. CREATE TICKER … RUN AS u is now accepted. Before, the server refused it with is not supported by this build yet.
  • SHOW ROLES has two new last columns, built_in and admin_of. built_in is true for global_admins and for each admins_<db> that CREATE DATABASE made; admin_of holds the database of such a role, else NULL. A role with such a name that a user made before this release shows built_in = false.
  • SHOW GRANTS with no ON has a new row kind users: one row for each rule on the users of a home (ON USERS IN DATABASE d), with the database in collection. These rows come last.
  • kd-tool backup writes the home of each user, the rules on users, a revoked self rule and the rules on the users of a home. A kd-tool before this release refuses such a dump, and a restore of such a dump into a server before this release is refused before any change. A restore never creates a database: a home that the target does not hold refuses the restore. A dump of an older kd-tool that grants through an admins_… role is refused, because it does not tell a built-in role from a plain role.

What you do: Give CONTROL ON USER u to the users that manage the credentials of u, and SET PASSWORD or RESET PASSWORD ON USER u (or ON USERS IN DATABASE d) to the users that manage passwords. A member of global_admins keeps every right. Change a client that reads an error text of these statements, or that reads SHOW USERS or SHOW ROLES by the column count, or that reads every row of SHOW GRANTS as a collection or a function rule. Drop a shared credential from all users but one. Replace each function RUN AS 'root' and each ticker of root or of a global admin: name a service user with the home default in RUN AS, give that user only the rights of the body, and give the creator CONTROL on that user. Back up with a kd-tool of this release, and run CREATE DATABASE on the target for each home before a restore. See the password of another user, who may call a function and users, roles and grants.

2026-10-29 — B4032: old users hold their password options, and two settings have new names ​

State: Planned.

What breaks: After the update, the metadata leader upgrades each user that an older version created. The user gets CONTROL on himself. Each password option that the user had no value for (CHANGE_OWN_PASSWORD, SET_OWN_PASSWORD) now holds the cluster value of the time of the upgrade. So a later SET CLUSTER of the two settings no longer changes the password rights of an upgraded user. SHOW CREATE USER and a dump of kd-tool backup show the stored values. The settings cluster.auth.change_own_password and cluster.auth.set_own_password are now cluster.auth.default_reset_password and cluster.auth.default_set_password. The old names work for one release. A node of an older build stops on the upgrade entry, so the leader upgrades the users only when every voter of the metadata group runs the new build. A role global_admins that a user created with an older version gets the name global_admins_old (or global_admins_old_2 and so on). It keeps its members, grants and rules. A statement or a script that names global_admins then names the built-in admin role. A role mapping of an identity provider to the old role grants the renamed role, and a kd-tool backup dump of the renamed role restores with its principal id. An audit row of a grant to the renamed role has no grantee and no rights. It keeps the action, the object, the actor and the time.

A login with MAP ROLES BY NAME ON never grants a built-in role. A group with the name global_admins or admins_<db> gives no role. Before this change, such a group granted the built-in role.

What you do: Set the two settings to the values that you want for the old users BEFORE the update. After the update, change the password rights of a user with ALTER USER <name> SET CHANGE_OWN_PASSWORD | SET_OWN_PASSWORD = ON | OFF | DEFAULT. Use the new setting names in your scripts. If you created a role global_admins, change each script to the new name. Stop all nodes, install the new build on each node, and start all nodes.

2026-10-28 — B3840: a branch is hidden from a user with no right on it ​

State: Planned.

What breaks: A branch now holds its own grants. A user that is not root sees a branch only when the user or one of its roles holds a right on it. A branch made before this change has no grant. After the update, SHOW BRANCHES does not list it for a user that is not root. USE BRANCH, a read, a write and a WATCH on it fail with branch not found: <name>.

What you do: Give the rights on each old branch as root: GRANT <rights> ON BRANCH <name> TO <user | ROLE r>. To let users create branches, give GRANT CREATE BRANCH [ON DATABASE <db>] TO <user | ROLE r> WITH <rights>.

2026-10-28 — B3838: CREATE FUNCTION needs a CREATE FUNCTION grant ​

State: In main.

What breaks: A logged-in user that is not root can no longer run CREATE FUNCTION … RUN AS '<self>' with no other right. The statement now needs CREATE FUNCTION, which no user has by default, and it fails with permission denied. CREATE OR REPLACE FUNCTION of a new name needs CREATE FUNCTION too. A GRANT or REVOKE of CONTROL on a function now needs CONTROL on it: the run-as user of the function can no longer give it.

What you do: Give the right to the users that create functions: GRANT CREATE FUNCTION TO ROLE <role>. See Who may create.

2026-10-27 — B3577: a file write carries a field map, and a row permission judges the row it leaves ​

State: In main.

What breaks:

  • The Raft log gets a new entry kind (op tag 17, a file write with a check of the row before it). A node of an older build cannot read it: it drops the replication message that holds it, and it stops with an error when it replays its log.
  • A client of this build that sends a field map (fields of put_file and putFile) to a server of an older build: the older server ignores the map and writes the file with no fields. It gives no error.
  • A file write of a user with only a MATCH or WHERE permission now fails with a conflict when another write changes the row between the check and the write. Before, such a user could not write a file at all.

What you do: Stop every node of the cluster, update every node, then start them again. Do not let nodes of the old and the new build run together. Update the servers before a client sends a field map. A client that gets the conflict of a file write sends the write again.

2026-10-26 — B3817: a history read refuses inside a period where the node did not hold the data ​

State: In main.

What breaks: A history read (AS OF, SHOW DIFF, CREATE BRANCH … AS OF, a WATCH … SINCE resume) refuses more targets:

  • On a node that the cluster removed from the voters of a shard, or that a split, a merge or a repair left with no copy of a shard: a target at or after the commit of that stop refuses with NotFound, while the node does not hold the data. A WATCH … SINCE resume from such a token gives a full fill.
  • On a node that the cluster adds to the voters of a shard again: a target before the add refuses. Before, the read answered the stale rows of the period with no error.
  • An upgraded server node does not start: a data directory of an older build (commits and no HISTORY_GAP file) refuses with exit code 3 until the operator sets KINEDB_SET_HISTORY_FLOOR_AT_HEAD=1. With the variable, the node sets a floor of every table at the commit before HEAD and starts; then a target older than HEAD refuses on that node.

What you do: Start each server node once with KINEDB_SET_HISTORY_FLOOR_AT_HEAD=1 on its first start on this build, then remove the variable. See Troubleshooting. Run a history read of an older target on a node that held the table then. After a downgrade to an older build and an upgrade again, delete the file HISTORY_GAP of the data directory and start once with the variable. See Time travel and history.

2026-10-23 — B3584: a Windows UNC, root-relative or drive-relative wasm path opens the Windows file ​

State: In main.

What breaks: The wasm option of the embedded KineDB constructor now reads three Windows path forms as Windows does. Each one opened another file before, or failed.

  • A UNC path \\server\share\k.wasm opens file://server/share/k.wasm. Before, it was a path under the working directory. A long path \\?\C:\… or \\?\UNC\… loses its prefix. Under a Windows working directory, //server/share/k.wasm is a UNC path too.
  • Under the Windows working directory C:\w, a root-relative path \k.wasm opens C:\k.wasm. Before, it opened C:\w\k.wasm. A path that starts with "/", for example /a/k.wasm, was a URL with no drive before, and now opens C:\a\k.wasm.
  • Under a Windows working directory, a drive-relative path C:k.wasm opens k.wasm in the working directory of drive C. Before, it was a URL with the scheme c: and went to fetch. Under a POSIX working directory, C:k.wasm is now the file C:k.wasm in that directory.
  • A Windows path (a drive path, a UNC path, or a path under a Windows working directory) loses each empty segment and each ".", and ".." stops at the drive or at the share. Before, the URL kept an empty segment, and ".." could leave the share.

A POSIX path under a POSIX working directory, a drive path with no empty segment, ".." or ".", and a URL with a scheme of two letters or more do not change. In a browser, which has no working directory, a relative path does not change.

What you do: If you used \k.wasm for a file in the working directory, write k.wasm or .\k.wasm.

2026-10-23 — B3507: a diverged sync merges from the lowest common ancestor, or refuses a criss-cross ​

State: In main.

What breaks: The Rust storage call sync_remote_commit merges two diverged commits from their common ancestor. Before, merge::find_common_ancestor returned the first common ancestor that its walk met. After a merge commit, that ancestor could be older than the lowest one, and a row that one side deleted came back. The answer also changed with the order of the parents of the merge commit. Now the call returns the lowest common ancestor. When the two commits have more than one lowest common ancestor (a criss-cross history), the sync now fails with a Sync error before any write. The HEAD and the rows do not change. merge::find_common_ancestor returns the same error. See the troubleshooting section.

What you do: Sync from a copy that holds one common base, or merge main in first. Then run the sync again.

2026-10-23 — B3584: a wasm path keeps "%", "#" and "?" as a part of the file name ​

State: In main.

What breaks: The wasm option of the embedded KineDB constructor takes a path, a URL, bytes or a module. Before, a POSIX or relative path was read as the text of a file: URL. A "%" in the path then started an escape, so a%20b.wasm opened the file a b.wasm. A "#" or a "?" cut the path. Now a POSIX or relative path, and the working directory of a relative path, are file names: a%20b.wasm opens the file a%20b.wasm, and "#" and "?" are a part of the name. A Windows drive path does this since B3528. A URL with a scheme does not change.

What you do: If your path holds a URL escape that you want decoded, pass a file: URL, for example new URL('a%20b.wasm', import.meta.url) or 'file:///opt/k/a%20b.wasm'. Otherwise, pass the real file name.

2026-10-23 — B3555: an AS OF read never writes a commit ​

State: In main.

What breaks: Before, an AS OF read flushed the write buffer, and that flush wrote a commit. Now an AS OF read never writes a commit: the buffered writes of the branch are one pending step on top of the newest commit. So HEAD~N can name another state when an earlier AS OF read gave a buffered write its own commit. An example: INSERT, then SELECT … AS OF HEAD, then UPDATE, then SELECT … AS OF HEAD~1. Before, HEAD~1 answered the rows after the INSERT. Now the INSERT and the UPDATE are one pending step, and HEAD~1 answers the rows before the INSERT. A single AS OF read answers the same rows as before. SHOW COMMITS lists one commit less after such a read.

What you do: Do not use an AS OF read to mark a point in the history. To read the state after a statement later, read SHOW COMMITS after the node flushed, and use AS OF '<hash>'. See Time travel and history.

2026-10-22 — B3482: a diverged sync merges from the true base, or refuses ​

State: In main.

What breaks: The Rust storage call sync_remote_commit merges two diverged commits from their common ancestor. Before, the walk to that ancestor stopped after 200 commits, and it stopped at a commit that the store does not hold. The sync then merged from an empty ancestor, and a row that one side deleted came back. Now the walk has no depth limit, so a history of more than 200 commits merges from the true ancestor. A commit that the store does not hold and that is older than the common ancestor (for example, after a vacuum) does not change the merge. When an absent commit is the common ancestor or can hide it, the sync now fails with a Sync error that names the full hash of that commit. The HEAD and the rows do not change. merge::is_ancestor and merge::find_common_ancestor also have no depth limit now.

What you do: Send the missing commits to the store, or rebuild this copy from the remote copy. Then run the sync again.

2026-10-22 — B3466: the S3 HEAD recovery refuses a damaged record or segment ​

State: In main.

What breaks: With --s3, a node whose S3 HEAD names a lost block scans the local segment files for the newest commit. When the scan cannot read an item (a record that fails its CRC32C check, a record whose bytes do not hash to its stored hash, a record whose LZ4 or zstd data does not decode, or that names a dictionary that the store does not hold, the record of the block that the S3 HEAD names in a torn tail, or a file that is not a segment), the node now does not start: it exits with code 3 and a NeedsOperator error that starts with B3466: and names each item. This is true with and without KINEDB_RECOVER_S3_HEAD_FROM_SEGMENTS=1. A block that another record holds in a good copy is not an item; a record whose content does not hash to its stored hash is always an item. Before, the scan skipped each such item with no log line. When the item held the newest commit, the node opened on its parent with the variable, and the commits after the parent were lost.

What you do: Restore each named segment object from a backup or a bucket version, delete the local cache directory, and start again. To accept the loss, start once with KINEDB_OPEN_WITH_DAMAGED_BLOCKS=1. See Troubleshooting.

2026-10-22 — B3506: the pack request names its server database (PackDb, 0x2F) ​

State: In main.

What breaks: The JS embedded SDK and the Rust sync client now ask for the pack with the new request PackDb (0x2F), for every server database, also default. A server of the first pack version (B3431, before B3506) does not know 0x2F. It answers "unknown msg_type: 0x2F", and the client then fills the table in pages. So a new client on such a server gets no pack, also for default, and its first fill is slower. The rows are correct. An old client on a new server still gets its pack (0x2A).

What you do: Update the server to get the pack again. See the first fill of an empty database.

2026-10-22 — B3398: a lone voter with a lost metadata Raft log does not start ​

State: In main.

What breaks: With --calvin, a node that is the only voter of the metadata group (for example, a 1-node cluster) now writes <data-dir>/shard_configs/1. When that node loses its metadata Raft log (<data-dir>/raft_wal/1), it does not start: it exits with code 3, and the boot error names shard 1 (metadata). Before, the node started a new metadata log at index 0, and only a catalog stamp check stopped some of these starts. KINEDB_REJOIN_PASSIVE_AFTER_LOST_RAFT_WAL=1 no longer starts such a node: no other member holds the log, so a passive group of one voter never elects.

An existing store of an older version whose metadata log names this node the only voter (for example, an existing 1-node --calvin store) has no shard_configs/1. Its first start on the new version refuses with exit 3 and an error that starts with B3398:. The refusal changes nothing on disk.

What you do: For the first start of such a store on the new version, set KINEDB_RECORD_LONE_METADATA_VOTER=1 once, for example in the environment of the service for that one start. The node writes shard_configs/1 and starts. Then unset the variable. See Troubleshooting.

When the node lost its metadata Raft log: restore the raft_wal directory (or set KINEDB_RAFT_WAL_DIR to its place), or restore a backup of the data directory. See Troubleshooting.

2026-10-22 — B3386: a refused metadata point stops the node with exit code 3 ​

State: In main.

What breaks: A --calvin node that fell behind the metadata log installs a metadata point from a peer. The node now logs [B3386_METADATA_POINT_NEEDS_OPERATOR] with the refusal text and exits with code 3 (needs an operator) in two cases only. These cases depend on the local state of the node, so a point of another node and a retry do not change them:

  • [B2985_POINT_LOCAL_TABLE_REFUSED]: the node holds a user table that the point does not name.
  • [B2318_METADATA_POINT_OLDER_THAN_LOCAL] the record of the installed point has <n> bytes, not 8: the record of the last installed point does not decode.

Before, the node kept running behind its peers and repeated the install once for each dwell, with a [B2318_METADATA_POINT_RETRY] line. Every other refusal still repeats in this way: a refusal that compares with the point of one peer (for example [B2972_POINT_SUPERSEDE_REFUSED], [B2992_POINT_SKELETON_BASE_MISSING], [B2318_METADATA_POINT_OTHER_MERGE]) and a read error of the record.

What you do: Read the refusal text in the log. It names the fix. See Troubleshooting and Troubleshooting. A unit file must not restart the node in a loop: set RestartPreventExitStatus=3.

2026-10-22 — B3392: a shard log can hold a heal entry that an older node does not read ​

State: In main.

What breaks: A divergent shard whose minority holds a newer write now heals itself: the leader of the shard writes a heal entry (Raft WAL tag 64, and the peer message 0xE6) into the shard log. A node of a kinedb version before B3392 does not read that entry. From the wire it logs [RAFT_MSG_UNREADABLE] and stops on that shard. From its own Raft WAL it does not start ("written by a NEWER build"). So a cluster that mixes the two versions can not heal such a shard, and the older node stops on it. The heal carries rows only: a table with an index, or a shard whose file tree or idempotency tree differs between the minority and the majority, keeps the refusal. A minority version with a ts above the applied index of the minority (AbovePoint) also keeps the refusal.

What you do: Run one kinedb version on every node of a cluster. To update, stop every node and start all of them on the new version. See Troubleshooting.

2026-10-22 — B3464: a sync refuses an ancestor commit that does not decode ​

State: In main.

What breaks: The Rust storage call sync_remote_commit (and merge::is_ancestor and merge::find_common_ancestor) walks the parents of the two commits. When the walk meets a commit block that the store holds but that does not decode, the call now fails with a Corruption error. The text of the error names the walk and the full hash of the block. A read error of the store in merge::find_common_ancestor now also fails the call; before, the walk skipped that block. The HEAD and the rows do not change. Before, the walk stopped at that block with no log line. A sync could then merge the two sides from an empty ancestor, and a row that the remote side deleted came back.

What you do: Find the block from the hash in the error. Update kinedb to the version that wrote that commit, or copy the commit again from a node that holds a good copy. Then run the sync again.

2026-10-22 — B3428: SHOW INDEXES shows WRITE-ONLY, not BUILDING ​

State: In main.

What breaks: The status column of SHOW INDEXES shows WRITE-ONLY for an index that the planner does not use yet. Before, it showed BUILDING. The new word is also true for a build that stopped. The fix column of such an index now names the repeat of its CREATE INDEX and the DROP INDEX.

What you do: A script or a monitor that compares status with BUILDING must compare it with WRITE-ONLY. See Indexes.

2026-10-22 — B3379: the S3 HEAD recovery refuses a newest commit that does not decode ​

State: In main.

What breaks: With --s3 and KINEDB_RECOVER_S3_HEAD_FROM_SEGMENTS=1, a node whose S3 HEAD names a lost block elects the newest commit that the segments hold. When that commit does not decode, the node now does not start: it exits with code 3 and a NeedsOperator error that starts with B1575: and names up to 8 commit blocks that do not decode. Before, the node opened on the parent of that commit, and the commits after the parent were lost. It printed the B1575: warning, but no line named the commit that does not decode. Without the variable, the B1575: refusal now also names these blocks, and it does not offer the variable.

What you do: Restore the missing segment object, or start the kinedb version that wrote the newest commit. See Troubleshooting.

2026-10-22 — B3427: a kd break outside a loop in a function is an error ​

State: In main.

What breaks: In a kd function body, a break or a continue that is not inside a loop of that function now fails with the Query error "break outside a loop" (or "continue outside a loop"). Before, it left the function and stopped (or advanced) the loop of the caller.

What you do: Put the break or the continue in the loop that it controls. To stop a loop of the caller, return a value from the function and test it in the loop. See kd.

2026-10-21 — B1810: one node owns one --s3 prefix ​

State: In main.

What breaks: A node with --s3 writes an owner marker PREFIX/OWNER and keeps its id in the file S3_OWNER of its data directory. A second node on a prefix that a node owns does not start: it exits with code 3 and a B1810: error. An empty data directory is a new node, so a restore from the bucket into a new directory refuses too. Before, both nodes started and wrote over each other's objects. A prefix that an older kinedb wrote has no marker: the first node that starts on it takes it, and a second node refuses.

What you do: Give each node its own PREFIX in --s3. To restore a lost node into a new directory, make sure that the old node does not run, and start the new node once with KINEDB_S3_TAKE_OVER_PREFIX=1. See Troubleshooting.

2026-10-19 — B3033: the embedded sync asks a window of fill pages ​

State: In main.

What breaks: The sync of @kinedb/embedded (JavaScript and Python) now writes WATCH … SINCE … WINDOW 4. A server from before B3026 refuses that clause with a parse error. The sync then falls back to window 1 for the session and logs one warning: "the server refused WINDOW … the fill falls back to window 1". JavaScript writes it with console.warn, and Python writes it to stderr. The fill completes, but each page costs one round trip, so a fill over a slow link is much slower. The wire does not change: the clause and the ack slot of WatchFillNext come from B3026.

What you do: Update the server before the embedded clients. See Troubleshooting.

2026-10-19 — B3064: a synced write refuses above 100,000 op-log entries or with a very long key ​

State: In main.

What breaks: The local database of an embedded sync (the JavaScript, Python and wasm clients) gets the index kinedb_sync_oplog_by_key on its op-log, the log of the writes that are not pushed. The open and each synced write build it automatically, but only while the op-log holds at most 100,000 entries. This number counts every entry of the op-log: the writes that wait and the writes that the server refused. A database that an older build left with more entries gets no index, and each synced write to it is refused with an error that starts with B3064: the op-log of this database has no usable index. Nothing is applied.

What you do: Push each synced table. The push does not need the index. One push does not always send every waiting write: a write that waits behind a sent write of the same row goes in the next push. So push again until the report shows sent: 0. A push does not remove a refused write. When the op-log still holds more than 100,000 entries, read refused() and remove the refused writes with discardRefused(). The next synced write then builds the index. See Troubleshooting.

Since B3084 (2026-10-19): the 100,000-entry bound is gone. The open builds the index for an op-log of any size, in parts of at most 192 KiB, so a synced write is no longer refused for the size of the op-log. Update the client; you do not need to push first.

A new refusal of a long key: With the index present, a synced write whose index key is longer than 65,535 bytes is refused. The index key is the table name plus the base64 text of the primary key, so a primary key of about 49 KB or more is refused. The error contains is longer than MAX_KEY_LEN (65535 bytes). Nothing is applied. A build before B3064 accepted such a write. An older build after a downgrade also refuses it (INFERRED: the index stays in the schema, and the engine maintains every index of a table). See Troubleshooting.

2026-10-19 — B3028: the local sync database stores its row versions in a new table ​

State: In main.

What breaks: The local database of an embedded sync (the JavaScript, Python and wasm clients) keeps the server version of each cached row in a shadow table. B3028 renames this table from kinedb_sync_row_version to kinedb_sync_row_version_v2 and gives it a new key shape: the key now sorts in the primary key order of the synced table. The first open with the new build rewrites the old table. The rewrite is automatic, runs in chunks of at most 131,072 rows and 32 MiB of key bytes, and drops the old table last. Native Rust and Python log one [B3028_VERSION_REWRITE] line for each chunk on stderr. The wasm32 build compiles out this line, so a browser logs nothing (B3067). A crash during the rewrite is safe: the next open starts it again.

The time and the memory: The rewrite of 1M version rows with short keys took about 14.2 s in Bun 1.4.2 on node:fs. The peak RSS was 464 MB (157 MB at the start of the open). The native rewrite on a file store peaks at 148 MB and takes 9.7 s. The time and the memory of the rewrite on OPFS (a browser) are NOT DETERMINED.

An older build: A DOWNGRADE after the rewrite is not supported. Since B3070 the new build keeps an empty placeholder table under the old name kinedb_sync_row_version. Its one column is b3028_moved_to_kinedb_sync_row_version_v2. A build before B3028 that opens such a database does not fail at the open. It fails LOUDLY at its first fill or its first synced write, with an error such as Query: unknown column in WHERE: row_key. It never reads version 0 silently. The test of this path is a SIMULATION: it runs the statements of the old build on the engine of this build. No test uses a real older binary, so the exact text of an older engine is NOT DETERMINED. See Troubleshooting.

What you do: Update the clients as usual. After a downgrade, use a build with B3028 or newer, or delete the local database and sync again. A write that was not pushed is lost with the local database, so push first. An old key that does not decode stops the open with an error that starts with NeedsOperator: B3028: . The repair is the same: delete the local database and sync again. A synced row whose primary key makes a version key of more than 65,535 bytes (about a 48 KiB key) is refused. A synced write in a transaction that you opened is refused while the old table exists. Run it with the Rust method transaction_local, which rewrites first. A JavaScript app opens the sync instead, and the open rewrites.

2026-10-19 — B2995: an S3 node refuses --gc-retention ​

State: In main.

What breaks: A node with --s3 and --gc-retention (or KINEDB_GC_RETENTION) does not start. It exits with code 1 before it opens the data directory. Before, the node started and reclaimed nothing: an S3 node runs no garbage collection.

What you do: Remove --gc-retention and KINEDB_GC_RETENTION from the start of each S3 node. Watch the size of its local directory and of its S3 prefix: both grow with the number of commits. See S3 storage.

2026-10-18 — B3026: a rights change between two pages of a fill ends the fill ​

State: In main.

What breaks: A change of your rights between two pages of a WATCH … SINCE fill now ends the fill with ONE failure frame (retry: true). This holds at every window, window 1 included, and for every client. Before, the fill went on with an empty rights stamp. A tail diff keeps its behaviour. A new server also reads the clause WINDOW <n>, and the JavaScript client writes WINDOW 16 by default when watch() has since. A server from before this change refuses that clause with a parse error.

What you do: When a fill ends with retry: true, subscribe again with the token of the last page that you stored. Update the server before the JavaScript clients. For a server from before this change, pass window: 1 to watch(): it writes no clause.

2026-10-18 — B2905: a read refuses a literal that cannot compare with the column ​

State: In main.

What breaks: A WHERE on a table now refuses a literal of a type that cannot compare with the type of the column. Before, such a comparison was false for every row and the read answered an empty result or 0, with no error. An example is WHERE ts >= '2026-10-16' on a TIMESTAMP column. The statement now fails with an error that starts with column 'ts' has type TIMESTAMP; the literal '2026-10-16' (TEXT) cannot be compared. UPDATE and DELETE with such a WHERE fail in the same way.

What you do: Write the literal in the type of the column. For a TIMESTAMP, use an integer of epoch microseconds. Check each statement that compares a text with a number or a time column: it did not fail before, and it did not match a row.

2026-10-18 — B2929: each CREATE ROLE gives the role a new principal id ​

State: In main.

What breaks: A role that this version creates has the principal id role#<16 hex digits>:<name>, not role_<name>. A MATCH row that holds role_<name> grants nothing to such a role. A role that an older version created keeps role_<name>, and its rows keep working. A dump from an older kd-tool restores each role with a new id, so its MATCH rows that name a role grant nothing. A node of an older version does not read the new log entry of CREATE ROLE (U117).

What you do: Read the id of each role in the column principal of SHOW ROLES. Write that id into a MATCH row that must grant to the role. Make a new dump with the new kd-tool: it keeps the id of each role, and its restore creates each role with that id. Upgrade all the nodes together.

2026-10-18 — B2929: DROP ROLE refuses a role that a rule still names ​

State: In main.

What breaks: DROP ROLE r worked before while a table rule or a function grant (GRANT INVOKE) named ROLE r. It now fails, and the error names each table and each function: role 'r' cannot be dropped: rules still name it: table orders, function f; revoke them first (B2929). The old behaviour kept the rules, so a later CREATE ROLE r got them back. A GRANT … TO ROLE r that the cluster orders after the DROP ROLE r is also refused, with role 'r' not found.

What you do: Before DROP ROLE r, revoke each rule that the error names: REVOKE … ON <table> FROM ROLE r and REVOKE INVOKE ON FUNCTION <f> FROM ROLE r. Then drop the role. Upgrade all the nodes together: a node of an older version does not read the new log entry of DROP ROLE (U116).

2026-10-18 — B2931: a node refuses a data shard that an older version grew outside its Raft log ​

State: In main.

What breaks: A --calvin node does not start on a data directory of a version before B2931 when a data shard has a Raft log on the node. That version added the nodes to a data shard outside the Raft log of the shard, so the voter record on a node can be missing or old. The node exits with code 3 and ONE error that starts with B2931: and names each shard.

What you do: Stop every node of the cluster first: an older node still changes the voters outside the log. Run SHOW RAFTGROUP <shard id> before the stop. Start every node once without the variable and compare the voter sets that the errors print. Then start every node once with KINEDB_ALLOW_RFN_MIRROR_MEMBERSHIP=1. When the error says high-water <N> > RF <RF>, an older node no longer changes the voters outside the log, so you need not stop every node: use a rolling upgrade that keeps a quorum of each shard up, or the voters file of KINEDB_RFN_MIRROR_VOTERS_FILE (B2953). The steps are in Troubleshooting.

2026-10-17 — B2888: a --calvin node refuses Raft timings that break the lease bound ​

State: In main.

What breaks: A --calvin node now checks KINEDB_RAFT_ELECTION_US and KINEDB_RAFT_HEARTBEAT_US before the store opens. Before, it read them with no check, and a value that did not parse fell back to the default in silence. The node now exits with code 3 and ONE error that starts with B2888: when a value is not a whole number of microseconds, when the heartbeat is 0, or when the election timeout is too short for the read lease. There is no variable to skip the check.

What you do: Set both variables to whole numbers. If the error names a bound, raise KINEDB_RAFT_ELECTION_US to the smallest value that the error prints, or lower the heartbeat. Or remove both variables to use the defaults.