1:- module(docstore, [
    2    ds_open/1,           % +File
    3    ds_close/0,
    4    ds_snapshot/1,       % +File
    5    ds_snapshot/0,
    6    ds_hook/3,           % +Col, +Action, :Goal
    7    ds_insert/1,         % +Dict
    8    ds_insert/2,         % +Dict, -Id
    9    ds_insert/3,         % +Col, +Dict, -Id
   10    ds_update/1,         % +Dict
   11    ds_update/2,         % +Id, +Dict
   12    ds_upsert/1,         % +Dict
   13    ds_upsert/2,         % +Dict, -Id
   14    ds_upsert/3,         % +Col, +Dict, -Id
   15    ds_move/3,           % +Col, +Id, +Col
   16    ds_col_get/3,        % +Col, +Id, -Dict
   17    ds_col_get/4,        % +Col, +Id, +Keys, -Dict
   18    ds_all/2,            % +Col, -List
   19    ds_all/3,            % +Col, +Keys, -List
   20    ds_all_ids/2,        % +Col, -List
   21    ds_find/3,           % +Col, +Cond, -List
   22    ds_find/4,           % +Col, +Cond, +Keys, -List
   23    ds_collection/2,     % ?Id, ?Col
   24    ds_col_remove/2,     % +Col, +Id
   25    ds_col_remove_cond/2,% +Col, Cond
   26    ds_remove_col/1,     % +Col
   27    ds_remove_key/2,     % +Id, +Key
   28    ds_tuples/3,         % +Col, +Keys, -Values
   29    ds_col_add_key/3,    % +Col, +Key, +Default
   30    ds_col_remove_key/2, % +Col, +Key
   31    ds_col_rename/2,     % +Col, +ColNew
   32    ds_col_rename_key/3, % +Col, +Key, +KeyNew
   33    ds_transactional/1,  % +Goal
   34    ds_uuid/1,           % -Uuid
   35    ds_id/2,             % +Doc, -Id
   36    ds_set_id/3          % +In, +Id, -Out
   37]).

Document-oriented database

Generic thread-safe in-memory transactional store for dict terms. */

   45:- use_module(library(apply)).   46:- use_module(library(random)).   47:- use_module(library(error)).   48:- use_module(library(debug)).   49
   50:- dynamic(col/2).   51:- dynamic(eav/3).   52:- dynamic(file/2).   53
   54:- dynamic(hook/3).
 ds_open(+File) is det
Opens the database file. Throws error(docstore_is_open) when the database is already open.
   62ds_open(File):-
   63    safely(ds_open_unsafe(File)).
   64
   65ds_open_unsafe(File):-
   66    debug(docstore, 'opening database ~p', [File]),
   67    (   file(_, _)
   68    ->  throw(error(docstore_is_open))
   69    ;   must_be(atom, File),
   70        catch(loadall(File), Error, clean),
   71        (   nonvar(Error)
   72        ->  throw(Error)
   73        ;   true),
   74        open(File, append, Stream, [
   75            encoding('utf8'), lock(write)
   76        ]),
   77        assertz(file(File, Stream))).
   78
   79% Cleans current database state.
   80% This is run on ds_close and during
   81% loading when loading fails.
   82
   83clean:-
   84    debug(docstore, 'cleaning database', []),
   85    retractall(col(_, _)),
   86    retractall(eav(_, _, _)),
   87    retractall(file(_, _)).
 ds_close is det
