Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ This adds a type called =person=, which can be set on any entity. There's anoth

The =person= has 2 properties, =name=, and =age=. They are both marked as unique, so they take a single value, not a list. If =:base/unique= was not true, the value would be a list. We also specify what type it is, which can be any elisp type. =employee= is similarly constructed, but has an interesting property, =reportees=, which is a =base/virtual-reversed= property, meaning that it is supplied with values, but rather can get them from the reversed relation of =employee/manager=.

A valid =base/type= maps to elisp types, so can be values such as =integer=, =float=, =vector=, =cons=, =symbol=, or =string=.

We'll explore how these types are used can be used in the section after next.
** The triples concept
A triple is a unit of data consisting of a /subject/, a /predicate/, an /object/, and, optionally, internal metadata about the unit. The triple can be thought of as a link between the subject and object via the predicate.
Expand Down
72 changes: 53 additions & 19 deletions triples-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -155,21 +155,45 @@
;; this will fail.
(should (= 0 (length (triples-db-select-pred-op db :person/age '> 1000))))))

(triples-deftest triples-test-db-select-pred-op-float ()
(triples-test-with-temp-db
(triples-add-schema db 'measurement '(value :base/unique t :base/type float))
(triples-set-subject db 'm1 '(measurement :value 20.5))
(triples-set-subject db 'm2 '(measurement :value 40.25))
(triples-set-subject db 'm3 '(measurement :value 60.0))
(should (= 2 (length (triples-db-select-pred-op db :measurement/value '> 20.5))))
(should (= 3 (length (triples-db-select-pred-op db :measurement/value '>= 20.5))))
(should (= 2 (length (triples-db-select-pred-op db :measurement/value '!= 20.5))))
(should (= 3 (length (triples-db-select-pred-op db :measurement/value '!= 30.0))))
(should (= 1 (length (triples-db-select-pred-op db :measurement/value '= 20.5))))
(should (= 2 (length (triples-db-select-pred-op db :measurement/value '< 60.0))))
(should (= 3 (length (triples-db-select-pred-op db :measurement/value '<= 60.0))))
(should (= 0 (length (triples-db-select-pred-op db :measurement/value '> 60.0))))
;; Test with a value that might cause issues if treated as string
(should (= 0 (length (triples-db-select-pred-op db :measurement/value '> 100.75))))))

(ert-deftest triples-test-symbols ()
(triples-test-with-temp-db
(triples-add-schema db 'enum '(value :base/unique t :base/type symbol))
(triples-set-type db 'foo 'enum :value 'bar)
(should (equal '(:value bar) (triples-get-type db 'foo 'enum)))))

(ert-deftest triples-test-builtin-emacsql-compat ()
(cl-loop for subject in '(1 a "a") do
(let ((triples-sqlite-interface 'builtin))
(triples-test-with-temp-db
(triples-add-schema db 'person
'(name :base/unique t :base/type string)
'(age :base/unique t :base/type integer))
(triples-set-type db subject 'person :name "Alice Aardvark" :age 41)
(should (equal (triples-get-type db subject 'person)
'(:age 41 :name "Alice Aardvark")))
'(age :base/unique t :base/type integer)
'(temperature :base/unique t :base/type float))
(triples-set-type db subject 'person :name "Alice Aardvark" :age 41 :temperature 36.6)
(should (equal (triples-test-plist-sort (triples-get-type db subject 'person))
(triples-test-plist-sort '(:age 41 :name "Alice Aardvark" :temperature 36.6))))
(triples-close db)
(let* ((triples-sqlite-interface 'emacsql)
(db (triples-connect db-file)))
(should (equal (triples-get-type db subject 'person)
'(:age 41 :name "Alice Aardvark")))
(should (equal (triples-test-plist-sort (triples-get-type db subject 'person))
(triples-test-plist-sort '(:age 41 :name "Alice Aardvark" :temperature 36.6))))
(triples-close db))
;; Just so the last close will work.
(setq db (triples-connect db-file))))))
Expand All @@ -180,15 +204,16 @@
(triples-test-with-temp-db
(triples-add-schema db 'person
'(name :base/unique t :base/type string)
'(age :base/unique t :base/type integer))
(triples-set-type db subject 'person :name "Alice Aardvark" :age 41)
(should (equal (triples-get-type db subject 'person)
'(:age 41 :name "Alice Aardvark")))
'(age :base/unique t :base/type integer)
'(temperature :base/unique t :base/type float))
(triples-set-type db subject 'person :name "Alice Aardvark" :age 41 :temperature 36.6)
(should (equal (triples-test-plist-sort (triples-get-type db subject 'person))
(triples-test-plist-sort '(:age 41 :name "Alice Aardvark" :temperature 36.6))))
(triples-close db)
(let* ((triples-sqlite-interface 'builtin)
(db (triples-connect db-file)))
(should (equal (triples-get-type db subject 'person)
'(:age 41 :name "Alice Aardvark")))
(should (equal (triples-test-plist-sort (triples-get-type db subject 'person))
(triples-test-plist-sort '(:age 41 :name "Alice Aardvark" :temperature 36.6))))
(triples-close db))
;; Just so the last close will work.
(setq db (triples-connect db-file))))))
Expand Down Expand Up @@ -273,9 +298,17 @@
("Bert" named/alias "Bert" (:index 0))
("Bert" named/alias "Berty" (:index 1)))))))

(defun triples-test-plist-sort (plist)
"Sort PLIST in a standard way, for comparison."
(kvalist->plist
(kvalist-sort (kvplist->alist plist)
(lambda (a b) (string< (format "%s" a) (format "%s" b))))))

(ert-deftest triples-schema-compliant ()
(let ((pal '((named/name :base/type string :base/unique t)
(named/alternate-names :base/type string :base/unique nil)
(measurement/value :base/type float :base/unique t)
(enum/value :base/type symbol :base/unique t)
;; Alias doesn't specify base/unique or base/type, so anything is fine.
(named/alias))))
(should (triples-verify-schema-compliant '(("foo" named/name "bar")) pal))
Expand All @@ -285,13 +318,14 @@
(should-error (triples-verify-schema-compliant '(("foo" named/alternate-names "bar" nil)) pal))
(should (triples-verify-schema-compliant '(("foo" named/alias "bar" nil)) pal))
(should (triples-verify-schema-compliant '(("foo" named/alias 5 nil)) pal))
(should (triples-verify-schema-compliant '(("foo" named/alias 5 (:index 0))) pal))))

(defun triples-test-plist-sort (plist)
"Sort PLIST in a standard way, for comparison."
(kvalist->plist
(kvalist-sort (kvplist->alist plist)
(lambda (a b) (string< (format "%s" a) (format "%s" b))))))
(should (triples-verify-schema-compliant '(("foo" named/alias 5 (:index 0))) pal))
;; Integers are not floats, so cannot be used for float values.
(should-error (triples-verify-schema-compliant '(("m1" measurement/value 36)) pal))
(should (triples-verify-schema-compliant '(("m1" measurement/value 36.6)) pal))
(should-error (triples-verify-schema-compliant '(("m1" measurement/value "not-a-float")) pal))
(should-error (triples-verify-schema-compliant '(("m1" measurement/value 36.6 (:index 0))) pal))
(should-error (triples-verify-schema-compliant '(("foo" enum/value "mysymbol")) pal))
(should (triples-verify-schema-compliant '(("foo" enum/value mysymbol)) pal))))

(ert-deftest triples-crud ()
(triples-test-with-temp-db
Expand Down
11 changes: 6 additions & 5 deletions triples.el
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
;;; triples.el --- A flexible triple-based database for use in apps -*- lexical-binding: t; -*-

;; Copyright (c) 2022, 2023 Free Software Foundation, Inc.
;; Copyright (c) 2022-2025 Free Software Foundation, Inc.

;; Author: Andrew Hyatt <ahyatt@gmail.com>
;; Homepage: https://github.com/ahyatt/triples
;; Package-Requires: ((seq "2.0") (emacs "28.1"))
;; Keywords: triples, kg, data, sqlite
;; Version: 0.5.1
;; Version: 0.6.0
;; This program 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 2 of the
Expand Down Expand Up @@ -205,6 +205,7 @@ to be stringified."
;; what it would be turned into by the pcase above.
((pred null) "()")
((pred integerp) val)
((pred floatp) val)
(_ (format "%S" val)))))

(defun triples-standardize-result (result)
Expand Down Expand Up @@ -328,9 +329,9 @@ If LIMIT is a positive integer, limit the results to that number."
(sqlite-select
db
(concat "SELECT * FROM triples WHERE predicate = ? AND "
(if (numberp val)
"CAST(object AS INTEGER) "
"object COLLATE NOCASE ")
(cond ((integerp val) "CAST(object AS INTEGER) ")
((floatp val) "CAST(object AS REAL) ")
(t "object COLLATE NOCASE "))
(symbol-name op) " ?"
(when properties " AND properties = ?")
(when (and limit (> limit 0)) (format " LIMIT %d" limit)))
Expand Down