]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-sql: driver-sqlite - Implement statement API
authorAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 10 Oct 2025 07:51:46 +0000 (10:51 +0300)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Thu, 29 Jan 2026 16:45:30 +0000 (16:45 +0000)
src/lib-sql/driver-sqlite.c
src/lib-sql/test-sql-sqlite.c

index 7911a42f24800872159fba04349f8c8a0f543b76..7cc98344a21127f45dd71d15a54e264e465a56b8 100644 (file)
@@ -3,6 +3,8 @@
 #include "lib.h"
 #include "eacces-error.h"
 #include "array.h"
+#include "hash.h"
+#include "llist.h"
 #include "ioloop.h"
 #include "str.h"
 #include "hex-binary.h"
@@ -31,11 +33,19 @@ struct sqlite_db {
        bool connected:1;
 };
 
+struct sqlite_prepared_statement {
+       struct sql_prepared_statement api;
+       sqlite3_stmt *handle;
+       char *error;
+       /* Prepared statement cannot be used concurrently by multiple statements */
+       bool locked:1;
+};
+
 struct sqlite_statement {
        struct sql_statement api;
-       struct event *event;
+       struct sqlite_prepared_statement *prep_stmt;
        sqlite3_stmt *handle;
-       char *error;
+       const char *error;
        int rc;
 };
 
