diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index e96e6b5..932c5e0 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -25,11 +25,13 @@ #include #include "queue.h" -#define MAX_ATOM_LENGTH 255 /* from atom.h, not exposed in erlang include */ -#define MAX_PATHNAME 512 /* unfortunately not in sqlite.h. */ +#define MAX_ATOM_LENGTH 255 /* from atom.h, not exposed in erlang include */ +#define MAX_SQLITE_NAME_LENGTH 255 /* Maximum name length. Using longer will return misuse */ +#define MAX_PATHNAME 512 /* unfortunately not in sqlite.h. */ static ErlNifResourceType *esqlite_connection_type = NULL; static ErlNifResourceType *esqlite_statement_type = NULL; +static ErlNifResourceType *esqlite_backup_type = NULL; /* database connection context */ typedef struct { @@ -47,6 +49,10 @@ typedef struct { sqlite3_stmt *statement; } esqlite_statement; +/* data associated with ongoing backup */ +typedef struct { + sqlite3_backup *backup; +} esqlite_backup; typedef enum { cmd_unknown, @@ -61,6 +67,11 @@ typedef enum { cmd_reset, cmd_column_names, cmd_column_types, + cmd_backup_init, + cmd_backup_step, + cmd_backup_remaining, + cmd_backup_pagecount, + cmd_backup_finish, cmd_close, cmd_stop, cmd_insert, @@ -115,36 +126,37 @@ static const char * get_sqlite3_return_code_msg(int r) { switch(r) { - case SQLITE_OK: return "ok"; - case SQLITE_ERROR : return "sqlite_error"; - case SQLITE_INTERNAL: return "internal"; - case SQLITE_PERM: return "perm"; - case SQLITE_ABORT: return "abort"; - case SQLITE_BUSY: return "busy"; - case SQLITE_LOCKED: return "locked"; - case SQLITE_NOMEM: return "nomem"; - case SQLITE_READONLY: return "readonly"; - case SQLITE_INTERRUPT: return "interrupt"; - case SQLITE_IOERR: return "ioerror"; - case SQLITE_CORRUPT: return "corrupt"; - case SQLITE_NOTFOUND: return "notfound"; - case SQLITE_FULL: return "full"; - case SQLITE_CANTOPEN: return "cantopen"; - case SQLITE_PROTOCOL: return "protocol"; - case SQLITE_EMPTY: return "empty"; - case SQLITE_SCHEMA: return "schema"; - case SQLITE_TOOBIG: return "toobig"; - case SQLITE_CONSTRAINT: return "constraint"; - case SQLITE_MISMATCH: return "mismatch"; - case SQLITE_MISUSE: return "misuse"; - case SQLITE_NOLFS: return "nolfs"; - case SQLITE_AUTH: return "auth"; - case SQLITE_FORMAT: return "format"; - case SQLITE_RANGE: return "range"; - case SQLITE_NOTADB: return "notadb"; - case SQLITE_ROW: return "row"; - case SQLITE_DONE: return "done"; + case SQLITE_OK: return "ok"; + case SQLITE_ERROR : return "sqlite_error"; + case SQLITE_INTERNAL: return "internal"; + case SQLITE_PERM: return "perm"; + case SQLITE_ABORT: return "abort"; + case SQLITE_BUSY: return "busy"; + case SQLITE_LOCKED: return "locked"; + case SQLITE_NOMEM: return "nomem"; + case SQLITE_READONLY: return "readonly"; + case SQLITE_INTERRUPT: return "interrupt"; + case SQLITE_IOERR: return "ioerror"; + case SQLITE_CORRUPT: return "corrupt"; + case SQLITE_NOTFOUND: return "notfound"; + case SQLITE_FULL: return "full"; + case SQLITE_CANTOPEN: return "cantopen"; + case SQLITE_PROTOCOL: return "protocol"; + case SQLITE_EMPTY: return "empty"; + case SQLITE_SCHEMA: return "schema"; + case SQLITE_TOOBIG: return "toobig"; + case SQLITE_CONSTRAINT: return "constraint"; + case SQLITE_MISMATCH: return "mismatch"; + case SQLITE_MISUSE: return "misuse"; + case SQLITE_NOLFS: return "nolfs"; + case SQLITE_AUTH: return "auth"; + case SQLITE_FORMAT: return "format"; + case SQLITE_RANGE: return "range"; + case SQLITE_NOTADB: return "notadb"; + case SQLITE_ROW: return "row"; + case SQLITE_DONE: return "done"; } + return "unknown"; } @@ -180,7 +192,7 @@ command_destroy(void *obj) esqlite_command *cmd = (esqlite_command *) obj; if(cmd->env != NULL) - enif_free_env(cmd->env); + enif_free_env(cmd->env); enif_free(cmd); } @@ -194,7 +206,7 @@ command_create() cmd->env = enif_alloc_env(); if(cmd->env == NULL) { - command_destroy(cmd); + command_destroy(cmd); return NULL; } @@ -246,6 +258,18 @@ destruct_esqlite_statement(ErlNifEnv *env, void *arg) stmt->statement = NULL; } +static void +destruct_esqlite_backup(ErlNifEnv *env, void *arg) +{ + esqlite_backup *backup = (esqlite_backup *) arg; + + if(backup->backup) { + sqlite3_backup_finish(backup->backup); + } + + backup->backup = NULL; +} + static ERL_NIF_TERM do_open(ErlNifEnv *env, esqlite_connection *db, const ERL_NIF_TERM arg) { @@ -262,11 +286,11 @@ do_open(ErlNifEnv *env, esqlite_connection *db, const ERL_NIF_TERM arg) */ rc = sqlite3_open(filename, &db->db); if(rc != SQLITE_OK) { - error = make_sqlite3_error_tuple(env, rc, db->db); - sqlite3_close_v2(db->db); - db->db = NULL; + error = make_sqlite3_error_tuple(env, rc, db->db); + sqlite3_close_v2(db->db); + db->db = NULL; - return error; + return error; } sqlite3_busy_timeout(db->db, 2000); @@ -286,7 +310,7 @@ update_callback(void *arg, int sqlite_operation_type, char const *sqlite_databas return; if(!cmd) - return; + return; rowid = enif_make_int64(cmd->env, sqlite_rowid); table = enif_make_string(cmd->env, sqlite_table, ERL_NIF_LATIN1); @@ -304,8 +328,10 @@ update_callback(void *arg, int sqlite_operation_type, char const *sqlite_databas default: return; } + cmd->type = cmd_notification; cmd->arg = enif_make_tuple3(cmd->env, type, table, rowid); + push_command(cmd->env, db, cmd); } @@ -410,11 +436,11 @@ do_prepare(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) ERL_NIF_TERM eos = enif_make_int(env, 0); if(!enif_inspect_iolist_as_binary(env, enif_make_list2(env, arg, eos), &bin)) - return make_error_tuple(env, "not an iolist"); + return make_error_tuple(env, "not an iolist"); stmt = enif_alloc_resource(esqlite_statement_type, sizeof(esqlite_statement)); if(!stmt) - return make_error_tuple(env, "no_memory"); + return make_error_tuple(env, "no_memory"); rc = sqlite3_prepare_v2(conn->db, (char *) bin.data, bin.size, &(stmt->statement), &tail); if(rc != SQLITE_OK) { @@ -495,21 +521,21 @@ do_bind(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt, const ERL_NIF_TERM arg) is_list = enif_get_list_length(env, arg, &list_length); if(!is_list) - return make_error_tuple(env, "bad_arg_list"); + return make_error_tuple(env, "bad_arg_list"); if(parameter_count != list_length) - return make_error_tuple(env, "args_wrong_length"); + return make_error_tuple(env, "args_wrong_length"); sqlite3_reset(stmt); list = arg; for(i=0; i < list_length; i++) { - enif_get_list_cell(env, list, &head, &tail); - r = bind_cell(env, head, stmt, i+1); - if(r == -1) - return make_error_tuple(env, "wrong_type"); - if(r != SQLITE_OK) - return make_sqlite3_error_tuple(env, r, db); - list = tail; + enif_get_list_cell(env, list, &head, &tail); + r = bind_cell(env, head, stmt, i+1); + if(r == -1) + return make_error_tuple(env, "wrong_type"); + if(r != SQLITE_OK) + return make_sqlite3_error_tuple(env, r, db); + list = tail; } return make_atom(env, "ok"); @@ -638,11 +664,10 @@ static ERL_NIF_TERM do_reset(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt) { int rc = sqlite3_reset(stmt); + if(rc != SQLITE_OK) + return make_sqlite3_error_tuple(env, rc, db); - if(rc == SQLITE_OK) - return make_atom(env, "ok"); - - return make_sqlite3_error_tuple(env, rc, db); + return make_atom(env, "ok"); } static ERL_NIF_TERM @@ -710,6 +735,158 @@ do_column_types(ErlNifEnv *env, sqlite3_stmt *stmt) return column_types; } +static ERL_NIF_TERM +do_backup_init(ErlNifEnv *env, sqlite3 *db, const ERL_NIF_TERM arg) +{ + int tuple_arity; + const ERL_NIF_TERM *elements; + sqlite3_backup *backup; + unsigned int size; + char dst_name[MAX_SQLITE_NAME_LENGTH]; + char src_name[MAX_SQLITE_NAME_LENGTH]; + esqlite_connection *src; + esqlite_backup *esqlite_backup; + ERL_NIF_TERM erl_backup_term; + + if(db == NULL) { + return make_error_tuple(env, "dst_closed"); + } + + if(!enif_get_tuple(env, arg, &tuple_arity, &elements)) { + return make_error_tuple(env, "no_tuple"); + } + if(tuple_arity != 3) { + return make_error_tuple(env, "invalid_tuple"); + } + + size = enif_get_string(env, elements[0], dst_name, MAX_PATHNAME, ERL_NIF_LATIN1); + if(size <= 0) + return make_error_tuple(env, "invalid_dst_name"); + + if(!enif_get_resource(env, elements[1], esqlite_connection_type, (void **) &src)) { + return make_error_tuple(env, "invalid_src_db"); + } + if(!src->db) { + return make_error_tuple(env, "src_closed"); + } + + size = enif_get_string(env, elements[2], src_name, MAX_PATHNAME, ERL_NIF_LATIN1); + if(size <= 0) + return make_error_tuple(env, "invalid_src_name"); + + backup = sqlite3_backup_init(db, dst_name, src->db, src_name); + if(backup == NULL) { + return make_sqlite3_error_tuple(env, sqlite3_errcode(db), db); + } + + esqlite_backup = enif_alloc_resource(esqlite_backup_type, sizeof(esqlite_backup)); + if(!esqlite_backup) { + // Release backup resouces + (void) sqlite3_backup_finish(backup); + return make_error_tuple(env, "no_memory"); + } + + esqlite_backup->backup = backup; + erl_backup_term = enif_make_resource(env, esqlite_backup); + enif_release_resource(esqlite_backup); + + return make_ok_tuple(env, erl_backup_term); +} + +static ERL_NIF_TERM +do_backup_step(ErlNifEnv *env, sqlite3 *db, const ERL_NIF_TERM arg) +{ + int tuple_arity; + const ERL_NIF_TERM *elements; + esqlite_backup *esqlite_backup; + int n_page = 0; + int rc; + + if(db == NULL) { + return make_error_tuple(env, "closed"); + } + + if(!enif_get_tuple(env, arg, &tuple_arity, &elements)) { + return make_error_tuple(env, "no_tuple"); + } + if(tuple_arity != 2) { + return make_error_tuple(env, "invalid_tuple"); + } + + if(!enif_get_resource(env, elements[0], esqlite_backup_type, (void **) &esqlite_backup)) { + return make_error_tuple(env, "invalid"); + } + if(!esqlite_backup->backup) { + return make_error_tuple(env, "backup"); + } + + if(!enif_get_int(env, elements[1], &n_page)) { + return make_error_tuple(env, "n_page"); + } + + rc = sqlite3_backup_step(esqlite_backup->backup, n_page); + if(rc == SQLITE_DONE) { + return make_atom(env, "done"); + } + + if(rc != SQLITE_OK) { + return make_sqlite3_error_tuple(env, rc, db); + } + + return make_atom(env, "ok"); +} + +static ERL_NIF_TERM +do_backup_remaining(ErlNifEnv *env, const ERL_NIF_TERM arg) +{ + esqlite_backup *esqlite_backup; + int remaining; + ERL_NIF_TERM remaining_term; + + if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { + return make_error_tuple(env, "invalid"); + } + + remaining = sqlite3_backup_remaining(esqlite_backup->backup); + remaining_term = enif_make_int64(env, remaining); + + return make_ok_tuple(env, remaining_term); +} + +static ERL_NIF_TERM +do_backup_pagecount(ErlNifEnv *env, const ERL_NIF_TERM arg) +{ + esqlite_backup *esqlite_backup; + int pagecount; + ERL_NIF_TERM pagecount_term; + + if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { + return make_error_tuple(env, "invalid"); + } + + pagecount = sqlite3_backup_pagecount(esqlite_backup->backup); + pagecount_term = enif_make_int64(env, pagecount); + + return make_ok_tuple(env, pagecount_term); +} + +static ERL_NIF_TERM +do_backup_finish(ErlNifEnv *env, const ERL_NIF_TERM arg) +{ + esqlite_backup *esqlite_backup; + + if(!enif_get_resource(env, arg, esqlite_backup_type, (void **) &esqlite_backup)) { + return make_error_tuple(env, "invalid"); + } + + if(esqlite_backup->backup) { + (void) sqlite3_backup_finish(esqlite_backup->backup); + esqlite_backup->backup = NULL; + } + + return make_atom(env, "ok"); +} + static ERL_NIF_TERM do_close(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) { @@ -755,6 +932,16 @@ evaluate_command(esqlite_command *cmd, esqlite_connection *conn) return do_column_names(cmd->env, stmt->statement); case cmd_column_types: return do_column_types(cmd->env, stmt->statement); + case cmd_backup_init: + return do_backup_init(cmd->env, conn->db, cmd->arg); + case cmd_backup_step: + return do_backup_step(cmd->env, conn->db, cmd->arg); + case cmd_backup_remaining: + return do_backup_remaining(cmd->env, cmd->arg); + case cmd_backup_pagecount: + return do_backup_pagecount(cmd->env, cmd->arg); + case cmd_backup_finish: + return do_backup_finish(cmd->env, cmd->arg); case cmd_close: return do_close(cmd->env, conn, cmd->arg); case cmd_last_insert_rowid: @@ -763,9 +950,13 @@ evaluate_command(esqlite_command *cmd, esqlite_connection *conn) return do_insert(cmd->env, conn, cmd->arg); case cmd_get_autocommit: return do_get_autocommit(cmd->env, conn); - default: - return make_error_tuple(cmd->env, "invalid_command"); + case cmd_unknown: // not handled + case cmd_stop: // not handled here + case cmd_notification: // not handled here. + break; } + + return make_error_tuple(cmd->env, "invalid_command"); } static ERL_NIF_TERM @@ -917,17 +1108,17 @@ esqlite_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) ErlNifPid pid; if(argc != 4) - return enif_make_badarg(env); + return enif_make_badarg(env); if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); + return enif_make_badarg(env); if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); + return make_error_tuple(env, "invalid_ref"); if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); + return make_error_tuple(env, "invalid_pid"); cmd = command_create(); if(!cmd) - return make_error_tuple(env, "command_create_failed"); + return make_error_tuple(env, "command_create_failed"); /* command */ cmd->type = cmd_exec; @@ -949,17 +1140,17 @@ esqlite_changes(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) ErlNifPid pid; if(argc != 3) - return enif_make_badarg(env); + return enif_make_badarg(env); if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &db)) - return enif_make_badarg(env); + return enif_make_badarg(env); if(!enif_is_ref(env, argv[1])) - return make_error_tuple(env, "invalid_ref"); + return make_error_tuple(env, "invalid_ref"); if(!enif_get_local_pid(env, argv[2], &pid)) - return make_error_tuple(env, "invalid_pid"); + return make_error_tuple(env, "invalid_pid"); cmd = command_create(); if(!cmd) - return make_error_tuple(env, "command_create_failed"); + return make_error_tuple(env, "command_create_failed"); /* command */ cmd->type = cmd_changes; @@ -1251,7 +1442,7 @@ esqlite_column_types(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) ErlNifPid pid; if(argc != 4) - return enif_make_badarg(env); + return enif_make_badarg(env); if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) return enif_make_badarg(env); @@ -1277,6 +1468,179 @@ esqlite_column_types(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) return push_command(env, conn, cmd); } +/* + * Backup functions + * + */ + + +static ERL_NIF_TERM +esqlite_backup_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *destination; + esqlite_command *cmd = NULL; + ErlNifPid pid; + + if(argc != 6) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &destination)) + return enif_make_badarg(env); + // 1 destination name + // 2 source connection with database + // 3 source name + if(!enif_is_ref(env, argv[4])) + return make_error_tuple(env, "invalid_ref"); + if(!enif_get_local_pid(env, argv[5], &pid)) + return make_error_tuple(env, "invalid_pid"); + + cmd = command_create(); + if(!cmd) + return make_error_tuple(env, "command_create_failed"); + + cmd->type = cmd_backup_init; + cmd->ref = enif_make_copy(cmd->env, argv[4]); + cmd->pid = pid; + cmd->arg = enif_make_tuple3(cmd->env, argv[1], argv[2], argv[3]); + + /* Use the connection of the destination database */ + return push_command(env, destination, cmd); +} + +static ERL_NIF_TERM +esqlite_backup_finish(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *conn; + esqlite_backup *backup; + esqlite_command *cmd = NULL; + ErlNifPid pid; + + if(argc != 4) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + return enif_make_badarg(env); + + if(!enif_is_ref(env, argv[2])) + return make_error_tuple(env, "invalid_ref"); + if(!enif_get_local_pid(env, argv[3], &pid)) + return make_error_tuple(env, "invalid_pid"); + + cmd = command_create(); + if(!cmd) + return make_error_tuple(env, "command_create_failed"); + + cmd->type = cmd_backup_finish; + cmd->ref = enif_make_copy(cmd->env, argv[2]); + cmd->pid = pid; + cmd->arg = enif_make_copy(cmd->env, argv[1]); + + return push_command(env, conn, cmd); +} + +static ERL_NIF_TERM +esqlite_backup_step(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *conn; + esqlite_backup *backup; + esqlite_command *cmd = NULL; + ErlNifPid pid; + + if(argc != 5) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + return enif_make_badarg(env); + // 1 backup + if(!enif_is_number(env, argv[2])) + return make_error_tuple(env, "invalid_count"); + if(!enif_is_ref(env, argv[3])) + return make_error_tuple(env, "invalid_ref"); + if(!enif_get_local_pid(env, argv[4], &pid)) + return make_error_tuple(env, "invalid_pid"); + + cmd = command_create(); + if(!cmd) + return make_error_tuple(env, "command_create_failed"); + + cmd->type = cmd_backup_step; + cmd->ref = enif_make_copy(cmd->env, argv[3]); + cmd->pid = pid; + cmd->arg = enif_make_tuple2(cmd->env, argv[1], argv[2]); + + return push_command(env, conn, cmd); +} + +/* + * Get the remaining pagecount of the backup. + */ + +static ERL_NIF_TERM +esqlite_backup_remaining(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *conn; + esqlite_backup *backup; + esqlite_command *cmd = NULL; + ErlNifPid pid; + + if(argc != 4) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + return enif_make_badarg(env); + // backup 1 + if(!enif_is_ref(env, argv[2])) + return make_error_tuple(env, "invalid_ref"); + if(!enif_get_local_pid(env, argv[3], &pid)) + return make_error_tuple(env, "invalid_pid"); + + cmd = command_create(); + if(!cmd) + return make_error_tuple(env, "command_create_failed"); + + cmd->type = cmd_backup_remaining; + cmd->ref = enif_make_copy(cmd->env, argv[2]); + cmd->pid = pid; + cmd->arg = enif_make_copy(cmd->env, argv[1]); + + return push_command(env, conn, cmd); +} + +/* + * Get the total pagecount of the backup + */ + +static ERL_NIF_TERM +esqlite_backup_pagecount(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) +{ + esqlite_connection *conn; + esqlite_backup *backup; + esqlite_command *cmd = NULL; + ErlNifPid pid; + + if(argc != 4) + return enif_make_badarg(env); + + if(!enif_get_resource(env, argv[0], esqlite_connection_type, (void **) &conn)) + return enif_make_badarg(env); + // backup 1 + if(!enif_is_ref(env, argv[2])) + return make_error_tuple(env, "invalid_ref"); + if(!enif_get_local_pid(env, argv[3], &pid)) + return make_error_tuple(env, "invalid_pid"); + + cmd = command_create(); + if(!cmd) + return make_error_tuple(env, "command_create_failed"); + + cmd->type = cmd_backup_pagecount; + cmd->ref = enif_make_copy(cmd->env, argv[2]); + cmd->pid = pid; + cmd->arg = enif_make_copy(cmd->env, argv[1]); + + return push_command(env, conn, cmd); +} + /* * Interrupt currently active query. */ @@ -1331,18 +1695,21 @@ on_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) { ErlNifResourceType *rt; - rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_connection_type", - destruct_esqlite_connection, ERL_NIF_RT_CREATE, NULL); - if(!rt) - return -1; + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_connection_type", destruct_esqlite_connection, + ERL_NIF_RT_CREATE, NULL); + if(!rt) return -1; esqlite_connection_type = rt; - rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_statement_type", - destruct_esqlite_statement, ERL_NIF_RT_CREATE, NULL); - if(!rt) - return -1; + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_statement_type", destruct_esqlite_statement, + ERL_NIF_RT_CREATE, NULL); + if(!rt) return -1; esqlite_statement_type = rt; + rt = enif_open_resource_type(env, "esqlite3_nif", "esqlite_backup_type", destruct_esqlite_backup, + ERL_NIF_RT_CREATE, NULL); + if(!rt) return -1; + esqlite_backup_type = rt; + atom_esqlite3 = make_atom(env, "esqlite3"); return 0; @@ -1374,6 +1741,13 @@ static ErlNifFunc nif_funcs[] = { {"bind", 5, esqlite_bind}, {"column_names", 4, esqlite_column_names}, {"column_types", 4, esqlite_column_types}, + + {"backup_init", 6, esqlite_backup_init}, + {"backup_step", 5, esqlite_backup_step}, + {"backup_remaining", 4, esqlite_backup_remaining}, + {"backup_pagecount", 4, esqlite_backup_pagecount}, + {"backup_finish", 4, esqlite_backup_finish}, + {"interrupt", 1, esqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"close", 3, esqlite_close} }; diff --git a/doc/overview.edoc b/doc/overview.edoc new file mode 100644 index 0000000..5428e27 --- /dev/null +++ b/doc/overview.edoc @@ -0,0 +1,47 @@ +@author Maas-Maarten Zeeman + +@title eSqlite Documentation + +@doc + +eSqlite is a library which makes it possible to use sqlite databases in erlang. It is implemented +as a NIF, which means that the sqlite database engine is linked to the erlang virtual machine. + +
+ +== Why Sqlite? == + +Sqlite is a implementation of SQL as a library. This means that you don't run a separate SQL server +that your program communicates with, but you embed the SQL implementation directly in your program. +Sqlite stores its data in a single file. The file format is portable between different machine +architectures. It supports atomic transactions and it is possible to access the file by multiple +processes and different programs. + +
+ +== Using == + +The main api is the {@link esqlite3} module. It contains more high level api methods to use the database. + +``` +%% Open a database +{ok, Conn} = esqlite3:open("my-database.db"). +''' + +This opens a connection to a database. When the file does not exist yet, it is created. +It is possible to share the connection between different processes. + +Sqlite supports a URI database naming scheme which makes it possible to open a database +in read-only mode, or use shared memory databases. More information on this can be found at: +[https://sqlite.org/uri.html#uri_filenames_in_sqlite] + +For example: + +``` +%% Open a shared memory database with transactional capabilities +{ok, Conn} = esqlite3:open("file:memdb1?mode=memory&cache=shared"). +''' + +This opens a shared memory database. Other processes can open the same database name and +access and store data consistently. + diff --git a/rebar.config.script b/rebar.config.script index 4a605d7..f58ba6a 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -23,9 +23,15 @@ CFlags = end. [ - {require_min_otp_vsn, "21"}, + {minimum_otp_vsn, "21.0"}, - {xref_checks, [undefined_function_calls]}, + {erl_opts, [debug_info, warnings_as_errors]}, + + {xref_checks, [undefined_function_calls, + undefined_functions, + locals_not_used, + deprecated_function_calls, + deprecated_functions]}, {port_env, [ %% Default darwin ldflags causes loading of system sqlite. Removed -bundle flag. @@ -56,7 +62,14 @@ CFlags = unmatched_returns, error_handling, race_conditions, - underspecs + underspecs, + unknown ]} - ]} + ]}, + + {edoc_opts, [{preprocess, true}, + {sort_functions, false}]}, + + {hex, [{doc, edoc}]} + ]. diff --git a/src/esqlite3.erl b/src/esqlite3.erl index ae17e34..e4250bf 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -1,10 +1,3 @@ -%% @author Maas-Maarten Zeeman -%% @copyright 2011 - 2017 Maas-Maarten Zeeman - -%% @doc Erlang API for sqlite3 databases - -%% Copyright 2011 - 2017 Maas-Maarten Zeeman -%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -16,52 +9,63 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. +%% +%% @author Maas-Maarten Zeeman +%% @copyright 2011 - 2022 Maas-Maarten Zeeman +%% @doc Erlang API for sqlite3 databases -module(esqlite3). -author("Maas-Maarten Zeeman "). %% higher-level export --export([open/1, open/2, - set_update_hook/2, set_update_hook/3, - exec/2, exec/3, exec/4, - changes/1, changes/2, - insert/2, - last_insert_rowid/1, - get_autocommit/1, - get_autocommit/2, - prepare/2, prepare/3, - step/1, step/2, - reset/1, - bind/2, bind/3, - fetchone/1, - fetchall/1, - fetchall/2, - fetchall/3, - column_names/1, column_names/2, - column_types/1, column_types/2, - close/1, close/2, - flush/0 - ]). +-export([ + open/1, open/2, + close/1, close/2, + set_update_hook/2, set_update_hook/3, + exec/2, exec/3, exec/4, + changes/1, changes/2, + insert/2, insert/3, + last_insert_rowid/1, + get_autocommit/1, get_autocommit/2, + prepare/2, prepare/3, + step/1, step/2, + reset/1, + bind/2, bind/3, + fetchone/1, + fetchall/1, fetchall/2, fetchall/3, + column_names/1, column_names/2, + column_types/1, column_types/2, + backup_init/4, backup_init/5, + backup_finish/1, backup_finish/2, + backup_remaining/1, backup_remaining/2, + backup_pagecount/1, backup_pagecount/2, + backup_step/2, backup_step/3, + flush/0 +]). -export([q/2, q/3, q/4, map/3, map/4, foreach/3, foreach/4]). -define(DEFAULT_TIMEOUT, infinity). -define(DEFAULT_CHUNK_SIZE, 5000). -%% - -record(connection, { - raw_connection :: esqlite_nif:raw_connection() + raw_connection :: esqlite3_nif:raw_connection() }). -record(statement, { - raw_connection :: esqlite_nif:raw_connection(), - raw_statement :: esqlite_nif:raw_statement() + raw_connection :: esqlite3_nif:raw_connection(), + raw_statement :: esqlite3_nif:raw_statement() +}). + +-record(backup, { + raw_connection :: esqlite3_nif:raw_connection(), + raw_backup :: esqlite3_nif:raw_backup() }). -type connection() :: #connection{}. -type statement() :: #statement{}. --type sql() :: esqlite_nif:sql(). +-type backup() :: #backup{}. +-type sql() :: esqlite3_nif:sql(). %% erlang -> sqlite type conversions %% @@ -77,7 +81,7 @@ -type row() :: tuple(). % tuple of cell_type -type cell_type() :: undefined | integer() | binary() | float(). --export_types([connection/0, statement/0, sql/0, row/0, row_id/0, cell_type/0]). +-export_type([connection/0, statement/0, sql/0, row/0, rowid/0, cell_type/0]). %% @doc Opens a sqlite3 database mentioned in Filename. %% @@ -113,6 +117,27 @@ open(Filename, Timeout) -> Error end. +%% @doc Close the database +-spec close(connection()) -> ok | {error, _}. +close(Connection) -> + close(Connection, ?DEFAULT_TIMEOUT). + +%% @doc Close the database +-spec close(connection(), timeout()) -> ok | {error, _}. +close(#connection{raw_connection=RawConnection}, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:close(RawConnection, Ref, self()), + receive_answer(RawConnection, Ref, Timeout). + +%% @doc Flush any stale answers left in the mailbox of the current process. +%% This can happen if there has been a timeout. Normally the nif functions +%% are called with the default 'infinite' timeout, so calling this is not +%% needed. +-spec flush() -> ok. +flush() -> + flush_answers(). + + %% @doc Subscribe to database notifications. When rows are inserted deleted %% or updates, the process will receive messages: %% ```{insert, string(), rowid()}''' @@ -126,13 +151,18 @@ open(Filename, Timeout) -> set_update_hook(Pid, Connection) -> set_update_hook(Pid, Connection, ?DEFAULT_TIMEOUT). -%% @doc Same as set_update_hook, but with an additional timeout parameter. +%% @doc Same as set_update_hook/2, but with an additional timeout parameter. +%% -spec set_update_hook(pid(), connection(), timeout()) -> ok | {error, term()}. set_update_hook(Pid, #connection{raw_connection=RawConnection}, Timeout) -> Ref = make_ref(), ok = esqlite3_nif:set_update_hook(RawConnection, Ref, self(), Pid), receive_answer(RawConnection, Ref, Timeout). +%% +%% q +%% + %% @doc Execute a sql statement, returns a list with tuples. -spec q(sql(), connection()) -> list(row()) | {error, _}. q(Sql, Connection) -> @@ -165,6 +195,10 @@ q(Sql, Args, Connection, Timeout) -> Error end. +%% +%% map +%% + %% @doc Execute statement and return a list with the result of F for each row. -spec map(Fun, sql(), connection()) -> list(Type) when Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), @@ -200,6 +234,10 @@ map(Fun, Sql, Args, Connection) -> Error end. +%% +%% foreach +%% + %% @doc Execute statement and call F with each row. -spec foreach(Fun, sql(), connection()) -> ok when Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), @@ -234,57 +272,8 @@ foreach(F, Sql, Args, Connection) -> end. %% --spec foreach_s(Fun, statement()) -> ok when - Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), - Row :: row(), - ColumnNames :: tuple(). -foreach_s(Fun, Statement) when is_function(Fun, 1) -> - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - ok; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - Fun(Row), - foreach_s(Fun, Statement) - end; -foreach_s(Fun, Statement) when is_function(Fun, 2) -> - ColumnNames = column_names(Statement), - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - ok; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - Fun(ColumnNames, Row), - foreach_s(Fun, Statement) - end. - +%% fetchall %% --spec map_s(Fun, statement()) -> list(Type) when - Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), - Row :: row(), - ColumnNames :: tuple(), - Type :: term(). -map_s(Fun, Statement) when is_function(Fun, 1) -> - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - []; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - [Fun(Row) | map_s(Fun, Statement)] - end; -map_s(Fun, Statement) when is_function(Fun, 2) -> - ColumnNames = column_names(Statement), - case try_multi_step(Statement, 1, [], 0) of - {'$done', []} -> - []; - {error, _} = Error -> - Error; - {rows, [Row | []]} -> - [Fun(ColumnNames, Row) | map_s(Fun, Statement)] - end. %% -spec fetchone(statement()) -> tuple(). @@ -321,42 +310,6 @@ fetchall(Statement, ChunkSize, Timeout) -> {error, _} = E -> E end. -%% return rows in reverse order --spec fetchall_internal(statement(), pos_integer(), list(row()), timeout()) -> - {'$done', list(row())} | - {error, _}. -fetchall_internal(Statement, ChunkSize, Rest, Timeout) -> - case try_multi_step(Statement, ChunkSize, Rest, 0, Timeout) of - {rows, Rows} -> fetchall_internal(Statement, ChunkSize, Rows, Timeout); - Else -> Else - end. - -%% Try a number of steps, when the database is busy, -%% return rows in revers order -try_multi_step(Statement, ChunkSize, Rest, Tries) -> - try_multi_step(Statement, ChunkSize, Rest, Tries, ?DEFAULT_TIMEOUT). - -%% Try a number of steps, when the database is busy, -%% return rows in revers order --spec try_multi_step(statement(), pos_integer(), list(tuple()), non_neg_integer(), timeout()) -> - {rows, list(tuple())} | - {'$done', list(tuple())} | - {error, term()}. -try_multi_step(_Statement, _ChunkSize, _Rest, Tries, _Timeout) when Tries > 5 -> - throw(too_many_tries); -try_multi_step(Statement, ChunkSize, Rest, Tries, Timeout) -> - case multi_step(Statement, ChunkSize, Timeout) of - {'$busy', Rows} -> %% core can fetch a number of rows (rows < ChunkSize) per 'multi_step' call and then get busy... - erlang:display({"busy", Tries}), - timer:sleep(100 * Tries), - try_multi_step(Statement, ChunkSize, Rows ++ Rest, Tries + 1, Timeout); - {rows, Rows} -> - {rows, Rows ++ Rest}; - {'$done', Rows} -> - {'$done', Rows ++ Rest}; - Else -> Else - end. - %% @doc Execute Sql statement. %% -spec exec(sql(), connection()) -> ok | {error, _}. @@ -422,7 +375,6 @@ last_insert_rowid(#connection{raw_connection=RawConnection}, Timeout) -> ok = esqlite3_nif:last_insert_rowid(RawConnection, Ref, self()), receive_answer(RawConnection, Ref, Timeout). -%% @doc Get autocommit %% @doc Check if the connection is in auto-commit mode. %% See: [https://sqlite.org/c3ref/get_autocommit.html] for more details. %% @@ -475,18 +427,6 @@ step(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, Timeo Else -> Else end. -%% make multiple sqlite steps per call -%% return rows in reverse order --spec multi_step(term(), pos_integer(), timeout()) -> - {rows, list(tuple())} | - {'$busy', list(tuple())} | - {'$done', list(tuple())} | - {error, _}. -multi_step(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, ChunkSize, Timeout) -> - Ref = make_ref(), - ok = esqlite3_nif:multi_step(RawConnection, RawStatement, ChunkSize, Ref, self()), - receive_answer(RawConnection, Ref, Timeout). - %% @doc Reset the prepared statement back to its initial state. %% -spec reset(statement()) -> ok | {error, _}. @@ -532,29 +472,190 @@ column_types(#statement{raw_statement=RawStatement, raw_connection=RawConnection ok = esqlite3_nif:column_types(RawConnection, RawStatement, Ref, self()), receive_answer(RawConnection, Ref, Timeout). -%% @doc Close the database --spec close(connection()) -> ok | {error, _}. -close(Connection) -> - close(Connection, ?DEFAULT_TIMEOUT). - -%% @doc Close the database --spec close(connection(), timeout()) -> ok | {error, _}. -close(#connection{raw_connection=RawConnection}, Timeout) -> +%% @doc make multiple sqlite steps per call return rows in reverse order +%% +-spec multi_step(term(), pos_integer(), timeout()) -> + {rows, list(tuple())} | + {'$busy', list(tuple())} | + {'$done', list(tuple())} | + {error, _}. +multi_step(#statement{raw_statement=RawStatement, raw_connection=RawConnection}, ChunkSize, Timeout) -> Ref = make_ref(), - ok = esqlite3_nif:close(RawConnection, Ref, self()), + ok = esqlite3_nif:multi_step(RawConnection, RawStatement, ChunkSize, Ref, self()), receive_answer(RawConnection, Ref, Timeout). +%% +%% Backup API +%% -%% @doc Flush any stale answers left in the mailbox of the current process. -%% This can happen if there has been a timeout. Normally the nif functions -%% are called with the default 'infinite' timeout, so calling this is not -%% needed. --spec flush() -> ok. -flush() -> - flush_answers(). +% @doc Initialize a backup procedure. +%% +-spec backup_init(connection(), string(), connection(), string()) -> {ok, backup()} | {error, _}. +backup_init(Dest, DestName, Src, SrcName) -> + backup_init(Dest, DestName, Src, SrcName, ?DEFAULT_TIMEOUT). + +%% @doc Like backup_init/4, but with an extra timeout value. +%% +-spec backup_init(connection(), string(), connection(), string(), timeout()) -> {ok, backup()} | {error, _}. +backup_init(#connection{raw_connection=Dest}, DestName, #connection{raw_connection=Src}, SrcName, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:backup_init(Dest, DestName, Src, SrcName, Ref, self()), + case receive_answer(Dest, Ref, Timeout) of + {ok, RawBackup} when is_reference(RawBackup) -> + {ok, #backup{raw_connection=Dest, raw_backup=RawBackup}}; + {error, _} = Error -> + Error + end. -%% Internal functions +%% @doc Release the resources held by the backup. +-spec backup_finish(backup()) -> ok | {error, _}. +backup_finish(Backup) -> + backup_finish(Backup, ?DEFAULT_TIMEOUT). +%% @doc Like backup_finish/1, but with an extra timeout. +-spec backup_finish(backup(), timeout()) -> ok | {error, _}. +backup_finish(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:backup_finish(Conn, Back, Ref, self()), + receive_answer(Conn, Ref, Timeout). + +%% @doc Do a backup step. +-spec backup_step(backup(), integer()) -> ok | {error, _}. +backup_step(Backup, NPage) -> + backup_step(Backup, NPage, ?DEFAULT_TIMEOUT). + +%% @doc Do a backup step. +-spec backup_step(backup(), integer(), timeout()) -> ok | {error, _}. +backup_step(#backup{raw_connection=Conn, raw_backup=Back}, NPage, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:backup_step(Conn, Back, NPage, Ref, self()), + receive_answer(Conn, Ref, Timeout). + + +%% @doc Get the remaining number of pages which need to be backed up. +-spec backup_remaining(backup()) -> {ok, pos_integer()} | {error, _}. +backup_remaining(Backup) -> + backup_remaining(Backup, ?DEFAULT_TIMEOUT). + +%% @doc Get the remaining number of pages which need to be backed up. +-spec backup_remaining(backup(), timeout()) -> {ok, pos_integer()} | {error, _}. +backup_remaining(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:backup_remaining(Conn, Back, Ref, self()), + case receive_answer(Conn, Ref, Timeout) of + {ok, R} when is_integer(R) -> + {ok, R}; + {error, _}=E -> + E + end. + +%% @doc Get the remaining number of pages which need to be backed up. +-spec backup_pagecount(backup()) -> {ok, pos_integer()} | {error, _}. +backup_pagecount(Backup) -> + backup_pagecount(Backup, ?DEFAULT_TIMEOUT). + +%% @doc Get the remaining number of pages which need to be backed up. +-spec backup_pagecount(backup(), timeout()) -> {ok, pos_integer()} | {error, _}. +backup_pagecount(#backup{raw_connection=Conn, raw_backup=Back}, Timeout) -> + Ref = make_ref(), + ok = esqlite3_nif:backup_pagecount(Conn, Back, Ref, self()), + case receive_answer(Conn, Ref, Timeout) of + {ok, R} when is_integer(R) -> + {ok, R}; + {error, _}=E -> + E + end. + +%% +%% Helpers +%% + +-spec foreach_s(Fun, statement()) -> ok when + Fun :: fun((Row) -> any()) | fun((ColumnNames, Row) -> any()), + Row :: row(), + ColumnNames :: tuple(). +foreach_s(Fun, Statement) when is_function(Fun, 1) -> + case try_multi_step(Statement, 1, [], 0) of + {'$done', []} -> + ok; + {error, _} = Error -> + Error; + {rows, [Row | []]} -> + Fun(Row), + foreach_s(Fun, Statement) + end; +foreach_s(Fun, Statement) when is_function(Fun, 2) -> + ColumnNames = column_names(Statement), + case try_multi_step(Statement, 1, [], 0) of + {'$done', []} -> + ok; + {error, _} = Error -> + Error; + {rows, [Row | []]} -> + Fun(ColumnNames, Row), + foreach_s(Fun, Statement) + end. + +-spec map_s(Fun, statement()) -> list(Type) when + Fun :: fun((Row) -> Type) | fun((ColumnNames, Row) -> Type), + Row :: row(), + ColumnNames :: tuple(), + Type :: term(). +map_s(Fun, Statement) when is_function(Fun, 1) -> + case try_multi_step(Statement, 1, [], 0) of + {'$done', []} -> + []; + {error, _} = Error -> + Error; + {rows, [Row | []]} -> + [Fun(Row) | map_s(Fun, Statement)] + end; +map_s(Fun, Statement) when is_function(Fun, 2) -> + ColumnNames = column_names(Statement), + case try_multi_step(Statement, 1, [], 0) of + {'$done', []} -> + []; + {error, _} = Error -> + Error; + {rows, [Row | []]} -> + [Fun(ColumnNames, Row) | map_s(Fun, Statement)] + end. + +%% return rows in reverse order +-spec fetchall_internal(statement(), pos_integer(), list(row()), timeout()) -> + {'$done', list(row())} | + {error, _}. +fetchall_internal(Statement, ChunkSize, Rest, Timeout) -> + case try_multi_step(Statement, ChunkSize, Rest, 0, Timeout) of + {rows, Rows} -> fetchall_internal(Statement, ChunkSize, Rows, Timeout); + Else -> Else + end. + +%% Try a number of steps, when the database is busy, +%% return rows in revers order +try_multi_step(Statement, ChunkSize, Rest, Tries) -> + try_multi_step(Statement, ChunkSize, Rest, Tries, ?DEFAULT_TIMEOUT). + +%% Try a number of steps, when the database is busy, +%% return rows in revers order +-spec try_multi_step(statement(), pos_integer(), list(tuple()), non_neg_integer(), timeout()) -> + {rows, list(tuple())} | + {'$done', list(tuple())} | + {error, term()}. +try_multi_step(_Statement, _ChunkSize, _Rest, Tries, _Timeout) when Tries > 5 -> + throw(too_many_tries); +try_multi_step(Statement, ChunkSize, Rest, Tries, Timeout) -> + case multi_step(Statement, ChunkSize, Timeout) of + {'$busy', Rows} -> %% core can fetch a number of rows (rows < ChunkSize) per 'multi_step' call and then get busy... + erlang:display({"busy", Tries}), + timer:sleep(100 * Tries), + try_multi_step(Statement, ChunkSize, Rows ++ Rest, Tries + 1, Timeout); + {rows, Rows} -> + {rows, Rows ++ Rest}; + {'$done', Rows} -> + {'$done', Rows ++ Rest}; + Else -> Else + end. receive_answer(RawConnection, Ref, Timeout) -> receive diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index 02a1678..2e7e3b6 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -1,10 +1,3 @@ -%% @author Maas-Maarten Zeeman -%% @copyright 2011 - 2017 Maas-Maarten Zeeman - -%% @doc Low level erlang API for sqlite3 databases - -%% Copyright 2011 - 2017 Maas-Maarten Zeeman -%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -16,35 +9,47 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. +%% +%% @author Maas-Maarten Zeeman +%% @copyright 2011 - 2022 Maas-Maarten Zeeman +%% +%% @doc Low level erlang API for sqlite3 databases. -module(esqlite3_nif). -author("Maas-Maarten Zeeman "). %% low-level exports --export([start/0, - open/4, - set_update_hook/4, - exec/4, - changes/3, - insert/4, - last_insert_rowid/3, - get_autocommit/3, - prepare/4, - multi_step/5, - reset/4, - finalize/4, - bind/5, - column_names/4, - column_types/4, - interrupt/1, - close/3 - ]). +-export([ + start/0, + open/4, + set_update_hook/4, + exec/4, + changes/3, + insert/4, + last_insert_rowid/3, + get_autocommit/3, + prepare/4, + multi_step/5, + reset/4, + finalize/4, + bind/5, + column_names/4, + column_types/4, + backup_init/6, + backup_step/5, + backup_remaining/4, + backup_pagecount/4, + backup_finish/4, + interrupt/1, + close/3 +]). -type raw_connection() :: reference(). -type raw_statement() :: reference(). +-type raw_backup() :: reference(). -type sql() :: iodata(). --export_type([raw_connection/0, raw_statement/0, sql/0]). +-export_type([raw_connection/0, raw_statement/0, raw_backup/0, sql/0]). -on_load(init/0). @@ -137,6 +142,31 @@ column_names(_Db, _Stmt, _Ref, _Dest) -> column_types(_Db, _Stmt, _Ref, _Dest) -> erlang:nif_error(nif_library_not_loaded). +%% @doc Initialize a backup procedure of a database. +-spec backup_init(raw_connection(), string(), raw_connection(), string(), reference(), pid()) -> ok | {error, _}. +backup_init(_DestDb, _DestName, _SourceDb, _SourceName, _Ref, _Dest) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Do a backup step. +-spec backup_step(raw_connection(), raw_backup(), integer(), reference(), pid()) -> ok | {error, _}. +backup_step(_Db, _Backup, _NPages, _Ref, _Dest) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Get the amount of remaining pages which need to be backed up. +-spec backup_remaining(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. +backup_remaining(_Db, _Backup, _Ref, _Dest) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Get the total number of pages which need to be backed up. +-spec backup_pagecount(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. +backup_pagecount(_Db, _Backup, _Ref, _Dest) -> + erlang:nif_error(nif_library_not_loaded). + +%% @doc Finish the backup. +-spec backup_finish(raw_connection(), raw_backup(), reference(), pid()) -> ok | {error, _}. +backup_finish(_Db, _Backup, _Ref, _Dest) -> + erlang:nif_error(nif_library_not_loaded). + %% @doc Interrupt all active queries. -spec interrupt(raw_connection()) -> ok. interrupt(_Db) -> diff --git a/test/dbs/.empty b/test/dbs/.empty new file mode 100644 index 0000000..e69de29 diff --git a/test/esqlite_test.erl b/test/esqlite_test.erl index ca3eacd..2bac6c2 100644 --- a/test/esqlite_test.erl +++ b/test/esqlite_test.erl @@ -6,8 +6,12 @@ -include_lib("eunit/include/eunit.hrl"). +-define(DB1, "./test/dbs/temp_db1.db"). +-define(DB2, "./test/dbs/temp_db2.db"). + open_single_database_test() -> - {ok, _C1} = esqlite3:open("test.db"), + cleanup(), + {ok, _C1} = esqlite3:open(?DB1), ok. close_test() -> @@ -26,13 +30,19 @@ close_test() -> ok. open_multiple_same_databases_test() -> - {ok, _C1} = esqlite3:open("test.db"), - {ok, _C2} = esqlite3:open("test.db"), + cleanup(), + + {ok, _C1} = esqlite3:open(?DB1), + {ok, _C2} = esqlite3:open(?DB1), + + cleanup(), ok. open_multiple_different_databases_test() -> - {ok, _C1} = esqlite3:open("test1.db"), - {ok, _C2} = esqlite3:open("test2.db"), + cleanup(), + {ok, _C1} = esqlite3:open(?DB1), + {ok, _C2} = esqlite3:open(?DB2), + cleanup(), ok. get_autocommit_test() -> @@ -293,7 +303,6 @@ reset_test() -> ok. - foreach_test() -> {ok, Db} = esqlite3:open(":memory:"), @@ -441,6 +450,65 @@ prepare_and_close_connection_test() -> ok. +backup_test() -> + cleanup(), + + {ok, Dest} = esqlite3:open(?DB1), + {ok, Source} = esqlite3:open(?DB2), + + {ok, Backup} = esqlite3:backup_init(Dest, "main", Source, "main"), + {ok, 0} = esqlite3:backup_remaining(Backup), + {ok, 0} = esqlite3:backup_pagecount(Backup), + done = esqlite3:backup_step(Backup, 1), + + cleanup(), + + ok. + +backup1_test() -> + cleanup(), + + {ok, Dest} = esqlite3:open(?DB1), + {ok, Source} = esqlite3:open(?DB2), + + [] = esqlite3:q("create table test(one, two)", Source), + [] = esqlite3:q("begin;", Source), + [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), + [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), + [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), + [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), + [] = esqlite3:q("insert into test values(randomblob(10000), randomblob(10000));", Source), + [] = esqlite3:q("commit;", Source), + + [{5}] = esqlite3:q("select count(*) from test", Source), + {error, {sqlite_error, "no such table: test"}} = esqlite3:q("select count(*) from test", Dest), + + {ok, Backup} = esqlite3:backup_init(Dest, "main", Source, "main"), + + {ok, 0} = esqlite3:backup_remaining(Backup), + {ok, 0} = esqlite3:backup_pagecount(Backup), + + %% Backup 1 page. + ok = esqlite3:backup_step(Backup, 1), + + {ok, 26} = esqlite3:backup_remaining(Backup), + {ok, 27} = esqlite3:backup_pagecount(Backup), + + %% Do all the remaining pages. + done = esqlite3:backup_step(Backup, -1), + + {ok, 0} = esqlite3:backup_remaining(Backup), + {ok, 27} = esqlite3:backup_pagecount(Backup), + + ok = esqlite3:backup_finish(Backup), + + [{5}] = esqlite3:q("select count(*) from test", Dest), + + cleanup(), + + ok. + + sqlite_version_test() -> {ok, Db} = esqlite3:open(":memory:"), {ok, Stmt} = esqlite3:prepare("select sqlite_version() as sqlite_version;", Db), @@ -496,5 +564,20 @@ garbage_collect_test() -> receive after 500 -> ok end, erlang:garbage_collect(), + ok. +%% +%% Helpers +%% + +cleanup() -> + rm_rf(?DB1), + rm_rf(?DB2). + +rm_rf(Filename) -> + case file:delete(Filename) of + ok -> ok; + {error, _} -> ok + end. +