Skip to content

Releases: Dev-Pasaka/kandra

Kandra 0.5.2

Choose a tag to compare

@Dev-Pasaka Dev-Pasaka released this 26 Oct 11:53
1a96031

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

  • @LookupIndex on a key column (#187, PR #195). A @LookupIndex on a @ClusteringKey or @PartitionKey property 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, …) made CREATE TABLE
    fail with no 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 as timestamp, type, key, ttl or date stay bare,
    so DDL (and the schema fingerprint) of every other entity is byte-for-byte unchanged. A reserved @ScyllaTable name 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 / saveWithNullsInBatchAndGet in batch { } and
    batchBlocking { }, and updateWithNulls / updateWithNullsAndGet on both repository types. A null property is written as NULL (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.SourceDebugExtension annotation'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-test and Testcontainers (#190, PR #192). org.testcontainers:cassandra and org.testcontainers:testcontainers are now api dependencies
    of kandra-test, so KandraTestcontainers.freshKeyspace() no longer fails with NoClassDefFoundError unless you add the Cassandra module by hand.
    A verifyTestcontainersIsApi task in check guards it.

  • Soft delete encoding (#198, PR #201). delete() on a @SoftDelete entity bound raw property values, skipping KandraCodec, so an enum or custom-typed column
    failed with a driver codec error. Values now go through the codec like every other write, and a null column is left UNSET.

  • Lost markApplied under clock skew (#203, PR #204). The migration claim is an IF NOT EXISTS LWT (Paxos ballot timestamp), but
    marking it APPLIED, releasing it after a failed up(), 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 stayed CLAIMED, 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_migrations and kandra_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, in kandra-migrate and
    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 by pool { requestTimeoutMillis }, so a loaded or distant cluster failed install(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 a null column UNSET (#189). On a @Version entity, update()/updateAndGet() used to bind NULL for every null
    non-key column, writing a tombstone per null column on every update — unlike save() and a plain update(), which leave it UNSET. It now leaves it
    UNSET too. If you relied on a versioned update() to clear a column, call updateWithNulls() instead.

Performance

  • Migration runner startup (#194, PR #197). Constructing a KandraMigrationRunner against an already-bootstrapped keyspace now does one plain read
    instead of CREATE TABLE IF NOT EXISTS plus a losing IF NOT EXISTS LWT (Paxos). The migrations jar is indexed once per JVM instead of scanned in full
    once per migration, each checksum is computed once per run(), 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

Choose a tag to compare

@Dev-Pasaka Dev-Pasaka released this 14 Oct 17:37

[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()'s ALLOW FILTERING is now an explicit opt-in (GH #12) — without
    @SecondaryIndex on the marker column, it throws KandraQueryException unless called with
    allowFullScan = true, instead of silently running ALLOW FILTERING with a WARN.
  • KandraBatchScope split into suspend-only and blocking (KandraBlockingBatchScope) types
    (GH #99) — mixing a blocking repository's saveInBatch into a suspend batch { } block (which
    would block the coroutine dispatcher) is now a compile error instead of a runtime footgun.
  • @Version LWT updates no longer blindly retry on transient errors (GH #15) — retrying the
    identical conditional statement after a timeout could raise a spurious
    KandraOptimisticLockException for a write that had already succeeded; see the retry recipe in
    docs/USER_GUIDE.md.
  • EACH_QUORUM (as a read consistency) and SERIAL/LOCAL_SERIAL (as a write consistency) now
    throw KandraQueryException
    instead 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_MIGRATE refuses to ALTER TABLE ADD a missing partition/clustering key column (GH #92)
    — throws KandraSchemaException instead of silently adding it as a plain column and corrupting row
    identity.

Added

  • saveAndGet() / saveIfNotExistsAndGet() / updateAndGet() on KandraRepository/
    KandraSuspendRepository (GH #132/#138/#136) — same writes as save()/saveIfNotExists()/
    update(), but return the entity actually persisted (generated id, @CreatedAt/@UpdatedAt,
    post-write @Version) instead of discarding it.
  • update()/updateAndGet() gained a serialConsistency parameter (GH #134) — lets a versioned
    update request SERIAL (cross-DC Paxos) instead of the previous hardcoded LOCAL_SERIAL for 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).
  • PoolConfig gained 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 driver RequestThrottlingException leak through unwrapped.
  • Consistency Strict Mode (GH #5) — opt-in, WARN-only warning when a query resolves to
    LOCAL_ONE/ONE in a deployment loadBalancing.allowedRemoteDcs marks as multi-DC.
  • kandra-codegen generates typed Koin/Kodein DI accessors (GH #17) — a compile-checked
    alternative to hand-typed named("...") qualifier lookups.
  • DebugConfig.rawQueryStrictMode — makes the raw()/rawQuery() injection heuristic throw
    KandraQueryException instead of only warning.

Fixed — correctness / data integrity

  • @Transient columns 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() — broke eq/gt/lt/in on any
    non-primitive column type (GH #144).
  • append()/remove() bound a raw Collection, not the column's actual CQL collection type —
    broke every Set<T> column (GH #145).
  • IN on a @SecondaryIndex column 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 through executeWithRetry (GH #29).
  • Eager SchemaRegistry validation for @PartitionKey/@ClusteringKey conflicts, blank @Column
    names, and identifier shape (GH #30); duplicate cqlNames now rejected eagerly, with frozen<>
    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_MIGRATE DDL 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 APPLIED row from losing to a
    CLAIMED one (GH #101); checksum now hashes every sibling compiled class file up() 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 @ScyllaTable on 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 the kandra-test skill 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 and decodeEntity column-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 the findAll() 10,000-row truncation
    documented in docs/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, saveIfNotExists LWT 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's cqlName is 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 KClass instead of
    recomputed every call (GH #13).
  • findActive()/findActiveSuspend() had no row cap — an unbounded ALLOW FILTERING scan 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 a Thread.sleep busy-wait with coroutine
    polling (GH #34).
  • kandra-runtime read-path/metrics polish: implicit LIMIT 1 where applicable, atomic
    cache getOrPut, cache-stampede protection, and more specific metrics labels (GH #106).

Testing & CI

  • kandra-ktor tests now run in CI, with new kandra-runtime unit test coverage (GH #16).
  • Codegen tests now assert on generated *Table.kt content, 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 + @SoftDelete combined storage-growth behavior documente...
Read more