Closes the database. Removes in-memory data. Runs close hooks. Hooks are ran before the file is closed and data is purged from memory. Throws error(database_is_not_open) when the database file is not open.
   97ds_close:-
   98    safely(close_unsafe).
   99
  100close_unsafe:-
  101    (   file(_, Stream)
  102    ->  true
  103    ;   throw(error(database_is_not_open))),
  104    close(Stream),
  105    clean,
  106    debug(docstore, 'database is closed', []).
  107
  108:- dynamic(load_tx_begin/0).  109
  110% Loads database contents from
  111% the given file if it exists.
  112
  113loadall(File):-
  114    exists_file(File), !,
  115    debug(docstore, 'loading from file ~p', [File]),
  116    retractall(load_tx_begin),
  117    setup_call_cleanup(
  118        open(File, read, Stream, [encoding('utf8')]),
  119        load(Stream),
  120        close(Stream)).
  121
  122loadall(_).
  123
  124% Loads database actions (assertz and retractall)
  125% from the stream until the end of the stream is
  126% reached.
  127
  128load(Stream):-
  129    read_term(Stream, Term, [
  130        dotlists(true)
  131    ]),
  132    (Term = end_of_file
  133    ->  (   load_tx_begin
  134        ->  retractall(col(_, _)),
  135            retractall(eav(_, _, _)),
  136            throw(error(failed_transaction))
  137        ;   true)
  138    ;   load_term(Term),
  139        load(Stream)).
  140
  141load_term(begin):-
  142    (   load_tx_begin
  143    ->  throw(error(double_begin))
  144    ;   assertz(load_tx_begin)).
  145
  146load_term(end):-
  147    (   load_tx_begin
  148    ->  retractall(load_tx_begin)
  149    ;   throw(error(end_without_begin))).
  150
  151load_term(assertz(Term)):-
  152    (   load_tx_begin
  153    ->  assertz(Term)
  154    ;   throw(error(no_tx_begin))).
  155
  156load_term(retractall(Term)):-
  157    (   load_tx_begin
  158    ->  retractall(Term)
  159    ;   throw(error(no_tx_begin))).
  160
  161load_term(Term):-
  162    throw(error(unknown_term(Term))).
 ds_snapshot(+File) is det
Writes the current database snapshot into the file.
  169ds_snapshot(File):-
  170    safely(ds_snapshot_unsafe(File)).
  171
  172ds_snapshot_unsafe(File):-
  173    setup_call_cleanup(
  174        open(File, write, Stream, [encoding('utf8')]),
  175        snapshot_dump(Stream),
  176        close(Stream)).
  177
  178snapshot_dump(Stream):-
  179    write_goal(Stream, begin),
  180    ((  col(Col, Id),
  181        write_goal(Stream, assertz(col(Col, Id))),
  182        fail
  183    ) ; true),
  184    ((  eav(Id, Name, Value),
  185        write_goal(Stream, assertz(eav(Id, Name, Value))),
  186        fail
  187    ) ; true),
  188    write_goal(Stream, end).
 ds_snapshot is det
Writes the snapshot of current database contents into its file. Implemented by running ds_snapshot/1 into a file (with a random name) and renaming the file using rename_file/2.
  199ds_snapshot:-
  200    safely(ds_snapshot_unsafe).
  201
  202ds_snapshot_unsafe:-
  203    file(Current, CurStream),
  204    file_directory_name(Current, Dir),
  205    ds_uuid(Name),
  206    atomic_list_concat([Dir, Name], /, New),
  207    ds_snapshot(New),
  208    retractall(file(_, _)),
  209    close(CurStream),
  210    rename_file(New, Current),
  211    open(Current, append, NewStream, [
  212        encoding('utf8'), lock(write)
  213    ]),
  214    assertz(file(Current, NewStream)).
  215
  216:- meta_predicate(ds_hook(+, +, :)).
 ds_hook(+Col, +Action, :Goal) is det
Adds new save/remove hook. Action is one of: before_save, before_remove. before_save hooks are executed before insert and update. before_remove hooks are executed before the document removal. During update only the updated fields are passed to the before_save hooks. Hooks are run in the current transaction. Hooks that fail or throw exception will end the transaction and discard changes.
  230ds_hook(Col, Action, Goal):-
  231    (   hook(Col, Action, Goal)
  232    ->  true
  233    ;   assertz(hook(Col, Action, Goal))).
 ds_insert(Doc) is det
Same as ds_insert/2 but the generated ID is ignored.
  240ds_insert(Doc):-
  241    ds_insert(Doc, _).
 ds_insert(+Doc, -Id) is det
Same as ds_insert/3 but collection name is taken from dict tag.
  248ds_insert(Doc, Id):-
  249    must_be(dict, Doc),
  250    is_dict(Doc, Col),
  251    ds_insert(Col, Doc, Id).
 ds_insert(+Col, +Doc, -Id) is det
