Query DSL
A Kotlin DSL that mirrors AQL syntax. Reads like a query, compiles like code.
FOR loops
Every query starts with FOR — iterate over a collection:
repo.findList {
FOR(repo) { person ->
// person is a type-safe expression — its properties are compile-time checked
RETURN(person)
}
} Filtering
Use comparison operators directly on typed fields:
FOR(repo) { person ->
FILTER(person.name EQ "Alice") // equals
FILTER(person.age NE 0) // not equals
FILTER(person.age GT 18) // greater than
FILTER(person.age GTE 18) // greater than or equal
FILTER(person.age LT 65) // less than
FILTER(person.age LTE 65) // less than or equal
RETURN(person)
} String matching
FILTER(person.name LIKE "Ali%") // SQL-like pattern
FILTER(person.email REGEX "^[a-z]+@") // regex match Collection operators
FILTER(person.age IN listOf(25, 30, 35))
FILTER(person.name NOT_IN listOf("test", "admin")) Logical operators
FILTER((person.age GTE 18) AND (person.age LTE 65))
FILTER((person.name EQ "Alice") OR (person.name EQ "Bob")) Sorting
FOR(repo) { person ->
SORT(person.name.ASC) // ascending
SORT(person.age.DESC) // descending
SORT(person.name.ASC, person.age.DESC) // multi-field sort
RETURN(person)
} Pagination
FOR(repo) { person ->
LIMIT(10) // first 10
LIMIT(20, 10) // skip 20, take 10
SKIP(5) // skip first 5
PAGE(page = 2, epp = 20) // page 2, 20 entries per page
RETURN(person)
} Nested property access
KSP generates accessors for nested types. Access nested fields with dots:
@Vault
data class Address(val city: String, val zip: String)
@Vault
data class Person(
val name: String,
val address: Address,
val tags: List<String> = emptyList(),
)
FOR(repo) { person ->
// Nested field access — compile-time checked
FILTER(person.address.city EQ "Berlin")
FILTER(person.address.zip EQ "10115")
// Array expansion — access fields inside lists
FILTER(person.tags.expand() EQ "vip")
// Generates: person.tags[*] == "vip"
RETURN(person)
} LET variables
Bind values or sub-queries to variables:
repo.findList {
val searchTerm = LET("search", "alice")
FOR(repo) { person ->
FILTER(CONTAINS(LOWER(person.name), searchTerm))
RETURN(person)
}
} INSERT, UPDATE, REMOVE
Insert
repo.findList {
val docs = LET("docs", listOf(
Person("Alice", 30),
Person("Bob", 25),
))
FOR(docs) { doc ->
INSERT(doc) INTO repo
}
} Update fields
repo.findList {
FOR(repo) { person ->
FILTER(person.name EQ "Alice")
UPDATE(person, repo) {
put({ name }) { CONCAT(name, " Updated".aql) }
}
RETURN_NEW(person)
}
} Remove
repo.find {
FOR(repo) { person ->
FILTER(person.age LT 18)
REMOVE(person._key).IN(repo)
}
} Return variants
RETURN(person) // return the document
RETURN_NEW(person) // return the document after UPDATE/INSERT
RETURN_OLD(person) // return the document before UPDATE/REMOVE
RETURN_DISTINCT(person) // return unique results
RETURN_COUNT() // return the count instead of documents AQL functions
Karango wraps 100+ AQL functions. See AQL Functions for the full reference. A few examples:
// String functions
FILTER(CONTAINS(LOWER(person.name), "alice"))
FILTER(LENGTH(person.name) GT 3)
// Math functions
val total = LET("total") { SUM(prices) }
// Array functions
val unique = LET("unique") { UNIQUE(person.tags) }
// Type checking
FILTER(IS_NOT_NULL(person.email))
// Document lookup
val doc = DOCUMENT(repo, "persons/abc123")