Merge pull request #81 from mmzeeman/remove-thread

Modernize Nif
This commit is contained in:
MM Zeeman
2022-05-29 23:22:00 +02:00
committed by GitHub
14 changed files with 2146 additions and 2826 deletions

View File

@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
otp_version: [21,22,23,24]
otp_version: [22,23,24]
os: [ubuntu-latest]
container:

4
.gitignore vendored
View File

@@ -11,3 +11,7 @@ _build
c_src/**/*.d
erl_crash.dump
tags
doc/*
!doc/style.css
!doc/overview.edoc
!doc/SQLite370.svg

View File

@@ -13,10 +13,9 @@ in the nif library or the sqlite database can crash the entire Erlang
VM. If you do not want to take this risk, it is always possible to
access the sqlite nif from a separate erlang node.
Special care has been taken not to block the scheduler of the calling
process. This is done by handling all commands from erlang within a
lightweight thread. The erlang scheduler will get control back when
the command has been added to the command-queue of the thread.
Special care has been taken not to block the normal erlang scheduler
of the calling process. This is done by handling neccesary commands
from erlang by using a dirty scheduler.
SQLite Compile Options
----------------------
@@ -66,3 +65,20 @@ INSERT INTO table VALUES("abcd", 1234);
Will not work, because it sees the value 'abcd' as a SQL object
value, and not a string literal.
Version 0.8.0
-------------
This version is a major derivation from previous versions. When
I started with this library it was implemented by using a separate
os level thread per connection. At the time this was the only way
to use functions in C which take longer to process than 1ms.
A lot has changed since then. The VM now has dirty schedulers
which make it possible to remove the thread per connection.
This makes it possible to open a lot more connections. On the
SQLite side some things have also changed. Extended error codes,
introspection into the internals. This release modernizes the
integration. In some places the API is no longer compatible and
will require small changes. In order to ease this process the
library now has typespecs, and the documentation was extended.

File diff suppressed because it is too large Load Diff

View File

@@ -1,195 +0,0 @@
// This file is part of Emonk released under the MIT license.
// See the LICENSE file for more information.
/* Adapted by: Maas-Maarten Zeeman <mmzeeman@xs4all.nl */
#include <assert.h>
#include <stdio.h>
#include "queue.h"
struct qitem_t
{
struct qitem_t* next;
void* data;
};
typedef struct qitem_t qitem;
struct queue_t
{
ErlNifMutex *lock;
ErlNifCond *cond;
qitem *head;
qitem *tail;
void *message;
int length;
};
queue *
queue_create()
{
queue *ret;
ret = (queue *) enif_alloc(sizeof(struct queue_t));
if(ret == NULL) goto error;
ret->lock = NULL;
ret->cond = NULL;
ret->head = NULL;
ret->tail = NULL;
ret->message = NULL;
ret->length = 0;
ret->lock = enif_mutex_create("queue_lock");
if(ret->lock == NULL) goto error;
ret->cond = enif_cond_create("queue_cond");
if(ret->cond == NULL) goto error;
return ret;
error:
if(ret->lock != NULL)
enif_mutex_destroy(ret->lock);
if(ret->cond != NULL)
enif_cond_destroy(ret->cond);
if(ret != NULL)
enif_free(ret);
return NULL;
}
void
queue_destroy(queue *queue)
{
ErlNifMutex *lock;
ErlNifCond *cond;
int length;
enif_mutex_lock(queue->lock);
lock = queue->lock;
cond = queue->cond;
length = queue->length;
queue->lock = NULL;
queue->cond = NULL;
queue->head = NULL;
queue->tail = NULL;
queue->length = -1;
enif_mutex_unlock(lock);
assert(length == 0 && "Attempting to destroy a non-empty queue.");
enif_cond_destroy(cond);
enif_mutex_destroy(lock);
enif_free(queue);
}
int
queue_has_item(queue *queue)
{
int ret;
enif_mutex_lock(queue->lock);
ret = (queue->head != NULL);
enif_mutex_unlock(queue->lock);
return ret;
}
int
queue_push(queue *queue, void *item)
{
qitem * entry = (qitem *) enif_alloc(sizeof(struct qitem_t));
if(entry == NULL)
return 0;
entry->data = item;
entry->next = NULL;
enif_mutex_lock(queue->lock);
assert(queue->length >= 0 && "Invalid queue size at push");
if(queue->tail != NULL)
queue->tail->next = entry;
queue->tail = entry;
if(queue->head == NULL)
queue->head = queue->tail;
queue->length += 1;
enif_cond_signal(queue->cond);
enif_mutex_unlock(queue->lock);
return 1;
}
void*
queue_pop(queue *queue)
{
qitem *entry;
void* item;
enif_mutex_lock(queue->lock);
/* Wait for an item to become available.
*/
while(queue->head == NULL)
enif_cond_wait(queue->cond, queue->lock);
assert(queue->length >= 0 && "Invalid queue size at pop.");
/* Woke up because queue->head != NULL
* Remove the entry and return the payload.
*/
entry = queue->head;
queue->head = entry->next;
entry->next = NULL;
if(queue->head == NULL) {
assert(queue->tail == entry && "Invalid queue state: Bad tail.");
queue->tail = NULL;
}
queue->length -= 1;
enif_mutex_unlock(queue->lock);
item = entry->data;
enif_free(entry);
return item;
}
int
queue_send(queue *queue, void *item)
{
enif_mutex_lock(queue->lock);
assert(queue->message == NULL && "Attempting to send multiple messages.");
queue->message = item;
enif_cond_signal(queue->cond);
enif_mutex_unlock(queue->lock);
return 1;
}
void *
queue_receive(queue *queue)
{
void *item;
enif_mutex_lock(queue->lock);
/* Wait for an item to become available.
*/
while(queue->message == NULL)
enif_cond_wait(queue->cond, queue->lock);
item = queue->message;
queue->message = NULL;
enif_mutex_unlock(queue->lock);
return item;
}

View File

@@ -1,24 +0,0 @@
// This file is part of Emonk released under the MIT license.
// See the LICENSE file for more information.
/* adapted by: Maas-Maarten Zeeman <mmzeeman@xs4all.nl */
#ifndef ESQLITE_QUEUE_H
#define ESQLITE_QUEUE_H
#include "erl_nif.h"
typedef struct queue_t queue;
queue * queue_create();
void queue_destroy(queue *queue);
int queue_has_item(queue *queue);
int queue_push(queue *queue, void* item);
void* queue_pop(queue *queue);
int queue_send(queue *queue, void* item);
void* queue_receive(queue *);
#endif