Inserts new document into the given collection. Gives back the generated ID. Document must be a dict. All values in the dict must be ground. Throws error(doc_has_id) when the document has the $id key. Runs before_save hooks.
  261ds_insert(Col, Doc, Id):-
  262    must_be(atom, Col),
  263    must_be(nonvar, Doc),
  264    (   get_dict('$id', Doc, _)
  265    ->  throw(error(doc_has_id))
  266    ;   true),
  267    ds_transactional(insert_unsafe(Col, Doc, Id)).
  268
  269insert_unsafe(Col, Doc, Id):-
  270    run_before_save_hooks(Col, Doc, Processed),
  271    ds_uuid(Id),
  272    dict_pairs(Processed, _, Pairs),
  273    must_be(ground, Pairs),
  274    run(assertz(col(Col, Id))),
  275    maplist(assert_eav(Id), Pairs).
  276
  277assert_eav(Id, Name-Value):-
  278    run(assertz(eav(Id, Name, Value))).
  279
  280% Executes save hooks.
  281
  282run_before_save_hooks(Col, Doc, Out):-
  283    findall(Goal, hook(Col, before_save, Goal), Goals),
  284    run_before_save_goals(Goals, Doc, Out).
  285
  286run_before_save_goals([Goal|Goals], Doc, Out):-
  287    debug(docstore, 'running save hook ~p', [Goal]),
  288    (   call(Goal, Doc, Tmp)
  289    ->  run_before_save_goals(Goals, Tmp, Out)
  290    ;   throw(error(before_save_hook_failed(Goal)))).
  291
  292run_before_save_goals([], Doc, Doc).
 ds_update(+Doc) is det
Updates the given document. Only changed properties are updated. Throws error if Doc contains no $id. Ignores updates to $id. Runs before_save hooks. Throws error(no_such_doc(Id)) when no document with the given Id exists.
  303ds_update(Doc):-
  304    must_be(dict, Doc),
  305    ds_id(Doc, Id),
  306    (   col(Col, Id)
  307    ->  true
  308    ;   throw(error(no_such_doc(Id)))),
  309    ds_transactional(update_unsafe(Col, Id, Doc)).
 ds_update(+Id, +Doc) is det
Updates the given document. The document id inside the document is ignored.
  316ds_update(Id, Doc):-
  317    must_be(dict, Doc),
  318    must_be(atom, Id),
  319    (   col(Col, Id)
  320    ->  true
  321    ;   throw(error(no_such_doc(Id)))),
  322    ds_transactional(update_unsafe(Col, Id, Doc)).
  323
  324update_unsafe(Col, Id, Doc):-
  325    run_before_save_hooks(Col, Doc, Processed),
  326    dict_pairs(Processed, _, Pairs),
  327    must_be(ground, Pairs),
  328    update_props(Id, Pairs).
  329
  330update_props(_, []).
  331
  332update_props(Id, [Name-Value|Props]):-
  333    update_prop(Id, Name, Value),
  334    update_props(Id, Props).
  335
  336update_prop(_, '$id', _):- !.
  337
  338update_prop(Id, Name, Value):-
  339    eav(Id, Name, Old), !,
  340    (   Value = Old
  341    ->  true
  342    ;   prop_update_unsafe(Id, Name, Value)).
  343
  344update_prop(Id, Name, Value):-
  345    run(assertz(eav(Id, Name, Value))).
  346
  347prop_update_unsafe(Id, Name, Value):-
  348    run(retractall(eav(Id, Name, _))),
  349    run(assertz(eav(Id, Name, Value))).
 ds_upsert(+Doc) is det
Same as ds_upsert/2 but ignores the generated id.
  356ds_upsert(Doc):-
  357    ds_upsert(Doc, _).
 ds_upsert(+Doc, -Id) is det
Same as ds_upsert/3 but uses dict tag as collection name when inserting.
  364ds_upsert(Doc, Id):-
  365    must_be(dict, Doc),
  366    (   get_dict('$id', Doc, Id)
  367    ->  ds_update(Doc)
  368    ;   ds_insert(Doc, Id)).
 ds_upsert(+Col, +Doc, -Id) is det
Inserts or updates the given document.
  374ds_upsert(Col, Doc, Id):-
  375    must_be(atom, Col),
  376    must_be(dict, Doc),
  377    (   get_dict('$id', Doc, Id)
  378    ->  ds_update(Doc)
  379    ;   ds_insert(Col, Doc, Id)).
 ds_move(+Col, +Id, +NewCol) is det
Moves the given document into the new collection. Throws error when the document does not exist.
  387ds_move(Col, Id, NewCol):-
  388    must_be(atom, Id),
  389    must_be(atom, Col),
  390    must_be(atom, NewCol),
  391    (   col(Col, Id)
  392    ->  ds_transactional((
  393            run(retractall(col(_, Id))),
  394            run(assertz(col(NewCol, Id)))
  395        ))
  396    ;   throw(error(no_such_doc_in(Id, Col)))).
 ds_col_get(+Col, +Id, -Doc) is semidet
