branch: externals/triples commit fb63dfe44a27ed06cb9fe95ed41bf40d41bd35fd Author: Andrew Hyatt <ahy...@gmail.com> Commit: Andrew Hyatt <ahy...@gmail.com>
Convert to sqlite. Change not yet finalized, this doesn't yet work. --- triples-test.el | 13 +++--- triples.el | 129 +++++++++++++++++++++++++++++++++----------------------- 2 files changed, 83 insertions(+), 59 deletions(-) diff --git a/triples-test.el b/triples-test.el index d94c1e54ec..2844dccf96 100644 --- a/triples-test.el +++ b/triples-test.el @@ -16,7 +16,8 @@ easily debug into it.") (progn (let ((db (triples-connect db-file))) (setq triples-test-db-file db-file) - ,@body)) + ,@body + (triples-close db))) (delete-file db-file)))) (defun triples-test-connect-db () @@ -188,14 +189,12 @@ easily debug into it.") (triples-add-schema db 'marker) (triples-set-type db "foo" 'marker) (should (equal '((1)) - (emacsql db [:select (funcall count) :from triples :where (= subject $s1) - :and (= predicate 'base/type) :and (= object 'marker)] - "foo"))) + (sqlite-select db "COUNT(*) FROM triples WHERE subject = ? AND predicate = 'base/type' AND object = 'marker'" + (triples-standardize-val "foo")))) (triples-set-type db "foo" 'marker) (should (equal '((1)) - (emacsql db [:select (funcall count) :from triples :where (= subject $s1) - :and (= predicate 'base/type) :and (= object 'marker)] - "foo"))))) + (sqlite-select db "COUNT(*) FROM triples WHERE subject = ? AND predicate = 'base/type' AND object = 'marker'" + (triples-standardize-val "foo")))))) (ert-deftest triples-readme () (triples-test-with-temp-db diff --git a/triples.el b/triples.el index 1e52471490..bf9403d9ea 100644 --- a/triples.el +++ b/triples.el @@ -4,7 +4,7 @@ ;; Author: Andrew Hyatt <ahy...@gmail.com> ;; Homepage: https://github.com/ahyatt/triples -;; Package-Requires: ((emacsql "3.0.0") cl-lib (seq "2.0")) +;; Package-Requires: ((seq "2.0") (emacs29)) ;; Keywords: triples, kg, data, sqlite ;; Version: 0.0 ;; This program is free software; you can redistribute it and/or @@ -27,29 +27,24 @@ ;; provide an API offering two-way links between all information stored. (require 'cl-macs) -(require 'emacsql) (require 'seq) ;;; Code: (defun triples-connect (file) "Connect to the database FILE and make sure it is populated." - (let* ((db (emacsql-sqlite3 file)) - (triple-table-exists - (emacsql db [:select name - :from sqlite_master - :where (= type table) :and (= name 'triples)]))) - (unless triple-table-exists - (emacsql db [:create-table triples ([(subject text :not-null) - (predicate text :not-null) - (object :not-null) - (properties)])]) - (emacsql db [:create-index subject_idx :on triples [subject]]) - (emacsql db [:create-index subject_predicate_idx :on triples [subject predicate]]) - (emacsql db [:create-index predicate_object_idx :on triples [predicate object]]) - (emacsql db [:create-unique-index subject_predicate_object_properties_idx :on triples [subject predicate object properties]])) + (let* ((db (sqlite-open file))) + (sqlite-execute db "CREATE TABLE IF NOT EXISTS triples(subject TEXT NOT NULL, predicate TEXT NOT NULL, object TEXT, PROPERTIES TEXT NOT NULL)") + (sqlite-execute db "CREATE INDEX IF NOT EXISTS subject_idx ON triples (subject)") + (sqlite-execute db "CREATE INDEX IF NOT EXISTS subject_predicate_idx ON triples (subject, predicate)") + (sqlite-execute db "CREATE INDEX IF NOT EXISTS predicate_object_idx ON triples (predicate, object)") + (sqlite-execute db "CREATE INDEX IF NOT EXISTS subject_predicate_object_properties_idx ON triples (subject, predicate, object, properties)") db)) +(defun triples-close (db) + "Close sqlite database DB." + (sqlite-close db)) + (defun triples--ensure-property-val (vec) "Return a VEC has 4 elements. We add a bogus value as a property because we want to be able @@ -81,52 +76,62 @@ values." "Add a colon to SYM." (intern (format ":%s" sym))) +(defun triples-standardize-val (val) + "If VAL is a string, return it as enclosed in quotes +This is done to have compatibility with the way emacsql stores +values." + (if (stringp val) + (format "\"%s\"" val) + val)) + (defun triples--add (db op) "Perform OP on DB." (pcase (car op) ('replace-subject (mapc (lambda (sub) - (emacsql db [:delete :from triples :where (= subject $s1)] sub)) + (sqlite-execute db "DELETE FROM TRIPLES WHERE subject = ?" + (list (triples-standardize-val sub)))) (triples--subjects (cdr op)))) ('replace-subject-type (mapc (lambda (sub-triples) (mapc (lambda (type) ;; We have to ignore base, which keeps type information in general. (unless (eq type 'base) - (emacsql db [:delete :from triples :where (= subject $s1) - :and (like predicate $r2)] - (car sub-triples) (format "%s/%%" type)))) + (sqlite-execute db "DELETE FROM TRIPLES WHERE SUBJECT = ? AND PREDICATE LIKE ?" + (list (triples-standardize-val (car sub-triples)) + (format "%s/%%" type))))) (seq-uniq (mapcar #'car (mapcar #'triples-combined-to-type-and-prop (mapcar #'cl-second (cdr sub-triples))))))) (triples--group-by-subjects (cdr op))))) - (mapc (lambda (triple) - (emacsql db [:replace :into triples - :values $v1] (triples--ensure-property-val - (apply #'vector triple)))) + (mapc (lambda (triple) + (sqlite-execute db "REPLACE INTO TRIPLES VALUES (?, ?, ?, ?)" + (triples--ensure-property-val + (apply #'vector (mapcar #'triples-standardize-val triple))))) (cdr op))) (defun triples-properties-for-predicate (db cpred) "Return the properties in DB for combined predicate CPRED as a plist." (mapcan (lambda (row) (list (intern (format ":%s" (nth 1 row))) (nth 2 row))) - (emacsql db [:select * :from triples :where (= subject $s1)] cpred))) + (sqlite-select db "SELECT * FROM TRIPLES WHERE subject = ?" + (list (triples-standardize-val cpred))))) (defun triples-predicates-for-type (db type) "Return all predicates defined for TYPE in DB." (mapcar #'car - (emacsql db [:select object :from triples :where (= subject $s1) - :and (= predicate 'schema/property)] type))) + (sqlite-select db "SELECT object FROM triples WHERE subject = ? AND predicate = 'schema/property'" + (list (triples-standardize-val type))))) (defun triples-verify-schema-compliant (db triples) "Error if TRIPLES is not compliant with schema in DB." (mapc (lambda (triple) (pcase-let ((`(,type . ,prop) (triples-combined-to-type-and-prop (nth 1 triple)))) (unless (or (eq type 'base) - (emacsql db [:select * :from triples :where (= subject $s1) - :and (= predicate 'schema/property) :and (= object $s2)] - type prop)) + (sqlite-select db "SELECT * FROM triples WHERE subject = ? AND predicate = 'schema/property' +AND object = ?" + (list (triples-standardize-val type) (triples-standardize-val prop)))) (error "Property %s not found in schema" (nth 1 triple))))) triples) (mapc (lambda (triple) @@ -170,6 +175,19 @@ PROPERTIES is a plist of properties, without TYPE prefixes." (triples-verify-schema-compliant db (cdr op)) (triples--add db op))) +(defmacro triples-with-transaction (db &rest body) + "Create a transaction using DB, executing BODY. +The transaction will abort if an error is thrown." + (declare (indent 0) (debug t)) + (let ((db-var (gensym "db"))) + `(condition-case + (let ((,db-var ,db)) + (progn + (sqlite-transaction ,db-var) + ,@body + (sqlite-commit ,db-var))) + (error (sqlite-rollback ,db-var))))) + (defun triples-set-types (db subject &rest combined-props) "Set all data for types in COMBINED-PROPS in DB for SUBJECT. COMBINED-PROPS is a plist which takes combined properties such as @@ -183,7 +201,7 @@ given in the COMBINED-PROPS will be removed." (plist-put (gethash (triples--decolon type) type-to-plist) (triples--encolon prop) val) type-to-plist))) combined-props) - (emacsql-with-transaction db + (triples-with-transaction db (cl-loop for k being the hash-keys of type-to-plist using (hash-values v) do (apply #'triples-set-type db subject k v))))) @@ -212,8 +230,9 @@ PROPERTIES is a plist of properties, without TYPE prefixes." (cons (cons (nth 2 db-triple) (nth 3 db-triple)) (gethash (nth 1 db-triple) preds)) preds)) - (emacsql db [:select * :from triples :where (= subject $s1) - :and (like predicate $r2)] subject (format "%s/%%" type))) + (sqlite-select db "SELECT * FROM triples WHERE subject = ? AND predicate LIKE ?" + (list (triples-standardize-val subject) + (format "%s/%%" type)))) (append (cl-loop for k being the hash-keys of preds using (hash-values v) nconc (list (triples--encolon (cdr (triples-combined-to-type-and-prop k))) @@ -230,23 +249,27 @@ PROPERTIES is a plist of properties, without TYPE prefixes." db (triples-type-and-prop-to-combined type pred)) :base/virtual-reversed))) (when reversed-prop - (let ((result (emacsql db [:select subject :from triples :where (= object $s1) - :and (= predicate $s2)] subject reversed-prop))) + (let ((result + (sqlite-select db "SELECT subject FROM triples WHERE object = ? AND predicate = ?" + (triples-standardize-val (subject)) + reversed-prop))) (when result (cons (triples--encolon pred) (list (mapcar #'car result))))))))))) (defun triples-remove-type (db subject type) "Remove TYPE for SUBJECT in DB, and all associated data." - (emacsql-with-transaction db - (emacsql db [:delete :from triples :where (= subject $s1) - :and (= predicate 'base/type)] subject) - (emacsql db [:delete :from triples :where (= subject $s1) - :and (like $r2)] subject (format "%s/%%" type)))) + (triples-with-transaction + db + (sqlite-execute db "DELETE FROM TRIPLES WHERE subject = ? AND PREDICATE = 'base/type' AND object = ?" + (list (triples-standardize-val subject) type)) + (sqlite-execute db "DELETE FROM TRIPLES WHERE subject = ? AND PREDICATE LIKE ?" + (list (triples-standardize-val subject) + (format "%s/%%" type))))) (defun triples-get-types (db subject) "From DB, get all types for SUBJECT." - (mapcar #'car (emacsql db [:select object :from triples :where (= subject $s1) - :and (= predicate 'base/type)] - subject))) + (mapcar #'car + (sqlite-select db "SELECT object FROM triples WHERE subject = ? AND predicate = 'base/type'" + (list (triples-standardize-val subject))))) (defun triples-get-subject (db subject) "From DB return all properties for SUBJECT as a single plist." @@ -260,7 +283,7 @@ PROPERTIES is a plist of properties, without TYPE prefixes." (defun triples-set-subject (db subject &rest type-vals-cons) "From DB set properties of SUBJECT to TYPE-VALS-CONS data. TYPE-VALS-CONS is a list of conses, combining a type and a plist of values." - (emacsql-with-transaction db + (triples-with-transaction db (triples-delete-subject db subject) (mapc (lambda (cons) (apply #'triples-set-type db subject cons)) @@ -268,23 +291,25 @@ TYPE-VALS-CONS is a list of conses, combining a type and a plist of values." (defun triples-delete-subject (db subject) "Delete all data in DB associated with SUBJECT." - (emacsql-with-transaction db - (emacsql db [:delete :from triples :where (= subject $s1)] subject))) + (sqlite-execute db "DELETE FROM triples WHERE SUBJECT = ?" + (list (triples-standardize-val subject)))) (defun triples-search (db cpred text) "Search DB for instances of combined property CPRED with TEXT." - (emacsql db [:select * :from triples :where (= predicate $i1) - :and (like object $r2)] (triples--decolon cpred) - (format "%%%s%%" text))) + (sqlite-select db "SELECT * FROM triples WHERE predicate = ? AND object LIKE ?" + (list (triples--decolon cpred) + (format "%%%s%%" text)))) (defun triples-with-predicate (db cpred) "Return all triples in DB with CPRED as its combined predicate." - (emacsql db [:select * :from triples :where (= predicate $i1)] (triples--decolon cpred))) + (sqlite-select db "SELECT * FROM triples WHERE predicate = ?" + (list (triples--decolon cpred)))) (defun triples-subjects-with-predicate-object (db cpred obj) "Return all subjects in DB with CPRED equal to OBJ." - (emacsql db [:select subject :from triples :where (= predicate $i1) :and (= object $s2)] - (triples--decolon cpred) obj)) + (sqlite-select db "SELECT subject FROM triples WHERE predicate = ? AND object = ?" + (list (triples--decolon cpred) + (triples-standardize-val obj)))) (defun triples-subjects-of-type (db type) "Return a list of all subjects with a particular TYPE in DB."