Skip to content

Kotlin Compiler Plugin for SQL Template DSL #74

@zantvoort

Description

@zantvoort

Motivation

SQL embedded in application code often relies on string interpolation. This approach is convenient but unsafe because it can easily lead to SQL injection when values are concatenated into the query.

Most frameworks solve this by introducing parameter APIs or query builders. These approaches often reduce readability or require SQL to be written in a fragmented way.

Modern languages are moving toward structured string templates. Java introduced string templates in Java 21 and Python recently introduced a similar concept. Kotlin currently has no built-in mechanism to capture template structure before interpolation.

Goal

Implement a compiler plugin that transforms Kotlin string interpolations inside Storm's TemplateBuilder lambdas into safe, parameterized template calls. The plugin should automatically wrap all interpolated expressions in t() calls, ensuring values are always separated from SQL text.

Compiler Plugin (storm-compiler-plugin)

The Storm compiler plugin should use the Kotlin K2 IR generation extension (IrElementTransformerVoid) to transform string interpolations at compile time.

When the user writes:

orm.query { "SELECT ${User::class} FROM ${User::class} WHERE id = $id" }

The compiler plugin should detect that the lambda has a TemplateContext receiver and automatically wrap each interpolated expression in a t() call:

  orm.query { "SELECT ${t(User::class)} FROM ${t(User::class)} WHERE id = ${t(id)}" }

The t() function is the single entry point for all template elements. It handles types (expanding to column lists), metamodel fields (resolving to column names with aliases), and plain values (becoming parameterized placeholders).

This transformation should happen at compile time and produce identical bytecode to writing t() manually. The resulting template is then processed by Storm's SQL template engine, which splits the string on the t() boundaries to obtain fragments and values.

The plugin should activate automatically via service loader once it is on the Kotlin compiler classpath. No additional configuration flags should be needed.

Runtime Safety Detection

The compiler plugin should inject a call to autoInterpolation() at the start of every transformed lambda. At runtime, Storm uses this signal to verify interpolation safety: if a TemplateBuilder lambda executes without autoInterpolation() being called and without any explicit t() or insert() calls, Storm should act based on the
storm.validation.interpolation_mode system property:

  • warn -- Logs a warning (default). Suitable for development.
  • fail -- Throws an IllegalStateException. Recommended for production.
  • none -- Disables the check entirely.

Fallback: Manual t() Wrapping

The compiler plugin should remain optional. Without it, users can wrap interpolations in t() manually:

  orm.query { "SELECT ${t(User::class)} FROM ${t(User::class)} WHERE id = ${t(id)}" }

The compiler plugin should detect existing t() and insert() calls and leave them unchanged, so mixing both styles is safe.

Scope

  • New storm-compiler-plugin module (Gradle-based)
  • Add autoInterpolation() marker method to TemplateContext and three-mode interpolation safety check in TemplateBuilder.build()
  • Update all Kotlin template examples in documentation to use auto-interpolation syntax (remove explicit t() wrapping)
  • New docs/string-templates.md explaining both the Kotlin compiler plugin and Java string templates
  • Add storm.validation.interpolation_mode property to configuration documentation with production hardening recommendations
  • Add compiler plugin setup to installation documentation and Quick Start
  • Remove storm-kotlin-validator module

Design Goals

  • Preserve natural SQL syntax (standard Kotlin string interpolation)
  • Avoid introducing new Kotlin language syntax
  • Enforce safe parameterization at compile time
  • Work entirely through a Kotlin compiler plugin
  • Maintain compatibility when the plugin is not applied (fallback to manual t() or warning/fail mode)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions