Skip to content

Compile-Time Checks

The kuery-client compiler plugin does more than rewrite interpolated runtime values into bind parameters — it also checks your SQL code at compile time. This page is the overview of every diagnostic the plugin can report:

DiagnosticDescriptionSeverityEnabled
KUERY_UNSAFE_SQL_STRINGDetects SQL expressions whose automatic parameter binding cannot be verifiedwarning (strict → error)always
KUERY_BIND_CALL_IN_SQL_TEMPLATEPrevents a placeholder returned by bind() from being bound again as a valuealways erroralways
KUERY_SQL_SYNTAXReports syntax errors in SQL reconstructed at compile timewarning (strict → error)sqlSyntaxCheck
KUERY_SQL_DIALECTReports SQL features unsupported by the configured dialectwarning (strict → error)sqlSyntaxCheck with a dialect
KUERY_REDUNDANT_TRIM_INDENTReports unnecessary trimIndent() calls when automatic trimming is enabledwarning (style — never escalated)autoTrimIndent

Every diagnostic can be suppressed per declaration with @Suppress. The warning-based diagnostics can also be reconfigured project-wide with -Xwarning-level; the unconditional KUERY_BIND_CALL_IN_SQL_TEMPLATE error cannot.

Unsafe SQL strings (KUERY_UNSAFE_SQL_STRING)

The plugin's conversion of string interpolation into named bind parameters only works when the SQL string is written directly as a string literal/template at the call site. If you pass anything else — for example a variable — the plugin cannot see the interpolation, and the string would be executed as raw SQL.

To prevent this from happening silently, the compiler plugin reports a KUERY_UNSAFE_SQL_STRING warning when it cannot guarantee that interpolation in an add() / + expression will be converted into bind parameters.

Noncompliant code

kotlin
// String variable: interpolation already happened before add()
val sql = "SELECT * FROM users WHERE user_id = $userId"
kueryClient.sql {
    add(sql) // KUERY_UNSAFE_SQL_STRING
}

// String concatenation
kueryClient.sql {
    add("SELECT * FROM users WHERE " + condition) // KUERY_UNSAFE_SQL_STRING
}

// Function call: the plugin cannot see what the function returns
kueryClient.sql {
    add(buildWhere()) // KUERY_UNSAFE_SQL_STRING
}

// Runtime transformation of a literal
kueryClient.sql {
    add("WHERE aaa".replace("aaa", input)) // KUERY_UNSAFE_SQL_STRING
}

// Scope function: the plugin does not inspect the lambda body
kueryClient.sql {
    add(run { "SELECT 1" }) // KUERY_UNSAFE_SQL_STRING
}

// Every branch must be safe; fragment is a String variable
kueryClient.sql {
    add(if (id > 0) "SELECT 1" else fragment) // KUERY_UNSAFE_SQL_STRING
}

Compliant code

kotlin
// String literal / template
kueryClient.sql {
    +"SELECT * FROM users WHERE user_id = $userId" // userId is bound as :p0
}

// trimIndent() / trimMargin() on a literal
kueryClient.sql {
    +"""
    SELECT *
    FROM users
    """.trimIndent()
}
kueryClient.sql {
    +"""
    |SELECT *
    |FROM users
    """.trimMargin()
}

// const val; a Java constant (static final String with a constant initializer) works the same
const val SELECT_ALL_USERS = "SELECT * FROM users"
kueryClient.sql {
    +SELECT_ALL_USERS
}

// if expression with a safe string in every branch
kueryClient.sql {
    +"SELECT * FROM users"
    add(if (asc) "ORDER BY user_id" else "ORDER BY user_id DESC")
}

// when expression; a throwing branch never produces a SQL string
kueryClient.sql {
    +"SELECT * FROM users"
    add(
        when (sort) {
            "name" -> "ORDER BY name"
            "created" -> "ORDER BY created_at DESC"
            else -> error("Unsupported sort: $sort")
        },
    )
}

Each SQL-producing expression is known at compile time, so the plugin can safely rewrite any interpolated values into bind parameters. With autoTrimIndent enabled, omit the explicit trimIndent() as described in KUERY_REDUNDANT_TRIM_INDENT.

Responding to the warning

In order of preference:

  1. Restructure into safe forms. Most dynamic SQL can be expressed with Kotlin control flow inside the sql { } block — if / when / loops around +"..." — which the plugin handles safely. See Building SQL.
  2. Suppress the warning with @Suppress("KUERY_UNSAFE_SQL_STRING") on the declaration, when you know the string is safe but the checker cannot see it.

bind() in a string template (KUERY_BIND_CALL_IN_SQL_TEMPLATE)

bind() only makes sense together with addUnsafe(). Calling it inside a string template passed to add() / + is reported as a KUERY_BIND_CALL_IN_SQL_TEMPLATE compile error. Interpolated values there are already bound automatically, so the placeholder returned by bind() would itself be bound as a value. The SQL would compare user_id against the literal string :p0 and silently match nothing.