Retrieves entry with the given id. Fails when the document with the given id does not exist or is not in the given collection.
  404ds_col_get(Col, Id, Doc):-
  405    must_be(atom, Col),
  406    must_be(atom, Id),
  407    col(Col, Id),
  408    doc(Id, Doc).
 ds_col_get(+Col, +Id, +Keys, -Doc) is semidet
Retrieves entry with the given id. Retrieves subset of properties. Fails when the document with the given id does not exist or is not in the given collection.
  417ds_col_get(Col, Id, Keys, Doc):-
  418    must_be(atom, Col),
  419    must_be(atom, Id),
  420    col(Col, Id),
  421    doc(Id, Keys, Doc).
 ds_all(+Col, -List) is det
Finds list of all documents in the given collection.
  428ds_all(Col, List):-
  429    must_be(atom, Col),
  430    findall(Doc, col_doc(Col, Doc), List).
 ds_all(+Col, +Keys, -List) is det
Finds list of all documents in the given collection. Retrieves subset of keys. Subset will always contain '$id'.
  438ds_all(Col, Keys, List):-
  439    must_be(atom, Col),
  440    findall(Doc, col_doc(Col, Keys, Doc), List).
  441
  442col_doc(Col, Doc):-
  443    col(Col, Id),
  444    doc(Id, Doc).
  445
  446col_doc(Col, Keys, Doc):-
  447    col(Col, Id),
  448    doc(Id, Keys, Doc).
 ds_all_ids(+Col, -List) is det
Retrieves the list of all document IDs in the collection.
  455ds_all_ids(Col, List):-
  456    must_be(atom, Col),
  457    findall(Id, col(Col, Id), List).
 ds_find(+Col, +Cond, -List) is semidet
Finds collection entries that satisfy condition(s). Cond is one of: Key = Value, Key \= Value, Key > Value, Key < Value, Key >= Value, Key =< Value, member(Value, Key), (Cond1, Cond2), (Cond1 ; Cond2).
  467ds_find(Col, Cond, List):-
  468    must_be(atom, Col),
  469    must_be(ground, Cond),
  470    findall(Doc, cond_doc(Col, Cond, Doc), List).
 ds_find(+Col, +Cond, +Keys, -List) is semidet
Same as ds_find/3 but retrieves subset of keys.
  476ds_find(Col, Cond, Keys, List):-
  477    must_be(atom, Col),
  478    must_be(list(atom), Keys),
  479    must_be(ground, Cond),
  480    findall(Doc, cond_doc(Col, Cond, Keys, Doc), List).
  481
  482cond_doc(Col, Cond, Dict):-
  483    col(Col, Id),
  484    cond(Cond, Id),
  485    doc(Id, Dict).
  486
  487cond_doc(Col, Cond, Keys, Dict):-
  488    col(Col, Id),
  489    cond(Cond, Id),
  490    doc(Id, Keys, Dict).
  491
  492% Succeeds when condition is satisfied
  493% on the given entity.
  494
  495cond(Name = Value, Id):- !,
  496    eav(Id, Name, Value).
  497
  498cond(Name \= Comp, Id):- !,
  499    eav(Id, Name, Value),
  500    Value \= Comp.
  501
  502cond(Name > Comp, Id):- !,
  503    eav(Id, Name, Value),
  504    Value > Comp.
  505
  506cond(Name < Comp, Id):- !,
  507    eav(Id, Name, Value),
  508    Value < Comp.
  509
  510cond(Name >= Comp, Id):- !,
  511    eav(Id, Name, Value),
  512    Value >= Comp.
  513
  514cond(Name =< Comp, Id):- !,
  515    eav(Id, Name, Value),
  516    Value =< Comp.
  517
  518cond(member(Item, Name), Id):- !,
  519    eav(Id, Name, Value),
  520    memberchk(Item, Value).
  521
  522cond(','(Left, Right), Id):- !,
  523    cond(Left, Id),
  524    cond(Right, Id).
  525
  526cond(';'(Left, _), Id):- !,
  527    cond(Left, Id).
  528
  529cond(';'(_, Right), Id):- !,
  530    cond(Right, Id).
  531
  532cond(Cond, _):-
  533    throw(error(invalid_condition(Cond))).
  534
  535% Finds document by ID.
  536
  537doc(Id, Dict):-
  538    col(Col, Id),
  539    doc_kv_pairs(Id, Pairs),
  540    dict_pairs(Dict, Col, ['$id'-Id|Pairs]).
  541
  542% Finds document by ID. Gives
  543% back subset of its properties.
  544
  545doc(Id, Keys, Dict):-
  546    col(Col, Id),
  547    doc_kv_pairs(Id, Keys, Pairs),
  548    dict_pairs(Dict, Col, ['$id'-Id|Pairs]).
  549
  550% Finds document properties
  551% with values.
  552
  553doc_kv_pairs(Id, Pairs):-
  554    findall(Pair, doc_kv_pair(Id, Pair), Pairs).
  555
  556% Finds document properties
  557% with values. Subset of properties.
  558
  559doc_kv_pairs(Id, Key, Doc):-
  560    atom(Key), !,
  561    doc_kv_pairs(Id, [Key], Doc).
  562
  563doc_kv_pairs(Id, Keys, Pairs):-
  564    (   key_list(Keys)
  565    ->  true
  566    ;   throw(error(invalid_key_set(Keys)))),
  567    findall(Pair, (
  568        member(Key, Keys),
  569        doc_kv_pair(Id, Key, Pair)
  570    ), Pairs).
  571
  572key_list([]).
  573
  574key_list([_|_]).
  575
  576doc_kv_pair(Id, Name-Value):-
  577    eav(Id, Name, Value).
  578
  579doc_kv_pair(Id, Name, Name-Value):-
  580    eav(Id, Name, Value).
 ds_collection(?Id, ?Col) is semidet
