Skip to content

Commit

Permalink
Merge pull request #454 from fluree/feature/equivalent-properties
Browse files Browse the repository at this point in the history
Feature/equivalent properties
  • Loading branch information
zonotope authored Apr 17, 2023
2 parents a98b64d + d2bb91d commit ffdf9a9
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 69 deletions.
3 changes: 2 additions & 1 deletion src/fluree/db/json_ld/ledger.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


(defn class-or-property?
[{:keys [type] :as node}]
[{:keys [type] :as _node}]
(some class+property-iris (util/sequential type)))

(def ^:const predefined-properties
Expand All @@ -32,6 +32,7 @@
"http://www.w3.org/2002/07/owl#Class" const/$owl:Class
"http://www.w3.org/2002/07/owl#ObjectProperty" const/$owl:ObjectProperty
"http://www.w3.org/2002/07/owl#DatatypeProperty" const/$owl:DatatypeProperty
"http://www.w3.org/2002/07/owl#equivalentProperty" const/$_predicate:equivalentProperty
;; shacl
"http://www.w3.org/ns/shacl#NodeShape" const/$sh:NodeShape
"http://www.w3.org/ns/shacl#PropertyShape" const/$sh:PropertyShape
Expand Down
168 changes: 105 additions & 63 deletions src/fluree/db/json_ld/vocab.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,13 @@
[fluree.db.constants :as const]
[fluree.db.util.async :refer [<? go-try]]
[fluree.db.query.range :as query-range]
[fluree.db.util.schema :as schema-util]))
[fluree.db.util.schema :as schema-util]
[clojure.set :as set]))

#?(:clj (set! *warn-on-reflection* true))

;; generates vocabulary/schema pre-cached maps.

(def property-sids #{const/$rdf:Property
const/$owl:DatatypeProperty
const/$owl:ObjectProperty})

(defn schema-details
[sid s-flakes]
(loop [[f & r] s-flakes
details (if (= sid const/$rdf:type)
{:id sid ;; rdf:type is predefined, so flakes to build map won't be present.
:class false
:idx? true
:ref? true}
{:id sid
:class true ;; default
:idx? true
:ref? false ;; could go from false->true if defined in vocab but hasn't been used yet
:subclassOf []
:equivalentProperty []})]
(if f
(let [pid (flake/p f)
details* (cond
(= const/$xsd:anyURI pid)
(assoc details :iri (flake/o f))

(= const/$rdf:type pid)
(if (property-sids (flake/o f))
(if (= const/$owl:ObjectProperty (flake/o f))
(assoc details :class false
:ref? true)
(assoc details :class false))
(if (= const/$xsd:anyURI (flake/o f))
(assoc details :class false
:ref? true)
;; it is a class, but we already did :class true as a default
details))

(= const/$rdfs:subClassOf pid)
(update details :subclassOf conj (flake/o f))

(= const/$_predicate:equivalentProperty pid)
(update details :equivalentProperty conj (flake/o f))

:else details)]
(recur r details*))
details)))


