From: Aki Tuomi Date: Fri, 10 Oct 2025 07:51:46 +0000 (+0300) Subject: lib-sql: driver-sqlite - Implement statement API X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=610f94b0c4b485ab24abba3ed43bc27845ebb9b7;p=thirdparty%2Fdovecot%2Fcore.git lib-sql: driver-sqlite - Implement statement API --- diff --git a/src/lib-sql/driver-sqlite.c b/src/lib-sql/driver-sqlite.c index 7911a42f24..7cc98344a2 100644 --- a/src/lib-sql/driver-sqlite.c +++ b/src/lib-sql/driver-sqlite.c @@ -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, + } }; diff --git a/src/lib-sql/test-sql-sqlite.c b/src/lib-sql/test-sql-sqlite.c index 80ff18fb27..caccddcd43 100644 --- a/src/lib-sql/test-sql-sqlite.c +++ b/src/lib-sql/test-sql-sqlite.c @@ -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);