Noncompliant code

kotlin
kueryClient.sql {
    +"SELECT * FROM users WHERE user_id = ${bind(userId)}" // KUERY_BIND_CALL_IN_SQL_TEMPLATE
}

Compliant code

kotlin
kueryClient.sql {
    +"SELECT * FROM users WHERE user_id = $userId"
}

This diagnostic is an error regardless of strict mode. Interpolate the value directly, or use addUnsafe() when you need bind() for dynamically assembled SQL — see addUnsafe() and bind().

SQL syntax errors (KUERY_SQL_SYNTAX)

With the sqlSyntaxCheck option enabled, the plugin also validates the SQL text itself at compile time: blocks whose complete statement is statically known are assembled exactly like at runtime and run through a SQL parser. A parse failure is reported as KUERY_SQL_SYNTAX. See SQL Syntax Check for configuration and examples.

Noncompliant code

kotlin
kueryClient.sql {
    +"SELECT *"
    +"FORM users" // KUERY_SQL_SYNTAX: FORM should be FROM
}

Compliant code

kotlin
kueryClient.sql {
    +"SELECT *"
    +"FROM users"
}

Unsupported SQL dialect features (KUERY_SQL_DIALECT)

When sqlSyntaxCheck is configured with a dialect, the plugin also reports SQL features that the selected dialect does not support as KUERY_SQL_DIALECT. See Choosing a dialect for the available dialects and examples.

The following examples assume sqlSyntaxCheck = "postgresql".

Noncompliant code

kotlin
// KUERY_SQL_DIALECT: ON DUPLICATE KEY UPDATE is a MySQL feature
kueryClient.sql {
    +"INSERT INTO users (id, name) VALUES ($id, $name) ON DUPLICATE KEY UPDATE name = $name"
}

Compliant code

kotlin
kueryClient.sql {
    +"INSERT INTO users (id, name) VALUES ($id, $name) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name"
}

Redundant trimIndent() (KUERY_REDUNDANT_TRIM_INDENT)

With the autoTrimIndent option enabled, every string passed to add() / + is trimmed automatically, so an explicit .trimIndent() left behind is redundant — it prevents the compile-time trimming and only adds an extra runtime trim. The plugin reports a KUERY_REDUNDANT_TRIM_INDENT warning for such calls; remove them when enabling the option. Details are in Basics.

This is a style issue, not a safety issue, so strict mode does not escalate it.

Noncompliant code

kotlin
kueryClient.sql {
    +"""
    SELECT *
    FROM users
    """.trimIndent() // KUERY_REDUNDANT_TRIM_INDENT
}

Compliant code

kotlin
kueryClient.sql {
    +"""
    SELECT *
    FROM users
    """
}

Strict mode

Enable strict in the Gradle plugin to report the SQL-safety diagnostics as compile errors instead of warnings. The option remains opt-in because it was added after Kuery Client's initial releases and changing the existing build behavior would be disruptive. It is recommended for new projects and is intended to become the default in a future release:

kotlin
kueryClient {
    strict = true
}

This makes the compiler plugin register KUERY_UNSAFE_SQL_STRING — and, when the SQL Syntax Check is enabled, KUERY_SQL_SYNTAX / KUERY_SQL_DIALECT — as error-severity diagnostics. @Suppress on the enclosing declaration keeps working for individual call sites, and addUnsafe() + bind() remain the sanctioned way to build SQL dynamically. KUERY_REDUNDANT_TRIM_INDENT stays a warning — a style issue, not a safety issue.

The escalated set may grow in minor releases as new safety diagnostics are added — enabling strict mode opts into those too.

An explicit -Xwarning-level for one of these diagnostics always wins over strict mode: strict only changes the default severity of the diagnostics you have not configured yourself. So a single diagnostic can still be lowered back to a warning (or disabled) module-wide with strict enabled:

kotlin
kotlin {
    compilerOptions {
        // keep everything else strict, but let the dialect check stay advisory
        freeCompilerArgs.add("-Xwarning-level=KUERY_SQL_DIALECT:warning")
    }
}

Configuring the severity manually

The checks are regular Kotlin compiler diagnostics, so the standard Kotlin mechanisms apply to every diagnostic in the table (except KUERY_BIND_CALL_IN_SQL_TEMPLATE, whose error severity cannot be lowered):

  • Suppress for specific code with @Suppress("<name>") on the enclosing declaration, e.g. @Suppress("KUERY_SQL_SYNTAX"). This also works for diagnostics escalated to errors.

  • Escalate a single diagnostic without strict mode:

    kotlin
    kotlin {
        compilerOptions {
            freeCompilerArgs.add("-Xwarning-level=KUERY_UNSAFE_SQL_STRING:error")
        }
    }
  • Lower one back to a warning with strict mode enabled (see above), or disable one project-wide with -Xwarning-level=<name>:disabled (not recommended).

  • allWarningsAsErrors (-Werror) likewise turns all warnings into errors.