Stop Writing 300 Queries
When You Need 30

The only Kotlin library with true query composition. Not another ORM.

Start in 30 Seconds

1

Download Project

2

Import into IntelliJ

  • Extract the ZIP file
  • File → New → Project from Existing Sources...
  • Select the extracted folder and choose "Gradle"
3

Run Your Code

  • Start the container: docker compose up -d db
  • Open src/main/kotlin/org/example/MyQuery.kt
  • Run the main() function!
1

Add the ExoQuery plugin and dependencies

plugins {
  id("io.exoquery.exoquery-plugin") version "2.2.20-1.7.1.PL"
  kotlin("plugin.serialization") version "2.2.20"
}

dependencies {
    implementation("io.exoquery:exoquery-runner-jdbc:1.7.1.PL")
    // ── Remember to include the right JDBC Driver ─────────────
    implementation("org.postgresql:postgresql:42.7.0")
}
3

Configure the database controller

import io.exoquery.*
import io.exoquery.controller.JdbcControllers
import javax.sql.DataSource

val ds: DataSource = createMyDataSource()
val ctl = JdbcControllers.Postgres(ds)
4

Run a Query!

data class Person(val name: String, val age: Int)
val query = sql { 
    Table<Person>().filter { p -> p.name == "Joe" } 
}
fun main() = println(query.buildFor.Postgres().runOn(ctl))
1

Add the ExoQuery plugin and dependencies

plugins {
  id("io.exoquery.exoquery-plugin") version "2.2.20-1.7.1.PL"
  kotlin("plugin.serialization") version "2.2.20"
}

dependencies {
    implementation("io.exoquery:exoquery-runner-r2dbc:1.7.1.PL")
    // ── Remember to include the right R2DBC Driver ─────────────
    implementation("org.postgresql:r2dbc-postgresql:1.0.5.RELEASE")
}
2

Configure the database controller

import io.r2dbc.spi.ConnectionFactories
import io.r2dbc.spi.ConnectionFactoryOptions
import io.exoquery.controller.r2dbc.R2dbcControllers

val connectionFactory = ConnectionFactories.get(
  ConnectionFactoryOptions.builder()
    .option(ConnectionFactoryOptions.DRIVER, "postgresql")
    .option(ConnectionFactoryOptions.HOST, "localhost")
    .option(ConnectionFactoryOptions.PORT, 5432)
    .option(ConnectionFactoryOptions.DATABASE, "mydb")
    .option(ConnectionFactoryOptions.USER, "user")
    .option(ConnectionFactoryOptions.PASSWORD, "password")
    .build()
)
val ctl = R2dbcControllers.Postgres(connectionFactory = connectionFactory)
3

Run a Query!

data class Person(val name: String, val age: Int)
val query = sql { 
    Table<Person>().filter { p -> p.name == "Joe" } 
}
fun main() = println(query.buildFor.Postgres().runOn(ctl))
1

Add the ExoQuery plugin and dependencies

plugins {
  id("io.exoquery.exoquery-plugin") version "2.2.20-1.7.1.PL"
  kotlin("plugin.serialization") version "2.2.20"
}

dependencies {
    implementation("io.exoquery:exoquery-runner-jdbc:1.7.1.PL")
    // ── Remember to include the right JDBC Driver ─────────────
    implementation("org.postgresql:postgresql:42.7.0")
}
2

Configure the database connection

// src/main/resources/application.conf
myDatabase {
    dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
    dataSource.databaseName=quill_test
    dataSource.serverName=localhost
    dataSource.portNumber=5432
    dataSource.user=postgres
    dataSource.password=${?POSTGRES_PASSWORD_ENV_VAR}
}
3

Create a Database Controller

import io.exoquery.*
import io.exoquery.controller.JdbcControllers
import javax.sql.DataSource

// Loads the "myDatabase" config you just created
val ctl = JdbcControllers.Postgres.fromConfig("myDatabase")
4

Run a Query!

data class Person(val name: String, val age: Int)
val query = sql { 
    Table<Person>().filter { p -> p.name == "Joe" } 
}
fun main() = println(query.buildFor.Postgres().runOn(ctl))
1

Add the plugin

plugins {
    kotlin("android") version "2.2.20"
    id("io.exoquery.exoquery-plugin") version "2.2.20-1.7.1.PL"
    kotlin("plugin.serialization") version "2.2.20"
}
2

Add the dependencies

dependencies {
    implementation("io.exoquery:exoquery-runner-android:1.7.1.PL")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2")
    implementation("androidx.sqlite:sqlite-framework:2.4.0")
}
3

Create the Database Controller

import io.exoquery.controller.android.TerpalAndroidDriver
import androidx.test.core.app.ApplicationProvider

val controller = TerpalAndroidDriver.fromApplicationContext(
    databaseName = "mydb",
    applicationContext = ApplicationProvider.getApplicationContext(),
    schema = MyTerpalSchema // Optional: define your schema
)
4

Run a Query!

data class Person(val name: String, val age: Int)
val query = sql { 
    Table<Person>().filter { p -> p.name == "Joe" } 
}
fun main() = println(query.buildFor.Sqlite().runOn(controller))
1

Add the plugin to your KMP build

plugins {
    kotlin("multiplatform") version "2.2.20"
    id("io.exoquery.exoquery-plugin") version "2.2.20-1.7.1.PL"
    kotlin("plugin.serialization") version "2.2.20"
}
2

