src/educkdb.erl

%% @author Maas-Maarten Zeeman <mmzeeman@xs4all.nl>
%% @copyright 2022-2024 Maas-Maarten Zeeman
%%
%% @doc Low level erlang API for duckdb databases.
%% @end

%% Copyright 2022-2024 Maas-Maarten Zeeman <mmzeeman@xs4all.nl>
%%
%% 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.

-module(educkdb).
-author("Maas-Maarten Zeeman <mmzeeman@xs4all.nl>").


%% low-level exports
-export([
    open/1, open/2,
    close/1,
    config_flag_info/0,

    connect/1,
    disconnect/1,

    query/2,

    prepare/2,
    statement_type/1,
    execute_prepared/1,
    parameter_name/2,
    parameter_type/2,
    parameter_count/1,
    parameter_index/2,
    clear_bindings/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
    result_extract/1,
    fetch_chunk/1,
    get_chunks/1,
    column_names/1,

    %% Chunks
    chunk_column_count/1,
    chunk_column_types/1,
    chunk_columns/1,
    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
]).


%% Utilities
-export([
    uuid_binary_to_uuid_string/1,
    uuid_string_to_uuid_binary/1,

    hugeint_to_integer/1,
    integer_to_hugeint/1,

    uhugeint_to_integer/1,
    integer_to_uhugeint/1
]).

-include("educkdb.hrl").

-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() :: uint64(). % DuckDB index type

-type int8() :: -16#7F..16#7F. % DuckDB tinyint
-type uint8() :: 0..16#FF. % DuckDB duckdb utinyint

-type int16() :: -16#7FFF..16#7FFF. % DuckDB  shortint
-type uint16() :: 0..16#FFFF. % DuckDB ushortint

-type int32() :: -16#7FFFFFFF..16#7FFFFFFF. % DuckDB integer
-type uint32() :: 0..16#FFFFFFFF. % DuckDB uinteger

-type int64() :: -16#7FFFFFFFFFFFFFFF..16#7FFFFFFFFFFFFFFF. % DuckDB bigint
-type uint64() :: 0..16#FFFFFFFFFFFFFFFF.  % DuckDB ubigint

-type hugeint() :: #hugeint{}. % struct, making up a 128 bit signed integer.
-type uhugeint() :: #uhugeint{}. % struct, making up a 128 bit unsigned integer.

-type second() :: calendar:time() | float(). %% In the range 0.0..60.0
-type time() :: {calendar:hour(), calendar:minute(), second()}.
-type datetime() :: {calendar:date(), time()}.

-type data() :: boolean()
              | int8() | int16() | int32() | int64() 
              | uint8() | uint16() | uint32() | uint64()
              | float() | hugeint() 
              | calendar:date() | time() | datetime()
              | binary()
              | list(data())
              | #{binary() => data()}
              | {map, list(data()), list(data())}.

-type type_name() :: boolean
                   | tinyint | smallint | integer | bigint 
                   | utinyint | usmallint | uinteger | ubigint
                   | float | double | hugeint | uhugeint
                   | timestamp | date | time
                   | interval 
                   | varchar | blob
                   | decimal
                   | timestamp_s | timestamp_ms | timestamp_ns
                   | enum
                   | interval
                   | list | struct | map
                   | uuid | json.  %% Note: decimal, timestamp_s, timestamp_ms, timestamp_ns and interval's are not supported yet.

-type statement_type() :: invalid
                        | select | insert | update | explain | delete | prepare | create
                        | execute | alter | transaction | copy | analyze | variable_set
                        | create_func | drop | export | pragma | vacuum | call | set | load
                        | relation | extension | logical_plan | attach | detach | multi
                        | unknown.


-type named_column() :: #{ name := binary(), data := list(data()), type := type_name() }.

-type bind_response() :: ok | {error, _}.
-type append_response() :: ok | {error, _}.

