Coverage report: /home/ellis/comp/core/lib/obj/store.lisp
Kind | Covered | All | % |
expression | 8 | 2070 | 0.4 |
branch | 0 | 172 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; store.lisp --- Data Store Protocols
3
;; Support for Lisp Stores.
7
;; Based on work from Elephant and XDB.
9
;; A STORE plays a similar role to ORMs in blub languages, but better since we
10
;; have CLOS and MOP for ultimate control. The purpose of a STORE is to
11
;; orchestrate the persistence of objects of a specific metaclass called
12
;; STORED. The metaclass adds an additional allocation target for slot-objects
13
;; which indicates that any access to them from Lisp will be delegated to the
16
;; The STORE itself is defined in this file and implements the 'controller'
17
;; side of the underlying protocol.
20
(in-package :obj/store)
22
(defparameter *store* obj/meta/stored::*default-store*)
24
;; support for swapping out multiple stores? compatibility matrix?
27
;; TODO 2024-12-05: eradicate direct usage of BTrees. otherwise why do we need
29
(defun make-btree (&optional (st *store*))
30
"Constructs a new BTree instance for use by the user. Each backend
31
returns its own internal type as appropriate and ensures that the
32
btree is associated with the store that created it."
35
(defun make-indexed-btree (&optional (sc *store*))
36
"Constructs a new indexed BTree instance for use by the user.
37
Each backend returns its own internal type as appropriate and
38
ensures that the btree is associated with the store
40
(build-indexed-btree sc))
43
(defclass dup-btree (btree) ())
45
(defgeneric build-dup-btree (store)
47
"Construct a btree of the appropriate type corresponding to this store."))
49
(defun make-dup-btree (&optional (store *store*))
50
(build-dup-btree store))
53
;; default implementation of simple sets using btrees
54
(defclass pset (stored-collection) ()
55
(:documentation "An unordered stored collection of unique elements according to serializer
58
(defgeneric insert-item (item pset)
59
(:documentation "Insert a new item into the pset"))
61
(defgeneric remove-item (item pset)
62
(:documentation "Remove specified item from pset"))
64
(defgeneric map-pset (fn pset)
65
(:documentation "Map operator for psets"))
67
(defgeneric find-item (item pset &key key test)
68
(:documentation "Find a an item in the pset using key and test"))
70
(defgeneric pset-list (pset)
71
(:documentation "Convert items of pset into a list for processing"))
73
(defgeneric build-pset (sc)
74
(:documentation "Construct an empty default pset or backend specific pset.
75
This is an internal function used by make-pset"))
77
(defgeneric drop-pset (pset)
78
(:documentation "Release pset storage to database for reuse"))
80
(defsclass default-pset (pset)
81
((btree :accessor pset-btree :initarg :btree)))
84
(defmethod drop-instance ((pset pset))
89
(defmethod slot-value-using-class ((class stored-class) (instance stored-object) (slot-def set-valued-slot-definition))
90
"Ensure that there is a slot-set in the slot (lazy instantiation)"
94
(setf (slot-value-using-class class instance slot-def)
95
(build-slot-set (get-store instance))))))
97
(defmethod (setf slot-value-using-class)
98
(new-value (class stored-class) (instance stored-object) (slot-def set-valued-slot-definition))
99
"Setting a value adds it to the slot set"
100
(if (or (null new-value)
101
(subtypep (type-of new-value) 'slot-set))
103
(slot-makunbound-using-class class instance slot-def)
105
(insert-item new-value (slot-value-using-class class instance slot-def))))
107
(defmethod slot-makunbound-using-class ((class stored-class) (instance stored-object) (slot-def set-valued-slot-definition))
108
"Make sure we reclaim the pset storage"
109
(awhen (and (slot-boundp-using-class class instance slot-def)
110
(slot-value-using-class class instance slot-def))
115
(defmacro set-list (object slotname)
116
"Sugar for getting a list from a set slot"
117
`(slot-set-list (slot-value ,object ,slotname)))
119
(defmacro set-insert (item object slotname)
120
"Sugar for inserting items under #'equal from the set slot"
121
`(insert-item ,item (slot-value ,object ,slotname)))
123
(defmacro set-remove (item object slotname)
124
"Sugar for removing items via #'equal from the set slot"
125
`(remove-item ,item (slot-value ,object ,slotname)))
127
;; A generic slot set implementation
128
(defclass slot-set () ()
129
(:documentation "A proxy object for a set stored in a slot."))
131
(defsclass stored-slot-set (slot-set stored-pset) ()
132
(:documentation "A default slot-set implementation"))
134
(defgeneric build-slot-set (sc)
135
(:documentation "Construct an empty default pset or backend specific pset.
136
This is an internal function used by make-pset"))
138
(defgeneric slot-set-list (slot-set)
139
(:documentation "Convert items of pset into a list for processing")
140
(:method ((set stored-slot-set))
143
(defgeneric map-slot-set (fn slot-set)
144
(:documentation "Map operator for psets")
145
(:method (fn (set stored-slot-set))
148
(defgeneric drop-slot-set (pset)
149
(:documentation "Release pset storage to database for reuse")
150
(:method ((set stored-slot-set))
151
(drop-instance set)))
154
(defmethod slot-value-using-class
155
((class stored-class) (instance stored-object) (slot-def association-slot-definition))
156
(if (eq (association-type slot-def) :ref)
158
(get-associated instance slot-def)))
160
(defmethod (setf slot-value-using-class)
161
(new-value (class stored-class) (instance stored-object) (slot-def association-slot-definition))
162
(add-association instance (slot-definition-name slot-def) new-value)
165
(defmethod slot-boundp-using-class
166
((class stored-class) (instance stored-object) (slot-def association-slot-definition))
167
(when (eq (association-type slot-def) :ref)
170
(defmethod slot-makunbound-using-class
171
((class stored-class) (instance stored-object) (slot-def association-slot-definition))
172
(when (eq (association-type slot-def) :ref)
173
(remove-association-end class instance slot-def nil)
174
(call-next-method))) ;; remove storage
177
;; =========================
179
;; =========================
181
(defun type-check-association (instance slot-def other-instance)
182
(when (null other-instance)
183
(return-from type-check-association t))
184
(unless (subtypep (type-of other-instance) (foreign-classname slot-def))
185
(cerror "Ignore and return"
186
"Value ~A written to association slot ~A of instance ~A
187
of class ~A must be a subtype of ~A"
188
other-instance (foreign-slotname slot-def) instance
189
(type-of instance) (foreign-classname slot-def))
190
(return-from type-check-association nil))
191
(unless (equal (spec instance) (spec other-instance))
192
(cerror "Ignore and return"
193
"Cannot association objects from different stores:
194
~A is in ~A and ~A is in ~A"
195
instance (get-store instance)
196
other-instance (get-store other-instance))
197
(return-from type-check-association nil))
200
(defun get-associated (instance slot-def)
201
(let* ((fclass (get-foreign-class slot-def))
202
(fslot (get-foreign-slot fclass slot-def))
203
(sc (get-store instance))
204
(index (get-association-index fslot sc)))
205
(flet ((map-obj (value oid)
206
(declare (ignore value))
207
(store-recreate-instance sc oid)))
208
(declare (dynamic-extent (function map-obj)))
209
(map-btree #'map-obj index :value (oid instance) :collect t))))
212
;; ==========================
214
;; ==========================
216
(defun update-association-end (class instance slot-def target)
217
"Get the association index and add the target object as a key that
218
refers back to this instance so we can get the set of referrers to target"
219
(let ((index (get-association-index slot-def (get-store instance))))
220
(when (and (eq (association-type slot-def) :ref)
221
(slot-boundp-using-class class instance slot-def))
222
(remove-kv (oid (slot-value-using-class class instance slot-def)) (oid instance) index))
223
(when (not (null instance))
224
(setf (get-value (oid target) index) (oid instance)))))
226
(defun remove-association-end (class instance slot-def associated)
227
(let ((index (get-association-index slot-def (get-store instance))))
228
(if (and (eq (association-type slot-def) :ref)
229
(slot-boundp-using-class class instance slot-def))
230
(remove-kv (oid (slot-value-using-class class instance slot-def)) (oid instance) index)
231
(when associated ;it is possible that the original association
232
;slot was not bound at the time of
233
;deletion. thus, remove the entry only when
235
(remove-kv (oid associated) (oid instance) index)))))
237
(defun update-other-association-end (class instance slot-def other-instance)
238
"Update the association index for the other object so that it maps from
239
us to it. Also add error handling."
240
(declare (ignore class))
241
(let* ((fclass (class-of other-instance))
242
(fslot (get-foreign-slot fclass slot-def))
243
(sc (get-store other-instance)))
244
(update-association-end fclass other-instance fslot instance)
245
(when (eq (association-type slot-def) :ref)
246
(stored-slot-writer sc instance other-instance (slot-definition-name fslot)))))
248
(defun get-foreign-class (slot-def)
249
(find-class (foreign-classname slot-def)))
251
(defun get-foreign-slot (fclass slot-def)
252
(find-slot-def-by-name fclass (foreign-slotname slot-def)))
254
;; =============================
255
;; Late-binding Initialization
256
;; =============================
258
(defun get-association-index (slot-def sc)
259
(ifret (get-association-slot-index slot-def sc)
260
(aif (get-store-association-index slot-def sc)
261
(progn (add-association-slot-index it slot-def sc) it)
262
(let ((new-idx (make-dup-btree sc)))
263
(add-slot-index sc new-idx (association-slot-base slot-def) (slot-definition-name slot-def))
264
(add-association-slot-index new-idx slot-def sc)
267
(defun get-store-association-index (slot-def sc)
268
(let* ((master (index-table sc))
269
(base (association-slot-base slot-def))
270
(slotname (slot-definition-name slot-def)))
271
(get-value (cons base slotname) master)))
273
;; ===============================
274
;; Association-specific slot API
275
;; ===============================
277
(defun add-association (instance slot associated)
278
(let* ((sc (get-store instance))
279
(class (class-of instance))
280
(slot-def (if (symbolp slot) (find-slot-def-by-name class slot) slot)))
281
(when (null slot-def)
282
(error "Slot ~A not found in class ~A for instance ~A" slot class instance))
283
(when (type-check-association instance slot-def associated)
284
(ensure-transaction (:store sc)
285
(case (association-type slot-def)
286
(:ref (update-association-end class instance slot-def associated)
287
(stored-slot-writer sc associated instance (slot-definition-name slot-def)))
288
(:m21 (update-other-association-end class instance slot-def associated))
289
(:m2m (update-association-end class instance slot-def associated)
290
(update-other-association-end class instance slot-def associated)))))))
292
(defun remove-association (instance slotname associated)
293
(let* ((class (class-of instance))
294
(fclass (class-of associated))
295
(slot-def (if (symbolp slotname) (find-slot-def-by-name class slotname) slotname))
296
(fslot (get-foreign-slot fclass slot-def))
297
(sc (get-store associated)))
298
(when (null slot-def)
299
(error "Slot ~A not found in class ~A for instance ~A" slotname class instance))
300
(when (type-check-association instance slot-def associated)
301
(ensure-transaction (:store sc)
302
(case (association-type slot-def)
303
(:ref (when (slot-boundp-using-class class instance slot-def)
304
(slot-makunbound-using-class class instance slot-def)))
305
(:m21 (when (slot-boundp-using-class fclass associated fslot)
306
(slot-makunbound-using-class fclass associated fslot)))
307
(:m2m (remove-association-end fclass associated fslot instance)
308
(remove-association-end class instance slot-def associated)))))))
310
(defun get-associations (instance slot)
311
(slot-value instance (if (symbolp slot) slot (slot-definition-name slot))))
313
(defun associatedp (instance slot associated)
314
(find associated (get-associations instance slot)))
317
(defgeneric next-oid (store)
319
"The source of unique object IDs."))
321
(let ((%next-oid -1))
322
(defmethod next-oid ((store list))
325
(defgeneric next-cid (store)
327
"The source of unique class schema IDs."))
329
(let ((%next-cid -1))
330
(defmethod next-cid ((store list))
333
(defun unindex-slot-value (sc key value old-name old-base)
334
(let* ((master (index-table sc))
335
(index (get-value (cons old-base old-name) master)))
336
(remove-kv key value index)))
339
(defclass stored-object-schema (object-schema upgradable-schema) ())
341
(defmethod print-object ((schema stored-object-schema) stream)
342
(print-unreadable-object (schema stream :type t)
343
(format stream "~A ~A (s: ~A p: ~A)"
344
(id schema) (schema-class-name schema)
345
(schema-successor schema) (schema-predecessor schema))))
347
(defun make-stored-object-schema (cid class-schema)
348
(let ((schema (logical-copy-schema 'stored-object-schema class-schema)))
349
(setf (id schema) cid)
352
(defun logical-copy-schema (type schema)
353
(assert (subtypep type 'schema:schema))
355
:class-name (schema-class-name schema)
356
:fields (copy-list (fields schema))))
358
(defun copy-schema (type schema)
359
(assert (subtypep type 'schema))
362
:name (schema:schema-class-name schema)
363
:successor (schema:schema-successor schema)
364
:predecessor (schema:schema-predecessor schema)
365
:fields (copy-array (schema:fields schema)))))
366
(when (subtypep (type-of schema) 'upgradable-schema)
367
(setf (id new) (id schema))
368
(setf (upgrade new) (upgrade schema))
369
(setf (version new) (version schema)))
373
(defmethod upgrade-db-instance ((instance stored-object) (new-schema upgradable-schema) (old-schema upgradable-schema) old-values)
374
"Upgrade a database instance from the old-schema to the new-schema.
375
This does mean loading it into memory (for now)!"
376
(let ((st (get-store instance))
377
(diff (schema-diff new-schema old-schema)))
378
(awhen (upgrade old-schema)
379
(apply-schema-change-fn instance it old-schema))
380
(loop for entry in diff do
381
(upgrade-instance-slot st instance (diff-type entry) (diff-recs entry) old-values))
382
(initialize-new-slots instance diff)
383
(set-instance-schema-id st (oid instance) (id new-schema))))
385
(defmethod upgrade-instance-slot (sc instance (type (eql :change)) recs old-values)
386
"Handle changes in class type"
387
(destructuring-bind (old-rec new-rec) recs
388
(with-slots ((old-type type) (old-name name) (old-args args)) old-rec
389
(cond ;; If it was not indexed, and now is, we have to notify the index of the new value
390
((and (member old-type '(:stored :cached))
391
(eq (slot-field-type new-rec) :indexed)
392
(slot-boundp instance old-name))
393
(setf (slot-value instance old-name) (slot-value instance old-name)))
394
;; If it was indexed, and the base index has changed
395
;; The new index will get updated as a natural part of the rest of the protocol
396
((and (member old-type '(:indexed :derived))
397
(not (eq (getf old-args :base)
398
(getf (slot-field-args new-rec) :base)))
399
(slot-boundp instance old-name))
400
(let ((slot-value (slot-value instance old-name)))
401
(unindex-slot-value sc slot-value (oid instance) old-name (getf old-args :base))))
402
;; If it was a stored slot and now isn't, drop it and add the new type back
403
((and (member old-type '(:stored :indexed :cached :derived))
404
(not (member (slot-field-type new-rec) '(:stored :indexed :cached :derived))))
405
(upgrade-instance-slot sc instance :rem (list old-rec) old-values)
406
(upgrade-instance-slot sc instance :add (list new-rec) old-values))
407
;; If the old slot was indexed
408
((and (eq old-type :indexed) (eq (slot-field-type new-rec) :indexed)
409
(not (eq (getf (slot-field-args old-rec) :base)
410
(getf (slot-field-args new-rec) :base))))
414
(defmethod upgrade-instance-slot (sc instance (type (eql :rem)) recs old-values)
415
"Handle slot removal and cleanup of values, such as sets"
416
(with-slots (type name args) (first recs)
417
(when (member type '(:stored :cached :indexed :derived))
418
(stored-slot-makunbound sc instance name))
419
(when (member type '(:indexed :derived))
420
(awhen (getf old-values name)
421
(unindex-slot-value sc (cdr it) (oid instance) name args)))
422
(when (eq type :set-valued)
423
(let ((set (and (stored-slot-boundp sc instance name)
424
(stored-slot-reader sc instance name))))
425
(when set (drop-btree set))
426
(slot-makunbound instance name)))))
428
(defmethod upgrade-instance-slot (sc instance (type (eql :add)) recs old-values)
429
"Not needed, new slots are initialized above"
430
(declare (ignore sc instance recs old-values))
433
(defun initialize-new-slots (instance diff)
434
(labels ((adding-stored? (entry)
435
(when (and (eq :add (diff-type entry))
436
(member (slot-field-type (first (diff-recs entry)))
437
'(:stored :indexed :cached :set-valued)))
438
(slot-field-name (first (diff-recs entry)))))
439
(change-to-stored? (entry)
440
(when (and (eq :change (diff-type entry))
441
(not (member (slot-field-type (first (diff-recs entry)))
442
'(:stored :indexed :cached :set-valued)))
443
(member (slot-field-type (second (diff-recs entry)))
444
'(:stored :indexed :cached :set-valued)))
445
(slot-field-name (second (diff-recs entry)))))
447
(or (adding-stored? entry)
448
(change-to-stored? entry)))
449
(compute-init-slots ()
450
(remove-if #'null (mapcar #'init-slot? diff))))
451
(apply #'shared-initialize instance (compute-init-slots) nil)))
453
(defmethod change-db-instance ((current stored-object) previous
454
new-schema old-schema)
455
"Change a database instance from one schema & class to another
456
These are different objects with the same oid"
457
(let ((sc (get-store current))
459
(diff (schema-diff new-schema old-schema)))
460
;; do we need to pass the stored object? Transient ops require previous?
461
(awhen (upgrade old-schema)
462
(apply-schema-change-fn current it old-schema))
463
;; Handle changed slots
464
(loop for entry in diff do
465
(change-instance-slot sc current previous (diff-type entry) (diff-recs entry)))
466
;; Initialize new slots (is this done by default?)
467
(initialize-new-slots current diff)
468
(uncache-instance sc oid)
469
(set-instance-schema-id sc oid (id new-schema))))
471
(defmethod change-instance-slot (sc current previous (type (eql :change)) recs)
472
"Handle changes in class type"
475
;; (dump-btree (instance-table sc))
476
;; (dump-index (index-table sc))
477
(destructuring-bind (old-rec new-rec) recs
478
(with-slots ((old-type type) (old-name name) (old-args args)) old-rec
479
(with-slots ((new-type type) (new-name name) (new-args args)) new-rec
480
(cond ;; If it was not indexed, and now is, we have to notify the index of the new value (?)
481
((and (member old-type '(:stored :cached))
482
(eq new-type :indexed) (slot-boundp previous old-name))
483
(setf (slot-value previous old-name) (slot-value previous old-name)))
484
;; If the old slot was indexed, we definitely need to unindex it to avoid
485
;; having the objects hang around in the index
486
((and (eq old-type :indexed) (eq new-type :indexed)
487
(slot-boundp previous old-name))
488
(unindex-slot-value sc (slot-value previous old-name)
489
(oid previous) old-name (getf old-args :base))
490
(setf (slot-value current new-name) (slot-value previous old-name)))
491
((and (eq old-type :indexed) (slot-boundp previous old-name))
492
(unindex-slot-value sc (slot-value previous old-name)
493
(oid previous) old-name (getf old-args :base)))
494
;; If it was a stored slot and now isn't, drop it and add the new type back
495
((and (member old-type '(:stored :indexed :cached))
496
(not (member new-type '(:stored :indexed :cached))))
497
(change-instance-slot sc current previous :rem (list old-rec))
498
(change-instance-slot sc current previous :add (list new-rec)))
501
(defmethod change-instance-slot (sc current previous (type (eql :rem)) recs)
502
"Handle slot removal and cleanup of values, such as sets"
503
(declare (ignore current))
504
(with-slots ((prev-type type) (prev-name name) (prev-args args)) (first recs)
505
(cond ((member prev-type '(:stored :cached :indexed))
506
(slot-makunbound previous prev-name))
507
((eq type :set-valued)
508
(let ((set (and (stored-slot-boundp sc previous prev-name)
509
(stored-slot-reader sc previous prev-name))))
510
(when set (drop-btree set))
511
(slot-makunbound previous prev-name))))))
513
(defmethod change-instance-slot (sc current previous (type (eql :add)) recs)
514
"Not needed, new slots are initialized above"
515
(declare (ignore sc current previous recs))
518
(defgeneric temp-spec (type spec))
519
(defgeneric delete-spec (type spec))
520
(defgeneric copy-spec (type src dst))
523
(defgeneric recreate-instance (instance &rest initargs &key &allow-other-keys)
524
(:method ((instance t) &rest args)
525
(declare (ignore args))
527
(:method ((instance stored-object) &rest args &key oid schema (st *store*))
528
(declare (ignore args))
529
;; Initialize basic instance data
530
(initial-stored-setup instance :oid oid :store st)
531
;; Update db instance data
533
(let ((official-schema (lookup-schema st (class-of instance))))
534
(unless (eq (name schema) (name official-schema))
535
(upgrade-db-instance instance official-schema schema nil))))
536
;; Load cached slots, set, assoc values, etc.
537
(shared-initialize instance t :oid oid)
539
(:method ((instance stored-collection) &rest initargs &key oid (st *store*))
540
(declare (ignore initargs))
541
;; Initialize basic instance data
542
(initial-stored-setup instance :oid oid :store st)
543
;; Load cached slots, set, assoc values, etc.
544
(shared-initialize instance t :oid oid)
547
(defmethod recreate-instance-using-class ((class t) &rest initargs &key &allow-other-keys)
548
"Implement a subset of the make-instance functionality to avoid initialize-instance
549
calls after the initial creation time"
550
(apply #'recreate-instance (allocate-instance class) initargs))
552
;; Class Redefinition
553
(defmethod update-instance-for-redefined-class :around ((instance stored-object) added-slots discarded-slots property-list &rest initargs)
554
(declare (ignore discarded-slots added-slots initargs))
555
(let* ((st (get-store instance))
556
;; (class (class-of instance))
557
(current-schema (get-current-db-schema st (type-of instance))))
558
;; (unless (match-schemas (%class-schema class) current-schema))
561
(let ((prior-schema (aif (schema:schema-predecessor current-schema)
562
(get-store-schema st it)
563
(error "If the schemas mismatch, a derived store schema should have been computed"))))
564
(assert (and current-schema prior-schema))
565
(upgrade-db-instance instance current-schema prior-schema property-list)))))
567
(defmethod update-instance-for-different-class :after ((previous stored-object) (current stored-object)
569
;; Update db to new class configuration
570
;; - handle indices, removals, associations and additions
571
(let* ((sc (get-store current))
572
(current-schema (lookup-schema sc (class-of current)))
573
(previous-schema (lookup-schema sc (class-of previous))))
574
(assert (eq sc (get-store previous)))
575
(change-db-instance current previous current-schema previous-schema)
576
;; Deal with new stored slot, cached and transient initialization
577
(let* ((diff-entries (schema-diff current-schema previous-schema))
578
(add-entries (remove-if-not (lambda (entry) (eq :add (diff-type entry))) diff-entries))
579
(add-names (when add-entries (mapcar #'field-name (mapcan #'diff-recs add-entries)))))
580
(apply #'shared-initialize current add-names initargs))))
588
:documentation "Data store initialization functions are
589
expected to initialize :spec on the call to
591
;; Generic support for the object, indexing and root protocols
594
:documentation "This is an instance of the data store btree. It should have an OID that is
595
fixed in the code and does not change between sessions. Usually this is
596
something like 0, 1 or -1")
599
:documentation "Schema id to schema database table")
601
:reader schema-name-index
602
:documentation "Schema name to schema database table")
604
:accessor schema-cache :initform (make-cache-table :test 'eq)
605
:documentation "This is a cache of class schemas stored in the database indexed by classid")
607
:accessor schema-classes :initform nil
608
:documentation "Maintains a list of all classes that have a cached schema value so we can shutdown cleanly")
610
:accessor schema-cache-lock :initform (make-mutex :name "cache-lock")
611
:documentation "Protection for updates to the cache from multiple threads. Do not override.")
614
:reader instance-table
615
:documentation "Contains map of oid to class ids")
616
(instance-class-index
617
:reader instance-class-index
618
:documentation "A reverse map of class id to oid")
620
:accessor instance-cache :initform (make-cache-table :test 'eql)
622
"This is an instance cache and part of the metaclass protocol. Data stores
623
should not override the default behavior.")
625
:accessor instance-cache-lock :initform (make-mutex :name "instance-cache")
626
:documentation "Protection for updates to the cache from multiple threads. Do not override.")
627
;; Root table for all indices
631
"This is another root for class indexing that is also a data store specific
632
stored btree instance with a unique OID that persists between sessions. No
633
cache is needed because we cache in the class slots.")
634
(serializer :accessor serializer :initform nil)
635
(deserializer :accessor deserializer :initform nil)))
637
(defmethod print-object ((self store) stream)
638
(print-unreadable-object (self stream :type t)
639
(format stream "~A" (second (spec self)))))
641
(defmethod initialize-instance :before ((instance stored)
645
"Each stored instance has an oid and a home store spec"
646
(declare (ignore initargs))
647
(initial-stored-setup instance :oid oid :store store))
649
(defun initial-stored-setup (instance &key oid store)
652
(setf (oid instance) oid)
653
(register-new-instance instance (class-of instance) store))
654
(setf (spec instance) (spec store))
655
(cache-instance store instance))
657
(defun class-schema-id (st class)
658
(if (subtypep (class-name class) 'btree)
659
(default-class-id (class-name class) st)
660
(id (lookup-schema st class))))
662
(defmethod register-instance ((self list) class instance)
663
(set-instance-schema-id self (oid instance) (class-schema-id self class)))
665
(defmethod register-instance ((st store) cl instance)
666
(set-instance-schema-id st (oid instance) (class-schema-id st cl)))
668
(defmethod set-instance-schema-id ((st store) oid cid)
669
(let ((table (instance-table st)))
670
(delete-key oid table)
671
(setf (get-value oid table) cid)))
673
(defmethod get-instance-class ((st store) oid &optional classname)
674
"Get the class object using the oid or using the provided classname"
676
(return-from get-instance-class (find-class classname)))
677
(let ((cid (oid->schema-id oid st)))
679
(signal-missing-instance oid (spec st))
680
(return-from get-instance-class (find-class 'stored-object)))
681
(get-schema-id-class st cid)))
683
(defmethod get-schema-id-class ((st store) cid)
684
"Get the class given the schema id"
685
(aif (default-class-id-type cid st)
687
(let ((schema (get-store-schema st cid)))
688
(values (find-class (schema-class-name schema)) schema))))
690
(define-condition missing-stored-instance (simple-condition)
691
((oid :initarg :oid :accessor error-oid)
692
(spec :initarg :spec :accessor error-spec)))
694
(defun signal-missing-instance (oid spec)
695
(cerror "Return a proxy object"
696
'missing-stored-instance
697
:format-control "Instance with OID ~A is not stored in ~A"
698
:format-arguments (list oid spec)
702
(defmethod store-recreate-instance ((st store) oid &optional classname)
703
"Called by the deserializer to return an instance"
706
;; Quick test since only the GC deletes object references
707
(awhen (get-cached-instance st oid)
708
(return-from store-recreate-instance it))
709
;; Update cache unless someone has before us!
710
(with-mutex ((instance-cache-lock st))
711
(aif (get-cached-instance st oid) it
712
(multiple-value-bind (class schema) (get-instance-class st oid classname)
713
(recreate-instance-using-class class :oid oid :store st :schema schema)))))
714
(missing-stored-instance (e)
717
(defmethod get-slot-def-index ((def association-effective-slot-definition) sc)
718
"Since endpoints of an association implement an index we should be able to perform
719
inverted-index relation functions on them directly"
720
(get-association-index def sc))
722
(defun register-new-instance (instance class store)
723
(setf (oid instance) (next-oid store))
724
(register-instance store class instance))
726
(defun check-valid-store (store)
727
(if-let ((ok (subtypep (type-of store) 'store)))
729
(error "This function requires a valid store")))
731
(defmethod build-pset ((sc store))
732
"Default pset method; override if backend has better policy"
733
(let ((btree (make-dup-btree sc)))
734
(make-instance 'default-pset :btree btree :sc sc)))
736
(defun make-pset (&key items pset (store *store*))
737
(let ((new-pset (build-pset store)))
738
(when (and items pset)
739
(error "Can only initialize a new pset with item list or pset to copy, not both"))
742
(insert-item item new-pset))
745
(map-pset (lambda (item)
746
(insert-item item new-pset))
750
(defmethod insert-item (item (pset default-pset))
751
(setf (get-value item (pset-btree pset)) t)
754
(defmethod remove-item (item (pset default-pset))
755
(delete-key (pset-btree pset) item)
758
(defmethod find-item (item (pset default-pset) &key key (test #'equal))
759
(if (not (or key test))
760
(get-value item (pset-btree pset))
761
(map-btree (lambda (elt dc)
762
(declare (ignore dc))
763
(let ((cmpval (if key (funcall key elt) elt)))
764
(if (funcall test item cmpval)
765
(return-from find-item elt))))
768
(defmethod map-pset (fn (pset default-pset))
769
(map-btree (lambda (key value)
770
(declare (ignore value))
775
(defmethod pset-list ((pset default-pset))
776
(map-btree #'(lambda (k v)
779
(pset-btree pset) :collect t))
781
(defmethod drop-pset ((pset default-pset))
782
(ensure-transaction (:store *store*)
783
(awhen (pset-btree pset)
786
(defmethod build-slot-set ((sc store))
787
(let ((btree (make-btree sc)))
788
(make-instance 'stored-slot-set :btree btree :sc sc)))
790
(defmethod drop-instance ((inst stored-object))
791
(drop-instance-slots inst)
794
(defmethod drop-instance ((inst stored))
795
(let ((sc (get-store inst)))
796
(with-mutex ((instance-cache-lock sc))
797
(remcache (oid inst) (instance-cache sc)))
798
(delete-key (oid inst) (instance-table sc))))
800
(defun drop-instance-slots (instance)
801
"A helper function for drop-instance, that deletes the storage of
802
stored slots of instance from the db"
803
(let ((class (class-of instance)))
804
(loop for slot-def in (class-slots class)
805
when (stored-p slot-def)
806
do (slot-makunbound-using-class class instance slot-def))))
808
(defun dropped-instance-p (st oid)
809
"An instance has not been dropped if it is in the instance
810
table and has a valid class id"
811
(multiple-value-bind (cid found?)
812
(get-value oid (instance-table st))
815
(defmethod oid->schema-id (oid (st store))
816
(get-value oid (instance-table st)))
818
(defgeneric default-class-id (base-type sc)
819
(:documentation "A method implemented by the store for providing
820
fixed class ids for basic btree derivative types")
821
(:method ((base-type t) (sc list))
824
(defgeneric default-class-id-type (id sc)
825
(:documentation "A method implemented by the store which provides
826
the type associated with a default id or nil if the id does not match"))
828
(defgeneric reserved-oid-p (sc oid)
829
(:documentation "Is this OID reserved by the store? GC doesn't touch"))
831
(defmethod add-class-store-schema (st (class stored-class) schema)
832
;; NOTE: Needs to be lock protected
833
(pushnew (class-name class) (schema-classes st))
834
(remove-class-store-schema st class)
835
(setf (get-store-schemas class)
836
(acons (spec st) schema (get-store-schemas class))))
838
(defmethod remove-class-store-schema (st (class stored-class))
839
;; NOTE: Needs to be lock protected
840
(setf (get-store-schemas class)
841
(remove (spec st) (get-store-schemas class)
842
:key #'car :test #'equalp)))
844
(defmethod get-class-store-schema (st (class stored-class))
845
(awhen (assoc (spec st) (get-store-schemas class))
848
(defmethod lookup-schema ((st store) (class stored-class))
849
"Get the latest db class schema from caches, etc."
850
;; Lookup class cached version
851
(awhen (get-class-store-schema st class)
852
(when (eq (schema:schema-successor it) nil)
853
(return-from lookup-schema it)))
854
;; Lookup stored version
855
(aif (get-current-db-schema st (class-name class))
858
(add-class-store-schema st class it))
860
(create-store-schema st class)))
862
(defmethod get-store-schema ((st store) schema-id &optional class)
863
"Find the db class schema by schema id. CLASS needs to be supplied
864
if the class object isn't registered via (SETF FIND-CLASS) yet."
865
(assert (typep schema-id 'fixnum))
866
;; Lookup in store cache
867
(std/macs:ifret (get-cache schema-id (schema-cache st))
868
;; Lookup in store table
869
(let* ((schema (get-value schema-id (schema-table st)))
870
(class (or class (find-class (schema:schema-class-name schema)))))
872
;; Update store cache
873
(with-mutex ((schema-cache-lock st))
874
(setf (get-cache schema-id (schema-cache st)) schema))
875
;; Also cache in class slot
876
(add-class-store-schema st class schema)
879
(defmethod create-store-schema ((st store) class)
880
"We don't have a cached store schema, so create a new one"
881
(ensure-finalized class)
882
(let ((schema (make-stored-object-schema (next-cid st) (get-class-schema class))))
884
(setf (get-value (id schema) (schema-table st))
886
;; Let get-store-schema cache it for us
887
(get-store-schema st (id schema) class)))
889
(defmethod update-store-schema ((st store) schema &optional update-cache)
890
"Use this to update the schema version that is on store and in
891
all the various caches"
892
(assert (typep schema 'stored-object-schema))
894
(let ((schema-id (id schema)))
895
(set-store-schema st schema-id schema)
897
(with-mutex ((schema-cache-lock st))
898
(setf (get-cache schema-id (schema-cache st)) schema))
899
(awhen (find-class (schema:schema-class-name schema) nil)
900
(add-class-store-schema st (find-class (schema:schema-class-name schema)) schema)))))
903
(defmethod set-store-schema ((st store) schema-id schema)
904
"Insert a new schema into the store table"
905
(setf (get-value schema-id (schema-table st))
908
(defmethod remove-store-schema ((st store) schema-id)
909
"Remove a schema from the store table; uncache separately"
910
(delete-key schema-id (schema-table st)))
912
(defmethod uncache-store-schema ((st store) schema-id)
915
(with-mutex ((schema-cache-lock st))
916
(remcache schema-id (schema-cache st)))
917
(remove-class-store-schema st (get-schema-id-class st schema-id)))
918
(program-error (e) ;; in case the class is gone for some reason
919
(warn "Error ~A in uncache-store-schema , ignoring" e)
922
(defun get-current-db-schema (sc name)
923
(awhen (sort (get-db-schemas sc name)
927
(defun get-db-schemas (st classname)
928
"Return schemas ordered oldest to youngest (ascending cids)"
930
(map-btree #'(lambda (cname schema)
931
(declare (ignore cname))
933
(schema-name-index st)
934
:value classname :collect t)
938
(defun update-derived-slot (class instance derived-slot-def)
939
"Make a copy of the functionality here to be more efficient"
940
(let ((sc (get-store instance)))
941
(multiple-value-bind (new-value index?)
942
(funcall (derived-fn derived-slot-def) instance)
944
(update-slot-index sc class instance derived-slot-def new-value)
945
(stored-slot-writer sc new-value instance
946
(slot-definition-name derived-slot-def))))))
948
(defun derived-index-updater (class instance written-slot-def)
949
"Compute the derived indices to update from the slot-def that is
950
being written to. Should be called in a transaction"
951
(awhen (derived-slot-triggers written-slot-def)
952
(dolist (derived-slot-def it)
953
(update-derived-slot class instance derived-slot-def))))
955
(defun update-slot-index (sc class instance slot-def new-value)
956
"Update an index value when written"
957
(let ((oid (oid instance)))
958
(let* ((idx (get-slot-def-index slot-def sc))
959
(old-value-bound-p (slot-boundp-using-class class instance slot-def))
960
(old-value (when old-value-bound-p
961
(slot-value-using-class class instance slot-def))))
963
(setf idx (ensure-slot-def-index slot-def sc)))
964
(when old-value-bound-p
965
(remove-kv old-value oid idx))
966
(setf (get-value new-value idx) oid))))
968
(defun get-store-index (slot-def sc)
969
"Get the slot-def's index from the store"
970
(let* ((master (index-table sc))
971
(base (indexed-slot-base slot-def))
972
(name (slot-definition-name slot-def)))
973
(get-value (cons base name) master)))
975
(defun ensure-slot-def-index (slot-def sc)
976
"If a slot's index does not exist, create it"
977
(aif (get-store-index slot-def sc)
978
(progn (add-slot-def-index it slot-def sc) it)
979
(let ((new-idx (make-btree sc)))
980
(add-slot-index sc new-idx (indexed-slot-base slot-def) (slot-definition-name slot-def))
981
(add-slot-def-index new-idx slot-def sc)
984
(defmethod add-slot-index ((sc store) new-index class-name index-name)
985
"Add it to the index table and the class slot def"
986
(setf (get-value (cons class-name index-name) (index-table sc))
989
(defmethod drop-slot-index ((sc store) class-name index-name)
990
(clear-slot-def-index (find-slot-def-by-name (find-class class-name) index-name) sc)
991
(delete-key (cons class-name index-name) (index-table sc)))
993
(defmethod rebuild-slot-index ((sc store) class-name index-name)
994
(drop-slot-index sc class-name index-name)
995
(let ((class (find-class class-name)))
996
(ensure-slot-def-index (find-slot-def-by-name class index-name) sc)
997
(map-class #'(lambda (instance)
998
(when (slot-boundp instance index-name)
999
(update-slot-index sc class instance
1000
(find-slot-def-by-name class index-name)
1001
(slot-value instance index-name))))
1004
(defun rebuild-slot-indices (sc class)
1005
"Rebuild all slot indices for CLASS, or all known classes
1006
if CLASS is NIL. CLASS may be a class or class name."
1007
(let* ((classes (list* (etypecase class
1008
(null (known-classes sc))
1010
(symbol (find-class class)))))
1011
(class-names (mapcar #'class-name classes)))
1012
(loop for class in classes
1013
for class-name in class-names
1015
(format t "=== class ~S~%" class)
1016
(dolist (slotname (indexed-slot-names class))
1017
(ensure-finalized class) ; for CLASS-SLOTS
1018
(when (member slotname (class-slots class) :key #'slot-definition-name)
1019
(format t "slot index ~S~%" slotname)
1020
(rebuild-slot-index sc class-name slotname)))))))
1022
(defun known-classes (sc)
1023
"Return all classes that are known both to SC and the current
1027
(maphash (lambda (cid schema)
1028
(declare (ignore cid))
1029
(let ((class (find-class (name schema) nil)))
1031
(warn "Class ~S not defined, ignoring." (name schema)))
1033
(schema-table sc)))))
1035
(defun map-class (fn class &key collect oids (sc *store*))
1036
"Perform a map operation over all instances of class. Takes a
1037
function of one argument, a class instance."
1038
(flet ((map-fn (cidx pcidx oid)
1039
(declare (ignore cidx pcidx))
1040
(funcall fn (store-recreate-instance sc oid)))
1041
(map-oid-fn (cidx pcidx oid)
1042
(declare (ignore cidx pcidx))
1044
(declare (dynamic-extent (function map-fn) (function map-oid-fn)))
1045
(let* ((classobj (if (symbolp class) (find-class class) class))
1046
(classname (if (symbolp class) class (class-name class)))
1047
(db-schemas (get-db-schemas sc classname))
1048
(schema-ids (if db-schemas
1049
(mapcar #'id (reverse db-schemas))
1050
(list (id (lookup-schema sc (if (symbolp class) (find-class class) class)))))))
1051
(unless (class-indexing-enabled-p classobj)
1052
(cerror "Ignore and return nil"
1053
"Class ~A is not indexed" classname)
1054
(return-from map-class nil))
1055
;; (dump-schema-status sc classname)
1056
(loop for schema-id in schema-ids appending
1057
(map-index (if oids #'map-oid-fn #'map-fn)
1058
(instance-class-index sc)
1060
:collect collect)))))
1062
(defun map-inverted-index (fn class index &rest args &key start end (value nil value-p) from-end collect oids)
1063
"map-inverted-index maps a function of two variables, taking key
1064
and instance, over a subset of class instances in the order
1065
defined by the index. Specify the class by classname or class object
1066
and index by quoted name. The index may be a slot index, derived index,
1067
or a valued association slot.
1069
To map only a subset of key-value pairs, specify the range
1070
using the :start and :end keywords; all elements greater than
1071
or equal to :start and less than or equal to :end will be
1072
traversed regardless of whether the start or end value is in
1075
Use nil in the place of start or end to specify the first
1076
element or last element, respectively.
1078
To map a single value, iff it exists, use the :value keyword.
1079
This is the only way to travers all nil values.
1081
To map from :end to :start in descending order, set :from-end
1082
to true. If :value is used, :from-end is ignored
1084
The 'oids' argument passes the oid of the instance to the provided
1085
function instead of the recreated instance."
1086
(declare (dynamic-extent args)
1088
(let* ((btree (if (symbolp index)
1089
(find-inverted-index class index)
1091
(class-obj (etypecase class
1092
(symbol (find-class class))
1093
(stored-class class)))
1094
(sc (get-store btree)))
1095
(flet ((map-obj (value oid)
1096
(funcall fn value (store-recreate-instance sc oid))))
1097
(cond ((eq 'association-effective-slot-definition (type-of (find-slot-def-by-name class-obj index)))
1098
(map-btree (if oids fn #'map-obj) btree :value (oid value) :collect collect))
1099
(value-p (map-btree (if oids fn #'map-obj) btree :value value :collect collect))
1100
(t (map-btree (if oids fn #'map-obj) btree :start start :end end :from-end from-end :collect collect))))))
1102
(defun get-unique-values (index &aux values)
1103
(with-btree-cursor (cur index)
1104
(multiple-value-bind (valid? value oid)
1106
(declare (ignore oid))
1110
(multiple-value-bind (valid? value oid)
1111
(btree::cursor-next-nodup cur)
1112
(declare (ignore oid))
1114
(return-from get-unique-values (nreverse values)))
1115
(push value values)))))))
1117
(defmethod sb-sequence:emptyp ((btree btree))
1118
(with-btree-cursor (cur btree)
1119
(multiple-value-bind (valid k) (cursor-next cur)
1120
(declare (ignore k))
1121
(cond ((not valid) ;; truly empty
1123
((eq btree (store-root (get-store btree)))
1124
(not (cursor-next cur)))
1127
(defmethod find-inverted-index ((class symbol) slot &key (null-on-fail nil) (sc *store*))
1128
(find-inverted-index (find-class class) slot :null-on-fail null-on-fail :sc sc))
1130
(defmethod find-inverted-index ((class stored-class) slot &key ignore-errors (store *store*))
1131
(ensure-finalized class)
1132
(flet ((assert-error ()
1133
(when ignore-errors (return-from find-inverted-index nil))
1134
(cerror "Return null and continue?"
1135
"Inverted slot index ~A not found for class ~A with indexed slots: ~A"
1136
slot (class-name class) (indexed-slot-names class))))
1137
(let ((slot-def (find-slot-def-by-name class slot)))
1138
(unless (and slot-def
1139
(or (eq (type-of slot-def) 'indexed-effective-slot-definition)
1140
(eq (type-of slot-def) 'derived-index-effective-slot-definition)))
1142
(let ((idx (get-slot-def-index slot-def store)))
1144
(setf idx (ensure-slot-def-index slot-def store)))
1147
;;; Controller Protocol
1148
(defgeneric open-store (st &key recover recover-fatal thread &allow-other-keys)
1149
(:documentation "Opens the underlying environment and all the necessary
1150
database tables. Different data stores may use different keys so all methods
1151
should &allow-other-keys. There are three standard keywords: :recover,
1152
:recover-fatal and :thread. Recover means that recovery should be checked for
1153
or performed on startup. Recover fatal means a full rebuild from log files is
1154
requested. Thread merely indicates to the data store that it is a threaded
1155
application and any steps that need to be taken (for example transaction
1156
implementation) are taken. :thread is usually true."))
1158
(defgeneric close-store (st)
1159
(:documentation "Close the db handles and environment. Should be in a state where lisp could be
1160
shut down without causing an inconsistent state in the db. Also, the object
1161
could be used by open-store to reopen the database."))
1163
(defgeneric optimize-layout (st &key &allow-other-keys)
1164
(:documentation "If supported, speed up the index and allocation by freeing up any available
1165
storage and return it to the free list. See the methods of data stores to
1166
determine what options are valid. Supported both on stores (all btrees and
1167
stored slots) and specific btrees."))
1170
;;; Controller User API
1174
(defun close-all-stores ()
1175
(loop for pair in *stores*
1176
do (close-store (cdr pair))))
1178
;; (pushnew 'close-all-stores sb-ext:*exit-hooks*)
1180
(defmacro with-open-store (spec &body body)
1181
"Executes the body with an open store,
1182
unconditionally closing the store on exit."
1183
`(let ((*store* nil))
1184
(declare (special *store*))
1188
(close-store *store*))))
1190
(defmacro with-store ((store) &body body)
1192
`(let* ((,ref ,store)
1197
(declare (special *store*))
1203
(defun add-to-root (key value &key (st *store*))
1204
"Add an arbitrary stored thing to the root, so you can
1205
retrieve it in a later session. Anything referenced by an
1206
object added to the root is considered reachable and thus live"
1207
(declare (type store st))
1208
;; (assert (not (eq key *elephant-properties-label*)))
1209
(setf (get-value key (store-root st)) value))
1211
(defun get-from-root (key &key (st *store*))
1212
"Get the value associated with key from the root. Returns two
1213
values, the value, or nil, and a boolean indicating whether a
1214
value was found or not (so you know if nil is a value or an
1215
indication of non-presence)"
1216
(declare (type store st))
1217
(get-value key (store-root st)))
1219
(defun root-existsp (key &key (st *store*))
1220
"Test whether a given key is instantiated in the root"
1221
(declare (type store st))
1222
(if (btree:existsp key (store-root st))
1226
(defun remove-from-root (key &key (st *store*))
1227
"Remove something from the root by the key value"
1228
(declare (type store st))
1229
(delete-key key (store-root st)))
1231
(defun map-root (fn &key (st *store*))
1232
"Takes a function of two arguments, key and value, to map over
1233
all key-value pairs in the root"
1234
(map-btree fn (store-root st)))
1237
(defmethod slot-value-using-class ((class stored-class) (instance stored-object) (slot-def stored-slot-definition))
1238
"Get the slot value from the database."
1239
(let ((name (slot-definition-name slot-def)))
1240
(stored-slot-reader (get-store instance) instance name)))
1242
(defmethod (setf slot-value-using-class) (new-value (class stored-class) (instance stored-object) (slot-def stored-slot-definition))
1243
"Set the slot value in the database."
1244
(let ((name (slot-definition-name slot-def)))
1246
((derived-slot-triggers slot-def)
1247
(stored-slot-writer (get-store instance) new-value instance name)
1248
(derived-index-updater class instance slot-def))
1249
(t (stored-slot-writer (get-store instance) new-value instance name))))
1252
(defmethod slot-boundp-using-class ((class stored-class) (instance stored-object) (slot-def stored-slot-definition))
1253
"Checks if the slot exists in the database."
1255
(let ((name (slot-definition-name slot-def)))
1256
(stored-slot-boundp (get-store instance) instance name))))
1258
(defmethod slot-boundp-using-class ((class stored-class) (instance stored-object) (slot-name symbol))
1259
"Checks if the slot exists in the database."
1260
(loop for slot in (class-slots class)
1261
for matches-p = (eq (slot-definition-name slot) slot-name)
1263
finally (return (if (and matches-p
1264
(subtypep (type-of slot) 'stored-slot-definition))
1265
(stored-slot-boundp (get-store instance) instance slot-name)
1266
(call-next-method)))))
1268
(defmethod slot-makunbound-using-class ((class stored-class) (instance stored-object) (slot-def stored-slot-definition))
1269
"Removes the slot value from the database."
1270
(stored-slot-makunbound (get-store instance) instance (slot-definition-name slot-def)))
1272
(defun valid-stored-reference-p (object sc)
1273
"Ensures that object can be written as a reference into store sc"
1274
(or (not (slot-boundp object 'spec))
1275
(eq (spec object) (spec sc))))
1277
(define-condition cross-store-error (error)
1278
((object :accessor error-object :initarg :object)
1279
(home :accessor error-home-store :initarg :home-store)
1280
(guest :accessor error-guest-store :initarg :guest-store))
1281
(:documentation "An error condition raised when an object is being written into a data store other
1282
than its home store")
1283
(:report (lambda (condition stream)
1284
(format stream "Attempted to write object ~A with home store ~A into store ~A"
1285
(error-object condition)
1286
(error-home-store condition)
1287
(error-guest-store condition)))))
1289
(defun signal-cross-store-error (object sc)
1290
(cerror "Proceed to write incorrect reference"
1291
'cross-reference-error
1293
:home-store (get-store object)
1297
(defmacro defstore (name super spec &rest options)
1298
"Define a new STORE class.")
1300
;; TODO 2024-12-05: do we want to pass DB by value here (in the environment of
1301
;; WITH-STORE) or are we better off binding DATABASE instances as
1303
(defmacro with-store ((sym &rest initargs &key &allow-other-keys) &body body)
1304
"Similar to WITH-DB but for STORE objects instead of DATABASEs.
1306
INITARGS may contain any number keys that have been registered with the
1307
current *STORE-BACKEND*.")