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
4 changes: 2 additions & 2 deletions doc/axiom.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ the `:where` triples are all equivalent.

## Variables

Examples: `?var`
Examples: `?var`, `$var`

Variables are written as symbols prefixed with a question mark `?`. Translating to SPARQL does not change the variable other than stringifying it.
Variables are written as symbols prefixed with a question mark `?` or dollar sign `$`. The sigil is not part of the variable name, so `?var` and `$var` identify the same variable. Translating to SPARQL preserves the sigil and otherwise only stringifies the symbol.

## Blank Nodes

Expand Down
2 changes: 1 addition & 1 deletion src/main/com/yetanalytics/flint/axiom/impl/format.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
(str kns ":" kname)))

(defn format-var-symbol
"Return the var `v-sym` as a string of the form `?var`."
"Return the var `v-sym` as a string of the form `?var` or `$var`."
[v-sym]
(str v-sym))

Expand Down
18 changes: 10 additions & 8 deletions src/main/com/yetanalytics/flint/axiom/impl/validation.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[c]
#?(:clj (int c) :cljs (.charCodeAt c)))

#?(:clj (def ^:private qmark-range [(char->int \?)]))
(def ^:private var-sigil-range (mapv char->int [\? \$]))
#?(:clj (def ^:private uscore-range [(char->int \_)]))
#?(:clj (def ^:private hyphen-range [(char->int \-)]))
#?(:clj (def ^:private bslash-range [(char->int \\)]))
Expand Down Expand Up @@ -188,9 +188,11 @@
(re-pattern (format "<%s*>" iri-banned))))