-export_type([database/0, connection/0, prepared_statement/0, result/0, sql/0,
              type_name/0, statement_type/0,
              int8/0, int16/0, int32/0, int64/0,
              uint8/0, uint16/0, uint32/0, uint64/0,
              idx/0, hugeint/0, uhugeint/0, second/0, time/0, datetime/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.
-spec open(Filename) -> Result
    when Filename :: string(),
         Result :: {ok, database()} | {error, _}.
open(Filename) ->
    open(Filename, #{}).

%% @doc Open, or create a duckdb file
%%
-spec open(Filename, Options) -> Result
    when Filename :: string(),
         Options :: map(),
         Result :: {ok, database()} | {error, _}.
open(_Filename, _Options) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Connect to the database. 
%%      Note: It is adviced to use the connection in a single process.
%%
-spec connect(Database) -> Result
    when Database :: database(),
         Result :: {ok, connection()} | {error, _}.
connect(_Db) ->
    erlang:nif_error(nif_library_not_loaded).


%% @doc Disconnect from the database. 
-spec disconnect(Connection) -> Result
    when Connection :: connection(),
         Result :: ok | {error, _}.
disconnect(_Connection) ->
    erlang:nif_error(nif_library_not_loaded).
                                 
%% @doc Close the database. All open connections will become unusable.
-spec close(Database) -> Result
    when Database :: database(),
         Result :: ok | {error, _}.
close(_Db) ->
    erlang:nif_error(nif_library_not_loaded).
 

%% @doc Return a map with config flags, and explanations. The options
%%      can vary depending on the underlying DuckDB version used.
%%      For more information about DuckDB configuration options, see: 
%%      <a href="https://duckdb.org/docs/sql/configuration" target="_parent" rel="noopener">DuckDB Configuration</a>.
-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. 
-spec query(Connection, Sql) -> Result
    when Connection :: connection(),
         Sql :: sql(),
         Result :: {ok, result()} | {error, _}.
query(_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).

%% @doc Return the statement type of a prepared statement.
-spec statement_type(PreparedStatement) -> Result
    when PreparedStatement :: prepared_statement(),
         Result :: statement_type().
statement_type(_PreparedStatement) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Execute a prepared statement. The answer is returned.
-spec execute_prepared(PreparedStatement) -> Result
    when PreparedStatement :: prepared_statement(),
         Result :: {ok, result()} | {error, _}.
execute_prepared(_PreparedStatement) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Return the parameter name of the prepared statement at the specified index.
-spec parameter_name(PreparedStatement, Index) -> ParameterName
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         ParameterName :: binary().
parameter_name(_PreparedStatement, _Index) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Return the parameter type of the prepared statement at the specified index.
-spec parameter_type(PreparedStatement, Index) -> ParameterType
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         ParameterType :: type_name().
parameter_type(_PreparedStatement, _Index) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Return the number of parameters in a prepared statement.
-spec parameter_count(PreparedStatement) -> non_neg_integer()
    when PreparedStatement :: prepared_statement().
parameter_count(_PreparedStatement) ->
    erlang:nif_error(nif_library_not_loaded).

-spec parameter_index(PreparedStatement, Name) -> non_neg_integer()
    when PreparedStatement :: prepared_statement(),
         Name :: atom() | binary().
parameter_index(_PreparedStatement, _Name) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Return the parameter type of the prepared statement at the specified index.
-spec clear_bindings(PreparedStatement) -> ok | error 
    when PreparedStatement :: prepared_statement().
clear_bindings(_PreparedStatement) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind a boolean to the prepared statement at the specified index.
-spec bind_boolean(PreparedStatement, Index, Boolean) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(), 
         Boolean :: boolean(),
         BindResponse :: 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).


%% @doc Bind an int8 to the prepared statement at the specified index.
-spec bind_int8(PreparedStatement, Index, Int8) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Int8 :: int8(),
         BindResponse :: bind_response().
bind_int8(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an int16 to the prepared statement at the specified index.
-spec bind_int16(PreparedStatement, Index, Int16) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Int16 :: int16(),
         BindResponse :: bind_response().
bind_int16(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an int32 to the prepared statement at the specified index.
-spec bind_int32(PreparedStatement, Index, Int32) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Int32 :: int32(),
         BindResponse :: bind_response().
bind_int32(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an int64 to the prepared statement at the specified index.
-spec bind_int64(PreparedStatement, Index, Int64) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Int64 :: int64(),
         BindResponse :: bind_response().
bind_int64(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an uint8 to the prepared statement at the specified index.
-spec bind_uint8(PreparedStatement, Index, UInt8) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         UInt8 :: uint8(),
         BindResponse :: bind_response().
bind_uint8(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an uint8 to the prepared statement at the specified index.
-spec bind_uint16(PreparedStatement, Index, UInt16) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         UInt16 :: uint16(),
         BindResponse :: bind_response().
bind_uint16(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an uint32 to the prepared statement at the specified index.
-spec bind_uint32(PreparedStatement, Index, UInt32) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         UInt32 :: uint32(),
         BindResponse :: bind_response().
bind_uint32(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an uint64 to the prepared statement at the specified index.
-spec bind_uint64(PreparedStatement, Index, UInt64) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         UInt64 :: uint64(),
         BindResponse :: bind_response().
bind_uint64(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an float to the prepared statement at the specified index. Note: Erlang's
%%      float() datatype is actually a DuckDB double. When binding an Erlang float
%%      variable you will lose precision.
-spec bind_float(PreparedStatement, Index, Float) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Float :: float(),
         BindResponse :: bind_response().
bind_float(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind an uint64 to the prepared statement at the specified index. Note: Erlang's
%%      float datatype is a DuckDB double. Using this function allows you to keep the
%%      precision.
-spec bind_double(PreparedStatement, Index, Double) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Double :: float(),
         BindResponse :: bind_response().
bind_double(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Bind a date to the prepared statement at the specified index. The date can be 
%%      either given as a calendar:date() tuple, or an integer with the number of 
%%      days since the first of January in the year 0.
-spec bind_date(PreparedStatement, Index, Date) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Date :: calendar:date() | integer(),
         BindResponse :: bind_response().
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 a time to the prepared statement at the specified index. The time can be either
%%      given as an {hour, minute, second} tuple (similar to calendar:time()), or as an integer
%%      with the number of microseconds since midnight.
-spec bind_time(PreparedStatement, Index, Time) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         Time :: calendar:time() | time() | non_neg_integer(),
         BindResponse :: bind_response().
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 a timestamp to the prepared statement at the specified index. The timestamp
%%      can be either a datetime tuple, or an integer with the microseconds since 1-Jan in 
%%      the year 0.
-spec bind_timestamp(PreparedStatement, Index, TimeStamp) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         TimeStamp :: calendar:datetime() | datetime() | integer(),
         BindResponse :: bind_response().
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 to the prepared statement at the specified index. Note: This
%      function is meant to bind null terminated strings in the database. Not arbitrary
%      binary data.
-spec bind_varchar(PreparedStatement, Index, IOData) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         IOData :: iodata(),
         BindResponse :: bind_response().
bind_varchar(_Stmt, _Index, _Value) -> 
    erlang:nif_error(nif_library_not_loaded).

% @doc Bind a null value to the prepared statement at the specified index.
-spec bind_null(PreparedStatement, Index) -> BindResponse
    when PreparedStatement :: prepared_statement(),
         Index :: idx(),
         BindResponse :: bind_response().
bind_null(_Stmt, _Index) ->
    erlang:nif_error(nif_library_not_loaded).

%%
%% Results
%%

%% @doc Get all data chunks from a query result.
-spec get_chunks(QueryResult) -> DataChunks
    when QueryResult :: result(),
         DataChunks :: list(data_chunk()).
get_chunks(_Result) -> 
    erlang:nif_error(nif_library_not_loaded).
 
%% @doc Fetches a data chunk from a result. The function should be called
%% repeatedly until the result is exhausted.
-spec fetch_chunk(QueryResult) -> DataChunk | '$end'
    when QueryResult :: result(),
         DataChunk :: data_chunk().
fetch_chunk(_Result) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Get the column names from the query result.
-spec column_names(QueryResult) -> Names 
    when QueryResult :: result(),
         Names :: list(binary()).
column_names(_Result) -> 
    erlang:nif_error(nif_library_not_loaded).

%%
%% Chunks
%%

%% @doc Return the number of columns in the data chunk.
-spec chunk_column_count(DataChunk) -> ColumnCount
    when DataChunk :: data_chunk(),
         ColumnCount :: uint64().
chunk_column_count(_Chunk) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Get the column types of the chunk
%% [todo] spec..
chunk_column_types(_Chunk) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Get the columns data of the chunk
%% [todo] spec
chunk_columns(_Chunk) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Return the number of tuples in th data chunk.
-spec chunk_size(DataChunk) -> TupleCount
    when DataChunk :: data_chunk(),
         TupleCount :: uint64().
chunk_size(_Chunk) ->
    erlang:nif_error(nif_library_not_loaded).


%%
%% Appender Interface
%%

%% @doc Create an appender. Appenders allow for high speed bulk insertions into the database. See
%%      <a href="https://duckdb.org/docs/data/appender">DuckDB Appender</a> for more information.
-spec appender_create(Connection, Schema, Table) -> Result 
    when Connection :: connection(),
         Schema :: undefined | binary(),
         Table :: binary,
         Result :: {ok, appender()} | {error, _}.
appender_create(_Connection, _Schema, _Table) -> 
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a boolean value to the current location in the row.
-spec append_boolean(Appender, Boolean) -> AppendResponse
    when Appender :: appender(),
         Boolean :: boolean(),
         AppendResponse :: 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).

%% @doc Append a tinyint to the current location in the row.
-spec append_int8(Appender, TinyInt) -> AppendResponse
    when Appender :: appender(),
         TinyInt :: int8(),
         AppendResponse :: append_response().
append_int8(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a smallint to the current location in the row.
-spec append_int16(Appender, SmallInt) -> AppendResponse
    when Appender :: appender(),
         SmallInt :: int16(),
         AppendResponse :: append_response().
append_int16(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append an integer (32 bit) to the current location in the row.
-spec append_int32(Appender, Integer) -> AppendResponse
    when Appender :: appender(),
         Integer :: int32(),
         AppendResponse :: append_response().
append_int32(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a bigint to the current location in the row.
-spec append_int64(Appender, BigInt) -> AppendResponse
    when Appender :: appender(),
         BigInt :: int64(),
         AppendResponse :: append_response().
append_int64(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a utinyint to the current location in the row.
-spec append_uint8(Appender, UTinyInt) -> AppendResponse
    when Appender :: appender(),
         UTinyInt :: uint8(),
         AppendResponse :: append_response().
append_uint8(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a usmallint to the current location in the row.
-spec append_uint16(Appender, USmallInt) -> AppendResponse
    when Appender :: appender(),
         USmallInt :: uint16(),
         AppendResponse :: append_response().
append_uint16(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append an unsigned integer (32 bit) to the current location in the row.
-spec append_uint32(Appender, UInteger) -> AppendResponse
    when Appender :: appender(),
         UInteger :: uint32(),
         AppendResponse :: append_response().
append_uint32(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a ubigint to the current location in the row.
-spec append_uint64(Appender, UBigInt) -> AppendResponse
    when Appender :: appender(),
         UBigInt :: uint64(),
         AppendResponse :: append_response().
append_uint64(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a float to the current location in the row. Note: duckdb floats
%%      are different than erlang floats. When you add an erlang float to a 
%%      duckdb float column, you will loose precision.
-spec append_float(Appender, Float) -> AppendResponse
    when Appender :: appender(),
         Float :: float(),
         AppendResponse :: append_response().
append_float(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a double to the current location in the row. Note: duckdb double's 
%%      are the same as erlang floats. 
-spec append_double(Appender, Double) -> AppendResponse
    when Appender :: appender(),
         Double :: float(),
         AppendResponse :: append_response().
append_double(_Appender, _Integer) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a time to the current location in the row.
-spec append_time(Appender, Time) -> AppendResponse
    when Appender :: appender(),
         Time :: calendar:time() | time() | non_neg_integer(),
         AppendResponse :: append_response().
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 a data to the current location in the row.
-spec append_date(Appender, Date) -> AppendResponse
    when Appender :: appender(),
         Date :: calendar:date() | integer(),
         AppendResponse :: append_response().
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 a timestamp to the current location in the row.
-spec append_timestamp(Appender, Timestamp) -> AppendResponse
    when Appender :: appender(),
         Timestamp :: calendar:datetime() | erlang:timestamp() | datetime() | integer(),
         AppendResponse :: append_response().
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).

%% @doc Append a varchar to the current location in the row.
-spec append_varchar(Appender, IOData) -> AppendResponse
    when Appender :: appender(),
         IOData :: iodata(),
         AppendResponse :: append_response().
append_varchar(_Appender, _IOData) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Append a null value to the current location in the row.
-spec append_null(Appender) -> AppendResponse
    when Appender :: appender(),
         AppendResponse :: append_response().
append_null(_Appender) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Finalize the current row, and start a new one.
-spec appender_end_row(Appender) -> AppendResponse
    when Appender :: appender(),
         AppendResponse :: append_response().
appender_end_row(_Appender) ->
    erlang:nif_error(nif_library_not_loaded).

%% @doc Flush the appender to the table, forcing the cache of the appender to be cleared
%%      and the data to be appended to the base table.
-spec appender_flush(Appender) -> AppendResult
    when Appender :: appender(),
         AppendResult :: append_response().
appender_flush(_Appender) ->
    erlang:nif_error(nif_library_not_loaded).


%%
%% Higher Level API
%%

result_extract(Result) ->
    case fetch_chunk(Result) of
        '$end' ->
            {ok, [], []};
        Chunk ->
            Names = column_names(Result),
            Types = chunk_column_types(Chunk),
            Info = column_info(Names, Types),
            Rows = chunk_rows(Chunk, Result, []),
            {ok, Info, Rows}
    end.

column_info(Names, Types) ->
    column_info(Names, Types, []).

column_info([], _, Acc) ->
    lists:reverse(Acc);
column_info([N|T], undefined, Acc) ->
    column_info(T, undefined, [#column{name=N}|Acc]);
column_info([Name|RestNames], [Type|RestTypes], Acc) ->
    column_info(RestNames, RestTypes, [#column{name=Name, type=Type}|Acc]).

chunk_rows('$end', _Result, Rows) ->
    lists:reverse(lists:flatten(Rows));
chunk_rows(Chunk, Result, Rows) ->
    R = lists:reverse(
          lists:map(fun list_to_tuple/1,
                    transpose(
                      chunk_columns(Chunk)))),

    chunk_rows(fetch_chunk(Result), Result, [R | Rows]).


%% @doc Do a simple sql query without parameters, and retrieve the result from the
%%      first data chunk.
-spec squery(Connection, Sql) -> Result
    when Connection :: connection(),
         Sql :: sql(),
         Result :: {ok, [ named_column() ]} | {error, _}.
squery(Connection, Sql) ->
    case query(Connection, Sql) of
        {ok, Result} ->
            result_extract(Result);
        {error, _}=E ->
            E
    end.

%% @doc Execute a prepared statement, and retrieve the first result from the first
%%      data chunk.
-spec execute(PreparedStatement) -> Result
    when PreparedStatement :: prepared_statement(),
         Result :: {ok, [ named_column() ]} | {error, _}.
execute(Stmt) ->
    case execute_prepared(Stmt) of
        {ok, Result} ->
            result_extract(Result);
        {error, _}=E ->
            E
    end.

%%
%% Utilities
%% 

transpose([[]|_]) -> [];
transpose(M) ->
    [ lists:map(fun hd/1, M) | transpose(lists:map(fun tl/1, M))].

%% @doc Convert a duckdb hugeint record to an erlang integer. 
-spec hugeint_to_integer(Hugeint) -> Integer
    when Hugeint :: hugeint(),
         Integer :: integer().
hugeint_to_integer(#hugeint{upper=Upper, lower=Lower}) ->
    (Upper bsl 64) bor Lower.

%% @doc Convert an erlang integer to a DuckDB hugeint.
%%
%% For more information on DuckDB numeric types: See <a href="https://duckdb.org/docs/sql/data_types/numeric" target="_parent" rel="noopener">DuckDB Numeric Data Types</a>.
-spec integer_to_hugeint(Integer) -> Hugeint
    when Integer :: integer(),
         Hugeint :: hugeint().
integer_to_hugeint(Int) ->
    #hugeint{upper=(Int bsr 64), lower=(Int band 16#FFFFFFFFFFFFFFFF)}.

%% @doc Convert a duckdb uhugeint record to an erlang integer. 
-spec uhugeint_to_integer(UHugeint) -> Integer
    when UHugeint :: uhugeint(),
         Integer :: integer().
uhugeint_to_integer(#uhugeint{upper=Upper, lower=Lower}) ->
    (Upper bsl 64) bor Lower.

-spec integer_to_uhugeint(NonNegInteger) -> UHugeint
    when NonNegInteger :: non_neg_integer(),
         UHugeint :: uhugeint().
integer_to_uhugeint(Int) when Int >= 0  ->
    #uhugeint{upper=(Int bsr 64), lower=(Int band 16#FFFFFFFFFFFFFFFF)}.


%% @doc Convert a binary represented UUID to a printable hex representation.
uuid_binary_to_uuid_string(Bin) ->
    Formatted = io_lib:format("~8.16.0b-~4.16.0b-~4.16.0b-~2.16.0b~2.16.0b-~12.16.0b", uuid_unpack(Bin)),
    erlang:iolist_to_binary(Formatted).

%% @doc Convert a printable UUID to a binary representation.
uuid_string_to_uuid_binary(U)->
    uuid_string_to_uuid_binary1(U, <<>>).

uuid_string_to_uuid_binary1(<<>>, Acc) -> Acc;
uuid_string_to_uuid_binary1(<<$-, Rest/binary>>, Acc) ->
    uuid_string_to_uuid_binary1(Rest, Acc); 
uuid_string_to_uuid_binary1(<<A, B, Rest/binary>>, Acc) ->
    Int = list_to_integer([A,B], 16),
    uuid_string_to_uuid_binary1(Rest, <<Acc/binary, Int>>).

%uuid_pack(TL, TM, THV, CSHR, CSL, N) ->
%  <<UUID:128>> = <<TL:32, TM:16, THV:16, CSHR:8, CSL:8, N:48>>,
%  UUID.

uuid_unpack(<<TL:32, TM:16, THV:16, CSHR:8, CSL:8, N:48>>) ->
  [TL, TM, THV, CSHR, CSL, N].