To get an idea of how this works, start with tests.lsp
First, when open-persistent-store is called, the persistent store is (obviously) opened. If the store doesn't exist, it is created. It is easier to write code with the assumption that there is always a persistent state that can be updated rather than having special code deal with a once only initialization.
All interaction with the contents of the store takes place within an atomic transaction. When the transaction ends, either all the updates to the store take place, or none do. This allows the programmer to briefly ignore consistency requirements during computation so long as the end result is consistent. The interface is fairly simple:
call-with-transaction pstore transaction-type reason receiver
pstore - a persistent store
transaction-type - one of :read-only, :read-write, or :read-cons
reason - a human readable string describing the purpose of the transaction
receiver - a procedure of one argument (the transaction object) which is invoked during the transaction
In the usual, expected case, the receiver runs, possibly makes updates to the store, and returns a value. When the receiver returns, the transaction commits (finishes) and the changes to the store are applied atomically.
In the less usual case, the transaction may be aborted. In this case, no (detectable) changes are made to the store.
The decision to commit or abort is usually made implicitly: if the receiver procedure returns normally, the transaction commits, if it performs a non-local exit, via a throw or through error recovery, the transaction aborts. However, the programmer can explicitly cause the transaction to commit or abort through a call to transaction/commit or transaction/abort.
The transaction-type indicates what kind of transaction is to be performed. Naturally :read-write is used to update the store and :read-only is used if no update is necessary. :read-cons is for the case of a transaction that only creates new persistent objects and does not change existing objects. The implementation ensures that transactions are isolated (transaction in progress are not visible to each other) and serializable (committed transactions can be placed in a consistent order even if executed in parallel). :read-only transactions only have to be consistent throughout execution, so many can (in theory) be run in parallel. :read-write transactions can be run in parallel only if they do not interfere with each other. :read-cons transactions are a special case of :read-write. Since new objects are not visible outside the transaction until the transaction commits, there can be no interference between multiple :read-cons transactions. (Support for :read-cons is sketchy, though. Although there should be no interference, each transaction must issue unique object ids and thus they do interfere implicitly. There are ways to deal with this, but I didn't implement them.)
The reason is simply a string that is recorded along with the transaction. This is to help the user understand the sequence of transactions when examining the history of the store.
The receiver is invoked on an object representing the transaction. It is often ignored because the implicit commit or abort behavior is usually used, but the argument is there if desired.
During the transaction, the special variable *current-transaction* is bound to the transaction object.
So let's look at some code:
(let ((store (persistent-store/open pathname))
id
vid
(object1 '(this is a list))
(object2 #(this is a vector))
(object3 (list 1.0d0 -1.0d0 0.125d0 -0.125d0 1024.0d0 -1024.0d0 .3d0 -.3d0))
(object4 (list #x87654321 #x12345678 1 -1 2147483647 -2147483648
#*101010101010101010101010101
#*100000000000000000000000000
#*000000000000000000000000001))
o1id
o2id
o3id
o4id)
(call-with-transaction
store :read-write "Save some objects."
(lambda (transaction)
(declare (ignore transaction))
(setf o1id (persistent-object/save object1 store)
o2id (persistent-object/save object2 store)
o3id (persistent-object/save object3 store)
o4id (persistent-object/save object4 store)))))
This code saves some simple lisp objects to the store. persistent-object/save writes the object to the store and returns a unique integer (the object-id). Obviously we'll want a :read-write (or :read-cons) transaction.Later on, we call
(call-with-transaction
store :read-only "Verify objects."
(lambda (transaction)
(declare (ignore transaction))
(verify
(verify-one-return-value (verify-value-equal object1))
(persistent-object/find store o1id))
(verify
(verify-one-return-value (verify-value-equalp object2))
(persistent-object/find store o2id))
(verify
(verify-one-return-value (verify-value-equal object3))
(persistent-object/find store o3id))
(verify
(verify-one-return-value (verify-value-equalp object4))
(persistent-object/find store o4id))))
persistent-object/find takes the integer returned by persistent-object/save and returns the object. In this case we use a :read-only transaction, but we would use :read-write if we were going to modify any objects.
One has to be careful about equality. The object returned by persistent-object/find may not be EQ to the object saved. It would be very difficult to implement this in general, and it isn't needed in most cases. Symbols are treated specially to preserve EQ-ness.
One has to be careful about equality. The object returned by persistent-object/find may not be EQ to the object saved. It would be very difficult to implement this in general, and it isn't needed in most cases. Symbols are treated specially to preserve EQ-ness.

