-module(libsql_ffi).
-export([
open/1, open_remote/2, open_replica/3, open_synced_database/3, close/1,
changes/1, total_changes/1, last_insert_rowid/1, interrupt/1,
sync/1, replication_index/1,
exec/2, exec_batch/3, query/3, query_named/3,
prepare/2, exec_prepared/2, query_prepared/2, finalize/1,
coerce_value/1, coerce_blob/1, null/0
]).
-export([
open_nif/1, open_remote_nif/2, open_replica_nif/3, open_synced_database_nif/3, close_nif/1,
changes_nif/1, total_changes_nif/1, last_insert_rowid_nif/1, interrupt_nif/1,
sync_nif/1, replication_index_nif/1,
exec_nif/2, exec_batch_nif/3, query_nif/3, query_named_nif/3,
prepare_nif/2, exec_prepared_nif/2, query_prepared_nif/2, finalize_nif/1
]).
-on_load(init/0).
%% Configuration for precompiled NIF downloads
-define(GITHUB_REPO, "yourusername/libsql-gleam").
-define(NIF_VERSION, "0.1.0").
init() ->
%% Ensure inets is started for httpc
case application:ensure_all_started(inets) of
{ok, _} -> ok;
_ -> ok
end,
PrivDir = code:priv_dir(libsql),
LocalSo = filename:join(PrivDir, "libsql_nif"),
case erlang:load_nif(LocalSo, 0) of
ok -> ok;
{error, {load_failed, _}} -> load_precompiled_or_fail(PrivDir);
{error, {load, _}} -> load_precompiled_or_fail(PrivDir);
Error -> Error
end.
load_precompiled_or_fail(_PrivDir) ->
case load_precompiled() of
ok -> ok;
_ -> {error, "Failed to load NIF. Please build from source with: make build"}
end.
load_precompiled() ->
Platform = get_platform(),
CacheDir = filename:basedir(user_cache, "libsql"),
ok = filelib:ensure_dir(filename:join(CacheDir, ".")),
SoFile = filename:join(CacheDir, "libsql_nif-" ++ ?NIF_VERSION ++ "-" ++ Platform),
case filelib:is_file(SoFile) of
true -> erlang:load_nif(SoFile, 0);
false -> download_and_load(SoFile, Platform)
end.
download_and_load(SoFile, Platform) ->
AssetName = case Platform of
"linux-x86_64" -> "libsql_nif-linux-x86_64.so";
"macos-aarch64" -> "libsql_nif-macos-aarch64.dylib";
"macos-x86_64" -> "libsql_nif-macos-x86_64.dylib";
_ -> "unsupported"
end,
case AssetName of
"unsupported" -> {error, unsupported_platform};
_ ->
Url = "https://github.com/" ++ ?GITHUB_REPO ++ "/releases/download/v" ++ ?NIF_VERSION ++ "/" ++ AssetName,
case download(Url, SoFile) of
ok -> erlang:load_nif(SoFile, 0);
Error -> Error
end
end.
download(Url, Dest) ->
case httpc:request(get, {Url, []}, [{timeout, 30000}], [{body_format, binary}]) of
{ok, {{_, 200, _}, _, Body}} ->
ok = filelib:ensure_dir(Dest),
ok = file:write_file(Dest, Body),
%% Make executable on Unix
case os:type() of
{unix, _} -> ok = file:change_mode(Dest, 8#755);
_ -> ok
end,
ok;
{ok, {{_, Status, _}, _, _}} ->
{error, {http_error, Status}};
{error, Reason} ->
{error, Reason}
end.
get_platform() ->
Os = case os:type() of
{unix, linux} -> "linux";
{unix, darwin} -> "macos";
{win32, _} -> "windows";
_ -> "unknown"
end,
Arch = case erlang:system_info(machine) of
"x86_64" -> "x86_64";
"aarch64" -> "aarch64";
"arm64" -> "aarch64";
"i386" -> "x86_64";
"i686" -> "x86_64";
_ -> "unknown"
end,
Os ++ "-" ++ Arch.
%% NIF stubs
open_nif(_Name) -> erlang:nif_error(nif_library_not_loaded).
open_remote_nif(_Url, _Token) -> erlang:nif_error(nif_library_not_loaded).
open_replica_nif(_Path, _Url, _Token) -> erlang:nif_error(nif_library_not_loaded).
open_synced_database_nif(_Path, _Url, _Token) -> erlang:nif_error(nif_library_not_loaded).
close_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
changes_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
total_changes_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
last_insert_rowid_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
interrupt_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
sync_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
replication_index_nif(_Connection) -> erlang:nif_error(nif_library_not_loaded).
exec_nif(_Sql, _Connection) -> erlang:nif_error(nif_library_not_loaded).
exec_batch_nif(_Sql, _Batches, _Connection) -> erlang:nif_error(nif_library_not_loaded).
query_nif(_Sql, _Arguments, _Connection) -> erlang:nif_error(nif_library_not_loaded).
query_named_nif(_Sql, _Arguments, _Connection) -> erlang:nif_error(nif_library_not_loaded).
prepare_nif(_Sql, _Connection) -> erlang:nif_error(nif_library_not_loaded).
exec_prepared_nif(_Arguments, _Statement) -> erlang:nif_error(nif_library_not_loaded).
query_prepared_nif(_Arguments, _Statement) -> erlang:nif_error(nif_library_not_loaded).
finalize_nif(_Statement) -> erlang:nif_error(nif_library_not_loaded).
open(Name) ->
case open_nif(unicode:characters_to_binary(Name)) of
{ok, Connection} -> {ok, Connection};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
open_remote(Url, Token) ->
case open_remote_nif(unicode:characters_to_binary(Url), unicode:characters_to_binary(Token)) of
{ok, Connection} -> {ok, Connection};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
open_replica(Path, Url, Token) ->
case open_replica_nif(unicode:characters_to_binary(Path), unicode:characters_to_binary(Url), unicode:characters_to_binary(Token)) of
{ok, Connection} -> {ok, Connection};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
open_synced_database(Path, Url, Token) ->
case open_synced_database_nif(unicode:characters_to_binary(Path), unicode:characters_to_binary(Url), unicode:characters_to_binary(Token)) of
{ok, Connection} -> {ok, Connection};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
close(Connection) ->
case close_nif(Connection) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
changes(Connection) ->
case changes_nif(Connection) of
{ok, Count} -> {ok, Count};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
total_changes(Connection) ->
case total_changes_nif(Connection) of
{ok, Count} -> {ok, Count};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
last_insert_rowid(Connection) ->
case last_insert_rowid_nif(Connection) of
{ok, Id} -> {ok, Id};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
interrupt(Connection) ->
case interrupt_nif(Connection) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
sync(Connection) ->
case sync_nif(Connection) of
{ok, {FrameNo, FramesSynced}} -> {ok, {FrameNo, FramesSynced}};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
replication_index(Connection) ->
case replication_index_nif(Connection) of
{ok, Index} -> {ok, Index};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
query(Sql, Connection, Arguments) when is_binary(Sql) ->
case query_nif(Sql, Arguments, Connection) of
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}};
{ok, Rows} ->
{ok, lists:map(fun erlang:list_to_tuple/1, Rows)}
end.
query_named(Sql, Connection, Arguments) when is_binary(Sql) ->
case query_named_nif(Sql, Arguments, Connection) of
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}};
{ok, Rows} ->
{ok, lists:map(fun erlang:list_to_tuple/1, Rows)}
end.
exec(Sql, Connection) when is_binary(Sql) ->
case exec_nif(Sql, Connection) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
exec_batch(Sql, Connection, Batches) when is_binary(Sql) ->
case exec_batch_nif(Sql, Batches, Connection) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
prepare(Sql, Connection) when is_binary(Sql) ->
case prepare_nif(Sql, Connection) of
{ok, Statement} -> {ok, Statement};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
exec_prepared(Arguments, Statement) ->
case exec_prepared_nif(Arguments, Statement) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
query_prepared(Arguments, Statement) ->
case query_prepared_nif(Arguments, Statement) of
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}};
{ok, Rows} ->
{ok, lists:map(fun erlang:list_to_tuple/1, Rows)}
end.
finalize(Statement) ->
case finalize_nif(Statement) of
{ok, nil} -> {ok, nil};
{error, {libsql_error, Code, Message, Offset}} ->
{error, {libsql_error, libsql:error_code_from_int(Code), Message, Offset}}
end.
coerce_value(X) -> X.
coerce_blob(Bin) -> {blob, Bin}.
null() -> undefined.