%%%===================================================================
%%% Copyright (c) 2023 Mathieu Kerjouan
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%% 1. Redistributions of source code must retain the above copyright
%%% notice, this list of conditions and the following disclaimer.
%%%
%%% 2. Redistributions in binary form must reproduce the above
%%% copyright notice, this list of conditions and the following
%%% disclaimer in the documentation and/or other materials provided
%%% with the distribution.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
%%% CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
%%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
%%% DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
%%% BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
%%% EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
%%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
%%% DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
%%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
%%% TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
%%% THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
%%% SUCH DAMAGE.
%%%
%%% @copyright 2023 Mathieu Kerjouan
%%% @author Mathieu Kerjouan
%%%
%%% @doc Main interface to CozoDB Erlang NIF `cozo_nif' module.
%%% @end
%%%===================================================================
-module(cozo).
% API.
-export([open/0, open/1, open/2, open/3]).
-export([close/1]).
-export([run/2,run/3,run/4]).
-export([import_relations/2, export_relations/2]).
-export([backup/2, restore/2, import_backup/2]).
% manage relations.
-export([list_relations/1]).
-export([remove_relation/2, remove_relations/2]).
-export([create_relation/3, replace_relation/3]).
-export([put_row/3, update_row/3, remove_row/3]).
-export([ensure_row/3, ensure_not_row/3]).
% extra management functions.
-export([list_columns/2, list_indices/2]).
-export([explain/2, describe/3]).
-export([get_triggers/2]).
-export([set_access_level/3, set_access_levels/3]).
-export([get_running_queries/1, kill/2, compact/1]).
% helpers
-export([create_filepath/3]).
-export([json_decoder/0, json_encoder/0]).
% includes.
-include_lib("kernel/include/logger.hrl").
-include("cozo.hrl").
%%--------------------------------------------------------------------
%% @doc returns the default json encoder (thoas)
%% @end
%%--------------------------------------------------------------------
-spec json_encoder() -> atom().
json_encoder() -> application:get_env(cozo, json_parser, thoas).
%%--------------------------------------------------------------------
%% @doc returns the default json decoder (thoas)
%% @end
%%--------------------------------------------------------------------
-spec json_decoder() -> atom().
json_decoder() -> application:get_env(cozo, json_parser, thoas).
%%--------------------------------------------------------------------
%% @doc Open the database with mem engine, with random path and
%% default options.
%%
%% == Examples ==
%%
%% ```
%% rr(cozo)
%% {ok, {0, State}} = cozo:open().
%% #cozo{ id = 0
%% , db_engine = mem
%% , db_path = "/tmp/cozodb_Lq4yHM0RbGxbIwlyPBcNPyqPEj7O4msJ"
%% , db_options = #{}
%% , db_parent = <0.161>
%% } = State.
%% '''
%%
%% @see open/1
%% @see open/2
%% @see open/3
%% @end
%%--------------------------------------------------------------------
-spec open() -> Return when
Return :: {ok, {db_id(), #cozo{}}}
| {error, term()}.
open() ->
DefaultEngine = application:get_env(cozo, engine, mem),
open(DefaultEngine).
%%--------------------------------------------------------------------
%% @doc Open a new database with custom engine and random path. The
%% random path is set to `/tmp` by default and the filename is prefixed
%% by `cozodb_'.
%%
%% == Examples ==
%%
%% ```
%% rr(cozo).
%% {ok {0, #cozo{ db_path = Path }}} = open(mem).
%% "/tmp/cozodb_aPmybeM4XTudF5qJPnG5hJusV2Evq5au" = Path.
%%
%% {ok, {1, #cozo{ db_path = SqlitePath }}} = open(sqlite).
%% "/tmp/cozodb_TDE4dQKtiXHIUjWyNEaAo1f7LAxrYk4O" = SqlitePath.
%%
%% {ok, {1, #cozo{ db_path = RocksDbPath }}} = open(rocksdb).
%% "/tmp/cozodb_S8jAUccVnge0cKfci9FYvjrK1O6fAO1S" = RocksDbPath
%% '''
%%
%% @see open/2
%% @see open/3
%% @end
%%--------------------------------------------------------------------
-spec open(Engine) -> Return when
Engine :: db_engine(),
Return :: {ok, {db_id(), #cozo{}}}
| {error, term()}.
open(Engine) ->
DefaultPath = application:get_env(cozo, db_path, "/tmp"),
DefaultPrefix = application:get_env(cozo, db_filename_prefix, "cozodb_"),
DefaultLength = application:get_env(cozo, db_filename_random_length, 32),
Path = create_filepath(DefaultPath, DefaultPrefix, DefaultLength),
open(Engine, Path).
%%--------------------------------------------------------------------
%% @doc Open a database with a custom engine and path.
%%
%% == Examples ==
%%
%% ```
%% {ok, {0, State}} = cozo:open(sqlite, "/tmp/database.db").
%% '''
%%
%% @see open/3
%% @end
%%--------------------------------------------------------------------
-spec open(Engine, Path) -> Return when
Engine :: db_engine(),
Path :: db_path(),
Return :: {ok, {db_id(), #cozo{}}}
| {error, term()}.
open(Engine, Path) ->
Options = case Engine of
sqlite ->
application:get_env(cozo, sqlite_options, #{});
rocksdb ->
application:get_env(cozo, rocksdb_options, #{});
_ -> #{}
end,
open(Engine, Path, Options).
%%--------------------------------------------------------------------
%% @doc Open a new databse with custom path and options.
%%
%% == Examples ==
%%
%% ```
%% {ok, {0, State}} = cozo:open(rocksdb, "/tmp/rocks.db", #{}).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec open(Engine, Path, DbOptions) -> Return when
Engine :: db_engine(),
Path :: db_path(),
DbOptions :: db_options(),
Return :: {ok, {db_id(), #cozo{}}}
| {error, term()}.
open(Engine, Path, DbOptions) ->
open1(Engine, Path, DbOptions, #cozo{}).
%%--------------------------------------------------------------------
%% @hidden
%% @doc validate supported engine.
%% @end
%%--------------------------------------------------------------------
open1(mem, Path, DbOptions, State) ->
NewState = State#cozo{ db_engine = mem },
open2("mem\n", Path, DbOptions, NewState);
open1(sqlite, Path, DbOptions, State) ->
NewState = State#cozo{ db_engine = sqlite },
open2("sqlite\n", Path, DbOptions, NewState);
open1(rocksdb, Path, DbOptions, State) ->
NewState = State#cozo{ db_engine = rocksdb },
open2("rocksdb\n", Path, DbOptions, NewState);
open1(Engine, _, _, _) ->
Reason = {bad_engine, Engine},
?LOG_ERROR("~p~n", [{cozo, open1, Reason}]),
{error, Reason}.
%%--------------------------------------------------------------------
%% @hidden
%% @doc validate path.
%% @todo check if the path exist (or not).
%% @end
%%--------------------------------------------------------------------
open2(Engine, Path, DbOptions, State)
when is_list(Path) ->
DirName = filename:dirname(Path),
case filelib:ensure_dir(DirName) of
ok ->
NewState = State#cozo{ db_path = Path },
open3(Engine, Path ++ "\n", DbOptions, NewState);
Elsewise ->
Elsewise
end;
open2(_, Path, _, _) ->
Reason = {bad_path, Path},
?LOG_ERROR("~p~n", [{cozo, open2, Reason}]),
{error, Reason}.
%%--------------------------------------------------------------------
%% @hidden
%% @doc Check DbOptions and convert it to json.
%% @end
%%--------------------------------------------------------------------
open3(Engine, Path, DbOptions, State)
when is_map(DbOptions) ->
try
Encoder = json_encoder(),
Json = Encoder:encode(DbOptions),
EncodedOptions = binary_to_list(Json) ++ "\n",
NewState = State#cozo{ db_options = DbOptions },
open_nif(Engine, Path, EncodedOptions, NewState)
catch
_Error:Reason ->
{error, Reason}
end;
open3(_, _, DbOptions, _) ->
Reason = {bad_options, DbOptions},
?LOG_ERROR("~p~n", [{cozo, open3, Reason}]),
{error, Reason}.
%%--------------------------------------------------------------------
%% @hidden
%% @doc Open the nif and return db state.
%% @end
%%--------------------------------------------------------------------
open_nif(Engine, Path, DbOptions, State) ->
case cozo_nif:open_db(Engine, Path, DbOptions) of
{ok, DbId} ->
NewState = State#cozo{ id = DbId
, db_parent = self()
},
?LOG_DEBUG("~p", [{cozo, open_nif, NewState}]),
{ok, {DbId, NewState}};
Elsewise ->
?LOG_ERROR("~p", [{cozo, open_nif, Elsewise}]),
Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Close an opened database.
%%
%% == Examples ==
%%
%% ```
%% ok = cozo:close(0).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec close(Db) -> Return when
Db :: db_id(),
Return :: ok | {error, term()}.
close(Db)
when is_integer(Db) ->
?LOG_DEBUG("~p", [{cozo, close, [Db]}]),
case cozo_nif:close_db(Db) of
ok -> ok;
{error, Error} -> {error, Error}
end.
%%--------------------------------------------------------------------
%% @doc run a query using cozoscript on defined db and custom
%% query. No parameters are passed and immutability is set to true.
%%
%% == Examples ==
%%
%% ```
%% {ok, #{ <<"headers">> => [<<"_0">>,<<"_1">>,<<"_2">>]
%% <<"next">> => null,
%% <<"ok">> => true,
%% <<"rows">> => [[1,2,3]],
%% <<"took">> => 2.40886e-4}
%% } = cozo:run(0, "?[] <- [[1, 2, 3]]").
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec run(Db, Query) -> Return when
Db :: db_id(),
Query :: db_query(),
Return :: query_return().
run(Db, Query) ->
run(Db, Query, #{}, true).
%%--------------------------------------------------------------------
%% @doc run a query using cozoscript on defined db with custom
%% params. Immutability is set to true.
%%
%% == Examples ==
%%
%% ```
%% {ok, #{ <<"headers">> => [<<"_0">>,<<"_1">>,<<"_2">>]
%% <<"next">> => null,
%% <<"ok">> => true,
%% <<"rows">> => [[1,2,3]],
%% <<"took">> => 2.40886e-4}
%% } = cozo:run(0, "?[] <- [[1, 2, 3]]", #{}).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec run(Db, Query, Params) -> Return when
Db :: db_id(),
Query :: db_query(),
Params :: query_params(),
Return :: query_return().
run(Db, Query, Params) ->
run(Db, Query, Params, true).
%%--------------------------------------------------------------------
%% @doc Run a query using cozoscript.
%%
%% == Examples ==
%%
%% ```
%% {ok, #{ <<"headers">> => [<<"_0">>,<<"_1">>,<<"_2">>]
%% <<"next">> => null,
%% <<"ok">> => true,
%% <<"rows">> => [[1,2,3]],
%% <<"took">> => 2.40886e-4}
%% } = cozo:run(0, "?[] <- [[1, 2, 3]]", #{ limit => 10 }, falsex).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec run(Db, Query, Params, Mutable) -> Return when
Db :: db_id(),
Query :: db_query(),
Params :: query_params(),
Mutable :: query_mutable(),
Return :: query_return().
run(Db, Query, Params, Mutable)
when is_binary(Query) ->
run(Db, binary_to_list(Query), Params, Mutable);
run(_Db, "", _Params, _Mutable) ->
{error, empty_query};
run(Db, [X|_] = Query, Params, Mutable)
when is_list(X) ->
NewQuery = string:join(Query, "\n"),
run(Db, NewQuery, Params, Mutable);
run(Db, Query, Params, Mutable)
when is_integer(Db) andalso is_list(Query) andalso
is_map(Params) andalso is_boolean(Mutable) ->
run1(Db, Query, Params, Mutable);
run(_Db, _Query, _Params, _Mutable) ->
{error, badarg}.
%%--------------------------------------------------------------------
%%
%%--------------------------------------------------------------------
run1(Db, Query, Params, Mutable) ->
NewQuery = Query ++ "\n",
Encoder = json_encoder(),
NewParams = binary_to_list(Encoder:encode(Params)) ++ "\n",
case {NewQuery, NewParams, Mutable} of
{"\n", "\n", _} ->
{error, "no query and no params"};
{NewQuery, NewParams, true} ->
run_query_parser(Db, NewQuery, NewParams, 0);
{NewQuery, NewParams, false} ->
run_query_parser(Db, NewQuery, NewParams, 1)
end.
%%--------------------------------------------------------------------
%% @doc Import relations as json.
%%
%% see https://docs.cozodb.org/en/latest/nonscript.html#API.import_relations
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:import_relations(Db, #{}).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec import_relations(Db, Json) -> Return when
Db :: pos_integer(),
Json :: map() | list(),
Return :: {ok, map()}
| {error, term()}.
import_relations(Db, Json)
when is_integer(Db) andalso is_map(Json) orelse is_list(Json) ->
Encoder = json_encoder(),
case Encoder:encode(Json) of
{ok, EncodedJson} ->
Payload = binary_to_list(EncodedJson) ++ "\n",
import_relations1(Db, Payload);
Elsewise -> Elsewise
end.
import_relations1(Db, Json) ->
case cozo_nif:import_relations_db(Db, Json) of
{ok, Result} ->
decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Export database relationships from json as map.
%%
%% see https://docs.cozodb.org/en/latest/nonscript.html#API.export_relations
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:export_relations(Db, #{}).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec export_relations(Db, Json) -> Return when
Db :: pos_integer(),
Json :: map() | list(),
Return :: {ok, map()}
| {error, term()}.
export_relations(Db, Json)
when is_integer(Db) andalso is_map(Json) orelse is_list(Json) ->
Encoder = json_encoder(),
case Encoder:encode(Json) of
{ok, EncodedJson} ->
AsList = binary_to_list(EncodedJson) ++ "\n",
export_relations1(Db, AsList);
Elsewise -> Elsewise
end.
export_relations1(Db, Json) ->
case cozo_nif:export_relations_db(Db, Json) of
{ok, Result} ->
decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Backup a database to a file.
%%
%% see https://docs.cozodb.org/en/latest/nonscript.html#API.backup
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:backup(Db, "/tmp/backup.db").
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec backup(Db, OutPath) -> Return when
Db :: pos_integer(),
OutPath :: string(),
Return :: {ok, map()}
| {error, term()}.
backup(Db, OutPath)
when is_integer(Db) andalso is_list(OutPath) ->
case cozo_nif:backup_db(Db, OutPath ++ "\n") of
{ok, Result} ->
decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Restore a database based from a backup path.
%%
%% see https://docs.cozodb.org/en/latest/nonscript.html#API.restore
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:restore(Db, "/tmp/backup.db").
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec restore(Db, InPath) -> Return when
Db :: pos_integer(),
InPath :: string(),
Return :: {ok, map()}
| {error, term()}.
restore(Db, InPath)
when is_integer(Db) andalso is_list(InPath) ->
case cozo_nif:restore_db(Db, InPath ++ "\n") of
{ok, Result} ->
decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Import a database backup from json like object as map.
%%
%% see https://docs.cozodb.org/en/latest/nonscript.html#API.import_from_backup
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:import_backup(Db, #{}).
%% '''
%%
%%
%% @end
%%--------------------------------------------------------------------
-spec import_backup(Db, Json) -> Return when
Db :: pos_integer(),
Json :: map(),
Return :: {ok, map()}
| {error, term()}.
import_backup(Db, Json)
when is_integer(Db) andalso is_map(Json) ->
Encoder = json_encoder(),
case Encoder:encode(Json) of
{ok, EncodedJson} ->
Payload = binary_to_list(EncodedJson) ++ "\n",
import_backup1(Db, Payload);
Elsewise -> Elsewise
end.
import_backup1(Db, Json) ->
case cozo_nif:import_backup_db(Db, Json) of
{ok, Result} ->
decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @doc Unstable interface. Returns the list of relations.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:list_relations(Db).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec list_relations(Db) -> Return when
Db :: db_id(),
Return :: query_return().
list_relations(Db) ->
run(Db, "::relations").
%%--------------------------------------------------------------------
%% @doc Unstable interface. Explain a query.
%%
%% see https://docs.cozodb.org/en/latest/sysops.html#explain
%%
%% == Examples ==
%%
%% ```
%% Query = "?[] <- [['hello', 'world', 'Cozo!']]",
%% {ok, _} = (Db, Query).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec explain(Db, Query) -> Return when
Db :: db_id(),
Query :: string(),
Return :: query_return().
explain(Db, Query) ->
Command = string:join(["::explain", "{", Query, "}"], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. List all columns for the stored relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:list_columns(Db, Column).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec list_columns(Db, Name) -> Return when
Db :: db_id(),
Name :: string(),
Return :: query_return().
list_columns(Db, Name) ->
Command = string:join(["::columns", Name], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. List all indices for the stored relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:list_indices(Db, Indice).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec list_indices(Db, Name) -> Return when
Db :: db_id(),
Name :: string(),
Return :: query_return().
list_indices(Db, Name) ->
Command = string:join(["::indices", Name], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Describe the stored relation and store it
%% in the metadata.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:describe(Db, Relation).
%% '''
%%
%% @see run/4
%% @end
%%--------------------------------------------------------------------
-spec describe(Db, Name, Description) -> Return when
Db :: db_id(),
Name :: string(),
Description :: string(),
Return :: query_return().
describe(Db, Name, Description) ->
Command = string:join(["::describe", Name, Description ++ "?"], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Remove a stored relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:remove_relations(Db, Relation).
%% '''
%%
%% @see run/4
%% @end
%%--------------------------------------------------------------------
-spec remove_relation(Db, Name) -> Return when
Db :: db_id(),
Name :: string(),
Return :: query_return().
remove_relation(Db, Name) ->
Command = string:join(["::remove", Name], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Remove a stored relations.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:remove_relations(Db, [R1, R2, R3]).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec remove_relations(Db, Names) -> Return when
Db :: db_id(),
Names :: [string(), ...],
Return :: query_return().
remove_relations(Db, Names) ->
Relations = string:join(Names, ","),
run(Db, Relations).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Display triggers.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:get_triggers(Db, Trigger).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec get_triggers(Db, Name) -> Return when
Db :: db_id(),
Name :: string(),
Return :: query_return().
get_triggers(Db, Name) ->
Command = string:join(["::show_triggers", Name], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:set_access_level(Db, Level, Name).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec set_access_level(Db, Level, Name) -> Return when
Db :: db_id(),
Level :: string(),
Name :: string(),
Return :: query_return().
set_access_level(Db, Level, Name) ->
Command = string:join(["::access_level", Level, Name], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:se_access_levelss(Db, Level, [N1, N2, N3]).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec set_access_levels(Db, Level, Names) -> Return when
Db :: db_id(),
Level :: string(),
Names :: [string(), ...],
Return :: query_return().
set_access_levels(Db, Level, Names) ->
Relations = string:join(Names, ","),
set_access_level(Db, Level, Relations).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Display running queries and their id.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:get_running_queries(Db).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec get_running_queries(Db) -> Return when
Db :: db_id(),
Return :: query_return().
get_running_queries(Db) ->
run(Db, "::running").
%%--------------------------------------------------------------------
%% @doc Unstable interface. Kill a running query specified by id.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:kill(Db, Id).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec kill(Db, Id) -> Return when
Db :: db_id(),
Id :: string(),
Return :: query_return().
kill(Db, Id) ->
Command = string:join(["::kill", Id], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Instructs Cozo to run a compaction job.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:compact(Db).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec compact(Db) -> Return when
Db :: db_id(),
Return :: query_return().
compact(Db) ->
run(Db, "::compact").
%%--------------------------------------------------------------------
%% @doc Unstable interface. Create a stored relation with the given
%% name and spec. No stored relation with the same name can exist
%% beforehand.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:create_relation(Db, "stored1", "c1").
%% % :create stored1 {c1}
%%
%% {ok, _} = cozo:create_relation(Db, stored2, c1).
%% % :create stored2 {c1}
%%
%% {ok, _} = cozo:create_relation(Db, stored3, ["c1","c2","c3"]).
%% % :create stored3 {c1,c2,c3}
%%
%% {ok, _} = cozo:create_relation(Db, stored4, [c1,c2,c3]).
%% % :create stored4 {c1,c2,c3}
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec create_relation(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string() | atom(),
Spec :: string() | atom()
| [string(), ...]
| [atom(), ...],
Return :: query_return().
create_relation(Db, Name, Spec)
when is_atom(Name) ->
create_relation(Db, atom_to_list(Name), Spec);
create_relation(Db, Name, Spec)
when is_atom(Spec) ->
create_relation(Db, Name, atom_to_list(Spec));
create_relation(Db, Name, [X|_] = Specs)
when is_atom(X) orelse is_list(X) ->
FullSpec = [ to_list(Item) || Item <- Specs ],
Relations = string:join(FullSpec, ","),
create_relation(Db, Name, Relations);
create_relation(Db, Name, Spec) ->
RawCommand = [":create", Name, "{", Spec, "}"],
Command = string:join(RawCommand, " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @hidden
%% @doc helper to convert string or atom to string
%% @end
%%--------------------------------------------------------------------
-spec to_list(string() | atom()) -> string().
to_list(List) when is_list(List) -> List;
to_list(Atom) when is_atom(Atom) -> atom_to_list(Atom).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Similar to `:create', except that if the
%% named stored relation exists beforehand, it is completely replaced.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:replace_relation(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec replace_relation(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
replace_relation(Db, Name, Spec) ->
RawCommand = [":replace", Name, "{", Spec, "}"],
Command = string:join(RawCommand, " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Put rows from the resulting relation into
%% the named stored relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:put_row(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec put_row(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
put_row(Db, Name, Spec) ->
Command = string:join([":put", Name, Spec], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Update rows in the named stored relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:update_row(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec update_row(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
update_row(Db, Name, Spec) ->
Command = string:join([":update", Name, Spec], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Remove rows from the named stored
%% relation.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:remove_row(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec remove_row(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
remove_row(Db, Name, Spec) ->
Command = string:join([":rm", Name, Spec], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Ensure that rows specified by the output
%% relation and spec exist in the database, and that no other process
%% has written to these rows when the enclosing transaction
%% commits. Useful for ensuring read-write consistency.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:ensure_row(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec ensure_row(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
ensure_row(Db, Name, Spec) ->
Command = string:join([":ensure", Name, Spec], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @doc Unstable interface. Ensure that rows specified by the output
%% relation and spec do not exist in the database and that no other
%% process has written to these rows when the enclosing transaction
%% commits.
%%
%% == Examples ==
%%
%% ```
%% {ok, _} = cozo:ensure_not_row(Db, Name, Spec).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec ensure_not_row(Db, Name, Spec) -> Return when
Db :: db_id(),
Name :: string(),
Spec :: string(),
Return :: query_return().
ensure_not_row(Db, Name, Spec) ->
Command = string:join([":ensure_not", Name, Spec], " "),
run(Db, Command).
%%--------------------------------------------------------------------
%% @hidden
%% @doc return the result in decoded json.
%% @end
%%--------------------------------------------------------------------
-spec run_query_parser(Db, Query, Params, Mutability) -> Return when
Db :: db_id(),
Query :: string(),
Params :: string(),
Mutability :: 0 | 1,
Return :: {ok, map()}
| {error, any()}.
run_query_parser(Db, Query, Params, Mutability) ->
case cozo_nif:run_query(Db, Query, Params, Mutability) of
{ok, Result} -> decode_json(Result);
Elsewise -> Elsewise
end.
%%--------------------------------------------------------------------
%% @hidden
%% @doc wrapper around JSON decoder, by default using `thoas'.
%% @end
%%--------------------------------------------------------------------
-spec decode_json(Message) -> Return when
Message :: string() | binary() | bitstring(),
Return :: {ok, map()}
| {error, term()}.
decode_json(Message) ->
Decoder = json_decoder(),
try Decoder:decode(Message) of
{ok, Decoded} -> {ok, Decoded};
{error, Error} -> {error, {Error, Message}};
Elsewise -> Elsewise
catch
error:_Reason -> {error, Message}
end.
%%--------------------------------------------------------------------
%% @hidden
%% @doc generate a random filename
%%
%% == Examples ==
%%
%% ```
%% "/tmp/prefix_PFin0sRLFiHPt9LU46w1Ei00bvp3b1hv"
%% = cozo:create_filepath("/tmp", "prefix_", 32).
%% '''
%%
%% @end
%%--------------------------------------------------------------------
-spec create_filepath(Path, Prefix, Length) -> Return when
Path :: string(),
Prefix :: string(),
Length :: pos_integer(),
Return :: string().
create_filepath(Path, Prefix, Length) ->
Alphabet =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789",
AlphabetLength = length(Alphabet),
RandomString = crypto:strong_rand_bytes(Length),
RandomName = [ lists:nth((X rem AlphabetLength)+1, Alphabet)
|| <<X>> <= RandomString ],
PrefixName = Prefix ++ RandomName,
filename:join([Path, PrefixName]).