Add the dependencies

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
                implementation("io.exoquery:exoquery-runner-native:1.7.1.PL")
            }
        }
    }
}
3

Create the Database Controller

import io.exoquery.controller.native.TerpalNativeDriver

val controller = TerpalNativeDriver.fromSchema(
    schema = MyTerpalSchema,
    databaseName = "mydb"
)
4

Run a Query!

data class Person(val name: String, val age: Int)
val query = sql { 
    Table<Person>().filter { p -> p.name == "Joe" } 
}
fun main() = println(query.buildFor.Sqlite().runOn(controller))

Why Nothing Else Works

Every Kotlin SQL library hits the same fundamental limitation: they can't compose queries. Here's the proof.

Developer tangled in SQL strings

The Woes of SQL Strings

JPA, SQLDelight

SQL lives in external .sq files or @Query annotations can't reference each other. So you need to duplicate logic, over and over and over!

Context switchingNo SQL reuseDuplicated logic
SQLDelight - Impossible
usersByStatus: SELECT * FROM users WHERE status = ? AND another_50_conditions......... userAccounts: SELECT * FROM accounts WHERE user_id IN ( -- Can't reference usersByStatus. Must duplicate the query! SELECT id FROM users WHERE status = ? AND another_50_conditions......... )
ExoQuery - Just Works!
@CapturedFunction fun usersBy(status: String) = sql { users.filter { it.status == status } } //sampleStart val usersByStatus = sql { usersBy(status) } val userAccounts = sql { accounts.filter { it.userId in usersBy(status).map { it.id } } } //sampleEnd
Developer struggling with DSL building blocks

The Woes of DSL Builders

Criteria, Exposed

DSLs like .eq(), .and(), .or() are needed. You can't use real Kotlin or Java control flow, so when logic gets complex, you end up with a mess.

DSL learning curveNo if/when/elvisNo query types
DSL Builders - Impossible
Player.select( // Can't use a normal Kotlin when expression! case().When( (Player.score greater 300) or Player.bonusCompleted, // Can't use if/else, let, or Elvis operator! case().When( Player.score.isNotNull(), case().When(Player.bonusCompleted eq true, Player.score * 3) .Else(Player.score * 2) ).Else(defaultScore) ).Else(staticBonus) )
ExoQuery - Just Works!
@Serializable data class Player(val name: String, val score: Int?, val bonusCompleted: Boolean) val players = sql { Table<Player>() } val defaultScoreRuntime = 100 val defaultScore = sql { param(defaultScoreRuntime) } //sampleStart // Native Kotlin control flow! players.map { a -> when { a.score > 300 || a.bonusCompleted -> a.score?.let { if (a.bonusCompleted) 3*it else 2*it } ?: defaultScore else -> staticBonus } } //sampleEnd
Developer fleeing from N+1 query problems

The Woes of ORM

Hibernate, JPA

Impedance mismatch, hidden lazy-loads, runtime reflection, and the dreaded N+1 problem.

N+1 queriesRuntime overheadCan't compose
Hibernate - Impossible
// Queries aren't composable values TypedQuery<User> q1 = em.createQuery(...); TypedQuery<Account> q2 = // Can't use q1! // Classic N+1 problem users.forEach { user -> user.accounts.forEach { account -> // Executes query for EACH user! } }
ExoQuery - Works
data class User(val id: Int, val name: String) data class Account(val id: Int, val userId: Int, val balance: Double) data class Transaction(val id: Int, val accountId: Int, val amount: Double) @SqlFragment fun accountsOf(user: User) = sql { composeFrom.join(Table<Account>()) { account -> account.userId == user.id } } @SqlFragment fun transactionsOf(account: Account) = sql { composeFrom.join(Table<Transaction>()) { transaction -> transaction.accountId == account.id } } //sampleStart // Take an existing query... val userAccounts = sql.select { val u = from(Users) val a = from(accountsOf(u)) u to a } // Composes to single flat SQL with JOINs, not N+1! sql.select { val (u, a) = from(userAccounts) val t = from(transactionsOf(a)) a to t } //sampleEnd

Why developers choose ExoQuery

Feature ExoQuery SQLDelight Exposed
Syntax ✓ Native Kotlin SQL Strings Specialized DSL
Basic Entity ✓ Regular Data Class Generated Function Table Object DSL
Type Safety ✓ Full ⚠ Only function-based ⚠ Only column-based
IDE Support ✓ Full ⚠ IntelliJ plugin ✓ IntelliJ Plugin Optional
Learning Curve ✓ Minimal SQL + Tooling DSL + Customizations

Performance

Compile-time SQL generation means zero runtime overhead and maximum efficiency

 
0ms
Runtime Overhead
 
0
Runtime Reflection
Up to
209%
Faster Cold Start

Execution profiles show the performance difference.

Other Libraries

Runtime reflection & SQL generation

Loading...

Your browser doesn't support embedded SVG. View directly.

Time
VS

ExoQuery

Compile-time SQL generation

Loading...

Your browser doesn't support embedded SVG. View directly.

≈ 1.86× less work needed!
Time
Key Insight: ExoQuery eliminates reflection and SQL preparation overhead by moving these operations to compile-time, resulting in true 0ms runtime overhead.

Still Not Convinced?

Check out these resources to see ExoQuery in action

ExoQuery Bear

Try Interactive Examples

Explore live code samples and see how ExoQuery simplifies your queries

View Examples →

Watch a Quick Introduction

4-minute overview of ExoQuery's key features