Finds which collection the document belongs to.
  587ds_collection(Id, Col):-
  588    col(Col, Id).
 ds_col_remove(+Col, +Id) is det
Removes the given document. Does nothing when the document does not exist. Runs before_remove hooks.
  596ds_col_remove(Col, Id):-
  597    must_be(atom, Id),
  598    must_be(atom, Col),
  599    (   col(Actual, Id)
  600    ->  (   Actual = Col
  601        ->  debug(docstore, 'removing document ~p', [Id]),
  602            ds_transactional(remove_unsafe(Id))
  603        ;   throw(error(document_not_in(Col))))
  604    ;   true).
  605
  606remove_unsafe(Id):-
  607    run_before_remove_hooks(Id),
  608    run(retractall(eav(Id, _, _))),
  609    run(retractall(col(_, Id))).
  610
  611run_before_remove_hooks(Id):-
  612    col(Col, Id), !,
  613    run_before_remove_hooks(Col, Id).
  614
  615run_before_remove_hooks(Col, Id):-
  616    findall(Goal, hook(Col, before_remove, Goal), Goals),
  617    run_before_remove_goals(Goals, Id).
  618
  619run_before_remove_goals([Goal|Goals], Id):-
  620    debug(docstore, 'running remove hook ~p', [Goal]),
  621    (   call(Goal, Id)
  622    ->  true
  623    ;   throw(error(before_remove_hook_fail(Goal)))),
  624    run_before_remove_goals(Goals, Id).
  625
  626run_before_remove_goals([], _).
 ds_col_remove_cond(+Col, +Cond) is det
Removes all documents from the collection that match the condition. Runs before_remove hooks. Cond expressions are same as in ds_find/3.
  635ds_col_remove_cond(Col, Cond):-
  636    must_be(atom, Col),
  637    findall(Id, (col(Col, Id), cond(Cond, Id)), Ids),
  638    ds_transactional(maplist(remove_unsafe, Ids)).
 ds_remove_col(Col) is det
Removes all documents from the given collection. Is equivalent of running ds_remove/1 for each document in the collection. Runs before_remove hooks.
  647ds_remove_col(Col):-
  648    must_be(atom, Col),
  649    debug(docstrore, 'removing collection ~p', [Col]),
  650    ds_all_ids(Col, Ids),
  651    ds_transactional(maplist(remove_unsafe, Ids)).
 ds_tuples(+Col, +Keys, -Values) is nondet
Provides backtrackable predicate-like view of documents. It does not support built-in indexing and therefore can be slow for purposes where some values are restricted.
  660ds_tuples(Col, Keys, Values):-
  661    must_be(atom, Col),
  662    must_be(list(atom), Keys),
  663    col(Col, Id),
  664    maplist(eav(Id), Keys, Values).
 ds_remove_key(+Id, +Key) is det
