Trying to understand library(persistency)

#1

SWI-Prolog (threaded, 64 bits, version 8.0.0)

I am considering using library(persistency) but my concept of how it should work and how it works are different.

Note: In trying to learn how to use library(persistency), this StackOverflow answer by Wouter Beek was quite helpful in getting started.

My initial concept was that any use of assert/1 would assert (create) a predicate into the database as is customary, e.g.

?- assert(fruit(apple,1.1,red).

?- fruit(apple,Cost,Color).
true ;
Cost = 1.1,
Color = red.

and also assert the predicate into a file (persistency file), in real time, e.g.

File: fruit.db would contain

fruit(apple,Cost,Color).

Then upon shut down and restart of Prolog, and consulting the file (source code file) which uses the persistency library, the asserted predicates would be restored. My concept had the persistency file being loaded back using use_module/1.

What I am finding is that in the file is not the argument of the assert predicates, but assert statements. In other words the file is not a database of predicates/facts, but a journal of asserts of predicates/facts, e.g.

expected:

fruit(apple,1.1,red).
fruit(banana,0.7,yellow).

found:

created(1555327034.666822).
assert(fruit(apple,1.1,red)).
assert(fruit(banana,0.7,yellow)).

Here is a demonstration of what I currently know. The questions are summarized at the end.

Using this

File: persistency_demo.pl

:- module(
  fruit,
  [
    add_fruit/3,    % +Name:atom, +Price:float, +Color:atom
    current_fruit/3 % ?Name:atom, ?Price:float, ?Color:atom
  ]
).

:- use_module(library(persistency)).

:- persistent(fruit(name:atom, price:float, color:atom)).

:- initialization(db_attach('fruit.journal', [])).

add_fruit(Name, Price, Color):-
  with_mutex(fruit_db, assert_fruit(Name, Price, Color)).

current_fruit(Name, Price, Color):-
  with_mutex(fruit_db, fruit(Name, Price, Color)).

%

The persistency library relies on a file.
Sometimes this might be referred to as database or just a *.pl file, I prefer to call give it the file type journal.
For this demonstration the file should not exist at the start of the demonstration.

Start SWI-Prolog

Welcome to SWI-Prolog (threaded, 64 bits, version 8.0.0)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

?- 

Note working directory to locate created journal file.

?- working_directory(Working_directory,Working_directory).
Working_directory = 'c:/prolog/'.

Note that the journal file does not yet exist.
If file the journal file exists then delete it

?- exists_file('c:/prolog/fruit.journal').
false.

Load the predicates that will use the journal.

?- consult("C:/Prolog/persistency_demo.pl").
true.

Note the details of the predicate fruit as this will change.

?- listing(fruit).
:- dynamic fruit:fruit/3.
true.
?- current_fruit(X,Y,Z).
false.

?- fruit(X,Y,Z).
Correct to: "fruit:fruit(X,Y,Z)"? yes
false.

?- fruit:fruit(pear,2.2,green).
false.

NB fruit:fruit/3 did NOT succeed.

?- fruit:fruit(X,Y,Z).
false.

Notice that fruit/3 did not return pear.

?- current_fruit(X,Y,Z).
false.

Notice that current_fruit/3 did not return pear.

?- listing(fruit).
:- dynamic fruit:fruit/3.

true.

?- add_fruit(apple, 1.10, red).
true.

Notice that a file was created or updated
c:/prolog/fruit.journal

?- exists_file('c:/prolog/fruit.journal').
true.

?- size_file('c:/prolog/fruit.journal',Size).
Size = 60.

The file exist but unable to read file or copy file to see what if anything was added.
It seems that only the created/1 predicate is added to the journal file.

?- fruit:fruit(X,Y,Z).
X = apple,
Y = 1.1,
Z = red.

?- current_fruit(X,Y,Z).
X = apple,
Y = 1.1,
Z = red.

?- listing(fruit).
:- dynamic fruit:fruit/3.

fruit:fruit(apple, 1.1, red).

true.

Notice that both fruit:fruit/3 and current_fruit/3 work as expected to query the data as opposed to fruit:fruit/3 and add_fruit/3, only add_fruit/3 worked to update the data.

add_fruit(banana,0.7,yellow).

?- size_file('c:/prolog/fruit.journal',Size).
Size = 60.

Notice that adding another item did not increase the size of the journal file.

?- fruit:fruit(X,Y,Z).
X = apple,
Y = 1.1,
Z = red ;
X = banana,
Y = 0.7,
Z = yellow.

?- current_fruit(X,Y,Z).
X = apple,
Y = 1.1,
Z = red.

?- listing(fruit).
:- dynamic fruit:fruit/3.

fruit:fruit(apple, 1.1, red).
fruit:fruit(banana, 0.7, yellow).

true.

Notice that current_fruit(X,Y,Z) return does not return same result as fruit:fruit(X,Y,Z) and listing(fruit).
This is because current_fruit/3 is wrapped with with_mutex/2 which destroys choice-points.

?- halt.

It seems at this point the asserts are written to the journal file.

Using an editor open file c:/prolog/fruit.journal

created(1555327034.666822).
assert(fruit(apple,1.1,red)).
assert(fruit(banana,0.7,yellow)).

Notice this file is not a list of predicates e.g.

fruit(apple,1.1,red).
fruit(banana,0.7,yellow).

but a journal of assert/1 statements to rebuild the predicates.

Close file in editor

Start Prolog (again).

?- listing(fruit).
ERROR: procedure `fruit' does not exist (DWIM could not correct goal)
^ Call: (13) call(prolog_listing:close_sources) ? creep
^ Exit: (13) call(prolog_listing:close_sources) ? abort
Execution Aborted
?- consult("C:/Prolog/persistency_demo.pl").
true.

?- listing(fruit).
:- dynamic fruit:fruit/3.

fruit:fruit(apple, 1.1, red).
fruit:fruit(banana, 0.7, yellow).

true.

?- current_fruit(X, Y, Z).
X = apple,
Y = 1.1,
Z = red.

?- fruit:fruit(X,Y,Z).
X = apple,
Y = 1.1,
Z = red ;
X = banana,
Y = 0.7,
Z = yellow.

In reading the help and looking at the source code on GitHub I see that via Meta predicates four new predicates are noted as being created:

name(Arg, ...)
assert_name(Arg, ...)
retract_name(Arg, ...)
retractall_name(Arg, ...)

based on a use of persistent/1, e.g.

:- persistent(fruit(name:atom, price:float, color:atom)).

creates

fruit/3
assert_fruit/3
retract_fruit/3
retractall_fruit/3

These can be seen using listing/1.
Note there are 5 predicates created; asserta_fruit/3 is the addition.

?- listing(fruit:X).

add_fruit(Name, Price, Color) :-
    with_mutex(fruit_db, assert_fruit(Name, Price, Color)).

current_fruit(Name, Price, Color) :-
    with_mutex(fruit_db, fruit(Name, Price, Color)).

:- dynamic fruit/3.

asserta_fruit(A, B, C) :-
    must_be(atom, A),
    must_be(float, B),
    must_be(atom, C),
    true,
    persistency:db_asserta(fruit:fruit(A, B, C)).

assert_fruit(A, B, C) :-
    must_be(atom, A),
    must_be(float, B),
    must_be(atom, C),
    true,
    persistency:db_assert(fruit:fruit(A, B, C)).

retract_fruit(A, B, C) :-
    persistency:db_retract(fruit:fruit(A, B, C)).

retractall_fruit(A, B, C) :-
    persistency:db_retractall(fruit:fruit(A, B, C)).
true.

In the help is the note

Thread safety must now be provided at the user-level. Can we provide generic thread safety? Basically, this means that we must wrap all exported predicates. That might better be done outside this library.

This is accomplished by wrapping any of the 5 generated predicates with with_mutex/2


Questions:

  1. Read data with_mutex/2.
    I understand the need for with_mutex/2 for writing to the file, but as in the example, is with_mutex/2 needed for reading the file? I ask because with_mutex/2 reads

Execute Goal while holding MutexId. If Goal leaves choice points, these are destroyed (as in once/1).

If so then should fruit:fruit/3 be used to get all of the results, or should a new predicate be created that can read all of the values and use with_mutex/2? If a new predicate is needed an example is desired.

  1. Looking at persistence file.
    I would like to be able to update the persistence file then release the lock on the file to view it without ending the Prolog session.

Once the predicate db_attach/2 is executed the file can not be viewed using as a text file using an editor or even copied to view the copy. To currently update the persistence file and release the lock requires using halt/0 which ends Prolog session. Using db_detach/0 also did not release the lock on the file.

Is there any way other than using halt/0 to update the persistence file then release the lock on the file while leaving the Prolog session active?

  1. Meaning of word database.
    The help documentation for db_attach/1 reads Use File as persistent database. This is what lead me to believing the persistence file would contain predicates without assert.

Should the persistence file really be considered a journal?
Or is that in the world of Prolog, database is the correct name for this type of file?

If journal is the correct word, should the associated predicate names and documentation be changed so as not to confuse others upon first reading who as me see the word database and think predicates without assert, and having the word journal to mean a file with asserts, or a file that can play back commands?


This is just some feedback on how I am using library(persistency).

While library(persistency) did not work as I first thought, it is proving useful for my use case. During the parsing a large data file (3 Gigabytes) there are many values that are codes. While some of the codes are unique for each entry and in the hundreds of thousands, the size of some of the sets of codes is more in the tens to hundreds. As a consistency check while parsing the individual codes are collected into files for review after parsing.

For each code I add two predicates to the module using library(persistency)),

  1. To check if the code exists in the database.
exists_eco(Identifier):-
  with_mutex(facts_journal, eco(Identifier)).
  1. To add the code to the database.
add_eco(Identifier):-
  with_mutex(facts_journal, assert_eco(Identifier)).

Then in the parser these are used as such

    {
        (
            % Does it exist
            exists_eco(Eco_id)
        ;
            % If not then add it
            add_eco(Eco_id)
        )
    }.

Here is an example module using library(persistency)

:- module(
  facts,
  [
    add_eco/1,          % +Identifier:string
    exists_eco/1        % ?Identifier:string
  ]
).

:- use_module(library(persistency)).

:- persistent(eco(identifier:string)).

:- initialization(db_attach('d:/cellular information/facts/facts.journal', [])).

exists_eco(Identifier):-
  with_mutex(facts_journal, eco(Identifier)).

add_eco(Identifier):-
  with_mutex(facts_journal, assert_eco(Identifier)).

If all of this reminds you of creating tables in a relational database during normalization, that is what I think while doing this.

This is a nice library to spend a few minutes with to understand and have in your tool box. Glad I took the time to understand it. Thanks to all who created it. :+1:

#2

Question 1: No, with_mutex/2 is generally not required for read operations. An exception is when you read, make a decision based upon what was read, and then update with a write as the read value may have changed prior to the write.

Question 2: db_sync/1 will do this for you. First try db_sync(always) so you can see it. Then consider one of the other options, such as db_sync(gc(40)).

Question 3: The file isn’t a true journal as syncing it will truncate the history. It does contain structured data that is accessible in different ways, so meets the definition of a database. The inclusion or exclusion of the functor assert is debatable, personally I don’t mind so long as it works, the purpose of this file is primarily to be read by machine rather than human.