From a119b47a3c93ace365c11844f9759853b7ff55a5 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Fri, 10 Oct 2025 16:49:37 -0500 Subject: [PATCH 01/12] add support for SPARQL SERVICE pattern parsing --- resources/sparql.bnf | 6 +++- src/fluree/db/query/sparql/translator.cljc | 16 ++++++++-- test/fluree/db/query/sparql_test.cljc | 35 +++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/resources/sparql.bnf b/resources/sparql.bnf index 27ac0ef43c..6bbb76ddd0 100644 --- a/resources/sparql.bnf +++ b/resources/sparql.bnf @@ -65,7 +65,11 @@ TriplesBlock ::= WS TriplesSameSubjectPath WS ( <'.'> TriplesBlock? WS )? GraphPatternNotTriples ::= GroupOrUnionGraphPattern | OptionalGraphPattern | MinusGraphPattern | GraphGraphPattern | ServiceGraphPattern | Filter | Bind | InlineData OptionalGraphPattern ::= <'OPTIONAL'> GroupGraphPattern GraphGraphPattern ::= <'GRAPH'> WS VarOrIri WS GroupGraphPattern -ServiceGraphPattern ::= <'SERVICE'> WS 'SILENT'? WS VarOrIri GroupGraphPattern +ServiceGraphPattern ::= <'SERVICE'> WS 'SILENT'? WS VarOrIri ServiceClause +ServiceClause ::= WS '{' ServiceContent '}' WS + ::= (NonBrace | NestedBrace)* + ::= '{' ServiceContent '}' + ::= #"[^{}]" Bind ::= <'BIND' WS '(' WS> Expression Var InlineData ::= <'VALUES'> WS DataBlock ::= InlineDataOneVar | InlineDataFull diff --git a/src/fluree/db/query/sparql/translator.cljc b/src/fluree/db/query/sparql/translator.cljc index 522c411826..ba40971940 100644 --- a/src/fluree/db/query/sparql/translator.cljc +++ b/src/fluree/db/query/sparql/translator.cljc @@ -558,10 +558,20 @@ [[_ & patterns]] (into [:minus] (mapv parse-term patterns))) +(defmethod parse-term :ServiceClause + [[_ & chars]] + (apply str chars)) + +(defn service-pattern + [silent? [service clause]] + [:service {:silent? silent? :service (parse-term service) :clause (parse-term clause)}]) + (defmethod parse-term :ServiceGraphPattern - [_] - (throw (ex-info "SERVICE is not a supported SPARQL pattern" - {:status 400 :error :db/invalid-query}))) + ;; ServiceGraphPattern ::= <'SERVICE'> WS 'SILENT'? WS VarOrIri GroupGraphPattern + [[_ & terms]] + (if (= "SILENT" (first terms)) + (service-pattern true (rest terms)) + (service-pattern false terms))) (defmethod parse-term :DataBlockValue ;; DataBlockValue ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | 'UNDEF' WS diff --git a/test/fluree/db/query/sparql_test.cljc b/test/fluree/db/query/sparql_test.cljc index a207cfca01..c1b7716647 100644 --- a/test/fluree/db/query/sparql_test.cljc +++ b/test/fluree/db/query/sparql_test.cljc @@ -738,7 +738,40 @@ {:select ["?y" "(as (min ?name) ?minName)"], :where [{"@id" "?y", ":name" "?name"}], :groupBy ["?y"]}]]} - (sparql/->fql query)))))) + (sparql/->fql query))))) + (testing "SERVICE" + (let [q "PREFIX : + SELECT ?foo ?bar + WHERE { + ?s :foo ?foo . + SERVICE { + ?s :bar ?bar . + } + }"] + (is (= [{"@id" "?s", ":foo" "?foo"} + [:service + {:silent? false, + :service "https://query.wikidata.org/sparql", + :clause "{ + ?s :bar ?bar . + }"}]] + (:where (sparql/->fql q))))) + (let [q "PREFIX : + SELECT ?foo ?bar + WHERE { + ?s :foo ?foo . + SERVICE SILENT { + ?s :bar ?bar . + } + }"] + (is (= [{"@id" "?s", ":foo" "?foo"} + [:service + {:silent? true, + :service "https://query.wikidata.org/sparql", + :clause "{ + ?s :bar ?bar . + }"}]] + (:where (sparql/->fql q))))))) (deftest parse-prefixes (testing "PREFIX" From 49346ca01800a79a8abf91fa7d66015ee69e68e2 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Fri, 10 Oct 2025 16:52:23 -0500 Subject: [PATCH 02/12] add support for jld-query :service pattern --- src/fluree/db/query/exec/where.cljc | 57 +++++++++++++++++++++++++++++ src/fluree/db/query/fql/parse.cljc | 23 ++++++++---- src/fluree/db/query/sparql.cljc | 13 +++++++ src/fluree/db/validation.cljc | 9 ++++- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index 0b7358e754..6843a5103d 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -11,7 +11,9 @@ [fluree.db.track :as track] [fluree.db.util :as util :refer [try* catch*]] [fluree.db.util.async :refer [solution + [solution vars binding] + (reduce (fn [solution* var] + (if-let [{type "type" v "value" dt "datatype" lang "xml:lang"} + (get binding var)] + (let [var-name (symbol (str "?" var)) + mch (cond (= "literal" type) + (cond-> (-> (unmatched-var var-name) + (match-value v dt)) + lang (match-lang v lang)) + (#{"uri" "bnode"} type) + (-> (unmatched-var var-name) + (match-iri v)) + :else + (throw (ex-info "Invalid SPARQL Query Results JSON Format." + {:status 400, :error :db/invalid-query + :spec "https://www.w3.org/TR/sparql11-results-json" + :binding binding})))] + (or + ;; add new var binding to solution + (update-solution-binding solution* var-name mch) + ;; already have a binding for the given var, no join + solution*)) + solution*)) + solution + vars)) + +(defn sparql-service-error! + [ex service sparql-q] + (log/error ex "Error processing service response " service sparql-q) + (ex-info (str "Error processing service response " service " due to exception: " (ex-message ex)) + {:status 400, :error :db/invalid-query} + ex)) + +(defmethod match-pattern :service + [_db _tracker solution pattern error-ch] + (let [{:keys [service silent? sparql-q]} (pattern-data pattern) + solution-ch (async/chan)] + (go + (let [response (async/! solution-ch solution) + (async/>! error-ch (sparql-service-error! response service sparql-q))) + (try* + (let [response* (json/parse response false) + vars (-> response* (get "head") (get "vars")) + bindings (-> response* (get "results") (get "bindings"))] + (->> bindings + (mapv (partial binding->solution solution vars)) + (async/onto-chan! solution-ch))) + (catch* e (async/>! error-ch (sparql-service-error! e service sparql-q))))))) + solution-ch)) (defmethod match-pattern :default [_db _tracker _solution pattern error-ch] (go diff --git a/src/fluree/db/query/fql/parse.cljc b/src/fluree/db/query/fql/parse.cljc index 8e82c8cabc..fd68f61deb 100644 --- a/src/fluree/db/query/fql/parse.cljc +++ b/src/fluree/db/query/fql/parse.cljc @@ -1,6 +1,7 @@ (ns fluree.db.query.fql.parse (:require #?(:cljs [cljs.reader :refer [read-string]]) [clojure.set :as set] + [clojure.string :as str] [clojure.walk :refer [postwalk]] [fluree.db.constants :as const] [fluree.db.datatype :as datatype] @@ -36,8 +37,9 @@ If `:parse-object-vars` is false then we will not attempt to parse string literals in the object position as variables, only specifically tagged @variable objects will be parsed as variables." - [bound-vars opts] - {:bound-vars (set bound-vars) + [bound-vars opts orig-context] + {:orig-context orig-context + :bound-vars (set bound-vars) :parse-object-vars? (get opts :object-var-parsing true)}) (defn parse-var-name @@ -639,7 +641,7 @@ (when-let [construct (:construct q)] (-> construct syntax/coerce-where - (parse-where-clause (var-parsing-config nil (:opts q)) context) + (parse-where-clause (var-parsing-config nil (:opts q) nil) context) unwrap-tuple-patterns select/construct-selector))) @@ -806,7 +808,7 @@ context (cond->> (json-ld/parse-context orig-context) parent-context (merge parent-context)) [vars values] (parse-values (:values q) context) - var-config (var-parsing-config vars (:opts q)) + var-config (var-parsing-config vars q orig-context) where (parse-where (:where q) var-config context) construct (parse-construct q context) grouping (parse-grouping q) @@ -831,6 +833,13 @@ (parse-query* context))] [(where/->pattern :query sub-query*)])) +(defmethod parse-pattern :service + [[_ {:keys [clause] :as data}] {:keys [orig-context]} _context] + (let [sparql (str/join " " (into (sparql/context->prefixes orig-context) + ["SELECT *" + (str "WHERE " clause)]))] + [(where/->pattern :service (assoc data :sparql-q sparql))])) + (defn parse-query [q] (log/trace "parse-query" q) @@ -982,7 +991,7 @@ (:context override-opts)) [vars values] (-> (get-named txn "values") (parse-values context)) - var-config (var-parsing-config vars override-opts) + var-config (var-parsing-config vars override-opts nil) where (-> (get-named txn "where") (parse-where var-config context)) var-config* (cond-> (update var-config :bound-vars into (where/clause-variables where)) @@ -1056,7 +1065,7 @@ context)) opts (-> (parse-txn-opts nil opts context) (assoc :object-var-parsing false)) - var-config (var-parsing-config nil opts) + var-config (var-parsing-config nil opts nil) parsed-txn (if turtle? (turtle/parse txn) (jld->parsed-triples txn var-config context)) @@ -1071,7 +1080,7 @@ [txn {:keys [format context] :as opts}] {:insert (if (= :turtle format) (turtle/parse txn) - (jld->parsed-triples txn (var-parsing-config nil (assoc opts :object-var-parsing false)) context)) + (jld->parsed-triples txn (var-parsing-config nil (assoc opts :object-var-parsing false) nil) context)) :context context :opts opts}) diff --git a/src/fluree/db/query/sparql.cljc b/src/fluree/db/query/sparql.cljc index 78d5be9b36..32d7dd09f0 100644 --- a/src/fluree/db/query/sparql.cljc +++ b/src/fluree/db/query/sparql.cljc @@ -70,3 +70,16 @@ (defn sparql-format? [opts] (= :sparql (:format opts))) + +(defn context->prefixes + [context] + (reduce-kv (fn [prefixes k v] + (let [prefix (if (keyword? k) (name k) k)] + (if (string? v) + (if (= "@base" prefix) + (conj prefixes (str "BASE <" v ">")) + (conj prefixes (str "PREFIX " prefix ": <" v ">"))) + ;; skip non-prefix context definitions + prefixes))) + [] + context)) diff --git a/src/fluree/db/validation.cljc b/src/fluree/db/validation.cljc index 215bdd4775..b2348131e6 100644 --- a/src/fluree/db/validation.cljc +++ b/src/fluree/db/validation.cljc @@ -309,10 +309,14 @@ ::bind [:+ {:error/message "bind values must be mappings from variables to functions"} [:catn [:var ::var] [:binding [:or ::function ::literal]]]] + ::service [:map + [:service :string] + [:silent? :boolean] + [:clause :string]] ::where-op [:and :keyword - [:enum {:error/message "unrecognized where operation, must be one of: graph, filter, optional, union, bind, values, exists, not-exists, minus"} - :graph :filter :optional :union :bind :query :values :exists :not-exists :minus]] + [:enum {:error/message "unrecognized where operation, must be one of: graph, filter, optional, union, bind, values, exists, not-exists, minus, service"} + :graph :filter :optional :union :bind :query :values :exists :not-exists :minus :service]] ::graph [:orn {:error/message "value of graph. Must be a ledger name or variable"} [:ledger ::ledger] [:variable ::var]] @@ -360,6 +364,7 @@ [:op ::where-op] [:bindings ::bind]]] [:graph [:tuple ::where-op ::graph [:ref ::where]]] + [:service [:tuple ::where-op ::service]] ;; TODO - because ::subquery is a separate registry it cannot be called here, validated in f.d.q.fql.syntax/coerce-subquery until resolved [:query [:catn [:op ::where-op] From 4f01604ba922b44b962fb0890aa00d36fca8a206 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Tue, 14 Oct 2025 08:12:34 -0500 Subject: [PATCH 03/12] allow override of from/from-named via opts --- src/fluree/db/query/api.cljc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/fluree/db/query/api.cljc b/src/fluree/db/query/api.cljc index d742c6459a..a63a566b25 100644 --- a/src/fluree/db/query/api.cljc +++ b/src/fluree/db/query/api.cljc @@ -241,8 +241,12 @@ (sanitize-query-options override-opts)) tracker (track/init opts) - default-aliases (some-> sanitized-query :from util/sequential) - named-aliases (some-> sanitized-query :from-named util/sequential)] + default-aliases (or (some-> opts :from util/sequential) + (some-> opts :ledger util/sequential) + (some-> sanitized-query :from util/sequential)) + named-aliases (or (some-> opts :from-named util/sequential) + (some-> opts :ledger util/sequential) + (some-> sanitized-query :from-named util/sequential))] (if (or (seq default-aliases) (seq named-aliases)) (let [ds ( Date: Tue, 14 Oct 2025 11:01:36 -0500 Subject: [PATCH 04/12] use solution values to constrain service query --- src/fluree/db/query/exec/where.cljc | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index 6843a5103d..75a633e70f 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -925,6 +925,35 @@ solution vars)) +(defn format-sparql-literal + "Format a value as a RDF data literal." + [mch] + (if-let [iri (get-iri mch)] + (str "<" iri ">") + (let [v (get-value mch) + dt (get-datatype-iri mch)] + (cond (get-optional mch) "UNDEF" + (datatype/JSON-LD-inferable-iris dt) (str (pr-str v)) + :else + (if-let [lang (get-lang mch)] + (str "\"" v "\"@" lang) + (if (= dt const/iri-json) + (str "\"" v "\"^^<" const/iri-rdf-json ">") + (str "\"" v "\"^^<" dt ">"))))))) + +(defn solution->values + "Format a solution as a SPARQL VALUES clause." + [solution] + (if (not-empty solution) + (let [vars (sort (keys solution))] + (str " VALUES " vars " { (" + (->> vars + (mapv (partial get solution)) + (mapv format-sparql-literal) + (reduce #(str %1 " " %2))) + ") }")) + "")) + (defn sparql-service-error! [ex service sparql-q] (log/error ex "Error processing service response " service sparql-q) @@ -934,10 +963,10 @@ (defmethod match-pattern :service [_db _tracker solution pattern error-ch] - (let [{:keys [service silent? sparql-q]} (pattern-data pattern) - solution-ch (async/chan)] + (let [{:keys [service silent? sparql-q]} (pattern-data pattern) + solution-ch (async/chan)] (go - (let [response (async/values solution)) {:headers {"Content-Type" "application/sparql-query" "Accept" "application/sparql-results+json"}}))] (if (util/exception? response) From f86dffe929d6f1294deb3ec22f4da58e6a989c32 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Tue, 14 Oct 2025 16:55:35 -0500 Subject: [PATCH 05/12] manage service joins purely locally This is much more inefficient, but some remote services can't handle a VALUES clause with variables that aren't in the query. We'll need to adding SERVICE var filtering in the future anyways to account for $-prefixed vars in the SERVICE clause, but this is the simple and correct way to handle it for now. --- src/fluree/db/query/exec/where.cljc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index 75a633e70f..b7a128f3db 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -920,7 +920,7 @@ ;; add new var binding to solution (update-solution-binding solution* var-name mch) ;; already have a binding for the given var, no join - solution*)) + (reduced nil))) solution*)) solution vars)) @@ -966,7 +966,7 @@ (let [{:keys [service silent? sparql-q]} (pattern-data pattern) solution-ch (async/chan)] (go - (let [response (async/values solution)) + (let [response (async/ response* (get "head") (get "vars")) bindings (-> response* (get "results") (get "bindings"))] (->> bindings - (mapv (partial binding->solution solution vars)) + (keep (partial binding->solution solution vars)) (async/onto-chan! solution-ch))) (catch* e (async/>! error-ch (sparql-service-error! e service sparql-q))))))) solution-ch)) From 21ea7b848ee2779bc98bd5890ff1f93c0665cee9 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Tue, 21 Oct 2025 16:38:09 -0500 Subject: [PATCH 06/12] use onto-chan! instead of >! onto-chan! closes the channel after it is done putting its collection, and I think it looks better than calling >! and the close! in a do-block. Without closing the downstream consumers are blocked. --- src/fluree/db/query/exec/where.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index b7a128f3db..b4e066fb20 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -971,7 +971,7 @@ "Accept" "application/sparql-results+json"}}))] (if (util/exception? response) (if silent? - (async/>! solution-ch solution) + (async/onto-chan! solution-ch [solution]) (async/>! error-ch (sparql-service-error! response service sparql-q))) (try* (let [response* (json/parse response false) From c99db00081aff471c734f460c0329980f4fd2ba8 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Tue, 21 Oct 2025 16:40:40 -0500 Subject: [PATCH 07/12] remove VALUES clause functions These will be a useful performance enhancement, but aren't needed for this first iteration. The only additional validation they require is to only bind vars that are present in the SERVICE clause - I ran into some errors from 3rd party services when trying to handle vars not in the clause (though that is not strictly in spec for SPARQL). --- src/fluree/db/query/exec/where.cljc | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index b4e066fb20..4954773a71 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -925,35 +925,6 @@ solution vars)) -(defn format-sparql-literal - "Format a value as a RDF data literal." - [mch] - (if-let [iri (get-iri mch)] - (str "<" iri ">") - (let [v (get-value mch) - dt (get-datatype-iri mch)] - (cond (get-optional mch) "UNDEF" - (datatype/JSON-LD-inferable-iris dt) (str (pr-str v)) - :else - (if-let [lang (get-lang mch)] - (str "\"" v "\"@" lang) - (if (= dt const/iri-json) - (str "\"" v "\"^^<" const/iri-rdf-json ">") - (str "\"" v "\"^^<" dt ">"))))))) - -(defn solution->values - "Format a solution as a SPARQL VALUES clause." - [solution] - (if (not-empty solution) - (let [vars (sort (keys solution))] - (str " VALUES " vars " { (" - (->> vars - (mapv (partial get solution)) - (mapv format-sparql-literal) - (reduce #(str %1 " " %2))) - ") }")) - "")) - (defn sparql-service-error! [ex service sparql-q] (log/error ex "Error processing service response " service sparql-q) From bb3437e9343b45da13a41e8dc9f03f9361cd670c Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Wed, 22 Oct 2025 13:17:31 -0500 Subject: [PATCH 08/12] remove the :ledger opt from named-aliases in a dataset --- src/fluree/db/query/api.cljc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fluree/db/query/api.cljc b/src/fluree/db/query/api.cljc index a63a566b25..36080f0062 100644 --- a/src/fluree/db/query/api.cljc +++ b/src/fluree/db/query/api.cljc @@ -245,7 +245,6 @@ (some-> opts :ledger util/sequential) (some-> sanitized-query :from util/sequential)) named-aliases (or (some-> opts :from-named util/sequential) - (some-> opts :ledger util/sequential) (some-> sanitized-query :from-named util/sequential))] (if (or (seq default-aliases) (seq named-aliases)) From 8a13b9fe42a104154f210c136d60e0218060fcb6 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Wed, 22 Oct 2025 14:21:00 -0500 Subject: [PATCH 09/12] add test for nonresponsive service endpoints We can't test responsive endpoints within this repo, so we'll just verify that errors are handled correctly. --- test/fluree/db/query/fql_test.clj | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/fluree/db/query/fql_test.clj b/test/fluree/db/query/fql_test.clj index 0f2719c366..7d62faf853 100644 --- a/test/fluree/db/query/fql_test.clj +++ b/test/fluree/db/query/fql_test.clj @@ -652,3 +652,33 @@ :where [{:id '?s :ex/foo '?foo}]}] (is (= "Error in value for \"select\"; Select must be a valid selector, a wildcard symbol (`*`), or a vector of selectors; Provided: [:* ?foo]; See documentation for details: https://next.developers.flur.ee/docs/reference/errorcodes#query-invalid-select" (ex-message @(fluree/query db query)))))))) + +(deftest service-test + (let [conn @(fluree/connect-memory) + db0 @(fluree/create conn "service-test") + db1 @(fluree/update db0 {"@context" {"ex" "http://example.com/"} + "insert" (mapv #(do {"@id" (str "ex:" %) + "@type" (if (odd? %) "ex:Odd" "ex:Even") + "ex:name" (str "name" %) + "ex:num" [(dec %) % (inc %)] + "ex:ref" {"@id" (str "ex:" (inc %))}}) + (range 10))})] + (testing "when service endpoint is nonresponsive" + (testing ":service returns an error" + (is (= "Error processing service response http://localhost:10000/sparql due to exception: xhttp error - http://localhost:10000/sparql - " + (ex-message @(fluree/query db1 {"@context" {"ex" "http://example.com/"} + "where" [{"@id" "?s" "@type" "ex:Even" "ex:ref" "?ref"} + [:service {:silent? false :service "http://localhost:10000/sparql" + :clause "{ ?rev ex:name ?name ; a ?type .}"}]] + "select" ["?s" "?type"]}))))) + (testing ":service silent returns a response without bindings from the remote service" + (is (= [["ex:0" nil] + ["ex:2" nil] + ["ex:4" nil] + ["ex:6" nil] + ["ex:8" nil]] + @(fluree/query db1 {"@context" {"ex" "http://example.com/"} + "where" [{"@id" "?s" "@type" "ex:Even" "ex:ref" "?ref"} + [:service {:silent? true :service "http://localhost:10000/sparql" + :clause "{ ?rev ex:name ?name ; a ?type .}"}]] + "select" ["?s" "?type"]}))))))) From 07c405e93c8f8ba7d9add020fbb9605d856eab77 Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Wed, 22 Oct 2025 14:26:44 -0500 Subject: [PATCH 10/12] use parsed context to generate SPARQL query prolog on SERVICE query --- src/fluree/db/query/fql/parse.cljc | 19 +++++++++---------- src/fluree/db/query/sparql.cljc | 26 +++++++++++++++++--------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/fluree/db/query/fql/parse.cljc b/src/fluree/db/query/fql/parse.cljc index fd68f61deb..5032857b45 100644 --- a/src/fluree/db/query/fql/parse.cljc +++ b/src/fluree/db/query/fql/parse.cljc @@ -37,9 +37,8 @@ If `:parse-object-vars` is false then we will not attempt to parse string literals in the object position as variables, only specifically tagged @variable objects will be parsed as variables." - [bound-vars opts orig-context] - {:orig-context orig-context - :bound-vars (set bound-vars) + [bound-vars opts] + {:bound-vars (set bound-vars) :parse-object-vars? (get opts :object-var-parsing true)}) (defn parse-var-name @@ -641,7 +640,7 @@ (when-let [construct (:construct q)] (-> construct syntax/coerce-where - (parse-where-clause (var-parsing-config nil (:opts q) nil) context) + (parse-where-clause (var-parsing-config nil (:opts q)) context) unwrap-tuple-patterns select/construct-selector))) @@ -808,7 +807,7 @@ context (cond->> (json-ld/parse-context orig-context) parent-context (merge parent-context)) [vars values] (parse-values (:values q) context) - var-config (var-parsing-config vars q orig-context) + var-config (var-parsing-config vars (:opts q)) where (parse-where (:where q) var-config context) construct (parse-construct q context) grouping (parse-grouping q) @@ -834,8 +833,8 @@ [(where/->pattern :query sub-query*)])) (defmethod parse-pattern :service - [[_ {:keys [clause] :as data}] {:keys [orig-context]} _context] - (let [sparql (str/join " " (into (sparql/context->prefixes orig-context) + [[_ {:keys [clause] :as data}] _var-config context] + (let [sparql (str/join " " (into (sparql/context->prefixes context) ["SELECT *" (str "WHERE " clause)]))] [(where/->pattern :service (assoc data :sparql-q sparql))])) @@ -991,7 +990,7 @@ (:context override-opts)) [vars values] (-> (get-named txn "values") (parse-values context)) - var-config (var-parsing-config vars override-opts nil) + var-config (var-parsing-config vars override-opts) where (-> (get-named txn "where") (parse-where var-config context)) var-config* (cond-> (update var-config :bound-vars into (where/clause-variables where)) @@ -1065,7 +1064,7 @@ context)) opts (-> (parse-txn-opts nil opts context) (assoc :object-var-parsing false)) - var-config (var-parsing-config nil opts nil) + var-config (var-parsing-config nil opts) parsed-txn (if turtle? (turtle/parse txn) (jld->parsed-triples txn var-config context)) @@ -1080,7 +1079,7 @@ [txn {:keys [format context] :as opts}] {:insert (if (= :turtle format) (turtle/parse txn) - (jld->parsed-triples txn (var-parsing-config nil (assoc opts :object-var-parsing false) nil) context)) + (jld->parsed-triples txn (var-parsing-config nil (assoc opts :object-var-parsing false)) context)) :context context :opts opts}) diff --git a/src/fluree/db/query/sparql.cljc b/src/fluree/db/query/sparql.cljc index 32d7dd09f0..8ca0cd207f 100644 --- a/src/fluree/db/query/sparql.cljc +++ b/src/fluree/db/query/sparql.cljc @@ -71,15 +71,23 @@ [opts] (= :sparql (:format opts))) +(defn extract-prefix + "A context key is a prefix if: + - it is a string with no colon + - it is a keyword with no namespace + Returns the string prefix if it is a prefix, falsey if not." + [k] + (or + (and (string? k) (not (str/includes? k ":")) k) + (and (keyword? k) (not (namespace k)) (name k)))) + (defn context->prefixes - [context] + [parsed-context] (reduce-kv (fn [prefixes k v] - (let [prefix (if (keyword? k) (name k) k)] - (if (string? v) - (if (= "@base" prefix) - (conj prefixes (str "BASE <" v ">")) - (conj prefixes (str "PREFIX " prefix ": <" v ">"))) - ;; skip non-prefix context definitions - prefixes))) + (if-let [prefix (extract-prefix k)] + (if (= "@base" prefix) + (conj prefixes (str "BASE <" (:id v) ">")) + (conj prefixes (str "PREFIX " prefix ": <" (:id v) ">"))) + prefixes)) [] - context)) + (dissoc parsed-context :type-key))) From f630bb658c50805100ab0c2b2012cf52da8e822f Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Wed, 22 Oct 2025 15:09:32 -0500 Subject: [PATCH 11/12] fix whitespace --- src/fluree/db/query/exec/where.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fluree/db/query/exec/where.cljc b/src/fluree/db/query/exec/where.cljc index 4954773a71..4a8b169ec8 100644 --- a/src/fluree/db/query/exec/where.cljc +++ b/src/fluree/db/query/exec/where.cljc @@ -953,6 +953,7 @@ (async/onto-chan! solution-ch))) (catch* e (async/>! error-ch (sparql-service-error! e service sparql-q))))))) solution-ch)) + (defmethod match-pattern :default [_db _tracker _solution pattern error-ch] (go From 4b28fe08789a30da6d3c6d95acb27890c838e12c Mon Sep 17 00:00:00 2001 From: Daniel Petranek Date: Thu, 23 Oct 2025 11:57:32 -0500 Subject: [PATCH 12/12] handle @vocab while translating @context to SPARQL --- src/fluree/db/query/sparql.cljc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fluree/db/query/sparql.cljc b/src/fluree/db/query/sparql.cljc index 8ca0cd207f..5dda2a4674 100644 --- a/src/fluree/db/query/sparql.cljc +++ b/src/fluree/db/query/sparql.cljc @@ -85,8 +85,10 @@ [parsed-context] (reduce-kv (fn [prefixes k v] (if-let [prefix (extract-prefix k)] - (if (= "@base" prefix) - (conj prefixes (str "BASE <" (:id v) ">")) + (case prefix + "base" (conj prefixes (str "BASE <" v ">")) + "vocab" prefixes ; not supported in SPARQL + ;; else (conj prefixes (str "PREFIX " prefix ": <" (:id v) ">"))) prefixes)) []