/** * bookmarkfs/src/db.c * ---- * * Copyright (C) 2024 CismonX * * This file is part of BookmarkFS. * * BookmarkFS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * BookmarkFS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with BookmarkFS. If not, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "db.h" #include #include #include #include #include "xstd.h" struct db_check_ctx { int status; }; struct db_pragma_ctx { char const *val; size_t val_len; int status; }; // Forward declaration start static int db_check_cb (void *, sqlite3_stmt *); static int db_pragma_cb (void *, sqlite3_stmt *); static void safeincr (sqlite3_context *, int, sqlite3_value **); // Forward declaration end static int db_check_cb ( void *user_data, sqlite3_stmt *stmt ) { struct db_check_ctx *ctx = user_data; size_t nbytes = sqlite3_column_bytes(stmt, 0); unsigned char const *result = sqlite3_column_text(stmt, 0); xassert(result != NULL); ctx->status = 0; if (nbytes != strlen("ok") || 0 != memcmp("ok", result, nbytes)) { log_printf("%s: expected 'ok', got '%s'", sqlite3_sql(stmt), result); ctx->status = -EIO; } return 0; } static int db_pragma_cb ( void *user_data, sqlite3_stmt *stmt ) { struct db_pragma_ctx *ctx = user_data; size_t val_len = sqlite3_column_bytes(stmt, 0); unsigned char const *val = sqlite3_column_text(stmt, 0); xassert(val != NULL); ctx->status = 0; if (val_len != ctx->val_len || 0 != memcmp(ctx->val, val, val_len)) { log_printf("%s: expected '%s', got '%s'", sqlite3_sql(stmt), ctx->val, val); ctx->status = -1; } return 0; } static void safeincr ( sqlite3_context *dbctx, int argc, sqlite3_value **argv ) { debug_assert(argc == 1); sqlite3_value *val = argv[0]; if (unlikely(SQLITE_INTEGER != sqlite3_value_type(val))) { sqlite3_result_error(dbctx, "value is not an integer", -1); return; } int64_t ival = sqlite3_value_int64(val); if (unlikely(ival == INT64_MAX)) { sqlite3_result_error(dbctx, "integer overflow", -1); return; } sqlite3_result_int64(dbctx, ival + 1); } int db_check ( sqlite3 *db ) { sqlite3_stmt *stmt = db_prepare(db, SQL_PRAGMA("quick_check(1)"), false); if (stmt == NULL) { return -1; } struct db_check_ctx qctx; ssize_t nrows = db_query(stmt, NULL, 0, false, db_check_cb, &qctx); if (nrows < 0) { return nrows; } xassert(nrows == 1); return qctx.status; } int db_config ( sqlite3 *db, struct db_conf_item const *items, size_t items_cnt ) { for (size_t idx = 0; idx < items_cnt; ++idx) { struct db_conf_item const *item = items + idx; int result; int status = sqlite3_db_config(db, item->op, item->value, &result); if (unlikely(status != SQLITE_OK)) { log_printf("sqlite3_db_config(): %s", sqlite3_errstr(status)); return status; } if (unlikely(result != item->value)) { log_puts("sqlite3_db_config() failed"); return -1; } } return 0; } int db_errno ( int err ) { if ((err & SQLITE_BUSY) == SQLITE_BUSY) { return EBUSY; } if (err == SQLITE_FULL) { return ENOSPC; } if (err == SQLITE_CONSTRAINT_UNIQUE) { // NOTE: This is an internal error and should not be // implicitly exposed to the filesystem. return EEXIST; } return EIO; } ssize_t db_exec ( sqlite3 *db, char const *sql, size_t sql_len, sqlite3_stmt **stmt_ptr, int64_t *values_buf ) { sqlite3_stmt *stmt = NULL; if (stmt_ptr != NULL) { stmt = *stmt_ptr; if (stmt != NULL) { goto query; } } stmt = db_prepare(db, sql, sql_len, stmt_ptr != NULL); if (unlikely(stmt == NULL)) { return -EIO; } if (stmt_ptr != NULL) { *stmt_ptr = stmt; } query: return db_query(stmt, NULL, 0, stmt_ptr != NULL, values_buf == NULL ? NULL : db_query_i64_cb, values_buf); } int db_fcntl ( sqlite3 *db, int op, int val ) { int status = sqlite3_file_control(db, "main", op, &val); if (status != SQLITE_OK) { log_printf("sqlite3_file_control: %s", sqlite3_errstr(status)); return status; } return 0; } sqlite3 * db_open ( char const *path, bool readonly ) { sqlite3 *db; int flags = SQLITE_OPEN_READWRITE; if (readonly) { flags = SQLITE_OPEN_READONLY; } flags |= SQLITE_OPEN_EXRESCODE; if (SQLITE_OK != sqlite3_open_v2(path, &db, flags, NULL)) { log_printf("sqlite3_open_v2(): %s", sqlite3_errmsg(db)); goto fail; } if (!readonly) { if (0 != sqlite3_db_readonly(db, "main")) { log_puts("cannot open database for read/write"); goto fail; } } return db; fail: sqlite3_close(db); return NULL; } int db_pragma ( sqlite3 *db, struct db_pragma_item const *items, size_t items_cnt ) { for (size_t idx = 0; idx < items_cnt; ++idx) { struct db_pragma_item const *item = items + idx; char const *sql = item->sql; if (sql == NULL) { continue; } size_t sql_len = item->sql_len; size_t val_len = item->val_len; struct db_pragma_ctx pragma_ctx = { .val = sql + sql_len - val_len, .val_len = val_len, }; bool again = true; query: ; sqlite3_stmt *stmt = db_prepare(db, sql, sql_len, false); if (unlikely(stmt == NULL)) { return -1; } ssize_t nrows = db_query(stmt, NULL, 0, false, db_pragma_cb, &pragma_ctx); if (nrows > 0) { if (pragma_ctx.status != 0) { return -1; } } else if (nrows == 0) { if (!again) { log_printf("bad pragma: %s", sql); return -1; } again = false; // strlen(" = ") == 3 sql_len = pragma_ctx.val - sql - 3; goto query; } } return 0; } sqlite3_stmt * db_prepare ( sqlite3 *db, char const *sql, size_t sql_len, bool persistent ) { sqlite3_stmt *stmt; int flags = 0; if (persistent) { flags |= SQLITE_PREPARE_PERSISTENT; } int err = sqlite3_prepare_v3(db, sql, sql_len, flags, &stmt, NULL); if (unlikely(err != SQLITE_OK)) { log_printf("sqlite3_prepare_v3(): %s, %s", sql, sqlite3_errmsg(db)); return NULL; } return stmt; } ssize_t db_query ( sqlite3_stmt *stmt, struct db_stmt_bind_item const *bind_items, int bind_cnt, bool persistent, db_query_row_func *row_cb, void *user_data ) { int err; ssize_t result = 0; if (bind_items == NULL) { goto fetch_rows; } for (int i = 0; i < bind_cnt; ) { struct db_stmt_bind_item const *bind_item = bind_items + i++; union db_value val = bind_item->val; ssize_t len = bind_item->len; if (len < 0) { if (val.i64 < 0) { continue; } err = sqlite3_bind_int64(stmt, i, val.i64); } else { if (val.text == NULL) { continue; } err = sqlite3_bind_text(stmt, i, val.text, len, SQLITE_STATIC); } if (unlikely(err != SQLITE_OK)) { log_printf("sqlite3_bind(): %d: %s", i, sqlite3_errstr(err)); result = -err; goto end; } } fetch_rows: #if 1 && defined(BOOKMARKFS_DEBUG) { char *sql = sqlite3_expanded_sql(stmt); debug_printf("%s", sql); sqlite3_free(sql); } #endif for (size_t nrows = 0; ; ) { err = sqlite3_step(stmt); switch (err) { case SQLITE_ROW: break; case SQLITE_DONE: no_more_rows: result = nrows; // fallthrough default: goto end; } ++nrows; if (row_cb == NULL) { continue; } if (0 != row_cb(user_data, stmt)) { goto no_more_rows; } } end: if (persistent) { if (bind_cnt > 0) { sqlite3_clear_bindings(stmt); } err = sqlite3_reset(stmt); } else { err = sqlite3_finalize(stmt); } if (err != SQLITE_OK) { char const *errmsg; if ((err & SQLITE_ERROR) == SQLITE_ERROR) { errmsg = sqlite3_errmsg(sqlite3_db_handle(stmt)); } else { errmsg = sqlite3_errstr(err); } log_printf("sqlite3_step(): %s", errmsg); result = -db_errno(err); } return result; } int db_query_i64_cb ( void *user_data, sqlite3_stmt *stmt ) { int64_t *arr = user_data; int ncols = sqlite3_column_count(stmt); for (int i = 0; i < ncols; ++i) { arr[i] = sqlite3_column_int64(stmt, i); } return 1; } int db_register_safeincr ( sqlite3 *db ) { int flags = SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS | SQLITE_DIRECTONLY; int err = sqlite3_create_function(db, "safeincr", 1, flags | SQLITE_UTF8, NULL, safeincr, NULL, NULL); if (unlikely(err != SQLITE_OK)) { log_printf("sqlite3_create_function(): %s", sqlite3_errstr(err)); return -1; } return 0; }