67
doc/SQLite370.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -1,10 +1,10 @@
@author Maas-Maarten Zeeman <mmzeeman@xs4all.nl>
@title eSqlite Documentation
@title ESQLite Documentation
@doc
eSqlite is a library which makes it possible to use sqlite databases in erlang. It is implemented
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.
<hr />

76
doc/style.css Normal file
View File

@@ -0,0 +1,76 @@
/* standard EDoc style sheet */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
margin-left: .25in;
margin-right: .2in;
margin-top: 0.2in;
margin-bottom: 0.2in;
color: #000000;
background-color: #ffffff;
}
h1,h2 {
margin-left: -0.2in;
}
div.navbar {
background-color: #add8e6;
padding: 0.2em;
}
h2.indextitle {
padding: 0.4em;
background-color: #add8e6;
}
h3.function,h3.typedecl {
background-color: #add8e6;
padding-left: 1em;
}
div.spec {
margin-left: 2em;
background-color: #eeeeee;
}
a.module {
text-decoration:none
}
a.module:hover {
background-color: #eeeeee;
}
ul.definitions {
list-style-type: none;
}
ul.index {
list-style-type: none;
background-color: #eeeeee;
}
/*
* Minor style tweaks
*/
ul {
list-style-type: square;
}
table {
border-collapse: collapse;
}
td {
padding: 3px;
vertical-align: middle;
}
/*
Tune styles
*/
table[summary="navigation bar"] {
background-image: url('SQLite370.svg');
background-size: 110px;
background-repeat: no-repeat;
background-position: center;
}
code, p>tt, a>tt {
font-size: 1.2em;
}
p {
line-height: 1.5;
}

View File

