Chris@0
|
1 /* $Id$
|
Chris@0
|
2
|
Chris@0
|
3 Part of SWI-Prolog
|
Chris@0
|
4
|
Chris@0
|
5 Author: Jan Wielemaker
|
Chris@0
|
6 E-mail: jan@swi.psy.uva.nl
|
Chris@0
|
7 WWW: http://www.swi-prolog.org
|
Chris@0
|
8 Copyright (C): 1985-2004, University of Amsterdam
|
Chris@0
|
9
|
Chris@0
|
10 This program is free software; you can redistribute it and/or
|
Chris@0
|
11 modify it under the terms of the GNU General Public License
|
Chris@0
|
12 as published by the Free Software Foundation; either version 2
|
Chris@0
|
13 of the License, or (at your option) any later version.
|
Chris@0
|
14
|
Chris@0
|
15 This program is distributed in the hope that it will be useful,
|
Chris@0
|
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@0
|
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@0
|
18 GNU General Public License for more details.
|
Chris@0
|
19
|
Chris@0
|
20 You should have received a copy of the GNU Lesser General Public
|
Chris@0
|
21 License along with this library; if not, write to the Free Software
|
Chris@0
|
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
Chris@0
|
23
|
Chris@0
|
24 As a special exception, if you link this library with other files,
|
Chris@0
|
25 compiled with a Free Software compiler, to produce an executable, this
|
Chris@0
|
26 library does not by itself cause the resulting executable to be covered
|
Chris@0
|
27 by the GNU General Public License. This exception does not however
|
Chris@0
|
28 invalidate any other reasons why the executable file might be covered by
|
Chris@0
|
29 the GNU General Public License.
|
Chris@0
|
30 */
|
Chris@0
|
31
|
Chris@0
|
32
|
Chris@0
|
33 :- module(db,
|
Chris@0
|
34 [ (db_term)/1, % +Declarations
|
Chris@0
|
35
|
Chris@0
|
36 db_attach/2, % :File, +Options
|
Chris@0
|
37
|
Chris@0
|
38 db_assert/1, % :Term
|
Chris@0
|
39 db_retractall/1, % :Term
|
Chris@0
|
40
|
Chris@0
|
41 db_sync/1, % :What
|
Chris@0
|
42 db_sync_all/1, % +What
|
Chris@0
|
43
|
Chris@0
|
44 op(1150, fx, (db_term))
|
Chris@0
|
45 ]).
|
Chris@0
|
46 :- use_module(library(debug)).
|
Chris@0
|
47 :- use_module(library(error)).
|
Chris@0
|
48
|
Chris@0
|
49 /** <module> Provide persistent dynamic predicates
|
Chris@0
|
50
|
Chris@0
|
51 This module provide simple persistent storage for one or more dynamic
|
Chris@0
|
52 predicates. A database is always associated with a module. A module that
|
Chris@0
|
53 wishes to maintain a database must declare the terms that can be placed
|
Chris@0
|
54 in the database using the predicate db_term(?Term).
|
Chris@0
|
55
|
Chris@0
|
56 db_term/1 is called by attach_db/1 to initialise all database predicates
|
Chris@0
|
57 as dynamic. In this mode it is called as db_term(-Term) and must
|
Chris@0
|
58 enumerate all database terms. While loading a database, it is called as
|
Chris@0
|
59 db_term(+Term) and must succeed for any valid database term. If it
|
Chris@0
|
60 fails, a warning is printed and the term is not asserted into the
|
Chris@0
|
61 database.
|
Chris@0
|
62
|
Chris@0
|
63 Here is a typical example:
|
Chris@0
|
64
|
Chris@0
|
65 ==
|
Chris@0
|
66 db_term(user(_Name, _Group)).
|
Chris@0
|
67
|
Chris@0
|
68 ...,
|
Chris@0
|
69 db_attach('users.db'),
|
Chris@0
|
70
|
Chris@0
|
71 ...,
|
Chris@0
|
72 user(Name, Group),
|
Chris@0
|
73
|
Chris@0
|
74 ...,
|
Chris@0
|
75 db_assert(user('Bob', 'administrator')),
|
Chris@0
|
76 ==
|
Chris@0
|
77
|
Chris@0
|
78 @tbd Deal with amount of `dirtyness'. I.e. only _gc_ if more than
|
Chris@0
|
79 _X_ percent is dirty.
|
Chris@0
|
80 @tbd Provide db_retract/1?
|
Chris@0
|
81 @tbd Type safety
|
Chris@0
|
82 @tbd Thread safety
|
Chris@0
|
83 @tbd Transaction management?
|
Chris@0
|
84
|
Chris@0
|
85 @author Jan Wielemaker
|
Chris@0
|
86 */
|
Chris@0
|
87
|
Chris@0
|
88 :- meta_predicate
|
Chris@0
|
89 db_attach(:, +),
|
Chris@0
|
90 db_assert(:),
|
Chris@0
|
91 db_retractall(:),
|
Chris@0
|
92 db_sync(:).
|
Chris@0
|
93
|
Chris@0
|
94
|
Chris@0
|
95 /*******************************
|
Chris@0
|
96 * DB *
|
Chris@0
|
97 *******************************/
|
Chris@0
|
98
|
Chris@0
|
99 :- dynamic
|
Chris@0
|
100 db_file/3, % Module, File, Modified
|
Chris@0
|
101 db_stream/2, % Module, Stream
|
Chris@0
|
102 db_dirty/1, % Module
|
Chris@0
|
103 db_option/2. % Module, Name(Value)
|
Chris@0
|
104
|
Chris@0
|
105 :- multifile
|
Chris@0
|
106 (db_term)/2. % Module, Term
|
Chris@0
|
107
|
Chris@0
|
108
|
Chris@0
|
109 /*******************************
|
Chris@0
|
110 * DECLARATIONS *
|
Chris@0
|
111 *******************************/
|
Chris@0
|
112
|
Chris@0
|
113 %% db_term(+Spec)
|
Chris@0
|
114 %
|
Chris@0
|
115 % Declare dynamic database terms. Declarations appear in a
|
Chris@0
|
116 % directive and have the following format:
|
Chris@0
|
117 %
|
Chris@0
|
118 % ==
|
Chris@0
|
119 % :- db_term
|
Chris@0
|
120 % <callable>,
|
Chris@0
|
121 % <callable>,
|
Chris@0
|
122 % ...
|
Chris@0
|
123 % ==
|
Chris@0
|
124
|
Chris@0
|
125 db_term(Spec) :-
|
Chris@0
|
126 throw(error(context_error(nodirective, db_term(Spec)), _)).
|
Chris@0
|
127
|
Chris@0
|
128 compile_db_term(Var) -->
|
Chris@0
|
129 { var(Var), !,
|
Chris@0
|
130 type_error(callable, Var)
|
Chris@0
|
131 }.
|
Chris@0
|
132 compile_db_term((A,B)) --> !,
|
Chris@0
|
133 compile_db_term(A),
|
Chris@0
|
134 compile_db_term(B).
|
Chris@0
|
135 compile_db_term(Term) -->
|
Chris@0
|
136 { functor(Term, Name, Arity), % Validates Term as callable
|
Chris@0
|
137 prolog_load_context(module, Module)
|
Chris@0
|
138 },
|
Chris@0
|
139 [ :- dynamic(Name/Arity),
|
Chris@0
|
140 db:db_term(Module, Term)
|
Chris@0
|
141 ].
|
Chris@0
|
142
|
Chris@0
|
143 :- multifile
|
Chris@0
|
144 user:term_expansion/2.
|
Chris@0
|
145
|
Chris@0
|
146 user:term_expansion((:- db_term(Spec)), Clauses) :-
|
Chris@0
|
147 phrase(compile_db_term(Spec), Clauses).
|
Chris@0
|
148
|
Chris@0
|
149
|
Chris@0
|
150 /*******************************
|
Chris@0
|
151 * ATTACH *
|
Chris@0
|
152 *******************************/
|
Chris@0
|
153
|
Chris@0
|
154 %% attach_db(:File, +Options)
|
Chris@0
|
155 %
|
Chris@0
|
156 % Use File as persistent database for the calling module. The
|
Chris@0
|
157 % calling module must defined db_term/1 to declare the database
|
Chris@0
|
158 % terms. Defined options:
|
Chris@0
|
159 %
|
Chris@0
|
160 % * sync(+Sync)
|
Chris@0
|
161 % One of =close= (close journal after write), =flush=
|
Chris@0
|
162 % (default, flush journal after write) or =none=
|
Chris@0
|
163 % (handle as fully buffered stream).
|
Chris@0
|
164
|
Chris@0
|
165 db_attach(Spec, Options) :-
|
Chris@0
|
166 strip_module(Spec, Module, File),
|
Chris@0
|
167 db_set_options(Module, Options),
|
Chris@0
|
168 db_attach_file(Module, File).
|
Chris@0
|
169
|
Chris@0
|
170 db_set_options(Module, Options) :-
|
Chris@0
|
171 retractall(db_option(Module, _)),
|
Chris@0
|
172 option(sync(Sync), Options, flush),
|
Chris@0
|
173 must_be(oneof([close,flush,none]), Sync),
|
Chris@0
|
174 assert(db_option(Module, sync(Sync))).
|
Chris@0
|
175
|
Chris@0
|
176 db_attach_file(Module, File) :-
|
Chris@0
|
177 db_file(Module, Old, _), !, % we already have a db
|
Chris@0
|
178 ( Old == File
|
Chris@0
|
179 -> true
|
Chris@0
|
180 ; permission_error(attach, db, File)
|
Chris@0
|
181 ).
|
Chris@0
|
182 db_attach_file(Module, File) :-
|
Chris@0
|
183 db_load(Module, File), !.
|
Chris@0
|
184 db_attach_file(Module, File) :-
|
Chris@0
|
185 assert(db_file(Module, File, 0)).
|
Chris@0
|
186
|
Chris@0
|
187 db_load(Module, File) :-
|
Chris@0
|
188 retractall(db_file(Module, _, _)),
|
Chris@0
|
189 catch(open(File, read, In, [encoding(utf8)]), _, fail), !,
|
Chris@0
|
190 debug(db, 'Loading database ~w', [File]),
|
Chris@0
|
191 call_cleanup((read_action(In, T0),
|
Chris@0
|
192 load_db(T0, In, Module)),
|
Chris@0
|
193 close(In)),
|
Chris@0
|
194 debug(db, 'Loaded ~w', [File]),
|
Chris@0
|
195 time_file(File, Modified),
|
Chris@0
|
196 assert(db_file(Module, File, Modified)).
|
Chris@0
|
197
|
Chris@0
|
198 load_db(end_of_file, _, _) :- !.
|
Chris@0
|
199 load_db(assert(Term), In, Module) :-
|
Chris@0
|
200 db_term(Module, Term), !,
|
Chris@0
|
201 assert(Module:Term),
|
Chris@0
|
202 read_action(In, T1),
|
Chris@0
|
203 load_db(T1, In, Module).
|
Chris@0
|
204 load_db(retractall(Term), In, Module) :-
|
Chris@0
|
205 db_term(Module, Term), !,
|
Chris@0
|
206 retractall(Module:Term),
|
Chris@0
|
207 set_dirty(Module),
|
Chris@0
|
208 read_action(In, T1),
|
Chris@0
|
209 load_db(T1, In, Module).
|
Chris@0
|
210 load_db(Term, In, Module) :-
|
Chris@0
|
211 print_message(error, illegal_term(Term)),
|
Chris@0
|
212 read_action(In, T1),
|
Chris@0
|
213 load_db(T1, In, Module).
|
Chris@0
|
214
|
Chris@0
|
215 db_clean(Module) :-
|
Chris@0
|
216 retractall(db_dirty(Module)),
|
Chris@0
|
217 ( db_term(Module, Term),
|
Chris@0
|
218 retractall(Module:Term),
|
Chris@0
|
219 fail
|
Chris@0
|
220 ; true
|
Chris@0
|
221 ).
|
Chris@0
|
222
|
Chris@0
|
223 %% db_assert(:Term) is det.
|
Chris@0
|
224 %
|
Chris@0
|
225 % Assert Term into the database and record it for persistency.
|
Chris@0
|
226 % Note that if the on-disk file has been modified it is first
|
Chris@0
|
227 % reloaded.
|
Chris@0
|
228
|
Chris@0
|
229 db_assert(Spec) :-
|
Chris@0
|
230 strip_module(Spec, Module, Term),
|
Chris@0
|
231 assert(Module:Term),
|
Chris@0
|
232 persistent(Module, assert(Term)).
|
Chris@0
|
233
|
Chris@0
|
234 persistent(Module, Action) :-
|
Chris@0
|
235 ( db_stream(Module, Stream)
|
Chris@0
|
236 -> true
|
Chris@0
|
237 ; db_file(Module, File, _Modified)
|
Chris@0
|
238 -> db_sync(Module, reload), % Is this correct?
|
Chris@0
|
239 open(File, append, Stream,
|
Chris@0
|
240 [ close_on_abort(false),
|
Chris@0
|
241 encoding(utf8),
|
Chris@0
|
242 lock(write)
|
Chris@0
|
243 ]),
|
Chris@0
|
244 assert(db_stream(Module, Stream))
|
Chris@0
|
245 ; existence_error(db_file, Module)
|
Chris@0
|
246 ),
|
Chris@0
|
247 write_action(Stream, Action),
|
Chris@0
|
248 sync(Module, Stream).
|
Chris@0
|
249
|
Chris@0
|
250 %% sync(+Module, +Stream) is det.
|
Chris@0
|
251 %
|
Chris@0
|
252 % Synchronise journal after a write. Using =close=, the journal
|
Chris@0
|
253 % file is closed, making it easier to edit the file externally.
|
Chris@0
|
254 % Using =flush= flushes the stream but does not close it. This
|
Chris@0
|
255 % provides better performance. Using =none=, the stream is not
|
Chris@0
|
256 % even flushed. This makes the journal sensitive to crashes, but
|
Chris@0
|
257 % much faster.
|
Chris@0
|
258
|
Chris@0
|
259 sync(Module, Stream) :-
|
Chris@0
|
260 db_option(Module, sync(Sync)),
|
Chris@0
|
261 ( Sync == close
|
Chris@0
|
262 -> db_sync(Module, close)
|
Chris@0
|
263 ; Sync == flush
|
Chris@0
|
264 -> flush_output(Stream)
|
Chris@0
|
265 ; true
|
Chris@0
|
266 ).
|
Chris@0
|
267
|
Chris@0
|
268 read_action(Stream, Action) :-
|
Chris@0
|
269 read_term(Stream, Action, [module(db)]).
|
Chris@0
|
270
|
Chris@0
|
271 write_action(Stream, Action) :-
|
Chris@0
|
272 \+ \+ ( numbervars(Action, 0, _, [singletons(true)]),
|
Chris@0
|
273 format(Stream, '~W.~n',
|
Chris@0
|
274 [ Action,
|
Chris@0
|
275 [ quoted(true),
|
Chris@0
|
276 numbervars(true),
|
Chris@0
|
277 module(db)
|
Chris@0
|
278 ]
|
Chris@0
|
279 ])
|
Chris@0
|
280 ).
|
Chris@0
|
281
|
Chris@0
|
282 %% db_retractall(:Term) is det.
|
Chris@0
|
283 %
|
Chris@0
|
284 % Retract all matching facts and do the same in the database. If
|
Chris@0
|
285 % Term is unbound, db_term/1 from the calling module is used as
|
Chris@0
|
286 % generator.
|
Chris@0
|
287 %
|
Chris@0
|
288 % @tbd Only flag dirty if clauses are deleted.
|
Chris@0
|
289
|
Chris@0
|
290 db_retractall(Spec) :-
|
Chris@0
|
291 strip_module(Spec, Module, Term),
|
Chris@0
|
292 ( var(Term)
|
Chris@0
|
293 -> forall(db_term(Module, Term),
|
Chris@0
|
294 db_retractall(Module:Term))
|
Chris@0
|
295 ; retractall(Module:Term),
|
Chris@0
|
296 set_dirty(Module),
|
Chris@0
|
297 persistent(Module, retractall(Term))
|
Chris@0
|
298 ).
|
Chris@0
|
299
|
Chris@0
|
300
|
Chris@0
|
301 set_dirty(Module) :-
|
Chris@0
|
302 ( db_dirty(Module)
|
Chris@0
|
303 -> true
|
Chris@0
|
304 ; assert(db_dirty(Module))
|
Chris@0
|
305 ).
|
Chris@0
|
306
|
Chris@0
|
307 %% db_sync(?What)
|
Chris@0
|
308 %
|
Chris@0
|
309 % Synchronise database with the associated file. What is one of:
|
Chris@0
|
310 %
|
Chris@0
|
311 % * reload
|
Chris@0
|
312 % Database is reloaded from file
|
Chris@0
|
313 % * gc
|
Chris@0
|
314 % Database was re-written, deleting all retractall
|
Chris@0
|
315 % statements.
|
Chris@0
|
316 % * close
|
Chris@0
|
317 % Database stream was closed
|
Chris@0
|
318 % * nop
|
Chris@0
|
319 % No-operation performed
|
Chris@0
|
320 %
|
Chris@0
|
321 % With unbound What, db_sync/1 will reload the database if it was
|
Chris@0
|
322 % modified on disk, gc it if it is dirty and close it if it is
|
Chris@0
|
323 % opened.
|
Chris@0
|
324
|
Chris@0
|
325 db_sync(Spec) :-
|
Chris@0
|
326 strip_module(Spec, Module, What),
|
Chris@0
|
327 db_sync(Module, What).
|
Chris@0
|
328
|
Chris@0
|
329
|
Chris@0
|
330 db_sync(Module, reload) :-
|
Chris@0
|
331 \+ db_stream(Module, _), % not open
|
Chris@0
|
332 db_file(Module, File, ModifiedWhenLoaded),
|
Chris@0
|
333 catch(time_file(File, Modified), _, fail),
|
Chris@0
|
334 Modified > ModifiedWhenLoaded, !, % Externally modified
|
Chris@0
|
335 debug(db, 'Database ~w was externally modified; reloading', [File]),
|
Chris@0
|
336 db_clean(Module),
|
Chris@0
|
337 db_load(Module, File).
|
Chris@0
|
338 db_sync(Module, gc) :-
|
Chris@0
|
339 db_dirty(Module), !,
|
Chris@0
|
340 db_sync(Module, close),
|
Chris@0
|
341 db_file(Module, File, Modified),
|
Chris@0
|
342 atom_concat(File, '.new', NewFile),
|
Chris@0
|
343 debug(db, 'Database ~w is dirty; cleaning', [File]),
|
Chris@0
|
344 open(NewFile, write, Out, [encoding(utf8)]),
|
Chris@0
|
345 ( db_term(Module, Term),
|
Chris@0
|
346 Module:Term,
|
Chris@0
|
347 write_action(Out, assert(Term)),
|
Chris@0
|
348 fail
|
Chris@0
|
349 ; true
|
Chris@0
|
350 ),
|
Chris@0
|
351 close(Out),
|
Chris@0
|
352 retractall(db_file(Module, File, Modified)),
|
Chris@0
|
353 rename_file(NewFile, File),
|
Chris@0
|
354 time_file(File, NewModified),
|
Chris@0
|
355 assert(db_file(Module, File, NewModified)).
|
Chris@0
|
356 db_sync(Module, close) :-
|
Chris@0
|
357 retract(db_stream(Module, Stream)), !,
|
Chris@0
|
358 db_file(Module, File, _),
|
Chris@0
|
359 debug(db, 'Database ~w is open; closing', [File]),
|
Chris@0
|
360 close(Stream),
|
Chris@0
|
361 time_file(File, Modified),
|
Chris@0
|
362 retractall(db_file(Module, File, _)),
|
Chris@0
|
363 assert(db_file(Module, File, Modified)).
|
Chris@0
|
364 db_sync(_, nop) :- !.
|
Chris@0
|
365 db_sync(_, _).
|
Chris@0
|
366
|
Chris@0
|
367
|
Chris@0
|
368 %% db_sync_all(+What)
|
Chris@0
|
369 %
|
Chris@0
|
370 % Sync all registered databases.
|
Chris@0
|
371
|
Chris@0
|
372 db_sync_all(What) :-
|
Chris@0
|
373 must_be(oneof([reload,gc,close]), What),
|
Chris@0
|
374 forall(db_file(Module, _, _),
|
Chris@0
|
375 db_sync(Module:What)).
|
Chris@0
|
376
|
Chris@0
|
377
|
Chris@0
|
378 /*******************************
|
Chris@0
|
379 * CLOSE *
|
Chris@0
|
380 *******************************/
|
Chris@0
|
381
|
Chris@0
|
382 close_dbs :-
|
Chris@0
|
383 forall(db_stream(_Module, Stream),
|
Chris@0
|
384 close(Stream)).
|
Chris@0
|
385
|
Chris@0
|
386 :- at_halt(close_dbs).
|
Chris@0
|
387
|
Chris@0
|
388
|