(def var-regex
(let [var-start (ranges->regex-charset var-start-range)
(let [var-sigil (ranges->regex-charset var-sigil-range)
var-start (ranges->regex-charset var-start-range)
var-body (ranges->regex-charset var-body-range)]
(re-pattern (format "\\?%s%s*"
(re-pattern (format "%s%s%s*"
var-sigil
var-start
var-body))))

Expand Down Expand Up @@ -259,8 +261,8 @@
`(.set ~r#)))
cp-ranges#)))))

#?(:clj (def ^{:private true :tag BitSet} qmark-bitset
(unicode-bitset qmark-range)))
#?(:clj (def ^{:private true :tag BitSet} var-sigil-bitset
(unicode-bitset var-sigil-range)))
#?(:clj (def ^{:private true :tag BitSet} uscore-bitset
(unicode-bitset uscore-range)))
#?(:clj (def ^{:private true :tag BitSet} hyphen-bitset
Expand Down Expand Up @@ -376,9 +378,9 @@
(loop [idx 0]
(cond
(>= idx ccnt)
(<= 2 ccnt) ; Need to have initial qmark + at least one start char
(<= 2 ccnt) ; Need to have a sigil + at least one start char
(= idx 0)
(recur-if (in-bitset? qmark-bitset vs idx)
(recur-if (in-bitset? var-sigil-bitset vs idx)
(inc idx))
(= idx 1)
(recur-if (in-bitset? var-start-bitset vs idx)
Expand Down Expand Up @@ -578,7 +580,7 @@
:cljs (boolean (re-matches bnode-regex bnode-str))))

(defn valid-var-symbol?
"Is `var-sym` a symbol that starts with `?`?"
"Is `var-sym` a symbol that starts with `?` or `$`?"
[var-sym]
(valid-var-str? (str var-sym)))

Expand Down
7 changes: 6 additions & 1 deletion src/main/com/yetanalytics/flint/axiom/protocol.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@
"Convert the prefixed IRI `this` into its string representation."))

(defprotocol Variable
"A SPARQL variable (e.g. `?var`)."
"A SPARQL variable (e.g. `?var` or `$var`)."
(-valid-variable? [this]
"Return `true` if `this` is a valid variable of its type.")
(-format-variable [this]
"Convert the variable `this` into its string representation."))

(defn variable-name
"Return the name of `variable` without its leading `?` or `$` sigil."
[variable]
(subs (-format-variable variable) 1))

(defprotocol BlankNode
"A SPARQL blank node (e.g. `_:b0`)."
(-valid-bnode? [this]
Expand Down
7 changes: 4 additions & 3 deletions src/main/com/yetanalytics/flint/error.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[com.yetanalytics.flint.validate.aggregate :as va]
[com.yetanalytics.flint.validate.bnode :as vb]
[com.yetanalytics.flint.validate.scope :as vs]
[com.yetanalytics.flint.validate.variable :as vv]
#?@(:clj [[clojure.core :refer [format]]]
:cljs [[goog.string :as gstring]
[goog.string.format]])))
Expand Down Expand Up @@ -157,8 +158,8 @@
(let [[nots ins] (split-with #(= ::vs/var-not-in-scope (:kind %))
scope-errs)
var-coll (if (not-empty nots)
(->> nots (mapcat :variables) distinct sort)
(->> ins (map :variable) distinct sort))
(->> nots (mapcat :variables) vv/distinct-vars sort)
(->> ins (map :variable) vv/distinct-vars sort))
var-count (->> var-coll count)
var-strs (->> var-coll (map str))
var-str (join-str-coll var-strs)]
Expand Down Expand Up @@ -195,7 +196,7 @@
(plural-s wild-count)
index-str
(plural-has wild-count)))
(let [var-coll (->> errs (mapcat :variables) distinct sort)
(let [var-coll (->> errs (mapcat :variables) vv/distinct-vars sort)
var-count (->> var-coll count)
var-strs (->> var-coll (map str))
var-str (join-str-coll var-strs)]
Expand Down
19 changes: 11 additions & 8 deletions src/main/com/yetanalytics/flint/spec/select.cljc
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
(ns com.yetanalytics.flint.spec.select
(:require [clojure.spec.alpha :as s]
[com.yetanalytics.flint.spec.axiom :as ax]
[com.yetanalytics.flint.spec.expr :as es]))
[com.yetanalytics.flint.axiom.protocol :as p]
[com.yetanalytics.flint.spec.axiom :as ax]
[com.yetanalytics.flint.spec.expr :as es]))

(defn- no-duplicate-vars?
[var-or-exprs]
(boolean (reduce (fn [seen [k x]]
(case k
:ax/var
(if (contains? seen x)
(reduced false)
(conj seen x))
(let [vname (p/variable-name x)]
(if (contains? seen vname)
(reduced false)
(conj seen vname)))
:select/expr-as-var
(let [v (-> x second second second)]
(if (contains? seen v)
(let [v (-> x second second second)
vname (p/variable-name v)]
(if (contains? seen vname)
(reduced false)
(conj seen v)))))
(conj seen vname)))))
#{}
var-or-exprs)))

Expand Down
14 changes: 8 additions & 6 deletions src/main/com/yetanalytics/flint/validate/aggregate.cljc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
(ns com.yetanalytics.flint.validate.aggregate
(:require [com.yetanalytics.flint.validate.variable :as vv]
[com.yetanalytics.flint.util :as u]
[com.yetanalytics.flint.validate.util :as vu]))
(:require [com.yetanalytics.flint.axiom.protocol :as p]
[com.yetanalytics.flint.validate.variable :as vv]
[com.yetanalytics.flint.util :as u]
[com.yetanalytics.flint.validate.util :as vu]))

;; In a query level which uses aggregates, only expressions consisting of
;; aggregates and constants may be projected, with one exception.
Expand All @@ -25,7 +26,7 @@
(reduce (fn [[valid-vars bad-vars] [k x]]
(case k
:ax/var
(if-not (valid-vars x)
(if-not (valid-vars (p/variable-name x))
[valid-vars (conj bad-vars x)]
[valid-vars bad-vars])
:select/expr-as-var
Expand All @@ -38,10 +39,10 @@
[valid-vars (concat bad-vars bad-expr-vars)]
;; Somehow already-projected vars are now valid,
;; at least according to Apache Jena's query parser
[(conj valid-vars v) bad-vars]))))
[(conj valid-vars (p/variable-name v)) bad-vars]))))
[group-by-vars []]
sel-clause)]
(not-empty bad-vars)))
(some-> bad-vars vv/distinct-vars not-empty)))

(defn- validate-agg-select
[[[_select-k select] loc]]
Expand All @@ -51,6 +52,7 @@
(->> ?group-by
(map vv/group-by-projected-vars)
(filter some?)
(map p/variable-name)
set)
#{})
[sel-k sel-v] select-cls]
Expand Down
17 changes: 12 additions & 5 deletions src/main/com/yetanalytics/flint/validate/scope.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns com.yetanalytics.flint.validate.scope
(:require [clojure.zip :as zip]
[com.yetanalytics.flint.axiom.protocol :as p]
[com.yetanalytics.flint.validate.variable :as vv]
[com.yetanalytics.flint.validate.util :as vu]
[com.yetanalytics.flint.util :as u]))
Expand Down Expand Up @@ -29,8 +30,9 @@
prev-elems (-> loc
zip/up ; :where/special
zip/lefts)
scope (set (mapcat vv/get-scope-vars prev-elems))]
(when (contains? scope bind-var)
scope (set (mapcat vv/get-scope-vars prev-elems))
scope-names (set (map p/variable-name scope))]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a set for scope is extra work for no reason.

Could use:

(->> prev-elems
     (mapcat vv/get-scope-vars)
     (map p/variable-name)
     set)

Or via transducing:

(into #{} 
     (comp (mapcat vv/get-scope-vars)
           (map p/variable-name))
     prev-elems)

(yes, I saw this is merged, sorry)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you're totally right, I was rushing. I'll fix that up, thanks!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually looking at this again I think perhaps the intermediate set was mainly for the error output (I just kind of followed @kelvinqian00's lead there), but the set part of that doesn't actually have to happen until we make the error below

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#47

(when (contains? scope-names (p/variable-name bind-var))
(in-scope-err-map bind-var scope loc :where/bind))))

(defn- validate-select
Expand All @@ -49,10 +51,15 @@
where-vars (-> where second vv/get-scope-vars)
group-vars (some-> ?group-by vv/group-by-projected-vars)
prev-vars (mapcat vv/get-scope-vars prev-elems)
scope (set (concat where-vars group-vars prev-vars))]
(if-some [bad-expr-vars (not-empty (filter #(not (scope %)) expr-vars))]
scope (set (concat where-vars group-vars prev-vars))
scope-names (set (map p/variable-name scope))]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, scope is only used to construct the scope-names set, and need not be a set

(if-some [bad-expr-vars (->> expr-vars
(remove #(contains? scope-names
(p/variable-name %)))
vv/distinct-vars
not-empty)]
(not-in-scope-err-map bad-expr-vars scope loc :select/expr-as-var)
(when (contains? scope bind-var)
(when (contains? scope-names (p/variable-name bind-var))
(in-scope-err-map bind-var scope loc :select/expr-as-var)))))

(defn- validate-node-locs
Expand Down
20 changes: 17 additions & 3 deletions src/main/com/yetanalytics/flint/validate/variable.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
(ns com.yetanalytics.flint.validate.variable
(:require [com.yetanalytics.flint.util :as u]
[com.yetanalytics.flint.spec.expr :as es]))
(:require [com.yetanalytics.flint.axiom.protocol :as p]
[com.yetanalytics.flint.util :as u]
[com.yetanalytics.flint.spec.expr :as es]))

(defn distinct-vars
"Return the distinct variables in `vars`, comparing their SPARQL names while
retaining the first encountered representation of each variable."
[vars]
(second
(reduce (fn [[seen ret] v]
(let [vname (p/variable-name v)]
(if (contains? seen vname)
[seen ret]
[(conj seen vname) (conj ret v)])))
[#{} []]
vars)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Expression variables
Expand Down Expand Up @@ -45,7 +59,7 @@
(invalid-agg-expr-vars valid-vars x))

(defmethod invalid-agg-expr-vars :ax/var [valid-vars [_ v]]
(if-not (valid-vars v) [v] []))
(if-not (valid-vars (p/variable-name v)) [v] []))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GROUP BY projection variables
Expand Down
13 changes: 11 additions & 2 deletions src/test/com/yetanalytics/flint/axiom_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@
(is (= "foo:bar" (p/-format-prefix-iri :foo/bar))))
(testing "Variables"
(is (p/-valid-variable? '?foo))
(is (= "?foo" (p/-format-variable '?foo))))
(is (p/-valid-variable? '$foo))
(is (= "?foo" (p/-format-variable '?foo)))
(is (= "$foo" (p/-format-variable '$foo)))
(is (= "foo"
(p/variable-name '?foo)
(p/variable-name '$foo)))
(let [custom-var (reify p/Variable
(-valid-variable? [_] true)
(-format-variable [_] "$custom"))]
(is (= "custom" (p/variable-name custom-var)))))
(testing "Blank Nodes"
(is (p/-valid-bnode? '_bar))
(is (= "_:bar" (p/-format-bnode '_bar))))
Expand Down Expand Up @@ -303,7 +312,7 @@
{:prefixes {:xsd (java.net.URI. "http://www.w3.org/2001/XMLSchema#")
:foo (java.net.URI. "http://foo.org/")}
:select ['?x]
:where [['?x :foo/time (java.time.Instant/EPOCH)]]})
:where [['?x :foo/time java.time.Instant/EPOCH]]})
(flint/format-query
{:prefixes {:xsd (java.net.URI. "http://www.w3.org/2001/XMLSchema#")
:foo (java.net.URI. "http://foo.org/")}
Expand Down
7 changes: 7 additions & 0 deletions src/test/com/yetanalytics/flint/error_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@
v/collect-nodes
vs/validate-scoped-vars
err/scope-error-msg)))
(is (= "1 variable in 2 `expr AS var` clauses was already defined in scope: ?x!'"
(err/scope-error-msg
[{:kind ::vs/var-in-scope :variable '?x}
{:kind ::vs/var-in-scope :variable '$x}])))
(is (= "1 variable at index 0 in 1 `expr AS var` clause was already defined in scope: ?x!'"
(->> '[{:delete [[?x ?y ?z]]
:where [[?x ?y ?z]
Expand Down Expand Up @@ -227,6 +231,9 @@
v/collect-nodes
va/validate-agg-selects
err/aggregate-error-msg)))
(is (= "1 variable was illegally used in SELECTs with aggregates: ?y!"
(err/aggregate-error-msg
[{:kind ::va/invalid-aggregate-var :variables ['?y '$y]}])))
(is (= "2 variables at index 0 were illegally used in SELECTs with aggregates: ?y and ?z!"
(->> '[{:delete [[?x ?y ?z]]
:where {:select [?x ?y ?z]
Expand Down
2 changes: 2 additions & 0 deletions src/test/com/yetanalytics/flint/format/axiom_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
(f/format-ast-node {} [:ax/prefix-iri :bar])))
(is (= "?xyz"
(f/format-ast-node {} [:ax/var '?xyz])))
(is (= "$xyz"
(f/format-ast-node {} [:ax/var '$xyz])))
(is (= "_:b0"
(f/format-ast-node {} [:ax/bnode '_b0])))
(is (= "[]"
Expand Down
42 changes: 24 additions & 18 deletions src/test/com/yetanalytics/flint/spec/axiom_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@

(deftest string-validation-gentest
(testing "variables"
(let [var-prop (make-prop (fn [s] (symbol (str "?" s)))
v/var-regex
v/valid-var-symbol?)]
(is (:pass? (tc/quick-check 100 var-prop)))))
(doseq [sigil ["?" "$"]]
(let [var-prop (make-prop (fn [s] (symbol (str sigil s)))
v/var-regex
v/valid-var-symbol?)]
(is (:pass? (tc/quick-check 100 var-prop))))))
(testing "blank nodes"
(let [bnode-prop (make-prop (fn [s] (symbol (str "_" s)))
v/bnode-regex
Expand Down Expand Up @@ -112,20 +113,22 @@

(deftest string-validation-unit-test
(testing "variable strings"
(multilingual-test
(fn [x] (v/valid-var-symbol? (symbol (str "?" x)))))
(are [x] (not (v/valid-var-symbol? (symbol (str "?" x))))
"???"
"foo bar"
"foo.bar"
".foobar"
"foo'bar"
"foo#bar"
(str "foo" (char 0x037E) "bar")
(str \u0308)
"·t"
biang-biang-noodles)
(doseq [sigil ["?" "$"]]
(multilingual-test
(fn [x] (v/valid-var-symbol? (symbol (str sigil x)))))
(are [x] (not (v/valid-var-symbol? (symbol (str sigil x))))
"???"
"foo bar"
"foo.bar"
".foobar"
"foo'bar"
"foo#bar"
(str "foo" (char 0x037E) "bar")
(str \u0308)
"·t"
biang-biang-noodles))
(is (v/valid-var-symbol? '?1234567890))
(is (v/valid-var-symbol? '$1234567890))
(is (not (v/valid-var-symbol? 'foo)))
(is (not (v/valid-var-symbol? '?foo))))
(testing "blank node strings"
Expand Down Expand Up @@ -239,9 +242,12 @@
(is (not (s/valid? ax/prefix-iri-spec :*)))))
(testing "variables"
(is (s/valid? ax/variable-spec '?foo))
(is (s/valid? ax/variable-spec '$foo))
(is (not (s/valid? ax/variable-spec "?foo")))
(is (not (s/valid? ax/variable-spec "$foo")))
(is (not (s/valid? ax/variable-spec 'foo)))
(is (not (s/valid? ax/variable-spec `?foo))))
(is (not (s/valid? ax/variable-spec `?foo)))
(is (not (s/valid? ax/variable-spec `$foo))))
(testing "blank nodes"
(is (s/valid? ax/bnode-spec '_))
(is (s/valid? ax/bnode-spec '_foo))
Expand Down
Loading
Loading