diff --git a/server/src/instant/db/cel_builder.clj b/server/src/instant/db/cel_builder.clj index af41247fef..cbd821bb0c 100644 --- a/server/src/instant/db/cel_builder.clj +++ b/server/src/instant/db/cel_builder.clj @@ -1,5 +1,5 @@ (ns instant.db.cel-builder - (:refer-clojure :exclude [get get-in and or = not=]) + (:refer-clojure :exclude [get get-in and or = not= < <= > >=]) (:import (com.google.protobuf NullValue) (dev.cel.common.ast CelConstant CelExpr CelExprFactory) (dev.cel.parser Operator))) @@ -104,9 +104,40 @@ cel-exprs)))) (defn get-in - "Build nested index: obj[k1][k2][k3]..." ^CelExpr [obj ks] (reduce (fn [^CelExpr acc k] (get acc k)) (->cel-expr obj *factory*) ks)) + +(defn < + ^CelExpr [a b] + (.newGlobalCall *factory* + (.getFunction Operator/LESS) + ^CelExpr/1 + (into-array CelExpr [(->cel-expr a *factory*) + (->cel-expr b *factory*)]))) + +(defn <= + ^CelExpr [a b] + (.newGlobalCall *factory* + (.getFunction Operator/LESS_EQUALS) + ^CelExpr/1 + (into-array CelExpr [(->cel-expr a *factory*) + (->cel-expr b *factory*)]))) + +(defn > + ^CelExpr [a b] + (.newGlobalCall *factory* + (.getFunction Operator/GREATER) + ^CelExpr/1 + (into-array CelExpr [(->cel-expr a *factory*) + (->cel-expr b *factory*)]))) + +(defn >= + ^CelExpr [a b] + (.newGlobalCall *factory* + (.getFunction Operator/GREATER_EQUALS) + ^CelExpr/1 + (into-array CelExpr [(->cel-expr a *factory*) + (->cel-expr b *factory*)]))) diff --git a/server/src/instant/db/instaql_topic.clj b/server/src/instant/db/instaql_topic.clj index d70ee37855..57f110d3c9 100644 --- a/server/src/instant/db/instaql_topic.clj +++ b/server/src/instant/db/instaql_topic.clj @@ -25,50 +25,54 @@ ;; --------- ;; form->ast! +(def ^:private comparison-ops #{:$gt :$gte :$lt :$lte}) + +(defn- comparison-op->cel-fn [op] + (case op + :$gt b/> + :$gte b/>= + :$lt b/< + :$lte b/<=)) + +(defn- resolve-cardinality-one-fwd-attr! + [etype label attrs] + (cond+ + :let [rev-attr (attr-model/seek-by-rev-ident-name [etype label] attrs)] + rev-attr (throw-not-supported! [:reverse-attribute]) + + :let [{:keys [id cardinality]} (attr-model/seek-by-fwd-ident-name [etype label] attrs)] + (not id) (throw-not-supported! [:unknown-attribute]) + (not= :one cardinality) (throw-not-supported! [:cardinality-many]) + + :else id)) + (defn- single-cond->cel-expr! [{:keys [etype attrs]} {:keys [cond-data]}] (let [{:keys [path v]} cond-data [v-type v-data] v] (cond - (> (count path) 1) + (clojure.core/> (count path) 1) (throw-not-supported! [:multi-part-path]) (and (= v-type :args-map) (contains? v-data :$isNull)) - (cond+ - :let [label (first path) - rev-attr (attr-model/seek-by-rev-ident-name [etype label] attrs)] - - rev-attr (throw-not-supported! [:reverse-attribute]) - - :let [{:keys [id cardinality] :as fwd-attr} (attr-model/seek-by-fwd-ident-name [etype label] attrs)] - - (not fwd-attr) (throw-not-supported! [:unknown-attribute]) - - (not= :one cardinality) (throw-not-supported! [:cardinality-many]) - - :else - (if (:$isNull v-data) - (b/= (b/get-in 'entity ["attrs" (str id)]) nil) - (b/not= (b/get-in 'entity ["attrs" (str id)]) nil))) + (let [id (resolve-cardinality-one-fwd-attr! etype (first path) attrs)] + (if (:$isNull v-data) + (b/= (b/get-in 'entity ["attrs" (str id)]) nil) + (b/not= (b/get-in 'entity ["attrs" (str id)]) nil))) + + (and (= v-type :args-map) (some comparison-ops (keys v-data))) + (let [id (resolve-cardinality-one-fwd-attr! etype (first path) attrs) + [op [_ cmp-val]] (first v-data)] + ((comparison-op->cel-fn op) + (b/get-in 'entity ["attrs" (str id)]) + cmp-val)) (not= v-type :value) (throw-not-supported! [:complex-value-type]) :else - (cond+ - :let [label (first path) - rev-attr (attr-model/seek-by-rev-ident-name [etype label] attrs)] - - rev-attr (throw-not-supported! [:reverse-attribute]) - - :let [{:keys [id cardinality] :as fwd-attr} (attr-model/seek-by-fwd-ident-name [etype label] attrs)] - - (not fwd-attr) (throw-not-supported! [:unknown-attribute]) - - (not= :one cardinality) (throw-not-supported! [:cardinality-many]) - - :else - (b/= (b/get-in 'entity ["attrs" (str id)]) v-data))))) + (let [id (resolve-cardinality-one-fwd-attr! etype (first path) attrs)] + (b/= (b/get-in 'entity ["attrs" (str id)]) v-data))))) (defn- where-cond->cel-expr! [ctx {:keys [where-cond]}] @@ -89,41 +93,23 @@ (let [{:keys [path v]} cond-data [v-type v-data] v] (cond - (> (count path) 1) + (clojure.core/> (count path) 1) (throw-not-supported! [:multi-part-path]) (and (= v-type :args-map) (contains? v-data :$isNull)) - (cond+ - :let [label (first path) - rev-attr (attr-model/seek-by-rev-ident-name [etype label] attrs)] - - rev-attr (throw-not-supported! [:reverse-attribute]) - - :let [{:keys [cardinality] :as fwd-attr} (attr-model/seek-by-fwd-ident-name [etype label] attrs)] - - (not fwd-attr) (throw-not-supported! [:unknown-attribute]) + (do (resolve-cardinality-one-fwd-attr! etype (first path) attrs) + nil) - (not= :one cardinality) (throw-not-supported! [:cardinality-many]) - - :else nil) + (and (= v-type :args-map) (some comparison-ops (keys v-data))) + (do (resolve-cardinality-one-fwd-attr! etype (first path) attrs) + nil) (not= v-type :value) (throw-not-supported! [:complex-value-type]) :else - (cond+ - :let [label (first path) - rev-attr (attr-model/seek-by-rev-ident-name [etype label] attrs)] - - rev-attr (throw-not-supported! [:reverse-attribute]) - - :let [{:keys [cardinality] :as fwd-attr} (attr-model/seek-by-fwd-ident-name [etype label] attrs)] - - (not fwd-attr) (throw-not-supported! [:unknown-attribute]) - - (not= :one cardinality) (throw-not-supported! [:cardinality-many]) - - :else nil)))) + (do (resolve-cardinality-one-fwd-attr! etype (first path) attrs) + nil)))) (defn- validate-child-form-where-cond! [ctx {:keys [where-cond]}] diff --git a/server/test/instant/db/instaql_topic_test.clj b/server/test/instant/db/instaql_topic_test.clj index 8050870165..4427f1b8cd 100644 --- a/server/test/instant/db/instaql_topic_test.clj +++ b/server/test/instant/db/instaql_topic_test.clj @@ -306,3 +306,63 @@ (is (false? (program {:etype "favoriteBook" :attrs {}}))) (is (false? (program {:etype "posts" :attrs {}}))))))) +(deftest comparison-operators + (with-zeneca-app + (fn [app _r] + (let [score-attr-id (random-uuid) + _ (tx/transact! + (aurora/conn-pool :write) + (attr-model/get-by-app-id (:id app)) + (:id app) + [[:add-attr {:id score-attr-id + :forward-identity [(random-uuid) "users" "score"] + :value-type :blob + :cardinality :one + :unique? false + :index? false}]]) + attrs (attr-model/get-by-app-id (:id app))] + + (testing "$gt" + (let [{:keys [program]} (iqt/instaql-topic + {:attrs attrs} + (iq/->forms! attrs {:users {:$ {:where {:score {:$gt 1000}}}}}))] + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 2000}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 1000}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 500}}))))) + + (testing "$gte" + (let [{:keys [program]} (iqt/instaql-topic + {:attrs attrs} + (iq/->forms! attrs {:users {:$ {:where {:score {:$gte 1000}}}}}))] + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 2000}}))) + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 1000}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 500}}))))) + + (testing "$lt" + (let [{:keys [program]} (iqt/instaql-topic + {:attrs attrs} + (iq/->forms! attrs {:users {:$ {:where {:score {:$lt 1000}}}}}))] + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 500}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 1000}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 2000}}))))) + + (testing "$lte" + (let [{:keys [program]} (iqt/instaql-topic + {:attrs attrs} + (iq/->forms! attrs {:users {:$ {:where {:score {:$lte 1000}}}}}))] + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 500}}))) + (is (true? (program {:etype "users" + :attrs {(str score-attr-id) 1000}}))) + (is (false? (program {:etype "users" + :attrs {(str score-attr-id) 2000}}))))))))) +