From 9423da353c16a88fef2a22d14e387c806d6fae1d Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Sat, 15 Jan 2011 14:17:50 +0300 Subject: [PATCH 01/10] Ignore .so and .beam --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8e53e12..de5856a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ TEST*.xml *.obj *.dll rebar.cross_compile.config -config.tmp \ No newline at end of file +config.tmp +*.beam +*.so From 108efe269b046b4099569d5778e93600b3888f3d Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Sat, 15 Jan 2011 14:24:58 +0300 Subject: [PATCH 02/10] Don't remove priv/ entirely on make clean. Just in case we want to add something other than .so/.dll there later. --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index 860e788..6186eee 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -17,7 +17,7 @@ test: $(REBAR_COMPILE) eunit clean: - -rm -rf deps ebin priv doc/* .eunit c_src/*.o config.tmp + -rm -rf deps ebin priv/*.so doc/* .eunit/* c_src/*.o config.tmp docs: $(REBAR_COMPILE) doc From 7c4ccd419ea592af0281df53edbb2c1e4bc5056c Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 11:44:39 +0300 Subject: [PATCH 03/10] Check statement for NULL after sqlite3_prepare_v2 --- c_src/sqlite3_drv.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index bc03fda..a82fe8a 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -251,7 +251,7 @@ static int sql_exec(sqlite3_drv_t *drv, char *command, int command_size) { #endif result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, (const char **) &rest); - if (result != SQLITE_OK) { + if ((result != SQLITE_OK) || (statement == NULL)) { return output_db_error(drv); } @@ -473,7 +473,7 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) (const char **) &rest); driver_free(command); - if (result != SQLITE_OK) { + if ((result != SQLITE_OK) || (statement == NULL)) { return output_db_error(drv); } @@ -927,7 +927,7 @@ static int prepare(sqlite3_drv_t *drv, char *command, int command_size) { #endif result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, (const char **) &rest); - if (result != SQLITE_OK) { + if ((result != SQLITE_OK) || (statement == NULL)) { return output_db_error(drv); } From 2cfabf6d7557995910f846770b737d9c775c4f60 Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 11:50:08 +0300 Subject: [PATCH 04/10] Remove const type cast --- c_src/sqlite3_drv.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index a82fe8a..50f4edd 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -242,15 +242,14 @@ static inline int sql_exec_statement( static int sql_exec(sqlite3_drv_t *drv, char *command, int command_size) { int result; - char *rest = NULL; + const char *rest = NULL; sqlite3_stmt *statement; #ifdef DEBUG fprintf(drv->log, "Preexec: %.*s\n", command_size, command); fflush(drv->log); #endif - result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, - (const char **) &rest); + result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, &rest); if ((result != SQLITE_OK) || (statement == NULL)) { return output_db_error(drv); } @@ -441,7 +440,7 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) int result; int index = 0; int type, size; - char *rest = NULL; + const char *rest = NULL; sqlite3_stmt *statement; long bin_size; char *command; @@ -469,8 +468,7 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) command = driver_alloc(size * sizeof(char)); ei_decode_binary(buffer, &index, command, &bin_size); // assert(bin_size == size) - result = sqlite3_prepare_v2(drv->db, command, size, &statement, - (const char **) &rest); + result = sqlite3_prepare_v2(drv->db, command, size, &statement, &rest); driver_free(command); if ((result != SQLITE_OK) || (statement == NULL)) { @@ -917,7 +915,7 @@ static void ready_async(ErlDrvData drv_data, ErlDrvThreadData thread_data) { static int prepare(sqlite3_drv_t *drv, char *command, int command_size) { int result; - char *rest = NULL; + const char *rest = NULL; sqlite3_stmt *statement; ErlDrvTermData spec[6]; @@ -925,8 +923,7 @@ static int prepare(sqlite3_drv_t *drv, char *command, int command_size) { fprintf(drv->log, "Preparing statement: %.*s\n", command_size, command); fflush(drv->log); #endif - result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, - (const char **) &rest); + result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, &rest); if ((result != SQLITE_OK) || (statement == NULL)) { return output_db_error(drv); } From 713420989680f46fe520d3c10664bb08ab81898a Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 11:59:03 +0300 Subject: [PATCH 05/10] Remove useless assignment to const char *rest --- c_src/sqlite3_drv.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index 50f4edd..4ba530d 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -242,7 +242,7 @@ static inline int sql_exec_statement( static int sql_exec(sqlite3_drv_t *drv, char *command, int command_size) { int result; - const char *rest = NULL; + const char *rest; sqlite3_stmt *statement; #ifdef DEBUG @@ -440,7 +440,7 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) int result; int index = 0; int type, size; - const char *rest = NULL; + const char *rest; sqlite3_stmt *statement; long bin_size; char *command; @@ -915,7 +915,7 @@ static void ready_async(ErlDrvData drv_data, ErlDrvThreadData thread_data) { static int prepare(sqlite3_drv_t *drv, char *command, int command_size) { int result; - const char *rest = NULL; + const char *rest; sqlite3_stmt *statement; ErlDrvTermData spec[6]; From 9d138346cd341ca7ae8ce4707b4a0225b8f3af19 Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 12:38:38 +0300 Subject: [PATCH 06/10] Suitable error message for empty statements --- c_src/sqlite3_drv.c | 12 +++++++++--- test/sqlite3_test.erl | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index 4ba530d..1b2e42a 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -250,8 +250,10 @@ static int sql_exec(sqlite3_drv_t *drv, char *command, int command_size) { fflush(drv->log); #endif result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, &rest); - if ((result != SQLITE_OK) || (statement == NULL)) { + if (result != SQLITE_OK) { return output_db_error(drv); + } else if (statement == NULL) { + return output_error(drv, SQLITE_MISUSE, "empty statement"); } return sql_exec_statement(drv, statement); @@ -471,8 +473,10 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) result = sqlite3_prepare_v2(drv->db, command, size, &statement, &rest); driver_free(command); - if ((result != SQLITE_OK) || (statement == NULL)) { + if (result != SQLITE_OK) { return output_db_error(drv); + } else if (statement == NULL) { + return output_error(drv, SQLITE_MISUSE, "empty statement"); } result = bind_parameters(drv, buffer, buffer_size, &index, statement, &type, &size); @@ -924,8 +928,10 @@ static int prepare(sqlite3_drv_t *drv, char *command, int command_size) { fflush(drv->log); #endif result = sqlite3_prepare_v2(drv->db, command, command_size, &statement, &rest); - if ((result != SQLITE_OK) || (statement == NULL)) { + if (result != SQLITE_OK) { return output_db_error(drv); + } else if (statement == NULL) { + return output_error(drv, SQLITE_MISUSE, "empty statement"); } if (drv->prepared_count >= drv->prepared_alloc) { diff --git a/test/sqlite3_test.erl b/test/sqlite3_test.erl index 5f117d0..e7bf266 100644 --- a/test/sqlite3_test.erl +++ b/test/sqlite3_test.erl @@ -61,6 +61,10 @@ basic_functionality() -> AbbyOnly = [{1, <<"abby">>, 20, 2000}], TableInfo = [{id, integer, [primary_key]}, {name, text, [not_null, unique]}, {age, integer}, {wage, integer}], drop_all_tables(ct), + ?debugMsg("Error message \"sqlite3 driver error: empty statement\" should be shown..."), + ?assertEqual( + {error, 21, "empty statement"}, + sqlite3:sql_exec(ct, "-- Comment")), ?assertEqual( [], sqlite3:list_tables(ct)), From 9f57f6ae39e34705e8fb929f1630005dc56b0893 Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 14:01:35 +0300 Subject: [PATCH 07/10] Support script execution (no error messages currently) --- c_src/sqlite3_drv.c | 31 ++++++++++++++++++++++++++++++- c_src/sqlite3_drv.h | 2 ++ src/sqlite3.erl | 25 ++++++++++++++++++++++++- test/sqlite3_test.erl | 19 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index 1b2e42a..a246b22 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -162,6 +162,9 @@ static int control( case CMD_PREPARED_COLUMNS: prepared_columns(driver_data, buf, len); break; + case CMD_SQL_EXEC_SCRIPT: + sql_exec_script(driver_data, buf, len); + break; default: unknown(driver_data, buf, len); } @@ -193,7 +196,7 @@ static inline int output_error( int term_count; return_error(drv, error_code, error, &dataset, &term_count); driver_output_term(drv->port, dataset, term_count); - return 0; + return 1; } static inline int output_db_error(sqlite3_drv_t *drv) { @@ -259,6 +262,32 @@ static int sql_exec(sqlite3_drv_t *drv, char *command, int command_size) { return sql_exec_statement(drv, statement); } +static int sql_exec_script(sqlite3_drv_t *drv, char *command, int command_size) { + int result; + const char *rest = command; + const char *end = command + command_size; + sqlite3_stmt *statement; + + // printf("SQL: %.*s\n", command_size, command); + + while (rest < end) { + result = sqlite3_prepare_v2(drv->db, command, end - command, &statement, &rest); + command = (char *) rest; // won't actually mutate! + if (result != SQLITE_OK) { + return output_db_error(drv); + } else if (statement == NULL) { + return output_error(drv, SQLITE_MISUSE, "empty statement"); + } + + result = sql_exec_statement(drv, statement); + if (result) { + // there was an error, bail out + return result; + } + } + return result; +} + static inline int decode_and_bind_param( sqlite3_drv_t *drv, char *buffer, int *p_index, sqlite3_stmt *statement, int param_index, int *p_type, int *p_size) { diff --git a/c_src/sqlite3_drv.h b/c_src/sqlite3_drv.h index b9c7a44..3609d70 100644 --- a/c_src/sqlite3_drv.h +++ b/c_src/sqlite3_drv.h @@ -34,6 +34,7 @@ #define CMD_PREPARED_CLEAR_BINDINGS 9 #define CMD_PREPARED_FINALIZE 10 #define CMD_PREPARED_COLUMNS 11 +#define CMD_SQL_EXEC_SCRIPT 12 // Number of bytes for each key // (160 bits for SHA1 hash) @@ -84,6 +85,7 @@ static int control(ErlDrvData drv_data, unsigned int command, char *buf, int len, char **rbuf, int rlen); static int sql_exec(sqlite3_drv_t *drv, char *buf, int len); static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buf, int len); +static int sql_exec_script(sqlite3_drv_t *drv, char *buf, int len); static int prepare(sqlite3_drv_t *drv, char *buf, int len); static int prepared_bind(sqlite3_drv_t *drv, char *buf, int len); static int prepared_step(sqlite3_drv_t *drv, char *buf, int len); diff --git a/src/sqlite3.erl b/src/sqlite3.erl index 8a402c5..6065654 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -15,7 +15,8 @@ -export([open/1, open/2]). -export([start_link/1, start_link/2]). -export([stop/0, close/1]). --export([sql_exec/1, sql_exec/2, sql_exec/3]). +-export([sql_exec/1, sql_exec/2, sql_exec/3, + sql_exec_script/2]). -export([prepare/2, bind/3, next/2, reset/2, clear_bindings/2, finalize/2, columns/2]). -export([create_table/2, create_table/3, create_table/4]). @@ -170,6 +171,17 @@ sql_exec(Db, SQL) -> sql_exec(Db, SQL, Params) -> gen_server:call(Db, {sql_bind_and_exec, SQL, Params}). +%%-------------------------------------------------------------------- +%% @spec sql_exec_script(Db :: atom(), Sql :: iodata()) -> sql_non_query_result() +%% @doc +%% Executes the Sql script (consisting of semicolon-separated statements) +%% directly on the Db database. Returns 'ok' if there were no errors. +%% @end +%%-------------------------------------------------------------------- +-spec sql_exec_script(atom(), iodata()) -> sql_non_query_result(). +sql_exec_script(Db, SQL) -> + gen_server:call(Db, {sql_exec_script, SQL}). + -spec prepare(atom(), iodata()) -> {ok, reference()} | sqlite_error(). prepare(Db, SQL) -> gen_server:call(Db, {prepare, SQL}). @@ -643,6 +655,9 @@ handle_call({sql_exec, SQL}, _From, State) -> handle_call({sql_bind_and_exec, SQL, Params}, _From, State) -> Reply = do_sql_bind_and_exec(SQL, Params, State), {reply, Reply, State}; +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); @@ -813,6 +828,7 @@ get_priv_dir() -> -define(PREPARED_CLEAR_BINDINGS, 9). -define(PREPARED_FINALIZE, 10). -define(PREPARED_COLUMNS, 11). +-define(SQL_EXEC_SCRIPT, 12). create_port_cmd(DbFile) -> atom_to_list(?DRIVER_NAME) ++ " " ++ DbFile. @@ -829,6 +845,10 @@ do_sql_bind_and_exec(SQL, Params, #state{port = Port}) -> ?dbg("SQL: ~s; Parameters: ~p~n", [SQL, Params]), exec(Port, {sql_bind_and_exec, SQL, Params}). +do_sql_exec_script(SQL, #state{port = Port}) -> + ?dbg("SQL: ~s~n", [SQL]), + exec(Port, {sql_exec_script, SQL}). + exec(_Port, {create_function, _FunctionName, _Function}) -> error_logger:error_report([{application, sqlite3}, "NOT IMPL YET"]); %port_control(Port, ?SQL_CREATE_FUNCTION, list_to_binary(Cmd)), @@ -840,6 +860,9 @@ exec(Port, {sql_bind_and_exec, SQL, Params}) -> Bin = term_to_binary({iolist_to_binary(SQL), Params}), port_control(Port, ?SQL_BIND_AND_EXEC_COMMAND, Bin), wait_result(Port); +exec(Port, {sql_exec_script, SQL}) -> + port_control(Port, ?SQL_EXEC_SCRIPT, SQL), + wait_result(Port); exec(Port, {prepare, SQL}) -> port_control(Port, ?PREPARE, SQL), wait_result(Port); diff --git a/test/sqlite3_test.erl b/test/sqlite3_test.erl index e7bf266..1f0b56d 100644 --- a/test/sqlite3_test.erl +++ b/test/sqlite3_test.erl @@ -233,6 +233,25 @@ prepared_test() -> ?assertEqual(ok, sqlite3:finalize(prepared, Ref2)), sqlite3:close(prepared). +script_test() -> + sqlite3:open(script, [in_memory]), + Script = string:join( + ["CREATE TABLE person(", + "id INTEGER", + ");", + " ", + "-- Comment", + "", + "INSERT INTO person (id) VALUES (1);", + "INSERT INTO person (id) VALUES (2);", + " " + ], "\n"), + ?assertEqual(ok, sqlite3:sql_exec_script(script, Script)), + ?assertEqual( + [{columns,["id"]},{rows,[{1},{2}]}], + sqlite3:read_all(script, person)), + sqlite3:close(script). + % create, read, update, delete %%==================================================================== %% Internal functions From 11452c5b88edcb84e64a64a69249b5ebf25b1852 Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Mon, 24 Jan 2011 15:48:48 +0300 Subject: [PATCH 08/10] Finished multiple statement support --- c_src/sqlite3_drv.c | 21 ++++++++++++++++----- src/sqlite3.erl | 22 ++++++++++++++++++---- test/sqlite3_test.erl | 25 +++++++++++++++++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index a246b22..15aedee 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -213,6 +213,16 @@ static inline int output_ok(sqlite3_drv_t *drv) { return driver_output_term(drv->port, spec, sizeof(spec) / sizeof(spec[0])); } +static inline int output_done(sqlite3_drv_t *drv) { + // Return {Port, ok} + ErlDrvTermData spec[] = { + ERL_DRV_PORT, driver_mk_port(drv->port), + ERL_DRV_ATOM, drv->atom_done, + ERL_DRV_TUPLE, 2 + }; + return driver_output_term(drv->port, spec, sizeof(spec) / sizeof(spec[0])); +} + static inline async_sqlite3_command *make_async_command( sqlite3_drv_t *drv, sqlite3_stmt *statement) { async_sqlite3_command *result = @@ -268,23 +278,24 @@ static int sql_exec_script(sqlite3_drv_t *drv, char *command, int command_size) const char *end = command + command_size; sqlite3_stmt *statement; - // printf("SQL: %.*s\n", command_size, command); - while (rest < end) { result = sqlite3_prepare_v2(drv->db, command, end - command, &statement, &rest); command = (char *) rest; // won't actually mutate! if (result != SQLITE_OK) { - return output_db_error(drv); + output_db_error(drv); + break; } else if (statement == NULL) { - return output_error(drv, SQLITE_MISUSE, "empty statement"); + output_error(drv, SQLITE_MISUSE, "empty statement"); + break; } result = sql_exec_statement(drv, statement); if (result) { // there was an error, bail out - return result; + break; } } + output_done(drv); return result; } diff --git a/src/sqlite3.erl b/src/sqlite3.erl index 6065654..ad6f451 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -172,13 +172,16 @@ sql_exec(Db, SQL, Params) -> gen_server:call(Db, {sql_bind_and_exec, SQL, Params}). %%-------------------------------------------------------------------- -%% @spec sql_exec_script(Db :: atom(), Sql :: iodata()) -> sql_non_query_result() +%% @spec sql_exec_script(Db :: atom(), Sql :: iodata()) -> [sql_result()] %% @doc %% Executes the Sql script (consisting of semicolon-separated statements) -%% directly on the Db database. Returns 'ok' if there were no errors. +%% directly on the Db database. Returns the list of their results (same as +%% if sql_exec/2 was called for all of them in order, but more efficient). +%% Note that any whitespace or comments after the last semicolon will be +%% considered an empty statement and produce the corresponding error. %% @end %%-------------------------------------------------------------------- --spec sql_exec_script(atom(), iodata()) -> sql_non_query_result(). +-spec sql_exec_script(atom(), iodata()) -> [sql_result()]. sql_exec_script(Db, SQL) -> gen_server:call(Db, {sql_exec_script, SQL}). @@ -862,7 +865,7 @@ exec(Port, {sql_bind_and_exec, SQL, Params}) -> wait_result(Port); exec(Port, {sql_exec_script, SQL}) -> port_control(Port, ?SQL_EXEC_SCRIPT, SQL), - wait_result(Port); + many_results_loop(Port); exec(Port, {prepare, SQL}) -> port_control(Port, ?PREPARE, SQL), wait_result(Port); @@ -899,6 +902,17 @@ wait_result(Port) -> {error, -1, Reason} end. +many_results_loop(Port) -> + do_many_results_loop(Port, []). + +do_many_results_loop(Port, Acc) -> + case wait_result(Port) of + done -> + lists:reverse(Acc); + Reply -> + do_many_results_loop(Port, [Reply | Acc]) + end. + parse_table_info(Info) -> [_, Tail] = string:tokens(Info, "()"), Cols = string:tokens(Tail, ","), diff --git a/test/sqlite3_test.erl b/test/sqlite3_test.erl index 1f0b56d..c746d53 100644 --- a/test/sqlite3_test.erl +++ b/test/sqlite3_test.erl @@ -17,6 +17,7 @@ -include_lib("eunit/include/eunit.hrl"). -define(FuncTest(Name), {??Name, fun Name/0}). +-define(WARN_ERROR_MESSAGE, io:format(user, "Error message should be shown...~n", [])). drop_all_tables(Db) -> Tables = sqlite3:list_tables(Db), @@ -61,7 +62,7 @@ basic_functionality() -> AbbyOnly = [{1, <<"abby">>, 20, 2000}], TableInfo = [{id, integer, [primary_key]}, {name, text, [not_null, unique]}, {age, integer}, {wage, integer}], drop_all_tables(ct), - ?debugMsg("Error message \"sqlite3 driver error: empty statement\" should be shown..."), + ?WARN_ERROR_MESSAGE, ?assertEqual( {error, 21, "empty statement"}, sqlite3:sql_exec(ct, "-- Comment")), @@ -81,7 +82,7 @@ basic_functionality() -> ?assertEqual( {rowid, 2}, sqlite3:write(ct, user, [{name, "marge"}, {age, 30}, {wage, 2000}])), - ?debugMsg("Error message \"sqlite3 driver error: constraint failed\" should be shown..."), + ?WARN_ERROR_MESSAGE, ?assertEqual( {error, 19, "constraint failed"}, sqlite3:write(ct, user, [{name, "marge"}, {age, 30}, {wage, 2000}])), @@ -241,15 +242,31 @@ script_test() -> ");", " ", "-- Comment", - "", "INSERT INTO person (id) VALUES (1);", "INSERT INTO person (id) VALUES (2);", " " ], "\n"), - ?assertEqual(ok, sqlite3:sql_exec_script(script, Script)), + ?WARN_ERROR_MESSAGE, + ?assertEqual( + [ok, ok, ok, {error, 21, "empty statement"}], + sqlite3:sql_exec_script(script, Script)), ?assertEqual( [{columns,["id"]},{rows,[{1},{2}]}], sqlite3:read_all(script, person)), + BadScript = string:join( + ["CREATE TABLE person2(", + "id INTEGER", + ");", + " ", + "-- Comment", + "SYNTAX ERROR;", + "INSERT INTO person (id) VALUES (2);", + " " + ], "\n"), + ?WARN_ERROR_MESSAGE, + ?assertEqual( + [ok, {error, 1, "near \"SYNTAX\": syntax error"}], + sqlite3:sql_exec_script(script, BadScript)), sqlite3:close(script). % create, read, update, delete From 6b9d7df385d421cda46ea6c7741e2524687dffba Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Fri, 11 Feb 2011 18:36:43 +0300 Subject: [PATCH 09/10] Add function variants which take Timeout. --- src/sqlite3.erl | 281 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 268 insertions(+), 13 deletions(-) diff --git a/src/sqlite3.erl b/src/sqlite3.erl index ad6f451..c95e861 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -14,20 +14,27 @@ %% API -export([open/1, open/2]). -export([start_link/1, start_link/2]). --export([stop/0, close/1]). --export([sql_exec/1, sql_exec/2, sql_exec/3, - sql_exec_script/2]). +-export([stop/0, close/1, close_timeout/2]). +-export([sql_exec/1, sql_exec/2, sql_exec_timeout/3, + sql_exec_script/2, sql_exec_script_timeout/3, + sql_exec/3, sql_exec_timeout/4]). -export([prepare/2, bind/3, next/2, reset/2, clear_bindings/2, finalize/2, - columns/2]). --export([create_table/2, create_table/3, create_table/4]). --export([list_tables/0, list_tables/1, table_info/1, table_info/2]). --export([write/2, write/3, write_many/2, write_many/3]). --export([update/3, update/4]). --export([read_all/2, read_all/3, read/2, read/3, read/4]). --export([delete/2, delete/3]). --export([drop_table/1, drop_table/2]). + columns/2, prepare_timeout/3, bind_timeout/4, next_timeout/3, + reset_timeout/3, clear_bindings_timeout/3, finalize_timeout/3, + columns_timeout/3]). +-export([create_table/2, create_table/3, create_table/4, create_table_timeout/4, + create_table_timeout/5]). +-export([list_tables/0, list_tables/1, list_tables_timeout/2, + table_info/1, table_info/2, table_info_timeout/3]). +-export([write/2, write/3, write_timeout/4, write_many/2, write_many/3, + write_many_timeout/4]). +-export([update/3, update/4, update_timeout/5]). +-export([read_all/2, read_all/3, read_all_timeout/3, read_all_timeout/4, + read/2, read/3, read/4, read_timeout/4, read_timeout/5]). +-export([delete/2, delete/3, delete_timeout/4]). +-export([drop_table/1, drop_table/2, drop_table_timeout/3]). -export([begin_transaction/1, commit_transaction/1, rollback_transaction/1]). --export([vacuum/0, vacuum/1]). +-export([vacuum/0, vacuum/1, vacuum_timeout/2]). %% -export([create_function/3]). @@ -128,6 +135,16 @@ open(Db, Options) -> close(Db) -> gen_server:call(Db, close). +%%-------------------------------------------------------------------- +%% @spec close_timeout(Db :: atom(), Timeout :: timeout()) -> ok +%% @doc +%% Closes the Db sqlite3 database. +%% @end +%%-------------------------------------------------------------------- +-spec close_timeout(atom(), timeout()) -> 'ok'. +close_timeout(Db, Timeout) -> + gen_server:call(Db, close, Timeout). + %%-------------------------------------------------------------------- %% @spec stop() -> ok %% @doc @@ -167,10 +184,35 @@ sql_exec(Db, SQL) -> %% database. Returns the result of the Sql call. %% @end %%-------------------------------------------------------------------- --spec sql_exec(atom(), iodata(), [sql_value() | {atom() | string() | integer(), sql_value()}]) -> sql_result(). +-spec sql_exec(atom(), iodata(), [sql_value() | {atom() | string() | integer(), sql_value()}]) -> + sql_result(). sql_exec(Db, SQL, Params) -> gen_server:call(Db, {sql_bind_and_exec, SQL, Params}). +%%-------------------------------------------------------------------- +%% @spec sql_exec_timeout(Db :: atom(), Sql :: iodata()) -> sql_result() +%% @doc +%% Executes the Sql statement directly on the Db database. Returns the +%% result of the Sql call. +%% @end +%%-------------------------------------------------------------------- +-spec sql_exec_timeout(atom(), iodata(), timeout()) -> sql_result(). +sql_exec_timeout(Db, SQL, Timeout) -> + gen_server:call(Db, {sql_exec, SQL}, Timeout). + +%%-------------------------------------------------------------------- +%% @spec sql_exec_timeout(Db :: atom(), Sql :: iodata(), Params) -> sql_result() +%% Params = [sql_value() | {atom() | string() | integer(), sql_value()}] +%% @doc +%% Executes the Sql statement with parameters Params directly on the Db +%% database. Returns the result of the Sql call. +%% @end +%%-------------------------------------------------------------------- +-spec sql_exec_timeout(atom(), iodata(), [sql_value() | {atom() | string() | integer(), sql_value()}], timeout()) -> + sql_result(). +sql_exec_timeout(Db, SQL, Params, Timeout) -> + gen_server:call(Db, {sql_bind_and_exec, SQL, Params}, Timeout). + %%-------------------------------------------------------------------- %% @spec sql_exec_script(Db :: atom(), Sql :: iodata()) -> [sql_result()] %% @doc @@ -185,6 +227,20 @@ sql_exec(Db, SQL, Params) -> sql_exec_script(Db, SQL) -> gen_server:call(Db, {sql_exec_script, SQL}). +%%-------------------------------------------------------------------- +%% @spec sql_exec_script_timeout(Db :: atom(), Sql :: iodata(), Timeout :: timeout()) -> [sql_result()] +%% @doc +%% Executes the Sql script (consisting of semicolon-separated statements) +%% directly on the Db database. Returns the list of their results (same as +%% if sql_exec/3 was called for all of them in order, but more efficient). +%% Note that any whitespace or comments after the last semicolon will be +%% considered an empty statement and produce the corresponding error. +%% @end +%%-------------------------------------------------------------------- +-spec sql_exec_script_timeout(atom(), iodata(), timeout()) -> [sql_result()]. +sql_exec_script_timeout(Db, SQL, Timeout) -> + gen_server:call(Db, {sql_exec_script, SQL}, Timeout). + -spec prepare(atom(), iodata()) -> {ok, reference()} | sqlite_error(). prepare(Db, SQL) -> gen_server:call(Db, {prepare, SQL}). @@ -213,6 +269,34 @@ finalize(Db, Ref) -> columns(Db, Ref) -> gen_server:call(Db, {columns, Ref}). +-spec prepare_timeout(atom(), iodata(), timeout()) -> {ok, reference()} | sqlite_error(). +prepare_timeout(Db, SQL, Timeout) -> + gen_server:call(Db, {prepare, SQL}, Timeout). + +-spec bind_timeout(atom(), reference(), sql_params(), timeout()) -> sql_non_query_result(). +bind_timeout(Db, Ref, Params, Timeout) -> + gen_server:call(Db, {bind, Ref, Params}, Timeout). + +-spec next_timeout(atom(), reference(), timeout()) -> tuple() | done | sqlite_error(). +next_timeout(Db, Ref, Timeout) -> + gen_server:call(Db, {next, Ref}, Timeout). + +-spec reset_timeout(atom(), reference(), timeout()) -> sql_non_query_result(). +reset_timeout(Db, Ref, Timeout) -> + gen_server:call(Db, {reset, Ref}, Timeout). + +-spec clear_bindings_timeout(atom(), reference(), timeout()) -> sql_non_query_result(). +clear_bindings_timeout(Db, Ref, Timeout) -> + gen_server:call(Db, {clear_bindings, Ref}, Timeout). + +-spec finalize_timeout(atom(), reference(), timeout()) -> sql_non_query_result(). +finalize_timeout(Db, Ref, Timeout) -> + gen_server:call(Db, {finalize, Ref}, Timeout). + +-spec columns_timeout(atom(), reference(), timeout()) -> sql_non_query_result(). +columns_timeout(Db, Ref, Timeout) -> + gen_server:call(Db, {columns, Ref}, Timeout). + %%-------------------------------------------------------------------- %% @spec create_table(Tbl :: atom(), TblInfo :: [{atom(), atom()}]) -> sql_non_query_result() %% @doc @@ -242,6 +326,21 @@ create_table(Tbl, Columns) -> create_table(Db, Tbl, Columns) -> gen_server:call(Db, {create_table, Tbl, Columns}). +%%-------------------------------------------------------------------- +%% @spec create_table_timeout(Db :: atom(), Tbl :: atom(), Columns, Timeout :: timeout()) -> sql_non_query_result() +%% Columns = [{atom(), atom()}] +%% @doc +%% Creates the Tbl table in Db using Columns as the table structure. +%% The table structure is a list of {column name, column type} pairs. +%% e.g. [{name, text}, {age, integer}] +%% +%% Returns the result of the create table call. +%% @end +%%-------------------------------------------------------------------- +-spec create_table_timeout(atom(), atom(), [{atom(), atom()}], timeout()) -> sql_non_query_result(). +create_table_timeout(Db, Tbl, Columns, Timeout) -> + gen_server:call(Db, {create_table, Tbl, Columns}, Timeout). + %%-------------------------------------------------------------------- %% @spec create_table(Db :: atom(), Tbl :: atom(), TblInfo, Constraints) -> sql_non_query_result() %% Columns = [{atom(), atom()}] @@ -259,6 +358,23 @@ create_table(Db, Tbl, Columns) -> create_table(Db, Tbl, Columns, Constraints) -> gen_server:call(Db, {create_table, Tbl, Columns, Constraints}). +%%-------------------------------------------------------------------- +%% @spec create_table_timeout(Db :: atom(), Tbl :: atom(), TblInfo, Constraints, Timeout) -> sql_non_query_result() +%% Columns = [{atom(), atom()}] +%% Constraints = [term()] +%% @doc +%% Creates the Tbl table in Db using Columns as the table structure and +%% Constraints as table constraints. +%% The table structure is a list of {column name, column type} pairs. +%% e.g. [{name, text}, {age, integer}] +%% +%% Returns the result of the create table call. +%% @end +%%-------------------------------------------------------------------- +-spec create_table_timeout(atom(), atom(), [{atom(), atom()}], [any()], timeout()) -> sql_non_query_result(). +create_table_timeout(Db, Tbl, Columns, Constraints, Timeout) -> + gen_server:call(Db, {create_table, Tbl, Columns, Constraints}, Timeout). + %%-------------------------------------------------------------------- %% @spec list_tables() -> [atom()] %% @doc @@ -279,6 +395,16 @@ list_tables() -> list_tables(Db) -> gen_server:call(Db, list_tables). +%%-------------------------------------------------------------------- +%% @spec list_tables_timeout(Db :: atom(), Timeout :: timeout()) -> [atom()] +%% @doc +%% Returns a list of tables for Db. +%% @end +%%-------------------------------------------------------------------- +-spec list_tables_timeout(atom(), timeout()) -> [atom()]. +list_tables_timeout(Db, Timeout) -> + gen_server:call(Db, list_tables, Timeout). + %%-------------------------------------------------------------------- %% @spec table_info(Tbl :: atom()) -> [any()] %% @doc @@ -299,6 +425,16 @@ table_info(Tbl) -> table_info(Db, Tbl) -> gen_server:call(Db, {table_info, Tbl}). +%%-------------------------------------------------------------------- +%% @spec table_info_timeout(Db :: atom(), Tbl :: atom()) -> [any()] +%% @doc +%% Returns table schema for Tbl in Db. +%% @end +%%-------------------------------------------------------------------- +-spec table_info_timeout(atom(), atom(), timeout()) -> [any()]. +table_info_timeout(Db, Tbl, Timeout) -> + gen_server:call(Db, {table_info, Tbl}, Timeout). + %%-------------------------------------------------------------------- %% @spec write(Tbl :: atom(), Data) -> sql_non_query_result() %% Data = [{Column :: atom(), Value :: sql_value()}] @@ -323,6 +459,18 @@ write(Tbl, Data) -> write(Db, Tbl, Data) -> gen_server:call(Db, {write, Tbl, Data}). +%%-------------------------------------------------------------------- +%% @spec write_timeout(Db :: atom(), Tbl :: atom(), Data) -> sql_non_query_result() +%% Data = [{Column :: atom(), Value :: sql_value()}] +%% @doc +%% Write Data into Tbl table in Db database. Value must be of the +%% same type as determined from table_info/3. +%% @end +%%-------------------------------------------------------------------- +-spec write_timeout(atom(), atom(), [{atom(), sql_value()}], timeout()) -> sql_non_query_result(). +write_timeout(Db, Tbl, Data, Timeout) -> + gen_server:call(Db, {write, Tbl, Data}, Timeout). + %%-------------------------------------------------------------------- %% @spec write_many(Tbl :: atom(), Data) -> sql_non_query_result() %% Data = [[{Column :: atom(), Value :: sql_value()}]] @@ -347,6 +495,18 @@ write_many(Tbl, Data) -> write_many(Db, Tbl, Data) -> gen_server:call(Db, {write_many, Tbl, Data}). +%%-------------------------------------------------------------------- +%% @spec write_many_timeout(Db :: atom(), Tbl :: atom(), Data) -> sql_non_query_result() +%% Data = [[{Column :: atom(), Value :: sql_value()}]] +%% @doc +%% Write all records in Data into table Tbl in database Db. Value +%% must be of the same type as determined from table_info/3. +%% @end +%%-------------------------------------------------------------------- +-spec write_many_timeout(atom(), atom(), [[{atom(), sql_value()}]], timeout()) -> sql_non_query_result(). +write_many_timeout(Db, Tbl, Data, Timeout) -> + gen_server:call(Db, {write_many, Tbl, Data}, Timeout). + %%-------------------------------------------------------------------- %% @spec update(Tbl :: atom(), {Key :: atom(), Value}, Data) -> sql_non_query_result() %% Value = any() @@ -373,6 +533,19 @@ update(Tbl, {Key, Value}, Data) -> update(Db, Tbl, {Key, Value}, Data) -> gen_server:call(Db, {update, Tbl, Key, Value, Data}). +%%-------------------------------------------------------------------- +%% @spec update_timeout(Db :: atom(), Tbl :: atom(), {Key :: atom(), Value}, Data) -> sql_non_query_result() +%% Value = sql_value() +%% Data = [{Column :: atom(), Value :: sql_value()}] +%% @doc +%% Updates rows into Tbl table in Db database such that the Value +%% matches the value in Key with Data. +%% @end +%%-------------------------------------------------------------------- +-spec update_timeout(atom(), atom(), {atom(), sql_value()}, [{atom(), sql_value()}], timeout()) -> sql_non_query_result(). +update_timeout(Db, Tbl, {Key, Value}, Data, Timeout) -> + gen_server:call(Db, {update, Tbl, Key, Value, Data}, Timeout). + %%-------------------------------------------------------------------- %% @spec read_all(Db :: atom(), Table :: atom()) -> sql_result() %% @doc @@ -383,6 +556,16 @@ update(Db, Tbl, {Key, Value}, Data) -> read_all(Db, Tbl) -> gen_server:call(Db, {read, Tbl}). +%%-------------------------------------------------------------------- +%% @spec read_all_timeout(Db :: atom(), Table :: atom()) -> sql_result() +%% @doc +%% Reads all rows from Table in Db. +%% @end +%%-------------------------------------------------------------------- +-spec read_all_timeout(atom(), atom(), timeout()) -> sql_result(). +read_all_timeout(Db, Tbl, Timeout) -> + gen_server:call(Db, {read, Tbl}, Timeout). + %%-------------------------------------------------------------------- %% @spec read_all(Db :: atom(), Table :: atom(), Columns :: [atom()]) -> sql_result() %% @doc @@ -393,6 +576,16 @@ read_all(Db, Tbl) -> read_all(Db, Tbl, Columns) -> gen_server:call(Db, {read, Tbl, Columns}). +%%-------------------------------------------------------------------- +%% @spec read_all_timeout(Db :: atom(), Table :: atom(), Columns :: [atom()]) -> sql_result() +%% @doc +%% Reads Columns in all rows from Table in Db. +%% @end +%%-------------------------------------------------------------------- +-spec read_all_timeout(atom(), atom(), [atom()], timeout()) -> sql_result(). +read_all_timeout(Db, Tbl, Columns, Timeout) -> + gen_server:call(Db, {read, Tbl, Columns}, Timeout). + %%-------------------------------------------------------------------- %% @spec read(Tbl :: atom(), Key) -> sql_result() %% Key = {Column :: atom(), Value :: sql_value()} @@ -435,6 +628,35 @@ read(Db, Tbl, {Column, Value}) -> read(Db, Tbl, {Key, Value}, Columns) -> gen_server:call(Db, {read, Tbl, Key, Value, Columns}). +%%-------------------------------------------------------------------- +%% @spec read_timeout(Db :: atom(), Tbl :: atom(), Key) -> sql_result() +%% Key = {Column :: atom(), Value :: sql_value()} +%% @doc +%% Reads a row from Tbl table in Db database such that the Value +%% matches the value in Column. ColValue must have the same type +%% as determined from table_info/3. +%% @end +%%-------------------------------------------------------------------- +-spec read_timeout(atom(), atom(), {atom(), sql_value()}, timeout()) -> sql_result(). +read_timeout(Db, Tbl, {Column, Value}, Timeout) -> + gen_server:call(Db, {read, Tbl, Column, Value}, Timeout). + +%%-------------------------------------------------------------------- +%% @spec read_timeout(Db, Tbl, Key, Columns) -> [any()] +%% Db = atom() +%% Tbl = atom() +%% Key = {Column :: atom(), Value :: sql_value()} +%% Columns = [atom()] +%% @doc +%% Reads a row from Tbl table in Db database such that the Value +%% matches the value in Column. Value must have the same type as +%% determined from table_info/3. +%% @end +%%-------------------------------------------------------------------- +-spec read_timeout(atom(), atom(), {atom(), sql_value()}, [atom()], timeout()) -> sql_result(). +read_timeout(Db, Tbl, {Key, Value}, Columns, Timeout) -> + gen_server:call(Db, {read, Tbl, Key, Value, Columns}, Timeout). + %%-------------------------------------------------------------------- %% @spec delete(Tbl :: atom(), Key) -> any() %% Key = {Column :: atom(), Value :: sql_value()} @@ -448,6 +670,19 @@ read(Db, Tbl, {Key, Value}, Columns) -> delete(Tbl, Key) -> delete(?MODULE, Tbl, Key). +%%-------------------------------------------------------------------- +%% @spec delete_timeout(Db :: atom(), Tbl :: atom(), Key) -> sql_non_query_result() +%% Key = {Column :: atom(), Value :: sql_value()} +%% @doc +%% Delete a row from Tbl table in Db database such that the Value +%% matches the value in Column. +%% Value must have the same type as determined from table_info/3. +%% @end +%%-------------------------------------------------------------------- +-spec delete_timeout(atom(), atom(), {atom(), any()}, timeout()) -> sql_non_query_result(). +delete_timeout(Db, Tbl, Key, Timeout) -> + gen_server:call(Db, {delete, Tbl, Key}, Timeout). + %%-------------------------------------------------------------------- %% @spec delete(Db :: atom(), Tbl :: atom(), Key) -> sql_non_query_result() %% Key = {Column :: atom(), Value :: sql_value()} @@ -481,6 +716,16 @@ drop_table(Tbl) -> drop_table(Db, Tbl) -> gen_server:call(Db, {drop_table, Tbl}). +%%-------------------------------------------------------------------- +%% @spec drop_table_timeout(Db :: atom(), Tbl :: atom()) -> sql_non_query_result() +%% @doc +%% Drop the table Tbl from Db database. +%% @end +%%-------------------------------------------------------------------- +-spec drop_table_timeout(atom(), atom(), timeout()) -> sql_non_query_result(). +drop_table_timeout(Db, Tbl, Timeout) -> + gen_server:call(Db, {drop_table, Tbl}, Timeout). + %%-------------------------------------------------------------------- %% @spec vacuum() -> sql_non_query_result() %% @doc @@ -501,6 +746,16 @@ vacuum() -> vacuum(Db) -> gen_server:call(Db, vacuum). +%%-------------------------------------------------------------------- +%% @spec vacuum_timeout(Db :: atom()) -> sql_non_query_result() +%% @doc +%% Vacuum the Db database. +%% @end +%%-------------------------------------------------------------------- +-spec vacuum_timeout(atom(), timeout()) -> sql_non_query_result(). +vacuum_timeout(Db, Timeout) -> + gen_server:call(Db, vacuum, Timeout). + %% %%-------------------------------------------------------------------- %% %% @spec create_function(Db :: atom(), FunctionName :: atom(), Function :: function()) -> term() %% %% @doc From 2ab2328f15be7b4de46f6af0a1783458257e7707 Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Fri, 11 Feb 2011 18:40:28 +0300 Subject: [PATCH 10/10] Remove confusing 'transaction' functions. --- src/sqlite3.erl | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/sqlite3.erl b/src/sqlite3.erl index c95e861..e6d17fd 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -33,7 +33,6 @@ read/2, read/3, read/4, read_timeout/4, read_timeout/5]). -export([delete/2, delete/3, delete_timeout/4]). -export([drop_table/1, drop_table/2, drop_table_timeout/3]). --export([begin_transaction/1, commit_transaction/1, rollback_transaction/1]). -export([vacuum/0, vacuum/1, vacuum_timeout/2]). %% -export([create_function/3]). @@ -767,39 +766,6 @@ vacuum_timeout(Db, Timeout) -> %% create_function(Db, FunctionName, Function) -> %% gen_server:call(Db, {create_function, FunctionName, Function}). -%%-------------------------------------------------------------------- -%% @spec begin_transaction(Db :: atom()) -> sql_non_query_result() -%% @doc -%% Begins a transaction in Db. -%% @end -%%-------------------------------------------------------------------- --spec begin_transaction(atom()) -> sql_non_query_result(). -begin_transaction(Db) -> - SQL = "BEGIN;", - sql_exec(Db, SQL). - -%%-------------------------------------------------------------------- -%% @spec commit_transaction(Db :: atom()) -> sql_non_query_result() -%% @doc -%% Commits the current transaction in Db. -%% @end -%%-------------------------------------------------------------------- --spec commit_transaction(atom()) -> sql_non_query_result(). -commit_transaction(Db) -> - SQL = "COMMIT;", - sql_exec(Db, SQL). - -%%-------------------------------------------------------------------- -%% @spec rollback_transaction(Db :: atom()) -> sql_non_query_result() -%% @doc -%% Rolls back the current transaction in Db. -%% @end -%%-------------------------------------------------------------------- --spec rollback_transaction(atom()) -> sql_non_query_result(). -rollback_transaction(Db) -> - SQL = "ROLLBACK;", - sql_exec(Db, SQL). - %%-------------------------------------------------------------------- %% @spec value_to_sql_unsafe(Value :: sql_value()) -> iolist() %% @doc