diff --git a/include/sqlite3.hrl b/include/sqlite3.hrl index 0ca0c30..1c56f53 100644 --- a/include/sqlite3.hrl +++ b/include/sqlite3.hrl @@ -32,7 +32,7 @@ -type table_constraints() :: table_constraint() | [table_constraint()]. -type table_info() :: [{atom(), sql_type()} | {atom(), sql_type(), column_constraints()}]. --type sqlite_error() :: {error, integer(), string()}. +-type sqlite_error() :: {error, integer(), string()} | {error, term()}. -type sql_params() :: [sql_value() | {atom() | string() | integer(), sql_value()}]. -type sql_non_query_result() :: ok | sqlite_error() | {rowid, integer()}. -type sql_result() :: sql_non_query_result() | [{columns, [string()]} | {rows, [tuple()]}]. diff --git a/src/sqlite3.erl b/src/sqlite3.erl index dfee112..d4eb130 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -866,9 +866,8 @@ handle_call(list_tables, _From, State) -> TableList = proplists:get_value(rows, Data), TableNames = [erlang:list_to_atom(erlang:binary_to_list(Name)) || {Name} <- TableList], {reply, TableNames, State}; -handle_call({table_info, Tbl}, _From, State) -> +handle_call({table_info, Tbl}, _From, State) when is_atom(Tbl) -> % make sure we only get table info. - % SQL Injection warning SQL = io_lib:format("select sql from sqlite_master where tbl_name = '~p' and type='table';", [Tbl]), Data = do_sql_exec(SQL, State), TableSql = proplists:get_value(rows, Data), @@ -879,6 +878,8 @@ handle_call({table_info, Tbl}, _From, State) -> [] -> {reply, table_does_not_exist, State} end; +handle_call({table_info, _NotAnAtom}, _From, State) -> + {reply, {error, badarg}, State}; handle_call({create_function, FunctionName, Function}, _From, #state{port = Port} = State) -> Reply = exec(Port, {create_function, FunctionName, Function}), {reply, Reply, State}; @@ -891,43 +892,83 @@ handle_call({sql_exec_script, SQL}, _From, State) -> Reply = do_sql_exec_script(SQL, State), {reply, Reply, State}; handle_call({create_table, Tbl, Columns}, _From, State) -> - SQL = sqlite3_lib:create_table_sql(Tbl, Columns), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:create_table_sql(Tbl, Columns) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({create_table, Tbl, Columns, Constraints}, _From, State) -> - SQL = sqlite3_lib:create_table_sql(Tbl, Columns, Constraints), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:create_table_sql(Tbl, Columns, Constraints) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({update, Tbl, Key, Value, Data}, _From, State) -> - SQL = sqlite3_lib:update_sql(Tbl, Key, Value, Data), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:update_sql(Tbl, Key, Value, Data) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({write, Tbl, Data}, _From, State) -> % insert into t1 (data,num) values ('This is sample data',3); - SQL = sqlite3_lib:write_sql(Tbl, Data), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:write_sql(Tbl, Data) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({write_many, Tbl, DataList}, _From, State) -> do_sql_exec("BEGIN;", State), [do_sql_exec(sqlite3_lib:write_sql(Tbl, Data), State) || Data <- DataList], do_handle_call_sql_exec("COMMIT;", State); handle_call({read, Tbl}, _From, State) -> % select * from Tbl where Key = Value; - SQL = sqlite3_lib:read_sql(Tbl), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:read_sql(Tbl) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({read, Tbl, Columns}, _From, State) -> - SQL = sqlite3_lib:read_sql(Tbl, Columns), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:read_sql(Tbl, Columns) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({read, Tbl, Key, Value}, _From, State) -> % select * from Tbl where Key = Value; - SQL = sqlite3_lib:read_sql(Tbl, Key, Value), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:read_sql(Tbl, Key, Value) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({read, Tbl, Key, Value, Columns}, _From, State) -> - SQL = sqlite3_lib:read_sql(Tbl, Key, Value, Columns), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:read_sql(Tbl, Key, Value, Columns) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({delete, Tbl, {Key, Value}}, _From, State) -> % delete from Tbl where Key = Value; - SQL = sqlite3_lib:delete_sql(Tbl, Key, Value), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:delete_sql(Tbl, Key, Value) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({drop_table, Tbl}, _From, State) -> - SQL = sqlite3_lib:drop_table_sql(Tbl), - do_handle_call_sql_exec(SQL, State); + try sqlite3_lib:drop_table_sql(Tbl) of + SQL -> do_handle_call_sql_exec(SQL, State) + catch + _:Exception -> + {reply, {error, Exception}, State} + end; handle_call({prepare, SQL}, _From, State = #state{port = Port, refs = Refs}) -> case exec(Port, {prepare, SQL}) of Index when is_integer(Index) -> @@ -940,17 +981,26 @@ handle_call({prepare, SQL}, _From, State = #state{port = Port, refs = Refs}) -> end, {reply, Reply, NewState}; handle_call({bind, Ref, Params}, _From, State = #state{port = Port, refs = Refs}) -> - Index = dict:fetch(Ref, Refs), - Reply = exec(Port, {bind, Index, Params}), + Reply = case dict:find(Ref, Refs) of + {ok, Index} -> + exec(Port, {bind, Index, Params}); + error -> + {error, badarg} + end, {reply, Reply, State}; handle_call({finalize, Ref}, _From, State = #state{port = Port, refs = Refs}) -> - Index = dict:fetch(Ref, Refs), - case exec(Port, {finalize, Index}) of - ok -> - Reply = ok, - NewState = State#state{refs = dict:erase(Ref, Refs)}; - Error -> - Reply = Error, + case dict:find(Ref, Refs) of + {ok, Index} -> + case exec(Port, {finalize, Index}) of + ok -> + Reply = ok, + NewState = State#state{refs = dict:erase(Ref, Refs)}; + Error -> + Reply = Error, + NewState = State + end; + error -> + Reply = {error, badarg}, NewState = State end, {reply, Reply, NewState}; @@ -959,8 +1009,7 @@ handle_call({Cmd, Ref}, _From, State = #state{port = Port, refs = Refs}) -> {ok, Index} -> exec(Port, {Cmd, Index}); error -> - {error, -1, - "Bad reference to prepared statement; it is already finalized or doesn't exist"} + {error, badarg} end, {reply, Reply, State}; handle_call(vacuum, _From, State) -> @@ -1144,7 +1193,7 @@ wait_result(Port) -> error_logger:error_msg("sqlite3 driver port closed with reason ~p~n", [Reason]), % ?dbg("Error: ~p~n", [Reason]), - {error, -1, Reason}; + {error, Reason}; Other when is_tuple(Other), element(1, Other) =/= '$gen_call', element(1, Other) =/= '$gen_cast' -> error_logger:error_msg("sqlite3 unexpected reply ~p~n", [Other]), @@ -1232,11 +1281,15 @@ build_primary_key_constraint(Tail, Acc) -> %% %% Currently supported constraints for {@link table_info()} and {@link sqlite3:create_table/4}. %% @end -%% @type sqlite_error() = {'error', integer(), string()}. +%% @type sqlite_error() = {'error', integer(), string()} | {'error', any()}. %% -%% Errors are reported by their SQLite result code -%% ([http://www.sqlite.org/c3ref/c_busy_recovery.html]) and a string containing -%% English-language text that describes the error. +%% Errors occuring on the C side are represented by 3-element tuples containing +%% atom 'error', SQLite result code ([http://www.sqlite.org/c3ref/c_abort.html], +%% [http://www.sqlite.org/c3ref/c_busy_recovery.html]) and an English-language error +%% message. +%% +%% Errors occuring on the Erlang side are represented by 2-element tuples with +%% first element 'error'. %% @end %% @type sql_non_query_result() = ok | sqlite_error() | {rowid, integer()}. %% The result returned by functions which call the database but don't return diff --git a/test/sqlite3_test.erl b/test/sqlite3_test.erl index b44f3c2..04c90d5 100644 --- a/test/sqlite3_test.erl +++ b/test/sqlite3_test.erl @@ -42,6 +42,7 @@ all_test_() -> fun close_db/1, [?FuncTest(basic_functionality), ?FuncTest(parametrized), + ?FuncTest(negative), ?FuncTest(blob), ?FuncTest(escaping), ?FuncTest(select_many_records), @@ -127,6 +128,10 @@ parametrized() -> sqlite3:sql_exec(ct, "INSERT INTO user1 (id, name) VALUES (?3, ?5)", [{3, 2}, {5, "joe"}]), sqlite3:sql_exec(ct, "INSERT INTO user1 (id, name) VALUES (:id, @name)", [{":id", 3}, {'@name', <<"jack">>}]), sqlite3:sql_exec(ct, "INSERT INTO user1 (id, name) VALUES (?, ?)", [4, "james"]), + ?WARN_ERROR_MESSAGE, + ?assertMatch( + {error, _, _}, + sqlite3:sql_exec(ct, "INSERT INTO user1 (id, name) VALUES (?, ?)", [4, bad_sql_value])), ?assertEqual( [{columns, ["id", "name"]}, {rows, [{1, <<"john">>}, {2, <<"joe">>}, {3, <<"jack">>}, {4, <<"james">>}]}], @@ -140,6 +145,12 @@ parametrized() -> {rows, [{null, 1.0, {blob, <<1,0,0>>}}]}], sqlite3:read_all(ct, user1)). +negative() -> + drop_table_if_exists(ct, negative), + sqlite3:create_table(ct, negative, [{id, int}]), + ?assertEqual({error, badarg}, + sqlite3:write(ct, negative, [{id, bad_sql_value}])). + blob() -> drop_table_if_exists(ct, blobs), sqlite3:create_table(ct, blobs, [{blob_col, blob}]), @@ -234,6 +245,7 @@ prepared_test() -> sqlite3:write(prepared, user, [{name, "marge"}, {age, 30}, {wage, 2000}]), {ok, Ref1} = sqlite3:prepare(prepared, "SELECT * FROM user"), {ok, Ref2} = sqlite3:prepare(prepared, "SELECT * FROM user WHERE name = ?"), + ?assertMatch({error, _}, sqlite3:next(prepared, make_ref())), ?assertEqual(Columns, sqlite3:columns(prepared, Ref1)), ?assertEqual(Abby, sqlite3:next(prepared, Ref1)), ?assertEqual(ok, sqlite3:reset(prepared, Ref1)), @@ -241,7 +253,7 @@ prepared_test() -> ?assertEqual(Marge, sqlite3:next(prepared, Ref1)), ?assertEqual(done, sqlite3:next(prepared, Ref1)), ?assertEqual(ok, sqlite3:finalize(prepared, Ref1)), - ?assertMatch({error, _, _}, sqlite3:next(prepared, Ref1)), + ?assertMatch({error, _}, sqlite3:next(prepared, Ref1)), ?assertEqual(ok, sqlite3:reset(prepared, Ref2)), ?assertEqual(ok, sqlite3:bind(prepared, Ref2, ["marge"])), ?assertEqual(Marge, sqlite3:next(prepared, Ref2)),