diff --git a/c_src/sqlite3_nif.c b/c_src/sqlite3_nif.c index 8d08d30..b4d2a4c 100644 --- a/c_src/sqlite3_nif.c +++ b/c_src/sqlite3_nif.c @@ -109,20 +109,6 @@ exqlite_mem_shutdown(void* ptr) { } -static const char* -get_sqlite3_error_msg(int rc, sqlite3* db) -{ - if (rc == SQLITE_MISUSE) { - return "Sqlite3 was invoked incorrectly."; - } - - const char* message = sqlite3_errmsg(db); - if (!message) { - return "No error message available."; - } - return message; -} - static ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* atom_name) { @@ -174,15 +160,15 @@ make_binary(ErlNifEnv* env, const void* bytes, unsigned int size) } static ERL_NIF_TERM -make_sqlite3_error_tuple(ErlNifEnv* env, int rc, sqlite3* db) +make_sqlite3_error_tuple(ErlNifEnv* env, int rc) { - const char* msg = get_sqlite3_error_msg(rc, db); - size_t len = strlen(msg); + const char* errstr = sqlite3_errstr(rc); - return enif_make_tuple2( + return enif_make_tuple3( env, make_atom(env, "error"), - make_binary(env, msg, len)); + enif_make_int(env, rc), + make_binary(env, errstr, strlen(errstr))); } static ERL_NIF_TERM @@ -209,12 +195,12 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) } if (!enif_get_int(env, argv[1], &flags)) { - return make_error_tuple(env, "invalid flags"); + return make_error_tuple(env, "invalid_flags"); } rc = sqlite3_open_v2(filename, &db, flags, NULL); if (rc != SQLITE_OK) { - return make_error_tuple(env, "database_open_failed"); + make_sqlite3_error_tuple(env, rc); } mutex = enif_mutex_create("exqlite:connection"); @@ -223,8 +209,6 @@ exqlite_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) return make_error_tuple(env, "failed_to_create_mutex"); } - sqlite3_busy_timeout(db, 2000); - conn = enif_alloc_resource(connection_type, sizeof(connection_t)); if (!conn) { sqlite3_close_v2(db); @@ -265,7 +249,7 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) if (autocommit == 0) { rc = sqlite3_exec(conn->db, "ROLLBACK;", NULL, NULL, NULL); if (rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } } @@ -282,7 +266,7 @@ exqlite_close(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) rc = sqlite3_close_v2(conn->db); if (rc != SQLITE_OK) { enif_mutex_unlock(conn->mutex); - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } conn->db = NULL; @@ -318,7 +302,7 @@ exqlite_execute(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) rc = sqlite3_exec(conn->db, (char*)bin.data, NULL, NULL, NULL); if (rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } return make_atom(env, "ok"); @@ -395,7 +379,7 @@ exqlite_prepare(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) if (rc != SQLITE_OK) { enif_release_resource(statement); - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } result = enif_make_resource(env, statement); @@ -510,7 +494,7 @@ exqlite_bind(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) } if (rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } list = tail; @@ -614,10 +598,6 @@ exqlite_multi_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) int rc = sqlite3_step(statement->statement); switch (rc) { - case SQLITE_BUSY: - sqlite3_reset(statement->statement); - return make_atom(env, "busy"); - case SQLITE_DONE: return enif_make_tuple2(env, make_atom(env, "done"), rows); @@ -628,7 +608,7 @@ exqlite_multi_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) default: sqlite3_reset(statement->statement); - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } } @@ -662,12 +642,10 @@ exqlite_step(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) env, make_atom(env, "row"), make_row(env, statement->statement)); - case SQLITE_BUSY: - return make_atom(env, "busy"); case SQLITE_DONE: return make_atom(env, "done"); default: - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } } @@ -843,7 +821,7 @@ exqlite_deserialize(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) memcpy(buffer, serialized.data, size); rc = sqlite3_deserialize(conn->db, "main", buffer, size, size, flags); if (rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } return make_atom(env, "ok"); @@ -988,7 +966,7 @@ exqlite_enable_load_extension(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[ rc = sqlite3_enable_load_extension(conn->db, enable_load_extension_value); if (rc != SQLITE_OK) { - return make_sqlite3_error_tuple(env, rc, conn->db); + return make_sqlite3_error_tuple(env, rc); } return make_atom(env, "ok"); } diff --git a/lib/exqlite.ex b/lib/exqlite.ex index 766d577..df67aaf 100644 --- a/lib/exqlite.ex +++ b/lib/exqlite.ex @@ -270,11 +270,16 @@ defmodule Exqlite do # TODO sql / statement @compile inline: [wrap_error: 1] - defp wrap_error({:error, error}) when is_list(error) do - {:error, SQLiteError.exception(error)} + defp wrap_error({:error, rc, message}) do + {:error, SQLiteError.exception(rc: rc, message: message)} end - defp wrap_error({:error, reason}) do + defp wrap_error({:error, {:wrong_type, value}}) do + message = "unsupported type for bind: " <> inspect(value) + {:error, UsageError.exception(message: message)} + end + + defp wrap_error({:error, reason}) when is_atom(reason) do {:error, UsageError.exception(message: reason)} end diff --git a/lib/exqlite/sqlite_error.ex b/lib/exqlite/sqlite_error.ex index f0eea31..3d7df4e 100644 --- a/lib/exqlite/sqlite_error.ex +++ b/lib/exqlite/sqlite_error.ex @@ -3,14 +3,6 @@ defmodule Exqlite.SQLiteError do The error emitted from SQLite. """ - defexception [:message, :statement] - - @type t :: %__MODULE__{message: String.t(), statement: String.t()} - - @impl true - def message(%__MODULE__{message: message, statement: nil}), do: message - - def message(%__MODULE__{message: message, statement: statement}) do - "#{message}: #{statement}" - end + defexception [:rc, :message] + @type t :: %__MODULE__{rc: integer, message: String.t()} end diff --git a/test/exqlite/extensions_test.exs b/test/exqlite/extensions_test.exs index bd23ab8..fc93b0c 100644 --- a/test/exqlite/extensions_test.exs +++ b/test/exqlite/extensions_test.exs @@ -20,7 +20,7 @@ defmodule Exqlite.ExtensionsTest do assert :ok = Exqlite.disable_load_extension(conn) - assert {:error, %Exqlite.UsageError{message: "not authorized"}} = + assert {:error, %Exqlite.SQLiteError{rc: 1, message: "SQL logic error"}} = Exqlite.prepare_fetch_all( conn, "select load_extension(?)", diff --git a/test/exqlite/integration_test.exs b/test/exqlite/integration_test.exs index 1a35e85..0eeb4ec 100644 --- a/test/exqlite/integration_test.exs +++ b/test/exqlite/integration_test.exs @@ -124,12 +124,12 @@ defmodule Exqlite.IntegrationTest do :ok = Exqlite.execute(conn1, "begin immediate") assert {:ok, :transaction} = Exqlite.transaction_status(conn1) - assert {:error, %Exqlite.UsageError{} = error} = + assert {:error, %Exqlite.SQLiteError{rc: 5} = error} = Exqlite.execute(conn2, "begin immediate") assert error.message == "database is locked" - # TODO assert Exception.message(error) == "database is locked" + assert {:ok, :idle} = Exqlite.transaction_status(conn2) :ok = Exqlite.execute(conn1, "commit") diff --git a/test/exqlite/sqlite_error_test.exs b/test/exqlite/sqlite_error_test.exs deleted file mode 100644 index 13ea8a2..0000000 --- a/test/exqlite/sqlite_error_test.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Exqlite.SQLiteErrorTest do - use ExUnit.Case, async: true - alias Exqlite.SQLiteError - - describe "message/1" do - test "with :statement" do - assert "a: b" == Exception.message(%SQLiteError{message: "a", statement: "b"}) - end - - test "without :statement" do - assert "a" == Exception.message(%SQLiteError{message: "a"}) - end - end -end diff --git a/test/exqlite_test.exs b/test/exqlite_test.exs index 3df5147..4856d1d 100644 --- a/test/exqlite_test.exs +++ b/test/exqlite_test.exs @@ -67,14 +67,15 @@ defmodule ExqliteTest do "select id, stuff from test order by id asc" ) - assert {:error, - %Exqlite.UsageError{message: "attempt to write a readonly database"} = - error} = + assert {:error, %Exqlite.SQLiteError{} = error} = Exqlite.execute( conn, "insert into test (stuff) values ('This is a test')" ) + assert error.rc == 8 + assert error.message == "attempt to write a readonly database" + assert Exception.message(error) == "attempt to write a readonly database" end @@ -110,8 +111,9 @@ defmodule ExqliteTest do {:ok, stmt} = Exqlite.prepare(conn, "insert into test(col) values(?)") :ok = Exqlite.bind(conn, stmt, ["something"]) - {:error, %Exqlite.UsageError{} = error} = Exqlite.step(conn, stmt) + {:error, %Exqlite.SQLiteError{} = error} = Exqlite.step(conn, stmt) + assert error.rc == 8 assert error.message == "attempt to write a readonly database" end @@ -172,7 +174,7 @@ defmodule ExqliteTest do end test "handles incorrect syntax", %{conn: conn} do - assert {:error, %Exqlite.UsageError{message: "near \"a\": syntax error"}} = + assert {:error, %Exqlite.SQLiteError{rc: 1, message: "SQL logic error"}} = Exqlite.execute( conn, "create a dumb table test (id integer primary key, stuff text)" @@ -326,14 +328,14 @@ defmodule ExqliteTest do end test "users table does not exist", %{conn: conn} do - assert {:error, %Exqlite.UsageError{} = error} = + assert {:error, %Exqlite.SQLiteError{rc: 1} = error} = Exqlite.prepare(conn, "select * from users where id < ?") - assert Exception.message(error) == "no such table: users" + assert Exception.message(error) == "SQL logic error" end test "supports utf8 in error messages", %{conn: conn} do - assert {:error, %Exqlite.UsageError{message: "no such table: 🌍"}} = + assert {:error, %Exqlite.SQLiteError{rc: 1, message: "SQL logic error"}} = Exqlite.prepare(conn, "select * from 🌍") end end @@ -427,21 +429,23 @@ defmodule ExqliteTest do end test "doesn't bind datetime value as string", %{conn: conn, stmt: stmt} do - assert {:error, - %Exqlite.UsageError{message: {:wrong_type, %DateTime{}}} = - error} = - Exqlite.bind(conn, stmt, [DateTime.utc_now()]) + utc_now = ~U[2023-12-23 05:56:02.253039Z] - assert is_binary(Exception.message(error)) + assert {:error, %Exqlite.UsageError{} = error} = + Exqlite.bind(conn, stmt, [utc_now]) + + assert Exception.message(error) == + "unsupported type for bind: ~U[2023-12-23 05:56:02.253039Z]" end test "doesn't bind date value as string", %{conn: conn, stmt: stmt} do - assert {:error, - %Exqlite.UsageError{message: {:wrong_type, %Date{}}} = - error} = - Exqlite.bind(conn, stmt, [Date.utc_today()]) + utc_today = Date.utc_today() - assert is_binary(Exception.message(error)) + assert {:error, %Exqlite.UsageError{} = error} = + Exqlite.bind(conn, stmt, [utc_today]) + + assert Exception.message(error) == + "unsupported type for bind: #{inspect(utc_today)}" end end @@ -522,14 +526,16 @@ defmodule ExqliteTest do :ok = Exqlite.close(conn) assert :ok = Exqlite.bind(conn, stmt, ["this is a test"]) - assert {:error, - %Exqlite.UsageError{message: "Sqlite3 was invoked incorrectly."} = error} = + assert {:error, %Exqlite.SQLiteError{} = error} = Exqlite.execute( conn, "create table test (id integer primary key, stuff text)" ) - assert Exception.message(error) == "Sqlite3 was invoked incorrectly." + assert error.rc == 21 + assert error.message == "bad parameter or other API misuse" + assert Exception.message(error) == "bad parameter or other API misuse" + assert :done == Exqlite.step(conn, stmt) end end @@ -637,7 +643,7 @@ defmodule ExqliteTest do test "can receive errors", %{conn: conn} do assert :ok = Exqlite.set_log_hook(self()) - assert {:error, %Exqlite.UsageError{message: "near \"some\": syntax error"}} = + assert {:error, %Exqlite.SQLiteError{rc: 1, message: "SQL logic error"}} = Exqlite.prepare(conn, "some invalid sql") assert_receive {:log, rc, msg} @@ -653,9 +659,12 @@ defmodule ExqliteTest do Task.async(fn -> :ok = Exqlite.set_log_hook(self()) - assert {:error, %Exqlite.UsageError{message: "near \"some\": syntax error"}} = + assert {:error, %Exqlite.SQLiteError{} = error} = Exqlite.prepare(conn, "some invalid sql") + assert error.rc == 1 + assert error.message == "SQL logic error" + assert_receive {:log, rc, msg} assert rc == 1 assert msg == "near \"some\": syntax error in \"some invalid sql\""