Clojure: SQLite application defined SQL functions with JDBC


SQLite provides a convenient interface called Application-Defined SQL Functions that lets you create custom SQL functions that call back into your application code to compute their results. This lets us extend SQLite in Clojure. Want to query with regex or jsonpath? Want custom aggregation functions? Don't want to write C? Don't want to manage different precompiled binaries for different operating systems. No problem Application Defined SQL Functions have you covered.

In Java

org.sqlite provides an abstract class org.sqlite.Function for implementing application defined SQL functions.

Bellow is how org.sqlite.Function is intended to be used from java using an anonymous class that extends org.sqlite.Function.

Function.create(conn, "hello_world", new Function() {
 @Override
 protected void xFunc() {
   result("Hello, world!")
 }
}, 0, Function.FLAG_DETERMINISTIC);

In Clojure

Clojure provides the proxy function for creating anonymous classes. However, it doesn't allow you to access protected super methods, and unfortunately org.sqlite.Function implements a bunch of methods as protected. So we will need to use the gen-class macro.

Create a gen-class

gen-class creates named classes for direct use as Java classes and allows us to expose inherited protected methods.

(gen-class
  :name sqlite.db.application-defined-functions.RegexCapture
  :prefix "regex-capture-"
  :extends org.sqlite.Function
  :exposes-methods {result superResult
                    value_text superValueText})

We extend org.sqlite.Function and expose the methods result and value_text binding them to superResult and superValueText respectively.

Override xFunc

We use defn to override the org.sqlite.Function class's xFunc method. It's important to note that the function name prefix must match the prefix specified in gen-class under :prefix. In this case that's regex-capture-.

(defn regex-capture-xFunc [this]
  (.superResult this
    (let [result (re-find
                   (re-pattern
                     (.superValueText  this 0))
                   (.superValueText  this 1))]
      (if (vector? result)
        (second result)
        result))))

Create a directory called classes

Ensure the default target output directory classes exists at the top level of the project.

Add classes directory to paths

Add the classes directory to the class path in deps.edn.

{:paths   ["src" "classes"]
 :deps
 {org.clojure/clojure               {:mvn/version "1.11.1"}
  com.github.seancorfield/next.jdbc {:mvn/version "1.3.874"}
  org.xerial/sqlite-jdbc            {:mvn/version "3.42.0.0"}}
 :aliases {}}

Generate the classes

Compile to generate classes using the compile function.

(compile 'sqlite.db.application-defined-functions)

Tests the SQL functions

To use the application defined SQL functions we need to create them. This loads them into SQLite for the duration of the current connection, meaning they can be used in any query that uses the same connection.

(let [my-datasource (jdbc/get-datasource
                      {:jdbcUrl "jdbc:sqlite:db/database.db"})]
  (with-open [conn (jdbc/get-connection my-datasource)]
    (Function/create
      conn
      "regex_capture"
      (sqlite.db.application-defined-functions.RegexCapture.))
    (jdbc/execute! conn
      ["select regex_capture(?, 'Hello, world!')"
       ", (world)!"])))

=>
[{:regex_capture(?, 'Hello, world!') "world"}]

Magic! SQLite is executing functions defined in Clojure.

Bonus: Automatically compile the SQL functions

To compile our SQLite function on repl launch add the following :main-opts to the :dev alias.

{:paths   ["src" "classes"]
 :deps
 {org.clojure/clojure               {:mvn/version "1.11.1"}
  com.github.seancorfield/next.jdbc {:mvn/version "1.3.874"}
  org.xerial/sqlite-jdbc            {:mvn/version "3.42.0.0"}}
 :aliases
 {:dev
  {:main-opts
   [;;; Ensures application defined functions are compiled
    ;; As they use gen-class to extend org.sqlite.Function
    "-e" "(compile 'sqlite.db.application-defined-functions)"
    "-r"]}}}

This means we don't have to remember to manually compile the SQLite functions (unless you change them during a repl session).

The full example project can be found here.

In a subsequent post I'll be sharing a helper macro that makes writing Application Defined SQL functions more convenient and more performant.