@@ -1,5 +1,5 @@
NifSharedSources = ["c_src/esqlite3_nif.c", "c_src/queue.c"].
NifSharedSources = ["c_src/esqlite3_nif.c"].
NifStaticSources = NifSharedSources ++ ["c_src/sqlite3/sqlite3.c"].
CFlagsDefault = "$CFLAGS -Os -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=1 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_USE_ALLOCA -DSQLITE_OMIT_AUTOINIT -DSQLITE_USE_URI -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_GEOPOLY".
DrvLdFlagsDefault = "-shared -lsqlite3".
@@ -23,9 +23,9 @@ CFlags =
end.
[
{minimum_otp_vsn, "21.0"},
{minimum_otp_vsn, "22.0"},
{erl_opts, [debug_info, warnings_as_errors]},
{erl_opts, [debug_info]},
{xref_checks, [undefined_function_calls,
undefined_functions,
@@ -68,6 +68,7 @@ CFlags =
]},
{edoc_opts, [{preprocess, true},
{stylesheet, "style.css"},
{sort_functions, false}]},
{hex, [{doc, edoc}]}

View File

@@ -1,7 +1,7 @@
{application, esqlite,
[
{description, "sqlite nif interface"},
{vsn, "0.7.3"},
{vsn, "0.8.0"},
{modules, [esqlite3, esqlite3_nif]},
{registered, []},
{licenses, ["Apache"]},

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,9 @@
%% @author Maas-Maarten Zeeman <mmzeeman@xs4all.nl>
%% @copyright 2011 - 2022 Maas-Maarten Zeeman
%%
%% @doc Low level Erlang API for sqlite3 databases.
%% @end
%% 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
@@ -9,47 +15,67 @@
%% 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 <mmzeeman@xs4all.nl>
%% @copyright 2011 - 2022 Maas-Maarten Zeeman
%%
%% @doc Low level erlang API for sqlite3 databases.
-module(esqlite3_nif).
-author("Maas-Maarten Zeeman <mmzeeman@xs4all.nl>").
%% 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,
backup_init/6,
backup_step/5,
backup_remaining/4,
backup_pagecount/4,
backup_finish/4,
open/1,
close/1,
error_info/1,
set_update_hook/2,
get_autocommit/1,
last_insert_rowid/1,
changes/1,
exec/2,
prepare/3,
column_names/1,
column_decltypes/1,
bind_int/3,
bind_int64/3,
bind_double/3,
bind_text/3,
bind_blob/3,
bind_null/2,
step/1,
reset/1,
interrupt/1,
close/3
backup_init/4,
backup_remaining/1,
backup_pagecount/1,
backup_step/2,
backup_finish/1,
memory_stats/1,
status/2
]).
-type raw_connection() :: reference().
-type raw_statement() :: reference().
-type raw_backup() :: reference().
-type sql() :: iodata().
-type esqlite3_ref() :: reference(). % Reference to a database connection handle. See [https://sqlite.org/c3ref/sqlite3.html] for more details.
-type esqlite3_stmt_ref() :: reference(). % Reference to a prepared statement object. See [https://sqlite.org/c3ref/stmt.html] for more details.
-type esqlite3_backup_ref() :: reference(). % Reference to a online backup object. See [https://sqlite.org/c3ref/backup.html] for more details.
-type sql() :: iodata(). % Make sure the iodata contains utf-8 encoded data.
-type rowid() :: integer().
-type cell() :: undefined | integer() | float() | binary().
-type row() :: list(cell()).
-type extended_errcode() :: integer(). % Extended sqlite3 error code. See [https://sqlite.org/rescode.html] for more details.
-type error() :: {error, extended_errcode()}.
-type error_info() :: #{ errcode := integer(),
extended_errcode := extended_errcode(),
errstr := unicode:unicode_binary(), % English-language text that describes the result code, as UTF-8
errmsg := unicode:unicode_binary(), % English-language text that describes the error, as UTF-8
error_offset := integer() % The byte offset to the token in the input sql.
}. % See: [https://sqlite.org/c3ref/errcode.html] for more information.
-export_type([raw_connection/0, raw_statement/0, raw_backup/0, sql/0]).
-export_type([esqlite3_ref/0, esqlite3_stmt_ref/0, esqlite3_backup_ref/0, sql/0, rowid/0, cell/0, row/0, error/0, error_info/0]).
-on_load(init/0).
@@ -61,138 +87,225 @@ init() ->
end,
ok = erlang:load_nif(NifFileName, 0).
%% @doc Start a low level thread which will can handle sqlite3 calls.
%%
-spec start() -> {ok, raw_connection()} | {error, _}.
start() ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Open the specified sqlite3 database.
%%
%% Sends an asynchronous open command over the connection and returns
%% ok immediately. When the database is opened
%%
-spec open(raw_connection(), reference(), pid(), string()) -> ok | {error, _}.
open(_Db, _Ref, _Dest, _Filename) ->
erlang:nif_error(nif_library_not_loaded).
-spec set_update_hook(raw_connection(), reference(), pid(), pid()) -> ok | {error, _}.
set_update_hook(_Db, _Ref, _Dest, _Pid) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Exec the query.
%%
%% Sends an asynchronous exec command over the connection and returns
%% ok immediately.
%%
%% When the statement is executed Dest will receive message {Ref, answer()}
%% with answer() integer | {error, reason()}
%%
-spec exec(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}.
exec(_Db, _Ref, _Dest, _Sql) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get the number of affected rows of last statement
%%
%% When the statement is executed Dest will receive message {Ref, answer()}
%% with answer() integer | {error, reason()}
-spec changes(raw_connection(), reference(), pid()) -> ok | {error, _}.
changes(_Db, _Ref, _Dest) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc
%%
-spec prepare(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}.
prepare(_Db, _Ref, _Dest, _Sql) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc
%%
-spec multi_step(raw_connection(), raw_statement(), pos_integer(), reference(), pid()) -> ok | {error, _}.
multi_step(_Db, _Stmt, _Chunk_Size, _Ref, _Dest) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc
%%
-spec reset(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}.
reset(_Db, _Stmt, _Ref, _Dest) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc
%%
-spec finalize(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}.
finalize(_Db, _Stmt, _Ref, _Dest) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Bind parameters to a prepared statement.
%%
-spec bind(raw_connection(), raw_statement(), reference(), pid(), list(any())) -> ok | {error, _}.
bind(_Db, _Stmt, _Ref, _Dest, _Args) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Retrieve the column names of the prepared statement
%%
-spec column_names(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}.
column_names(_Db, _Stmt, _Ref, _Dest) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Retrieve the column types of the prepared statement
%%
-spec column_types(raw_connection(), raw_statement(), reference(), pid()) -> ok | {error, _}.
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) ->
%% It is possible to use sqlite's uri filenames to open files.
%% See: [https://sqlite.org/uri.html] for more information.
-spec open(Filename) -> OpenResult when
Filename :: string(),
OpenResult :: {ok, esqlite3_ref()} | error().
open(_Filename) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Close the connection.
%%
-spec close(raw_connection(), reference(), pid()) -> ok | {error, _}.
close(_Db, _Ref, _Dest) ->
-spec close(Connection) -> CloseResult
when Connection :: esqlite3_ref(),
CloseResult :: ok | {error, _}.
close(_Db) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Insert record
%% @doc Get an error messages for the last occurred error.
%%
-spec insert(raw_connection(), reference(), pid(), sql()) -> ok | {error, _}.
insert(_Db, _Ref, _Dest, _Sql) ->
-spec error_info(Connection) -> ErrorInfo
when Connection :: esqlite3_ref(),
ErrorInfo :: error_info().
error_info(_Db) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Set an update hook
-spec set_update_hook(Connection, Pid) -> Result
when Connection :: esqlite3_ref(),
Pid :: pid(),
Result :: ok.
set_update_hook(_Db, _Pid) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Execute a sql statement
-spec exec(Connection, Sql) -> ExecResult
when Connection :: esqlite3_ref(),
Sql :: sql(),
ExecResult :: ok | error().
exec(_Connection, _Sql) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Compile a sql statement.
-spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult
when Connection :: esqlite3_ref(),
Sql :: sql(),
PrepareFlags :: non_neg_integer(),
PrepareResult :: {ok, esqlite3_stmt_ref()} | error().
prepare(_Connection, _Sql, _PrepareFlags) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind an integer to a position in a prepared statement.
-spec bind_int(Statement, Index, Value) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Value :: integer(), %% [todo] Should be a 32 bit integer range.
Result :: ok | error().
bind_int(_Statement, _Index, _Value) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind a 64 bit integer to a position in a prepared statement.
-spec bind_int64(Statement, Index, Value) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Value :: integer(), %% [todo] Should be a 64 bit integer range.
Result :: ok | error().
bind_int64(_Statement, _Index, _Value) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind an double/float to a position in a prepared statement.
-spec bind_double(Statement, Index, Value) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Value :: float(), %% [todo] Should be a 64 bit integer range.
Result :: ok | error().
bind_double(_Statement, _Index, _Value) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind a utf-8 string to a position in a prepared statement.
-spec bind_text(Statement, Index, Value) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Value :: iodata(), %% [todo] Should be a utf-8 iodata.
Result :: ok | error().
bind_text(_Statement, _Index, _Value) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind a blob to a position in a prepared statement.
-spec bind_blob(Statement, Index, Value) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Value :: iodata(),
Result :: ok | error().
bind_blob(_Statement, _Index, _Value) ->
erlang:nif_error(nif_library_not_loaded).
% @doc Bind a null to a position in a prepared statement.
-spec bind_null(Statement, Index) -> Result when
Statement :: esqlite3_stmt_ref(),
Index :: integer(),
Result :: ok | error().
bind_null(_Statement, _Index) ->
erlang:nif_error(nif_library_not_loaded).
-spec step(Statement) -> StepResult when
Statement :: esqlite3_stmt_ref(),
StepResult :: row() | '$done' | error().
step(_Statement) ->
erlang:nif_error(nif_library_not_loaded).
-spec reset(Statement) -> ResetResult when
Statement :: esqlite3_stmt_ref(),
ResetResult :: ok | error().
reset(_Statement) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Retrieve the column names of the prepared statement
%%
-spec column_names(Statement) -> Names when
Statement :: esqlite3_stmt_ref(),
Names :: list(unicode:unicode_binary()).
column_names(_Stmt) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Retrieve the declared datatypes of all columns.
%%
-spec column_decltypes(Statement) -> Types when
Statement :: esqlite3_stmt_ref(),
Types :: list(undefined | unicode:unicode_binary()).
column_decltypes(_Stmt) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Initialize a backup procedure of a database.
-spec backup_init(Destination, DestinationName, Source, SourceName) -> InitResult when
Destination :: esqlite3_ref(),
DestinationName :: iodata(),
Source :: esqlite3_ref(),
SourceName :: iodata(),
InitResult :: {ok, esqlite3_backup_ref()} | error().
backup_init(_Dest, _DestName, _Src, _SrcName) ->
erlang:nif_error(nif_library_not_loaded).
-spec backup_remaining(Backup) -> Remaining when
Backup :: esqlite3_backup_ref(),
Remaining :: integer().
backup_remaining(_Backup) ->
erlang:nif_error(nif_library_not_loaded).
-spec backup_pagecount(Backup) -> Pagecount when
Backup :: esqlite3_backup_ref(),
Pagecount :: integer().
backup_pagecount(_Backup) ->
erlang:nif_error(nif_library_not_loaded).
-spec backup_step(Backup, NPage) -> Result when
Backup :: esqlite3_backup_ref(),
NPage :: integer(),
Result :: ok | '$done' | error().
backup_step(_Backup, _PageCount) ->
erlang:nif_error(nif_library_not_loaded).
-spec backup_finish(Backup) -> Result when
Backup :: esqlite3_backup_ref(),
Result :: ok | error().
backup_finish(_Backup) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Interrupt all active queries.
-spec interrupt(esqlite3_ref()) -> ok.
interrupt(_Db) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get the last insert rowid.
%%
-spec last_insert_rowid(raw_connection(), reference(), pid()) -> ok | {error, _}.
last_insert_rowid(_Db, _Ref, _Dest) ->
-spec last_insert_rowid(esqlite3_ref()) -> rowid().
last_insert_rowid(_Connection) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get automcommit
%% @doc Get number of changes insert, delete of the most recent completed
%% INSERT, DELETE or UPDATE statement.
%%
-spec get_autocommit(raw_connection(), reference(), pid()) -> ok | {error, _}.
get_autocommit(_Db, _Ref, _Dest) ->
-spec changes(esqlite3_ref()) -> integer().
changes(_Connection) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get autocommit
%%
-spec get_autocommit(esqlite3_ref()) -> true | false.
get_autocommit(_Connection) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get memory statistics
%%
-spec memory_stats(HighwaterResetFlag) -> Stats when
HighwaterResetFlag :: integer(),
Stats :: #{ used := integer(), highwater := integer() }.
memory_stats(_Flag) ->
erlang:nif_error(nif_library_not_loaded).
%% @doc Get sqlite status information.
%%
%% <code>
%% MEMORY_USED 0
%% PAGECACHE_USED 1
%% PAGECACHE_OVERFLOW 2
%% MALLOC_SIZE 5
%% PARSER_STACK 6
%% PAGECACHE_SIZE 7
%% MALLOC_COUNT 8
%% </code>
%%
-spec status(Op, HighwaterResetFlag) -> Stats when
Op :: integer(),
HighwaterResetFlag :: integer(),
Stats :: #{ used := non_neg_integer(), highwater := non_neg_integer() }.
status(_Op, _Flag) ->
erlang:nif_error(nif_library_not_loaded).

View File

@@ -19,27 +19,82 @@ close_test() ->
{ok, C} = esqlite3:open(":memory:"),
ok = esqlite3:close(C),
%% Check if functions still return sensible values.
{error, closed} = esqlite3:set_update_hook(self(), C),
{error, closed} = esqlite3:changes(C),
{error, closed} = esqlite3:get_autocommit(C),
{error, closed} = esqlite3:last_insert_rowid(C),
%% Double close should also work.
ok = esqlite3:close(C),
{error, _} = esqlite3:exec("create table test(one, two, three)", C),
%% Check if functions still return sensible values.
?assertError(closed, esqlite3:set_update_hook(C, self())),
?assertError(closed, esqlite3:changes(C)),
?assertError(closed, esqlite3:get_autocommit(C)),
?assertError(closed, esqlite3:last_insert_rowid(C)),
?assertEqual({error, 21},
esqlite3:exec(C, "create table test(one, two, three)")),
ok.
prepare_test() ->
{ok, C} = esqlite3:open(":memory:"),
?assertMatch({ok, {esqlite3_stmt, _}}, esqlite3:prepare(C, "select 1")),
ok = esqlite3:close(C),
ok.
prepare_after_close_test() ->
{ok, C} = esqlite3:open(":memory:"),
?assertEqual(ok, esqlite3:close(C)),
?assertMatch({error, 21}, esqlite3:prepare(C, "select 1")),
ok.
column_names_test() ->
{ok, C} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare(C, "select 1 as one"),
?assertEqual([<<"one">>], esqlite3:column_names(Stmt)),
{ok, Stmt1} = esqlite3:prepare(C, <<"select 1 as 😀"/utf8>>),
?assertEqual([<<"😀"/utf8>>], esqlite3:column_names(Stmt1)),
{ok, Stmt2} = esqlite3:prepare(C, <<"select 1">>),
?assertEqual([<<"1">>], esqlite3:column_names(Stmt2)),
{ok, Stmt3} = esqlite3:prepare(C, <<"select 1, 2, 3">>),
?assertEqual([<<"1">>, <<"2">>, <<"3">>], esqlite3:column_names(Stmt3)),
ok.
column_decltypes_test() ->
{ok, C} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3"),
?assertEqual([undefined, undefined, undefined], esqlite3:column_decltypes(Stmt)),
ok.
step_test() ->
{ok, C} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare(C, "select 1, 2, 3;" ),
?assertEqual([1,2,3], esqlite3:step(Stmt)),
?assertEqual('$done', esqlite3:step(Stmt)),
%% After the done, the statement is reset and
?assertEqual([1,2,3], esqlite3:step(Stmt)),
?assertEqual('$done', esqlite3:step(Stmt)),
ok.
iodata_test() ->
{ok, C} = esqlite3:open(":memory:"),
{error, no_iodata} = esqlite3:exec(1000, C),
{error, no_iodata} = esqlite3:insert(1000, C),
?assertError(badarg, esqlite3:exec(C, 1000)),
?assertError(badarg, esqlite3:exec(C, 1000)),
ok.
open_multiple_same_databases_test() ->
cleanup(),
%% Sqlite allows opening the same file multiple
%% times
{ok, _C1} = esqlite3:open(?DB1),
{ok, _C2} = esqlite3:open(?DB1),
@@ -55,71 +110,91 @@ open_multiple_different_databases_test() ->
get_autocommit_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db),
%% By default, the database is in autocommit mode
true = esqlite3:get_autocommit(Db),
ok = esqlite3:exec("BEGIN;", Db),
ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"),
true = esqlite3:get_autocommit(Db),
%% After a begin statement, the connection will not be in autocommit mode anymore
ok = esqlite3:exec(Db, "BEGIN;"),
false = esqlite3:get_autocommit(Db),
ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db),
ok = esqlite3:exec("COMMIT;", Db),
ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"),
ok = esqlite3:exec(Db, "COMMIT;"),
%% After a commit statement, the connection will be in autocommit mode
true = esqlite3:get_autocommit(Db),
ok.
last_insert_rowid_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db),
ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db),
{ok, 1} = esqlite3:last_insert_rowid(Db),
ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is another test');", Db),
{ok, 2} = esqlite3:last_insert_rowid(Db),
ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"),
ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"),
1 = esqlite3:last_insert_rowid(Db),
ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is another test');"),
2 = esqlite3:last_insert_rowid(Db),
ok.
update_hook_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:set_update_hook(self(), Db),
ok = esqlite3:exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);", Db),
ok = esqlite3:exec("INSERT INTO test (val) VALUES ('this is a test');", Db),
ok = receive {insert, "test", 1} -> ok after 150 -> no_message end,
ok = esqlite3:exec("UPDATE test SET val = 'a new test' WHERE id = 1;", Db),
ok = receive {update, "test", 1} -> ok after 150 -> no_message end,
ok = esqlite3:exec("DELETE FROM test WHERE id = 1;", Db),
ok = receive {delete, "test", 1} -> ok after 150 -> no_message end,
ok = esqlite3:set_update_hook(Db, self()),
ok = esqlite3:exec(Db, "CREATE TABLE test (id INTEGER PRIMARY KEY, val STRING);"),
ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"),
ok = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end,
ok = esqlite3:exec(Db, "UPDATE test SET val = 'a new test' WHERE id = 1;"),
ok = receive {update, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end,
ok = esqlite3:exec(Db, "DELETE FROM test WHERE id = 1;"),
ok = receive {delete, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end,
ok = esqlite3:set_update_hook(Db, undefined),
ok = esqlite3:exec(Db, "INSERT INTO test (val) VALUES ('this is a test');"),
no_message = receive {insert, <<"main">>, <<"test">>, 1} -> ok after 150 -> no_message end,
ok.
simple_query_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
{ok, 1} = esqlite3:changes(Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"),
?assertEqual(1, esqlite3:changes(Db)),
ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db),
{ok, 1} = esqlite3:changes(Db),
ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db),
{ok, 1} = esqlite3:changes(Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
{ok, 1} = esqlite3:changes(Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec("select * from test_table;", Db),
ok = esqlite3:exec(Db, "insert into test_table values('hello2', 11);"),
?assertEqual(1, esqlite3:changes(Db)),
ok = esqlite3:exec(Db, "insert into test_table values('hello3', 12);"),
?assertEqual(1, esqlite3:changes(Db)),
ok = esqlite3:exec(Db, "insert into test_table values('hello4', 13);"),
?assertEqual(1, esqlite3:changes(Db)),
ok = esqlite3:exec(Db, "commit;"),
ok = esqlite3:exec(Db, "select * from test_table;"),
ok = esqlite3:exec("delete from test_table;", Db),
{ok, 4} = esqlite3:changes(Db),
ok = esqlite3:exec(Db, "delete from test_table;"),
?assertEqual(4, esqlite3:changes(Db)),
ok.
prepare_test() ->
prepare2_test() ->
{ok, Db} = esqlite3:open(":memory:"),
esqlite3:exec("begin;", Db),
esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
{ok, Statement} = esqlite3:prepare("insert into test_table values('one', 2)", Db),
esqlite3:exec(Db, "begin;"),
esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
{ok, Statement} = esqlite3:prepare(Db, "insert into test_table values('one', 2)"),
'$done' = esqlite3:step(Statement),
{ok, 1} = esqlite3:changes(Db),
1 = esqlite3:changes(Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
ok = esqlite3:exec(Db, "insert into test_table values('hello4', 13);"),
%% Check if the values are there.
[{<<"one">>, 2}, {<<"hello4">>, 13}] = esqlite3:q("select * from test_table order by two", Db),
esqlite3:exec("commit;", Db),
[[<<"one">>, 2], [<<"hello4">>, 13]] = esqlite3:q(Db, "select * from test_table order by two"),
esqlite3:exec(Db, "commit;"),
esqlite3:close(Db),
ok.
@@ -127,178 +202,176 @@ prepare_test() ->
bind_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "commit;"),
%% Create a prepared statement
{ok, Statement} = esqlite3:prepare("insert into test_table values(?1, ?2)", Db),
esqlite3:bind(Statement, [one, 2]),
{ok, Statement} = esqlite3:prepare(Db, "insert into test_table values(?1, ?2)"),
ok = esqlite3:bind(Statement, [one, 2]),
'$done' = esqlite3:step(Statement),
ok = esqlite3:bind(Statement, ["three", 4]),
esqlite3:step(Statement),
esqlite3:bind(Statement, ["three", 4]),
ok = esqlite3:bind(Statement, ["five", 6]),
esqlite3:step(Statement),
esqlite3:bind(Statement, ["five", 6]),
ok = esqlite3:bind(Statement, [[<<"se">>, $v, "en"], 8]), % iolist bound as text
esqlite3:step(Statement),
esqlite3:bind(Statement, [[<<"se">>, $v, "en"], 8]), % iolist bound as text
ok = esqlite3:bind(Statement, [<<"nine">>, 10]), % iolist bound as text
esqlite3:step(Statement),
esqlite3:bind(Statement, [<<"nine">>, 10]), % iolist bound as text
ok = esqlite3:bind(Statement, [{blob, [<<"eleven">>, 0]}, 12]), % iolist bound as blob with trailing eos.
esqlite3:step(Statement),
esqlite3:bind(Statement, [{blob, [<<"eleven">>, 0]}, 12]), % iolist bound as blob with trailing eos.
esqlite3:step(Statement),
esqlite3:bind(Statement, ["empty", undefined]), % 'undefined' is converted to SQL null
ok = esqlite3:bind(Statement, ["empty", undefined]), % 'undefined' is converted to SQL null
esqlite3:step(Statement),
%% int64
esqlite3:bind(Statement, [int64, 308553449069486081]),
ok = esqlite3:bind(Statement, [int64, 308553449069486081]),
esqlite3:step(Statement),
%
%% negative int64
esqlite3:bind(Statement, [negative_int64, -308553449069486081]),
ok = esqlite3:bind(Statement, [negative_int64, -308553449069486081]),
esqlite3:step(Statement),
%% utf-8
esqlite3:bind(Statement, [[<<228,184,138,230,181,183>>], 100]),
ok = esqlite3:bind(Statement, [[<<228,184,138,230,181,183>>], 100]),
esqlite3:step(Statement),
?assertEqual([{<<"one">>, 2}],
esqlite3:q("select one, two from test_table where two = '2'", Db)),
?assertEqual([{<<"three">>, 4}],
esqlite3:q("select one, two from test_table where two = 4", Db)),
?assertEqual([{<<"five">>, 6}],
esqlite3:q("select one, two from test_table where two = 6", Db)),
?assertEqual([{<<"seven">>, 8}],
esqlite3:q("select one, two from test_table where two = 8", Db)),
?assertEqual([{<<"nine">>, 10}],
esqlite3:q("select one, two from test_table where two = 10", Db)),
?assertEqual([{{blob, <<$e,$l,$e,$v,$e,$n,0>>}, 12}],
esqlite3:q("select one, two from test_table where two = 12", Db)),
?assertEqual([{<<"empty">>, undefined}],
esqlite3:q("select one, two from test_table where two is null", Db)),
?assertEqual([[<<"one">>, 2]],
esqlite3:q(Db, "select one, two from test_table where two = '2'")),
?assertEqual([[<<"three">>, 4]],
esqlite3:q(Db, "select one, two from test_table where two = 4")),
?assertEqual([[<<"five">>, 6]],
esqlite3:q(Db, "select one, two from test_table where two = 6")),
?assertEqual([[<<"seven">>, 8]],
esqlite3:q(Db, "select one, two from test_table where two = 8")),
?assertEqual([[<<"nine">>, 10]],
esqlite3:q(Db, "select one, two from test_table where two = 10")),
?assertEqual([[<<$e,$l,$e,$v,$e,$n,0>>, 12]],
esqlite3:q(Db, "select one, two from test_table where two = 12")),
?assertEqual([[<<"empty">>, undefined]],
esqlite3:q(Db, "select one, two from test_table where two is null")),
?assertEqual([{<<"int64">>, 308553449069486081}],
esqlite3:q("select one, two from test_table where one = 'int64';", Db)),
?assertEqual([{<<"negative_int64">>, -308553449069486081}],
esqlite3:q("select one, two from test_table where one = 'negative_int64';", Db)),
?assertEqual([[<<"int64">>, 308553449069486081]],
esqlite3:q(Db, "select one, two from test_table where one = 'int64';")),
?assertEqual([[<<"negative_int64">>, -308553449069486081]],
esqlite3:q(Db, "select one, two from test_table where one = 'negative_int64';")),
%% utf-8
?assertEqual([{<<228,184,138,230,181,183>>, 100}],
esqlite3:q("select one, two from test_table where two = 100", Db)),
?assertEqual([[<<228,184,138,230,181,183>>, 100]],
esqlite3:q(Db, "select one, two from test_table where two = 100")),
ok.
bind_for_queries_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "commit;"),
?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[test_table], Db)),
?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
["test_table"], Db)),
?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[<<"test_table">>], Db)),
?assertEqual([{1}], esqlite3:q(<<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[[<<"test_table">>]], Db)),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[test_table])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
["test_table"])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[<<"test_table">>])),
?assertEqual([[1]], esqlite3:q(Db, <<"SELECT count(type) FROM sqlite_master WHERE type='table' AND name=?;">>,
[[<<"test_table">>]])),
ok.
column_names_test() ->
column_names2_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"),
ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"),
ok = esqlite3:exec(Db, "commit;"),
%% All columns
{ok, Stmt} = esqlite3:prepare("select * from test_table", Db),
{one, two} = esqlite3:column_names(Stmt),
{row, {<<"hello1">>, 10}} = esqlite3:step(Stmt),
{one, two} = esqlite3:column_names(Stmt),
{row, {<<"hello2">>, 20}} = esqlite3:step(Stmt),
{one, two} = esqlite3:column_names(Stmt),
{ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"),
[<<"one">>, <<"two">>] = esqlite3:column_names(Stmt),
[<<"hello1">>, 10] = esqlite3:step(Stmt),
[<<"one">>, <<"two">>] = esqlite3:column_names(Stmt),
[<<"hello2">>, 20] = esqlite3:step(Stmt),
[<<"one">>, <<"two">>] = esqlite3:column_names(Stmt),
'$done' = esqlite3:step(Stmt),
{one, two} = esqlite3:column_names(Stmt),
[<<"one">>, <<"two">>] = esqlite3:column_names(Stmt),
%% One column
{ok, Stmt2} = esqlite3:prepare("select two from test_table", Db),
{two} = esqlite3:column_names(Stmt2),
{row, {10}} = esqlite3:step(Stmt2),
{two} = esqlite3:column_names(Stmt2),
{row, {20}} = esqlite3:step(Stmt2),
{two} = esqlite3:column_names(Stmt2),
{ok, Stmt2} = esqlite3:prepare(Db, "select two from test_table"),
[<<"two">>] = esqlite3:column_names(Stmt2),
[10] = esqlite3:step(Stmt2),
[<<"two">>] = esqlite3:column_names(Stmt2),
[20] = esqlite3:step(Stmt2),
[<<"two">>] = esqlite3:column_names(Stmt2),
'$done' = esqlite3:step(Stmt2),
{two} = esqlite3:column_names(Stmt2),
[<<"two">>] = esqlite3:column_names(Stmt2),
%% No columns
{ok, Stmt3} = esqlite3:prepare("values(1);", Db),
{column1} = esqlite3:column_names(Stmt3),
{row, {1}} = esqlite3:step(Stmt3),
{column1} = esqlite3:column_names(Stmt3),
{ok, Stmt3} = esqlite3:prepare(Db, "values(1);"),
[<<"column1">>] = esqlite3:column_names(Stmt3),
[1] = esqlite3:step(Stmt3),
[<<"column1">>] = esqlite3:column_names(Stmt3),
%% Things get a bit weird when you retrieve the column name
%% when calling an aggragage function.
{ok, Stmt4} = esqlite3:prepare("select date('now');", Db),
{'date(\'now\')'} = esqlite3:column_names(Stmt4),
{row, {Date}} = esqlite3:step(Stmt4),
{ok, Stmt4} = esqlite3:prepare(Db, "select date('now');"),
[<<"date(\'now\')">>] = esqlite3:column_names(Stmt4),
[Date] = esqlite3:step(Stmt4),
true = is_binary(Date),
%% Some statements have no column names
{ok, Stmt5} = esqlite3:prepare("create table dummy(a, b, c);", Db),
{} = esqlite3:column_names(Stmt5),
{ok, Stmt5} = esqlite3:prepare(Db, "create table dummy(a, b, c);"),
[] = esqlite3:column_names(Stmt5),
ok.
column_types_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 20);", Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table test_table(one varchar(10), two int);"),
ok = esqlite3:exec(Db, "insert into test_table values('hello1', 10);"),
ok = esqlite3:exec(Db, "insert into test_table values('hello2', 20);"),
ok = esqlite3:exec(Db, "commit;"),
%% All columns
{ok, Stmt} = esqlite3:prepare("select * from test_table", Db),
?assertEqual({'varchar(10)', 'INT'}, esqlite3:column_types(Stmt)),
{row, {<<"hello1">>, 10}} = esqlite3:step(Stmt),
{'varchar(10)', 'INT'} = esqlite3:column_types(Stmt),
{row, {<<"hello2">>, 20}} = esqlite3:step(Stmt),
{'varchar(10)', 'INT'} = esqlite3:column_types(Stmt),
'$done' = esqlite3:step(Stmt),
{'varchar(10)', 'INT'} = esqlite3:column_types(Stmt),
{ok, Stmt} = esqlite3:prepare(Db, "select * from test_table"),
?assertEqual([<<"varchar(10)">>, <<"INT">>], esqlite3:column_decltypes(Stmt)),
%% Some statements have no column types
{ok, Stmt2} = esqlite3:prepare("create table dummy(a, b, c);", Db),
{} = esqlite3:column_types(Stmt2),
{ok, Stmt2} = esqlite3:prepare(Db, "create table dummy(a, b, c);"),
[] = esqlite3:column_decltypes(Stmt2),
{ok, Stmt3} = esqlite3:prepare(Db, "select 1, 2, 3;"),
[undefined, undefined, undefined] = esqlite3:column_decltypes(Stmt3),
ok.
nil_column_types_test() ->
nil_column_decltypes_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table t1(c1 variant);", Db),
ok = esqlite3:exec("commit;", Db),
ok = esqlite3:exec(Db, "begin;"),
ok = esqlite3:exec(Db, "create table t1(c1 variant);"),
ok = esqlite3:exec(Db, "commit;"),
{ok, Stmt} = esqlite3:prepare(Db, "select c1 + 1, c1 from t1"),
?assertEqual([undefined, <<"variant">>], esqlite3:column_decltypes(Stmt)),
{ok, Stmt} = esqlite3:prepare("select c1 + 1, c1 from t1", Db),
{nil, variant} = esqlite3:column_types(Stmt),
ok.
reset_test() ->
{ok, Db} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare("select * from (values (1), (2));", Db),
{row, {1}} = esqlite3:step(Stmt),
{ok, Stmt} = esqlite3:prepare(Db, "select * from (values (1), (2));"),
[1] = esqlite3:step(Stmt),
ok = esqlite3:reset(Stmt),
{row, {1}} = esqlite3:step(Stmt),
{row, {2}} = esqlite3:step(Stmt),
[1] = esqlite3:step(Stmt),
[2] = esqlite3:step(Stmt),
'$done' = esqlite3:step(Stmt),
% After a done the statement is automatically reset.
{row, {1}} = esqlite3:step(Stmt),
[1] = esqlite3:step(Stmt),
% Calling reset multiple times...
ok = esqlite3:reset(Stmt),
@@ -307,155 +380,41 @@ reset_test() ->
ok = esqlite3:reset(Stmt),
% The statement should still be reset.
{row, {1}} = esqlite3:step(Stmt),
[1] = esqlite3:step(Stmt),
ok.
foreach_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db),
ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
ok = esqlite3:exec("commit;", Db),
F = fun(Row) ->
case Row of
{Key, Value} ->
put(Key, Value);
_ ->
ok
end
end,
esqlite3:foreach(F, "select * from test_table;", Db),
10 = get(<<"hello1">>),
11 = get(<<"hello2">>),
12 = get(<<"hello3">>),
13 = get(<<"hello4">>),
ok.
bind_for_foreach_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db),
ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
ok = esqlite3:exec("commit;", Db),
F = fun(Row) ->
case Row of
{Key, Value} ->
put(Key, Value);
_ ->
ok
end
end,
esqlite3:foreach(F, "select * from test_table where one = ?;", ["hello1"], Db),
10 = get(<<"hello1">>),
ok.
map_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db),
ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
ok = esqlite3:exec("commit;", Db),
F = fun(Row) -> Row end,
[{<<"hello1">>,10},
{<<"hello2">>,11},
{<<"hello3">>,12},
{<<"hello4">>,13}] = esqlite3:map(F, "select * from test_table", Db),
%% Test that when the row-names are added..
Assoc = fun(Names, Row) ->
lists:zip(tuple_to_list(Names), tuple_to_list(Row))
end,
[[{one,<<"hello1">>},{two,10}],
[{one,<<"hello2">>},{two,11}],
[{one,<<"hello3">>},{two,12}],
[{one,<<"hello4">>},{two,13}]] = esqlite3:map(Assoc, "select * from test_table", Db),
ok.
bind_for_map_test() ->
{ok, Db} = esqlite3:open(":memory:"),
ok = esqlite3:exec("begin;", Db),
ok = esqlite3:exec("create table test_table(one varchar(10), two int);", Db),
ok = esqlite3:exec("insert into test_table values('hello1', 10);", Db),
ok = esqlite3:exec("insert into test_table values('hello2', 11);", Db),
ok = esqlite3:exec("insert into test_table values('hello3', 12);", Db),
ok = esqlite3:exec("insert into test_table values('hello4', 13);", Db),
ok = esqlite3:exec("commit;", Db),
F = fun(Row) -> Row end,
[{<<"hello1">>,10}]
= esqlite3:map(F, "select * from test_table where one = ?", ["hello1"], Db),
%% Test that when the row-names are added..
Assoc = fun(Names, Row) ->
lists:zip(tuple_to_list(Names), tuple_to_list(Row))
end,
[[{one,<<"hello1">>},{two,10}]] = esqlite3:map(Assoc, "select * from test_table where one = ?", ["hello1"], Db),
ok.
error1_msg_test() ->
{ok, Db} = esqlite3:open(":memory:"),
%% Not sql.
{error, {sqlite_error, _Msg1}} = esqlite3:exec("dit is geen sql", Db),
{error, 1} = esqlite3:exec(Db, "dit is geen sql"),
%% Database test does not exist.
{error, {sqlite_error, _Msg2}} = esqlite3:exec("select * from test;", Db),
{error, 1} = esqlite3:exec(Db, "select * from test;"),
%% Opening non-existant database.
{error, {cantopen, _Msg3}} = esqlite3:open("/dit/bestaat/niet"),
{error, 14} = esqlite3:open("/dit/bestaat/niet"),
ok.
prepare_and_close_connection_test() ->
{ok, Db} = esqlite3:open(":memory:"),
[] = esqlite3:q("create table test(one, two, three)", Db),
ok = esqlite3:exec(["insert into test values(1,2,3);"], Db),
{ok, Stmt} = esqlite3:prepare("select * from test", Db),
[] = esqlite3:q(Db, "create table test(one, two, three)"),
ok = esqlite3:exec(Db, ["insert into test values(1,2,3);"]),
{ok, Stmt} = esqlite3:prepare(Db, "select * from test"),
%% The prepated statment works.
{row, {1,2,3}} = esqlite3:step(Stmt),
[1,2,3] = esqlite3:step(Stmt),
'$done' = esqlite3:step(Stmt),
ok = esqlite3:close(Db),
ok = esqlite3:reset(Stmt),
%% Internally sqlite3_close_v2 is used by the nif. This will destruct the
%% connection when the last perpared statement is finalized
{row, {1,2,3}} = esqlite3:step(Stmt),
[1,2,3] = esqlite3:step(Stmt),
'$done' = esqlite3:step(Stmt),
ok.
@@ -466,10 +425,12 @@ backup_test() ->
{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),
{ok, Backup} = esqlite3:backup_init(Dest, <<"main">>, Source, <<"main">>),
0 = esqlite3:backup_remaining(Backup),
0 = esqlite3:backup_pagecount(Backup),
'$done' = esqlite3:backup_step(Backup, 1),
cleanup(),
@@ -481,38 +442,39 @@ backup1_test() ->
{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),
[] = esqlite3:q(Source, "create table test(one, two)"),
[] = esqlite3:q(Source, "begin;"),
[] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"),
[] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"),
[] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"),
[] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"),
[] = esqlite3:q(Source, "insert into test values(randomblob(10000), randomblob(10000));"),
[] = esqlite3:q(Source, "commit;"),
[{5}] = esqlite3:q("select count(*) from test", Source),
{error, {sqlite_error, "no such table: test"}} = esqlite3:q("select count(*) from test", Dest),
[[5]] = esqlite3:q(Source, "select count(*) from test"),
{error, 1} = esqlite3:q(Dest, "select count(*) from test"),
#{ errmsg := <<"no such table: test">> } = esqlite3:error_info(Dest),
{ok, Backup} = esqlite3:backup_init(Dest, "main", Source, "main"),
{ok, 0} = esqlite3:backup_remaining(Backup),
{ok, 0} = esqlite3:backup_pagecount(Backup),
0 = esqlite3:backup_remaining(Backup),
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),
26 = esqlite3:backup_remaining(Backup),
27 = esqlite3:backup_pagecount(Backup),
%% Do all the remaining pages.
done = esqlite3:backup_step(Backup, -1),
'$done' = esqlite3:backup_step(Backup, -1),
{ok, 0} = esqlite3:backup_remaining(Backup),
{ok, 27} = esqlite3:backup_pagecount(Backup),
0 = esqlite3:backup_remaining(Backup),
27 = esqlite3:backup_pagecount(Backup),
ok = esqlite3:backup_finish(Backup),
[{5}] = esqlite3:q("select count(*) from test", Dest),
[[5]] = esqlite3:q(Dest, "select count(*) from test"),
cleanup(),
@@ -521,47 +483,58 @@ backup1_test() ->
sqlite_version_test() ->
{ok, Db} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare("select sqlite_version() as sqlite_version;", Db),
{sqlite_version} = esqlite3:column_names(Stmt),
?assertEqual({row, {<<"3.38.0">>}}, esqlite3:step(Stmt)),
{ok, Stmt} = esqlite3:prepare(Db, "select sqlite_version() as sqlite_version;"),
[<<"sqlite_version">>] = esqlite3:column_names(Stmt),
?assertEqual([<<"3.38.0">>], esqlite3:step(Stmt)),
ok.
sqlite_source_id_test() ->
{ok, Db} = esqlite3:open(":memory:"),
{ok, Stmt} = esqlite3:prepare("select sqlite_source_id() as sqlite_source_id;", Db),
{sqlite_source_id} = esqlite3:column_names(Stmt),
?assertEqual({row, {<<"2022-02-22 18:58:40 40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab">>}},
{ok, Stmt} = esqlite3:prepare(Db, "select sqlite_source_id() as sqlite_source_id;"),
[<<"sqlite_source_id">>] = esqlite3:column_names(Stmt),
?assertEqual([<<"2022-02-22 18:58:40 40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab">>],
esqlite3:step(Stmt)),
ok.
interrupt_on_timeout_test() ->
{ok, Db} = esqlite3:open(":memory:"),
CreateTableQuery = "CREATE TABLE all_numbers_in_the_world (number int not null);",
ok = esqlite3:exec(CreateTableQuery, Db),
VeryLongQuery = "
WITH RECURSIVE
for(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM for WHERE i < 10000000)
INSERT INTO all_numbers_in_the_world SELECT i FROM for;
",
try
ok = esqlite3:exec(VeryLongQuery, [], Db, 10)
catch
{error, timeout, _} ->
?assertMatch([{0}], esqlite3:q("SELECT COUNT(*) FROM all_numbers_in_the_world", Db)),
%% There is now a stale answer, because the recursive query was interrupted.
receive
{esqlite3, _, {error, {interrupt, "interrupted"}}} ->
ok
end
end.
{ok, Db1} = esqlite3:open("file:memdb1?mode=memory&cache=shared"),
% {ok, Db2} = esqlite3:open("file:memdb1?mode=memory&cache=shared"),
Self = self(),
F = fun() ->
CreateTableQuery = "CREATE TABLE all_numbers_in_the_world (number int not null);",
ok = esqlite3:exec(Db1, CreateTableQuery),
VeryLongQuery = "
WITH RECURSIVE
for(i) AS (VALUES(1) UNION ALL SELECT i+1 FROM for WHERE i < 10000000)
INSERT INTO all_numbers_in_the_world SELECT i FROM for;
",
%% The query was interrupted
Self ! {msg, esqlite3:exec(Db1, VeryLongQuery)}
end,
spawn(F),
timer:sleep(10),
ok = esqlite3:interrupt(Db1),
%% The query was interrupted, so no result
?assertEqual([[0]], esqlite3:q(Db1, "SELECT COUNT(*) FROM all_numbers_in_the_world")),
%% We should have gotten an interrupt error.
Msg = receive {msg, M} -> M end,
?assertEqual({error, 9}, Msg),
ok.
garbage_collect_test() ->
F = fun() ->
{ok, Db} = esqlite3:open(":memory:"),
[] = esqlite3:q("create table test(one, two, three)", Db),
[] = esqlite3:q("insert into test values(1, '2', 3.0)", Db),
{ok, Stmt} = esqlite3:prepare("select * from test", Db),
{row, {1, <<"2">>, 3.0}} = esqlite3:step(Stmt),
[] = esqlite3:q(Db, "create table test(one, two, three)"),
[] = esqlite3:q(Db, "insert into test values(1, '2', 3.0)"),
{ok, Stmt} = esqlite3:prepare(Db, "select * from test"),
[1, <<"2">>, 3.0] = esqlite3:step(Stmt),
'$done' = esqlite3:step(Stmt),
ok = esqlite3:close(Db)
end,
@@ -574,7 +547,6 @@ garbage_collect_test() ->
receive after 500 -> ok end,
erlang:garbage_collect(),
ok.
%%