bookmarkfs/src/db.c
2024-12-31 18:09:03 +08:00

431 lines
10 KiB
C

/**
* bookmarkfs/src/db.c
* ----
*
* Copyright (C) 2024 CismonX <admin@cismon.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "db.h"
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#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;
}