%% 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 %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% 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, close/1, error_info/1, %% db connection functions set_update_hook/2, get_autocommit/1, last_insert_rowid/1, changes/1, %% queries exec/2, prepare/2, prepare/3, %% prepared statement functions 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, backup_init/4, backup_remaining/1, backup_pagecount/1, backup_step/2, backup_finish/1, bind/2, q/2, q/3, fetchall/1, status/0, status/1, status/2 ]). -define(DEFAULT_TIMEOUT, infinity). -define(DEFAULT_CHUNK_SIZE, 5000). -define(SQLITE_PREPARE_PERSISTENT, 16#01). -define(SQLITE_PREPARE_NO_VTAB, 16#04). -record(esqlite3, { db :: esqlite3_nif:esqlite3_ref() }). -record(esqlite3_stmt, { stmt :: esqlite3_nif:esqlite3_stmt_ref() }). -record(esqlite3_backup, { backup :: esqlite3_nif:esqlite3_backup_ref() }). -type esqlite3() :: #esqlite3{}. -type esqlite3_stmt() :: #esqlite3_stmt{}. -type esqlite3_backup() :: #esqlite3_backup{}. -type sql() :: esqlite3_nif:sql(). -type prepare_flags() :: persistent | no_vtab. -type status_info() :: #{ memory_used => stats(), pagecache_used => stats(), pagecache_overflow => stats(), malloc_size := stats(), parser_stack := stats(), pagecache_size := stats(), malloc_count := stats() }. -type stats() :: #{ used := non_neg_integer(), highwater := non_neg_integer() }. -type rowid() :: esqlite3_nif:rowid(). -type cell() :: esqlite3_nif:cell(). -type row() :: esqlite3_nif:row(). -type error() :: esqlite3_nif:error(). -export_type([esqlite3/0, esqlite3_stmt/0, esqlite3_backup/0, prepare_flags/0, sql/0, row/0, rowid/0, cell/0]). %% @doc Opens a sqlite3 database mentioned in Filename. %% %% The standard supplied sqlite3 library supports uri filenames, which makes %% it possible to open the connection to the database in read-only mode. More %% information about this can be found here: [https://sqlite.org/uri.html] %% %% Example: % %% ```open("file:data.db")''' %% Opens "data.db" in the current working directory %% ```open("file:data.db?mode=ro&cache=private")''' %% Opens "data.db" in read only mode with a private cache %% ```open("file:memdb1?mode=memory&cache=shared")''' %% Opens a shared memory database named memdb1 with a shared cache. %% -spec open(Filename) -> OpenResult when Filename :: string(), OpenResult :: {ok, esqlite3()} | error(). open(Filename) -> case esqlite3_nif:open(Filename) of {ok, Connection} -> {ok, #esqlite3{db=Connection}}; {error, _}=Error -> Error end. %% @doc Close the database -spec close(Connection) -> CloseResult when Connection :: esqlite3(), CloseResult :: ok | {error, _}. close(#esqlite3{db=Connection}) -> esqlite3_nif:close(Connection). %% @doc Return a description of the last occurred error. -spec error_info(Connection) -> ErrorInfo when Connection :: esqlite3(), ErrorInfo :: esqlite3_nif:error_info(). error_info(#esqlite3{db=Connection}) -> esqlite3_nif:error_info(Connection). %% @doc Interrupt a long running query. See [https://sqlite.org/c3ref/interrupt.html] for more details. -spec interrupt(Connection) -> Result when Connection :: esqlite3(), Result:: ok. interrupt(#esqlite3{db=Db}) -> esqlite3_nif:interrupt(Db). %% @doc Subscribe to database notifications. When rows are inserted deleted %% or updates, the registered process will receive messages: %% ```{insert, binary(), binary(), rowid()}''' %% When a new row has been inserted. %% ```{delete, binary(), binary(), rowid()}''' %% When a new row has been deleted. %% ```{update, binary(), binary(), rowid()}''' %% When a row has been updated. %% -spec set_update_hook(Connection, Pid) -> Result when Connection :: esqlite3(), Pid :: pid(), Result :: ok. set_update_hook(#esqlite3{db=Connection}, Pid) -> esqlite3_nif:set_update_hook(Connection, Pid). %%% %%% q %%% %% @doc Execute a sql statement, returns the result as list rows. -spec q(Connection, Sql) -> Result when Connection :: esqlite3(), Sql :: sql(), Result :: list(row()) | error(). q(Connection, Sql) -> q(Connection, Sql, []). %% @doc Execute statement, bind args and return a list rows. -spec q(Connection, Sql, Args) -> Result when Connection :: esqlite3(), Sql :: sql(), Args :: list(), Result :: list(row()) | error(). q(Connection, Sql, []) -> case prepare(Connection, Sql) of {ok, Statement} -> fetchall(Statement); {error, _Msg}=Error -> Error end; q(Connection, Sql, Args) -> case prepare(Connection, Sql) of {ok, Statement} -> case bind(Statement, Args) of ok -> fetchall(Statement); {error, _}=Error -> Error end; {error, _Msg}=Error -> Error end. %% %% fetchall %% % @doc Fetch all rows from the prepared statement. -spec fetchall(Statement) -> Result when Statement :: esqlite3_stmt(), Result :: list(row()) | error(). fetchall(Statement) -> fetchall1(Statement, []). fetchall1(Statement, Acc) -> case step(Statement) of Row when is_list(Row) -> fetchall1(Statement, [Row|Acc]); '$done' -> lists:reverse(Acc); {error, _} = E -> E end. %% @doc Get the last inserted rowid. %% See [https://sqlite.org/c3ref/set_last_insert_rowid.html] for more details. -spec last_insert_rowid(Connection) -> RowidResult when Connection :: esqlite3(), RowidResult :: integer(). last_insert_rowid(#esqlite3{db=Connection}) -> esqlite3_nif:last_insert_rowid(Connection). %% @doc Get the number of changes in the most recent INSERT, UPDATE or DELETE. %% See [https://sqlite.org/c3ref/changes.html] for more details. -spec changes(Connection) -> ChangesResult when Connection :: esqlite3(), ChangesResult :: integer(). changes(#esqlite3{db=Connection}) -> esqlite3_nif:changes(Connection). %% @doc Check if the connection is in auto-commit mode. %% See: [https://sqlite.org/c3ref/get_autocommit.html] for more details. %% -spec get_autocommit(Connection) -> AutocommitResult when Connection :: esqlite3(), AutocommitResult :: true | false. get_autocommit(#esqlite3{db=Connection}) -> esqlite3_nif:get_autocommit(Connection). %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. %% -spec exec(Connection, Sql) -> ExecResult when Connection :: esqlite3(), Sql :: sql(), ExecResult :: ok | error(). exec(#esqlite3{db=Connection}, Sql) -> esqlite3_nif:exec(Connection, Sql). %% %% Prepared Statements %% %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. %% -spec prepare(Connection, Sql) -> PrepareResult when Connection :: esqlite3(), Sql :: sql(), PrepareResult :: {ok, esqlite3_stmt()} | error(). prepare(Connection, Sql) -> prepare(Connection, Sql, []). %% @doc Compile a SQL statement. Returns a cached compiled statement which can be used in %% queries. -spec prepare(Connection, Sql, PrepareFlags) -> PrepareResult when Connection :: esqlite3(), Sql :: sql(), PrepareFlags :: list(prepare_flags()), PrepareResult :: {ok, esqlite3_stmt()} | {error, _}. prepare(#esqlite3{db=Connection}, Sql, PrepareFlags) -> case esqlite3_nif:prepare(Connection, Sql, props_to_prepare_flag(PrepareFlags)) of {ok, Stmt} -> {ok, #esqlite3_stmt{stmt=Stmt}}; {error, _}=Error -> Error end. %% @doc Bind an array of values as parameters of a prepared statement -spec bind(Statement, Args) -> Result when Statement :: esqlite3_stmt(), Args :: list(), Result :: ok | {error, _}. bind(#esqlite3_stmt{}=Statement, Args) when is_list(Args) -> bind1(Statement, 1, Args). bind1(_Statement, _Column, []) -> ok; bind1(Statement, Column, [Arg | Args]) -> case bind_arg(Statement, Column, Arg) of ok -> bind1(Statement, Column + 1, Args); {error, _}=Error -> Error end. % Bind with automatic tyoe conversion bind_arg(Statement, Column, undefined) -> bind_null(Statement, Column); bind_arg(Statement, Column, null) -> bind_null(Statement, Column); bind_arg(Statement, Column, Atom) when is_atom(Atom) -> bind_text(Statement, Column, atom_to_binary(Atom, utf8)); bind_arg(Statement, Column, Int) when is_integer(Int) -> bind_int64(Statement, Column, Int); bind_arg(Statement, Column, Float) when is_float(Float) -> bind_double(Statement, Column, Float); bind_arg(Statement, Column, Bin) when is_binary(Bin) -> bind_text(Statement, Column, Bin); bind_arg(Statement, Column, String) when is_list(String) -> bind_text(Statement, Column, String); %% Explicit type binds. bind_arg(Statement, Column, {int, Value}) -> bind_int(Statement, Column, Value); bind_arg(Statement, Column, {int64, Value}) -> bind_int64(Statement, Column, Value); bind_arg(Statement, Column, {float, Value}) -> bind_double(Statement, Column, Value); bind_arg(Statement, Column, {text, Value}) -> bind_text(Statement, Column, Value); bind_arg(Statement, Column, {blob, Value}) -> bind_blob(Statement, Column, Value). -spec bind_int(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), Value :: integer(), BindResult :: ok | error(). bind_int(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_int(Stmt, Index, Value). -spec bind_int64(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: non_neg_integer(), Value :: integer(), BindResult :: ok | error(). bind_int64(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_int64(Stmt, Index, Value). -spec bind_double(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), Value :: float(), BindResult :: ok | error(). bind_double(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_double(Stmt, Index, Value). -spec bind_text(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), Value :: iodata(), BindResult :: ok | error(). bind_text(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_text(Stmt, Index, Value). -spec bind_blob(Statement, Index, Value) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), Value :: iodata(), BindResult :: ok | error(). bind_blob(#esqlite3_stmt{stmt=Stmt}, Index, Value) -> esqlite3_nif:bind_blob(Stmt, Index, Value). -spec bind_null(Statement, Index) -> BindResult when Statement :: esqlite3_stmt(), Index :: integer(), BindResult :: ok | error(). bind_null(#esqlite3_stmt{stmt=Stmt}, Index) -> esqlite3_nif:bind_null(Stmt, Index). -spec step(Statement) -> StepResult when Statement :: esqlite3_stmt(), StepResult:: row() | '$done' | error(). step(#esqlite3_stmt{stmt=Stmt}) -> esqlite3_nif:step(Stmt). % @doc Reset the prepared statement. -spec reset(Statement) -> ResetResult when Statement :: esqlite3_stmt(), ResetResult:: ok | error(). reset(#esqlite3_stmt{stmt=Stmt}) -> esqlite3_nif:reset(Stmt). %% @doc Return the column names of the prepared statement. %% -spec column_names(Statement) -> Names when Statement :: esqlite3_stmt(), Names :: list(binary()). column_names(#esqlite3_stmt{stmt=Stmt}) -> esqlite3_nif:column_names(Stmt). %% @doc Return the column types of the prepared statement. %% -spec column_decltypes(Statement) -> Types when Statement :: esqlite3_stmt(), Types :: list(binary() | undefined). column_decltypes(#esqlite3_stmt{stmt=Stmt}) -> esqlite3_nif:column_decltypes(Stmt). %% %% Backup API %% % @doc Initialize a backup procedure. % See [https://sqlite.org/backup.html] for more details on the backup api. -spec backup_init(esqlite3(), iodata(), esqlite3(), iodata()) -> {ok, esqlite3_backup()} | {error, _}. backup_init(#esqlite3{db=Dest}, DestName, #esqlite3{db=Src}, SrcName) -> case esqlite3_nif:backup_init(Dest, DestName, Src, SrcName) of {ok, BackupRef} -> {ok, #esqlite3_backup{backup=BackupRef}}; {error, _}=Error -> Error end. %% @doc Release the resources held by the backup. -spec backup_finish(Backup) -> Result when Backup :: esqlite3_backup(), Result :: ok | error(). backup_finish(#esqlite3_backup{backup=Backup}) -> esqlite3_nif:backup_finish(Backup). %% @doc Do a backup step. -spec backup_step(esqlite3_backup(), integer()) -> ok | '$done' | error(). backup_step(#esqlite3_backup{backup=Backup}, NPage) -> esqlite3_nif:backup_step(Backup, NPage). %% @doc Get the remaining number of pages which need to be backed up. -spec backup_remaining(Backup) -> Remaining when Backup :: esqlite3_backup(), Remaining :: integer(). backup_remaining(#esqlite3_backup{backup=Backup}) -> esqlite3_nif:backup_remaining(Backup). %% @doc Get the remaining number of pages which need to be backed up. -spec backup_pagecount(Backup) -> Pagecount when Backup :: esqlite3_backup(), Pagecount :: integer(). backup_pagecount(#esqlite3_backup{backup=Backup}) -> esqlite3_nif:backup_pagecount(Backup). %% %% Status %% %% @doc Get all internal status information from sqlite. %% -spec status() -> StatusInfo when StatusInfo :: status_info(). status() -> status(false). %% @doc Specify which internal status information you need, when true %% is passed, the highwater information from the status information %% will be reset. -spec status(ArgsOrResetHighWater) -> Status when ArgsOrResetHighWater :: list(atom()) | boolean(), Status :: status_info(). status(Args) when is_list(Args) -> status(Args, false); status(ResetHighWater) when ResetHighWater =:= true orelse ResetHighWater =:= false -> status([memory_used,pagecache_used, pagecache_overflow, malloc_size, parser_stack, pagecache_size, malloc_count], ResetHighWater); status(Op) when is_atom(Op) -> status(Op, false). status(Args, ResetHighWater) when is_list(Args) -> status1(Args, #{}, ResetHighWater); status(Op, ResetHighWater) -> esqlite3_nif:status(op_arg(Op), reset_arg(ResetHighWater)). %% %% Helpers %% status1([], Acc, _ResetArg) -> Acc; status1([S|Rest], Acc, ResetArg) -> status1(Rest, Acc#{ S => status(S, ResetArg)}, ResetArg). reset_arg(true) -> 1; reset_arg(false) -> 0. op_arg(memory_used) -> 0; op_arg(pagecache_used) -> 1; op_arg(pagecache_overflow) -> 2; op_arg(malloc_size) -> 5; op_arg(parser_stack) -> 6; op_arg(pagecache_size) -> 7; op_arg(malloc_count) -> 8. props_to_prepare_flag(Props) -> Flag = case proplists:get_value(no_vtab, Props, false) of true -> ?SQLITE_PREPARE_NO_VTAB; false -> 0 end, case proplists:get_value(persistent, Props, false) of true -> Flag bor ?SQLITE_PREPARE_PERSISTENT; false -> Flag end.