Removes key from the given document. Does nothing when the document or entry does not exist.
  672ds_remove_key(Id, Key):-
  673    must_be(atom, Id),
  674    must_be(atom, Key),
  675    ds_transactional(ds_remove_key_unsafe(Id, Key)).
  676
  677ds_remove_key_unsafe(Id, Key):-
  678    run(retractall(eav(Id, Key, _))).
 ds_col_add_key(+Col, +Key, +Default) is det
Adds each document new key with the default value. Runs before_save hooks.
  685ds_col_add_key(Col, Key, Default):-
  686    must_be(atom, Col),
  687    must_be(atom, Key),
  688    must_be(ground, Default),
  689    ds_transactional(ds_col_add_key_unsafe(Col, Key, Default)).
  690
  691ds_col_add_key_unsafe(Col, Key, Default):-
  692    ds_all_ids(Col, Ids),
  693    maplist(ds_add_key_unsafe(Col, Key, Default), Ids).
  694
  695ds_add_key_unsafe(Col, Key, Default, Id):-
  696    dict_create(Dict, Col, ['$id'-Id, Key-Default]),
  697    update_unsafe(Col, Id, Dict).
 ds_col_remove_key(+Col, +Key) is det
Removes given key from the document collection. Throws error(cannot_remove_id) when key is $id. save_before hooks are not executed.
  706ds_col_remove_key(Col, Key):-
  707    must_be(atom, Col),
  708    must_be(atom, Key),
  709    (   Key = '$id'
  710    ->  throw(error(cannot_remove_id))
  711    ;   true),
  712    ds_transactional(ds_col_remove_key_unsafe(Col, Key)).
  713
  714ds_col_remove_key_unsafe(Col, Key):-
  715    ds_all_ids(Col, Ids),
  716    maplist(ds_col_remove_key_id(Key), Ids).
  717
  718ds_col_remove_key_id(Key, Id):-
  719    run(retractall(eav(Id, Key, _))).
 ds_col_rename(+Col, +ColNew) is det
Rename collection. Relatively expensive operation in terms of journal space. Needs entry per document in the collection.
  727ds_col_rename(Col, ColNew):-
  728    must_be(atom, Col),
  729    must_be(atom, ColNew),
  730    ds_transactional(ds_col_rename_unsafe(Col, ColNew)).
  731
  732ds_col_rename_unsafe(Col, ColNew):-
  733    ds_all_ids(Col, Ids),
  734    run(retractall(col(Col, _))),
  735    maplist(new_col_id(ColNew), Ids).
  736
  737new_col_id(Col, Id):-
  738    run(assertz(col(Col, Id))).
 ds_col_rename_key(+Col, +Key, +KeyNew) is det
Renames a key in collection. Relatively expensive operation in terms of journal space. Needs 2 entries per document in the collection. Does not run hooks.
  747ds_col_rename_key(Col, Key, KeyNew):-
  748    must_be(atom, Col),
  749    must_be(atom, Key),
  750    must_be(atom, KeyNew),
  751    ds_transactional(ds_col_rename_key_unsafe(Col, Key, KeyNew)).
  752
  753ds_col_rename_key_unsafe(Col, Key, KeyNew):-
  754    ds_all_ids(Col, Ids),
  755    maplist(rename_col_key(Key, KeyNew), Ids).
  756
  757rename_col_key(Key, KeyNew, Id):-
  758    (   eav(Id, Key, Value)
  759    ->  run(retractall(eav(Id, Key, _))),
  760        run(assertz(eav(Id, KeyNew, Value)))
  761    ;   true).
  762
  763:- meta_predicate(ds_transactional(0)).
 ds_transactional(:Goal) is det
