Repository navigation
Fixes for five bugs reported while building the Circaso backend (#187–#191), a migration-runner startup speed-up (#194), a soft-delete
encoding fix (#198), a migration-claim clock-skew fix (#203) and LWT-contention handling at startup (#205). Drop-in for 0.5.1 except for one behavior change to versioned update(), called out under Changed.
Fixed
-
@LookupIndexon a key column (#187, PR #195). A@LookupIndexon a@ClusteringKeyor@PartitionKeyproperty generated a lookup table
that declared the indexed column twice (Duplicate column 'request_id' declaration), and the lookup INSERT named it twice. The lookup table now holds
the indexed column once plus the other primary-key columns. Entities with a lookup on a regular column generate exactly the same DDL as before. -
Reserved words as column names (#188, PR #200). A column named after a reserved CQL word (
token,order,from,default, …) madeCREATE TABLE
fail withno viable alternative at input. Kandra now double-quotes those names, in lowercase, everywhere it generates CQL: DDL, INSERT/UPDATE/DELETE/SELECT,
lookup and index statements, and query-DSL predicates. The quoted set is measured: every keyword of the CQL grammar was tried as a bare column against
Cassandra 4.1 and ScyllaDB 2026.1, and only the ones that fail are quoted. Unreserved words such astimestamp,type,key,ttlordatestay bare,
so DDL (and the schema fingerprint) of every other entity is byte-for-byte unchanged. A reserved@ScyllaTablename or keyspace is rejected at registration
with a clear message. On 0.5.1, rename the column with@Column(name = "..."). -
Clearing a column in a batch or an update (#189, PR #196). New
saveWithNullsInBatch/saveWithNullsInBatchAndGetinbatch { }and
batchBlocking { }, andupdateWithNulls/updateWithNullsAndGeton both repository types. Anullproperty is written asNULL(a tombstone)
instead of being left UNSET, so a column can be cleared atomically with other writes or through a version-checked update. -
Migration checksum and source layout (#191, PR #193). A migration whose
up()calls an inline function (forEach,map,use, …) carries
Kotlin's inline source map (SMAP, with line numbers and the file's line count) in the@kotlin.jvm.internal.SourceDebugExtensionannotation's string
constants. The checksum kept those, so reformatting the file, or adding a line anywhere in it, failed an already-applied migration. The normalizer now
drops every string that annotation references. Upgrading is automatic: a row applied by 0.5.1 or earlier that matches the old checksum is accepted
and rewritten to the new one. If a migration's lines already moved before you upgrade, update its row once
(UPDATE kandra_migrations SET checksum = ? WHERE version = ?) after checking the CQL is unchanged. -
kandra-testand Testcontainers (#190, PR #192).org.testcontainers:cassandraandorg.testcontainers:testcontainersare nowapidependencies
ofkandra-test, soKandraTestcontainers.freshKeyspace()no longer fails withNoClassDefFoundErrorunless you add the Cassandra module by hand.
AverifyTestcontainersIsApitask incheckguards it. -
Soft delete encoding (#198, PR #201).
delete()on a@SoftDeleteentity bound raw property values, skippingKandraCodec, so an enum or custom-typed column
failed with a driver codec error. Values now go through the codec like every other write, and anullcolumn is left UNSET. -
Lost
markAppliedunder clock skew (#203, PR #204). The migration claim is anIF NOT EXISTSLWT (Paxos ballot timestamp), but
marking itAPPLIED, releasing it after a failedup(), and the #191 checksum upgrade were plain writes with the client's
timestamp. With the application clock behind the cluster's they were silently dropped: a migration that ran stayedCLAIMED, and
a failed one was never released. All three are now conditional (IF status = 'CLAIMED'/IF checksum = <old>). -
LWT timeouts under contention (#205). Several replicas starting together race the same
kandra_migrationsandkandra_ddl_locks
rows; the server times a Paxos round out rather than queueing, and the driver rethrows CAS timeouts.KandraMigrationRunner.claim()
propagated that and crashed the replica at startup. Every CAS write (claim, mark applied, lock claim/done/release, inkandra-migrateand
kandra-ktor) now re-reads its row on a timeout and continues from what it finds, retrying with backoff when the write did not land. -
Connection-init timeout (#205). The driver's 5 s timeout for connection-setup queries (
system.peers, schema refresh,USE) was not
reached bypool { requestTimeoutMillis }, so a loaded or distant cluster failedinstall(Kandra)with
query 'SELECT * FROM system.peers' timed out after PT5S. It now follows the request timeout, never below the driver default.
Changed
- Versioned
update()leaves anullcolumn UNSET (#189). On a@Versionentity,update()/updateAndGet()used to bindNULLfor every null
non-key column, writing a tombstone per null column on every update — unlikesave()and a plainupdate(), which leave it UNSET. It now leaves it
UNSET too. If you relied on a versionedupdate()to clear a column, callupdateWithNulls()instead.
Performance
- Migration runner startup (#194, PR #197). Constructing a
KandraMigrationRunneragainst an already-bootstrapped keyspace now does one plain read
instead ofCREATE TABLE IF NOT EXISTSplus a losingIF NOT EXISTSLWT (Paxos). The migrations jar is indexed once per JVM instead of scanned in full
once per migration, each checksum is computed once perrun(), and the runner prepares its statements once. Stored checksums are unchanged. - Further runtime findings from the same review are tracked in #199.
Maven Central: com.kandraorm.kandra:*:0.5.2 (BOM: com.kandraorm.kandra:kandra-bom:0.5.2). Full changelog: docs/changelog/0.5.2.md. Verified on Cassandra 4.1 (Docker), ScyllaDB 2026.1 (Docker) and a 3-node ScyllaDB Cloud cluster.