dotfiles/emacs/.emacs.d/elpa/closql-20260601.1540/closql.el

659 lines
26 KiB
EmacsLisp
Raw Normal View History

;;; closql.el --- Store EIEIO objects using EmacSQL -*- lexical-binding:t -*-
;; Copyright (C) 2016-2026 Jonas Bernoulli
;; Author: Jonas Bernoulli <emacs.closql@jonas.bernoulli.dev>
;; Homepage: https://github.com/emacscollective/closql
;; Keywords: extensions
;; Package-Version: 20260601.1540
;; Package-Revision: d382e7427f5d
;; Package-Requires: (
;; (emacs "28.1")
;; (compat "31.0")
;; (cond-let "1.1")
;; (emacsql "4.4")
;; (llama "1.0"))
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published
;; by the Free Software Foundation, either version 3 of the License,
;; or (at your option) any later version.
;;
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Store uniform EIEIO objects in an EmacSQL database. SQLite is used
;; as backend. This library imposes some restrictions on what kind of
;; objects can be stored; it isn't intended to store arbitrary objects.
;; All objects have to share a common superclass and subclasses cannot
;; add any additional instance slots.
;;; Code:
(require 'compat)
(require 'cond-let)
(require 'eieio)
(require 'eieio-base)
(require 'emacsql)
(require 'emacsql-sqlite)
(require 'llama)
(eval-when-compile (require 'subr-x))
(eval-when-compile
(cl-pushnew 'connection eieio--known-slot-names))
(defmacro closql-with-transaction (db &rest body)
(declare (indent 1))
`(emacsql-with-transaction (oref ,db connection)
,@body))
;;; Objects
(defclass closql-object ()
((closql-class-prefix :initform nil :allocation :class)
(closql-class-suffix :initform nil :allocation :class)
(closql-table :initform nil :allocation :class)
(closql-primary-key :initform nil :allocation :class)
(closql-foreign-key :initform nil :allocation :class)
(closql-order-by :initform nil :allocation :class)
(closql-database :initform nil :initarg :closql-database))
:abstract t)
(defun closql--closql-object-p (obj)
;; Prevent a recursive load when the class object is autoloaded.
;; See c1a9b816ec. Don't #'quote; doesn't exist in older releases.
(cl-letf (((symbol-function 'eieio--full-class-object)
#'eieio--class-object))
(closql-object--eieio-childp obj)))
;;;; Oref
(define-advice eieio-oref (:around (fn obj slot) closql-object)
"If OBJ is a `closql-object', delegate to `closql-oref'."
(if (closql--closql-object-p obj)
(closql-oref obj slot)
(funcall fn obj slot)))
(defun closql--oref (obj slot)
(aref obj (eieio--slot-name-index (eieio--object-class obj) slot)))
(defun closql--oref-default (class slot)
(let ((class (if (symbolp class)
(cl--find-class class)
(eieio--object-class class))))
(aref (eieio--class-class-allocation-values class)
(eieio--class-slot-name-index class slot))))
(defun closql-oref (obj slot)
(cl-check-type slot symbol)
(let ((class (eieio--object-class obj)))
(cond-let
([c (eieio--slot-name-index class slot)]
(let ((value (aref obj c)))
(if (eq value eieio--unbound)
(closql-dref obj slot)
value)))
([c (eieio--class-slot-name-index class slot)]
(aref (eieio--class-class-allocation-values class) c))
((slot-missing obj slot 'oref)))))
(cl-defgeneric closql-dref (obj slot)
(let ((c (eieio--slot-name-index (eieio--object-class obj) slot))
(db (closql--oref obj 'closql-database))
(props (closql--slot-properties obj slot)))
(cond-let
([class (alist-get :closql-class props)]
(aset obj c
(closql--remake-instances class db
(emacsql
db `[:select * :from $i1
:where (= $i2 $s3)
:order-by ,(or (closql--oref-default class 'closql-order-by)
[(asc $i4)])]
(closql--oref-default class 'closql-table)
(closql--oref-default class 'closql-foreign-key)
(closql--oref obj (closql--oref-default obj 'closql-primary-key))
(closql--oref-default class 'closql-primary-key)))))
([table (alist-get :closql-table props)]
(let ((columns (closql--table-columns db table)))
(aset obj c
(mapcar
(if (length= columns 2) #'cadr #'cdr)
(emacsql
db [:select * :from $i1
:where (= $i2 $s3)
:order-by [(asc $i4)]]
table
(car columns)
(closql--oref obj (closql--oref-default obj 'closql-primary-key))
(cadr columns))))))
([tables (alist-get :closql-tables props)]
(pcase-let ((`(,slot-table ,data-table) tables))
(aset obj c
(mapcar
#'cdr
(emacsql
db [:select $i1 :from $i2
:join $i3 :on (= $i4 $i5)
:where (= $i6 $s7)
:order-by [(asc id)]]
(intern (format "%s:*" data-table))
data-table slot-table
(intern (format "%s:id" slot-table))
(intern (format "%s:id" data-table))
(intern (format "%s:%s" slot-table
(closql--oref-default obj 'closql-table)))
(closql--oref obj (closql--oref-default obj 'closql-primary-key))
(closql--oref obj 'id))))))
((slot-unbound obj (eieio--object-class obj) slot 'oref)))))
;;;; Oset
(define-advice eieio-oset (:around (fn obj slot value) closql-object)
"If OBJ is a `closql-object', delegate to `closql-oset'."
(if (closql--closql-object-p obj)
(closql-oset obj slot value)
(funcall fn obj slot value)))
(defun closql--oset (obj slot value)
(aset obj (eieio--slot-name-index (eieio--object-class obj) slot) value))
(defun closql-oset (obj slot value)
(cl-check-type slot symbol)
(let ((class (eieio--object-class obj)))
(cond-let
([c (eieio--slot-name-index class slot)]
(eieio--validate-slot-value class c value slot)
(when (and (not (eq slot 'closql-database))
(closql--oref obj 'closql-database))
(closql-dset obj slot value))
(aset obj c value))
([c (eieio--class-slot-name-index class slot)]
(eieio--validate-class-slot-value class c value slot)
(aset (eieio--class-class-allocation-values class) c value))
((slot-missing obj slot 'oset value)))))
(cl-defgeneric closql-dset (obj slot value &optional drop-unknown)
(let* ((db (closql--oref obj 'closql-database))
(key (oref-default obj closql-primary-key))
(id (closql--oref obj key))
(props (closql--slot-properties obj slot)))
(cond-let
((alist-get :closql-class props)
(error "Not implemented for closql-class slots: oset"))
[[tables (alist-get :closql-tables props)]]
([table (or (alist-get :closql-table props)
(car tables))]
(closql-with-transaction db
(let ((columns (closql--table-columns db table)))
;; Caller might have modified value in place.
(closql--oset obj slot eieio--unbound)
(let ((list1 (closql-oref obj slot))
(list2 value)
elt1 elt2)
(cond (tables
(setq list1 (mapcar (##list (car %)) list1))
(setq list2 (mapcar (if (atom (car list2))
#'list
(##list (car %)))
list2)))
((length= columns 2)
(setq list1 (mapcar #'list list1))
(setq list2 (mapcar #'list list2))))
;; `list2' may not be sorted at all and `list1' has to
;; be sorted because Elisp and SQLite sort differently.
(setq list1 (compat-call sort list1 :lessp #'string< :key #'car))
(setq list2 (compat-call sort list2 :lessp #'string< :key #'car))
(while (progn (setq elt1 (car list1))
(setq elt2 (car list2))
(or elt1 elt2))
(let ((key1 (car elt1))
(key2 (car elt2)))
(cond
((and elt1 (or (not elt2) (string< key1 key2)))
(apply #'emacsql db
`[:delete-from $i1
:where ,(closql--where-equal (cons id elt1) 1)]
table
(cl-mapcan #'list columns (cons id elt1)))
(pop list1))
((string= key1 key2)
(unless (equal elt1 elt2)
(cl-mapc
(lambda (col val1 val2)
(unless (equal val1 val2)
(emacsql db [:update $i1 :set (= $i2 $s3)
:where (and (= $i4 $s5) (= $i6 $s7))]
table col val2
(car columns) id
(cadr columns) key2)))
(cddr columns)
(cdr elt1)
(cdr elt2)))
(pop list1)
(pop list2))
(drop-unknown
(ignore-errors
(emacsql db [:insert-into $i1 :values $v2]
table (vconcat (cons id elt2))))
(pop list2))
(t
(emacsql db [:insert-into $i1 :values $v2]
table (vconcat (cons id elt2)))
(pop list2)))))))))
((emacsql db [:update $i1 :set (= $i2 $s3) :where (= $i4 $s5)]
(oref-default obj closql-table)
slot
(if (eq value eieio--unbound) 'eieio-unbound value)
key id)))))
;;;; Slot Properties
(defun closql--slot-properties (object-or-class slot)
(and-let ((desc (cl-find slot
(closql--object-slots object-or-class)
:key #'cl--slot-descriptor-name)))
(cl--slot-descriptor-props desc)))
(defun closql--object-slots (object-or-class)
(eieio-class-slots
(cond
((eieio-object-p object-or-class) (eieio--object-class object-or-class))
((eieio--class-p object-or-class) object-or-class)
((find-class object-or-class 'error)))))
(defconst closql--slot-properties '(:closql-class :closql-table :closql-tables))
(define-advice eieio-defclass-internal
(:after (cname _superclasses slots _options) closql-object)
"Handle additional slot properties used by `closql-object' derived classes."
(when-let* ((class (cl--find-class cname))
(_(child-of-class-p class 'closql-object)))
(pcase-dolist (`(,name . ,slot) slots)
(let ((desc (cl-find name
(cl-coerce (eieio--class-slots class) 'list)
:key (lambda (elt) (aref elt 1)))))
(dolist (prop closql--slot-properties)
(when-let
((v (plist-get slot prop)))
(setf (alist-get prop (cl--slot-descriptor-props desc)) v)))))))
(define-advice eieio--slot-override
(:after (old new _skipnil) closql-object)
"Handle additional slot properties used by `closql-object' derived classes."
(dolist (prop closql--slot-properties)
(when-let
((v (alist-get prop (cl--slot-descriptor-props new))))
(setf (alist-get prop (cl--slot-descriptor-props old)) v))))
;;; Database
(defclass closql-database (eieio-singleton)
((name :initform nil :allocation :class)
(object-class :initform nil :allocation :class)
(file :initform nil :allocation :class)
(schemata :initform nil :allocation :class)
(version :initform nil :allocation :class)
(disabled :initform nil :allocation :class)
(connection :initform nil :initarg :connection))
:abstract t)
(cl-defmethod closql-db ((class (subclass closql-database))
&optional livep connection-class)
(cond-let*
([db (oref-default class singleton)]
[conn (and (not (eq db eieio--unbound))
(oref db connection))]
[_(emacsql-live-p conn)]
db)
((not livep)
(let* ((file (closql--db-prepare-storage class))
(connection-class (or connection-class
(emacsql-sqlite-default-connection)))
(conn (make-instance connection-class :file file))
(db (make-instance class))) ; ignores slot arguments
(oset db connection conn)
(emacsql conn [:pragma (= foreign-keys on)])
(cond-let
((not (emacsql-sqlite-list-tables db))
(closql--db-create-schema db))
[[code-version (oref-default db version)]
[data-version (closql--db-get-version db)]]
((< code-version data-version)
(message "Please update %s package (database schema version %s < %s)"
(oref-default db name) code-version data-version)
(oset-default class disabled t)
(emacsql-close db)
(setq db nil))
((closql--db-update-schema db)))
db))))
(cl-defmethod closql--db-prepare-storage ((class (subclass closql-database)))
(when-let ((file (oref-default class file)))
(when (symbolp file)
(setq file (symbol-value file)))
(make-directory (file-name-directory file) t)
file))
(cl-defmethod closql--db-create-schema ((db closql-database))
(closql-with-transaction db
(pcase-dolist (`(,table . ,schema)
(symbol-value (oref-default db schemata)))
(emacsql db [:create-table $i1 $S2] table schema))
(closql--db-set-version db (oref-default db version))))
(cl-defmethod closql--db-update-schema ((db closql-database))
(let ((code-version (oref-default db version))
(data-version (closql--db-get-version db)))
(when (< data-version code-version)
(oset-default db disabled t)
(emacsql-close db)
(error "Please update %s database (schema version %s < %s)"
(oref-default db name) data-version code-version))))
(cl-defmethod emacsql-live-p ((db closql-database))
(and-let ((conn (oref db connection)))
(emacsql-live-p conn)))
(cl-defmethod emacsql-enable-debugging ((db closql-database))
(emacsql-enable-debugging (oref db connection)))
(cl-defmethod emacsql-close ((db closql-database))
(emacsql-close (oref db connection))
(oset db connection nil))
(cl-defmethod emacsql ((db closql-database) sql &rest args)
(mapcar #'closql--extern-unbound
(apply #'emacsql (oref db connection) sql
(mapcar (lambda (arg)
(if (stringp arg)
(let ((copy (copy-sequence arg)))
(set-text-properties 0 (length copy) nil copy)
copy)
arg))
args))))
(cl-defmethod closql-insert ((db closql-database) obj &optional replace)
(closql--oset obj 'closql-database db)
(let (alist)
(dolist (slot (eieio-class-slots (eieio--object-class obj)))
(setq slot (cl--slot-descriptor-name slot))
(when (alist-get :closql-table (closql--slot-properties obj slot))
(push (cons slot (closql-oref obj slot)) alist)
(closql--oset obj slot eieio--unbound)))
(closql-with-transaction db
(emacsql db
(if replace
[:insert-or-replace-into $i1 :values $v2]
[:insert-into $i1 :values $v2])
(oref-default obj closql-table)
(pcase-let ((`(,class ,_db . ,values)
(closql--intern-unbound
(closql--coerce obj 'list))))
(vconcat (cons (closql--abbrev-class class) values))))
(pcase-dolist (`(,slot . ,value) alist)
(closql-dset obj slot value))))
obj)
(cl-defmethod closql-delete ((obj closql-object))
(let ((key (oref-default obj closql-primary-key)))
(emacsql (closql--oref obj 'closql-database)
[:delete-from $i1 :where (= $i2 $s3)]
(oref-default obj closql-table)
key
(closql--oref obj key))))
(cl-defmethod closql-reload ((obj closql-object))
(or (closql-get (closql--oref obj 'closql-database)
(closql--oref obj (oref-default obj closql-primary-key))
(eieio-object-class obj))
(error "Cannot reload object")))
(cl-defmethod closql-get ((db closql-database) ident &optional class resolve)
(unless class
(setq class (oref-default db object-class)))
(and-let ((row (car (emacsql db [:select * :from $i1
:where (= $i2 $s3)]
(oref-default class closql-table)
(oref-default class closql-primary-key)
ident))))
(closql--remake-instance class db row resolve)))
(cl-defmethod closql-query ((db closql-database) &optional select pred class)
(if select
(let ((value (closql-select db select pred class)))
(if (and select (symbolp select))
(mapcar #'car value)
value))
(closql-entries db pred class)))
(cl-defmethod closql-entries ((db closql-database) &optional pred class)
(unless class
(setq class (oref-default db object-class)))
(mapcar (lambda (row)
(closql--remake-instance class db row))
(closql-select db '* pred class)))
(cl-defmethod closql-select ((db closql-database) select &optional pred class)
(unless class
(setq class (oref-default db object-class)))
(emacsql db
`[:select $i1 :from $i2
,@(and pred [:where class :in $v3])
,@(if-let ((order (oref-default class closql-order-by)))
(list :order-by order)
'(:order-by [(asc $i4)]))]
select
(oref-default class closql-table)
(and pred (closql-where-class-in pred db))
(oref-default class closql-primary-key)))
(defun closql--table-columns (db table)
(mapcar #'cadr (emacsql db [:pragma (funcall table-info $i1)] table)))
(defun closql--db-get-version (db)
(caar (emacsql db [:pragma user-version])))
(defun closql--db-set-version (db version)
(cl-assert (integerp version))
(emacsql db [:pragma (= user-version $s1)] version))
;;; Object/Row Conversion
(cl-defmethod closql--remake-instance ((class (subclass closql-object))
db row &optional resolve)
(pcase-let*
((`(,abbrev . ,values) (closql--extern-unbound row))
(class-sym (closql--expand-abbrev class abbrev))
(class-obj (eieio--class-object class-sym))
(obj (copy-sequence (eieio--class-default-object-cache class-obj)))
(values (apply #'vector (cons db values))))
(dotimes (i (length (eieio--class-slots class-obj)))
(aset obj (1+ i) (aref values i)))
(when resolve
(closql--resolve-slots obj))
(when eieio-backward-compatibility
(aset obj 0 class-sym))
obj))
(defun closql--remake-instances (class db rows)
(declare (indent defun))
(mapcar (lambda (row)
(closql--remake-instance class db row))
rows))
(cl-defmethod closql--resolve-slots ((obj closql-object))
(dolist (slot (eieio-class-slots (eieio--object-class obj)))
(setq slot (cl--slot-descriptor-name slot))
(when (and (not (slot-boundp obj slot))
(let ((props (closql--slot-properties obj slot)))
(or (alist-get :closql-class props)
(alist-get :closql-table props))))
(closql--oset obj slot (closql-oref obj slot)))))
(defun closql--intern-unbound (row)
(mapcar (lambda (elt)
(if (eq elt eieio--unbound) 'eieio-unbound elt))
row))
(defun closql--extern-unbound (row)
(mapcar (lambda (elt)
(if (eq elt 'eieio-unbound) eieio--unbound elt))
row))
(defun closql--coerce (object type)
(cl-coerce (let* ((length (length object))
(vector (make-vector length -1)))
(dotimes (i length)
(aset vector i (aref object i)))
;; Do not assume eieio-backward-compatibility is enabled.
(when (eieio--class-p (aref vector 0))
(aset vector 0 (eieio--class-name (aref vector 0))))
vector)
type))
(cl-defmethod closql--abbrev-class ((class (subclass closql-object)))
(let ((name (symbol-name class))
(prefix (oref-default class closql-class-prefix))
(suffix (oref-default class closql-class-suffix)))
(intern (substring name
(if prefix (length prefix) 0)
(if suffix (- (length suffix)) nil)))))
(cl-defmethod closql--expand-abbrev ((class (subclass closql-object)) abbrev)
(intern (concat (oref-default class closql-class-prefix)
(symbol-name abbrev)
(oref-default class closql-class-suffix))))
(defun closql--where-equal (value offset)
(vector
(cons 'and
(mapcar (lambda (v)
(if v
(list '=
(intern (format "$i%i" (incf offset)))
(intern (format "$s%i" (incf offset))))
(list 'isnull
(intern (format "$i%i" (1- (incf offset 2)))))))
value))))
(defun closql-where-class-in (args &optional db)
(when (symbolp args)
(setq args (list args)))
(cond
((vectorp args)
(unless db
(error "closql-where-class-in: DB cannot be nil if ARGS is a vector"))
(let ((class (oref-default db object-class))
(abbrevs nil))
(mapc (lambda (arg)
(let ((str (symbol-name arg)))
(unless (string-match "\\`\\(!\\)?\\([^*]+\\)\\(\\*\\)?\\'" str)
(error "`closql-where-class-in': invalid type: %s" arg))
(let* ((exclude (match-beginning 1))
(a (intern (match-string 2 str)))
(a (cond ((match-beginning 3)
(closql--list-subabbrevs
(closql--expand-abbrev class a)))
((not (class-abstract-p
(closql--expand-abbrev class a)))
(list a)))))
(setq abbrevs
(if exclude
(cl-set-difference abbrevs a)
(nconc abbrevs a))))))
args)
(vconcat abbrevs)))
((vconcat
(mapcar #'closql--abbrev-class
(mapcan (lambda (sym)
(let ((str (symbol-name sym)))
(cond ((string-suffix-p "--eieio-childp" str)
(closql--list-subclasses
(intern (substring str 0 -14)) nil))
((string-suffix-p "-p" str)
(list (intern (substring str 0 -2))))
((list sym)))))
args))))))
(defun closql--list-subclasses (class &optional result)
(unless (class-abstract-p class)
(cl-pushnew class result))
(dolist (child (eieio--class-children (cl--find-class class)))
(setq result (closql--list-subclasses child result)))
result)
(cl-defmethod closql--list-subabbrevs ((class (subclass closql-object))
&optional wildcards)
(sort (named-let types ((class class))
(let ((children (eieio--class-children (cl--find-class class)))
;; An abstract base-class may violate its own naming rules.
(abbrev (ignore-errors (closql--abbrev-class class))))
(nconc (and (not (class-abstract-p class)) (list abbrev))
(and wildcards children
(list (if abbrev (intern (format "%s*" abbrev)) '*)))
(mapcan #'types children))))
#'string<))
(cl-defmethod closql--set-object-class ((db closql-database) obj class)
(let* ((table (oref-default obj closql-table))
(key (oref-default obj closql-primary-key))
(id (closql--oref obj key)))
(aset obj 0
(aref (copy-sequence
(eieio--class-default-object-cache
(eieio--class-object class)))
0))
(emacsql db [:update $i1 :set (= class $s2) :where (= $i3 $s4)]
table
(closql--abbrev-class class)
key id)))
;;; Utilities
(defun closql-format (object string &rest slots)
"Format a string out of a format STRING and an OBJECT's SLOTS.
STRING is a format-string like for `format'. OBJECT is an Eieio
object and SLOTS are slots of that object, their values are used
like `format' uses its OBJECTS arguments (which are unrelated to
this function's OBJECT argument, they just have similar names).
While this function does not have much to do with the purpose of
`closql', it is being defined here anyway because Eieio does not
define a similar function under a more appropriate name such as
`eieio-format'."
(apply #'format string
(mapcar (lambda (slot) (eieio-oref object slot)) slots)))
;;; _
(provide 'closql)
;; Local Variables:
;; indent-tabs-mode: nil
;; lisp-indent-local-overrides: ((cond . 0) (interactive . 0))
;; read-symbol-shorthands: (
;; ("and$" . "cond-let--and$")
;; ("thread$" . "cond-let--thread$")
;; ("when$" . "cond-let--when$")
;; ("and-let*" . "cond-let--and-let*")
;; ("and-let" . "cond-let--and-let")
;; ("if-let*" . "cond-let--if-let*")
;; ("if-let" . "cond-let--if-let")
;; ("when-let*" . "cond-let--when-let*")
;; ("when-let" . "cond-let--when-let")
;; ("while-let*" . "cond-let--while-let*")
;; ("while-let" . "cond-let--while-let"))
;; End:
;;; closql.el ends here