1:- module(docstore, [
2 ds_open/1, 3 ds_close/0,
4 ds_snapshot/1, 5 ds_snapshot/0,
6 ds_hook/3, 7 ds_insert/1, 8 ds_insert/2, 9 ds_insert/3, 10 ds_update/1, 11 ds_update/2, 12 ds_upsert/1, 13 ds_upsert/2, 14 ds_upsert/3, 15 ds_move/3, 16 ds_col_get/3, 17 ds_col_get/4, 18 ds_all/2, 19 ds_all/3, 20 ds_all_ids/2, 21 ds_find/3, 22 ds_find/4, 23 ds_collection/2, 24 ds_col_remove/2, 25 ds_col_remove_cond/2, 26 ds_remove_col/1, 27 ds_remove_key/2, 28 ds_tuples/3, 29 ds_col_add_key/3, 30 ds_col_remove_key/2, 31 ds_col_rename/2, 32 ds_col_rename_key/3, 33 ds_transactional/1, 34 ds_uuid/1, 35 ds_id/2, 36 ds_set_id/3 37]).
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).
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
82
83clean:-
84 debug(docstore, 'cleaning database', []),
85 retractall(col(_, _)),
86 retractall(eav(_, _, _)),
87 retractall(file(_, _)).
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
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
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))).
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).
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(+, +, :)).
230ds_hook(Col, Action, Goal):-
231 ( hook(Col, Action, Goal)
232 -> true
233 ; assertz(hook(Col, Action, Goal))).
240ds_insert(Doc):-
241 ds_insert(Doc, _).
248ds_insert(Doc, Id):-
249 must_be(dict, Doc),
250 is_dict(Doc, Col),
251 ds_insert(Col, Doc, Id).
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
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).
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)).
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))).
356ds_upsert(Doc):-
357 ds_upsert(Doc, _).
364ds_upsert(Doc, Id):-
365 must_be(dict, Doc),
366 ( get_dict('$id', Doc, Id)
367 -> ds_update(Doc)
368 ; ds_insert(Doc, Id)).
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)).
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)))).
404ds_col_get(Col, Id, Doc):-
405 must_be(atom, Col),
406 must_be(atom, Id),
407 col(Col, Id),
408 doc(Id, Doc).
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).
428ds_all(Col, List):-
429 must_be(atom, Col),
430 findall(Doc, col_doc(Col, Doc), List).
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).
455ds_all_ids(Col, List):-
456 must_be(atom, Col),
457 findall(Id, col(Col, Id), List).
467ds_find(Col, Cond, List):-
468 must_be(atom, Col),
469 must_be(ground, Cond),
470 findall(Doc, cond_doc(Col, Cond, Doc), List).
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
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
536
537doc(Id, Dict):-
538 col(Col, Id),
539 doc_kv_pairs(Id, Pairs),
540 dict_pairs(Dict, Col, ['$id'-Id|Pairs]).
541
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
552
553doc_kv_pairs(Id, Pairs):-
554 findall(Pair, doc_kv_pair(Id, Pair), Pairs).
555
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).
587ds_collection(Id, Col):-
588 col(Col, Id).
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([], _).
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)).
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)).
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).
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, _))).
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).
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, _))).
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))).
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)).
772ds_transactional(Goal):-
773 safely(with_tx_unsafe(Goal)).
774
775:- meta_predicate(with_tx_unsafe(0)). 776
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) 787 ; commit) 788 ; discard, 789 debug(docstore, 'transactional run failed', [])).
790
791:- meta_predicate(safely(0)). 792
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
808
809run(Goal):-
810 ( tx(_)
811 -> ( file(_, _)
812 -> assertz(log(Goal))
813 ; throw(error(docstore_not_open)))
814 ; throw(error(transaction_not_active))).
815
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
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
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))).
883ds_id(Doc, Id):-
884 ( get_dict('$id', Doc, Actual)
885 -> Id = Actual
886 ; throw(error(doc_has_no_id(Doc)))).
894ds_set_id(In, Id, Out):-
895 must_be(dict, In),
896 must_be(atom, Id),
897 Out = In.put('$id', Id).
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
923
924uuid_pattern(Pat):-
925 Pat = [_, _, _, _, _, _, _, _,
926 -, _, _, _, _,
927 -, '4', _, _, _,
928 -, 'a', _, _, _,
929 -, _, _, _, _, _, _, _, _, _, _, _, _]
Document-oriented database
Generic thread-safe in-memory transactional store for dict terms. */