branch: externals/triples commit 5e17182a5374a1656761bb26832fd21c6f168685 Author: Andrew Hyatt <ahy...@gmail.com> Commit: GitHub <nore...@github.com>
Add a float type and document types (#18) --- README.org | 2 ++ triples-test.el | 72 ++++++++++++++++++++++++++++++++++++++++++--------------- triples.el | 11 +++++---- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/README.org b/README.org index 1e4112e24b..34b6151183 100644 --- a/README.org +++ b/README.org @@ -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. diff --git a/triples-test.el b/triples-test.el index 39c9ef6021..590f11aa27 100644 --- a/triples-test.el +++ b/triples-test.el @@ -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)))))) @@ -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)))))) @@ -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)) @@ -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 diff --git a/triples.el b/triples.el index 4eb1412295..63d20b447a 100644 --- a/triples.el +++ b/triples.el @@ -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 <ahy...@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 @@ -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) @@ -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)))