Coverage report: /home/ellis/comp/core/lib/obj/db.lisp
Kind | Covered | All | % |
expression | 97 | 474 | 20.5 |
branch | 12 | 60 | 20.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; lib/obj/db/proto.lisp --- Database Protocol
14
(defvar *database-backend* nil)
15
(defvar *database-collection-type* 'list)
16
(defvar *default-database-version* '(0 1 0))
17
(defvar *default-kv-size* 8)
18
(defparameter *save-database-backend-on-load* nil)
20
(defvar *database-backends* (make-hash-table)
21
"Hash Table where keys are a database backend designator and values
22
are a list of functions which are responsible for doing all initialization
23
such as loading shared libraries and setting variables.")
25
(defvar *database-backend-options* (make-hash-table)
26
"Hash Table where keys are a database backend designator and values are a
27
lambda-list which will be interpreted by PARSE-DATABASE-BACKEND-OPTIONS within
28
the body of WITH-DB forms.")
30
(defvar *database-backend-close-options* '(close destroy))
32
(defun add-database-loader (backend thunk)
33
(let ((flist (gethash backend *database-backends*)))
34
(setf (gethash backend *database-backends*) (pushnew thunk flist :test 'equalp))))
36
(defun add-database-backend-option (backend option)
37
"Add a new database backend option."
38
(let ((olist (gethash backend *database-backend-options*)))
39
(setf (gethash backend *database-backend-options*) (pushnew option olist))))
41
(defun set-database-backend (backend options &rest thunks)
42
"Set the loaders (a sequence of thunks) and options for the designated database
43
backend keyword BACKEND."
44
(setf (gethash backend *database-backends*) thunks
45
(gethash backend *database-backend-options*) options))
47
(declaim (inline %load-database-backend))
48
(defun %load-database-backend (backend)
49
(when-let ((be (gethash backend *database-backends*)))
53
(defun load-database-backend (backend &optional save)
54
"Load database BACKEND and set value of *DATABASE-BACKEND*. When SAVE is
55
non-nil also arrange for the BACKEND to be loaded on init when this core is
57
(let ((*save-database-backend-on-load* save))
58
(%load-database-backend backend)
59
(setq *database-backend* backend)))
61
(defun %database-backend-option-key (item)
62
(keywordicate (if (atom item) item (car item))))
64
;; TODO 2024-11-10: should we handle &rest/&optional too?
65
(defun parse-database-backend-options (initargs)
66
"Parse INITARGS as a plist of database options for current *DATABASE-BACKEND*."
67
(mapcar ;; for each registered database backend option..
69
(let ((key (%database-backend-option-key opt)))
70
(if (member key initargs)
71
(let ((match (getf initargs key)))
72
(if (atom opt) (cons opt match) (cons (car opt) match)))
74
(gethash *database-backend* *database-backend-options*)))
76
(defgeneric set-database-backend-option (db key val)
77
(:method (db (key (eql :open)) val)
80
(:method (db (key (eql :close)) val)
83
(:method (db (key (eql :destroy)) val)
87
(:method (db (key (eql :path)) val)
89
(:method (db (key (eql :name)) val)
91
(:method (db (key (eql :id)) val)
93
(:method (db (key (eql :sap)) val)
95
(:method (db (key (eql :opts)) val)
96
(setf (db-opts db) val))
97
(:method (db (key (eql :opt)) (val cons))
98
(set-db-opt db (car val) (cdr val)))
99
(:method (db (key (eql :shutdown)) val)
100
(shutdown-db db :wait (eql val :wait))))
102
(defun set-database-backend-options (db &rest options)
104
(set-database-backend-option
106
(keywordicate (car opt))
111
(defun do-database-backend-init-options (db &rest options)
112
(apply 'set-database-backend-options
118
(member (car x) *database-backend-close-options*)))
121
(defun do-database-backend-close-options (db &rest options)
122
(apply 'set-database-backend-options
128
(not (member (car x) *database-backend-close-options*))))
131
(defmacro with-db ((var &rest initargs &key (db '*db*) &allow-other-keys)
133
"Bind VAR to a DATABASE instance produced by parsing INITARGS for the extent
136
`(let ((,opts ',(parse-database-backend-options initargs))
138
;; ,@(when open (remf initargs :open) `((open-db ,var)))
139
(apply 'do-database-backend-init-options ,var ,opts)
140
(unwind-protect (progn ,@body)
141
;; ,@(when close (remf initargs :close) `((close-db ,var)))
142
;; ,@(when destroy (remf initargs :destroy) `((destroy-db ,var)))
143
(apply 'do-database-backend-close-options ,var ,opts)))))
146
(defconfig db-config ()
147
((backend :initform :rdb :type database-backend-designator)
151
(define-condition db-condition () ()
152
(:documentation "Superclass for DB conditions."))
154
(deferror not-a-database (db-condition invalid-argument) ()
155
(:documentation "Error signaled when an illegal DB is detected.")
157
:reason "Object is not a database")
161
(defgeneric db (self)
162
(:documentation "Return the Database associated with SELF."))
164
(defgeneric db-lock (self)
165
(:documentation "Return an optional database MUTEX."))
167
(defgeneric database-version (self)
168
(:documentation "Return the version associated with a given database SELF."))
170
(defmethod database-version :around (self)
171
(declare (ignorable self))
172
(let ((version (call-next-method)))
173
(std/macs:ifret version
174
*default-database-version*)))
176
(defun prior-version-p (v1 v2)
177
"Is v1 an equal or earlier version than v2"
178
(cond ((and (null v1) (null v2)) t)
179
((and (null v1) (not (null v2))) t)
180
((and (not (null v1)) (null v2)) nil)
181
((< (car v1) (car v2)) t)
182
((> (car v1) (car v2)) nil)
183
((= (car v1) (car v2))
184
(prior-version-p (cdr v1) (cdr v2)))
185
(t (error "Version comparison problem: (prior-version-p ~A ~A)" v1 v2))))
187
(defclass database ()
188
((db :initform nil :initarg :db :accessor db))
189
(:documentation "Base class for Database objects."))
191
(defclass database-collection () ()
192
(:documentation "A collection of DATABASE objects."))
194
(defgeneric make-db (engine &rest initargs &key &allow-other-keys)
195
(:documentation "Dispatch initializer for databases. An ENGINE must be supplied, which is
196
usually a key such as :ROCKSDB or :SQLITE."))
198
(defgeneric connect-db (db &key &allow-other-keys)
199
(:documentation "Connect the database DB."))
201
(defgeneric query-db (db query &key &allow-other-keys)
202
(:documentation "Execute QUERY against DB."))
204
(defgeneric db-get (db key &key &allow-other-keys)
205
(:documentation "Return the value associated with KEY from DB."))
207
(defgeneric (setf db-get) (db key val &key &allow-other-keys))
209
(defgeneric db-set (db key val &key &allow-other-keys)
210
(:documentation "Set the value associated with KEY from DB to VAL.")
211
(:method (db key val &rest args)
212
(setf (apply #'db-get db key args) val)))
214
(defgeneric close-db (db &key &allow-other-keys)
215
(:documentation "Close a database."))
217
(defgeneric open-db (self)
218
(:documentation "Open a database."))
220
(defgeneric destroy-db (self)
221
(:documentation "Destroy all traces of a database, deleting any on-disk data and shutting down
222
in-memory objects."))
224
(defgeneric find-db (name dbs &key &allow-other-keys)
225
(:documentation "Return the db by NAME, from a collection of databases DBS."))
227
(defgeneric insert-db (name dbs &key &allow-other-keys)
228
(:documentation "Inserts a database by NAME into the database-collection DBS."))
230
(defgeneric db-open-p (self)
231
(:documentation "Return T when database SELF is open.")
232
(:method ((self t)) (not-a-database self))
233
(:method ((self database)) (when (db self) t)))
235
(defgeneric db-closed-p (self)
236
(:documentation "Return T when database SELF is closed.")
237
(:method ((self t)) (not-a-database self))
238
(:method ((self database)) (unless (db self) t)))
241
(defun slot-val (instance slot-name)
243
(slot-boundp instance slot-name))
244
(slot-value instance slot-name)))
246
(defgeneric get-val (object element &key &allow-other-keys)
247
(:documentation "Returns the value in a object based on the supplied element name and possible
249
(:method (object element &key data-type)
253
(gethash element object))
255
(slot-val object element))
259
((equal 'alist data-type)
260
(second (assoc element object :test #'equal)))
261
((equal 'plist data-type)
262
(get object element))
264
(error "Does not handle this type of object. Implement your own get-val method.")))
266
(second (assoc element object :test #'equal))
267
(error "Does not handle this type of object. Implement your own get-val method."))))))))
269
(defgeneric (setf get-val) (new-value object element &key &allow-other-keys)
270
(:documentation "Set the value in a object based on the supplied element name and possible type
272
(:method (new-value object element &key data-type)
273
(typecase (or data-type object)
274
(hash-table (setf (gethash element object) new-value))
275
(standard-object (setf (slot-value object element) new-value))
278
(cond ((equal 'alist data-type)
279
(replace object (list (list element new-value))))
280
((equal 'plist data-type)
281
;;TODO: Implement this properly.
282
(get object element ))
284
(error "Does not handle this type of object. Implement your own get-val method.")))
286
(replace object (list (list element new-value)))
287
(error "Does not handle this type of object. Implement your own get-val method.")))))))
289
(defgeneric get-value (elt obj)
290
(:method (elt (obj sequence))
291
(find elt obj :test 'equal))
292
(:method (elt (obj hash-table))
295
(defgeneric (setf get-value) (new elt obj))
297
(defgeneric put-kv (self kv)
298
(:documentation "Insert a KeyVal object."))
299
(defgeneric put-key (self key val)
300
(:documentation "Insert a KEY and VAL."))
301
(defgeneric put-key-ts (self key val ts)
302
(:documentation "Insert a KEY and VAL with associated timestamp TS."))
303
(defgeneric get-key (self key &key)
304
(:documentation "Get value of KEY."))
305
(defgeneric multi-get (self keys &key)
306
(:documentation "Retrieve multiple KEYS from SELF."))
308
(defgeneric insert-key (self key val &key)
309
(:documentation "Insert KEY:VAL into SELF."))
310
(defgeneric insert-kv (self kv &key)
311
(:documentation "Insert KV object into SELF."))
312
(defgeneric delete-key (self key &key)
313
(:documentation "Delete value associated with KEY from SELF."))
314
(defmethod remove-kv (key value self))
315
(defgeneric delete-key-ts (self key ts)
316
(:documentation "Delete value associated with KEY and TS from SELF."))
317
(defgeneric delete-key-range (self start end &key)
318
(:documentation "Delete values associates with keys between START and END from SELF."))
319
(defgeneric flush-db (self &key)
320
(:documentation "Flush the database SELF."))
321
(defgeneric sync-db (self other &key) ;;nyi
322
(:documentation "Perform a synchronization on SELF using OTHER."))
323
(defgeneric load-db (self)
324
(:documentation "Load an existing database."))
325
(defgeneric db-stats (self &optional type)
326
(:documentation "Return TYPE stats of given database."))
327
(defgeneric db-metadata (self &optional type)
328
(:documentation "Return TYPE metdata of given database."))
329
(defgeneric db-prop (self type)
330
(:documentation "Return TYPE property of given database."))
331
(defgeneric db-opt (self key)
332
(:documentation "Return value of database option KEY."))
333
(defgeneric db-opts (self)
334
(:documentation "Accessor for database options of SELF."))
335
(defgeneric (setf db-opts) (new self)
336
(:documentation "Return value of database option KEY."))
337
(defgeneric (setf db-opt) (new self key &key &allow-other-keys)
338
(:documentation "Set the value of database option KEY."))
339
(defgeneric set-db-opt (self key val &key &allow-other-keys)
340
(:documentation "Convenience setter for DB-OPT.")
341
(:method ((self t) key val &key)
342
(setf (db-opt self key) val))
343
(:method ((self t) key val &key push)
344
(setf (db-opt self key :push push) val)))
345
(defgeneric repair-db (self &key)
346
(:documentation "Attempt to repair the database SELF."))
347
(defgeneric backup-db (self &key)
348
(:documentation "Create a new backup for database SELF."))
349
(defgeneric db-backup (self)
350
(:documentation "Access the current backup of database SELF."))
351
(defgeneric secondary-db (self)
352
(:documentation "Accessor for the secondary-db of a database SELF."))
353
(defgeneric restore-db (self from &key)
354
(:documentation "Restore database SELF from object FROM."))
355
(defgeneric snapshot-db (self)
356
(:documentation "Create a new snapshot for database SELF."))
357
(defgeneric write-batch (self batch &key)
358
(:documentation "Write BATCH to database SELF."))
359
(defgeneric shutdown-db (self &key wait &allow-other-keys)
360
(:documentation "Shutdown database SELF."))
361
(defgeneric ingest-into-db (self file &key)
362
(:documentation "Ingest an external file into the database"))
365
(defgeneric merge-key (self key val &key)
366
(:documentation "Perform a merge operation on SELF using KEY and VAL."))
367
(defgeneric merge-kv (self kv &key)
368
(:documentation "Perform a merge operation on SELF using object KV."))
370
(defmacro with-merge-op ())
373
(defgeneric open-column (self col &key)
374
(:documentation "Open and return a column from SELF."))
375
(defgeneric open-columns (self &rest columns)
376
(:documentation "Open the columns or all columns belonging to SELF."))
377
(defgeneric open-columns* (self)
378
(:documentation "Open all columns belonging to SELF."))
379
(defgeneric open-with-columns (self &rest names)
380
(:documentation "Open a database with columns indicated by NAMES or all columns belonging to
381
SELF. This function may error when (DB-OPEN-P SELF) is non-nil."))
382
(defgeneric close-column (self &optional error)
383
(:documentation "Close the column SELF. When ERROR is non-nil signal an error if the
384
column is already closed."))
385
(defgeneric close-columns (self)
386
(:documentation "Close the columns belonging to SELF."))
387
(defgeneric destroy-column (self &optional error)
388
(:documentation "Close the column SELF. When ERROR is non-nil signal an error if the
389
column is already closed."))
390
(defgeneric destroy-columns (self)
391
(:documentation "Close the columns belonging to SELF."))
392
(defgeneric create-column (self cf)
393
(:documentation "Create the column belonging to SELF."))
394
(defgeneric create-columns (self)
395
(:documentation "Create the columns belonging to SELF."))
396
(defgeneric find-column (col self &key)
397
(:documentation "Find the column COL in SELF."))
398
(defgeneric (setf find-column) (new col self &key)
399
(:documentation "Find the column COL in SELF."))
400
(defgeneric flush-column (self col &key)
401
(:documentation "Flush the column COL in SELF."))
402
(defgeneric add-column (col self)
403
(:documentation "Add a column to SELF."))
404
(defgeneric column-opts (col))
405
(defgeneric (setf column-opts) (new col))
408
(defstruct (kv (:constructor make-kv (&optional key val)))
409
(key (make-octets *default-kv-size*) :type octet-vector)
410
(val (make-octets *default-kv-size*) :type octet-vector))
412
(defgeneric make-val (val)
413
(:documentation "Coerce VAL into an OCTET-VECTOR.")
414
(:method ((val null))
416
(:method ((val string))
417
(sb-ext:string-to-octets val))
418
(:method ((val vector))
419
(if (octet-vector-p val)
423
(coerce val 'octet-vector)))
425
(defgeneric make-key (key)
426
(:documentation "Coerce KEY into an OCTET-VECTOR.")
427
(:method ((val null))
429
(:method ((val string))
430
(sb-ext:string-to-octets val))
431
(:method ((val integer))
432
(integer-to-octets val))
433
(:method ((val vector))
434
(if (octet-vector-p val)
438
(coerce val 'octet-vector)))
442
;; In our system, transactions must be one of the following:
445
;; - A subclass of TRANSACTION-OBJECT
446
;; - Implement a TRANSACTION-DB method which returns an instance of DATABASE
448
;; Simple transactions are non-nil lists which are handled according to the
449
;; current database backend.
453
- *TXN* is bound to the current transaction being executed. A value of NIL
454
represents no transaction. The current *DATABASE-BACKEND* may modify this
455
variable within the EXECUTE-TRANSACTION method.
456
- should never be bound within the body of a transaction
458
- The macros WITH-TRANSACTION and ENSURE-TRANSACTION will always abort the
459
transaction in response to any non-local exit.
461
- WITH-TRANSACTION passes *TXN* to EXECUTE-TRANSACTION
464
(deftype simple-transaction () `(and (not null) list))
466
(defvar *default-txn* '(nil nil nil))
468
"The current transaction.")
470
(defclass transaction-object () ()
471
(:documentation "Base class for transaction objects."))
473
(defgeneric (setf transaction-opts) (new txn))
475
(defgeneric make-transaction (self &key &allow-other-keys)
476
(:documentation "Make a new transaction object.")
477
(:method ((self null) &key) *default-txn*))
479
(defgeneric prepare-transaction (self &key)
480
(:documentation "Prepare a transaction."))
482
(defgeneric rollback-transaction (self &key)
483
(:documentation "Rollback a transaction."))
485
(defgeneric commit-transaction (self &key)
486
(:documentation "Commit a transaction."))
488
(defgeneric execute-transaction (self kernel &rest args &key &allow-other-keys)
490
"Interface to the backend transaction kernel (a function). The body of the
491
kernel function should be executed in an environment that protects against
492
non-local exits, provides ACIDic properties and binds any relevant parameters."))
494
(defgeneric abort-transaction (self &key &allow-other-keys))
496
(defgeneric transaction-object-p (self)
497
(:documentation "Return Non-nil if SELF is a transaction object.")
499
(or (typep 'simple-transaction self)
500
(subtypep (type-of (transaction-db self)) 'database)))
501
(:method ((self transaction-object)) t))
503
(defgeneric transaction-object (self)
504
(:documentation "Return the underlying object of a transaction.")
505
(:method ((self list)) (second self)))
506
(defgeneric transaction-store (self)
507
(:documentation "Return the underlying STORE of a transaction.")
508
(:method ((self list)) (first self)))
509
(defgeneric transaction-db (self)
510
(:documentation "Return the underlying TRANSACTION-DB of a transaction. This may or may not
511
return the same value as DB depending on backend.")
512
(:method ((self t)) *db*))
513
(defgeneric transaction-prior (self)
514
(:documentation "Return the previous transaction of SELF if any.")
515
(:method ((self list)) (third self)))
517
(defun known-transaction (db txn)
518
"Search for a prior TXN known by this DB."
520
(or (and (transaction-object-p txn)
521
(or (eq db (transaction-db txn))
522
(eq db (transaction-store txn)))
524
(known-transaction db (transaction-prior txn))))))
526
(define-condition transaction-retry-count-exceeded (error)
527
((count :initarg :count :accessor retry-count :initform 0)))
529
(defvar *default-txn-wait* 0.1)
530
(defvar *default-txn-retry* 0)
533
(defmacro with-transaction ((&rest initargs
541
"Execute a body with a transaction in place. On success, the transaction is
542
committed. Otherwise, the transaction is aborted."
543
(with-gensyms (%db %txn-fn)
545
(remf initargs :store)
547
(remf initargs :retries)
548
(remf initargs :wait)
549
`(let* ((,%db (or ,db ,store))
550
(,%txn-fn (lambda () ,@body)))
551
(funcall #'execute-transaction ,%db ,%txn-fn
552
:txn (awhen (known-transaction ,%db ,txn) (transaction-object it))
553
,@(when retries `(:retries ,retries))
554
,@(when wait `(:wait ,wait))
557
(defmacro current-transaction (db)
558
"Return the current transaction associated with database DB."
561
(when (and ,txn (eq (transaction-db ,txn) ,db))
562
(transaction-object ,txn)))))
564
(defmacro ensure-transaction ((&rest initargs &key
571
"Execute BODY with an existing transaction or a new transaction if one does not
572
exist. This macro allows for the sequencing of database actions to be run
573
atomically regardless of whether there is an existing transaction or not."
574
(with-gensyms (%db %txn-fn)
576
(remf initargs :store)
578
(remf initargs :retries)
579
(remf initargs :wait)
580
`(let ((,%db (or ,db ,store))
581
(,%txn-fn (lambda () ,@body)))
582
(if (known-transaction ,%db ,txn)
584
(funcall #'execute-transaction ,%db
587
,@(when retries `(:retries ,retries))
588
,@(when wait `(:wait ,wait)))))))
590
(defmacro with-batch-transaction ((batch size list &rest txn-options) &body body)
591
"Perform a set of DB operations over a sequence of elements LIST in batches of
592
SIZE. Transaction keywords accepted by WITH-TRANSACTION are accepted
593
immediately following LIST."
594
`(loop for ,batch in (group ,list ,size)
595
do (with-transaction ,txn-options