@@ -124,12 +134,74 @@ static struct event_category event_category_sqlite = {
 
 static const char*
 driver_sqlite_result_str(struct sql_db *_db, int rc);
+static int
+driver_sqlite_prepared_statement_reopen(struct sqlite_db *db,
+                                       struct sqlite_prepared_statement *stmt);
+
+static void driver_sqlite_finalize_handle(struct sql_db *db,
+                                         struct sqlite3_stmt **_handle,
+                                         const char *query)
+{
+       sqlite3_stmt *handle = *_handle;
+       i_assert(handle != NULL);
+       *_handle = NULL;
+
+       int rc = sqlite3_finalize(handle);
+       if (rc == SQLITE_NOMEM) {
+               i_fatal_status(FATAL_OUTOFMEM, "sqlite3_finalize(%s) failed: %s (%d)",
+                              query, sqlite3_errstr(rc), rc);
+       } else if (rc != SQLITE_OK) {
+               const char *errstr = driver_sqlite_result_str(db, rc);
+               e_debug(db->event, "sqlite3_finalize(%s) failed: %s",
+                       query, errstr);
+       }
+}
+
+static void driver_sqlite_finalize_prepared_statements(struct sqlite_db *db)
+{
+       if (!hash_table_is_created(db->api.prepared_stmt_hash))
+               return;
+       struct hash_iterate_context *iter =
+               hash_table_iterate_init(db->api.prepared_stmt_hash);
+       char *key ATTR_UNUSED;
+       struct sql_prepared_statement *value;
+
+       while (hash_table_iterate(iter, db->api.prepared_stmt_hash, &key, &value)) {
+               /* finalize handle */
+               struct sqlite_prepared_statement *stmt =
+                       container_of(value, struct sqlite_prepared_statement, api);
+               if (stmt->handle != NULL)
+                       driver_sqlite_finalize_handle(&db->api, &stmt->handle,
+                                                     stmt->api.query_template);
+       }
+
+       hash_table_iterate_deinit(&iter);
+}
+
+static void driver_sqlite_reopen_prepared_statements(struct sqlite_db *db)
+{
+       if (!hash_table_is_created(db->api.prepared_stmt_hash))
+               return;
+       struct hash_iterate_context *iter =
+               hash_table_iterate_init(db->api.prepared_stmt_hash);
+       char *key ATTR_UNUSED;
+       struct sql_prepared_statement *value;
+
+       while (hash_table_iterate(iter, db->api.prepared_stmt_hash, &key, &value)) {
+               struct sqlite_prepared_statement *stmt =
+                       container_of(value, struct sqlite_prepared_statement, api);
+               driver_sqlite_prepared_statement_reopen(db, stmt);
+       }
+
+       hash_table_iterate_deinit(&iter);
+}
 
 static void driver_sqlite_disconnect(struct sql_db *_db)
 {
        struct sqlite_db *db = container_of(_db, struct sqlite_db, api);
 
        sql_connection_log_finished(_db);
+       driver_sqlite_finalize_prepared_statements(db);
        int rc = sqlite3_close(db->sqlite);
        if (rc != SQLITE_OK) {
                e_error(db->api.event, "sqlite3_close() failed: %s (%d)",
@@ -218,6 +290,7 @@ static int driver_sqlite_connect(struct sql_db *_db)
        case SQLITE_OK:
                db->connected = TRUE;
                sqlite3_busy_timeout(db->sqlite, sqlite_busy_timeout);
+               driver_sqlite_reopen_prepared_statements(db);
                return 1;
        case SQLITE_READONLY:
                i_assert(!db->set->readonly);
@@ -410,29 +483,80 @@ driver_sqlite_result_log(const struct sqlite_result *result, const char *query)
 }
 
 static struct sql_statement *
-driver_sqlite_statement_init(struct sql_db *_db, const char *query_template)
+driver_sqlite_statement_init_prepared(struct sql_prepared_statement *_prep_stmt)
 {
+       struct sqlite_prepared_statement *prep_stmt =
+               container_of(_prep_stmt, struct sqlite_prepared_statement, api);
+       struct sqlite_db *db = container_of(_prep_stmt->db, struct sqlite_db, api);
+       i_assert(!prep_stmt->locked);
+
        pool_t pool = pool_alloconly_create("sqlite statement", 1024);
+       struct sqlite_statement *stmt = p_new(pool, struct sqlite_statement, 1);
+       stmt->api.pool = pool;
+       stmt->api.db = _prep_stmt->db;
+       stmt->api.query_template = _prep_stmt->query_template;
+
+       /* handle is only valid if we are connected */
+       if (driver_sqlite_connect(_prep_stmt->db) < 0) {
+               i_free(prep_stmt->error);
+               prep_stmt->error = i_strdup(
+                       driver_sqlite_result_str(_prep_stmt->db,
+                                                db->connect_rc));
+       }
+
+       i_assert(prep_stmt->handle != NULL || prep_stmt->error != NULL);
+
+       stmt->error = p_strdup(stmt->api.pool, prep_stmt->error);
+       stmt->prep_stmt = prep_stmt;
+       stmt->handle = prep_stmt->handle;
+       prep_stmt->locked = TRUE;
+
+       return &stmt->api;
+}
+
+static void
+driver_sqlite_release_prepared_statement(struct sqlite_statement *stmt)
+{
+       i_assert(stmt->prep_stmt != NULL);
+       i_assert(stmt->prep_stmt->locked);
+
+       struct sqlite_prepared_statement *prep_stmt = stmt->prep_stmt;
+       prep_stmt->locked = FALSE;
+       stmt->prep_stmt = NULL;
+       stmt->handle = NULL;
+
+       if (prep_stmt->handle == NULL)
+               return;
+
+       sqlite3_reset(prep_stmt->handle);
+       sqlite3_clear_bindings(prep_stmt->handle);
+}
+
+static struct sql_statement *
+driver_sqlite_statement_init(struct sql_db *_db, const char *query_template)
+{
        struct sqlite_db *db = container_of(_db, struct sqlite_db, api);
+       pool_t pool = pool_alloconly_create("sqlite statement", 1024);
        struct sqlite_statement *stmt = p_new(pool, struct sqlite_statement, 1);
        const char *tail;
-       int rc;
        stmt->api.db = _db;
+       stmt->api.pool = pool;
        stmt->api.query_template = p_strdup(pool, query_template);
-       stmt->event = event_create(_db->event);
 
        if (driver_sqlite_connect(_db) < 0) {
-               rc = db->connect_rc;
+               stmt->rc = db->connect_rc;
        } else {
-               rc = sqlite3_prepare_v2(db->sqlite, query_template, -1,
+               stmt->rc = sqlite3_prepare_v2(db->sqlite, query_template, -1,
                                        &stmt->handle, &tail);
        }
-       if (!SQLITE_IS_OK(rc)) {
-               stmt->error = p_strdup(pool, driver_sqlite_result_str(_db, rc));
-               stmt->rc = rc;
+       if (!SQLITE_IS_OK(stmt->rc)) {
+               stmt->error = p_strdup(pool,
+                                      driver_sqlite_result_str(_db,
+                                                               stmt->rc));
        } else if (*tail != '\0') {
-               e_warning(_db->event, "Query '%s': '%s' unparsed",
-                         query_template, tail);
+               stmt->error = p_strdup_printf(stmt->api.pool, "'%s' unparsed",
+                                             tail);
+               stmt->rc = SQLITE_ERROR;
        }
        return &stmt->api;
 }
@@ -441,26 +565,17 @@ static void driver_sqlite_statement_abort(struct sql_statement *_stmt)
 {
        struct sqlite_statement *stmt =
                container_of(_stmt, struct sqlite_statement, api);
-       int rc = SQLITE_OK;
-       if (stmt->handle != NULL)
-               rc = sqlite3_finalize(stmt->handle);
-       if (rc == SQLITE_NOMEM) {
-               i_fatal_status(FATAL_OUTOFMEM, "finalize failed: %s (%d)",
-                              sqlite3_errstr(rc), rc);
-       } else if (rc != SQLITE_OK) {
-               e_warning(stmt->event, "finalize failed: %s (%d)",
-                         sqlite3_errstr(rc), rc);
+
+       if (stmt->prep_stmt != NULL) {
+               driver_sqlite_release_prepared_statement(stmt);
+       } else if (stmt->handle != NULL) {
+               driver_sqlite_finalize_handle(stmt->api.db, &stmt->handle,
+                                             stmt->api.query_template);
        }
-       event_unref(&stmt->event);
-}
 
-static void driver_sqlite_statement_free(struct sqlite_statement **_stmt)
-{
-       struct sqlite_statement *stmt = *_stmt;
-       *_stmt = NULL;
-       if (stmt == NULL)
-               return;
-       driver_sqlite_statement_abort(&stmt->api);
+       i_assert(stmt->handle == NULL);
+       i_assert(stmt->prep_stmt == NULL);
+       pool_unref(&stmt->api.pool);
 }
 
 static int driver_sqlite_exec_query(struct sqlite_db *db, const char *query,
@@ -546,7 +661,9 @@ static void driver_sqlite_result_free(struct sql_result *_result)
        if (_result->callback)
                return;
 
-       driver_sqlite_statement_free(&result->stmt);
+       driver_sqlite_statement_abort(&result->stmt->api);
+       result->stmt = NULL;
+
        event_unref(&result->api.event);
        i_free(result->row);
        i_free(result->error);
@@ -788,6 +905,173 @@ driver_sqlite_escape_blob(struct sql_db *_db ATTR_UNUSED,
        return str_c(str);
 }
 
+static struct sql_result *
+driver_sqlite_statement_query_s(struct sql_statement *_stmt)
+{
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       struct sqlite_result *result =
+               driver_sqlite_statement_result_prepare(stmt);
+
+       driver_sqlite_result_log(result, sql_statement_get_log_query(_stmt));
+       return &result->api;
+}
+
+static int
+driver_sqlite_prepared_statement_reopen(struct sqlite_db *db,
+                                       struct sqlite_prepared_statement *prep_stmt)
+{
+       /* Maybe it works this time round */
+       i_free(prep_stmt->error);
+       prep_stmt->api.db = &db->api;
+        /* driver_sqlite_result_str() ignores rc for connect failures */
+       int rc = SQLITE_OK;
+       if (driver_sqlite_connect(&db->api) < 0 ||
+           (rc = sqlite3_prepare_v2(db->sqlite, prep_stmt->api.query_template,
+                                    -1, &prep_stmt->handle, NULL)) != SQLITE_OK) {
+               prep_stmt->error =
+                       i_strdup(driver_sqlite_result_str(&db->api, rc));
+               return -1;
+       } else {
+               e_debug(db->api.event, "Prepared query '%s'",
+                       prep_stmt->api.query_template);
+       }
+       return 0;
+}
+
+static struct sql_prepared_statement *
+driver_sqlite_prepared_statement_init(struct sql_db *_db,
+                                     const char *query_template)
+{
+       struct sqlite_db *db = container_of(_db, struct sqlite_db, api);
+       struct sqlite_prepared_statement *prep_stmt =
+               i_new(struct sqlite_prepared_statement, 1);
+       prep_stmt->api.query_template = i_strdup(query_template);
+       prep_stmt->api.refcount = 1;
+       prep_stmt->api.db = _db;
+
+       (void)driver_sqlite_prepared_statement_reopen(db, prep_stmt);
+
+       return &prep_stmt->api;
+}
+
+static void
+driver_sqlite_prepared_statement_deinit(struct sql_prepared_statement *_prep_stmt)
+{
+       struct sqlite_prepared_statement *prep_stmt =
+               container_of(_prep_stmt, struct sqlite_prepared_statement, api);
+       if (prep_stmt->handle != NULL) {
+               driver_sqlite_finalize_handle(prep_stmt->api.db, &prep_stmt->handle,
+                                             prep_stmt->api.query_template);
+       }
+       i_free(prep_stmt->api.query_template);
+       i_free(prep_stmt->error);
+       i_free(_prep_stmt);
+}
+
+static void
+driver_sqlite_bind_error(const char *func, struct sqlite_statement *stmt,
+                        unsigned int column_idx)
+{
+       const char *errstr = driver_sqlite_result_str(stmt->api.db, stmt->rc);
+       if (stmt->rc == SQLITE_NOMEM) {
+               i_fatal_status(FATAL_OUTOFMEM, "%s(%u) for query '%s': %s",
+                              func, column_idx, stmt->api.query_template,
+                              errstr);
+       } else if (stmt->rc != SQLITE_OK) {
+               stmt->error = p_strdup_printf(stmt->api.pool, "%s(%u) for query '%s': %s",
+                       func, column_idx, stmt->api.query_template,
+                       errstr);
+       }
+}
+
+static void
+driver_sqlite_statement_bind_str(struct sql_statement *_stmt,
+                                unsigned int column_idx, const char *value)
+{
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       if (stmt->rc != SQLITE_OK)
+               return;
+       stmt->rc = sqlite3_bind_text(stmt->handle, column_idx + 1, value, -1,
+                                    SQLITE_TRANSIENT);
+       driver_sqlite_bind_error("sqlite3_bind_text", stmt, column_idx);
+}
+
+static void
+driver_sqlite_statement_bind_binary(struct sql_statement *_stmt,
+                                   unsigned int column_idx, const void *value,
+                                   size_t value_size)
+{
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       if (stmt->rc != SQLITE_OK)
+               return;
+       stmt->rc = sqlite3_bind_blob(stmt->handle, column_idx + 1, value,
+                                    value_size, SQLITE_TRANSIENT);
+       driver_sqlite_bind_error("sqlite3_bind_blob", stmt, column_idx);
+}
+
+static void
+driver_sqlite_statement_bind_int64(struct sql_statement *_stmt,
+                                  unsigned int column_idx, int64_t value)
+{
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       if (stmt->rc != SQLITE_OK)
+               return;
+       stmt->rc = sqlite3_bind_int64(stmt->handle, column_idx + 1, value);
+       driver_sqlite_bind_error("sqlite3_bind_int64", stmt, column_idx);
+}
+
+static void
+driver_sqlite_statement_bind_double(struct sql_statement *_stmt,
+                                   unsigned int column_idx, double value)
+{
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       if (stmt->rc != SQLITE_OK)
+               return;
+       stmt->rc = sqlite3_bind_double(stmt->handle, column_idx + 1, value);
+       driver_sqlite_bind_error("sqlite3_bind_double", stmt, column_idx);
+}
+
+static void
+driver_sqlite_statement_bind_uuid(struct sql_statement *stmt,
+                                 unsigned int column_idx, const guid_128_t uuid)
+
+{
+       const char *uuid_str = guid_128_to_uuid_string(uuid, FORMAT_RECORD);
+       driver_sqlite_statement_bind_str(stmt, column_idx, uuid_str);
+}
+
+static void
+driver_sqlite_update_stmt(struct sql_transaction_context *_ctx,
+                         struct sql_statement *_stmt,
+                         unsigned int *affected_rows)
+{
+       struct sqlite_transaction_context *ctx =
+               container_of(_ctx, struct sqlite_transaction_context, ctx);
+       struct sqlite_db *db =
+               container_of(_ctx->db, struct sqlite_db, api);
+       struct sqlite_statement *stmt =
+               container_of(_stmt, struct sqlite_statement, api);
+       /* execute statement */
+       struct sql_result *_res = driver_sqlite_statement_query_s(&stmt->api);
+       struct sqlite_result *res =
+               container_of(_res, struct sqlite_result, api);
+       if (sql_result_next_row(_res) < 0) {
+               ctx->rc = res->rc;
+               i_free(ctx->error);
+               ctx->error = i_strdup(driver_sqlite_result_str(stmt->api.db, ctx->rc));
+               if (affected_rows != NULL)
+                       *affected_rows = 0;
+       } else if (SQLITE_IS_OK(res->rc) && affected_rows != NULL)
+               *affected_rows = sqlite3_changes(db->sqlite);
+
+       sql_result_unref(_res);
+}
+
 const struct sql_db driver_sqlite_db = {
        .name = "sqlite",
        .flags =
@@ -812,6 +1096,24 @@ const struct sql_db driver_sqlite_db = {
                .update = driver_sqlite_update,
 
                .escape_blob = driver_sqlite_escape_blob,
+
+               .prepared_statement_init = driver_sqlite_prepared_statement_init,
+               .prepared_statement_deinit = driver_sqlite_prepared_statement_deinit,
+
+               .statement_init = driver_sqlite_statement_init,
+               .statement_init_prepared = driver_sqlite_statement_init_prepared,
+               .statement_abort = driver_sqlite_statement_abort,
+
+               .statement_bind_str = driver_sqlite_statement_bind_str,
+               .statement_bind_binary = driver_sqlite_statement_bind_binary,
+               .statement_bind_int64 = driver_sqlite_statement_bind_int64,
+               .statement_bind_double = driver_sqlite_statement_bind_double,
+               .statement_bind_uuid = driver_sqlite_statement_bind_uuid,
+
+               .statement_query_s = driver_sqlite_statement_query_s,
+
+               .update_stmt = driver_sqlite_update_stmt,
+
        }
 };
 
index 80ff18fb271fa7b550feb427bb828fbb288452fd..caccddcd433c69cdce07e600d32f5d01c1218ae3 100644 (file)
@@ -8,7 +8,11 @@
 static const char sql_create_db[] =
 "CREATE TABLE bar(\n"
 "  foo VARCHAR(255)\n"
-");\n";
+");\n"
+"CREATE TABLE test2(\n"
+"   str VARCHAR(255), uuid VARCHAR(36),\n"
+"   num INT, blob BLOB\n"
+")\n";
 
 static void setup_database(struct sql_db *sql)
 {
@@ -58,8 +62,68 @@ static void test_sql_sqlite(void)
        test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_LAST);
 
        sql_result_unref(cursor);
-       sql_unref(&sql);
 
+       struct sql_prepared_statement *prep_stmt =
+               sql_prepared_statement_init(sql, "INSERT INTO bar VALUES(?)");
+       struct sql_statement *stmt =
+               sql_statement_init_prepared(prep_stmt);
+       sql_statement_bind_str(stmt, 0, "value3");
+       cursor = sql_statement_query_s(&stmt);
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_LAST);
+       sql_result_unref(cursor);
+
+       stmt = sql_statement_init(sql, "SELECT foo FROM bar WHERE foo = ?");
+       sql_statement_bind_str(stmt, 0, "value3");
+       cursor = sql_statement_query_s(&stmt);
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_OK);
+       test_assert_ucmp(sql_result_get_fields_count(cursor), ==, 1);
+       test_assert_strcmp(sql_result_get_field_name(cursor, 0), "foo");
+       test_assert_strcmp(sql_result_get_field_value(cursor, 0), "value3");
+       sql_result_unref(cursor);
+       sql_prepared_statement_unref(&prep_stmt);
+
+       stmt = sql_statement_init(sql, "INSERT INTO test2 VALUES(?,?,?,?)");
+       sql_statement_bind_str(stmt, 0, "test_str");
+       guid_128_t uuid;
+       int ret = guid_128_from_uuid_string("426b3821-3c6c-4ed7-a936-ec8d664c53d0", uuid);
+       i_assert(ret == 0);
+       sql_statement_bind_uuid(stmt, 1, uuid);
+       sql_statement_bind_int64(stmt, 2, 123456);
+       sql_statement_bind_binary(stmt, 3, "\xFF\xFF\x00\x00\xFF", 5);
+       cursor = sql_statement_query_s(&stmt);
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_LAST);
+       sql_result_unref(cursor);
+
+       stmt = sql_statement_init(sql, "SELECT * FROM test2");
+       cursor = sql_statement_query_s(&stmt);
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_OK);
+       test_assert_ucmp(sql_result_get_fields_count(cursor), ==, 4);
+       test_assert_strcmp(sql_result_get_field_name(cursor, 0), "str");
+       test_assert_strcmp(sql_result_get_field_value(cursor, 0), "test_str");
+       test_assert_strcmp(sql_result_get_field_name(cursor, 1), "uuid");
+       test_assert_strcmp(sql_result_get_field_value(cursor, 1), "426b3821-3c6c-4ed7-a936-ec8d664c53d0");
+       test_assert_strcmp(sql_result_get_field_name(cursor, 2), "num");
+       test_assert_strcmp(sql_result_get_field_value(cursor, 2), "123456");
+       size_t size;
+       const unsigned char *value =
+               sql_result_get_field_value_binary(cursor, 3, &size);
+       test_assert_ucmp(size, ==, 5);
+       test_assert_memcmp(value, "\xFF\xFF\x00\x00\xFF", 5);
+       sql_result_unref(cursor);
+
+       prep_stmt = sql_prepared_statement_init(sql, "SELECT foo FROM bar WHERE foo = ?");
+       sql_disconnect(sql);
+       stmt = sql_statement_init_prepared(prep_stmt);
+       sql_statement_bind_str(stmt, 0, "value3");
+       cursor = sql_statement_query_s(&stmt);
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_OK);
+       test_assert_ucmp(sql_result_get_fields_count(cursor), ==, 1);
+       test_assert_strcmp(sql_result_get_field_name(cursor, 0), "foo");
+       test_assert_strcmp(sql_result_get_field_value(cursor, 0), "value3");
+       sql_result_unref(cursor);
+       sql_prepared_statement_unref(&prep_stmt);
+
+       sql_unref(&sql);
        driver_sqlite_deinit();
        sql_drivers_deinit_without_drivers();
        settings_simple_deinit(&set);