%% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% @author Maas-Maarten Zeeman %% @copyright 2022 Maas-Maarten Zeeman %% %% @doc Low level erlang API for duckdb databases. -module(educkdb). -author("Maas-Maarten Zeeman "). %% low-level exports -export([ open/1, open/2, close/1, config_flag_info/0, connect/1, disconnect/1, query/2, prepare/2, execute_prepared/1, bind_boolean/3, bind_int8/3, bind_int16/3, bind_int32/3, bind_int64/3, bind_uint8/3, bind_uint16/3, bind_uint32/3, bind_uint64/3, bind_float/3, bind_double/3, bind_date/3, bind_time/3, bind_timestamp/3, bind_varchar/3, bind_null/2, %% Results extract_result/1, get_chunk/2, get_chunks/1, chunk_count/1, column_names/1, %% Chunks extract_chunk/1, get_chunk_column_count/1, get_chunk_size/1, appender_create/3, append_boolean/2, append_int8/2, append_int16/2, append_int32/2, append_int64/2, append_uint8/2, append_uint16/2, append_uint32/2, append_uint64/2, append_float/2, append_double/2, append_time/2, append_date/2, append_timestamp/2, append_varchar/2, append_null/1, appender_flush/1, appender_end_row/1 ]). %% High Level Api -export([ squery/2, execute/1 ]). %% low-level api -export([ query_cmd/2, execute_prepared_cmd/1 ]). -type database() :: reference(). -type connection() :: reference(). -type prepared_statement() :: reference(). -type result() :: reference(). -type appender() :: reference(). -type data_chunk() :: reference(). -type sql() :: iodata(). -type idx() :: 0..16#FFFFFFFFFFFFFFFF. -type int8() :: -16#7F..16#7F. -type uint8() :: 0..16#FF. -type int16() :: -16#7FFF..16#7FFF. -type uint16() :: 0..16#FFFF. -type int32() :: -16#7FFFFFFF..16#7FFFFFFF. -type uint32() :: 0..16#FFFFFFFF. -type int64() :: -16#7FFFFFFFFFFFFFFF..16#7FFFFFFFFFFFFFFF. -type uint64() :: 0..16#FFFFFFFFFFFFFFFF. -type bind_response() :: ok | {error, _}. -type append_response() :: ok | {error, _}. -export_type([database/0, connection/0, prepared_statement/0, result/0, sql/0, int8/0, int16/0, int32/0, int64/0, uint8/0, uint16/0, uint32/0, uint64/0, idx/0 ]). -define(SEC_TO_MICS(S), (S * 1000000)). -define(MIC_TO_SECS(S), (S / 1000000.0)). -define(MIN_TO_MICS(S), (S * 60000000)). -define(HOUR_TO_MICS(S), (S * 3600000000)). -define(EPOCH_OFFSET, 62167219200000000). -on_load(init/0). init() -> NifName = "educkdb_nif", NifFileName = case code:priv_dir(educkdb) of {error, bad_name} -> filename:join("priv", NifName); Dir -> filename:join(Dir, NifName) end, ok = erlang:load_nif(NifFileName, 0). %% %% Startup & Configure %% %% @doc Open, or create a duckdb database with default options. %% open(Filename) -> open(Filename, #{}). %% @doc Open, or create a duckdb file %% % -spec open(, map()) -> {ok, database()} | {error, _}. open(_Filename, _Options) -> erlang:nif_error(nif_library_not_loaded). %% @doc Connect to the database. In the background a thread is started which %% is used by long running commands. Note: It is adviced to use the %% connection in a single process. %% -spec connect(database()) -> {ok, connection()} | {error, _}. connect(_Db) -> erlang:nif_error(nif_library_not_loaded). %% @doc Disconnect from the database. Stops the thread. %% The calling pid will receive: %% {disconnect, Ref, ok | {error, _}}. -spec disconnect(connection()) -> ok | {error, _}. disconnect(_Connection) -> erlang:nif_error(nif_library_not_loaded). %% @doc Close the database. All open connections will become unusable. -spec close(database()) -> ok | {error, _}. close(_Db) -> erlang:nif_error(nif_library_not_loaded). %% @doc Return a list with config flags, and explanation -spec config_flag_info() -> map(). config_flag_info() -> erlang:nif_error(nif_library_not_loaded). %% %% Query %% %% @doc Query the database. The answer the answer is returned immediately. %% Special care has been taken to prevent blocking the scheduler. A reference %% to a result data structure will be returned. -spec query(connection(), sql()) -> {ok, result()} | {error, _}. query(Conn, Sql) -> case query_cmd(Conn, Sql) of {ok, Ref} -> receive {educkdb, Ref, Answer} -> Answer end; {error, _}=E -> E end. %% @doc Query the database. The answer is send back as a result to %% the calling process. -spec query_cmd(connection(), sql()) -> {ok, reference()} | {error, _}. query_cmd(_Conn, _Sql) -> erlang:nif_error(nif_library_not_loaded). %% %% Prepared Statements %% %% @doc Compile, and prepare a statement for later execution. -spec prepare(connection(), sql()) -> {ok, prepared_statement()} | {error, _}. prepare(_Conn, _Sql) -> erlang:nif_error(nif_library_not_loaded). execute_prepared(PreparedStatement) -> case execute_prepared_cmd(PreparedStatement) of {ok, Ref} -> receive {educkdb, Ref, Answer} -> Answer end; {error, _}=E -> E end. -spec execute_prepared_cmd(prepared_statement()) -> bind_response(). execute_prepared_cmd(_Stmt) -> erlang:nif_error(nif_library_not_loaded). -spec bind_boolean(prepared_statement(), idx(), boolean()) -> bind_response(). bind_boolean(Statement, Index, true) -> bind_boolean_intern(Statement, Index, 1); bind_boolean(Statement, Index, false) -> bind_boolean_intern(Statement, Index, 0). -spec bind_boolean_intern(prepared_statement(), idx(), 0..1) -> bind_response(). bind_boolean_intern(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_int8(prepared_statement(), idx(), int8()) -> bind_response(). bind_int8(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_int16(prepared_statement(), idx(), int16()) -> bind_response(). bind_int16(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_int32(prepared_statement(), idx(), int32()) -> bind_response(). bind_int32(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_int64(prepared_statement(), idx(), int64()) -> bind_response(). bind_int64(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_uint8(prepared_statement(), idx(), uint8()) -> bind_response(). bind_uint8(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_uint16(prepared_statement(), idx(), uint16()) -> bind_response(). bind_uint16(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_uint32(prepared_statement(), idx(), uint32()) -> bind_response(). bind_uint32(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_uint64(prepared_statement(), idx(), uint64()) -> bind_response(). bind_uint64(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_float(prepared_statement(), idx(), float()) -> bind_response(). bind_float(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_double(prepared_statement(), idx(), float()) -> bind_response(). bind_double(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). %% @doc bind_date(Stmt, Index, {Y, M, D}=Date) when is_integer(Y) andalso is_integer(M) andalso is_integer(D) -> bind_date_intern(Stmt, Index, calendar:date_to_gregorian_days(Date)); bind_date(Stmt, Index, Days) when is_integer(Days) -> bind_date_intern(Stmt, Index, Days). -spec bind_date_intern(prepared_statement(), idx(), integer()) -> bind_response(). bind_date_intern(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). %% @doc bind_time(Stmt, Index, {H, M, S}) -> bind_time_intern(Stmt, Index, ?HOUR_TO_MICS(H) + ?MIN_TO_MICS(M) + floor(?SEC_TO_MICS(S))); bind_time(Stmt, Index, Micros) when is_integer(Micros) -> bind_time_intern(Stmt, Index, Micros). bind_time_intern(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). %% @doc bind_timestamp(Stmt, Index, {MegaSecs, Secs, MicroSecs}) -> bind_timestamp_intern(Stmt, Index, ?EPOCH_OFFSET + ?SEC_TO_MICS(MegaSecs * 1000000) + ?SEC_TO_MICS(Secs) + MicroSecs); bind_timestamp(Stmt, Index, {{_, _, _}=Date, {Hour, Minute, Second}}) -> Mics = ?SEC_TO_MICS(calendar:datetime_to_gregorian_seconds({Date, {Hour, Minute, 0}})), RemMics = floor(?SEC_TO_MICS(Second)), bind_timestamp_intern(Stmt, Index, Mics + RemMics); bind_timestamp(Stmt, Index, Micros) when is_integer(Micros) -> bind_timestamp_intern(Stmt, Index, Micros). bind_timestamp_intern(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). % @doc Bind a iolist as varchar. % Note: be really carefull, value must be valid utf8 data. -spec bind_varchar(prepared_statement(), idx(), iodata()) -> bind_response(). bind_varchar(_Stmt, _Index, _Value) -> erlang:nif_error(nif_library_not_loaded). -spec bind_null(prepared_statement(), idx()) -> bind_response(). bind_null(_Stmt, _Index) -> erlang:nif_error(nif_library_not_loaded). %% %% Results %% %% -spec get_chunks(result()) -> [data_chunk()]. get_chunks(_Result) -> erlang:nif_error(nif_library_not_loaded). -spec get_chunk(result(), uint64()) -> {ok, data_chunk()} | {error, _}. get_chunk(_Result, _ChunkIndex) -> erlang:nif_error(nif_library_not_loaded). -spec chunk_count(result()) -> uint64(). chunk_count(_Result) -> erlang:nif_error(nif_library_not_loaded). -spec column_names(result()) -> [binary()]. column_names(_Result) -> erlang:nif_error(nif_library_not_loaded). %% %% Chunks %% %-spec chunk_extract(data_chunk()) -> uint64(). extract_chunk(_Chunk) -> erlang:nif_error(nif_library_not_loaded). -spec get_chunk_column_count(data_chunk()) -> uint64(). get_chunk_column_count(_Chunk) -> erlang:nif_error(nif_library_not_loaded). -spec get_chunk_size(data_chunk()) -> uint64(). get_chunk_size(_Chunk) -> erlang:nif_error(nif_library_not_loaded). %% %% Appender Interface %% -spec appender_create(connection(), string(), string()) -> {ok, appender()} | {error, _}. appender_create(_Connection, _Schema, _Table) -> erlang:nif_error(nif_library_not_loaded). -spec append_boolean(appender(), boolean()) -> append_response(). append_boolean(Appender, true) -> append_boolean_intern(Appender, 1); append_boolean(Appender, false) -> append_boolean_intern(Appender, 0). -spec append_boolean_intern(appender(), int32()) -> append_response(). append_boolean_intern(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_int8(appender(), int8()) -> append_response(). append_int8(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_int16(appender(), int16()) -> append_response(). append_int16(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_int32(appender(), int32()) -> append_response(). append_int32(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_int64(appender(), integer()) -> append_response(). append_int64(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_uint8(appender(), uint8()) -> append_response(). append_uint8(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_uint16(appender(), uint16()) -> append_response(). append_uint16(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_uint32(appender(), uint32()) -> append_response(). append_uint32(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_uint64(appender(), uint64()) -> append_response(). append_uint64(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_float(appender(), float()) -> append_response(). append_float(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). -spec append_double(appender(), float()) -> append_response(). append_double(_Appender, _Integer) -> erlang:nif_error(nif_library_not_loaded). %% @doc append_time(Appender, {H, M, S}) -> append_time_intern(Appender, ?HOUR_TO_MICS(H) + ?MIN_TO_MICS(M) + floor(?SEC_TO_MICS(S))); append_time(Appender, Micros) when is_integer(Micros) -> append_time_intern(Appender, Micros). append_time_intern(_Appender, _Time) -> erlang:nif_error(nif_library_not_loaded). %% @doc append_date(Appender, {Y, M, D}=Date) when is_integer(Y) andalso is_integer(M) andalso is_integer(D) -> append_date_intern(Appender, calendar:date_to_gregorian_days(Date)); append_date(Appender, Days) when is_integer(Days) -> append_date_intern(Appender, Days). append_date_intern(_Appender, _Date) -> erlang:nif_error(nif_library_not_loaded). %% @doc append_timestamp(Appender, {MegaSecs, Secs, MicroSecs}) -> append_timestamp_intern(Appender, ?EPOCH_OFFSET + ?SEC_TO_MICS(MegaSecs * 1000000) + ?SEC_TO_MICS(Secs) + MicroSecs); append_timestamp(Appender, {{_, _, _}=Date, {Hour, Minute, Second}}) -> Millies = ?SEC_TO_MICS(calendar:datetime_to_gregorian_seconds({Date, {Hour, Minute, 0}})), RemMillies = floor(?SEC_TO_MICS(Second)), append_timestamp_intern(Appender, Millies + RemMillies); append_timestamp(Appender, Micros) when is_integer(Micros) -> append_timestamp_intern(Appender, Micros). append_timestamp_intern(_Appender, _Timestamp) -> erlang:nif_error(nif_library_not_loaded). -spec append_varchar(appender(), iodata()) -> append_response(). append_varchar(_Appender, _IOData) -> erlang:nif_error(nif_library_not_loaded). -spec append_null(appender()) -> append_response(). append_null(_Appender) -> erlang:nif_error(nif_library_not_loaded). -spec appender_flush(appender()) -> append_response(). appender_flush(_Appender) -> erlang:nif_error(nif_library_not_loaded). -spec appender_end_row(appender()) -> append_response(). appender_end_row(_Appender) -> erlang:nif_error(nif_library_not_loaded). %% %% Higher Level API %% %% @doc Extra extract_result(Result) -> extract_result1(Result, chunk_count(Result)). extract_result1(_Result, 0) -> {ok, []}; extract_result1(Result, N) when N > 0 -> case get_chunk(Result, 0) of {ok, Chunk} -> Names = column_names(Result), Columns = extract_chunk(Chunk), {ok, lists:zipwith(fun(Column, Name) -> Column#{ name => Name } end, Columns, Names)}; {error, _}=E -> E end. %% @doc Do a simple sql query without parameters, and retrieve the first data chunk. squery(Connection, Sql) -> case query(Connection, Sql) of {ok, Result} -> extract_result(Result); {error, _}=E -> E end. %% @doc Execute a prepared statement, and retrieve the first data chunk. execute(Stmt) -> case educkdb:execute_prepared(Stmt) of {ok, Result} -> educkdb:extract_result(Result); {error, _}=E ->E end.