diff --git a/c_src/sqlite3_drv.c b/c_src/sqlite3_drv.c index 34ffa63..4d6ab38 100644 --- a/c_src/sqlite3_drv.c +++ b/c_src/sqlite3_drv.c @@ -365,20 +365,24 @@ static inline int decode_and_bind_param( case ERL_BINARY_EXT: char_buf_val = driver_alloc(*p_size * sizeof(char)); ei_decode_binary(buffer, p_index, char_buf_val, &bin_size); - // assert(bin_size == *p_size) result = sqlite3_bind_text(statement, param_index, char_buf_val, *p_size, &driver_free_fun); break; case ERL_SMALL_TUPLE_EXT: // assume this is {blob, Blob} ei_get_type(buffer, p_index, p_type, p_size); ei_decode_tuple_header(buffer, p_index, p_size); - assert (*p_size == 2); + if (*p_size != 2) { + output_error(drv, SQLITE_MISUSE, "bad parameter type"); + return 1; + } ei_skip_term(buffer, p_index); // skipped the atom 'blob' ei_get_type(buffer, p_index, p_type, p_size); - assert (*p_type == ERL_BINARY_EXT); + if (*p_type != ERL_BINARY_EXT) { + output_error(drv, SQLITE_MISUSE, "bad parameter type"); + return 1; + } char_buf_val = driver_alloc(*p_size * sizeof(char)); ei_decode_binary(buffer, p_index, char_buf_val, &bin_size); - // assert(bin_size == *p_size) result = sqlite3_bind_blob(statement, param_index, char_buf_val, *p_size, &driver_free_fun); break; default: @@ -399,76 +403,95 @@ static int bind_parameters( int i, cur_list_size = -1, param_index = 1, param_indices_are_explicit = 0, result = 0; long param_index_long; char param_name[MAXATOMLEN + 1]; // parameter names shouldn't be longer than 256! - while (*p_index < buffer_size) { - ei_decode_list_header(buffer, p_index, &cur_list_size); - for (i = 0; i < cur_list_size; i++) { + char *acc_string; + result = ei_decode_list_header(buffer, p_index, &cur_list_size); + if (result) { + // probably all parameters are integers between 0 and 255 + // and the list was encoded as string (see ei documentation) + ei_get_type(buffer, p_index, p_type, p_size); + if (*p_type != ERL_STRING_EXT) { + return output_error(drv, SQLITE_ERROR, + "error while binding parameters"); + } + acc_string = driver_alloc(sizeof(char*) * (*p_size + 1)); + ei_decode_string(buffer, p_index, acc_string); + for (param_index = 1; param_index <= *p_size; param_index++) { + sqlite3_bind_int(statement, param_index, (int) acc_string[param_index - 1]); + } + driver_free(acc_string); + return 0; + } + for (i = 0; i < cur_list_size; i++) { + if (*p_index >= buffer_size) { + return output_error(drv, SQLITE_ERROR, + "error while binding parameters"); + } + ei_get_type(buffer, p_index, p_type, p_size); + if (*p_type == ERL_SMALL_TUPLE_EXT) { + int old_index = *p_index; + // param with name or explicit index + param_indices_are_explicit = 1; + if (*p_size != 2) { + return output_error(drv, SQLITE_MISUSE, + "tuple should contain index or name, and value"); + } + ei_decode_tuple_header(buffer, p_index, p_size); ei_get_type(buffer, p_index, p_type, p_size); - if (*p_type == ERL_SMALL_TUPLE_EXT) { - int old_index = *p_index; - // param with name or explicit index - param_indices_are_explicit = 1; - if (*p_size != 2) { - return output_error(drv, SQLITE_MISUSE, - "tuple should contain index or name, and value"); + // first element of tuple is int (index), atom, or string (name) + switch (*p_type) { + case ERL_SMALL_INTEGER_EXT: + case ERL_INTEGER_EXT: + ei_decode_long(buffer, p_index, ¶m_index_long); + param_index = param_index_long; + break; + case ERL_ATOM_EXT: + ei_decode_atom(buffer, p_index, param_name); + // insert zero terminator + param_name[*p_size] = '\0'; + if (strncmp(param_name, "blob", 5) == 0) { + // this isn't really a parameter name! + *p_index = old_index; + param_indices_are_explicit = 0; + goto IMPLICIT_INDEX; // yuck } - ei_decode_tuple_header(buffer, p_index, p_size); - ei_get_type(buffer, p_index, p_type, p_size); - // first element of tuple is int (index), atom, or string (name) - switch (*p_type) { - case ERL_SMALL_INTEGER_EXT: - case ERL_INTEGER_EXT: - ei_decode_long(buffer, p_index, ¶m_index_long); - param_index = param_index_long; - break; - case ERL_ATOM_EXT: - ei_decode_atom(buffer, p_index, param_name); - // insert zero terminator - param_name[*p_size] = '\0'; - if (strncmp(param_name, "blob", 5) == 0) { - // this isn't really a parameter name! - *p_index = old_index; - param_indices_are_explicit = 0; - goto IMPLICIT_INDEX; // yuck - } - else { - param_index = sqlite3_bind_parameter_index(statement, param_name); - } - break; - case ERL_STRING_EXT: - if (*p_size >= MAXATOMLEN) { - return output_error(drv, SQLITE_TOOBIG, "parameter name too long"); - } - ei_decode_string(buffer, p_index, param_name); - // insert zero terminator - param_name[*p_size] = '\0'; + else { param_index = sqlite3_bind_parameter_index(statement, param_name); - break; - default: - return output_error( - drv, SQLITE_MISMATCH, - "parameter index must be given as integer, atom, or string"); } - result = decode_and_bind_param( - drv, buffer, p_index, statement, param_index, p_type, p_size); - if (result != SQLITE_OK) { - return result; // error has already been output + break; + case ERL_STRING_EXT: + if (*p_size >= MAXATOMLEN) { + return output_error(drv, SQLITE_TOOBIG, "parameter name too long"); } + ei_decode_string(buffer, p_index, param_name); + // insert zero terminator + param_name[*p_size] = '\0'; + param_index = sqlite3_bind_parameter_index(statement, param_name); + break; + default: + return output_error( + drv, SQLITE_MISMATCH, + "parameter index must be given as integer, atom, or string"); + } + result = decode_and_bind_param( + drv, buffer, p_index, statement, param_index, p_type, p_size); + if (result != SQLITE_OK) { + return result; // error has already been output + } + } + else { + IMPLICIT_INDEX: + if (param_indices_are_explicit) { + return output_error( + drv, SQLITE_MISUSE, + "parameters without indices shouldn't follow indexed or named parameters"); } - else { - IMPLICIT_INDEX: - if (param_indices_are_explicit) { - return output_error( - drv, SQLITE_MISUSE, - "parameters without indices shouldn't follow indexed or named parameters"); - } - result = decode_and_bind_param( - drv, buffer, p_index, statement, param_index, p_type, p_size); - if (result != SQLITE_OK) { - return result; // error has already been output - } - ++param_index; + result = decode_and_bind_param( + drv, buffer, p_index, statement, param_index, p_type, p_size); + if (result != SQLITE_OK) { + return result; // error has already been output } + ++param_index; } } return result; @@ -516,7 +539,7 @@ static int sql_bind_and_exec(sqlite3_drv_t *drv, char *buffer, int buffer_size) ei_decode_version(buffer, &index, NULL); result = ei_decode_tuple_header(buffer, &index, &size); - if (size != 2) { + if (result || (size != 2)) { return output_error(drv, SQLITE_MISUSE, "Expected a tuple of SQL command and params"); } @@ -1019,7 +1042,7 @@ POPULATE_COMMAND: async_command->binaries = binaries; async_command->row_count = 1; #ifdef DEBUG - fprintf(drv->log, "Total term count: %p %d, columns count: %dx%d\n", statement, term_count, column_count); + fprintf(drv->log, "Total term count: %p %d, columns count: %d\n", statement, term_count, column_count); fflush(drv->log); #endif } diff --git a/include/sqlite3.hrl b/include/sqlite3.hrl index f0e03a4..0ca0c30 100644 --- a/include/sqlite3.hrl +++ b/include/sqlite3.hrl @@ -20,9 +20,19 @@ -endif. -define(NULL_ATOM, null). --type(sql_value() :: number() | ?NULL_ATOM | iodata() | {blob, binary()}). +-type sql_value() :: number() | ?NULL_ATOM | iodata() | {blob, binary()}. +-type sql_type() :: integer | text | double | real | blob | string(). --type(sqlite_error() :: {error, integer(), string()}). --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()]}]). +-type pk_constraint() :: autoincrement | desc | asc. +-type pk_constraints() :: pk_constraint() | [pk_constraint()]. +-type column_constraint() :: non_null | primary_key | {primary_key, pk_constraints()} + | unique | {default, sql_value()}. +-type column_constraints() :: column_constraint() | [column_constraint()]. +-type table_constraint() :: {primary_key, [atom()]} | {unique, [atom()]}. +-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 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/rebar b/rebar index 308a1fe..156bc5a 100755 Binary files a/rebar and b/rebar differ diff --git a/src/sqlite3.app.src b/src/sqlite3.app.src index 3ef2df4..166255f 100644 --- a/src/sqlite3.app.src +++ b/src/sqlite3.app.src @@ -1,7 +1,7 @@ {application, sqlite3, [ {description, "SQLite3 Interface"}, - {vsn, "1.0"}, + {vsn, "1.0.1"}, {modules, []}, {registered, []}, {applications, [kernel, stdlib]}, diff --git a/src/sqlite3.erl b/src/sqlite3.erl index adb3300..1fe9ba6 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -8,6 +8,8 @@ %%%------------------------------------------------------------------- -module(sqlite3). -include("sqlite3.hrl"). +-export_types([sql_value/0, sql_type/0, table_info/0, sqlite_error/0, + sql_params/0, sql_non_query_result/0, sql_result/0]). -behaviour(gen_server). @@ -189,7 +191,7 @@ 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() +%% @spec sql_exec_timeout(Db :: atom(), Sql :: iodata(), Timeout :: timeout()) -> sql_result() %% @doc %% Executes the Sql statement directly on the Db database. Returns the %% result of the Sql call. @@ -200,7 +202,7 @@ 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() +%% @spec sql_exec_timeout(Db :: atom(), Sql :: iodata(), Params, Timeout :: timeout()) -> sql_result() %% Params = [sql_value() | {atom() | string() | integer(), sql_value()}] %% @doc %% Executes the Sql statement with parameters Params directly on the Db @@ -297,7 +299,7 @@ columns_timeout(Db, Ref, Timeout) -> gen_server:call(Db, {columns, Ref}, Timeout). %%-------------------------------------------------------------------- -%% @spec create_table(Tbl :: atom(), TblInfo :: [{atom(), atom()}]) -> sql_non_query_result() +%% @spec create_table(Tbl :: atom(), TblInfo :: table_info()) -> sql_non_query_result() %% @doc %% Creates the Tbl table using TblInfo as the table structure. The %% table structure is a list of {column name, column type} pairs. @@ -306,13 +308,13 @@ columns_timeout(Db, Ref, Timeout) -> %% Returns the result of the create table call. %% @end %%-------------------------------------------------------------------- --spec create_table(atom(), [{atom(), atom()}]) -> sql_non_query_result(). +-spec create_table(atom(), table_info()) -> sql_non_query_result(). create_table(Tbl, Columns) -> create_table(?MODULE, Tbl, Columns). %%-------------------------------------------------------------------- %% @spec create_table(Db :: atom(), Tbl :: atom(), Columns) -> sql_non_query_result() -%% Columns = [{atom(), atom()}] +%% Columns = table_info() %% @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. @@ -321,13 +323,13 @@ create_table(Tbl, Columns) -> %% Returns the result of the create table call. %% @end %%-------------------------------------------------------------------- --spec create_table(atom(), atom(), [{atom(), atom()}]) -> sql_non_query_result(). +-spec create_table(atom(), atom(), table_info()) -> sql_non_query_result(). 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()}] +%% Columns = table_info() %% @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. @@ -336,13 +338,13 @@ create_table(Db, Tbl, Columns) -> %% Returns the result of the create table call. %% @end %%-------------------------------------------------------------------- --spec create_table_timeout(atom(), atom(), [{atom(), atom()}], timeout()) -> sql_non_query_result(). +-spec create_table_timeout(atom(), atom(), table_info(), 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()}] +%% Columns = table_info() %% Constraints = [term()] %% @doc %% Creates the Tbl table in Db using Columns as the table structure and @@ -353,13 +355,14 @@ create_table_timeout(Db, Tbl, Columns, Timeout) -> %% Returns the result of the create table call. %% @end %%-------------------------------------------------------------------- --spec create_table(atom(), atom(), [{atom(), atom()}], [any()]) -> sql_non_query_result(). +-spec create_table(atom(), atom(), table_info(), table_constraints()) -> + sql_non_query_result(). 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()}] +%% Columns = table_info() %% Constraints = [term()] %% @doc %% Creates the Tbl table in Db using Columns as the table structure and @@ -370,7 +373,8 @@ create_table(Db, Tbl, Columns, Constraints) -> %% Returns the result of the create table call. %% @end %%-------------------------------------------------------------------- --spec create_table_timeout(atom(), atom(), [{atom(), atom()}], [any()], timeout()) -> sql_non_query_result(). +-spec create_table_timeout(atom(), atom(), table_info(), table_constraints(), timeout()) -> + sql_non_query_result(). create_table_timeout(Db, Tbl, Columns, Constraints, Timeout) -> gen_server:call(Db, {create_table, Tbl, Columns, Constraints}, Timeout). @@ -405,32 +409,32 @@ list_tables_timeout(Db, Timeout) -> gen_server:call(Db, list_tables, Timeout). %%-------------------------------------------------------------------- -%% @spec table_info(Tbl :: atom()) -> [any()] +%% @spec table_info(Tbl :: atom()) -> table_info() %% @doc %% Returns table schema for Tbl. %% @end %%-------------------------------------------------------------------- --spec table_info(atom()) -> [any()]. +-spec table_info(atom()) -> table_info(). table_info(Tbl) -> table_info(?MODULE, Tbl). %%-------------------------------------------------------------------- -%% @spec table_info(Db :: atom(), Tbl :: atom()) -> [any()] +%% @spec table_info(Db :: atom(), Tbl :: atom()) -> table_info() %% @doc %% Returns table schema for Tbl in Db. %% @end %%-------------------------------------------------------------------- --spec table_info(atom(), atom()) -> [any()]. +-spec table_info(atom(), atom()) -> table_info(). table_info(Db, Tbl) -> gen_server:call(Db, {table_info, Tbl}). %%-------------------------------------------------------------------- -%% @spec table_info_timeout(Db :: atom(), Tbl :: atom()) -> [any()] +%% @spec table_info_timeout(Db :: atom(), Tbl :: atom(), Timeout :: timeout()) -> table_info() %% @doc %% Returns table schema for Tbl in Db. %% @end %%-------------------------------------------------------------------- --spec table_info_timeout(atom(), atom(), timeout()) -> [any()]. +-spec table_info_timeout(atom(), atom(), timeout()) -> table_info(). table_info_timeout(Db, Tbl, Timeout) -> gen_server:call(Db, {table_info, Tbl}, Timeout). @@ -459,7 +463,7 @@ write(Db, Tbl, Data) -> gen_server:call(Db, {write, Tbl, Data}). %%-------------------------------------------------------------------- -%% @spec write_timeout(Db :: atom(), Tbl :: atom(), Data) -> sql_non_query_result() +%% @spec write_timeout(Db :: atom(), Tbl :: atom(), Data, Timeout :: timeout()) -> sql_non_query_result() %% Data = [{Column :: atom(), Value :: sql_value()}] %% @doc %% Write Data into Tbl table in Db database. Value must be of the @@ -495,7 +499,7 @@ 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() +%% @spec write_many_timeout(Db :: atom(), Tbl :: atom(), Data, Timeout :: timeout()) -> sql_non_query_result() %% Data = [[{Column :: atom(), Value :: sql_value()}]] %% @doc %% Write all records in Data into table Tbl in database Db. Value @@ -528,12 +532,13 @@ update(Tbl, {Key, Value}, Data) -> %% matches the value in Key with Data. %% @end %%-------------------------------------------------------------------- --spec update(atom(), atom(), {atom(), sql_value()}, [{atom(), sql_value()}]) -> sql_non_query_result(). +-spec update(atom(), atom(), {atom(), sql_value()}, [{atom(), sql_value()}]) -> + sql_non_query_result(). 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() +%% @spec update_timeout(Db :: atom(), Tbl :: atom(), {Key :: atom(), Value}, Data, Timeout :: timeout()) -> sql_non_query_result() %% Value = sql_value() %% Data = [{Column :: atom(), Value :: sql_value()}] %% @doc @@ -541,7 +546,8 @@ update(Db, Tbl, {Key, Value}, Data) -> %% matches the value in Key with Data. %% @end %%-------------------------------------------------------------------- --spec update_timeout(atom(), atom(), {atom(), sql_value()}, [{atom(), sql_value()}], timeout()) -> sql_non_query_result(). +-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). @@ -556,7 +562,7 @@ read_all(Db, Tbl) -> gen_server:call(Db, {read, Tbl}). %%-------------------------------------------------------------------- -%% @spec read_all_timeout(Db :: atom(), Table :: atom()) -> sql_result() +%% @spec read_all_timeout(Db :: atom(), Table :: atom(), Timeout :: timeout()) -> sql_result() %% @doc %% Reads all rows from Table in Db. %% @end @@ -576,7 +582,7 @@ read_all(Db, Tbl, Columns) -> gen_server:call(Db, {read, Tbl, Columns}). %%-------------------------------------------------------------------- -%% @spec read_all_timeout(Db :: atom(), Table :: atom(), Columns :: [atom()]) -> sql_result() +%% @spec read_all_timeout(Db :: atom(), Table :: atom(), Columns :: [atom()], Timeout :: timeout()) -> sql_result() %% @doc %% Reads Columns in all rows from Table in Db. %% @end @@ -628,7 +634,7 @@ 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() +%% @spec read_timeout(Db :: atom(), Tbl :: atom(), Key, Timeout :: timeout()) -> sql_result() %% Key = {Column :: atom(), Value :: sql_value()} %% @doc %% Reads a row from Tbl table in Db database such that the Value @@ -641,7 +647,7 @@ read_timeout(Db, Tbl, {Column, Value}, Timeout) -> gen_server:call(Db, {read, Tbl, Column, Value}, Timeout). %%-------------------------------------------------------------------- -%% @spec read_timeout(Db, Tbl, Key, Columns) -> [any()] +%% @spec read_timeout(Db, Tbl, Key, Columns, Timeout :: timeout()) -> [any()] %% Db = atom() %% Tbl = atom() %% Key = {Column :: atom(), Value :: sql_value()} @@ -670,7 +676,7 @@ delete(Tbl, Key) -> delete(?MODULE, Tbl, Key). %%-------------------------------------------------------------------- -%% @spec delete_timeout(Db :: atom(), Tbl :: atom(), Key) -> sql_non_query_result() +%% @spec delete_timeout(Db :: atom(), Tbl :: atom(), Key, Timeout :: timeout()) -> sql_non_query_result() %% Key = {Column :: atom(), Value :: sql_value()} %% @doc %% Delete a row from Tbl table in Db database such that the Value @@ -716,7 +722,7 @@ drop_table(Db, Tbl) -> gen_server:call(Db, {drop_table, Tbl}). %%-------------------------------------------------------------------- -%% @spec drop_table_timeout(Db :: atom(), Tbl :: atom()) -> sql_non_query_result() +%% @spec drop_table_timeout(Db :: atom(), Tbl :: atom(), Timeout :: timeout()) -> sql_non_query_result() %% @doc %% Drop the table Tbl from Db database. %% @end @@ -746,7 +752,7 @@ vacuum(Db) -> gen_server:call(Db, vacuum). %%-------------------------------------------------------------------- -%% @spec vacuum_timeout(Db :: atom()) -> sql_non_query_result() +%% @spec vacuum_timeout(Db :: atom(), Timeout :: timeout()) -> sql_non_query_result() %% @doc %% Vacuum the Db database. %% @end @@ -1181,15 +1187,40 @@ build_primary_key_constraint(Tail, Acc) -> %% {no_on_conflict, NoOnConflictClause}. %%-------------------------------------------------------------------- -%% @type sql_value() = number() | 'null' | iodata(). +%% @type sql_value() = null | number() | iodata() | {blob, binary()}. %% -%% Values accepted in SQL statements include numbers, atom 'null', -%% and io:iolist(). +%% Values accepted in SQL statements are atom 'null', numbers, +%% strings (represented as iodata()) and blobs. +%% @end +%% @type sql_type() = integer | text | double | blob | atom() | string(). +%% +%% Types of SQLite columns are represented by atoms 'integer', 'text', 'double', +%% 'blob'. Other atoms and strings may also be used (e.g. "VARCHAR(20)", 'smallint', etc.) +%% See [http://www.sqlite.org/datatype3.html]. +%% @end +%% @type pk_constraint() = autoincrement | desc | asc. +%% See {@link pk_constraints()}. +%% @type pk_constraints() = pk_constraint() | [pk_constraint()]. +%% See {@link column_constraint()}. +%% @type column_constraint() = non_null | primary_key | {primary_key, pk_constraints()} +%% | unique | {default, sql_value()}. +%% See {@link column_constraints()}. +%% @type column_constraints() = column_constraint() | [column_constraint()]. +%% See {@link table_info()}. +%% @type table_info() = [{atom(), sql_type()} | {atom(), sql_type(), column_constraints()}]. +%% +%% Describes the columns of an SQLite table: each tuple contains name, type and constraints (if any) +%% of one column. +%% @end +%% @type table_constraint() = {primary_key, [atom()]} | {unique, [atom()]}. +%% @type table_constraints() = table_constraint() | [table_constraint()]. +%% +%% Currently supported constraints for {@link table_info()} and {@link sqlite3:create_table/4}. %% @end %% @type sqlite_error() = {'error', integer(), string()}. %% %% Errors are reported by their SQLite result code -%% (http://www.sqlite.org/c3ref/c_busy_recovery.html) and a string containing +%% ([http://www.sqlite.org/c3ref/c_busy_recovery.html]) and a string containing %% English-language text that describes the error. %% @end %% @type sql_non_query_result() = ok | sqlite_error() | {rowid, integer()}. diff --git a/src/sqlite3_lib.erl b/src/sqlite3_lib.erl index de2428b..f3e2979 100644 --- a/src/sqlite3_lib.erl +++ b/src/sqlite3_lib.erl @@ -25,7 +25,7 @@ %% @doc Maps sqlite3 column type. %% @end %%-------------------------------------------------------------------- --spec col_type_to_string(atom() | string()) -> string(). +-spec col_type_to_string(sql_type()) -> string(). col_type_to_string(integer) -> "INTEGER"; col_type_to_string(text) -> @@ -36,6 +36,8 @@ col_type_to_string(real) -> "REAL"; col_type_to_string(blob) -> "BLOB"; +col_type_to_string(Atom) when is_atom(Atom) -> + string:to_upper(atom_to_list(Atom)); col_type_to_string(String) when is_list(String) -> String. @@ -52,7 +54,10 @@ col_type_to_atom("TEXT") -> col_type_to_atom("REAL") -> double; col_type_to_atom("BLOB") -> - blob. + blob; +col_type_to_atom(String) -> + list_to_atom(string:to_lower(String)). + %%-------------------------------------------------------------------- %% @spec value_to_sql_unsafe(Value :: sql_value()) -> iolist() @@ -196,7 +201,7 @@ read_cols_sql(Columns) -> %% @doc Generates a table create stmt in SQL. %% @end %%-------------------------------------------------------------------- --spec create_table_sql(atom(), [{atom(), atom()} | {atom(), atom(), [any()]}]) -> iolist(). +-spec create_table_sql(atom(), table_info()) -> iolist(). create_table_sql(Tbl, Columns) -> ["CREATE TABLE ", atom_to_list(Tbl), " (", map_intersperse(fun column_sql_for_create_table/1, Columns, ", "), ");"]. @@ -212,11 +217,11 @@ create_table_sql(Tbl, Columns) -> %% @doc Generates a table create stmt in SQL. %% @end %%-------------------------------------------------------------------- --spec create_table_sql(atom(), [{atom(), atom()} | {atom(), atom(), [any()]}], [any()]) -> iolist(). +-spec create_table_sql(atom(), table_info(), table_constraints()) -> iolist(). create_table_sql(Tbl, Columns, TblConstraints) -> ["CREATE TABLE ", atom_to_list(Tbl), " (", map_intersperse(fun column_sql_for_create_table/1, Columns, ", "), ", ", - map_intersperse(fun table_constraint_sql/1, TblConstraints, ", "), + table_constraint_sql(TblConstraints), ");"]. %%-------------------------------------------------------------------- @@ -334,7 +339,7 @@ drop_table_sql(Tbl) -> %% @doc Works like string:join for iolists --spec map_intersperse(fun((X) -> iolist()), [X], [iolist() | integer()]) -> iolist(). +-spec map_intersperse(fun((X) -> iolist()), [X], iolist()) -> iolist(). map_intersperse(_Fun, [], _Sep) -> []; map_intersperse(Fun, [Elem], _Sep) -> [Fun(Elem)]; map_intersperse(Fun, [Head | Tail], Sep) -> [Fun(Head), Sep | map_intersperse(Fun, Tail, Sep)]. @@ -373,20 +378,29 @@ hex_str_to_bin([X, Y | Tail], Acc) -> column_sql_for_create_table({Name, Type}) -> [atom_to_list(Name), " ", col_type_to_string(Type)]; column_sql_for_create_table({Name, Type, Constraints}) -> - [atom_to_list(Name), " ", col_type_to_string(Type), - " " | map_intersperse(fun constraint_sql/1, Constraints, " ")]. + [atom_to_list(Name), " ", col_type_to_string(Type), " ", constraint_sql(Constraints)]. --spec constraint_sql(any()) -> iolist(). +-spec pk_constraint_sql(pk_constraints()) -> iolist(). +pk_constraint_sql(Constraint) -> + case Constraint of + desc -> "DESC"; + asc -> "ASC"; + autoincrement -> "AUTOINCREMENT"; + _ when is_list(Constraint) -> map_intersperse(fun pk_constraint_sql/1, Constraint, " ") + end. + +-spec constraint_sql(column_constraints()) -> iolist(). constraint_sql(Constraint) -> case Constraint of primary_key -> "PRIMARY KEY"; - {primary_key, desc} -> "PRIMARY KEY DESC"; + {primary_key, C} -> ["PRIMARY KEY ", pk_constraint_sql(C)]; unique -> "UNIQUE"; not_null -> "NOT NULL"; - {default, DefaultValue} -> ["DEFAULT ", value_to_sql(DefaultValue)] + {default, DefaultValue} -> ["DEFAULT ", value_to_sql(DefaultValue)]; + _ when is_list(Constraint) -> map_intersperse(fun constraint_sql/1, Constraint, " ") end. --spec table_constraint_sql(any()) -> iolist(). +-spec table_constraint_sql(table_constraints()) -> iolist(). table_constraint_sql(TableConstraint) -> case TableConstraint of {primary_key, Columns} -> @@ -394,8 +408,10 @@ table_constraint_sql(TableConstraint) -> map_intersperse(fun indexed_column_sql/1, Columns, ", "), ")"]; {unique, Columns} -> ["UNIQUE(", - map_intersperse(fun indexed_column_sql/1, Columns, ", "), ")"] + map_intersperse(fun indexed_column_sql/1, Columns, ", "), ")"]; %% TODO: foreign key + _ when is_list(TableConstraint) -> + map_intersperse(fun table_constraint_sql/1, TableConstraint, ", ") end. indexed_column_sql({ColumnName, asc}) -> [atom_to_list(ColumnName), " ASC"]; @@ -427,6 +443,12 @@ create_table_sql_test() -> ?assertFlat( "CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT);", create_table_sql(user, [{id, integer, [primary_key]}, {name, text}])), + ?assertFlat( + "CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);", + create_table_sql(user, [{id, integer, [{primary_key, autoincrement}]}, {name, text}])), + ?assertFlat( + "CREATE TABLE user (id INTEGER PRIMARY KEY DESC, name TEXT);", + create_table_sql(user, [{id, integer, [{primary_key, desc}]}, {name, text}])), ?assertFlat( "CREATE TABLE user (id INTEGER, name TEXT, PRIMARY KEY(id));", create_table_sql(user, diff --git a/test/sqlite3_test.erl b/test/sqlite3_test.erl index a207bc9..1bfe276 100644 --- a/test/sqlite3_test.erl +++ b/test/sqlite3_test.erl @@ -47,7 +47,8 @@ all_test_() -> ?FuncTest(select_many_records), ?FuncTest(nonexistent_table_info), ?FuncTest(large_number), - ?FuncTest(unicode)]}. + ?FuncTest(unicode), + ?FuncTest(acc_string_encoding)]}. open_db() -> sqlite3:open(ct, [in_memory]). @@ -61,7 +62,11 @@ basic_functionality() -> Columns = ["id", "name", "age", "wage"], AllRows = [{1, <<"abby">>, 20, 2000}, {2, <<"marge">>, 30, 2000}], AbbyOnly = [{1, <<"abby">>, 20, 2000}], - TableInfo = [{id, integer, [primary_key]}, {name, text, [not_null, unique]}, {age, integer}, {wage, integer}], + TableInfo = [{id, integer, [{primary_key, [asc, autoincrement]}]}, + {name, text, [not_null, unique]}, + {age, integer, not_null}, + {wage, integer}], + TableInfo1 = lists:keyreplace(age, 1, TableInfo, {age, integer, [not_null]}), drop_all_tables(ct), ?WARN_ERROR_MESSAGE, ?assertEqual( @@ -72,10 +77,10 @@ basic_functionality() -> sqlite3:list_tables(ct)), ok = sqlite3:create_table(ct, user, TableInfo), ?assertEqual( - [user], + [user, sqlite_sequence], sqlite3:list_tables(ct)), ?assertEqual( - TableInfo, + TableInfo1, sqlite3:table_info(ct, user)), ?assertEqual( {rowid, 1}, @@ -216,6 +221,9 @@ unicode() -> sqlite3:write(ct, unicode, [{str, UnicodeString}]), ?assertEqual([{unicode:characters_to_binary(UnicodeString)}], rows(sqlite3:read_all(ct, unicode))). +acc_string_encoding() -> + ?assertEqual([{62}], rows(sqlite3:sql_exec(ct, "SELECT ? + ?", [30,32]))). + prepared_test() -> Columns = ["id", "name", "age", "wage"], Abby = {1, <<"abby">>, 20, 2000},