Sqlx4k
Communitysmyrgeorge
Write Kotlin (JVM, Android or Kotlin Multiplatform) database code with sqlx4k, the coroutine-first SQL toolkit for PostgreSQL, MySQL/MariaDB, SQLite and SQLCipher: Gradle setup, @Table entities, @Repository interfaces with compile-time validated @Query methods, transactions, migrations and error handling. Use when a project depends on sqlx4k (io.github.smyrgeorge:sqlx4k-*, the io.github.smyrgeorge.sqlx4k Gradle plugin, or imports from io.github.smyrgeorge.sqlx4k), or when the user asks to use it.
Overview
- Repository
- smyrgeorge/sqlx4k
- Publisher
- smyrgeorge
- Stars
- 322
- Forks
- 10
- Bundled files
- 4
- License
- MIT
- Updated
- Nov 9, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/smyrgeorge/sqlx4k.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/sqlx4k" ~/.claude/skills/sqlx4k
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/smyrgeorge/sqlx4k.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/sqlx4k" ~/.codex/skills/sqlx4k
rm -rf "$tmp"Install the Sqlx4k agent skill from https://github.com/smyrgeorge/sqlx4k/tree/main/skills/sqlx4k into this project's skills folder.Instructions
sqlx4k
sqlx4k is not an ORM: you write the SQL, and sqlx4k runs it with coroutines, maps the rows, and checks your queries at build time. Its code generator (KSP) writes the repository implementations, the CRUD statements and the row mappers.
Full documentation: https://smyrgeorge.github.io/sqlx4k/ (an index for agents: llms.txt at the same address).
Defaults: follow them in this order
Apply these defaults to all new code, and always when setting up a project from scratch. Deviate only when the project already does otherwise, or when the user asks.
- Set up the project with the Gradle plugin. It adds the driver and configures the code generator. Do not add the sqlx4k dependencies or wire KSP by hand in a new project.
- Access the data through repositories. Map every table the code reads or writes to a
@Tableentity, and access it through a@Repositoryinterface extendingContextCrudRepository(context parameters). Write every query as a@Querymethod, so the build validates it. Use the plain driver API (execute,fetchAll, …) only for what a repository cannot express: DDL, dynamic SQL, or a result that does not map to an entity. - Write entities with
save.save(entity)inserts a new entity (id0) and updates an existing one, so the calling code does not need to know which. Callinsertorupdateonly when the entity's id is not anIntorLonggenerated by the database (e.g. aUuid, or any@Id(insert = true)), wheresavecannot tell new from existing.
A project from scratch
1. Set up Gradle with the plugin
plugins {
kotlin("multiplatform") // or kotlin("jvm")
id("io.github.smyrgeorge.sqlx4k") version "1.18.0"
}
sqlx4k {
driver = PostgreSQL // also: MySQL, MariaDB, SQLite, SQLiteCipher
generatedCodePackage = "com.example.db" // where the generated code goes
// sourceSets = listOf("main") // for a plain kotlin("jvm") project
}The plugin adds the driver dependency and configures KSP. Without it, the code generator must be set up by hand (see references/code-generation.md).
2. Map a table
@Table("users")
data class User(
@Id
val id: Long = 0, // generated by the database; 0 until the entity is saved, so `save` knows it is new
val name: String, // column `name`
@Column(name = "mail_address")
val email: String,
@Column(insert = false, update = false)
val createdAt: Instant? = null, // column `created_at` (snake_case): set by the database, read back
)Prefer a database-generated Long (or Int) id starting at 0: it is what lets save work. Use
@Id(insert = true) only for an id the application must set (e.g. a Uuid), and then write with insert/update.
3. Declare a repository
@Repository
interface UserRepository : ContextCrudRepository<User> {
@Query("SELECT * FROM users WHERE mail_address = :email")
context(context: QueryExecutor)
suspend fun findOneByEmail(email: String): Result<User?>
@Query("SELECT * FROM users WHERE name = :name")
context(context: QueryExecutor)
suspend fun findAllByName(name: String): Result<List<User>>
@Query("SELECT count(*) FROM users")
context(context: QueryExecutor)
suspend fun countAll(): Result<Long>
}The build generates UserRepositoryImpl (an object, in generatedCodePackage) with these methods plus the CRUD
operations: save (insert or update), delete, batchInsert, batchUpdate, and the lower-level insert and
update.
4. Use it
val db = PostgreSQL(url = "postgresql://localhost:5432/app", username = "app", password = "secret")
// The QueryExecutor is the database, a connection or a transaction, taken from the scope.
val updated: User = db.transaction {
// save inserts a new entity (id = 0) and updates an existing one.
val alice = UserRepositoryImpl.save(User(name = "Alice", email = "[email protected]")).getOrThrow() // insert
UserRepositoryImpl.save(alice.copy(name = "Alice Smith")).getOrThrow() // update
}
with(db) {
val alice: User? = UserRepositoryImpl.findOneByEmail("[email protected]").getOrThrow()
}Without context parameters, extend CrudRepository<User> instead and pass the executor as the first argument of every
method: suspend fun findOneByEmail(context: QueryExecutor, email: String): Result<User?>, called as
UserRepositoryImpl.findOneByEmail(db, "...").
Rules that the build enforces
The code generator validates every @Query and fails the build when a rule is broken. Write them right the first time:
The method name prefix decides the statement and the return type:
Prefix Statement Return type findAll/findAllBy…SELECTResult<List<T>>findOneBy…SELECTResult<T?>countAll/countBy…SELECT count(...)Result<Long>existsBy…SELECTResult<Boolean>deleteAll/deleteBy…DELETEResult<Long>execute…INSERT/UPDATE/DELETEResult<Long>streamAll/streamAllBy…SELECTFlow<T>stream*methods are notsuspend: the query runs when the flow is collected.Parameters are named:
:emailbinds the method parameteremail. Never concatenate values into the SQL.Columns must exist on the entity, by their column name (
mail_address, notemail), in theSELECTlist,WHERE,ORDER BY,GROUP BY,HAVINGandSET. A single-table query must target the entity's own table.find*methods must select every column: useSELECT *(expanded to the entity's columns at build time).countBy…,findOneBy…andexistsBy…cannot useGROUP BY/HAVING, and acount*must select a singlecount(...).One statement per
@Query, and at most one@Idper entity.For SQL the parser does not understand (vendor-specific syntax), use
@Query(..., checkSyntax = false).
Pitfalls
- Every call returns a
Result. Call.getOrThrow()or handle the failure; an ignoredResulthides errors.db.transaction { }throws instead (db.transactionCatching { }returns aResult). - A transaction is rolled back when its block throws or returns a failed
Result. Run every statement of the block on its receiver (or, with context parameters, inside the block), not ondb, or it runs outside the transaction. - Entities are immutable:
save(likeinsert/update) returns a new instance with the values the database stored (the generated id, defaults). Keep the returned instance, not the one you passed in: saving the original again would insert a second row. savedecides from the id: anInt/Longid of0(ornull) inserts, any other value updates. With another id type,savefails at runtime, not at build time; with an id the application sets (@Id(insert = true)), it would update a row that does not exist yet. In both cases callinsertfor new entities andupdatefor existing ones.findOneBy…fails (MultipleRowsReturned) when the query matches more than one row.batchInsert/batchUpdatereturn the row count, not the entities: generated ids are not read back.- Reading columns by hand needs imports:
asLong(),asInstant(),asUuid(), … are extension functions inio.github.smyrgeorge.sqlx4k.impl.extensions(PostgreSQL arrays:io.github.smyrgeorge.sqlx4k.postgres.extensions). - SQLite in memory (
sqlite::memory:) needs a pool of exactly one connection. - MySQL has no
RETURNINGclause (MariaDB has it forINSERTandDELETEonly): do not write one in a@Querythere. The generated CRUD operations re-read the row themselves.
Errors
Failures are SQLErrors. Branch on kind, which is the same on every database, rather than on vendor codes:
with(db) {
UserRepositoryImpl.save(user).onFailure { e ->
when ((e as? SQLError)?.kind) {
SQLError.Kind.UniqueViolation -> println("The email is already taken.")
else -> throw e
}
}
}Kinds: UniqueViolation, ForeignKeyViolation, NotNullViolation, CheckViolation, ExclusionViolation,
Deadlock, SerializationFailure, LockTimeout (the last three: retry the transaction), Other. code tells what
failed (Database, PoolTimedOut, OptimisticLockFailed, …); sqlState and nativeCode carry the vendor codes.
Reference
Read these only when the task needs them:
- references/code-generation.md: entities (
@Column,@Transient,@Version,@Converter), repository interfaces (Arrow too), batch operations, hooks, validation options, in-memory test repositories, setup without the plugin. - references/driver-api.md: plain SQL with
execute/fetchAll/fetch, statements and binding, row mappers, supported types, transactions, savepoints,TransactionContext, the connection pool. - references/databases.md: the driver of each database, connection URLs, platform notes, PostgreSQL LISTEN/NOTIFY and PGMQ.
- references/migrations.md: migration files and
migrate.
Bundled files
FAQ
What does Sqlx4k do?
Write Kotlin (JVM, Android or Kotlin Multiplatform) database code with sqlx4k, the coroutine-first SQL toolkit for PostgreSQL, MySQL/MariaDB, SQLite and SQLCipher: Gradle setup, @Table entities, @Repository interfaces with compile-time validated @Query methods, transactions, migrations and error handling. Use when a project depends on sqlx4k (io.github.smyrgeorge:sqlx4k-*, the io.github.smyrgeorge.sqlx4k Gradle plugin, or imports from io.github.smyrgeorge.sqlx4k), or when the user asks to use it.
Who made Sqlx4k?
smyrgeorge (a community member) publishes it in the smyrgeorge/sqlx4k repository on GitHub.
Is Sqlx4k free?
Yes. Sqlx4k is open source, published by smyrgeorge under the MIT license.
What comes with Sqlx4k?
Its instructions and 4 files the agent reads only when a task needs them, such as references, examples or scripts.
Which agents can use Sqlx4k?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.