In-memory repositories¶
Experimental
In-memory repository generation is currently in early development; its behavior and the generated API may change in future releases.
The companion sqlx4k-codegen-test module generates a thread-safe, in-memory implementation of every @Repository
interface, so you can unit-test the code that depends on your repositories without a real database. For a repository
named Sqlx4kRepository, it generates a class named InMemorySqlx4kRepository.
It is a separate module from sqlx4k-codegen, so in-memory generation is opt-in per dependency: add it only to the KSP
configuration that processes your repositories:
ksp {
// The generated implementations are emitted into this package (same option used by sqlx4k-codegen).
arg("output-package", "io.github.smyrgeorge.sqlx4k.examples.postgres")
}
dependencies {
// Real @Repository implementations (production).
add("kspMacosArm64", implementation("io.github.smyrgeorge:sqlx4k-codegen:1.17.0"))
// In-memory implementations for unit tests.
add("kspMacosArm64Test", "io.github.smyrgeorge:sqlx4k-codegen-test:1.17.0")
}
Given the Sqlx4kRepository from Code generation, use the generated
implementation directly in your tests:
val repo = InMemorySqlx4kRepository()
// A QueryExecutor is still accepted to match the interface, but it is never touched:
// it works entirely in memory, so any instance (even a no-op fake) will do.
val saved = repo.insert(db, Sqlx4k(id = 1, test = "test")).getOrThrow()
val all = repo.findAll(db).getOrThrow()
What the generated implementation provides¶
- Thread-safe storage: entities live in a
HashMapguarded by aMutex. - Full CRUD:
insert,update,delete,save,batchInsert,batchUpdate. Numeric@Idkeys withinsert = falseget auto-incrementing ids; application-provided ids (@Id(insert = true)) are stored as given.@Versionentities get the same optimistic locking as the real implementation:update/deleteonly apply when the stored version matches (failing withOptimisticLockFailedotherwise) andupdateincrements it. - Whole-table queries:
findAll,countAll,deleteAll. - Derived
@Querymethods: the in-memory behavior is derived from each method's@QuerySQL: the statement is parsed and itsWHEREclause becomes a predicate over the stored entities (columns are mapped back to properties via snake_case /@Column). Supports=,<>,<,<=,>,>=,IS [NOT] NULL,AND/OR, named parameters, and simple literals, forfind*/stream*/count*/exists*(SELECT),delete*(DELETE), andexecute*(UPDATESET …/DELETE). For example@Query("select * from users where name = :name")behaves likefilter { it.name == name }. - Repository hooks: the overridden
preInsert/afterInsert/... /aroundQueryhooks are honored exactly like the real generated implementations. - Test helpers:
clear()empties the store (and resets the id sequence) andfindAllStored()returns a snapshot.
Methods the generator cannot translate¶
Anything the generator cannot translate is emitted as a stub that throws NotImplementedError:
- a
WHEREit doesn't understand (e.g.LIKE,IN, subqueries, function calls), - a query whose result also depends on a join,
DISTINCT,GROUP BY/HAVING, aLIMIT/OFFSETor (forfind*/stream*) anORDER BY, - an
execute*that isn't a plain UPDATE/DELETE.
The generated class is open, so provide the behavior yourself by subclassing and overriding just those methods. Use
the withStore { } helper for thread-safe access to the backing map (the MutableMap is the lambda receiver):
class TestSqlx4kRepository : InMemorySqlx4kRepository() {
// A method whose WHERE the generator could not translate (e.g. LIKE), so it was left as a stub.
override suspend fun findAllByTestLike(context: QueryExecutor, pattern: String): Result<List<Sqlx4k>> =
withStore {
val prefix = pattern.removeSuffix("%")
runCatching { values.filter { it.test.startsWith(prefix) } }
}
}