diff --git a/c_src/sqlite3_nif.c b/c_src/sqlite3_nif.c index 53b7813..8fb47e5 100644 --- a/c_src/sqlite3_nif.c +++ b/c_src/sqlite3_nif.c @@ -448,194 +448,124 @@ exqlite_prepare(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) } static ERL_NIF_TERM -bind(ErlNifEnv* env, const ERL_NIF_TERM arg, sqlite3_stmt* statement, int index) +raise_badarg(ErlNifEnv* env, ERL_NIF_TERM term) { - int rc; - int the_int; - ErlNifSInt64 the_long_int; - double the_double; - char the_atom[MAX_ATOM_LENGTH + 1]; - ErlNifBinary the_blob; - int arity; - const ERL_NIF_TERM* tuple; - - if (enif_get_int64(env, arg, &the_long_int)) { - rc = sqlite3_bind_int64(statement, index, the_long_int); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } - - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as 64 bit integer"), - arg)); - } + ERL_NIF_TERM badarg = enif_make_tuple2(env, make_atom(env, "badarg"), term); + return enif_raise_exception(env, badarg); +} - if (enif_get_int(env, arg, &the_int)) { - rc = sqlite3_bind_int(statement, index, the_int); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } +static ERL_NIF_TERM +exqlite_reset(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as integer"), - arg)); - } + sqlite3_reset(statement->statement); + return make_atom(env, "ok"); +} - if (enif_get_double(env, arg, &the_double)) { - rc = sqlite3_bind_double(statement, index, the_double); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } +static ERL_NIF_TERM +exqlite_bind_parameter_count(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as double"), - arg)); - } + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - if (enif_get_atom(env, arg, the_atom, sizeof(the_atom), ERL_NIF_LATIN1)) { - if (0 == strcmp("undefined", the_atom) || 0 == strcmp("nil", the_atom)) { - rc = sqlite3_bind_null(statement, index); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } + int bind_parameter_count = sqlite3_bind_parameter_count(statement->statement); + return enif_make_int(env, bind_parameter_count); +} - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as null"), - arg)); - } +static ERL_NIF_TERM +exqlite_bind_text(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - rc = sqlite3_bind_text(statement, index, the_atom, strlen(the_atom), SQLITE_TRANSIENT); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } + unsigned int idx; + if (!enif_get_uint(env, argv[1], &idx)) + return raise_badarg(env, argv[1]); - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as text"), - arg)); - } + ErlNifBinary text; + if (!enif_inspect_binary(env, argv[2], &text)) + return raise_badarg(env, argv[2]); - if (enif_inspect_iolist_as_binary(env, arg, &the_blob)) { - rc = sqlite3_bind_text(statement, index, (char*)the_blob.data, the_blob.size, SQLITE_TRANSIENT); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } + int rc = sqlite3_bind_text(statement->statement, idx, (char*)text.data, text.size, SQLITE_TRANSIENT); + return enif_make_int(env, rc); +} - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as text"), - arg)); - } +static ERL_NIF_TERM +exqlite_bind_blob(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - if (enif_get_tuple(env, arg, &arity, &tuple)) { - if (arity != 2) { - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as blob"), - arg)); - } + unsigned int idx; + if (!enif_get_uint(env, argv[1], &idx)) + return raise_badarg(env, argv[1]); - if (enif_get_atom(env, tuple[0], the_atom, sizeof(the_atom), ERL_NIF_LATIN1)) { - if (0 == strcmp("blob", the_atom)) { - if (enif_inspect_iolist_as_binary(env, tuple[1], &the_blob)) { - rc = sqlite3_bind_blob(statement, index, the_blob.data, the_blob.size, SQLITE_TRANSIENT); - if (rc == SQLITE_OK) { - return make_atom(env, "ok"); - } - - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument as blob"), - arg)); - } - } - } - } + ErlNifBinary blob; + if (!enif_inspect_binary(env, argv[2], &blob)) + return raise_badarg(env, argv[2]); - return enif_raise_exception( - env, - make_bind_error( - env, - make_message(env, "Failed to bind argument"), - arg)); + int rc = sqlite3_bind_blob(statement->statement, idx, (char*)blob.data, blob.size, SQLITE_TRANSIENT); + return enif_make_int(env, rc); } -/// -/// @brief Binds arguments to the sql statement -/// static ERL_NIF_TERM -exqlite_bind(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +exqlite_bind_integer(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { - assert(env); + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - unsigned int parameter_count = 0; - unsigned int argument_list_length = 0; - connection_t* conn = NULL; - statement_t* statement = NULL; - ERL_NIF_TERM list; - ERL_NIF_TERM head; - ERL_NIF_TERM tail; + unsigned int idx; + if (!enif_get_uint(env, argv[1], &idx)) + return raise_badarg(env, argv[1]); - if (argc != 3) { - return enif_make_badarg(env); - } - - if (!enif_get_resource(env, argv[0], connection_type, (void**)&conn)) { - return make_error_tuple(env, "invalid_connection"); - } - - if (!enif_get_resource(env, argv[1], statement_type, (void**)&statement)) { - return make_error_tuple(env, "invalid_statement"); - } + ErlNifSInt64 i; + if (!enif_get_int64(env, argv[2], &i)) + return raise_badarg(env, argv[2]); - if (!enif_get_list_length(env, argv[2], &argument_list_length)) { - return make_error_tuple(env, "bad_argument_list"); - } + int rc = sqlite3_bind_int64(statement->statement, idx, i); + return enif_make_int(env, rc); +} - parameter_count = (unsigned int)sqlite3_bind_parameter_count(statement->statement); - if (parameter_count != argument_list_length) { - return make_error_tuple(env, "arguments_wrong_length"); - } +static ERL_NIF_TERM +exqlite_bind_float(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - sqlite3_reset(statement->statement); + unsigned int idx; + if (!enif_get_uint(env, argv[1], &idx)) + return raise_badarg(env, argv[1]); - list = argv[2]; - for (unsigned int i = 0; i < argument_list_length; i++) { - enif_get_list_cell(env, list, &head, &tail); - ERL_NIF_TERM result = bind(env, head, statement->statement, i + 1); + double f; + if (!enif_get_double(env, argv[2], &f)) + return raise_badarg(env, argv[2]); - // We are going to ignore this, we have to pass it. - ERL_NIF_TERM reason; + int rc = sqlite3_bind_double(statement->statement, idx, f); + return enif_make_int(env, rc); +} - // Bind will set an exception if anything happens during that phase. - if (enif_has_pending_exception(env, &reason)) { - return make_error_tuple(env, "failed_to_bind_argument"); - } +static ERL_NIF_TERM +exqlite_bind_null(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + statement_t* statement; + if (!enif_get_resource(env, argv[0], statement_type, (void**)&statement)) + return raise_badarg(env, argv[0]); - list = tail; - } + unsigned int idx; + if (!enif_get_uint(env, argv[1], &idx)) + return raise_badarg(env, argv[1]); - return make_atom(env, "ok"); + int rc = sqlite3_bind_null(statement->statement, idx); + return enif_make_int(env, rc); } static ERL_NIF_TERM @@ -1272,6 +1202,38 @@ exqlite_interrupt(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) return make_atom(env, "ok"); } +static ERL_NIF_TERM +exqlite_errmsg(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + connection_t* conn; + statement_t* statement; + const char* msg; + + if (enif_get_resource(env, argv[0], connection_type, (void**)&conn)) { + msg = sqlite3_errmsg(conn->db); + } else if (enif_get_resource(env, argv[0], statement_type, (void**)&statement)) { + msg = sqlite3_errmsg(sqlite3_db_handle(statement->statement)); + } else { + return raise_badarg(env, argv[0]); + } + + if (!msg) + return make_atom(env, "nil"); + + return make_binary(env, msg, strlen(msg)); +} + +static ERL_NIF_TERM +exqlite_errstr(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int rc; + if (!enif_get_int(env, argv[0], &rc)) + return raise_badarg(env, argv[0]); + + const char* msg = sqlite3_errstr(rc); + return make_binary(env, msg, strlen(msg)); +} + // // Most of our nif functions are going to be IO bounded // @@ -1282,7 +1244,13 @@ static ErlNifFunc nif_funcs[] = { {"execute", 2, exqlite_execute, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"changes", 1, exqlite_changes, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"prepare", 2, exqlite_prepare, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"bind", 3, exqlite_bind, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"reset", 1, exqlite_reset, ERL_NIF_DIRTY_JOB_CPU_BOUND}, + {"bind_parameter_count", 1, exqlite_bind_parameter_count}, + {"bind_text", 3, exqlite_bind_text}, + {"bind_blob", 3, exqlite_bind_blob}, + {"bind_integer", 3, exqlite_bind_integer}, + {"bind_float", 3, exqlite_bind_float}, + {"bind_null", 2, exqlite_bind_null}, {"step", 2, exqlite_step, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"multi_step", 3, exqlite_multi_step, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"columns", 2, exqlite_columns, ERL_NIF_DIRTY_JOB_IO_BOUND}, @@ -1295,6 +1263,8 @@ static ErlNifFunc nif_funcs[] = { {"set_update_hook", 2, exqlite_set_update_hook, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"set_log_hook", 1, exqlite_set_log_hook, ERL_NIF_DIRTY_JOB_IO_BOUND}, {"interrupt", 1, exqlite_interrupt, ERL_NIF_DIRTY_JOB_IO_BOUND}, + {"errmsg", 1, exqlite_errmsg}, + {"errstr", 1, exqlite_errstr}, }; ERL_NIF_INIT(Elixir.Exqlite.Sqlite3NIF, nif_funcs, on_load, NULL, NULL, on_unload) diff --git a/lib/exqlite/bind_error.ex b/lib/exqlite/bind_error.ex deleted file mode 100644 index d82a332..0000000 --- a/lib/exqlite/bind_error.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Exqlite.BindError do - @moduledoc """ - An argument failed to bind. - """ - - defexception [:message, :argument] - - @type t :: %__MODULE__{ - message: String.t(), - argument: term() - } - - @impl true - def message(%__MODULE__{message: message, argument: nil}), do: message - - def message(%__MODULE__{message: message, argument: argument}), - do: "#{message} #{inspect(argument)}" -end diff --git a/lib/exqlite/connection.ex b/lib/exqlite/connection.ex index f774ce4..25f8d2d 100644 --- a/lib/exqlite/connection.ex +++ b/lib/exqlite/connection.ex @@ -652,12 +652,12 @@ defmodule Exqlite.Connection do defp bind_params(%Query{ref: ref, statement: statement} = query, params, state) when ref != nil do - case Sqlite3.bind(state.db, ref, params) do - :ok -> - {:ok, query} - - {:error, reason} -> - {:error, %Error{message: to_string(reason), statement: statement}, state} + try do + Sqlite3.bind(ref, params) + rescue + e -> {:error, %Error{message: Exception.message(e), statement: statement}, state} + else + :ok -> {:ok, query} end end diff --git a/lib/exqlite/sqlite3.ex b/lib/exqlite/sqlite3.ex index 0ac3404..13c410c 100644 --- a/lib/exqlite/sqlite3.ex +++ b/lib/exqlite/sqlite3.ex @@ -110,25 +110,100 @@ defmodule Exqlite.Sqlite3 do @spec prepare(db(), String.t()) :: {:ok, statement()} | {:error, reason()} def prepare(conn, sql), do: Sqlite3NIF.prepare(conn, sql) - @spec bind(db(), statement(), nil) :: :ok | {:error, reason()} - def bind(conn, statement, nil), do: bind(conn, statement, []) - - @spec bind(db(), statement(), list()) :: :ok | {:error, reason()} - def bind(conn, statement, args) do - Sqlite3NIF.bind(conn, statement, Enum.map(args, &convert/1)) - rescue - err in ErlangError -> - case err do - %{original: %{message: message, argument: argument}} -> - reraise Exqlite.BindError, - [message: message, argument: argument], - __STACKTRACE__ - - %{reason: message} -> - reraise Exqlite.BindError, [message: message], __STACKTRACE__ - end + @doc """ + Resets a prepared statement. + + See: https://sqlite.org/c3ref/reset.html + """ + @spec reset(statement) :: :ok + def reset(stmt), do: Sqlite3NIF.reset(stmt) + + @doc """ + Returns number of SQL parameters in a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?") + iex> Sqlite3.bind_parameter_count(stmt) + 2 + + """ + @spec bind_parameter_count(statement) :: integer + def bind_parameter_count(stmt), do: Sqlite3NIF.bind_parameter_count(stmt) + + @type bind_value :: + NaiveDateTime.t() + | DateTime.t() + | Date.t() + | Time.t() + | number + | iodata + | {:blob, iodata} + | atom + + @deprecated "Use `bind/2` instead" + @spec bind(db, statement, [bind_value]) :: :ok + def bind(_conn, stmt, args), do: bind(stmt, args) + + @doc """ + Resets a prepared statement and binds values to it. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?, ?, ?, ?") + iex> Sqlite3.bind(stmt, [42, 3.14, "Alice", {:blob, <<0, 0, 0>>}, nil]) + iex> Sqlite3.step(conn, stmt) + {:row, [42, 3.14, "Alice", <<0, 0, 0>>, nil]} + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind(stmt, [42, 3.14, "Alice"]) + ** (ArgumentError) expected 1 arguments, got 3 + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?") + iex> Sqlite3.bind(stmt, [42]) + ** (ArgumentError) expected 2 arguments, got 1 + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind(stmt, [:erlang.list_to_pid(~c"<0.0.0>")]) + ** (ArgumentError) unsupported type: #PID<0.0.0> + + """ + @spec bind(statement, [bind_value] | nil) :: :ok + def bind(stmt, nil), do: bind(stmt, []) + + def bind(stmt, args) do + params_count = bind_parameter_count(stmt) + args_count = length(args) + + if args_count == params_count do + reset(stmt) + bind_all(args, stmt, 1) + else + raise ArgumentError, "expected #{params_count} arguments, got #{args_count}" + end + end + + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + defp bind_all([param | params], stmt, idx) do + case convert(param) do + i when is_integer(i) -> bind_integer(stmt, idx, i) + f when is_float(f) -> bind_float(stmt, idx, f) + b when is_binary(b) -> bind_text(stmt, idx, b) + b when is_list(b) -> bind_text(stmt, idx, IO.iodata_to_binary(b)) + nil -> bind_null(stmt, idx) + :undefined -> bind_null(stmt, idx) + a when is_atom(a) -> bind_text(stmt, idx, Atom.to_string(a)) + {:blob, b} when is_binary(b) -> bind_blob(stmt, idx, b) + {:blob, b} when is_list(b) -> bind_blob(stmt, idx, IO.iodata_to_binary(b)) + _other -> raise ArgumentError, "unsupported type: #{inspect(param)}" + end + + bind_all(params, stmt, idx + 1) end + defp bind_all([], _stmt, _idx), do: :ok + @spec columns(db(), statement()) :: {:ok, [binary()]} | {:error, reason()} def columns(conn, statement), do: Sqlite3NIF.columns(conn, statement) @@ -303,6 +378,96 @@ defmodule Exqlite.Sqlite3 do Sqlite3NIF.set_log_hook(pid) end + @sqlite_ok 0 + + @doc """ + Binds a text value to a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind_text(stmt, 1, "Alice") + :ok + + """ + @spec bind_text(statement, non_neg_integer, String.t()) :: :ok + def bind_text(stmt, index, text) do + case Sqlite3NIF.bind_text(stmt, index, text) do + @sqlite_ok -> :ok + rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc) + end + end + + @doc """ + Binds a blob value to a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind_blob(stmt, 1, <<0, 0, 0>>) + :ok + + """ + @spec bind_blob(statement, non_neg_integer, binary) :: :ok + def bind_blob(stmt, index, blob) do + case Sqlite3NIF.bind_blob(stmt, index, blob) do + @sqlite_ok -> :ok + rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc) + end + end + + @doc """ + Binds an integer value to a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind_integer(stmt, 1, 42) + :ok + + """ + @spec bind_integer(statement, non_neg_integer, integer) :: :ok + def bind_integer(stmt, index, integer) do + case Sqlite3NIF.bind_integer(stmt, index, integer) do + @sqlite_ok -> :ok + rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc) + end + end + + @doc """ + Binds a float value to a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind_float(stmt, 1, 3.14) + :ok + + """ + @spec bind_float(statement, non_neg_integer, float) :: :ok + def bind_float(stmt, index, float) do + case Sqlite3NIF.bind_float(stmt, index, float) do + @sqlite_ok -> :ok + rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc) + end + end + + @doc """ + Binds a null value to a prepared statement. + + iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?") + iex> Sqlite3.bind_null(stmt, 1) + :ok + + """ + @spec bind_null(statement, non_neg_integer) :: :ok + def bind_null(stmt, index) do + case Sqlite3NIF.bind_null(stmt, index) do + @sqlite_ok -> :ok + rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc) + end + end + + defp errmsg(stmt), do: Sqlite3NIF.errmsg(stmt) + defp errstr(rc), do: Sqlite3NIF.errstr(rc) + defp convert(%Date{} = val), do: Date.to_iso8601(val) defp convert(%Time{} = val), do: Time.to_iso8601(val) defp convert(%NaiveDateTime{} = val), do: NaiveDateTime.to_iso8601(val) diff --git a/lib/exqlite/sqlite3_nif.ex b/lib/exqlite/sqlite3_nif.ex index 356daca..85a2800 100644 --- a/lib/exqlite/sqlite3_nif.ex +++ b/lib/exqlite/sqlite3_nif.ex @@ -35,10 +35,6 @@ defmodule Exqlite.Sqlite3NIF do @spec prepare(db(), String.t()) :: {:ok, statement()} | {:error, reason()} def prepare(_conn, _sql), do: :erlang.nif_error(:not_loaded) - @spec bind(db(), statement(), list()) :: - :ok | {:error, reason()} | {:error, {atom(), any()}} - def bind(_conn, _statement, _args), do: :erlang.nif_error(:not_loaded) - @spec step(db(), statement()) :: :done | :busy | {:row, row()} | {:error, reason()} def step(_conn, _statement), do: :erlang.nif_error(:not_loaded) @@ -73,5 +69,32 @@ defmodule Exqlite.Sqlite3NIF do @spec set_log_hook(pid()) :: :ok | {:error, reason()} def set_log_hook(_pid), do: :erlang.nif_error(:not_loaded) + @spec bind_parameter_count(statement) :: integer + def bind_parameter_count(_stmt), do: :erlang.nif_error(:not_loaded) + + @spec bind_text(statement, non_neg_integer, String.t()) :: :ok + def bind_text(_stmt, _index, _text), do: :erlang.nif_error(:not_loaded) + + @spec bind_blob(statement, non_neg_integer, binary) :: :ok + def bind_blob(_stmt, _index, _blob), do: :erlang.nif_error(:not_loaded) + + @spec bind_integer(statement, non_neg_integer, integer) :: :ok + def bind_integer(_stmt, _index, _integer), do: :erlang.nif_error(:not_loaded) + + @spec bind_float(statement, non_neg_integer, float) :: :ok + def bind_float(_stmt, _index, _float), do: :erlang.nif_error(:not_loaded) + + @spec bind_null(statement, non_neg_integer) :: :ok + def bind_null(_stmt, _index), do: :erlang.nif_error(:not_loaded) + + @spec reset(statement) :: :ok + def reset(_stmt), do: :erlang.nif_error(:not_loaded) + + @spec errmsg(db | statement) :: String.t() | nil + def errmsg(_db_or_stmt), do: :erlang.nif_error(:not_loaded) + + @spec errstr(integer) :: String.t() + def errstr(_rc), do: :erlang.nif_error(:not_loaded) + # add statement inspection tooling https://sqlite.org/c3ref/expanded_sql.html end diff --git a/test/exqlite/sqlite3_test.exs b/test/exqlite/sqlite3_test.exs index 5bb908f..485ce29 100644 --- a/test/exqlite/sqlite3_test.exs +++ b/test/exqlite/sqlite3_test.exs @@ -2,6 +2,7 @@ defmodule Exqlite.Sqlite3Test do use ExUnit.Case alias Exqlite.Sqlite3 + doctest Exqlite.Sqlite3 describe ".open/1" do test "opens a database in memory" do @@ -253,7 +254,10 @@ defmodule Exqlite.Sqlite3Test do Sqlite3.execute(conn, "create table test (id integer primary key, stuff text)") {:ok, statement} = Sqlite3.prepare(conn, "insert into test (stuff) values (?1)") - {:error, :arguments_wrong_length} = Sqlite3.bind(conn, statement, []) + + assert_raise ArgumentError, "expected 1 arguments, got 0", fn -> + Sqlite3.bind(conn, statement, []) + end end test "binds datetime value as string" do @@ -296,6 +300,165 @@ defmodule Exqlite.Sqlite3Test do end end + describe ".bind_text/3" do + setup do + {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + {:ok, stmt} = Sqlite3.prepare(conn, "select ?") + {:ok, conn: conn, stmt: stmt} + end + + test "binds text value", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_text(stmt, 1, "hello") + assert {:row, ["hello"]} = Sqlite3.step(conn, stmt) + end + + test "binds emojis", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_text(stmt, 1, "hello 👋 world 🌏") + assert {:row, ["hello 👋 world 🌏"]} = Sqlite3.step(conn, stmt) + end + + test "errors on invalid statement" do + assert_raise ArgumentError, "argument error: nil", fn -> + Sqlite3.bind_text(_not_stmt = nil, 1, "hello") + end + end + + test "errors on invalid index", %{stmt: stmt} do + assert_raise Exqlite.Error, "column index out of range", fn -> + Sqlite3.bind_text(stmt, _out_of_range = 2, "hello") + end + end + + test "errors on invalid text argument", %{stmt: stmt} do + assert_raise ArgumentError, "argument error: 1", fn -> + Sqlite3.bind_text(stmt, 1, _not_text = 1) + end + end + end + + describe ".bind_blob/3" do + setup do + {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + {:ok, stmt} = Sqlite3.prepare(conn, "select ?") + {:ok, conn: conn, stmt: stmt} + end + + test "binds binary value", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_blob(stmt, 1, <<0, 0, 0>>) + assert {:row, [<<0, 0, 0>>]} = Sqlite3.step(conn, stmt) + end + + test "errors on invalid statement" do + assert_raise ArgumentError, "argument error: nil", fn -> + Sqlite3.bind_blob(_not_stmt = nil, 1, "hello") + end + end + + test "errors on invalid index", %{stmt: stmt} do + assert_raise Exqlite.Error, "column index out of range", fn -> + Sqlite3.bind_blob(stmt, _out_of_range = 2, "hello") + end + end + + test "errors on invalid blob argument", %{stmt: stmt} do + assert_raise ArgumentError, "argument error: 1", fn -> + Sqlite3.bind_blob(stmt, 1, _not_binary = 1) + end + end + end + + describe ".bind_integer/3" do + setup do + {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + {:ok, stmt} = Sqlite3.prepare(conn, "select ?") + {:ok, conn: conn, stmt: stmt} + end + + test "binds integer value", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_integer(stmt, 1, 42) + assert {:row, [42]} = Sqlite3.step(conn, stmt) + end + + test "binds integers larger than INT32_MAX", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_integer(stmt, 1, 0xFFFFFFFF + 1) + assert {:row, [0x100000000]} = Sqlite3.step(conn, stmt) + end + + test "errors on invalid statement" do + assert_raise ArgumentError, "argument error: nil", fn -> + Sqlite3.bind_integer(_not_stmt = nil, 1, 42) + end + end + + test "errors on invalid index", %{stmt: stmt} do + assert_raise Exqlite.Error, "column index out of range", fn -> + Sqlite3.bind_integer(stmt, _out_of_range = 2, 42) + end + end + + test "errors on invalid blob argument", %{stmt: stmt} do + assert_raise ArgumentError, "argument error: \"42\"", fn -> + Sqlite3.bind_integer(stmt, 1, _not_integer = "42") + end + end + end + + describe ".bind_float/3" do + setup do + {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + {:ok, stmt} = Sqlite3.prepare(conn, "select ?") + {:ok, conn: conn, stmt: stmt} + end + + test "binds float value", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_float(stmt, 1, 3.14) + assert {:row, [3.14]} = Sqlite3.step(conn, stmt) + end + + test "errors on invalid statement" do + assert_raise ArgumentError, "argument error: nil", fn -> + Sqlite3.bind_float(_not_stmt = nil, 1, 3.14) + end + end + + test "errors on invalid index", %{stmt: stmt} do + assert_raise Exqlite.Error, "column index out of range", fn -> + Sqlite3.bind_float(stmt, _out_of_range = 2, 3.14) + end + end + + test "errors on invalid blob argument", %{stmt: stmt} do + assert_raise ArgumentError, "argument error: \"3.14\"", fn -> + Sqlite3.bind_float(stmt, 1, _not_float = "3.14") + end + end + end + + describe ".bind_null/2" do + setup do + {:ok, conn} = Sqlite3.open(":memory:", [:readonly]) + {:ok, stmt} = Sqlite3.prepare(conn, "select ?") + {:ok, conn: conn, stmt: stmt} + end + + test "binds null value", %{conn: conn, stmt: stmt} do + assert :ok = Sqlite3.bind_null(stmt, 1) + assert {:row, [nil]} = Sqlite3.step(conn, stmt) + end + + test "errors on invalid statement" do + assert_raise ArgumentError, "argument error: nil", fn -> + Sqlite3.bind_null(_not_stmt = nil, 1) + end + end + + test "errors on invalid index", %{stmt: stmt} do + assert_raise Exqlite.Error, "column index out of range", fn -> + Sqlite3.bind_null(stmt, _out_of_range = 2) + end + end + end + describe ".columns/2" do test "returns the column definitions" do {:ok, conn} = Sqlite3.open(":memory:") @@ -375,9 +538,9 @@ defmodule Exqlite.Sqlite3Test do {:ok, statement} = Sqlite3.prepare(conn, "insert into test (stuff) values (?1)") - assert_raise Exqlite.BindError, fn -> - Sqlite3.bind(conn, statement, [%ArgumentError{}]) - end + assert_raise ArgumentError, + "unsupported type: %ArgumentError{message: \"argument error\"}", + fn -> Sqlite3.bind(conn, statement, [%ArgumentError{}]) end end end