Runs given goal that modifies the database contents in a transactional mode. When the goal throws exception or fails, no changes by it are persisted.
  772ds_transactional(Goal):-
  773    safely(with_tx_unsafe(Goal)).
  774
  775:- meta_predicate(with_tx_unsafe(0)).  776
  777% Starts transaction. Catches exception
  778% when exception happens. Rethrows the
  779% exception.
  780
  781with_tx_unsafe(Goal):-
  782    begin,
  783    (   catch(Goal, Error, discard)
  784    ->  (   nonvar(Error)
  785        ->  debug(docstore, 'transactional run ended with exception', []),
  786            throw(Error) % rethrow
  787        ;   commit) % commit
  788    ;   discard, % discard on fail
  789        debug(docstore, 'transactional run failed', [])).
  790
  791:- meta_predicate(safely(0)).  792
  793% Runs the Goal with global mutex.
  794% All modifications to the database
  795% are run using it.
  796
  797safely(Goal):-
  798    with_mutex(db_store, Goal).
  799
  800:- dynamic(log/1).  801:- dynamic(tx/1).  802
  803:- meta_predicate(run(0)).  804
  805% Helper to run and log the goal.
  806% Throws error(database_not_open) when
  807% the database is not open.
  808
  809run(Goal):-
  810    (   tx(_)
  811    ->  (   file(_, _)
  812        ->  assertz(log(Goal))
  813        ;   throw(error(docstore_not_open)))
  814    ;   throw(error(transaction_not_active))).
  815
  816% Starts transaction. tx/1 is used for
  817% detecting transaction nesting. During
  818% transaction nesting, only the last commit
  819% has effect.
  820
  821begin:-
  822    (   tx(N)
  823    ->  retractall(tx(_)),
  824        N1 is N + 1,
  825        debug(docstore, 'beginning fallthrough transaction (~p)', [N1]),
  826        assertz(tx(N1))
  827    ;   debug(docstore, 'beginning transaction (0)', []),
  828        assertz(tx(0))).
  829
  830% Ends transaction. Has effect only
  831% when it ends the outer transaction.
  832
  833commit:-
  834    (   tx(0)
  835    ->  debug(docstore, 'committing changes (0)', []),
  836        file(_, Stream),
  837        write_goal(Stream, begin),
  838        ((  log(Goal),
  839            Goal = _:Local,
  840            write_goal(Stream, Local),
  841            once(Goal),
  842            fail
  843        ) ; true),
  844        write_goal(Stream, end),
  845        flush_output(Stream),
  846        retractall(log(_)),
  847        retractall(tx(_))
  848    ;   tx(N),
  849        debug(docstore, 'fallthrough commit (~p)', [N]),
  850        retractall(tx(_)),
  851        N1 is N - 1,
  852        assertz(tx(N1))).
  853
  854write_goal(Stream, Goal):-
  855    write_term(Stream, Goal, [
  856        ignore_ops,
  857        quoted,
  858        dotlists(true),
  859        nl(true),
  860        fullstop(true)
  861    ]).
  862
  863% Ends transaction by empting the
  864% goal log. Has effect only when
  865% it ends the outer transaction.
  866
  867discard:-
  868    (   tx(0)
  869    ->  debug(docstore, 'discarding changes', []),
  870        retractall(log(_)),
  871        retractall(tx(_))
  872    ;   tx(N),
  873        debug(docstore, 'fallthrough discard (~p)', [N]),
  874        retractall(tx(_)),
  875        N1 is N - 1,
  876        assertz(tx(N1))).
 ds_id(+Doc, -Id) is det
Extracts document id from the given document. Equivalent to Doc.'$id'.
  883ds_id(Doc, Id):-
  884    (   get_dict('$id', Doc, Actual)
  885    ->  Id = Actual
  886    ;   throw(error(doc_has_no_id(Doc)))).
 ds_set_id(+In, +Id, -Out) is det
Sets the document id. Throws error when the document is not a dict or id is not an atom.
  894ds_set_id(In, Id, Out):-
  895    must_be(dict, In),
  896    must_be(atom, Id),
  897    Out = In.put('$id', Id).
 ds_uuid(-Id) is det
Generates UUID version 4 identifier. More info: http://en.wikipedia.org/wiki/Universally_unique_identifier
  905ds_uuid(Id):-
  906    uuid_pattern(Pat),
  907    maplist(fill, Pat),
  908    atom_chars(Id, Pat).
  909
  910fill(Place):-
  911    var(Place), !,
  912    uuid_rand(Place).
  913
  914fill(_).
  915
  916uuid_rand(Hex):-
  917    List = ['0', '1', '2', '3', '4', '5',
  918        '6', '7', '8', '9', 'a', 'b',
  919        'c', 'd', 'e', 'f'],
  920    random_member(Hex, List).
  921
  922% Patterns for UUID version 4.
  923
  924uuid_pattern(Pat):-
  925    Pat = [_, _, _, _, _, _, _, _,
  926        -, _, _, _, _,
  927        -, '4', _, _, _,
  928        -, 'a', _, _, _,
  929        -, _, _, _, _, _, _, _, _, _, _, _, _]