diff --git a/src/sqlite3.erl b/src/sqlite3.erl index cdb0015..f1cedd0 100644 --- a/src/sqlite3.erl +++ b/src/sqlite3.erl @@ -485,9 +485,13 @@ handle_call({table_info, Tbl}, _From, #state{port = Port} = State) -> SQL = io_lib:format("select sql from sqlite_master where tbl_name = '~p' and type='table';", [Tbl]), Data = exec(Port, {sql_exec, SQL}), TableSql = proplists:get_value(rows, Data), - [{Info}] = TableSql, - ColumnList = parse_table_info(binary_to_list(Info)), - {reply, ColumnList, State}; + case TableSql of + [{Info}] -> + ColumnList = parse_table_info(binary_to_list(Info)), + {reply, ColumnList, State}; + [] -> + {reply, table_does_not_exist, State} + end; handle_call({create_function, FunctionName, Function}, _From, #state{port = Port} = State) -> % make sure we only get table info. % SQL Injection warning @@ -630,15 +634,29 @@ build_table_info([[ColName, ColType | Constraints] | Tl], Acc) -> %% TODO conflict-clause parsing build_constraints([]) -> []; -build_constraints(["PRIMARY", "KEY", "ASC" | Tail]) -> [primary_key | build_constraints(Tail)]; -build_constraints(["PRIMARY", "KEY", "DESC" | Tail]) -> [{primary_key, desc} | build_constraints(Tail)]; -build_constraints(["PRIMARY", "KEY" | Tail]) -> [primary_key | build_constraints(Tail)]; +build_constraints(["PRIMARY", "KEY" | Tail]) -> + {Constraint, Rest} = build_primary_key_constraint(Tail), + [Constraint | build_constraints(Rest)]; build_constraints(["UNIQUE" | Tail]) -> [unique | build_constraints(Tail)]; build_constraints(["NOT", "NULL" | Tail]) -> [not_null | build_constraints(Tail)]; build_constraints(["DEFAULT", DefaultValue | Tail]) -> [{default, sqlite3_lib:sql_to_value(DefaultValue)} | build_constraints(Tail)]. % build_constraints(["CHECK", Check | Tail]) -> ... % build_constraints(["REFERENCES", Check | Tail]) -> ... +build_primary_key_constraint(Tokens) -> build_primary_key_constraint(Tokens, []). + +build_primary_key_constraint(["ASC" | Rest], Acc) -> + build_primary_key_constraint(Rest, [asc | Acc]); +build_primary_key_constraint(["DESC" | Rest], Acc) -> + build_primary_key_constraint(Rest, [desc | Acc]); +build_primary_key_constraint(["AUTOINCREMENT" | Rest], Acc) -> + build_primary_key_constraint(Rest, [autoincrement | Acc]); +build_primary_key_constraint(Tail, []) -> + {primary_key, Tail}; +build_primary_key_constraint(Tail, Acc) -> + {{primary_key, lists:reverse(Acc)}, Tail}. + + conflict_clause(["ON", "CONFLICT", ResolutionString | Tail]) -> Resolution = case ResolutionString of "ROLLBACK" -> rollback; @@ -658,3 +676,11 @@ conflict_clause(NoOnConflictClause) -> %% and io:iolist(). %% @end %%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Tests +%%-------------------------------------------------------------------- +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-endif. diff --git a/src/sqlite3_lib.erl b/src/sqlite3_lib.erl index 47eaa76..70f0f15 100644 --- a/src/sqlite3_lib.erl +++ b/src/sqlite3_lib.erl @@ -116,14 +116,14 @@ sql_to_value(String) -> end. %%-------------------------------------------------------------------- -%% @spec write_value_sql(Value :: [term()]) -> iolist() +%% @spec write_value_sql(Value :: [sql_value()]) -> iolist() %% @doc %% Creates the values portion of the sql stmt. %% @end %%-------------------------------------------------------------------- --spec write_value_sql(any()) -> iolist(). +-spec write_value_sql(sql_value()) -> iolist(). write_value_sql(Values) -> - map_intersperse(fun value_to_sql/1, Values, ","). + map_intersperse(fun value_to_sql/1, Values, ", "). %%-------------------------------------------------------------------- @@ -133,7 +133,7 @@ write_value_sql(Values) -> %%-------------------------------------------------------------------- -spec write_col_sql([atom()]) -> iolist(). write_col_sql(Cols) -> - map_intersperse(fun atom_to_list/1, Cols, ","). + map_intersperse(fun atom_to_list/1, Cols, ", "). %%-------------------------------------------------------------------- %% @spec escape(IoData :: iodata()) -> iodata() @@ -355,3 +355,18 @@ indexed_column_sql(ColumnName) -> atom_to_list(ColumnName). %% and io:iolist(). %% @end %%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Tests +%%-------------------------------------------------------------------- +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-define(FLAT(X), iolist_to_binary(X)). + +quote_test() -> + ?assertEqual(<<"'abc'">>, ?FLAT(value_to_sql("abc"))), + ?assertEqual(<<"'a''b''''c'">>, ?FLAT(value_to_sql("a'b''c"))). + + +-endif. diff --git a/src/sqlite3_test.erl b/src/sqlite3_test.erl index 890a91b..261a0af 100644 --- a/src/sqlite3_test.erl +++ b/src/sqlite3_test.erl @@ -17,27 +17,91 @@ -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -create_table_test() -> - file:delete("ct.db"), - sqlite3:open(ct), - sqlite3:create_table(ct, user, [{id, integer, [primary_key]}, {name, text}, {age, integer}, {wage, integer}]), - [user] = sqlite3:list_tables(ct), - [{id, integer, [primary_key]}, {name, text}, {age, integer}, {wage, integer}] = sqlite3:table_info(ct, user), - {id, Id1} = sqlite3:write(ct, user, [{name, "abby"}, {age, 20}, {wage, 2000}]), - Id1 = 1, - {id, Id2} = sqlite3:write(ct, user, [{name, "marge"}, {age, 30}, {wage, 2000}]), - Id2 = 2, - [{columns, Columns}, {rows, Rows1}] = sqlite3:sql_exec(ct, "select * from user;"), - Columns = ["id", "name", "age", "wage"], - Rows1 = [{1, <<"abby">>, 20, 2000}, {2, <<"marge">>, 30, 2000}], - [{columns, Columns}, {rows, Rows2}] = sqlite3:read(ct, user, {name, "abby"}), - Rows2 = [{1, <<"abby">>, 20, 2000}], - [{columns, Columns}, {rows, Rows1}] = sqlite3:read(ct, user, {wage, 2000}), - sqlite3:delete(ct, user, {name, "abby"}), - sqlite3:drop_table(ct, user), -%sqlite3:delete_db(ct) - sqlite3:close(ct). +drop_all_tables(Db) -> + Tables = sqlite3:list_tables(Db), + [sqlite3:drop_table(Db, Table) || Table <- Tables], + Tables. +drop_table_if_exists(Db, Table) -> + case lists:member(Table, sqlite3:list_tables(Db)) of + true -> sqlite3:drop_table(Db, Table); + false -> ok + end. + +rows(SqlExecReply) -> + [{columns, _Columns}, {rows, Rows}] = SqlExecReply, + Rows. + +basic_functionality_test() -> + Columns = ["id", "name", "age", "wage"], + AllRows = [{1, <<"abby">>, 20, 2000}, {2, <<"marge">>, 30, 2000}], + AbbyOnly = [{1, <<"abby">>, 20, 2000}], + sqlite3:open(ct), + drop_all_tables(ct), + ?assertEqual( + [], + sqlite3:list_tables(ct)), + {ok, TableId} = sqlite3:create_table(ct, user, [{id, integer, [primary_key]}, {name, text}, {age, integer}, {wage, integer}]), + ?assertEqual( + [user], + sqlite3:list_tables(ct)), + ?assertEqual( + [{id, integer, [primary_key]}, {name, text}, {age, integer}, {wage, integer}], + sqlite3:table_info(ct, user)), + ?assertEqual( + {id, 1}, + sqlite3:write(ct, user, [{name, "abby"}, {age, 20}, {wage, 2000}])), + ?assertEqual( + {id, 2}, + sqlite3:write(ct, user, [{name, "marge"}, {age, 30}, {wage, 2000}])), + ?assertEqual( + [{columns, Columns}, {rows, AllRows}], + sqlite3:sql_exec(ct, "select * from user;")), + ?assertEqual( + [{columns, Columns}, {rows, AbbyOnly}], + sqlite3:read(ct, user, {name, "abby"})), + ?assertEqual( + [{columns, Columns}, {rows, AllRows}], + sqlite3:read(ct, user, {wage, 2000})), + ?assertEqual( + {ok, TableId}, + sqlite3:delete(ct, user, {name, "marge"})), + ?assertEqual( + [{columns, Columns}, {rows, AbbyOnly}], + sqlite3:sql_exec(ct, "select * from user;")), + ?assertEqual( + {ok, TableId}, + sqlite3:drop_table(ct, user)), + sqlite3:close(ct). + +select_many_records_test() -> + sqlite3:open(ct), + drop_table_if_exists(ct, many_records), + sqlite3:create_table(ct, many_records, [{id, integer}, {name, text}]), + lists:foreach(fun(X) -> sqlite3:write(ct, many_records, [{id, X}, {name, "bar"}]) end, lists:seq(1, 1024)), + Columns = ["id", "name"], + ?assertEqual( + [{columns, Columns}, {rows, [{1, <<"bar">>}]}], + sqlite3:read(ct, many_records, {id, 1})), + ?assertEqual( + 10, + length(rows(sqlite3:sql_exec(ct, "select * from many_records limit 10;")))), + ?assertEqual( + 100, + length(rows(sqlite3:sql_exec(ct, "select * from many_records limit 100;")))), + ?assertEqual( + 1000, + length(rows(sqlite3:sql_exec(ct, "select * from many_records limit 1000;")))), + ?assertEqual( + 1024, + length(rows(sqlite3:sql_exec(ct, "select * from many_records;")))), + sqlite3:close(ct). + +nonexistent_table_info_test() -> + sqlite3:open(ct), + ?assertEqual(table_does_not_exist, sqlite3:table_info(ct, nonexistent)), + sqlite3:close(ct). + -endif. % create, read, update, delete