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")