Repository navigation
Releases: Dev-Pasaka/kandra
Release list
Kandra 0.5.2
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.
v0.4.7
[0.4.7] — 2026-10-12
No version was cut between 0.4.6 (2026-07-23) and this release — everything below merged to main
across that gap in 66 PRs (GH #5–#151) with no corresponding gradle.properties bump, so this entry
consolidates all of it. The headline work: three adversarial review rounds (2026-09-08 pre-multi-DC
review, 2026-09-09 post-fix critical audit, 2026-10-11 docs reality-check follow-up) each
specifically re-checked the fixes from the round before, which is how several of the "Fixed" items
below are themselves fixes to regressions introduced by an earlier fix in this same release.
Breaking / behavior changes
findActive()'sALLOW FILTERINGis now an explicit opt-in (GH #12) — without
@SecondaryIndexon the marker column, it throwsKandraQueryExceptionunless called with
allowFullScan = true, instead of silently runningALLOW FILTERINGwith a WARN.KandraBatchScopesplit into suspend-only and blocking (KandraBlockingBatchScope) types
(GH #99) — mixing a blocking repository'ssaveInBatchinto a suspendbatch { }block (which
would block the coroutine dispatcher) is now a compile error instead of a runtime footgun.@VersionLWT updates no longer blindly retry on transient errors (GH #15) — retrying the
identical conditional statement after a timeout could raise a spurious
KandraOptimisticLockExceptionfor a write that had already succeeded; see the retry recipe in
docs/USER_GUIDE.md.EACH_QUORUM(as a read consistency) andSERIAL/LOCAL_SERIAL(as a write consistency) now
throwKandraQueryExceptioninstead of reaching the driver unvalidated (GH #140/#141) — a caller
relying on the previous unvalidated pass-through will now get a clear exception at query time.AUTO_MIGRATErefuses toALTER TABLE ADDa missing partition/clustering key column (GH #92)
— throwsKandraSchemaExceptioninstead of silently adding it as a plain column and corrupting row
identity.
Added
saveAndGet()/saveIfNotExistsAndGet()/updateAndGet()onKandraRepository/
KandraSuspendRepository(GH #132/#138/#136) — same writes assave()/saveIfNotExists()/
update(), but return the entity actually persisted (generated id,@CreatedAt/@UpdatedAt,
post-write@Version) instead of discarding it.update()/updateAndGet()gained aserialConsistencyparameter (GH #134) — lets a versioned
update requestSERIAL(cross-DC Paxos) instead of the previous hardcodedLOCAL_SERIALfor its
IF version = ?check.throttle { }config block — client-side admission control wiring the driver's own
ConcurrencyLimitingRequestThrottler(maxConcurrentRequests/maxQueueSize), off by default
(GH #81/ISS-073).PoolConfiggained local/remote connection-pool size fields — previously no
Kandra-level configuration at all (GH #80).KandraThrottledException(GH #103) — wraps a backpressure/admission-control rejection instead
of letting the raw driverRequestThrottlingExceptionleak through unwrapped.- Consistency Strict Mode (GH #5) — opt-in, WARN-only warning when a query resolves to
LOCAL_ONE/ONEin a deploymentloadBalancing.allowedRemoteDcsmarks as multi-DC. kandra-codegengenerates typed Koin/Kodein DI accessors (GH #17) — a compile-checked
alternative to hand-typednamed("...")qualifier lookups.DebugConfig.rawQueryStrictMode— makes theraw()/rawQuery()injection heuristic throw
KandraQueryExceptioninstead of only warning.
Fixed — correctness / data integrity
@Transientcolumns broke every full-row decode (GH #130).save()discarded the entity's generated id/timestamps (GH #132);update()discarded the
actually-persisted post-update version (GH #136);saveIfNotExists()discarded the actually-
persisted entity (GH #138) — all three addressed by the new...AndGet()methods above.- Predicate values bypassed codec encoding in
buildWhere()— brokeeq/gt/lt/inon any
non-primitive column type (GH #144). append()/remove()bound a rawCollection, not the column's actual CQL collection type —
broke everySet<T>column (GH #145).INon a@SecondaryIndexcolumn silently queried the partition key instead of the indexed column
(GH #146).- Counter columns threw on decode whenever any counter cell in a row was untouched (NULL) (GH #93).
- Repositories silently discarded the plugin's own configured
StatementBuilder/codec. BatchEngine-bypassing write paths now route throughexecuteWithRetry(GH #29).- Eager
SchemaRegistryvalidation for@PartitionKey/@ClusteringKeyconflicts, blank@Column
names, and identifier shape (GH #30); duplicatecqlNames now rejected eagerly, withfrozen<>
support for collection keys/nesting (GH #31). - DI qualifier collisions are now detected before binding instead of resolving the wrong repository
at runtime (GH #35). - The
AUTO_CREATE/AUTO_MIGRATEDDL bootstrap claim is now keyed by schema fingerprint (GH #90)
and fenced by holder with a clock-skew-proof staleness fallback (GH #91), after an LWT claim guard
against concurrent-instance races was added first (GH #79). - Migration claim resolution now distinguishes losing a race to an
APPLIEDrow from losing to a
CLAIMEDone (GH #101); checksum now hashes every sibling compiled class fileup()produces
(object expressions, anonymous classes, SAM lambdas), not just the migration's own (GH #102), and
normalizes compiled bytecode before hashing (GH #63); migration claim staleness is now measured by
the DB cluster's own clock, not the app instance's (GH #64); first crash-safety pass
(CLAIMED/APPLIED distinction + kandra-migrate test suite) (GH #26/#37). - Codegen now gives clear diagnostics for file-name collisions on same-simple-name nested entity
classes and for@ScyllaTableon a non-data-class, instead of a late, confusing failure (GH #104). FakePreparedStatement.bind()now simulates real binding instead of unconditionally throwing
(GH #28) — the fix thekandra-testskill was still describing the pre-fix state of as of the
2026-09-09 docs pass.
Fixed — consistency & multi-DC
LOGGED BATCH/versioned-update consistency anddecodeEntitycolumn-map caching (GH #54/#68);
generic reads (find/findAll/findPage/exists) ignored configured read consistency, and
unpaged reads had no row cap (GH #55/#67 — the latter is thefindAll()10,000-row truncation
documented indocs/USER_GUIDE.md/docs/features/repositories.md).- Multi-DC failover, credential rotation re-applying to the live session, keyspace/DC identifier
validation, and a dead pool-config field removed (GH #58/#61/#65/#69). - Retry idempotency,
saveIfNotExistsLWT retry, versioned-update TTL preservation, and soft-delete
consistency (GH #56/#57/#59/#62). - Suspend-safe batch collection, failure-path metrics, and RF-aware Strict Mode (GH #60/#82/#83).
- Lookup-index reads and versioned-update lookup-table writes both bypassed configured consistency
(GH #96);findById()cache hits silently ignored the caller's consistency override (GH #95); a
cache invalidate-after-write race could pin a stale value indefinitely, and@CacheResult's
per-process-only scope is now documented (GH #100). - Strict Mode's RF-vs-consistency math was wrong for multi-DC
NetworkTopologyStrategy, causing
false-positive warnings (GH #98).
Fixed — security / hardening
raw()/rawQuery()'s injection guard is no longer suppressed by an unrelated bound parameter
elsewhere in the same string (GH #32), and now also catches a bare;statement terminator or a
--//* */comment marker — closing a gap where a payload against an unquoted numeric/UUID column
reached the driver undetected (GH #142).KandraColumnRef'scqlNameis now validated against CQL identifier shape (GH #33).KandraCredentials.toString()no longer prints the plaintext password (GH #66).- SSL config (
requireEncryption/minimumTlsVersion/cipherSuites) is now actually enforced,
not dead fields (GH #78); TLS floor now fails closed, the reverse-DNS hostname-verification bug is
fixed, and pool size is validated (GH #105). - A missing-auth fallback is now logged,
existsQuery's raw CQL path is guarded, and predicate
columns are validated (GH #107).
Fixed — performance / scalability
- Entity reflection (copy fn, properties, constructor) is now cached per
KClassinstead of
recomputed every call (GH #13). findActive()/findActiveSuspend()had no row cap — an unboundedALLOW FILTERINGscan could
OOM the process (GH #94).- Suspend read/write paths no longer block the coroutine dispatcher on a prepared-statement cache
miss (GH #27); graceful shutdown's drain loop replaced aThread.sleepbusy-wait with coroutine
polling (GH #34). kandra-runtimeread-path/metrics polish: implicitLIMIT 1where applicable, atomic
cachegetOrPut, cache-stampede protection, and more specific metrics labels (GH #106).
Testing & CI
kandra-ktortests now run in CI, with newkandra-runtimeunit test coverage (GH #16).- Codegen tests now assert on generated
*Table.ktcontent, not just file existence (GH #37, part). - New SSL round-trip, multi-DC failover, and migration-claim concurrency test coverage (GH #84);
kandra-multidc/SSL integration suites are now wired into CI instead of tagged "manual" with zero
scheduled execution (GH #97); multi-DC/SSL test fixtures hardened — fixed compose ports, pause-vs-
real-partition realism, a missing hostname-mismatch SSL test (GH #108). - Jakarta validator factory reuse, codegen nullability, and health-check debounce (GH #36, items
1/2/4). - Assorted low-severity findings from the 2026-09-09 post-fix critical audit (GH #109).
Documentation
@LookupIndex+@SoftDeletecombined storage-growth behavior documente...