(defn map-pred-id+iri
"In the schema map, we index properties by both integer :id and :iri for easy lookup of either."
[properties]
Expand Down Expand Up @@ -103,8 +57,7 @@
(defn calc-subclass
"Calculates subclass map for use with queries for rdf:type."
[property-maps]
(let [classes (filter #(true? (:class %)) property-maps)
subclass-map (recur-sub-classes (vals property-maps))]
(let [subclass-map (recur-sub-classes (vals property-maps))]
;; map subclasses for both subject-id and iri
(reduce-kv
(fn [acc class-id subclasses]
Expand All @@ -117,20 +70,109 @@
(into #{} (keep #(when (true? (:ref? %)) (:id %)) property-maps)))


(defn update-with*
[{:keys [pred] :as schema} t vocab-flakes]
(loop [[s-flakes & r] (partition-by flake/s vocab-flakes)
pred* pred]
(if s-flakes
(let [sid (flake/s (first s-flakes))
prop-map (schema-details sid s-flakes)]
(recur r
(assoc pred* (:id prop-map) prop-map
(:iri prop-map) prop-map)))
(assoc schema :t t
:pred pred*
:subclasses (delay (calc-subclass pred*))))))
(def property-sids #{const/$rdf:Property
const/$owl:DatatypeProperty
const/$owl:ObjectProperty})

(defn initial-property-map
[sid]
(if (= sid const/$rdf:type)
{:id sid ; rdf:type is predefined, so flakes to build map won't be present.
:class false
:idx? true
:ref? true}
{:id sid
:class true ; default
:idx? true
:ref? false ; could go from false->true if defined in vocab but hasn't been used yet
:subclassOf #{}
:equivalentProperty #{}}))

(defn add-subclass
[prop-map subclass]
(update prop-map :subclassOf conj subclass))

(defn add-equivalent-property
[prop-map prop]
(update prop-map :equivalentProperty conj prop))

(defn update-equivalent-property
[prop-map sid prop]
(let [initial-map (initial-property-map sid)
with-equivalent-property (fnil add-equivalent-property initial-map)]
(update prop-map sid with-equivalent-property prop)))

(defn update-all-equivalent-properties
[prop-map sid o-props]
(reduce (fn [p-map o-prop]
(-> p-map
(update-equivalent-property sid o-prop)
(update-equivalent-property o-prop sid)))
prop-map o-props))

(defn update-equivalent-properties
[pred-map sid obj]
(let [s-props (-> pred-map
(get-in [sid :equivalentProperty])
(conj sid))
o-props (-> pred-map
(get-in [obj :equivalentProperty])
(conj obj))]
(reduce (fn [p-map s-prop]
(update-all-equivalent-properties p-map s-prop o-props))
pred-map s-props)))

(defn update-pred-map
[pred-map vocab-flake]
(let [[sid pid obj] ((juxt flake/s flake/p flake/o) vocab-flake)
initial-map (initial-property-map sid)
with-properties (fnil assoc initial-map)
with-subclass (fnil add-subclass initial-map)]
(cond
(= const/$xsd:anyURI pid)
(update pred-map sid with-properties :iri obj)

(= const/$rdf:type pid)
(if (property-sids obj)
(if (= const/$owl:ObjectProperty obj)
(update pred-map sid with-properties :class false, :ref? true)
(update pred-map sid with-properties :class false))
(if (= const/$xsd:anyURI obj)
(update pred-map sid with-properties :class false, :ref? true)
;; it is a class, but we already did :class true as a default
pred-map))

(= const/$rdfs:subClassOf pid)
(update pred-map sid with-subclass obj)

(= const/$_predicate:equivalentProperty pid)
(update-equivalent-properties pred-map sid obj)

:else pred-map)))

(defn with-vocab-flakes
[pred-map vocab-flakes]
(let [new-pred-map (reduce update-pred-map pred-map vocab-flakes)]
(reduce-kv (fn [preds k v]
(if (number? k)
(assoc preds k v, (:iri v) v)
preds))
{"@type" {:iri "@type"
:ref? true
:idx? true
:id const/$rdf:type}}
new-pred-map)))

(defn refresh-subclasses
[{:keys [pred] :as schema}]
(assoc schema :subclasses (delay (calc-subclass pred))))

(defn update-with*
[schema t vocab-flakes]
(-> schema
(assoc :t t)
(update :pred with-vocab-flakes vocab-flakes)
refresh-subclasses))

(defn update-with
"When creating a new db from a transaction, merge new schema changes
Expand Down
31 changes: 26 additions & 5 deletions src/fluree/db/query/exec/where.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,36 @@
(unmatched? p) (assoc (::var p) (match-predicate p flake))
(unmatched? o) (assoc (::var o) (match-object o flake)))))

(defn get-equivalent-properties
[db prop]
(-> db
(get-in [:schema :pred prop :equivalentProperty])
not-empty))

(defmethod match-pattern :tuple
[db solution pattern filters error-ch]
(let [cur-vals (assign-matched-values pattern solution filters)
_ (log/debug "assign-matched-values returned:" cur-vals)
flake-ch (resolve-flake-range db error-ch cur-vals)
(let [[s p o] (assign-matched-values pattern solution filters)
match-ch (async/chan 2 (comp cat
(map (fn [flake]
(match-flake solution pattern flake)))))]
(async/pipe flake-ch match-ch)))
(match-flake solution pattern flake)))))
p-val (::val p)]

(if-let [props (and p-val (get-equivalent-properties db p-val))]
(let [prop-ch (async/to-chan! (conj props p-val))]
(async/pipeline-async 2
match-ch
(fn [prop ch]
(let [p* (assoc p ::val prop)]
(-> db
(resolve-flake-range error-ch [s p* o])
(async/pipe ch))))
prop-ch))

(-> db
(resolve-flake-range error-ch [s p o])
(async/pipe match-ch)))

match-ch))

(defmethod match-pattern :iri
[db solution pattern filters error-ch]
Expand Down
56 changes: 56 additions & 0 deletions test/fluree/db/query/property_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
(ns fluree.db.query.property-test
(:require [clojure.test :refer [deftest is testing]]
[fluree.db.test-utils :as test-utils]
[fluree.db.json-ld.api :as fluree]))

(deftest ^:integration equivalent-properties-test
(testing "Equivalent properties"
(let [conn (test-utils/create-conn)
ledger @(fluree/create conn "query/equivalent-properties")
context {"vocab1" "http://vocab1.example.org/"
"vocab2" "http://vocab1.example.org/"
"vocab3" "http://vocab3.example.fr/"
"ex" "http://example.org/ns/"
"rdf" "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
"owl" "http://www.w3.org/2002/07/owl#"}
db (-> ledger
fluree/db
(fluree/stage {"@context" context
"@graph" [{"@id" "vocab1:givenName"
"@type" "rdf:Property"}
{"@id" "vocab2:firstName"
"@type" "rdf:Property"
"owl:equivalentProperty" {"@id" "vocab1:givenName"}}
{"@id" "vocab3:prenom"
"@type" "rdf:Property"
"owl:equivalentProperty" {"@id" "vocab2:firstName"}}]})
deref
(fluree/stage {"@context" context
"@graph" [{"@id" "ex:brian"
"vocab1:givenName" "Brian"}
{"@id" "ex:ben"
"vocab2:firstName" "Ben"}
{"@id" "ex:francois"
"vocab3:prenom" "Francois"}]})
deref)]
(testing "querying for the property defined to be equivalent"
(is (= [["Brian"] ["Ben"] ["Francois"]]
@(fluree/query db '{"@context" {"vocab1" "http://vocab1.example.org/"
"vocab2" "http://vocab1.example.org/"}
:select [?name]
:where [[?s "vocab2:firstName" ?name]]}))
"returns all values"))
(testing "querying for the symmetric property"
(is (= [["Brian"] ["Ben"] ["Francois"]]
@(fluree/query db '{"@context" {"vocab1" "http://vocab1.example.org/"
"vocab2" "http://vocab1.example.org/"}
:select [?name]
:where [[?s "vocab1:givenName" ?name]]}))
"returns all values"))
(testing "querying for the transitive properties"
(is (= [["Brian"] ["Ben"] ["Francois"]]
@(fluree/query db '{"@context" {"vocab1" "http://vocab1.example.org/"
"vocab3" "http://vocab3.example.fr/"}
:select [?name]
:where [[?s "vocab3:prenom" ?name]]}))
"returns all values")))))

0 comments on commit ffdf9a9

Please sign in to comment.