diff --git a/c_src/esqlite3_nif.c b/c_src/esqlite3_nif.c index 698340a..c59905c 100644 --- a/c_src/esqlite3_nif.c +++ b/c_src/esqlite3_nif.c @@ -90,11 +90,19 @@ make_ok_tuple(ErlNifEnv *env, ERL_NIF_TERM value) } static ERL_NIF_TERM -make_error_tuple(ErlNifEnv *env, const char *reason) +make_error_tuple(ErlNifEnv *env, const char *reason) { return enif_make_tuple2(env, make_atom(env, "error"), make_atom(env, reason)); } +static ERL_NIF_TERM +make_sqlite3_error_tuple(ErlNifEnv *env, const char *msg) +{ + return enif_make_tuple2(env, make_atom(env, "error"), + enif_make_tuple2(env, make_atom(env, "sqlite3_error"), + enif_make_string(env, msg, ERL_NIF_LATIN1))); +} + static void command_destroy(void *obj) { @@ -185,7 +193,7 @@ do_open(ErlNifEnv *env, esqlite_connection *db, const ERL_NIF_TERM arg) */ rc = sqlite3_open(filename, &db->db); if(rc != SQLITE_OK) { - error = make_error_tuple(env, sqlite3_errmsg(db->db)); + error = make_sqlite3_error_tuple(env, sqlite3_errmsg(db->db)); sqlite3_close(db->db); db->db = NULL; @@ -207,7 +215,7 @@ do_exec(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) rc = sqlite3_exec(conn->db, (char *) bin.data, NULL, NULL, NULL); if(rc != SQLITE_OK) - return make_error_tuple(env, sqlite3_errmsg(conn->db)); + return make_sqlite3_error_tuple(env, sqlite3_errmsg(conn->db)); return make_atom(env, "ok"); } @@ -231,7 +239,7 @@ do_prepare(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) rc = sqlite3_prepare_v2(conn->db, (char *) bin.data, bin.size, &(stmt->statement), &tail); if(rc != SQLITE_OK) - return make_error_tuple(env, sqlite3_errmsg(conn->db)); + return make_sqlite3_error_tuple(env, sqlite3_errmsg(conn->db)); enif_keep_resource(conn); stmt->connection = conn; @@ -299,7 +307,7 @@ do_bind(ErlNifEnv *env, sqlite3 *db, sqlite3_stmt *stmt, const ERL_NIF_TERM arg) if(r == -1) return make_error_tuple(env, "wrong_type"); if(r != SQLITE_OK) - return make_error_tuple(env, sqlite3_errmsg(db)); + return make_sqlite3_error_tuple(env, sqlite3_errmsg(db)); list = tail; } @@ -413,7 +421,7 @@ do_close(ErlNifEnv *env, esqlite_connection *conn, const ERL_NIF_TERM arg) rc = sqlite3_close(conn->db); if(rc != SQLITE_OK) - return make_error_tuple(env, sqlite3_errmsg(conn->db)); + return make_sqlite3_error_tuple(env, sqlite3_errmsg(conn->db)); conn->db = NULL; return make_atom(env, "ok"); diff --git a/src/edbc.hrl b/src/edbc.hrl new file mode 100644 index 0000000..51582ea --- /dev/null +++ b/src/edbc.hrl @@ -0,0 +1,4 @@ +%% +%% + +-record(edbc_column_info, {name, type, default, notnull, pk}). diff --git a/src/esqlite3.erl b/src/esqlite3.erl index 59ae2ed..c426c51 100644 --- a/src/esqlite3.erl +++ b/src/esqlite3.erl @@ -26,10 +26,12 @@ prepare/2, prepare/3, step/1, step/2, bind/2, bind/3, + fetchone/1, + fetchall/1, column_names/1, column_names/2, close/1, close/2]). --export([q/2, map/3, foreach/3]). +-export([q/2, q/3, map/3, foreach/3]). -define(DEFAULT_TIMEOUT, infinity). @@ -56,8 +58,16 @@ open(Filename, Timeout) -> %% @doc Execute a sql statement, returns a list with tuples. q(Sql, Connection) -> + q(Sql, [], Connection). + +%% @doc Execute statement, bind args and return a list with tuples as result. +q(Sql, [], Connection) -> {ok, Statement} = prepare(Sql, Connection), - do_steps(Statement). + fetchall(Statement); +q(Sql, Args, Connection) -> + {ok, Statement} = prepare(Sql, Connection), + ok = bind(Statement, Args), + fetchall(Statement). %% map(F, Sql, Connection) -> @@ -101,13 +111,21 @@ map_s(F, Statement) when is_function(F, 2) -> [F(ColumnNames, Row) | map_s(F, Statement)] end. +%% +fetchone(Statement) -> + case try_step(Statement, 0) of + '$done' -> ok; + Row when is_tuple(Row) -> + Row + end. + %% -do_steps(Statement) -> +fetchall(Statement) -> case try_step(Statement, 0) of '$done' -> []; Row when is_tuple(Row) -> - [Row | do_steps(Statement)] + [Row | fetchall(Statement)] end. %% Try the step, when the database is busy, diff --git a/src/esqlite3_nif.erl b/src/esqlite3_nif.erl index d689aa2..8fdc38e 100644 --- a/src/esqlite3_nif.erl +++ b/src/esqlite3_nif.erl @@ -26,6 +26,7 @@ exec/4, prepare/4, step/3, + finalize/3, bind/4, column_names/3, close/3 @@ -75,6 +76,12 @@ prepare(_Db, _Ref, _Dest, _Sql) -> step(_Stmt, _Ref, _Dest) -> exit(nif_library_not_loaded). +%% @doc +%% +%% +finalize(_Stmt, _Ref, _Dest) -> + exit(nif_library_not_loaded). + %% @doc Bind parameters to a prepared statement. %% %% @spec bind(statement(), reference(), pid(), []) -> ok | {error, message()} diff --git a/src/gen_db.erl b/src/gen_db.erl index 779a0af..a2e9105 100644 --- a/src/gen_db.erl +++ b/src/gen_db.erl @@ -1,41 +1,78 @@ %% %% Generic database interface. Sort of... %% -%% Inspired by python's db api %% -module(gen_db). -export([behaviour_info/1]). --export([open/2, execute/2, execute/3, close/1]). +-export([open/2, close/1, run/2, run/3, execute/2, execute/3, commit/1, rollback/1, tables/1, describe_table/2, column_names/2]). + +-include("edbc.hrl"). behaviour_info(callbacks) -> - [{handle_open, 1}, - {handle_execute, 3}, - {handle_close, 1}]; + [{open, 1}, + {close, 1}, + {commit, 1}, + {rollback, 1}, + {run, 3}, + {execute, 3}, + {tables, 1}, + {describe_table, 2}]; behaviour_info(_Other) -> undefined. --record(gen_connection, {module, connection}). +-record(connection, {driver, connection_data}). %% @doc Open a connection to a new database %% -open(Module, ModuleArgs) -> - {ok, Conn} = Module:handle_open(ModuleArgs), - {ok, #gen_connection{module=Module, connection=Conn}}. - -%% @doc Prepare and execute a database operation -%% -execute(Operation, Connection) -> - execute(Operation, [], Connection). - -%% @doc Prepare and execute a database operation -%% -execute(Operation, Args, #gen_connection{module=Module, connection=Connection}) -> - Module:handle_execute(Operation, Args, Connection). +open(Driver, Args) -> + {ok, ConnectionData} = Driver:open(Args), + {ok, #connection{driver=Driver, connection_data=ConnectionData}}. %% @doc Close a database connection. %% -close(#gen_connection{module=Module, connection=Connection}) -> - Module:handle_close(Connection). +close(#connection{driver=Driver, connection_data=Data}) -> + Driver:close(Data). + +%% @doc Commit all changes +commit(#connection{driver=Driver, connection_data=Data}) -> + Driver:commit(Data). + +%% @doc Rollback all changes +rollback(#connection{driver=Driver, connection_data=Data}) -> + Driver:rollback(Data). + +%% @doc Return a list with tablenames... +tables(#connection{driver=Driver, connection_data=Data}) -> + Driver:tables(Data). + +%% @doc +describe_table(TableName, #connection{driver=Driver, connection_data=Data}) -> + Driver:describe_table(TableName, Data). + +%% @doc +column_names(TableName, Connection) -> + [Info#edbc_column_info.name || Info <- describe_table(TableName, Connection) ]. + + +%% @doc Execute the statement, without returning results. +run(Sql, Connection) -> + run(Sql, [], Connection). + +run(Sql, Params, #connection{driver=Driver, connection_data=Data}) -> + Driver:run(Sql, Params, Data). + +%% @doc Execute the statement, return the result +execute(Sql, Connection) -> + execute(Sql, [], Connection). + +execute(Sql, Params, #connection{driver=Driver, connection_data=Data}) -> + Driver:execute(Sql, Params, Data). + + + + + + diff --git a/src/sqlite.erl b/src/sqlite.erl index 6c07188..b1baf5e 100644 --- a/src/sqlite.erl +++ b/src/sqlite.erl @@ -4,46 +4,92 @@ -module(sqlite). +-include("edbc.hrl"). + -behaviour(gen_db). --export([handle_open/1, handle_execute/3, handle_close/1]). +-export([open/1, run/3, execute/3, close/1, commit/1, rollback/1, tables/1, describe_table/2]). %% @doc Open a database connection %% -handle_open(DatabaseName) -> - esqlite3:open(DatabaseName). - -%% @doc Execute a query and return the results -%% -handle_execute(Operation, Args, Connection) -> - {ok, Stmt} = esqlite3:prepare(Connection, Operation), - ok = esqlite3:bind(Stmt, Args), - Answer = execute(Stmt), - %% TODO Finalize the statement. - Answer. +open([DatabaseName]) -> + {ok, C} = esqlite3:open(DatabaseName), + ok = esqlite3:exec(<<"BEGIN TRANSACTION;">>, C), + {ok, C}. %% @doc Close the connection %% -handle_close(Connection) -> - esqlite:close(Connection). +close(Connection) -> + ok = esqlite3:close(Connection). -%% @doc +%% @doc %% -execute(Statement) -> - execute(Statement, [], 0). - -%% @doc -%% -execute(_Statement, _Acc, Tries) when Tries > 5 -> - throw(too_many_tries); -execute(Statement, Acc, Tries) -> - case esqlite3:step(Statement) of - '$done' -> - lists:reverse(Acc); - '$busy' -> - timer:sleep(100), %% This is a bit lame... there is a trigger api for this. - execute(Statement, Acc, Tries + 1); - V when is_tuple(V) -> - execute(Statement, [V | Acc], 0) +run(Sql, [], Connection) -> + case esqlite3:exec(Sql, Connection) of + {error, Error} -> {error, ?MODULE, Error}; + ok -> ok + end; +run(Sql, Args, Connection) -> + case esqlite3:prepare(Sql, Connection) of + {error, Error} -> + {error, ?MODULE, Error}; + {ok, Stmt} -> + case esqlite3:bind(Stmt, Args) of + {error, Error} -> {error, ?MODULE, Error}; + ok -> + case esqlite3:fetchone(Stmt) of + {error, Error} -> {error, ?MODULE, Error}; + _ -> ok + end + end end. + +%% @doc Execute a query and return the results +%% +execute(Sql, [], Connection) -> + case esqlite3:prepare(Sql, Connection) of + {error, Error} -> + {error, ?MODULE, Error}; + {ok, Stmt} -> + {ok, esqlite3:column_names(Stmt), esqlite3:fetchall(Stmt)} + end; +execute(Sql, Args, Connection) -> + case esqlite3:prepare(Sql, Connection) of + {error, Error} -> + {error, ?MODULE, Error}; + {ok, Stmt} -> + case esqlite3:bind(Stmt, Args) of + ok -> + Names = esqlite3:column_names(Stmt), + Result = esqlite3:fetchall(Stmt), + {ok, Names, Result}; + {error, Error} -> + {error, ?MODULE, Error} + end + end. + +%% +commit(Connection) -> + ok = run(<<"COMMIT;">>, [], Connection). + +%% +rollback(Connection) -> + ok = run(<<"ROLLBACK;">>, [], Connection). + +%% +tables(Connection) -> + esqlite3:map(fun({TableName}) -> list_to_atom(TableName) end, + <<"SELECT name FROM sqlite_master WHERE type='table' ORDER by name;">>, Connection). + +%% +describe_table(TableName, Connection) when is_atom(TableName) -> + esqlite3:map(fun({_Cid, ColumnName, ColumnType, NotNull, Default, PrimaryKey}) -> + #edbc_column_info{name=list_to_atom(ColumnName), + type=ColumnType, + default=Default, + notnull=NotNull =/= 0, + pk=PrimaryKey =/= 0} + end, + [<<"PRAGMA table_info('">>, atom_to_list(TableName), <<"');">>], Connection). +