diff --git a/CHANGELOG.md b/CHANGELOG.md index 86567202b..c913540d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v0.17.9 + +### Other Improvements + +* Implement CQL ToTime and rearrange ToDate and ToDateTime ([#747](https://github.com/samply/blaze/pull/747)) +* Improve CQL Error Message on Subtract ([#755](https://github.com/samply/blaze/pull/755)) + +The full changelog can be found [here](https://github.com/samply/blaze/milestone/47?closed=1). + ## v0.17.8 ### Other diff --git a/README.md b/README.md index 8a48ee0ae..89d428c5f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The goal of this project is to provide a FHIR® Store with an internal CQL Evalu Blaze passes all [Touchstone FHIR 4.0.1 Basic Tests][12] and almost all [CQL Tests][3]. Please refer to the [Conformance](docs/conformance.md) section and report any issues you encounter during evaluation. -Latest release: [v0.17.8][5] +Latest release: [v0.17.9][5] ## Quick Start @@ -72,7 +72,7 @@ Unless required by applicable law or agreed to in writing, software distributed [3]: [4]: -[5]: +[5]: [6]: [7]: [8]: diff --git a/docs/deployment/docker-deployment.md b/docs/deployment/docker-deployment.md index f8ea6894c..b68d04a1b 100644 --- a/docs/deployment/docker-deployment.md +++ b/docs/deployment/docker-deployment.md @@ -27,7 +27,7 @@ Blaze should log something like this: 2021-06-27T11:02:37.834Z ee086ef908c1 main INFO [blaze.core:64] - JVM version: 16.0.2 2021-06-27T11:02:37.834Z ee086ef908c1 main INFO [blaze.core:65] - Maximum available memory: 1738 MiB 2021-06-27T11:02:37.835Z ee086ef908c1 main INFO [blaze.core:66] - Number of available processors: 8 -2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.17.8 in 8.2 seconds +2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.17.9 in 8.2 seconds ``` In order to test connectivity, query the health endpoint: @@ -47,7 +47,7 @@ that should return: ```json { "name": "Blaze", - "version": "0.17.8" + "version": "0.17.9" } ``` diff --git a/docs/deployment/manual-deployment.md b/docs/deployment/manual-deployment.md index 299e7a41e..edc4ac528 100644 --- a/docs/deployment/manual-deployment.md +++ b/docs/deployment/manual-deployment.md @@ -2,12 +2,12 @@ The installation works under Windows, Linux and macOS. The only dependency is an installed OpenJDK 11. Blaze is tested with [AdoptOpenJDK][1]. -Blaze runs on the JVM and comes as single JAR file. Download the most recent version [here](https://github.com/samply/blaze/releases/tag/v0.17.8). Look for `blaze-0.17.8-standalone.jar`. +Blaze runs on the JVM and comes as single JAR file. Download the most recent version [here](https://github.com/samply/blaze/releases/tag/v0.17.9). Look for `blaze-0.17.9-standalone.jar`. After the download, you can start blaze with the following command (Linux, macOS): ```sh -java -jar blaze-0.17.8-standalone.jar -m blaze.core +java -jar blaze-0.17.9-standalone.jar -m blaze.core ``` Blaze will run with an in-memory, volatile database for testing and demo purposes. @@ -17,14 +17,14 @@ Blaze can be run with durable storage by setting the environment variables `STOR Under Linux/macOS: ```sh -STORAGE=standalone java -jar blaze-0.17.8-standalone.jar -m blaze.core +STORAGE=standalone java -jar blaze-0.17.9-standalone.jar -m blaze.core ``` Under Windows, you need to set the Environment variables in the PowerShell before starting Blaze: ```powershell $Env:STORAGE="standalone" -java -jar blaze-0.17.8-standalone.jar -m blaze.core +java -jar blaze-0.17.9-standalone.jar -m blaze.core ``` This will create three directories called `index`, `transaction` and `resource` inside the current working directory, one for each database part used. @@ -42,7 +42,7 @@ The output should look like this: 2021-06-27T11:02:37.834Z ee086ef908c1 main INFO [blaze.core:64] - JVM version: 16.0.2 2021-06-27T11:02:37.834Z ee086ef908c1 main INFO [blaze.core:65] - Maximum available memory: 1738 MiB 2021-06-27T11:02:37.835Z ee086ef908c1 main INFO [blaze.core:66] - Number of available processors: 8 -2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.17.8 in 8.2 seconds +2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.17.9 in 8.2 seconds ``` In order to test connectivity, query the health endpoint: @@ -62,7 +62,7 @@ that should return: ```json { "name": "Blaze", - "version": "0.17.8" + "version": "0.17.9" } ``` diff --git a/modules/anomaly/src/blaze/anomaly.clj b/modules/anomaly/src/blaze/anomaly.clj index ee4e84112..d05a559d6 100644 --- a/modules/anomaly/src/blaze/anomaly.clj +++ b/modules/anomaly/src/blaze/anomaly.clj @@ -110,8 +110,8 @@ (defmacro try-one - "Applies a try-catch arround `body` catching exceptions of `type`, returning - an anomaly with `category` and possible message of the exception." + "Applies a try-catch form arround `body` catching exceptions of `type`, + returning an anomaly with `category` and possible message of the exception." [type category & body] `(try ~@body @@ -125,7 +125,10 @@ `(try-one Throwable ~category ~@body)) -(defmacro try-anomaly [& body] +(defmacro try-anomaly + "Applies a try-catch form arround `body` catching all Throwables and returns + an anomaly created by the `anomaly` function." + [& body] `(try ~@body (catch Throwable e# diff --git a/modules/cql/src/blaze/elm/compiler/type_operators.clj b/modules/cql/src/blaze/elm/compiler/type_operators.clj index a68c76237..98110464f 100644 --- a/modules/cql/src/blaze/elm/compiler/type_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/type_operators.clj @@ -257,4 +257,14 @@ (p/to-string x)) -;; TODO 22.31. ToTime +;; 22.31. ToTime +(defrecord ToTimeOperatorExpression [operand] + core/Expression + (-eval [_ {:keys [now] :as context} resource scope] + (p/to-time (core/-eval operand context resource scope) now))) + + +(defmethod core/compile* :elm.compiler.type/to-time + [context {:keys [operand]}] + (when-let [operand (core/compile* context operand)] + (->ToTimeOperatorExpression operand))) diff --git a/modules/cql/src/blaze/elm/date_time.clj b/modules/cql/src/blaze/elm/date_time.clj index fb808c279..e8c2576a8 100644 --- a/modules/cql/src/blaze/elm/date_time.clj +++ b/modules/cql/src/blaze/elm/date_time.clj @@ -70,7 +70,11 @@ (- months (:months other)) (- millis (:millis other))) (throw (ex-info (str "Invalid RHS subtracting from Period. Expected Period but was `" (type other) "`.") - {:op :subtract :this this :other other}))))) + {:op :subtract :this this :other other})))) + + Object + (toString [_] + (format "Period[month = %d, millis = %d]" months millis))) (defn period [years months millis] @@ -527,13 +531,43 @@ ;; 16.20. Subtract +(defn- year-out-of-range-msg [result period year] + (format "Year %s out of range while subtracting the period %s from the year %s." + result period year)) + + +(defn year-out-of-range-ex-info [year period result] + (ex-info (year-out-of-range-msg result period year) + {:op :subtract :year year :period period})) + + +(defn- year-month-out-of-range-msg [result period year-month] + (format "Year-month %s out of range while subtracting the period %s from the year-month %s." + result period year-month)) + + +(defn year-month-out-of-range-ex-info [year-month period result] + (ex-info (year-month-out-of-range-msg result period year-month) + {:op :subtract :year-month year-month :period period})) + + +(defn- date-out-of-range-msg [result period date] + (format "Date %s out of range while subtracting the period %s from the date %s." + result period date)) + + +(defn date-out-of-range-ex-info [date period result] + (ex-info (date-out-of-range-msg result period date) + {:op :subtract :date date :period period})) + + (extend-protocol p/Subtract Year (subtract [this other] (if (instance? Period other) (let [result (time/minus this (time/years (quot (:months other) 12)))] (if (time/before? result min-year) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (year-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to Year. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -543,7 +577,7 @@ (if (instance? Period other) (let [result (time/minus this (time/years (quot (:months other) 12)))] (if (time/before? result date-time-min-year) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (year-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to Year. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -553,7 +587,7 @@ (if (instance? Period other) (let [result (time/minus this (time/months (:months other)))] (if (time/before? result min-year-month) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (year-month-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to YearMonth. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -563,7 +597,7 @@ (if (instance? Period other) (let [result (time/minus this (time/months (:months other)))] (if (time/before? result date-time-min-year-month) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (year-month-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to YearMonth. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -576,7 +610,7 @@ (time/months (:months other)) (time/days (quot (:millis other) 86400000)))] (if (time/before? result min-date) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (date-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to LocalDate. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -589,7 +623,7 @@ (time/months (:months other)) (time/days (quot (:millis other) 86400000)))] (if (time/before? result date-time-min-date) - (throw (ex-info "Out of range." {:op :subtract :this this :other other})) + (throw (date-out-of-range-ex-info this other result)) result)) (throw (ex-info (str "Invalid RHS adding to LocalDate. Expected Period but was `" (type other) "`.") {:op :subtract :this this :other other})))) @@ -1233,14 +1267,6 @@ ;; 22.22. ToDate (extend-protocol p/ToDate - nil - (to-date [_ _]) - - String - (to-date [s _] - (-> (system/parse-date s) - (ba/exceptionally (constantly nil)))) - Year (to-date [this _] this) @@ -1277,9 +1303,6 @@ ;; 22.23. ToDateTime (extend-protocol p/ToDateTime - nil - (to-date-time [_ _]) - Instant (to-date-time [this now] (-> (.atOffset this (.getOffset ^OffsetDateTime now)) @@ -1320,11 +1343,7 @@ OffsetDateTime (to-date-time [this now] (-> (.withOffsetSameInstant this (.getOffset ^OffsetDateTime now)) - (.toLocalDateTime))) - - String - (to-date-time [s now] - (p/to-date-time (system/parse-date-time s) now))) + (.toLocalDateTime)))) ;; 22.30. ToString @@ -1360,3 +1379,23 @@ LocalDateTime (to-string [x] (str x))) + + +;; 22.31. ToTime +(extend-protocol p/ToTime + LocalTime + (to-time [this _] + this) + + LocalDateTime + (to-time [this _] + (.toLocalTime this)) + + OffsetDateTime + (to-time [this now] + (-> (.withOffsetSameInstant this (.getOffset ^OffsetDateTime now)) + (.toLocalTime))) + + PrecisionLocalTime + (to-time [this _] + (.-local_time this))) diff --git a/modules/cql/src/blaze/elm/deps_infer.clj b/modules/cql/src/blaze/elm/deps_infer.clj index af98a7edc..bd6e1aa1a 100644 --- a/modules/cql/src/blaze/elm/deps_infer.clj +++ b/modules/cql/src/blaze/elm/deps_infer.clj @@ -620,6 +620,9 @@ (derive :elm.deps.type/to-quantity :elm.deps.type/unary-expression) +;; 22.31. ToTime +(derive :elm.deps.type/to-time :elm.deps.type/unary-expression) + ;; 23. Clinical Operators diff --git a/modules/cql/src/blaze/elm/nil.clj b/modules/cql/src/blaze/elm/nil.clj index d5c914e39..82e17c558 100644 --- a/modules/cql/src/blaze/elm/nil.clj +++ b/modules/cql/src/blaze/elm/nil.clj @@ -284,6 +284,18 @@ (to-boolean [_])) +;; 22.22. ToDate +(extend-protocol p/ToDate + nil + (to-date [_ _])) + + +;; 22.23. ToDateTime +(extend-protocol p/ToDateTime + nil + (to-date-time [_ _])) + + ;; 22.24. ToDecimal (extend-protocol p/ToDecimal nil @@ -312,3 +324,9 @@ (extend-protocol p/ToString nil (to-string [_])) + + +;; 22.31. ToTime +(extend-protocol p/ToTime + nil + (to-time [_ _])) diff --git a/modules/cql/src/blaze/elm/protocols.clj b/modules/cql/src/blaze/elm/protocols.clj index beadceef6..142909bd7 100644 --- a/modules/cql/src/blaze/elm/protocols.clj +++ b/modules/cql/src/blaze/elm/protocols.clj @@ -308,3 +308,9 @@ ;; 22.30. ToString (defprotocol ToString (to-string [x])) + + +;; 22.31. ToTime +(defprotocol ToTime + "Converts an object into something usable as Time relative to `now`." + (to-time [x now])) diff --git a/modules/cql/src/blaze/elm/string.clj b/modules/cql/src/blaze/elm/string.clj index 84f3ea707..9f071dbd3 100644 --- a/modules/cql/src/blaze/elm/string.clj +++ b/modules/cql/src/blaze/elm/string.clj @@ -1,7 +1,9 @@ (ns blaze.elm.string "Implementation of the string type." (:require + [blaze.anomaly :as ba] [blaze.elm.protocols :as p] + [blaze.fhir.spec.type.system :as system] [clojure.string :as str])) @@ -45,6 +47,21 @@ nil))) +;; 22.22. ToDate +(extend-protocol p/ToDate + String + (to-date [s _] + (-> (system/parse-date s) + (ba/exceptionally (constantly nil))))) + + +;; 22.23. ToDateTime +(extend-protocol p/ToDateTime + String + (to-date-time [s now] + (p/to-date-time (system/parse-date-time s) now))) + + ;; 22.24. ToDecimal (extend-protocol p/ToDecimal String @@ -53,8 +70,17 @@ (p/to-decimal (BigDecimal. s)) (catch Exception _)))) + ;; 22.30. ToString (extend-protocol p/ToString String (to-string [s] (str s))) + + +;; 22.31. ToTime +(extend-protocol p/ToTime + String + (to-time [s _] + (-> (system/parse-time s) + (ba/exceptionally (constantly nil))))) diff --git a/modules/cql/test/blaze/cql_test.clj b/modules/cql/test/blaze/cql_test.clj index e36e95197..16c228611 100644 --- a/modules/cql/test/blaze/cql_test.clj +++ b/modules/cql/test/blaze/cql_test.clj @@ -155,10 +155,11 @@ "Decimal18D55ToString" ; TODO: implement "Quantity5D5CMToString" ; TODO: implement "BooleanTrueToString" ; TODO: implement - "ToTime1" ; TODO: implement - "ToTime2" ; TODO: implement - "ToTime3" ; TODO: implement - "ToTime4" ; TODO: implement + "ToTime1" ; shouldn't start with T + "ToTime2" ; time zone? + "ToTime3" ; time zone? + "ToTime4" ; time zone? + "ToTimeMalformed" ; should return null "StringToDateTimeMalformed" ; should return null "ToDateTimeMalformed" ; should return null }) diff --git a/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj b/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj index 63f0bb4a5..77e2c3853 100644 --- a/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj @@ -1080,7 +1080,32 @@ #elm/date "2019-01-01" #elm/quantity [1 "year"] (system/date 2018 1 1) #elm/date "2012-02-29" #elm/quantity [1 "year"] (system/date 2011 2 28) #elm/date "2019-01-01" #elm/quantity [1 "month"] (system/date 2018 12 1) - #elm/date "2019-01-01" #elm/quantity [1 "day"] (system/date 2018 12 31))) + #elm/date "2019-01-01" #elm/quantity [1 "day"] (system/date 2018 12 31)) + + (testing "out of range" + (testing "year" + (given (ba/try-anomaly (c/compile {} (elm/subtract [#elm/date "2022" #elm/quantity [2022 "year"]]))) + ::anom/category := ::anom/fault + ::anom/message := "Year 0 out of range while subtracting the period Period[month = 24264, millis = 0] from the year 2022." + :op := :subtract + :year := (system/date 2022) + :period (date-time/period 2022 0 0))) + + (testing "year-month" + (given (ba/try-anomaly (c/compile {} (elm/subtract [#elm/date "2022-07" #elm/quantity [2022 "year"]]))) + ::anom/category := ::anom/fault + ::anom/message := "Year-month 0000-07 out of range while subtracting the period Period[month = 24264, millis = 0] from the year-month 2022-07." + :op := :subtract + :year-month := (system/date 2022 7) + :period (date-time/period 2022 0 0))) + + (testing "date" + (given (ba/try-anomaly (c/compile {} (elm/subtract [#elm/date "2022-07-01" #elm/quantity [2022 "year"]]))) + ::anom/category := ::anom/fault + ::anom/message := "Date 0000-07-01 out of range while subtracting the period Period[month = 24264, millis = 0] from the date 2022-07-01." + :op := :subtract + :date := (system/date 2022 7 1) + :period (date-time/period 2022 0 0))))) ;; TODO: find a solution to avoid overflow #_(testing "Subtracting a positive amount of years from a year makes it smaller" @@ -1150,7 +1175,15 @@ #elm/date-time "2019-01-01T00" #elm/quantity [1 "day"] (system/date-time 2018 12 31 0 0 0) #elm/date-time "2019-01-01T00" #elm/quantity [1 "hour"] (system/date-time 2018 12 31 23 0 0) #elm/date-time "2019-01-01T00" #elm/quantity [1 "minute"] (system/date-time 2018 12 31 23 59 0) - #elm/date-time "2019-01-01T00" #elm/quantity [1 "second"] (system/date-time 2018 12 31 23 59 59))) + #elm/date-time "2019-01-01T00" #elm/quantity [1 "second"] (system/date-time 2018 12 31 23 59 59)) + + (testing "out of range" + (given (ba/try-anomaly (c/compile {} (elm/subtract [#elm/date-time "2022" #elm/quantity [2022 "year"]]))) + ::anom/category := ::anom/fault + ::anom/message := "Year 0 out of range while subtracting the period Period[month = 24264, millis = 0] from the year 2022." + :op := :subtract + :year := (system/date-time 2022) + :period (date-time/period 2022 0 0)))) (testing "Time - Quantity" (are [x y res] (= res (c/compile {} (elm/subtract [x y]))) diff --git a/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj b/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj index 2908208f5..2d19179e7 100644 --- a/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj @@ -212,7 +212,7 @@ "12:30:15" "12:30:16" false "12:30:16" "12:30:15" false - "12:30.00" "12:30" nil + "12:30:00" "12:30" nil "12:00" "12" nil) diff --git a/modules/cql/test/blaze/elm/compiler/date_time_operators_test.clj b/modules/cql/test/blaze/elm/compiler/date_time_operators_test.clj index 7171bc51e..e4c0a03e6 100644 --- a/modules/cql/test/blaze/elm/compiler/date_time_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/date_time_operators_test.clj @@ -849,11 +849,12 @@ ;; ;; At least one component other than timezoneOffset must be specified, and no ;; component may be specified at a precision below an unspecified precision. -;; For example, minute may be null, but if it is, second, and millisecond -;; must all be null as well. +;; For example, minute may be null, but if it is, second, and millisecond must +;; all be null as well. ;; -;; If timezoneOffset is not specified, it is defaulted to the timezone offset -;; of the evaluation request. +;; Although the milliseconds are specified with a separate component, seconds +;; and milliseconds are combined and represented as a Decimal for the purposes +;; of comparison. (deftest compile-time-test (testing "Static hour" (are [elm res] (= res (c/compile {} elm)) diff --git a/modules/cql/test/blaze/elm/compiler/test_util.clj b/modules/cql/test/blaze/elm/compiler/test_util.clj index 1133486c8..008d6047c 100644 --- a/modules/cql/test/blaze/elm/compiler/test_util.clj +++ b/modules/cql/test/blaze/elm/compiler/test_util.clj @@ -5,6 +5,7 @@ [blaze.elm.literal :as elm] [blaze.elm.literal-spec] [blaze.elm.spec] + [blaze.fhir.spec.type.system :as system] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] [clojure.test :refer [is testing]]) @@ -60,13 +61,18 @@ {:name "ab"} {:name "b"} {:name "ba"} - {:name "A"}]}}}) + {:name "A"} + {:name "12:54:00"} + {:name "2020-01-02T03:04:05.006Z"}]}}}) (def dynamic-eval-ctx {:parameters {"true" true "false" false "nil" nil "1" 1 "2" 2 "3" 3 "4" 4 - "empty-string" "" "a" "a" "ab" "ab" "b" "b" "ba" "ba" "A" "A"}}) + "empty-string" "" "a" "a" "ab" "ab" "b" "b" "ba" "ba" "A" "A" + "12:54:00" (system/time 12 54 00) + "2020-01-02T03:04:05.006Z" (system/date-time 2020 1 2 3 4 5 6 ZoneOffset/UTC)} + :now now}) (defn dynamic-compile-eval [elm] diff --git a/modules/cql/test/blaze/elm/compiler/type_operators_test.clj b/modules/cql/test/blaze/elm/compiler/type_operators_test.clj index eec3eebd4..9474397d8 100644 --- a/modules/cql/test/blaze/elm/compiler/type_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/type_operators_test.clj @@ -1441,7 +1441,7 @@ (is (= '(to-string (param-ref "x")) (core/-form expr)))))) -;; TODO 22.31. ToTime +;; 22.31. ToTime ;; ;; The ToTime operator converts the value of its argument to a Time value. ;; @@ -1464,4 +1464,34 @@ ;; For DateTime values, the result is the same as extracting the Time component ;; from the DateTime value. ;; -;; If the argument is null, the result is null. \ No newline at end of file +;; If the argument is null, the result is null. +(deftest compile-to-time-test + (let [eval #(core/-eval % {:now tu/now} nil nil)] + (testing "String" + (are [x res] (= res (eval (tu/compile-unop elm/to-time elm/string x))) + "12:54:30" (system/time 12 54 30) + "12:54:30.010" (system/time 12 54 30 10) + + "aaaa" nil + "12:54" nil + "24:54:00" nil + "23:60:00" nil + "14-30-00.0" nil)) + + (testing "Time" + (are [x res] (= res (eval (tu/compile-unop elm/to-time elm/time x))) + "12:54" (system/time 12 54) + "12:54:00" (system/time 12 54 00) + "12:54:30.010" (system/time 12 54 30 10))) + + (testing "DateTime" + (are [x res] (= res (eval (tu/compile-unop elm/to-time elm/date-time x))) + "2020-03-08T12:54:00" (system/time 12 54 00) + "2020-03-08T12:54:30.010" (system/time 12 54 30 10))) + + (testing "dynamic" + (are [x res] (= res (tu/dynamic-compile-eval (elm/to-time x))) + #elm/parameter-ref "12:54:00" (system/time 12 54 00) + #elm/parameter-ref "2020-01-02T03:04:05.006Z" (system/time 3 4 5 6)))) + + (tu/testing-unary-null elm/to-time)) diff --git a/modules/cql/test/blaze/elm/literal.clj b/modules/cql/test/blaze/elm/literal.clj index fd4a48676..d0efde792 100644 --- a/modules/cql/test/blaze/elm/literal.clj +++ b/modules/cql/test/blaze/elm/literal.clj @@ -8,6 +8,9 @@ [clojure.string :as str])) +(set! *warn-on-reflection* true) + + ;; 1. Simple Values ;; 1.1. Literal @@ -461,9 +464,12 @@ timezone-offset (assoc :timezoneOffset timezone-offset))))) +;; 18.18. Time (defn time [arg] (if (string? arg) - (time (map integer (str/split arg #"[:.]"))) + (time (map integer (str/split (if (.contains ^String arg ".") + (subs (str arg "000") 0 12) + arg) #"[:.]"))) (let [[hour minute second millisecond] arg] (cond-> {:type "Time" @@ -882,7 +888,9 @@ (defn to-string [operand] {:type "ToString" :operand operand}) - +;; 22.31. ToTime +(defn to-time [operand] + {:type "ToTime" :operand operand}) ;; 23. Clinical Operators diff --git a/modules/cql/test/blaze/elm/literal_spec.clj b/modules/cql/test/blaze/elm/literal_spec.clj index c7c294143..cc144bfa7 100644 --- a/modules/cql/test/blaze/elm/literal_spec.clj +++ b/modules/cql/test/blaze/elm/literal_spec.clj @@ -493,6 +493,11 @@ :ret :elm/expression) +;; 22.31. ToTime +(s/fdef elm/to-time + :args (s/cat :operand :elm/expression) + :ret :elm/expression) + ;; 23. Clinical Operators diff --git a/modules/fhir-structure/src/blaze/fhir/spec/type/system.clj b/modules/fhir-structure/src/blaze/fhir/spec/type/system.clj index 082e54f4e..64b82bde3 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/type/system.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/type/system.clj @@ -9,7 +9,7 @@ * DateTime * Time * Quantity" - (:refer-clojure :exclude [boolean? decimal? integer? string? type]) + (:refer-clojure :exclude [boolean? decimal? integer? string? time type]) (:require [blaze.anomaly :as ba] [cognitect.anomalies :as anom] @@ -538,6 +538,34 @@ (some->> x (.equals time)))) +(defn time + "Returns a System.Time" + ([hour minute] + (LocalTime/of (int hour) (int minute))) + ([hour minute second] + (LocalTime/of (int hour) (int minute) (int second))) + ([hour minute second millis] + (LocalTime/of (int hour) (int minute) (int second) + (unchecked-multiply-int (int millis) 1000000)))) + + +(defn parse-time* [s] + (LocalTime/parse s)) + + +(defn- time-string? [s] + (.matches (re-matcher #"([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?" s))) + + +(defn parse-time + "Parses `s` into a System.Time. + + Returns an anomaly if `s` isn't a valid System.Time." + [s] + (if (time-string? s) + (ba/try-one DateTimeParseException ::anom/incorrect (parse-time* s)) + (ba/incorrect (format "Invalid date-time value `%s`." s)))) + ;; ---- Other ----------------------------------------------------------------- diff --git a/modules/fhir-structure/test/blaze/fhir/spec/type/system_test.clj b/modules/fhir-structure/test/blaze/fhir/spec/type/system_test.clj index cfe7ee8c4..0a38ea876 100644 --- a/modules/fhir-structure/test/blaze/fhir/spec/type/system_test.clj +++ b/modules/fhir-structure/test/blaze/fhir/spec/type/system_test.clj @@ -513,6 +513,9 @@ (testing "type" (is (= :system/time (system/type (LocalTime/of 0 0 0))))) + (testing "time" + (is (= (system/time 3 4) (LocalTime/of 3 4)))) + (testing "system equals" (are [a b res] (= res (system/equals a b)) (LocalTime/of 0 0 0) (LocalTime/of 0 0 0) true @@ -527,3 +530,20 @@ (LocalTime/of 0 0 0) (Object.) false (Object.) (LocalTime/of 0 0 0) false))) + + +(deftest parse-time-test + (testing "valid" + (are [s d] (= d (system/parse-time s)) + "03:04:05" (system/time 3 4 5) + "03:04:05.1" (system/time 3 4 5 100) + "03:04:05.01" (system/time 3 4 5 10) + "03:04:05.006" (system/time 3 4 5 6))) + + (testing "invalid" + (are [s] (= ::anom/incorrect (::anom/category (system/parse-time s))) + "a" + "" + "25:00:00" + "12:60:00" + "12:12:60"))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj index 3f12a2d19..8fc265d67 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj @@ -31,18 +31,27 @@ 512) +(defn- evaluate-expression-1-error-msg [expression-name e] + (format "Error while evaluating the expression `%s`: %s" expression-name + (ex-message e))) + + (defn- evaluate-expression-1 [{:keys [library-context] :as context} subject-handle expression-name] (try (expr/eval context (get library-context expression-name) subject-handle) (catch Exception e - (log/error (format "Error while evaluating the expression `%s`:" - expression-name) (ex-message (ex-cause e))) - (log/error e) - (ba/fault - (ex-message e) - :fhir/issue "exception" - :expression-name expression-name)))) + (let [ex-data (ex-data e)] + ;; only log if the exception hasn't ex-data because exception with + ;; ex-data are controlled by us and so are not unexpected + (when-not ex-data + (log/error (evaluate-expression-1-error-msg expression-name e)) + (log/error e)) + (-> (ba/fault + (evaluate-expression-1-error-msg expression-name e) + :fhir/issue "exception" + :expression-name expression-name) + (merge ex-data)))))) (defn- close-batch-db! [{:keys [db]}] @@ -129,11 +138,12 @@ (defn- unwrap-library-context {:arglists '([context])} - [{{:keys [compiled-expression-defs parameter-default-values]} :library + [{:keys [parameters] + {:keys [compiled-expression-defs parameter-default-values]} :library :as context}] (assoc context :library-context compiled-expression-defs - :parameters parameter-default-values)) + :parameters (merge parameter-default-values parameters))) (defn evaluate-expression diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj index 0abb040d5..a6471fc17 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj @@ -22,13 +22,17 @@ :fhir.resource/type) +(s/def ::parameters + (s/map-of string? any?)) + + (s/def ::context (s/keys :req-un [:blaze.db/db ::now ::library ::subject-type :blaze.fhir.operation.evaluate-measure/report-type])) (s/def ::individual-context - (s/keys :req-un [:blaze.db/db ::now ::library])) + (s/keys :req-un [:blaze.db/db ::now ::library] :opt-un [::parameters])) (s/fdef cql/evaluate-expression diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj index b5257f971..edd8b1ed6 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj @@ -6,9 +6,11 @@ [blaze.db.api :as d] [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.elm.compiler.library :as library] + [blaze.elm.date-time :as date-time] [blaze.elm.expression :as expr] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.cql-spec] + [blaze.fhir.spec.type.system :as system] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] @@ -37,7 +39,7 @@ (OffsetDateTime/now ^Clock clock)) -(def cql +(def cql-gender "library Retrieve using FHIR version '4.0.0' include FHIRHelpers version '4.0.0' @@ -48,7 +50,20 @@ Patient.gender = 'male'") -(defn- compile-library [node] +(def cql-error + "library Retrieve + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + parameter Year2022 Date + + context Patient + + define InInitialPopulation: + Year2022 - 2022 'years'") + + +(defn- compile-library [node cql] (when-ok [library (cql-translator/translate cql)] (library/compile-library node library {}))) @@ -65,7 +80,7 @@ [:put {:fhir/type :fhir/Patient :id "2" :gender #fhir/code"female"}]]] (let [context {:db (d/db node) :now (now clock) - :library (compile-library node) + :library (compile-library node cql-gender) :subject-type "Patient" :report-type "population"}] (is (= 1 (cql/evaluate-expression context "InInitialPopulation"))))) @@ -76,13 +91,13 @@ [[[:put {:fhir/type :fhir/Patient :id "0"}]]] (let [context {:db (d/db node) :now (now clock) - :library (compile-library node) + :library (compile-library node cql-gender) :subject-type "Patient" :report-type "population"}] (with-redefs [expr/eval (failing-eval "msg-222453")] (given (cql/evaluate-expression context "InInitialPopulation") ::anom/category := ::anom/fault - ::anom/message := "msg-222453")))))) + ::anom/message := "Error while evaluating the expression `InInitialPopulation`: msg-222453")))))) (deftest evaluate-individual-expression-test @@ -94,7 +109,7 @@ patient (d/resource-handle db "Patient" "0") context {:db db :now (now clock) - :library (compile-library node)}] + :library (compile-library node cql-gender)}] (is (true? (cql/evaluate-individual-expression context patient "InInitialPopulation")))))) (testing "no match" @@ -105,8 +120,27 @@ patient (d/resource-handle db "Patient" "0") context {:db db :now (now clock) - :library (compile-library node)}] - (is (false? (cql/evaluate-individual-expression context patient "InInitialPopulation"))))))) + :library (compile-library node cql-gender)}] + (is (false? (cql/evaluate-individual-expression context patient "InInitialPopulation")))))) + + (testing "error" + (with-system-data [{:blaze.db/keys [node] :blaze.test/keys [clock]} + mem-node-system] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + (let [db (d/db node) + patient (d/resource-handle db "Patient" "0") + context {:db db + :now (now clock) + :library (compile-library node cql-error) + :parameters {"Year2022" (system/date 2022)}}] + (given (cql/evaluate-individual-expression context patient "InInitialPopulation") + ::anom/category := ::anom/fault + ::anom/message := "Error while evaluating the expression `InInitialPopulation`: Year 0 out of range while subtracting the period Period[month = 24264, millis = 0] from the year 2022." + :fhir/issue := "exception" + :expression-name := "InInitialPopulation" + :op := :subtract + :year := (system/date 2022) + :period := (date-time/period 2022 0 0)))))) (def two-value-eval @@ -120,13 +154,13 @@ [[[:put {:fhir/type :fhir/Patient :id "0"}]]] (let [context {:db (d/db node) :now (now clock) - :library (compile-library node) + :library (compile-library node cql-gender) :subject-type "Patient" :report-type "population"}] (with-redefs [expr/eval (failing-eval "msg-221825")] (given (cql/calc-strata context "" "") ::anom/category := ::anom/fault - ::anom/message := "msg-221825"))))) + ::anom/message := "Error while evaluating the expression ``: msg-221825"))))) (testing "multiple values" (with-system-data [{:blaze.db/keys [node] :blaze.test/keys [clock]} @@ -134,7 +168,7 @@ [[[:put {:fhir/type :fhir/Patient :id "0"}]]] (let [context {:db (d/db node) :now (now clock) - :library (compile-library node) + :library (compile-library node cql-gender) :subject-type "Patient" :report-type "population"}] (with-redefs [expr/eval two-value-eval] @@ -157,4 +191,4 @@ (with-redefs [expr/eval (failing-eval "msg-221154")] (given (cql/calc-individual-strata nil nil nil nil) ::anom/category := ::anom/fault - ::anom/message := "msg-221154")))) + ::anom/message := "Error while evaluating the expression `null`: msg-221154")))) diff --git a/perf-test/gatling/pom.xml b/perf-test/gatling/pom.xml index 6464d4f17..766e2c7f0 100644 --- a/perf-test/gatling/pom.xml +++ b/perf-test/gatling/pom.xml @@ -5,7 +5,7 @@ samply.blaze gatling - 0.17.8 + 0.17.9 1.8 diff --git a/pom.xml b/pom.xml index 633f6e89e..b61e43b73 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 samply blaze - 0.17.8 + 0.17.9 blaze A FHIR Store with internal, fast CQL Evaluation Engine diff --git a/src/blaze/system.clj b/src/blaze/system.clj index bac30e8c3..18f72cf9a 100644 --- a/src/blaze/system.clj +++ b/src/blaze/system.clj @@ -85,7 +85,7 @@ (def ^:private root-config - {:blaze/version "0.17.8" + {:blaze/version "0.17.9" :blaze/clock {}