[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/triples d82cc1d6b8 14/19: Finish basic sqlite layer, an
From: |
ELPA Syncer |
Subject: |
[elpa] externals/triples d82cc1d6b8 14/19: Finish basic sqlite layer, and fix everything so tests work. |
Date: |
Sat, 5 Nov 2022 11:58:17 -0400 (EDT) |
branch: externals/triples
commit d82cc1d6b8c2cc439dce20cf3399f96e27d0701a
Author: Andrew Hyatt <ahyatt@gmail.com>
Commit: Andrew Hyatt <ahyatt@gmail.com>
Finish basic sqlite layer, and fix everything so tests work.
Also, finish testing for the basic sqlite layer.
---
triples-test.el | 101 ++++++++++++++++++++++++++++---
triples.el | 184 ++++++++++++++++++++++++++++++++++++++------------------
2 files changed, 217 insertions(+), 68 deletions(-)
diff --git a/triples-test.el b/triples-test.el
index 2844dccf96..2c9afe098d 100644
--- a/triples-test.el
+++ b/triples-test.el
@@ -26,6 +26,87 @@ easily debug into it.")
(let ((sql-database triples-test-db-file))
(sql-sqlite (format "*schema test db SQL %s*" triples-test-db-file))))
+(ert-deftest triples-test-insert ()
+ (triples-test-with-temp-db
+ (triples--insert db "sub" 'pred "obj")
+ ;; Test for emacsql compability
+ (should (equal (sqlite-select db "SELECT * FROM triples")
+ '(("\"sub\"" "pred" "\"obj\"" "()"))))
+ ;; Test that it replaces - this shouldn't result in two rows.
+ (triples--insert db "sub" 'pred "obj")
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples")
+ '((1))))
+ ;; Test that colons in the predicate are stripped away when stored.
+ (triples--insert db "sub" :test/pred "obj")
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples WHERE
predicate = ?"
+ '("test/pred"))
+ '((1))))
+ ;; Test we correctly test for bad inputs.
+ (should-error (triples--insert db "sub" "pred" "obj"))
+ (should-error (triples--insert db "sub" 'pred "obj" '(ordinary-list)))
+ (should-error (triples--insert db "sub" 'pred "obj" "string"))
+ ;; Test that we can have symbol subject and objects
+ (triples--insert db 'sub 'pred 'obj)
+ (should (equal (sqlite-select db "SELECT * FROM triples WHERE subject = ?"
'("sub"))
+ '(("sub" "pred" "obj" "()"))))))
+
+(ert-deftest triples-test-delete ()
+ (triples-test-with-temp-db
+ (triples--insert db 1 'pred 2)
+ (triples--insert db 2 'pred 1)
+ (triples--delete db 1)
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples")
+ '((1))))
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples WHERE
subject = ?" '(1))
+ '((0))))
+ (triples--insert db 1 'pred 2)
+ (triples--delete db nil nil 2)
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples WHERE
object = ?" '(2))
+ '((0))))
+ (triples--insert db 1 'pred 2)
+ (triples--delete db nil 'pred nil)
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples")
+ '((0))))))
+
+(ert-deftest triples-test-delete-subject-predicate-prefix ()
+ (triples-test-with-temp-db
+ (triples--insert db 1 'test/foo 2)
+ (triples--insert db 1 'bar/bar 1)
+ (triples--delete-subject-predicate-prefix db 1 'test)
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples")
+ '((1))))
+ ;; Make sure colons are stripped.
+ (triples--delete-subject-predicate-prefix db 1 :bar)
+ (should (equal (sqlite-select db "SELECT count(*) FROM triples")
+ '((0))))))
+
+(ert-deftest triples-test-select ()
+ (triples-test-with-temp-db
+ (triples--insert db 1 'pred 2 '(:a 1))
+ (let ((expected '((1 pred 2 (:a 1)))))
+ (should (equal (triples--select db 1) expected))
+ (should (equal (triples--select db nil 'pred) expected))
+ (should (equal (triples--select db nil nil 2) expected))
+ (should (equal (triples--select db 1 nil 2) expected))
+ (should (equal (triples--select db 1 'pred 2) expected))
+ (should (equal '((1)) (triples--select db 1 nil nil nil '(subject))))
+ (should (equal '((1 pred)) (triples--select db 1 nil nil nil '(subject
predicate)))))))
+
+(ert-deftest triples-test-select-with-pred-prefix ()
+ (triples-test-with-temp-db
+ (triples--insert db 'sub1 'pred/foo 'obj)
+ (triples--insert db 'sub1 'pred/bar 'obj)
+ (triples--insert db 'sub2 'pred/foo 'obj)
+ (should (equal (triples-test-list-sort (triples--select-pred-prefix db
'sub1 'pred))
+ (triples-test-list-sort '((sub1 pred/foo obj nil)
+ (sub1 pred/bar obj nil)))))))
+
+(ert-deftest triples-test-select-predicate-object-fragment ()
+ (triples-test-with-temp-db
+ (triples--insert db 'sub1 'pred/foo "a whole phrase")
+ (should (equal (triples--select-predicate-object-fragment db 'pred/foo
"whole")
+ '((sub1 pred/foo "a whole phrase" nil))))))
+
(defun triples-test-op-equals (result target)
(and (equal (car result) (car target))
(seq-set-equal-p (cdr result) (cdr target) #'equal)))
@@ -164,15 +245,15 @@ easily debug into it.")
(ert-deftest triples-with-predicate ()
(triples-test-with-temp-db
(triples-add-schema db 'named '(name))
- (should-not (triples-with-predicate db :named/name))
+ (should-not (triples-with-predicate db 'named/name))
(triples-set-type db "foo" 'named :name "My Name Is Fred Foo")
(triples-set-type db "bar" 'named :name "My Name Is Betty Bar")
(should (equal
- '(("bar" named/name "My Name Is Betty Bar" (:empty t))
- ("foo" named/name "My Name Is Fred Foo" (:empty t)))
- (sort (triples-with-predicate db :named/name)
- (lambda (a b)
- (string< (car a) (car b))))))))
+ (triples-test-list-sort
+ '(("bar" named/name "My Name Is Betty Bar" nil)
+ ("foo" named/name "My Name Is Fred Foo" nil)))
+ (triples-test-list-sort
+ (triples-with-predicate db 'named/name))))))
(ert-deftest triples-subjects-of-type ()
(triples-test-with-temp-db
@@ -189,12 +270,12 @@ easily debug into it.")
(triples-add-schema db 'marker)
(triples-set-type db "foo" 'marker)
(should (equal '((1))
- (sqlite-select db "COUNT(*) FROM triples WHERE subject = ?
AND predicate = 'base/type' AND object = 'marker'"
- (triples-standardize-val "foo"))))
+ (sqlite-select db "SELECT COUNT(*) FROM triples WHERE
subject = ? AND predicate = 'base/type' AND object = 'marker'"
+ (list (triples-standardize-val "foo")))))
(triples-set-type db "foo" 'marker)
(should (equal '((1))
- (sqlite-select db "COUNT(*) FROM triples WHERE subject = ?
AND predicate = 'base/type' AND object = 'marker'"
- (triples-standardize-val "foo"))))))
+ (sqlite-select db "SELECT COUNT(*) FROM triples WHERE
subject = ? AND predicate = 'base/type' AND object = 'marker'"
+ (list (triples-standardize-val "foo")))))))
(ert-deftest triples-readme ()
(triples-test-with-temp-db
diff --git a/triples.el b/triples.el
index bf9403d9ea..37c51e7e3c 100644
--- a/triples.el
+++ b/triples.el
@@ -28,32 +28,24 @@
(require 'cl-macs)
(require 'seq)
+(require 'subr-x)
;;; Code:
(defun triples-connect (file)
"Connect to the database FILE and make sure it is populated."
(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 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)")
+ (sqlite-execute db "CREATE UNIQUE 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
-to enforce unique constraints, which sqlite will not do will NULL
-values."
- (if (= (length vec) 4)
- vec
- (vconcat vec '((:empty t)))))
-
(defun triples--subjects (triples)
"Return all unique subjects in TRIPLES."
(seq-uniq (mapcar #'car triples)))
@@ -79,10 +71,108 @@ values."
(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))
+values. Turn a symbol into a string as well, but not a quoted
+one, because sqlite cannot handle symbols."
+ (if val
+ (pcase (type-of val)
+ ('string (format "\"%s\"" val))
+ ('symbol (format "%s" val))
+ ('cons (format "%s" val))
+ (_ val))
+ ;; Just to save a bit of space, let's use "()" instead of "null", which is
+ ;; what it would be turned into by the pcase above.
+ "()"))
+
+(defun triples-standardize-result (result)
+ "Return RESULT in standardized form.
+This imitates the way emacsql returns items, with strings
+becoming either symbols, lists, or strings depending on whether
+the string itself is wrapped in quotes."
+ (if (and (string-prefix-p "\"" result)
+ (string-suffix-p "\"" result))
+ (string-remove-suffix "\"" (string-remove-prefix "\"" result))
+ (read result)))
+
+(defun triples--insert (db subject predicate object &optional properties)
+ "Insert triple to DB: SUBJECT, PREDICATE, OBJECT with PROPERTIES.
+This is a SQL replace operation, because we don't want any
+duplicates; if the triple is the same, it has to differ at least
+with PROPERTIES. This is a low-level function that bypasses our
+normal schema checks, so should not be called from client programs."
+ (unless (symbolp predicate)
+ (error "Predicates in triples must always be symbols"))
+ (unless (plistp properties)
+ (error "Properties stored must always be plists"))
+ (sqlite-execute db "REPLACE INTO TRIPLES VALUES (?, ?, ?, ?)"
+ (list (triples-standardize-val subject)
+ (triples-standardize-val (triples--decolon predicate))
+ (triples-standardize-val object)
+ ;; Properties cannot be null, since in sqlite each
null value
+ ;; is distinct from each other, so replace would not
replace
+ ;; duplicate triples each with null properties.
+ (triples-standardize-val properties))))
+
+(defun triples--delete (db &optional subject predicate object properties)
+ "Delete triples matching SUBJECT, PREDICATE, OBJECT, PROPERTIES.
+If any of these are nil, they will not selected for. If you set
+all to nil, everything will be deleted, so be careful!"
+ (sqlite-execute
+ db
+ (concat "DELETE FROM TRIPLES"
+ (when (or subject predicate object properties)
+ (concat " WHERE "
+ (string-join
+ (seq-filter #'identity
+ (list (when subject "SUBJECT = ?")
+ (when predicate "PREDICATE = ?")
+ (when object "OBJECT = ?")
+ (when properties "PROPERTIES = ?")))
+ " AND "))))
+ (mapcar #'triples-standardize-val (seq-filter #'identity (list subject
predicate object properties)))))
+
+(defun triples--delete-subject-predicate-prefix (db subject pred-prefix)
+ "Delete triples matching SUBJECT and predicates with PRED-PREFIX."
+ (unless (symbolp pred-prefix)
+ (error "Predicates in triples must always be symbols"))
+ (sqlite-execute db "DELETE FROM TRIPLES WHERE subject = ? AND predicate LIKE
?"
+ (list (triples-standardize-val subject)
+ (format "%s/%%" (triples--decolon pred-prefix)))))
+
+(defun triples--select-pred-prefix (db subject pred-prefix)
+ "Return rows matching SUBJECT and PRED-PREFIX."
+ (mapcar (lambda (row) (mapcar #'triples-standardize-result row))
+ (sqlite-select db "SELECT * FROM triples WHERE subject = ? AND
predicate LIKE ?"
+ (list (triples-standardize-val subject)
+ (format "%s/%%" pred-prefix)))))
+
+(defun triples--select-predicate-object-fragment (db predicate object-fragment)
+ "Return rows with PREDICATE and with OBJECT-FRAGMENT in object."
+ (mapcar (lambda (row) (mapcar #'triples-standardize-result row))
+ (sqlite-select db "SELECT * from triples WHERE predicate = ? AND
object LIKE ?"
+ (list (triples-standardize-val predicate)
+ (format "%%%s%%" object-fragment)))))
+
+(defun triples--select (db &optional subject predicate object properties
selector)
+ "Return rows matching SUBJECT, PREDICATE, OBJECT, PROPERTIES.
+If any of these are nil, they are not included in the select
+statement. The SELECTOR is list of symbols subject, precicate,
+object, properties to retrieve or nil for *."
+ (mapcar (lambda (row) (mapcar #'triples-standardize-result row))
+ (sqlite-select db
+ (concat "SELECT "
+ (if selector
+ (mapconcat (lambda (e) (format "%s" e))
selector ", ")
+ "*") " FROM triples"
+ (when (or subject predicate object
properties)
+ (concat " WHERE "
+ (string-join
+ (seq-filter #'identity
+ (list (when subject
"SUBJECT = ?")
+ (when
predicate "PREDICATE = ?")
+ (when object
"OBJECT = ?")
+ (when
properties "PROPERTIES = ?")))
+ " AND "))))
+ (mapcar #'triples-standardize-val (seq-filter
#'identity (list subject predicate object properties))))))
(defun triples--add (db op)
"Perform OP on DB."
@@ -90,48 +180,39 @@ values."
('replace-subject
(mapc
(lambda (sub)
- (sqlite-execute db "DELETE FROM TRIPLES WHERE subject = ?"
- (list (triples-standardize-val sub))))
+ (triples--delete db 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)
- (sqlite-execute db "DELETE FROM TRIPLES WHERE SUBJECT
= ? AND PREDICATE LIKE ?"
- (list (triples-standardize-val (car
sub-triples))
- (format "%s/%%" type)))))
+ (triples--delete-subject-predicate-prefix db (car
sub-triples) 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)
- (sqlite-execute db "REPLACE INTO TRIPLES VALUES (?, ?, ?, ?)"
- (triples--ensure-property-val
- (apply #'vector (mapcar #'triples-standardize-val
triple)))))
+ (apply #'triples--insert db 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)))
- (sqlite-select db "SELECT * FROM TRIPLES WHERE subject = ?"
- (list (triples-standardize-val cpred)))))
+ (triples--select db cpred)))
(defun triples-predicates-for-type (db type)
"Return all predicates defined for TYPE in DB."
(mapcar #'car
- (sqlite-select db "SELECT object FROM triples WHERE subject = ? AND
predicate = 'schema/property'"
- (list (triples-standardize-val type)))))
+ (triples--select db type 'schema/property nil nil '(object))))
(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)
- (sqlite-select db "SELECT * FROM triples WHERE subject
= ? AND predicate = 'schema/property'
-AND object = ?"
- (list (triples-standardize-val type)
(triples-standardize-val prop))))
+ (triples--select db type 'schema/property prop nil))
(error "Property %s not found in schema" (nth 1 triple)))))
triples)
(mapc (lambda (triple)
@@ -180,13 +261,13 @@ PROPERTIES is a plist of properties, without TYPE
prefixes."
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))
+ `(let ((,db-var ,db))
+ (condition-case nil
(progn
(sqlite-transaction ,db-var)
,@body
- (sqlite-commit ,db-var)))
- (error (sqlite-rollback ,db-var)))))
+ (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.
@@ -201,7 +282,8 @@ 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)
- (triples-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)))))
@@ -230,9 +312,7 @@ 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))
- (sqlite-select db "SELECT * FROM triples WHERE subject = ? AND
predicate LIKE ?"
- (list (triples-standardize-val subject)
- (format "%s/%%" type))))
+ (triples--select-pred-prefix db subject 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)))
@@ -250,26 +330,20 @@ PROPERTIES is a plist of properties, without TYPE
prefixes."
:base/virtual-reversed)))
(when reversed-prop
(let ((result
- (sqlite-select db "SELECT subject FROM triples WHERE
object = ? AND predicate = ?"
- (triples-standardize-val (subject))
- reversed-prop)))
+ (triples--select db nil reversed-prop subject nil
'(subject))))
(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."
(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)))))
+ (triples--delete db subject 'base/type type)
+ (triples--delete-subject-predicate-prefix db subject type)))
(defun triples-get-types (db subject)
"From DB, get all types for SUBJECT."
(mapcar #'car
- (sqlite-select db "SELECT object FROM triples WHERE subject = ? AND
predicate = 'base/type'"
- (list (triples-standardize-val subject)))))
+ (triples--select db subject 'base/type nil nil '(object))))
(defun triples-get-subject (db subject)
"From DB return all properties for SUBJECT as a single plist."
@@ -291,25 +365,19 @@ 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."
- (sqlite-execute db "DELETE FROM triples WHERE SUBJECT = ?"
- (list (triples-standardize-val subject))))
+ (triples--delete db subject))
(defun triples-search (db cpred text)
"Search DB for instances of combined property CPRED with TEXT."
- (sqlite-select db "SELECT * FROM triples WHERE predicate = ? AND object LIKE
?"
- (list (triples--decolon cpred)
- (format "%%%s%%" text))))
+ (triples--select-predicate-object-fragment db cpred text))
(defun triples-with-predicate (db cpred)
"Return all triples in DB with CPRED as its combined predicate."
- (sqlite-select db "SELECT * FROM triples WHERE predicate = ?"
- (list (triples--decolon cpred))))
+ (triples--select db nil cpred))
(defun triples-subjects-with-predicate-object (db cpred obj)
"Return all subjects in DB with CPRED equal to OBJ."
- (sqlite-select db "SELECT subject FROM triples WHERE predicate = ? AND
object = ?"
- (list (triples--decolon cpred)
- (triples-standardize-val obj))))
+ (triples--select db nil cpred obj))
(defun triples-subjects-of-type (db type)
"Return a list of all subjects with a particular TYPE in DB."
- [elpa] externals/triples 5e8abd2989 01/19: Initial commit of triples module., (continued)
- [elpa] externals/triples 5e8abd2989 01/19: Initial commit of triples module., ELPA Syncer, 2022/11/05
- [elpa] externals/triples cc5629fe5c 07/19: Wrap all database access in `triples-set-types' in a transaction., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 257de87fdc 08/19: Minor code cleanup., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 4627d6ed6d 10/19: Fix minor mistakes in ert tests., ELPA Syncer, 2022/11/05
- [elpa] externals/triples fb63dfe44a 12/19: Convert to sqlite., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 0252dad7d1 17/19: Fixes from code review from Stefan Monnier., ELPA Syncer, 2022/11/05
- [elpa] externals/triples d17b3d6e17 19/19: Merge branch 'combined'., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 2dae3d49b9 18/19: Various fixes for emacsql code, which wasn't being tested correctly., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 6afcb290ca 15/19: Support both emacs 29 sqlite and emacsql., ELPA Syncer, 2022/11/05
- [elpa] externals/triples aca95ba7f3 03/19: Ensure that we don't duplicate triples., ELPA Syncer, 2022/11/05
- [elpa] externals/triples d82cc1d6b8 14/19: Finish basic sqlite layer, and fix everything so tests work.,
ELPA Syncer <=
- [elpa] externals/triples 8d7d3c13f4 05/19: Make the combined to and from functions public., ELPA Syncer, 2022/11/05
- [elpa] externals/triples ad6e329540 04/19: Fix for ert tests broken by the last commit., ELPA Syncer, 2022/11/05
- [elpa] externals/triples fdbbd5f61d 09/19: Added package-requires., ELPA Syncer, 2022/11/05
- [elpa] externals/triples cef7ad3a81 11/19: Remove emacs requirement for now., ELPA Syncer, 2022/11/05
- [elpa] externals/triples cca16121d9 13/19: Fix bugs in `triples-remove-type'., ELPA Syncer, 2022/11/05
- [elpa] externals/triples 3593f55dfb 16/19: Support numbers stored via emacsql., ELPA Syncer, 2022/11/05