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 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 diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index bc03fda..15aedee 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) { @@ -210,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 = @@ -242,22 +255,50 @@ 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; 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) { return output_db_error(drv); + } else if (statement == NULL) { + return output_error(drv, SQLITE_MISUSE, "empty statement"); } 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; + + 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) { + output_db_error(drv); + break; + } else if (statement == NULL) { + output_error(drv, SQLITE_MISUSE, "empty statement"); + break; + } + + result = sql_exec_statement(drv, statement); + if (result) { + // there was an error, bail out + break; + } + } + output_done(drv); + 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) { @@ -441,7 +482,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; sqlite3_stmt *statement; long bin_size; char *command; @@ -469,12 +510,13 @@ 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) { 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); @@ -917,7 +959,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; sqlite3_stmt *statement; ErlDrvTermData spec[6]; @@ -925,10 +967,11 @@ 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) { 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/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..e6d17fd 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -14,19 +14,26 @@ %% 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]). +-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]). --export([begin_transaction/1, commit_transaction/1, rollback_transaction/1]). --export([vacuum/0, vacuum/1]). + 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([vacuum/0, vacuum/1, vacuum_timeout/2]). %% -export([create_function/3]). @@ -127,6 +134,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 @@ -166,10 +183,63 @@ 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 +%% 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/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_result()]. +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}). @@ -198,6 +268,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 @@ -227,6 +325,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()}] @@ -244,6 +357,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 @@ -264,6 +394,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 @@ -284,6 +424,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()}] @@ -308,6 +458,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()}]] @@ -332,6 +494,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() @@ -358,6 +532,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 @@ -368,6 +555,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 @@ -378,6 +575,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()} @@ -420,6 +627,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()} @@ -433,6 +669,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()} @@ -466,6 +715,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 @@ -486,6 +745,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 @@ -497,39 +766,6 @@ vacuum(Db) -> %% 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 @@ -643,6 +879,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 +1052,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 +1069,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 +1084,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), + many_results_loop(Port); exec(Port, {prepare, SQL}) -> port_control(Port, ?PREPARE, SQL), wait_result(Port); @@ -876,6 +1123,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 5f117d0..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,6 +62,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), + ?WARN_ERROR_MESSAGE, + ?assertEqual( + {error, 21, "empty statement"}, + sqlite3:sql_exec(ct, "-- Comment")), ?assertEqual( [], sqlite3:list_tables(ct)), @@ -77,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}])), @@ -229,6 +234,41 @@ 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"), + ?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 %%==================================================================== %% Internal functions