From 2180898da508e30d930618a49e892392e3015cea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:00:31 +0000 Subject: [PATCH 01/24] chore(deps): update codecov/codecov-action digest to e28ff12 --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ba14fd272..a14599d61 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -46,7 +46,7 @@ jobs: run: make test-coverage - name: Codecov Upload - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 with: name: codecov-umbrella token: ${{ secrets.CODECOV_TOKEN }} From 7f1110b45eef990680288c79b9690cd0c672e4b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:58:21 +0000 Subject: [PATCH 02/24] chore(deps): update actions/checkout digest to 692973e --- .github/workflows/build.yml | 56 +++++++++++++++++----------------- .github/workflows/coverage.yml | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30cbf0b02..21d0261bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: cljfmt: 0.12.0 - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Check Formatting run: make fmt @@ -40,7 +40,7 @@ jobs: clj-kondo: '2024.03.13' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Lint run: make lint @@ -58,7 +58,7 @@ jobs: run: npm install -g fsh-sushi - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Build working-directory: job-ig @@ -148,7 +148,7 @@ jobs: cli: '1.11.2.1446' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 @@ -189,7 +189,7 @@ jobs: cli: '1.11.2.1446' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 @@ -223,7 +223,7 @@ jobs: cli: '1.11.2.1446' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 @@ -268,7 +268,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 @@ -316,7 +316,7 @@ jobs: run: docker load --input /tmp/${{ matrix.image }}.tar - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Run Trivy Vulnerability Scanner uses: aquasecurity/trivy-action@master @@ -345,7 +345,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -856,7 +856,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -924,7 +924,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -968,7 +968,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1007,7 +1007,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1048,7 +1048,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1083,7 +1083,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1121,7 +1121,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1150,7 +1150,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1181,7 +1181,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1224,7 +1224,7 @@ jobs: cli: '1.11.2.1446' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 @@ -1268,7 +1268,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1309,7 +1309,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1341,7 +1341,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1385,7 +1385,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1423,7 +1423,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 @@ -1507,7 +1507,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 @@ -1565,7 +1565,7 @@ jobs: cli: '1.11.2.1446' - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 @@ -1662,7 +1662,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Download Blaze Uberjar if: ${{ matrix.image.context == '.' }} @@ -1775,7 +1775,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Setup Node uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a14599d61..0095bd965 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -30,7 +30,7 @@ jobs: run: npm install -g fsh-sushi - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Cache Local Maven Repo uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4 From c72cdba1a8838a1727a9bcbdd6f2e077b5705e0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:32:14 +0000 Subject: [PATCH 03/24] chore(deps): update dependency ring/ring-core to v1.12.2 --- modules/rest-util/deps.edn | 2 +- modules/server/deps.edn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/rest-util/deps.edn b/modules/rest-util/deps.edn index 3dc860155..2e5dded83 100644 --- a/modules/rest-util/deps.edn +++ b/modules/rest-util/deps.edn @@ -23,7 +23,7 @@ :exclusions [com.cognitect/transit-clj]} ring/ring-core - {:mvn/version "1.12.1" + {:mvn/version "1.12.2" :exclusions [org.apache.commons/commons-fileupload2-core crypto-equality/crypto-equality diff --git a/modules/server/deps.edn b/modules/server/deps.edn index fb5d28bef..4cdfd48aa 100644 --- a/modules/server/deps.edn +++ b/modules/server/deps.edn @@ -6,7 +6,7 @@ {:local/root "../module-base"} ring/ring-core - {:mvn/version "1.12.1" + {:mvn/version "1.12.2" :exclusions [org.apache.commons/commons-fileupload2-core crypto-equality/crypto-equality From 64ae52e70c4170ca4845b9caf2ca34657c1fb7b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:49:23 +0000 Subject: [PATCH 04/24] chore(deps): update dependency svelte to v4.2.18 --- modules/frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 77c059b20..f7d20af5b 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -4170,9 +4170,9 @@ } }, "node_modules/svelte": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.17.tgz", - "integrity": "sha512-N7m1YnoXtRf5wya5Gyx3TWuTddI4nAyayyIWFojiWV5IayDYNV5i2mRp/7qNGol4DtxEYxljmrbgp1HM6hUbmQ==", + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", + "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", From bc311b74e75b1ae40cd042a2c537a68eb742bb0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 03:04:39 +0000 Subject: [PATCH 05/24] chore(deps): update dependency @sveltejs/kit to v2.5.16 --- modules/frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index f7d20af5b..448c248b9 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1062,9 +1062,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.10.tgz", - "integrity": "sha512-OqoyTmFG2cYmCFAdBfW+Qxbg8m23H4dv6KqwEt7ofr/ROcfcIl3Z/VT56L22H9f0uNZyr+9Bs1eh2gedOCK9kA==", + "version": "2.5.16", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.16.tgz", + "integrity": "sha512-09Ypy+ibuhTCTpRFRnR+cDI3VARiu16o7vVSjETAA43ZCLtqvrNrVxUkJ/fKHrAjx2peKWilcHE8+SbW2Z/AsQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { From 28f5aee91d39085c6f3ab3272c128f6ca06e69b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 06:44:05 +0000 Subject: [PATCH 06/24] chore(deps): update dependency @sveltejs/adapter-static to v3.0.2 --- modules/frontend/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 448c248b9..cece6aefc 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1053,10 +1053,11 @@ } }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz", - "integrity": "sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz", + "integrity": "sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==", "dev": true, + "license": "MIT", "peerDependencies": { "@sveltejs/kit": "^2.0.0" } From bb1ba51a01731aafad988097cab36e5c086a6373 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:10:12 +0000 Subject: [PATCH 07/24] chore(deps): update docker/build-push-action action to v6 --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21d0261bc..002a75291 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -250,7 +250,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 with: context: . tags: blaze:latest @@ -282,7 +282,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 with: context: modules/frontend tags: blaze-frontend:latest @@ -1717,7 +1717,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push to GHCR - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 @@ -1748,7 +1748,7 @@ jobs: - name: Build and push to DockerHub if: ${{ env.dockerhub_username != '' }} - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 From 463df434b9caa47ea2975007c9bb9fe3c3bfd7cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:04:26 +0000 Subject: [PATCH 08/24] chore(deps): update dependency @sveltejs/adapter-auto to v3.2.2 --- modules/frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index cece6aefc..402e72462 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1025,9 +1025,9 @@ "dev": true }, "node_modules/@sveltejs/adapter-auto": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.1.tgz", - "integrity": "sha512-/3xx8ZFCD5UBc/7AbyXkFF3HNCzWAp2xncH8HA4doGjoGQEN7PmwiRx4Y9nOzi4mqDqYYUic0gaIAE2khWWU4Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz", + "integrity": "sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==", "dev": true, "license": "MIT", "dependencies": { From ea9f2a3a83614f13ac0740441ce066676a4823f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:56:04 +0000 Subject: [PATCH 09/24] chore(deps): update docker/build-push-action digest to 31159d4 --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 002a75291..e7c3f0523 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -250,7 +250,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 with: context: . tags: blaze:latest @@ -282,7 +282,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 with: context: modules/frontend tags: blaze-frontend:latest @@ -1717,7 +1717,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push to GHCR - uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 @@ -1748,7 +1748,7 @@ jobs: - name: Build and push to DockerHub if: ${{ env.dockerhub_username != '' }} - uses: docker/build-push-action@c382f710d39a5bb4e430307530a720f50c2d3318 # v6 + uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 From 8bc5f491244be340df6f95e08c22380b2407f9f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 02:26:28 +0000 Subject: [PATCH 10/24] chore(deps): update dependency @sveltejs/kit to v2.5.17 --- modules/frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 402e72462..5fa7363bb 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1063,9 +1063,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.16", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.16.tgz", - "integrity": "sha512-09Ypy+ibuhTCTpRFRnR+cDI3VARiu16o7vVSjETAA43ZCLtqvrNrVxUkJ/fKHrAjx2peKWilcHE8+SbW2Z/AsQ==", + "version": "2.5.17", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", + "integrity": "sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==", "hasInstallScript": true, "license": "MIT", "dependencies": { From 4497165266c6e0d59584adb584e7fe38cd894ff0 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 24 Jun 2024 07:01:13 +0200 Subject: [PATCH 11/24] Add Record for Complex Type Ratio Needed in: #1051 --- .../admin-api/test/blaze/admin_api_test.clj | 2 +- modules/db/test/blaze/db/api_test.clj | 16 +-- .../src/blaze/fhir/spec/impl.clj | 4 + .../src/blaze/fhir/spec/type.clj | 5 + .../src/blaze/fhir/spec/type/system_spec.clj | 4 + .../blaze/fhir/spec/type_test_mem.clj | 2 + .../test/blaze/fhir/spec/generators.clj | 10 +- .../test/blaze/fhir/spec/type/system_test.clj | 28 ++++ .../test/blaze/fhir/spec/type_test.clj | 124 +++++++++++++++++- .../test/blaze/fhir/spec_test.clj | 81 +++++++++++- .../src/blaze/interaction/util.clj | 2 +- .../interaction/transaction/bundle_test.clj | 32 ++--- .../blaze/interaction/transaction_test.clj | 4 +- .../test/blaze/interaction/update_test.clj | 8 +- modules/jepsen/src/blaze/jepsen/register.clj | 2 +- .../src/blaze/job/async_interaction.clj | 6 +- .../src/blaze/job/async_interaction/util.clj | 8 +- .../job-scheduler/src/blaze/job_scheduler.clj | 2 +- .../test/blaze/job_scheduler_test.clj | 8 +- modules/job-util/src/blaze/job/util.clj | 12 +- modules/job-util/test/blaze/job/util_test.clj | 4 +- .../operation/evaluate_measure/measure.clj | 6 +- .../evaluate_measure/measure/util.clj | 4 +- .../evaluate_measure/measure_test.clj | 2 +- 24 files changed, 313 insertions(+), 63 deletions(-) diff --git a/modules/admin-api/test/blaze/admin_api_test.clj b/modules/admin-api/test/blaze/admin_api_test.clj index 8e2f8b54a..ac51e25a7 100644 --- a/modules/admin-api/test/blaze/admin_api_test.clj +++ b/modules/admin-api/test/blaze/admin_api_test.clj @@ -581,7 +581,7 @@ [#fhir/Coding {:system #fhir/uri"system-192253" :code #fhir/code"code-192300"}]} - :subject (type/map->Reference {:reference (str "Patient/" pat-id)})}])) + :subject (type/reference {:reference (str "Patient/" pat-id)})}])) (range 120))) (mapv (range 100))) diff --git a/modules/db/test/blaze/db/api_test.clj b/modules/db/test/blaze/db/api_test.clj index 749554af3..8860c4041 100644 --- a/modules/db/test/blaze/db/api_test.clj +++ b/modules/db/test/blaze/db/api_test.clj @@ -1400,7 +1400,7 @@ (with-system-data [{:blaze.db/keys [node]} config] [[[:put {:fhir/type :fhir/Patient :id "0" :active true - :name [(type/map->HumanName {:family (apply str (repeat 1000 "a"))})]}]]] + :name [(type/human-name {:family (apply str (repeat 1000 "a"))})]}]]] (testing "as first clause" (given (pull-type-query node "Patient" [["family" (apply str (repeat 1000 "a"))]]) @@ -1419,7 +1419,7 @@ (with-system-data [{:blaze.db/keys [node]} config] [[[:put {:fhir/type :fhir/Patient :id "0" :active true - :name [(type/map->HumanName {:family name})]}]]] + :name [(type/human-name {:family name})]}]]] (testing "as first clause" (given (pull-type-query node "Patient" [["family" name]]) @@ -4563,7 +4563,7 @@ (defn- patient-w-identifier [i] {:fhir/type :fhir/Patient :id (str i) - :identifier [(type/map->Identifier {:value (str i)})]}) + :identifier [(type/identifier {:value (str i)})]}) (deftest type-query-identifier-non-matching-test (st/unstrument) @@ -4586,15 +4586,15 @@ [[[:put {:fhir/type :fhir/Patient :id "0" :active true :identifier - [(type/map->Identifier {:system system :value "0"})]}] + [(type/identifier {:system system :value "0"})]}] [:put {:fhir/type :fhir/Patient :id "1" :active true :identifier - [(type/map->Identifier {:system system :value "0"})]}] + [(type/identifier {:system system :value "0"})]}] [:put {:fhir/type :fhir/Patient :id "2" :active true :identifier - [(type/map->Identifier {:system system :value "0"})]}]]] + [(type/identifier {:system system :value "0"})]}]]] (doseq [value (if system ["0" "foo|0"] ["0" "|0"])] (given (pull-type-query node "Patient" [["identifier" value]]) @@ -6356,9 +6356,9 @@ (defn- observation-create-op [id] [:create {:fhir/type :fhir/Observation :id (format "%05d" id) :category - [(type/map->CodeableConcept + [(type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system #fhir/uri"system-141902" :code (type/code (format "%05d" id))})]})]}]) diff --git a/modules/fhir-structure/src/blaze/fhir/spec/impl.clj b/modules/fhir-structure/src/blaze/fhir/spec/impl.clj index 0fd7010ce..6d54377d5 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/impl.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/impl.clj @@ -311,6 +311,7 @@ :fhir/Coding :fhir/CodeableConcept :fhir/Quantity + :fhir/Ratio :fhir/Period :fhir/Identifier :fhir/HumanName @@ -425,6 +426,7 @@ :fhir.json/Meta :fhir.json/Attachment :fhir.json/Quantity + :fhir.json/Ratio :fhir.json/Period :fhir.json/Identifier :fhir.json/HumanName @@ -549,6 +551,7 @@ :fhir.xml/Coding :fhir.xml/CodeableConcept :fhir.xml/Quantity + :fhir.xml/Ratio :fhir.xml/Period :fhir.xml/Identifier :fhir.xml/HumanName @@ -590,6 +593,7 @@ :fhir.cbor/Meta :fhir.cbor/Attachment :fhir.cbor/Quantity + :fhir.cbor/Ratio :fhir.cbor/Period :fhir.cbor/Identifier :fhir.cbor/HumanName diff --git a/modules/fhir-structure/src/blaze/fhir/spec/type.clj b/modules/fhir-structure/src/blaze/fhir/spec/type.clj index 77de41e01..0e94f40ed 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/type.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/type.clj @@ -1045,6 +1045,11 @@ :hash-num 40 :interned (and (nil? id) (p/-interned extension) (nil? value))) +(declare ratio) + +(def-complex-type Ratio [^String id extension numerator denominator] + :hash-num 48) + (declare period) (def-complex-type Period [^String id extension ^:primitive start ^:primitive end] diff --git a/modules/fhir-structure/src/blaze/fhir/spec/type/system_spec.clj b/modules/fhir-structure/src/blaze/fhir/spec/type/system_spec.clj index 2e9b3eeff..5284df41f 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/type/system_spec.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/type/system_spec.clj @@ -33,6 +33,10 @@ :args (s/cat :x any?) :ret boolean?) +(s/fdef system/date + :args (s/cat :year int? :month (s/? int?) :day (s/? int?)) + :ret system/date?) + ;; ---- System.DateTime ------------------------------------------------------- (s/fdef system/date-time? diff --git a/modules/fhir-structure/test-perf/blaze/fhir/spec/type_test_mem.clj b/modules/fhir-structure/test-perf/blaze/fhir/spec/type_test_mem.clj index cd94ee05b..dc9dce381 100644 --- a/modules/fhir-structure/test-perf/blaze/fhir/spec/type_test_mem.clj +++ b/modules/fhir-structure/test-perf/blaze/fhir/spec/type_test_mem.clj @@ -83,6 +83,8 @@ #fhir/Quantity{} 56 + #fhir/Ratio{} 48 + #fhir/Period{} 48 #fhir/Identifier{} 64 diff --git a/modules/fhir-structure/test/blaze/fhir/spec/generators.clj b/modules/fhir-structure/test/blaze/fhir/spec/generators.clj index 15ebeaf94..92e4e7cfd 100644 --- a/modules/fhir-structure/test/blaze/fhir/spec/generators.clj +++ b/modules/fhir-structure/test/blaze/fhir/spec/generators.clj @@ -265,7 +265,15 @@ ;; TODO: Range -;; TODO: Ratio +(defn ratio + [& {:keys [id extension numerator denominator] + :or {id (gen/return nil) + extension (extensions) + numerator (nilable (quantity)) + denominator (nilable (quantity))}}] + (->> (gen/tuple id extension numerator denominator) + (to-map [:id :extension :numerator :denominator]) + (gen/fmap type/ratio))) ;; TODO: RatioRange 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 72f9a50ee..40f0353c5 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 @@ -175,6 +175,34 @@ nil #system/date-time"2020")) + (testing "date" + (testing "year" + (are [year date] (= date (system/date year)) + 1000 #system/date"1000" + 2024 #system/date"2024" + 9999 #system/date"9999") + + (given-thrown (system/date -1) + :message := "Invalid value for Year (valid values 1 - 9999): -1")) + + (testing "year-month" + (are [year month date] (= date (system/date year month)) + 1000 1 #system/date"1000-01" + 2024 6 #system/date"2024-06" + 9999 12 #system/date"9999-12") + + (given-thrown (system/date 2024 0) + :message := "Invalid value for MonthOfYear (valid values 1 - 12): 0")) + + (testing "year-month-day" + (are [year month day date] (= date (system/date year month day)) + 1000 1 1 #system/date"1000-01-01" + 2024 6 15 #system/date"2024-06-15" + 9999 12 31 #system/date"9999-12-31") + + (given-thrown (system/date 2023 2 29) + :message := "Invalid date 'February 29' as '2023' is not a leap year"))) + (testing "system equals" (testing "same precision" (testing "within date" diff --git a/modules/fhir-structure/test/blaze/fhir/spec/type_test.clj b/modules/fhir-structure/test/blaze/fhir/spec/type_test.clj index 0039fe286..46e93bd64 100644 --- a/modules/fhir-structure/test/blaze/fhir/spec/type_test.clj +++ b/modules/fhir-structure/test/blaze/fhir/spec/type_test.clj @@ -1986,6 +1986,10 @@ #fhir/Attachment{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]} #fhir/Attachment{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Attachment{}))) + (is (false? (p/-has-secondary-content #fhir/Attachment{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Attachment{} @@ -2062,6 +2066,10 @@ #fhir/Extension{:url "foo" :value "bar"} #fhir/Extension{:url "foo" :value "bar"}))) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Extension{}))) + (is (false? (p/-has-secondary-content #fhir/Extension{})))) + (testing "to-json" (are [code json] (= json (gen-json-string code)) #fhir/Extension{} "{}" @@ -2125,6 +2133,10 @@ (prop/for-all [x (fg/coding :extension (gen/vector string-extension-gen 1))] (not-interned? x (recreate type/coding x)))))) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Coding{}))) + (is (false? (p/-has-secondary-content #fhir/Coding{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Coding{} @@ -2177,6 +2189,10 @@ (prop/for-all [x (fg/codeable-concept :extension (gen/vector string-extension-gen 1))] (not-interned? x (recreate type/codeable-concept x)))))) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/CodeableConcept{}))) + (is (false? (p/-has-secondary-content #fhir/CodeableConcept{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/CodeableConcept{} @@ -2235,6 +2251,10 @@ #fhir/Quantity{:code #fhir/code"foo"} #fhir/Quantity{:code #fhir/code"foo"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Quantity{}))) + (is (false? (p/-has-secondary-content #fhir/Quantity{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Quantity{} @@ -2246,7 +2266,7 @@ #fhir/Quantity{:extension [#fhir/Extension{}]} "4f5028ac" - #fhir/Quantity{:value 1M} + #fhir/Quantity{:value #fhir/decimal 1M} "4adf97ab" #fhir/Quantity{:comparator #fhir/code"comparator-153342"} @@ -2271,10 +2291,88 @@ #fhir/Quantity{} "#fhir/Quantity{}" #fhir/Quantity{:id "212329"} "#fhir/Quantity{:id \"212329\"}"))) +(deftest ratio-test + (testing "type" + (is (= :fhir/Ratio (type/type #fhir/Ratio{})))) + + (testing "interned" + (are [x y] (not-interned? x y) + #fhir/Ratio{:id "foo"} + #fhir/Ratio{:id "foo"} + + #fhir/Ratio{:extension [#fhir/Extension{:url "foo" :value "bar"}]} + #fhir/Ratio{:extension [#fhir/Extension{:url "foo" :value "bar"}]} + + #fhir/Ratio{:numerator #fhir/Quantity{:value #fhir/decimal 1M}} + #fhir/Ratio{:numerator #fhir/Quantity{:value #fhir/decimal 1M}} + + #fhir/Ratio{:denominator #fhir/Quantity{:value #fhir/decimal 1M}} + #fhir/Ratio{:denominator #fhir/Quantity{:value #fhir/decimal 1M}}) + + (are [x y] (interned? x y) + #fhir/Ratio{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]} + #fhir/Ratio{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]} + + #fhir/Ratio{:numerator #fhir/Quantity{:code #fhir/code"foo"}} + #fhir/Ratio{:numerator #fhir/Quantity{:code #fhir/code"foo"}} + + #fhir/Ratio{:denominator #fhir/Quantity{:code #fhir/code"foo"}} + #fhir/Ratio{:denominator #fhir/Quantity{:code #fhir/code"foo"}})) + + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Ratio{}))) + (is (false? (p/-has-secondary-content #fhir/Ratio{})))) + + (testing "hash-into" + (are [x hex] (= hex (murmur3 x)) + #fhir/Ratio{} + "d271c07f" + + #fhir/Ratio{:id "id-130710"} + "e3c0ee3c" + + #fhir/Ratio{:extension [#fhir/Extension{}]} + "23473d24" + + #fhir/Ratio{:numerator #fhir/Quantity{:value #fhir/decimal 1M}} + "fbf83a67" + + #fhir/Ratio{:denominator #fhir/Quantity{:value #fhir/decimal 1M}} + "7f2075fb")) + + (testing "references" + (are [x refs] (= refs (type/references x)) + #fhir/Ratio{} + [])) + + (testing "print" + (are [v s] (= s (pr-str v)) + #fhir/Ratio{} "#fhir/Ratio{}" + #fhir/Ratio{:id "212329"} "#fhir/Ratio{:id \"212329\"}"))) + (deftest period-test (testing "type" (is (= :fhir/Period (type/type #fhir/Period{})))) + (testing "interned" + (are [x y] (not-interned? x y) + #fhir/Period{:id "foo"} + #fhir/Period{:id "foo"} + + #fhir/Period{:extension [#fhir/Extension{:url "foo" :value "bar"}]} + #fhir/Period{:extension [#fhir/Extension{:url "foo" :value "bar"}]} + + #fhir/Period{:start #fhir/dateTime"2020"} + #fhir/Period{:start #fhir/dateTime"2020"}) + + (are [x y] (interned? x y) + #fhir/Period{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]} + #fhir/Period{:extension [#fhir/Extension{:url "foo" :value #fhir/code"bar"}]})) + + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Period{}))) + (is (false? (p/-has-secondary-content #fhir/Period{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Period{} @@ -2324,6 +2422,10 @@ #fhir/Identifier{:use #fhir/code"foo"} #fhir/Identifier{:use #fhir/code"foo"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Identifier{}))) + (is (false? (p/-has-secondary-content #fhir/Identifier{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Identifier{} @@ -2388,6 +2490,10 @@ #fhir/HumanName{:use #fhir/code"foo"} #fhir/HumanName{:use #fhir/code"foo"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/HumanName{}))) + (is (false? (p/-has-secondary-content #fhir/HumanName{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/HumanName{} @@ -2464,6 +2570,10 @@ #fhir/Address{:type #fhir/code"foo"} #fhir/Address{:type #fhir/code"foo"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Address{}))) + (is (false? (p/-has-secondary-content #fhir/Address{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Address{} @@ -2546,6 +2656,10 @@ #fhir/Reference{:type #fhir/code"foo"} #fhir/Reference{:type #fhir/code"foo"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Reference{}))) + (is (false? (p/-has-secondary-content #fhir/Reference{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Reference{} @@ -2649,6 +2763,10 @@ #fhir/Meta{:tag [#fhir/Coding{:system #fhir/uri"foo" :code #fhir/code"bar"}]} #fhir/Meta{:tag [#fhir/Coding{:system #fhir/uri"foo" :code #fhir/code"bar"}]})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/Meta{}))) + (is (false? (p/-has-secondary-content #fhir/Meta{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/Meta{} @@ -2718,6 +2836,10 @@ #fhir/BundleEntrySearch{:mode #fhir/code"match"} #fhir/BundleEntrySearch{:mode #fhir/code"match"})) + (testing "primary/secondary content" + (is (true? (p/-has-primary-content #fhir/BundleEntrySearch{}))) + (is (false? (p/-has-secondary-content #fhir/BundleEntrySearch{})))) + (testing "hash-into" (are [x hex] (= hex (murmur3 x)) #fhir/BundleEntrySearch{} diff --git a/modules/fhir-structure/test/blaze/fhir/spec_test.clj b/modules/fhir-structure/test/blaze/fhir/spec_test.clj index a4083d827..3e73d10e6 100644 --- a/modules/fhir-structure/test/blaze/fhir/spec_test.clj +++ b/modules/fhir-structure/test/blaze/fhir/spec_test.clj @@ -3532,7 +3532,7 @@ #fhir/Quantity{} {:value 1M} - #fhir/Quantity{:value 1M} + #fhir/Quantity{:value #fhir/decimal 1M} {:value "1"} ::s2/invalid))) @@ -3552,7 +3552,7 @@ #fhir/Quantity{:extension [#fhir/Extension{} #fhir/Extension{}]} {:extension [{} {}]} - #fhir/Quantity{:value 1M} + #fhir/Quantity{:value #fhir/decimal 1M} {:value 1} #fhir/Quantity{:comparator #fhir/code"code-153342"} @@ -3567,6 +3567,83 @@ #fhir/Quantity{:code #fhir/code"code-153427"} {:code "code-153427"})))) +(deftest ratio-test + (testing "FHIR spec" + (testing "valid" + (satisfies-prop 1000 + (prop/for-all [x (fg/ratio)] + (s2/valid? :fhir/Ratio x)))) + + (testing "invalid" + (are [x] (not (s2/valid? :fhir/Ratio x)) + #fhir/Ratio{:numerator "1"}))) + + (testing "transforming" + (testing "JSON" + (satisfies-prop 1000 + (prop/for-all [x (fg/ratio)] + (= (->> x + fhir-spec/unform-json + fhir-spec/parse-json + (s2/conform :fhir.json/Ratio)) + x)))) + + (testing "XML" + (satisfies-prop 1000 + (prop/for-all [x (fg/ratio)] + (= (->> x + fhir-spec/unform-xml + (s2/conform :fhir.xml/Ratio)) + x)))) + + (testing "CBOR" + (satisfies-prop 1000 + (prop/for-all [x (fg/ratio)] + (= (->> x + fhir-spec/unform-cbor + fhir-spec/parse-cbor + (s2/conform :fhir.cbor/Ratio)) + x))))) + + (testing "conforming" + (testing "JSON" + (are [json fhir] (= fhir (s2/conform :fhir.json/Ratio json)) + {} + #fhir/Ratio{} + + {:id "id-151304"} + #fhir/Ratio{:id "id-151304"} + + {:extension [{}]} + #fhir/Ratio{:extension [#fhir/Extension{}]} + + {:numerator {:value 1M}} + #fhir/Ratio{:numerator #fhir/Quantity{:value #fhir/decimal 1M}} + + {:numerator "foo"} + ::s2/invalid))) + + (testing "unforming" + (testing "JSON" + (are [fhir json] (= json (fhir-spec/parse-json (fhir-spec/unform-json fhir))) + #fhir/Ratio{} + {} + + #fhir/Ratio{:id "id-134428"} + {:id "id-134428"} + + #fhir/Ratio{:extension [#fhir/Extension{}]} + {:extension [{}]} + + #fhir/Ratio{:extension [#fhir/Extension{} #fhir/Extension{}]} + {:extension [{} {}]} + + #fhir/Ratio{:numerator #fhir/Quantity{:value #fhir/decimal 1M}} + {:numerator {:value 1}} + + #fhir/Ratio{:denominator #fhir/Quantity{:value #fhir/decimal 1M}} + {:denominator {:value 1}})))) + (deftest period-test (testing "FHIR spec" (testing "valid" diff --git a/modules/interaction/src/blaze/interaction/util.clj b/modules/interaction/src/blaze/interaction/util.clj index edbc76cdc..028936cb7 100644 --- a/modules/interaction/src/blaze/interaction/util.clj +++ b/modules/interaction/src/blaze/interaction/util.clj @@ -117,7 +117,7 @@ (let [meta (into {} (keep (fn [[k v]] (when (and v (not (#{:versionId :lastUpdated} k))) [k v]))) meta)] (if (empty? meta) (dissoc resource :meta) - (assoc resource :meta (type/map->Meta meta))))) + (assoc resource :meta (type/meta meta))))) (defn keep? "Determines whether `tx-op` is a keep operator." diff --git a/modules/interaction/test/blaze/interaction/transaction/bundle_test.clj b/modules/interaction/test/blaze/interaction/transaction/bundle_test.clj index 99c516cb6..ed80037ed 100644 --- a/modules/interaction/test/blaze/interaction/transaction/bundle_test.clj +++ b/modules/interaction/test/blaze/interaction/transaction/bundle_test.clj @@ -26,8 +26,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-220129" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"POST" @@ -41,8 +41,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-220200" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"POST" @@ -59,8 +59,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-220200" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"POST" @@ -75,8 +75,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-220200" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"POST" @@ -91,8 +91,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-214728" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"PUT" @@ -106,8 +106,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-214728" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"PUT" @@ -124,8 +124,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "id-214728" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH})} + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH})} :request {:fhir/type :fhir.Bundle.entry/request :method #fhir/code"PUT" @@ -158,8 +158,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "0" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH}) + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH}) :gender #fhir/code"male"} :request {:fhir/type :fhir.Bundle.entry/request diff --git a/modules/interaction/test/blaze/interaction/transaction_test.clj b/modules/interaction/test/blaze/interaction/transaction_test.clj index d5e95057d..3128b0eae 100644 --- a/modules/interaction/test/blaze/interaction/transaction_test.clj +++ b/modules/interaction/test/blaze/interaction/transaction_test.clj @@ -438,8 +438,8 @@ [{:fhir/type :fhir.Bundle/entry :resource {:fhir/type :fhir/Patient :id "0" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH}) + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH}) :gender #fhir/code"female"} :request {:fhir/type :fhir.Bundle.entry/request diff --git a/modules/interaction/test/blaze/interaction/update_test.clj b/modules/interaction/test/blaze/interaction/update_test.clj index 93604cf37..da4c5c535 100644 --- a/modules/interaction/test/blaze/interaction/update_test.clj +++ b/modules/interaction/test/blaze/interaction/update_test.clj @@ -499,8 +499,8 @@ ::reitit/match patient-match :headers {"if-match" if-match} :body {:fhir/type :fhir/Patient :id "0" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH}) + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH}) :birthDate #fhir/date"2020"}})] (testing "Returns 200" @@ -541,8 +541,8 @@ ::reitit/match patient-match :headers {"if-match" if-match} :body {:fhir/type :fhir/Patient :id "0" - :meta (type/map->Meta {:versionId #fhir/id"1" - :lastUpdated Instant/EPOCH}) + :meta (type/meta {:versionId #fhir/id"1" + :lastUpdated Instant/EPOCH}) :birthDate #fhir/date"2020"}})] (testing "Returns 200" diff --git a/modules/jepsen/src/blaze/jepsen/register.clj b/modules/jepsen/src/blaze/jepsen/register.clj index 0346092a4..e2b9341a0 100644 --- a/modules/jepsen/src/blaze/jepsen/register.clj +++ b/modules/jepsen/src/blaze/jepsen/register.clj @@ -61,7 +61,7 @@ @(-> (fhir-client/update base-uri {:fhir/type :fhir/Observation :id "0" - :subject (type/map->Reference {:reference (str "Patient/" (random-uuid))})} + :subject (type/reference {:reference (str "Patient/" (random-uuid))})} context) (ac/exceptionally (constantly nil)))) diff --git a/modules/job-async-interaction/src/blaze/job/async_interaction.clj b/modules/job-async-interaction/src/blaze/job/async_interaction.clj index fad07ce7f..cb41d08e1 100644 --- a/modules/job-async-interaction/src/blaze/job/async_interaction.clj +++ b/modules/job-async-interaction/src/blaze/job/async_interaction.clj @@ -38,9 +38,9 @@ :input [(u/request-bundle-input (str "Bundle/" bundle-id)) {:fhir/type :fhir.Task/input - :type (type/map->CodeableConcept + :type (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri u/parameter-uri) :code #fhir/code"t"})]}) :value (type/unsignedInt t)}]}) @@ -91,7 +91,7 @@ (response-bundle context entries))))))) (defn add-response-bundle-reference [job response-bundle-id] - (->> (type/map->Reference {:reference (str "Bundle/" response-bundle-id)}) + (->> (type/reference {:reference (str "Bundle/" response-bundle-id)}) (job-util/add-output job output-uri "bundle"))) (defn- add-processing-duration [job start] diff --git a/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj b/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj index c3685b07d..3364960e2 100644 --- a/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj +++ b/modules/job-async-interaction/src/blaze/job/async_interaction/util.clj @@ -15,15 +15,15 @@ (defn request-bundle-input [reference] {:fhir/type :fhir.Task/input - :type (type/map->CodeableConcept + :type (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri parameter-uri) :code #fhir/code"bundle"})]}) - :value (type/map->Reference {:reference reference})}) + :value (type/reference {:reference reference})}) (defn processing-duration [start] - (type/map->Quantity + (type/quantity {:value (type/decimal (BigDecimal/valueOf (- (System/currentTimeMillis) start) 3)) :unit #fhir/string"s" :system #fhir/uri"http://unitsofmeasure.org" diff --git a/modules/job-scheduler/src/blaze/job_scheduler.clj b/modules/job-scheduler/src/blaze/job_scheduler.clj index 61591fc5a..f21d313c9 100644 --- a/modules/job-scheduler/src/blaze/job_scheduler.clj +++ b/modules/job-scheduler/src/blaze/job_scheduler.clj @@ -143,7 +143,7 @@ (comp type/integer inc type/value)) (defn- job-number-identifier [job-number] - (type/map->Identifier + (type/identifier {:use #fhir/code"official" :system (type/uri job-util/job-number-url) :value (str job-number)})) diff --git a/modules/job-scheduler/test/blaze/job_scheduler_test.clj b/modules/job-scheduler/test/blaze/job_scheduler_test.clj index ec0c37ac8..75eb81ba8 100644 --- a/modules/job-scheduler/test/blaze/job_scheduler_test.clj +++ b/modules/job-scheduler/test/blaze/job_scheduler_test.clj @@ -282,9 +282,9 @@ (is (s/valid? :blaze/job-scheduler job-scheduler))))) (defn- job-type [type] - (type/map->CodeableConcept + (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri job-util/type-url) :code (type/code type)})]})) @@ -304,7 +304,7 @@ [#fhir/Coding {:system #fhir/uri"https://samply.github.io/blaze/fhir/CodeSystem/AsyncInteractionJobParameter" :code #fhir/code"bundle"}]} - :value (type/map->Reference {:reference (str "Bundle/" bundle-id)})}]}) + :value (type/reference {:reference (str "Bundle/" bundle-id)})}]}) (defn bundle [id] {:fhir/type :fhir/Bundle @@ -412,7 +412,7 @@ :fhir/type := :fhir/Task job-util/job-number := "1" jtu/combined-status := :ready - bundle-input := (type/map->Reference {:reference (str "Bundle/" bundle-id)}))) + bundle-input := (type/reference {:reference (str "Bundle/" bundle-id)}))) (testing "the bundle is created" (given @(d/pull node (d/resource-handle (d/db node) "Bundle" bundle-id)) diff --git a/modules/job-util/src/blaze/job/util.clj b/modules/job-util/src/blaze/job/util.clj index 64890ea4a..bc02d30b0 100644 --- a/modules/job-util/src/blaze/job/util.clj +++ b/modules/job-util/src/blaze/job/util.clj @@ -22,16 +22,16 @@ "https://samply.github.io/blaze/fhir/CodeSystem/JobOutput") (defn- mk-status-reason [reason] - (type/map->CodeableConcept + (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri status-reason-url) :code (type/code reason)})]})) (defn- mk-sub-status [system-url code] - (type/map->CodeableConcept + (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri system-url) :code (type/code code)})]})) @@ -111,9 +111,9 @@ (defn task-output [system code value] {:fhir/type :fhir.Task/output - :type (type/map->CodeableConcept + :type (type/codeable-concept {:coding - [(type/map->Coding + [(type/coding {:system (type/uri system) :code (type/code code)})]}) :value value}) diff --git a/modules/job-util/test/blaze/job/util_test.clj b/modules/job-util/test/blaze/job/util_test.clj index edc4dc955..3e0cd3010 100644 --- a/modules/job-util/test/blaze/job/util_test.clj +++ b/modules/job-util/test/blaze/job/util_test.clj @@ -26,9 +26,9 @@ (ig/init {:blaze.fhir/structure-definition-repo {}}))) (defn- codeable-concept [system code] - (type/map->CodeableConcept + (type/codeable-concept {:coding - [(type/map->Coding {:system (type/uri system) :code (type/code code)})]})) + [(type/coding {:system (type/uri system) :code (type/code code)})]})) (deftest job-number-test (is (= (job-util/job-number diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj index a112d92b1..9fdeed1fb 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj @@ -320,7 +320,7 @@ (type/extension {:url "https://samply.github.io/blaze/fhir/StructureDefinition/eval-duration" :value - (type/map->Quantity + (type/quantity {:code #fhir/code"s" :system #fhir/uri"http://unitsofmeasure.org" :unit #fhir/string"s" @@ -344,12 +344,12 @@ :measure (type/canonical (canonical context measure)) :date now :period - (type/map->Period + (type/period {:start (type/dateTime (str start)) :end (type/dateTime (str end))})} subject-handle - (assoc :subject (type/map->Reference {:reference (local-ref subject-handle)})) + (assoc :subject (type/reference {:reference (local-ref subject-handle)})) (seq result) (assoc :group result))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj index 1c4b98de9..37cf94b3f 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj @@ -30,10 +30,10 @@ expression))) (defn list-reference [list-id] - (type/map->Reference {:reference (str "List/" list-id)})) + (type/reference {:reference (str "List/" list-id)})) (defn- resource-reference [{:keys [id] :as resource}] - (type/map->Reference {:reference (str (name (type/type resource)) "/" id)})) + (type/reference {:reference (str (name (type/type resource)) "/" id)})) (defn population-tx-ops [list-id handles] [[:create diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj index 134227b6c..f1bc41542 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj @@ -60,7 +60,7 @@ :id "1" :url #fhir/uri"0" :content - [(type/map->Attachment + [(type/attachment {:contentType #fhir/code"text/cql" :data (type/->Base64Binary (b64-encode query))})]} :request From 994694db525ffe7d92414d37fc846356ad50b2e5 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 24 Jun 2024 12:42:42 +0200 Subject: [PATCH 12/24] Add Estimated Number of Keys Function We need this functionality for all key-value instead of only for RocksDB because we will use it in the CQL expression cache. Needed in: #1051 --- modules/admin-api/src/blaze/admin_api.clj | 3 ++- modules/kv/src/blaze/db/kv.clj | 7 +++++++ modules/kv/src/blaze/db/kv/mem.clj | 16 ++++++++++++---- modules/kv/src/blaze/db/kv/protocols.clj | 4 +++- modules/kv/src/blaze/db/kv_spec.clj | 7 ++++++- modules/kv/test/blaze/db/kv/mem_test.clj | 16 +++++++++++++++- modules/rocksdb/src/blaze/db/kv/rocksdb.clj | 3 +++ .../rocksdb/test/blaze/db/kv/rocksdb_test.clj | 15 ++++++++++++++- 8 files changed, 62 insertions(+), 9 deletions(-) diff --git a/modules/admin-api/src/blaze/admin_api.clj b/modules/admin-api/src/blaze/admin_api.clj index b94cfd975..d234eea33 100644 --- a/modules/admin-api/src/blaze/admin_api.clj +++ b/modules/admin-api/src/blaze/admin_api.clj @@ -4,6 +4,7 @@ [blaze.admin-api.validation] [blaze.anomaly :as ba :refer [if-ok]] [blaze.async.comp :as ac :refer [do-sync]] + [blaze.db.kv :as kv] [blaze.db.kv.rocksdb :as rocksdb] [blaze.elm.expression :as-alias expr] [blaze.fhir.response.create :as create-response] @@ -86,7 +87,7 @@ (defn- column-family-data [db column-family] (let [long-property (partial rocksdb/long-property db column-family)] {:name (name column-family) - :estimate-num-keys (long-property "rocksdb.estimate-num-keys") + :estimate-num-keys (kv/estimate-num-keys db column-family) :estimate-live-data-size (long-property "rocksdb.estimate-live-data-size") :live-sst-files-size (long-property "rocksdb.live-sst-files-size") :size-all-mem-tables (long-property "rocksdb.size-all-mem-tables")})) diff --git a/modules/kv/src/blaze/db/kv.clj b/modules/kv/src/blaze/db/kv.clj index 0bc62da4d..685c45afa 100644 --- a/modules/kv/src/blaze/db/kv.clj +++ b/modules/kv/src/blaze/db/kv.clj @@ -183,3 +183,10 @@ Writes are atomic. Blocks." [store entries] (p/-write store entries)) + +(defn estimate-num-keys + "Returns the estimated number of keys in `column-family` of `store`. + + Returns an anomaly if the column-family was not found." + [store column-family] + (p/-estimate-num-keys store column-family)) diff --git a/modules/kv/src/blaze/db/kv/mem.clj b/modules/kv/src/blaze/db/kv/mem.clj index bfab9fd71..2f8eb4dd2 100644 --- a/modules/kv/src/blaze/db/kv/mem.clj +++ b/modules/kv/src/blaze/db/kv/mem.clj @@ -110,14 +110,17 @@ (set! closed? true))) (defn- column-family-not-found-msg [column-family] - (format "column family `%s` not found" (name column-family))) + (format "Column family `%s` not found." (name column-family))) + +(defn- column-family-not-found-anom [column-family] + (ba/not-found (column-family-not-found-msg column-family))) (deftype MemKvSnapshot [db] p/KvSnapshot (-new-iterator [_ column-family] (if-let [db (get db column-family)] (->MemKvIterator db (atom {:rest (seq db)}) false) - (throw-anom (ba/not-found (column-family-not-found-msg column-family))))) + (throw-anom (column-family-not-found-anom column-family)))) (-snapshot-get [_ column-family k] (some-> (get-in db [column-family k]) (copy))) @@ -127,7 +130,7 @@ (defn- assoc-copy [m column-family k v] (when (nil? m) - (throw-anom (ba/not-found (column-family-not-found-msg column-family)))) + (throw-anom (column-family-not-found-anom column-family))) (assoc m (copy k) (copy v))) (defn- put-entries [db entries] @@ -172,7 +175,12 @@ (-write [_ entries] (swap! db write-entries entries) - nil)) + nil) + + (-estimate-num-keys [_ column-family] + (if-let [m (get @db column-family)] + (count m) + (column-family-not-found-anom column-family)))) (def ^:private bytes-cmp (reify Comparator diff --git a/modules/kv/src/blaze/db/kv/protocols.clj b/modules/kv/src/blaze/db/kv/protocols.clj index 1919f7d4e..b924d8151 100644 --- a/modules/kv/src/blaze/db/kv/protocols.clj +++ b/modules/kv/src/blaze/db/kv/protocols.clj @@ -43,4 +43,6 @@ (-delete [store entries]) - (-write [store entries])) + (-write [store entries]) + + (-estimate-num-keys [store column-family])) diff --git a/modules/kv/src/blaze/db/kv_spec.clj b/modules/kv/src/blaze/db/kv_spec.clj index 367513dd7..381b5eea6 100644 --- a/modules/kv/src/blaze/db/kv_spec.clj +++ b/modules/kv/src/blaze/db/kv_spec.clj @@ -4,7 +4,8 @@ [blaze.coll.spec :as cs] [blaze.db.kv :as kv] [blaze.db.kv.spec] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) (s/fdef kv/valid? :args (s/cat :iter ::kv/iterator) @@ -79,3 +80,7 @@ (s/fdef kv/write! :args (s/cat :kv-store :blaze.db/kv-store :entries (cs/coll-of ::kv/write-entry))) + +(s/fdef kv/estimate-num-keys + :args (s/cat :kv-store :blaze.db/kv-store :column-family simple-keyword?) + :ret (s/or :estimate-num-keys nat-int? :anomaly ::anom/anomaly)) diff --git a/modules/kv/test/blaze/db/kv/mem_test.clj b/modules/kv/test/blaze/db/kv/mem_test.clj index 3887f5182..1749ee173 100644 --- a/modules/kv/test/blaze/db/kv/mem_test.clj +++ b/modules/kv/test/blaze/db/kv/mem_test.clj @@ -15,13 +15,14 @@ [clojure.test :as test :refer [deftest is testing]] [cognitect.anomalies :as anom] [integrant.core :as ig] + [juxt.iota :refer [given]] [taoensso.timbre :as log]) (:import [java.lang AutoCloseable])) (set! *warn-on-reflection* true) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -576,5 +577,18 @@ (kv/write! kv-store [[:delete :default (ba 0x00)]]) (is (nil? (kv/get kv-store :default (ba 0x00))))))) +(deftest estimate-num-keys-test + (with-system [{kv-store ::kv/mem} config] + (is (zero? (kv/estimate-num-keys kv-store :default))) + + (given (kv/estimate-num-keys kv-store :foo) + ::anom/category := ::anom/not-found + ::anom/message := "Column family `foo` not found.")) + + (with-system-data [{kv-store ::kv/mem} config] + [[:default (ba 0x00) (ba 0x10)]] + + (is (= 1 (kv/estimate-num-keys kv-store :default))))) + (deftest init-component-test (is (kv/store? (ig/init-key ::kv/mem {})))) diff --git a/modules/rocksdb/src/blaze/db/kv/rocksdb.clj b/modules/rocksdb/src/blaze/db/kv/rocksdb.clj index 26e0e05d1..78129ed90 100644 --- a/modules/rocksdb/src/blaze/db/kv/rocksdb.clj +++ b/modules/rocksdb/src/blaze/db/kv/rocksdb.clj @@ -206,6 +206,9 @@ (impl/write-wb! cfhs wb entries) (.write db write-opts wb))) + (-estimate-num-keys [store column-family] + (p/-long-property store column-family "rocksdb.estimate-num-keys")) + p/Rocks (-path [_] path) diff --git a/modules/rocksdb/test/blaze/db/kv/rocksdb_test.clj b/modules/rocksdb/test/blaze/db/kv/rocksdb_test.clj index 9ceb6a4ec..19079339a 100644 --- a/modules/rocksdb/test/blaze/db/kv/rocksdb_test.clj +++ b/modules/rocksdb/test/blaze/db/kv/rocksdb_test.clj @@ -27,7 +27,7 @@ (set! *warn-on-reflection* true) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -624,6 +624,19 @@ (kv/write! db [[:delete :default (ba 0x00)]]) (is (nil? (kv/get db :default (ba 0x00))))))) +(deftest estimate-num-keys-test + (with-system [{db ::kv/rocksdb} (config (new-temp-dir!))] + (is (zero? (kv/estimate-num-keys db :default))) + + (given (kv/estimate-num-keys db :foo) + ::anom/category := ::anom/not-found + ::anom/message := "Column family `foo` not found.")) + + (with-system-data [{db ::kv/rocksdb} (config (new-temp-dir!))] + [[:default (ba 0x00) (ba 0x10)]] + + (is (= 1 (kv/estimate-num-keys db :default))))) + (deftest path-test (with-system [{db ::kv/rocksdb} (config (new-temp-dir!))] (is (string? (rocksdb/path db))))) From a2d8afe0f799de3b3a2837c3106564a53f1684d0 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 24 Jun 2024 13:09:51 +0200 Subject: [PATCH 13/24] Migrate Cache Collector into it's own Module We need the cache collector in the CQL expression cache. Needed in: #1051 --- dev/blaze/dev.clj | 2 +- modules/cache-collector/.clj-kondo/config.edn | 3 + modules/cache-collector/Makefile | 25 +++ modules/cache-collector/deps.edn | 45 ++++++ .../src/blaze}/cache_collector.clj | 35 +++-- .../src/blaze}/cache_collector/protocols.clj | 2 +- .../src/blaze/cache_collector/spec.clj | 7 + .../test/blaze/cache_collector_test.clj | 142 ++++++++++++++++++ modules/cache-collector/tests.edn | 5 + modules/db/deps.edn | 3 + .../db/src/blaze/db/cache_collector/spec.clj | 7 - modules/db/src/blaze/db/resource_cache.clj | 2 +- .../db/test/blaze/db/cache_collector_test.clj | 108 ------------- .../db/test/blaze/db/resource_cache_test.clj | 4 +- profiling/blaze/profiling.clj | 2 +- resources/blaze.edn | 2 +- 16 files changed, 257 insertions(+), 137 deletions(-) create mode 100644 modules/cache-collector/.clj-kondo/config.edn create mode 100644 modules/cache-collector/Makefile create mode 100644 modules/cache-collector/deps.edn rename modules/{db/src/blaze/db => cache-collector/src/blaze}/cache_collector.clj (75%) rename modules/{db/src/blaze/db => cache-collector/src/blaze}/cache_collector/protocols.clj (61%) create mode 100644 modules/cache-collector/src/blaze/cache_collector/spec.clj create mode 100644 modules/cache-collector/test/blaze/cache_collector_test.clj create mode 100644 modules/cache-collector/tests.edn delete mode 100644 modules/db/src/blaze/db/cache_collector/spec.clj delete mode 100644 modules/db/test/blaze/db/cache_collector_test.clj diff --git a/dev/blaze/dev.clj b/dev/blaze/dev.clj index b20dd3bf2..71136521f 100644 --- a/dev/blaze/dev.clj +++ b/dev/blaze/dev.clj @@ -3,7 +3,7 @@ [blaze.byte-string :as bs] [blaze.db.api :as d] [blaze.db.api-spec] - [blaze.db.cache-collector.protocols :as ccp] + [blaze.cache-collector.protocols :as ccp] [blaze.db.resource-cache :as resource-cache] [blaze.db.resource-store :as rs] [blaze.db.tx-log :as tx-log] diff --git a/modules/cache-collector/.clj-kondo/config.edn b/modules/cache-collector/.clj-kondo/config.edn new file mode 100644 index 000000000..035b03646 --- /dev/null +++ b/modules/cache-collector/.clj-kondo/config.edn @@ -0,0 +1,3 @@ +{:config-paths + ["../../../.clj-kondo/root" + "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"]} diff --git a/modules/cache-collector/Makefile b/modules/cache-collector/Makefile new file mode 100644 index 000000000..2fff67259 --- /dev/null +++ b/modules/cache-collector/Makefile @@ -0,0 +1,25 @@ +fmt: + cljfmt check + +lint: + clj-kondo --lint src test deps.edn + +prep: + clojure -X:deps prep + +test: prep + clojure -M:test:kaocha --profile :ci + +test-coverage: prep + clojure -M:test:coverage + +deps-tree: + clojure -X:deps tree + +deps-list: + clojure -X:deps list + +clean: + rm -rf .clj-kondo/.cache .cpcache target + +.PHONY: fmt lint prep test test-coverage deps-tree deps-list clean diff --git a/modules/cache-collector/deps.edn b/modules/cache-collector/deps.edn new file mode 100644 index 000000000..cdb76985b --- /dev/null +++ b/modules/cache-collector/deps.edn @@ -0,0 +1,45 @@ +{:deps + {blaze/metrics + {:local/root "../metrics"} + + blaze/module-base + {:local/root "../module-base"} + + com.github.ben-manes.caffeine/caffeine + {:mvn/version "3.1.8"}} + + :aliases + {:test + {:extra-paths ["test"] + + :extra-deps + {blaze/module-test-util + {:local/root "../module-test-util"}}} + + :kaocha + {:extra-deps + {lambdaisland/kaocha + {:mvn/version "1.85.1342"}} + + :main-opts ["-m" "kaocha.runner"]} + + :test-perf + {:extra-paths ["test-perf"] + + :extra-deps + {blaze/fhir-test-util + {:local/root "../fhir-test-util"} + + criterium/criterium + {:mvn/version "0.4.6"} + + org.openjdk.jol/jol-core + {:mvn/version "0.17"}}} + + :coverage + {:extra-deps + {cloverage/cloverage + {:mvn/version "1.2.4"}} + + :main-opts ["-m" "cloverage.coverage" "--codecov" "-p" "src" "-s" "test" + "-e" ".+spec"]}}} diff --git a/modules/db/src/blaze/db/cache_collector.clj b/modules/cache-collector/src/blaze/cache_collector.clj similarity index 75% rename from modules/db/src/blaze/db/cache_collector.clj rename to modules/cache-collector/src/blaze/cache_collector.clj index 4db50791f..d78f1f5d9 100644 --- a/modules/db/src/blaze/db/cache_collector.clj +++ b/modules/cache-collector/src/blaze/cache_collector.clj @@ -1,13 +1,13 @@ -(ns blaze.db.cache-collector +(ns blaze.cache-collector (:require - [blaze.db.cache-collector.protocols :as p] - [blaze.db.cache-collector.spec] + [blaze.cache-collector.protocols :as p] + [blaze.cache-collector.spec] [blaze.metrics.core :as metrics] [blaze.module :as m] [clojure.spec.alpha :as s] [integrant.core :as ig]) (:import - [com.github.benmanes.caffeine.cache Cache] + [com.github.benmanes.caffeine.cache AsyncCache Cache] [com.github.benmanes.caffeine.cache.stats CacheStats])) (set! *warn-on-reflection* true) @@ -17,7 +17,12 @@ (-stats [cache] (.stats cache)) (-estimated-size [cache] - (.estimatedSize cache))) + (.estimatedSize cache)) + AsyncCache + (-stats [cache] + (.stats (.synchronous cache))) + (-estimated-size [cache] + (.estimatedSize (.synchronous cache)))) (defn- sample-xf [f] (map (fn [[name stats estimated-size]] {:label-values [name] :value (f stats estimated-size)}))) @@ -36,47 +41,47 @@ (when cache [name (p/-stats cache) (p/-estimated-size cache)])))) -(defmethod m/pre-init-spec :blaze.db/cache-collector [_] +(defmethod m/pre-init-spec :blaze/cache-collector [_] (s/keys :req-un [::caches])) -(defmethod ig/init-key :blaze.db/cache-collector +(defmethod ig/init-key :blaze/cache-collector [_ {:keys [caches]}] (metrics/collector (let [stats (into [] mapper caches)] [(counter-metric - "blaze_db_cache_hits_total" + "blaze_cache_hits_total" "Returns the number of times Cache lookup methods have returned a cached value." (fn [stats _] (.hitCount ^CacheStats stats)) stats) (counter-metric - "blaze_db_cache_misses_total" + "blaze_cache_misses_total" "Returns the number of times Cache lookup methods have returned an uncached (newly loaded) value, or null." (fn [stats _] (.missCount ^CacheStats stats)) stats) (counter-metric - "blaze_db_cache_load_successes_total" + "blaze_cache_load_successes_total" "Returns the number of times Cache lookup methods have successfully loaded a new value." (fn [stats _] (.loadSuccessCount ^CacheStats stats)) stats) (counter-metric - "blaze_db_cache_load_failures_total" + "blaze_cache_load_failures_total" "Returns the number of times Cache lookup methods failed to load a new value, either because no value was found or an exception was thrown while loading." (fn [stats _] (.loadFailureCount ^CacheStats stats)) stats) (counter-metric - "blaze_db_cache_load_seconds_total" + "blaze_cache_load_seconds_total" "Returns the total number of seconds the cache has spent loading new values." (fn [stats _] (/ (double (.totalLoadTime ^CacheStats stats)) 1e9)) stats) (counter-metric - "blaze_db_cache_evictions_total" + "blaze_cache_evictions_total" "Returns the number of times an entry has been evicted." (fn [stats _] (.evictionCount ^CacheStats stats)) stats) (gauge-metric - "blaze_db_cache_estimated_size" + "blaze_cache_estimated_size" "Returns the approximate number of entries in this cache." (fn [_ estimated-size] estimated-size) stats)]))) -(derive :blaze.db/cache-collector :blaze.metrics/collector) +(derive :blaze/cache-collector :blaze.metrics/collector) diff --git a/modules/db/src/blaze/db/cache_collector/protocols.clj b/modules/cache-collector/src/blaze/cache_collector/protocols.clj similarity index 61% rename from modules/db/src/blaze/db/cache_collector/protocols.clj rename to modules/cache-collector/src/blaze/cache_collector/protocols.clj index d2098c858..8b813df4b 100644 --- a/modules/db/src/blaze/db/cache_collector/protocols.clj +++ b/modules/cache-collector/src/blaze/cache_collector/protocols.clj @@ -1,4 +1,4 @@ -(ns blaze.db.cache-collector.protocols) +(ns blaze.cache-collector.protocols) (defprotocol StatsCache (-stats [_]) diff --git a/modules/cache-collector/src/blaze/cache_collector/spec.clj b/modules/cache-collector/src/blaze/cache_collector/spec.clj new file mode 100644 index 000000000..1eaceda59 --- /dev/null +++ b/modules/cache-collector/src/blaze/cache_collector/spec.clj @@ -0,0 +1,7 @@ +(ns blaze.cache-collector.spec + (:require + [blaze.cache-collector.protocols :as p] + [clojure.spec.alpha :as s])) + +(s/def :blaze.cache-collector/caches + (s/map-of string? (s/nilable #(satisfies? p/StatsCache %)))) diff --git a/modules/cache-collector/test/blaze/cache_collector_test.clj b/modules/cache-collector/test/blaze/cache_collector_test.clj new file mode 100644 index 000000000..fbc255a5b --- /dev/null +++ b/modules/cache-collector/test/blaze/cache_collector_test.clj @@ -0,0 +1,142 @@ +(ns blaze.cache-collector-test + (:require + [blaze.cache-collector] + [blaze.metrics.core :as metrics] + [blaze.module.test-util :refer [with-system]] + [blaze.test-util :as tu :refer [given-thrown]] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest testing]] + [integrant.core :as ig] + [juxt.iota :refer [given]]) + (:import + [com.github.benmanes.caffeine.cache AsyncCache Cache Caffeine] + [java.util.function Function])) + +(set! *warn-on-reflection* true) +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(def ^Cache cache (-> (Caffeine/newBuilder) (.recordStats) (.build))) +(def ^AsyncCache async-cache (-> (Caffeine/newBuilder) (.recordStats) (.buildAsync))) + +(def config + {:blaze/cache-collector + {:caches + {"name-135224" cache + "name-145135" async-cache + "name-093214" nil}}}) + +(deftest init-test + (testing "nil config" + (given-thrown (ig/init {:blaze/cache-collector nil}) + :key := :blaze/cache-collector + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map?)) + + (testing "missing config" + (given-thrown (ig/init {:blaze/cache-collector {}}) + :key := :blaze/cache-collector + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :caches)))) + + (testing "invalid caches" + (given-thrown (ig/init {:blaze/cache-collector {:caches ::invalid}}) + :key := :blaze/cache-collector + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map? + [:cause-data ::s/problems 0 :val] := ::invalid))) + +(deftest cache-collector-test + (with-system [{collector :blaze/cache-collector} config] + + (testing "all zero on fresh cache" + (given (metrics/collect collector) + [0 :name] := "blaze_cache_hits" + [0 :type] := :counter + [0 :samples count] := 2 + [0 :samples 0 :value] := 0.0 + [0 :samples 0 :label-values] := ["name-135224"] + [0 :samples 1 :value] := 0.0 + [0 :samples 1 :label-values] := ["name-145135"] + [1 :name] := "blaze_cache_misses" + [1 :type] := :counter + [1 :samples count] := 2 + [1 :samples 0 :value] := 0.0 + [1 :samples 1 :value] := 0.0 + [2 :name] := "blaze_cache_load_successes" + [2 :type] := :counter + [2 :samples count] := 2 + [2 :samples 0 :value] := 0.0 + [2 :samples 1 :value] := 0.0 + [3 :name] := "blaze_cache_load_failures" + [3 :type] := :counter + [3 :samples count] := 2 + [3 :samples 0 :value] := 0.0 + [3 :samples 1 :value] := 0.0 + [4 :name] := "blaze_cache_load_seconds" + [4 :type] := :counter + [4 :samples count] := 2 + [4 :samples 0 :value] := 0.0 + [4 :samples 1 :value] := 0.0 + [5 :name] := "blaze_cache_evictions" + [5 :type] := :counter + [5 :samples count] := 2 + [5 :samples 0 :value] := 0.0 + [5 :samples 1 :value] := 0.0 + [6 :name] := "blaze_cache_estimated_size" + [6 :type] := :gauge + [6 :samples count] := 2 + [6 :samples 0 :value] := 0.0 + [6 :samples 1 :value] := 0.0)) + + (testing "one load" + (.get cache "1" (reify Function (apply [_ key] key))) + (.get async-cache "1" (reify Function (apply [_ key] key))) + (Thread/sleep 100) + + (given (metrics/collect collector) + [0 :name] := "blaze_cache_hits" + [0 :samples 0 :value] := 0.0 + [0 :samples 1 :value] := 0.0 + [1 :name] := "blaze_cache_misses" + [1 :samples 0 :value] := 1.0 + [1 :samples 1 :value] := 1.0 + [2 :name] := "blaze_cache_load_successes" + [2 :samples 0 :value] := 1.0 + [2 :samples 1 :value] := 1.0 + [3 :name] := "blaze_cache_load_failures" + [3 :samples 0 :value] := 0.0 + [3 :samples 1 :value] := 0.0 + [5 :name] := "blaze_cache_evictions" + [5 :samples 0 :value] := 0.0 + [5 :samples 1 :value] := 0.0 + [6 :name] := "blaze_cache_estimated_size" + [6 :samples 0 :value] := 1.0 + [6 :samples 1 :value] := 1.0)) + + (testing "one loads and one hit" + (.get cache "1" (reify Function (apply [_ key] key))) + (.get async-cache "1" (reify Function (apply [_ key] key))) + (Thread/sleep 100) + + (given (metrics/collect collector) + [0 :name] := "blaze_cache_hits" + [0 :samples 0 :value] := 1.0 + [0 :samples 1 :value] := 1.0 + [1 :name] := "blaze_cache_misses" + [1 :samples 0 :value] := 1.0 + [1 :samples 1 :value] := 1.0 + [2 :name] := "blaze_cache_load_successes" + [2 :samples 0 :value] := 1.0 + [2 :samples 1 :value] := 1.0 + [3 :name] := "blaze_cache_load_failures" + [3 :samples 0 :value] := 0.0 + [3 :samples 1 :value] := 0.0 + [5 :name] := "blaze_cache_evictions" + [5 :samples 0 :value] := 0.0 + [5 :samples 1 :value] := 0.0 + [6 :name] := "blaze_cache_estimated_size" + [6 :samples 0 :value] := 1.0 + [6 :samples 1 :value] := 1.0)))) diff --git a/modules/cache-collector/tests.edn b/modules/cache-collector/tests.edn new file mode 100644 index 000000000..94fe5636c --- /dev/null +++ b/modules/cache-collector/tests.edn @@ -0,0 +1,5 @@ +#kaocha/v1 + #merge + [{} + #profile {:ci {:reporter kaocha.report/documentation + :color? false}}] diff --git a/modules/db/deps.edn b/modules/db/deps.edn index 53b6700e5..8cc5cfb6b 100644 --- a/modules/db/deps.edn +++ b/modules/db/deps.edn @@ -10,6 +10,9 @@ blaze/byte-string {:local/root "../byte-string"} + blaze/cache-collector + {:local/root "../cache-collector"} + blaze/coll {:local/root "../coll"} diff --git a/modules/db/src/blaze/db/cache_collector/spec.clj b/modules/db/src/blaze/db/cache_collector/spec.clj deleted file mode 100644 index fec1f8f2c..000000000 --- a/modules/db/src/blaze/db/cache_collector/spec.clj +++ /dev/null @@ -1,7 +0,0 @@ -(ns blaze.db.cache-collector.spec - (:require - [blaze.db.cache-collector.protocols :as p] - [clojure.spec.alpha :as s])) - -(s/def :blaze.db.cache-collector/caches - (s/map-of string? (s/nilable #(satisfies? p/StatsCache %)))) diff --git a/modules/db/src/blaze/db/resource_cache.clj b/modules/db/src/blaze/db/resource_cache.clj index 9453e7c30..9e090695c 100644 --- a/modules/db/src/blaze/db/resource_cache.clj +++ b/modules/db/src/blaze/db/resource_cache.clj @@ -4,7 +4,7 @@ Caffeine is used because it have better performance characteristics as a ConcurrentHashMap." (:require - [blaze.db.cache-collector.protocols :as ccp] + [blaze.cache-collector.protocols :as ccp] [blaze.db.resource-cache.spec] [blaze.db.resource-store :as rs] [blaze.db.resource-store.spec] diff --git a/modules/db/test/blaze/db/cache_collector_test.clj b/modules/db/test/blaze/db/cache_collector_test.clj deleted file mode 100644 index 3f1adde1f..000000000 --- a/modules/db/test/blaze/db/cache_collector_test.clj +++ /dev/null @@ -1,108 +0,0 @@ -(ns blaze.db.cache-collector-test - (:require - [blaze.db.cache-collector] - [blaze.metrics.core :as metrics] - [blaze.module.test-util :refer [with-system]] - [blaze.test-util :as tu :refer [given-thrown]] - [clojure.spec.alpha :as s] - [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [deftest testing]] - [integrant.core :as ig] - [juxt.iota :refer [given]]) - (:import - [com.github.benmanes.caffeine.cache Cache Caffeine] - [java.util.function Function])) - -(set! *warn-on-reflection* true) -(st/instrument) - -(test/use-fixtures :each tu/fixture) - -(def ^Cache cache (-> (Caffeine/newBuilder) (.recordStats) (.build))) - -(def config - {:blaze.db/cache-collector - {:caches {"name-135224" cache "name-093214" nil}}}) - -(deftest init-test - (testing "nil config" - (given-thrown (ig/init {:blaze.db/cache-collector nil}) - :key := :blaze.db/cache-collector - :reason := ::ig/build-failed-spec - [:cause-data ::s/problems 0 :pred] := `map?)) - - (testing "missing config" - (given-thrown (ig/init {:blaze.db/cache-collector {}}) - :key := :blaze.db/cache-collector - :reason := ::ig/build-failed-spec - [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :caches)))) - - (testing "invalid caches" - (given-thrown (ig/init {:blaze.db/cache-collector {:caches ::invalid}}) - :key := :blaze.db/cache-collector - :reason := ::ig/build-failed-spec - [:cause-data ::s/problems 0 :pred] := `map? - [:cause-data ::s/problems 0 :val] := ::invalid))) - -(deftest cache-collector-test - (with-system [{collector :blaze.db/cache-collector} config] - - (testing "all zero on fresh cache" - (given (metrics/collect collector) - [0 :name] := "blaze_db_cache_hits" - [0 :type] := :counter - [0 :samples 0 :value] := 0.0 - [1 :name] := "blaze_db_cache_misses" - [1 :type] := :counter - [1 :samples 0 :value] := 0.0 - [2 :name] := "blaze_db_cache_load_successes" - [2 :type] := :counter - [2 :samples 0 :value] := 0.0 - [3 :name] := "blaze_db_cache_load_failures" - [3 :type] := :counter - [3 :samples 0 :value] := 0.0 - [4 :name] := "blaze_db_cache_load_seconds" - [4 :type] := :counter - [4 :samples 0 :value] := 0.0 - [5 :name] := "blaze_db_cache_evictions" - [5 :type] := :counter - [5 :samples 0 :value] := 0.0 - [6 :name] := "blaze_db_cache_estimated_size" - [6 :type] := :gauge - [6 :samples 0 :value] := 0.0)) - - (testing "one load" - (.get cache 1 (reify Function (apply [_ key] key))) - (Thread/sleep 100) - - (given (metrics/collect collector) - [0 :name] := "blaze_db_cache_hits" - [0 :samples 0 :value] := 0.0 - [1 :name] := "blaze_db_cache_misses" - [1 :samples 0 :value] := 1.0 - [2 :name] := "blaze_db_cache_load_successes" - [2 :samples 0 :value] := 1.0 - [3 :name] := "blaze_db_cache_load_failures" - [3 :samples 0 :value] := 0.0 - [5 :name] := "blaze_db_cache_evictions" - [5 :samples 0 :value] := 0.0 - [6 :name] := "blaze_db_cache_estimated_size" - [6 :samples 0 :value] := 1.0)) - - (testing "one loads and one hit" - (.get cache 1 (reify Function (apply [_ key] key))) - (Thread/sleep 100) - - (given (metrics/collect collector) - [0 :name] := "blaze_db_cache_hits" - [0 :samples 0 :value] := 1.0 - [1 :name] := "blaze_db_cache_misses" - [1 :samples 0 :value] := 1.0 - [2 :name] := "blaze_db_cache_load_successes" - [2 :samples 0 :value] := 1.0 - [3 :name] := "blaze_db_cache_load_failures" - [3 :samples 0 :value] := 0.0 - [5 :name] := "blaze_db_cache_evictions" - [5 :samples 0 :value] := 0.0 - [6 :name] := "blaze_db_cache_estimated_size" - [6 :samples 0 :value] := 1.0)))) diff --git a/modules/db/test/blaze/db/resource_cache_test.clj b/modules/db/test/blaze/db/resource_cache_test.clj index 598b4e040..acfd6e689 100644 --- a/modules/db/test/blaze/db/resource_cache_test.clj +++ b/modules/db/test/blaze/db/resource_cache_test.clj @@ -1,6 +1,6 @@ (ns blaze.db.resource-cache-test (:require - [blaze.db.cache-collector.protocols :as ccp] + [blaze.cache-collector.protocols :as ccp] [blaze.db.kv :as kv] [blaze.db.kv.mem] [blaze.db.resource-cache :as resource-cache] @@ -23,7 +23,7 @@ (set! *warn-on-reflection* true) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) diff --git a/profiling/blaze/profiling.clj b/profiling/blaze/profiling.clj index bb48315e4..e06c5dec5 100644 --- a/profiling/blaze/profiling.clj +++ b/profiling/blaze/profiling.clj @@ -2,7 +2,7 @@ "Profiling namespace without test dependencies." (:require [blaze.system :as system] - [blaze.db.cache-collector :as cc] + [blaze.cache-collector :as cc] [blaze.db.kv.rocksdb :as rocksdb] [blaze.db.resource-cache :as resource-cache] [clojure.tools.namespace.repl :refer [refresh]] diff --git a/resources/blaze.edn b/resources/blaze.edn index 2573aa5cf..b65ae6282 100644 --- a/resources/blaze.edn +++ b/resources/blaze.edn @@ -327,7 +327,7 @@ :blaze.db.node.tx-indexer/duration-seconds {} - :blaze.db/cache-collector + :blaze/cache-collector {:caches {"tx-cache" #blaze/ref :blaze.db.main/tx-cache "resource-cache" #blaze/ref :blaze.db/resource-cache}} From 484cdd449633030e1d40f7b218142a18be76208e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:10:11 +0000 Subject: [PATCH 14/24] chore(deps): update softprops/action-gh-release digest to a74c6b7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7c3f0523..cc4e85898 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1767,7 +1767,7 @@ jobs: steps: - name: Release - uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2 + uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2 with: files: target/blaze-*-standalone.jar From 855bb10403ec6f06bc38ac986e4161325e7827a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:14:10 +0000 Subject: [PATCH 15/24] chore(deps): update dependency @types/node to v20.14.8 --- modules/frontend-e2e/package-lock.json | 6 +++--- modules/frontend/package-lock.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/frontend-e2e/package-lock.json b/modules/frontend-e2e/package-lock.json index 9eb8dacb1..f5223ea28 100644 --- a/modules/frontend-e2e/package-lock.json +++ b/modules/frontend-e2e/package-lock.json @@ -30,9 +30,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "20.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", + "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 5fa7363bb..9530aab02 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1179,9 +1179,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "20.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", + "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", "devOptional": true, "license": "MIT", "dependencies": { From e50ac3ec4982cdc9f490857db955b3854ef6b347 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 07:16:03 +0000 Subject: [PATCH 16/24] chore(deps): update dependency lambdaisland/kaocha to v1.91.1392 --- modules/cache-collector/deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cache-collector/deps.edn b/modules/cache-collector/deps.edn index cdb76985b..9d0c7f829 100644 --- a/modules/cache-collector/deps.edn +++ b/modules/cache-collector/deps.edn @@ -19,7 +19,7 @@ :kaocha {:extra-deps {lambdaisland/kaocha - {:mvn/version "1.85.1342"}} + {:mvn/version "1.91.1392"}} :main-opts ["-m" "kaocha.runner"]} From 48942a83fbc96bbccbf0f89cf3e36753f85f9e86 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 00:43:19 +0000 Subject: [PATCH 17/24] chore(deps): update dependency com.google.protobuf/protobuf-java to v4.27.2 --- modules/byte-buffer/deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/byte-buffer/deps.edn b/modules/byte-buffer/deps.edn index 9792f91bf..54828cb07 100644 --- a/modules/byte-buffer/deps.edn +++ b/modules/byte-buffer/deps.edn @@ -1,6 +1,6 @@ {:deps {com.google.protobuf/protobuf-java - {:mvn/version "4.27.1"}} + {:mvn/version "4.27.2"}} :aliases {:test From fa18fd852fabbf1c45e769744661f266dcb275d6 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Wed, 12 Jun 2024 18:02:06 +0200 Subject: [PATCH 18/24] Cache Results of the Exists CQL Expression Closes: #1051 --- .github/scripts/check-bloom-filter.sh | 10 + ...check-patient-last-change-index-missing.sh | 9 + .../check-patient-last-change-index-state.sh | 9 + .../scripts/test-cql-expr-cache-metrics.sh | 19 + .github/scripts/test-metrics.sh | 5 +- .github/workflows/build.yml | 205 +- Makefile | 2 +- cljfmt.edn | 3 +- dev/blaze/dev.clj | 37 +- docs/api/admin.md | 7 + docs/deployment/environment-variables.md | 3 + docs/implementation/cql.md | 88 + docs/implementation/database.md | 31 +- docs/monitoring/blaze.json | 6323 ++++++++++------- docs/performance/cql.md | 242 +- .../cql/code-value-search-100k-fh.png | Bin 28811 -> 25425 bytes .../cql/code-value-search-100k-fh.txt | 24 +- .../cql/code-value-search-100k.png | Bin 20972 -> 22808 bytes .../cql/code-value-search-100k.txt | 24 +- docs/performance/cql/code-value-search-1M.png | Bin 22291 -> 24407 bytes docs/performance/cql/code-value-search-1M.txt | 24 +- docs/performance/cql/condition-450-rare.cql | 459 ++ docs/performance/cql/condition-450-rare.yml | 5 + docs/performance/cql/duration.jq | 2 +- docs/performance/cql/other.md | 39 - docs/performance/cql/result.jq | 2 +- docs/performance/cql/search-all.sh | 16 +- docs/performance/cql/search.sh | 2 +- .../cql/simple-code-search-100k-fh.gnuplot | 2 +- .../cql/simple-code-search-100k-fh.png | Bin 24400 -> 19720 bytes .../cql/simple-code-search-100k-fh.txt | 24 +- .../cql/simple-code-search-100k.gnuplot | 2 +- .../cql/simple-code-search-100k.png | Bin 24100 -> 19266 bytes .../cql/simple-code-search-100k.txt | 24 +- .../cql/simple-code-search-1M.gnuplot | 2 +- .../performance/cql/simple-code-search-1M.png | Bin 19858 -> 20177 bytes .../performance/cql/simple-code-search-1M.txt | 24 +- .../cql/ten-code-search-100k-fh.png | Bin 19219 -> 23272 bytes .../cql/ten-code-search-100k-fh.txt | 16 +- .../cql/ten-code-search-100k.gnuplot | 2 +- docs/performance/cql/ten-code-search-100k.png | Bin 23620 -> 23572 bytes docs/performance/cql/ten-code-search-100k.txt | 16 +- .../cql/ten-code-search-1M.gnuplot | 2 +- docs/performance/cql/ten-code-search-1M.png | Bin 19850 -> 17942 bytes docs/performance/cql/ten-code-search-1M.txt | 16 +- modules/admin-api/deps.edn | 3 + modules/admin-api/src/blaze/admin_api.clj | 85 +- .../admin-api/test/blaze/admin_api_test.clj | 87 +- modules/coll/deps.edn | 4 +- .../clj-kondo.exports/blaze/coll/config.edn | 2 + modules/coll/src/blaze/coll/core.clj | 11 + modules/cql/.clj-kondo/config.edn | 8 +- modules/cql/deps.edn | 2 +- modules/cql/src/blaze/cql.clj | 7 + modules/cql/src/blaze/elm/code.clj | 8 + modules/cql/src/blaze/elm/compiler.clj | 20 + .../elm/compiler/arithmetic_operators.clj | 30 +- .../blaze/elm/compiler/clinical_operators.clj | 35 +- .../elm/compiler/conditional_operators.clj | 78 +- modules/cql/src/blaze/elm/compiler/core.clj | 110 + .../elm/compiler/date_time_operators.clj | 544 +- .../src/blaze/elm/compiler/external_data.clj | 139 +- .../cql/src/blaze/elm/compiler/function.clj | 17 +- .../blaze/elm/compiler/interval_operators.clj | 77 +- .../cql/src/blaze/elm/compiler/library.clj | 24 +- .../elm/compiler/library/resolve_refs.clj | 52 + .../src/blaze/elm/compiler/library/spec.clj | 7 +- .../src/blaze/elm/compiler/library_spec.clj | 16 +- .../src/blaze/elm/compiler/list_operators.clj | 250 +- .../blaze/elm/compiler/logical_operators.clj | 266 +- .../elm/compiler/logical_operators/util.clj | 55 + modules/cql/src/blaze/elm/compiler/macros.clj | 523 +- .../elm/compiler/nullological_operators.clj | 73 +- .../cql/src/blaze/elm/compiler/parameters.clj | 30 +- .../cql/src/blaze/elm/compiler/queries.clj | 119 +- .../src/blaze/elm/compiler/reusing_logic.clj | 175 +- modules/cql/src/blaze/elm/compiler/spec.clj | 11 +- .../blaze/elm/compiler/string_operators.clj | 233 +- .../blaze/elm/compiler/structured_values.clj | 121 +- .../src/blaze/elm/compiler/type_operators.clj | 214 +- modules/cql/src/blaze/elm/compiler_spec.clj | 24 +- modules/cql/src/blaze/elm/date_time.clj | 4 +- modules/cql/src/blaze/elm/decimal.clj | 8 +- modules/cql/src/blaze/elm/expression.clj | 2 +- .../cql/src/blaze/elm/expression/cache.clj | 252 + .../elm/expression/cache/bloom_filter.clj | 92 + .../expression/cache/bloom_filter/spec.clj | 29 + .../expression/cache/bloom_filter_spec.clj | 26 + .../src/blaze/elm/expression/cache/codec.clj | 80 + .../blaze/elm/expression/cache/codec/by_t.clj | 47 + .../blaze/elm/expression/cache/codec/form.clj | 19 + .../blaze/elm/expression/cache/protocols.clj | 8 + .../src/blaze/elm/expression/cache/spec.clj | 20 + .../src/blaze/elm/expression/cache_spec.clj | 33 + modules/cql/src/blaze/elm/expression/spec.clj | 4 + modules/cql/src/blaze/elm/expression_spec.clj | 5 +- modules/cql/src/blaze/elm/interval.clj | 19 +- modules/cql/src/blaze/elm/quantity.clj | 10 +- modules/cql/src/blaze/elm/ratio.clj | 19 +- modules/cql/src/blaze/elm/resource.clj | 64 + ...ternal_data_spec.clj => resource_spec.clj} | 12 +- modules/cql/test/blaze/elm/code_test.clj | 16 + .../elm/compiler/aggregate_operators_test.clj | 60 +- .../compiler/arithmetic_operators_test.clj | 142 +- .../elm/compiler/clinical_operators_test.clj | 36 +- .../elm/compiler/clinical_values_test.clj | 39 +- .../compiler/comparison_operators_test.clj | 52 +- .../compiler/conditional_operators_test.clj | 164 +- .../elm/compiler/date_time_operators_test.clj | 287 +- .../blaze/elm/compiler/external_data_test.clj | 167 +- .../elm/compiler/interval_operators_test.clj | 162 +- .../compiler/library/resolve_refs_test.clj | 27 + .../test/blaze/elm/compiler/library_test.clj | 466 +- .../elm/compiler/list_operators_test.clj | 481 +- .../compiler/logical_operators/util_test.clj | 132 + .../elm/compiler/logical_operators_test.clj | 368 +- .../compiler/nullological_operators_test.clj | 17 +- .../blaze/elm/compiler/parameters_test.clj | 5 +- .../test/blaze/elm/compiler/queries_test.clj | 10 +- .../blaze/elm/compiler/reusing_logic_test.clj | 183 +- .../elm/compiler/string_operators_test.clj | 117 +- .../elm/compiler/structured_values_test.clj | 301 +- .../cql/test/blaze/elm/compiler/test_util.clj | 473 +- .../elm/compiler/type_operators_test.clj | 244 +- .../expression/cache/bloom_filter_test.clj | 120 + .../elm/expression/cache/codec/by_t_spec.clj | 15 + .../elm/expression/cache/codec/form_spec.clj | 10 + .../blaze/elm/expression/cache/codec_spec.clj | 15 + .../test/blaze/elm/expression/cache_test.clj | 322 + modules/cql/test/blaze/elm/literal.clj | 19 + modules/cql/test/blaze/elm/quantity_test.clj | 24 +- modules/cql/test/blaze/elm/ratio_spec.clj | 9 +- modules/cql/test/blaze/elm/ratio_test.clj | 18 + modules/cql/test/blaze/elm/resource_test.clj | 30 + modules/cql/test/data_readers.clj | 5 + .../src/blaze/db/impl/protocols.clj | 4 +- modules/db-stub/src/blaze/db/api_stub.clj | 10 +- modules/db/.clj-kondo/config.edn | 2 +- modules/db/NOTES.md | 125 - modules/db/deps.edn | 3 + modules/db/src/blaze/db/api.clj | 8 + modules/db/src/blaze/db/api_spec.clj | 4 + modules/db/src/blaze/db/impl/batch_db.clj | 7 + modules/db/src/blaze/db/impl/db.clj | 21 +- .../db/impl/index/patient_last_change.clj | 64 + modules/db/src/blaze/db/node.clj | 75 +- .../db/node/patient_last_change_index.clj | 13 + modules/db/src/blaze/db/node/tx_indexer.clj | 4 +- .../src/blaze/db/node/tx_indexer/verify.clj | 64 +- .../db/src/blaze/db/search_param_registry.clj | 41 +- .../blaze/db/search_param_registry_spec.clj | 6 +- .../db/test-perf/blaze/db/api_test_perf.clj | 1 + modules/db/test/blaze/db/api_test.clj | 43 + .../db/test/blaze/db/impl/batch_db_spec.clj | 1 + modules/db/test/blaze/db/impl/db_spec.clj | 1 + .../impl/index/patient_last_change/spec.clj | 12 + .../impl/index/patient_last_change_spec.clj | 27 + .../index/patient_last_change_test_util.clj | 13 + .../blaze/db/impl/index/rts_as_of_spec.clj | 2 +- .../test/blaze/db/impl/search_param_spec.clj | 2 +- .../node/patient_last_change_index_spec.clj | 14 + .../node/patient_last_change_index_test.clj | 42 + .../blaze/db/node/tx_indexer/verify_spec.clj | 6 +- .../blaze/db/node/tx_indexer/verify_test.clj | 193 +- .../db/test/blaze/db/node/tx_indexer_spec.clj | 5 +- modules/db/test/blaze/db/node_test.clj | 14 +- .../blaze/db/search_param_registry_test.clj | 21 +- modules/db/test/blaze/db/test_util.clj | 6 +- .../src/blaze/fhir/spec/spec.clj | 3 + modules/frontend/package-lock.json | 14 +- .../src/lib/resource/json/array.svelte | 4 +- .../src/routes/__admin/+layout.svelte | 9 + .../routes/__admin/{+page.ts => +layout.ts} | 5 +- .../frontend/src/routes/__admin/+page.svelte | 2 +- .../src/routes/__admin/cql/+page.svelte | 45 + .../frontend/src/routes/__admin/cql/+page.ts | 29 + .../__admin/cql/bloom-filter-row.svelte | 21 + .../__admin/cql/bloom-filters/+server.ts | 9 + .../src/routes/__admin/feature-row.svelte | 14 +- .../test/blaze/job/async_interaction_test.clj | 4 + .../test/blaze/job/re_index_test.clj | 4 + .../test/blaze/job_scheduler_test.clj | 4 + modules/module-base/src/blaze/util.clj | 6 +- modules/module-base/test/blaze/util_test.clj | 4 +- .../blaze/fhir/operation/evaluate_measure.clj | 3 + .../fhir/operation/evaluate_measure/cql.clj | 4 +- .../operation/evaluate_measure/measure.clj | 51 +- .../evaluate_measure/measure/spec.clj | 6 +- .../evaluate_measure/measure/util.clj | 23 + .../operation/evaluate_measure/cql/spec.clj | 6 +- .../operation/evaluate_measure/cql_spec.clj | 7 +- .../operation/evaluate_measure/cql_test.clj | 13 +- .../evaluate_measure/measure/util_spec.clj | 4 + .../evaluate_measure/measure/util_test.clj | 42 +- .../evaluate_measure/measure_spec.clj | 3 +- .../evaluate_measure/measure_test.clj | 192 +- .../q50-specimen-condition-reference.cql | 2 +- .../q57-mii-specimen-reference.cql | 40 + .../q57-mii-specimen-reference.json | 139 + .../q58-overly-large-nonparsable-query.cql | 509 ++ .../q58-overly-large-nonparsable-query.json | 57 + .../operation/evaluate_measure/test_util.clj | 8 +- .../fhir/operation/evaluate_measure_test.clj | 5 + .../rest-util/src/blaze/handler/fhir/util.clj | 4 +- .../src/blaze/db/kv/rocksdb/metrics.clj | 4 +- modules/scheduler/src/blaze/scheduler.clj | 10 + .../scheduler/src/blaze/scheduler_spec.clj | 4 + .../thread_pool_executor_collector_test.clj | 4 +- profiling/blaze/profiling.clj | 15 +- resources/blaze.edn | 147 +- src/blaze/system.clj | 7 +- 211 files changed, 13739 insertions(+), 5070 deletions(-) create mode 100755 .github/scripts/check-bloom-filter.sh create mode 100755 .github/scripts/check-patient-last-change-index-missing.sh create mode 100755 .github/scripts/check-patient-last-change-index-state.sh create mode 100755 .github/scripts/test-cql-expr-cache-metrics.sh create mode 100644 docs/api/admin.md create mode 100644 docs/implementation/cql.md create mode 100644 docs/performance/cql/condition-450-rare.cql create mode 100644 docs/performance/cql/condition-450-rare.yml create mode 100644 modules/coll/resources/clj-kondo.exports/blaze/coll/config.edn create mode 100644 modules/cql/src/blaze/cql.clj create mode 100644 modules/cql/src/blaze/elm/compiler/library/resolve_refs.clj create mode 100644 modules/cql/src/blaze/elm/compiler/logical_operators/util.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/bloom_filter.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/bloom_filter/spec.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/bloom_filter_spec.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/codec.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/codec/by_t.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/codec/form.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/protocols.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache/spec.clj create mode 100644 modules/cql/src/blaze/elm/expression/cache_spec.clj create mode 100644 modules/cql/src/blaze/elm/resource.clj rename modules/cql/src/blaze/elm/{compiler/external_data_spec.clj => resource_spec.clj} (53%) create mode 100644 modules/cql/test/blaze/elm/code_test.clj create mode 100644 modules/cql/test/blaze/elm/compiler/library/resolve_refs_test.clj create mode 100644 modules/cql/test/blaze/elm/compiler/logical_operators/util_test.clj create mode 100644 modules/cql/test/blaze/elm/expression/cache/bloom_filter_test.clj create mode 100644 modules/cql/test/blaze/elm/expression/cache/codec/by_t_spec.clj create mode 100644 modules/cql/test/blaze/elm/expression/cache/codec/form_spec.clj create mode 100644 modules/cql/test/blaze/elm/expression/cache/codec_spec.clj create mode 100644 modules/cql/test/blaze/elm/expression/cache_test.clj create mode 100644 modules/cql/test/blaze/elm/ratio_test.clj create mode 100644 modules/cql/test/blaze/elm/resource_test.clj delete mode 100644 modules/db/NOTES.md create mode 100644 modules/db/src/blaze/db/impl/index/patient_last_change.clj create mode 100644 modules/db/src/blaze/db/node/patient_last_change_index.clj create mode 100644 modules/db/test/blaze/db/impl/index/patient_last_change/spec.clj create mode 100644 modules/db/test/blaze/db/impl/index/patient_last_change_spec.clj create mode 100644 modules/db/test/blaze/db/impl/index/patient_last_change_test_util.clj create mode 100644 modules/db/test/blaze/db/node/patient_last_change_index_spec.clj create mode 100644 modules/db/test/blaze/db/node/patient_last_change_index_test.clj rename modules/frontend/src/routes/__admin/{+page.ts => +layout.ts} (85%) create mode 100644 modules/frontend/src/routes/__admin/cql/+page.svelte create mode 100644 modules/frontend/src/routes/__admin/cql/+page.ts create mode 100644 modules/frontend/src/routes/__admin/cql/bloom-filter-row.svelte create mode 100644 modules/frontend/src/routes/__admin/cql/bloom-filters/+server.ts create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.json create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.json diff --git a/.github/scripts/check-bloom-filter.sh b/.github/scripts/check-bloom-filter.sh new file mode 100755 index 000000000..1c2426358 --- /dev/null +++ b/.github/scripts/check-bloom-filter.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/util.sh" + +BASE="http://localhost:8080/fhir" +HASH="$1" +PATIENT_COUNT="$2" + +test "patient count" "$(curl -s "$BASE/__admin/cql/bloom-filters" | jq -r ".[] | select(.hash == \"$HASH\") | .patientCount")" "$PATIENT_COUNT" diff --git a/.github/scripts/check-patient-last-change-index-missing.sh b/.github/scripts/check-patient-last-change-index-missing.sh new file mode 100755 index 000000000..7963cd461 --- /dev/null +++ b/.github/scripts/check-patient-last-change-index-missing.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/util.sh" + +BASE="http://localhost:8080/fhir" +curl -s "$BASE/__admin/dbs/index/column-families" | jq -r '.[].name' | grep -q "patient-last-change-index" + +test "exit code" "$?" "1" diff --git a/.github/scripts/check-patient-last-change-index-state.sh b/.github/scripts/check-patient-last-change-index-state.sh new file mode 100755 index 000000000..0bd79ab16 --- /dev/null +++ b/.github/scripts/check-patient-last-change-index-state.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/util.sh" + +BASE="http://localhost:8080/fhir" +STATE="$(curl -s "$BASE/__admin/dbs/index/column-families/patient-last-change-index/state" | jq -r .type)" + +test "state" "$STATE" "$1" diff --git a/.github/scripts/test-cql-expr-cache-metrics.sh b/.github/scripts/test-cql-expr-cache-metrics.sh new file mode 100755 index 000000000..f3f723891 --- /dev/null +++ b/.github/scripts/test-cql-expr-cache-metrics.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/util.sh" + +URL="http://localhost:8081/metrics" + +num-metrics() { + NAME="$1" + FILTER="$2" + curl -s "$URL" | grep "$NAME" | grep -c "$FILTER" +} + +# CQL expression cache is available +test "blaze_cache_estimated_size cql-expr-cache" "$(num-metrics "blaze_cache_estimated_size" "name=\"cql-expr-cache\"")" "1" + +# other caches are still available +test "blaze_cache_estimated_size tx-cache" "$(num-metrics "blaze_cache_estimated_size" "name=\"tx-cache\"")" "1" +test "blaze_cache_estimated_size resource-cache" "$(num-metrics "blaze_cache_estimated_size" "name=\"resource-cache\"")" "1" diff --git a/.github/scripts/test-metrics.sh b/.github/scripts/test-metrics.sh index a739a5c2e..b57741b8a 100755 --- a/.github/scripts/test-metrics.sh +++ b/.github/scripts/test-metrics.sh @@ -15,6 +15,5 @@ test "blaze_rocksdb_block_cache_data_miss index" "$(num-metrics "blaze_rocksdb_b test "blaze_rocksdb_block_cache_data_miss transaction" "$(num-metrics "blaze_rocksdb_block_cache_data_miss" "name=\"transaction\"")" "1" test "blaze_rocksdb_block_cache_data_miss resource" "$(num-metrics "blaze_rocksdb_block_cache_data_miss" "name=\"resource\"")" "1" -test "blaze_rocksdb_table_reader_usage_bytes index" "$(num-metrics "blaze_rocksdb_table_reader_usage_bytes" "name=\"index\"")" "14" -test "blaze_rocksdb_table_reader_usage_bytes transaction" "$(num-metrics "blaze_rocksdb_table_reader_usage_bytes" "name=\"transaction\"")" "1" -test "blaze_rocksdb_table_reader_usage_bytes resource" "$(num-metrics "blaze_rocksdb_table_reader_usage_bytes" "name=\"resource\"")" "1" +test "blaze_cache_estimated_size tx-cache" "$(num-metrics "blaze_cache_estimated_size" "name=\"tx-cache\"")" "1" +test "blaze_cache_estimated_size resource-cache" "$(num-metrics "blaze_cache_estimated_size" "name=\"resource-cache\"")" "1" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc4e85898..534286a8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: - master - develop tags: - - 'v*.*.*' + - 'v*.*.*' pull_request: branches: - master @@ -83,6 +83,7 @@ jobs: - async - byte-buffer - byte-string + - cache-collector - cassandra - coll - cql @@ -333,6 +334,158 @@ jobs: with: sarif_file: trivy-results.sarif + cql-expr-cache-test: + needs: build + runs-on: ubuntu-22.04 + + steps: + - name: Check out Git repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + + - name: Install Blazectl + run: .github/scripts/install-blazectl.sh + + - name: Download Blaze Image + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 + with: + name: blaze-image + path: /tmp + + - name: Load Blaze Image + run: docker load --input /tmp/blaze.tar + + - name: Run Blaze + run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -e CQL_EXPR_CACHE_SIZE=1000 -p 8080:8080 -p 8081:8081 -v blaze-data:/app/data blaze:latest + + - name: Wait for Blaze + run: .github/scripts/wait-for-url.sh http://localhost:8080/health + + - name: Docker Logs + run: docker logs blaze + + - name: Check Capability Statement + run: .github/scripts/check-capability-statement.sh + + - name: Ensure that the State of PatientLastChange Index is Current + run: .github/scripts/check-patient-last-change-index-state.sh current + + - name: Load Data + run: blazectl --no-progress --server http://localhost:8080/fhir upload .github/test-data/synthea + + - name: Prometheus Metrics + run: .github/scripts/test-cql-expr-cache-metrics.sh + + - name: Check Total-Number of Resources are 92114 + run: .github/scripts/check-total-number-of-resources.sh 92114 + + - name: Evaluate CQL Query 1 + run: .github/scripts/evaluate-measure.sh q1 56 + + - name: Evaluate CQL Query 1 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q1 56 + + - name: Evaluate CQL Query 1 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q1 56 + + - name: Evaluate CQL Query 1 on Individual Patients + run: .github/scripts/evaluate-patient-q1-measure.sh + + - name: Evaluate CQL Query 2 + run: .github/scripts/evaluate-measure.sh q2 42 + + - name: Evaluate CQL Query 2 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q2 42 + + - name: Evaluate CQL Query 2 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q2 42 + + - name: Evaluate CQL Query 4 + run: .github/scripts/evaluate-measure.sh q4 0 + + - name: Check Bloom Filter + run: .github/scripts/check-bloom-filter.sh 484cc96511a406d40ee8eeb4de6a49b05b3766ddef8eb05ac57c6139d774eddc 0 + + - name: Evaluate CQL Query 4 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q4 0 + + - name: Evaluate CQL Query 4 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q4 0 + + - name: Evaluate CQL Query 7 + run: .github/scripts/evaluate-measure.sh q7 81 + + - name: Evaluate CQL Query 7 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q7 81 + + - name: Evaluate CQL Query 7 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q7 81 + + - name: Evaluate CQL Query 14 + run: .github/scripts/evaluate-measure.sh q14 96 + + - name: Check Bloom Filter + run: .github/scripts/check-bloom-filter.sh fab7fc40f75f294787d8b2090788a291cdaf71640d6dcfe5441c00403225a256 96 + + - name: Evaluate CQL Query 14 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q14 96 + + - name: Evaluate CQL Query 14 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q14 96 + + - name: Evaluate CQL Query 17 + run: .github/scripts/evaluate-measure.sh q17 120 + + - name: Evaluate CQL Query 17 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q17 120 + + - name: Evaluate CQL Query 17 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q17 120 + + - name: Evaluate CQL Query 20 using Blazectl + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh q20-stratifier-city 120 + + - name: Evaluate CQL Query 21 using Blazectl + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh q21-stratifier-city-of-only-women 64 + + - name: Evaluate CQL Query 26 using Blazectl + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh q26-stratifier-bmi 120 + + - name: Evaluate CQL Query 27 using Blazectl + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh q27-stratifier-calculated-bmi 120 + + - name: Evaluate CQL Query 32 using Blazectl + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh q32-stratifier-underweight 120 + + - name: Evaluate CQL Query 36 + run: .github/scripts/evaluate-measure.sh q36-parameter 86 + + - name: Check Bloom Filter + run: .github/scripts/check-bloom-filter.sh d4fc6cde1636852f9e362a68ca7be027a66bf7cb38ebff9c256c3eb2179c2639 86 + + - name: Evaluate CQL Query 36 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q36-parameter 86 + + - name: Evaluate CQL Query 37 + run: .github/scripts/evaluate-measure.sh q37-overlaps 24 + + - name: Check Bloom Filter + run: .github/scripts/check-bloom-filter.sh 8a572962e0540de1bb4f4bf5c0101ff06be3ae69f4cd489509a6cf8d1646d1e1 24 + + - name: Evaluate CQL Query 37 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q37-overlaps 24 + + - name: Evaluate CQL Query 37 - Subject List + run: .github/scripts/evaluate-measure-subject-list.sh q37-overlaps 24 + + - name: Evaluate CQL Query 46 + run: .github/scripts/evaluate-measure.sh q46-between-date 19 + + - name: Check Bloom Filter + run: .github/scripts/check-bloom-filter.sh 5a1fe6d1b996aed4783f0ee04e6e74927d7ddccc407b0818a8a800769458020b 19 + + - name: Evaluate CQL Query 46 using Blazectl + run: .github/scripts/evaluate-measure-blazectl.sh q46-between-date 19 + integration-test: needs: build runs-on: ubuntu-22.04 @@ -736,7 +889,7 @@ jobs: - name: Delete run: .github/scripts/delete.sh - - name: Delete Violating Referential Integrity + - name: Delete Violating Referential Integrity run: .github/scripts/check-referential-integrity-for-delete.sh 409 - name: Batch @@ -751,7 +904,7 @@ jobs: - name: Transaction Read/Write run: .github/scripts/transaction-rw.sh - - name: Transactional Delete Preserving Referential Integrity + - name: Transactional Delete Preserving Referential Integrity run: .github/scripts/transactional-delete.sh - name: Transaction with Invalid (Null) Resource @@ -1548,6 +1701,50 @@ jobs: - name: Fetch Patient Expecting an Error run: .github/scripts/fetch-resource-0-with-missing-resource-content.sh + build-patient-last-change-index-test: + needs: build + runs-on: ubuntu-22.04 + + steps: + - name: Check out Git repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + + - name: Install Blazectl + run: .github/scripts/install-blazectl.sh + + - name: Download Blaze Image + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4 + with: + name: blaze-image + path: /tmp + + - name: Load Blaze Image + run: docker load --input /tmp/blaze.tar + + - name: Run Blaze v0.27 + run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -p 8080:8080 -v blaze-data:/app/data samply/blaze:0.27 + + - name: Wait for Blaze + run: .github/scripts/wait-for-url.sh http://localhost:8080/health + + - name: Load Data + run: blazectl --no-progress --server http://localhost:8080/fhir upload .github/test-data/synthea + + - name: Ensure that the PatientLastChange Index does not exist + run: .github/scripts/check-patient-last-change-index-missing.sh + + - name: Shut down Blaze + run: docker stop blaze && docker rm blaze + + - name: Run Latest Blaze + run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -e LOG_LEVEL=debug -p 8080:8080 -v blaze-data:/app/data blaze:latest + + - name: Wait for Blaze + run: .github/scripts/wait-for-url.sh http://localhost:8080/health + + - name: Ensure that the State of PatientLastChange Index is Current + run: .github/scripts/check-patient-last-change-index-state.sh current + jepsen-distributed-test: needs: build runs-on: ubuntu-22.04 @@ -1628,6 +1825,7 @@ jobs: if: github.event_name != 'pull_request' || (github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name) needs: - image-scan + - cql-expr-cache-test - integration-test - integration-test-synthea-1000 - not-enforcing-referential-integrity-test @@ -1646,6 +1844,7 @@ jobs: - frontend-test - missing-resource-content-test - custom-search-parameters-test + - build-patient-last-change-index-test runs-on: ubuntu-22.04 permissions: packages: write diff --git a/Makefile b/Makefile index cb44a3c2c..0ca2d8caa 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ $(MODULES): $(MAKE) -C $@ $(MAKECMDGOALS) fmt-root: - cljfmt check + cljfmt check resources src test deps.edn fmt: $(MODULES) fmt-root diff --git a/cljfmt.edn b/cljfmt.edn index 5e57e0508..f65b7b03c 100644 --- a/cljfmt.edn +++ b/cljfmt.edn @@ -12,4 +12,5 @@ deftests [[:block 1]] satisfies-prop [[:block 1]] clojure.test.check.properties/for-all [[:block 1]] - blaze.metrics.core/collector [[:block 0]]}} + blaze.metrics.core/collector [[:block 0]] + reify-expr [[:inner 0] [:inner 1]]}} diff --git a/dev/blaze/dev.clj b/dev/blaze/dev.clj index 71136521f..8faa1c770 100644 --- a/dev/blaze/dev.clj +++ b/dev/blaze/dev.clj @@ -1,20 +1,22 @@ (ns blaze.dev (:require - [blaze.byte-string :as bs] - [blaze.db.api :as d] - [blaze.db.api-spec] - [blaze.cache-collector.protocols :as ccp] - [blaze.db.resource-cache :as resource-cache] - [blaze.db.resource-store :as rs] - [blaze.db.tx-log :as tx-log] - [blaze.spec] - [blaze.system :as system] - [blaze.system-spec] - [clojure.repl :refer [pst]] - [clojure.spec.test.alpha :as st] - [clojure.tools.namespace.repl :refer [refresh]] - [java-time.api :as time] - [taoensso.timbre :as log])) + [blaze.byte-string :as bs] + [blaze.cache-collector.protocols :as ccp] + [blaze.cache-collector.protocols :as ccp] + [blaze.db.api :as d] + [blaze.db.api-spec] + [blaze.db.resource-cache :as resource-cache] + [blaze.db.resource-store :as rs] + [blaze.db.tx-log :as tx-log] + [blaze.elm.expression :as-alias expr] + [blaze.spec] + [blaze.system :as system] + [blaze.system-spec] + [clojure.repl :refer [pst]] + [clojure.spec.test.alpha :as st] + [clojure.tools.namespace.repl :refer [refresh]] + [java-time.api :as time] + [taoensso.timbre :as log])) (defonce system nil) @@ -58,6 +60,11 @@ (resource-cache/invalidate-all! (:blaze.db/resource-cache system)) ) +;; CQL Expression Cache +(comment + (str (ccp/-stats (::expr/cache system))) + ) + ;; RocksDB Stats (comment (.reset (system [:blaze.db.kv.rocksdb/stats :blaze.db.index-kv-store/stats])) diff --git a/docs/api/admin.md b/docs/api/admin.md new file mode 100644 index 000000000..2a05ecbf7 --- /dev/null +++ b/docs/api/admin.md @@ -0,0 +1,7 @@ +# Admin API + +## OpenAPI Spec + +```sh +curl http://localhost:8080/fhir/__admin/openapi.json +``` diff --git a/docs/deployment/environment-variables.md b/docs/deployment/environment-variables.md index 00e5194bb..074343370 100644 --- a/docs/deployment/environment-variables.md +++ b/docs/deployment/environment-variables.md @@ -110,6 +110,9 @@ More information about distributed deployment are available [here](distributed-b | DB_SYNC_TIMEOUT | 10000 | v0.15 | โ€” | Timeout in milliseconds for all reading FHIR interactions acquiring the newest database state. | | DB_SEARCH_PARAM_BUNDLE | โ€” | v0.21 | โ€” | Name of a custom search parameter bundle file. | | ENABLE_ADMIN_API | โ€” | v0.26 | โ€” | Set to `true` if the optional Admin API should be enabled. Needed by the frontend. | +| CQL_EXPR_CACHE_SIZE | 128 (128 MiB) | v0.28 | โ€” | Size of the CQL expression cache. Will be disabled if not given. | +| CQL_EXPR_CACHE_REFRESH | PT24H | v0.28 | โ€” | The duration after which a Bloom filter of the CQL expression cache will be refreshed. | +| CQL_EXPR_CACHE_THREADS | 4 | v0.28 | โ€” | The maximum number of parallel Bloom filter calculations for the CQL expression cache. | ยน Deprecated diff --git a/docs/implementation/cql.md b/docs/implementation/cql.md new file mode 100644 index 000000000..178f353cb --- /dev/null +++ b/docs/implementation/cql.md @@ -0,0 +1,88 @@ +# CQL + +## Expression Cache + +* bloom filter + * the set we like to build is the set of expressions returning true + * if the bloom filter returns false, we can be sure that the expression is not in the set of expressions returning true, so it will certainly return false + * the number of expressions returning true is far less then the number of expressions returning false + * we'll fill the Bloom filter with the expressions that returned true + * the problem is the following + * if we don't have filled the filter with all expression, the answer we get has no value + * we don't know whether the expression isn't in the set because we didn't put it there or because if returned false + * but we could use two filters + * one for all expressions returning true and one for all returning false + * if the expression isn't in both filters, it is new and so we have the evaluate it + * we could use a bloom filter for each expression + * then we would see whether we have a bloom filter or not + * we would insert Patients for which the expression returns true + * the first query evaluation is only used for insertion + * after that we mark the filter as ready for query + * now we can determine whether a expression is not true for a certain Patient + * + +* we'll use one Bloom filter per expression +* that Bloom filters will be stored in a Caffeine cache by expression hash +* each Bloom filter will be assigned the t of its creation +* the Bloom filters of all expressions of a query will be collected at the start of the query evaluation +* if a Bloom filter isn't found, its calculation will be queued and carried out asynchronously +* existing Bloom filters are immutable and will be used in query evaluation + * the Patient ID will be used to test whether this Patient isn't in the Bloom filter + * if the Patient ID wasn't found, the expression will return false + * if the Patient ID is found, the expression will be evaluated normally + +### Bloom Filter Calculation + +* the Bloom filter will be calculated for a particular exists expression +* it will be calculated based on a database with a particular t +* that t will be assigned to the Bloom filter +* the calculation will evaluate the expression for each patent of the database +* the ID's of Patients for which the expression returns true will be put into the Bloom filter + +## Or Expressions + +Expressions like: + +``` +define InInitialPopulation: + exists [Observation] or + exists [Condition] or + exists [Encounter] or + exists [Specimen] +``` + +are compiled to Blaze expressions: + +```edn +(or + (or + (or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (exists + (retrieve + "Encounter"))) + (exists + (retrieve + "Specimen"))) +``` + +which can be represented as the following tree + +``` + ^ + ^ | + ^ | | +O C E S +``` + +``` + ^ +| ^ +| | ^ +S E C O +``` diff --git a/docs/implementation/database.md b/docs/implementation/database.md index fff496494..87f2b15bf 100644 --- a/docs/implementation/database.md +++ b/docs/implementation/database.md @@ -38,16 +38,17 @@ There are two different sets of indices, ones which depend on the database value ### Indices depending on t -| Name | Key Parts | Value | -|--------------|-----------|-------------------------------| -| ResourceAsOf | type id t | content-hash, num-changes, op | -| TypeAsOf | type t id | content-hash, num-changes, op | -| SystemAsOf | t type id | content-hash, num-changes, op | -| TxSuccess | t | instant | -| TxError | t | anomaly | -| TByInstant | instant | t | -| TypeStats | type t | total, num-changes | -| SystemStats | t | total, num-changes | +| Name | Key Parts | Value | +|-------------------|-----------|-------------------------------| +| ResourceAsOf | type id t | content-hash, num-changes, op | +| TypeAsOf | type t id | content-hash, num-changes, op | +| SystemAsOf | t type id | content-hash, num-changes, op | +| PatientLastChange | pat-id t | - | +| TxSuccess | t | instant | +| TxError | t | anomaly | +| TByInstant | instant | t | +| TypeStats | type t | total, num-changes | +| SystemStats | t | total, num-changes | #### ResourceAsOf @@ -83,12 +84,16 @@ In addition to direct resource lookup, the `ResourceAsOf` index is used for list #### TypeAsOf -The `TypeAsOf` index contains the same information as the `ResourceAsOf` index with the difference that the components of the key are ordered `type`, `t` and `id` instead of `type`, `id` and `t`. The index is used for listing all versions of all resources of a particular type. Such history listings start with the `t` of the database value going into the past. This is done by not only choosing the resource version with the latest `t` less or equal the database values `t` but instead using all older versions. Such versions even include deleted versions because in FHIR it is allowed to bring back a resource to a new life after it was already deleted. The listing is done by simply scanning through the index in reverse. Because the key is ordered by `type`, `t` and `id`, the entries will be first ordered by time, newest first, and second by resource identifier. +The `TypeAsOf` index contains the same information as the `ResourceAsOf` index with the difference that the components of the key are ordered `type`, `t` and `id` instead of `type`, `id` and `t`. The index is used for listing all versions of all resources of a particular type. Such history listings start with the `t` of the database value going into the past. This is done by not only choosing the resource version with the latest `t` less or equal the database values `t` but instead using all older versions. Such versions even include deleted versions because in FHIR it is allowed to bring back a resource to a new life after it was already deleted. The listing is done by simply scanning through the index in reverse. Because the key is ordered by `type`, `t` and `id`, the entries will be first ordered by time, newest first, and second by resource identifier. #### SystemAsOf In the same way the `TypeAsOf` index uses a different key ordering in comparison to the `ResourceAsOf` index, the `SystemAsOf` index will use the key order `t`, `type` and `id` in order to provide a global time axis order by resource type and by identifier secondarily. +#### PatientLastChange + +The `PatientLastChange` index contains all changes to resources in the compartment of a particular Patient on reverse chronological order. Using the `PatientLastChange` index it's possible to detect the `t` of the last change in a Patient compartment. The CQL cache uses this index to invalidate cached results of expressions in the Patient context. + #### TxSuccess The `TxSuccess` index contains the real point in time, as `java.time.Instant`, successful transactions happened. In other words, this index maps each `t` which is just a monotonically increasing number to a real point in time. @@ -115,14 +120,14 @@ The `SystemStats` index keeps track of the total number of resources, and the nu The indices not depending on `t` directly point to the resource versions by their content hash. -| Name | Key Parts | Value | +| Name | Key Parts | Value | |-------------------------------------|----------------------------------------------------------------|-------| | SearchParamValueResource | search-param, type, value, id, hash-prefix | - | | ResourceSearchParamValue | type, id, hash-prefix, search-param, value | - | | CompartmentSearchParamValueResource | comp-code, comp-id, search-param, type, value, id, hash-prefix | - | | CompartmentResourceType | comp-code, comp-id, type, id | - | | SearchParam | code, type | id | -| ActiveSearchParams | id | - | +| ActiveSearchParams | id | - | #### SearchParamValueResource diff --git a/docs/monitoring/blaze.json b/docs/monitoring/blaze.json index 33d5b5886..f61b26da3 100644 --- a/docs/monitoring/blaze.json +++ b/docs/monitoring/blaze.json @@ -24,7 +24,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 74, + "id": 79, "links": [ { "asDropdown": false, @@ -447,7 +447,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "blaze_db_cache_estimated_size{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}", + "expr": "blaze_cache_estimated_size{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}", "hide": false, "interval": "", "legendFormat": "", @@ -546,7 +546,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(blaze_db_cache_hits_total{job=\"$job\",instance=\"$instance\", name=\"resource-cache\"}[1m]) / (rate(blaze_db_cache_hits_total[1m]) + rate(blaze_db_cache_misses_total[1m]))", + "expr": "rate(blaze_cache_hits_total{job=\"$job\",instance=\"$instance\", name=\"resource-cache\"}[1m]) / (rate(blaze_cache_hits_total[1m]) + rate(blaze_cache_misses_total[1m]))", "hide": false, "interval": "", "legendFormat": "", @@ -644,7 +644,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(blaze_db_cache_load_successes_total{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}[1m])", + "expr": "rate(blaze_cache_load_successes_total{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}[1m])", "hide": false, "interval": "", "legendFormat": "", @@ -742,7 +742,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(blaze_db_cache_evictions_total{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}[1m])", + "expr": "rate(blaze_cache_evictions_total{job=\"$job\",instance=\"$instance\",name=\"resource-cache\"}[1m])", "hide": false, "interval": "", "legendFormat": "", @@ -754,142 +754,143 @@ "type": "timeseries" }, { - "collapsed": false, + "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 15 + "y": 14 }, "id": 160, - "panels": [], - "title": "RocksDB Block Cache", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 16 - }, - "id": 162, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ + "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "blaze_rocksdb_block_cache_usage_bytes{job=\"$job\",instance=\"$instance\"}", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "normal", - "metric": "process_cpu_seconds_total", - "range": true, - "refId": "A", - "step": 10 - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] }, - "editorMode": "code", - "exemplar": true, - "expr": "blaze_rocksdb_block_cache_pinned_usage_bytes{job=\"$job\",instance=\"$instance\"}", - "format": "time_series", - "hide": false, - "interval": "", - "intervalFactor": 2, - "legendFormat": "pinned", - "metric": "process_cpu_seconds_total", - "range": true, - "refId": "B", - "step": 10 + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 162, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "blaze_rocksdb_block_cache_usage_bytes{job=\"$job\",instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "normal", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "blaze_rocksdb_block_cache_pinned_usage_bytes{job=\"$job\",instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "pinned", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "B", + "step": 10 + } + ], + "title": "Usage Bytes", + "type": "timeseries" } ], - "title": "Usage Bytes", - "type": "timeseries" + "title": "RocksDB Block Cache", + "type": "row" }, { - "collapsed": false, + "collapsed": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" @@ -898,1347 +899,3018 @@ "h": 1, "w": 24, "x": 0, - "y": 22 + "y": 15 }, "id": 68, - "panels": [], - "repeat": "database", - "repeatDirection": "h", - "targets": [ + "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "refId": "A" - } - ], - "title": "RocksDB $database Database", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" }, - "thresholdsStyle": { - "mode": "off" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 63, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "max": 1, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_block_cache_data_hit_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m]) / (rate(blaze_rocksdb_block_cache_data_hit_total[1m]) + rate(blaze_rocksdb_block_cache_data_miss_total[1m]))", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Data Hit Ratio", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Added blocks per second.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] }, + "unit": "ops" + }, + "overrides": [ { - "color": "red", - "value": 80 + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] } ] }, - "unit": "percentunit" + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 119, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_block_cache_data_add_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Data Additions", + "type": "timeseries" }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 23 - }, - "id": 63, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 121, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_block_cache_data_insert_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Data Insert Bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 69, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_block_cache_index_hit_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m]) / (rate(blaze_rocksdb_block_cache_index_hit_total[1m]) + rate(blaze_rocksdb_block_cache_index_miss_total[1m]))", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Index Hit Ratio", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Added blocks per second.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 79, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_block_cache_index_add_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Index Additions", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_block_cache_index_insert_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Block Cache Index Insert Bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 64, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_keys_read_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "read", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_keys_written_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "written", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_keys_updated_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "updated", + "range": true, + "refId": "C" + } + ], + "title": "Key Operations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 65, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_seek_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "seek", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_next_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "next", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_prev_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "prev", + "range": true, + "refId": "C" + } + ], + "title": "Iterator Operations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 95, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_iterators_created_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "created", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_iterators_deleted_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "deleted", + "range": true, + "refId": "B" + } + ], + "title": "Iterators Created/Deleted", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Open Iterators. Should be zero. If not please file an issue.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 34 + }, + "id": 266, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "blaze_rocksdb_iterators_created_total{job=\"$job\",instance=\"$instance\", name=\"$database\"} - blaze_rocksdb_iterators_deleted_total", + "hide": false, + "interval": "", + "legendFormat": "created", + "range": true, + "refId": "A" + } + ], + "title": "Iterators Open", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 34 + }, + "id": 114, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_compression_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": " {{name}}", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + } + ], + "title": "Compression Seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 34 + }, + "id": 115, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_decompression_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "__auto", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + } + ], + "title": "Decompression Seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 40 + }, + "id": 113, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_compaction_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": " {{name}}", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + } + ], + "title": "Compaction Seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 40 + }, + "id": 94, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_stall_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Stall Seconds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 40 + }, + "id": 97, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_wal_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "WAL Bytes Written", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 46 + }, + "id": 155, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_wal_syncs_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "WAL Sync", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_block_cache_data_hit_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m]) / (rate(blaze_rocksdb_block_cache_data_hit_total[1m]) + rate(blaze_rocksdb_block_cache_data_miss_total[1m]))", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Data Hit Ratio", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Added blocks per second.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 46 + }, + "id": 112, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 23 - }, - "id": 119, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_flush_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": " {{name}}", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + } + ], + "title": "Memtable Flush Seconds", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_block_cache_data_add_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Data Additions", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 46 + }, + "id": 154, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 23 - }, - "id": 121, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "editorMode": "code", + "expr": "rate(blaze_rocksdb_file_opens_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "File Opens", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_block_cache_data_insert_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Data Insert Bytes", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "description": "The total number of writes ending up with a timeout.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" }, - "thresholdsStyle": { - "mode": "off" - } + "overrides": [] }, - "links": [], - "mappings": [], - "max": 1, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 52 }, - "unit": "percentunit" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 29 - }, - "id": 69, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "id": 98, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_write_timeout_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Write Timeouts", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_block_cache_index_hit_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m]) / (rate(blaze_rocksdb_block_cache_index_hit_total[1m]) + rate(blaze_rocksdb_block_cache_index_miss_total[1m]))", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Index Hit Ratio", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Added blocks per second.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, - "viz": true + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] } ] - } - ] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 29 - }, - "id": 79, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_block_cache_index_add_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Index Additions", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 52 }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" + "id": 157, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 29 - }, - "id": 80, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_blocks_decompressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Blocks Decompressed", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_block_cache_index_insert_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, - "refId": "A" - } - ], - "title": "Block Cache Index Insert Bytes", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 52 + }, + "id": 234, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 35 - }, - "id": 64, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_keys_read_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "read", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_keys_written_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "written", - "range": true, - "refId": "B" + "editorMode": "code", + "expr": "rate(blaze_rocksdb_bloom_filter_useful_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "useful", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_bloom_filter_full_positive_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "FullFilter not avoided", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "rate(blaze_rocksdb_bloom_filter_full_true_positive_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "FullFilter not avoided but exists", + "range": true, + "refId": "C" + } + ], + "title": "Bloom Filter", + "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_keys_updated_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "updated", - "range": true, - "refId": "C" - } - ], - "title": "Key Operations", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] }, + "unit": "ops" + }, + "overrides": [ { - "color": "red", - "value": 80 + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] } ] }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 35 - }, - "id": 65, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 58 }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_seek_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "seek", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "id": 156, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_next_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "next", - "range": true, - "refId": "B" + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_blocks_compressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Blocks Compressed", + "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_prev_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "prev", - "range": true, - "refId": "C" - } - ], - "title": "Iterator Operations", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 58 }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "id": 158, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 35 - }, - "id": 95, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "editorMode": "code", + "exemplar": true, + "expr": "rate(blaze_rocksdb_blocks_not_compressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "hide": false, + "interval": "", + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Blocks Not Compressed", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_iterators_created_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "created", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "description": "Values are per Column Family and stacked.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_iterators_deleted_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "deleted", - "range": true, - "refId": "B" - } - ], - "title": "Iterators Created/Deleted", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 58 }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "id": 186, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 41 - }, - "id": 113, - "links": [], - "options": { - "legend": { - "calcs": [ - "mean" + "editorMode": "code", + "exemplar": false, + "expr": "blaze_rocksdb_table_reader_usage_bytes{job=\"$job\",instance=\"$instance\", name=\"$database\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{column_family}}", + "metric": "process_cpu_seconds_total", + "range": true, + "refId": "A", + "step": 10 + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "multi", - "sort": "none" + "title": "Table Reader Memory Usage", + "type": "timeseries" } - }, - "pluginVersion": "8.4.4", + ], + "repeat": "database", + "repeatDirection": "h", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_compaction_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": " {{name}}", - "metric": "process_cpu_seconds_total", - "range": true, - "refId": "A", - "step": 10 + "refId": "A" } ], - "title": "Compaction Seconds", - "type": "timeseries" + "title": "RocksDB $database Database", + "type": "row" }, { + "collapsed": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 59, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops" }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 71 + }, + "id": 61, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "sum(rate(http_fhir_requests_total{job=\"$job\",instance=\"$instance\"}[1m])) by (interaction, code)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{interaction}} {{code}}", + "refId": "A", + "step": 10 + } + ], + "title": "Total", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 77 + }, + "id": 62, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "repeat": "quantile", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 41 - }, - "id": 114, - "links": [], - "options": { - "legend": { - "calcs": [ - "mean" + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(${quantile:raw}, sum(rate(http_fhir_request_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (interaction, method, le))", + "interval": "", + "legendFormat": "{{interaction}} ({{method}})", + "range": true, + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "multi", - "sort": "none" + "title": "Durations $quantile Quantile", + "type": "timeseries" } - }, - "pluginVersion": "8.4.4", + ], "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_compression_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": " {{name}}", - "metric": "process_cpu_seconds_total", - "range": true, - "refId": "A", - "step": 10 + "refId": "A" } ], - "title": "Compression Seconds", - "type": "timeseries" + "title": "FHIR RESTful API Requests", + "type": "row" }, { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "id": 261, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 72 + }, + "id": 259, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "repeat": "quantile", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 41 - }, - "id": 115, - "links": [], - "options": { - "legend": { - "calcs": [ - "mean" + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(${quantile:raw}, sum(rate(fhir_generate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (format, le))", + "interval": "", + "legendFormat": "{{format}}", + "range": true, + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "multi", - "sort": "none" + "title": "Durations $quantile Quantile", + "type": "timeseries" } + ], + "title": "HTTP Response Generation", + "type": "row" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - "pluginVersion": "8.4.4", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 46, + "panels": [], "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_decompression_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "__auto", - "metric": "process_cpu_seconds_total", - "range": true, - "refId": "A", - "step": 10 + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refId": "A" } ], - "title": "Decompression Seconds", - "type": "timeseries" + "title": "Evaluate Measure", + "type": "row" }, { "datasource": { @@ -2296,7 +3968,7 @@ } ] }, - "unit": "ops" + "unit": "s" }, "overrides": [] }, @@ -2304,39 +3976,34 @@ "h": 6, "w": 8, "x": 0, - "y": 47 + "y": 19 }, - "id": 155, + "id": 48, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_wal_syncs_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, + "expr": "histogram_quantile(0.5, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", + "legendFormat": "{{subject_type}}", "refId": "A" } ], - "title": "WAL Sync", + "title": "Evaluation Durations 0.5 Quantile", "type": "timeseries" }, { @@ -2344,7 +4011,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2374,7 +4040,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -2396,7 +4062,7 @@ } ] }, - "unit": "percentunit" + "unit": "s" }, "overrides": [] }, @@ -2404,39 +4070,34 @@ "h": 6, "w": 8, "x": 8, - "y": 47 + "y": 19 }, - "id": 94, + "id": 49, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_stall_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, + "expr": "histogram_quantile(0.9, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", + "legendFormat": "{{subject_type}}", "refId": "A" } ], - "title": "Stall Seconds", + "title": "Evaluation Durations 0.9 Quantile", "type": "timeseries" }, { @@ -2444,7 +4105,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2474,7 +4134,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -2496,7 +4156,7 @@ } ] }, - "unit": "binBps" + "unit": "s" }, "overrides": [] }, @@ -2504,39 +4164,34 @@ "h": 6, "w": 8, "x": 16, - "y": 47 + "y": 19 }, - "id": 97, + "id": 50, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_wal_bytes_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", - "range": true, + "expr": "histogram_quantile(0.99, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", + "legendFormat": "{{subject_type}}", "refId": "A" } ], - "title": "WAL Bytes Written", + "title": "Evaluation Durations 0.99 Quantile", "type": "timeseries" }, { @@ -2544,7 +4199,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "The total number of writes ending up with a timeout.", + "description": "Estimated number of CQL expressions covered by a Bloom filter.", "fieldConfig": { "defaults": { "color": { @@ -2574,7 +4229,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -2596,17 +4251,17 @@ } ] }, - "unit": "ops" + "unit": "short" }, "overrides": [] }, "gridPos": { "h": 6, - "w": 8, + "w": 6, "x": 0, - "y": 53 + "y": 25 }, - "id": 98, + "id": 271, "options": { "legend": { "calcs": [], @@ -2627,16 +4282,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_write_timeout_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "expr": "blaze_cache_estimated_size{job=\"$job\",instance=\"$instance\",name=\"cql-expr-cache\"}", "hide": false, "interval": "", - "legendFormat": "{{name}}", + "legendFormat": "", "range": true, "refId": "A" } ], - "title": "Write Timeouts", + "title": "Estimated Number of Bloom Filters", "type": "timeseries" }, { @@ -2681,6 +4335,7 @@ }, "links": [], "mappings": [], + "max": 1, "min": 0, "thresholds": { "mode": "absolute", @@ -2701,17 +4356,14 @@ }, "gridPos": { "h": 6, - "w": 8, - "x": 8, - "y": 53 + "w": 6, + "x": 6, + "y": 25 }, - "id": 112, - "links": [], + "id": 272, "options": { "legend": { - "calcs": [ - "mean" - ], + "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false @@ -2729,19 +4381,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_flush_seconds_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "format": "time_series", + "expr": "rate(blaze_cache_hits_total{job=\"$job\",instance=\"$instance\", name=\"cql-expr-cache\"}[1m]) / (rate(blaze_cache_hits_total[1m]) + rate(blaze_cache_misses_total[1m]))", + "hide": false, "interval": "", - "intervalFactor": 2, - "legendFormat": " {{name}}", - "metric": "process_cpu_seconds_total", + "legendFormat": "", "range": true, - "refId": "A", - "step": 10 + "refId": "A" } ], - "title": "Memtable Flush Seconds", + "title": "Hit Ratio", "type": "timeseries" }, { @@ -2778,7 +4426,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -2806,11 +4454,11 @@ }, "gridPos": { "h": 6, - "w": 8, - "x": 16, - "y": 53 + "w": 6, + "x": 12, + "y": 25 }, - "id": 154, + "id": 273, "options": { "legend": { "calcs": [], @@ -2831,15 +4479,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(blaze_rocksdb_file_opens_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "expr": "rate(blaze_cache_load_successes_total{job=\"$job\",instance=\"$instance\",name=\"cql-expr-cache\"}[1m])", "hide": false, "interval": "", - "legendFormat": "{{name}}", + "legendFormat": "", "range": true, "refId": "A" } ], - "title": "File Opens", + "title": "Loads", "type": "timeseries" }, { @@ -2847,7 +4495,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -2874,10 +4521,10 @@ "type": "linear" }, "showPoints": "never", - "spanNulls": false, + "spanNulls": true, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -2901,40 +4548,15 @@ }, "unit": "ops" }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 6, - "w": 8, - "x": 0, - "y": 59 + "w": 6, + "x": 18, + "y": 25 }, - "id": 156, + "id": 274, "options": { "legend": { "calcs": [], @@ -2955,16 +4577,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_blocks_compressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "expr": "rate(blaze_cache_evictions_total{job=\"$job\",instance=\"$instance\",name=\"cql-expr-cache\"}[1m])", "hide": false, "interval": "", - "legendFormat": "{{name}}", + "legendFormat": "", "range": true, "refId": "A" } ], - "title": "Blocks Compressed", + "title": "Evictions", "type": "timeseries" }, { @@ -2972,7 +4593,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -3002,7 +4622,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -3010,6 +4630,7 @@ }, "links": [], "mappings": [], + "max": 1, "min": 0, "thresholds": { "mode": "absolute", @@ -3024,42 +4645,17 @@ } ] }, - "unit": "ops" + "unit": "percentunit" }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 6, "w": 8, - "x": 8, - "y": 59 + "x": 0, + "y": 31 }, - "id": 157, + "id": 275, "options": { "legend": { "calcs": [], @@ -3080,16 +4676,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_blocks_decompressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "expr": "rate(blaze_cql_expr_cache_bloom_filter_useful_total{job=\"$job\",instance=\"$instance\"}[1m]) / (rate(blaze_cql_expr_cache_bloom_filter_useful_total[1m]) + rate(blaze_cql_expr_cache_bloom_filter_not_useful_total[1m]))", "hide": false, "interval": "", - "legendFormat": "{{name}}", + "legendFormat": "", "range": true, "refId": "A" } ], - "title": "Blocks Decompressed", + "title": "Bloom Filter Useful Ratio", "type": "timeseries" }, { @@ -3097,6 +4692,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "description": "Should be always 1%.", "fieldConfig": { "defaults": { "color": { @@ -3134,6 +4730,7 @@ }, "links": [], "mappings": [], + "max": 0.02, "min": 0, "thresholds": { "mode": "absolute", @@ -3148,23 +4745,23 @@ } ] }, - "unit": "ops" + "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 6, "w": 8, - "x": 16, - "y": 59 + "x": 9, + "y": 31 }, - "id": 234, + "id": 276, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "multi", @@ -3179,41 +4776,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(blaze_rocksdb_bloom_filter_useful_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", + "expr": "rate(blaze_cql_expr_cache_bloom_filter_false_positive_total{job=\"$job\",instance=\"$instance\"}[1m]) / (rate(blaze_cql_expr_cache_bloom_filter_useful_total[1m]))", "hide": false, "interval": "", - "legendFormat": "useful", + "legendFormat": "", "range": true, "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_bloom_filter_full_positive_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "FullFilter not avoided", - "range": true, - "refId": "B" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(blaze_rocksdb_bloom_filter_full_true_positive_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "FullFilter not avoided but exists", - "range": true, - "refId": "C" } ], - "title": "Bloom Filter", + "title": "Bloom Filter False Positive Rate", "type": "timeseries" }, { @@ -3221,7 +4792,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "Values are per Column Family and stacked.", "fieldConfig": { "defaults": { "color": { @@ -3248,10 +4818,10 @@ "type": "linear" }, "showPoints": "never", - "spanNulls": true, + "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -3273,23 +4843,22 @@ } ] }, - "unit": "bytes" + "unit": "s" }, "overrides": [] }, "gridPos": { "h": 6, - "w": 16, + "w": 8, "x": 0, - "y": 65 + "y": 37 }, - "id": 186, - "links": [], + "id": 277, "options": { "legend": { "calcs": [], "displayMode": "list", - "placement": "right", + "placement": "bottom", "showLegend": true }, "tooltip": { @@ -3297,7 +4866,7 @@ "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { @@ -3305,19 +4874,13 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": false, - "expr": "blaze_rocksdb_table_reader_usage_bytes{job=\"$job\",instance=\"$instance\", name=\"$database\"}", - "format": "time_series", - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{column_family}}", - "metric": "process_cpu_seconds_total", + "expr": "histogram_quantile(0.5, sum(rate(blaze_cql_expr_cache_bloom_filter_creation_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "legendFormat": "{{subject_type}}", "range": true, - "refId": "A", - "step": 10 + "refId": "A" } ], - "title": "Table Reader Memory Usage", + "title": "Bloom Filter Creation Durations 0.5 Quantile", "type": "timeseries" }, { @@ -3325,7 +4888,6 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -3355,7 +4917,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -3377,55 +4939,30 @@ } ] }, - "unit": "ops" + "unit": "s" }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { - "legend": false, - "tooltip": false, - "viz": true - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 6, "w": 8, - "x": 16, - "y": 65 + "x": 8, + "y": 37 }, - "id": 158, + "id": 280, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { @@ -3433,44 +4970,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "exemplar": true, - "expr": "rate(blaze_rocksdb_blocks_not_compressed_total{job=\"$job\",instance=\"$instance\", name=\"$database\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{name}}", + "expr": "histogram_quantile(0.9, sum(rate(blaze_cql_expr_cache_bloom_filter_creation_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "legendFormat": "{{subject_type}}", "range": true, "refId": "A" } ], - "title": "Blocks Not Compressed", + "title": "Bloom Filter Creation Durations 0.9 Quantile", "type": "timeseries" }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 71 - }, - "id": 59, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "refId": "A" - } - ], - "title": "FHIR RESTful API Requests", - "type": "row" - }, { "datasource": { "type": "prometheus", @@ -3527,18 +5035,17 @@ } ] }, - "unit": "ops" + "unit": "s" }, "overrides": [] }, "gridPos": { "h": 6, - "w": 24, - "x": 0, - "y": 72 + "w": 8, + "x": 16, + "y": 37 }, - "id": 61, - "links": [], + "id": 281, "options": { "legend": { "calcs": [], @@ -3551,24 +5058,21 @@ "sort": "none" } }, - "pluginVersion": "8.4.4", + "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" - }, - "expr": "sum(rate(http_fhir_requests_total{job=\"$job\",instance=\"$instance\"}[1m])) by (interaction, code)", - "format": "time_series", - "hide": false, - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{interaction}} {{code}}", - "refId": "A", - "step": 10 + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(blaze_cql_expr_cache_bloom_filter_creation_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "legendFormat": "{{subject_type}}", + "range": true, + "refId": "A" } ], - "title": "Total", + "title": "Bloom Filter Creation Durations 0.99 Quantile", "type": "timeseries" }, { @@ -3627,7 +5131,7 @@ } ] }, - "unit": "s" + "unit": "bytes" }, "overrides": [] }, @@ -3635,9 +5139,9 @@ "h": 6, "w": 8, "x": 0, - "y": 78 + "y": 43 }, - "id": 62, + "id": 278, "options": { "legend": { "calcs": [], @@ -3651,8 +5155,6 @@ } }, "pluginVersion": "8.4.4", - "repeat": "quantile", - "repeatDirection": "h", "targets": [ { "datasource": { @@ -3661,35 +5163,21 @@ }, "editorMode": "code", "exemplar": true, - "expr": "histogram_quantile(${quantile:raw}, sum(rate(http_fhir_request_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (interaction, method, le))", + "expr": "histogram_quantile(0.5, sum(rate(blaze_cql_expr_cache_bloom_filter_bytes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", "interval": "", - "legendFormat": "{{interaction}} ({{method}})", + "legendFormat": "{{op}}", "range": true, "refId": "B" } ], - "title": "Durations $quantile Quantile", + "title": "Bloom Filter Bytes 0.5 Quantile", "type": "timeseries" }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 84 - }, - "id": 261, - "panels": [], - "title": "HTTP Response Generation", - "type": "row" - }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "", "fieldConfig": { "defaults": { "color": { @@ -3741,17 +5229,17 @@ } ] }, - "unit": "s" + "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 6, "w": 8, - "x": 0, - "y": 85 + "x": 8, + "y": 43 }, - "id": 259, + "id": 279, "options": { "legend": { "calcs": [], @@ -3765,8 +5253,6 @@ } }, "pluginVersion": "8.4.4", - "repeat": "quantile", - "repeatDirection": "h", "targets": [ { "datasource": { @@ -3775,351 +5261,21 @@ }, "editorMode": "code", "exemplar": true, - "expr": "histogram_quantile(${quantile:raw}, sum(rate(fhir_generate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (format, le))", + "expr": "histogram_quantile(0.9, sum(rate(blaze_cql_expr_cache_bloom_filter_bytes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", "interval": "", - "legendFormat": "{{format}}", + "legendFormat": "{{op}}", "range": true, "refId": "B" } ], - "title": "Durations $quantile Quantile", + "title": "Bloom Filter Bytes 0.9 Quantile", "type": "timeseries" }, - { - "collapsed": true, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 91 - }, - "id": 46, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "custom": {}, - "links": [] - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 49 - }, - "hiddenSeries": false, - "id": 48, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.2.2", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "histogram_quantile(0.5, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", - "legendFormat": "{{subject_type}}", - "refId": "A" - } - ], - "thresholds": [], - "timeRegions": [], - "title": "Evaluation Durations 0.5 Quantile", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "logBase": 1, - "min": "0", - "show": true - }, - { - "format": "short", - "logBase": 1, - "show": true - } - ], - "yaxis": { - "align": false - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "custom": {}, - "links": [] - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 49 - }, - "hiddenSeries": false, - "id": 49, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.2.2", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "histogram_quantile(0.9, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", - "legendFormat": "{{subject_type}}", - "refId": "A" - } - ], - "thresholds": [], - "timeRegions": [], - "title": "Evaluation Durations 0.9 Quantile", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "logBase": 1, - "min": "0", - "show": true - }, - { - "format": "short", - "logBase": 1, - "show": true - } - ], - "yaxis": { - "align": false - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "custom": {}, - "links": [] - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 49 - }, - "hiddenSeries": false, - "id": 50, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.2.2", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "histogram_quantile(0.99, sum(rate(fhir_evaluate_measure_evaluate_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (subject_type, le))", - "legendFormat": "{{subject_type}}", - "refId": "A" - } - ], - "thresholds": [], - "timeRegions": [], - "title": "Evaluation Durations 0.99 Quantile", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "logBase": 1, - "min": "0", - "show": true - }, - { - "format": "short", - "logBase": 1, - "show": true - } - ], - "yaxis": { - "align": false - } - } - ], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "refId": "A" - } - ], - "title": "Evaluate Measure", - "type": "row" - }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 92 - }, - "id": 86, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "refId": "A" - } - ], - "title": "Node", - "type": "row" - }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "At 100 % the indexer is fully utilized.", "fieldConfig": { "defaults": { "color": { @@ -4157,7 +5313,6 @@ }, "links": [], "mappings": [], - "max": 1, "min": 0, "thresholds": { "mode": "absolute", @@ -4172,23 +5327,23 @@ } ] }, - "unit": "percentunit" + "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 6, "w": 8, - "x": 0, - "y": 93 + "x": 16, + "y": 43 }, - "id": 22, + "id": 282, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "multi", @@ -4202,628 +5357,748 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, + "editorMode": "code", "exemplar": true, - "expr": "1 - rate(blaze_db_node_duration_seconds_sum{job=\"$job\",instance=\"$instance\",op=\"poll-tx-queue\"}[1m])", + "expr": "histogram_quantile(0.99, sum(rate(blaze_cql_expr_cache_bloom_filter_bytes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", "interval": "", - "legendFormat": "", + "legendFormat": "{{op}}", + "range": true, "refId": "B" } ], - "title": "Indexer Utilization", + "title": "Bloom Filter Bytes 0.99 Quantile", "type": "timeseries" }, { + "collapsed": true, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 49 + }, + "id": 86, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "At 100 % the indexer is fully utilized.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 80 + }, + "id": 22, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" + "exemplar": true, + "expr": "1 - rate(blaze_db_node_duration_seconds_sum{job=\"$job\",instance=\"$instance\",op=\"poll-tx-queue\"}[1m])", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Indexer Utilization", + "type": "timeseries" }, - "overrides": [ - { - "__systemRef": "hideSeriesFrom", - "matcher": { - "id": "byNames", - "options": { - "mode": "exclude", - "names": [ - "index-resources" - ], - "prefix": "All except:", - "readOnly": true - } - }, - "properties": [ - { - "id": "custom.hideFrom", - "value": { + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, - "viz": true + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "index-resources" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] } ] - } - ] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 93 - }, - "id": 87, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 80 + }, + "id": 87, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "exemplar": true, + "expr": "histogram_quantile(0.5, sum(rate(blaze_db_node_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (op, le))", + "interval": "", + "legendFormat": "{{op}}", + "refId": "B" + } + ], + "title": "Durations 0.5 Quantile", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.5, sum(rate(blaze_db_node_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (op, le))", - "interval": "", - "legendFormat": "{{op}}", - "refId": "B" - } - ], - "title": "Durations 0.5 Quantile", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 80 }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "id": 88, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 93 - }, - "id": 88, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(blaze_db_node_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (op, le))", + "interval": "", + "legendFormat": "{{op}}", + "refId": "B" + } + ], + "title": "Durations 0.99 Quantile", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.99, sum(rate(blaze_db_node_duration_seconds_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (op, le))", - "interval": "", - "legendFormat": "{{op}}", - "refId": "B" - } - ], - "title": "Durations 0.99 Quantile", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "Mean number of commands per transaction.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "Mean number of commands per transaction.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 86 + }, + "id": 90, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 99 - }, - "id": 90, - "options": { - "legend": { - "calcs": [ - "mean" + "exemplar": true, + "expr": "histogram_quantile(0.5, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "interval": "", + "legendFormat": "", + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "title": "Transaction Size 0.5 Quantile", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.5, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Transaction Size 0.5 Quantile", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "90% quantile of number of commands per transaction.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "90% quantile of number of commands per transaction.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 86 + }, + "id": 91, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 99 - }, - "id": 91, - "options": { - "legend": { - "calcs": [ - "mean" + "exemplar": true, + "expr": "histogram_quantile(0.9, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "interval": "", + "legendFormat": "", + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "title": "Transaction Size 0.9 Quantile", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.9, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Transaction Size 0.9 Quantile", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "99% quantile of number of commands per transaction.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "99% quantile of number of commands per transaction.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 86 + }, + "id": 92, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 16, - "y": 99 - }, - "id": 92, - "options": { - "legend": { - "calcs": [ - "mean" + "exemplar": true, + "expr": "histogram_quantile(0.99, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "interval": "", + "legendFormat": "", + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false + "title": "Transaction Size 0.99 Quantile", + "type": "timeseries" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.4", - "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.99, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Transaction Size 0.99 Quantile", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "1% quantile of number of commands per transaction.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" + "description": "1% quantile of number of commands per transaction.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 92 + }, + "id": 93, + "options": { + "legend": { + "calcs": [ + "mean" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": false }, - "thresholdsStyle": { - "mode": "off" + "tooltip": { + "mode": "multi", + "sort": "none" } }, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "pluginVersion": "8.4.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 105 - }, - "id": 93, - "options": { - "legend": { - "calcs": [ - "mean" + "exemplar": true, + "expr": "histogram_quantile(0.01, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", + "interval": "", + "legendFormat": "", + "refId": "B" + } ], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "tooltip": { - "mode": "multi", - "sort": "none" + "title": "Transaction Size 0.01 Quantile", + "type": "timeseries" } - }, - "pluginVersion": "8.4.4", + ], "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "exemplar": true, - "expr": "histogram_quantile(0.01, sum(rate(blaze_db_node_transaction_sizes_bucket{job=\"$job\",instance=\"$instance\"}[1m])) by (le))", - "interval": "", - "legendFormat": "", - "refId": "B" + "refId": "A" } ], - "title": "Transaction Size 0.01 Quantile", - "type": "timeseries" + "title": "Node", + "type": "row" }, { "collapsed": true, @@ -4835,7 +6110,7 @@ "h": 1, "w": 24, "x": 0, - "y": 111 + "y": 50 }, "id": 103, "panels": [ @@ -4886,7 +6161,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4902,7 +6178,7 @@ "h": 6, "w": 8, "x": 0, - "y": 132 + "y": 51 }, "id": 104, "options": { @@ -4981,7 +6257,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4997,7 +6274,7 @@ "h": 6, "w": 8, "x": 8, - "y": 132 + "y": 51 }, "id": 105, "options": { @@ -5076,7 +6353,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5092,7 +6370,7 @@ "h": 6, "w": 8, "x": 16, - "y": 132 + "y": 51 }, "id": 106, "options": { @@ -5171,7 +6449,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5187,7 +6466,7 @@ "h": 6, "w": 8, "x": 0, - "y": 138 + "y": 57 }, "id": 109, "options": { @@ -5268,7 +6547,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5284,7 +6564,7 @@ "h": 6, "w": 8, "x": 8, - "y": 138 + "y": 57 }, "id": 110, "options": { @@ -5365,7 +6645,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5381,7 +6662,7 @@ "h": 6, "w": 8, "x": 16, - "y": 138 + "y": 57 }, "id": 111, "options": { @@ -5438,7 +6719,7 @@ "h": 1, "w": 24, "x": 0, - "y": 112 + "y": 51 }, "id": 31, "panels": [ @@ -6271,7 +7552,7 @@ "h": 1, "w": 24, "x": 0, - "y": 113 + "y": 52 }, "id": 24, "panels": [ @@ -6670,7 +7951,7 @@ "h": 1, "w": 24, "x": 0, - "y": 114 + "y": 53 }, "id": 37, "panels": [ @@ -7175,7 +8456,7 @@ "h": 1, "w": 24, "x": 0, - "y": 115 + "y": 54 }, "id": 34, "panels": [], @@ -7255,7 +8536,7 @@ "h": 7, "w": 8, "x": 0, - "y": 116 + "y": 55 }, "id": 4, "options": { @@ -7349,7 +8630,7 @@ "h": 7, "w": 8, "x": 8, - "y": 116 + "y": 55 }, "id": 42, "options": { @@ -7444,7 +8725,7 @@ "h": 7, "w": 8, "x": 16, - "y": 116 + "y": 55 }, "id": 83, "options": { @@ -7539,7 +8820,7 @@ "h": 7, "w": 8, "x": 0, - "y": 123 + "y": 62 }, "id": 2, "options": { @@ -7633,7 +8914,7 @@ "h": 7, "w": 8, "x": 8, - "y": 123 + "y": 62 }, "id": 43, "options": { @@ -7727,7 +9008,7 @@ "h": 7, "w": 8, "x": 16, - "y": 123 + "y": 62 }, "id": 82, "options": { @@ -7822,7 +9103,7 @@ "h": 7, "w": 8, "x": 0, - "y": 130 + "y": 69 }, "id": 122, "options": { @@ -7917,7 +9198,7 @@ "h": 7, "w": 8, "x": 8, - "y": 130 + "y": 69 }, "id": 210, "options": { @@ -7950,7 +9231,7 @@ "type": "timeseries" } ], - "refresh": "", + "refresh": "10s", "revision": 1, "schemaVersion": 38, "style": "dark", @@ -7985,7 +9266,7 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(blaze_db_cache_hits_total, job)", + "definition": "label_values(blaze_cache_hits_total, job)", "hide": 0, "includeAll": false, "label": "Job", @@ -7993,7 +9274,7 @@ "name": "job", "options": [], "query": { - "query": "label_values(blaze_db_cache_hits_total, job)", + "query": "label_values(blaze_cache_hits_total, job)", "refId": "StandardVariableQuery" }, "refresh": 1, @@ -8012,14 +9293,14 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(blaze_db_cache_estimated_size{job=\"$job\"}, instance)", + "definition": "label_values(blaze_cache_estimated_size{job=\"$job\"}, instance)", "hide": 0, "includeAll": false, "multi": false, "name": "instance", "options": [], "query": { - "query": "label_values(blaze_db_cache_estimated_size{job=\"$job\"}, instance)", + "query": "label_values(blaze_cache_estimated_size{job=\"$job\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 1, @@ -8129,6 +9410,6 @@ "timezone": "", "title": "Blaze", "uid": "Q-h9isMWk", - "version": 1, + "version": 5, "weekStart": "" } diff --git a/docs/performance/cql.md b/docs/performance/cql.md index d36383746..909ae2841 100644 --- a/docs/performance/cql.md +++ b/docs/performance/cql.md @@ -87,42 +87,42 @@ The same can be said for the LEA58 system. | Dataset | System | Code | # Hits | Time (s) | StdDev | Pat./s | |---------|--------|---------|-------:|---------:|-------:|--------:| -| 100k | LEA25 | 17861-6 | 2 k | 0.16 | 0.011 | 630.6 k | -| 100k | LEA25 | 8310-5 | 60 k | 0.25 | 0.017 | 393.1 k | -| 100k | LEA25 | 72514-3 | 100 k | 0.29 | 0.009 | 341.5 k | -| 100k | LEA36 | 17861-6 | 2 k | 0.09 | 0.006 | 1.086 M | -| 100k | LEA36 | 8310-5 | 60 k | 0.12 | 0.003 | 807.1 k | -| 100k | LEA36 | 72514-3 | 100 k | 0.17 | 0.006 | 604.8 k | -| 100k | LEA47 | 17861-6 | 2 k | 0.06 | 0.002 | 1.629 M | -| 100k | LEA47 | 8310-5 | 60 k | 0.08 | 0.004 | 1.307 M | -| 100k | LEA47 | 72514-3 | 100 k | 0.09 | 0.003 | 1.068 M | -| 100k | LEA58 | 17861-6 | 2 k | 0.07 | 0.002 | 1.504 M | -| 100k | LEA58 | 8310-5 | 60 k | 0.08 | 0.001 | 1.298 M | -| 100k | LEA58 | 72514-3 | 100 k | 0.08 | 0.002 | 1.207 M | -| 100k-fh | LEA25 | 788-0 | 2 k | 0.21 | 0.009 | 475.3 k | -| 100k-fh | LEA25 | 44261-6 | 57 k | 0.30 | 0.012 | 331.4 k | -| 100k-fh | LEA25 | 72514-3 | 100 k | 0.38 | 0.021 | 265.8 k | -| 100k-fh | LEA36 | 788-0 | 2 k | 0.12 | 0.007 | 860.0 k | -| 100k-fh | LEA36 | 44261-6 | 57 k | 0.17 | 0.008 | 573.5 k | -| 100k-fh | LEA36 | 72514-3 | 100 k | 0.20 | 0.007 | 490.9 k | -| 100k-fh | LEA47 | 788-0 | 2 k | 0.07 | 0.002 | 1.415 M | -| 100k-fh | LEA47 | 44261-6 | 57 k | 0.10 | 0.002 | 995.8 k | -| 100k-fh | LEA47 | 72514-3 | 100 k | 0.12 | 0.004 | 809.9 k | -| 100k-fh | LEA58 | 788-0 | 2 k | 0.06 | 0.003 | 1.659 M | -| 100k-fh | LEA58 | 44261-6 | 57 k | 0.07 | 0.002 | 1.521 M | -| 100k-fh | LEA58 | 72514-3 | 100 k | 0.08 | 0.001 | 1.232 M | -| 1M | LEA25 | 17861-6 | 25 k | 8.04 | 0.059 | 124.3 k | -| 1M | LEA25 | 8310-5 | 603 k | 11.40 | 0.043 | 87.7 k | -| 1M | LEA25 | 72514-3 | 998 k | 13.16 | 0.049 | 76.0 k | -| 1M | LEA36 | 17861-6 | 25 k | 3.90 | 0.009 | 256.1 k | -| 1M | LEA36 | 8310-5 | 603 k | 5.74 | 0.023 | 174.2 k | -| 1M | LEA36 | 72514-3 | 998 k | 6.68 | 0.036 | 149.6 k | -| 1M | LEA47 | 17861-6 | 25 k | 0.59 | 0.003 | 1.705 M | -| 1M | LEA47 | 8310-5 | 603 k | 0.64 | 0.003 | 1.557 M | -| 1M | LEA47 | 72514-3 | 998 k | 0.76 | 0.006 | 1.324 M | -| 1M | LEA58 | 17861-6 | 25 k | 0.61 | 0.005 | 1.633 M | -| 1M | LEA58 | 8310-5 | 603 k | 0.67 | 0.005 | 1.495 M | -| 1M | LEA58 | 72514-3 | 998 k | 0.75 | 0.003 | 1.336 M | +| 100k | LEA25 | 17861-6 | 2 k | 0.08 | 0.005 | 1.203 M | +| 100k | LEA25 | 8310-5 | 60 k | 0.29 | 0.008 | 342.8 k | +| 100k | LEA25 | 72514-3 | 100 k | 0.42 | 0.008 | 237.7 k | +| 100k | LEA36 | 17861-6 | 2 k | 0.05 | 0.001 | 2.000 M | +| 100k | LEA36 | 8310-5 | 60 k | 0.15 | 0.003 | 667.8 k | +| 100k | LEA36 | 72514-3 | 100 k | 0.23 | 0.005 | 442.2 k | +| 100k | LEA47 | 17861-6 | 2 k | 0.05 | 0.002 | 1.873 M | +| 100k | LEA47 | 8310-5 | 60 k | 0.09 | 0.004 | 1.104 M | +| 100k | LEA47 | 72514-3 | 100 k | 0.14 | 0.004 | 731.9 k | +| 100k | LEA58 | 17861-6 | 2 k | 0.05 | 0.002 | 1.845 M | +| 100k | LEA58 | 8310-5 | 60 k | 0.08 | 0.001 | 1.315 M | +| 100k | LEA58 | 72514-3 | 100 k | 0.09 | 0.002 | 1.116 M | +| 100k-fh | LEA25 | 788-0 | 2 k | 0.08 | 0.005 | 1.299 M | +| 100k-fh | LEA25 | 44261-6 | 57 k | 0.26 | 0.017 | 379.7 k | +| 100k-fh | LEA25 | 72514-3 | 100 k | 0.40 | 0.025 | 250.9 k | +| 100k-fh | LEA36 | 788-0 | 2 k | 0.05 | 0.001 | 1.854 M | +| 100k-fh | LEA36 | 44261-6 | 57 k | 0.13 | 0.004 | 756.1 k | +| 100k-fh | LEA36 | 72514-3 | 100 k | 0.20 | 0.003 | 489.4 k | +| 100k-fh | LEA47 | 788-0 | 2 k | 0.05 | 0.001 | 1.938 M | +| 100k-fh | LEA47 | 44261-6 | 57 k | 0.08 | 0.003 | 1.332 M | +| 100k-fh | LEA47 | 72514-3 | 100 k | 0.09 | 0.002 | 1.100 M | +| 100k-fh | LEA58 | 788-0 | 2 k | 0.05 | 0.002 | 1.930 M | +| 100k-fh | LEA58 | 44261-6 | 57 k | 0.07 | 0.001 | 1.385 M | +| 100k-fh | LEA58 | 72514-3 | 100 k | 0.09 | 0.001 | 1.161 M | +| 1M | LEA25 | 17861-6 | 25 k | 0.47 | 0.011 | 2.148 M | +| 1M | LEA25 | 8310-5 | 603 k | 10.69 | 1.232 | 93.6 k | +| 1M | LEA25 | 72514-3 | 998 k | 16.74 | 1.959 | 59.8 k | +| 1M | LEA36 | 17861-6 | 25 k | 0.44 | 0.003 | 2.283 M | +| 1M | LEA36 | 8310-5 | 603 k | 4.61 | 0.031 | 217.0 k | +| 1M | LEA36 | 72514-3 | 998 k | 7.15 | 0.018 | 139.8 k | +| 1M | LEA47 | 17861-6 | 25 k | 0.47 | 0.004 | 2.138 M | +| 1M | LEA47 | 8310-5 | 603 k | 0.64 | 0.008 | 1.555 M | +| 1M | LEA47 | 72514-3 | 998 k | 0.97 | 0.009 | 1.032 M | +| 1M | LEA58 | 17861-6 | 25 k | 0.48 | 0.005 | 2.069 M | +| 1M | LEA58 | 8310-5 | 603 k | 0.63 | 0.004 | 1.587 M | +| 1M | LEA58 | 72514-3 | 998 k | 0.73 | 0.006 | 1.375 M | ### Example CQL Query @@ -195,42 +195,42 @@ For the LEA58 system, the relative performance is the same for all datasets. | Dataset | System | Code | Value | # Hits | Time (s) | StdDev | Pat./s | |---------|--------|---------|--------:|-------:|---------:|-------:|--------:| -| 100k | LEA25 | 29463-7 | 13.6 kg | 10 k | 2.64 | 0.033 | 37.9 k | -| 100k | LEA25 | 29463-7 | 75.3 kg | 50 k | 1.41 | 0.007 | 70.7 k | -| 100k | LEA25 | 29463-7 | 185 kg | 100 k | 0.35 | 0.018 | 285.1 k | -| 100k | LEA36 | 29463-7 | 13.6 kg | 10 k | 1.04 | 0.012 | 95.9 k | -| 100k | LEA36 | 29463-7 | 75.3 kg | 50 k | 0.71 | 0.006 | 141.3 k | -| 100k | LEA36 | 29463-7 | 185 kg | 100 k | 0.20 | 0.009 | 511.1 k | -| 100k | LEA47 | 29463-7 | 13.6 kg | 10 k | 0.68 | 0.004 | 147.2 k | -| 100k | LEA47 | 29463-7 | 75.3 kg | 50 k | 0.47 | 0.010 | 214.1 k | -| 100k | LEA47 | 29463-7 | 185 kg | 100 k | 0.12 | 0.004 | 855.7 k | -| 100k | LEA58 | 29463-7 | 13.6 kg | 10 k | 0.47 | 0.004 | 210.7 k | -| 100k | LEA58 | 29463-7 | 75.3 kg | 50 k | 0.31 | 0.002 | 320.2 k | -| 100k | LEA58 | 29463-7 | 185 kg | 100 k | 0.09 | 0.001 | 1.057 M | -| 100k-fh | LEA25 | 29463-7 | 13.6 kg | 100 k | 1.40 | 0.046 | 71.3 k | -| 100k-fh | LEA25 | 29463-7 | 75.3 kg | 100 k | 0.86 | 0.007 | 116.4 k | -| 100k-fh | LEA25 | 29463-7 | 185 kg | 100 k | 0.44 | 0.018 | 228.2 k | -| 100k-fh | LEA36 | 29463-7 | 13.6 kg | 100 k | 0.78 | 0.003 | 127.9 k | -| 100k-fh | LEA36 | 29463-7 | 75.3 kg | 100 k | 0.49 | 0.015 | 202.8 k | -| 100k-fh | LEA36 | 29463-7 | 185 kg | 100 k | 0.25 | 0.006 | 397.9 k | -| 100k-fh | LEA47 | 29463-7 | 13.6 kg | 100 k | 0.45 | 0.005 | 222.7 k | -| 100k-fh | LEA47 | 29463-7 | 75.3 kg | 100 k | 0.29 | 0.005 | 349.2 k | -| 100k-fh | LEA47 | 29463-7 | 185 kg | 100 k | 0.15 | 0.005 | 663.4 k | -| 100k-fh | LEA58 | 29463-7 | 13.6 kg | 100 k | 0.30 | 0.002 | 331.9 k | -| 100k-fh | LEA58 | 29463-7 | 75.3 kg | 100 k | 0.19 | 0.002 | 536.2 k | -| 100k-fh | LEA58 | 29463-7 | 185 kg | 100 k | 0.10 | 0.004 | 976.5 k | -| 1M | LEA25 | 29463-7 | 13.6 kg | 99 k | 719.84 | 3.734 | 1.4 k | -| 1M | LEA25 | 29463-7 | 75.3 kg | 500 k | 479.52 | 11.096 | 2.1 k | -| 1M | LEA25 | 29463-7 | 185 kg | 998 k | 103.51 | 40.442 | 9.7 k | -| 1M | LEA36 | 29463-7 | 13.6 kg | 99 k | 432.80 | 1.586 | 2.3 k | -| 1M | LEA36 | 29463-7 | 75.3 kg | 500 k | 265.29 | 1.618 | 3.8 k | -| 1M | LEA36 | 29463-7 | 185 kg | 998 k | 7.72 | 0.045 | 129.5 k | -| 1M | LEA47 | 29463-7 | 13.6 kg | 99 k | 138.82 | 1.378 | 7.2 k | -| 1M | LEA47 | 29463-7 | 75.3 kg | 500 k | 8.09 | 0.015 | 123.6 k | -| 1M | LEA47 | 29463-7 | 185 kg | 998 k | 1.03 | 0.004 | 973.7 k | -| 1M | LEA58 | 29463-7 | 13.6 kg | 99 k | 4.18 | 0.004 | 239.3 k | -| 1M | LEA58 | 29463-7 | 75.3 kg | 500 k | 2.67 | 0.008 | 374.2 k | -| 1M | LEA58 | 29463-7 | 185 kg | 998 k | 0.78 | 0.003 | 1.288 M | +| 100k | LEA25 | 29463-7 | 13.6 kg | 10 k | 0.35 | 0.019 | 286.1 k | +| 100k | LEA25 | 29463-7 | 75.3 kg | 50 k | 0.84 | 0.025 | 118.8 k | +| 100k | LEA25 | 29463-7 | 185 kg | 100 k | 1.23 | 0.022 | 81.1 k | +| 100k | LEA36 | 29463-7 | 13.6 kg | 10 k | 0.14 | 0.007 | 698.5 k | +| 100k | LEA36 | 29463-7 | 75.3 kg | 50 k | 0.36 | 0.011 | 275.7 k | +| 100k | LEA36 | 29463-7 | 185 kg | 100 k | 0.57 | 0.016 | 176.4 k | +| 100k | LEA47 | 29463-7 | 13.6 kg | 10 k | 0.08 | 0.007 | 1.252 M | +| 100k | LEA47 | 29463-7 | 75.3 kg | 50 k | 0.19 | 0.004 | 519.2 k | +| 100k | LEA47 | 29463-7 | 185 kg | 100 k | 0.35 | 0.016 | 286.2 k | +| 100k | LEA58 | 29463-7 | 13.6 kg | 10 k | 0.07 | 0.002 | 1.409 M | +| 100k | LEA58 | 29463-7 | 75.3 kg | 50 k | 0.13 | 0.004 | 780.9 k | +| 100k | LEA58 | 29463-7 | 185 kg | 100 k | 0.18 | 0.006 | 543.8 k | +| 100k-fh | LEA25 | 29463-7 | 13.6 kg | 100 k | 6.40 | 0.072 | 15.6 k | +| 100k-fh | LEA25 | 29463-7 | 75.3 kg | 100 k | 3.23 | 0.037 | 31.0 k | +| 100k-fh | LEA25 | 29463-7 | 185 kg | 100 k | 1.18 | 0.017 | 84.7 k | +| 100k-fh | LEA36 | 29463-7 | 13.6 kg | 100 k | 2.45 | 0.023 | 40.8 k | +| 100k-fh | LEA36 | 29463-7 | 75.3 kg | 100 k | 1.27 | 0.020 | 78.6 k | +| 100k-fh | LEA36 | 29463-7 | 185 kg | 100 k | 0.50 | 0.005 | 199.6 k | +| 100k-fh | LEA47 | 29463-7 | 13.6 kg | 100 k | 0.78 | 0.021 | 128.3 k | +| 100k-fh | LEA47 | 29463-7 | 75.3 kg | 100 k | 0.45 | 0.006 | 221.1 k | +| 100k-fh | LEA47 | 29463-7 | 185 kg | 100 k | 0.17 | 0.005 | 572.4 k | +| 100k-fh | LEA58 | 29463-7 | 13.6 kg | 100 k | 0.74 | 0.022 | 134.4 k | +| 100k-fh | LEA58 | 29463-7 | 75.3 kg | 100 k | 0.43 | 0.007 | 234.5 k | +| 100k-fh | LEA58 | 29463-7 | 185 kg | 100 k | 0.18 | 0.004 | 565.8 k | +| 1M | LEA25 | 29463-7 | 13.6 kg | 99 k | 2.91 | 0.169 | 344.2 k | +| 1M | LEA25 | 29463-7 | 75.3 kg | 500 k | 15.89 | 1.056 | 62.9 k | +| 1M | LEA25 | 29463-7 | 185 kg | 998 k | 27.61 | 0.948 | 36.2 k | +| 1M | LEA36 | 29463-7 | 13.6 kg | 99 k | 1.11 | 0.012 | 901.9 k | +| 1M | LEA36 | 29463-7 | 75.3 kg | 500 k | 3.19 | 0.042 | 313.1 k | +| 1M | LEA36 | 29463-7 | 185 kg | 998 k | 10.61 | 0.030 | 94.3 k | +| 1M | LEA47 | 29463-7 | 13.6 kg | 99 k | 0.60 | 0.018 | 1.664 M | +| 1M | LEA47 | 29463-7 | 75.3 kg | 500 k | 1.55 | 0.012 | 646.4 k | +| 1M | LEA47 | 29463-7 | 185 kg | 998 k | 2.17 | 0.022 | 460.9 k | +| 1M | LEA58 | 29463-7 | 13.6 kg | 99 k | 0.57 | 0.012 | 1.754 M | +| 1M | LEA58 | 29463-7 | 75.3 kg | 500 k | 0.93 | 0.011 | 1.078 M | +| 1M | LEA58 | 29463-7 | 185 kg | 998 k | 1.30 | 0.022 | 772.0 k | ### CQL Query @@ -302,30 +302,30 @@ The same can be said for the LEA58 system. | Dataset | System | # Hits | Time (s) | StdDev | Pat./s | |---------|--------|-------:|---------:|-------:|--------:| -| 100k | LEA25 | 395 | 0.70 | 0.011 | 142.0 k | -| 100k | LEA25 | 95 k | 0.44 | 0.022 | 227.6 k | -| 100k | LEA36 | 395 | 0.40 | 0.010 | 249.3 k | -| 100k | LEA36 | 95 k | 0.22 | 0.009 | 448.5 k | -| 100k | LEA47 | 395 | 0.23 | 0.001 | 437.5 k | -| 100k | LEA47 | 95 k | 0.14 | 0.002 | 731.9 k | -| 100k | LEA58 | 395 | 0.16 | 0.001 | 607.1 k | -| 100k | LEA58 | 95 k | 0.11 | 0.002 | 941.9 k | -| 100k-fh | LEA25 | 2 k | 1.30 | 0.008 | 76.7 k | -| 100k-fh | LEA25 | 98 k | 0.47 | 0.006 | 214.4 k | -| 100k-fh | LEA36 | 2 k | 0.75 | 0.008 | 133.1 k | -| 100k-fh | LEA36 | 98 k | 0.29 | 0.009 | 343.9 k | -| 100k-fh | LEA47 | 2 k | 0.45 | 0.003 | 224.7 k | -| 100k-fh | LEA47 | 98 k | 0.16 | 0.003 | 628.0 k | -| 100k-fh | LEA58 | 2 k | 0.31 | 0.003 | 322.3 k | -| 100k-fh | LEA58 | 98 k | 0.10 | 0.002 | 1.0 M | -| 1M | LEA25 | 4 k | 13.60 | 0.073 | 73.5 k | -| 1M | LEA25 | 954 k | 11.87 | 0.027 | 84.2 k | -| 1M | LEA36 | 4 k | 7.24 | 0.009 | 138.1 k | -| 1M | LEA36 | 954 k | 6.09 | 0.035 | 164.3 k | -| 1M | LEA47 | 4 k | 2.11 | 0.005 | 473.3 k | -| 1M | LEA47 | 954 k | 1.18 | 0.003 | 846.4 k | -| 1M | LEA58 | 4 k | 1.39 | 0.003 | 719.2 k | -| 1M | LEA58 | 954 k | 0.95 | 0.004 | 1.053 M | +| 100k | LEA25 | 395 | 0.13 | 0.006 | 778.5 k | +| 100k | LEA25 | 95 k | 0.38 | 0.019 | 265.8 k | +| 100k | LEA36 | 395 | 0.07 | 0.003 | 1.488 M | +| 100k | LEA36 | 95 k | 0.21 | 0.007 | 471.0 k | +| 100k | LEA47 | 395 | 0.06 | 0.002 | 1.628 M | +| 100k | LEA47 | 95 k | 0.13 | 0.004 | 796.7 k | +| 100k | LEA58 | 395 | 0.06 | 0.002 | 1.705 M | +| 100k | LEA58 | 95 k | 0.09 | 0.002 | 1.170 M | +| 100k-fh | LEA25 | 2 k | 0.13 | 0.007 | 747.3 k | +| 100k-fh | LEA25 | 98 k | 0.35 | 0.019 | 282.9 k | +| 100k-fh | LEA36 | 2 k | 0.08 | 0.000 | 1.320 M | +| 100k-fh | LEA36 | 98 k | 0.18 | 0.003 | 547.0 k | +| 100k-fh | LEA47 | 2 k | 0.06 | 0.002 | 1.708 M | +| 100k-fh | LEA47 | 98 k | 0.09 | 0.002 | 1.171 M | +| 100k-fh | LEA58 | 2 k | 0.06 | 0.001 | 1.774 M | +| 100k-fh | LEA58 | 98 k | 0.08 | 0.001 | 1.178 M | +| 1M | LEA25 | 4 k | 1.14 | 0.243 | 873.6 k | +| 1M | LEA25 | 954 k | 17.57 | 0.530 | 56.9 k | +| 1M | LEA36 | 4 k | 0.52 | 0.021 | 1.905 M | +| 1M | LEA36 | 954 k | 5.52 | 0.034 | 181.2 k | +| 1M | LEA47 | 4 k | 0.50 | 0.004 | 2.002 M | +| 1M | LEA47 | 954 k | 0.82 | 0.008 | 1.217 M | +| 1M | LEA58 | 4 k | 0.51 | 0.014 | 1.963 M | +| 1M | LEA58 | 954 k | 0.73 | 0.008 | 1.378 M | ### CQL Query Frequent @@ -383,6 +383,56 @@ define InInitialPopulation: cql/search.sh condition-ten-rare ``` +## All Code Search + +### Data + +| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | +|---------|--------|-------:|---------:|-------:|--------:| +| 100k | LEA25 | 99 k | 0.45 | 0.008 | 220.5 k | +| 100k | LEA36 | 99 k | 0.24 | 0.008 | 422.0 k | +| 100k | LEA47 | 99 k | 0.14 | 0.002 | 740.2 k | +| 100k | LEA58 | 99 k | 0.09 | 0.002 | 1.101 M | +| 100k-fh | LEA25 | 100 k | 0.38 | 0.014 | 263.0 k | +| 100k-fh | LEA36 | 100 k | 0.20 | 0.001 | 493.5 k | +| 100k-fh | LEA47 | 100 k | 0.10 | 0.001 | 1.046 M | +| 100k-fh | LEA58 | 100 k | 0.11 | 0.002 | 893.9 k | +| 1M | LEA25 | 995 k | 19.89 | 0.855 | 50.3 k | +| 1M | LEA36 | 995 k | 5.97 | 0.020 | 167.5 k | +| 1M | LEA47 | 995 k | 1.06 | 0.012 | 947.4 k | +| 1M | LEA58 | 995 k | 0.74 | 0.003 | 1.344 M | + +### CQL Query + +```sh +cql/search.sh condition-all +``` + +## Inpatient Stress Search + +### Data + +| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | +|---------|--------|-------:|---------:|-------:|--------:| +| 100k | LEA25 | 2 k | 0.69 | 0.027 | 144.9 k | +| 100k | LEA36 | 2 k | 0.39 | 0.007 | 256.6 k | +| 100k | LEA47 | 2 k | 0.24 | 0.005 | 422.0 k | +| 100k | LEA58 | 2 k | 0.16 | 0.002 | 619.2 k | +| 100k-fh | LEA25 | 2 k | 2.18 | 0.036 | 45.9 k | +| 100k-fh | LEA36 | 2 k | 1.40 | 0.014 | 71.2 k | +| 100k-fh | LEA47 | 2 k | 0.51 | 0.003 | 196.6 k | +| 100k-fh | LEA58 | 2 k | 0.53 | 0.003 | 187.9 k | +| 1M | LEA25 | 16 k | 8.79 | 0.613 | 113.8 k | +| 1M | LEA36 | 16 k | 3.76 | 0.029 | 265.7 k | +| 1M | LEA47 | 16 k | 1.82 | 0.009 | 549.2 k | +| 1M | LEA58 | 16 k | 1.14 | 0.005 | 876.3 k | + +### CQL Query + +```sh +cql/search.sh inpatient-stress +``` + ## Condition Code Stratification ### Data diff --git a/docs/performance/cql/code-value-search-100k-fh.png b/docs/performance/cql/code-value-search-100k-fh.png index b419e3171c715ed4ad90c34f6499143d7101dd6b..72da531baf63422ee68bd2291649d3df7d1570bf 100644 GIT binary patch literal 25425 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 Vu$R(&5CxHF=Ja{e)M?JY{0IM-P=Wve diff --git a/docs/performance/cql/code-value-search-100k-fh.txt b/docs/performance/cql/code-value-search-100k-fh.txt index 18e4189df..c64d5ce97 100644 --- a/docs/performance/cql/code-value-search-100k-fh.txt +++ b/docs/performance/cql/code-value-search-100k-fh.txt @@ -1,12 +1,12 @@ -| 100k-fh | LEA25 | 29463-7 | 13.6 kg | 100 k | 1.40 | 0.046 | 71.3 k | -| 100k-fh | LEA25 | 29463-7 | 75.3 kg | 100 k | 0.86 | 0.007 | 116.4 k | -| 100k-fh | LEA25 | 29463-7 | 185 kg | 100 k | 0.44 | 0.018 | 228.2 k | -| 100k-fh | LEA36 | 29463-7 | 13.6 kg | 100 k | 0.78 | 0.003 | 127.9 k | -| 100k-fh | LEA36 | 29463-7 | 75.3 kg | 100 k | 0.49 | 0.015 | 202.8 k | -| 100k-fh | LEA36 | 29463-7 | 185 kg | 100 k | 0.25 | 0.006 | 397.9 k | -| 100k-fh | LEA47 | 29463-7 | 13.6 kg | 100 k | 0.45 | 0.005 | 222.7 k | -| 100k-fh | LEA47 | 29463-7 | 75.3 kg | 100 k | 0.29 | 0.005 | 349.2 k | -| 100k-fh | LEA47 | 29463-7 | 185 kg | 100 k | 0.15 | 0.005 | 663.4 k | -| 100k-fh | LEA58 | 29463-7 | 13.6 kg | 100 k | 0.30 | 0.002 | 331.9 k | -| 100k-fh | LEA58 | 29463-7 | 75.3 kg | 100 k | 0.19 | 0.002 | 536.2 k | -| 100k-fh | LEA58 | 29463-7 | 185 kg | 100 k | 0.10 | 0.004 | 976.5 k | +| 100k-fh | LEA25 | 29463-7 | 13.6 kg | 100 k | 6.40 | 0.072 | 15.6 k | +| 100k-fh | LEA25 | 29463-7 | 75.3 kg | 100 k | 3.23 | 0.037 | 31.0 k | +| 100k-fh | LEA25 | 29463-7 | 185 kg | 100 k | 1.18 | 0.017 | 84.7 k | +| 100k-fh | LEA36 | 29463-7 | 13.6 kg | 100 k | 2.45 | 0.023 | 40.8 k | +| 100k-fh | LEA36 | 29463-7 | 75.3 kg | 100 k | 1.27 | 0.020 | 78.6 k | +| 100k-fh | LEA36 | 29463-7 | 185 kg | 100 k | 0.50 | 0.005 | 199.6 k | +| 100k-fh | LEA47 | 29463-7 | 13.6 kg | 100 k | 0.78 | 0.021 | 128.3 k | +| 100k-fh | LEA47 | 29463-7 | 75.3 kg | 100 k | 0.45 | 0.006 | 221.1 k | +| 100k-fh | LEA47 | 29463-7 | 185 kg | 100 k | 0.17 | 0.005 | 572.4 k | +| 100k-fh | LEA58 | 29463-7 | 13.6 kg | 100 k | 0.74 | 0.022 | 134.4 k | +| 100k-fh | LEA58 | 29463-7 | 75.3 kg | 100 k | 0.43 | 0.007 | 234.5 k | +| 100k-fh | LEA58 | 29463-7 | 185 kg | 100 k | 0.18 | 0.004 | 565.8 k | diff --git a/docs/performance/cql/code-value-search-100k.png b/docs/performance/cql/code-value-search-100k.png index 6636334fcecb46fc70c0b1cd85df7ae525f5180e..e93ce73419bf130e3972bee89bdad85ba27d065d 100644 GIT binary patch literal 22808 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

NU;krLp_8>R2LKF*g*D+Eha8%UpW~9m!;Fl3 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 diff --git a/docs/performance/cql/code-value-search-1M.txt b/docs/performance/cql/code-value-search-1M.txt index bcf84b698..7c7b74c38 100644 --- a/docs/performance/cql/code-value-search-1M.txt +++ b/docs/performance/cql/code-value-search-1M.txt @@ -1,12 +1,12 @@ -| 1M | LEA25 | 29463-7 | 13.6 kg | 99 k | 719.84 | 3.734 | 1.4 k | -| 1M | LEA25 | 29463-7 | 75.3 kg | 500 k | 479.52 | 11.096 | 2.1 k | -| 1M | LEA25 | 29463-7 | 185 kg | 998 k | 103.51 | 40.442 | 9.7 k | -| 1M | LEA36 | 29463-7 | 13.6 kg | 99 k | 432.80 | 1.586 | 2.3 k | -| 1M | LEA36 | 29463-7 | 75.3 kg | 500 k | 265.29 | 1.618 | 3.8 k | -| 1M | LEA36 | 29463-7 | 185 kg | 998 k | 7.72 | 0.045 | 129.5 k | -| 1M | LEA47 | 29463-7 | 13.6 kg | 99 k | 138.82 | 1.378 | 7.2 k | -| 1M | LEA47 | 29463-7 | 75.3 kg | 500 k | 8.09 | 0.015 | 123.6 k | -| 1M | LEA47 | 29463-7 | 185 kg | 998 k | 1.03 | 0.004 | 973.7 k | -| 1M | LEA58 | 29463-7 | 13.6 kg | 99 k | 4.18 | 0.004 | 239.3 k | -| 1M | LEA58 | 29463-7 | 75.3 kg | 500 k | 2.67 | 0.008 | 374.2 k | -| 1M | LEA58 | 29463-7 | 185 kg | 998 k | 0.78 | 0.003 | 1288 k | +| 1M | LEA25 | 29463-7 | 13.6 kg | 99 k | 2.91 | 0.169 | 344.2 k | +| 1M | LEA25 | 29463-7 | 75.3 kg | 500 k | 15.89 | 1.056 | 62.9 k | +| 1M | LEA25 | 29463-7 | 185 kg | 998 k | 27.61 | 0.948 | 36.2 k | +| 1M | LEA36 | 29463-7 | 13.6 kg | 99 k | 1.11 | 0.012 | 901.9 k | +| 1M | LEA36 | 29463-7 | 75.3 kg | 500 k | 3.19 | 0.042 | 313.1 k | +| 1M | LEA36 | 29463-7 | 185 kg | 998 k | 10.61 | 0.030 | 94.3 k | +| 1M | LEA47 | 29463-7 | 13.6 kg | 99 k | 0.60 | 0.018 | 1664 k | +| 1M | LEA47 | 29463-7 | 75.3 kg | 500 k | 1.55 | 0.012 | 646.4 k | +| 1M | LEA47 | 29463-7 | 185 kg | 998 k | 2.17 | 0.022 | 460.9 k | +| 1M | LEA58 | 29463-7 | 13.6 kg | 99 k | 0.57 | 0.012 | 1754 k | +| 1M | LEA58 | 29463-7 | 75.3 kg | 500 k | 0.93 | 0.011 | 1078 k | +| 1M | LEA58 | 29463-7 | 185 kg | 998 k | 1.30 | 0.022 | 772.0 k | diff --git a/docs/performance/cql/condition-450-rare.cql b/docs/performance/cql/condition-450-rare.cql new file mode 100644 index 000000000..f2833c03d --- /dev/null +++ b/docs/performance/cql/condition-450-rare.cql @@ -0,0 +1,459 @@ +library "condition-450-rare" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem sct: 'http://snomed.info/sct' + +context Patient + +define InInitialPopulation: + exists [Condition: Code '0' from sct] or + exists [Condition: Code '1' from sct] or + exists [Condition: Code '2' from sct] or + exists [Condition: Code '3' from sct] or + exists [Condition: Code '4' from sct] or + exists [Condition: Code '5' from sct] or + exists [Condition: Code '6' from sct] or + exists [Condition: Code '7' from sct] or + exists [Condition: Code '8' from sct] or + exists [Condition: Code '9' from sct] or + exists [Condition: Code '10' from sct] or + exists [Condition: Code '11' from sct] or + exists [Condition: Code '12' from sct] or + exists [Condition: Code '13' from sct] or + exists [Condition: Code '14' from sct] or + exists [Condition: Code '15' from sct] or + exists [Condition: Code '16' from sct] or + exists [Condition: Code '17' from sct] or + exists [Condition: Code '18' from sct] or + exists [Condition: Code '19' from sct] or + exists [Condition: Code '20' from sct] or + exists [Condition: Code '21' from sct] or + exists [Condition: Code '22' from sct] or + exists [Condition: Code '23' from sct] or + exists [Condition: Code '24' from sct] or + exists [Condition: Code '25' from sct] or + exists [Condition: Code '26' from sct] or + exists [Condition: Code '27' from sct] or + exists [Condition: Code '28' from sct] or + exists [Condition: Code '29' from sct] or + exists [Condition: Code '30' from sct] or + exists [Condition: Code '31' from sct] or + exists [Condition: Code '32' from sct] or + exists [Condition: Code '33' from sct] or + exists [Condition: Code '34' from sct] or + exists [Condition: Code '35' from sct] or + exists [Condition: Code '36' from sct] or + exists [Condition: Code '37' from sct] or + exists [Condition: Code '38' from sct] or + exists [Condition: Code '39' from sct] or + exists [Condition: Code '40' from sct] or + exists [Condition: Code '41' from sct] or + exists [Condition: Code '42' from sct] or + exists [Condition: Code '43' from sct] or + exists [Condition: Code '44' from sct] or + exists [Condition: Code '45' from sct] or + exists [Condition: Code '46' from sct] or + exists [Condition: Code '47' from sct] or + exists [Condition: Code '48' from sct] or + exists [Condition: Code '49' from sct] or + exists [Condition: Code '50' from sct] or + exists [Condition: Code '51' from sct] or + exists [Condition: Code '52' from sct] or + exists [Condition: Code '53' from sct] or + exists [Condition: Code '54' from sct] or + exists [Condition: Code '55' from sct] or + exists [Condition: Code '56' from sct] or + exists [Condition: Code '57' from sct] or + exists [Condition: Code '58' from sct] or + exists [Condition: Code '59' from sct] or + exists [Condition: Code '60' from sct] or + exists [Condition: Code '61' from sct] or + exists [Condition: Code '62' from sct] or + exists [Condition: Code '63' from sct] or + exists [Condition: Code '64' from sct] or + exists [Condition: Code '65' from sct] or + exists [Condition: Code '66' from sct] or + exists [Condition: Code '67' from sct] or + exists [Condition: Code '68' from sct] or + exists [Condition: Code '69' from sct] or + exists [Condition: Code '70' from sct] or + exists [Condition: Code '71' from sct] or + exists [Condition: Code '72' from sct] or + exists [Condition: Code '73' from sct] or + exists [Condition: Code '74' from sct] or + exists [Condition: Code '75' from sct] or + exists [Condition: Code '76' from sct] or + exists [Condition: Code '77' from sct] or + exists [Condition: Code '78' from sct] or + exists [Condition: Code '79' from sct] or + exists [Condition: Code '80' from sct] or + exists [Condition: Code '81' from sct] or + exists [Condition: Code '82' from sct] or + exists [Condition: Code '83' from sct] or + exists [Condition: Code '84' from sct] or + exists [Condition: Code '85' from sct] or + exists [Condition: Code '86' from sct] or + exists [Condition: Code '87' from sct] or + exists [Condition: Code '88' from sct] or + exists [Condition: Code '89' from sct] or + exists [Condition: Code '90' from sct] or + exists [Condition: Code '91' from sct] or + exists [Condition: Code '92' from sct] or + exists [Condition: Code '93' from sct] or + exists [Condition: Code '94' from sct] or + exists [Condition: Code '95' from sct] or + exists [Condition: Code '96' from sct] or + exists [Condition: Code '97' from sct] or + exists [Condition: Code '98' from sct] or + exists [Condition: Code '99' from sct] or + exists [Condition: Code '100' from sct] or + exists [Condition: Code '101' from sct] or + exists [Condition: Code '102' from sct] or + exists [Condition: Code '103' from sct] or + exists [Condition: Code '104' from sct] or + exists [Condition: Code '105' from sct] or + exists [Condition: Code '106' from sct] or + exists [Condition: Code '107' from sct] or + exists [Condition: Code '108' from sct] or + exists [Condition: Code '109' from sct] or + exists [Condition: Code '110' from sct] or + exists [Condition: Code '111' from sct] or + exists [Condition: Code '112' from sct] or + exists [Condition: Code '113' from sct] or + exists [Condition: Code '114' from sct] or + exists [Condition: Code '115' from sct] or + exists [Condition: Code '116' from sct] or + exists [Condition: Code '117' from sct] or + exists [Condition: Code '118' from sct] or + exists [Condition: Code '119' from sct] or + exists [Condition: Code '120' from sct] or + exists [Condition: Code '121' from sct] or + exists [Condition: Code '122' from sct] or + exists [Condition: Code '123' from sct] or + exists [Condition: Code '124' from sct] or + exists [Condition: Code '125' from sct] or + exists [Condition: Code '126' from sct] or + exists [Condition: Code '127' from sct] or + exists [Condition: Code '128' from sct] or + exists [Condition: Code '129' from sct] or + exists [Condition: Code '130' from sct] or + exists [Condition: Code '131' from sct] or + exists [Condition: Code '132' from sct] or + exists [Condition: Code '133' from sct] or + exists [Condition: Code '134' from sct] or + exists [Condition: Code '135' from sct] or + exists [Condition: Code '136' from sct] or + exists [Condition: Code '137' from sct] or + exists [Condition: Code '138' from sct] or + exists [Condition: Code '139' from sct] or + exists [Condition: Code '140' from sct] or + exists [Condition: Code '141' from sct] or + exists [Condition: Code '142' from sct] or + exists [Condition: Code '143' from sct] or + exists [Condition: Code '144' from sct] or + exists [Condition: Code '145' from sct] or + exists [Condition: Code '146' from sct] or + exists [Condition: Code '147' from sct] or + exists [Condition: Code '148' from sct] or + exists [Condition: Code '149' from sct] or + exists [Condition: Code '150' from sct] or + exists [Condition: Code '151' from sct] or + exists [Condition: Code '152' from sct] or + exists [Condition: Code '153' from sct] or + exists [Condition: Code '154' from sct] or + exists [Condition: Code '155' from sct] or + exists [Condition: Code '156' from sct] or + exists [Condition: Code '157' from sct] or + exists [Condition: Code '158' from sct] or + exists [Condition: Code '159' from sct] or + exists [Condition: Code '160' from sct] or + exists [Condition: Code '161' from sct] or + exists [Condition: Code '162' from sct] or + exists [Condition: Code '163' from sct] or + exists [Condition: Code '164' from sct] or + exists [Condition: Code '165' from sct] or + exists [Condition: Code '166' from sct] or + exists [Condition: Code '167' from sct] or + exists [Condition: Code '168' from sct] or + exists [Condition: Code '169' from sct] or + exists [Condition: Code '170' from sct] or + exists [Condition: Code '171' from sct] or + exists [Condition: Code '172' from sct] or + exists [Condition: Code '173' from sct] or + exists [Condition: Code '174' from sct] or + exists [Condition: Code '175' from sct] or + exists [Condition: Code '176' from sct] or + exists [Condition: Code '177' from sct] or + exists [Condition: Code '178' from sct] or + exists [Condition: Code '179' from sct] or + exists [Condition: Code '180' from sct] or + exists [Condition: Code '181' from sct] or + exists [Condition: Code '182' from sct] or + exists [Condition: Code '183' from sct] or + exists [Condition: Code '184' from sct] or + exists [Condition: Code '185' from sct] or + exists [Condition: Code '186' from sct] or + exists [Condition: Code '187' from sct] or + exists [Condition: Code '188' from sct] or + exists [Condition: Code '189' from sct] or + exists [Condition: Code '190' from sct] or + exists [Condition: Code '191' from sct] or + exists [Condition: Code '192' from sct] or + exists [Condition: Code '193' from sct] or + exists [Condition: Code '194' from sct] or + exists [Condition: Code '195' from sct] or + exists [Condition: Code '196' from sct] or + exists [Condition: Code '197' from sct] or + exists [Condition: Code '198' from sct] or + exists [Condition: Code '199' from sct] or + exists [Condition: Code '200' from sct] or + exists [Condition: Code '201' from sct] or + exists [Condition: Code '202' from sct] or + exists [Condition: Code '203' from sct] or + exists [Condition: Code '204' from sct] or + exists [Condition: Code '205' from sct] or + exists [Condition: Code '206' from sct] or + exists [Condition: Code '207' from sct] or + exists [Condition: Code '208' from sct] or + exists [Condition: Code '209' from sct] or + exists [Condition: Code '210' from sct] or + exists [Condition: Code '211' from sct] or + exists [Condition: Code '212' from sct] or + exists [Condition: Code '213' from sct] or + exists [Condition: Code '214' from sct] or + exists [Condition: Code '215' from sct] or + exists [Condition: Code '216' from sct] or + exists [Condition: Code '217' from sct] or + exists [Condition: Code '218' from sct] or + exists [Condition: Code '219' from sct] or + exists [Condition: Code '220' from sct] or + exists [Condition: Code '221' from sct] or + exists [Condition: Code '222' from sct] or + exists [Condition: Code '223' from sct] or + exists [Condition: Code '224' from sct] or + exists [Condition: Code '225' from sct] or + exists [Condition: Code '226' from sct] or + exists [Condition: Code '227' from sct] or + exists [Condition: Code '228' from sct] or + exists [Condition: Code '229' from sct] or + exists [Condition: Code '230' from sct] or + exists [Condition: Code '231' from sct] or + exists [Condition: Code '232' from sct] or + exists [Condition: Code '233' from sct] or + exists [Condition: Code '234' from sct] or + exists [Condition: Code '235' from sct] or + exists [Condition: Code '236' from sct] or + exists [Condition: Code '237' from sct] or + exists [Condition: Code '238' from sct] or + exists [Condition: Code '239' from sct] or + exists [Condition: Code '240' from sct] or + exists [Condition: Code '241' from sct] or + exists [Condition: Code '242' from sct] or + exists [Condition: Code '243' from sct] or + exists [Condition: Code '244' from sct] or + exists [Condition: Code '245' from sct] or + exists [Condition: Code '246' from sct] or + exists [Condition: Code '247' from sct] or + exists [Condition: Code '248' from sct] or + exists [Condition: Code '249' from sct] or + exists [Condition: Code '250' from sct] or + exists [Condition: Code '251' from sct] or + exists [Condition: Code '252' from sct] or + exists [Condition: Code '253' from sct] or + exists [Condition: Code '254' from sct] or + exists [Condition: Code '255' from sct] or + exists [Condition: Code '256' from sct] or + exists [Condition: Code '257' from sct] or + exists [Condition: Code '258' from sct] or + exists [Condition: Code '259' from sct] or + exists [Condition: Code '260' from sct] or + exists [Condition: Code '261' from sct] or + exists [Condition: Code '262' from sct] or + exists [Condition: Code '263' from sct] or + exists [Condition: Code '264' from sct] or + exists [Condition: Code '265' from sct] or + exists [Condition: Code '266' from sct] or + exists [Condition: Code '267' from sct] or + exists [Condition: Code '268' from sct] or + exists [Condition: Code '269' from sct] or + exists [Condition: Code '270' from sct] or + exists [Condition: Code '271' from sct] or + exists [Condition: Code '272' from sct] or + exists [Condition: Code '273' from sct] or + exists [Condition: Code '274' from sct] or + exists [Condition: Code '275' from sct] or + exists [Condition: Code '276' from sct] or + exists [Condition: Code '277' from sct] or + exists [Condition: Code '278' from sct] or + exists [Condition: Code '279' from sct] or + exists [Condition: Code '280' from sct] or + exists [Condition: Code '281' from sct] or + exists [Condition: Code '282' from sct] or + exists [Condition: Code '283' from sct] or + exists [Condition: Code '284' from sct] or + exists [Condition: Code '285' from sct] or + exists [Condition: Code '286' from sct] or + exists [Condition: Code '287' from sct] or + exists [Condition: Code '288' from sct] or + exists [Condition: Code '289' from sct] or + exists [Condition: Code '290' from sct] or + exists [Condition: Code '291' from sct] or + exists [Condition: Code '292' from sct] or + exists [Condition: Code '293' from sct] or + exists [Condition: Code '294' from sct] or + exists [Condition: Code '295' from sct] or + exists [Condition: Code '296' from sct] or + exists [Condition: Code '297' from sct] or + exists [Condition: Code '298' from sct] or + exists [Condition: Code '299' from sct] or + exists [Condition: Code '300' from sct] or + exists [Condition: Code '301' from sct] or + exists [Condition: Code '302' from sct] or + exists [Condition: Code '303' from sct] or + exists [Condition: Code '304' from sct] or + exists [Condition: Code '305' from sct] or + exists [Condition: Code '306' from sct] or + exists [Condition: Code '307' from sct] or + exists [Condition: Code '308' from sct] or + exists [Condition: Code '309' from sct] or + exists [Condition: Code '310' from sct] or + exists [Condition: Code '311' from sct] or + exists [Condition: Code '312' from sct] or + exists [Condition: Code '313' from sct] or + exists [Condition: Code '314' from sct] or + exists [Condition: Code '315' from sct] or + exists [Condition: Code '316' from sct] or + exists [Condition: Code '317' from sct] or + exists [Condition: Code '318' from sct] or + exists [Condition: Code '319' from sct] or + exists [Condition: Code '320' from sct] or + exists [Condition: Code '321' from sct] or + exists [Condition: Code '322' from sct] or + exists [Condition: Code '323' from sct] or + exists [Condition: Code '324' from sct] or + exists [Condition: Code '325' from sct] or + exists [Condition: Code '326' from sct] or + exists [Condition: Code '327' from sct] or + exists [Condition: Code '328' from sct] or + exists [Condition: Code '329' from sct] or + exists [Condition: Code '330' from sct] or + exists [Condition: Code '331' from sct] or + exists [Condition: Code '332' from sct] or + exists [Condition: Code '333' from sct] or + exists [Condition: Code '334' from sct] or + exists [Condition: Code '335' from sct] or + exists [Condition: Code '336' from sct] or + exists [Condition: Code '337' from sct] or + exists [Condition: Code '338' from sct] or + exists [Condition: Code '339' from sct] or + exists [Condition: Code '340' from sct] or + exists [Condition: Code '341' from sct] or + exists [Condition: Code '342' from sct] or + exists [Condition: Code '343' from sct] or + exists [Condition: Code '344' from sct] or + exists [Condition: Code '345' from sct] or + exists [Condition: Code '346' from sct] or + exists [Condition: Code '347' from sct] or + exists [Condition: Code '348' from sct] or + exists [Condition: Code '349' from sct] or + exists [Condition: Code '350' from sct] or + exists [Condition: Code '351' from sct] or + exists [Condition: Code '352' from sct] or + exists [Condition: Code '353' from sct] or + exists [Condition: Code '354' from sct] or + exists [Condition: Code '355' from sct] or + exists [Condition: Code '356' from sct] or + exists [Condition: Code '357' from sct] or + exists [Condition: Code '358' from sct] or + exists [Condition: Code '359' from sct] or + exists [Condition: Code '360' from sct] or + exists [Condition: Code '361' from sct] or + exists [Condition: Code '362' from sct] or + exists [Condition: Code '363' from sct] or + exists [Condition: Code '364' from sct] or + exists [Condition: Code '365' from sct] or + exists [Condition: Code '366' from sct] or + exists [Condition: Code '367' from sct] or + exists [Condition: Code '368' from sct] or + exists [Condition: Code '369' from sct] or + exists [Condition: Code '370' from sct] or + exists [Condition: Code '371' from sct] or + exists [Condition: Code '372' from sct] or + exists [Condition: Code '373' from sct] or + exists [Condition: Code '374' from sct] or + exists [Condition: Code '375' from sct] or + exists [Condition: Code '376' from sct] or + exists [Condition: Code '377' from sct] or + exists [Condition: Code '378' from sct] or + exists [Condition: Code '379' from sct] or + exists [Condition: Code '380' from sct] or + exists [Condition: Code '381' from sct] or + exists [Condition: Code '382' from sct] or + exists [Condition: Code '383' from sct] or + exists [Condition: Code '384' from sct] or + exists [Condition: Code '385' from sct] or + exists [Condition: Code '386' from sct] or + exists [Condition: Code '387' from sct] or + exists [Condition: Code '388' from sct] or + exists [Condition: Code '389' from sct] or + exists [Condition: Code '390' from sct] or + exists [Condition: Code '391' from sct] or + exists [Condition: Code '392' from sct] or + exists [Condition: Code '393' from sct] or + exists [Condition: Code '394' from sct] or + exists [Condition: Code '395' from sct] or + exists [Condition: Code '396' from sct] or + exists [Condition: Code '397' from sct] or + exists [Condition: Code '398' from sct] or + exists [Condition: Code '399' from sct] or + exists [Condition: Code '400' from sct] or + exists [Condition: Code '401' from sct] or + exists [Condition: Code '402' from sct] or + exists [Condition: Code '403' from sct] or + exists [Condition: Code '404' from sct] or + exists [Condition: Code '405' from sct] or + exists [Condition: Code '406' from sct] or + exists [Condition: Code '407' from sct] or + exists [Condition: Code '408' from sct] or + exists [Condition: Code '409' from sct] or + exists [Condition: Code '410' from sct] or + exists [Condition: Code '411' from sct] or + exists [Condition: Code '412' from sct] or + exists [Condition: Code '413' from sct] or + exists [Condition: Code '414' from sct] or + exists [Condition: Code '415' from sct] or + exists [Condition: Code '416' from sct] or + exists [Condition: Code '417' from sct] or + exists [Condition: Code '418' from sct] or + exists [Condition: Code '419' from sct] or + exists [Condition: Code '420' from sct] or + exists [Condition: Code '421' from sct] or + exists [Condition: Code '422' from sct] or + exists [Condition: Code '423' from sct] or + exists [Condition: Code '424' from sct] or + exists [Condition: Code '425' from sct] or + exists [Condition: Code '426' from sct] or + exists [Condition: Code '427' from sct] or + exists [Condition: Code '428' from sct] or + exists [Condition: Code '429' from sct] or + exists [Condition: Code '430' from sct] or + exists [Condition: Code '431' from sct] or + exists [Condition: Code '432' from sct] or + exists [Condition: Code '433' from sct] or + exists [Condition: Code '434' from sct] or + exists [Condition: Code '435' from sct] or + exists [Condition: Code '436' from sct] or + exists [Condition: Code '437' from sct] or + exists [Condition: Code '438' from sct] or + exists [Condition: Code '439' from sct] or + exists [Condition: Code '62718007' from sct] or + exists [Condition: Code '234466008' from sct] or + exists [Condition: Code '288959006' from sct] or + exists [Condition: Code '47505003' from sct] or + exists [Condition: Code '698754002' from sct] or + exists [Condition: Code '157265008' from sct] or + exists [Condition: Code '15802004' from sct] or + exists [Condition: Code '14760008' from sct] or + exists [Condition: Code '36923009' from sct] or + exists [Condition: Code '45816000' from sct] diff --git a/docs/performance/cql/condition-450-rare.yml b/docs/performance/cql/condition-450-rare.yml new file mode 100644 index 000000000..55b09b91c --- /dev/null +++ b/docs/performance/cql/condition-450-rare.yml @@ -0,0 +1,5 @@ +library: cql/condition-450-rare.cql +group: +- type: Patient + population: + - expression: InInitialPopulation diff --git a/docs/performance/cql/duration.jq b/docs/performance/cql/duration.jq index 9718df3c8..837808cc1 100644 --- a/docs/performance/cql/duration.jq +++ b/docs/performance/cql/duration.jq @@ -1 +1 @@ -.extension[0].valueQuantity.value +.extension[] | select(.url == "https://samply.github.io/blaze/fhir/StructureDefinition/eval-duration") | .valueQuantity.value diff --git a/docs/performance/cql/other.md b/docs/performance/cql/other.md index 7f0419d8f..7fcaf6462 100644 --- a/docs/performance/cql/other.md +++ b/docs/performance/cql/other.md @@ -18,42 +18,3 @@ ```sh cql/search.sh condition-50-rare ``` - -## All Code Search - -### Data - -| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | -|---------|--------|-------:|---------:|-------:|--------:| -| 100k | LEA25 | 99 k | 2.51 | 0.015 | 39.8 k | -| 100k | LEA36 | 99 k | 1.55 | 0.018 | 64.5 k | -| 100k | LEA47 | 99 k | 0.93 | 0.021 | 107.8 k | -| 100k | LEA58 | 99 k | 0.63 | 0.009 | 159.1 k | -| 100k-fh | LEA58 | 100 k | 1.55 | 0.006 | 64.7 k | -| 1M | LEA47 | 995 k | 4.75 | 0.014 | 210.5 k | -| 1M | LEA58 | 995 k | 6.05 | 0.017 | 165.4 k | - -### CQL Query - -```sh -cql/search.sh condition-all -``` - -## Inpatient Stress Search - -### Data - -| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | -|---------|--------|-------:|---------:|-------:|--------:| -| 100k | LEA25 | 2 k | 4.73 | 0.032 | 21.1 k | -| 100k | LEA36 | 2 k | 2.97 | 0.029 | 33.7 k | -| 100k | LEA47 | 2 k | 1.59 | 0.008 | 63.0 k | -| 100k | LEA58 | 2 k | 1.27 | 0.023 | 78.8 k | -| 100k-fh | LEA58 | 2 k | 4.41 | 0.041 | 22.7 k | -| 1M | LEA58 | 16 k | 11.08 | 0.044 | 90.2 k | - -### CQL Query - -```sh -cql/search.sh inpatient-stress -``` diff --git a/docs/performance/cql/result.jq b/docs/performance/cql/result.jq index f5222b4a5..d73f91851 100644 --- a/docs/performance/cql/result.jq +++ b/docs/performance/cql/result.jq @@ -1,4 +1,4 @@ { - duration: .extension[0].valueQuantity.value, + duration: .extension[] | select(.url == "https://samply.github.io/blaze/fhir/StructureDefinition/eval-duration") | .valueQuantity.value, result: .group[0].population[0].count } diff --git a/docs/performance/cql/search-all.sh b/docs/performance/cql/search-all.sh index 50f9c0e02..753e54b5c 100755 --- a/docs/performance/cql/search-all.sh +++ b/docs/performance/cql/search-all.sh @@ -65,12 +65,12 @@ cql/search.sh condition-ten-rare #cql/search.sh condition-50-rare #cql/search.sh condition-50-rare -#restart "$COMPOSE_FILE" -#cql/search.sh condition-all -#cql/search.sh condition-all -#cql/search.sh condition-all +restart "$COMPOSE_FILE" +cql/search.sh condition-all +cql/search.sh condition-all +cql/search.sh condition-all -#restart "$COMPOSE_FILE" -#cql/search.sh inpatient-stress -#cql/search.sh inpatient-stress -#cql/search.sh inpatient-stress +restart "$COMPOSE_FILE" +cql/search.sh inpatient-stress +cql/search.sh inpatient-stress +cql/search.sh inpatient-stress diff --git a/docs/performance/cql/search.sh b/docs/performance/cql/search.sh index 4648a95e8..9ab3d0898 100755 --- a/docs/performance/cql/search.sh +++ b/docs/performance/cql/search.sh @@ -10,7 +10,7 @@ FILE="$1" echo "Counting Patients with criteria from $FILE..." COUNT="$(blazectl --server "$BASE" evaluate-measure "$SCRIPT_DIR/$FILE.yml" 2> /dev/null | jq -r '.group[0].population[0].count')" -for i in {0..6} +for i in {0..8} do blazectl --server "$BASE" evaluate-measure "$SCRIPT_DIR/$FILE.yml" 2> /dev/null |\ jq -rf "$SCRIPT_DIR/duration.jq" >> "$START_EPOCH-$FILE.times" diff --git a/docs/performance/cql/simple-code-search-100k-fh.gnuplot b/docs/performance/cql/simple-code-search-100k-fh.gnuplot index 481e8678c..b9c5952eb 100644 --- a/docs/performance/cql/simple-code-search-100k-fh.gnuplot +++ b/docs/performance/cql/simple-code-search-100k-fh.gnuplot @@ -15,7 +15,7 @@ set title "Simple Code Search - Dataset 100k-fh" set xlabel 'System' set ylabel 'Patients/s' set format y "%.0f k" -set yrange [0:1800] +set yrange [0:2000] # Define grid set grid ytics diff --git a/docs/performance/cql/simple-code-search-100k-fh.png b/docs/performance/cql/simple-code-search-100k-fh.png index f2d5a24b73bce12062915b369a51cce3ddcb7eea..11922fd63d3f0c00e0fbf965094188516299b180 100644 GIT binary patch literal 19720 zcmdVC2UL~Wwk3Q-K}7{YL=2!Jf|5kZ8T1H9P6C1iMWQ4D$;k{NA|e?C2_iX37C}+T zND!4IDmh6MB=^90hlmr4{ z+Zj2cI)OmuLm;gCx`_-wDaljF!#9eH3bI7P8tI?7vgBX_;Q-+bQCia_daT>?yrS!x zi?jKE25Z-{=qc;Qw@d2owZ@(K<2)0>Ov;|IROl=u*?t)~{dxb9Po} zt^eD%smX7r+P8`^*|w#g9np<>{MbN8M@KLA?Af#ZgM((5F0pXfPxsZdx3{y?O^&p2 zaEI;Py*p)DLzj`c;hTN2UV)Xxn*G@Lc=4Kvx%tr7uU`iT!@|NotsZvvX=-Y!31+J+ zlw)~VR3v0(W_C^L>&S@PP}4z^l1JX-Z67`qm6nbU54U%8JX~C*Y;rx|^F(9QrcFY2 zU!{zUjC6H7oO?Xyn`QUMh`WDmZPnnU=jGkXz!T>@@dY=k!)aq~Zr-ZF&dz@G>=_YvE^+CTcQg>J81ogEos_WT)6l4 zT$8tVcWP;Au_;G09KZPH)s2mMoRhJN3xQVl3-S) zhqk)~ZT8W07hO5EBdyoIm_sWacdnIr{tVI3dnMnawPlcP!1es`w4{ z*M-$5mXMa-h?+s<7H(F?-TwITr@`Ad$x~l4fTenV^d#${# ztTcS#a@_exmxSkHczF1)#YHV`ZC(``%3FpWQ@6^?z1LP(+<%VnUmf|-)zxMCY4ztP zLs20i1_p+rk`g9i`^4d`I9xp*GYw5oPD!r*GG3VbIr{1SWAUiUZ#=L-%alB?)v)r8#mr*O;#yP@u{t?)w=U6>DHsr(8gHFp02L* zdU~a{Juk7iMspSN3JSMQek*nu#3o;~wJmp_w@J|~xjL$~apOjD@iM_LF2>y@S96t~ zJ$p7eHz#o7#7J|(o#bS*bLY~XecXVcTtE`Q*6o$j+OUkUYn7qE914b3Y) z%b_en`h}$(1RtE<6Fv52YpYAR@G*lI<{~$&%CGr4Iy#PxjbS^g?(IHBB)eA8)#WkV zlC*DRTz+3)&l0Z`C2;if&7B-t+ke&wpzql#r=^+5y_X^s0jAj)24t3g=hsY# z+{{dtC?O$X==n3`dO&SML)*uX1qB87@7+6J;&?Q$YR9Gx+qP}v+NRcexUjF=x#z0A z{X?Y_IV&^oCF^vl_6bQ5Ei5c3OA_s~%0@qB%`7Zq;}f#8vxi4Qx%EY?KJzdm*gqn^ z6GxUfqz11MZYv)=cFgv}Gqw711}-ixHnwEs2~91nTGI#AMwc&N9w^$Ex3QvkDqPBK zWsyJj{o2P$^74}4UBHERG{>Lz?+csz`f9ztes^5l0n^8yvJ6Eu#VGPYeFcx*(a|ySVx|%I77>b9GRrF|nH1cw>vwZ;;W=|4NN#y~Ih>%C zOL%)K(dgje!-t7|gM(F$h##8fs{P)aE4I@#%~QQqCQ&@D)0c8|>gj|nKOFJ&^c*mE zMXX-(zJH zgh031A!(wisj0ZzZ1@S2yFo!TG{Yk!bGX-4^T)}_$pHb|W?Whl&)mO%f7daCM~0ei zZf;mkT>E{raOLx@dj-ZOzf}qg3!l0}71F>OB8 zd+SrmEnC*60==tQdt3(*F0RQ*G{GrZSz+=?^{*T1>*=)XRCzi-=NS7PVPRnr6m-TT z`qOji=3d%N!-(XVbkH*YwdwIHp1#Y$@<~LNAUQh6`QmEYx7zXFjB8OmpEF5@!p`S2CK}kv02gCK?2LSmKxFCj>J*TAwPRh8k(T zsei{HWEYp2$vwmSR8@;x_b_3I|cj~yb22nv_8I$_K3^M%8K)kP*hPdQy#nP z=jRs~$oA*j{MvCcxvA87f3-~>w7v_j}M-<+J|8U^WT z+1Zn`!>#q}zQ`mttT4Sp_3(>EoC(GxA03!n#ew#F)@Hf;uG!4o9QTnU)$iZ;l({?i zy!1>+Na*eD4cTYbe@;hdyPCB0we(xARu?Wj!PDb@>+9+q92^wntA3)<4!8sR;AqP{Q{`MqH6>g3l~yLnCdI?9IH z(mvriwop~3odc|V9RUc=ZrdS+2mBU@V)A|Td-tK{V5w{G67p6k?2N=Rr` zpbD(I{PQS+wmrwV%4gmB7iDD;XWq8eK5lEV>MXF{d*YIuiAhpGfRdctraa+7+a4|Z z=nytK%Hs1sko6*Ju)4u)DtmVC28LuGx~TguaKEjU)eGkd1E#=T`}UpJ&`5aQBrWK3 zs;TLy;$yLe>YzjKVB11Pn^~sY9GXo7* zuU^*u@R}fC&yS8rEyu!=H2aWd%c#Ys?Mfq0Zr6ZR^E6& znEdqVkM2@uEa!y_7l5k~;E1KcDVO~olhQIW?~oifZe^(WAn7#r84t0xGU1J1;xQWQ z>mTW_SEwQT;x@`fPD=RoPa3mcE?T0VAV#AEteNAZqxTJnvAXVHRoqT_3rLNEf+9gC z09B*RW$JsTerfpn)Dgh-;0(P&S~XA4m4Lm1>KYpEvqL!mKm(-?*@>+2gH&XB+DxZ}aG=q2(=Gr+l}U%i3-&IJ_IOwr-t zN1~)xIfrmpwH*r{s}Bz=Tna|sjyYjMITP;t;>C-7`}T#CM@B?2N_Z4Cx1cEDs;2KB zb8YddsHjLyO*QrB5v@GMD}qq@U;`6FlhxpeGgte`EEL+!)R(2qAa@A%PEe$4#` zz+u^o7vbKo0D0<7_zxXA6mm>zYJ2|k2a%DHM>U@E%*?pFAOX&3;b33ngc=WC#$CJE z^iBhrJvyp!iS2^tY_n{xxWdA5!vd>wflT6b%lvw;m%rXYdZm7%T4eO<`bKJ|YExC! z@rEctUncG{%)X^+L%pDo-af-M|00o>P zNzcXSC}`_9Qdv35oYlJ`6rF$hlZ>qFu3ftnlGF!ctrSK^NB!^K{Q`8Yr46Do(N%mX zNDfIY^2ZVXx8HH&vlBh#?Fqg(%qUnlD7IZZseFr!j#^D&i|g{QD`(HP?X`XJBq`}| zkQ|eQ2dG#@=>3-;KYpa&xpVV|>reGQ9elj1cE4v|$_R z=libzHxS|M?G*rndWAMuotz4uKab|QaMIpBH(h}_=*`==!lI&EHjrs+f5N%T&r^8( zo9KoQ6l@_7q+B^SWS5t(frSCW4)^wIXln9-yWhOI;VEbyJ0G7WCq3AQwx;HS`g(eurOvi{ z_wEgnOH4?ZneJB*6%}P(T$%4VJ8Jpdl8Y+?TscP6*{N?1@!n_aCIk=~)>ULfW zE32`-K223sDI#;wv!_p44;+w^mA&J;b=x)oJ%4}yiSE*5-LUH?@ay2P`y-UJ@h)zyt*s3W4Sq*hSy?3{46Xzr zoilpe)}62T<~SSS*-fBO;%5+SGwRZ;7#mC3wMFujh{vzV`Uqa6=1EYO48!urXAGHh zv$GE{58ULWnECwr^~wCqr)f@qFSe}}???|P+mQo0dJn8bUmu~{_G}cH{XDnx_cwm^ zi8`E2B9703g4D1tmihsq@B8hw({&Dp$YbNGdZ$RSIx=EOp5(Or)sdP(aHmm|=8Y>F z+ukXWM>0IWFR>veJX{BA%-63K%T(du{UliWAEoBhtNEHYLPE|GiEnVC4>2=eym(RM zQYr3pRdGBh^D}CD)JG!=i<(&puVt6T+2Q&pPTXOlwqIT#(at&61dp#%NNQPJbOTk! z(L_AuYg$%OE6@8}(OCD-b&K7922Xn@I;;< zvUx*xq+h0i#i-<-pCe*x1=G{h_nE{;dV4F+JDMPqgQw=?dqvz)fGz~mkt4nld;$Vs!y?|R9%(9cenf>Z?s0$&XalW#1(0%F+}&e^9b}k; zUI2#RPIv_c_lr8E-5i`5BI;e<<@J9)~B9x?PdrqH8nMG7gRrGWo00QLo)YN zm)?S#r-^vZfq?=15wBUJ$JF=J zfJU*gh5*CQEm|YX&K@R6ZLDPCF~1AK6cVy?Mu$D*_N`kx>FEXBDMJ)xWNw$1mPSOh z-`cWEx8#BYSgXyK7Xa!i%zGp}ekCb{^|=H2eo8+V92Mo>S4{)CV)CnWjt&xsTZJQ4g77e9LRC|A-knubVz ze+0Mvl`F1`vnkil1~Q6<`kp*_5=74tIp?%LeMo31v*)@vUMa#2)1<^i64~r3c8K*} zUBHo{>q=r57uQ!&3G}7wiBE;(lv|Y>b*Au5V}$qNe$ zU@t#93vB`LJv?6I?nOpFzR9TR5m1CTKqpiw?JR@*!b01tt4KE}FGwh1RlVdJHzF#Z zm*2MTEC3r%%*}0i_l}i?rSFa34x%GY(A=@zgt!5aX1LhR@7(%M!O+}}%~GS&AKU8F8 z&6?t*t+dw>CfS5Gqxk!fWWb$SzIFXLzNE?((BUd(ASYK18MVVe zlt2*8-niBB0IeSgvMsZ$wzf9p+hsFBn=T;)ex9N4-M8PtX0)_fLgb@77ja13=6^JZ z8RVI6^EydEh}l2q(`FSuIUXHVo~u)_{Z5W+p-72OdOD|qLLF|2-?ALVcI=9@^eZfX z=rKdYj$Qt)2eGkPb0%a2b2>7e278-T5#}*j`YlCeo|aWve4?UySJDCPfd5;+e0kY+ zqn-$%2D(^BmSK5c-z9KHnKCHe*aU2FkQ~cI+58vK_xgs0s{4$iy}fxJ3--Hqg$gnjZ&{|FNR{)wQ*5h&mt@KffAC7CUHZ z_cUzB+r-3_ISeXWk84TH_z{_%o#&BHA+SCjRv?x-jn~%KpU=Hiqi0)&EOd0Bbg;g@ zK0cnAIX5R~xGilL4G3UlRMcR9e?zqJ%v=9m-@bjr!Gb`8+o~MjgjlX%;(4wOJ(6^u zPru!`--E}rK768l&FkCiTToSXUU~@gR+6M;tM(JP21(MKia&AUL|1n=U>x`PB8caa zG0BGIUV^q?APBM1Y}+rf+>)d)T{bl}1<9$Vp+VffZ_}MhVVB7`9Zm>WXc`ERP?l|1 zQBHPtQ%8sHHa#jr6BSP498zZ+c#v|8$W;(F5Joc-6DW0(Cr+prBrHHPm-JqJF`QBt zz%#6O?p(yfhb`)hv?Q@9eD*LimMd8$mR)DyA41Y4JSF5@z31j?78XZab`eC@Z{T?z zmXy>Fm52B0$eQPT7m^9M6o6A#xp%CEu-oi2sRJ&9F^dqvrN0Tufy2GN zzhIiIATJN;0Hq01tD}<>Xx010pQBec?fK)ap@|p(TkCL!=JPoB0U5HyBT5Gb2fd-B zWEi|@Zf-t)?AZH;2Cjn#4{~uS)#Mj0IvfHxzJC2WG)+A2{CKAzcqG7C&8_tWb1E{1 zj!_gehk^Q_zXm^=w*45hH(;GnC?G}tc>X^C8ez{ZaU<2x8#gL}kt<)n-axia_J^+a zzouQ~C05<`g#i2z55&a9Bd9mv25@#sal30k zs!;XFGO7J`grEQH5jqfdRBm9PUS%BFGiby;$~SsNHt-4n0sRJ(aq9gNiIP1wuNoWg zue!HBc{+|9hOEt%ahip;xU@7XJba(1lcKhE;qX@E$=uvrXvsCueUMqul666Er@mfs zz^f9?Je9{{M>#n1^7GLrd|y`=1vCP-Ps7BtiV*wt>lZ+zxW}*Tw!NDXk$Vu4X(c6* zzP~(PLTs$>qAs!!<@L_{&r)J^arYBN507Q+kiCOL;6CBk-|VQE#E#JWL&StIq(nq( z4{ah&PEO6q&A>FqMv{aMf8l*x^!_GX(`-`w)?soi4cvFm^ng==T!glS`I7Lu{zh6&2KCx6rZ*-1i2 zWQRPgmR*ASwO;Gg)<3-fwM>O>EbadixY-WxfHWX`l5ooXubQi|ajO-UK?CU>kQY7X z&6_u)$0VOw))u?h)!ls;9UXc!Kt-fpB+?*igFN>>f|SGdiY9}TPaf3uLu!nUurfCn za-B{L3Tj3rYO*TL$gs|<+b!w!A}41Gq8zY;ot>SMlG4&;QjU9Y@DvWSwUrf6s&cfD z^qRQnR)UW{DZl-}1h?$s-mq~actL$t6&t<|9H@BxI=NIvB{%`qBpz-ZtedDm#s<&mbZXRwOs`~`c z{h_UGyr+B(G#TU@f#iSxzE)G_XY^2lA3nTjY`n6(3_WkpPfKvju!E8{le4pma&n)u zFWe|BbiR1;JFsB+ukZd4z`F^yZtGSJ^)lFU?l|;d>>Tt~fEmcZ7B&uevqx3R1dw!4Vb7&GY>DDaeHPjnN$)Sj`{>cM z0=4~1qk`CPiW@hqw4OjvrI2M0ahhZgJ7)O!(Id1tWr$FJY5a(}ndtvU3tfpvZ@gIp zb=ZCWhqbZsAzD8W5%hX2NIh9(ztZcOGFwpbD7S6vE_O(Mc-3Brb1Rjj{XVL}mBdnO zH~G}UZGSI2Mi;0*zj}3Hb$J2OIN3Fy%5T-qCT3>Ha4&{6AphXJOG)|MzfVQ8d(R%! zG5TG*dP<#Dq{FW7`K!34D@cXNJlernUW;6{jx^+l zii*EzT?3b#9RAR{bVq(`U4*}CUF`>Lzy|Jr@N2xD`Rv&d4vxC&>g}60ynOi*XH>Vy z?t+ySC(T)H?Hx3+@537E#gOW?n%c&6|CtXn&ljJ-<&|u<-L)ClEnKP_t>9!^Fk zMt2`gfxO(@#rcW-yLTTAk_$i7MiRbw9XJiHXt$pI6Xl_5>vH+EMq}-fBdZ6Bq1&H5 z8)BvYQel;(&H_0_jlZTX>7vzaZ7+yF6c!iRSgiV#9XfDpPkKOL}NufB#`Nwz`SBJtRtqPR*@bi1y*gLqRLE38TJ{+`a+orU3`>zScimwvY|)O2(hXlR;)_HIH=sC@nYLEn|)wWSTv z$AGyZoYK1RQG&-rL`vE+L(~6`>!f-2Zp5R; zH@ZAlL#4QS^=HMk^-+&qwEoO&=n7|fgjw3r((~O4_WP~*DZd^O15@RkSSMHqClFYC zJ2y-`=pz>9gR92n4HJ9TA$FIq{x|x^9N(25Tt|=+MEhu8eK zH}ik~3-eP11(t{2E2D-0hX>+;i%=o0x{9V?X4sR4c12ugiy z3&n;;4YU_cZcEElRt}i{hHgLVUN3QVL=#rFyE{2wl_PsAkT|rp!|M81 zM-A6fQ&O&6y?QtvE!rJH$Vn7iNLRl9XoZRd{Ya?`k{bLBP{(@QC(2q{T7a2`M@Kt9 ze>Q(kM8^9e+fJC9o%Mhw1%T#f4&@S}Ir<}iXo>IMy^BmoMibd{r&8_PIrS9H@eoJj zj17}O^=SPHqZh{%IFE++^5SR~T0l-tPDDOv$*3Xs_DjAzRJ#F5mEUf$M4(}9(((Ci z_Tfz31uBSu6y)UKyKBKJ5;gDNlUigE5F9}S4taueXtv4|(7VbjDssX7fPC$RI}d;( z=D`D1=n}`@;x&%i8L{NnmX;-;(%_WPO~K+kA&epQD1>q@j2HHZ#7W=@ZBn*{#e3ii z5Hptv|Ae(l3eyp_iO{o&oFb#}anw$%>Dv!~Ci(}+;~R@6;j|o1??yLIAWIQ!}&jmAOtC85tnTv&Ki;+S-V324_sT>U+_hYAcD9~{RfE0UZ-QcC#4o#47j#`#bvU);NAki*ZO=2@SX<(B3jKYw!R^3c{pDUox>r1|xS_*0BFLv0uhS5UqZUSxUJ)pa5J zLM0Xx_Q)_UJtugMku}gv5D^kuo{3%4wC=_ULP|uV#f%lg5k2?$@X%1!(kfVR?jV(V z!^(*Azq4e|%DXdAYMFz=qoTq>LdYp7-c?m`a0gbZIoXvQp}I5UrM`DssIBvNt|4<Yy?I$jVvuk`unc}O2YLG zKECkl7w8C2`7!m3w4|MX6V#II5eo(CbDTrNt;u@2y2_D!7Et$zj)*Z-BWtK}9nUS$ zZqsu8dHzSN#5%~U$(flpa1z6=#T8Z;BWP=Y-E=R}%|)_@%lq?Z9Z5+^G$=!UAHsm# zb@hWYI8n>c47O;+Uf5pORvMAOpuZbH)#90UVp6rF}Jq zchvRtmOf_av1@}>G$~vEV$6mB1Oep)FE6_AI3%}D9(1VLlF!S-!yx98y{8meS0qY{ zOh{OGVQHeAdfPT@bMs%Qe*LBJ0<9_AX@6qegV(Hz=x+Yn1E(Z3Vz3Bofl?p?ar zY#i|Y;y|oURqgNSSaYvM?X^eWO(~RwH^uXrDv-9(J*w|rx1NG;g#Y+)HE%V%LAUz91{}q{0BF}#tj>`tHBWfvl3uSAGI{PkXA6E zp{qec0~~+gl{tAGfCO*_Xb&itkN73rW{(^?R9ROyipu*@(Uo+`;M{e~DBG@XZs3vd zAVLDae0dZg1n3`q2aO|imVbhf*n=xj#m;jJ9y@kSTs+1Xiu)uyt^g5_eI2h}<=5Vi z+bcvuB?;qz-|()W>j3UsUsH3Oj}J9b3?5il*J2q35(4siw9QCEqcuK09-0$PP+8du z+&X^GAm0-og@q*-FXHw}4x6lZ!O4Sl2aaFl0&iG0Un13G^?97hA$kY?3FNjJxw|jf zzFc^knmU3`4^(Z$Po8Omh|A!Gd8QCv_9OMD_*WR#@uXLQJD+W|j;6cZyZn$m>Gv=3`;rn7u%>p`MtE3Qq-MtzX5wV*l<9u<8>iq$K(&F3bF6?fJ zlfD5v#LoJPZ+0JaQJyOdPjak#VTXjVWtWp8a6ZAC@jq9a@v?t4ER%5_&AShB`3i;d zN$UC9WQ6ong!PWkXf8+=Sgdw-CR;wEB^d3ZlE#z$D~j@e&IwCB+|LaHXo^rOBFSYC6bK}w?_Imgvy*B!!uDT)YpO>^^SI-C${A%#rN z&jUcIr2W(LWDUkARR?PNRRsqQ#ym98KRyHOZ~~|}Bzyk_u-yIo%9@&wVq&@>_MzvL z9_JsTglnnF40fzX%K+XomsTgh0S%2E+qcIhB%C5~Y-CBe%76hhnv#tEDyl1=>O^LY zlNzM_ic|xRBqA&v5gvYzNnGUM!D|-Yf4Z3lp6BHqLdY|d>;I{bBV$&*dsj{bB}T#Adt1x} zmb0y^_`fY>tUEa`py(;B%@h_jj8^~~Me1ZydcjnWX#>Sr3D1!aDa6^nnjM&S0SrP0 z^8`IZP9eeMygXjs8BA8e2UJOF(F3d}ajU4PEc9Q7K}J**yN?vPj47!4`?+NMl-c&| zQ~FAhOY;u~YLr=*%=;Wk$haZn%_ImbCG%L7fcVPXTuQgmXHl1~J=;L@b_;pNOsjai zh?E51b9MicIPQ^?f3@iuZ$K2QIWvt&GgI~-jm1hR@;`rci}LVHf_DJznZ4}E$jAt3 zs2}?ZHpDSxS#o!F)=^wsoEhSK^Vd*X>%O zwQK8W&T47t{^4+b1-n8-CaJj$7nYI4L%vQZL8NH_KsEl$S|?9lM~;k&ih5T_DV5Pt z!j@bt4_5>3ZW1YQus$LfcW`&>HwDYz@uBYpP8a88(Y?A^0R`LUSArAvuL{}T1I zuInTvJxw-Bqkr2xgqsOm7fn#y%_b@;38%5}z(5scWvZFhL!`5ZE;lPD`;gI z#5{ULw|DO-4Ao!1#x?&beDTmg-y7)bLp6h&fExatRXSQ!Jd^=AT>)MJTNfPC5$wU` zR7nK|b8vF;6&^$o-o2VhFb&6xA|nMlO^l3qP{RsH(}|7(F+-w8C{X28f7NsTYpL&g zql+NR5N)3=eF{s8W~v{kb_w$ao@X}1&w;h?J=~WxNeWL>|JUt&xVQgITl%lv%ima6U0L`$q}+N)jO=+Zf4&{-slP*y>_V5!$-FBo)FuwQi%Gwe>hWF_4a2;efyZ zAv{uBHe3fd%hTV5qsOc-;dA=mwgRT|qNAh1`A0`aL{c&I>$5gIy>9uRT=#Q(I}Ny` znt}@Y7~r}6{rzwlp{6mavLFNx;l0z+()@hi57vQ0DY|Q*4B%zZJ%Gm1CQ_}ULr1bH z{JFnzVHUctuCpTm7Yxm!mbo?ETNo0`QB%s!OQZ~qcM zck$&SDgWj3EI}PyR5DXmQ4tgrWQ!+_N&a$qs~|(Mp5Qa!xNxco5)~1a-UF8}F!7ED z2#JUc^!LLN2> zCb+J^zYxmzx#$UmuWR@#0e*hC-b(DgowT!?#9)PohX;hoOXc#Gp}%dRM08(>M51gc zrl${74pa0vhK7Z0q@aL1?Y3}hEsElV_gX2&AQIY9TcDXhVmJevt_wztpmBhyqc>@k zU2)k&v;L~-)+S}x-Z|+D zKeR#k!6>23IKXypmE_8-2D~>kj6!x57*_uHV`9P^0xb4y>DMoG(=LMUpcj1Y+BK5P z3NA27v&Mbx_4-qp_2at>Ln#7$pfJ#oqoXt&9ny*BU zb8?PBr`?B17n~@}KjfJ=$AgD)8@@cRueV`Mc6?s@dZr16;4l!4_l`JjECbeec46KW%0hh~8poJ|Xw#Iq+`((pKozOUPSOcZ5h2JJv%>l$ zO$4s1PYe#$0H23a8FA)`st{x~KVtmtfMF?1@0=PmlBQh5cI?>kMD_DnE0n*w!5Q3H z=E?T4mN!wF9n+#}%s~u$_lgP&AJ57J9GY@@0Y^uH@s{5c1gUAyo(Xv_&PGE~Z;auf z_lM9c`czy{Q2V#cd_1^5>YP(mDrhs*oji>zY)vh3G|eRUPu?a5^cA)ykvLL$JJ>4U@#Diqch}_M2rU6t{_yD&&d0sIf|;PS z7|*!WA$yM@=)r@pLqpYy@B|{=U0c8LL@@Le+y%y4PaHqKTing@EFl|iQhYlGYpfQEqHQr0xr!|PD0#ybnq zU_Hjootcze_4X}#u@K#7vUo3U{eAC27({h*!+_!9{srbrR9somNEzeu=kuf*cep{M;(wl5Dd!xkeEi^J-|%gf75NlpE! z{+-1GRs$<61u{Bxe9RYvfm^z5T11pVnZTIp*|j;xO-e7$>@44bDfQ-ZZ|`%8FCc5` z|ERmq{VnU~h3y?AG!1LuG#9dj5mOh`S%^R2^DxI8jEBYgn1H|>I(4}kM!p!h{QJB@ zJPfy7=W;K>G>?lNj)!=K9syBC#%sEU?YzyrFjH$w(NSLW-!sn=v81_0xILhHEMhJg zm|U2q%xgsic3(U`noHq47ha!83^! zD>WeMx-;{gN>|u0A{Um!M0tKCNj9PsBi2qJYO_lfhMs_W3^hc$| z4<&Y$Ipt-DLB0qg&5!($x|lH>rM6T^-gkkTK~v4p&>JNMVRlUSx!gzC5WT>Au&@o7 z9)l2z;6j9m!I4x}R%Z1WV`3nGXopq5ybCQ3gBh9$KXA{T#SUg@TS%@gs7S=B5cg4; zAx8q27kMrfkQCFJ#tPix5PZfi_YjLH({gRE*jZW~g zAPF}$sSZ4fFQAL@U%g9?#RNjiQ!dPq){aHh+jcUBRV2GpYsR)K%&4n%2Gi>LJE(~-Hl4@ zAwNFn-0&rtq()7s(x3$Wf?R}&uiRJ`d~yS*r@BMWerJ1RN*FB$*@RJ%_KLYos-WgT zA>(lX90~S?_9*+)MoyNLFkgvILFFUP*09A^mq3X>MJ%-GRmL!B|>@esj%sd!uU?1)O!xH{y!@cK)D;z3v z^71A4kO2sV;VM1)3VK!J=)F)<1PC{ydvA{q6#;m-;@}{PHYhd;+Y15K4}+}}Xo8~C z@N;tV7miu)_wUg$F?&5-yB1Pb-a_W;clJi1!7R<;p4ZDo3adPd+KF>lI-gr;C`=GZ z<8BHH10&i@OiVyz6 zL^aT!R9>DhUlWJf`>Fo|dmpM0V@ZSewtW`brR;khsR2_VIAE%8In9$d`5tap45JO0 zKv6o@0;>oH8*O2P+L_g}Ip)E2=Zmm`U;yCY_Dv<<*QIi=``v)Q{$r+20oaRs!~foo z|1YxuDM5^RF)}bbOHQs@fTK1L=^z(qeVH0=`Ms3?(MPtVWk1IUEZ7MZISctjTIA^y z2+z&|xFE_|l_F=cC@~fhqRuwxs$pOR%Pt$R=dFz^p#lz7DQ69&dQ9w@n+dYAnxd@+ z2#U;$O`q0B21nr~K8lQFr=x_l4tr&HcQ-odN<=I_dV|c&PgH*mC&!*pS%5T;W*aPF zkR#zoWY&A6dPGrPo|2Lh%EAz$+r{MH6Xw*ziS$P)_es;xOh)~2m6mCqR$;!yUfAXlR7vqwUv|vCY&H7DG zO)ZS1Wnr+D^ieP7aD1b^jA2(HxW%W}I*Y>|^c5T-RMeex_l=;7kvnuG<4yJT$Fv*N z`o7ZgWhYWxqieoFA6d)L#h=6k-LkmK}dLMj>z@~WQ)%5hO1in2D zY%1g@4e9BPZk}vfUG*|b5f&4B5f=xPiO;%FrR%p7BE+So`QP8Sd-pmYEcDdW6+*Ud zhGY3FqXU~>4?C()@JF$nxf_p}BOOjHn8g?eW)@)oMC+t`0|7fRJ4;mi@bf1>0e#Zu za)O7%{;;}?)YQcfeo{kQD#dYKRkRzhim2oG*cg0V#Ov3uL82g#)Yi&_puzmtX4y9E zNH8kw#AKwClR$9I1|OJ)U`qfp!Pd02On<^4e4SUhAqy=m`%U6FKBjrD7(QnXB1lGaK{fWV?&d$!s)dvtdg~i1ijCdfZ4f7$#(E>U6?ZpL#;K<0@-l-P`wsFbj<&vzdx9^+fwa3K7U_)z8$0TLpNU@#mWu23yX-v9~%=86g2*M`iSr3 zW`-vdZ%2PNzJEXCBKAV)rmEeAbuya{-n>r8`f>2)_LI^+yUx~ literal 24400 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%0DxgV*fK=zyq@PS)6{2XbzhxC{!b> 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

    HG0!DX&O5`~dtpUN{OC_9yK`USgT%O5q# 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 bzb=f{cx`0eWcl<)rxgk<=j_sq)jhZiW{osz diff --git a/docs/performance/cql/simple-code-search-100k-fh.txt b/docs/performance/cql/simple-code-search-100k-fh.txt index 3ce450754..e59726008 100644 --- a/docs/performance/cql/simple-code-search-100k-fh.txt +++ b/docs/performance/cql/simple-code-search-100k-fh.txt @@ -1,12 +1,12 @@ -| 100k-fh | LEA25 | 788-0 | 2 k | 0.21 | 0.009 | 475.3 k | -| 100k-fh | LEA25 | 44261-6 | 57 k | 0.30 | 0.012 | 331.4 k | -| 100k-fh | LEA25 | 72514-3 | 100 k | 0.38 | 0.021 | 265.8 k | -| 100k-fh | LEA36 | 788-0 | 2 k | 0.12 | 0.007 | 860.0 k | -| 100k-fh | LEA36 | 44261-6 | 57 k | 0.17 | 0.008 | 573.5 k | -| 100k-fh | LEA36 | 72514-3 | 100 k | 0.20 | 0.007 | 490.9 k | -| 100k-fh | LEA47 | 788-0 | 2 k | 0.07 | 0.002 | 1415 k | -| 100k-fh | LEA47 | 44261-6 | 57 k | 0.10 | 0.002 | 995.8 k | -| 100k-fh | LEA47 | 72514-3 | 100 k | 0.12 | 0.004 | 809.9 k | -| 100k-fh | LEA58 | 788-0 | 2 k | 0.06 | 0.003 | 1659 k | -| 100k-fh | LEA58 | 44261-6 | 57 k | 0.07 | 0.002 | 1521 k | -| 100k-fh | LEA58 | 72514-3 | 100 k | 0.08 | 0.001 | 1232 k | +| 100k-fh | LEA25 | 788-0 | 2 k | 0.08 | 0.005 | 1299 k | +| 100k-fh | LEA25 | 44261-6 | 57 k | 0.26 | 0.017 | 379.7 k | +| 100k-fh | LEA25 | 72514-3 | 100 k | 0.40 | 0.025 | 250.9 k | +| 100k-fh | LEA36 | 788-0 | 2 k | 0.05 | 0.001 | 1854 k | +| 100k-fh | LEA36 | 44261-6 | 57 k | 0.13 | 0.004 | 756.1 k | +| 100k-fh | LEA36 | 72514-3 | 100 k | 0.20 | 0.003 | 489.4 k | +| 100k-fh | LEA47 | 788-0 | 2 k | 0.05 | 0.001 | 1938 k | +| 100k-fh | LEA47 | 44261-6 | 57 k | 0.08 | 0.003 | 1332 k | +| 100k-fh | LEA47 | 72514-3 | 100 k | 0.09 | 0.002 | 1100 k | +| 100k-fh | LEA58 | 788-0 | 2 k | 0.05 | 0.002 | 1930 k | +| 100k-fh | LEA58 | 44261-6 | 57 k | 0.07 | 0.001 | 1385 k | +| 100k-fh | LEA58 | 72514-3 | 100 k | 0.09 | 0.001 | 1161 k | diff --git a/docs/performance/cql/simple-code-search-100k.gnuplot b/docs/performance/cql/simple-code-search-100k.gnuplot index 5c90a2b17..a57019841 100644 --- a/docs/performance/cql/simple-code-search-100k.gnuplot +++ b/docs/performance/cql/simple-code-search-100k.gnuplot @@ -15,7 +15,7 @@ set title "Simple Code Search - Dataset 100k" set xlabel 'System' set ylabel 'Patients/s' set format y "%.0f k" -set yrange [0:1800] +set yrange [0:2200] # Define grid set grid ytics diff --git a/docs/performance/cql/simple-code-search-100k.png b/docs/performance/cql/simple-code-search-100k.png index 90de7beb0440005886bce1c02f23ca8e6c12e653..875073f836548c858cec7e214f7c909eb201ea91 100644 GIT binary patch literal 19266 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 Yq}CX=@moTZArQ`;RUo}Ot$*YH0geL9ZvX%Q literal 24100 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 literal 19858 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 D0d~jB literal 19219 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 diff --git a/docs/performance/cql/ten-code-search-100k-fh.txt b/docs/performance/cql/ten-code-search-100k-fh.txt index cf07e7acd..abcbbdd33 100644 --- a/docs/performance/cql/ten-code-search-100k-fh.txt +++ b/docs/performance/cql/ten-code-search-100k-fh.txt @@ -1,8 +1,8 @@ -| 100k-fh | LEA25 | 2 k | 1.30 | 0.008 | 76.7 k | -| 100k-fh | LEA25 | 98 k | 0.47 | 0.006 | 214.4 k | -| 100k-fh | LEA36 | 2 k | 0.75 | 0.008 | 133.1 k | -| 100k-fh | LEA36 | 98 k | 0.29 | 0.009 | 343.9 k | -| 100k-fh | LEA47 | 2 k | 0.45 | 0.003 | 224.7 k | -| 100k-fh | LEA47 | 98 k | 0.16 | 0.003 | 628.0 k | -| 100k-fh | LEA58 | 2 k | 0.31 | 0.003 | 322.3 k | -| 100k-fh | LEA58 | 98 k | 0.10 | 0.002 | 1000 k | +| 100k-fh | LEA25 | 2 k | 0.13 | 0.007 | 747.3 k | +| 100k-fh | LEA25 | 98 k | 0.35 | 0.019 | 282.9 k | +| 100k-fh | LEA36 | 2 k | 0.08 | 0.000 | 1320 k | +| 100k-fh | LEA36 | 98 k | 0.18 | 0.003 | 547.0 k | +| 100k-fh | LEA47 | 2 k | 0.06 | 0.002 | 1708 k | +| 100k-fh | LEA47 | 98 k | 0.09 | 0.002 | 1171 k | +| 100k-fh | LEA58 | 2 k | 0.06 | 0.001 | 1774 k | +| 100k-fh | LEA58 | 98 k | 0.08 | 0.001 | 1178 k | diff --git a/docs/performance/cql/ten-code-search-100k.gnuplot b/docs/performance/cql/ten-code-search-100k.gnuplot index ae278e9b3..5ed92909f 100644 --- a/docs/performance/cql/ten-code-search-100k.gnuplot +++ b/docs/performance/cql/ten-code-search-100k.gnuplot @@ -15,7 +15,7 @@ set title "Ten Code Search - Dataset 100k" set xlabel 'System' set ylabel 'Patients/s' set format y "%.0f k" -set yrange [0:] +set yrange [0:1900] # Define grid set grid ytics diff --git a/docs/performance/cql/ten-code-search-100k.png b/docs/performance/cql/ten-code-search-100k.png index 1a6bbe4e89826bec12e5a8f8e15ae21d610c149b..23559e5e6a4e8832e17e55d00cc777d0473ff568 100644 GIT binary patch literal 23572 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 Ll!@swhFAX=PnhIB literal 23620 zcmch<2VBm5-#31Qvq4%&ic(s%)1WP(sid@rwuX|TT?i#9G$?5hO(`veK^N+-q&^A*L^?Fb3gyC*L9xP#nEy6j^FS5{e0f*^9|HEd1C8k#?1r*VXKm& zf+m5m!H+;7=_cQRKPh;n@(TYVHB&jEKv*aKC$8xEeF9-GK}q3=mfNGT4tKe2)@!np zToieGH8$H&CRMYV?w~yql*6qx$(x-@uXlKpN)h*L>GxA|Mw!Q+hBeVsC#5>{CuN7t z4v{bq>2K84IYPS&JxVzH@gtBD2yrC;|NO-b@@E^$%gZ-9KB^tO z>F@vG!2`jyvaYU@goK2QA5!j9{*@M-?CdkcdK<3nWn+^!F)_)Ve|>SVsj+d^bz-&D zLiqATr}X3{LqkK0b+!*5KD>DGf|Zq3TU&dhqdU#}_wN^$m#dkT+3(HFxCjUcP~GhP z@x!~Xf>v93X=y1lGxJ)jW1WYD%VmO`bkd`w^mJ*bZok;r*pLwFoX*P1bAEhz+1YOH z?u`u%ye7r%U0o(@cOMz5CH)bYzf4qhw`C|7qYAxTY`&{ZXGc#7TkNHZb!gY*U{lg+7=g&>7%8|F^ z*fp27?Giu#C?+OmC2F)e<5i3O>C<07d^p};n`mahc9-%Eleg_*PRthjvrleDa1pT=}8uK!NLMgUlwkXFYTxl-{x4a}QHf zQ}5oro0m7=Y~rhy?sEBZZB32$V9e#?!HlBz-(LFp`8|8~Y`nIAdNI~_ZE@jeZ*!X7 zz7n6&ru3kl0ueEX6Q4g@!+ZC#oconql#d0Ah>6{}aYNt0V038cHiM8^M}ezZW`1U7 zeMiUYM;71qBG0^_pz31p<)@m%H(7j!Mn+OHGGwhjJ>WCQtyoegKX>jN#pcbzLPDpU z!rk*BwE7gs-&asr=sWqAOgWd`e>*)r{vQ(q!-Izp7Z(?e z3NNWuUKsCm-ihTKA0Ll}xtr?59Cb~<^L6>NZf^N6UhuQN|Mue2c`P<|e}!YmPVs3>&=oN! z8oSDF#3gE}pLt=JoRq4WEW*b2uBGK6O_s~>7j@ZHt;z^-$IgcO`pmpMbwx$p!b|D# z@vJw?XoJdcQJtEP*x^3gsDtO6Yso;^Lm3p2=Fu>V;AwNk;$>d@CTLsF}VI>-A7AgmR zBpp#o{8B5s-rm|;@a);~N>s2}L)4Vek6mw{GnHj1#UKaCNaa%OH%_RQf&6_uAX=Bf0eI$I>R#46J4bOXcES0Ry zW?(xXu(fEN%>zAj_4SwfBTaI1bBzmJ z0?T@4%qIlj@AW&&otmD`;r|9(!>CDIOpH$c_-zJOgQJ!fmwgwjO~_d?pkaV{FVK`qc3d=Je;!b#!#BZEcmw@5XXEI67u1clB+|fAZu>dV0;LPxe(|`%gbp zZOO9S5?kAq>tNvF;o;@=2AlQXai6utL2B8RP~|8*oodJK@$r*<8Yb5FCLVQMtZJq*_otgw15BphYugFev_mWdf@-t(Oo;3yL{JI zYu1m&@VT=77+w~;bLUP$K>-SjQ3~7Lq23VL7bz)OFJJEUmvWnwJL54kbP9_rF_B}3 zIcoRwH1?8*x4b1?hC;7jKc=J1ofs*60P&GMSPiQ)~Vd z+rlnuKc3>dcJ->Qt?l`af*i-LV%mMj**Q2Yt*qvTMeBIPqSeTglaueJ$49i-=iZ~6 zt*fa?Pf4k2ZWia^Nli*hV)4AmN=ia`bk}VCFcwP8S}!ip@y*X03DX_p#%?{=(QBfj zqOjO?eY|n`xXyu34|@j&j*~Brq-I!f*tXyuJ(YZJW!>_Q7CA@9+SmQ|+7u6ql-ph! z^0Be8FcD2nO}235`LPyY5(isb4}62%TroYbs%NtW`S~>RcUWZk?YvN1Qgt#SV)AB{ zb#&%`JB&7!B9;2|sl`NR5tihUBS)$VTl@8H<*J@M$t-@}(9SOP$&=oZ#?)k5v=xW$ z3=;S8@o`nogW}?m9^CXn)6>&xs;U4SVPQ$42dzJg96Z=o;#(3Qe^yIt`)rJe+hbx^ zw#4sb6wT>-d3kwy7Dh%k_g6=vW-#sCX>DQg)MH;;u7jleRNsxQOfQU!BvA2y2v7i5 zm**u99eV%a10y5j=f~2^b@4}Wy%{BI9_P*tqis1jIF!A6H#0j6Y_f{vy|=|HzCoZRjsWOY;0F+2UQX^qz zt{$1wRaYly{dso_ds<$nN03DN*yt(Et4r^wE%BDPt!O1Bp7Z?L+6~$xT|a?u#l*z? zL*wH1_8W}1=BS970HmG8-FiL{%OjyM#m6(elA2L00UXd$CcCy^{`Tr`4IG;|@4vQh z5pF4ox_56S_z)_yk&%%eZ&PEc&hiDpvlXU_jTd4M6Zo22TJ%gz)+0++EcfOe+#aYP ze!d+Y1!bnAHvSk}J<2H_SmP?AJzDkAqh;SR&67h%O}bj!+9ujxPh+2+dU8CNS-PP5 z!!5=|)T0HjwFg4xy1WqzY{{vqOwt}MAvz{)ZEb)jhj*Wh<)==%J7T1rWod0~ee~!x z1-2fiLs^0C+jHKKTKLPehJ1XLnK|r}ZmdKh$NW1fLLDR!+1ZmnzQ;L6z$PqRY z2z3M!%Kw|c7^3Wc@PNVB*LP8rE|6VXx}&-}sI14$(~|{Y#E--Mch17dxCI{)Teqzg zFiAjQqV&o}Vqslb0;(mwmfZaPW zRW#pO{rU5GTU$1NH#aw@*M_#<*yq=;Uw`ssUu1P-W21we-Nj3nIQ`oTFK75B@-JIv z>Iw-7Vf9{<&U;T94#xu4IH}ixqHHSajL)J9jXhShvg`?5IHM9KYx~J!)tqQ zZ|RVaqbDjmJBvS3;SyzKWiMWwe)MQJa9++<;*GY{7ulk}mxe|jz0A@s$?}R^U5RFh z=YBrE+3D$5Sy>16@82bPDUFj9EWD0~@w4BKc>WVX3T!QmBh%*s6~2^c&KreAMEETB zN2aHzNAeqecREzH{3IhIWJ?3y{?u~Ve!k77BS#GvgT6HArCqslWL>%A@`avvzkx)^ zD=I51TUkAuT61x6p!uT%RH*~fI2K5O3OAbO*#RzGFGNv ze(>PX;ls1U-}#4f9j3v(`%ye7yG#5r68m%Kb}=yM8XE52u_OHc{a3)vRaMF(%g0uE z_P5@Ri7`ScNBQ;gS}~=fArQJtp3V?4^K-Rm&JrSK(|u`5;+h;&X~A!hd%Jd!`YC%z zr3H}`=#P($xwyJw0qA5HYd1)HP=9{41>eo7j`$Uc-}J~fGBqaITRS&jmtwB{@Zs6A zLdshB64~_jU-#fYzAnh>NHxQ*U55`HO3K`HZ9gmPm2rM4&%a)iy=jd~liZgb5A4{s zz_?@gUt-ZdX8BW|h?jjV;|=!M1U%i=+Pbu`fVHkM8Y|^a$HFqv({t9&Zgg<)^4f}f z%jp9L4y>#-17xx zC1qtsaJBZ=7aiO4X&D&YzQ3nP){+2`ijlm0XOHBi`h;UBMh`^onp$32CqH+3-3~t5 zVt6MZHsPMy(!}ScrlxoAuF_uigVAHFL7f*LI5*1%ZJ8 z01pifL6eZLkeqnaJ3hXKVl2pT`;~R=hw}1MgBhSmE|)HyR8@T{ys$JoigJ7S@ZpUu zv*`wT4MMm%OFfhfOJg5`nfybEn3$SK73}Rn13dbxcE~F#Dw5|suCWr(eHjM<;MJ@9 zG@7SQ37vhnX~*SyREF)_EzHb>|0LF&wyTg4zJB=vc60&ShbEWQ0SXDb4qDO3qm+a8bv8B8GcZ(`3M%GxQKII| zPjqql8!0a1BN09UErW2VV3o2*(1;atY zL4_TQW}9y9;3-~T^OUcMKYDX+yhDZj9LS&h6g#W`$KI_>Vri&jiHZFH{PUBO3*&_o zZ2R`H$oi~wdQQxKTdTOcFT?P)u-Af~mj%1WVSI>`1R&SeQI} zS1~LsOd%$$FhAeEs~BLcsk76#>?R2zZk?vI&KJB_SX5Nneahne`4>e+tAIaHP)?sd zjkdFA_wM4Q5uUq>3gkk9f^JiNl_}b(^t`%)j-68S6D{=sLaUjW}Q3tQG_f zOZXB=BMiBz>?SqO@CQ6F3Rd94V!({CmRCg8NjvpSvNyKN$aR&vI$vNPkO&nerAiC@Q60<_bO8yJ?Jq@~?ndapAVO7rE*7ck4DiM>J$lvm^? zJ1%!EE-#M`568&*`T)0~o@PCNB;&1r;>1UMe0Pk}KVV~g!;|E16>JD~&I^>fHJm5C zEoq7aQ~iTJx~NTo>uy%Q(6czJn6MYYw{ zGhV)QYJOoPd1;J>eAAgTXUM2_>w-J60)9V>i!+@aY2-PRw%4D5iRoi`xkFd6Q9XZ> zUek8Y`}@~J>1b#|!^3mH+T-HlAZ+|-%^7_XA`^Acnu^upcPwX;Yya>#DYc-#>TT(k zi9zGGgX+Q3?lL`AKAU?^-YoK%?0$>lch1hPt^e6Q2yVcuy8*4{pNoi#Z`pJeSe1Gu zV}c*G0eF+0j^buneRH#pV=v(-^|iMQrUY4?iO?v;O^-;9uIuUQYFxG&hq_zxafZVm zgd0D$2?qfraBYnW)dWJ6pOWr>inHQUQ2*Y4{OIZC_6=QOaS`jl_)JPt(&VxU=)$>q zdQc8?b92uQ+WVDH#|P?TprV1hEJ<%75PrlPk)-wU8MEDe`uw@4yZePhyFmj+22DNr zDo_9HVDW%vIam{&cX9ADxIDjp_7KW0Hn)W59Itl2^K04ll~=DH@tde71Sqfpm{%2V zSDVkv$w?eAa%|0}=-Rf?&pL|UduyOakSqu-Br@wy5AH@sOWHN1LFImU(3(!3Zre7Z zTF}!&yM4P0G>w-p1>-gHp-uL4xF0B9rmj;N7tk?Y)c)#u%Iwi-LrUt%L}~Kr=b%_% zWG7;x&$s8-x3nA<61rQ}4H(|`?OR`8Uy^r;5jK;`*Fpk;VYj{u)dkcU?9J-V?-N}m z6X2i&1D8+@=H@PK*|O!ti4**h&|%w}ng+(kRLNgoo_KsuGjdLk_l&Bls;zCxt<~W! ziY7uyn~W3V(xsKI^|g6wnT5EltS{x|*Y&6KN>=$KB@2s+jC6D^<6R+KaGiXl(HXEJ zOA=WPD*6L;b$V*5;1T~phTCZ29VNc&?*q5d1@`4M8%siW&&d&GwZNBGiR71tRD`{B z`t<$i=(d%`Y1|yF5tswIdV1$fO}mGOq1_5!_^yz{vdJ%gllb?cZEA+jUlUa7bY2*; z`r}7~y2XC(>Tg?s(Xawv8@|xsrlh8BZfxWrZU{0mG7!7=Vs8T!$l`p%7tfzt%gM<> zrx-XPwp~N~#fbCrfj|Z4^)+w#3W(<{GG0T&!>D62mQ`VCX}taE7l?SY1U)S~JDXYH zjPJ_NkCv8}sb|uopm(8Qy!2W1u=NJE^Y$(RAKxPIMvnY~vvXE%F4Pf^=^AnLo88c+ z>FAt$-d)F2=^Z2|k*hve&tb#i_|w(Z)t{#2^-flQiJJ6`jKQC7x0L7R=A_31Jmw)$ z)>c*yV`UaF$@)mK_JL%3&X05Wi-?NGNV(smdDr_BYrCnT0VH5*X*2`7!fU?cG6Z%M zAD@Y$c@O)P?Cgfx+S;nBl>Gb$P_*)E{W`)La%>xk8dq#=2{aolkKAZx@EYBtix(%*>=6-a^nn8MHyS(m={ppLhM@cteg`phCh^ zU}0`z)$etpqYOt*r-9x*(i!u(QKJ zDq8p+WRQ10T#>!Mzdy@i!OO?hHD@f|E4iQ`s&;T*SAL7lS(`jTr&htuIeC+xB_~{b z2Pb6au6#{U()AC8uZ6Y${kvJ9LKd`IGP0W}+@Qe|K)1JU-{w7VVCv`3_*^NE85 zn3Sx2U~(K9TIu3qgkanQC9%4?8ikd2U4cHRe`G`-cgLn4B`d+iXR*AwInQ&MlGGd> z$RO7~p817Qp%_pifVskX#^ygvtmQ4YHI0l`p~a)Nb^*A9t2&mf=02k$DSbtK|K*ZJ zfciT&e>g5|TwKpzzkUr=2L6uakJ68pW-k^^1Msr1?Vdg;l(KS1+vSTFQ&Uo=W@l@v zs(|pn%sAb+=es&;LP>fpD(xx6l&hmP zWI8J=tL6C#RDj#JZqaSNzH!39*?9`h9{vviBxsKJ%EATt3OHY+ox%ja0h?0<9`~Tj zmoHljKg2~gM3`kBl9lZO->$4wQc7Ot7x_89{^)_QWfiuO?9$K^R9^sAz;9p!vQwu{ z`G-Q~hFo~#`gJIfYYV+0P*h|go4L4TV_zmTp%{Pf?ZsZ|?d@G|W=Z*p^|WOt|Lwof z0#uYirN5?HAY^zAM0;Q3{q^ z?k5Z3eiqFQ8#ZKRW#u}wVcWU@L7@wH&(xm)Hf8mv1io5bnuVUlw2(wX5IIpOvyopw zpbIx0evd*?`9y0@kcT>#Kj6d(1qHaS@(03&AAW+UAW*#U?%OwAccsTGMtc?5>LHn9 zDO6Qg7nUrac6wgMEbYCNotb$7u0w}PO-;>)E2j9Lj8QM+4F4waG)M^$G&aPN%m;GP z9br0nywc!}26x|2H*hV~qLj!bGKe`x88+mri`*sGccIWf{s%nyoxaZG06WAvWZTfu z!%v^y_65f8M*Uj z+0VH-8Rx!$F+FAFso!xDnK?oMgrfQvggfwIf`j+ScrAQMR58%k?*!3Mms#{bzyZJa z{d*8V;76=FSb?{1-!@j6Qb4ugWq$J(zt8<4G{)@9$phiVlkc`by#sUNx%Sp=W{5Vr z21<213IQ0?`LkzxhK4Hoow4ariQqC*u!5_f)*SgF19K1NfSQ_irYz+R`aQb|gjq8| zhHd8RbIZfcbDun4bh3&PpA9K*xH1P_U~ur(Yj5}ZQ?kgkx3@n!fEFFE!NAP?A~jWo zTu4j|w%2(Zn|$x(+9o69P3Ujt5!~=lxwYG?TCvrXew-^L7F9 zfi3Oi(^nvecDTDdy0V6jk;vK@i zBpftN)>>+{%lPT`20qOBuHw~39g`xcI$4(07IpZywkq?I8k-1|FLf zv-+t@#3G8l>4P}#DyF5UM@L1mv9SpsJQzMt2KC02yo^0fX4m)oFZqoM6v!V+UVdS} z)s-)*x<9MMUS3g6EjUeq4~YVf#PLF>f2i$s#GC1n2yM&GPKEri`&5bVU85q8a2jE& zPYkHWocA9_N2{do3@S&(Lo5P<0b0amp^F2m2M@l&V*~Lxe(V^wRZoGSUdtIH=@|*()2gi?#|Ev1kdAo}iin7%8**OA1%J1Lq;my48(z1<(nouoe-Y z*6ec)j9293$c02j!yS(DZP~PeBaxX&;2)In52R3!<8F(QP7;NTq53BfxT>2p#65NZ zX^Q>{mWwP8cpDfPY}vf|(C(8N(r0Jbg@iKSyfMsGb9EJst&Nlp6Q~rJUR=TbV^*;t zmKWEYI@wmSTSx59c4wd6ZtKV@AyKd_={uiE{YdQdL2N8K@=dd|vspscmX^nkAE)23 zV{u_Y!|o2VGzBc!SFCDY8Qc1RVtI2B5AgyaFkpcC+fc5iVix+Nk8vE9$u#)uFLU6vpXrOsWX%J&c&T!wMn;c8z4}PVa26Bd;zC*Ii)Ta3@3E>zj;rnm* zS3Spbq^BR(t`BcKY}2gwx-$dBUD4awger&`g`)7!;rfy@X@wsqVG zIQz!FtFxBX5w!ByjlX+o!O36SmzF$;Tgt-Gv8(qE^bR17*RS1Ap1cELo)o1qP6kxY zk55WxFs)TRXhZ(%*N9#@_g5iYBQBoDqw}J#zhC>z8ALG*{`A|JPFHWZBBzx6u?Q9# zKoGQ!9Xod-=ch(RPLQjix6+wketrGO9osPq%17|PVCmDc_ThG-LBg7cWmnTzc^^=( zy7y;N$ufY9z4^$*p+D%k))K-Epbbg)if=z==j4otjI7R0e(_>pdNJ$f6vv{aOst8K zQHMDNs!ev>3;LxKZO7xEUcYkR(Xc4}T+e8{FQ3AR6PJHy(W7P0tDrL>TYZf3!~^qk9e1}>U2!JchJ#kYG~BKF4?t9PfM$>xA$oH zzbH@ussC7@mah>D)U81aI(jIb-Me0{C<;-est-N#Z)|B99vl=$JP8I*-@w2N-Ap#b zQiX(wn#IV-=I9zu{|YNpT6rW)ev_{NhfLP|cMwTarxpc#Nx6}*1``F9tts6=+-GIs z(YLvk}` z27o^gp5duUJMf zguoG}>fvcXl;qyN8EOki5{UN!4)>|~c%&3xd(5U36l{g-tdm0OBqvvj;-ng*cfKX7 zyuDot9ykPEA)$IarXp`M;fI$lU6PIfe*#8W(kFV=Vnv%Kq2Y0=MP=L6v!U-ml3Ih5 zhic!D{wxAi0=)yuG1YN#JpUUJ#++vn1l+-M<~F0~2;7h&=&J}|C?^WATKxQ8u4sZM zw>He-2lok>IM=>q3akI*qeDNDYx@jG&L#2$?u5pv36rB)+1ZHS_2O3Gdx9?KS=BI# zMHksNrod!!pjjd#5Cqw0dyMNc;9y~!xZ=LS9mO&@3waq1-(jb2IRk^}fj?fk_>tjL zO-+~gd?tV`h)L67)3HFa-Yh;-YwZ7eJ-!LxXB zrWGTrl&FJx>~z{%T8|K%neXy#u3wxu4b|Oq;L|R(OVoIk&4{!g^!y8>O?0Jy)bkKHZmeE(JrX!`xv--jwE zrr4uny|$`}f|;_ov~-6_{G^Y>3|FjI!i&x{gS>Hws(^(?O?i2F2D-Y3BqW?)!wKy` zENte=_hx2JG>ehk`TVACuug<*Ndt&h} zRbSj)>yiBA>(F@`YyC1HVhw-ckd`ij3~=ur-J(pgkRQ*11DvW4>b(C|h6Q3C+G|&d zugNK7QH<-+KZzRIi95R=Mn>N3I!H?Jd;5HZW2j{HhoidZm(I=?A3Fbq?VZpB_kmWP zIH_Qu`TRA$r?T>`xp&|_D=Sga(eU3xixB|{lJTN`fX*8ZpAiy1g6S!FJbZk7z`u!! ziMy$|4jo!RIS0*yg$gT4gy$1@e{r@raMc8hbMQ;o4gzS0RdkG z&u?D8zKUuB4blYl1-rMQF&si~ntsj*GH4aGwO#nWussm73J(q4dsi{Zk%<_a`2a@H z527CT1^!k}T$~@QgVx0H%X`iWGW=JXLsIJXwffvs+~>}A_c7@>+UYNN44G^}O!(Tx z>iycX+VF^p1RO~d`1}b^o`fK1{rq{}r62mp?xTYo+a|rwA08hu;sx6Q#UbdI@_lc0 zWfETF(W6Jas=CpCucjkbc9>{sGGgUdb619k2!zoxWPj;M=#!S<&16RHS)M zRojah5Vc=uO@HSW4PPAo*nUvdp{pq_zCP(6ipdS+IgCqujQETWBffBtL#6NHPY5XJ zHIOY3vHLxtsW0$nLNhF~V8ZO5wQAd8?0h+sWX4j&q|kNNC)QVG7sDIM2brI|U>W!v zV`pp2h~(Gr0EgeCcm=jJ?lOAf@8U3qZ6FTAN`}pby708v15&fi3A7-{!g#9q=bxXk zB;AdW2c+LmsCY4h+RW83qA0jn!=f=CmOZ>#+?m51zv zMp1(-;&_v3rR==grgTkmb?#ie@&T!-cJA)(dU`E@1gmjEUOl~O!cj=4nG|>vB66iJUl46VX}EaA%a9Bs3Hs=T1DH=$jK2I9~O^#_{$%Typ*s#!M)wUH#9EGEIyB!pniStF~J4h*=Li!Sq(%X9#Ap|Az3l}cDcJANIYSCA5 z*FO|<7&WuR?yLxGH&`1h5Uz~!2EnHv^uu?mk_*Pj`eYe31*`44v?+mMiecl?%kv}+ za%d0GZ;*zWhPB;|A`e0c?Rye-BRO%9v18IT}8O08KRiX}K|0Exhka0a(hQLO=kk+a&1NkU5% z6s#*Pm79(sJ`P)t!yC^#IK+tBoq+)kpHz{(>houKBUonz#l>sgGrzvtNIxZU0w+0n z7-3EXvt<;+JEZay7K>!e9<+LycD%^Ygc)Y(KA(uBr zMZ`j_tn|3J*vH;J?L#3xH}2|@%?-s%efQN*#>%L5xCOyw{bzB`gYOS%E)-Wnh|`)N z`7}5kyk1sSb&Dno?z#D4+ljA$p+KuWaY zu~R=aGJ`dEk7QF@n?WV1Z3)*SU0y0`YHCVKs4rnPbKn12j|2PgTAup>d7I}zOIsUH z>z2O<6(;RY;tnlf>q9vqiYLRHEwMY%8IdhF%&`r(w=^=^!-^EuvD_O`JI|Py)DpYT z_TK~}s}Vm%-X1pnFYLy*|nx_;&K#LwnjoiKfU_yNq1Sh~$L zE+lTf1d#(qGok_VQtfe5mCL}ax zQ4;IpQ2WNZ|5&xDAnBByqS1}Rp}ljDc8h%ZkuNPPAeMs`ZBxI?>DaOB3Tz-dpAp6b zNYl{L($dxzJ^Rl8^z)OMf0zjN=EPtv518xq=x76N?eL5YN8C!>@@FTA!fycmZ~;h6~*Hfeeej zE|~tem(II(?ZOtkNwr79ZF2AZO2b@xJy78cj$>j-nu4T9M9|aGVK6UcaU4`;h5Exs>exsa*Fz5_JFl!|PrUd@!g5%>vaK z4Q{lz_q2w_ULK`?F|PZEGbt)VEEYR#bO;z0#yn3jx*eur;UO4E z8n!>H4PP=c@^Mcadij-ARKPos{=}f_;oWyg$*{KK9zdpxc!<6uFaHkM^y0;fc@AyI z$${?uX<*B=9X}9GC(lk72w6NLc?K8C$IY$5;k`>*NmZ3sKp@iLw2@KG;It)mzsgj# zmrMyl93ScQ<+e;EUio_CRMNM1rLMzA*Xw+CYdN0nZtz^m`LQE0f*xSMuw?o&%W|nu zQvMft&Ey#b!V1Cr(LqZV2cl7X6B5|K%Ts{<>M?hPG{>}?)G-d?paiKNajYXVHy5)E zx1jre`lQuKLVDDbnu<~zlBU^MrH|}t>tpxYs-OtW(R>=WcA_Ued}-H!+?}?I&UK1; zy>lyXzBR}b!{*kd=$?pIG^FwTa`I#`VaBD7V9N)qR}`$GqM}*B$(CZ#>BRWZKg9Q= zc0)4o6UNf;4@Kl*qGa88jg{2`VEJD_``=4Up6oySly3Z|QDc~Ti01;-gizp-kl`(n z{uLGz>wI~(43BmlTqPX>9PkmM9Ip^R%g%;I#fzxYty@)Sx~+{f`(-N{H# z4-N@o`9y+cQbCN37#Os{Cqs;#os0N`Y>fZ)>(J0N8=*o$>xK=nN6Ibq*DwI@{{6|# z_t*gaKYjjuQblD81;qtd*T%8mmv91s(;tdmJCt%nU5IliAW18cF4%eq6<+^ie1-_r z6WP_*WWuoFkgsWMm%|1K`}%_LO!wE%j4tdtXD6p2JA7%mHk*2D;@j6!|KE++)P3+g zvQg?NMm3=1UA5kV;(&+G&Ux}NjUoC=b-;X6@IF8pC85R-aSm;SW?}5 zW!KE#PxOV-;j4O052z(;6@bi*#KUbvFj7rzE31EX|K{uOCMS{LHu^>KoSd8hb&&uH z4+|R$s_ut#NxEqhGkjO8W~l*_}U+1@;v`6{dI-q#!0p+7K>kX*pGmStl_ucmkI|KL5S!VQBcN$4Nw# z7-31w9-`EJ1_@yA-o03nf`U$v`}<_`wCUh)IwZBRE3wlddBAsi7$intLnBUkNtl=k z)HOEdZ+rGDn^((d}U zh57^nk*Xl$a1iqc2yIrWr?C0w7Zx6T_z7a?NW3oZ=h|9vm>AB^nt-E0z-mzsAO1ju z5>tf-4k&YPxAh>tlk^v)84)e(onAyx3~MylJ`wcAWPM5J-_BJ2nqS)E>MbcFBZCoM z4N@?wNm-0aVN64w9P{UqCg+gQg!>y#gZu;3jtfI|@z=Xg zfD52rhKAC>%d4Rzj$E1A18_$a$w``?!>ZSz6Ehb09hN_PPLB2eVbhd;1k z;)^0q1WvB}R~6)cBP8bb!DlOl$Sq>pU>N6%7=a!$Owp81$`XyWi9i@@BR?nzrFA#{ zd$sHLPv+bDpAQ%+gx`}wu|cq|W{wFrMpS?2e`mn3-cISUB0F7B`L$n?dnce-$$Y0L zN}FMpg%XAeSC;-OK-IdV^zY|htQEh9*UG*9#?70U2c(m~9UP4LclZ_vL8W8xnTUS= znk4*s4Lxn>Wa0R4MP7@}wml$)Q(SNVN=`8T6Lu2#2kf+3hLt%Kd?|*6=P#J_Cn?KJ z{_;f=V~3baf>Mkqm~D0&UVTf7iA2@t4*jo;gh1EtHROlJA2p=3$4RTj@$$dXL%+PE zU$XAztN41OHx4w6t81b3Tn~@(sVy6yNBC}O4Y~Gg-LdNLjO?#c^0~`tc5d#xqhpEJ zqVTLBj%fHRl%;?1KY*h|eiI9`f6_qRSr{n!qz^@v5gFG-|EZfnZ%u#ovB!pO2PgCn zFTknZ^fBG!pNq(({UZ{@w0B8vU(LBR*;8zcPKmHJkiLnX=yzjJ`WhlJ!QY? zWnrP@{Yn@=iOjdkzpt!#!C3*&K&}1p>CxhPTj6vZ=Vx)MZgn$-BSNO9wg=oH+sy5( z%(HPLbJ9=$GjbZY?w`_6zg2FQdB^Q`vs1EL2-)p`%?;bAKhS@CrTs?Zj+;gX?G9R6 z8giPf64|+Ck^U66O1cG~wW{KY!qH`!@#_0yh0*yqVPy>S$vQgXhyWxc$X_(ae+MI4MAFCa_IQ%@8ibdwlxv$Us{0NZ7T!T>_;%L)F zNKD?iWu8=mwuLw|%m#ekF}71sRAiqihmXu_Y~FMga-1vC@$5Fw{<5i^0Yk2^aMjbX zylot^&9|bR#RVVP0Q%ESpcBN`APXg))^x~k6Z(EB3A-u&h-xn})!{^-z>EDhDwg7p zcFXN)K{CN0t}}2o=;aWP%On8QBqMwRQD6lQ9&*WY9kb;QStlL{m`MK zCYVu#y!B)KEQR$DlTJ_nfkAP^@?MyfoJSr9#=whZJej)f>FTARcsq9NfIv6$Eps2O z>yOq(93ueZ3mUN6M1NnO2(D#h#8z!~J8mBf-ZNI(^SA;8f+{)?bT$tRjG7lBigO!= zHKwXCln9*4IK+-C*n?Li2Op@&z5Rg7w`O~HZ;(F*$xHUof^e!#dxnW@jSC$f4*&7$ z%y|Rk6Sei6F-6HHff<&V2rro~=sBgOegQbzWqrD0_xeY?soj@%EkS2D8HNm9%v326 zeyFPV&S&libh*_J7+^tGl`{eEI9RBdw?!YGtOiSZ$tDLgGkFE#3W|7{(K020R)#4B zS#TonZ$!m?Q9-VET=SU-~u7`7OsT^2Lku zFdU(*SV?X{$d! z&8_kqtWYlc1_y%_5=b3Oh-+NC^(1kn*0JfA5dGb_;D58c^NWzjw~^b3qXJn0q8Joph)v|2RNvIjuv93XR4_7o@nV?zbx3aQp9TvWB*~R!$^emD3 z$>;8g32lSxg1HC?OPG~{!-oXmwL!~`Op&8K+AK2xMS{0~wEZU%yUcDin4wdjb?_cQ-eeiB9kD?*lPA1@12V&>lJopRAax z=Eurs*_dU_&KAKK)TLM8dai4)!3q#eIyveb7{z&R+d&}_5f^X}kXQWR2|tGX1}&bR zPrpHP-_>=ypItf|eh9lwc9U!*^|B4W7a0jwR|HPEX#;c@5(DmVWuzjYh!yG?8WN>x z2Zx&Tn4JSs$kCWTJbn0rouCb6*$OgC3fWLVE=@xPQb6Lb_Phja@~9|euCk&5uz=#u zaBt@mWY}u^^S;_JoKJ`f0Lu8Qt>bHxJx4JohoKZJmuQ2L2TxUd51vBin}GqN9+}oU z7cOK1SfFxP-zG2uDAjo(lP&E!5*89tdwbWxKIg?N8_B*oCu>y+e!>)Oi`pe&9^cFZ zl9FHpQPIqK8-jleChpt7ing}sj%-9PVF@THQP5(*Km;Drp+ny)Dgx4B&EniRIR}UI z4<|~4IlmuQK>b27nwps*PDEZSthUI)87QroSP>T1+_&n$7ZqS&e(>&D2JmCLKsPC7 zTI4lq>gqHuN}pE`^^0G`JRnvSju^pOLQo1rAuUq~(tvK&e*S!?Bdod~1|GJ(r8qW1 zXOZV%U!Mgge};zgar)LtiQ4cFSC1>)=Ty5_-H#ifpmqB6D+D+YjdSzxpsR-`7FYq0 zjPYwMXpA}Gq>1|aQ+sU$QWe$S2M1sp8iRUf=H^Uo(r_yf#o*+8wyJQuQK6kNI?zkT z7Yi1rC@taP5krU`vm=nmPDYCbN0Veb82WA^yCdrD8Wb?IRs(Ikr4*?MlM&abJzdW-xL((2U!fF^dk{_j)%$8sFI7l9wg3^RT)KWN*$~tR;2`T)1juy{I zN(u2U*wS!99)Cp zb9#Qmiov{?QK8`SALe)zK*QTNU&rqf2ayjsbw`QXb(Z+5E1>>pCTGLdfcTHsKzezM z8#=I?UMp?h&V>}u}Uy>B`G2C z?D=z|U;xh;*fWa7i0R|3B%xdP)x-oj71ax03Dh4W&ik2Lk$O9iLwue*!4i!^)at{X zSAfoJn4ekxbj3z@`ukk$k<*D9+&J|tF*Vfyfenm$vq4Fa2qhF)$7+kuPD<97w>+@!W9|2=xzx-hjd&g(8`-qd1pr^{I<$9NPj5IJs2O_$} zMI7#JuSO&t(i;HBR+-h`YJR`P)18Jal9z4=pap1cNz~j=3q9uQ)eSh74B<)K3kgXC zMuGw`wI0R+;c&y1IONu+Kg6G_2{8D2#GHXsLLw>v^IzdPW0sAGM{eJ_6M^mieM*#N z#8K9xqaV`^gJSJ5dSL zY6P#IyaF^FJ|V^hG8vMMh-W%}dwC@1DXjl3^t?7NZv`L{jkpD#VT`so^g6`vkjw!P zd6ky->BENsjIUMc$7?L$bc%p-vtN}pAHWOK6-0IFjoVM0uVm5XZ5OE?+y?nCnw-(| z$uM#K3m8Abgfp-S1XN}T7aN>IDD5$`m)NwR79xRzJA^KUqX;pb+pH&bepP?PV%z5H zDJ;Iwcd+J?_20>~CwlBU&XnWUj&woCH&sMK5KAA+Lt6Gb)JS{GJR|i4ra!`LRaiz` z>kr@vufD=;SbpXKHVKcJB)PqllaoolB6F`y-ug?y7Iq zhez|3i-}1*aq04r!h!;%56?|(5Pb4jiE-DimiMY}Jm+5o1#K}X=f93)%vA9?EN-`R zy?L0jw<7w6J%H-%;K*~4z%(W&(rW;m6wOdb9N*g zPf^S`?#t%iF};nXI?;--q5Xe?`sJ9kAb;IelyDt+zZ|X0>oYgDt9f;UpD)-&_7M~f zSO)ih<;8H~z~t<#nuR`dKYIp)E#=Jh8#jJ6 z%$6DeoS%y;kdAQJD-Ag*)$V+=@>>|mghm0itFfs`nf(`;0JUqw@qJ~2*kv3E0Capt z1r5LxAA4~A*R@6rNER+6(;=t5g-QxIZ5Goa8_NVSGN$k}HMP2UV^T^AW?>NmsJGcm z&=*Q0K5TTZhF_axr!b^}j3?$Xi9xLh$~_Vym>A{avaSeaRp=WXZA(;ioy<62`l^ zR{3xoQ2^pRHDYyNzn&ID)*P>^QbTfnAbz9GHawGD+)8N^M3Qh~RX7K7sTeT8S+S^^ zRI973qWkv`qwPpBJ7Rg+ze3oANY79h@u)@{Q=8E)aT<-mU^h;mWJ>^_Qd$3jfK5Zg z0-=?h`0gK?nj!*VyQk~x!sRwLmN6|QuzkT%!*g>dVGRO2$ohEU3Ie&=*vxPM2c*qw zq+5ZVI`8f~5fD&BtG}Z#8^e%%YxFn|2?>qC-Vln?=WiJWz?X z4(G|Djp9({c#W4j(SeF=cR@OE;MDV{PhWBzy+P@|Fc}{bg40$5z!tr{q-ZlQ3%}9{ zht@F&di4D}rtluX5x{xAV`E-OKmmRt*4)TbOPD;BU%Z8qpl^KP!cR=|*CB7xuZnRv zS)BT48;w;BF|W!(*s>CEy3lp>L+%7{4Gt}Wp;#t}4h&^YLBxB><;Gb=77;PQ5)P<< zFv)fo=O|~M*qeYee7LERyoP|1+P+)Zq5{FuMhPbZ-_?xBjW(|=gj>*efQxa?W^10K zNuy^Ra#o;bhz8>AUlicHni?!+U@{zOgN7M(2**v-Ak&5{NQglyA&QKGA@#0e+)=Yg@Iv17>i-9v2+zX+ diff --git a/docs/performance/cql/ten-code-search-100k.txt b/docs/performance/cql/ten-code-search-100k.txt index 5ff2fe72c..df927925f 100644 --- a/docs/performance/cql/ten-code-search-100k.txt +++ b/docs/performance/cql/ten-code-search-100k.txt @@ -1,8 +1,8 @@ -| 100k | LEA25 | 395 | 0.70 | 0.011 | 142.0 k | -| 100k | LEA25 | 95 k | 0.44 | 0.022 | 227.6 k | -| 100k | LEA36 | 395 | 0.40 | 0.010 | 249.3 k | -| 100k | LEA36 | 95 k | 0.22 | 0.009 | 448.5 k | -| 100k | LEA47 | 395 | 0.23 | 0.001 | 437.5 k | -| 100k | LEA47 | 95 k | 0.14 | 0.002 | 731.9 k | -| 100k | LEA58 | 395 | 0.16 | 0.001 | 607.1 k | -| 100k | LEA58 | 95 k | 0.11 | 0.002 | 941.9 k | +| 100k | LEA25 | 395 | 0.13 | 0.006 | 778.5 k | +| 100k | LEA25 | 95 k | 0.38 | 0.019 | 265.8 k | +| 100k | LEA36 | 395 | 0.07 | 0.003 | 1488 k | +| 100k | LEA36 | 95 k | 0.21 | 0.007 | 471.0 k | +| 100k | LEA47 | 395 | 0.06 | 0.002 | 1785 k | +| 100k | LEA47 | 95 k | 0.12 | 0.003 | 861.4 k | +| 100k | LEA58 | 395 | 0.06 | 0.002 | 1705 k | +| 100k | LEA58 | 95 k | 0.09 | 0.002 | 1170 k | diff --git a/docs/performance/cql/ten-code-search-1M.gnuplot b/docs/performance/cql/ten-code-search-1M.gnuplot index 1a554eb74..b62236bf4 100644 --- a/docs/performance/cql/ten-code-search-1M.gnuplot +++ b/docs/performance/cql/ten-code-search-1M.gnuplot @@ -15,7 +15,7 @@ set title "Ten Code Search - Dataset 1M" set xlabel 'System' set ylabel 'Patients/s' set format y "%.0f k" -set yrange [0:] +set yrange [0:2200] # Define grid set grid ytics diff --git a/docs/performance/cql/ten-code-search-1M.png b/docs/performance/cql/ten-code-search-1M.png index ce05adec8ebb345b9d3911c810dd4f247a28873f..0dad3d0385dbbf68a7ffdc987204eedbf17aaa0e 100644 GIT binary patch literal 17942 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 MR3N5Z(EsDV0RSyW (if (and (= "index" db-name) (= "patient-last-change-index" column-family)) + (ring/response (plc/state db)) + (column-family-state-not-found db-name column-family)) + (ac/completed-future)))) + (defn- column-family-not-found-msg [db-name column-family] (format "The column family `%s` in database `%s` was not found." column-family db-name)) @@ -217,7 +234,8 @@ (defn- router [{:keys [context-path admin-node validator db-sync-timeout dbs create-job-handler read-job-handler search-type-job-handler - pause-job-handler resume-job-handler cancel-job-handler] + pause-job-handler resume-job-handler cancel-job-handler + cql-cache-stats-handler cql-bloom-filters-handler] :or {context-path "" db-sync-timeout 10000} :as context}] @@ -324,6 +342,10 @@ {:type "array"}}}}}}}}] ["/{column-family}" {} + ["/state" + {:get + {:handler cf-state-handler + :summary "Fetch the state of a column family of a database."}}] ["/metadata" {:get {:handler cf-metadata-handler @@ -386,7 +408,36 @@ {:handler resume-job-handler}}] ["/$cancel" {:post - {:handler cancel-job-handler}}]]]] + {:handler cancel-job-handler}}]]] + ["/cql" + {} + ["/cache-stats" + {:get + {:handler cql-cache-stats-handler + :summary "Fetch CQL cache stats." + :openapi + {:operation-id "cql-cache-stats" + :responses + {200 + {:description "CQL cache stats." + :content + {"application/json" + {:schema + {:type "object"}}}}}}}}] + ["/bloom-filters" + {:get + {:handler cql-bloom-filters-handler + :summary "Fetch the list of all CQL Bloom filters." + :openapi + {:operation-id "cql-bloom-filters" + :responses + {200 + {:description "A list of CQL Bloom filters." + :content + {"application/json" + {:schema + {:type "array" + :items {"$ref" "#/components/schemas/BloomFilter"}}}}}}}}}]]] {:path (str context-path "/__admin") :syntax :bracket})) @@ -454,6 +505,32 @@ (ring/header "Last-Modified" (fhir-util/last-modified tx)) (ring/header "ETag" (fhir-util/etag tx))))))) +(def ^:private bloom-filter-xf + (comp + (take 100) + (map + #(select-keys % [::bloom-filter/hash ::bloom-filter/t ::bloom-filter/expr-form + ::bloom-filter/patient-count ::bloom-filter/mem-size])) + (map #(update % ::bloom-filter/hash str)))) + +(defn- cql-cache-stats-handler [{::expr/keys [cache]}] + (if cache + (fn [_] + (-> (ring/response {:total (ec/total cache)}) + (ac/completed-future))) + (fn [_] + (-> (ring/not-found {:msg "The feature \"CQL Expression Cache\" is disabled."}) + (ac/completed-future))))) + +(defn- cql-bloom-filters-handler [{::expr/keys [cache]}] + (if cache + (fn [_] + (-> (ring/response (into [] bloom-filter-xf (ec/list-by-t cache))) + (ac/completed-future))) + (fn [_] + (-> (ring/not-found {:msg "The feature \"CQL Expression Cache\" is disabled."}) + (ac/completed-future))))) + (defmethod m/pre-init-spec :blaze/admin-api [_] (s/keys :req-un [:blaze/context-path ::admin-node :blaze/job-scheduler ::read-job-handler ::search-type-job-handler @@ -468,7 +545,9 @@ :create-job-handler (create-job-handler job-scheduler) :pause-job-handler (job-action-handler job-scheduler js/pause-job) :resume-job-handler (job-action-handler job-scheduler js/resume-job) - :cancel-job-handler (job-action-handler job-scheduler js/cancel-job))) + :cancel-job-handler (job-action-handler job-scheduler js/cancel-job) + :cql-cache-stats-handler (cql-cache-stats-handler context) + :cql-bloom-filters-handler (cql-bloom-filters-handler context))) ((wrap-json-output {}) (fn [{:keys [uri]}] (-> (ring/not-found {"uri" uri}) diff --git a/modules/admin-api/test/blaze/admin_api_test.clj b/modules/admin-api/test/blaze/admin_api_test.clj index ac51e25a7..d3629d89e 100644 --- a/modules/admin-api/test/blaze/admin_api_test.clj +++ b/modules/admin-api/test/blaze/admin_api_test.clj @@ -4,12 +4,16 @@ [blaze.async.comp :as ac :refer [do-sync]] [blaze.db.api :as d] [blaze.db.api-stub] + [blaze.db.impl.index.patient-last-change :as plc] [blaze.db.kv :as-alias kv] [blaze.db.kv.rocksdb :as rocksdb] [blaze.db.node :as node :refer [node?]] [blaze.db.resource-store :as rs] [blaze.db.resource-store.kv :as rs-kv] [blaze.db.tx-log :as tx-log] + [blaze.elm.compiler :as c] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache :as ec] [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type :as type] [blaze.fhir.test-util :refer [structure-definition-repo]] @@ -50,6 +54,7 @@ :kv-store (ig/ref :blaze.db.main/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.main/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} [:blaze.db/node :blaze.db.admin/node] @@ -60,6 +65,7 @@ :kv-store (ig/ref :blaze.db.admin/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.admin/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} [::tx-log/local :blaze.db.main/tx-log] @@ -116,7 +122,9 @@ :max-bytes-for-level-base-in-mb 1 :target-file-size-base-in-mb 1} :type-stats-index nil - :system-stats-index nil}} + :system-stats-index nil + :cql-bloom-filter nil + :cql-bloom-filter-by-t nil}} [::kv/mem :blaze.db.admin/index-kv-store] {:column-families @@ -199,6 +207,8 @@ :blaze.page-store/local {:secure-rng (ig/ref :blaze.test/fixed-rng)} + :blaze/scheduler {} + :blaze.test/fixed-rng {} :blaze.test/fixed-rng-fn {}}) @@ -418,7 +428,20 @@ ["content" "application/json" "schema" "type"] := "array" ["content" "application/json" "schema" "items" "type"] := "object" ["content" "application/json" "schema" "items" "properties" "dataSize" "type"] := "number" - ["content" "application/json" "schema" "items" "properties" "totalRawKeySize" "type"] := "number"))))))))) + ["content" "application/json" "schema" "items" "properties" "totalRawKeySize" "type"] := "number"))))) + + (testing "cql-bloom-filters" + (let [op (get-in body ["paths" "/fhir/__admin/cql/bloom-filters" "get"])] + (given op + "operationId" := "cql-bloom-filters" + "summary" := "Fetch the list of all CQL Bloom filters.") + + (testing "responses" + (testing "200" + (given (get-in op ["responses" "200"]) + "description" := "A list of CQL Bloom filters." + ["content" "application/json" "schema" "type"] := "array" + ["content" "application/json" "schema" "items" "$ref"] := "#/components/schemas/BloomFilter"))))))))) (deftest root-test (testing "without settings and features" @@ -454,7 +477,8 @@ (with-handler [handler] (assoc-in (config (new-temp-dir!)) [:blaze/admin-api :features] - [{:name "OpenID Authentication" + [{:key "open-id-authentication" + :name "OpenID Authentication" :toggle "OPENID_PROVIDER_URL" :enabled true}]) [] @@ -465,6 +489,8 @@ :status := 200 [:body "settings" count] := 0 [:body "features" count] := 1 + [:body "features" 0 count] := 4 + [:body "features" 0 "key"] := "open-id-authentication" [:body "features" 0 "name"] := "OpenID Authentication" [:body "features" 0 "toggle"] := "OPENID_PROVIDER_URL" [:body "features" 0 "enabled"] := true))))) @@ -545,6 +571,23 @@ [:body 0 "liveSstFilesSize"] := 0 [:body 0 "sizeAllMemTables"] := 2048))))) +(deftest column-family-state-test + (with-handler [handler] (config (new-temp-dir!)) [] + (testing "patient-last-change-index" + (with-redefs [plc/state (fn [_] {:type :current})] + (given @(handler + {:request-method :get + :uri "/fhir/__admin/dbs/index/column-families/patient-last-change-index/state"}) + :status := 200 + :body := {"type" "current"}))) + + (testing "other column-family" + (given @(handler + {:request-method :get + :uri "/fhir/__admin/dbs/index/column-families/default/state"}) + :status := 404 + [:body "msg"] := "The state of the column family `default` in database `index` was not found.")))) + (deftest column-family-metadata-test (with-handler [handler] (config (new-temp-dir!)) [] (testing "search-param-value-index in index database" @@ -945,3 +988,41 @@ (given body "resourceType" := "Task" "status" := "cancelled"))))) + +(defn- with-cql-expr-cache [config] + (-> (assoc config + ::expr/cache + {:node (ig/ref :blaze.db.main/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {}) + (assoc-in [:blaze/admin-api ::expr/cache] (ig/ref ::expr/cache)))) + +(deftest cql-bloom-filters-test + (testing "without expression cache" + (with-handler [handler] (config (new-temp-dir!)) [] + (testing "not-found" + (given @(handler + {:request-method :get + :uri "/fhir/__admin/cql/bloom-filters"}) + :status := 404 + [:body "msg"] := "The feature \"CQL Expression Cache\" is disabled.")))) + + (with-handler [handler {::expr/keys [cache]}] (with-cql-expr-cache (config (new-temp-dir!))) [] + (let [elm {:type "Exists" + :operand {:type "Retrieve" :dataType "{http://hl7.org/fhir}Observation"}} + expr (c/compile {:eval-context "Patient"} elm)] + (ec/get cache expr)) + + (Thread/sleep 100) + + (testing "success" + (given @(handler + {:request-method :get + :uri "/fhir/__admin/cql/bloom-filters"}) + :status := 200 + [:body count] := 1 + [:body 0 "hash"] := "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985" + [:body 0 "t"] := 0 + [:body 0 "exprForm"] := "(exists (retrieve \"Observation\"))" + [:body 0 "patientCount"] := 0 + [:body 0 "memSize"] := 11981)))) diff --git a/modules/coll/deps.edn b/modules/coll/deps.edn index 233518014..ea44c6e51 100644 --- a/modules/coll/deps.edn +++ b/modules/coll/deps.edn @@ -1,4 +1,6 @@ -{:aliases +{:paths ["src" "resources"] + + :aliases {:test {:extra-paths ["test"] diff --git a/modules/coll/resources/clj-kondo.exports/blaze/coll/config.edn b/modules/coll/resources/clj-kondo.exports/blaze/coll/config.edn new file mode 100644 index 000000000..626f56def --- /dev/null +++ b/modules/coll/resources/clj-kondo.exports/blaze/coll/config.edn @@ -0,0 +1,2 @@ +{:lint-as + {blaze.coll.core/with-open-coll clojure.core/with-open}} diff --git a/modules/coll/src/blaze/coll/core.clj b/modules/coll/src/blaze/coll/core.clj index 37957cfcd..e2b0c0582 100644 --- a/modules/coll/src/blaze/coll/core.clj +++ b/modules/coll/src/blaze/coll/core.clj @@ -57,3 +57,14 @@ (.nth ^Indexed coll i)) ([coll i not-found] (.nth ^Indexed coll i not-found))) + +(defmacro with-open-coll + "Like `clojure.core/with-open` but opens and closes the resources on every + reduce call to `coll`." + [bindings coll] + `(reify + Sequential + IReduceInit + (reduce [_ rf# init#] + (with-open ~bindings + (reduce rf# init# ~coll))))) diff --git a/modules/cql/.clj-kondo/config.edn b/modules/cql/.clj-kondo/config.edn index dc262ed13..ca7000645 100644 --- a/modules/cql/.clj-kondo/config.edn +++ b/modules/cql/.clj-kondo/config.edn @@ -2,17 +2,21 @@ ["../../../.clj-kondo/root" "../../anomaly/resources/clj-kondo.exports/blaze/anomaly" "../../async/resources/clj-kondo.exports/blaze/async" + "../../coll/resources/clj-kondo.exports/blaze/coll" "../../db-stub/resources/clj-kondo.exports/blaze/db-stub" + "../../module-base/resources/clj-kondo.exports/prom-metrics/prom-metrics" "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"] :lint-as - {blaze.elm.compiler.macros/defunop clojure.core/defn + {blaze.db.impl.macros/with-open-coll clojure.core/with-open + blaze.elm.compiler.macros/defunop clojure.core/defn blaze.elm.compiler.macros/defbinop clojure.core/defn blaze.elm.compiler.macros/defternop clojure.core/defn blaze.elm.compiler.macros/defnaryop clojure.core/defn blaze.elm.compiler.macros/defaggop clojure.core/defn blaze.elm.compiler.macros/defbinopp clojure.core/defn - blaze.elm.compiler.macros/defunopp clojure.core/defn} + blaze.elm.compiler.macros/defunopp clojure.core/defn + blaze.elm.compiler.macros/reify-expr clojure.core/reify} :linters {;; because of macros in modules/cql/src/blaze/elm/compiler.clj diff --git a/modules/cql/deps.edn b/modules/cql/deps.edn index 89fefe625..4aa5f5e73 100644 --- a/modules/cql/deps.edn +++ b/modules/cql/deps.edn @@ -64,4 +64,4 @@ {:mvn/version "1.2.4"}} :main-opts ["-m" "cloverage.coverage" "--codecov" "-p" "src" "-s" "test" - "-e" ".*spec$"]}}} + "-e" ".*spec$" "-e" "blaze.elm.compiler.macros"]}}} diff --git a/modules/cql/src/blaze/cql.clj b/modules/cql/src/blaze/cql.clj new file mode 100644 index 000000000..85e543b59 --- /dev/null +++ b/modules/cql/src/blaze/cql.clj @@ -0,0 +1,7 @@ +(ns blaze.cql + (:require + [blaze.elm.compiler.external-data :as ed] + [blaze.module :refer [reg-collector]])) + +(reg-collector ::retrieve-total + ed/retrieve-total) diff --git a/modules/cql/src/blaze/elm/code.clj b/modules/cql/src/blaze/elm/code.clj index 1b0fb3272..4b1259bd0 100644 --- a/modules/cql/src/blaze/elm/code.clj +++ b/modules/cql/src/blaze/elm/code.clj @@ -26,6 +26,14 @@ core/Expression (-static [_] true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) (-eval [this _ _ _] this) (-form [_] diff --git a/modules/cql/src/blaze/elm/compiler.clj b/modules/cql/src/blaze/elm/compiler.clj index d823f0211..6b178a421 100644 --- a/modules/cql/src/blaze/elm/compiler.clj +++ b/modules/cql/src/blaze/elm/compiler.clj @@ -42,5 +42,25 @@ [context expression] (core/compile* context expression)) +(defn attach-cache + "Attaches expression `cache` to `expression` returning a tuple of expression + that uses `cache` in order to improve evaluation performance and list of + attached Bloom filters. + + Otherwise the semantics of the returned expression have to be the same as that + of `expression`." + [expression cache] + ((first (core/-attach-cache expression cache)))) + +(defn resolve-refs + "Resolves expressions defined in `expression-defs` in `expression`." + [expression expression-defs] + (core/-resolve-refs expression expression-defs)) + +(defn resolve-params + "Resolves `parameters` in `expression`." + [expression parameters] + (core/-resolve-params expression parameters)) + (defn form [expression] (core/-form expression)) diff --git a/modules/cql/src/blaze/elm/compiler/arithmetic_operators.clj b/modules/cql/src/blaze/elm/compiler/arithmetic_operators.clj index c63af970d..0a7cfc18d 100644 --- a/modules/cql/src/blaze/elm/compiler/arithmetic_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/arithmetic_operators.clj @@ -6,7 +6,7 @@ (:require [blaze.anomaly :as ba :refer [throw-anom]] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinop defunop]] + [blaze.elm.compiler.macros :refer [defbinop defunop reify-expr]] [blaze.elm.date-time :as date-time] [blaze.elm.decimal :as decimal] [blaze.elm.protocols :as p] @@ -115,22 +115,30 @@ (p/predecessor x)) ;; 16.19. Round +(defn- round-op [operand precision] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper round-op cache operand precision)) + (-resolve-refs [_ expression-defs] + (round-op (core/-resolve-refs operand expression-defs) + (core/-resolve-refs precision expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper round-op parameters operand precision)) + (-eval [_ context resource scope] + (p/round (core/-eval operand context resource scope) + (core/-eval precision context resource scope))) + (-form [_] + (->> (some-> (core/-form precision) list) + (cons (core/-form operand)) + (cons 'round))))) + (defmethod core/compile* :elm.compiler.type/round [context {:keys [operand precision]}] (let [operand (core/compile* context operand) precision (some->> precision (core/compile* context))] (if (and (core/static? operand) (core/static? precision)) (p/round operand precision) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (p/round (core/-eval operand context resource scope) - (core/-eval precision context resource scope))) - (-form [_] - (->> (some-> (core/-form precision) list) - (cons (core/-form operand)) - (cons 'round))))))) + (round-op operand precision)))) ;; 16.20. Subtract (defbinop subtract [x y] diff --git a/modules/cql/src/blaze/elm/compiler/clinical_operators.clj b/modules/cql/src/blaze/elm/compiler/clinical_operators.clj index 4c53f9b17..afac9e115 100644 --- a/modules/cql/src/blaze/elm/compiler/clinical_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/clinical_operators.clj @@ -5,6 +5,7 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.protocols :as p])) ;; 23.3. CalculateAge @@ -12,19 +13,31 @@ ;; see normalizer.clj ;; 23.4. CalculateAgeAt +(defn- calculate-age-at-op [birth-date date chrono-precision precision] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper-2 calculate-age-at-op cache birth-date + date chrono-precision precision)) + (-resolve-refs [_ expression-defs] + (calculate-age-at-op (core/-resolve-refs birth-date expression-defs) + (core/-resolve-refs date expression-defs) + chrono-precision precision)) + (-resolve-params [_ parameters] + (calculate-age-at-op (core/-resolve-params birth-date parameters) + (core/-resolve-params date parameters) + chrono-precision precision)) + (-eval [_ context resource scope] + (p/duration-between + (core/-eval birth-date context resource scope) + (core/-eval date context resource scope) + chrono-precision)) + (-form [_] + (list 'calculate-age-at (core/-form birth-date) (core/-form date) + precision)))) + (defmethod core/compile* :elm.compiler.type/calculate-age-at [context {[birth-date date] :operand precision :precision}] (when-let [birth-date (core/compile* context birth-date)] (when-let [date (core/compile* context date)] (let [chrono-precision (some-> precision core/to-chrono-unit)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (p/duration-between - (core/-eval birth-date context resource scope) - (core/-eval date context resource scope) - chrono-precision)) - (-form [_] - (list 'calculate-age-at (core/-form birth-date) (core/-form date) - precision))))))) + (calculate-age-at-op birth-date date chrono-precision precision))))) diff --git a/modules/cql/src/blaze/elm/compiler/conditional_operators.clj b/modules/cql/src/blaze/elm/compiler/conditional_operators.clj index 245d9f203..821dc1e54 100644 --- a/modules/cql/src/blaze/elm/compiler/conditional_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/conditional_operators.clj @@ -5,13 +5,50 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.protocols :as p])) ;; 15.1. Case +(defn- attach-cache [items cache] + (reduce + (fn [[items bfs] [when then]] + (let [[when w-bfs] ((first (core/-attach-cache when cache))) + [then t-bfs] ((first (core/-attach-cache then cache)))] + [(conj items [when then]) (into bfs (into w-bfs t-bfs))])) + [[] []] + items)) + +(defn- resolve-refs [items expression-defs] + (mapv + (fn [[when then]] + [(core/-resolve-refs when expression-defs) + (core/-resolve-refs then expression-defs)]) + items)) + +(defn- resolve-param-refs [items parameters] + (mapv + (fn [[when then]] + [(core/-resolve-params when parameters) + (core/-resolve-params then parameters)]) + items)) + (defn- comparand-case-op [comparand items else] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-attach-cache [_ cache] + (let [[comparand c-bfs] ((first (core/-attach-cache comparand cache))) + [items i-bfs] (attach-cache items cache) + [else e-bfs] ((first (core/-attach-cache else cache)))] + [(fn [] [(comparand-case-op comparand items else) (into c-bfs (into i-bfs e-bfs))])])) + (-resolve-refs [_ expression-defs] + (comparand-case-op + (core/-resolve-refs comparand expression-defs) + (resolve-refs items expression-defs) + (core/-resolve-refs else expression-defs))) + (-resolve-params [_ parameters] + (comparand-case-op + (core/-resolve-params comparand parameters) + (resolve-param-refs items parameters) + (core/-resolve-params else parameters))) (-eval [_ context resource scope] (let [comparand (core/-eval comparand context resource scope)] (loop [[[when then] & next-items] items] @@ -24,9 +61,19 @@ `(~'case ~(core/-form comparand) ~@(map core/-form (flatten items)) ~(core/-form else))))) (defn- multi-conditional-case-op [items else] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-attach-cache [_ cache] + (let [[items i-bfs] (attach-cache items cache) + [else e-bfs] ((first (core/-attach-cache else cache)))] + [(fn [] [(multi-conditional-case-op items else) (into i-bfs e-bfs)])])) + (-resolve-refs [_ expression-defs] + (multi-conditional-case-op + (resolve-refs items expression-defs) + (core/-resolve-refs else expression-defs))) + (-resolve-params [_ parameters] + (multi-conditional-case-op + (resolve-param-refs items parameters) + (core/-resolve-params else parameters))) (-eval [_ context resource scope] (loop [[[when then] & next-items] items] (if (core/-eval when context resource scope) @@ -41,7 +88,7 @@ [context {:keys [comparand else] items :caseItem}] (let [comparand (some->> comparand (core/compile* context)) items - (map + (mapv (fn [{:keys [when then]}] [(core/compile* context when) (core/compile* context then)]) @@ -53,17 +100,22 @@ ;; 15.2. If (defn- if-op [condition then else] - (reify - core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper if-op cache condition then else)) + (-resolve-refs [_ expression-defs] + (if-op + (core/-resolve-refs condition expression-defs) + (core/-resolve-refs then expression-defs) + (core/-resolve-refs else expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper if-op parameters condition then else)) (-eval [_ context resource scope] (if (core/-eval condition context resource scope) (core/-eval then context resource scope) (core/-eval else context resource scope))) (-form [_] - (list 'if (core/-form condition) (core/-form then) - (core/-form else))))) + (list 'if (core/-form condition) (core/-form then) (core/-form else))))) (defmethod core/compile* :elm.compiler.type/if [context {:keys [condition then else]}] diff --git a/modules/cql/src/blaze/elm/compiler/core.clj b/modules/cql/src/blaze/elm/compiler/core.clj index 1f29cf8c7..56728a034 100644 --- a/modules/cql/src/blaze/elm/compiler/core.clj +++ b/modules/cql/src/blaze/elm/compiler/core.clj @@ -12,6 +12,26 @@ (defprotocol Expression (-static [expression]) + (-attach-cache [expression cache] + "Attaches `cache` to `expression` so the expression can obtain a Bloom filter. + + Returns a vector with a function as first element that will return tuple of + expression and possible list of attached Bloom filters if called with no + argument. + + If the Bloom filter is available, returns a new expression holding the Bloom + filter, so it can be used to increase evaluation performance. + + Expressions that don't like to obtain a Bloom filter should call + `-attach-cache` on it's operands in order to allow them to possible obtain a + Bloom filter.") + (-patient-count [expression] + "Returns the number of patients from an attached Bloom filter. That patient + count can be used by other expressions (most likely and/or/case) to + reorder their operands so that expressions with less patients get evaluated + first. Returns nil if unknown.") + (-resolve-refs [expression expression-defs]) + (-resolve-params [expression parameters]) (-eval [expression context resource scope] "Evaluates `expression` on `resource` using `context` and optional `scope` for scoped expressions inside queries.") @@ -23,10 +43,84 @@ (defn static? [x] (-static x)) +(defn attach-cache-expressions [cache expressions] + (reduce + (fn [[expressions bfs] expression] + (let [[expression expression-bfs] ((first (-attach-cache expression cache)))] + [(conj expressions expression) (into bfs expression-bfs)])) + [[] []] + expressions)) + +(defn attach-cache-helper-list [constructor cache ops] + (let [[ops bfs] (attach-cache-expressions cache ops)] + [(fn [] [(constructor ops) bfs])])) + +(defn attach-cache-helper + ([constructor cache op] + (let [[op op-bfs] ((first (-attach-cache op cache)))] + [(fn [] [(constructor op) op-bfs])])) + ([constructor cache op-1 op-2] + (let [[op-1 op-1-bfs] ((first (-attach-cache op-1 cache))) + [op-2 op-2-bfs] ((first (-attach-cache op-2 cache)))] + [(fn [] [(constructor op-1 op-2) (into op-1-bfs op-2-bfs)])])) + ([constructor cache op-1 op-2 op-3] + (let [[op-1 op-1-bfs] ((first (-attach-cache op-1 cache))) + [op-2 op-2-bfs] ((first (-attach-cache op-2 cache))) + [op-3 op-3-bfs] ((first (-attach-cache op-3 cache)))] + [(fn [] [(constructor op-1 op-2 op-3) (into op-1-bfs (into op-2-bfs op-3-bfs))])]))) + +(defn attach-cache-helper-1 + ([constructor cache op arg] + (let [[op op-bfs] ((first (-attach-cache op cache)))] + [(fn [] [(constructor op arg) op-bfs])])) + ([constructor cache op-1 op-2 arg] + (let [[op-1 op-1-bfs] ((first (-attach-cache op-1 cache))) + [op-2 op-2-bfs] ((first (-attach-cache op-2 cache)))] + [(fn [] [(constructor op-1 op-2 arg) (into op-1-bfs op-2-bfs)])]))) + +(defn attach-cache-helper-2 + ([constructor cache op arg-1 arg-2] + (let [[op op-bfs] ((first (-attach-cache op cache)))] + [(fn [] [(constructor op arg-1 arg-2) op-bfs])])) + ([constructor cache op-1 op-2 arg-1 arg-2] + (let [[op-1 op-1-bfs] ((first (-attach-cache op-1 cache))) + [op-2 op-2-bfs] ((first (-attach-cache op-2 cache)))] + [(fn [] [(constructor op-1 op-2 arg-1 arg-2) (into op-1-bfs op-2-bfs)])]))) + +(defn resolve-refs-helper + ([constructor expression-defs op-1] + (constructor (-resolve-refs op-1 expression-defs))) + ([constructor expression-defs op-1 op-2] + (constructor (-resolve-refs op-1 expression-defs) + (-resolve-refs op-2 expression-defs))) + ([constructor expression-defs op-1 op-2 op-3] + (constructor (-resolve-refs op-1 expression-defs) + (-resolve-refs op-2 expression-defs) + (-resolve-refs op-3 expression-defs)))) + +(defn resolve-params-helper + ([constructor parameters op-1] + (constructor (-resolve-params op-1 parameters))) + ([constructor parameters op-1 op-2] + (constructor (-resolve-params op-1 parameters) + (-resolve-params op-2 parameters))) + ([constructor parameters op-1 op-2 op-3] + (constructor (-resolve-params op-1 parameters) + (-resolve-params op-2 parameters) + (-resolve-params op-3 parameters)))) + (extend-protocol Expression nil (-static [_] true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) (-eval [expr _ _ _] expr) (-form [_] @@ -35,6 +129,14 @@ Object (-static [_] true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) (-eval [expr _ _ _] expr) (-form [expr] @@ -43,6 +145,14 @@ IReduceInit (-static [_] true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) (-eval [expr _ _ _] expr) (-form [expr] diff --git a/modules/cql/src/blaze/elm/compiler/date_time_operators.clj b/modules/cql/src/blaze/elm/compiler/date_time_operators.clj index a6f2347d4..26d9228c0 100644 --- a/modules/cql/src/blaze/elm/compiler/date_time_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/date_time_operators.clj @@ -5,7 +5,7 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinopp defunop defunopp]] + [blaze.elm.compiler.macros :refer [defbinopp defunop defunopp reify-expr]] [blaze.elm.date-time :as date-time] [blaze.elm.protocols :as p] [blaze.fhir.spec.type.system :as system]) @@ -26,6 +26,94 @@ (.toLocalDateTime))) ;; 18.6. Date +(defn- date-op + ([year] + (reify + system/SystemType + (-type [_] :system/date) + + core/Expression + (-static [_] + false) + (-attach-cache [_ cache] + (core/attach-cache-helper date-op cache year)) + (-patient-count [_] + nil) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-op expression-defs year)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-op parameters year)) + (-eval [_ context resource scope] + (some-> (core/-eval year context resource scope) system/date)) + (-form [_] + (list 'date (core/-form year))) + + Object + (equals [this other] + (.equals ^Object (core/-form this) (core/-form other))) + (hashCode [this] + (.hashCode ^Object (core/-form this))))) + ([year month] + (reify + system/SystemType + (-type [_] :system/date) + + core/Expression + (-static [_] + false) + (-attach-cache [_ cache] + (core/attach-cache-helper date-op cache year month)) + (-patient-count [_] + nil) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-op expression-defs year month)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-op parameters year month)) + (-eval [_ context resource scope] + (when-let [year (core/-eval year context resource scope)] + (if-let [month (core/-eval month context resource scope)] + (system/date year month) + (system/date year)))) + (-form [_] + (list 'date (core/-form year) (core/-form month))) + + Object + (equals [this other] + (.equals ^Object (core/-form this) (core/-form other))) + (hashCode [this] + (.hashCode ^Object (core/-form this))))) + ([year month day] + (reify + system/SystemType + (-type [_] :system/date) + + core/Expression + (-static [_] + false) + (-attach-cache [_ cache] + (core/attach-cache-helper date-op cache year month day)) + (-patient-count [_] + nil) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-op expression-defs year month day)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-op parameters year month day)) + (-eval [_ context resource scope] + (when-let [year (core/-eval year context resource scope)] + (if-let [month (core/-eval month context resource scope)] + (if-let [day (core/-eval day context resource scope)] + (system/date year month day) + (system/date year month)) + (system/date year)))) + (-form [_] + (list 'date (core/-form year) (core/-form month) (core/-form day))) + + Object + (equals [this other] + (.equals ^Object (core/-form this) (core/-form other))) + (hashCode [this] + (.hashCode ^Object (core/-form this)))))) + (defmethod core/compile* :elm.compiler.type/date [context {:keys [year month day]}] (let [year (some->> year (core/compile* context)) @@ -36,61 +124,209 @@ (system/date year month day) (some? day) - (reify - system/SystemType - (-type [_] :system/date) - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [year (core/-eval year context resource scope)] - (if-let [month (core/-eval month context resource scope)] - (if-let [day (core/-eval day context resource scope)] - (system/date year month day) - (system/date year month)) - (system/date year)))) - (-form [_] - (list 'date (core/-form year) (core/-form month) (core/-form day)))) + (date-op year month day) (and (int? month) (int? year)) (system/date year month) (some? month) - (reify - system/SystemType - (-type [_] :system/date) - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [year (core/-eval year context resource scope)] - (if-let [month (core/-eval month context resource scope)] - (system/date year month) - (system/date year)))) - (-form [_] - (list 'date (core/-form year) (core/-form month)))) + (date-op year month) (int? year) (system/date year) :else - (when year - (reify - system/SystemType - (-type [_] :system/date) - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (some-> (core/-eval year context resource scope) system/date)) - (-form [_] - (list 'date (core/-form year)))))))) + (some-> year date-op)))) ;; 18.7. DateFrom (defunop date-from [x] (p/date-from x)) ;; 18.8. DateTime +(defn- date-time-static-op + [year month day hour minute second millisecond timezone-offset] + (reify-expr core/Expression + (-eval [_ {:keys [now]} _ _] + (to-local-date-time-with-offset + now year month day hour minute second millisecond timezone-offset)) + (-form [_] + (list 'date-time (core/-form year) (core/-form month) + (core/-form day) (core/-form hour) (core/-form minute) + (core/-form second) (core/-form millisecond) + (core/-form timezone-offset))))) + +(defn- date-time-dynamic-op + ([year month day hour minute second millisecond] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (date-time-dynamic-op + (core/-resolve-refs year expression-defs) + (core/-resolve-refs month expression-defs) + (core/-resolve-refs day expression-defs) + (core/-resolve-refs hour expression-defs) + (core/-resolve-refs minute expression-defs) + (core/-resolve-refs second expression-defs) + (core/-resolve-refs millisecond expression-defs))) + (-resolve-params [_ parameters] + (date-time-dynamic-op + (core/-resolve-params year parameters) + (core/-resolve-params month parameters) + (core/-resolve-params day parameters) + (core/-resolve-params hour parameters) + (core/-resolve-params minute parameters) + (core/-resolve-params second parameters) + (core/-resolve-params millisecond parameters))) + (-eval [_ context resource scope] + (system/date-time + (core/-eval year context resource scope) + (core/-eval month context resource scope) + (core/-eval day context resource scope) + (core/-eval hour context resource scope) + (or (core/-eval minute context resource scope) 0) + (or (core/-eval second context resource scope) 0) + (or (core/-eval millisecond context resource scope) 0))) + (-form [_] + (list 'date-time (core/-form year) (core/-form month) + (core/-form day) (core/-form hour) (core/-form minute) + (core/-form second) (core/-form millisecond))))) + ([year month day hour minute second millisecond timezone-offset] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (date-time-dynamic-op + (core/-resolve-refs year expression-defs) + (core/-resolve-refs month expression-defs) + (core/-resolve-refs day expression-defs) + (core/-resolve-refs hour expression-defs) + (core/-resolve-refs minute expression-defs) + (core/-resolve-refs second expression-defs) + (core/-resolve-refs millisecond expression-defs) + (core/-resolve-refs timezone-offset expression-defs))) + (-resolve-params [_ parameters] + (date-time-dynamic-op + (core/-resolve-params year parameters) + (core/-resolve-params month parameters) + (core/-resolve-params day parameters) + (core/-resolve-params hour parameters) + (core/-resolve-params minute parameters) + (core/-resolve-params second parameters) + (core/-resolve-params millisecond parameters) + (core/-resolve-params timezone-offset parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (to-local-date-time-with-offset + now + (core/-eval year context resource scope) + (core/-eval month context resource scope) + (core/-eval day context resource scope) + (core/-eval hour context resource scope) + (or (core/-eval minute context resource scope) 0) + (or (core/-eval second context resource scope) 0) + (or (core/-eval millisecond context resource scope) 0) + timezone-offset)) + (-form [_] + (list 'date-time (core/-form year) (core/-form month) + (core/-form day) (core/-form hour) (core/-form minute) + (core/-form second) (core/-form millisecond) + (core/-form timezone-offset)))))) + +(defn- date-time-dynamic-timezone-offset-op + [year month day hour minute second millisecond timezone-offset] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (date-time-dynamic-timezone-offset-op + (core/-resolve-refs year expression-defs) + (core/-resolve-refs month expression-defs) + (core/-resolve-refs day expression-defs) + (core/-resolve-refs hour expression-defs) + (core/-resolve-refs minute expression-defs) + (core/-resolve-refs second expression-defs) + (core/-resolve-refs millisecond expression-defs) + (core/-resolve-refs timezone-offset expression-defs))) + (-resolve-params [_ parameters] + (let [timezone-offset (core/-resolve-params timezone-offset parameters)] + (if (number? timezone-offset) + (date-time-dynamic-op + (core/-resolve-params year parameters) + (core/-resolve-params month parameters) + (core/-resolve-params day parameters) + (core/-resolve-params hour parameters) + (core/-resolve-params minute parameters) + (core/-resolve-params second parameters) + (core/-resolve-params millisecond parameters) + timezone-offset) + (date-time-dynamic-timezone-offset-op + (core/-resolve-params year parameters) + (core/-resolve-params month parameters) + (core/-resolve-params day parameters) + (core/-resolve-params hour parameters) + (core/-resolve-params minute parameters) + (core/-resolve-params second parameters) + (core/-resolve-params millisecond parameters) + timezone-offset)))) + (-eval [_ {:keys [now] :as context} resource scope] + (to-local-date-time-with-offset + now + (core/-eval year context resource scope) + (core/-eval month context resource scope) + (core/-eval day context resource scope) + (core/-eval hour context resource scope) + (or (core/-eval minute context resource scope) 0) + (or (core/-eval second context resource scope) 0) + (or (core/-eval millisecond context resource scope) 0) + (core/-eval timezone-offset context resource scope))) + (-form [_] + (list 'date-time (core/-form year) (core/-form month) + (core/-form day) (core/-form hour) (core/-form minute) + (core/-form second) (core/-form millisecond) + (core/-form timezone-offset))))) + +(defn- date-time-date-op [year month day] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper date-time-date-op cache year month day)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-time-date-op expression-defs year month day)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-time-date-op parameters year month day)) + (-eval [_ context resource scope] + (when-let [year (core/-eval year context resource scope)] + (if-let [month (core/-eval month context resource scope)] + (if-let [day (core/-eval day context resource scope)] + (system/date-time year month day) + (system/date-time year month)) + (system/date-time year)))) + (-form [_] + (list 'date-time (core/-form year) (core/-form month) + (core/-form day))))) + +(defn- date-time-year-month-op [year month] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper date-time-year-month-op cache year month)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-time-year-month-op expression-defs year month)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-time-year-month-op parameters year month)) + (-eval [_ context resource scope] + (when-let [year (core/-eval year context resource scope)] + (if-let [month (core/-eval month context resource scope)] + (system/date-time year month) + (system/date-time year)))) + (-form [_] + (list 'date-time (core/-form year) (core/-form month))))) + +(defn- date-time-year-op [year] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper date-time-year-op cache year)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper date-time-year-op expression-defs year)) + (-resolve-params [_ parameters] + (core/resolve-params-helper date-time-year-op parameters year)) + (-eval [_ context resource scope] + (some-> (core/-eval year context resource scope) system/date-time)) + (-form [_] + (list 'date-time (core/-form year))))) + (defmethod core/compile* :elm.compiler.type/date-time [context {:keys [year month day hour minute second millisecond] timezone-offset :timezoneOffset @@ -108,38 +344,12 @@ (cond (and (int? millisecond) (int? second) (int? minute) (int? hour) (int? day) (int? month) (int? year)) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now]} _ _] - (to-local-date-time-with-offset - now year month day hour minute second millisecond timezone-offset)) - (-form [_] - (list 'date-time (core/-form year) (core/-form month) - (core/-form day) (core/-form hour) (core/-form minute) - (core/-form second) (core/-form millisecond) - (core/-form timezone-offset)))) + (date-time-static-op year month day hour minute second millisecond + timezone-offset) (some? hour) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (to-local-date-time-with-offset - now - (core/-eval year context resource scope) - (core/-eval month context resource scope) - (core/-eval day context resource scope) - (core/-eval hour context resource scope) - (or (core/-eval minute context resource scope) 0) - (or (core/-eval second context resource scope) 0) - (or (core/-eval millisecond context resource scope) 0) - timezone-offset)) - (-form [_] - (list 'date-time (core/-form year) (core/-form month) - (core/-form day) (core/-form hour) (core/-form minute) - (core/-form second) (core/-form millisecond) - (core/-form timezone-offset)))) + (date-time-dynamic-op year month day hour minute second millisecond + timezone-offset) :else (throw (ex-info "Need at least an hour if timezone offset is given." @@ -147,25 +357,8 @@ (some? timezone-offset) (if (some? hour) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (to-local-date-time-with-offset - now - (core/-eval year context resource scope) - (core/-eval month context resource scope) - (core/-eval day context resource scope) - (core/-eval hour context resource scope) - (or (core/-eval minute context resource scope) 0) - (or (core/-eval second context resource scope) 0) - (or (core/-eval millisecond context resource scope) 0) - (core/-eval timezone-offset context resource scope))) - (-form [_] - (list 'date-time (core/-form year) (core/-form month) - (core/-form day) (core/-form hour) (core/-form minute) - (core/-form second) (core/-form millisecond) - (core/-form timezone-offset)))) + (date-time-dynamic-timezone-offset-op year month day hour minute second + millisecond timezone-offset) (throw (ex-info "Need at least an hour if timezone offset is given." {:expression expression}))) @@ -176,68 +369,25 @@ (system/date-time year month day hour minute second millisecond) (some? hour) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (system/date-time - (core/-eval year context resource scope) - (core/-eval month context resource scope) - (core/-eval day context resource scope) - (core/-eval hour context resource scope) - (or (core/-eval minute context resource scope) 0) - (or (core/-eval second context resource scope) 0) - (or (core/-eval millisecond context resource scope) 0))) - (-form [_] - (list 'date-time (core/-form year) (core/-form month) - (core/-form day) (core/-form hour) (core/-form minute) - (core/-form second) (core/-form millisecond)))) + (date-time-dynamic-op year month day hour minute second millisecond) (and (int? day) (int? month) (int? year)) (system/date-time year month day) (some? day) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [year (core/-eval year context resource scope)] - (if-let [month (core/-eval month context resource scope)] - (if-let [day (core/-eval day context resource scope)] - (system/date-time year month day) - (system/date-time year month)) - (system/date-time year)))) - (-form [_] - (list 'date-time (core/-form year) (core/-form month) - (core/-form day)))) + (date-time-date-op year month day) (and (int? month) (int? year)) (system/date-time year month) (some? month) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [year (core/-eval year context resource scope)] - (if-let [month (core/-eval month context resource scope)] - (system/date-time year month) - (system/date-time year)))) - (-form [_] - (list 'date-time (core/-form year) (core/-form month)))) + (date-time-year-month-op year month) (int? year) (system/date-time year) :else - (when year - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (some-> (core/-eval year context resource scope) system/date-time)) - (-form [_] - (list 'date-time (core/-form year))))))))) + (some-> year date-time-year-op))))) ;; 18.9. DateTimeComponentFrom (defunopp date-time-component-from [x precision] @@ -252,14 +402,12 @@ (p/duration-between operand-1 operand-2 precision)) ;; 18.13. Now -(defrecord NowExpression [] - core/Expression - (-static [_] - false) - (-eval [_ {:keys [now]} _ _] - now)) - -(def now-expression (->NowExpression)) +(def ^:private now-expression + (reify-expr core/Expression + (-eval [_ {:keys [now]} _ _] + now) + (-form [_] + 'now))) (defmethod core/compile* :elm.compiler.type/now [_ _] now-expression) @@ -277,6 +425,66 @@ (p/same-or-after x y precision)) ;; 18.18. Time +(defn- time-op + ([hour] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper time-op expression-defs hour)) + (-resolve-params [_ parameters] + (time-op (core/-resolve-params hour parameters))) + (-eval [_ context resource scope] + (date-time/local-time (core/-eval hour context resource scope))) + (-form [_] + (list 'time (core/-form hour))))) + ([hour minute] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper time-op expression-defs hour minute)) + (-resolve-params [_ parameters] + (core/resolve-params-helper time-op parameters hour minute)) + (-eval [_ context resource scope] + (date-time/local-time + (core/-eval hour context resource scope) + (core/-eval minute context resource scope))) + (-form [_] + (list 'time (core/-form hour) (core/-form minute))))) + ([hour minute second] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper time-op expression-defs hour minute second)) + (-resolve-params [_ parameters] + (core/resolve-params-helper time-op parameters hour minute second)) + (-eval [_ context resource scope] + (date-time/local-time + (core/-eval hour context resource scope) + (core/-eval minute context resource scope) + (core/-eval second context resource scope))) + (-form [_] + (list 'time (core/-form hour) (core/-form minute) (core/-form second))))) + ([hour minute second millisecond] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (time-op + (core/-resolve-refs hour expression-defs) + (core/-resolve-refs minute expression-defs) + (core/-resolve-refs second expression-defs) + (core/-resolve-refs millisecond expression-defs))) + (-resolve-params [_ parameters] + (time-op + (core/-resolve-params hour parameters) + (core/-resolve-params minute parameters) + (core/-resolve-params second parameters) + (core/-resolve-params millisecond parameters))) + (-eval [_ context resource scope] + (date-time/local-time + (core/-eval hour context resource scope) + (core/-eval minute context resource scope) + (core/-eval second context resource scope) + (core/-eval millisecond context resource scope))) + (-form [_] + (list 'time (core/-form hour) (core/-form minute) (core/-form second) + (core/-form millisecond)))))) + (defmethod core/compile* :elm.compiler.type/time [context {:keys [hour minute second millisecond]}] (let [hour (some->> hour (core/compile* context)) @@ -288,62 +496,28 @@ (date-time/local-time hour minute second millisecond) (some? millisecond) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (date-time/local-time (core/-eval hour context resource scope) - (core/-eval minute context resource scope) - (core/-eval second context resource scope) - (core/-eval millisecond context resource scope))) - (-form [_] - (list 'time (core/-form hour) (core/-form minute) (core/-form second) - (core/-form millisecond)))) + (time-op hour minute second millisecond) (and (int? second) (int? minute) (int? hour)) (date-time/local-time hour minute second) (some? second) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (date-time/local-time (core/-eval hour context resource scope) - (core/-eval minute context resource scope) - (core/-eval second context resource scope))) - (-form [_] - (list 'time (core/-form hour) (core/-form minute) (core/-form second)))) + (time-op hour minute second) (and (int? minute) (int? hour)) (date-time/local-time hour minute) (some? minute) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (date-time/local-time (core/-eval hour context resource scope) - (core/-eval minute context resource scope))) - (-form [_] - (list 'time (core/-form hour) (core/-form minute)))) + (time-op hour minute) (int? hour) (date-time/local-time hour) :else - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (date-time/local-time (core/-eval hour context resource scope))) - (-form [_] - (list 'time (core/-form hour))))))) + (time-op hour)))) (def ^:private time-of-day-expr - (reify - core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [now]} _ _] (.toLocalTime ^OffsetDateTime now)) (-form [_] @@ -355,9 +529,7 @@ time-of-day-expr) (def ^:private today-expr - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [now]} _ _] (DateDate/fromLocalDate (.toLocalDate ^OffsetDateTime now))) (-form [_] diff --git a/modules/cql/src/blaze/elm/compiler/external_data.clj b/modules/cql/src/blaze/elm/compiler/external_data.clj index a7c657f1d..ab0cef999 100644 --- a/modules/cql/src/blaze/elm/compiler/external_data.clj +++ b/modules/cql/src/blaze/elm/compiler/external_data.clj @@ -7,48 +7,24 @@ [blaze.anomaly :as ba :refer [if-ok]] [blaze.coll.core :as coll] [blaze.db.api :as d] - [blaze.db.impl.index.resource-handle :as rh] [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.compiler.structured-values] + [blaze.elm.resource :as cr] [blaze.elm.spec] [blaze.elm.util :as elm-util] - [blaze.fhir.spec.type.protocols :as p] - [clojure.string :as str]) + [blaze.fhir.spec.references :as fsr] + [prometheus.alpha :as prom :refer [defcounter]]) (:import [blaze.elm.compiler.structured_values SourcePropertyExpression] - [clojure.lang ILookup] [java.util List])) (set! *warn-on-reflection* true) -;; A resource that is a wrapper of a resource-handle that will lazily pull the -;; resource content if some property other than :id is accessed. -(deftype Resource [db handle content] - p/FhirType - (-type [_] - (p/-type handle)) - - ILookup - (valAt [r key] - (.valAt r key nil)) - (valAt [_ key not-found] - (case key - :id (rh/id handle) - (-> (or @content (vreset! content @(d/pull-content db handle))) - (get key not-found)))) - - Object - (toString [_] - (.toString handle))) - -(defn resource? [x] - (instance? Resource x)) - -(defn mk-resource [db handle] - (Resource. db handle (volatile! nil))) - -(defn resource-mapper [db] - (map (partial mk-resource db))) +(defcounter retrieve-total + "Number of times a retrieve expression was evaluated." + {:namespace "blaze" + :subsystem "cql"}) (defn- code->clause-value [{:keys [system code]}] (str system "|" code)) @@ -79,36 +55,27 @@ [node context data-type property codes] (let [clauses (-to-clauses codes property) query (d/compile-compartment-query node context data-type clauses)] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [db]} {:keys [id]} _] - (coll/eduction (resource-mapper db) (d/execute-query db query id))) + (prom/inc! retrieve-total) + (coll/eduction (cr/resource-mapper db) (d/execute-query db query id))) (-form [_] `(~'retrieve ~data-type ~(d/query-clauses query)))))) -(defn- split-reference [s] - (when-let [idx (str/index-of s \/)] - [(subs s 0 idx) (subs s (inc idx))])) - ;; TODO: find a better solution than hard coding this case -(defrecord SpecimenPatientExpression [] - core/Expression - (-static [_] - false) - (-eval [_ {:keys [db]} resource _] - (let [{{:keys [reference]} :subject} resource] - (when reference - (when-let [[type id] (split-reference reference)] - (when (and (= "Patient" type) (string? id)) - (let [{:keys [op] :as handle} (d/resource-handle db "Patient" id)] - (when-not (identical? :delete op) - [(mk-resource db handle)]))))))) - (-form [_] - '(retrieve (Specimen) "Patient"))) - (def ^:private specimen-patient-expr - (->SpecimenPatientExpression)) + (reify-expr core/Expression + (-eval [_ {:keys [db]} resource _] + (prom/inc! retrieve-total) + (let [{{:keys [reference]} :subject} resource] + (when reference + (when-let [[type id] (fsr/split-literal-ref reference)] + (when (and (= "Patient" type) (string? id)) + (let [{:keys [op] :as handle} (d/resource-handle db "Patient" id)] + (when-not (identical? :delete op) + [(cr/mk-resource db handle)]))))))) + (-form [_] + '(retrieve (Specimen) "Patient")))) (defn- context-expr "Returns an expression which, when evaluated, returns all resources of type @@ -119,20 +86,17 @@ (case data-type "Patient" specimen-patient-expr) - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [db]} {:keys [id]} _] + (prom/inc! retrieve-total) (coll/eduction - (resource-mapper db) + (cr/resource-mapper db) (d/list-compartment-resource-handles db context id data-type))) (-form [_] `(~'retrieve ~data-type))))) (def ^:private resource-expr - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ _ resource _] [resource]) (-form [_] @@ -145,10 +109,12 @@ (ba/unsupported "Unsupported related context retrieve expression without result type.")) (defn- related-context-expr-without-codes [related-context-expr data-type] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (related-context-expr-without-codes + (core/-resolve-refs related-context-expr expression-defs) data-type)) (-eval [_ context resource scope] + (prom/inc! retrieve-total) (when-let [context-resource (core/-eval related-context-expr context resource scope)] (core/-eval (context-expr (-> context-resource :fhir/type name) data-type) @@ -158,6 +124,21 @@ (-form [_] (list 'retrieve (core/-form related-context-expr) data-type)))) +(defn- related-context-expr-with-codes [related-context-expr data-type query] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (related-context-expr-with-codes + (core/-resolve-refs related-context-expr expression-defs) data-type query)) + (-eval [_ {:keys [db] :as context} resource scope] + (prom/inc! retrieve-total) + (when-let [{:keys [id]} (core/-eval related-context-expr context resource scope)] + (when (string? id) + (coll/eduction + (cr/resource-mapper db) + (d/execute-query db query id))))) + (-form [_] + (list 'retrieve (core/-form related-context-expr) data-type (d/query-clauses query))))) + (defn- related-context-expr [node context-expr data-type code-property codes] (if (seq codes) @@ -166,17 +147,7 @@ (if (= "http://hl7.org/fhir" value-type-ns) (let [clauses [(into [code-property] (map code->clause-value) codes)]] (if-ok [query (d/compile-compartment-query node context-type data-type clauses)] - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [db] :as context} resource scope] - (when-let [{:keys [id]} (core/-eval context-expr context resource scope)] - (when (string? id) - (coll/eduction - (resource-mapper db) - (d/execute-query db query id))))) - (-form [_] - (list 'retrieve (core/-form context-expr) data-type (d/query-clauses query)))) + (related-context-expr-with-codes context-expr data-type query) ba/throw-anom)) (ba/throw-anom (unsupported-type-ns-anom value-type-ns)))) (ba/throw-anom unsupported-related-context-expr-without-type-anom)) @@ -184,20 +155,18 @@ (defn- unfiltered-context-expr [node data-type code-property codes] (if (empty? codes) - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [db]} _ _] - (coll/eduction (resource-mapper db) (d/type-list db data-type))) + (prom/inc! retrieve-total) + (coll/eduction (cr/resource-mapper db) (d/type-list db data-type))) (-form [_] `(~'retrieve ~data-type))) (let [clauses [(into [code-property] (map code->clause-value) codes)]] (if-ok [query (d/compile-type-query node data-type clauses)] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [db]} _ _] - (coll/eduction (resource-mapper db) (d/execute-query db query))) + (prom/inc! retrieve-total) + (coll/eduction (cr/resource-mapper db) (d/execute-query db query))) (-form [_] `(~'retrieve ~data-type ~(d/query-clauses query)))) ba/throw-anom)))) diff --git a/modules/cql/src/blaze/elm/compiler/function.clj b/modules/cql/src/blaze/elm/compiler/function.clj index e0d922999..5ae3f7df7 100644 --- a/modules/cql/src/blaze/elm/compiler/function.clj +++ b/modules/cql/src/blaze/elm/compiler/function.clj @@ -1,11 +1,20 @@ (ns blaze.elm.compiler.function (:require - [blaze.elm.compiler.core :as core])) + [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]])) (defn arity-n [name fn-expr operand-names operands] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-attach-cache [_ cache] + (let [[fn-expr fn-expr-bfs] ((first (core/-attach-cache fn-expr cache))) + [operands operands-bfs] (core/attach-cache-expressions cache operands)] + [(fn [] [(arity-n name fn-expr operand-names operands) (into (or fn-expr-bfs []) operands-bfs)])])) + (-resolve-refs [_ expression-defs] + (arity-n name (core/-resolve-refs fn-expr expression-defs) operand-names + (map #(core/-resolve-refs % expression-defs) operands))) + (-resolve-params [_ parameters] + (arity-n name (core/-resolve-params fn-expr parameters) operand-names + (map #(core/-resolve-params % parameters) operands))) (-eval [_ context resource scope] (let [values (map #(core/-eval % context resource scope) operands)] (core/-eval fn-expr context resource (merge scope (zipmap operand-names values))))) diff --git a/modules/cql/src/blaze/elm/compiler/interval_operators.clj b/modules/cql/src/blaze/elm/compiler/interval_operators.clj index 9a2a33002..91f0192a4 100644 --- a/modules/cql/src/blaze/elm/compiler/interval_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/interval_operators.clj @@ -6,7 +6,7 @@ (:require [blaze.elm.compiler.arithmetic-operators :as ao] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinop defbinopp defunop]] + [blaze.elm.compiler.macros :refer [defbinop defbinopp defunop reify-expr]] [blaze.elm.interval :refer [interval]] [blaze.elm.protocols :as p])) @@ -16,34 +16,49 @@ asType (when (= "ToDateTime" type) "{urn:hl7-org:elm-types:r1}DateTime"))) -(defrecord IntervalExpression - [type low high low-closed-expression high-closed-expression low-closed - high-closed] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [low (core/-eval low context resource scope) - high (core/-eval high context resource scope) - low-closed (or (core/-eval low-closed-expression context resource - scope) - low-closed) - high-closed (or (core/-eval high-closed-expression context resource - scope) - high-closed)] - (interval - (if low-closed - (if (nil? low) - (ao/min-value type) - low) - (p/successor low)) - (if high-closed - (if (nil? high) - (ao/max-value type) - high) - (p/predecessor high))))) - (-form [_] - (list 'interval (core/-form low) (core/-form high)))) +(defn- interval-expr [type low high low-closed-expression + high-closed-expression low-closed high-closed] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (interval-expr + type + (core/-resolve-refs low expression-defs) + (core/-resolve-refs high expression-defs) + (core/-resolve-refs low-closed-expression expression-defs) + (core/-resolve-refs high-closed-expression expression-defs) + low-closed + high-closed)) + (-resolve-params [_ parameters] + (interval-expr + type + (core/-resolve-params low parameters) + (core/-resolve-params high parameters) + (core/-resolve-params low-closed-expression parameters) + (core/-resolve-params high-closed-expression parameters) + low-closed + high-closed)) + (-eval [_ context resource scope] + (let [low (core/-eval low context resource scope) + high (core/-eval high context resource scope) + low-closed (or (core/-eval low-closed-expression context resource + scope) + low-closed) + high-closed (or (core/-eval high-closed-expression context resource + scope) + high-closed)] + (interval + (if low-closed + (if (nil? low) + (ao/min-value type) + low) + (p/successor low)) + (if high-closed + (if (nil? high) + (ao/max-value type) + high) + (p/predecessor high))))) + (-form [_] + (list 'interval (core/-form low) (core/-form high))))) (defmethod core/compile* :elm.compiler.type/interval [context {:keys [low high] @@ -77,8 +92,8 @@ (ao/max-value type) high) (p/predecessor high)))) - (->IntervalExpression type low high low-closed-expression - high-closed-expression low-closed high-closed)))) + (interval-expr type low high low-closed-expression + high-closed-expression low-closed high-closed)))) ;; 19.2. After (defbinopp after [operand-1 operand-2 precision] diff --git a/modules/cql/src/blaze/elm/compiler/library.clj b/modules/cql/src/blaze/elm/compiler/library.clj index 66dceab92..47f2d11d2 100644 --- a/modules/cql/src/blaze/elm/compiler/library.clj +++ b/modules/cql/src/blaze/elm/compiler/library.clj @@ -3,6 +3,7 @@ [blaze.anomaly :as ba :refer [if-ok when-ok]] [blaze.elm.compiler :as c] [blaze.elm.compiler.function :as function] + [blaze.elm.compiler.library.resolve-refs :refer [resolve-refs]] [blaze.elm.normalizer :as normalizer])) (defn- compile-expression-def @@ -53,11 +54,12 @@ itself is returned. Returns an anomaly on errors." - {:arglists '([context expression-def])} + {:arglists '([context parameter-def])} [context {:keys [default] :as parameter-def}] (if (some? default) (-> (ba/try-anomaly - (assoc parameter-def :default (c/compile context default))) + (let [context (assoc context :eval-context "Unfiltered")] + (assoc parameter-def :default (c/compile context default)))) (ba/exceptionally #(assoc % :context context :elm/expression default))) parameter-def)) @@ -72,6 +74,23 @@ {} (-> library :parameters :def))) +(defn resolve-all-refs [expression-defs] + (resolve-refs #{} expression-defs)) + +(defn- unfiltered-expr-names [expression-defs] + (into + #{} + (keep (fn [[name {:keys [context]}]] (when (= "Unfiltered" context) name))) + expression-defs)) + +(defn- resolve-param-refs-xf [parameters] + (map + (fn [[name expr-def]] + [name (update expr-def :expression c/resolve-params parameters)]))) + +(defn resolve-param-refs [expression-defs parameters] + (into {} (resolve-param-refs-xf parameters) expression-defs)) + (defn compile-library "Compiles `library` using `node`. @@ -81,6 +100,7 @@ context (assoc opts :node node :library library)] (when-ok [{:keys [function-defs] :as context} (compile-function-defs context library) expression-defs (expression-defs context library) + expression-defs (resolve-refs (unfiltered-expr-names expression-defs) expression-defs) parameter-default-values (parameter-default-values context library)] {:expression-defs expression-defs :function-defs function-defs diff --git a/modules/cql/src/blaze/elm/compiler/library/resolve_refs.clj b/modules/cql/src/blaze/elm/compiler/library/resolve_refs.clj new file mode 100644 index 000000000..f02ef4874 --- /dev/null +++ b/modules/cql/src/blaze/elm/compiler/library/resolve_refs.clj @@ -0,0 +1,52 @@ +(ns blaze.elm.compiler.library.resolve-refs + (:require + [blaze.anomaly :as ba] + [blaze.elm.compiler :as c] + [clojure.string :as str] + [clojure.walk :as walk])) + +(defn- has-refs? [non-refs {:keys [expression]}] + (identical? + (walk/postwalk + #(if (sequential? %) + (if (and (= 'expr-ref (first %)) (not (non-refs (second %)))) + ::ref + (some #{::ref} %)) + %) + (c/form expression)) + ::ref)) + +(defn- split-by-having-refs [non-refs expression-defs] + (reduce-kv + (fn [[with without] name expression-def] + (if (has-refs? non-refs expression-def) + [(assoc with name expression-def) without] + [with (assoc without name expression-def)])) + [{} {}] + expression-defs)) + +(defn resolve-refs* [expr-def without-refs] + (update expr-def :expression c/resolve-refs without-refs)) + +(defn unresolvable-msg [with-refs] + (format "The following expression definitions contain unresolvable references: %s." + (str/join "," (map key with-refs)))) + +(defn resolve-refs [non-refs expression-defs] + (let [[with-refs without-refs] (split-by-having-refs non-refs expression-defs)] + (cond + (empty? with-refs) + without-refs + + (= (count with-refs) (count expression-defs)) + (ba/incorrect (unresolvable-msg with-refs)) + + :else + (let [resolvable-defs (apply dissoc without-refs non-refs)] + (recur + non-refs + (reduce-kv + (fn [ret name expr-def] + (assoc ret name (resolve-refs* expr-def resolvable-defs))) + without-refs + with-refs)))))) diff --git a/modules/cql/src/blaze/elm/compiler/library/spec.clj b/modules/cql/src/blaze/elm/compiler/library/spec.clj index 3ceccaddd..305659ae2 100644 --- a/modules/cql/src/blaze/elm/compiler/library/spec.clj +++ b/modules/cql/src/blaze/elm/compiler/library/spec.clj @@ -2,7 +2,6 @@ (:require [blaze.anomaly-spec] [blaze.elm.compiler :as-alias c] - [blaze.elm.compiler-spec] [blaze.elm.compiler.expression-def :as-alias expression-def] [blaze.elm.compiler.function-def :as-alias function-def] [blaze.elm.compiler.spec] @@ -36,5 +35,11 @@ (s/def ::c/parameter-default-values (s/map-of :elm/name ::c/expression)) +(s/def ::c/parameters + (s/map-of :elm/name ::c/expression)) + (s/def ::c/library (s/keys :req-un [::c/expression-defs ::c/function-defs ::c/parameter-default-values])) + +(s/def ::c/options + map?) diff --git a/modules/cql/src/blaze/elm/compiler/library_spec.clj b/modules/cql/src/blaze/elm/compiler/library_spec.clj index d1318e391..243a6a4e7 100644 --- a/modules/cql/src/blaze/elm/compiler/library_spec.clj +++ b/modules/cql/src/blaze/elm/compiler/library_spec.clj @@ -1,15 +1,21 @@ (ns blaze.elm.compiler.library-spec (:require [blaze.anomaly-spec] + [blaze.db.spec] [blaze.elm.compiler :as-alias c] - [blaze.elm.compiler-spec] [blaze.elm.compiler.library :as library] [blaze.elm.compiler.library.spec] - [blaze.elm.compiler.spec] - [blaze.fhir.spec.spec] [clojure.spec.alpha :as s] - [cognitect.anomalies :as anom])) + [cognitect.anomalies :as-alias anom])) + +(s/fdef library/resolve-all-refs + :args (s/cat :expression-defs ::c/expression-defs) + :ret (s/or :expression-defs ::c/expression-defs :anomaly ::anom/anomaly)) + +(s/fdef library/resolve-param-refs + :args (s/cat :expression-defs ::c/expression-defs :parameters ::c/parameters) + :ret ::c/expression-defs) (s/fdef library/compile-library - :args (s/cat :node :blaze.db/node :library :elm/library :opts map?) + :args (s/cat :node :blaze.db/node :library :elm/library :opts ::c/options) :ret (s/or :library ::c/library :anomaly ::anom/anomaly)) diff --git a/modules/cql/src/blaze/elm/compiler/list_operators.clj b/modules/cql/src/blaze/elm/compiler/list_operators.clj index db30ef07e..193cdda87 100644 --- a/modules/cql/src/blaze/elm/compiler/list_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/list_operators.clj @@ -7,7 +7,7 @@ [blaze.anomaly :as ba] [blaze.coll.core :as coll] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinop defunop]] + [blaze.elm.compiler.macros :refer [defbinop defunop reify-expr]] [blaze.elm.compiler.queries :as queries] [blaze.elm.protocols :as p] [blaze.util :refer [conj-vec]] @@ -19,9 +19,13 @@ ;; 20.1. List (defn list-op [elements] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper-list list-op cache elements)) + (-resolve-refs [_ expression-defs] + (list-op (mapv #(core/-resolve-refs % expression-defs) elements))) + (-resolve-params [_ parameters] + (list-op (mapv #(core/-resolve-params % parameters) elements))) (-eval [_ context resource scope] (mapv #(core/-eval % context resource scope) elements)) (-form [_] @@ -36,16 +40,12 @@ (defmethod core/compile* :elm.compiler.type/current [_ {:keys [scope]}] (if scope - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ _ _ scopes] (get scopes scope)) (-form [_] (list 'current scope))) - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ _ _ scope] scope) (-form [_] @@ -66,51 +66,63 @@ ;; 20.8. Exists (defunop exists - {:optimizations #{:first :non-distinct}} + {:optimizations #{:first :non-distinct} + :cache true} [list] (not (coll/empty? list))) ;; 20.9. Filter +(defn- scoped-filter-op [source condition scope] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (scoped-filter-op + (core/-resolve-refs source expression-defs) + (core/-resolve-refs condition expression-defs) + scope)) + (-resolve-params [_ parameters] + (scoped-filter-op + (core/-resolve-params source parameters) + (core/-resolve-params condition parameters) + scope)) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (filterv + (fn [x] + (core/-eval condition context resource (assoc scopes scope x))) + source))) + (-form [_] + (list 'filter (core/-form source) (core/-form condition) scope)))) + +(defn- filter-op [source condition] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (filter-op + (core/-resolve-refs source expression-defs) + (core/-resolve-refs condition expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper filter-op parameters source condition)) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (filterv (partial core/-eval condition context resource) source))) + (-form [_] + (list 'filter (core/-form source) (core/-form condition))))) + (defmethod core/compile* :elm.compiler.type/filter [context {:keys [source condition scope]}] (let [source (core/compile* context source) condition (core/compile* context condition)] (if scope - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (filterv - (fn [x] - (core/-eval condition context resource (assoc scopes scope x))) - source))) - (-form [_] - (list 'filter (core/-form source) (core/-form condition) scope))) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (filterv (partial core/-eval condition context resource) source))) - (-form [_] - (list 'filter (core/-form source) (core/-form condition))))))) + (scoped-filter-op source condition scope) + (filter-op source condition)))) ;; 20.10. First ;; ;; TODO: orderBy -(defmethod core/compile* :elm.compiler.type/first - [context {:keys [source]}] - (let [source (core/compile* (assoc context :optimizations #{:first :non-distinct}) source)] - (if (core/static? source) - (first source) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (coll/first (core/-eval source context resource scopes))) - (-form [_] - (list 'first (core/-form source))))))) +(defunop first + {:optimizations #{:first :non-distinct} + :operand-key :source} + [source] + (coll/first source)) ;; 20.11. Flatten (defunop flatten [list] @@ -126,69 +138,89 @@ (flatten [] list)))) ;; 20.12. ForEach +(defn- scoped-for-each [source element scope] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (scoped-for-each + (core/-resolve-refs source expression-defs) + (core/-resolve-refs element expression-defs) + scope)) + (-resolve-params [_ parameters] + (scoped-for-each + (core/-resolve-params source parameters) + (core/-resolve-params element parameters) + scope)) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (mapv + (fn [x] + (core/-eval element context resource (assoc scopes scope x))) + source))) + (-form [_] + (list 'for-each (core/-form source) (core/-form element) scope)))) + +(defn- for-each [source element] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (for-each + (core/-resolve-refs source expression-defs) + (core/-resolve-refs element expression-defs))) + (-resolve-params [_ parameters] + (for-each + (core/-resolve-params source parameters) + (core/-resolve-params element parameters))) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (mapv (partial core/-eval element context resource) source))) + (-form [_] + (list 'for-each (core/-form source) (core/-form element))))) + (defmethod core/compile* :elm.compiler.type/for-each [context {:keys [source element scope]}] (let [source (core/compile* context source) element (core/compile* context element)] (if scope - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (mapv - (fn [x] - (core/-eval element context resource (assoc scopes scope x))) - source))) - (-form [_] - (list 'for-each (core/-form source) (core/-form element) scope))) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (mapv (partial core/-eval element context resource) source))) - (-form [_] - (list 'for-each (core/-form source) (core/-form element))))))) + (scoped-for-each source element scope) + (for-each source element)))) ;; 20.16. IndexOf +(defn- index-of-op [source element] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper index-of-op cache source element)) + (-resolve-refs [_ expression-defs] + (index-of-op (core/-resolve-refs source expression-defs) + (core/-resolve-refs element expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper index-of-op parameters source element)) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (when-let [element (core/-eval element context resource scopes)] + (or + (first + (keep-indexed + (fn [idx x] + (when + (p/equal element x) + idx)) + source)) + -1)))) + (-form [_] + (list 'index-of (core/-form source) (core/-form element))))) + (defmethod core/compile* :elm.compiler.type/index-of [context {:keys [source element]}] (let [source (core/compile* context source) element (core/compile* context element)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (when-let [element (core/-eval element context resource scopes)] - (or - (first - (keep-indexed - (fn [idx x] - (when - (p/equal element x) - idx)) - source)) - -1)))) - (-form [_] - (list 'index-of (core/-form source) (core/-form element)))))) + (index-of-op source element))) ;; 20.18. Last ;; ;; TODO: orderBy -(defmethod core/compile* :elm.compiler.type/last - [context {:keys [source]}] - (let [source (core/compile* context source)] - (if (core/static? source) - (peek source) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (peek (core/-eval source context resource scopes))) - (-form [_] - (list 'last (core/-form source))))))) +(defunop last + {:operand-key :source} + [source] + (peek source)) ;; 20.24. Repeat ;; @@ -204,23 +236,32 @@ (throw e))))) ;; 20.26. Slice +(defn- slice-op [source start-index end-index] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper slice-op cache source start-index end-index)) + (-resolve-refs [_ expression-defs] + (slice-op (core/-resolve-refs source expression-defs) + (core/-resolve-refs start-index expression-defs) + (core/-resolve-refs end-index expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper slice-op parameters source start-index end-index)) + (-eval [_ context resource scopes] + (when-let [source (core/-eval source context resource scopes)] + (let [start-index (or (core/-eval start-index context resource scopes) 0) + end-index (or (core/-eval end-index context resource scopes) (count source))] + (if (or (neg? start-index) (< end-index start-index)) + [] + (subvec source start-index end-index))))) + (-form [_] + (list 'slice (core/-form source) (core/-form start-index) (core/-form end-index))))) + (defmethod core/compile* :elm.compiler.type/slice [context {:keys [source] start-index :startIndex end-index :endIndex}] (let [source (core/compile* context source) start-index (core/compile* context start-index) end-index (core/compile* context end-index)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scopes] - (when-let [source (core/-eval source context resource scopes)] - (let [start-index (or (core/-eval start-index context resource scopes) 0) - end-index (or (core/-eval end-index context resource scopes) (count source))] - (if (or (neg? start-index) (< end-index start-index)) - [] - (subvec source start-index end-index))))) - (-form [_] - (list 'slice (core/-form source) (core/-form start-index) (core/-form end-index)))))) + (slice-op source start-index end-index))) ;; 20.27. Sort (defmethod core/compile* :elm.compiler.type/sort @@ -233,9 +274,8 @@ (case type "ByDirection" (let [comp (queries/comparator direction)] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + ;; TODO: other methods (-eval [_ context resource scopes] (when-let [source (core/-eval source context resource scopes)] (sort-by identity comp source))) diff --git a/modules/cql/src/blaze/elm/compiler/logical_operators.clj b/modules/cql/src/blaze/elm/compiler/logical_operators.clj index 1785b883e..f2e4278aa 100644 --- a/modules/cql/src/blaze/elm/compiler/logical_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/logical_operators.clj @@ -5,13 +5,23 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defunop]])) + [blaze.elm.compiler.logical-operators.util :as u] + [blaze.elm.compiler.macros :refer [defunop reify-expr]] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [prometheus.alpha :as prom])) ;; 13.1. And -(defn- nil-and-expr [x] - (reify core/Expression - (-static [_] - false) +(defn- and-nil-op [x] + (reify-expr core/Expression + (-attach-cache [_ cache] + [(fn [] (and-nil-op ((first (core/-attach-cache x cache)))))]) + (-patient-count [_] + 0) + (-resolve-refs [_ expression-defs] + (and-nil-op (core/-resolve-refs x expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper and-nil-op parameters x)) (-eval [_ context resource scope] (when (false? (core/-eval x context resource scope)) false)) @@ -25,7 +35,83 @@ true nil false false nil nil - (nil-and-expr x))) + (and-nil-op x))) + +(defn- and-list-op [op ops] + (reify-expr core/Expression + (-attach-cache [_ _] + (Exception. "Can't attach a cache to `and-list-op`.")) + (-patient-count [_] + nil) + (-resolve-refs [_ _] + (Exception. "Can't resolve references in `and-list-op`.")) + (-resolve-params [_ _] + (Exception. "Can't resolve references in `and-list-op`.")) + (-eval [_ context resource scope] + (reduce + (fn [a op] + (if (false? a) + (reduced false) + (let [b (core/-eval op context resource scope)] + (cond + (false? b) (reduced false) + (and (true? a) (true? b)) true)))) + (core/-eval op context resource scope) + ops)) + (-form [_] + `(~'and ~(core/-form op) ~@(map core/-form ops))))) + +(defn- and-cmp [[a-op] [b-op]] + (let [a-count (or (core/-patient-count a-op) Long/MAX_VALUE) + b-count (or (core/-patient-count b-op) Long/MAX_VALUE)] + (- a-count b-count))) + +(defn and-op [a b] + (reify-expr core/Expression + (-attach-cache [_ cache] + (let [[fa a-kind a] (core/-attach-cache a cache) + [fb b-kind b] (core/-attach-cache b cache)] + (cond + (and (identical? :and a-kind) (identical? :and b-kind)) + (u/and-attach-cache-result + and-list-op + (u/merge-sorted and-cmp a b)) + + (identical? :and a-kind) + (u/and-attach-cache-result + and-list-op + (u/insert-sorted and-cmp a (fb))) + + (identical? :and b-kind) + (u/and-attach-cache-result + and-list-op + (u/insert-sorted and-cmp b (fa))) + + :else + (let [a (fa) + b (fb)] + (if (pos? (and-cmp a b)) + (u/and-attach-cache-result and-list-op [b a]) + (u/and-attach-cache-result and-list-op [a b])))))) + (-patient-count [_] + (let [count-a (core/-patient-count a) + count-b (core/-patient-count b)] + (when (and count-a count-b) + (min count-a count-b)))) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper and-op expression-defs a b)) + (-resolve-params [_ parameters] + (core/resolve-params-helper and-op parameters a b)) + (-eval [_ context resource scope] + (let [a (core/-eval a context resource scope)] + (if (false? a) + false + (let [b (core/-eval b context resource scope)] + (cond + (false? b) false + (and (true? a) (true? b)) true))))) + (-form [_] + (list 'and (core/-form a) (core/-form b))))) (defn- dynamic-and "Creates an and-expression where `a` is known to be dynamic and `b` could be @@ -34,20 +120,8 @@ (condp identical? b true a false false - nil (nil-and-expr a) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [a (core/-eval a context resource scope)] - (if (false? a) - false - (let [b (core/-eval b context resource scope)] - (cond - (false? b) false - (and (true? a) (true? b)) true))))) - (-form [_] - (list 'and (core/-form a) (core/-form b)))))) + nil (and-nil-op a) + (and-op a b))) (defmethod core/compile* :elm.compiler.type/and [context {[a b] :operand}] @@ -64,15 +138,23 @@ (throw (Exception. "Unsupported Implies expression. Please normalize the ELM tree before compiling."))) ;; 13.3 Not +(declare not-op) + (defunop not [operand] (when (some? operand) (not operand))) ;; 13.4. Or -(defn- nil-or-expr [x] - (reify core/Expression - (-static [_] - false) +(defn- or-nil-op [x] + (reify-expr core/Expression + (-attach-cache [_ cache] + [(fn [] (or-nil-op ((first (core/-attach-cache x cache)))))]) + (-patient-count [_] + (core/-patient-count x)) + (-resolve-refs [_ expression-defs] + (or-nil-op (core/-resolve-refs x expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper or-nil-op parameters x)) (-eval [_ context resource scope] (when (true? (core/-eval x context resource scope)) true)) @@ -86,7 +168,90 @@ true true false nil nil nil - (nil-or-expr x))) + (or-nil-op x))) + +(defn- or-list-op [tuples] + (reify-expr core/Expression + (-attach-cache [_ _] + (Exception. "Can't attach a cache to `or-list-op`.")) + (-patient-count [_] + nil) + (-resolve-refs [_ _] + (Exception. "Can't resolve references in `or-list-op`.")) + (-resolve-params [_ _] + (Exception. "Can't resolve references in `or-list-op`.")) + (-eval [_ context resource scope] + (reduce + (fn [_ [op bf]] + ;; TODO: handle nil + (if bf + (if (bloom-filter/might-contain? bf resource) + (do (prom/inc! ec/bloom-filter-not-useful-total "or") + (if (core/-eval op context resource scope) + (reduced true) + (do (prom/inc! ec/bloom-filter-false-positive-total "or") + false))) + (do (prom/inc! ec/bloom-filter-useful-total "or") + (reduced false))) + (if (core/-eval op context resource scope) + (reduced true) + false))) + false + tuples)) + (-form [_] + `(~'or ~@(map (comp core/-form first) tuples))))) + +(defn- or-cmp [[a-op] [b-op]] + (let [a-count (or (core/-patient-count a-op) Long/MAX_VALUE) + b-count (or (core/-patient-count b-op) Long/MAX_VALUE)] + (- b-count a-count))) + +(defn or-op [a b] + (reify-expr core/Expression + (-attach-cache [_ cache] + (let [[fa a-kind a] (core/-attach-cache a cache) + [fb b-kind b] (core/-attach-cache b cache)] + (cond + (and (identical? :or a-kind) (identical? :or b-kind)) + (u/or-attach-cache-result + or-list-op + (u/merge-sorted or-cmp a b)) + + (identical? :or a-kind) + (u/or-attach-cache-result + or-list-op + (u/insert-sorted or-cmp a (fb))) + + (identical? :or b-kind) + (u/or-attach-cache-result + or-list-op + (u/insert-sorted or-cmp b (fa))) + + :else + (let [a (fa) + b (fb)] + (if (pos? (or-cmp a b)) + (u/or-attach-cache-result or-list-op [b a]) + (u/or-attach-cache-result or-list-op [a b])))))) + (-patient-count [_] + (let [count-a (core/-patient-count a) + count-b (core/-patient-count b)] + (when (and count-a count-b) + (max count-a count-b)))) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper or-op expression-defs a b)) + (-resolve-params [_ parameters] + (core/resolve-params-helper or-op parameters a b)) + (-eval [_ context resource scope] + (let [a (core/-eval a context resource scope)] + (if (true? a) + true + (let [b (core/-eval b context resource scope)] + (cond + (true? b) true + (and (false? a) (false? b)) false))))) + (-form [_] + (list 'or (core/-form a) (core/-form b))))) (defn- dynamic-or "Creates an or-expression where `a` is known to be dynamic and `b` could be @@ -95,20 +260,8 @@ (condp identical? b true true false a - nil (nil-or-expr a) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [a (core/-eval a context resource scope)] - (if (true? a) - true - (let [b (core/-eval b context resource scope)] - (cond - (true? b) true - (and (false? a) (false? b)) false))))) - (-form [_] - (list 'or (core/-form a) (core/-form b)))))) + nil (or-nil-op a) + (or-op a b))) (defmethod core/compile* :elm.compiler.type/or [context {[a b] :operand}] @@ -120,32 +273,31 @@ (dynamic-or a (core/compile* context b))))) ;; 13.5 Xor +(defn- xor-op [a b] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper xor-op cache a b)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper xor-op expression-defs a b)) + (-resolve-params [_ parameters] + (core/resolve-params-helper xor-op parameters a b)) + (-eval [_ context resource scope] + (when-some [a (core/-eval a context resource scope)] + (when-some [b (core/-eval b context resource scope)] + (if a (not b) b)))) + (-form [_] + (list 'xor (core/-form a) (core/-form b))))) + (defn- dynamic-xor "Creates an xor-expression where `a` is known to be dynamic and `b` could be static or dynamic." [a b] (condp identical? b true - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [a (core/-eval a context resource scope)] - (when (some? a) - (not a)))) - (-form [_] - (list 'not (core/-form a)))) + (not-op a) false a nil nil - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-some [a (core/-eval a context resource scope)] - (when-some [b (core/-eval b context resource scope)] - (if a (not b) b)))) - (-form [_] - (list 'xor (core/-form a) (core/-form b)))))) + (xor-op a b))) (defmethod core/compile* :elm.compiler.type/xor [context {[a b] :operand}] diff --git a/modules/cql/src/blaze/elm/compiler/logical_operators/util.clj b/modules/cql/src/blaze/elm/compiler/logical_operators/util.clj new file mode 100644 index 000000000..a0fb4659f --- /dev/null +++ b/modules/cql/src/blaze/elm/compiler/logical_operators/util.clj @@ -0,0 +1,55 @@ +(ns blaze.elm.compiler.logical-operators.util + (:require + [blaze.elm.expression.cache.bloom-filter :as bloom-filter])) + +(defn insert-sorted [cmp coll x] + (loop [coll (seq coll) result []] + (cond + (empty? coll) + (conj result x) + + (neg? (cmp x (first coll))) + (into result (cons x coll)) + + :else + (recur (rest coll) (conj result (first coll)))))) + +(defn merge-sorted [cmp coll1 coll2] + (loop [result [] + coll1 (seq coll1) + coll2 (seq coll2)] + (cond + (and (empty? coll1) (empty? coll2)) result + (empty? coll1) (into result coll2) + (empty? coll2) (into result coll1) + :else (let [x1 (first coll1) + x2 (first coll2)] + (if (neg? (cmp x2 x1)) + (recur (conj result x2) coll1 (rest coll2)) + (recur (conj result x1) (rest coll1) coll2)))))) + +(defn and-attach-cache-result [op triples] + [(fn [] + [(op (ffirst triples) (mapv first (rest triples))) + (into [] (mapcat second) triples)]) + :and + triples]) + +(defn or-attach-cache-result [op triples] + [(fn [] + [(let [triples (reverse triples)] + (op + (vec + (reduce + (fn [[[_ last-bf] :as r] [op _ bf]] + (cons + (if (and last-bf bf) + [op (bloom-filter/merge last-bf bf)] + [op]) + r)) + (let [[[op _ bf]] triples] + (when op (list [op bf]))) + (rest triples))))) + (into [] (mapcat second) triples)]) + :or + triples]) diff --git a/modules/cql/src/blaze/elm/compiler/macros.clj b/modules/cql/src/blaze/elm/compiler/macros.clj index df2512efe..ba5ec1a35 100644 --- a/modules/cql/src/blaze/elm/compiler/macros.clj +++ b/modules/cql/src/blaze/elm/compiler/macros.clj @@ -1,143 +1,411 @@ (ns blaze.elm.compiler.macros (:require - [blaze.elm.compiler.core :as core])) + [blaze.anomaly :as ba] + [blaze.elm.compiler.core :as core] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [blaze.elm.expression.cache.spec] + [clojure.spec.alpha :as s] + [prometheus.alpha :as prom] + [taoensso.timbre :as log])) (set! *warn-on-reflection* true) -(defn- compile-kw [name] - (keyword "elm.compiler.type" (clojure.core/name name))) +(defn- find-form [form body] + (some #(when (= form (first %)) %) body)) + +(def ^:private ^:const unknown nil) + +(defmacro reify-expr [_ & body] + `(reify + core/Expression + (~'-static [~'_] + false) + ~(if-let [form (find-form '-attach-cache body)] + form + (list '-attach-cache ['expr '_] `[(fn [] [~'expr])])) + ~(if-let [form (find-form '-patient-count body)] + form + (list '-patient-count ['_] unknown)) + ~(if-let [form (find-form '-resolve-refs body)] + form + (list '-resolve-refs ['expr '_] 'expr)) + ~(if-let [form (find-form '-resolve-params body)] + form + (list '-resolve-params ['expr '_] 'expr)) + ~(if-let [form (find-form '-eval body)] + form + (list '-eval ['expr '_ '_ '_] 'expr)) + ~(if-let [form (find-form '-form body)] + form + (list '-form ['_] 'nil)) -(defn generate-binding-vector + Object + (~'equals [~'this ~'other] + (.equals ^Object (core/-form ~'this) (core/-form ~'other))) + (~'hashCode [~'this] + (.hashCode ^Object (core/-form ~'this))))) + +(defn- generate-binding-vector "Creates a binding vector of at least `[operand-binding operand]` and - optionally `[operand-binding operand expr-binding expr-sym]` if `expr-binding` - is given." - [operand-binding operand expr-binding expr-sym] - (cond-> [operand-binding operand] expr-binding (conj expr-binding expr-sym))) - -(defn generate-unop [name operand-sym operand-binding expr-binding expr-sym body] - (let [context-sym (gensym "context") - resource-sym (gensym "resource") - scope-sym (gensym "scope")] - `(reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ ~context-sym ~resource-sym ~scope-sym] - (let ~(generate-binding-vector - operand-binding `(core/-eval ~operand-sym ~context-sym ~resource-sym ~scope-sym) - expr-binding expr-sym) - ~@body)) - (~'-form [~'_] - (list (quote ~name) (core/-form ~operand-sym)))))) + optionally `[operand-binding operand elm-elm-expr-binding elm-expr]` if + `elm-elm-expr-binding` is given." + [operand-binding operand elm-expr-binding elm-expr] + (cond-> [operand-binding operand] elm-expr-binding (conj elm-expr-binding elm-expr))) + +(defn- compile-kw [name] + (keyword "elm.compiler.type" (clojure.core/name name))) (defmacro defunop {:arglists '([name attr-map? bindings & body])} [name & more] (let [attr-map (when (map? (first more)) (first more)) more (if (map? (first more)) (next more) more) - [[operand-binding expr-binding] & body] more - context-sym (gensym "context") - operand-sym (gensym "operand") - expr-sym (gensym "expr")] - `(defmethod core/compile* ~(compile-kw name) - [~context-sym ~expr-sym] - (let [~operand-sym (core/compile* (merge ~context-sym ~(dissoc attr-map :cache)) (:operand ~expr-sym))] - (if (core/static? ~operand-sym) - (let [~operand-binding ~operand-sym - ~(or expr-binding '_) ~expr-sym] - ~@body) - ~(generate-unop name operand-sym operand-binding expr-binding expr-sym body)))))) + [[operand-binding elm-expr-binding] & body] more + operand-key (or (:operand-key attr-map) :operand) + attr-map (dissoc attr-map :operand-key) + op (symbol (str name "-op")) + cache-op (symbol (str name "-cache-op")) + caching-op (symbol (str name "-caching-op")) + elm-expr (gensym "elm-expr") + operand (gensym "operand") + context (gensym "context") + eval-context (gensym "eval-context") + resource (gensym "resource") + scope (gensym "scope") + bloom-filter (gensym "bloom-filter") + expr (gensym "expr")] + `(do + ~(when (:cache attr-map) + `(do + (declare ~caching-op) + + (s/fdef ~cache-op + :args ~(if elm-expr-binding + `(s/cat :operand core/expr? + :bloom-filter ::ec/bloom-filter + :expr :elm/expression) + `(s/cat :operand core/expr? + :bloom-filter ::ec/bloom-filter)) + :ret core/expr?) + + (defn ~cache-op + ~(str "Creates a " name " operator with attached Bloom filter that will be used to increase performance of evaluation.") + ~(cond-> [operand bloom-filter] elm-expr-binding (conj elm-expr)) + (log/trace (format "Create expression `%s` with attached Bloom filter." (list (quote ~name) (core/-form ~operand)))) + (reify-expr core/Expression + (~'-attach-cache [~expr ~'cache] + (if-let [~bloom-filter (ec/get ~'cache ~expr)] + ~(if elm-expr-binding + `[(fn [] + [(~cache-op ~operand ~bloom-filter ~elm-expr) + [~bloom-filter] + ~bloom-filter])] + `[(fn [] + [(~cache-op ~operand ~bloom-filter) + [~bloom-filter] + ~bloom-filter])]) + ~(if elm-expr-binding + `[(fn [] + [(~caching-op ~operand ~elm-expr) + [(ba/unavailable "No Bloom filter available.")]])] + `[(fn [] + [(~caching-op ~operand) + [(ba/unavailable "No Bloom filter available.")]])]))) + (~'-patient-count [~'_] + (::bloom-filter/patient-count ~bloom-filter)) + (~'-resolve-refs [~'_ ~'expression-defs] + ~(if elm-expr-binding + `(~caching-op + (core/-resolve-refs ~operand ~'expression-defs) + ~elm-expr) + `(~caching-op + (core/-resolve-refs ~operand ~'expression-defs)))) + (~'-resolve-params [~'_ ~'parameters] + ~(if elm-expr-binding + `(~caching-op + (core/-resolve-params ~operand ~'parameters) + ~elm-expr) + `(~caching-op + (core/-resolve-params ~operand ~'parameters)))) + (~'-eval [~'_ ~context ~resource ~scope] + (if (bloom-filter/might-contain? ~bloom-filter ~resource) + (let [res# (let ~(generate-binding-vector + operand-binding `(core/-eval ~operand + ~context + ~resource + ~scope) + elm-expr-binding elm-expr) + ~@body)] + (prom/inc! ec/bloom-filter-not-useful-total ~(clojure.core/name name)) + (when-not res# + (prom/inc! ec/bloom-filter-false-positive-total ~(clojure.core/name name))) + res#) + (do (prom/inc! ec/bloom-filter-useful-total ~(clojure.core/name name)) + false))) + (~'-form [~'_] + (list (quote ~name) (core/-form ~operand))))) + + (s/fdef ~caching-op + :args ~(if elm-expr-binding + `(s/cat :operand core/expr? :expr :elm/expression) + `(s/cat :operand core/expr?)) + :ret core/expr?) + + (defn ~caching-op + ~(str "Creates a " name " operator that will handle cache attachment.") + ~(cond-> [operand] elm-expr-binding (conj elm-expr)) + (reify-expr core/Expression + (~'-attach-cache [~expr ~'cache] + (if-let [~bloom-filter (ec/get ~'cache ~expr)] + ;;TODO: add metric of how many times a bloom filter was available + ~(if elm-expr-binding + `[(fn [] + [(~cache-op ~operand ~bloom-filter ~elm-expr) + [~bloom-filter] + ~bloom-filter])] + `[(fn [] + [(~cache-op ~operand ~bloom-filter) + [~bloom-filter] + ~bloom-filter])]) + ~(if elm-expr-binding + `[(fn [] + [(~caching-op ~operand ~elm-expr) + [(ba/unavailable "No Bloom filter available.")]])] + `[(fn [] + [(~caching-op ~operand) + [(ba/unavailable "No Bloom filter available.")]])]))) + (~'-resolve-refs [~'_ ~'expression-defs] + ~(if elm-expr-binding + `(~caching-op + (core/-resolve-refs ~operand ~'expression-defs) + ~elm-expr) + `(~caching-op + (core/-resolve-refs ~operand ~'expression-defs)))) + (~'-resolve-params [~'_ ~'parameters] + ~(if elm-expr-binding + `(~caching-op + (core/-resolve-params ~operand ~'parameters) + ~elm-expr) + `(~caching-op + (core/-resolve-params ~operand ~'parameters)))) + (~'-eval [~'_ ~context ~resource ~scope] + (let ~(generate-binding-vector + operand-binding `(core/-eval ~operand ~context ~resource ~scope) + elm-expr-binding elm-expr) + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~operand))))))) + + (defn ~op + ~(str "Creates a " name " operator that will only delegate cache attachment.") + ~(cond-> [operand] elm-expr-binding (conj elm-expr)) + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + ~(if elm-expr-binding + `(core/attach-cache-helper-1 ~op ~'cache ~operand ~elm-expr) + `(core/attach-cache-helper ~op ~'cache ~operand))) + (~'-resolve-refs [~'_ ~'expression-defs] + ~(if elm-expr-binding + `(~op + (core/-resolve-refs ~operand ~'expression-defs) + ~elm-expr) + `(~op + (core/-resolve-refs ~operand ~'expression-defs)))) + (~'-resolve-params [~'_ ~'parameters] + ~(if elm-expr-binding + `(~op + (core/-resolve-params ~operand ~'parameters) + ~elm-expr) + `(~op + (core/-resolve-params ~operand ~'parameters)))) + (~'-eval [~'_ ~context ~resource ~scope] + (let ~(generate-binding-vector + operand-binding `(core/-eval ~operand ~context ~resource ~scope) + elm-expr-binding elm-expr) + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~operand))))) + + (defmethod core/compile* ~(compile-kw name) + [{~eval-context :eval-context :as ~context} + {~operand ~operand-key :as ~elm-expr}] + (let [~operand (core/compile* (merge ~context ~(dissoc attr-map :cache)) ~operand)] + (if (core/static? ~operand) + (let ~(generate-binding-vector + operand-binding operand + elm-expr-binding elm-expr) + ~@body) + ~(if (:cache attr-map) + `(if (= "Patient" ~eval-context) + ~(if elm-expr-binding + `(~caching-op ~operand ~elm-expr) + `(~caching-op ~operand)) + ~(if elm-expr-binding + `(~op ~operand ~elm-expr) + `(~op ~operand))) + (if elm-expr-binding + `(~op ~operand ~elm-expr) + `(~op ~operand))))))))) (defmacro defbinop {:arglists '([name attr-map? bindings & body])} [name & more] (let [attr-map (when (map? (first more)) (first more)) more (if (map? (first more)) (next more) more) - [[op-1-binding op-2-binding] & body] more] - `(defmethod core/compile* ~(compile-kw name) - [context# {[operand-1# operand-2#] :operand}] - (let [context# (merge context# ~attr-map) - operand-1# (core/compile* context# operand-1#) - operand-2# (core/compile* context# operand-2#)] - (if (and (core/static? operand-1#) (core/static? operand-2#)) - (let [~op-1-binding operand-1# - ~op-2-binding operand-2#] - ~@body) - (reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ context# resource# scope#] - (let [~op-1-binding (core/-eval operand-1# context# resource# scope#) - ~op-2-binding (core/-eval operand-2# context# resource# scope#)] - ~@body)) - (~'-form [~'_] - (list (quote ~name) (core/-form operand-1#) (core/-form operand-2#))))))))) + [[op-1-binding op-2-binding] & body] more + context (gensym "context") + op (symbol (str name "-op")) + op-1 (gensym "op-1") + op-2 (gensym "op-2")] + `(do + (defn ~op [~op-1 ~op-2] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper ~op ~'cache ~op-1 ~op-2)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op + (core/-resolve-refs ~op-1 ~'expression-defs) + (core/-resolve-refs ~op-2 ~'expression-defs))) + (~'-resolve-params [~'_ ~'parameters] + (core/resolve-params-helper ~op ~'parameters ~op-1 ~op-2)) + (~'-eval [~'_ context# resource# scope#] + (let [~op-1-binding (core/-eval ~op-1 context# resource# scope#) + ~op-2-binding (core/-eval ~op-2 context# resource# scope#)] + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~op-1) (core/-form ~op-2))))) + + (defmethod core/compile* ~(compile-kw name) + [~context {[~op-1 ~op-2] :operand}] + (let [context# ~(if attr-map `(merge ~context ~attr-map) context) + ~op-1 (core/compile* context# ~op-1) + ~op-2 (core/compile* context# ~op-2)] + (if (and (core/static? ~op-1) (core/static? ~op-2)) + (let [~op-1-binding ~op-1 + ~op-2-binding ~op-2] + ~@body) + (~op ~op-1 ~op-2))))))) (defmacro defternop {:arglists '([name bindings & body])} [name [op-1-binding op-2-binding op-3-binding] & body] - `(defmethod core/compile* ~(compile-kw name) - [context# {[operand-1# operand-2# operand-3#] :operand}] - (let [operand-1# (core/compile* context# operand-1#) - operand-2# (core/compile* context# operand-2#) - operand-3# (core/compile* context# operand-3#)] - (reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ context# resource# scope#] - (let [~op-1-binding (core/-eval operand-1# context# resource# scope#) - ~op-2-binding (core/-eval operand-2# context# resource# scope#) - ~op-3-binding (core/-eval operand-3# context# resource# scope#)] - ~@body)) - (~'-form [~'_] - (list (quote ~name) (core/-form operand-1#) (core/-form operand-2#) - (core/-form operand-3#))))))) + (let [op (symbol (str name "-op")) + op-1 (gensym "op-1") + op-2 (gensym "op-2") + op-3 (gensym "op-3")] + `(do + (defn ~op [~op-1 ~op-2 ~op-3] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper ~op ~'cache ~op-1 ~op-2 ~op-3)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op + (core/-resolve-refs ~op-1 ~'expression-defs) + (core/-resolve-refs ~op-2 ~'expression-defs) + (core/-resolve-refs ~op-3 ~'expression-defs))) + (~'-resolve-params [~'_ ~'parameters] + (core/resolve-params-helper ~op ~'parameters ~op-1 ~op-2 ~op-3)) + (~'-eval [~'_ context# resource# scope#] + (let [~op-1-binding (core/-eval ~op-1 context# resource# scope#) + ~op-2-binding (core/-eval ~op-2 context# resource# scope#) + ~op-3-binding (core/-eval ~op-3 context# resource# scope#)] + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~op-1) (core/-form ~op-2) + (core/-form ~op-3))))) + (defmethod core/compile* ~(compile-kw name) + [context# {[~op-1 ~op-2 ~op-3] :operand}] + (let [~op-1 (core/compile* context# ~op-1) + ~op-2 (core/compile* context# ~op-2) + ~op-3 (core/compile* context# ~op-3)] + (if (and (core/static? ~op-1) (core/static? ~op-2) (core/static? ~op-3)) + (let [~op-1-binding ~op-1 + ~op-2-binding ~op-2 + ~op-3-binding ~op-3] + ~@body) + (~op ~op-1 ~op-2 ~op-3))))))) (defmacro defnaryop {:arglists '([name bindings & body])} [name [operands-binding] & body] - `(defmethod core/compile* ~(compile-kw name) - [context# {operands# :operand}] - (let [operands# (mapv #(core/compile* context# %) operands#)] - (reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ context# resource# scope#] - (let [~operands-binding (mapv #(core/-eval % context# resource# scope#) operands#)] - ~@body)) - (~'-form [~'_] - (cons (quote ~name) (map core/-form operands#))))))) + (let [op (symbol (str name "-op"))] + `(do + (defn ~op [~operands-binding] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper-list ~op ~'cache ~operands-binding)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op + (mapv #(core/-resolve-refs % ~'expression-defs) ~operands-binding))) + (~'-resolve-params [~'_ ~'parameters] + (~op + (mapv #(core/-resolve-params % ~'parameters) ~operands-binding))) + (~'-eval [~'_ context# resource# scope#] + (let [~operands-binding (mapv #(core/-eval % context# resource# scope#) ~operands-binding)] + ~@body)) + (~'-form [~'_] + (cons (quote ~name) (map core/-form ~operands-binding))))) + + (defmethod core/compile* ~(compile-kw name) + [context# {operands# :operand}] + (~op (mapv #(core/compile* context# %) operands#)))))) (defmacro defaggop {:arglists '([name bindings & body])} [name [source-binding] & body] - `(defmethod core/compile* ~(compile-kw name) - [context# {source# :source}] - (let [source# (core/compile* context# source#)] - (reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ context# resource# scope#] - (let [~source-binding (core/-eval source# context# resource# scope#)] - ~@body)) - (~'-form [~'_] - (list (quote ~name) (core/-form source#))))))) + (let [op (symbol (str name "-op"))] + `(do + (defn ~op [~source-binding] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper ~op ~'cache ~source-binding)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op (core/-resolve-refs ~source-binding ~'expression-defs))) + (~'-resolve-params [~'_ ~'parameters] + (~op (core/-resolve-params ~source-binding ~'parameters))) + (~'-eval [~'_ context# resource# scope#] + (let [~source-binding (core/-eval ~source-binding context# resource# scope#)] + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~source-binding))))) + + (defmethod core/compile* ~(compile-kw name) + [context# {source# :source}] + (~op (core/compile* context# source#)))))) (defmacro defunopp {:arglists '([name bindings & body])} - [name [operand-binding precision-binding expr-binding] & body] - `(defmethod core/compile* ~(compile-kw name) - [context# {operand# :operand precision# :precision :as expr#}] - (let [operand# (core/compile* context# operand#) - ~precision-binding (some-> precision# core/to-chrono-unit) - ~(or expr-binding '_) expr#] - (reify core/Expression - (~'-static [~'_] - false) - (~'-eval [~'_ context# resource# scope#] - (let [~operand-binding (core/-eval operand# context# resource# scope#)] - ~@body)) - (~'-form [~'_] - (list (quote ~name) (core/-form operand#) precision#)))))) + [name [operand-binding precision-binding] & body] + (let [op (symbol (str name "-op")) + operand (gensym "operand") + precision (gensym "precision")] + `(do + (defn ~op [~operand ~precision-binding ~precision] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper-2 ~op ~'cache ~operand ~precision-binding + ~precision)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op + (core/-resolve-refs ~operand ~'expression-defs) + ~precision-binding ~precision)) + (~'-resolve-params [~'_ ~'parameters] + (~op + (core/-resolve-params ~operand ~'parameters) + ~precision-binding ~precision)) + (~'-eval [~'_ context# resource# scope#] + (let [~operand-binding (core/-eval ~operand context# resource# scope#)] + ~@body)) + (~'-form [~'_] + (list (quote ~name) (core/-form ~operand) ~precision)))) + + (defmethod core/compile* ~(compile-kw name) + [context# {operand# :operand precision# :precision}] + (~op + (core/compile* context# operand#) + (some-> precision# core/to-chrono-unit) + precision#))))) (defmacro defbinopp {:arglists '([name attr-map? bindings & body])} @@ -147,15 +415,23 @@ [[op-1-binding op-2-binding precision-binding] & body] more precision-required (:required (meta precision-binding)) context (gensym "context") + op (symbol (str name "-op")) + precision-op (symbol (str name "-precision-op")) op-1 (gensym "op-1") op-2 (gensym "op-2") precision (gensym "precision")] `(do ~(when-not precision-required - `(defn ~(symbol (str name "-op")) [~op-1 ~op-2] - (reify core/Expression - (~'-static [~'_] - false) + `(defn ~op [~op-1 ~op-2] + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper ~op ~'cache ~op-1 ~op-2)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~op + (core/-resolve-refs ~op-1 ~'expression-defs) + (core/-resolve-refs ~op-2 ~'expression-defs))) + (~'-resolve-params [~'_ ~'parameters] + (core/resolve-params-helper ~op ~'parameters ~op-1 ~op-2)) (~'-eval [~'_ context# resource# scope#] (let [~op-1-binding (core/-eval ~op-1 context# resource# scope#) ~op-2-binding (core/-eval ~op-2 context# resource# scope#) @@ -164,11 +440,22 @@ (~'-form [~'_] (list (quote ~name) (core/-form ~op-1) (core/-form ~op-2)))))) - (defn ~(symbol (str name "-precision-op")) + (defn ~precision-op [~op-1 ~op-2 ~precision-binding ~precision] - (reify core/Expression - (~'-static [~'_] - false) + (reify-expr core/Expression + (~'-attach-cache [~'_ ~'cache] + (core/attach-cache-helper-2 ~precision-op ~'cache ~op-1 ~op-2 + ~precision-binding ~precision)) + (~'-resolve-refs [~'_ ~'expression-defs] + (~precision-op + (core/-resolve-refs ~op-1 ~'expression-defs) + (core/-resolve-refs ~op-2 ~'expression-defs) + ~precision-binding ~precision)) + (~'-resolve-params [~'_ ~'parameters] + (~precision-op + (core/-resolve-params ~op-1 ~'parameters) + (core/-resolve-params ~op-2 ~'parameters) + ~precision-binding ~precision)) (~'-eval [~'_ context# resource# scope#] (let [~op-1-binding (core/-eval ~op-1 context# resource# scope#) ~op-2-binding (core/-eval ~op-2 context# resource# scope#)] @@ -188,8 +475,7 @@ (let [~op-1-binding ~op-1 ~op-2-binding ~op-2] ~@body) - (~(symbol (str name "-precision-op")) ~op-1 ~op-2 - ~precision-binding ~precision)))) + (~precision-op ~op-1 ~op-2 ~precision-binding ~precision)))) `(defmethod core/compile* ~(compile-kw name) [~context {[~op-1 ~op-2] :operand ~precision :precision}] @@ -202,6 +488,5 @@ ~op-2-binding ~op-2] ~@body) (if ~precision - (~(symbol (str name "-precision-op")) ~op-1 ~op-2 - ~precision-binding ~precision) - (~(symbol (str name "-op")) ~op-1 ~op-2))))))))) + (~precision-op ~op-1 ~op-2 ~precision-binding ~precision) + (~op ~op-1 ~op-2))))))))) diff --git a/modules/cql/src/blaze/elm/compiler/nullological_operators.clj b/modules/cql/src/blaze/elm/compiler/nullological_operators.clj index ba911eb59..f462f57dc 100644 --- a/modules/cql/src/blaze/elm/compiler/nullological_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/nullological_operators.clj @@ -5,7 +5,7 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defunop]])) + [blaze.elm.compiler.macros :refer [defunop reify-expr]])) ;; 14.1. Null (defmethod core/compile* :elm.compiler.type/null @@ -19,41 +19,50 @@ ;; subsequent arguments must be of that same type. ;; ;; TODO: The list type argument is missing in the doc. +(defn- coalesce-op [operands] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper-list coalesce-op cache operands)) + (-resolve-refs [_ expression-defs] + (coalesce-op (mapv #(core/-resolve-refs % expression-defs) operands))) + (-resolve-params [_ parameters] + (coalesce-op (mapv #(core/-resolve-params % parameters) operands))) + (-eval [_ context resource scope] + (reduce + (fn [_ operand] + (let [operand (core/-eval operand context resource scope)] + (when (some? operand) + (reduced operand)))) + nil + operands)) + (-form [_] + `(~'coalesce ~@(map core/-form operands))))) + +(defn coalesce-list-op [list] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper coalesce-op cache list)) + (-resolve-refs [_ expression-defs] + (coalesce-op (core/-resolve-refs list expression-defs))) + (-resolve-params [_ parameters] + (coalesce-op (core/-resolve-params list parameters))) + (-eval [_ context resource scope] + (reduce + (fn [_ elem] + (let [elem (core/-eval elem context resource scope)] + (when (some? elem) + (reduced elem)))) + nil + (core/-eval list context resource scope))))) + (defmethod core/compile* :elm.compiler.type/coalesce [context {operands :operand}] (if (= 1 (count operands)) (let [operand (first operands)] - (if (= "List" (:type operand)) - (let [operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (reduce - (fn [_ elem] - (let [elem (core/-eval elem context resource scope)] - (when (some? elem) - (reduced elem)))) - nil - (core/-eval operand context resource scope))))) - (let [operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (core/-eval operand context resource scope)))))) - (let [operands (mapv #(core/compile* context %) operands)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (reduce - (fn [_ operand] - (let [operand (core/-eval operand context resource scope)] - (when (some? operand) - (reduced operand)))) - nil - operands)))))) + (cond-> (core/compile* context operand) + (= "List" (:type operand)) + coalesce-list-op)) + (coalesce-op (mapv #(core/compile* context %) operands)))) ;; 14.3. IsFalse (defunop is-false [operand] diff --git a/modules/cql/src/blaze/elm/compiler/parameters.clj b/modules/cql/src/blaze/elm/compiler/parameters.clj index 97e91221c..5fa9e863f 100644 --- a/modules/cql/src/blaze/elm/compiler/parameters.clj +++ b/modules/cql/src/blaze/elm/compiler/parameters.clj @@ -5,7 +5,8 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.anomaly :as ba :refer [throw-anom]] - [blaze.elm.compiler.core :as core])) + [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]])) ;; 7.1. ParameterDef ;; @@ -17,17 +18,20 @@ (format "Value of parameter `%s` not found." name) :context context)) -(defrecord ParameterRef [name] - core/Expression - (-static [_] - false) - (-eval [_ {:keys [parameters] :as context} _ _] - (let [value (get parameters name ::not-found)] - (if (identical? ::not-found value) - (throw-anom (parameter-value-not-found-anom context name)) - value))) - (-form [_] - `(~'param-ref ~name))) +(defn- param-ref [name] + (reify-expr core/Expression + (-resolve-params [expr parameters] + (let [value (get parameters name ::not-found)] + (if (identical? ::not-found value) + expr + value))) + (-eval [_ {:keys [parameters] :as context} _ _] + (let [value (get parameters name ::not-found)] + (if (identical? ::not-found value) + (throw-anom (parameter-value-not-found-anom context name)) + value))) + (-form [_] + `(~'param-ref ~name)))) (defn- find-parameter-def "Returns the parameter-def with `name` from `library` or nil if not found." @@ -44,5 +48,5 @@ [{:keys [library] :as context} {:keys [name]}] ;; TODO: look into other libraries (:libraryName) (if-let [{:keys [name]} (find-parameter-def library name)] - (->ParameterRef name) + (param-ref name) (throw-anom (parameter-def-not-found-anom context name)))) diff --git a/modules/cql/src/blaze/elm/compiler/queries.clj b/modules/cql/src/blaze/elm/compiler/queries.clj index 0df74039e..465bd2bc1 100644 --- a/modules/cql/src/blaze/elm/compiler/queries.clj +++ b/modules/cql/src/blaze/elm/compiler/queries.clj @@ -7,7 +7,7 @@ (:require [blaze.coll.core :as coll] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.structured-values :as structured-values] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.protocols :as p] [blaze.fhir.spec]) (:import @@ -18,12 +18,18 @@ (defprotocol XformFactory (-create [_ context resource scope] "Creates a xform which filters and/or shapes query sources.") + (-resolve-refs [_ expression-defs]) + (-resolve-params [_ parameters]) (-form [_])) (defn- where-xform-factory [alias expr] (reify XformFactory (-create [_ context resource scope] (filter #(core/-eval expr context resource (assoc scope alias %)))) + (-resolve-refs [_ expression-defs] + (where-xform-factory alias (core/-resolve-refs expr expression-defs))) + (-resolve-params [_ parameters] + (where-xform-factory alias (core/-resolve-params expr parameters))) (-form [_] `(~'filter (~'fn [~(symbol alias)] ~(core/-form expr)))))) @@ -31,6 +37,10 @@ (reify XformFactory (-create [_ context resource scope] (map #(core/-eval expr context resource (assoc scope alias %)))) + (-resolve-refs [_ expression-defs] + (return-xform-factory* alias (core/-resolve-refs expr expression-defs))) + (-resolve-params [_ parameters] + (return-xform-factory* alias (core/-resolve-params expr parameters))) (-form [_] `(~'map (~'fn [~(symbol alias)] ~(core/-form expr)))))) @@ -38,6 +48,10 @@ (reify XformFactory (-create [_ _ _ _] (distinct)) + (-resolve-refs [this _] + this) + (-resolve-params [this _] + this) (-form [_] 'distinct))) @@ -47,6 +61,10 @@ (comp (-create xform-factory context resource scope) (distinct))) + (-resolve-refs [_ expression-defs] + (composed-distinct-xform-factory (-resolve-refs xform-factory expression-defs))) + (-resolve-params [_ parameters] + (composed-distinct-xform-factory (-resolve-params xform-factory parameters))) (-form [_] `(~'comp ~(-form xform-factory) ~'distinct)))) @@ -60,6 +78,10 @@ (reify XformFactory (-create [_ context resource scope] (transduce (map #(-create % context resource scope)) comp factories)) + (-resolve-refs [_ expression-defs] + (composed-xform-factory (mapv #(-resolve-refs % expression-defs) factories))) + (-resolve-params [_ parameters] + (composed-xform-factory (mapv #(-resolve-params % parameters) factories))) (-form [_] `(~'comp ~@(map -form factories))))) @@ -77,9 +99,13 @@ return-xform-factory))) (defn- eduction-expr [xform-factory source] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (eduction-expr (-resolve-refs xform-factory expression-defs) + (core/-resolve-refs source expression-defs))) + (-resolve-params [_ parameters] + (eduction-expr (-resolve-params xform-factory parameters) + (core/-resolve-params source parameters))) (-eval [_ context resource scope] (coll/eduction (-create xform-factory context resource scope) @@ -88,9 +114,13 @@ `(~'eduction-query ~(-form xform-factory) ~(core/-form source))))) (defn- into-vector-expr [xform-factory source] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (into-vector-expr (-resolve-refs xform-factory expression-defs) + (core/-resolve-refs source expression-defs))) + (-resolve-params [_ parameters] + (into-vector-expr (-resolve-params xform-factory parameters) + (core/-resolve-params source parameters))) (-eval [_ context resource scope] (into [] @@ -134,9 +164,15 @@ (symbol (:direction sort-by-item)))) (defn- xform-sort-expr [xform-factory source sort-by-item] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (xform-sort-expr (-resolve-refs xform-factory expression-defs) + (core/-resolve-refs source expression-defs) + (update sort-by-item :expression core/-resolve-refs expression-defs))) + (-resolve-params [_ parameters] + (xform-sort-expr (-resolve-params xform-factory parameters) + (core/-resolve-params source parameters) + (update sort-by-item :expression core/-resolve-params parameters))) (-eval [_ context resource scope] ;; TODO: build a comparator of all sort by items (->> (into @@ -192,26 +228,51 @@ (throw (Exception. (str "Unsupported number of " (count sources) " sources in query."))))) ;; 10.3. AliasRef -(defrecord AliasRefExpression [key] - core/Expression - (-static [_] - false) - (-eval [_ _ _ scopes] - (get scopes key)) - (-form [_] - `(~'alias-ref ~(symbol key)))) - (defmethod core/compile* :elm.compiler.type/alias-ref [_ {:keys [name]}] - (->AliasRefExpression name)) + (reify-expr core/Expression + (-eval [_ _ _ scopes] + (get scopes name)) + (-form [_] + `(~'alias-ref ~(symbol name))))) ;; 10.7 IdentifierRef (defmethod core/compile* :elm.compiler.type/identifier-ref [_ {:keys [name]}] - (structured-values/->SingleScopePropertyExpression (keyword name))) + (let [key (keyword name)] + (reify-expr core/Expression + (-eval [_ _ _ value] + (p/get value key)) + (-form [_] + `(~key ~'default))))) ;; 10.14. With ;; 10.15. Without +(defn- relationship-clause-xform-factory [lhs-alias rhs-alias rhs such-that exists-fn form-sym] + (reify XformFactory + (-create [_ context resource scope] + (filter + (fn [lhs-item] + (let [scope (assoc scope lhs-alias lhs-item)] + (exists-fn + #(core/-eval such-that context resource (assoc scope rhs-alias %)) + (core/-eval rhs context resource scope)))))) + (-resolve-refs [_ expression-defs] + (relationship-clause-xform-factory + lhs-alias rhs-alias (core/-resolve-refs rhs expression-defs) + (core/-resolve-refs such-that expression-defs) exists-fn form-sym)) + (-resolve-params [_ parameters] + (relationship-clause-xform-factory + lhs-alias rhs-alias (core/-resolve-params rhs parameters) + (core/-resolve-params such-that parameters) exists-fn form-sym)) + (-form [_] + `(~'filter + (~'fn [~(symbol lhs-alias)] + (~form-sym + (~'fn [~(symbol rhs-alias)] + ~(core/-form such-that)) + ~(core/-form rhs))))))) + (defn compile-relationship-clause "We use the terms `lhs` and `rhs` for left-hand-side and right-hand-side of the semi-join here. @@ -223,18 +284,4 @@ such-that (core/compile* context such-that) exists-fn (if (= "With" type) coll/some (comp not coll/some)) form-sym (if (= "With" type) 'exists 'not-exists)] - (reify XformFactory - (-create [_ context resource scope] - (filter - (fn [lhs-item] - (let [scope (assoc scope lhs-alias lhs-item)] - (exists-fn - #(core/-eval such-that context resource (assoc scope rhs-alias %)) - (core/-eval rhs context resource scope)))))) - (-form [_] - `(~'filter - (~'fn [~(symbol lhs-alias)] - (~form-sym - (~'fn [~(symbol rhs-alias)] - ~(core/-form such-that)) - ~(core/-form rhs)))))))) + (relationship-clause-xform-factory lhs-alias rhs-alias rhs such-that exists-fn form-sym))) diff --git a/modules/cql/src/blaze/elm/compiler/reusing_logic.clj b/modules/cql/src/blaze/elm/compiler/reusing_logic.clj index 463fe96ec..dfa142ec8 100644 --- a/modules/cql/src/blaze/elm/compiler/reusing_logic.clj +++ b/modules/cql/src/blaze/elm/compiler/reusing_logic.clj @@ -8,6 +8,7 @@ [blaze.db.api :as d] [blaze.elm.code :as code] [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.interval :as interval] [blaze.elm.protocols :as p] [blaze.elm.quantity :as quantity] @@ -26,16 +27,16 @@ (defn- expression-not-found-anom [context name] (ba/incorrect (format "Expression `%s` not found." name) :context context)) -(defrecord ExpressionRef [name] - core/Expression - (-static [_] - false) - (-eval [_ {:keys [expression-defs] :as context} resource _] - (if-let [{:keys [expression]} (get expression-defs name)] - (core/-eval expression context resource nil) - (throw-anom (expression-not-found-anom context name)))) - (-form [_] - `(~'expr-ref ~name))) +(defn- expr-ref [name] + (reify-expr core/Expression + (-resolve-refs [expr expression-defs] + (or (:expression (get expression-defs name)) expr)) + (-eval [_ {:keys [expression-defs] :as context} resource _] + (if-let [{:keys [expression]} (get expression-defs name)] + (core/-eval expression context resource nil) + (throw-anom (expression-not-found-anom context name)))) + (-form [_] + (list 'expr-ref name)))) (defn- find-def "Returns the def with `name` from `library` or nil if not found." @@ -64,9 +65,7 @@ ;; The referenced expression has a concrete context but we are in the ;; Unfiltered context. So we map the referenced expression over all ;; concrete resources. - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ {:keys [db expression-defs] :as context} _ _] (if-some [{:keys [expression]} (get expression-defs name)] (mapv @@ -76,8 +75,8 @@ :else (if-let [result-type-name (:resultTypeName def)] - (vary-meta (->ExpressionRef name) assoc :result-type-name result-type-name) - (->ExpressionRef name))) + (vary-meta (expr-ref name) assoc :result-type-name result-type-name) + (expr-ref name))) (throw-anom (expression-def-not-found-anom context name))))) (defprotocol ToQuantity @@ -96,51 +95,71 @@ nil (-to-quantity [_])) -(defrecord ToQuantityFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (-to-quantity (core/-eval operand context resource scope))) - (-form [_] - `(~'call "ToQuantity" ~(core/-form operand)))) - -(defrecord ToCodeFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [{:keys [system version code]} (core/-eval operand context resource scope)] - (code/to-code (type/value system) (type/value version) (type/value code)))) - (-form [_] - `(~'call "ToCode" ~(core/-form operand)))) - -(defrecord ToDateFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (type/value (core/-eval operand context resource scope))) - (-form [_] - `(~'call "ToDate" ~(core/-form operand)))) - -(defrecord ToDateTimeFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (p/to-date-time (type/value (core/-eval operand context resource scope)) now)) - (-form [_] - `(~'call "ToDateTime" ~(core/-form operand)))) - -(defrecord ToStringFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (some-> (type/value (core/-eval operand context resource scope)) str)) - (-form [_] - `(~'call "ToString" ~(core/-form operand)))) +(defn- to-quantity-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-quantity-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-quantity-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper to-quantity-function-expr parameters operand)) + (-eval [_ context resource scope] + (-to-quantity (core/-eval operand context resource scope))) + (-form [_] + `(~'call "ToQuantity" ~(core/-form operand))))) + +(defn- to-code-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-code-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-code-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper to-code-function-expr parameters operand)) + (-eval [_ context resource scope] + (let [{:keys [system version code]} (core/-eval operand context resource scope)] + (code/to-code (type/value system) (type/value version) (type/value code)))) + (-form [_] + `(~'call "ToCode" ~(core/-form operand))))) + +(defn- to-date-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-date-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-date-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper to-date-function-expr parameters operand)) + (-eval [_ context resource scope] + (type/value (core/-eval operand context resource scope))) + (-form [_] + `(~'call "ToDate" ~(core/-form operand))))) + +(defn- to-date-time-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-date-time-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-date-time-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (to-date-time-function-expr (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (p/to-date-time (type/value (core/-eval operand context resource scope)) now)) + (-form [_] + `(~'call "ToDateTime" ~(core/-form operand))))) + +(defn- to-string-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-string-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-string-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper to-string-function-expr parameters operand)) + (-eval [_ context resource scope] + (some-> (type/value (core/-eval operand context resource scope)) str)) + (-form [_] + `(~'call "ToString" ~(core/-form operand))))) (defprotocol ToInterval (-to-interval [x context])) @@ -155,14 +174,18 @@ nil (-to-interval [_ _])) -(defrecord ToIntervalFunctionExpression [operand] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (-to-interval (core/-eval operand context resource scope) context)) - (-form [_] - `(~'call "ToInterval" ~(core/-form operand)))) +(defn- to-interval-function-expr [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-interval-function-expr cache operand)) + (-resolve-refs [_ expression-defs] + (to-interval-function-expr (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (to-interval-function-expr (core/-resolve-params operand parameters))) + (-eval [_ context resource scope] + (-to-interval (core/-eval operand context resource scope) context)) + (-form [_] + `(~'call "ToInterval" ~(core/-form operand))))) (defn- function-def-not-found-anom [context name] (ba/incorrect @@ -181,34 +204,32 @@ (let [operands (map #(core/compile* context %) operands)] (case name "ToQuantity" - (->ToQuantityFunctionExpression (first operands)) + (to-quantity-function-expr (first operands)) "ToDate" - (->ToDateFunctionExpression (first operands)) + (to-date-function-expr (first operands)) "ToDateTime" - (->ToDateTimeFunctionExpression (first operands)) + (to-date-time-function-expr (first operands)) "ToString" - (->ToStringFunctionExpression (first operands)) + (to-string-function-expr (first operands)) "ToCode" - (->ToCodeFunctionExpression (first operands)) + (to-code-function-expr (first operands)) "ToDecimal" (first operands) "ToInterval" - (->ToIntervalFunctionExpression (first operands)) + (to-interval-function-expr (first operands)) (compile-function context name operands)))) ;; 9.5 OperandRef (defmethod core/compile* :elm.compiler.type/operand-ref [_ {:keys [name]}] - (reify core/Expression - (-static [_] - false) + (reify-expr core/Expression (-eval [_ _ _ scope] (scope name)) (-form [_] diff --git a/modules/cql/src/blaze/elm/compiler/spec.clj b/modules/cql/src/blaze/elm/compiler/spec.clj index 82ac73145..f78cc4b9f 100644 --- a/modules/cql/src/blaze/elm/compiler/spec.clj +++ b/modules/cql/src/blaze/elm/compiler/spec.clj @@ -1,15 +1,20 @@ (ns blaze.elm.compiler.spec (:require [blaze.db.spec] + [blaze.elm.compiler :as-alias c] [blaze.elm.compiler.core :as core] [blaze.elm.spec] + [blaze.fhir.spec.spec] [clojure.spec.alpha :as s])) -(s/def :blaze.elm.compiler/expression +(s/def ::c/expression core/expr?) -(s/def :blaze.elm.compiler/function +(s/def ::c/function fn?) +(s/def ::c/eval-context + (s/or :unfiltered #{"Unfiltered"} :resource-type :fhir.resource/type)) + (s/def :elm/compile-context - (s/keys :req-un [:elm/library :blaze.db/node])) + (s/keys :req-un [:elm/library ::c/eval-context :blaze.db/node])) diff --git a/modules/cql/src/blaze/elm/compiler/string_operators.clj b/modules/cql/src/blaze/elm/compiler/string_operators.clj index 8ac443d4d..9b9f714e5 100644 --- a/modules/cql/src/blaze/elm/compiler/string_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/string_operators.clj @@ -5,7 +5,7 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinop defnaryop defternop defunop]] + [blaze.elm.compiler.macros :refer [defbinop defnaryop defternop defunop reify-expr]] [blaze.elm.protocols :as p] [blaze.elm.string :as string] [clojure.string :as str])) @@ -13,28 +13,39 @@ (set! *warn-on-reflection* true) ;; 17.1. Combine +(defn combine-op + ([source] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (combine-op (core/-resolve-refs source expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper combine-op parameters source)) + (-eval [_ context resource scope] + (when-let [source (core/-eval source context resource scope)] + (string/combine source))) + (-form [_] + (list 'combine (core/-form source))))) + ([source separator] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (combine-op (core/-resolve-refs source expression-defs) + (core/-resolve-refs separator expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper combine-op parameters source separator)) + (-eval [_ context resource scope] + (when-let [source (core/-eval source context resource scope)] + (string/combine (core/-eval separator context resource scope) + source))) + (-form [_] + (list 'combine (core/-form source) (core/-form separator)))))) + (defmethod core/compile* :elm.compiler.type/combine [context {:keys [source separator]}] (let [source (core/compile* context source) separator (some->> separator (core/compile* context))] (if separator - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [source (core/-eval source context resource scope)] - (string/combine (core/-eval separator context resource scope) - source))) - (-form [_] - (list 'combine (core/-form source) (core/-form separator)))) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [source (core/-eval source context resource scope)] - (string/combine source))) - (-form [_] - (list 'combine (core/-form source))))))) + (combine-op source separator) + (combine-op source)))) ;; 17.2. Concatenate (defnaryop concatenate [strings] @@ -50,19 +61,26 @@ (p/indexer x index)) ;; 17.7. LastPositionOf +(defn last-position-of-op [pattern string] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper last-position-of-op cache pattern string)) + (-resolve-refs [_ expression-defs] + (last-position-of-op (core/-resolve-refs pattern expression-defs) + (core/-resolve-refs string expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper last-position-of-op parameters pattern string)) + (-eval [_ context resource scope] + (when-let [^String pattern (core/-eval pattern context resource scope)] + (when-let [^String string (core/-eval string context resource scope)] + (.lastIndexOf string pattern)))) + (-form [_] + (list 'last-position-of (core/-form pattern) (core/-form string))))) + (defmethod core/compile* :elm.compiler.type/last-position-of [context {:keys [pattern string]}] - (let [pattern (core/compile* context pattern) - string (core/compile* context string)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [^String pattern (core/-eval pattern context resource scope)] - (when-let [^String string (core/-eval string context resource scope)] - (.lastIndexOf string pattern)))) - (-form [_] - (list 'last-position-of (core/-form pattern) (core/-form string)))))) + (last-position-of-op (core/compile* context pattern) + (core/compile* context string))) ;; 17.8. Length (defunop length [x] @@ -78,19 +96,26 @@ (some? (re-matches (re-pattern pattern) s)))) ;; 17.12. PositionOf +(defn position-of-op [pattern string] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper position-of-op cache pattern string)) + (-resolve-refs [_ expression-defs] + (position-of-op (core/-resolve-refs pattern expression-defs) + (core/-resolve-refs string expression-defs))) + (-resolve-params [_ parameters] + (core/resolve-params-helper position-of-op parameters pattern string)) + (-eval [_ context resource scope] + (when-let [^String pattern (core/-eval pattern context resource scope)] + (when-let [^String string (core/-eval string context resource scope)] + (.indexOf string pattern)))) + (-form [_] + (list 'position-of (core/-form pattern) (core/-form string))))) + (defmethod core/compile* :elm.compiler.type/position-of [context {:keys [pattern string]}] - (let [pattern (core/compile* context pattern) - string (core/compile* context string)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [^String pattern (core/-eval pattern context resource scope)] - (when-let [^String string (core/-eval string context resource scope)] - (.indexOf string pattern)))) - (-form [_] - (list 'position-of (core/-form pattern) (core/-form string)))))) + (position-of-op (core/compile* context pattern) + (core/compile* context string))) ;; 17.13. ReplaceMatches (defternop replace-matches [s pattern substitution] @@ -98,46 +123,44 @@ (str/replace s (re-pattern pattern) substitution))) ;; 17.14. Split +(defn- split-op [string separator] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper split-op cache string separator)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper split-op expression-defs string separator)) + (-resolve-params [_ parameters] + (core/resolve-params-helper split-op parameters string separator)) + (-eval [_ context resource scope] + (when-let [string (core/-eval string context resource scope)] + (if (= "" string) + [string] + (if-let [separator (core/-eval separator context resource scope)] + (case (count separator) + 0 + [string] + 1 + (loop [[char & more] string + result [] + acc (StringBuilder.)] + (if (= (str char) separator) + (if more + (recur more (conj result (str acc)) (StringBuilder.)) + (conj result (str acc))) + (if more + (recur more result (.append acc char)) + (conj result (str (.append acc char)))))) + ;; TODO: implement split with more than one char. + (throw (Exception. "TODO: implement split with separators longer than one char."))) + [string])))) + (-form [_] + (list 'split (core/-form string) (core/-form separator))))) + (defmethod core/compile* :elm.compiler.type/split [context {string :stringToSplit :keys [separator]}] (let [string (core/compile* context string) separator (some->> separator (core/compile* context))] - (if separator - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [string (core/-eval string context resource scope)] - (if (= "" string) - [string] - (if-let [separator (core/-eval separator context resource scope)] - (case (count separator) - 0 - [string] - 1 - (loop [[char & more] string - result [] - acc (StringBuilder.)] - (if (= (str char) separator) - (if more - (recur more (conj result (str acc)) (StringBuilder.)) - (conj result (str acc))) - (if more - (recur more result (.append acc char)) - (conj result (str (.append acc char)))))) - ;; TODO: implement split with more than one char. - (throw (Exception. "TODO: implement split with separators longer than one char."))) - [string])))) - (-form [_] - (list 'split (core/-form string) (core/-form separator)))) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [string (core/-eval string context resource scope)] - [string])) - (-form [_] - (list 'split (core/-form string))))))) + (split-op string separator))) ;; 17.16. StartsWith (defbinop starts-with [s prefix] @@ -145,34 +168,48 @@ (str/starts-with? s prefix))) ;; 17.17. Substring +(defn substring-op + ([string start-index] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper substring-op cache string start-index)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper substring-op expression-defs string start-index)) + (-resolve-params [_ parameters] + (core/resolve-params-helper substring-op parameters string start-index)) + (-eval [_ context resource scope] + (when-let [^String string (core/-eval string context resource scope)] + (when-let [start-index (core/-eval start-index context resource scope)] + (when (and (<= 0 start-index) (< start-index (count string))) + (subs string start-index))))) + (-form [_] + (list 'substring (core/-form string) (core/-form start-index))))) + ([string start-index length] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper substring-op cache string start-index length)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper substring-op expression-defs string start-index length)) + (-resolve-params [_ parameters] + (core/resolve-params-helper substring-op parameters string start-index length)) + (-eval [_ context resource scope] + (when-let [^String string (core/-eval string context resource scope)] + (when-let [start-index (core/-eval start-index context resource scope)] + (when (and (<= 0 start-index) (< start-index (count string))) + (subs string start-index (min (+ start-index length) + (count string))))))) + (-form [_] + (list 'substring (core/-form string) (core/-form start-index) + (core/-form length)))))) + (defmethod core/compile* :elm.compiler.type/substring [context {string :stringToSub start-index :startIndex :keys [length]}] (let [string (core/compile* context string) start-index (core/compile* context start-index) length (some->> length (core/compile* context))] (if length - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [^String string (core/-eval string context resource scope)] - (when-let [start-index (core/-eval start-index context resource scope)] - (when (and (<= 0 start-index) (< start-index (count string))) - (subs string start-index (min (+ start-index length) - (count string))))))) - (-form [_] - (list 'substring (core/-form string) (core/-form start-index) - (core/-form length)))) - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (when-let [^String string (core/-eval string context resource scope)] - (when-let [start-index (core/-eval start-index context resource scope)] - (when (and (<= 0 start-index) (< start-index (count string))) - (subs string start-index))))) - (-form [_] - (list 'substring (core/-form string) (core/-form start-index))))))) + (substring-op string start-index length) + (substring-op string start-index)))) ;; 17.18. Upper (defunop upper [s] diff --git a/modules/cql/src/blaze/elm/compiler/structured_values.clj b/modules/cql/src/blaze/elm/compiler/structured_values.clj index fe31d4094..fa11077e2 100644 --- a/modules/cql/src/blaze/elm/compiler/structured_values.clj +++ b/modules/cql/src/blaze/elm/compiler/structured_values.clj @@ -8,6 +8,7 @@ [blaze.coll.core :as coll] [blaze.elm.code :as code] [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.protocols :as p] [blaze.fhir.spec.type :as type] [clojure.string :as str]) @@ -41,26 +42,39 @@ {} elements)) +(defn tuple [elements] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (tuple + (reduce-kv + (fn [r key value] + (assoc r key (core/-resolve-refs value expression-defs))) + {} + elements))) + (-resolve-params [_ parameters] + (tuple + (reduce-kv + (fn [r key value] + (assoc r key (core/-resolve-params value parameters))) + {} + elements))) + (-eval [_ context resource scope] + (reduce-kv + (fn [r key value] + (assoc r key (core/-eval value context resource scope))) + {} + elements)) + (-form [_] + (reduce-kv + (fn [r key value] + (assoc r key (core/-form value))) + {} + elements)))) + (defmethod core/compile* :elm.compiler.type/tuple [context {elements :element}] (let [elements (compile-elements context elements)] - (if (every? core/static? (vals elements)) - elements - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (reduce-kv - (fn [r key value] - (assoc r key (core/-eval value context resource scope))) - {} - elements)) - (-form [_] - (reduce-kv - (fn [r key value] - (assoc r key (core/-form value))) - {} - elements)))))) + (cond-> elements (some (comp not core/static?) (vals elements)) tuple))) ;; 2.2. Instance (defmethod core/compile* :elm.compiler.type/instance @@ -77,46 +91,43 @@ core/Expression (-static [_] false) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [_ expression-defs] + (->SourcePropertyExpression (core/-resolve-refs source expression-defs) key)) + (-resolve-params [_ parameters] + (->SourcePropertyExpression (core/-resolve-params source parameters) key)) (-eval [_ context resource scope] (p/get (core/-eval source context resource scope) key)) (-form [_] `(~key ~(core/-form source)))) -(defrecord SourcePropertyValueExpression [source key] - core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (type/value (p/get (core/-eval source context resource scope) key))) - (-form [_] - `(:value (~key ~(core/-form source))))) - -(defrecord SingleScopePropertyExpression [key] - core/Expression - (-static [_] - false) - (-eval [_ _ _ value] - (p/get value key)) - (-form [_] - `(~key ~'default))) - -(defrecord ScopePropertyExpression [scope-key key] - core/Expression - (-static [_] - false) - (-eval [_ _ _ scope] - (p/get (get scope scope-key) key)) - (-form [_] - `(~key ~(symbol (name scope-key))))) - -(defrecord ScopePropertyValueExpression [scope-key key] - core/Expression - (-static [_] - false) - (-eval [_ _ _ scope] - (type/value (p/get (get scope scope-key) key))) - (-form [_] - `(:value (~key ~(symbol (name scope-key)))))) +(defn- source-property-value-expr [source key] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (source-property-value-expr (core/-resolve-refs source expression-defs) key)) + (-resolve-params [_ parameters] + (source-property-value-expr (core/-resolve-params source parameters) key)) + (-eval [_ context resource scope] + (type/value (p/get (core/-eval source context resource scope) key))) + (-form [_] + `(:value (~key ~(core/-form source)))))) + +(defn- scope-property-expr [scope-key key] + (reify-expr core/Expression + (-eval [_ _ _ scope] + (p/get (get scope scope-key) key)) + (-form [_] + `(~key ~(symbol (name scope-key)))))) + +(defn- scope-property-value-expr [scope-key key] + (reify-expr core/Expression + (-eval [_ _ _ scope] + (type/value (p/get (get scope scope-key) key))) + (-form [_] + `(:value (~key ~(symbol (name scope-key))))))) (defn- path->key [path] (let [[first-part more] (str/split path #"\." 2)] @@ -132,10 +143,10 @@ (cond source (if value? - (->SourcePropertyValueExpression (core/compile* context source) key) + (source-property-value-expr (core/compile* context source) key) (->SourcePropertyExpression (core/compile* context source) key)) scope (if value? - (->ScopePropertyValueExpression scope key) - (->ScopePropertyExpression scope key))))) + (scope-property-value-expr scope key) + (scope-property-expr scope key))))) diff --git a/modules/cql/src/blaze/elm/compiler/type_operators.clj b/modules/cql/src/blaze/elm/compiler/type_operators.clj index e5ee9a088..b37be11fc 100644 --- a/modules/cql/src/blaze/elm/compiler/type_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/type_operators.clj @@ -6,7 +6,7 @@ (:require [blaze.anomaly :as ba :refer [throw-anom]] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.macros :refer [defbinop defunop]] + [blaze.elm.compiler.macros :refer [defbinop defunop reify-expr]] [blaze.elm.date-time :as date-time] [blaze.elm.protocols :as p] [blaze.elm.quantity :as quantity] @@ -72,19 +72,24 @@ "Invalid As expression without `as-type` and `as-type-specifier`." :expression expression)))) +(defn as-op [type pred operand] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (as-op type pred (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (as-op type pred (core/-resolve-params operand parameters))) + (-eval [_ context resource scope] + (let [value (core/-eval operand context resource scope)] + (when (pred value) + value))) + (-form [_] + `(~'as ~type ~(core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/as [context {:keys [operand] :as expression}] (when-some [operand (core/compile* context operand)] (let [[type pred] (matches-type-fn expression)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (let [value (core/-eval operand context resource scope)] - (when (pred value) - value))) - (-form [_] - `(~'as ~type ~(core/-form operand))))))) + (as-op type pred operand)))) ;; TODO 22.2. CanConvert @@ -93,16 +98,10 @@ (p/can-convert-quantity x unit)) ;; 22.4. Children -(defmethod core/compile* :elm.compiler.type/children - [context {:keys [source]}] - (when-let [source (core/compile* context source)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (p/children (core/-eval source context resource scope))) - (-form [_] - (list 'children (core/-form source)))))) +(defunop children + {:operand-key :source} + [source] + (p/children source)) ;; TODO 22.5. Convert @@ -116,34 +115,47 @@ (some? (p/to-boolean operand)))) ;; 22.8. ConvertsToDate +(defn- converts-to-date-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper converts-to-date-op cache operand)) + (-resolve-refs [_ expression-defs] + (converts-to-date-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (converts-to-date-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (when-let [operand (core/-eval operand context resource scope)] + (when (some? operand) + (some? (p/to-date operand now))))) + (-form [_] + (list 'converts-to-date (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/converts-to-date [context {:keys [operand]}] - (when-let [operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (when-let [operand (core/-eval operand context resource scope)] - (when (some? operand) - (some? (p/to-date operand now))))) - (-form [_] - (list 'converts-to-date (core/-form operand)))))) + (some-> (core/compile* context operand) converts-to-date-op)) ;; 22.9. ConvertsToDateTime +(defn converts-to-date-time-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper converts-to-date-time-op cache operand)) + (-resolve-refs [_ expression-defs] + (converts-to-date-time-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (converts-to-date-time-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (when-let [operand (core/-eval operand context resource scope)] + (when (some? operand) + (some? (p/to-date-time operand now))))) + (-form [_] + (list 'converts-to-date-time (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/converts-to-date-time [context {:keys [operand]}] (when-let [operand (core/compile* context operand)] (if (system/date? operand) (some? (p/to-date-time operand nil)) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (when-let [operand (core/-eval operand context resource scope)] - (when (some? operand) - (some? (p/to-date-time operand now))))) - (-form [_] - (list 'converts-to-date-time (core/-form operand))))))) + (converts-to-date-time-op operand)))) ;; 22.10. ConvertsToDecimal (defunop converts-to-decimal [operand] @@ -176,29 +188,29 @@ (some? (p/to-string operand)))) ;; 22.16. ConvertsToTime +(defn- converts-to-time-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper converts-to-time-op cache operand)) + (-resolve-refs [_ expression-defs] + (converts-to-time-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (converts-to-time-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (when-some [operand (core/-eval operand context resource scope)] + (some? (p/to-time operand now)))) + (-form [_] + (list 'converts-to-time (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/converts-to-time [context {:keys [operand]}] - (when-let [operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (when-some [operand (core/-eval operand context resource scope)] - (some? (p/to-time operand now)))) - (-form [_] - (list 'converts-to-time (core/-form operand)))))) + (some-> (core/compile* context operand) converts-to-time-op)) ;; 22.17. Descendents -(defmethod core/compile* :elm.compiler.type/descendents - [context {:keys [source]}] - (when-let [source (core/compile* context source)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (p/descendents (core/-eval source context resource scope))) - (-form [_] - (list 'descendents (core/-form source)))))) +(defunop descendents + {:operand-key :source} + [source] + (p/descendents source)) ;; 22.18. Is (defn- matches-elm-named-type-is [type-name] @@ -242,17 +254,21 @@ is-type-specifier (matches-type-specifier-is is-type-specifier))) +(defn is-op [type pred operand] + (reify-expr core/Expression + (-resolve-refs [_ expression-defs] + (is-op type pred (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (is-op type pred (core/-resolve-params operand parameters))) + (-eval [_ context resource scope] + (pred (core/-eval operand context resource scope))) + (-form [_] + `(~'is ~type ~(core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/is [context {:keys [operand] :as expression}] - (let [[type pred] (matches-type-is expression) - operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ context resource scope] - (pred (core/-eval operand context resource scope))) - (-form [_] - `(~'is ~type ~(core/-form operand)))))) + (let [[type pred] (matches-type-is expression)] + (is-op type pred (core/compile* context operand)))) ;; 22.19. ToBoolean (defunop to-boolean [x] @@ -268,32 +284,46 @@ (p/to-concept x)) ;; 22.22. ToDate +(defn to-date-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-date-op cache operand)) + (-resolve-refs [_ expression-defs] + (to-date-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (to-date-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (p/to-date (core/-eval operand context resource scope) now)) + (-form [_] + (list 'to-date (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/to-date [context {:keys [operand]}] (when-let [operand (core/compile* context operand)] (if (system/date? operand) (p/to-date operand nil) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (p/to-date (core/-eval operand context resource scope) now)) - (-form [_] - (list 'to-date (core/-form operand))))))) + (to-date-op operand)))) ;; 22.23. ToDateTime +(defn to-date-time-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-date-time-op cache operand)) + (-resolve-refs [_ expression-defs] + (to-date-time-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (to-date-time-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (p/to-date-time (core/-eval operand context resource scope) now)) + (-form [_] + (list 'to-date-time (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/to-date-time [context {:keys [operand]}] (when-let [operand (core/compile* context operand)] (if (system/date? operand) (p/to-date-time operand nil) - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (p/to-date-time (core/-eval operand context resource scope) now)) - (-form [_] - (list 'to-date-time (core/-form operand))))))) + (to-date-time-op operand)))) ;; 22.24. ToDecimal (defunop to-decimal [x] @@ -324,13 +354,19 @@ (p/to-string x)) ;; 22.31. ToTime +(defn- to-time-op [operand] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper to-time-op cache operand)) + (-resolve-refs [_ expression-defs] + (to-time-op (core/-resolve-refs operand expression-defs))) + (-resolve-params [_ parameters] + (to-time-op (core/-resolve-params operand parameters))) + (-eval [_ {:keys [now] :as context} resource scope] + (p/to-time (core/-eval operand context resource scope) now)) + (-form [_] + (list 'to-time (core/-form operand))))) + (defmethod core/compile* :elm.compiler.type/to-time [context {:keys [operand]}] - (when-let [operand (core/compile* context operand)] - (reify core/Expression - (-static [_] - false) - (-eval [_ {:keys [now] :as context} resource scope] - (p/to-time (core/-eval operand context resource scope) now)) - (-form [_] - (list 'to-time (core/-form operand)))))) + (some-> (core/compile* context operand) to-time-op)) diff --git a/modules/cql/src/blaze/elm/compiler_spec.clj b/modules/cql/src/blaze/elm/compiler_spec.clj index 059bdfbf3..ff88b5040 100644 --- a/modules/cql/src/blaze/elm/compiler_spec.clj +++ b/modules/cql/src/blaze/elm/compiler_spec.clj @@ -2,11 +2,33 @@ (:require [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.library-spec] [blaze.elm.compiler.spec] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache :as-alias ec] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.expression.spec] [blaze.elm.spec] [blaze.fhir.spec-spec] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) (s/fdef c/compile :args (s/cat :context :elm/compile-context :expression :elm/expression) :ret core/expr?) + +(s/fdef c/attach-cache + :args (s/cat :expression core/expr? :cache ::expr/cache) + :ret (s/tuple core/expr? (s/coll-of (s/or :bloom-filter ::ec/bloom-filter :anomaly ::anom/anomaly)))) + +(s/fdef c/resolve-refs + :args (s/cat :expression core/expr? :expression-defs ::c/expression-defs) + :ret core/expr?) + +(s/fdef c/resolve-params + :args (s/cat :expression core/expr? :parameters ::c/parameters) + :ret core/expr?) + +(s/fdef c/form + :args (s/cat :expression core/expr?) + :ret list?) diff --git a/modules/cql/src/blaze/elm/date_time.clj b/modules/cql/src/blaze/elm/date_time.clj index eb0bd42c8..9756b068a 100644 --- a/modules/cql/src/blaze/elm/date_time.clj +++ b/modules/cql/src/blaze/elm/date_time.clj @@ -9,7 +9,9 @@ [blaze.fhir.spec.type.system :as system] [java-time.api :as time]) (:import - [blaze.fhir.spec.type.system Date DateDate DateTime DateTimeDate DateTimeYear DateTimeYearMonth DateYear DateYearMonth] + [blaze.fhir.spec.type.system + Date DateDate DateTime DateTimeDate DateTimeYear DateTimeYearMonth DateYear + DateYearMonth] [java.time DateTimeException LocalDateTime LocalTime OffsetDateTime] [java.time.temporal ChronoField ChronoUnit Temporal TemporalAccessor])) diff --git a/modules/cql/src/blaze/elm/decimal.clj b/modules/cql/src/blaze/elm/decimal.clj index c3b73107d..93a6abda4 100644 --- a/modules/cql/src/blaze/elm/decimal.clj +++ b/modules/cql/src/blaze/elm/decimal.clj @@ -13,7 +13,8 @@ [blaze.elm.protocols :as p] [clojure.math :as math]) (:import - [java.math RoundingMode])) + [java.math RoundingMode] + [tech.units.indriya.function RationalNumber])) (set! *warn-on-reflection* true) @@ -242,7 +243,10 @@ (extend-protocol p/ToDecimal BigDecimal (to-decimal [x] - (-> x constrain-scale check-overflow))) + (-> x constrain-scale check-overflow)) + RationalNumber + (to-decimal [x] + (.bigDecimalValue x))) (defn from-literal [s] (if-let [d (p/to-decimal s)] diff --git a/modules/cql/src/blaze/elm/expression.clj b/modules/cql/src/blaze/elm/expression.clj index ec16731c7..c670cf0ae 100644 --- a/modules/cql/src/blaze/elm/expression.clj +++ b/modules/cql/src/blaze/elm/expression.clj @@ -1,5 +1,5 @@ (ns blaze.elm.expression - (:refer-clojure :exclude [eval]) + (:refer-clojure :exclude [eval hash]) (:require [blaze.elm.compiler.core :as core])) diff --git a/modules/cql/src/blaze/elm/expression/cache.clj b/modules/cql/src/blaze/elm/expression/cache.clj new file mode 100644 index 000000000..ce0f04c27 --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache.clj @@ -0,0 +1,252 @@ +(ns blaze.elm.expression.cache + "Expression cache API." + (:refer-clojure :exclude [get list]) + (:require + [blaze.anomaly :as ba] + [blaze.async.comp :as ac] + [blaze.cache-collector.protocols :as ccp] + [blaze.coll.core :refer [with-open-coll]] + [blaze.db.impl.iterators :as i] + [blaze.db.kv :as kv] + [blaze.db.spec] + [blaze.elm.compiler.core :as core] + [blaze.elm.expression :as expr] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [blaze.elm.expression.cache.codec :as codec] + [blaze.elm.expression.cache.codec.by-t :as codec-by-t] + [blaze.elm.expression.cache.codec.form :as form] + [blaze.elm.expression.cache.protocols :as p] + [blaze.elm.expression.cache.spec] + [blaze.elm.expression.spec] + [blaze.executors :as ex] + [blaze.module :as m :refer [reg-collector]] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [java-time.api :as time] + [prometheus.alpha :refer [defcounter]] + [taoensso.timbre :as log]) + (:import + [com.github.benmanes.caffeine.cache + AsyncCache AsyncCacheLoader AsyncLoadingCache Caffeine Weigher] + [com.google.common.hash HashCode] + [java.lang AutoCloseable] + [java.util.concurrent TimeUnit])) + +(set! *warn-on-reflection* true) + +(defcounter bloom-filter-useful-total + "Number of times Bloom filter has avoided expression evaluation." + {:namespace "blaze" + :subsystem "cql_expr_cache"} + "name") + +(defcounter bloom-filter-not-useful-total + "Number of times Bloom filter has not avoided expression evaluation." + {:namespace "blaze" + :subsystem "cql_expr_cache"} + "name") + +(defcounter bloom-filter-false-positive-total + "Number of false positives reported by Bloom all filters." + {:namespace "blaze" + :subsystem "cql_expr_cache"} + "name") + +(defn get + "Returns the Bloom filter of `expression` from `cache` or nil if it isn't + available yet." + [cache expression] + (p/-get cache expression)) + +(defn get-disk + "Returns the Bloom filter with `hash` from `cache` or an anomaly if the Bloom + filter was not found." + [cache hash] + (p/-get-disk cache hash)) + +(defn delete-disk! + "Deletes the Bloom filter with `hash` from `cache` or returns an anomaly if + the Bloom filter was not found." + [cache hash] + (p/-delete-disk cache hash)) + +(defn list-by-t + "Returns a reducible collection of all Bloom filters in `cache` ordered by t + descending." + [cache] + (p/-list-by-t cache)) + +(defn total + "Returns the total number of Bloom filters in `cache`." + [cache] + (p/-total cache)) + +(def ^:private weigher + (reify Weigher + (weigh [_ _ bloom-filter] + (::bloom-filter/mem-size bloom-filter)))) + +(defn- load-bloom-filter [kv-store hash] + (some->> (kv/get kv-store :cql-bloom-filter (.asBytes ^HashCode hash)) + (codec/decode-value hash))) + +(defn- load-bloom-filter-from-expr [kv-store expression] + (let [key (form/hash (pr-str (core/-form expression)))] + (load-bloom-filter kv-store key))) + +(defn- bloom-filter-creation-counter ^AutoCloseable [state] + (swap! state update :num-running-bloom-filter-creations inc) + (reify AutoCloseable + (close [_] + (swap! state update :num-running-bloom-filter-creations dec)))) + +(defn- mem-cache + [state {:keys [kv-store] :as node} executor max-size-in-mb refresh] + (-> (Caffeine/newBuilder) + (.weigher weigher) + (.maximumWeight (bit-shift-left max-size-in-mb 20)) + (.refreshAfterWrite refresh) + (.executor executor) + (.recordStats) + (.buildAsync + (reify AsyncCacheLoader + (asyncLoad [_ expression executor] + (if-let [bloom-filter (load-bloom-filter-from-expr kv-store expression)] + (ac/completed-future bloom-filter) + (ac/supply-async + #(with-open [_ (bloom-filter-creation-counter state)] + (let [bloom-filter (bloom-filter/create node expression)] + (kv/write! + kv-store + [(codec/put-entry bloom-filter) + (codec-by-t/put-entry bloom-filter)]) + bloom-filter)) + executor))) + (asyncReload [_ expression old-bloom-filter executor] + (ac/supply-async + #(with-open [_ (bloom-filter-creation-counter state)] + (let [bloom-filter (bloom-filter/recreate node old-bloom-filter + expression)] + (kv/write! + kv-store + [(codec/put-entry bloom-filter) + (codec-by-t/delete-entry old-bloom-filter) + (codec-by-t/put-entry bloom-filter)]) + bloom-filter)) + executor)))))) + +(def ^:private ^:const ^long expression-size-limit + "The limit of form size of cacheable expressions. + + Bigger expressions will not be cache to keep memory consumption under control. + + The current value is 10 kB." + (bit-shift-left 10 10)) + +(def ^:private ^:const ^long concurrent-bloom-filter-creation-limit + "Maximum number of concurrent Bloom filter creations allowed. + + This limit should prevent the over-saturation of Bloom filter creations." + 500) + +(defn concurrent-bloom-filter-creation-limit-reached? + [{:keys [num-running-bloom-filter-creations]}] + (< concurrent-bloom-filter-creation-limit num-running-bloom-filter-creations)) + +(defn- overly-large? [expression] + (< expression-size-limit (count (pr-str (core/-form expression))))) + +(defn- not-found-anom [hash] + (ba/not-found (format "The Bloom filter with hash `%s` was not found." hash))) + +(defrecord Cache [state ^AsyncLoadingCache mem-cache node kv-store] + p/Cache + (-get [_ expression] + (if (overly-large? expression) + (log/debug "Skip caching overly large CQL expression.") + (if-let [future (.getIfPresent mem-cache expression)] + (when (.isDone future) + (.get future)) + (if (concurrent-bloom-filter-creation-limit-reached? @state) + (log/debug "Skip caching CQL expression because the concurrent Bloom filter creation limit of" concurrent-bloom-filter-creation-limit "is reached.") + (let [future (.get mem-cache expression)] + (when (.isDone future) + (.get future))))))) + + (-get-disk [_ hash] + (or (load-bloom-filter kv-store hash) + (not-found-anom hash))) + + (-delete-disk [_ hash] + (if-let [bloom-filter (load-bloom-filter kv-store hash)] + (kv/write! + kv-store + [(codec/delete-entry bloom-filter) + (codec-by-t/delete-entry bloom-filter)]) + (not-found-anom hash))) + + (-list-by-t [_] + (with-open-coll [snapshot (kv/new-snapshot kv-store)] + (i/entries snapshot :cql-bloom-filter-by-t (map codec-by-t/decoder)))) + + (-total [_] + (kv/estimate-num-keys kv-store :cql-bloom-filter)) + + ccp/StatsCache + (-stats [_] + (.stats (.synchronous mem-cache))) + + (-estimated-size [_] + (.estimatedSize (.synchronous mem-cache)))) + +(defmethod m/pre-init-spec ::expr/cache [_] + (s/keys :req-un [:blaze.db/node ::executor] :opt-un [::max-size-in-mb ::refresh])) + +(defmethod ig/init-key ::expr/cache + [_ + {:keys [executor max-size-in-mb refresh] {:keys [kv-store] :as node} :node + :or {max-size-in-mb 100 refresh (time/hours 24)}}] + (log/info "Create CQL expression cache with a memory size of" max-size-in-mb "MiB and a refresh duration of" (str refresh)) + (let [state (atom {:num-running-bloom-filter-creations 0})] + (->Cache state (mem-cache state node executor max-size-in-mb refresh) node kv-store))) + +(defmethod ig/halt-key! ::expr/cache + [_ {:keys [mem-cache]}] + (log/info "Stopping CQL expression cache...") + (.cleanUp (.synchronous ^AsyncCache mem-cache))) + +(defmethod m/pre-init-spec ::executor [_] + (s/keys :opt-un [::num-threads])) + +(defn- executor-init-msg [num-threads] + (format "Init CQL expression cache executor with %d threads" num-threads)) + +(defmethod ig/init-key ::executor + [_ {:keys [num-threads] :or {num-threads 4}}] + (log/info (executor-init-msg num-threads)) + (ex/io-pool num-threads "cql-expr-cache-%d")) + +(defmethod ig/halt-key! ::executor + [_ executor] + (log/info "Stopping CQL expression cache executor...") + (ex/shutdown! executor) + (if (ex/await-termination executor 10 TimeUnit/SECONDS) + (log/info "CQL expression cache executor was stopped successfully") + (log/warn "Got timeout while stopping the CQL expression cache executor"))) + +(derive ::executor :blaze.metrics/thread-pool-executor) + +(reg-collector ::bloom-filter-creation-duration-seconds + bloom-filter/bloom-filter-creation-duration-seconds) + +(reg-collector ::bloom-filter-useful-total + bloom-filter-useful-total) + +(reg-collector ::bloom-filter-not-useful-total + bloom-filter-not-useful-total) + +(reg-collector ::bloom-filter-false-positive-total + bloom-filter-false-positive-total) + +(reg-collector ::bloom-filter-bytes + bloom-filter/bloom-filter-bytes) diff --git a/modules/cql/src/blaze/elm/expression/cache/bloom_filter.clj b/modules/cql/src/blaze/elm/expression/cache/bloom_filter.clj new file mode 100644 index 000000000..92dbac656 --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/bloom_filter.clj @@ -0,0 +1,92 @@ +(ns blaze.elm.expression.cache.bloom-filter + (:refer-clojure :exclude [merge]) + (:require + [blaze.db.api :as d] + [blaze.elm.compiler.core :as core] + [blaze.elm.expression :as expr] + [blaze.elm.expression.cache.codec :as codec] + [blaze.elm.expression.cache.codec.form :as form] + [blaze.elm.resource :as cr] + [prometheus.alpha :as prom :refer [defhistogram]] + [taoensso.timbre :as log]) + (:import + [blaze.elm.expression.cache.codec BloomFilterContainer] + [blaze.elm.resource Resource] + [com.google.common.hash BloomFilter] + [java.time OffsetDateTime])) + +(set! *warn-on-reflection* true) + +(defhistogram bloom-filter-bytes + "Bloom filter sizes in bytes." + {:namespace "blaze" + :subsystem "cql_expr_cache"} + (take 12 (iterate #(* 4 %) 1))) + +(defhistogram bloom-filter-creation-duration-seconds + "Durations in Cassandra resource store." + {:namespace "blaze" + :subsystem "cql_expr_cache"} + (take 14 (iterate #(* 2 %) 0.1))) + +(defn might-contain? + "Returns true if `resource` might have been put in `bloom-filter` or false if + this is definitely not the case." + {:arglists '([bloom-filter resource])} + [^BloomFilterContainer bloom-filter ^Resource resource] + (or (< (.-t bloom-filter) (.-lastChangeT resource)) + (.mightContain ^BloomFilter (.-filter bloom-filter) (.-id resource)))) + +(defn merge [bloom-filter-a bloom-filter-b] + (.merge ^BloomFilterContainer bloom-filter-a bloom-filter-b)) + +(defn- calc-mem-size [n p] + (long (/ (* (- n) (Math/log p)) (* (Math/log 2) (Math/log 2)) 8))) + +(defn build-bloom-filter [expression t resource-ids] + (let [n (count resource-ids) + p (double 0.01) + filter (BloomFilter/create codec/id-funnel (int (max 10000 n)) p) + mem-size (calc-mem-size (max 10000 n) p) + expr-form (pr-str (core/-form expression))] + (prom/observe! bloom-filter-bytes mem-size) + (run! #(.put filter %) resource-ids) + (BloomFilterContainer. (form/hash expr-form) t expr-form n filter mem-size))) + +(defn- calc-bloom-filter [db xform expression] + (with-open [batch-db (d/new-batch-db db) + _ (prom/timer bloom-filter-creation-duration-seconds)] + (build-bloom-filter + expression + (d/t batch-db) + (into + [] + (comp (map (partial cr/mk-resource batch-db)) + xform + (filter (partial expr/eval {:db batch-db :now (OffsetDateTime/now)} expression)) + (map :id)) + (d/type-list db "Patient"))))) + +(defn- create-bloom-filter-msg [expression db] + (format "Create Bloom filter for expression `%s` evaluating it for %d patients." + (core/-form expression) (d/type-total db "Patient"))) + +(defn create [node expression] + (let [db (d/db node)] + (log/debug (create-bloom-filter-msg expression db)) + (calc-bloom-filter db identity expression))) + +(defn recreate + {:arglists '([node old-bloom-filter expression])} + [node {::keys [t expr-form] :as old-bloom-filter} expression] + (let [db (d/db node)] + (log/debug "Recreate Bloom filter for expression" + expr-form + "last created at t =" t + "evaluating it for" + (d/type-total db "Patient") + "Patient resources") + (calc-bloom-filter + db + (filter (partial might-contain? old-bloom-filter)) + expression))) diff --git a/modules/cql/src/blaze/elm/expression/cache/bloom_filter/spec.clj b/modules/cql/src/blaze/elm/expression/cache/bloom_filter/spec.clj new file mode 100644 index 000000000..ea4126d28 --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/bloom_filter/spec.clj @@ -0,0 +1,29 @@ +(ns blaze.elm.expression.cache.bloom-filter.spec + (:require + [blaze.elm.expression.cache :as-alias ec] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [clojure.spec.alpha :as s]) + (:import + [blaze.elm.expression.cache.codec BloomFilterContainer] + [com.google.common.hash BloomFilter HashCode])) + +(s/def ::bloom-filter/hash + #(instance? HashCode %)) + +(s/def ::bloom-filter/t + :blaze.db/t) + +(s/def ::bloom-filter/expr-form + string?) + +(s/def ::bloom-filter/patient-count + nat-int?) + +(s/def ::bloom-filter/filter + #(instance? BloomFilter %)) + +(s/def ::bloom-filter/mem-size + nat-int?) + +(s/def ::ec/bloom-filter + #(instance? BloomFilterContainer %)) diff --git a/modules/cql/src/blaze/elm/expression/cache/bloom_filter_spec.clj b/modules/cql/src/blaze/elm/expression/cache/bloom_filter_spec.clj new file mode 100644 index 000000000..c5972ce85 --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/bloom_filter_spec.clj @@ -0,0 +1,26 @@ +(ns blaze.elm.expression.cache.bloom-filter-spec + (:require + [blaze.db.spec] + [blaze.elm.compiler.core :as core] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.resource :as cr] + [clojure.spec.alpha :as s])) + +(s/fdef bloom-filter/might-contain? + :args (s/cat :bloom-filter ::ec/bloom-filter :resource cr/resource?) + :ret boolean?) + +(s/fdef bloom-filter/merge + :args (s/cat :bloom-filter-a ::ec/bloom-filter :bloom-filter-b ::ec/bloom-filter) + :ret (s/nilable ::ec/bloom-filter)) + +(s/fdef bloom-filter/create + :args (s/cat :node :blaze.db/node :expression core/expr?) + :ret ::ec/bloom-filter) + +(s/fdef bloom-filter/recreate + :args (s/cat :node :blaze.db/node :old-bloom-filter ::ec/bloom-filter + :expression core/expr?) + :ret ::ec/bloom-filter) diff --git a/modules/cql/src/blaze/elm/expression/cache/codec.clj b/modules/cql/src/blaze/elm/expression/cache/codec.clj new file mode 100644 index 000000000..945acb4ba --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/codec.clj @@ -0,0 +1,80 @@ +(ns blaze.elm.expression.cache.codec + (:require + [blaze.byte-buffer :as bb] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [blaze.elm.expression.cache.codec.form :as form]) + (:import + [clojure.lang ILookup] + [com.fasterxml.jackson.databind.util ByteBufferBackedInputStream] + [com.google.common.hash BloomFilter Funnel Funnels HashCode] + [java.io ByteArrayOutputStream DataOutputStream] + [java.nio.charset StandardCharsets])) + +(set! *warn-on-reflection* true) + +(def ^:private ^:const ^long version 0) + +(definterface IBloomFilterContainer + (merge [other])) + +(deftype BloomFilterContainer [hash ^long t exprForm ^long patientCount + ^BloomFilter filter ^long memSize] + IBloomFilterContainer + (merge [_ other] + (when (.isCompatible filter (.-filter ^BloomFilterContainer other)) + (let [newFilter (doto (.copy filter) (.putAll (.-filter ^BloomFilterContainer other)))] + (when (< (.expectedFpp newFilter) 0.01) + (BloomFilterContainer. nil (min t (.-t ^BloomFilterContainer other)) nil + (.approximateElementCount newFilter) + newFilter memSize))))) + ILookup + (valAt [r key] + (.valAt r key nil)) + (valAt [_ key not-found] + (case key + ::bloom-filter/hash hash + ::bloom-filter/t t + ::bloom-filter/expr-form exprForm + ::bloom-filter/patient-count patientCount + ::bloom-filter/filter filter + ::bloom-filter/mem-size memSize + not-found))) + +(def ^Funnel id-funnel + (Funnels/stringFunnel StandardCharsets/ISO_8859_1)) + +(defn- encode-value [{::bloom-filter/keys [t expr-form filter]}] + (let [out (ByteArrayOutputStream.) + data-out (DataOutputStream. out) + form (.getBytes ^String expr-form StandardCharsets/UTF_8)] + (.writeByte data-out version) + (.writeLong data-out t) + (.writeInt data-out (alength form)) + (.write data-out ^bytes form) + (.writeTo ^BloomFilter filter data-out) + (.toByteArray out))) + +(defn put-entry + "Creates a put-entry for column-family `cql-bloom-filter` with the hash of + `bloom-filter` as key and `bloom-filter` as value." + {:arglists '([bloom-filter])} + [{::bloom-filter/keys [hash] :as bloom-filter}] + [:put :cql-bloom-filter (.asBytes ^HashCode hash) (encode-value bloom-filter)]) + +(defn delete-entry + "Creates a delete-entry for column-family `cql-bloom-filter` with the hash of + `bloom-filter` as key." + {:arglists '([bloom-filter])} + [{::bloom-filter/keys [hash]}] + [:delete :cql-bloom-filter (.asBytes ^HashCode hash)]) + +(defn- decode-value* [hash buf] + (assert (zero? (bb/get-byte! buf)) "assume version is always zero") + (let [t (bb/get-long! buf) + expr-form (form/decode! buf) + mem-size (bb/remaining buf) + filter (BloomFilter/readFrom (ByteBufferBackedInputStream. buf) id-funnel)] + (BloomFilterContainer. hash t expr-form (.approximateElementCount filter) filter mem-size))) + +(defn decode-value [hash byte-array] + (decode-value* hash (bb/wrap byte-array))) diff --git a/modules/cql/src/blaze/elm/expression/cache/codec/by_t.clj b/modules/cql/src/blaze/elm/expression/cache/codec/by_t.clj new file mode 100644 index 000000000..b27781bbc --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/codec/by_t.clj @@ -0,0 +1,47 @@ +(ns blaze.elm.expression.cache.codec.by-t + (:require + [blaze.byte-buffer :as bb] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [blaze.elm.expression.cache.codec.form :as form]) + (:import + [com.google.common.hash HashCode] + [java.nio.charset StandardCharsets])) + +(set! *warn-on-reflection* true) + +(defn- encode-key [{::bloom-filter/keys [t hash]}] + (-> (bb/allocate (+ Long/BYTES 32)) + (bb/put-long! t) + (bb/put-byte-array! (.asBytes ^HashCode hash)) + bb/array)) + +(defn- encode-value [{::bloom-filter/keys [expr-form patient-count mem-size]}] + (let [form (.getBytes ^String expr-form StandardCharsets/UTF_8)] + (-> (bb/allocate (+ Integer/BYTES (alength form) Long/BYTES Long/BYTES)) + (bb/put-int! (alength form)) + (bb/put-byte-array! form) + (bb/put-long! patient-count) + (bb/put-long! mem-size) + bb/array))) + +(defn put-entry [bloom-filter] + [:put :cql-bloom-filter-by-t (encode-key bloom-filter) + (encode-value bloom-filter)]) + +(defn delete-entry [bloom-filter] + [:delete :cql-bloom-filter-by-t (encode-key bloom-filter)]) + +(defn- decode-value* [buf] + (let [form (form/decode! buf) + patient-count (bb/get-long! buf) + mem-size (bb/get-long! buf)] + #::bloom-filter{:expr-form form :patient-count patient-count + :mem-size mem-size})) + +(defn decoder [[kb vb]] + (let [hash (byte-array 32) + t (bb/get-long! kb)] + (bb/copy-into-byte-array! kb hash) + (assoc (decode-value* vb) + ::bloom-filter/t t + ::bloom-filter/hash (HashCode/fromBytes hash)))) diff --git a/modules/cql/src/blaze/elm/expression/cache/codec/form.clj b/modules/cql/src/blaze/elm/expression/cache/codec/form.clj new file mode 100644 index 000000000..84c25849b --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/codec/form.clj @@ -0,0 +1,19 @@ +(ns blaze.elm.expression.cache.codec.form + (:refer-clojure :exclude [hash]) + (:require + [blaze.byte-buffer :as bb]) + (:import + [com.google.common.hash HashCode Hashing] + [java.nio.charset StandardCharsets])) + +(set! *warn-on-reflection* true) + +(defn hash ^HashCode [expr-form] + (-> (Hashing/sha256) + (.hashString expr-form StandardCharsets/UTF_8))) + +(defn decode! [buf] + (let [len (bb/get-int! buf) + bytes (byte-array len)] + (bb/copy-into-byte-array! buf bytes) + (String. bytes StandardCharsets/UTF_8))) diff --git a/modules/cql/src/blaze/elm/expression/cache/protocols.clj b/modules/cql/src/blaze/elm/expression/cache/protocols.clj new file mode 100644 index 000000000..450ccbdeb --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/protocols.clj @@ -0,0 +1,8 @@ +(ns blaze.elm.expression.cache.protocols) + +(defprotocol Cache + (-get [cache expression]) + (-get-disk [cache hash]) + (-delete-disk [cache hash]) + (-list-by-t [cache]) + (-total [cache])) diff --git a/modules/cql/src/blaze/elm/expression/cache/spec.clj b/modules/cql/src/blaze/elm/expression/cache/spec.clj new file mode 100644 index 000000000..9134bdf6c --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache/spec.clj @@ -0,0 +1,20 @@ +(ns blaze.elm.expression.cache.spec + (:require + [blaze.db.tx-log.spec] + [blaze.elm.expression.cache :as-alias ec] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.executors :as ex] + [clojure.spec.alpha :as s] + [java-time.api :as time])) + +(s/def ::ec/max-size-in-mb + nat-int?) + +(s/def ::ec/refresh + time/duration?) + +(s/def ::ec/executor + ex/executor?) + +(s/def ::ec/num-threads + pos-int?) diff --git a/modules/cql/src/blaze/elm/expression/cache_spec.clj b/modules/cql/src/blaze/elm/expression/cache_spec.clj new file mode 100644 index 000000000..67858ee16 --- /dev/null +++ b/modules/cql/src/blaze/elm/expression/cache_spec.clj @@ -0,0 +1,33 @@ +(ns blaze.elm.expression.cache-spec + (:require + [blaze.db.tx-log.spec] + [blaze.elm.compiler.core :as core] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [blaze.elm.expression.cache.bloom-filter-spec] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.expression.cache.spec] + [blaze.fhir.spec.spec] + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) + +(s/fdef ec/get + :args (s/cat :cache ::expr/cache :expression core/expr?) + :ret (s/nilable ::ec/bloom-filter)) + +(s/fdef ec/get-disk + :args (s/cat :cache ::expr/cache :hash ::bloom-filter/hash) + :ret (s/or :result ::ec/bloom-filter :anomaly ::anom/anomaly)) + +(s/fdef ec/delete-disk! + :args (s/cat :cache ::expr/cache :hash ::bloom-filter/hash) + :ret (s/nilable ::anom/anomaly)) + +(s/fdef ec/list-by-t + :args (s/cat :cache ::expr/cache) + :ret (s/nilable ::ec/bloom-filter)) + +(s/fdef ec/total + :args (s/cat :cache ::expr/cache) + :ret nat-int?) diff --git a/modules/cql/src/blaze/elm/expression/spec.clj b/modules/cql/src/blaze/elm/expression/spec.clj index 89a0d3995..746e78d03 100644 --- a/modules/cql/src/blaze/elm/expression/spec.clj +++ b/modules/cql/src/blaze/elm/expression/spec.clj @@ -3,6 +3,7 @@ [blaze.db.api-spec] [blaze.elm.compiler :as-alias c] [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache.protocols :as p] [blaze.elm.spec] [clojure.spec.alpha :as s] [java-time.api :as time])) @@ -10,6 +11,9 @@ (s/def ::now time/offset-date-time?) +(s/def ::expr/cache + #(satisfies? p/Cache %)) + (s/def ::parameters (s/map-of :elm/name ::c/expression)) diff --git a/modules/cql/src/blaze/elm/expression_spec.clj b/modules/cql/src/blaze/elm/expression_spec.clj index f50ccf60f..8e4de215f 100644 --- a/modules/cql/src/blaze/elm/expression_spec.clj +++ b/modules/cql/src/blaze/elm/expression_spec.clj @@ -2,15 +2,14 @@ (:require [blaze.db.api-spec] [blaze.elm.compiler :as-alias c] - [blaze.elm.compiler.external-data :as ed] - [blaze.elm.compiler.library-spec] [blaze.elm.compiler.spec] [blaze.elm.expression :as expr] [blaze.elm.expression.spec] + [blaze.elm.resource :as cr] [blaze.fhir.spec] [clojure.spec.alpha :as s])) (s/fdef expr/eval :args (s/cat :context ::expr/context :expression ::c/expression - :resource (s/nilable ed/resource?))) + :resource (s/nilable cr/resource?))) diff --git a/modules/cql/src/blaze/elm/interval.clj b/modules/cql/src/blaze/elm/interval.clj index ab362dae0..26c59d9c7 100644 --- a/modules/cql/src/blaze/elm/interval.clj +++ b/modules/cql/src/blaze/elm/interval.clj @@ -1,6 +1,7 @@ (ns blaze.elm.interval "Implementation of the interval type." (:require + [blaze.elm.compiler.core :as core] [blaze.elm.date-time :refer [temporal?]] [blaze.elm.protocols :as p])) @@ -99,7 +100,23 @@ (union [a b] (let [[left right] (if (p/less (:start a) (:start b)) [a b] [b a])] (when (p/greater-or-equal (:end left) (p/predecessor (:start right))) - (->Interval (:start left) (:end right)))))) + (->Interval (:start left) (:end right))))) + + core/Expression + (-static [_] + true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) + (-eval [this _ _ _] + this) + (-form [_] + (list 'interval (core/-form start) (core/-form end)))) (defn interval "Returns an interval with the given `start` and `end` bounds." diff --git a/modules/cql/src/blaze/elm/quantity.clj b/modules/cql/src/blaze/elm/quantity.clj index 764642c46..3e649ec21 100644 --- a/modules/cql/src/blaze/elm/quantity.clj +++ b/modules/cql/src/blaze/elm/quantity.clj @@ -68,10 +68,18 @@ Quantity (-static [_] true) + (-attach-cache [quantity _] + [quantity]) + (-patient-count [_] + nil) + (-resolve-refs [quantity _] + quantity) + (-resolve-params [quantity _] + quantity) (-eval [quantity _ _ _] quantity) (-form [quantity] - `(~'quantity ~(.getValue quantity) ~(format-unit (.getUnit quantity))))) + `(~'quantity ~(p/to-decimal (.getValue quantity)) ~(format-unit (.getUnit quantity))))) (defprotocol QuantityDivide (quantity-divide [divisor quantity])) diff --git a/modules/cql/src/blaze/elm/ratio.clj b/modules/cql/src/blaze/elm/ratio.clj index cc0a5aad1..c8ad66751 100644 --- a/modules/cql/src/blaze/elm/ratio.clj +++ b/modules/cql/src/blaze/elm/ratio.clj @@ -3,6 +3,7 @@ Section numbers are according to https://cql.hl7.org/04-logicalspecification.html." (:require + [blaze.elm.compiler.core :as core] [blaze.elm.protocols :as p] [clojure.string :as str])) @@ -19,7 +20,23 @@ (if other (p/equivalent (p/divide numerator denominator) (p/divide (:numerator other) (:denominator other))) - false))) + false)) + + core/Expression + (-static [_] + true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) + (-eval [this _ _ _] + this) + (-form [_] + (list 'ratio (core/-form numerator) (core/-form denominator)))) (defn ratio "Creates a ratio between two quantities." diff --git a/modules/cql/src/blaze/elm/resource.clj b/modules/cql/src/blaze/elm/resource.clj new file mode 100644 index 000000000..8d67ffefe --- /dev/null +++ b/modules/cql/src/blaze/elm/resource.clj @@ -0,0 +1,64 @@ +(ns blaze.elm.resource + (:require + [blaze.db.api :as d] + [blaze.db.impl.index.resource-handle :as rh] + [blaze.elm.compiler.core :as core] + [blaze.elm.spec] + [blaze.fhir.spec.type.protocols :as p]) + (:import + [clojure.lang ILookup])) + +(set! *warn-on-reflection* true) + +;; A resource that is a wrapper of a resource-handle that will lazily pull the +;; resource content if some property other than :id is accessed. +(deftype Resource [db id handle ^long lastChangeT content] + p/FhirType + (-type [_] + (p/-type handle)) + + ILookup + (valAt [r key] + (.valAt r key nil)) + (valAt [_ key not-found] + (case key + :id id + (-> (or @content (vreset! content @(d/pull-content db handle))) + (get key not-found)))) + + core/Expression + (-static [_] + true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) + (-eval [expr _ _ _] + expr) + (-form [_] + (list 'resource (name (p/-type handle)) id (rh/t handle))) + + Object + (toString [_] + (str (name (p/-type handle)) "[id = " id ", t = " (rh/t handle) ", last-change-t = " lastChangeT "]"))) + +(defn resource? [x] + (instance? Resource x)) + +(defn- patient-last-change-t [db handle] + (or (d/patient-compartment-last-change-t db (rh/id handle)) (rh/t handle))) + +(defn- last-change-t [db handle] + (if (identical? :fhir/Patient (p/-type handle)) + (patient-last-change-t db handle) + (d/t db))) + +(defn mk-resource [db handle] + (Resource. db (rh/id handle) handle (last-change-t db handle) (volatile! nil))) + +(defn resource-mapper [db] + (map (partial mk-resource db))) diff --git a/modules/cql/src/blaze/elm/compiler/external_data_spec.clj b/modules/cql/src/blaze/elm/resource_spec.clj similarity index 53% rename from modules/cql/src/blaze/elm/compiler/external_data_spec.clj rename to modules/cql/src/blaze/elm/resource_spec.clj index 0f9f98570..ba21278c5 100644 --- a/modules/cql/src/blaze/elm/compiler/external_data_spec.clj +++ b/modules/cql/src/blaze/elm/resource_spec.clj @@ -1,16 +1,16 @@ -(ns blaze.elm.compiler.external-data-spec +(ns blaze.elm.resource-spec (:require [blaze.db.spec] - [blaze.elm.compiler.external-data :as ed] + [blaze.elm.resource :as cr] [clojure.spec.alpha :as s])) -(s/fdef ed/resource? +(s/fdef cr/resource? :args (s/cat :x any?) :ret boolean?) -(s/fdef ed/mk-resource +(s/fdef cr/mk-resource :args (s/cat :db :blaze.db/db :handle :blaze.db/resource-handle) - :ret ed/resource?) + :ret cr/resource?) -(s/fdef ed/resource-mapper +(s/fdef cr/resource-mapper :args (s/cat :db :blaze.db/db)) diff --git a/modules/cql/test/blaze/elm/code_test.clj b/modules/cql/test/blaze/elm/code_test.clj new file mode 100644 index 000000000..17b69c6fd --- /dev/null +++ b/modules/cql/test/blaze/elm/code_test.clj @@ -0,0 +1,16 @@ +(ns blaze.elm.code-test + (:require + [blaze.elm.code :as code] + [blaze.elm.compiler :as c] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(deftest to-code-test + (testing "attach-cache" + (let [code (code/to-code "foo" "bar" "baz")] + (is (= [code] (st/with-instrument-disabled (c/attach-cache code ::cache))))))) diff --git a/modules/cql/test/blaze/elm/compiler/aggregate_operators_test.clj b/modules/cql/test/blaze/elm/compiler/aggregate_operators_test.clj index 659477b3c..c12010c69 100644 --- a/modules/cql/test/blaze/elm/compiler/aggregate_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/aggregate_operators_test.clj @@ -47,9 +47,7 @@ #elm/list [] true {:type "Null"} true)) - (ctu/testing-unary-dynamic elm/all-true) - - (ctu/testing-unary-form elm/all-true)) + (ctu/testing-unary-op elm/all-true)) ;; 21.2. AnyTrue ;; @@ -72,9 +70,7 @@ #elm/list [] false {:type "Null"} false)) - (ctu/testing-unary-dynamic elm/any-true) - - (ctu/testing-unary-form elm/any-true)) + (ctu/testing-unary-op elm/any-true)) ;; 21.3. Avg ;; @@ -97,9 +93,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/avg) - - (ctu/testing-unary-form elm/avg)) + (ctu/testing-unary-op elm/avg)) ;; 21.4. Count ;; @@ -124,9 +118,7 @@ #elm/list [] 0 {:type "Null"} 0)) - (ctu/testing-unary-dynamic elm/count) - - (ctu/testing-unary-form elm/count)) + (ctu/testing-unary-op elm/count)) ;; 21.5. GeometricMean ;; @@ -150,9 +142,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/geometric-mean) - - (ctu/testing-unary-form elm/geometric-mean)) + (ctu/testing-unary-op elm/geometric-mean)) ;; 21.6. Product ;; @@ -176,9 +166,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/product) - - (ctu/testing-unary-form elm/product)) + (ctu/testing-unary-op elm/product)) ;; 21.7. Max ;; @@ -203,9 +191,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/max) - - (ctu/testing-unary-form elm/max)) + (ctu/testing-unary-op elm/max)) ;; 21.8. Median ;; @@ -229,9 +215,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/median) - - (ctu/testing-unary-form elm/median)) + (ctu/testing-unary-op elm/median)) ;; 21.9. Min ;; @@ -256,9 +240,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/min) - - (ctu/testing-unary-form elm/min)) + (ctu/testing-unary-op elm/min)) ;; 21.10. Mode ;; @@ -282,9 +264,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/mode) - - (ctu/testing-unary-form elm/mode)) + (ctu/testing-unary-op elm/mode)) ;; 21.11. PopulationVariance ;; @@ -306,9 +286,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/population-variance) - - (ctu/testing-unary-form elm/population-variance)) + (ctu/testing-unary-op elm/population-variance)) ;; 21.12. PopulationStdDev ;; @@ -330,9 +308,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/population-std-dev) - - (ctu/testing-unary-form elm/population-std-dev)) + (ctu/testing-unary-op elm/population-std-dev)) ;; 21.13. Sum ;; @@ -355,9 +331,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/sum) - - (ctu/testing-unary-form elm/sum)) + (ctu/testing-unary-op elm/sum)) ;; 21.14. StdDev ;; @@ -379,9 +353,7 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/std-dev) - - (ctu/testing-unary-form elm/std-dev)) + (ctu/testing-unary-op elm/std-dev)) ;; 21.15. Variance ;; @@ -403,6 +375,4 @@ #elm/list [] nil {:type "Null"} nil)) - (ctu/testing-unary-dynamic elm/variance) - - (ctu/testing-unary-form elm/variance)) + (ctu/testing-unary-op elm/variance)) 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 83fd2f26b..25d5cae43 100644 --- a/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/arithmetic_operators_test.clj @@ -16,7 +16,7 @@ [blaze.elm.literal :as elm] [blaze.elm.literal-spec] [blaze.elm.protocols :as p] - [blaze.elm.quantity :as quantity] + [blaze.elm.quantity :refer [quantity]] [blaze.elm.util-spec] [blaze.fhir.spec.type.system :as system] [blaze.test-util :refer [satisfies-prop]] @@ -68,21 +68,21 @@ (testing "Quantity" (are [x res] (= res (ctu/compile-unop elm/abs elm/quantity x)) - [-1] (quantity/quantity 1 "1") - [0] (quantity/quantity 0 "1") - [1] (quantity/quantity 1 "1") + [-1] (quantity 1 "1") + [0] (quantity 0 "1") + [1] (quantity 1 "1") - [-1M] (quantity/quantity 1M "1") - [0M] (quantity/quantity 0M "1") - [1M] (quantity/quantity 1M "1") + [-1M] (quantity 1M "1") + [0M] (quantity 0M "1") + [1M] (quantity 1M "1") - [-1 "m"] (quantity/quantity 1 "m") - [0 "m"] (quantity/quantity 0 "m") - [1 "m"] (quantity/quantity 1 "m") + [-1 "m"] (quantity 1 "m") + [0 "m"] (quantity 0 "m") + [1 "m"] (quantity 1 "m") - [-1M "m"] (quantity/quantity 1M "m") - [0M "m"] (quantity/quantity 0M "m") - [1M "m"] (quantity/quantity 1M "m")))) + [-1M "m"] (quantity 1M "m") + [0M "m"] (quantity 0M "m") + [1M "m"] (quantity 1M "m")))) (testing "Dynamic" (are [elm res] (= res (ctu/dynamic-compile-eval (elm/abs elm))) @@ -91,9 +91,7 @@ (ctu/testing-unary-null elm/abs) - (ctu/testing-unary-dynamic elm/abs) - - (ctu/testing-unary-form elm/abs)) + (ctu/testing-unary-op elm/abs)) ;; 16.2. Add ;; @@ -231,8 +229,8 @@ (testing "UCUM quantity" (are [x y res] (p/equal res (ctu/compile-binop elm/add elm/quantity x y)) - [1 "m"] [1 "m"] (quantity/quantity 2 "m") - [1 "m"] [1 "cm"] (quantity/quantity 1.01M "m"))) + [1 "m"] [1 "m"] (quantity 2 "m") + [1 "m"] [1 "cm"] (quantity 1.01M "m"))) (testing "Incompatible UCUM Quantity Subtractions" (are [x y] (thrown? UnconvertibleException (ctu/compile-binop elm/add elm/quantity x y)) @@ -364,9 +362,7 @@ #elm/time "00:00:00" #elm/quantity [1 "minute"] (date-time/local-time 0 1 0) #elm/time "00:00:00" #elm/quantity [1 "second"] (date-time/local-time 0 0 1))) - (ctu/testing-binary-dynamic elm/add) - - (ctu/testing-binary-form elm/add)) + (ctu/testing-binary-op elm/add)) ;; 16.3. Ceiling ;; @@ -381,9 +377,7 @@ (ctu/testing-unary-null elm/ceiling) - (ctu/testing-unary-dynamic elm/ceiling) - - (ctu/testing-unary-form elm/ceiling)) + (ctu/testing-unary-op elm/ceiling)) ;; 16.4. Divide ;; @@ -445,24 +439,24 @@ (testing "Quantity" (testing "Static" (are [x y res] (p/equal res (ctu/compile-binop elm/divide elm/quantity x y)) - [1 "m"] [1 "s"] (quantity/quantity 1 "m/s") - [1M "m"] [1M "s"] (quantity/quantity 1M "m/s") + [1 "m"] [1 "s"] (quantity 1 "m/s") + [1M "m"] [1M "s"] (quantity 1M "m/s") - [12 "cm2"] [3 "cm"] (quantity/quantity 4 "cm"))) + [12 "cm2"] [3 "cm"] (quantity 4 "cm"))) (ctu/testing-binary-null elm/divide #elm/quantity [1])) (testing "Quantity/Integer" (testing "Static" (are [x y res] (p/equal res (ctu/compile-binop elm/divide elm/quantity elm/integer x y)) - [1M "m"] "2" (quantity/quantity 0.5M "m"))) + [1M "m"] "2" (quantity 0.5M "m"))) (ctu/testing-binary-null elm/divide #elm/quantity [1] #elm/integer "1")) (testing "Quantity/Decimal" (testing "Static" (are [x y res] (p/equal res (ctu/compile-binop elm/divide elm/quantity elm/decimal x y)) - [2.5M "m"] "2.5" (quantity/quantity 1M "m"))) + [2.5M "m"] "2.5" (quantity 1M "m"))) (ctu/testing-binary-null elm/divide #elm/quantity [1] #elm/decimal "1.1")) @@ -472,9 +466,7 @@ (let [elm (elm/equal [(elm/multiply [(elm/divide [decimal decimal]) decimal]) decimal])] (true? (core/-eval (c/compile {} elm) {} nil nil)))))) - (ctu/testing-binary-dynamic elm/divide) - - (ctu/testing-binary-form elm/divide)) + (ctu/testing-binary-op elm/divide)) ;; 16.5. Exp ;; @@ -488,9 +480,7 @@ (ctu/testing-unary-null elm/exp) - (ctu/testing-unary-dynamic elm/exp) - - (ctu/testing-unary-form elm/exp)) + (ctu/testing-unary-op elm/exp)) ;; 16.6. Floor ;; @@ -505,9 +495,7 @@ (ctu/testing-unary-null elm/floor) - (ctu/testing-unary-dynamic elm/floor) - - (ctu/testing-unary-form elm/floor)) + (ctu/testing-unary-op elm/floor)) ;; 16.7. HighBoundary ;; @@ -549,9 +537,7 @@ (ctu/testing-binary-null elm/log #elm/decimal "1.1")) - (ctu/testing-binary-dynamic elm/log) - - (ctu/testing-binary-form elm/log)) + (ctu/testing-binary-op elm/log)) ;; 16.9. LowBoundary ;; @@ -593,9 +579,7 @@ (ctu/testing-unary-null elm/ln) - (ctu/testing-unary-dynamic elm/ln) - - (ctu/testing-unary-form elm/ln)) + (ctu/testing-unary-op elm/ln)) ;; 16.11. MaxValue ;; @@ -719,9 +703,7 @@ #elm/integer "1" #elm/integer "0" nil #elm/decimal "1" #elm/decimal "0" nil)) - (ctu/testing-binary-dynamic elm/modulo) - - (ctu/testing-binary-form elm/modulo)) + (ctu/testing-binary-op elm/modulo)) ;; 16.14. Multiply ;; @@ -758,14 +740,12 @@ (testing "Quantity" (are [x y res] (p/equal res (core/-eval (c/compile {} (elm/multiply [x y])) {} nil nil)) - #elm/quantity [1 "m"] #elm/integer "2" (quantity/quantity 2 "m") - #elm/quantity [1 "m"] #elm/quantity [2 "m"] (quantity/quantity 2 "m2")) + #elm/quantity [1 "m"] #elm/integer "2" (quantity 2 "m") + #elm/quantity [1 "m"] #elm/quantity [2 "m"] (quantity 2 "m2")) (ctu/testing-binary-null elm/multiply #elm/quantity [1])) - (ctu/testing-binary-dynamic elm/multiply) - - (ctu/testing-binary-form elm/multiply)) + (ctu/testing-binary-op elm/multiply)) ;; 16.15. Negate ;; @@ -787,16 +767,14 @@ (testing "Quantity" (are [x res] (= res (c/compile {} (elm/negate x))) - #elm/quantity [1] (quantity/quantity -1 "1") - #elm/quantity [1M] (quantity/quantity -1M "1") - #elm/quantity [1 "m"] (quantity/quantity -1 "m") - #elm/quantity [1M "m"] (quantity/quantity -1M "m"))) + #elm/quantity [1] (quantity -1 "1") + #elm/quantity [1M] (quantity -1M "1") + #elm/quantity [1 "m"] (quantity -1 "m") + #elm/quantity [1M "m"] (quantity -1M "m"))) (ctu/testing-unary-null elm/negate) - (ctu/testing-unary-dynamic elm/negate) - - (ctu/testing-unary-form elm/negate)) + (ctu/testing-unary-op elm/negate)) ;; 16.16. Power ;; @@ -829,9 +807,7 @@ #elm/decimal "10" #elm/integer "2" 100M #elm/decimal "10" #elm/integer "2" 100M)) - (ctu/testing-binary-dynamic elm/power) - - (ctu/testing-binary-form elm/power)) + (ctu/testing-binary-op elm/power)) ;; 16.17. Precision ;; @@ -908,14 +884,12 @@ (testing "Quantity" (are [x res] (= res (c/compile {} (elm/predecessor x))) (elm/quantity [decimal/min]) nil - #_#_#elm/quantity [0 "m"] (quantity/quantity -1 "m") ; TODO: implement - #elm/quantity [0M "m"] (quantity/quantity -1E-8M "m"))) + #_#_#elm/quantity [0 "m"] (quantity -1 "m") ; TODO: implement + #elm/quantity [0M "m"] (quantity -1E-8M "m"))) (ctu/testing-unary-null elm/predecessor) - (ctu/testing-unary-dynamic elm/predecessor) - - (ctu/testing-unary-form elm/predecessor)) + (ctu/testing-unary-op elm/predecessor)) ;; 16.19. Round ;; @@ -929,7 +903,7 @@ ;; precision is not specified or null, 0 is assumed. (deftest compile-round-test (testing "without precision" - (testing "static" + (testing "Static" (are [x res] (= res (c/compile {} (elm/round [x]))) #elm/integer "1" 1M #elm/decimal "1" 1M @@ -965,13 +939,7 @@ (ctu/testing-unary-null elm/round) - (ctu/testing-unary-dynamic elm/round) - - (ctu/testing-unary-form elm/round) - - (ctu/testing-binary-dynamic elm/round) - - (ctu/testing-binary-form elm/round)) + (ctu/testing-unary-op elm/round)) ;; 16.20. Subtract ;; @@ -1079,8 +1047,8 @@ (testing "UCUM quantity" (are [x y res] (p/equal res (core/-eval (c/compile {} (elm/subtract [x y])) {} nil nil)) - #elm/quantity [1 "m"] #elm/quantity [1 "m"] (quantity/quantity 0 "m") - #elm/quantity [1 "m"] #elm/quantity [1 "cm"] (quantity/quantity 0.99 "m"))) + #elm/quantity [1 "m"] #elm/quantity [1 "m"] (quantity 0 "m") + #elm/quantity [1 "m"] #elm/quantity [1 "cm"] (quantity 0.99 "m"))) (testing "Incompatible UCUM Quantity Subtractions" (are [x y] (thrown? UnconvertibleException (core/-eval (c/compile {} (elm/subtract [x y])) {} nil nil)) @@ -1189,9 +1157,7 @@ #elm/time "00:00:00" #elm/quantity [1 "minute"] (date-time/local-time 23 59 0) #elm/time "00:00:00" #elm/quantity [1 "second"] (date-time/local-time 23 59 59))) - (ctu/testing-binary-dynamic elm/subtract) - - (ctu/testing-binary-form elm/subtract)) + (ctu/testing-binary-op elm/subtract)) ;; 16.21. Successor ;; @@ -1256,14 +1222,12 @@ (testing "Quantity" (are [x res] (= res (c/compile {} (elm/successor x))) (elm/quantity [decimal/max]) nil - #_#_#elm/quantity [0 "m"] (quantity/quantity 1 "m") ; TODO: implement - #elm/quantity [0M "m"] (quantity/quantity 1E-8M "m"))) + #_#_#elm/quantity [0 "m"] (quantity 1 "m") ; TODO: implement + #elm/quantity [0M "m"] (quantity 1E-8M "m"))) (ctu/testing-unary-null elm/successor) - (ctu/testing-unary-dynamic elm/successor) - - (ctu/testing-unary-form elm/successor)) + (ctu/testing-unary-op elm/successor)) ;; 16.22. Truncate ;; @@ -1278,9 +1242,7 @@ (ctu/testing-unary-null elm/truncate) - (ctu/testing-unary-dynamic elm/truncate) - - (ctu/testing-unary-form elm/truncate)) + (ctu/testing-unary-op elm/truncate)) ;; 16.23. TruncatedDivide ;; @@ -1340,6 +1302,4 @@ (ctu/testing-binary-null elm/truncated-divide #elm/integer "1" #elm/decimal "1.1")) - (ctu/testing-binary-dynamic elm/truncated-divide) - - (ctu/testing-binary-form elm/truncated-divide)) + (ctu/testing-binary-op elm/truncated-divide)) diff --git a/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj b/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj index 0bf5ec552..fa9f0deaa 100644 --- a/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj @@ -52,26 +52,36 @@ (deftest compile-calculate-age-at-test (testing "Static" (are [elm res] (= res (core/-eval (c/compile {} elm) {:now ctu/now} nil nil)) - #elm/calculate-age-at [#elm/date "2018" #elm/date "2019" "year"] 1 - #elm/calculate-age-at [#elm/date "2018" #elm/date "2018" "year"] 0 - #elm/calculate-age-at [#elm/date "2018" #elm/date "2019" "month"] nil + #elm/calculate-age-at [#elm/date"2018" #elm/date"2019" "year"] 1 + #elm/calculate-age-at [#elm/date"2018" #elm/date"2018" "year"] 0 + #elm/calculate-age-at [#elm/date"2018" #elm/date"2019" "month"] nil - #elm/calculate-age-at [#elm/date "2018-01" #elm/date "2019-02" "year"] 1 - #elm/calculate-age-at [#elm/date "2018-01" #elm/date "2018-12" "year"] 0 - #elm/calculate-age-at [#elm/date "2018-01" #elm/date "2018-12" "month"] 11 - #elm/calculate-age-at [#elm/date "2018-01" #elm/date "2018-12" "day"] nil + #elm/calculate-age-at [#elm/date"2018-01" #elm/date"2019-02" "year"] 1 + #elm/calculate-age-at [#elm/date"2018-01" #elm/date"2018-12" "year"] 0 + #elm/calculate-age-at [#elm/date"2018-01" #elm/date"2018-12" "month"] 11 + #elm/calculate-age-at [#elm/date"2018-01" #elm/date"2018-12" "day"] nil - #elm/calculate-age-at [#elm/date "2018-01-01" #elm/date "2019-02-02" "year"] 1 - #elm/calculate-age-at [#elm/date "2018-01" #elm/date "2018-12-15" "year"] 0 - #elm/calculate-age-at [#elm/date "2018-01-01" #elm/date "2018-12-02" "month"] 11 - #elm/calculate-age-at [#elm/date "2018-01-01" #elm/date "2018-02-01" "day"] 31 + #elm/calculate-age-at [#elm/date"2018-01-01" #elm/date"2019-02-02" "year"] 1 + #elm/calculate-age-at [#elm/date"2018-01" #elm/date"2018-12-15" "year"] 0 + #elm/calculate-age-at [#elm/date"2018-01-01" #elm/date"2018-12-02" "month"] 11 + #elm/calculate-age-at [#elm/date"2018-01-01" #elm/date"2018-02-01" "day"] 31 #elm/calculate-age-at [#elm/date-time"2018-01-01" #elm/date-time"2018-02-01" "day"] 31)) - (ctu/testing-binary-null elm/calculate-age-at #elm/date "2018") + (ctu/testing-binary-null elm/calculate-age-at #elm/date"2018") (ctu/testing-binary-null elm/calculate-age-at #elm/date-time"2018-01-01") - (ctu/testing-binary-dynamic elm/calculate-age-at) + (ctu/testing-binary-precision-dynamic elm/calculate-age-at "year" "month" "day") + + (ctu/testing-binary-precision-attach-cache elm/calculate-age-at "year" "month" "day") + + (ctu/testing-binary-precision-patient-count elm/calculate-age-at "year" "month" "day") + + (ctu/testing-binary-precision-resolve-refs elm/calculate-age-at "year" "month" "day") + + (ctu/testing-binary-precision-resolve-params elm/calculate-age-at "year" "month" "day") + + (ctu/testing-binary-precision-equals-hash-code elm/calculate-age-at "year" "month" "day") (ctu/testing-binary-precision-form elm/calculate-age-at "year" "month" "day")) diff --git a/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj b/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj index 855ce84eb..bc467be19 100644 --- a/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj +++ b/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj @@ -15,8 +15,8 @@ [blaze.elm.date-time :as date-time] [blaze.elm.literal] [blaze.elm.literal-spec] - [blaze.elm.quantity :as quantity] - [blaze.elm.ratio :as ratio] + [blaze.elm.quantity :refer [quantity]] + [blaze.elm.ratio :refer [ratio]] [blaze.test-util :refer [satisfies-prop]] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] @@ -279,7 +279,7 @@ (testing "Examples" (are [elm res] (= res (c/compile {} elm)) {:type "Quantity"} nil - #elm/quantity [1] (quantity/quantity 1 "") + #elm/quantity [1] (quantity 1 "") #elm/quantity [1 "year"] (date-time/period 1 0 0) #elm/quantity [2 "years"] (date-time/period 2 0 0) #elm/quantity [1 "month"] (date-time/period 0 1 0) @@ -296,14 +296,14 @@ #elm/quantity [2 "seconds"] (date-time/period 0 0 2000) #elm/quantity [1 "millisecond"] (date-time/period 0 0 1) #elm/quantity [2 "milliseconds"] (date-time/period 0 0 2) - #elm/quantity [1 "s"] (quantity/quantity 1 "s") - #elm/quantity [1 "cm2"] (quantity/quantity 1 "cm2"))) + #elm/quantity [1 "s"] (quantity 1 "s") + #elm/quantity [1 "cm2"] (quantity 1 "cm2"))) (testing "form" (are [elm res] (= res (c/form (c/compile {} elm))) - #elm/quantity [1] '(quantity 1 "1") - #elm/quantity [1 "s"] '(quantity 1 "s") - #elm/quantity [2 "cm2"] '(quantity 2 "cm2"))) + #elm/quantity [1] '(quantity 1M "1") + #elm/quantity [1 "s"] '(quantity 1M "s") + #elm/quantity [2 "cm2"] '(quantity 2M "cm2"))) (testing "Periods" (satisfies-prop 100 @@ -318,12 +318,17 @@ (deftest compile-ratio-test (testing "Examples" (are [elm res] (= res (c/compile {} elm)) - #elm/ratio [[1 "s"] [1 "s"]] (ratio/ratio (quantity/quantity 1 "s") (quantity/quantity 1 "s")) - #elm/ratio [[1 ""] [128 ""]] (ratio/ratio (quantity/quantity 1 "") (quantity/quantity 128 "")) - #elm/ratio [[1 "s"] [1 ""]] (ratio/ratio (quantity/quantity 1 "s") (quantity/quantity 1 "")) - #elm/ratio [[1 ""] [1 "s"]] (ratio/ratio (quantity/quantity 1 "") (quantity/quantity 1 "s")) - #elm/ratio [[1 "cm2"] [1 "s"]] (ratio/ratio (quantity/quantity 1 "cm2") (quantity/quantity 1 "s")) - #elm/ratio [[1] [1]] (ratio/ratio (quantity/quantity 1 "") (quantity/quantity 1 "")) - #elm/ratio [[1] [1 "s"]] (ratio/ratio (quantity/quantity 1 "") (quantity/quantity 1 "s")) - #elm/ratio [[1 "s"] [1]] (ratio/ratio (quantity/quantity 1 "s") (quantity/quantity 1 "")) - #elm/ratio [[5 "mg"] [10 "g"]] (ratio/ratio (quantity/quantity 5 "mg") (quantity/quantity 10 "g"))))) + #elm/ratio [[1 "s"] [1 "s"]] (ratio (quantity 1 "s") (quantity 1 "s")) + #elm/ratio [[1 ""] [128 ""]] (ratio (quantity 1 "") (quantity 128 "")) + #elm/ratio [[1 "s"] [1 ""]] (ratio (quantity 1 "s") (quantity 1 "")) + #elm/ratio [[1 ""] [1 "s"]] (ratio (quantity 1 "") (quantity 1 "s")) + #elm/ratio [[1 "cm2"] [1 "s"]] (ratio (quantity 1 "cm2") (quantity 1 "s")) + #elm/ratio [[1] [1]] (ratio (quantity 1 "") (quantity 1 "")) + #elm/ratio [[1] [1 "s"]] (ratio (quantity 1 "") (quantity 1 "s")) + #elm/ratio [[1 "s"] [1]] (ratio (quantity 1 "s") (quantity 1 "")) + #elm/ratio [[5 "mg"] [10 "g"]] (ratio (quantity 5 "mg") (quantity 10 "g")))) + + (testing "form" + (has-form + (ratio (quantity 3 "m") (quantity 1 "s")) + '(ratio (quantity 3M "m") (quantity 1M "s"))))) 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 5a87eb6b0..e80c420cd 100644 --- a/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/comparison_operators_test.clj @@ -199,7 +199,7 @@ "2012" "2013" false? "2013" "2012" false?) - (ctu/testing-binary-null elm/equal #elm/date "2013")) + (ctu/testing-binary-null elm/equal #elm/date"2013")) (testing "Date with year-month precision" (are [x y pred] (pred (ctu/compile-binop elm/equal elm/date x y)) @@ -207,7 +207,7 @@ "2013-01" "2013-02" false? "2013-02" "2013-01" false?) - (ctu/testing-binary-null elm/equal #elm/date "2013-01")) + (ctu/testing-binary-null elm/equal #elm/date"2013-01")) (testing "Date with full precision" (are [x y pred] (pred (ctu/compile-binop elm/equal elm/date x y)) @@ -215,7 +215,7 @@ "2013-01-01" "2013-01-02" false? "2013-01-02" "2013-01-01" false?) - (ctu/testing-binary-null elm/equal #elm/date "2013-01-01")) + (ctu/testing-binary-null elm/equal #elm/date"2013-01-01")) (testing "Date with differing precisions" (are [x y pred] (pred (ctu/compile-binop elm/equal elm/date x y)) @@ -261,9 +261,7 @@ (ctu/testing-binary-null elm/equal (ctu/code "a" "0"))) - (ctu/testing-binary-dynamic elm/equal) - - (ctu/testing-binary-form elm/equal)) + (ctu/testing-binary-op elm/equal)) ;; 12.2. Equivalent ;; @@ -441,9 +439,7 @@ {:type "Null"} (ctu/code "a" "0") false? (ctu/code "a" "0") {:type "Null"} false?)) - (ctu/testing-binary-dynamic elm/equivalent) - - (ctu/testing-binary-form elm/equivalent)) + (ctu/testing-binary-op elm/equivalent)) ;; 12.3. Greater ;; @@ -510,7 +506,7 @@ "2014" "2013" true? "2013" "2013" false?) - (ctu/testing-binary-null elm/greater #elm/date "2013")) + (ctu/testing-binary-null elm/greater #elm/date"2013")) (testing "DateTime with year precision" (are [x y pred] (pred (ctu/compile-binop elm/greater elm/date-time x y)) @@ -562,9 +558,7 @@ (ctu/testing-binary-null elm/greater #elm/quantity [1])) - (ctu/testing-binary-dynamic elm/greater) - - (ctu/testing-binary-form elm/greater)) + (ctu/testing-binary-op elm/greater)) ;; 12.4. GreaterOrEqual ;; @@ -638,7 +632,7 @@ "2013-06-15" "2013-06-15" true? "2013-06-14" "2013-06-15" false?) - (ctu/testing-binary-null elm/greater-or-equal #elm/date "2013-06-15")) + (ctu/testing-binary-null elm/greater-or-equal #elm/date"2013-06-15")) (testing "DateTime with year precision" (are [x y pred] (pred (ctu/compile-binop elm/greater-or-equal elm/date-time x y)) @@ -646,7 +640,7 @@ "2013" "2013" true? "2012" "2013" false?) - (ctu/testing-binary-null elm/greater-or-equal #elm/date "2013")) + (ctu/testing-binary-null elm/greater-or-equal #elm/date"2013")) (testing "DateTime with year-month precision" (are [x y pred] (pred (ctu/compile-binop elm/greater-or-equal elm/date-time x y)) @@ -654,7 +648,7 @@ "2013-06" "2013-06" true? "2013-05" "2013-06" false?) - (ctu/testing-binary-null elm/greater-or-equal #elm/date "2013-06")) + (ctu/testing-binary-null elm/greater-or-equal #elm/date"2013-06")) (testing "DateTime with date precision" (are [x y pred] (pred (ctu/compile-binop elm/greater-or-equal elm/date-time x y)) @@ -662,7 +656,7 @@ "2013-06-15" "2013-06-15" true? "2013-06-14" "2013-06-15" false?) - (ctu/testing-binary-null elm/greater-or-equal #elm/date "2013-06-15")) + (ctu/testing-binary-null elm/greater-or-equal #elm/date"2013-06-15")) (testing "DateTime with mixed precision" (are [x y] (nil? (ctu/compile-binop elm/greater-or-equal elm/date-time x y)) @@ -695,9 +689,7 @@ (ctu/testing-binary-null elm/greater-or-equal #elm/quantity [1])) - (ctu/testing-binary-dynamic elm/greater-or-equal) - - (ctu/testing-binary-form elm/greater-or-equal)) + (ctu/testing-binary-op elm/greater-or-equal)) ;; 12.5. Less ;; @@ -762,7 +754,7 @@ "2012" "2013" true? "2013" "2013" false?) - (ctu/testing-binary-null elm/less #elm/date "2013")) + (ctu/testing-binary-null elm/less #elm/date"2013")) (testing "Comparing dates with mixed precisions (year and year-month) results in null." (are [x y pred] (pred (ctu/compile-binop elm/less elm/date x y)) @@ -774,7 +766,7 @@ "2013-06-14" "2013-06-15" true? "2013-06-15" "2013-06-15" false?) - (ctu/testing-binary-null elm/less #elm/date "2013-06-15")) + (ctu/testing-binary-null elm/less #elm/date"2013-06-15")) (testing "Comparing dates with mixed precisions (year-month and full) results in null." (are [x y pred] (pred (ctu/compile-binop elm/less elm/date x y)) @@ -832,9 +824,7 @@ (ctu/testing-binary-null elm/less #elm/quantity [1])) - (ctu/testing-binary-dynamic elm/less) - - (ctu/testing-binary-form elm/less)) + (ctu/testing-binary-op elm/less)) ;; 12.6. LessOrEqual ;; @@ -900,7 +890,7 @@ "2013-06-15" "2013-06-15" true? "2013-06-16" "2013-06-15" false?) - (ctu/testing-binary-null elm/less-or-equal #elm/date "2013-06-15")) + (ctu/testing-binary-null elm/less-or-equal #elm/date"2013-06-15")) (testing "Mixed Date and DateTime" (are [x y pred] (pred (c/compile {} (elm/less-or-equal [x y]))) @@ -913,7 +903,7 @@ "2013" "2013" true? "2014" "2013" false?) - (ctu/testing-binary-null elm/less-or-equal #elm/date "2013")) + (ctu/testing-binary-null elm/less-or-equal #elm/date"2013")) (testing "DateTime with year-month precision" (are [x y pred] (pred (ctu/compile-binop elm/less-or-equal elm/date-time x y)) @@ -921,7 +911,7 @@ "2013-06" "2013-06" true? "2013-07" "2013-06" false?) - (ctu/testing-binary-null elm/less-or-equal #elm/date "2013-06")) + (ctu/testing-binary-null elm/less-or-equal #elm/date"2013-06")) (testing "DateTime with date precision" (are [x y pred] (pred (ctu/compile-binop elm/less-or-equal elm/date-time x y)) @@ -929,7 +919,7 @@ "2013-06-15" "2013-06-15" true? "2013-06-16" "2013-06-15" false?) - (ctu/testing-binary-null elm/less-or-equal #elm/date "2013-06-15")) + (ctu/testing-binary-null elm/less-or-equal #elm/date"2013-06-15")) (testing "Time" (are [x y pred] (pred (ctu/compile-binop elm/less-or-equal elm/time x y)) @@ -958,9 +948,7 @@ (ctu/testing-binary-null elm/less-or-equal #elm/quantity [1])) - (ctu/testing-binary-dynamic elm/less-or-equal) - - (ctu/testing-binary-form elm/less-or-equal)) + (ctu/testing-binary-op elm/less-or-equal)) ;; 12.7. NotEqual ;; diff --git a/modules/cql/test/blaze/elm/compiler/conditional_operators_test.clj b/modules/cql/test/blaze/elm/compiler/conditional_operators_test.clj index 6fec8c56c..b87b2f5de 100644 --- a/modules/cql/test/blaze/elm/compiler/conditional_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/conditional_operators_test.clj @@ -7,9 +7,11 @@ [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] + [blaze.elm.expression.cache :as ec] [blaze.elm.literal-spec] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [are deftest is testing]])) + [clojure.test :as test :refer [are deftest is testing]] + [juxt.iota :refer [given]])) (st/instrument) (ctu/instrument-compile) @@ -22,6 +24,17 @@ (test/use-fixtures :each fixture) +(defn- from-names [names] + (mapv + (fn [n] + {:type "ExpressionDef" :name n + :expression n + :context "Unfiltered"}) + names)) + +(defn- index-by-name [expr-defs] + (into {} (map (fn [{:keys [name] :as expr-def}] [name expr-def])) expr-defs)) + ;; 15.1. Case ;; ;; The Case operator allows for multiple conditional expressions to be chained @@ -119,7 +132,116 @@ :caseItem [{:when #elm/parameter-ref "b" :then #elm/parameter-ref "1"}] - :else #elm/parameter-ref "2"}))))))) + :else #elm/parameter-ref "2"})))))) + + (testing "attach cache" + (testing "multi-conditional" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm {:type "Case" + :caseItem + [{:when #elm/exists #elm/retrieve{:type "Encounter"} + :then #elm/exists #elm/retrieve{:type "Observation"}}] + :else #elm/exists #elm/retrieve{:type "Condition"}} + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 3 + [1 0] := '(exists (retrieve "Encounter")) + [1 1] := '(exists (retrieve "Observation")) + [1 2] := '(exists (retrieve "Condition")))))) + + (testing "comparand-based" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm {:type "Case" + :comparand #elm/exists #elm/retrieve{:type "Encounter"} + :caseItem + [{:when #elm/exists #elm/retrieve{:type "Observation"} + :then #elm/exists #elm/retrieve{:type "Condition"}}] + :else #elm/exists #elm/retrieve{:type "MedicationAdministration"}} + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 4 + [1 0] := '(exists (retrieve "Encounter")) + [1 1] := '(exists (retrieve "Observation")) + [1 2] := '(exists (retrieve "Condition")) + [1 3] := '(exists (retrieve "MedicationAdministration"))))))) + + (testing "resolve expression references" + (testing "multi-conditional" + (let [elm {:type "Case" + :caseItem + [{:when #elm/expression-ref "w" + :then #elm/expression-ref "t"}] + :else #elm/expression-ref "e"} + expr-defs (from-names ["w" "t" "e"]) + ctx {:library {:statements {:def expr-defs}}} + expr (c/resolve-refs (c/compile ctx elm) (index-by-name expr-defs))] + (has-form expr '(case "w" "t" "e")))) + + (testing "comparand-based" + (let [elm {:type "Case" + :comparand #elm/expression-ref "c" + :caseItem + [{:when #elm/expression-ref "w" + :then #elm/expression-ref "t"}] + :else #elm/expression-ref "e"} + expr-defs (from-names ["c" "w" "t" "e"]) + ctx {:library {:statements {:def expr-defs}}} + expr (c/resolve-refs (c/compile ctx elm) (index-by-name expr-defs))] + (has-form expr '(case "c" "w" "t" "e"))))) + + (testing "resolve parameters" + (testing "multi-conditional" + (let [elm {:type "Case" + :caseItem + [{:when #elm/parameter-ref "w" + :then #elm/parameter-ref "t"}] + :else #elm/parameter-ref "e"} + ctx {:library {:parameters {:def [{:name "w"} {:name "t"} {:name "e"}]}}} + expr (c/resolve-params (c/compile ctx elm) {"w" "w" "t" "t" "e" "e"})] + (has-form expr '(case "w" "t" "e")))) + + (testing "comparand-based" + (let [elm {:type "Case" + :comparand #elm/parameter-ref "c" + :caseItem + [{:when #elm/parameter-ref "w" + :then #elm/parameter-ref "t"}] + :else #elm/parameter-ref "e"} + ctx {:library {:parameters {:def [{:name "c"} {:name "w"} {:name "t"} {:name "e"}]}}} + expr (c/resolve-params (c/compile ctx elm) {"c" "c" "w" "w" "t" "t" "e" "e"})] + (has-form expr '(case "c" "w" "t" "e"))))) + + (testing "equals/hashCode" + (testing "multi-conditional" + (let [elm {:type "Case" + :caseItem + [{:when #elm/parameter-ref "w" + :then #elm/parameter-ref "t"}] + :else #elm/parameter-ref "e"} + ctx {:library {:parameters {:def [{:name "w"} {:name "t"} {:name "e"}]}}} + expr-1 (c/compile ctx elm) + expr-2 (c/compile ctx elm)] + (is (= 1 (count (set [expr-1 expr-2])))))) + + (testing "comparand-based" + (let [elm {:type "Case" + :comparand #elm/parameter-ref "c" + :caseItem + [{:when #elm/parameter-ref "w" + :then #elm/parameter-ref "t"}] + :else #elm/parameter-ref "e"} + ctx {:library {:parameters {:def [{:name "c"} {:name "w"} {:name "t"} {:name "e"}]}}} + expr-1 (c/compile ctx elm) + expr-2 (c/compile ctx elm)] + (is (= 1 (count (set [expr-1 expr-2])))))))) ;; 15.2. If ;; @@ -156,4 +278,40 @@ (let [expr (ctu/dynamic-compile #elm/if [#elm/parameter-ref "x" #elm/parameter-ref "y" #elm/parameter-ref "z"])] - (has-form expr '(if (param-ref "x") (param-ref "y") (param-ref "z")))))) + (has-form expr '(if (param-ref "x") (param-ref "y") (param-ref "z"))))) + + (testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) %2)] + (let [elm #elm/if [#elm/exists #elm/retrieve{:type "Encounter"} + #elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 3 + [1 0 c/form] := '(exists (retrieve "Encounter")) + [1 1 c/form] := '(exists (retrieve "Observation")) + [1 2 c/form] := '(exists (retrieve "Condition")))))) + + (testing "resolve expression references" + (let [elm #elm/if [#elm/expression-ref "c" + #elm/expression-ref "t" + #elm/expression-ref "e"] + expr-defs (from-names ["c" "t" "e"]) + ctx {:library {:statements {:def expr-defs}}} + expr (c/resolve-refs (c/compile ctx elm) (index-by-name expr-defs))] + (has-form expr '(if "c" "t" "e")))) + + (testing "resolve parameters" + (let [elm #elm/if [#elm/parameter-ref "c" + #elm/parameter-ref "t" + #elm/parameter-ref "e"] + ctx {:library {:parameters {:def [{:name "c"} {:name "t"} {:name "e"}]}}} + expr (c/resolve-params (c/compile ctx elm) {"c" "c" "t" "t" "e" "e"})] + (has-form expr '(if "c" "t" "e")))) + + (ctu/testing-equals-hash-code #elm/if [#elm/parameter-ref "x" + #elm/parameter-ref "y" + #elm/parameter-ref "z"])) 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 7fb98799e..e265cdbbf 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 @@ -7,7 +7,8 @@ [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.compiler.date-time-operators] + [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.date-time :as date-time] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] @@ -183,7 +184,7 @@ (let [elm #elm/date [#elm/parameter-ref "year"] expr (c/compile compile-ctx elm)] - (is (= '(date (param-ref "year")) (core/-form expr))) + (has-form expr '(date (param-ref "year"))) (is (false? (core/-static expr))))) @@ -192,8 +193,7 @@ #elm/parameter-ref "month"] expr (c/compile compile-ctx elm)] - (is (= '(date (param-ref "year") (param-ref "month")) - (core/-form expr))) + (has-form expr '(date (param-ref "year") (param-ref "month"))) (is (false? (core/-static expr))))) @@ -203,10 +203,55 @@ #elm/parameter-ref "day"] expr (c/compile compile-ctx elm)] - (is (= '(date (param-ref "year") (param-ref "month") (param-ref "day")) - (core/-form expr))) + (has-form expr '(date (param-ref "year") (param-ref "month") (param-ref "day"))) - (is (false? (core/-static expr)))))))) + (is (false? (core/-static expr))))))) + + (testing "resolve parameters" + (let [compile-ctx {:library + {:parameters + {:def + [{:name "year"} + {:name "month"} + {:name "day"} + {:name "x"}]}}}] + + (testing "year" + (testing "with parameter-ref in expression with unresolved parameter-ref" + (let [elm #elm/date [#elm/add [#elm/parameter-ref "year" #elm/parameter-ref "x"]] + expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date (add 2024 (param-ref "x"))))))) + + (testing "year-month" + (let [elm #elm/date [#elm/parameter-ref "year" #elm/parameter-ref "month"]] + + (testing "with only the year parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date 2024 (param-ref "month"))))) + + (testing "with only the month parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"month" 6})] + (has-form expr '(date (param-ref "year") 6)))))) + + (testing "date" + (let [elm #elm/date [#elm/parameter-ref "year" + #elm/parameter-ref "month" + #elm/parameter-ref "day"]] + + (testing "with only the year parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date 2024 (param-ref "month") (param-ref "day"))))) + + (testing "with only the month parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"month" 6})] + (has-form expr '(date (param-ref "year") 6 (param-ref "day"))))) + + (testing "with only the day parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"day" 15})] + (has-form expr '(date (param-ref "year") (param-ref "month") 15)))))))) + + (ctu/testing-binary-op elm/date) + (ctu/testing-ternary-op elm/date)) ;; 18.7. DateFrom ;; @@ -226,9 +271,7 @@ (ctu/testing-unary-null elm/date-from) - (ctu/testing-unary-dynamic elm/date-from) - - (ctu/testing-unary-form elm/date-from)) + (ctu/testing-unary-op elm/date-from)) ;; 18.8. DateTime ;; @@ -417,7 +460,7 @@ (let [elm #elm/date-time [#elm/parameter-ref "year"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year")) (core/-form expr))) + (has-form expr '(date-time (param-ref "year"))) (is (false? (core/-static expr))))) @@ -426,8 +469,7 @@ #elm/parameter-ref "month"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month")) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month"))) (is (false? (core/-static expr))))) @@ -437,9 +479,8 @@ #elm/parameter-ref "day"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day")) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day"))) (is (false? (core/-static expr))))) @@ -455,7 +496,7 @@ #elm/integer "8"] expr (c/compile compile-ctx elm)] - (is (= '(date-time 1 2 3 4 5 6 7 8) (core/-form expr))) + (has-form expr '(date-time 1 2 3 4 5 6 7 8)) (is (false? (core/-static expr))))) @@ -470,11 +511,10 @@ #elm/integer "1"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") - (param-ref "minute") (param-ref "second") - (param-ref "millisecond") 1) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + (param-ref "minute") (param-ref "second") + (param-ref "millisecond") 1)) (is (false? (core/-static expr))))) @@ -489,12 +529,11 @@ #elm/parameter-ref "timezone-offset"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") - (param-ref "minute") (param-ref "second") - (param-ref "millisecond") - (param-ref "timezone-offset")) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + (param-ref "minute") (param-ref "second") + (param-ref "millisecond") + (param-ref "timezone-offset"))) (is (false? (core/-static expr)))))) @@ -506,9 +545,9 @@ #elm/parameter-ref "hour"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") 0 0 0) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + 0 0 0)) (is (false? (core/-static expr))))) @@ -520,10 +559,9 @@ #elm/parameter-ref "minute"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") - (param-ref "minute") 0 0) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + (param-ref "minute") 0 0)) (is (false? (core/-static expr))))) @@ -536,10 +574,9 @@ #elm/parameter-ref "second"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") - (param-ref "minute") (param-ref "second") 0) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + (param-ref "minute") (param-ref "second") 0)) (is (false? (core/-static expr))))) @@ -553,13 +590,101 @@ #elm/parameter-ref "millisecond"] expr (c/compile compile-ctx elm)] - (is (= '(date-time (param-ref "year") (param-ref "month") - (param-ref "day") (param-ref "hour") - (param-ref "minute") (param-ref "second") - (param-ref "millisecond")) - (core/-form expr))) + (has-form expr '(date-time (param-ref "year") (param-ref "month") + (param-ref "day") (param-ref "hour") + (param-ref "minute") (param-ref "second") + (param-ref "millisecond"))) - (is (false? (core/-static expr))))))))) + (is (false? (core/-static expr)))))))) + + (testing "resolve parameters" + (let [compile-ctx {:library + {:parameters + {:def + [{:name "year"} + {:name "month"} + {:name "day"} + {:name "hour"} + {:name "minute"} + {:name "second"} + {:name "millisecond"} + {:name "timezone-offset"} + {:name "x"}]}}}] + + (testing "year" + (testing "with parameter-ref in expression with unresolved parameter-ref" + (let [elm #elm/date-time [#elm/add [#elm/parameter-ref "year" #elm/parameter-ref "x"]] + expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date-time (add 2024 (param-ref "x"))))))) + + (testing "year-month" + (let [elm #elm/date-time [#elm/parameter-ref "year" #elm/parameter-ref "month"]] + + (testing "with only the year parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date-time 2024 (param-ref "month"))))) + + (testing "with only the month parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"month" 6})] + (has-form expr '(date-time (param-ref "year") 6)))))) + + (testing "date" + (let [elm #elm/date-time [#elm/parameter-ref "year" + #elm/parameter-ref "month" + #elm/parameter-ref "day"]] + + (testing "with only the year parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date-time 2024 (param-ref "month") (param-ref "day"))))) + + (testing "with only the month parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"month" 6})] + (has-form expr '(date-time (param-ref "year") 6 (param-ref "day"))))) + + (testing "with only the day parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"day" 15})] + (has-form expr '(date-time (param-ref "year") (param-ref "month") 15)))))) + + (testing "with timezone offset" + (let [elm #elm/date-time [#elm/parameter-ref "year" + #elm/parameter-ref "month" + #elm/parameter-ref "day" + #elm/parameter-ref "hour" + #elm/parameter-ref "minute" + #elm/parameter-ref "second" + #elm/parameter-ref "millisecond" + #elm/parameter-ref "timezone-offset"]] + + (testing "with all parameter-refs resolved" + (let [params {"year" 2024 "month" 6 "day" 15 "hour" 16 + "minute" 50 "second" 23 "millisecond" 42 + "timezone-offset" 1.5M} + expr (c/resolve-params (c/compile compile-ctx elm) params)] + (has-form expr '(date-time 2024 6 15 16 50 23 42 1.5M)))) + + (testing "with only the year parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"year" 2024})] + (has-form expr '(date-time 2024 (param-ref "month") (param-ref "day") + (param-ref "hour") (param-ref "minute") + (param-ref "second") (param-ref "millisecond") + (param-ref "timezone-offset"))))) + + (testing "with only the month parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"month" 6})] + (has-form expr '(date-time (param-ref "year") 6 (param-ref "day") + (param-ref "hour") (param-ref "minute") + (param-ref "second") (param-ref "millisecond") + (param-ref "timezone-offset"))))) + + (testing "with only the day parameter-ref resolved" + (let [expr (c/resolve-params (c/compile compile-ctx elm) {"day" 15})] + (has-form expr '(date-time (param-ref "year") (param-ref "month") 15 + (param-ref "hour") (param-ref "minute") + (param-ref "second") (param-ref "millisecond") + (param-ref "timezone-offset"))))))))) + + (ctu/testing-binary-op elm/date-time) + (ctu/testing-ternary-op elm/date-time)) ;; 18.9. DateTimeComponentFrom ;; @@ -590,11 +715,8 @@ (are [x precision res] (= res (eval (compile elm/date-time x precision))) "2019-04-17T12:48" "Hour" 12)) - (ctu/testing-unary-precision-dynamic elm/date-time-component-from "Year" "Month" - "Day" "Hour" "Minute" "Second" "Millisecond") - - (ctu/testing-unary-precision-form elm/date-time-component-from "Year" "Month" - "Day" "Hour" "Minute" "Second" "Millisecond")) + (ctu/testing-unary-precision-op elm/date-time-component-from "Year" "Month" + "Day" "Hour" "Minute" "Second" "Millisecond")) ;; 18.10. DifferenceBetween ;; @@ -662,9 +784,7 @@ "2018-01" "2018-01" "Day" "2018-01-01" "2018-01-01" "Hour")))) - (ctu/testing-binary-precision-dynamic elm/difference-between "Year" "Month" "Day") - - (ctu/testing-binary-precision-form elm/difference-between "Year" "Month" "Day")) + (ctu/testing-binary-precision-only-op elm/difference-between "Year" "Month" "Day")) ;; 18.11. DurationBetween ;; @@ -731,9 +851,7 @@ "2018-01" "2018-01" "Day" "2018-01-01" "2018-01-01" "Hour")))) - (ctu/testing-binary-precision-dynamic elm/duration-between "Year" "Month" "Day") - - (ctu/testing-binary-precision-form elm/duration-between "Year" "Month" "Day")) + (ctu/testing-binary-precision-only-op elm/duration-between "Year" "Month" "Day")) ;; 18.12. Not Equal ;; @@ -802,9 +920,9 @@ "2019-04-17" "2019-04-17" true? "2019-04-17" "2019-04-18" false?) - (ctu/testing-binary-null elm/same-as #elm/date "2019") - (ctu/testing-binary-null elm/same-as #elm/date "2019-04") - (ctu/testing-binary-null elm/same-as #elm/date "2019-04-17") + (ctu/testing-binary-null elm/same-as #elm/date"2019") + (ctu/testing-binary-null elm/same-as #elm/date"2019-04") + (ctu/testing-binary-null elm/same-as #elm/date"2019-04-17") (testing "with year precision" (are [x y pred] (pred (ctu/compile-binop-precision elm/same-as elm/date x y "year")) @@ -837,13 +955,7 @@ "2019-04-17" "2019-04-17" true? "2019-04-17" "2019-04-18" true?))) - (ctu/testing-binary-dynamic elm/same-as) - - (ctu/testing-binary-precision-dynamic elm/same-as) - - (ctu/testing-binary-form elm/same-as) - - (ctu/testing-binary-precision-form elm/same-as)) + (ctu/testing-binary-precision-op elm/same-as)) ;; 18.15. SameOrBefore ;; @@ -906,9 +1018,9 @@ "2019-04-17" "2019-04-17" true? "2019-04-17" "2019-04-16" false?) - (ctu/testing-binary-null elm/same-or-before #elm/date "2019") - (ctu/testing-binary-null elm/same-or-before #elm/date "2019-04") - (ctu/testing-binary-null elm/same-or-before #elm/date "2019-04-17") + (ctu/testing-binary-null elm/same-or-before #elm/date"2019") + (ctu/testing-binary-null elm/same-or-before #elm/date"2019-04") + (ctu/testing-binary-null elm/same-or-before #elm/date"2019-04-17") (testing "with year precision" (are [x y pred] (pred (ctu/compile-binop-precision elm/same-or-before elm/date x y "year")) @@ -944,13 +1056,7 @@ "2019-04" "2019-04" true? "2019-04" "2019-03" true?))) - (ctu/testing-binary-dynamic elm/same-or-before) - - (ctu/testing-binary-precision-dynamic elm/same-or-before) - - (ctu/testing-binary-form elm/same-or-before) - - (ctu/testing-binary-precision-form elm/same-or-before)) + (ctu/testing-binary-precision-op elm/same-or-before)) ;; 18.15. SameOrAfter ;; @@ -1013,9 +1119,9 @@ "2019-04-17" "2019-04-17" true? "2019-04-17" "2019-04-18" false?) - (ctu/testing-binary-null elm/same-or-after #elm/date "2019") - (ctu/testing-binary-null elm/same-or-after #elm/date "2019-04") - (ctu/testing-binary-null elm/same-or-after #elm/date "2019-04-17") + (ctu/testing-binary-null elm/same-or-after #elm/date"2019") + (ctu/testing-binary-null elm/same-or-after #elm/date"2019-04") + (ctu/testing-binary-null elm/same-or-after #elm/date"2019-04-17") (testing "with year precision" (are [x y pred] (pred (ctu/compile-binop-precision elm/same-or-after elm/date x y "year")) @@ -1051,13 +1157,7 @@ "2019-04" "2019-04" true? "2019-04" "2019-05" true?))) - (ctu/testing-binary-dynamic elm/same-or-after) - - (ctu/testing-binary-precision-dynamic elm/same-or-after) - - (ctu/testing-binary-form elm/same-or-after) - - (ctu/testing-binary-precision-form elm/same-or-after)) + (ctu/testing-binary-precision-op elm/same-or-after)) ;; 18.18. Time ;; @@ -1144,7 +1244,7 @@ (let [elm #elm/time [#elm/parameter-ref "hour"] expr (c/compile compile-ctx elm)] - (is (= '(time (param-ref "hour")) (core/-form expr))) + (has-form expr '(time (param-ref "hour"))) (is (false? (core/-static expr))))) @@ -1153,8 +1253,7 @@ #elm/parameter-ref "minute"] expr (c/compile compile-ctx elm)] - (is (= '(time (param-ref "hour") (param-ref "minute")) - (core/-form expr))) + (has-form expr '(time (param-ref "hour") (param-ref "minute"))) (is (false? (core/-static expr))))) @@ -1164,9 +1263,8 @@ #elm/parameter-ref "second"] expr (c/compile compile-ctx elm)] - (is (= '(time (param-ref "hour") (param-ref "minute") - (param-ref "second")) - (core/-form expr))) + (has-form expr '(time (param-ref "hour") (param-ref "minute") + (param-ref "second"))) (is (false? (core/-static expr))))) @@ -1177,9 +1275,8 @@ #elm/parameter-ref "millisecond"] expr (c/compile compile-ctx elm)] - (is (= '(time (param-ref "hour") (param-ref "minute") - (param-ref "second") (param-ref "millisecond")) - (core/-form expr))) + (has-form expr '(time (param-ref "hour") (param-ref "minute") + (param-ref "second") (param-ref "millisecond"))) (is (false? (core/-static expr)))))))) diff --git a/modules/cql/test/blaze/elm/compiler/external_data_test.clj b/modules/cql/test/blaze/elm/compiler/external_data_test.clj index 9898f16ea..e33b488fa 100644 --- a/modules/cql/test/blaze/elm/compiler/external_data_test.clj +++ b/modules/cql/test/blaze/elm/compiler/external_data_test.clj @@ -10,13 +10,11 @@ [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.external-data :as ed] - [blaze.elm.compiler.external-data-spec] + [blaze.elm.compiler.external-data] [blaze.elm.compiler.library :as library] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.expression :as expr] [blaze.elm.expression-spec] - [blaze.elm.expression.cache :as-alias expr-cache] [blaze.elm.util-spec] [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type] @@ -43,16 +41,6 @@ (defn- eval-context [db] {:db db :now (OffsetDateTime/now)}) -(defn- resource [db type id] - (ed/mk-resource db (d/resource-handle db type id))) - -(deftest resource-test - (testing "toString" - (with-system-data [{:blaze.db/keys [node]} mem-node-config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - - (is (= "Patient[id = 0, t = 1]" (str (ctu/resource (d/db node) "Patient" "0"))))))) - ;; 11.1. Retrieve ;; ;; All access to external data within ELM is represented by Retrieve expressions. @@ -99,6 +87,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) '(retrieve-resource))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) '(retrieve-resource))) + (testing "form" (has-form expr '(retrieve-resource)))))) @@ -124,6 +124,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) '(retrieve "Observation"))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) '(retrieve "Observation"))) + (testing "form" (has-form expr '(retrieve "Observation"))))) @@ -166,6 +178,20 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve "Observation" [["code" "system-192253|code-192300"]]))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve "Observation" [["code" "system-192253|code-192300"]]))) + (testing "form" (has-form expr '(retrieve "Observation" [["code" "system-192253|code-192300"]])))))) @@ -220,6 +246,28 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve + "Observation" + [["code" + "system-192253|code-192300" + "system-192253|code-140541"]]))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve + "Observation" + [["code" + "system-192253|code-192300" + "system-192253|code-140541"]]))) + (testing "form" (has-form expr '(retrieve @@ -281,6 +329,28 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve + "Observation" + [["code" + "system-192253|code-192300" + "system-192253|code-140541"]]))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve + "Observation" + [["code" + "system-192253|code-192300" + "system-192253|code-140541"]]))) + (testing "form" (has-form expr '(retrieve @@ -313,6 +383,20 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve (Specimen) "Patient"))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve (Specimen) "Patient"))) + (testing "form" (has-form expr '(retrieve (Specimen) "Patient"))))))) @@ -352,6 +436,20 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve "Medication" [["code" "system-225806|code-225809"]]))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve "Medication" [["code" "system-225806|code-225809"]]))) + (testing "form" (has-form expr '(retrieve "Medication" [["code" "system-225806|code-225809"]])))))) @@ -396,9 +494,7 @@ define InInitialPopulation: [\"name-133756\" -> Observation] ") - compile-context {::expr-cache/enabled? false} - {:keys [expression-defs]} (library/compile-library - node library compile-context) + {:keys [expression-defs]} (library/compile-library node library {}) db (d/db node) patient (ctu/resource db "Patient" "0") eval-context (assoc (eval-context db) :expression-defs expression-defs) @@ -413,8 +509,23 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve (singleton-from (retrieve-resource)) "Observation"))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve (singleton-from (retrieve-resource)) "Observation"))) + (testing "form" - (has-form expr '(retrieve (expr-ref "name-133756") "Observation")))))) + (has-form expr + '(retrieve (singleton-from (retrieve-resource)) "Observation")))))) (testing "with pre-compiled database query" (with-system-data [{:blaze.db/keys [node]} mem-node-config] @@ -443,9 +554,7 @@ define InInitialPopulation: [\"name-133730\" -> Observation: Code 'code-133657' from sys] ") - compile-context {::expr-cache/enabled? false} - {:keys [expression-defs]} (library/compile-library - node library compile-context) + {:keys [expression-defs]} (library/compile-library node library {}) db (d/db node) patient (ctu/resource db "Patient" "0") eval-context (assoc (eval-context db) :expression-defs expression-defs) @@ -460,9 +569,25 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) + '(retrieve (singleton-from (retrieve-resource)) "Observation" + [["code" "system-133620|code-133657"]]))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) + '(retrieve (singleton-from (retrieve-resource)) "Observation" + [["code" "system-133620|code-133657"]]))) + (testing "form" (has-form expr - '(retrieve (expr-ref "name-133730") "Observation" + '(retrieve (singleton-from (retrieve-resource)) "Observation" [["code" "system-133620|code-133657"]])))))) (testing "unknown code property" diff --git a/modules/cql/test/blaze/elm/compiler/interval_operators_test.clj b/modules/cql/test/blaze/elm/compiler/interval_operators_test.clj index 41e81c40b..5152577b3 100644 --- a/modules/cql/test/blaze/elm/compiler/interval_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/interval_operators_test.clj @@ -68,20 +68,36 @@ (testing "Static" (testing "Integer" (are [s e res] (= res (ctu/compile-binop elm/interval elm/integer s e)) - "1" "2" (interval 1 2))) + "1" "2" (interval 1 2)) + + (testing "form" + (has-form (interval 1 2) '(interval 1 2)))) (testing "Decimal" (are [s e res] (= res (ctu/compile-binop elm/interval elm/decimal s e)) - "1" "2" (interval 1M 2M))) + "1" "2" (interval 1M 2M)) + + (testing "form" + (has-form (interval 1M 2M) '(interval 1M 2M)))) (testing "Date" (are [s e res] (= res (ctu/compile-binop elm/interval elm/date s e)) - "2020" "2021" (interval #system/date"2020" #system/date"2021"))) + "2020" "2021" (interval #system/date"2020" #system/date"2021")) + + (testing "form" + (has-form + (interval #system/date"2020" #system/date"2021") + '(interval #system/date"2020" #system/date"2021")))) (testing "DateTime" (are [s e res] (= res (ctu/compile-binop elm/interval elm/date-time s e)) "2020" "2021" (interval #system/date-time"2020" #system/date-time"2021")) + (testing "form" + (has-form + (interval #system/date-time"2020" #system/date-time"2021") + '(interval #system/date-time"2020" #system/date-time"2021"))) + (testing "with ToDateTime" (are [s e res] (= res (c/compile {} (elm/interval [(elm/to-date-time (elm/date s)) (elm/date-time e)]))) "2020" "2021" (interval #system/date-time"2020" #system/date-time"2021")))) @@ -113,6 +129,10 @@ (are [elm] (thrown? Exception (core/-eval (c/compile {} elm) {} nil nil)) #elm/interval [#elm/integer "5" #elm/integer "3"])) + (testing "attach-cache" + (let [interval (interval 1 1)] + (is (= [interval] (st/with-instrument-disabled (c/attach-cache interval ::cache)))))) + (testing "form" (let [elm# (elm/interval [(elm/as ["{urn:hl7-org:elm-types:r1}Integer" #elm/parameter-ref "x"]) (elm/as ["{urn:hl7-org:elm-types:r1}Integer" #elm/parameter-ref "y"])]) @@ -211,9 +231,9 @@ "2019-04-17" "2019-04-17" false? "2019-04-17" "2019-04-18" false?) - (ctu/testing-binary-null elm/after #elm/date "2019") - (ctu/testing-binary-null elm/after #elm/date "2019-04") - (ctu/testing-binary-null elm/after #elm/date "2019-04-17") + (ctu/testing-binary-null elm/after #elm/date"2019") + (ctu/testing-binary-null elm/after #elm/date"2019-04") + (ctu/testing-binary-null elm/after #elm/date"2019-04-17") (testing "with year precision" (are [x y pred] (pred (ctu/compile-binop-precision elm/after elm/date x y "year")) @@ -254,15 +274,9 @@ "2019-04" "2019-05" false? "2019-04-17" "2019-04-16" false? "2019-04-17" "2019-04-17" false? - "2019-04-17" "2019-04-18" false?))) + "2019-04-17" "2019-04-18" false?)) - (ctu/testing-binary-dynamic elm/after) - - (ctu/testing-binary-precision-dynamic elm/after) - - (ctu/testing-binary-form elm/after) - - (ctu/testing-binary-precision-form elm/after)) + (ctu/testing-binary-precision-op elm/after))) ;; 19.3. Before ;; @@ -352,9 +366,9 @@ "2019-04-17" "2019-04-17" false? "2019-04-17" "2019-04-16" false?) - (ctu/testing-binary-null elm/before #elm/date "2019") - (ctu/testing-binary-null elm/before #elm/date "2019-04") - (ctu/testing-binary-null elm/before #elm/date "2019-04-17") + (ctu/testing-binary-null elm/before #elm/date"2019") + (ctu/testing-binary-null elm/before #elm/date"2019-04") + (ctu/testing-binary-null elm/before #elm/date"2019-04-17") (testing "with year precision" (are [x y pred] (pred (ctu/compile-binop-precision elm/before elm/date x y @@ -398,13 +412,7 @@ "2019-04-17" "2019-04-17" false? "2019-04-17" "2019-04-16" false?))) - (ctu/testing-binary-dynamic elm/before) - - (ctu/testing-binary-precision-dynamic elm/before) - - (ctu/testing-binary-form elm/before) - - (ctu/testing-binary-precision-form elm/before)) + (ctu/testing-binary-precision-op elm/before)) ;; 19.4. Collapse ;; @@ -461,9 +469,7 @@ {:type "Null"} [(interval #system/date-time"2012-01-01" #system/date-time"2012-05-25")])) - (ctu/testing-binary-dynamic elm/collapse) - - (ctu/testing-binary-form elm/collapse)) + (ctu/testing-binary-op elm/collapse)) ;; 19.5. Contains ;; @@ -530,13 +536,7 @@ #elm/list [] {:type "Null"} nil?)) - (ctu/testing-binary-dynamic elm/contains) - - (ctu/testing-binary-precision-dynamic elm/contains) - - (ctu/testing-binary-form elm/contains) - - (ctu/testing-binary-precision-form elm/contains)) + (ctu/testing-binary-precision-op elm/contains)) ;; 19.6. End ;; @@ -573,9 +573,7 @@ (ctu/testing-unary-null elm/end) - (ctu/testing-unary-dynamic elm/end) - - (ctu/testing-unary-form elm/end)) + (ctu/testing-unary-op elm/end)) ;; 19.7. Ends ;; @@ -608,13 +606,7 @@ (ctu/testing-binary-null elm/ends interval-zero) - (ctu/testing-binary-dynamic elm/ends) - - (ctu/testing-binary-precision-dynamic elm/ends) - - (ctu/testing-binary-form elm/ends) - - (ctu/testing-binary-precision-form elm/ends)) + (ctu/testing-binary-precision-op elm/ends)) ;; 19.8. Equal ;; @@ -663,9 +655,7 @@ (ctu/testing-binary-null elm/except interval-zero)) - (ctu/testing-binary-dynamic elm/except) - - (ctu/testing-binary-form elm/except)) + (ctu/testing-binary-op elm/except)) ;; 19.11. Expand ;; @@ -755,13 +745,7 @@ (ctu/testing-binary-null elm/includes interval-zero)) - (ctu/testing-binary-dynamic elm/includes) - - (ctu/testing-binary-precision-dynamic elm/includes) - - (ctu/testing-binary-form elm/includes) - - (ctu/testing-binary-precision-form elm/includes)) + (ctu/testing-binary-precision-op elm/includes)) ;; 19.14. IncludedIn ;; @@ -824,9 +808,7 @@ (ctu/testing-binary-null elm/intersect interval-zero)) - (ctu/testing-binary-dynamic elm/intersect) - - (ctu/testing-binary-form elm/intersect)) + (ctu/testing-binary-op elm/intersect)) ;; 19.16. Meets ;; @@ -857,13 +839,7 @@ (ctu/testing-binary-null elm/meets-before interval-zero) - (ctu/testing-binary-dynamic elm/meets-before) - - (ctu/testing-binary-precision-dynamic elm/meets-before) - - (ctu/testing-binary-form elm/meets-before) - - (ctu/testing-binary-precision-form elm/meets-before)) + (ctu/testing-binary-precision-op elm/meets-before)) ;; 19.18. MeetsAfter ;; @@ -887,13 +863,7 @@ (ctu/testing-binary-null elm/meets-after interval-zero) - (ctu/testing-binary-dynamic elm/meets-after) - - (ctu/testing-binary-precision-dynamic elm/meets-after) - - (ctu/testing-binary-form elm/meets-after) - - (ctu/testing-binary-precision-form elm/meets-after)) + (ctu/testing-binary-precision-op elm/meets-after)) ;; 19.20. Overlaps ;; @@ -947,13 +917,7 @@ (ctu/testing-binary-null elm/overlaps interval-zero) - (ctu/testing-binary-dynamic elm/overlaps) - - (ctu/testing-binary-precision-dynamic elm/overlaps) - - (ctu/testing-binary-form elm/overlaps) - - (ctu/testing-binary-precision-form elm/overlaps)) + (ctu/testing-binary-precision-op elm/overlaps)) ;; 19.21. OverlapsBefore ;; @@ -1004,9 +968,7 @@ (ctu/testing-unary-null elm/point-from) - (ctu/testing-unary-dynamic elm/point-from) - - (ctu/testing-unary-form elm/point-from)) + (ctu/testing-unary-op elm/point-from)) ;; 19.24. ProperContains ;; @@ -1047,15 +1009,11 @@ #elm/list [#elm/integer "1"] #elm/integer "2" false? #elm/list [#elm/integer "1" #elm/integer "2"] #elm/integer "1" true? - #elm/list [#elm/integer "1" #elm/integer "2"] #elm/integer "2" true?))) + #elm/list [#elm/integer "1" #elm/integer "2"] #elm/integer "2" true?)) - (ctu/testing-binary-dynamic elm/proper-contains) + (ctu/testing-binary-null elm/proper-contains #elm/list [])) - (ctu/testing-binary-precision-dynamic elm/proper-contains) - - (ctu/testing-binary-form elm/proper-contains) - - (ctu/testing-binary-precision-form elm/proper-contains)) + (ctu/testing-binary-precision-op elm/proper-contains)) ;; 19.25. ProperIn ;; @@ -1094,13 +1052,7 @@ (ctu/testing-binary-null elm/proper-includes interval-zero)) - (ctu/testing-binary-dynamic elm/proper-includes) - - (ctu/testing-binary-precision-dynamic elm/proper-includes) - - (ctu/testing-binary-form elm/proper-includes) - - (ctu/testing-binary-precision-form elm/proper-includes)) + (ctu/testing-binary-precision-op elm/proper-includes)) ;; 19.27. ProperIncludedIn ;; @@ -1151,9 +1103,7 @@ (ctu/testing-unary-null elm/start) - (ctu/testing-unary-dynamic elm/start) - - (ctu/testing-unary-form elm/start)) + (ctu/testing-unary-op elm/start)) ;; 19.30. Starts ;; @@ -1179,13 +1129,7 @@ (ctu/testing-binary-null elm/starts interval-zero) - (ctu/testing-binary-dynamic elm/starts) - - (ctu/testing-binary-precision-dynamic elm/starts) - - (ctu/testing-binary-form elm/starts) - - (ctu/testing-binary-precision-form elm/starts)) + (ctu/testing-binary-precision-op elm/starts)) ;; 19.31. Union ;; @@ -1220,9 +1164,7 @@ (ctu/testing-binary-null elm/union interval-zero)) - (ctu/testing-binary-dynamic elm/union) - - (ctu/testing-binary-form elm/union)) + (ctu/testing-binary-op elm/union)) ;; 19.32. Width ;; @@ -1240,6 +1182,4 @@ (ctu/testing-unary-null elm/width) - (ctu/testing-unary-dynamic elm/width) - - (ctu/testing-unary-form elm/width)) + (ctu/testing-unary-op elm/width)) diff --git a/modules/cql/test/blaze/elm/compiler/library/resolve_refs_test.clj b/modules/cql/test/blaze/elm/compiler/library/resolve_refs_test.clj new file mode 100644 index 000000000..dd3362f03 --- /dev/null +++ b/modules/cql/test/blaze/elm/compiler/library/resolve_refs_test.clj @@ -0,0 +1,27 @@ +(ns blaze.elm.compiler.library.resolve-refs-test + (:require + [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.library-spec] + [blaze.elm.compiler.library.resolve-refs :refer [resolve-refs]] + [blaze.elm.compiler.macros :refer [reify-expr]] + [blaze.fhir.spec.type.system] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest testing]] + [cognitect.anomalies :as anom] + [juxt.iota :refer [given]])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(def ^:private expr-a-ref-b + (reify-expr core/Expression + (-form [_] + (list 'a (list 'expr-ref "b"))))) + +(deftest resolve-refs-test + (testing "with one unresolvable ref" + (given (resolve-refs #{} {"a" {:expression expr-a-ref-b}}) + ::anom/category := ::anom/incorrect + ::anom/message := "The following expression definitions contain unresolvable references: a."))) diff --git a/modules/cql/test/blaze/elm/compiler/library_test.clj b/modules/cql/test/blaze/elm/compiler/library_test.clj index a564b6e48..b16971a93 100644 --- a/modules/cql/test/blaze/elm/compiler/library_test.clj +++ b/modules/cql/test/blaze/elm/compiler/library_test.clj @@ -3,8 +3,10 @@ [blaze.cql-translator :as t] [blaze.db.api-stub :refer [mem-node-config]] [blaze.elm.compiler :as c] + [blaze.elm.compiler-spec] [blaze.elm.compiler.library :as library] [blaze.elm.compiler.library-spec] + [blaze.elm.expression.cache :as ec] [blaze.fhir.spec.type.system] [blaze.module.test-util :refer [with-system]] [blaze.test-util :as tu] @@ -17,7 +19,9 @@ (test/use-fixtures :each tu/fixture) -(def default-opts {}) +(def ^:private default-opts {}) + +(def ^:private expr-form (comp c/form :expression)) ;; 5.1. Library ;; @@ -65,9 +69,10 @@ (testing "one static expression" (let [library (t/translate "library Test define Foo: true")] (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) - [:expression-defs "Foo" :context] := "Patient" - [:expression-defs "Foo" :expression] := true)))) + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["Foo" :context] := "Patient" + ["Foo" :expression] := true))))) (testing "one dynamic expression" (let [library (t/translate "library Test @@ -75,11 +80,10 @@ context Patient define Gender: Patient.gender")] (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) - [:expression-defs "Patient" :context] := "Patient" - [:expression-defs "Patient" :expression c/form] := '(singleton-from (retrieve-resource)) - [:expression-defs "Gender" :context] := "Patient" - [:expression-defs "Gender" :expression c/form] := '(:gender (expr-ref "Patient")))))) + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["Gender" :context] := "Patient" + ["Gender" expr-form] := '(:gender (singleton-from (retrieve-resource)))))))) (testing "one function" (let [library (t/translate "library Test @@ -88,13 +92,16 @@ define function Gender(P Patient): P.gender define InInitialPopulation: Gender(Patient)")] (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) - [:expression-defs "InInitialPopulation" :context] := "Patient" - [:expression-defs "InInitialPopulation" :resultTypeName] := "{http://hl7.org/fhir}AdministrativeGender" - [:expression-defs "InInitialPopulation" :expression c/form] := '(call "Gender" (expr-ref "Patient")) - [:function-defs "Gender" :context] := "Patient" - [:function-defs "Gender" :resultTypeName] := "{http://hl7.org/fhir}AdministrativeGender" - [:function-defs "Gender" :function] :? fn?)))) + (let [{:keys [expression-defs function-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" :resultTypeName] := "{http://hl7.org/fhir}AdministrativeGender" + ["InInitialPopulation" expr-form] := '(call "Gender" (singleton-from (retrieve-resource)))) + + (given function-defs + ["Gender" :context] := "Patient" + ["Gender" :resultTypeName] := "{http://hl7.org/fhir}AdministrativeGender" + ["Gender" :function] :? fn?))))) (testing "two functions, one calling the other" (let [library (t/translate "library Test @@ -104,31 +111,146 @@ define function Inc2(i System.Integer): Inc(i) + 1 define InInitialPopulation: Inc2(1)")] (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) - [:expression-defs "InInitialPopulation" :context] := "Patient" - [:expression-defs "InInitialPopulation" :expression c/form] := '(call "Inc2" 1))))) + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" expr-form] := '(call "Inc2" 1)))))) + + (testing "expressions from Patient context are resolved" + (let [library (t/translate "library Test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Female: + Patient.gender = 'female' + + define HasObservation: + exists [Observation] + + define HasCondition: + exists [Condition] + + define Inclusion: + Female and + HasObservation + + define Exclusion: + HasCondition + + define InInitialPopulation: + Inclusion and + not Exclusion")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" expr-form] := + '(and + (and + (equal + (call + "ToString" + (:gender + (singleton-from (retrieve-resource)))) + "female") + (exists + (retrieve + "Observation"))) + (not + (exists + (retrieve + "Condition"))))))))) (testing "expressions from Unfiltered context are not resolved" (let [library (t/translate "library Test - using FHIR version '4.0.0' - include FHIRHelpers version '4.0.0' + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' - codesystem atc: 'http://fhir.de/CodeSystem/dimdi/atc' + codesystem atc: 'http://fhir.de/CodeSystem/dimdi/atc' - context Unfiltered + context Unfiltered - define TemozolomidRefs: - [Medication: Code 'L01AX03' from atc] M return 'Medication/' + M.id + define TemozolomidRefs: + [Medication: Code 'L01AX03' from atc] M return 'Medication/' + M.id - context Patient + context Patient - define InInitialPopulation: - exists from [MedicationStatement] M - where M.medication.reference in TemozolomidRefs")] + define InInitialPopulation: + Patient.gender = 'female' and + exists from [MedicationStatement] M + where M.medication.reference in TemozolomidRefs")] (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) - [:expression-defs "InInitialPopulation" :context] := "Patient" - [:expression-defs "InInitialPopulation" :expression c/form] := '(exists (eduction-query (comp (filter (fn [M] (contains (expr-ref "TemozolomidRefs") (call "ToString" (:reference (:medication M)))))) distinct) (retrieve "MedicationStatement"))))))) + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["TemozolomidRefs" :context] := "Unfiltered" + ["TemozolomidRefs" expr-form] := + '(vector-query + (comp + (map + (fn [M] + (concatenate "Medication/" (call "ToString" (:id M))))) + distinct) + (retrieve + "Medication" + [["code" + "http://fhir.de/CodeSystem/dimdi/atc|L01AX03"]])) + + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" expr-form] := + '(and + (equal + (call + "ToString" + (:gender + (singleton-from + (retrieve-resource)))) + "female") + (exists + (eduction-query + (comp + (filter + (fn + [M] + (contains + (expr-ref + "TemozolomidRefs") + (call + "ToString" + (:reference + (:medication + M)))))) + distinct) + (retrieve + "MedicationStatement"))))))))) + + (testing "expressions without refs are preserved" + (let [library (t/translate "library Retrieve + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define InInitialPopulation: + true + + define AllEncounters: + [Encounter] + + define Gender: + Patient.gender")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (given expression-defs + ["Patient" :context] := "Patient" + ["Patient" expr-form] := '(singleton-from (retrieve-resource)) + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" expr-form] := true + ["AllEncounters" :context] := "Patient" + ["AllEncounters" expr-form] := '(retrieve "Encounter") + ["Gender" :context] := "Patient" + ["Gender" expr-form] := '(:gender (singleton-from (retrieve-resource)))))))) (testing "with compile-time error" (testing "function" @@ -177,7 +299,7 @@ (with-system [{:blaze.db/keys [node]} mem-node-config] (given (library/compile-library node library {}) [:expression-defs "InInitialPopulation" :context] := "Patient" - [:expression-defs "InInitialPopulation" :expression c/form] := + [:expression-defs "InInitialPopulation" expr-form] := '(exists (eduction-query (comp @@ -204,4 +326,280 @@ distinct) (retrieve "Observation"))) - [:function-defs "hasDiagnosis" :function] := nil))))) + [:function-defs "hasDiagnosis" :function] := nil)))) + + (testing "with related context" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define \"name-133756\": + singleton from ([Patient]) + + define InInitialPopulation: + [\"name-133756\" -> Observation]")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (given (library/compile-library node library {}) + [:expression-defs "InInitialPopulation" :context] := "Patient" + [:expression-defs "InInitialPopulation" expr-form] := + '(retrieve + (singleton-from + (retrieve-resource)) + "Observation"))))) + + (testing "and expression" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define InInitialPopulation: + exists [Observation] and + exists [Condition] and + exists [Encounter] and + exists [Specimen]")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (given (library/compile-library node library {}) + [:expression-defs "InInitialPopulation" :context] := "Patient" + [:expression-defs "InInitialPopulation" expr-form] := + '(and + (and + (and + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (exists + (retrieve + "Encounter"))) + (exists + (retrieve + "Specimen"))))))) + + (testing "and expression with named expressions" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Criterion_1: + exists [Observation] and + exists [Condition] + + define Criterion_2: + exists [Encounter] and + exists [Specimen] + + define InInitialPopulation: + Criterion_1 and + Criterion_2")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + in-initial-population (get expression-defs "InInitialPopulation")] + + (testing "after compilation the named expressions are resolved + the structure however is retained + so the and-expressions are still binary" + (given in-initial-population + :context := "Patient" + expr-form := + '(and + (and + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (and + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen")))))) + + (testing "after attaching the cache we get one single, flat and-expression" + (with-redefs [ec/get (fn [_ _])] + (let [{:keys [expression]} in-initial-population] + (given (st/with-instrument-disabled (c/attach-cache expression ::cache)) + [0 c/form] := + '(and + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition")) + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen"))))))))))) + + (testing "or expression" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define InInitialPopulation: + exists [Observation] or + exists [Condition] or + exists [Encounter] or + exists [Specimen]")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (given (library/compile-library node library {}) + [:expression-defs "InInitialPopulation" :context] := "Patient" + [:expression-defs "InInitialPopulation" expr-form] := + '(or + (or + (or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (exists + (retrieve + "Encounter"))) + (exists + (retrieve + "Specimen"))))))) + + (testing "or expression with named expressions" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Criterion_1: + exists [Observation] or + exists [Condition] + + define Criterion_2: + exists [Encounter] or + exists [Specimen] + + define InInitialPopulation: + Criterion_1 or + Criterion_2")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + in-initial-population (get expression-defs "InInitialPopulation")] + + (testing "after compilation the named expressions are resolved + the structure however is retained + so the or-expressions are still binary" + (given in-initial-population + :context := "Patient" + expr-form := + '(or + (or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (or + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen")))))) + + (testing "after attaching the cache we get one single, flat or-expression" + (with-redefs [ec/get (fn [_ _])] + (let [{:keys [expression]} in-initial-population] + (given (st/with-instrument-disabled (c/attach-cache expression ::cache)) + [0 c/form] := + '(or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition")) + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen"))))))))))) + + (testing "mixed and and or expressions" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Criterion_1: + exists [Observation] or + exists [Condition] + + define Criterion_2: + exists [Encounter] or + exists [Specimen] + + define InInitialPopulation: + Criterion_1 and + Criterion_2")] + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + in-initial-population (get expression-defs "InInitialPopulation")] + + (testing "after compilation the named expressions are resolved + the structure however is retained + so the or-expressions are still binary" + (given in-initial-population + :context := "Patient" + expr-form := + '(and + (or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (or + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen")))))) + + (testing "after attaching the cache the expressions don't change" + (with-redefs [ec/get (fn [_ _])] + (let [{:keys [expression]} in-initial-population] + (given (st/with-instrument-disabled (c/attach-cache expression ::cache)) + [0 c/form] := + '(and + (or + (exists + (retrieve + "Observation")) + (exists + (retrieve + "Condition"))) + (or + (exists + (retrieve + "Encounter")) + (exists + (retrieve + "Specimen"))))))))))))) diff --git a/modules/cql/test/blaze/elm/compiler/list_operators_test.clj b/modules/cql/test/blaze/elm/compiler/list_operators_test.clj index 4f3c7b306..eb5bc6b47 100644 --- a/modules/cql/test/blaze/elm/compiler/list_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/list_operators_test.clj @@ -4,23 +4,39 @@ Section numbers are according to https://cql.hl7.org/04-logicalspecification.html." (:require + [blaze.anomaly :as ba] [blaze.anomaly-spec] + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.elm.compiler :as c] [blaze.elm.compiler-spec] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.list-operators] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] + [blaze.elm.expression :as expr] [blaze.elm.expression-spec] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache-spec] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] - [blaze.elm.quantity :as quantity] - [blaze.test-util :refer [satisfies-prop]] + [blaze.elm.quantity :refer [quantity]] + [blaze.elm.resource :as cr] + [blaze.elm.spec :as elm-spec] + [blaze.fhir.test-util] + [blaze.module.test-util :refer [with-system]] + [blaze.test-util :refer [given-thrown satisfies-prop]] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [are deftest is testing]] - [clojure.test.check.properties :as prop])) + [clojure.test.check.properties :as prop] + [integrant.core :as ig] + [juxt.iota :refer [given]]) + (:import + [java.time OffsetDateTime])) +(set! *warn-on-reflection* true) (st/instrument) (ctu/instrument-compile) @@ -64,9 +80,53 @@ #elm/list [#elm/parameter-ref "1" #elm/parameter-ref "nil"] [1 nil] #elm/list [#elm/parameter-ref "1" #elm/parameter-ref "2"] [1 2]) - (testing "form" - (let [expr (ctu/dynamic-compile #elm/list [#elm/parameter-ref "x"])] - (has-form expr '(list (param-ref "x"))))))) + (let [expr (ctu/dynamic-compile #elm/list [#elm/parameter-ref "x"])] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {"x" 1}) '(list 1))) + + (testing "form" + (has-form expr '(list (param-ref "x")))))) + + (testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (testing "with one element" + (let [elm (elm/list [#elm/exists #elm/retrieve{:type "Observation"}]) + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 1 + [1 0] := '(exists (retrieve "Observation"))))) + + (testing "with two elements" + (let [elm (elm/list [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}]) + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 2 + [1 0] := '(exists (retrieve "Observation")) + [1 1] := '(exists (retrieve "Condition"))))))) + + (testing "resolve expression references" + (let [elm #elm/list [#elm/expression-ref "x"] + expr-def {:type "ExpressionDef" :name "x" :expression 1 + :context "Patient"} + ctx {:library {:statements {:def [expr-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-def})] + (has-form expr '(list 1)))) + + (ctu/testing-equals-hash-code #elm/list [#elm/parameter-ref "1"])) ;; 20.2. Contains ;; @@ -86,9 +146,24 @@ (prop/for-all [x (s/gen int?)] (= x (core/-eval (c/compile {} #elm/current nil) {} nil x)))) - (testing "form" - (let [expr (c/compile {} #elm/current nil)] - (has-form expr 'current)))) + (let [expr (c/compile {} #elm/current nil)] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) 'current)) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) 'current)) + + (testing "form" + (has-form expr 'current))) + + (ctu/testing-equals-hash-code #elm/current nil)) (testing "named scope" (satisfies-prop 100 @@ -97,9 +172,24 @@ (let [expr (c/compile {} (elm/current scope))] (= x (core/-eval expr {} nil {scope x}))))) - (testing "form" - (let [expr (c/compile {} #elm/current "x")] - (has-form expr '(current "x")))))) + (let [expr (c/compile {} #elm/current "x")] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (has-form (c/resolve-refs expr {}) '(current "x"))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {}) '(current "x"))) + + (testing "form" + (has-form expr '(current "x")))) + + (ctu/testing-equals-hash-code #elm/current "x"))) ;; 20.4. Distinct ;; @@ -121,14 +211,12 @@ #elm/list [{:type "Null"}] [nil] #elm/list [{:type "Null"} {:type "Null"}] [nil nil] #elm/list [{:type "Null"} {:type "Null"} {:type "Null"}] [nil nil nil] - #elm/list [#elm/quantity [100 "cm"] #elm/quantity [1 "m"]] [(quantity/quantity 100 "cm")] - #elm/list [#elm/quantity [1 "m"] #elm/quantity [100 "cm"]] [(quantity/quantity 1 "m")]) + #elm/list [#elm/quantity [100 "cm"] #elm/quantity [1 "m"]] [(quantity 100 "cm")] + #elm/list [#elm/quantity [1 "m"] #elm/quantity [100 "cm"]] [(quantity 1 "m")]) (ctu/testing-unary-null elm/distinct) - (ctu/testing-unary-dynamic elm/distinct) - - (ctu/testing-unary-form elm/distinct)) + (ctu/testing-unary-op elm/distinct)) ;; 20.5. Equal ;; @@ -144,9 +232,29 @@ ;; 20.8. Exists ;; -;; The Exists operator returns true if the list contains any elements. +;; The Exists operator returns true if the list contains any non-null elements. ;; ;; If the argument is null, the result is false. +(def ^:private exists-config + (assoc mem-node-config + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {})) + +(defmethod elm-spec/expression :elm.spec.type/exists-test [_] + map?) + +(defmethod core/compile* :elm.compiler.type/exists-test + [_ _] + (reify-expr core/Expression + (-attach-cache [_ _] + [(reify-expr core/Expression + (-form [_] + 'exists-test-with-cache))]) + (-form [_] + 'exists-test))) + (deftest compile-exists-test (testing "Static" (are [list res] (= res (c/compile {} (elm/exists list))) @@ -166,7 +274,109 @@ {:type "Null"} false) - (ctu/testing-unary-form elm/exists))) + (testing "doesn't attach the cache downstream because there is no need for double caching" + (with-system [{::expr/keys [cache]} exists-config] + (let [elm #elm/exists {:type "ExistsTest"} + expr (c/compile {:eval-context "Patient"} elm) + ;; ensure Bloom filter is available + _ (do (c/attach-cache expr cache) (Thread/sleep 100)) + expr (first (c/attach-cache expr cache))] + + (has-form expr '(exists exists-test)) + + (testing "also not at a second cache attachment" + (has-form (first (c/attach-cache expr cache)) '(exists exists-test)))))) + + (testing "with caching expressions" + (with-system-data [{:blaze.db/keys [node] ::expr/keys [cache]} exists-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + + (let [db (d/db node) + patient (cr/mk-resource db (d/resource-handle db "Patient" "0")) + elm #elm/exists #elm/retrieve{:type "Observation"} + compile-context + {:node node + :eval-context "Patient" + :library {}} + expr (c/compile compile-context elm) + eval-context + {:db db + :now (OffsetDateTime/now)}] + + (testing "has no Observation at the beginning" + (let [[expr bloom-filters] (c/attach-cache expr cache)] + (is (every? ba/unavailable? bloom-filters)) + (is (false? (expr/eval eval-context expr patient))))) + + (Thread/sleep 100) + + (testing "has still no Observation after the Bloom filter is filled" + (let [[expr bloom-filters] (c/attach-cache expr cache)] + (given bloom-filters + count := 1 + [0 ::bloom-filter/expr-form] := (pr-str '(exists (retrieve "Observation"))) + [0 ::bloom-filter/patient-count] := 0) + (is (false? (expr/eval eval-context expr patient))))) + + (let [tx-op [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}] + db-after @(d/transact node [tx-op])] + + (testing "has an Observation after transaction" + (let [[expr bloom-filters] (c/attach-cache expr cache) + patient (cr/mk-resource db-after (d/resource-handle db "Patient" "0"))] + (testing "even so the bloom filter is still the old one" + (given bloom-filters + count := 1 + [0 ::bloom-filter/expr-form] := (pr-str '(exists (retrieve "Observation"))) + [0 ::bloom-filter/patient-count] := 0)) + (is (true? (expr/eval (assoc eval-context :db db-after) expr patient))))) + + (testing "has still no Observation at the old database" + (let [[expr] (c/attach-cache expr cache)] + (is (false? (expr/eval eval-context expr patient))))))))) + + (testing "without caching expressions" + (with-system-data [{:blaze.db/keys [node]} mem-node-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + + (let [db (d/db node) + patient (cr/mk-resource db (d/resource-handle db "Patient" "0")) + elm #elm/exists #elm/retrieve{:type "Observation"} + compile-context + {:node node + :eval-context "Patient" + :library {}} + expr (c/compile compile-context elm) + eval-context + {:db db + :now (OffsetDateTime/now)}] + + (testing "has no Observation at the beginning" + (is (false? (expr/eval eval-context expr patient)))) + + (let [tx-op [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}] + db-after @(d/transact node [tx-op])] + + (testing "has an Observation after transaction" + (let [patient (cr/mk-resource db-after (d/resource-handle db "Patient" "0"))] + (is (true? (expr/eval (assoc eval-context :db db-after) expr patient))))) + + (testing "has still no Observation at the old database" + (is (false? (expr/eval eval-context expr patient))))))))) + + (ctu/testing-unary-dynamic elm/exists) + + (ctu/testing-unary-patient-count elm/exists) + + (ctu/testing-unary-resolve-refs elm/exists) + + (ctu/testing-unary-resolve-params elm/exists) + + (ctu/testing-unary-equals-hash-code elm/exists) + + (ctu/testing-unary-form elm/exists)) ;; 20.9. Filter ;; @@ -196,25 +406,82 @@ {:type "Null"} #elm/boolean "true" nil)))) - (testing "form and static" - (testing "with scope" - (let [expr (ctu/dynamic-compile {:type "Filter" - :source #elm/parameter-ref "x" - :condition #elm/parameter-ref "y" - :scope "A"})] - - (has-form expr '(filter (param-ref "x") (param-ref "y") "A")) - - (is (false? (core/-static expr))))) - - (testing "without scope" - (let [expr (ctu/dynamic-compile {:type "Filter" - :source #elm/parameter-ref "x" - :condition #elm/parameter-ref "y"})] - - (has-form expr '(filter (param-ref "x") (param-ref "y"))) - - (is (false? (core/-static expr))))))) + (testing "with scope" + (let [expr (ctu/dynamic-compile {:type "Filter" + :source #elm/parameter-ref "x" + :condition #elm/parameter-ref "y" + :scope "A"})] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm {:type "Filter" + :source #elm/expression-ref "x" + :condition #elm/expression-ref "y" + :scope "A"} + source-def {:type "ExpressionDef" :name "x" :expression [1] + :context "Patient"} + condition-def {:type "ExpressionDef" :name "y" :expression true + :context "Patient"} + ctx {:library {:statements {:def [source-def condition-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" source-def "y" condition-def})] + (has-form expr '(filter [1] true "A")))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {"x" [1]}) + '(filter [1] (param-ref "y") "A"))) + + (testing "form" + (has-form expr '(filter (param-ref "x") (param-ref "y") "A"))) + + (ctu/testing-equals-hash-code {:type "Filter" + :source #elm/parameter-ref "x" + :condition #elm/parameter-ref "y" + :scope "A"}))) + + (testing "without scope" + (let [expr (ctu/dynamic-compile {:type "Filter" + :source #elm/parameter-ref "x" + :condition #elm/parameter-ref "y"})] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm {:type "Filter" + :source #elm/expression-ref "x" + :condition #elm/expression-ref "y"} + source-def {:type "ExpressionDef" :name "x" :expression [1] + :context "Patient"} + condition-def {:type "ExpressionDef" :name "y" :expression true + :context "Patient"} + ctx {:library {:statements {:def [source-def condition-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" source-def "y" condition-def})] + (has-form expr '(filter [1] true)))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {"x" [1]}) + '(filter [1] (param-ref "y")))) + + (testing "form" + (has-form expr '(filter (param-ref "x") (param-ref "y")))) + + (ctu/testing-equals-hash-code {:type "Filter" + :source #elm/parameter-ref "x" + :condition #elm/parameter-ref "y"})))) ;; 20.10. First ;; @@ -236,9 +503,7 @@ (ctu/testing-unary-null elm/first) - (ctu/testing-unary-dynamic elm/first) - - (ctu/testing-unary-form elm/first)) + (ctu/testing-unary-op elm/first)) ;; 20.11. Flatten ;; @@ -256,9 +521,7 @@ (ctu/testing-unary-null elm/flatten) - (ctu/testing-unary-dynamic elm/flatten) - - (ctu/testing-unary-form elm/flatten)) + (ctu/testing-unary-op elm/flatten)) ;; 20.12. ForEach ;; @@ -293,25 +556,78 @@ {:type "Null"} {:type "Null"} nil)))) - (testing "form and static" - (testing "with scope" - (let [expr (ctu/dynamic-compile {:type "ForEach" - :source #elm/parameter-ref "x" - :element #elm/parameter-ref "y" - :scope "A"})] - - (has-form expr '(for-each (param-ref "x") (param-ref "y") "A")) - - (is (false? (core/-static expr))))) - - (testing "without scope" - (let [expr (ctu/dynamic-compile {:type "ForEach" - :source #elm/parameter-ref "x" - :element #elm/parameter-ref "y"})] - - (has-form expr '(for-each (param-ref "x") (param-ref "y"))) - - (is (false? (core/-static expr))))))) + (testing "with scope" + (let [expr (ctu/dynamic-compile {:type "ForEach" + :source #elm/parameter-ref "x" + :element #elm/parameter-ref "y" + :scope "A"})] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm {:type "ForEach" + :source #elm/expression-ref "x" + :element #elm/current "A" + :scope "A"} + expr-def {:type "ExpressionDef" :name "x" :expression [1] + :context "Patient"} + ctx {:library {:statements {:def [expr-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-def})] + (has-form expr '(for-each [1] (current "A") "A")))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {"x" [1]}) + '(for-each [1] (param-ref "y") "A"))) + + (testing "form" + (has-form expr '(for-each (param-ref "x") (param-ref "y") "A"))) + + (ctu/testing-equals-hash-code {:type "ForEach" + :source #elm/parameter-ref "x" + :element #elm/parameter-ref "y" + :scope "A"}))) + + (testing "without scope" + (let [expr (ctu/dynamic-compile {:type "ForEach" + :source #elm/parameter-ref "x" + :element #elm/parameter-ref "y"})] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm {:type "ForEach" + :source #elm/expression-ref "x" + :element #elm/current nil} + expr-defs {:type "ExpressionDef" :name "x" :expression [1] + :context "Patient"} + ctx {:library {:statements {:def [expr-defs]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-defs})] + (has-form expr '(for-each [1] current)))) + + (testing "resolve parameters" + (has-form (c/resolve-params expr {"x" [1]}) + '(for-each [1] (param-ref "y")))) + + (testing "form" + (has-form expr '(for-each (param-ref "x") (param-ref "y")))) + + (ctu/testing-equals-hash-code {:type "ForEach" + :source #elm/parameter-ref "x" + :element #elm/parameter-ref "y"})))) ;; 20.13. In ;; @@ -350,9 +666,7 @@ (ctu/testing-binary-dynamic-null elm/index-of #elm/list [] #elm/integer "1") - (ctu/testing-binary-dynamic elm/index-of) - - (ctu/testing-binary-form elm/index-of)) + (ctu/testing-binary-op elm/index-of)) ;; 20.17. Intersect ;; @@ -378,9 +692,7 @@ (ctu/testing-unary-null elm/last) - (ctu/testing-unary-dynamic elm/last) - - (ctu/testing-unary-form elm/last)) + (ctu/testing-unary-op elm/last)) ;; 20.19. Not Equal ;; @@ -430,14 +742,15 @@ #elm/list [#elm/integer "1"] 1 {:type "Null"} nil) - (are [list] (thrown? Exception (core/-eval (c/compile {} (elm/singleton-from list)) {} nil nil)) - #elm/list [#elm/integer "1" #elm/integer "1"]) + (let [expr #elm/singleton-from #elm/list [#elm/integer "1" #elm/integer "1"]] + (given-thrown (core/-eval (c/compile {} expr) {} nil nil) + :message := "More than one element in `SingletonFrom` expression." + [:expression :type] := "SingletonFrom" + [:expression :operand :type] := "List")) (ctu/testing-unary-null elm/singleton-from) - (ctu/testing-unary-dynamic elm/singleton-from) - - (ctu/testing-unary-form elm/singleton-from)) + (ctu/testing-unary-op elm/singleton-from)) ;; 20.26. Slice ;; @@ -466,17 +779,7 @@ {:type "Null"} #elm/integer "0" #elm/integer "0" nil {:type "Null"} {:type "Null"} {:type "Null"} nil) - (let [expr (ctu/dynamic-compile {:type "Slice" - :source #elm/parameter-ref "x" - :startIndex #elm/parameter-ref "y" - :endIndex #elm/parameter-ref "z"})] - - (testing "expression is dynamic" - (is (false? (core/-static expr)))) - - (testing "form" - (is (= '(slice (param-ref "x") (param-ref "y") (param-ref "z")) - (core/-form expr)))))) + (ctu/testing-ternary-op elm/slice)) ;; 20.27. Sort ;; @@ -506,7 +809,7 @@ (is (false? (core/-static expr)))) (testing "form" - (is (= '(sort (param-ref "x") :asc) (core/-form expr)))))) + (has-form expr '(sort (param-ref "x") :asc))))) ;; 20.28. Times ;; @@ -565,9 +868,7 @@ (ctu/testing-binary-null elm/times #elm/list[#elm/tuple{"name" #elm/string "hans"}]) - (ctu/testing-binary-dynamic elm/times) - - (ctu/testing-binary-form elm/times)) + (ctu/testing-binary-op elm/times)) ;; 20.29. Union ;; diff --git a/modules/cql/test/blaze/elm/compiler/logical_operators/util_test.clj b/modules/cql/test/blaze/elm/compiler/logical_operators/util_test.clj new file mode 100644 index 000000000..628405b9b --- /dev/null +++ b/modules/cql/test/blaze/elm/compiler/logical_operators/util_test.clj @@ -0,0 +1,132 @@ +(ns blaze.elm.compiler.logical-operators.util-test + (:require + [blaze.elm.compiler.logical-operators.util :as u] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [blaze.elm.literal-spec] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(deftest attach-cache-result-test + (testing "empty input" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity nil)))] + (is (empty? tuples)) + (is (empty? bfs)))) + + (testing "with one triple" + (testing "with direct Bloom filter" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op [::bf] ::bf]])))] + (testing "the direct Bloom filter of the triple is also the merged Bloom filter" + (is (= [[::op ::bf]] tuples))) + + (testing "the Bloom filter is returned in the collection of all Bloom filters" + (is (= [::bf] bfs))))) + + (testing "with one indirect Bloom filter" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op [::bf]]])))] + (testing "there is no merged Bloom filter" + (is (= [[::op nil]] tuples))) + + (testing "the Bloom filter is returned in the collection of all Bloom filters" + (is (= [::bf] bfs))))) + + (testing "with one direct and one indirect Bloom filter" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op [::indirect-bf ::direct-bf] ::direct-bf]])))] + (testing "the direct Bloom filter of the triple is also the merged Bloom filter" + (is (= [[::op ::direct-bf]] tuples))) + + (testing "the Bloom filter is returned in the collection of all Bloom filters" + (is (= [::indirect-bf ::direct-bf] bfs))))) + + (testing "with two indirect Bloom filters" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op [::bf-1 ::bf-2]]])))] + (testing "there is no merged Bloom filter" + (is (= [[::op nil]] tuples))) + + (testing "the Bloom filters are returned in the collection of all Bloom filters" + (is (= [::bf-1 ::bf-2] bfs)))))) + + (testing "with two triples" + (testing "only the second having a direct Bloom filter" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1]] [::op-2 [::bf-2] ::bf-2]])))] + (testing "there is no merged Bloom filter on the first tuple" + (is (= [[::op-1] [::op-2 ::bf-2]] tuples))) + + (testing "the indirect Bloom filter is also part of all Bloom filters" + (is (= [::bf-1 ::bf-2] bfs))))) + + (testing "only the first having a direct Bloom filter" + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2]]])))] + (testing "there are no merged Bloom filters because we need the second having one" + (is (= [[::op-1] [::op-2 nil]] tuples))) + + (testing "the indirect Bloom filter is also part of all Bloom filters" + (is (= [::bf-1 ::bf-2] bfs))))) + + (testing "with both having direct Bloom filters" + (with-redefs [bloom-filter/merge + (fn [bf-2 bf-1] + (assert (= ::bf-1 bf-1)) + (assert (= ::bf-2 bf-2)) + ::merged-bf)] + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2] ::bf-2]])))] + (testing "the first tuple has the merged Bloom filter while the second starts with the direct Bloom filter" + (is (= [[::op-1 ::merged-bf] [::op-2 ::bf-2]] tuples))) + + (testing "the merged Bloom filter is not part of all Bloom filters" + (is (= [::bf-1 ::bf-2] bfs))))) + + (testing "but merge returns nil" + (with-redefs [bloom-filter/merge + (fn [bf-2 bf-1] + (assert (= ::bf-1 bf-1)) + (assert (= ::bf-2 bf-2)) + nil)] + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2] ::bf-2]])))] + (testing "there is no merged Bloom filter at the first tuple" + (is (= [[::op-1 nil] [::op-2 ::bf-2]] tuples))) + + (testing "the merged Bloom filter is not part of all Bloom filters" + (is (= [::bf-1 ::bf-2] bfs)))))))) + + (testing "with three triples" + (testing "with all having direct Bloom filters" + (with-redefs [bloom-filter/merge + (fn [bf-a bf-b] + (cond + (and (= ::bf-3 bf-a) (= ::bf-2 bf-b)) + ::bf-m1 + (and (= ::bf-m1 bf-a) (= ::bf-1 bf-b)) + ::bf-m2))] + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2] ::bf-2] [::op-3 [::bf-3] ::bf-3]])))] + (testing "the first two tuples have the merged Bloom filters while the last starts with the direct Bloom filter" + (is (= [[::op-1 ::bf-m2] [::op-2 ::bf-m1] [::op-3 ::bf-3]] tuples))) + + (testing "the merged Bloom filter is not part of all Bloom filters" + (is (= [::bf-1 ::bf-2 ::bf-3] bfs))))) + + (testing "merge returns nil on the second merge" + (with-redefs [bloom-filter/merge + (fn [bf-a bf-b] + (cond + (and (= ::bf-3 bf-a) (= ::bf-2 bf-b)) + ::bf-m1))] + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2] ::bf-2] [::op-3 [::bf-3] ::bf-3]])))] + (testing "there is no merged Bloom filter at the first tuple" + (is (= [[::op-1 nil] [::op-2 ::bf-m1] [::op-3 ::bf-3]] tuples))) + + (testing "the merged Bloom filter is not part of all Bloom filters" + (is (= [::bf-1 ::bf-2 ::bf-3] bfs)))))) + + (testing "merge returns nil" + (with-redefs [bloom-filter/merge (fn [_ _])] + (let [[tuples bfs] ((first (u/or-attach-cache-result identity [[::op-1 [::bf-1] ::bf-1] [::op-2 [::bf-2] ::bf-2] [::op-3 [::bf-3] ::bf-3]])))] + (testing "there are no merged Bloom filter at the first two tuples" + (is (= [[::op-1] [::op-2 nil] [::op-3 ::bf-3]] tuples))) + + (testing "the merged Bloom filter is not part of all Bloom filters" + (is (= [::bf-1 ::bf-2 ::bf-3] bfs))))))))) diff --git a/modules/cql/test/blaze/elm/compiler/logical_operators_test.clj b/modules/cql/test/blaze/elm/compiler/logical_operators_test.clj index 9c230ae14..b21d40207 100644 --- a/modules/cql/test/blaze/elm/compiler/logical_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/logical_operators_test.clj @@ -4,14 +4,19 @@ Section numbers are according to https://cql.hl7.org/04-logicalspecification.html." (:require + [blaze.anomaly :as ba] [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.logical-operators] + [blaze.elm.compiler.logical-operators :as ops] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [are deftest testing]])) + [clojure.test :as test :refer [are deftest is testing]] + [juxt.iota :refer [given]])) (st/instrument) (ctu/instrument-compile) @@ -90,7 +95,7 @@ #elm/parameter-ref "a" {:type "Null"} '(and nil (param-ref "a")) #elm/parameter-ref "a" #elm/parameter-ref "b" '(and (param-ref "a") (param-ref "b")))) - (testing "static" + (testing "Static" (are [x y pred] (pred (core/-static (ctu/dynamic-compile (elm/and [x y])))) #elm/boolean "true" #elm/boolean "true" true? #elm/boolean "true" #elm/boolean "false" true? @@ -110,7 +115,174 @@ #elm/parameter-ref "a" #elm/boolean "true" false? #elm/parameter-ref "a" #elm/boolean "false" true? #elm/parameter-ref "a" {:type "Null"} false? - #elm/parameter-ref "a" #elm/parameter-ref "b" false?))) + #elm/parameter-ref "a" #elm/parameter-ref "b" false?)) + + (testing "attach cache" + (testing "only one bloom filter available" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (when-not (= '(exists (retrieve "Observation")) (c/form expr)) + (bloom-filter/build-bloom-filter expr 0 nil)))] + (let [elm #elm/and [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(and (exists (retrieve "Condition")) + (exists (retrieve "Observation"))) + [1 count] := 2 + [1 0 ::bloom-filter/expr-form] := "(exists (retrieve \"Condition\"))" + [1 1] := (ba/unavailable "No Bloom filter available."))))) + + (testing "both bloom filters available with first having more patients" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (cond + (= (c/form expr) '(exists (retrieve "Observation"))) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"]) + (= (c/form expr) '(exists (retrieve "Condition"))) + (bloom-filter/build-bloom-filter expr 0 ["0"])))] + (let [elm #elm/and [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Observation"}] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(and (exists (retrieve "Condition")) + (exists (retrieve "Observation"))) + [1 count] := 2 + [1 0 ::bloom-filter/patient-count] := 1 + [1 1 ::bloom-filter/patient-count] := 2)))) + + (testing "with three expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"]) + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0"]) + '(exists (retrieve "Specimen")) + nil))] + (let [elm #elm/and [#elm/exists #elm/retrieve{:type "Observation"} + #elm/and [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Specimen"}]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(and (exists (retrieve "Condition")) + (exists (retrieve "Observation")) + (exists (retrieve "Specimen"))) + [1 count] := 3 + [1 0 ::bloom-filter/patient-count] := 1 + [1 1 ::bloom-filter/patient-count] := 2 + [1 2 ::bloom-filter/patient-count] := nil)))) + + (testing "with four expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + nil + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2"]) + '(exists (retrieve "Specimen")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"]) + '(exists (retrieve "MedicationAdministration")) + (bloom-filter/build-bloom-filter expr 0 ["0"])))] + (let [elm #elm/and [#elm/and [#elm/exists #elm/retrieve{:type "MedicationAdministration"} + #elm/exists #elm/retrieve{:type "Specimen"}] + #elm/and [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Observation"}]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(and (exists (retrieve "MedicationAdministration")) + (exists (retrieve "Specimen")) + (exists (retrieve "Condition")) + (exists (retrieve "Observation"))) + [1 count] := 4 + [1 0 ::bloom-filter/patient-count] := 1 + [1 1 ::bloom-filter/patient-count] := 2 + [1 2 ::bloom-filter/patient-count] := 3 + [1 3 ::bloom-filter/patient-count] := nil)))) + + (testing "with five expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + nil + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2"]) + '(exists (retrieve "Specimen")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"]) + '(exists (retrieve "MedicationAdministration")) + (bloom-filter/build-bloom-filter expr 0 ["0"]) + '(exists (retrieve "Procedure")) + nil))] + (let [elm #elm/and [#elm/and [#elm/exists #elm/retrieve{:type "MedicationAdministration"} + #elm/and [#elm/exists #elm/retrieve{:type "Procedure"} + #elm/exists #elm/retrieve{:type "Specimen"}]] + #elm/and [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Observation"}]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(and (exists (retrieve "MedicationAdministration")) + (exists (retrieve "Specimen")) + (exists (retrieve "Condition")) + (exists (retrieve "Procedure")) + (exists (retrieve "Observation"))) + [1 count] := 5 + [1 0 ::bloom-filter/patient-count] := 1 + [1 1 ::bloom-filter/patient-count] := 2 + [1 2 ::bloom-filter/patient-count] := 3 + [1 3 ::bloom-filter/patient-count] := nil + [1 4 ::bloom-filter/patient-count] := nil))))) + + (ctu/testing-binary-op elm/and)) + +(deftest and-op-patient-count-test + (testing "both nil" + (let [op (ops/and-op + (reify-expr core/Expression) + (reify-expr core/Expression))] + (is (nil? (core/-patient-count op))))) + + (testing "first nil" + (let [op (ops/and-op + (reify-expr core/Expression) + (reify-expr core/Expression + (-patient-count [_] 0)))] + (is (nil? (core/-patient-count op))))) + + (testing "second nil" + (let [op (ops/and-op + (reify-expr core/Expression + (-patient-count [_] 0)) + (reify-expr core/Expression))] + (is (nil? (core/-patient-count op))))) + + (testing "first smaller" + (let [op (ops/and-op + (reify-expr core/Expression + (-patient-count [_] 1)) + (reify-expr core/Expression + (-patient-count [_] 2)))] + (is (= 1 (core/-patient-count op))))) + + (testing "second smaller" + (let [op (ops/and-op + (reify-expr core/Expression + (-patient-count [_] 2)) + (reify-expr core/Expression + (-patient-count [_] 1)))] + (is (= 1 (core/-patient-count op)))))) ;; 13.2. Implies ;; @@ -136,9 +308,7 @@ (ctu/testing-unary-null elm/not) - (ctu/testing-unary-dynamic elm/not) - - (ctu/testing-unary-form elm/not)) + (ctu/testing-unary-op elm/not)) ;; 13.4. Or ;; @@ -204,7 +374,7 @@ #elm/parameter-ref "a" {:type "Null"} '(or nil (param-ref "a")) #elm/parameter-ref "a" #elm/parameter-ref "b" '(or (param-ref "a") (param-ref "b")))) - (testing "static" + (testing "Static" (are [x y pred] (pred (core/-static (ctu/dynamic-compile (elm/or [x y])))) #elm/boolean "true" #elm/boolean "true" true? #elm/boolean "true" #elm/boolean "false" true? @@ -224,7 +394,181 @@ #elm/parameter-ref "a" #elm/boolean "true" true? #elm/parameter-ref "a" #elm/boolean "false" false? #elm/parameter-ref "a" {:type "Null"} false? - #elm/parameter-ref "a" #elm/parameter-ref "b" false?))) + #elm/parameter-ref "a" #elm/parameter-ref "b" false?)) + + (ctu/testing-binary-dynamic elm/or) + + (testing "attach cache" + (testing "only one bloom filter available" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (when-not (= '(exists (retrieve "Observation")) (c/form expr)) + (bloom-filter/build-bloom-filter expr 0 nil)))] + (let [elm #elm/or [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 2 + [1 0] := (ba/unavailable "No Bloom filter available.") + [1 1 ::bloom-filter/expr-form] := "(exists (retrieve \"Condition\"))")))) + + (testing "both bloom filters available with second having more patients" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (cond + (= (c/form expr) '(exists (retrieve "Observation"))) + (bloom-filter/build-bloom-filter expr 0 ["0"]) + (= (c/form expr) '(exists (retrieve "Condition"))) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"])))] + (let [elm #elm/or [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(or (exists (retrieve "Condition")) + (exists (retrieve "Observation"))) + [1 count] := 2 + [1 0 ::bloom-filter/patient-count] := 2 + [1 1 ::bloom-filter/patient-count] := 1)))) + + (testing "with three expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1"]) + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0"]) + '(exists (retrieve "Specimen")) + nil))] + (let [elm #elm/or [#elm/exists #elm/retrieve{:type "Observation"} + #elm/or [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Specimen"}]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(or (exists (retrieve "Specimen")) + (exists (retrieve "Observation")) + (exists (retrieve "Condition"))) + [1 count] := 3 + [1 0 ::bloom-filter/patient-count] := nil + [1 1 ::bloom-filter/patient-count] := 2 + [1 2 ::bloom-filter/patient-count] := 1)))) + + (testing "with four expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2" "3"]) + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2"]) + '(exists (retrieve "Specimen")) + nil + '(exists (retrieve "MedicationAdministration")) + (bloom-filter/build-bloom-filter expr 0 ["0"])))] + (let [elm #elm/or [#elm/or [#elm/exists #elm/retrieve{:type "MedicationAdministration"} + #elm/exists #elm/retrieve{:type "Specimen"}] + #elm/or [#elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Observation"}]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(or (exists (retrieve "Specimen")) + (exists (retrieve "Observation")) + (exists (retrieve "Condition")) + (exists (retrieve "MedicationAdministration"))) + [1 count] := 4 + [1 0 ::bloom-filter/patient-count] := nil + [1 1 ::bloom-filter/patient-count] := 4 + [1 2 ::bloom-filter/patient-count] := 3 + [1 3 ::bloom-filter/patient-count] := 1)))) + + (testing "with five expressions" + (with-redefs [ec/get (fn [cache expr] + (assert (= ::cache cache)) + (condp = (c/form expr) + '(exists (retrieve "Observation")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2" "3"]) + '(exists (retrieve "Condition")) + (bloom-filter/build-bloom-filter expr 0 ["0" "1" "2"]) + '(exists (retrieve "Specimen")) + nil + '(exists (retrieve "Procedure")) + nil + '(exists (retrieve "MedicationAdministration")) + (bloom-filter/build-bloom-filter expr 0 ["0"])))] + (let [elm #elm/or [#elm/or [#elm/exists #elm/retrieve{:type "MedicationAdministration"} + #elm/exists #elm/retrieve{:type "Specimen"}] + #elm/or [#elm/exists #elm/retrieve{:type "Condition"} + #elm/or [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Procedure"}]]] + ctx {:eval-context "Patient"} + expr (c/compile ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0 c/form] := '(or (exists (retrieve "Specimen")) + (exists (retrieve "Procedure")) + (exists (retrieve "Observation")) + (exists (retrieve "Condition")) + (exists (retrieve "MedicationAdministration"))) + [1 count] := 5 + [1 0 ::bloom-filter/patient-count] := nil + [1 1 ::bloom-filter/patient-count] := nil + [1 2 ::bloom-filter/patient-count] := 4 + [1 3 ::bloom-filter/patient-count] := 3 + [1 4 ::bloom-filter/patient-count] := 1))))) + + (ctu/testing-binary-resolve-refs elm/or) + + (ctu/testing-binary-resolve-params elm/or) + + (ctu/testing-binary-equals-hash-code elm/or) + + (ctu/testing-binary-form elm/or)) + +(deftest or-op-patient-count-test + (testing "both nil" + (let [op (ops/or-op + (reify-expr core/Expression) + (reify-expr core/Expression))] + (is (nil? (core/-patient-count op))))) + + (testing "first nil" + (let [op (ops/or-op + (reify-expr core/Expression) + (reify-expr core/Expression + (-patient-count [_] 0)))] + (is (nil? (core/-patient-count op))))) + + (testing "second nil" + (let [op (ops/or-op + (reify-expr core/Expression + (-patient-count [_] 0)) + (reify-expr core/Expression))] + (is (nil? (core/-patient-count op))))) + + (testing "first larger" + (let [op (ops/or-op + (reify-expr core/Expression + (-patient-count [_] 2)) + (reify-expr core/Expression + (-patient-count [_] 1)))] + (is (= 2 (core/-patient-count op))))) + + (testing "second larger" + (let [op (ops/or-op + (reify-expr core/Expression + (-patient-count [_] 1)) + (reify-expr core/Expression + (-patient-count [_] 2)))] + (is (= 2 (core/-patient-count op)))))) ;; 13.5. Xor ;; @@ -293,7 +637,7 @@ #elm/parameter-ref "a" {:type "Null"} nil #elm/parameter-ref "a" #elm/parameter-ref "b" '(xor (param-ref "a") (param-ref "b")))) - (testing "static" + (testing "Static" (are [x y pred] (pred (core/-static (ctu/dynamic-compile (elm/xor [x y])))) #elm/boolean "true" #elm/boolean "true" true? #elm/boolean "true" #elm/boolean "false" true? @@ -313,4 +657,6 @@ #elm/parameter-ref "a" #elm/boolean "true" false? #elm/parameter-ref "a" #elm/boolean "false" false? #elm/parameter-ref "a" {:type "Null"} true? - #elm/parameter-ref "a" #elm/parameter-ref "b" false?))) + #elm/parameter-ref "a" #elm/parameter-ref "b" false?)) + + (ctu/testing-binary-op elm/xor)) diff --git a/modules/cql/test/blaze/elm/compiler/nullological_operators_test.clj b/modules/cql/test/blaze/elm/compiler/nullological_operators_test.clj index cb988959b..9bdc6bccc 100644 --- a/modules/cql/test/blaze/elm/compiler/nullological_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/nullological_operators_test.clj @@ -51,8 +51,9 @@ (testing "expression is dynamic" (are [elm] (false? (core/-static (ctu/dynamic-compile (elm/coalesce elm)))) [] - [{:type "Null"}] - [#elm/list []]))) + [#elm/list []])) + + (ctu/testing-binary-op elm/coalesce)) ;; 14.3. IsFalse ;; @@ -72,9 +73,7 @@ #elm/parameter-ref "false" true? #elm/parameter-ref "nil" false?)) - (ctu/testing-unary-dynamic elm/is-false) - - (ctu/testing-unary-form elm/is-false)) + (ctu/testing-unary-op elm/is-false)) ;; 14.4. IsNull ;; @@ -94,9 +93,7 @@ #elm/parameter-ref "false" false? #elm/parameter-ref "nil" true?)) - (ctu/testing-unary-dynamic elm/is-null) - - (ctu/testing-unary-form elm/is-null)) + (ctu/testing-unary-op elm/is-null)) ;; 14.5. IsTrue ;; @@ -116,6 +113,4 @@ #elm/parameter-ref "false" false? #elm/parameter-ref "nil" false?)) - (ctu/testing-unary-dynamic elm/is-true) - - (ctu/testing-unary-form elm/is-true)) + (ctu/testing-unary-op elm/is-true)) diff --git a/modules/cql/test/blaze/elm/compiler/parameters_test.clj b/modules/cql/test/blaze/elm/compiler/parameters_test.clj index ecac3d7fe..11d8c3fb5 100644 --- a/modules/cql/test/blaze/elm/compiler/parameters_test.clj +++ b/modules/cql/test/blaze/elm/compiler/parameters_test.clj @@ -9,12 +9,12 @@ [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.parameters :refer [->ParameterRef]] + [blaze.elm.compiler.parameters] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.literal] [blaze.elm.literal-spec] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [deftest is testing]] + [clojure.test :as test :refer [deftest testing]] [cognitect.anomalies :as anom] [juxt.iota :refer [given]])) @@ -41,7 +41,6 @@ {:def [{:name "parameter-def-101820"}]}}} expr (c/compile context #elm/parameter-ref "parameter-def-101820")] - (is (= (->ParameterRef "parameter-def-101820") expr)) (testing "form" (has-form expr '(param-ref "parameter-def-101820"))))) diff --git a/modules/cql/test/blaze/elm/compiler/queries_test.clj b/modules/cql/test/blaze/elm/compiler/queries_test.clj index 5a9451a4d..4ec18f3f6 100644 --- a/modules/cql/test/blaze/elm/compiler/queries_test.clj +++ b/modules/cql/test/blaze/elm/compiler/queries_test.clj @@ -14,7 +14,7 @@ [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.literal] [blaze.elm.literal-spec] - [blaze.elm.quantity :as quantity] + [blaze.elm.quantity :refer [quantity]] [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type] [clojure.spec.test.alpha :as st] @@ -75,13 +75,13 @@ expr (c/compile {} elm)] (testing "eval" - (is (= [(quantity/quantity 1 "m") (quantity/quantity 2 "m")] (core/-eval expr {} nil nil)))) + (is (= [(quantity 1 "m") (quantity 2 "m")] (core/-eval expr {} nil nil)))) (testing "form" (has-form expr '(sorted-vector-query distinct - [(quantity 2 "m") - (quantity 1 "m") - (quantity 1 "m")] + [(quantity 2M "m") + (quantity 1M "m") + (quantity 1M "m")] [asc (:value S)])))) (testing "with IdentifierRef" diff --git a/modules/cql/test/blaze/elm/compiler/reusing_logic_test.clj b/modules/cql/test/blaze/elm/compiler/reusing_logic_test.clj index 943f4518c..2268ddbf6 100644 --- a/modules/cql/test/blaze/elm/compiler/reusing_logic_test.clj +++ b/modules/cql/test/blaze/elm/compiler/reusing_logic_test.clj @@ -11,10 +11,11 @@ [blaze.elm.compiler.core-spec] [blaze.elm.compiler.function :as function] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] + [blaze.elm.expression.cache :as ec] [blaze.elm.interval :as interval] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] - [blaze.elm.quantity :as quantity] + [blaze.elm.quantity :refer [quantity]] [blaze.fhir.spec.type.system :as system] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [are deftest is testing]] @@ -70,6 +71,27 @@ ;; ;; The FunctionRef type defines an expression that invokes a previously defined ;; function. The result of evaluating each operand is passed to the function. +(defmacro testing-function-ref-attach-cache [name] + `(testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm# #elm/function-ref [~name #elm/exists #elm/retrieve{:type "Observation"}] + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 1 + [1 0] := '(~'exists (~'retrieve "Observation"))))))) + +(defmacro testing-function-ref-resolve-refs [name] + `(testing "resolve expression references" + (let [elm# #elm/function-ref [~name #elm/expression-ref "x"] + expr-def# {:type "ExpressionDef" :name "x" :expression "y" + :context "Unfiltered"} + ctx# {:library {:statements {:def [expr-def#]}}} + expr# (c/resolve-refs (c/compile ctx# elm#) {"x" expr-def#})] + (has-form expr# '(~'call ~name "y"))))) + (deftest compile-function-ref-test (testing "Throws error on missing function" (given (ba/try-anomaly (c/compile {} #elm/function-ref ["name-175844"])) @@ -87,9 +109,23 @@ (testing "eval" (is (= 1 (core/-eval expr {} nil nil)))) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach cache" + (is (= [expr []] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {})] + (has-form expr (list 'call function-name)))) + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {}) + (list 'call function-name))) + (testing "form" (has-form expr (list 'call function-name))))) @@ -97,6 +133,7 @@ (let [function-name "name-180815" fn-expr (c/compile {} #elm/negate #elm/operand-ref"x") compile-ctx {:library {:parameters {:def [{:name "a"}]}} + :eval-context "Patient" :function-defs {function-name {:function (partial function/arity-n function-name fn-expr ["x"])}}} elm (elm/function-ref [function-name #elm/parameter-ref "a"]) expr (c/compile compile-ctx elm)] @@ -107,9 +144,37 @@ -1 1 0 0)) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm (elm/function-ref [function-name #elm/exists #elm/retrieve{:type "Observation"}]) + expr (c/compile compile-ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 1 + [1 0] := '(exists (retrieve "Observation")))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm (elm/function-ref [function-name #elm/expression-ref "x"]) + expr-def {:type "ExpressionDef" :name "x" :expression "a" + :context "Unfiltered"} + ctx (assoc compile-ctx :library {:statements {:def [expr-def]}}) + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-def})] + (has-form expr (list 'call function-name "a")))) + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {"a" 1}) + (list 'call function-name 1)) + + (has-form (core/-resolve-params expr {}) + (list 'call function-name '(param-ref "a")))) + (testing "form" (has-form expr (list 'call function-name '(param-ref "a")))))) @@ -117,6 +182,7 @@ (let [function-name "name-184652" fn-expr (c/compile {} #elm/add [#elm/operand-ref"x" #elm/operand-ref"y"]) compile-ctx {:library {:parameters {:def [{:name "a"} {:name "b"}]}} + :eval-context "Patient" :function-defs {function-name {:function (partial function/arity-n function-name fn-expr ["x" "y"])}}} elm (elm/function-ref [function-name #elm/parameter-ref "a" #elm/parameter-ref "b"]) expr (c/compile compile-ctx elm)] @@ -127,9 +193,44 @@ 1 0 1 0 1 1)) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm (elm/function-ref [function-name + #elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}]) + expr (c/compile compile-ctx elm)] + (given (st/with-instrument-disabled (c/attach-cache expr ::cache)) + count := 2 + [0] := expr + [1 count] := 2 + [1 0] := '(exists (retrieve "Observation")) + [1 1] := '(exists (retrieve "Condition")))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm (elm/function-ref [function-name + #elm/expression-ref "x" + #elm/expression-ref "y"]) + expr-defs [{:type "ExpressionDef" :name "x" :expression "a" + :context "Unfiltered"} + {:type "ExpressionDef" :name "y" :expression "b" + :context "Unfiltered"}] + ctx (assoc compile-ctx :library {:statements {:def expr-defs}}) + expr (c/resolve-refs (c/compile ctx elm) (zipmap ["x" "y"] expr-defs))] + (has-form expr (list 'call function-name "a" "b")))) + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {"a" 1 "b" 2}) + (list 'call function-name 1 2)) + + (has-form (core/-resolve-params expr {}) + (list 'call function-name '(param-ref "a") '(param-ref "b")))) + (testing "form" (has-form expr (list 'call function-name '(param-ref "a") '(param-ref "b")))))) @@ -140,13 +241,21 @@ (testing "eval" (are [x res] (= res (core/-eval expr {:parameters {"x" x}} nil nil)) - {:value 23M :code "kg"} (quantity/quantity 23M "kg") - {:value 42M} (quantity/quantity 42M "1") + {:value 23M :code "kg"} (quantity 23M "kg") + {:value 42M} (quantity 42M "1") {} nil)) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToQuantity") + + (testing-function-ref-resolve-refs "ToQuantity") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {}) + '(call "ToQuantity" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToQuantity" (param-ref "x")))))) @@ -164,9 +273,17 @@ #fhir/date"2023-05" #system/date"2023-05" #fhir/date"2023-05-07" #system/date"2023-05-07")) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToDate") + + (testing-function-ref-resolve-refs "ToDate") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {}) + '(call "ToDate" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToDate" (param-ref "x")))))) @@ -190,9 +307,20 @@ #fhir/instant"2021-02-23T15:12:45Z" #system/date-time"2021-02-23T15:12:45" #fhir/instant"2021-02-23T15:12:45+01:00" #system/date-time"2021-02-23T14:12:45")) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToDateTime") + + (testing-function-ref-resolve-refs "ToDateTime") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {"x" #fhir/dateTime"2022-02"}) + '(call "ToDateTime" #fhir/dateTime"2022-02")) + + (has-form (core/-resolve-params expr {}) + '(call "ToDateTime" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToDateTime" (param-ref "x")))))) @@ -208,9 +336,17 @@ #fhir/code{:id "foo" :value "code-211914"} "code-211914" #fhir/code{:id "foo"} nil)) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToString") + + (testing-function-ref-resolve-refs "ToString") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {}) + '(call "ToString" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToString" (param-ref "x")))))) @@ -226,9 +362,17 @@ :code "code-140828"} (code/to-code "system-140820" "version-140924" "code-140828"))) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToCode") + + (testing-function-ref-resolve-refs "ToCode") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {}) + '(call "ToCode" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToCode" (param-ref "x")))))) @@ -259,9 +403,24 @@ (system/date-time 2021 2 23 14 12 45) nil))) - (testing "static" + (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing-function-ref-attach-cache "ToInterval") + + (testing-function-ref-resolve-refs "ToInterval") + + (testing "resolve parameters" + (has-form (core/-resolve-params expr {"x" #fhir/Period + {:start #fhir/dateTime"2021-02-23T15:12:45+01:00" + :end #fhir/dateTime"2021-02-23T16:00:00+01:00"}}) + '(call "ToInterval" #fhir/Period + {:start #fhir/dateTime"2021-02-23T15:12:45+01:00" + :end #fhir/dateTime"2021-02-23T16:00:00+01:00"})) + + (has-form (core/-resolve-params expr {}) + '(call "ToInterval" (param-ref "x")))) + (testing "form" (has-form expr '(call "ToInterval" (param-ref "x"))))))) diff --git a/modules/cql/test/blaze/elm/compiler/string_operators_test.clj b/modules/cql/test/blaze/elm/compiler/string_operators_test.clj index 75a7cb641..01d737177 100644 --- a/modules/cql/test/blaze/elm/compiler/string_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/string_operators_test.clj @@ -9,6 +9,7 @@ [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] + [blaze.elm.compiler.string-operators] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] @@ -79,7 +80,7 @@ ;; ;; If any argument is null, the result is null. (deftest compile-concatenate-test - (are [args res] (= res (core/-eval (c/compile {} {:type "Concatenate" :operand args}) {} nil nil)) + (are [args res] (= res (core/-eval (c/compile {} (elm/concatenate args)) {} nil nil)) [#elm/string "a"] "a" [#elm/string "a" #elm/string "b"] "ab" @@ -87,16 +88,18 @@ [{:type "Null"}] nil) (testing "form" - (are [args form] (= form (c/form (c/compile {} {:type "Concatenate" :operand args}))) + (are [args form] (= form (c/form (c/compile {} (elm/concatenate args)))) [#elm/string "a"] '(concatenate "a") [#elm/string "a" #elm/string "b"] '(concatenate "a" "b") [#elm/string "a" {:type "Null"}] '(concatenate "a" nil))) - (testing "static" - (are [args] (false? (core/-static (c/compile {} {:type "Concatenate" :operand args}))) + (testing "Static" + (are [args] (false? (core/-static (c/compile {} (elm/concatenate args)))) [#elm/string "a"] [#elm/string "a" #elm/string "b"] - [#elm/string "a" {:type "Null"}]))) + [#elm/string "a" {:type "Null"}])) + + (ctu/testing-binary-op elm/concatenate)) ;; 17.3. EndsWith ;; @@ -107,7 +110,7 @@ ;; ;; If either argument is null, the result is null. (deftest compile-ends-with-test - (testing "static" + (testing "Static" (are [s suffix pred] (pred (c/compile {} (elm/ends-with [s suffix]))) #elm/string "a" #elm/string "a" true? #elm/string "ab" #elm/string "b" true? @@ -125,9 +128,7 @@ (ctu/testing-binary-null elm/ends-with #elm/string "a") - (ctu/testing-binary-dynamic elm/ends-with) - - (ctu/testing-binary-form elm/ends-with)) + (ctu/testing-binary-op elm/ends-with)) ;; 17.4. Equal ;; @@ -168,9 +169,7 @@ (ctu/testing-binary-null elm/indexer #elm/list [] #elm/integer "0")) - (ctu/testing-binary-dynamic elm/indexer) - - (ctu/testing-binary-form elm/indexer)) + (ctu/testing-binary-op elm/indexer)) ;; 17.7. LastPositionOf ;; @@ -189,9 +188,7 @@ (ctu/testing-binary-dynamic-null elm/last-position-of #elm/string "a" #elm/string "a") - (ctu/testing-binary-dynamic elm/last-position-of) - - (ctu/testing-binary-form elm/last-position-of)) + (ctu/testing-binary-op elm/last-position-of)) ;; 17.8. Length ;; @@ -205,7 +202,7 @@ (deftest compile-length-test ;; It's important to use identical? here because we like to test that length ;; returns a long instead of an integer. - (testing "static" + (testing "Static" (are [x res] (identical? res (c/compile {} (elm/length x))) #elm/string "" 0 #elm/string "a" 1 @@ -239,9 +236,7 @@ (is (identical? count (core/-eval expr {:db db} patient nil))))))) - (ctu/testing-unary-dynamic elm/length) - - (ctu/testing-unary-form elm/length)) + (ctu/testing-unary-op elm/length)) ;; 17.9. Lower ;; @@ -255,7 +250,7 @@ ;; ;; If the argument is null, the result is null. (deftest compile-lower-test - (testing "static" + (testing "Static" (are [s res] (= res (c/compile {} (elm/lower s))) #elm/string "" "" #elm/string "A" "a")) @@ -267,9 +262,7 @@ (ctu/testing-unary-null elm/lower) - (ctu/testing-unary-dynamic elm/lower) - - (ctu/testing-unary-form elm/lower)) + (ctu/testing-unary-op elm/lower)) ;; 17.10. Matches ;; @@ -292,9 +285,7 @@ (ctu/testing-binary-null elm/matches #elm/string "a") - (ctu/testing-binary-dynamic elm/matches) - - (ctu/testing-binary-form elm/matches)) + (ctu/testing-binary-op elm/matches)) ;; 17.11. NotEqual ;; @@ -317,9 +308,7 @@ (ctu/testing-binary-dynamic-null elm/position-of #elm/string "a" #elm/string "a") - (ctu/testing-binary-dynamic elm/position-of) - - (ctu/testing-binary-form elm/position-of)) + (ctu/testing-binary-op elm/position-of)) ;; 17.13. ReplaceMatches ;; @@ -337,14 +326,12 @@ ;; such, CQL does not prescribe a particular dialect, but recommends the use of ;; the PCRE dialect. (deftest compile-replace-matches-test - (are [s pattern substitution res] (= res (core/-eval (c/compile {} (elm/replace-matches [s pattern substitution])) {} nil nil)) + (are [s pattern substitution res] (= res (c/compile {} (elm/replace-matches [s pattern substitution]))) #elm/string "a" #elm/string "a" #elm/string "b" "b") (ctu/testing-ternary-dynamic-null elm/replace-matches #elm/string "a" #elm/string "a" #elm/string "a") - (ctu/testing-ternary-dynamic elm/replace-matches) - - (ctu/testing-ternary-form elm/replace-matches)) + (ctu/testing-ternary-op elm/replace-matches)) ;; 17.14. Split ;; @@ -356,39 +343,17 @@ ;; separator, the result is a list of strings containing one element that is the ;; value of the stringToSplit argument. (deftest compile-split-test - (testing "Without separator" - (are [s res] (= res (core/-eval (c/compile {} {:type "Split" :stringToSplit s}) {} nil nil)) - #elm/string "" [""] - #elm/string "a" ["a"] - - {:type "Null"} nil) - - (testing "form and static" - (let [expr (ctu/dynamic-compile {:type "Split" - :stringToSplit #elm/parameter-ref "x"})] - - (has-form expr '(split (param-ref "x"))) - - (is (false? (core/-static expr)))))) - - (testing "With separator" - (are [s separator res] (= res (core/-eval (c/compile {} {:type "Split" :stringToSplit s :separator separator}) {} nil nil)) - #elm/string "" #elm/string "," [""] - #elm/string "a,b" #elm/string "," ["a" "b"] - #elm/string "a,,b" #elm/string "," ["a" "" "b"] - - {:type "Null"} #elm/string "," nil - #elm/string "a" {:type "Null"} ["a"] - {:type "Null"} {:type "Null"} nil) - - (testing "form and static" - (let [expr (ctu/dynamic-compile {:type "Split" - :stringToSplit #elm/parameter-ref "x" - :separator #elm/parameter-ref "y"})] + (testing "Dynamic" + (are [s separator res] (= res (ctu/dynamic-compile-eval (elm/split [s separator]))) + #elm/parameter-ref "empty-string" #elm/string "," [""] + #elm/parameter-ref "a,b" #elm/string "," ["a" "b"] + #elm/parameter-ref "a,,b" #elm/string "," ["a" "" "b"] - (has-form expr '(split (param-ref "x") (param-ref "y"))) + #elm/parameter-ref "nil" #elm/string "," nil + #elm/parameter-ref "a" {:type "Null"} ["a"] + #elm/parameter-ref "nil" {:type "Null"} nil)) - (is (false? (core/-static expr))))))) + (ctu/testing-binary-op elm/split)) ;; 17.15. SplitOnMatches ;; @@ -413,7 +378,7 @@ ;; ;; If either argument is null, the result is null. (deftest compile-starts-with-test - (testing "static" + (testing "Static" (are [s prefix pred] (pred (c/compile {} (elm/starts-with [s prefix]))) #elm/string "a" #elm/string "a" true? #elm/string "ba" #elm/string "b" true? @@ -431,9 +396,7 @@ (ctu/testing-binary-null elm/starts-with #elm/string "a") - (ctu/testing-binary-dynamic elm/starts-with) - - (ctu/testing-binary-form elm/starts-with)) + (ctu/testing-binary-op elm/starts-with)) ;; 17.17. Substring ;; @@ -449,7 +412,7 @@ ;; TODO: what todo if the length is out of range? (deftest compile-substring-test (testing "Without length" - (are [s start-index res] (= res (core/-eval (c/compile {} {:type "Substring" :stringToSub s :startIndex start-index}) {} nil nil)) + (are [s start-index res] (= res (core/-eval (c/compile {} (elm/substring [s start-index])) {} nil nil)) #elm/string "ab" #elm/integer "1" "b" #elm/string "a" #elm/integer "-1" nil @@ -468,7 +431,7 @@ (is (false? (core/-static expr)))))) (testing "With length" - (are [s start-index length res] (= res (core/-eval (c/compile {} {:type "Substring" :stringToSub s :startIndex start-index :length length}) {} nil nil)) + (are [s start-index length res] (= res (core/-eval (c/compile {} (elm/substring [s start-index length])) {} nil nil)) #elm/string "a" #elm/integer "0" #elm/integer "1" "a" #elm/string "a" #elm/integer "0" #elm/integer "2" "a" #elm/string "abc" #elm/integer "1" #elm/integer "1" "b" @@ -487,7 +450,11 @@ (has-form expr '(substring (param-ref "x") (param-ref "y") (param-ref "z"))) - (is (false? (core/-static expr))))))) + (is (false? (core/-static expr)))))) + + (ctu/testing-binary-op elm/substring) + + (ctu/testing-ternary-op elm/substring)) ;; 17.18. Upper ;; @@ -501,7 +468,7 @@ ;; ;; If the argument is null, the result is null. (deftest compile-upper-test - (testing "static" + (testing "Static" (are [s res] (= res (c/compile {} (elm/upper s))) #elm/string "" "" #elm/string "a" "A")) @@ -511,8 +478,4 @@ #elm/parameter-ref "empty-string" "" #elm/parameter-ref "a" "A")) - (ctu/testing-unary-null elm/upper) - - (ctu/testing-unary-dynamic elm/upper) - - (ctu/testing-unary-form elm/upper)) + (ctu/testing-unary-op elm/upper)) diff --git a/modules/cql/test/blaze/elm/compiler/structured_values_test.clj b/modules/cql/test/blaze/elm/compiler/structured_values_test.clj index 96111250e..232cf2f22 100644 --- a/modules/cql/test/blaze/elm/compiler/structured_values_test.clj +++ b/modules/cql/test/blaze/elm/compiler/structured_values_test.clj @@ -9,6 +9,7 @@ [blaze.elm.compiler :as c] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] + [blaze.elm.compiler.structured-values] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.literal] [blaze.elm.literal-spec] @@ -54,12 +55,12 @@ #elm/tuple{"id" #elm/parameter-ref "1" "name" #elm/parameter-ref "a"} {:id 1 :name "a"}) - (testing "static" + (testing "Static" (is (false? (core/-static (ctu/dynamic-compile #elm/tuple{"id" #elm/parameter-ref "1"}))))) (testing "form" - (is (= '{:id (param-ref "x")} - (core/-form (ctu/dynamic-compile #elm/tuple{"id" #elm/parameter-ref "x"}))))))) + (let [expr (ctu/dynamic-compile #elm/tuple{"id" #elm/parameter-ref "x"})] + (has-form expr '{:id (param-ref "x")}))))) ;; 2.2. Instance ;; @@ -109,10 +110,8 @@ entity {:fhir/type :fhir/Patient :id "0" :identifier [identifier]} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "identifier"])] + elm #elm/scope-property ["R" "identifier"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= identifier (coll/first (core/-eval expr nil nil {"R" entity}))))) @@ -120,6 +119,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:identifier R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:identifier R))))) @@ -131,10 +142,8 @@ entity {:fhir/type :fhir/Patient :id "0" :identifier [identifier]} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "identifier"])] + elm #elm/scope-property ["R" "identifier"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= identifier (coll/first (core/-eval expr nil nil {"R" entity}))))) @@ -142,6 +151,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:identifier R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:identifier R)))))) @@ -154,10 +175,8 @@ entity {:fhir/type :fhir/Patient :id "0" :extension [extension]} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "extension"])] + elm #elm/scope-property ["R" "extension"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= extension (coll/first (core/-eval expr nil nil {"R" entity}))))) @@ -165,6 +184,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:extension R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:extension R)))))) @@ -173,10 +204,8 @@ (let [entity {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "gender"])] + elm #elm/scope-property ["R" "gender"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= #fhir/code"male" (core/-eval expr nil nil {"R" entity})))) @@ -184,6 +213,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:gender R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:gender R))))) @@ -191,10 +232,8 @@ (let [entity {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "gender"])] + elm #elm/scope-property ["R" "gender"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= #fhir/code"male" (core/-eval expr nil nil {"R" entity})))) @@ -202,6 +241,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:gender R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:gender R)))))) @@ -210,10 +261,8 @@ (fn [x] {:fhir/type :fhir/Patient :id "0" :birthDate x}) - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "birthDate.value"])] + elm #elm/scope-property ["R" "birthDate.value"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (are [birth-date res] (= res (core/-eval expr nil nil {"R" (entity birth-date)})) @@ -225,6 +274,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value (:birthDate R))))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:value (:birthDate R)))))) @@ -233,10 +294,8 @@ (let [entity {:fhir/type :fhir/Observation :id "0" :value "value-114318"} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "value"])] + elm #elm/scope-property ["R" "value"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= "value-114318" (core/-eval expr nil nil {"R" entity})))) @@ -244,6 +303,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:value R))))) @@ -251,10 +322,8 @@ (let [entity {:fhir/type :fhir/Observation :id "0" :value "value-114318"} - expr - (c/compile - {:eval-context "Patient"} - #elm/scope-property ["R" "value"])] + elm #elm/scope-property ["R" "value"] + expr (c/compile {:eval-context "Patient"} elm)] (testing "eval" (is (= "value-114318" (core/-eval expr nil nil {"R" entity})))) @@ -262,6 +331,18 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value R)))) + + (ctu/testing-equals-hash-code elm) + (testing "form" (has-form expr '(:value R)))))))) @@ -279,14 +360,32 @@ source {:fhir/type :fhir/Patient :id "0" :identifier [identifier]} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Patient" + :expression source}] (testing "eval" - (is (= identifier (coll/first (core/-eval expr {:expression-defs {"Patient" {:expression source}}} nil nil))))) + (is (= identifier (coll/first (core/-eval expr {:expression-defs {"Patient" expr-def}} nil nil))))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Patient" expr-def})] + (has-form expr (list :identifier source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:identifier (expr-ref "Patient"))))) + (testing "form" (has-form expr '(:identifier (expr-ref "Patient")))))) @@ -302,14 +401,32 @@ source {:fhir/type :fhir/Patient :id "0" :identifier [identifier]} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Patient" + :expression source}] (testing "eval" - (is (= identifier (coll/first (core/-eval expr {:expression-defs {"Patient" {:expression source}}} nil nil))))) + (is (= identifier (coll/first (core/-eval expr {:expression-defs {"Patient" expr-def}} nil nil))))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Patient" expr-def})] + (has-form expr (list :identifier source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:identifier (expr-ref "Patient"))))) + (testing "form" (has-form expr '(:identifier (expr-ref "Patient"))))))) @@ -322,14 +439,32 @@ source {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Patient" + :expression source}] (testing "eval" - (is (= #fhir/code"male" (core/-eval expr {:expression-defs {"Patient" {:expression source}}} nil nil)))) + (is (= #fhir/code"male" (core/-eval expr {:expression-defs {"Patient" expr-def}} nil nil)))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Patient" expr-def})] + (has-form expr (list :gender source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:gender (expr-ref "Patient"))))) + (testing "form" (has-form expr '(:gender (expr-ref "Patient")))))) @@ -341,14 +476,32 @@ source {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Patient" + :expression source}] (testing "eval" - (is (= #fhir/code"male" (core/-eval expr {:expression-defs {"Patient" {:expression source}}} nil nil)))) + (is (= #fhir/code"male" (core/-eval expr {:expression-defs {"Patient" expr-def}} nil nil)))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Patient" expr-def})] + (has-form expr (list :gender source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:gender (expr-ref "Patient"))))) + (testing "form" (has-form expr '(:gender (expr-ref "Patient"))))))) @@ -373,6 +526,24 @@ (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr-def {:type "ExpressionDef" + :context "Patient" + :name "Patient" + :expression (source #fhir/date"2023-05-07")} + expr (c/resolve-refs expr {"Patient" expr-def})] + (has-form expr (list :value (list :birthDate (source #fhir/date"2023-05-07")))))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value (:birthDate (expr-ref "Patient")))))) + (testing "form" (has-form expr '(:value (:birthDate (expr-ref "Patient"))))))) @@ -385,14 +556,32 @@ source {:fhir/type :fhir/Observation :id "0" :value "value-114318"} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Observation" + :expression source}] (testing "eval" - (is (= "value-114318" (core/-eval expr {:expression-defs {"Observation" {:expression source}}} nil nil)))) + (is (= "value-114318" (core/-eval expr {:expression-defs {"Observation" expr-def}} nil nil)))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Observation" expr-def})] + (has-form expr (list :value source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value (expr-ref "Observation"))))) + (testing "form" (has-form expr '(:value (expr-ref "Observation")))))) @@ -404,14 +593,32 @@ source {:fhir/type :fhir/Observation :id "0" :value "value-114318"} - expr (c/compile {:library library :eval-context "Patient"} elm)] + expr (c/compile {:library library :eval-context "Patient"} elm) + expr-def {:type "ExpressionDef" + :context "Patient" + :name "Observation" + :expression source}] (testing "eval" - (is (= "value-114318" (core/-eval expr {:expression-defs {"Observation" {:expression source}}} nil nil)))) + (is (= "value-114318" (core/-eval expr {:expression-defs {"Observation" expr-def}} nil nil)))) (testing "expression is dynamic" (is (false? (core/-static expr)))) + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [expr (c/resolve-refs expr {"Observation" expr-def})] + (has-form expr (list :value source)))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {})] + (has-form expr '(:value (expr-ref "Observation"))))) + (testing "form" (has-form expr '(:value (expr-ref "Observation"))))))) diff --git a/modules/cql/test/blaze/elm/compiler/test_util.clj b/modules/cql/test/blaze/elm/compiler/test_util.clj index 792bc4ded..468f8ddb0 100644 --- a/modules/cql/test/blaze/elm/compiler/test_util.clj +++ b/modules/cql/test/blaze/elm/compiler/test_util.clj @@ -1,17 +1,21 @@ (ns blaze.elm.compiler.test-util (:require + [blaze.anomaly :as ba] [blaze.db.api :as d] [blaze.elm.compiler :as c] + [blaze.elm.compiler-spec] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.external-data :as ed] + [blaze.elm.expression.cache :as ec] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] + [blaze.elm.resource :as cr] [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]]) + [clojure.test :refer [is testing]] + [juxt.iota :refer [given]]) (:import [java.time OffsetDateTime ZoneOffset])) @@ -43,7 +47,8 @@ (def now (OffsetDateTime/now (ZoneOffset/ofHours 0))) (def dynamic-compile-ctx - {:library + {:eval-context "Patient" + :library {:parameters {:def [{:name "true"} @@ -62,6 +67,8 @@ {:name "ab"} {:name "b"} {:name "ba"} + {:name "a,b"} + {:name "a,,b"} {:name "A"} {:name "2019"} {:name "2020"} @@ -75,7 +82,8 @@ (def dynamic-eval-ctx {:parameters {"true" true "false" false "nil" nil "-1" -1 "1" 1 "2" 2 "3" 3 "4" 4 - "empty-string" "" "x" "x" "y" "y" "z" "z" "a" "a" "ab" "ab" "b" "b" "ba" "ba" "A" "A" + "empty-string" "" "x" "x" "y" "y" "z" "z" + "a" "a" "ab" "ab" "b" "b" "ba" "ba" "a,b" "a,b" "a,,b" "a,,b" "A" "A" "2019" (system/date 2019) "2020" (system/date 2020) "2021" (system/date 2021) @@ -190,6 +198,128 @@ (let [expr# (dynamic-compile ~elm-constructor)] (has-form expr# (quote ~form-name)))))) +(defmacro testing-unary-dynamic [elm-constructor] + `(testing "expression is dynamic" + (is (false? (core/-static (dynamic-compile (~elm-constructor + #elm/parameter-ref "x"))))))) + +(defmacro testing-unary-precision-dynamic + [elm-constructor & precisions] + `(testing "expression is dynamic" + (doseq [precision# ~(vec precisions)] + (is (false? (core/-static (dynamic-compile (~elm-constructor + [#elm/parameter-ref "x" + precision#])))))))) + +(defmacro testing-unary-attach-cache + "Works with unary and aggregate operators." + [elm-constructor] + `(testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (let [elm# (~elm-constructor #elm/exists #elm/retrieve{:type "Observation"}) + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 1 + [1 0] := '(~'exists (~'retrieve "Observation"))))))) + +(defmacro testing-unary-precision-attach-cache + [elm-constructor & precisions] + `(testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor [#elm/exists #elm/retrieve{:type "Observation"} precision#]) + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 1 + [1 0] := '(~'exists (~'retrieve "Observation")))))))) + +(defmacro testing-unary-patient-count [elm-constructor] + `(testing "patient count" + (let [expr# (dynamic-compile (~elm-constructor #elm/parameter-ref "x"))] + (is (nil? (core/-patient-count expr#)))))) + +(defmacro testing-unary-precision-patient-count [elm-constructor & precisions] + `(testing "patient count" + (doseq [precision# ~(vec precisions)] + (let [expr# (dynamic-compile (~elm-constructor [#elm/parameter-ref "x" + precision#]))] + (is (nil? (core/-patient-count expr#))))))) + +(defmacro testing-unary-resolve-refs + "Works with unary and aggregate operators." + [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve expression references" + (let [elm# (~elm-constructor #elm/expression-ref "x") + expr-def# {:type "ExpressionDef" :name "x" :expression "y" + :context "Unfiltered"} + ctx# {:library {:statements {:def [expr-def#]}}} + expr# (c/resolve-refs (c/compile ctx# elm#) {"x" expr-def#})] + (has-form expr# '(~form-name "y")))))) + +(defmacro testing-unary-precision-resolve-refs + "Works with unary and aggregate operators." + [elm-constructor & precisions] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve expression references" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor [#elm/expression-ref "x" precision#]) + expr-def# {:type "ExpressionDef" :name "x" :expression "y" + :context "Unfiltered"} + ctx# {:library {:statements {:def [expr-def#]}}} + expr# (c/resolve-refs (c/compile ctx# elm#) {"x" expr-def#})] + (has-form expr# (list '~form-name "y" precision#))))))) + +(defmacro testing-unary-resolve-params + "Works with unary and aggregate operators." + [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve parameters" + (let [elm# (~elm-constructor #elm/parameter-ref "x") + ctx# {:library {:parameters {:def [{:name "x"}]}}} + expr# (c/resolve-params (c/compile ctx# elm#) {"x" "y"})] + (has-form expr# '(~form-name "y")))))) + +(defmacro testing-unary-precision-resolve-params + "Works with unary and aggregate operators." + [elm-constructor & precisions] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve parameters" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor [#elm/parameter-ref "x" precision#]) + ctx# {:library {:parameters {:def [{:name "x"}]}}} + expr# (c/resolve-params (c/compile ctx# elm#) {"x" "y"})] + (has-form expr# (list '~form-name "y" precision#))))))) + +(defmacro testing-unary-equals-hash-code + [elm-constructor] + `(testing "equals/hashCode" + (let [elm# (~elm-constructor #elm/parameter-ref "x") + ctx# {:library {:parameters {:def [{:name "x"}]}}} + expr-1# (c/compile ctx# elm#) + expr-2# (c/compile ctx# elm#)] + (is (and (.equals ^Object expr-1# expr-2#) + (= (.hashCode ^Object expr-1#) + (.hashCode ^Object expr-2#))))))) + +(defmacro testing-unary-precision-equals-hash-code + [elm-constructor & precisions] + `(testing "equals/hashCode" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor [#elm/parameter-ref "x" precision#]) + ctx# {:library {:parameters {:def [{:name "x"}]}}} + expr-1# (c/compile ctx# elm#) + expr-2# (c/compile ctx# elm#)] + (is (and (.equals ^Object expr-1# expr-2#) + (= (.hashCode ^Object expr-1#) + (.hashCode ^Object expr-2#)))))))) + (defmacro testing-unary-form "Works with unary and aggregate operators." [elm-constructor] @@ -211,6 +341,43 @@ (has-form expr# (list '~form-name '(~'param-ref "x") precision#)))))))) +(defmacro testing-binary-equals-hash-code + [elm-constructor] + `(testing "equals/hashCode" + (let [elm# (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y"]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"}]}}} + expr-1# (c/compile ctx# elm#) + expr-2# (c/compile ctx# elm#)] + (is (and (.equals ^Object expr-1# expr-2#) + (= (.hashCode ^Object expr-1#) + (.hashCode ^Object expr-2#))))))) + +(defmacro testing-binary-precision-equals-hash-code + ([elm-constructor] + `(testing-binary-precision-equals-hash-code ~elm-constructor "year" "month")) + ([elm-constructor & precisions] + `(testing "equals/hashCode" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y" precision#]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"}]}}} + expr-1# (c/compile ctx# elm#) + expr-2# (c/compile ctx# elm#)] + (is (and (.equals ^Object expr-1# expr-2#) + (= (.hashCode ^Object expr-1#) + (.hashCode ^Object expr-2#))))))))) + +(defmacro testing-ternary-equals-hash-code + [elm-constructor] + `(testing "equals/hashCode" + (let [elm# (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y" + #elm/parameter-ref "z"]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"} {:name "z"}]}}} + expr-1# (c/compile ctx# elm#) + expr-2# (c/compile ctx# elm#)] + (is (and (.equals ^Object expr-1# expr-2#) + (= (.hashCode ^Object expr-1#) + (.hashCode ^Object expr-2#))))))) + (defmacro testing-binary-form [elm-constructor] (let [form-name (symbol (name elm-constructor))] `(testing "form" @@ -250,25 +417,144 @@ `(testing "expression is dynamic" (is (false? (core/-static (dynamic-compile ~elm-constructor)))))) -(defmacro testing-unary-dynamic [elm-constructor] - `(testing "expression is dynamic" - (is (false? (core/-static (dynamic-compile (~elm-constructor - #elm/parameter-ref "x"))))))) - -(defmacro testing-unary-precision-dynamic - [elm-constructor & precisions] - `(testing "expression is dynamic" - (doseq [precision# ~(vec precisions)] - (is (false? (core/-static (dynamic-compile (~elm-constructor - [#elm/parameter-ref "x" - precision#])))))))) - (defmacro testing-binary-dynamic [elm-constructor] `(testing "expression is dynamic" (is (false? (core/-static (dynamic-compile (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y"]))))))) +(defn mock-cache-get [cache expr] + (assert (= ::cache cache)) + (when (= '(exists (retrieve "Observation")) (c/form expr)) + (c/form expr))) + +(defmacro testing-binary-attach-cache + [elm-constructor] + `(testing "attach cache" + (with-redefs [ec/get mock-cache-get] + (let [elm# (~elm-constructor + [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"}]) + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 2 + [1 0] := '(~'exists (~'retrieve "Observation")) + [1 1] := (ba/unavailable "No Bloom filter available.")))))) + +(defmacro testing-ternary-attach-cache + [elm-constructor] + `(testing "attach cache" + (with-redefs [ec/get mock-cache-get] + (let [elm# (~elm-constructor + [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"} + #elm/exists #elm/retrieve{:type "Specimen"}]) + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 3 + [1 0] := '(~'exists (~'retrieve "Observation")) + [1 1] := (ba/unavailable "No Bloom filter available.") + [1 2] := (ba/unavailable "No Bloom filter available.")))))) + +(defmacro testing-binary-precision-attach-cache + ([elm-constructor] + `(testing-binary-precision-attach-cache ~elm-constructor "year" "month")) + ([elm-constructor & precisions] + `(testing "attach cache" + (with-redefs [ec/get #(do (assert (= ::cache %1)) (c/form %2))] + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor + [#elm/exists #elm/retrieve{:type "Observation"} + #elm/exists #elm/retrieve{:type "Condition"} + precision#]) + ctx# {:eval-context "Patient"} + expr# (c/compile ctx# elm#)] + (given (st/with-instrument-disabled (c/attach-cache expr# ::cache)) + count := 2 + [0] := expr# + [1 count] := 2 + [1 0] := '(~'exists (~'retrieve "Observation")) + [1 1] := '(~'exists (~'retrieve "Condition"))))))))) + +(defmacro testing-binary-patient-count [elm-constructor] + `(testing "patient count" + (is (nil? (core/-patient-count (dynamic-compile (~elm-constructor + [#elm/parameter-ref "x" + #elm/parameter-ref "y"]))))))) + +(defmacro testing-binary-precision-patient-count + ([elm-constructor] + `(testing-binary-precision-patient-count ~elm-constructor "year" "month")) + ([elm-constructor & precisions] + `(testing "patient count" + (doseq [precision# ~(vec precisions)] + (is (nil? (core/-patient-count (dynamic-compile (~elm-constructor + [#elm/parameter-ref "x" + #elm/parameter-ref "y" + precision#]))))))))) + +(defmacro testing-ternary-patient-count [elm-constructor] + `(testing "patient count" + (is (nil? (core/-patient-count (dynamic-compile (~elm-constructor + [#elm/parameter-ref "x" + #elm/parameter-ref "y" + #elm/parameter-ref "z"]))))))) + +(defmacro testing-binary-resolve-refs + [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve expression references" + (let [elm# (~elm-constructor + [#elm/expression-ref "x" + #elm/expression-ref "y"]) + expr-defs# [{:type "ExpressionDef" :name "x" :expression "a" + :context "Unfiltered"} + {:type "ExpressionDef" :name "y" :expression "b" + :context "Unfiltered"}] + ctx# {:library {:statements {:def expr-defs#}}} + expr# (c/resolve-refs (c/compile ctx# elm#) (zipmap ["x" "y"] expr-defs#))] + (has-form expr# '(~form-name "a" "b")))))) + +(defmacro testing-ternary-resolve-refs [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve expression references" + (let [elm# (~elm-constructor + [#elm/expression-ref "x" + #elm/expression-ref "y" + #elm/expression-ref "z"]) + expr-defs# [{:type "ExpressionDef" :name "x" :expression "a" + :context "Unfiltered"} + {:type "ExpressionDef" :name "y" :expression "b" + :context "Unfiltered"} + {:type "ExpressionDef" :name "z" :expression "c" + :context "Unfiltered"}] + ctx# {:library {:statements {:def expr-defs#}}} + expr# (c/resolve-refs (c/compile ctx# elm#) (zipmap ["x" "y" "z"] expr-defs#))] + (has-form expr# '(~form-name "a" "b" "c")))))) + +(defmacro testing-binary-resolve-params [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve parameters" + (let [elm# (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y"]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"}]}}} + expr# (c/resolve-params (c/compile ctx# elm#) {"x" "a" "y" "b"})] + (has-form expr# '(~form-name "a" "b")))))) + +(defmacro testing-ternary-resolve-params [elm-constructor] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve parameters" + (let [elm# (~elm-constructor [#elm/parameter-ref "x" #elm/parameter-ref "y" + #elm/parameter-ref "z"]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"} {:name "z"}]}}} + expr# (c/resolve-params (c/compile ctx# elm#) {"x" "a" "y" "b" "z" "c"})] + (has-form expr# '(~form-name "a" "b" "c")))))) + (defmacro testing-binary-precision-dynamic ([elm-constructor] `(testing-binary-precision-dynamic ~elm-constructor "year" "month")) @@ -280,6 +566,41 @@ #elm/parameter-ref "y" precision#]))))))))) +(defmacro testing-binary-precision-resolve-refs + ([elm-constructor] + `(testing-binary-precision-resolve-refs ~elm-constructor "year" "month")) + ([elm-constructor & precisions] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve expression references" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor + [#elm/expression-ref "x" + #elm/expression-ref "y" + precision#]) + expr-def-x# {:type "ExpressionDef" :name "x" :expression "a" + :context "Unfiltered"} + expr-def-y# {:type "ExpressionDef" :name "y" :expression "b" + :context "Unfiltered"} + ctx# {:library {:statements {:def [expr-def-x# expr-def-y#]}}} + expr# (c/resolve-refs (c/compile ctx# elm#) {"x" expr-def-x# + "y" expr-def-y#})] + (has-form expr# (list (quote ~form-name) "a" "b" precision#)))))))) + +(defmacro testing-binary-precision-resolve-params + ([elm-constructor] + `(testing-binary-precision-resolve-params ~elm-constructor "year" "month")) + ([elm-constructor & precisions] + (let [form-name (symbol (name elm-constructor))] + `(testing "resolve parameters" + (doseq [precision# ~(vec precisions)] + (let [elm# (~elm-constructor + [#elm/parameter-ref "x" + #elm/parameter-ref "y" + precision#]) + ctx# {:library {:parameters {:def [{:name "x"} {:name "y"}]}}} + expr# (c/resolve-params (c/compile ctx# elm#) {"x" "a" "y" "b"})] + (has-form expr# (list (quote ~form-name) "a" "b" precision#)))))))) + (defmacro testing-ternary-dynamic [elm-constructor] `(testing "expression is dynamic" (is (false? (core/-static (dynamic-compile (~elm-constructor @@ -287,8 +608,124 @@ #elm/parameter-ref "y" #elm/parameter-ref "z"]))))))) +(defmacro testing-unary-op [elm-constructor] + `(do + (testing-unary-dynamic ~elm-constructor) + + (testing-unary-attach-cache ~elm-constructor) + + (testing-unary-patient-count ~elm-constructor) + + (testing-unary-resolve-refs ~elm-constructor) + + (testing-unary-resolve-params ~elm-constructor) + + (testing-unary-equals-hash-code ~elm-constructor) + + (testing-unary-form ~elm-constructor))) + +(defmacro testing-unary-precision-op [elm-constructor & precisions] + `(do + (testing-unary-precision-dynamic ~elm-constructor ~@precisions) + + (testing-unary-precision-attach-cache ~elm-constructor ~@precisions) + + (testing-unary-precision-patient-count ~elm-constructor ~@precisions) + + (testing-unary-precision-resolve-refs ~elm-constructor ~@precisions) + + (testing-unary-precision-resolve-params ~elm-constructor ~@precisions) + + (testing-unary-precision-equals-hash-code ~elm-constructor ~@precisions) + + (testing-unary-precision-form ~elm-constructor ~@precisions))) + +(defmacro testing-binary-op [elm-constructor] + `(do + (testing-binary-dynamic ~elm-constructor) + + (testing-binary-attach-cache ~elm-constructor) + + (testing-binary-patient-count ~elm-constructor) + + (testing-binary-resolve-refs ~elm-constructor) + + (testing-binary-resolve-params ~elm-constructor) + + (testing-binary-equals-hash-code ~elm-constructor) + + (testing-binary-form ~elm-constructor))) + +(defmacro testing-binary-precision-op [elm-constructor] + `(do + (testing-binary-dynamic ~elm-constructor) + + (testing-binary-precision-dynamic ~elm-constructor) + + (testing-binary-attach-cache ~elm-constructor) + + (testing-binary-precision-attach-cache ~elm-constructor) + + (testing-binary-patient-count ~elm-constructor) + + (testing-binary-precision-patient-count ~elm-constructor) + + (testing-binary-resolve-refs ~elm-constructor) + + (testing-binary-precision-resolve-refs ~elm-constructor) + + (testing-binary-resolve-params ~elm-constructor) + + (testing-binary-precision-resolve-params ~elm-constructor) + + (testing-binary-equals-hash-code ~elm-constructor) + + (testing-binary-precision-equals-hash-code ~elm-constructor) + + (testing-binary-form ~elm-constructor) + + (testing-binary-precision-form ~elm-constructor))) + +(defmacro testing-binary-precision-only-op [elm-constructor & precisions] + `(do + (testing-binary-precision-dynamic ~elm-constructor ~@precisions) + + (testing-binary-precision-attach-cache ~elm-constructor ~@precisions) + + (testing-binary-precision-patient-count ~elm-constructor ~@precisions) + + (testing-binary-precision-resolve-refs ~elm-constructor ~@precisions) + + (testing-binary-precision-resolve-params ~elm-constructor ~@precisions) + + (testing-binary-precision-equals-hash-code ~elm-constructor ~@precisions) + + (testing-binary-precision-form ~elm-constructor ~@precisions))) + +(defmacro testing-ternary-op [elm-constructor] + `(do + (testing-ternary-dynamic ~elm-constructor) + + (testing-ternary-attach-cache ~elm-constructor) + + (testing-ternary-patient-count ~elm-constructor) + + (testing-ternary-resolve-refs ~elm-constructor) + + (testing-ternary-resolve-params ~elm-constructor) + + (testing-ternary-equals-hash-code ~elm-constructor) + + (testing-ternary-form ~elm-constructor))) + +(defmacro testing-equals-hash-code [elm] + `(testing "equals/hashCode" + (let [expr-1# (dynamic-compile ~elm) + expr-2# (dynamic-compile ~elm)] + (is (= 1 (count (set [expr-1# expr-2#]))))))) + (defn resource [db type id] - (ed/mk-resource db (d/resource-handle db type id))) + (cr/mk-resource db (d/resource-handle db type id))) (defn eval-unfiltered [elm] (core/-eval (c/compile {:eval-context "Unfiltered"} elm) {} nil nil)) 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 2c7d1a746..0dd5160ee 100644 --- a/modules/cql/test/blaze/elm/compiler/type_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/type_operators_test.clj @@ -9,16 +9,16 @@ [blaze.elm.compiler.clinical-operators] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.compiler.type-operators] [blaze.elm.concept :as concept] [blaze.elm.decimal :as decimal] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] [blaze.elm.protocols :as p] - [blaze.elm.quantity :as quantity] + [blaze.elm.quantity :refer [quantity]] [blaze.elm.quantity-spec] - [blaze.elm.ratio :as ratio] + [blaze.elm.ratio :refer [ratio]] [blaze.elm.util-spec] [blaze.fhir.spec.type.system :as system] [clojure.spec.test.alpha :as st] @@ -100,10 +100,33 @@ #elm/as ["{urn:hl7-org:elm-types:r1}DateTime" #elm/date-time"2019-09-04"] (system/date-time 2019 9 4))) - (testing "expression is dynamic" - (is (false? (core/-static (ctu/dynamic-compile - #elm/as["{urn:hl7-org:elm-types:r1}Integer" - #elm/parameter-ref "x"]))))) + (let [expr (ctu/dynamic-compile #elm/as["{urn:hl7-org:elm-types:r1}Integer" + #elm/parameter-ref "x"])] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm #elm/as["{urn:hl7-org:elm-types:r1}Integer" + #elm/expression-ref "x"] + expr-def {:type "ExpressionDef" :name "x" :expression "y" + :context "Patient"} + ctx {:library {:statements {:def [expr-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-def})] + (has-form expr '(as elm/integer "y")))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {"x" "y"})] + (has-form expr '(as elm/integer "y"))))) + + (ctu/testing-equals-hash-code #elm/as["{urn:hl7-org:elm-types:r1}Integer" + #elm/parameter-ref "x"]) (testing "form" (are [elm form] (= form (c/form (c/compile {} elm))) @@ -176,9 +199,7 @@ (ctu/testing-binary-null elm/can-convert-quantity #elm/quantity [1 "m"] #elm/string "m") - (ctu/testing-binary-dynamic elm/can-convert-quantity) - - (ctu/testing-binary-form elm/can-convert-quantity)) + (ctu/testing-binary-op elm/can-convert-quantity)) ;; 22.4. Children ;; @@ -201,9 +222,7 @@ (ctu/testing-unary-null elm/children) - (ctu/testing-unary-dynamic elm/children) - - (ctu/testing-unary-form elm/children)) + (ctu/testing-unary-op elm/children)) ;; TODO 22.5. Convert ;; @@ -249,16 +268,14 @@ ;; If either argument is null, the result is null. (deftest compile-convert-quantity-test (are [argument unit res] (p/equal res (core/-eval (c/compile {} (elm/convert-quantity [argument unit])) {} nil nil)) - #elm/quantity [5 "mg"] #elm/string "g" (quantity/quantity 0.005 "g")) + #elm/quantity [5 "mg"] #elm/string "g" (quantity 0.005M "g")) (are [argument unit] (nil? (core/-eval (c/compile {} (elm/convert-quantity [argument unit])) {} nil nil)) #elm/quantity [5 "mg"] #elm/string "m") (ctu/testing-binary-null elm/convert-quantity #elm/quantity [5 "mg"] #elm/string "m") - (ctu/testing-binary-dynamic elm/convert-quantity) - - (ctu/testing-binary-form elm/convert-quantity)) + (ctu/testing-binary-op elm/convert-quantity)) ;; 22.7. ConvertsToBoolean ;; @@ -356,9 +373,7 @@ (ctu/testing-unary-null elm/converts-to-boolean) - (ctu/testing-unary-dynamic elm/converts-to-boolean) - - (ctu/testing-unary-form elm/converts-to-boolean)) + (ctu/testing-unary-op elm/converts-to-boolean)) ;; 22.8. ConvertsToDate ;; @@ -412,9 +427,7 @@ (ctu/testing-unary-null elm/converts-to-date) - (ctu/testing-unary-dynamic elm/converts-to-date) - - (ctu/testing-unary-form elm/converts-to-date)) + (ctu/testing-unary-op elm/converts-to-date)) ;; 22.9. ConvertsToDateTime ;; @@ -465,9 +478,7 @@ (ctu/testing-unary-null elm/converts-to-date-time) - (ctu/testing-unary-dynamic elm/converts-to-date-time) - - (ctu/testing-unary-form elm/converts-to-date-time)) + (ctu/testing-unary-op elm/converts-to-date-time)) ;; 22.10. ConvertsToDecimal ;; @@ -525,9 +536,7 @@ (ctu/testing-unary-null elm/converts-to-decimal) - (ctu/testing-unary-dynamic elm/converts-to-decimal) - - (ctu/testing-unary-form elm/converts-to-decimal)) + (ctu/testing-unary-op elm/converts-to-decimal)) ;; 22.11. ConvertsToLong ;; @@ -582,9 +591,7 @@ (ctu/testing-unary-null elm/converts-to-long) - (ctu/testing-unary-dynamic elm/converts-to-long) - - (ctu/testing-unary-form elm/converts-to-long)) + (ctu/testing-unary-op elm/converts-to-long)) ;; 22.12. ConvertsToInteger ;; @@ -638,9 +645,7 @@ (ctu/testing-unary-null elm/converts-to-integer) - (ctu/testing-unary-dynamic elm/converts-to-integer) - - (ctu/testing-unary-form elm/converts-to-integer)) + (ctu/testing-unary-op elm/converts-to-integer)) ;; 22.13. ConvertsToQuantity ;; @@ -710,9 +715,7 @@ (ctu/testing-unary-null elm/converts-to-quantity) - (ctu/testing-unary-dynamic elm/converts-to-quantity) - - (ctu/testing-unary-form elm/converts-to-quantity)) + (ctu/testing-unary-op elm/converts-to-quantity)) ;; 22.14. ConvertsToRatio ;; @@ -748,9 +751,7 @@ (ctu/testing-unary-null elm/converts-to-ratio) - (ctu/testing-unary-dynamic elm/converts-to-ratio) - - (ctu/testing-unary-form elm/converts-to-ratio)) + (ctu/testing-unary-op elm/converts-to-ratio)) ;; 22.15. ConvertsToString ;; @@ -822,9 +823,7 @@ (ctu/testing-unary-null elm/converts-to-string) - (ctu/testing-unary-dynamic elm/converts-to-string) - - (ctu/testing-unary-form elm/converts-to-string)) + (ctu/testing-unary-op elm/converts-to-string)) ;; 22.16. ConvertsToTime ;; @@ -891,9 +890,7 @@ (ctu/testing-unary-null elm/converts-to-time) - (ctu/testing-unary-dynamic elm/converts-to-time) - - (ctu/testing-unary-form elm/converts-to-time)) + (ctu/testing-unary-op elm/converts-to-time)) ;; 22.17. Descendents ;; @@ -907,11 +904,7 @@ ;; If the source is null, the result is null. (deftest compile-to-descendents-test (testing "Code" - (testing "expression is dynamic" - (is (not (core/static? (c/compile {} (elm/descendents (ctu/code "system-134534" "code-134551"))))))) - - (are [x res] (= res (core/-eval (c/compile {} (elm/descendents x)) - {:now ctu/now} nil nil)) + (are [x res] (= res (c/compile {} (elm/descendents x))) (ctu/code "system-134534" "code-134551") ["code-134551" nil "system-134534" nil])) @@ -919,9 +912,7 @@ (ctu/testing-unary-null elm/descendents) - (ctu/testing-unary-dynamic elm/descendents) - - (ctu/testing-unary-form elm/descendents)) + (ctu/testing-unary-op elm/descendents)) ;; 22.18. Is ;; @@ -1036,10 +1027,33 @@ #elm/is ["{urn:hl7-org:elm-types:r1}DateTime" #elm/string "2019-09-04"] #elm/is ["{urn:hl7-org:elm-types:r1}DateTime" {:type "Null"}])) - (testing "expression is dynamic" - (is (false? (core/-static (ctu/dynamic-compile - #elm/is["{urn:hl7-org:elm-types:r1}Integer" - #elm/parameter-ref "x"]))))) + (let [expr (ctu/dynamic-compile #elm/is["{urn:hl7-org:elm-types:r1}Integer" + #elm/parameter-ref "x"])] + + (testing "expression is dynamic" + (is (false? (core/-static expr)))) + + (testing "attach-cache" + (is (= [expr] (st/with-instrument-disabled (c/attach-cache expr ::cache))))) + + (testing "patient count" + (is (nil? (core/-patient-count expr)))) + + (testing "resolve expression references" + (let [elm #elm/is["{urn:hl7-org:elm-types:r1}Integer" + #elm/expression-ref "x"] + expr-def {:type "ExpressionDef" :name "x" :expression "y" + :context "Patient"} + ctx {:library {:statements {:def [expr-def]}}} + expr (c/resolve-refs (c/compile ctx elm) {"x" expr-def})] + (has-form expr '(is elm/integer "y")))) + + (testing "resolve parameters" + (let [expr (c/resolve-params expr {"x" "y"})] + (has-form expr '(is elm/integer "y"))))) + + (ctu/testing-equals-hash-code #elm/is["{urn:hl7-org:elm-types:r1}Integer" + #elm/parameter-ref "x"]) (testing "form" (are [elm form] (= form (c/form (c/compile {} elm))) @@ -1160,9 +1174,7 @@ (ctu/testing-unary-null elm/to-boolean) - (ctu/testing-unary-dynamic elm/to-boolean) - - (ctu/testing-unary-form elm/to-boolean)) + (ctu/testing-unary-op elm/to-boolean)) ;; 22.20. ToChars ;; @@ -1190,9 +1202,7 @@ (ctu/testing-unary-null elm/to-chars) - (ctu/testing-unary-dynamic elm/to-chars) - - (ctu/testing-unary-form elm/to-chars)) + (ctu/testing-unary-op elm/to-chars)) ;; 22.21. ToConcept ;; @@ -1219,9 +1229,7 @@ (ctu/testing-unary-null elm/to-concept) - (ctu/testing-unary-dynamic elm/to-concept) - - (ctu/testing-unary-form elm/to-concept)) + (ctu/testing-unary-op elm/to-concept)) ;; 22.22. ToDate ;; @@ -1297,9 +1305,7 @@ (ctu/testing-unary-null elm/to-date) - (ctu/testing-unary-dynamic elm/to-date) - - (ctu/testing-unary-form elm/to-date)) + (ctu/testing-unary-op elm/to-date)) ;; 22.23. ToDateTime ;; @@ -1363,9 +1369,7 @@ (ctu/testing-unary-null elm/to-date-time) - (ctu/testing-unary-dynamic elm/to-date-time) - - (ctu/testing-unary-form elm/to-date-time)) + (ctu/testing-unary-op elm/to-date-time)) ;; 22.24. ToDecimal ;; @@ -1413,9 +1417,7 @@ (ctu/testing-unary-null elm/to-decimal) - (ctu/testing-unary-dynamic elm/to-decimal) - - (ctu/testing-unary-form elm/to-decimal)) + (ctu/testing-unary-op elm/to-decimal)) ;; 22.25. ToInteger ;; @@ -1456,9 +1458,7 @@ (ctu/testing-unary-null elm/to-integer) - (ctu/testing-unary-dynamic elm/to-integer) - - (ctu/testing-unary-form elm/to-integer)) + (ctu/testing-unary-op elm/to-integer)) ;; 22.26. ToList ;; @@ -1490,9 +1490,7 @@ #elm/parameter-ref "nil" [] #elm/parameter-ref "a" ["a"])) - (ctu/testing-unary-dynamic elm/to-list) - - (ctu/testing-unary-form elm/to-list)) + (ctu/testing-unary-op elm/to-list)) ;; 22.27. ToLong ;; @@ -1537,9 +1535,7 @@ (ctu/testing-unary-null elm/to-long) - (ctu/testing-unary-dynamic elm/to-long) - - (ctu/testing-unary-form elm/to-long)) + (ctu/testing-unary-op elm/to-long)) ;; 22.28. ToQuantity ;; @@ -1574,16 +1570,16 @@ (deftest compile-to-quantity-test (testing "String" (are [x res] (p/equal res (ctu/compile-unop elm/to-quantity elm/string x)) - "-1" (quantity/quantity -1 "1") - "1" (quantity/quantity 1 "1") + "-1" (quantity -1 "1") + "1" (quantity 1 "1") - "1'm'" (quantity/quantity 1 "m") - "1 'm'" (quantity/quantity 1 "m") - "1 'm'" (quantity/quantity 1 "m") + "1'm'" (quantity 1 "m") + "1 'm'" (quantity 1 "m") + "1 'm'" (quantity 1 "m") - "10 'm'" (quantity/quantity 10 "m") + "10 'm'" (quantity 10 "m") - "1.1 'm'" (quantity/quantity 1.1M "m")) + "1.1 'm'" (quantity 1.1M "m")) (are [x] (nil? (ctu/compile-unop elm/to-quantity elm/string x)) (str (- decimal/min 1e-8M)) @@ -1595,30 +1591,28 @@ (testing "Integer" (are [x res] (= res (ctu/compile-unop elm/to-quantity elm/integer x)) - "1" (quantity/quantity 1 "1"))) + "1" (quantity 1 "1"))) (testing "Decimal" (are [x res] (p/equal res (ctu/compile-unop elm/to-quantity elm/decimal x)) - "1" (quantity/quantity 1 "1") - "1.1" (quantity/quantity 1.1M "1"))) + "1" (quantity 1 "1") + "1.1" (quantity 1.1M "1"))) (testing "Ratio" (are [x res] (p/equal res (ctu/compile-unop elm/to-quantity elm/ratio x)) - [[1] [1]] (quantity/quantity 1 "1") - [[-1] [1]] (quantity/quantity -1 "1") + [[1] [1]] (quantity 1 "1") + [[-1] [1]] (quantity -1 "1") - [[1 "s"] [1 "s"]] (quantity/quantity 1 "1") - [[1 "s"] [2 "s"]] (quantity/quantity 2 "1") + [[1 "s"] [1 "s"]] (quantity 1 "1") + [[1 "s"] [2 "s"]] (quantity 2 "1") - [[1 "m"] [1 "s"]] (quantity/quantity 1 "s/m") - [[1 "s"] [1 "m"]] (quantity/quantity 1 "m/s") - [[100 "cm"] [1 "m"]] (quantity/quantity 1 "1"))) + [[1 "m"] [1 "s"]] (quantity 1 "s/m") + [[1 "s"] [1 "m"]] (quantity 1 "m/s") + [[100 "cm"] [1 "m"]] (quantity 1 "1"))) (ctu/testing-unary-null elm/to-quantity) - (ctu/testing-unary-dynamic elm/to-quantity) - - (ctu/testing-unary-form elm/to-quantity)) + (ctu/testing-unary-op elm/to-quantity)) ;; 22.29. ToRatio ;; @@ -1638,24 +1632,24 @@ (deftest compile-to-ratio-test (testing "String" (are [x res] (p/equal res (ctu/compile-unop elm/to-ratio elm/string x)) - "-1:-1" (ratio/ratio (quantity/quantity -1 "1") (quantity/quantity -1 "1")) - "1:1" (ratio/ratio (quantity/quantity 1 "1") (quantity/quantity 1 "1")) - "1:100" (ratio/ratio (quantity/quantity 1 "1") (quantity/quantity 100 "1")) - "100:1" (ratio/ratio (quantity/quantity 100 "1") (quantity/quantity 1 "1")) + "-1:-1" (ratio (quantity -1 "1") (quantity -1 "1")) + "1:1" (ratio (quantity 1 "1") (quantity 1 "1")) + "1:100" (ratio (quantity 1 "1") (quantity 100 "1")) + "100:1" (ratio (quantity 100 "1") (quantity 1 "1")) - "1'm':1'm'" (ratio/ratio (quantity/quantity 1 "m") (quantity/quantity 1 "m")) - "1 'm':1 'm'" (ratio/ratio (quantity/quantity 1 "m") (quantity/quantity 1 "m")) - "1 'm':1 'm'" (ratio/ratio (quantity/quantity 1 "m") (quantity/quantity 1 "m")) + "1'm':1'm'" (ratio (quantity 1 "m") (quantity 1 "m")) + "1 'm':1 'm'" (ratio (quantity 1 "m") (quantity 1 "m")) + "1 'm':1 'm'" (ratio (quantity 1 "m") (quantity 1 "m")) - "2'm':1'm'" (ratio/ratio (quantity/quantity 2 "m") (quantity/quantity 1 "m")) - "1'm':2'm'" (ratio/ratio (quantity/quantity 1 "m") (quantity/quantity 2 "m")) + "2'm':1'm'" (ratio (quantity 2 "m") (quantity 1 "m")) + "1'm':2'm'" (ratio (quantity 1 "m") (quantity 2 "m")) - "1'cm':1'm'" (ratio/ratio (quantity/quantity 1 "cm") (quantity/quantity 1 "m")) - "1'm':1'cm'" (ratio/ratio (quantity/quantity 1 "m") (quantity/quantity 1 "cm")) + "1'cm':1'm'" (ratio (quantity 1 "cm") (quantity 1 "m")) + "1'm':1'cm'" (ratio (quantity 1 "m") (quantity 1 "cm")) - "10 'm':10 'm'" (ratio/ratio (quantity/quantity 10 "m") (quantity/quantity 10 "m")) + "10 'm':10 'm'" (ratio (quantity 10 "m") (quantity 10 "m")) - "1.1 'm':1.1 'm'" (ratio/ratio (quantity/quantity 1.1M "m") (quantity/quantity 1.1M "m")))) + "1.1 'm':1.1 'm'" (ratio (quantity 1.1M "m") (quantity 1.1M "m")))) (are [x] (nil? (ctu/compile-unop elm/to-ratio elm/string x)) ":" @@ -1667,9 +1661,7 @@ (ctu/testing-unary-null elm/to-ratio) - (ctu/testing-unary-dynamic elm/to-ratio) - - (ctu/testing-unary-form elm/to-ratio)) + (ctu/testing-unary-op elm/to-ratio)) ;; 22.30. ToString ;; @@ -1746,9 +1738,7 @@ (ctu/testing-unary-null elm/to-string) - (ctu/testing-unary-dynamic elm/to-string) - - (ctu/testing-unary-form elm/to-string)) + (ctu/testing-unary-op elm/to-string)) ;; 22.31. ToTime ;; @@ -1805,6 +1795,4 @@ (ctu/testing-unary-null elm/to-time) - (ctu/testing-unary-dynamic elm/to-time) - - (ctu/testing-unary-form elm/to-time)) + (ctu/testing-unary-op elm/to-time)) diff --git a/modules/cql/test/blaze/elm/expression/cache/bloom_filter_test.clj b/modules/cql/test/blaze/elm/expression/cache/bloom_filter_test.clj new file mode 100644 index 000000000..9d34f9232 --- /dev/null +++ b/modules/cql/test/blaze/elm/expression/cache/bloom_filter_test.clj @@ -0,0 +1,120 @@ +(ns blaze.elm.expression.cache.bloom-filter-test + (:require + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] + [blaze.elm.compiler :as c] + [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.expression.cache.bloom-filter :as bloom-filter] + [blaze.elm.expression.cache.codec-spec] + [blaze.elm.expression.cache.codec.by-t-spec] + [blaze.elm.expression.cache.codec.form-spec] + [blaze.elm.literal] + [blaze.fhir.test-util] + [blaze.module.test-util :refer [with-system]] + [blaze.test-util] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest testing]] + [juxt.iota :refer [given]] + [taoensso.timbre :as log])) + +(st/instrument) +(ctu/instrument-compile) +(log/set-min-level! :trace) + +(defn- fixture [f] + (st/instrument) + (ctu/instrument-compile) + (f) + (st/unstrument)) + +(test/use-fixtures :each fixture) + +(deftest create-test + (testing "with empty database" + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm)] + + (given (bloom-filter/create node expr) + ::bloom-filter/t := 0 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 0 + ::bloom-filter/mem-size := 11981)))) + + (testing "with one Patient with one Observation" + (with-system-data [{:blaze.db/keys [node]} mem-node-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm)] + + (given (bloom-filter/create node expr) + ::bloom-filter/t := 1 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 1 + ::bloom-filter/mem-size := 11981)))) + + (testing "with two Patients on of which has one Observation" + (with-system-data [{:blaze.db/keys [node]} mem-node-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}] + [:put {:fhir/type :fhir/Patient :id "1"}]]] + + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm)] + + (given (bloom-filter/create node expr) + ::bloom-filter/t := 1 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 1 + ::bloom-filter/mem-size := 11981))))) + +(deftest recreate-test + (testing "with empty database" + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm) + bloom-filter (bloom-filter/create node expr)] + + (given (bloom-filter/recreate node bloom-filter expr) + ::bloom-filter/t := 0 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 0 + ::bloom-filter/mem-size := 11981)))) + + (testing "with one Patient with one Observation added" + (with-system [{:blaze.db/keys [node]} mem-node-config] + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm) + bloom-filter (bloom-filter/create node expr)] + + @(d/transact node [[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]) + + (given (bloom-filter/recreate node bloom-filter expr) + ::bloom-filter/t := 1 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 1 + ::bloom-filter/mem-size := 11981)))) + + (testing "with one additional Patient with one Observation added" + (with-system-data [{:blaze.db/keys [node]} mem-node-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + + (let [elm #elm/exists #elm/retrieve{:type "Observation"} + expr (c/compile {:eval-context "Patient"} elm) + bloom-filter (bloom-filter/create node expr)] + + @(d/transact node [[:put {:fhir/type :fhir/Patient :id "1"}] + [:put {:fhir/type :fhir/Observation :id "1" + :subject #fhir/Reference{:reference "Patient/1"}}]]) + + (given (bloom-filter/recreate node bloom-filter expr) + ::bloom-filter/t := 2 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 1 + ::bloom-filter/mem-size := 11981))))) diff --git a/modules/cql/test/blaze/elm/expression/cache/codec/by_t_spec.clj b/modules/cql/test/blaze/elm/expression/cache/codec/by_t_spec.clj new file mode 100644 index 000000000..7108cccad --- /dev/null +++ b/modules/cql/test/blaze/elm/expression/cache/codec/by_t_spec.clj @@ -0,0 +1,15 @@ +(ns blaze.elm.expression.cache.codec.by-t-spec + (:require + [blaze.db.kv.spec] + [blaze.elm.expression.cache :as-alias ec] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.expression.cache.codec.by-t :as codec-by-t] + [clojure.spec.alpha :as s])) + +(s/fdef codec-by-t/put-entry + :args (s/cat :bloom-filter ::ec/bloom-filter) + :ret :blaze.db.kv/write-entry) + +(s/fdef codec-by-t/delete-entry + :args (s/cat :bloom-filter ::ec/bloom-filter) + :ret :blaze.db.kv/write-entry) diff --git a/modules/cql/test/blaze/elm/expression/cache/codec/form_spec.clj b/modules/cql/test/blaze/elm/expression/cache/codec/form_spec.clj new file mode 100644 index 000000000..f54f6cd1b --- /dev/null +++ b/modules/cql/test/blaze/elm/expression/cache/codec/form_spec.clj @@ -0,0 +1,10 @@ +(ns blaze.elm.expression.cache.codec.form-spec + (:require + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.expression.cache.codec.form :as codec-form] + [clojure.spec.alpha :as s])) + +(s/fdef codec-form/hash + :args (s/cat :expr-form ::bloom-filter/expr-form) + :ret ::bloom-filter/hash) diff --git a/modules/cql/test/blaze/elm/expression/cache/codec_spec.clj b/modules/cql/test/blaze/elm/expression/cache/codec_spec.clj new file mode 100644 index 000000000..252234e0f --- /dev/null +++ b/modules/cql/test/blaze/elm/expression/cache/codec_spec.clj @@ -0,0 +1,15 @@ +(ns blaze.elm.expression.cache.codec-spec + (:require + [blaze.db.kv.spec] + [blaze.elm.expression.cache :as-alias ec] + [blaze.elm.expression.cache.bloom-filter.spec] + [blaze.elm.expression.cache.codec :as codec] + [clojure.spec.alpha :as s])) + +(s/fdef codec/put-entry + :args (s/cat :bloom-filter ::ec/bloom-filter) + :ret :blaze.db.kv/write-entry) + +(s/fdef codec/delete-entry + :args (s/cat :bloom-filter ::ec/bloom-filter) + :ret :blaze.db.kv/write-entry) diff --git a/modules/cql/test/blaze/elm/expression/cache_test.clj b/modules/cql/test/blaze/elm/expression/cache_test.clj new file mode 100644 index 000000000..b006372f7 --- /dev/null +++ b/modules/cql/test/blaze/elm/expression/cache_test.clj @@ -0,0 +1,322 @@ +(ns blaze.elm.expression.cache-test + (:require + [blaze.anomaly :as ba] + [blaze.cache-collector.protocols :as ccp] + [blaze.coll.core :as coll] + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] + [blaze.elm.compiler :as c] + [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.expression :as expr] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache-spec] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] + [blaze.elm.expression.cache.codec-spec] + [blaze.elm.expression.cache.codec.by-t-spec] + [blaze.elm.literal :as elm] + [blaze.executors :as ex] + [blaze.fhir.test-util] + [blaze.log] + [blaze.metrics.spec] + [blaze.module.test-util :refer [with-system]] + [blaze.test-util :refer [given-thrown]] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]] + [integrant.core :as ig] + [java-time.api :as time] + [juxt.iota :refer [given]] + [taoensso.timbre :as log]) + (:import + [com.github.benmanes.caffeine.cache AsyncLoadingCache] + [com.github.benmanes.caffeine.cache.stats CacheStats] + [com.google.common.hash HashCode])) + +(set! *warn-on-reflection* true) +(st/instrument) +(ctu/instrument-compile) +(log/set-min-level! :trace) + +(defn- fixture [f] + (st/instrument) + (ctu/instrument-compile) + (f) + (st/unstrument)) + +(test/use-fixtures :each fixture) + +(def ^:private config + (assoc mem-node-config + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {})) + +(deftest init-test + (testing "nil config" + (given-thrown (ig/init {::expr/cache nil}) + :key := ::expr/cache + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map?)) + + (testing "missing config" + (given-thrown (ig/init {::expr/cache {}}) + :key := ::expr/cache + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)))) + + (testing "invalid max size" + (given-thrown (ig/init {::expr/cache {:max-size-in-mb ::invalid}}) + :key := ::expr/cache + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)) + [:cause-data ::s/problems 2 :pred] := `nat-int? + [:cause-data ::s/problems 2 :val] := ::invalid)) + + (testing "invalid refresh" + (given-thrown (ig/init {::expr/cache {:refresh ::invalid}}) + :key := ::expr/cache + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)) + [:cause-data ::s/problems 2 :pred] := `time/duration? + [:cause-data ::s/problems 2 :val] := ::invalid)) + + (testing "init" + (with-system [{::expr/keys [cache]} config] + (is (s/valid? ::expr/cache cache))))) + +(deftest executor-init-test + (testing "nil config" + (given-thrown (ig/init {::ec/executor nil}) + :key := ::ec/executor + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map?)) + + (testing "invalid num-threads" + (given-thrown (ig/init {::ec/executor {:num-threads ::invalid}}) + :key := ::ec/executor + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `pos-int? + [:cause-data ::s/problems 0 :val] := ::invalid))) + +(deftest executor-shutdown-timeout-test + (let [{::ec/keys [executor] :as system} (ig/init {::ec/executor {}})] + + ;; will produce a timeout, because the function runs 11 seconds + (ex/execute! executor #(Thread/sleep 11000)) + + ;; ensure that the function is called before the scheduler is halted + (Thread/sleep 100) + + (ig/halt! system) + + ;; the scheduler is shut down + (is (ex/shutdown? executor)) + + ;; but it isn't terminated yet + (is (not (ex/terminated? executor))))) + +(deftest bloom-filter-creation-duration-seconds-collector-init-test + (with-system [{collector ::ec/bloom-filter-creation-duration-seconds} {::ec/bloom-filter-creation-duration-seconds {}}] + (is (s/valid? :blaze.metrics/collector collector)))) + +(deftest bloom-filter-useful-total-collector-init-test + (with-system [{collector ::ec/bloom-filter-useful-total} {::ec/bloom-filter-useful-total {}}] + (is (s/valid? :blaze.metrics/collector collector)))) + +(deftest bloom-filter-not-useful-total-collector-init-test + (with-system [{collector ::ec/bloom-filter-not-useful-total} {::ec/bloom-filter-not-useful-total {}}] + (is (s/valid? :blaze.metrics/collector collector)))) + +(deftest bloom-filter-false-positive-total-collector-init-test + (with-system [{collector ::ec/bloom-filter-false-positive-total} {::ec/bloom-filter-false-positive-total {}}] + (is (s/valid? :blaze.metrics/collector collector)))) + +(deftest bloom-filter-bytes-collector-init-test + (with-system [{collector ::ec/bloom-filter-bytes} {::ec/bloom-filter-bytes {}}] + (is (s/valid? :blaze.metrics/collector collector)))) + +(def ^:private config + (assoc mem-node-config + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {})) + +(defn- compile-exists-expr [resource-type] + (let [elm (elm/exists (elm/retrieve {:type resource-type}))] + (c/compile {:eval-context "Patient"} elm))) + +(defn- create-bloom-filter! + "Creates the Bloom filters used in `expr` and wait's some time to ensure that + the creation is finished." + [expr cache] + (c/attach-cache expr cache) + (Thread/sleep 100)) + +(deftest get-test + (testing "one Bloom filter on empty database" + (with-system [{::expr/keys [cache]} config] + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (ec/get cache (compile-exists-expr "Observation")) + ::bloom-filter/t := 0 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 0 + ::bloom-filter/mem-size := 11981)))) + +(deftest get-disk-test + (testing "an empty database contains no Bloom filter" + (with-system [{::expr/keys [cache]} config] + (is (ba/not-found? (ec/get-disk cache (HashCode/fromString "d4fc6cde1636852f9e362a68ca7be027a66bf7cb38ebff9c256c3eb2179c2639")))))) + + (testing "one Bloom filter on empty database" + (with-system [{::expr/keys [cache]} config] + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (ec/get-disk cache (HashCode/fromString "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985")) + ::bloom-filter/t := 0 + ::bloom-filter/expr-form := "(exists (retrieve \"Observation\"))" + ::bloom-filter/patient-count := 0 + ::bloom-filter/mem-size := 11990)))) + +(deftest delete-test + (testing "an empty database contains no Bloom filter" + (with-system [{::expr/keys [cache]} config] + (is (ba/not-found? (ec/delete-disk! cache (HashCode/fromString "d4fc6cde1636852f9e362a68ca7be027a66bf7cb38ebff9c256c3eb2179c2639")))))) + + (testing "one Bloom filter on empty database" + (with-system [{::expr/keys [cache]} config] + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (let [hash (HashCode/fromString "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985")] + (is (not (ba/anomaly? (ec/get-disk cache hash)))) + + (ec/delete-disk! cache hash) + + (is (ba/not-found? (ec/get-disk cache hash))))))) + +(deftest list-by-t-test + (testing "an empty database contains zero Bloom filters" + (with-system [{::expr/keys [cache]} config] + (is (coll/empty? (ec/list-by-t cache))))) + + (testing "one Bloom filter on empty database" + (with-system [{::expr/keys [cache]} config] + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/hash str] := "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985" + [0 ::bloom-filter/t] := 0 + [0 ::bloom-filter/expr-form] := "(exists (retrieve \"Observation\"))" + [0 ::bloom-filter/patient-count] := 0 + [0 ::bloom-filter/mem-size] := 11981))) + + (testing "one Bloom filter on database with one patient" + (with-system-data [{::expr/keys [cache]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/hash str] := "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985" + [0 ::bloom-filter/t] := 1 + [0 ::bloom-filter/expr-form] := "(exists (retrieve \"Observation\"))" + [0 ::bloom-filter/patient-count] := 1 + [0 ::bloom-filter/mem-size] := 11981))) + + (testing "two Bloom filters on database with one patient" + (with-system-data [{::expr/keys [cache]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (create-bloom-filter! (compile-exists-expr "Observation") cache) + (create-bloom-filter! (compile-exists-expr "Condition") cache) + + (given (into [] (ec/list-by-t cache)) + count := 2 + [0 ::bloom-filter/hash str] := "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985" + [0 ::bloom-filter/t] := 1 + [0 ::bloom-filter/expr-form] := "(exists (retrieve \"Observation\"))" + [0 ::bloom-filter/patient-count] := 1 + [0 ::bloom-filter/mem-size] := 11981 + [1 ::bloom-filter/hash str] := "b24882a623bc9c78572630b7c5f288553a0c5e31d6c0d9a21e0c3ec43a0d78e7" + [1 ::bloom-filter/t] := 1 + [1 ::bloom-filter/expr-form] := "(exists (retrieve \"Condition\"))" + [1 ::bloom-filter/patient-count] := 0 + [1 ::bloom-filter/mem-size] := 11981))) + + (testing "Bloom filter updates are reflected in the list" + (with-system-data [{::expr/keys [cache] :blaze.db/keys [node]} + (assoc-in config [::expr/cache :refresh] (time/millis 1))] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + @(d/transact node [[:put {:fhir/type :fhir/Patient :id "1"}] + [:put {:fhir/type :fhir/Observation :id "1" + :subject #fhir/Reference{:reference "Patient/1"}}]]) + + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/hash str] := "78c3f9b9e187480870ce815ad6d324713dfa2cbd12968c5b14727fef7377b985" + [0 ::bloom-filter/t] := 2 + [0 ::bloom-filter/expr-form] := "(exists (retrieve \"Observation\"))" + [0 ::bloom-filter/patient-count] := 2 + [0 ::bloom-filter/mem-size] := 11981))) + + (testing "an old Bloom filter is loaded from the store even if the t was increased in the meantime" + (with-system-data [{::expr/keys [cache] :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (testing "creates the Bloom filter with t=1" + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/t] := 1)) + + ;; invalidates the cache + (.invalidateAll (.synchronous ^AsyncLoadingCache (:mem-cache cache))) + + ;; advances the database + @(d/transact node [[:put {:fhir/type :fhir/Patient :id "1"}] + [:put {:fhir/type :fhir/Observation :id "1" + :subject #fhir/Reference{:reference "Patient/1"}}]]) + + (testing "doesn't create a new Bloom filter because the old one is still in the store" + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/t] := 1))))) + +(deftest total-test + (testing "an empty database contains zero Bloom filters" + (with-system [{::expr/keys [cache]} config] + (is (zero? (ec/total cache))))) + + (testing "one Bloom filter on empty database" + (with-system [{::expr/keys [cache]} config] + (create-bloom-filter! (compile-exists-expr "Observation") cache) + + (is (= 1 (ec/total cache)))))) + +(deftest stats-test + (with-system [{::expr/keys [cache]} config] + (is (zero? (.hitCount ^CacheStats (ccp/-stats cache)))) + (is (zero? (ccp/-estimated-size cache))))) diff --git a/modules/cql/test/blaze/elm/literal.clj b/modules/cql/test/blaze/elm/literal.clj index 6d39f94ce..45fd03868 100644 --- a/modules/cql/test/blaze/elm/literal.clj +++ b/modules/cql/test/blaze/elm/literal.clj @@ -394,11 +394,26 @@ {:type "ReplaceMatches" :operand ops}) +;; 17.14. Split +(defn split [[s separator]] + {:type "Split" + :stringToSplit s + :separator separator}) + ;; 17.16. StartsWith (defn starts-with [ops] {:type "StartsWith" :operand ops}) +;; 17.17. Substring +(defn substring [[s start-index length]] + (cond-> + {:type "Substring" + :stringToSub s + :startIndex start-index} + length + (assoc :length length))) + ;; 17.18. Upper (defn upper [x] {:type "Upper" @@ -671,6 +686,10 @@ (defn singleton-from [list] {:type "SingletonFrom" :operand list}) +;; 20.26. Slice +(defn slice [[source start-index end-index]] + {:type "Slice" :source source :startIndex start-index :endIndex end-index}) + ;; 20.28. Times (defn times [lists] {:type "Times" :operand lists}) diff --git a/modules/cql/test/blaze/elm/quantity_test.clj b/modules/cql/test/blaze/elm/quantity_test.clj index 9600a0f13..25b4e3fe3 100644 --- a/modules/cql/test/blaze/elm/quantity_test.clj +++ b/modules/cql/test/blaze/elm/quantity_test.clj @@ -1,7 +1,8 @@ (ns blaze.elm.quantity-test (:require + [blaze.elm.compiler.test-util :refer [has-form]] [blaze.elm.protocols :as p] - [blaze.elm.quantity :as quantity] + [blaze.elm.quantity :refer [quantity]] [blaze.test-util :as tu] [clojure.java.io :as io] [clojure.spec.test.alpha :as st] @@ -16,7 +17,7 @@ (deftest quantity-test (testing "Commonly Used UCUM Codes for Healthcare Units" (testing "special units" - (are [unit] (quantity/quantity 1 unit) + (are [unit] (quantity 1 unit) "U/L" "10*3/uL" "mm[Hg]")) @@ -26,22 +27,25 @@ (drop 1) (map #(str/split % #"\t")) (map first) - (map #(try (quantity/quantity 1 %) (catch Exception e (ex-data e)))) + (map #(try (quantity 1 %) (catch Exception e (ex-data e)))) (filter ::anom/category) (map :unit) (count) (= 20) - (is))))) + (is)))) + + (testing "form" + (has-form (quantity 1M "m") '(quantity 1M "m")))) ;; 2.3. Property (deftest property-test (testing "the value of a quantity is always a BigDecimal" (are [quantity] (= BigDecimal (class (p/get quantity :value))) - (quantity/quantity 1M "m") - (quantity/quantity 1 "m") - (quantity/quantity (int 1) "m") - (p/divide (quantity/quantity 1M "m") (quantity/quantity 1M "s")) - (p/divide (quantity/quantity 1M "m") (quantity/quantity 2M "s")))) + (quantity 1M "m") + (quantity 1 "m") + (quantity (int 1) "m") + (p/divide (quantity 1M "m") (quantity 1M "s")) + (p/divide (quantity 1M "m") (quantity 2M "s")))) (testing "get on unknown key returns nil" - (is (nil? (p/get (quantity/quantity 1M "m") ::unknown))))) + (is (nil? (p/get (quantity 1M "m") ::unknown))))) diff --git a/modules/cql/test/blaze/elm/ratio_spec.clj b/modules/cql/test/blaze/elm/ratio_spec.clj index 0043b358b..6a8480a7b 100644 --- a/modules/cql/test/blaze/elm/ratio_spec.clj +++ b/modules/cql/test/blaze/elm/ratio_spec.clj @@ -1,15 +1,10 @@ (ns blaze.elm.ratio-spec - (:refer-clojure :exclude [ratio?]) (:require [blaze.anomaly-spec] [blaze.elm.quantity :as quantity] + [blaze.elm.quantity-spec] [blaze.elm.ratio :as ratio] - [clojure.spec.alpha :as s]) - (:import - [blaze.elm.ratio Ratio])) - -(defn ratio? [x] - (instance? Ratio x)) + [clojure.spec.alpha :as s])) (s/fdef ratio/ratio :args (s/cat :numerator quantity/quantity? :denominator quantity/quantity?)) diff --git a/modules/cql/test/blaze/elm/ratio_test.clj b/modules/cql/test/blaze/elm/ratio_test.clj new file mode 100644 index 000000000..db2bb88b8 --- /dev/null +++ b/modules/cql/test/blaze/elm/ratio_test.clj @@ -0,0 +1,18 @@ +(ns blaze.elm.ratio-test + (:require + [blaze.elm.compiler :as c] + [blaze.elm.quantity :refer [quantity]] + [blaze.elm.ratio :refer [ratio]] + [blaze.elm.ratio-spec] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(deftest to-ratio-test + (testing "attach-cache" + (let [ratio (ratio (quantity 1 "1") (quantity 2 "1"))] + (is (= [ratio] (st/with-instrument-disabled (c/attach-cache ratio ::cache))))))) diff --git a/modules/cql/test/blaze/elm/resource_test.clj b/modules/cql/test/blaze/elm/resource_test.clj new file mode 100644 index 000000000..2c25c3e0a --- /dev/null +++ b/modules/cql/test/blaze/elm/resource_test.clj @@ -0,0 +1,30 @@ +(ns blaze.elm.resource-test + (:require + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] + [blaze.elm.compiler :as c] + [blaze.elm.expression-spec] + [blaze.elm.resource :as cr] + [blaze.elm.resource-spec] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]])) + +(set! *warn-on-reflection* true) +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(defn- resource [db type id] + (cr/mk-resource db (d/resource-handle db type id))) + +(deftest resource-test + (with-system-data [{:blaze.db/keys [node]} mem-node-config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + + (let [resource (resource (d/db node) "Patient" "0")] + (testing "attach-cache" + (is (= [resource] (st/with-instrument-disabled (c/attach-cache resource ::cache))))) + + (testing "toString" + (is (= "Patient[id = 0, t = 1, last-change-t = 1]" (str resource))))))) diff --git a/modules/cql/test/data_readers.clj b/modules/cql/test/data_readers.clj index b42869f8b..08a9d1aa3 100644 --- a/modules/cql/test/data_readers.clj +++ b/modules/cql/test/data_readers.clj @@ -29,6 +29,7 @@ elm/not blaze.elm.literal/not elm/or blaze.elm.literal/or elm/xor blaze.elm.literal/xor + elm/coalesce blaze.elm.literal/coalesce elm/is-false blaze.elm.literal/is-false elm/is-null blaze.elm.literal/is-null elm/is-true blaze.elm.literal/is-true @@ -42,6 +43,7 @@ elm/lower blaze.elm.literal/lower elm/matches blaze.elm.literal/matches elm/starts-with blaze.elm.literal/starts-with + elm/substring blaze.elm.literal/substring elm/upper blaze.elm.literal/upper elm/add blaze.elm.literal/add elm/ceiling blaze.elm.literal/ceiling @@ -76,8 +78,11 @@ elm/distinct blaze.elm.literal/distinct elm/exists blaze.elm.literal/exists elm/first blaze.elm.literal/first + elm/flatten blaze.elm.literal/flatten + elm/index-of blaze.elm.literal/index-of elm/last blaze.elm.literal/last elm/singleton-from blaze.elm.literal/singleton-from + elm/slice blaze.elm.literal/slice elm/all-true blaze.elm.literal/all-true elm/any-true blaze.elm.literal/any-true elm/avg blaze.elm.literal/avg diff --git a/modules/db-protocols/src/blaze/db/impl/protocols.clj b/modules/db-protocols/src/blaze/db/impl/protocols.clj index 0ada5fc25..1432c8d2e 100644 --- a/modules/db-protocols/src/blaze/db/impl/protocols.clj +++ b/modules/db-protocols/src/blaze/db/impl/protocols.clj @@ -23,6 +23,8 @@ [db compartment tid] [db compartment tid start-id]) + (-patient-compartment-last-change-t [db patient-id]) + (-count-query [db query] "Returns a CompletableFuture that will complete with the count of the matching resource handles.") @@ -110,4 +112,4 @@ (-list-by-type [_ type]) (-list-by-target [_ target]) (-linked-compartments [_ resource]) - (-compartment-resources [_ type])) + (-compartment-resources [_ compartment-type] [_ compartment-type type])) diff --git a/modules/db-stub/src/blaze/db/api_stub.clj b/modules/db-stub/src/blaze/db/api_stub.clj index 429d53d44..2719526a2 100644 --- a/modules/db-stub/src/blaze/db/api_stub.clj +++ b/modules/db-stub/src/blaze/db/api_stub.clj @@ -27,6 +27,7 @@ :kv-store (ig/ref :blaze.db/index-kv-store) :resource-indexer (ig/ref ::node/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} ::tx-log/local @@ -56,8 +57,11 @@ :resource-as-of-index nil :type-as-of-index nil :system-as-of-index nil + :patient-last-change-index nil :type-stats-index nil - :system-stats-index nil}} + :system-stats-index nil + :cql-bloom-filter nil + :cql-bloom-filter-by-t nil}} ::rs/kv {:kv-store (ig/ref :blaze.db/resource-kv-store) @@ -77,7 +81,9 @@ :blaze.db.node.resource-indexer/executor {} :blaze.db/search-param-registry - {:structure-definition-repo structure-definition-repo}}) + {:structure-definition-repo structure-definition-repo} + + :blaze/scheduler {}}) (defmacro with-system-data "Runs `body` inside a system that is initialized from `config`, bound to diff --git a/modules/db/.clj-kondo/config.edn b/modules/db/.clj-kondo/config.edn index aa614a66c..3c358d626 100644 --- a/modules/db/.clj-kondo/config.edn +++ b/modules/db/.clj-kondo/config.edn @@ -2,12 +2,12 @@ ["../../../.clj-kondo/root" "../../anomaly/resources/clj-kondo.exports/blaze/anomaly" "../../async/resources/clj-kondo.exports/blaze/async" + "../../coll/resources/clj-kondo.exports/blaze/coll" "../../module-base/resources/clj-kondo.exports/prom-metrics/prom-metrics" "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"] :lint-as {blaze.db.api-test-perf/with-system-data clojure.core/with-open - blaze.db.impl.db/with-open-coll clojure.core/with-open blaze.db.test-util/with-system-data clojure.core/with-open} :linters diff --git a/modules/db/NOTES.md b/modules/db/NOTES.md deleted file mode 100644 index dee7125e5..000000000 --- a/modules/db/NOTES.md +++ /dev/null @@ -1,125 +0,0 @@ -# Blaze - -## Features - -### Versioned read of resources - -* I need to get to a particular version of a resource -* versionId could be the content-hash - -### Normal read returning the last known Version of a Resource - -* need to get the content-hash of a resource given a t -* can be done with the ResourceAsOf index - -### Search - -* stable search also in recent past (at given t) -* - -## Principles - -* don't optimize for deleted resources because deleting a resource is not common - -## Indices - -### Independent from t - -| Name | Key Parts | Value | -|---|---|---| -| SVR | c-hash tid value id hash-prefix | - | -| RSV | tid id hash-prefix c-hash value | - | -| CSVR | co-c-hash co-res-id sp-c-hash tid value id hash-prefix | - | -| CompartmentResource | co-c-hash co-res-id tid id | - | -| SearchParam | code tid | id | -| ActiveSearchParams | id | - | - -### Depend on t - -| Name | Key Parts | Value | -|---|---|---| -| TxSuccess | t | transaction | -| TxError | t | anomaly | -| TByInstant | inst-ms (desc) | t | -| ResourceAsOf | tid id t | hash, state | -| TypeAsOf | tid t id | hash, state | -| SystemAsOf | t tid id | hash, state | -| TypeStats | tid t | total, num-changes | -| SystemStats | t | total, num-changes | - -We can make hashes in SearchParam indices shorter (4-bytes) because we only need to differentiate between the versions of a resource. The odds of a hash collision is 1 out of 10000 for about 1000 versions. In case of a hash collision we would produce a false positive query hit. So we would return more resources instead of less, which is considered fine in FHIR. - -### Search param Value Resource version (SVR) - -The key consists of: - -* c-hash - a 4-byte hash of the code of the search parameter -* tid - the 4-byte type id -* value - the value encoded depending on the search parameter -* id - the logical id of the resource -* hash-prefix - a 4-byte prefix of the resource content hash - -The total size of the key is 4 + 4 + value-size + id-size + 4 = 12 + value-size + id-size bytes. - -The value is empty. - -The key contains the id of the resource for two reasons, first we can skip to the next resource by seeking with max-hash, not having to test all versions of a resource against ResourceAsOf and second, going into ResourceAsOf will be local because it is sorted by id. - -The SVR index is comparable to the AVET index in Datomic. Search parameters are the equivalent of indexed attributes in Datomic. - -### Resource version Search param Value (RSV) - - - -### Compartment Search-param Value Resource (CSVR) - -Same as the SVR index but prefixed with a compartment the resource belongs to. This index is used in [variant searches][2] and in CQL evaluation within the Patient context. In the CQL Patient context all retrieves are relative to one patient. Using that patient as compartment in the CSVR index allows for efficient implementation of that retrieves. - -The key consists of: - -* co-c-hash - a 4-byte hash of the code of the compartment -* co-res-id - the logical id of the resource of the compartment -* c-hash - a 4-byte hash of the code of the search parameter -* tid - the 4-byte type id -* value - the value encoded depending on the search parameter -* id - the logical id of the resource -* hash-prefix - a 4-byte prefix of the resource content hash - -The total size of the key is 4 + co-res-id-size 4 + 4 + value-size + id-size + 4 = 16 + co-res-id-size 4 + value-size + id-size bytes. - -The value is empty. - -### TByInstant - -Provides access to t's by instant (point in time). It encodes the instant as milliseconds since epoch as descending long from Long/MAX_VALUE so that TODO WHY??? - -### ResourceAsOf - -The key consists of: - -* tid - the 4-byte type id -* id - the variable length id (max 64 byte) - - -### TxSuccess - -### TxError - -### TypeStats / SystemStats - -total = number of non-deleted resources -num-changes = total number of changes (creates, updates, deletes) - -## Search - -The FHIR search parameters have different types. The search implementation depends on that types. The following sections describe the implementation by type. - -### Date - -The date search parameter type is used for the data types date, dateTime, instant, Period and Timing. The search is always performed against a range. Both the value given in the search and the target value in resources have either an implicit or an explicit range. For example the range of a date like 2020-02-09 starts at 2020-02-09T00:00:00.000 and ends at 2020-02-09T23:59:59.999. - -By default the search is an equal search were the range of the search value have to fully contain the range of the target value. In addition to the equal search, other search operators are possible. - - -[1]: -[2]: diff --git a/modules/db/deps.edn b/modules/db/deps.edn index 8cc5cfb6b..05ddc9e45 100644 --- a/modules/db/deps.edn +++ b/modules/db/deps.edn @@ -37,6 +37,9 @@ blaze/db-resource-store {:local/root "../db-resource-store"} + blaze/scheduler + {:local/root "../scheduler"} + blaze/spec {:local/root "../spec"} diff --git a/modules/db/src/blaze/db/api.clj b/modules/db/src/blaze/db/api.clj index 20641a401..dc4c02e15 100644 --- a/modules/db/src/blaze/db/api.clj +++ b/modules/db/src/blaze/db/api.clj @@ -264,6 +264,14 @@ [node-or-db code type clauses] (p/-compile-compartment-query-lenient node-or-db code type clauses)) +;; ---- Patient-Compartment-Level Functions ----------------------------------- + +(defn patient-compartment-last-change-t + "Returns the `t` of last change of any resource in the patient compartment or + nil if the patient has no resources." + [db patient-id] + (p/-patient-compartment-last-change-t db (codec/id-byte-string patient-id))) + ;; ---- Common Query Functions ------------------------------------------------ (defn count-query diff --git a/modules/db/src/blaze/db/api_spec.clj b/modules/db/src/blaze/db/api_spec.clj index 701774c8a..c2f1dec3e 100644 --- a/modules/db/src/blaze/db/api_spec.clj +++ b/modules/db/src/blaze/db/api_spec.clj @@ -130,6 +130,10 @@ :clauses :blaze.db.query/clauses) :ret (s/or :query :blaze.db/query :anomaly ::anom/anomaly)) +(s/fdef d/patient-compartment-last-change-t + :args (s/cat :db :blaze.db/db :patient-id :blaze.resource/id) + :ret (s/nilable :blaze.db/t)) + ;; ---- Common Query Functions ------------------------------------------------ (s/fdef d/execute-query diff --git a/modules/db/src/blaze/db/impl/batch_db.clj b/modules/db/src/blaze/db/impl/batch_db.clj index 06963e211..cc6fdd39d 100644 --- a/modules/db/src/blaze/db/impl/batch_db.clj +++ b/modules/db/src/blaze/db/impl/batch_db.clj @@ -13,6 +13,7 @@ [blaze.db.impl.codec :as codec] [blaze.db.impl.index :as index] [blaze.db.impl.index.compartment.resource :as cr] + [blaze.db.impl.index.patient-last-change :as plc] [blaze.db.impl.index.resource-as-of :as rao] [blaze.db.impl.index.resource-handle :as rh] [blaze.db.impl.index.search-param-value-resource :as sp-vr] @@ -108,6 +109,12 @@ (-compartment-resource-handles [db compartment tid start-id] (cr/resource-handles db compartment tid start-id)) + ;; ---- Patient-Compartment-Level Functions --------------------------------- + + (-patient-compartment-last-change-t [_ patient-id] + (with-open [plci (kv/new-iterator snapshot :patient-last-change-index)] + (plc/last-change-t plci patient-id t))) + ;; ---- Common Query Functions ---------------------------------------------- (-count-query [db query] diff --git a/modules/db/src/blaze/db/impl/db.clj b/modules/db/src/blaze/db/impl/db.clj index 16aee6b42..51f0683aa 100644 --- a/modules/db/src/blaze/db/impl/db.clj +++ b/modules/db/src/blaze/db/impl/db.clj @@ -2,29 +2,19 @@ "Primary Database Implementation" (:require [blaze.async.comp :as ac :refer [do-sync]] + [blaze.coll.core :refer [with-open-coll]] [blaze.db.impl.batch-db :as batch-db] + [blaze.db.impl.index.patient-last-change :as plc] [blaze.db.impl.index.system-stats :as system-stats] [blaze.db.impl.index.type-stats :as type-stats] [blaze.db.impl.protocols :as p] [blaze.db.kv :as kv]) (:import - [clojure.lang IReduceInit Sequential] [java.io Writer])) (set! *warn-on-reflection* true) (set! *unchecked-math* :warn-on-boxed) -(defmacro with-open-coll - "Like `clojure.core/with-open` but opens and closes the resources on every - reduce call to `coll`." - [bindings coll] - `(reify - Sequential - IReduceInit - (reduce [_ rf# init#] - (with-open ~bindings - (reduce rf# init# ~coll))))) - (deftype Db [node kv-store basis-t t] p/Db (-node [_] @@ -84,6 +74,13 @@ (with-open-coll [batch-db (batch-db/new-batch-db node basis-t t)] (p/-compartment-resource-handles batch-db compartment tid start-id))) + ;; ---- Patient-Compartment-Level Functions --------------------------------- + + (-patient-compartment-last-change-t [_ patient-id] + (with-open [snapshot (kv/new-snapshot kv-store) + plci (kv/new-iterator snapshot :patient-last-change-index)] + (plc/last-change-t plci patient-id t))) + ;; ---- Common Query Functions ---------------------------------------------- (-count-query [_ query] diff --git a/modules/db/src/blaze/db/impl/index/patient_last_change.clj b/modules/db/src/blaze/db/impl/index/patient_last_change.clj new file mode 100644 index 000000000..8f5421678 --- /dev/null +++ b/modules/db/src/blaze/db/impl/index/patient_last_change.clj @@ -0,0 +1,64 @@ +(ns blaze.db.impl.index.patient-last-change + "Functions for accessing the PatientLastChange index." + (:require + [blaze.byte-buffer :as bb] + [blaze.byte-string :as bs] + [blaze.db.impl.bytes :as bytes] + [blaze.db.impl.codec :as codec] + [blaze.db.kv :as kv]) + (:import + [java.nio.charset StandardCharsets])) + +(set! *warn-on-reflection* true) +(set! *unchecked-math* :warn-on-boxed) + +(defn- encode-key [patient-id t] + (-> (bb/allocate (unchecked-add-int (bs/size patient-id) codec/t-size)) + (bb/put-byte-string! patient-id) + (bb/put-long! (codec/descending-long ^long t)) + bb/array)) + +(defn index-entry [patient-id t] + [:patient-last-change-index (encode-key patient-id t) bytes/empty]) + +(defn last-change-t + "Returns the `t` of last change of any resource in the patient compartment not + newer than `t` or nil if the patient has no resources." + [plci patient-id t] + (kv/seek! plci (encode-key patient-id t)) + (when (kv/valid? plci) + (let [bb (bb/wrap (kv/key plci)) + patient-id-size (bs/size patient-id)] + (when (and (< patient-id-size (bb/remaining bb)) + (= patient-id (bs/from-byte-buffer! bb patient-id-size))) + (-> (bb/get-long! bb) + (codec/descending-long)))))) + +(def ^:private state-key + (.getBytes "patient-last-change-state" StandardCharsets/ISO_8859_1)) + +(defn- encode-state [{:keys [type t]}] + (if (identical? :current type) + (byte-array [0]) + (-> (bb/allocate (inc Long/BYTES)) + (bb/put-byte! 1) + (bb/put-long! t) + bb/array))) + +(defn decode-state [bytes] + (let [buf (bb/wrap bytes)] + (if (zero? (bb/get-byte! buf)) + {:type :current} + {:type :building + :t (bb/get-long! buf)}))) + +(defn state + "Returns the state of the PatientLastChange index. + + The initial state is `{:type :building :t 0}`." + [kv-store] + (or (some-> (kv/get kv-store :default state-key) decode-state) + {:type :building :t 0})) + +(defn state-index-entry [state] + [:default state-key (encode-state state)]) diff --git a/modules/db/src/blaze/db/node.clj b/modules/db/src/blaze/db/node.clj index 0ef3b8124..f07ce5c6f 100644 --- a/modules/db/src/blaze/db/node.clj +++ b/modules/db/src/blaze/db/node.clj @@ -9,12 +9,14 @@ [blaze.db.impl.codec :as codec] [blaze.db.impl.db :as db] [blaze.db.impl.index :as index] + [blaze.db.impl.index.patient-last-change :as plc] [blaze.db.impl.index.resource-handle :as rh] [blaze.db.impl.index.t-by-instant :as t-by-instant] [blaze.db.impl.index.tx-error :as tx-error] [blaze.db.impl.index.tx-success :as tx-success] [blaze.db.impl.protocols :as p] [blaze.db.kv :as kv] + [blaze.db.node.patient-last-change-index :as node-plc] [blaze.db.node.protocols :as np] [blaze.db.node.resource-indexer :as resource-indexer] [blaze.db.node.resource-indexer.spec] @@ -33,6 +35,7 @@ [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type :as type] [blaze.module :as m :refer [reg-collector]] + [blaze.scheduler :as sched] [blaze.spec] [blaze.util :refer [conj-vec]] [clojure.spec.alpha :as s] @@ -103,9 +106,9 @@ (remove-watch state future)))) future)) -(defn- index-tx [db-before tx-data] +(defn- index-tx [search-param-registry db-before tx-data] (with-open [_ (prom/timer duration-seconds "index-transactions")] - (tx-indexer/index-tx db-before tx-data))) + (tx-indexer/index-tx search-param-registry db-before tx-data))) (defn- advance-t! [state t] (log/trace "advance state to t =" t) @@ -150,13 +153,13 @@ "This is the main transaction handling function. If indexes resources and transaction data and commits either success or error." - [{:keys [resource-indexer kv-store] :as node} + [{:keys [resource-indexer search-param-registry kv-store] :as node} {:keys [t instant tx-cmds] :as tx-data}] (log/trace "index transaction with t =" t "and" (count tx-cmds) "command(s)") (prom/observe! transaction-sizes (count tx-cmds)) (let [timer (prom/timer duration-seconds "index-resources") future (resource-indexer/index-resources resource-indexer tx-data) - result (index-tx (np/-db node) tx-data)] + result (index-tx search-param-registry (np/-db node) tx-data)] (if (ba/anomaly? result) (commit-error! node t result) (do @@ -400,19 +403,6 @@ :or {enforce-referential-integrity true}}] {:blaze.db/enforce-referential-integrity enforce-referential-integrity}) -(defmethod m/pre-init-spec :blaze.db/node [_] - (s/keys - :req-un - [:blaze.db/tx-log - :blaze.db/tx-cache - ::indexer-executor - :blaze.db/kv-store - ::resource-indexer - :blaze.db/resource-store - :blaze.db/search-param-registry] - :opt-un - [:blaze.db/enforce-referential-integrity])) - (def ^:private expected-kv-store-version 0) (defn- kv-store-version [kv-store] @@ -450,9 +440,56 @@ (fn sync-standalone [^Node node] (ac/completed-future (db/db node (:t @(.-state node))))))) +(defn- index-patient-last-change-index! + [{:keys [kv-store] :as node} current-t {:keys [t] :as tx-data}] + (log/trace "Build PatientLastChange index with t =" t) + (when-ok [entries (node-plc/index-entries node tx-data)] + (store-tx-entries! kv-store entries)) + (vreset! current-t t)) + +(defn- poll-and-index-patient-last-change-index! + [node queue current-t poll-timeout] + (run! (partial index-patient-last-change-index! node current-t) + (poll-tx-queue! queue poll-timeout))) + +(defn build-patient-last-change-index + [{:keys [tx-log kv-store run? state poll-timeout] :as node}] + (let [{:keys [type t]} (plc/state kv-store)] + (when (identical? :building type) + (let [start-t (inc t) + end-t (:t @state) + current-t (volatile! start-t)] + (log/info "Building PatientLastChange index starting at t =" start-t) + (with-open [queue (tx-log/new-queue tx-log start-t)] + (while (and @run? (< @current-t end-t)) + (try + (poll-and-index-patient-last-change-index! node queue current-t poll-timeout) + (catch Exception e + (log/error "Error while building the PatientLastChange index." e))))) + (if (>= @current-t end-t) + (do + (store-tx-entries! kv-store [(plc/state-index-entry {:type :current})]) + (log/info "Finished building PatientLastChange index.")) + (log/info "Partially build PatientLastChange index up to t =" @current-t + "at a goal of t =" end-t "Will continue at next start.")))))) + +(defmethod m/pre-init-spec :blaze.db/node [_] + (s/keys + :req-un + [:blaze.db/tx-log + :blaze.db/tx-cache + ::indexer-executor + :blaze.db/kv-store + ::resource-indexer + :blaze.db/resource-store + :blaze.db/search-param-registry + :blaze/scheduler] + :opt-un + [:blaze.db/enforce-referential-integrity])) + (defmethod ig/init-key :blaze.db/node [_ {:keys [storage tx-log tx-cache indexer-executor kv-store resource-indexer - resource-store search-param-registry poll-timeout] + resource-store search-param-registry scheduler poll-timeout] :or {poll-timeout (time/seconds 1)} :as config}] (init-msg config) @@ -463,6 +500,8 @@ (volatile! true) poll-timeout (ac/future))] + (when (= :building (:type (plc/state kv-store))) + (sched/submit scheduler #(build-patient-last-change-index node))) (execute node indexer-executor) node)) diff --git a/modules/db/src/blaze/db/node/patient_last_change_index.clj b/modules/db/src/blaze/db/node/patient_last_change_index.clj new file mode 100644 index 000000000..3c634cddd --- /dev/null +++ b/modules/db/src/blaze/db/node/patient_last_change_index.clj @@ -0,0 +1,13 @@ +(ns blaze.db.node.patient-last-change-index + (:require + [blaze.anomaly :refer [when-ok]] + [blaze.db.impl.db :as db] + [blaze.db.impl.index.patient-last-change :as plc] + [blaze.db.node.tx-indexer :as tx-indexer])) + +(defn index-entries + {:arglists '([node tx-data])} + [{:keys [search-param-registry] :as node} {:keys [t] :as tx-data}] + (when-ok [entries (tx-indexer/index-tx search-param-registry (db/db node (dec t)) tx-data)] + (-> (filterv (comp #{:patient-last-change-index} first) entries) + (conj (plc/state-index-entry {:type :building :t t}))))) diff --git a/modules/db/src/blaze/db/node/tx_indexer.clj b/modules/db/src/blaze/db/node/tx_indexer.clj index 59a07178c..65d3b30ae 100644 --- a/modules/db/src/blaze/db/node/tx_indexer.clj +++ b/modules/db/src/blaze/db/node/tx_indexer.clj @@ -5,7 +5,7 @@ [taoensso.timbre :as log])) (defn index-tx - [db-before {:keys [t tx-cmds]}] + [search-param-registry db-before {:keys [t tx-cmds]}] (log/trace "verify transaction commands with t =" t "based on db with t =" (d/basis-t db-before)) - (verify/verify-tx-cmds db-before t tx-cmds)) + (verify/verify-tx-cmds search-param-registry db-before t tx-cmds)) diff --git a/modules/db/src/blaze/db/node/tx_indexer/verify.clj b/modules/db/src/blaze/db/node/tx_indexer/verify.clj index 0894e78d4..1a9ae55b5 100644 --- a/modules/db/src/blaze/db/node/tx_indexer/verify.clj +++ b/modules/db/src/blaze/db/node/tx_indexer/verify.clj @@ -3,11 +3,13 @@ [blaze.anomaly :as ba :refer [throw-anom]] [blaze.db.api :as d] [blaze.db.impl.codec :as codec] + [blaze.db.impl.index.patient-last-change :as plc] [blaze.db.impl.index.resource-handle :as rh] [blaze.db.impl.index.rts-as-of :as rts] [blaze.db.impl.index.system-stats :as system-stats] [blaze.db.impl.index.type-stats :as type-stats] [blaze.db.kv.spec] + [blaze.db.search-param-registry :as sr] [blaze.fhir.hash :as hash] [blaze.util :as u] [clojure.string :as str] @@ -98,8 +100,8 @@ statistics of the transaction outcome. Throws an anomaly on conflicts." - {:arglists '([db-before t res cmd])} - (fn [_db-before _t _res {:keys [op]}] op)) + {:arglists '([search-param-registry db-before t res cmd])} + (fn [_search-param-registry _db-before _t _res {:keys [op]}] op)) (defn- verify-tx-cmd-create-msg [type id] (format "verify-tx-cmd :create %s/%s" type id)) @@ -111,18 +113,29 @@ (when (d/resource-handle db type id) (throw-anom (ba/conflict (id-collision-msg type id (d/t db)))))) -(defn- index-entries [tid id t hash num-changes op] - (rts/index-entries tid (codec/id-byte-string id) t hash num-changes op)) +(defn- index-entries + "Creates index entries for the resource with `tid` and `id`. + + `refs` are used to update the PatientLastChange index in case a Patient is + referenced." + [tid id t hash num-changes op refs] + (let [id (codec/id-byte-string id)] + (into + (rts/index-entries tid id t hash num-changes op) + (keep (fn [[ref-type ref-id]] + (when (= "Patient" ref-type) + (plc/index-entry (codec/id-byte-string ref-id) t)))) + refs))) (def ^:private inc-0 (fnil inc 0)) (defmethod verify-tx-cmd "create" - [db-before t res {:keys [type id hash]}] + [_search-param-registry db-before t res {:keys [type id hash refs]}] (log/trace (verify-tx-cmd-create-msg type id)) (with-open [_ (prom/timer duration-seconds "verify-create")] (check-id-collision! db-before type id) (let [tid (codec/tid type)] - (-> (update res :entries into (index-entries tid id t hash 1 :create)) + (-> (update res :entries into (index-entries tid id t hash 1 :create refs)) (update :new-resources conj [type id]) (update-in [:stats tid :num-changes] inc-0) (update-in [:stats tid :total] inc-0))))) @@ -159,7 +172,8 @@ (ba/conflict (precondition-version-failed-msg type id if-none-match) :http/status 412)) (defmethod verify-tx-cmd "put" - [db-before t res {:keys [type id hash if-match if-none-match] :as tx-cmd}] + [_search-param-registry db-before t res + {:keys [type id hash if-match if-none-match refs] :as tx-cmd}] (log/trace (verify-tx-cmd-put-msg type id (u/to-seq if-match) if-none-match)) (with-open [_ (prom/timer duration-seconds "verify-put")] (let [tid (codec/tid type) @@ -181,7 +195,7 @@ :else (cond-> - (-> (update res :entries into (index-entries tid id t hash (inc num-changes) :put)) + (-> (update res :entries into (index-entries tid id t hash (inc num-changes) :put refs)) (update :new-resources conj [type id]) (update-in [:stats tid :num-changes] inc-0)) (or (nil? old-t) (identical? :delete op)) @@ -193,7 +207,7 @@ (format "verify-tx-cmd :keep %s/%s" type id))) (defmethod verify-tx-cmd "keep" - [db-before _ res {:keys [type id hash if-match] :as tx-cmd}] + [_search-param-registry db-before _ res {:keys [type id hash if-match] :as tx-cmd}] (log/trace (verify-tx-cmd-keep-msg type id (u/to-seq if-match))) (with-open [_ (prom/timer duration-seconds "verify-keep")] (let [if-match (u/to-seq if-match) @@ -210,27 +224,37 @@ :else res)))) +(defn- patient-refs + "Returns references from `resource-handle` to Patient resources." + [search-param-registry db type resource-handle] + (into + [] + (comp (mapcat #(d/include db resource-handle % "Patient")) + (map (fn [{:keys [id]}] ["Patient" id]))) + (sr/compartment-resources search-param-registry "Patient" type))) + (defmethod verify-tx-cmd "delete" - [db-before t res {:keys [type id]}] + [search-param-registry db-before t res {:keys [type id]}] (log/trace "verify-tx-cmd :delete" (str type "/" id)) (with-open [_ (prom/timer duration-seconds "verify-delete")] (let [tid (codec/tid type) - {:keys [num-changes op] :or {num-changes 0}} - (d/resource-handle db-before type id)] + {:keys [num-changes op] :or {num-changes 0} :as old-resource-handle} + (d/resource-handle db-before type id) + refs (some->> old-resource-handle (patient-refs search-param-registry db-before type))] (cond-> - (-> (update res :entries into (index-entries tid id t hash/deleted-hash (inc num-changes) :delete)) + (-> (update res :entries into (index-entries tid id t hash/deleted-hash (inc num-changes) :delete refs)) (update :del-resources conj [type id]) (update-in [:stats tid :num-changes] inc-0)) (and op (not (identical? :delete op))) (update-in [:stats tid :total] (fnil dec 0)))))) (defmethod verify-tx-cmd :default - [_db-before _t res _tx-cmd] + [_search-param-registry _db-before _t res _tx-cmd] res) -(defn- verify-tx-cmds** [db-before t tx-cmds] +(defn- verify-tx-cmds** [search-param-registry db-before t tx-cmds] (reduce - (partial verify-tx-cmd db-before t) + (partial verify-tx-cmd search-param-registry db-before t) {:entries [] :new-resources #{} :del-resources #{}} @@ -309,11 +333,11 @@ db new-resources del-resources type id refs)))) cmds)) -(defn- verify-tx-cmds* [db-before t cmds] +(defn- verify-tx-cmds* [search-param-registry db-before t cmds] (ba/try-anomaly (let [cmds (resolve-ids db-before cmds)] (detect-duplicate-commands! cmds) - (let [res (verify-tx-cmds** db-before t cmds)] + (let [res (verify-tx-cmds** search-param-registry db-before t cmds)] (check-referential-integrity! db-before res cmds) (post-process-res db-before t res))))) @@ -322,7 +346,7 @@ outcome if it is successful or an anomaly if it fails. The `t` is for the new transaction to commit." - [db-before t cmds] + [search-param-registry db-before t cmds] (with-open [_ (prom/timer duration-seconds "verify-tx-cmds") batch-db-before (d/new-batch-db db-before)] - (verify-tx-cmds* batch-db-before t cmds))) + (verify-tx-cmds* search-param-registry batch-db-before t cmds))) diff --git a/modules/db/src/blaze/db/search_param_registry.clj b/modules/db/src/blaze/db/search_param_registry.clj index 832b31612..e3f1c68a0 100644 --- a/modules/db/src/blaze/db/search_param_registry.clj +++ b/modules/db/src/blaze/db/search_param_registry.clj @@ -52,15 +52,20 @@ (p/-linked-compartments search-param-registry resource)) (defn compartment-resources - "Returns a seq of `[type codes]` tuples of resources in compartment of `type`. + "Returns a seq of `[type codes]` tuples of resources in compartment of + `compartment-type` or a list of codes if the optional `type` is given. Example: - * `[\"Observation\" [\"subject\" \"performer\"]]` and others for \"Patient\"" - [search-param-registry type] - (p/-compartment-resources search-param-registry type)) + * `[\"Observation\" [\"subject\" \"performer\"]]` and others for \"Patient\" + * `[\"subject\"]` and others for \"Patient\" and \"Observation\"" + ([search-param-registry compartment-type] + (p/-compartment-resources search-param-registry compartment-type)) + ([search-param-registry compartment-type type] + (p/-compartment-resources search-param-registry compartment-type type))) (deftype MemSearchParamRegistry [url-index index target-index compartment-index - compartment-resource-index] + compartment-resource-index + compartment-resource-index-by-type] p/SearchParamRegistry (-get [_ code type] (or (get-in index [type code]) @@ -94,8 +99,11 @@ #{} (compartment-index (name (fhir-spec/fhir-type resource))))) - (-compartment-resources [_ type] - (compartment-resource-index type []))) + (-compartment-resources [_ compartment-type] + (compartment-resource-index compartment-type [])) + + (-compartment-resources [_ compartment-type type] + (get-in compartment-resource-index-by-type [compartment-type type] []))) (def ^:private object-mapper (j/object-mapper @@ -170,6 +178,14 @@ [res-type param-codes]))) resource-defs)}) +(defn- index-compartment-resources-by-type [{def-code :code resource-defs :resource}] + {def-code + (reduce + (fn [res {res-type :code param-codes :param}] + (cond-> res param-codes (assoc res-type param-codes))) + {} + resource-defs)}) + (def ^:private list-search-param {:type "special" :name "_list"}) @@ -257,7 +273,10 @@ patient-compartment (read-classpath-json-resource "blaze/db/compartment/patient.json")] (when-ok [url-index (build-url-index entries) index (build-index url-index entries)] - (->MemSearchParamRegistry url-index (add-special index) - (build-target-index url-index entries) - (index-compartment-def index patient-compartment) - (index-compartment-resources patient-compartment))))) + (->MemSearchParamRegistry + url-index + (add-special index) + (build-target-index url-index entries) + (index-compartment-def index patient-compartment) + (index-compartment-resources patient-compartment) + (index-compartment-resources-by-type patient-compartment))))) diff --git a/modules/db/src/blaze/db/search_param_registry_spec.clj b/modules/db/src/blaze/db/search_param_registry_spec.clj index d804451e4..0d6e879a5 100644 --- a/modules/db/src/blaze/db/search_param_registry_spec.clj +++ b/modules/db/src/blaze/db/search_param_registry_spec.clj @@ -38,5 +38,7 @@ (s/fdef sr/compartment-resources :args (s/cat :search-param-registry :blaze.db/search-param-registry - :type :fhir.resource/type) - :ret (s/coll-of (s/tuple :fhir.resource/type (s/coll-of string?)))) + :compartment-type :fhir.resource/type + :type (s/? :fhir.resource/type)) + :ret (s/or :all (s/coll-of (s/tuple :fhir.resource/type (s/coll-of string?))) + :by-type (s/coll-of string?))) diff --git a/modules/db/test-perf/blaze/db/api_test_perf.clj b/modules/db/test-perf/blaze/db/api_test_perf.clj index 972c292b0..f99f3cd74 100644 --- a/modules/db/test-perf/blaze/db/api_test_perf.clj +++ b/modules/db/test-perf/blaze/db/api_test_perf.clj @@ -58,6 +58,7 @@ :resource-as-of-index nil :type-as-of-index nil :system-as-of-index nil + :patient-last-change-index nil :type-stats-index nil :system-stats-index nil}} diff --git a/modules/db/test/blaze/db/api_test.clj b/modules/db/test/blaze/db/api_test.clj index 8860c4041..07cc98b7a 100644 --- a/modules/db/test/blaze/db/api_test.clj +++ b/modules/db/test/blaze/db/api_test.clj @@ -5230,6 +5230,49 @@ [0 :fhir/type] := :fhir/Observation [0 :id] := "0"))))) +(deftest patient-compartment-last-change-t-test + (testing "non-existing patient" + (with-system [{:blaze.db/keys [node]} config] + + (testing "just returns nil" + (is (nil? (d/patient-compartment-last-change-t (d/db node) "0")))))) + + (testing "single patient" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]]] + + (testing "has no resources in its compartment" + (is (nil? (d/patient-compartment-last-change-t (d/db node) "0")))))) + + (testing "observation created in same transaction as patient" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (is (= 1 (d/patient-compartment-last-change-t (d/db node) "0"))))) + + (testing "observation created after the patient" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]] + [[:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (testing "the last change comes from the second transaction" + (is (= 2 (d/patient-compartment-last-change-t (d/db node) "0")))) + + (testing "at t=1 there was no change" + (is (nil? (d/patient-compartment-last-change-t (d/as-of (d/db node) 1) "0")))))) + + (testing "patient with last change in t=1 isn't affected by later patient added in t=2" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/Patient :id "0"}]] + [[:put {:fhir/type :fhir/Patient :id "1"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/1"}}]]] + + (is (nil? (d/patient-compartment-last-change-t (d/db node) "0")))))) + (defmethod ig/init-key ::defective-resource-store [_ {:keys [hashes-to-store]}] (let [store (atom {})] (reify diff --git a/modules/db/test/blaze/db/impl/batch_db_spec.clj b/modules/db/test/blaze/db/impl/batch_db_spec.clj index d7bd01719..6b9940f19 100644 --- a/modules/db/test/blaze/db/impl/batch_db_spec.clj +++ b/modules/db/test/blaze/db/impl/batch_db_spec.clj @@ -4,6 +4,7 @@ [blaze.db.impl.batch-db :as batch-db] [blaze.db.impl.batch-db.patient-everything-spec] [blaze.db.impl.index.compartment.resource-spec] + [blaze.db.impl.index.patient-last-change-spec] [blaze.db.impl.index.resource-as-of-spec] [blaze.db.impl.index.system-as-of-spec] [blaze.db.impl.index.type-as-of-spec] diff --git a/modules/db/test/blaze/db/impl/db_spec.clj b/modules/db/test/blaze/db/impl/db_spec.clj index c84f3335a..e4ee44ec4 100644 --- a/modules/db/test/blaze/db/impl/db_spec.clj +++ b/modules/db/test/blaze/db/impl/db_spec.clj @@ -5,6 +5,7 @@ [blaze.db.impl.codec-spec] [blaze.db.impl.db :as db] [blaze.db.impl.index-spec] + [blaze.db.impl.index.patient-last-change-spec] [blaze.db.impl.index.system-stats-spec] [blaze.db.impl.index.type-stats-spec] [blaze.db.impl.search-param-spec] diff --git a/modules/db/test/blaze/db/impl/index/patient_last_change/spec.clj b/modules/db/test/blaze/db/impl/index/patient_last_change/spec.clj new file mode 100644 index 000000000..e6d6d9da1 --- /dev/null +++ b/modules/db/test/blaze/db/impl/index/patient_last_change/spec.clj @@ -0,0 +1,12 @@ +(ns blaze.db.impl.index.patient-last-change.spec + (:require + [blaze.db.impl.index.patient-last-change :as-alias plc] + [blaze.db.impl.index.patient-last-change.state :as-alias state] + [blaze.db.tx-log.spec] + [clojure.spec.alpha :as s])) + +(s/def ::state/type + #{:building :current}) + +(s/def ::plc/state + (s/keys :req-un [::state/type] :opt-un [:blaze.db/t])) diff --git a/modules/db/test/blaze/db/impl/index/patient_last_change_spec.clj b/modules/db/test/blaze/db/impl/index/patient_last_change_spec.clj new file mode 100644 index 000000000..394025087 --- /dev/null +++ b/modules/db/test/blaze/db/impl/index/patient_last_change_spec.clj @@ -0,0 +1,27 @@ +(ns blaze.db.impl.index.patient-last-change-spec + (:require + [blaze.db.impl.codec-spec] + [blaze.db.impl.index.patient-last-change :as plc] + [blaze.db.impl.index.patient-last-change.spec] + [blaze.db.kv-spec] + [blaze.db.tx-log.spec] + [blaze.fhir.hash.spec] + [clojure.spec.alpha :as s])) + +(s/fdef plc/index-entry + :args (s/cat :patient-id :blaze.db/id-byte-string :t :blaze.db/t) + :ret :blaze.db.kv/put-entry) + +(s/fdef plc/last-change-t + :args (s/cat :plci :blaze.db.kv/iterator + :patient-id :blaze.db/id-byte-string + :t :blaze.db/t) + :ret (s/nilable :blaze.db/t)) + +(s/fdef plc/state + :args (s/cat :kv-store :blaze.db/kv-store) + :ret ::plc/state) + +(s/fdef plc/state-index-entry + :args (s/cat :state ::plc/state) + :ret :blaze.db.kv/put-entry) diff --git a/modules/db/test/blaze/db/impl/index/patient_last_change_test_util.clj b/modules/db/test/blaze/db/impl/index/patient_last_change_test_util.clj new file mode 100644 index 000000000..ffbcf4775 --- /dev/null +++ b/modules/db/test/blaze/db/impl/index/patient_last_change_test_util.clj @@ -0,0 +1,13 @@ +(ns blaze.db.impl.index.patient-last-change-test-util + (:require + [blaze.byte-buffer :as bb] + [blaze.byte-string :as bs] + [blaze.db.impl.codec :as codec])) + +(set! *unchecked-math* :warn-on-boxed) + +(defn decode-key [byte-array] + (let [buf (bb/wrap byte-array) + patient-id-len (- (bb/remaining buf) codec/t-size)] + {:patient-id (codec/id-string (bs/from-byte-buffer! buf patient-id-len)) + :t (codec/descending-long (bb/get-long! buf))})) diff --git a/modules/db/test/blaze/db/impl/index/rts_as_of_spec.clj b/modules/db/test/blaze/db/impl/index/rts_as_of_spec.clj index 3d3adbe6a..50ddb8a02 100644 --- a/modules/db/test/blaze/db/impl/index/rts_as_of_spec.clj +++ b/modules/db/test/blaze/db/impl/index/rts_as_of_spec.clj @@ -15,4 +15,4 @@ :hash :blaze.resource/hash :num-changes nat-int? :op keyword?) - :ret (s/coll-of :blaze.db.kv/put-entry-w-cf)) + :ret (s/coll-of :blaze.db.kv/put-entry)) diff --git a/modules/db/test/blaze/db/impl/search_param_spec.clj b/modules/db/test/blaze/db/impl/search_param_spec.clj index ea07cc361..48fbcb3f0 100644 --- a/modules/db/test/blaze/db/impl/search_param_spec.clj +++ b/modules/db/test/blaze/db/impl/search_param_spec.clj @@ -84,5 +84,5 @@ :linked-compartments (s/nilable (s/coll-of (s/tuple string? string?))) :hash :blaze.resource/hash :resource :blaze/resource) - :ret (s/or :entries (cs/coll-of :blaze.db.kv/put-entry-w-cf) + :ret (s/or :entries (cs/coll-of :blaze.db.kv/put-entry) :anomaly ::anom/anomaly)) diff --git a/modules/db/test/blaze/db/node/patient_last_change_index_spec.clj b/modules/db/test/blaze/db/node/patient_last_change_index_spec.clj new file mode 100644 index 000000000..038817cb2 --- /dev/null +++ b/modules/db/test/blaze/db/node/patient_last_change_index_spec.clj @@ -0,0 +1,14 @@ +(ns blaze.db.node.patient-last-change-index-spec + (:require + [blaze.db.kv.spec] + [blaze.db.node.patient-last-change-index :as node-plc] + [blaze.db.node.tx-indexer-spec] + [blaze.db.spec] + [blaze.db.tx-log.spec] + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) + +(s/fdef node-plc/index-entries + :args (s/cat :node :blaze.db/node + :tx-data :blaze.db/tx-data) + :ret (s/or :entries (s/coll-of :blaze.db.kv/put-entry) :anomaly ::anom/anomaly)) diff --git a/modules/db/test/blaze/db/node/patient_last_change_index_test.clj b/modules/db/test/blaze/db/node/patient_last_change_index_test.clj new file mode 100644 index 000000000..189b1f30a --- /dev/null +++ b/modules/db/test/blaze/db/node/patient_last_change_index_test.clj @@ -0,0 +1,42 @@ +(ns blaze.db.node.patient-last-change-index-test + (:require + [blaze.db.impl.index.patient-last-change :as plc] + [blaze.db.impl.index.patient-last-change-test-util :as plc-tu] + [blaze.db.node.patient-last-change-index :as node-plc] + [blaze.db.node.patient-last-change-index-spec] + [blaze.db.test-util :refer [config with-system-data]] + [blaze.fhir.hash :as hash] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest]] + [juxt.iota :refer [given]]) + (:import + [java.nio.charset StandardCharsets] + [java.time Instant])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(def patient-0 {:fhir/type :fhir/Patient :id "0"}) +(def observation-0 {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}) +(def hash-observation-0 (hash/generate observation-0)) + +(deftest patient-last-change-index-entries-test + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put patient-0]]] + + (given (node-plc/index-entries + node + {:t 2 + :instant Instant/EPOCH + :tx-cmds [{:op "put" :type "Observation" :id "0" + :hash hash-observation-0 :refs [["Patient" "0"]]}]}) + count := 2 + [0 0] := :patient-last-change-index + [0 1 plc-tu/decode-key] := {:patient-id "0" :t 2} + + [1 0] := :default + [1 1 #(String. ^bytes % StandardCharsets/ISO_8859_1)] := "patient-last-change-state" + [1 2 plc/decode-state] := {:type :building :t 2}))) diff --git a/modules/db/test/blaze/db/node/tx_indexer/verify_spec.clj b/modules/db/test/blaze/db/node/tx_indexer/verify_spec.clj index 88b251be4..50fb1139f 100644 --- a/modules/db/test/blaze/db/node/tx_indexer/verify_spec.clj +++ b/modules/db/test/blaze/db/node/tx_indexer/verify_spec.clj @@ -8,12 +8,16 @@ [blaze.db.impl.index.type-stats-spec] [blaze.db.kv.spec] [blaze.db.node.tx-indexer.verify :as verify] + [blaze.db.search-param-registry.spec] [blaze.db.spec] [blaze.db.tx-log.spec] [clojure.spec.alpha :as s] [cognitect.anomalies :as anom])) (s/fdef verify/verify-tx-cmds - :args (s/cat :db-before :blaze.db/db :t :blaze.db/t :cmds :blaze.db/tx-cmds) + :args (s/cat :search-param-registry :blaze.db/search-param-registry + :db-before :blaze.db/db + :t :blaze.db/t + :cmds :blaze.db/tx-cmds) :ret (s/or :entries (s/coll-of :blaze.db.kv/put-entry) :anomaly ::anom/anomaly)) diff --git a/modules/db/test/blaze/db/node/tx_indexer/verify_test.clj b/modules/db/test/blaze/db/node/tx_indexer/verify_test.clj index 358cdb52b..9abc4b1ec 100644 --- a/modules/db/test/blaze/db/node/tx_indexer/verify_test.clj +++ b/modules/db/test/blaze/db/node/tx_indexer/verify_test.clj @@ -1,6 +1,8 @@ (ns blaze.db.node.tx-indexer.verify-test (:require + [blaze.byte-string :as bs] [blaze.db.api :as d] + [blaze.db.impl.index.patient-last-change-test-util :as plc-tu] [blaze.db.impl.index.resource-as-of-test-util :as rao-tu] [blaze.db.impl.index.rts-as-of-test-util :as rts-tu] [blaze.db.impl.index.system-as-of-test-util :as sao-tu] @@ -13,7 +15,7 @@ [blaze.db.node.tx-indexer.verify :as verify] [blaze.db.node.tx-indexer.verify-spec] [blaze.db.search-param-registry] - [blaze.db.test-util :refer [config with-system-data]] + [blaze.db.test-util :refer [config search-param-registry with-system-data]] [blaze.db.tx-cache] [blaze.db.tx-log.local] [blaze.fhir.hash :as hash] @@ -29,7 +31,7 @@ [taoensso.timbre :as log])) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -42,6 +44,9 @@ :subject #fhir/Reference{:reference "Patient/0"}}) (def observation-1 {:fhir/type :fhir/Observation :id "1" :subject #fhir/Reference{:reference "Patient/0"}}) +(def allergy-intolerance-0 {:fhir/type :fhir/AllergyIntolerance :id "0" + :patient #fhir/Reference{:reference "Patient/0"}}) + (deftest verify-tx-cmds-test (testing "adding one Patient to an empty store" (let [hash (hash/generate patient-0)] @@ -49,6 +54,7 @@ if-none-match [nil "*"]] (with-system [{:blaze.db/keys [node]} config] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 1 [(cond-> {:op (name op) :type "Patient" :id "0" :hash hash} if-none-match @@ -83,6 +89,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [(cond-> {:op "put" :type "Patient" :id "0" :hash hash} if-match @@ -118,6 +125,7 @@ [[:delete "Patient" "0"]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 3 [(cond-> {:op "put" :type "Patient" :id "0" :hash hash} if-match @@ -150,6 +158,7 @@ [[[:put patient-0]]] (is (empty? (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Patient" :id "0" :hash (hash/generate patient-0)}]))))) @@ -158,7 +167,10 @@ (with-system [{:blaze.db/keys [node]} config] (let [tx-cmd {:op "keep" :type "Patient" :id "0" :hash (hash/generate patient-0)}] - (given (verify/verify-tx-cmds (d/db node) 1 [tx-cmd]) + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 1 + [tx-cmd]) ::anom/category := ::anom/conflict ::anom/message := "Keep failed on `Patient/0`." :blaze.db/tx-cmd := tx-cmd)))) @@ -169,7 +181,10 @@ [[:put patient-0-v2]]] (let [tx-cmd {:op "keep" :type "Patient" :id "0" :hash (hash/generate patient-0)}] - (given (verify/verify-tx-cmds (d/db node) 1 [tx-cmd]) + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 1 + [tx-cmd]) ::anom/category := ::anom/conflict ::anom/message := "Keep failed on `Patient/0`." :blaze.db/tx-cmd := tx-cmd)))) @@ -185,6 +200,7 @@ :hash (hash/generate patient-0-v2) :if-match if-match}] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 1 [tx-cmd]) ::anom/category := ::anom/conflict @@ -192,7 +208,7 @@ :http/status := 412 :blaze.db/tx-cmd := tx-cmd)))))) - (testing "keeping a non-matching hash and non-matching if-match patient fails" + (testing "keeping a non-matching hash and non-matching if-match Patient fails" (with-system-data [{:blaze.db/keys [node]} config] [[[:put patient-0]] [[:put patient-0-v2]]] @@ -202,7 +218,10 @@ (let [tx-cmd {:op "keep" :type "Patient" :id "0" :hash (hash/generate patient-0) :if-match if-match}] - (given (verify/verify-tx-cmds (d/db node) 1 [tx-cmd]) + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 1 + [tx-cmd]) ::anom/category := ::anom/conflict ::anom/message := "Precondition `W/\"3\"` failed on `Patient/0`." :http/status := 412 @@ -215,6 +234,7 @@ (testing "with different if-matches" (doseq [if-match [nil 1 [1] [1 2]]] (is (empty? (verify/verify-tx-cmds + search-param-registry (d/db node) 1 [(cond-> {:op "keep" :type "Patient" :id "0" @@ -225,6 +245,7 @@ (testing "deleting a Patient from an empty store" (with-system [{:blaze.db/keys [node]} config] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 1 [{:op "delete" :type "Patient" :id "0"}]) @@ -255,6 +276,7 @@ [[[:delete "Patient" "0"]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0"}]) @@ -285,6 +307,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0"}]) @@ -310,12 +333,85 @@ [4 1 ss-tu/decode-key] := {:t 2} [4 2 ss-tu/decode-val] := {:total 0 :num-changes 2}))) + (testing "deleting an existing Observation" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put patient-0] + [:put observation-0]]] + + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 2 + [{:op "delete" :type "Observation" :id "0"}]) + + count := 6 + + [0 0] := :resource-as-of-index + [0 1 rao-tu/decode-key] := {:type "Observation" :id "0" :t 2} + [0 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [1 0] := :type-as-of-index + [1 1 tao-tu/decode-key] := {:type "Observation" :t 2 :id "0"} + [1 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [2 0] := :system-as-of-index + [2 1 sao-tu/decode-key] := {:t 2 :type "Observation" :id "0"} + [2 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [3 0] := :patient-last-change-index + [3 1 plc-tu/decode-key] := {:patient-id "0" :t 2} + [3 2 bs/from-byte-array] := bs/empty + + [4 0] := :type-stats-index + [4 1 ts-tu/decode-key] := {:type "Observation" :t 2} + [4 2 ts-tu/decode-val] := {:total 0 :num-changes 2} + + [5 0] := :system-stats-index + [5 1 ss-tu/decode-key] := {:t 2} + [5 2 ss-tu/decode-val] := {:total 1 :num-changes 3}))) + + (testing "deleting an existing AllergyIntolerance" + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put patient-0] + [:put allergy-intolerance-0]]] + + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 2 + [{:op "delete" :type "AllergyIntolerance" :id "0"}]) + + count := 6 + + [0 0] := :resource-as-of-index + [0 1 rao-tu/decode-key] := {:type "AllergyIntolerance" :id "0" :t 2} + [0 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [1 0] := :type-as-of-index + [1 1 tao-tu/decode-key] := {:type "AllergyIntolerance" :t 2 :id "0"} + [1 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [2 0] := :system-as-of-index + [2 1 sao-tu/decode-key] := {:t 2 :type "AllergyIntolerance" :id "0"} + [2 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} + + [3 0] := :patient-last-change-index + [3 1 plc-tu/decode-key] := {:patient-id "0" :t 2} + [3 2 bs/from-byte-array] := bs/empty + + [4 0] := :type-stats-index + [4 1 ts-tu/decode-key] := {:type "AllergyIntolerance" :t 2} + [4 2 ts-tu/decode-val] := {:total 0 :num-changes 2} + + [5 0] := :system-stats-index + [5 1 ss-tu/decode-key] := {:t 2} + [5 2 ss-tu/decode-val] := {:total 1 :num-changes 3}))) + (testing "adding a second Patient to a store containing already one" (let [hash (hash/generate patient-1)] (with-system-data [{:blaze.db/keys [node]} config] [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Patient" :id "1" :hash hash}]) @@ -343,34 +439,44 @@ (testing "adding an observation referring to an existing patient" (let [hash (hash/generate observation-0)] - (with-system-data [{:blaze.db/keys [node]} config] - [[[:put patient-0]]] + (doseq [op [:create :put] + if-none-match [nil "*"]] + (with-system-data [{:blaze.db/keys [node]} config] + [[[:put patient-0]]] - (given (verify/verify-tx-cmds - (d/db node) 2 - [{:op "put" :type "Observation" :id "0" :hash hash :refs [["Patient" "0"]]}]) + (given (verify/verify-tx-cmds + search-param-registry + (d/db node) 2 + [(cond-> {:op (name op) :type "Observation" :id "0" + :hash hash :refs [["Patient" "0"]]} + if-none-match + (assoc :if-none-match if-none-match))]) - count := 5 + count := 6 - [0 0] := :resource-as-of-index - [0 1 rao-tu/decode-key] := {:type "Observation" :id "0" :t 2} - [0 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op :put} + [0 0] := :resource-as-of-index + [0 1 rao-tu/decode-key] := {:type "Observation" :id "0" :t 2} + [0 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op op} - [1 0] := :type-as-of-index - [1 1 tao-tu/decode-key] := {:type "Observation" :t 2 :id "0"} - [1 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op :put} + [1 0] := :type-as-of-index + [1 1 tao-tu/decode-key] := {:type "Observation" :t 2 :id "0"} + [1 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op op} - [2 0] := :system-as-of-index - [2 1 sao-tu/decode-key] := {:t 2 :type "Observation" :id "0"} - [2 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op :put} + [2 0] := :system-as-of-index + [2 1 sao-tu/decode-key] := {:t 2 :type "Observation" :id "0"} + [2 2 rts-tu/decode-val] := {:hash hash :num-changes 1 :op op} - [3 0] := :type-stats-index - [3 1 ts-tu/decode-key] := {:type "Observation" :t 2} - [3 2 ts-tu/decode-val] := {:total 1 :num-changes 1} + [3 0] := :patient-last-change-index + [3 1 plc-tu/decode-key] := {:patient-id "0" :t 2} + [3 2 bs/from-byte-array] := bs/empty - [4 0] := :system-stats-index - [4 1 ss-tu/decode-key] := {:t 2} - [4 2 ss-tu/decode-val] := {:total 2 :num-changes 2})))) + [4 0] := :type-stats-index + [4 1 ts-tu/decode-key] := {:type "Observation" :t 2} + [4 2 ts-tu/decode-val] := {:total 1 :num-changes 1} + + [5 0] := :system-stats-index + [5 1 ss-tu/decode-key] := {:t 2} + [5 2 ss-tu/decode-val] := {:total 2 :num-changes 2}))))) (testing "update conflict" (testing "adding an observation referring to an empty store" @@ -378,6 +484,7 @@ (with-system [{:blaze.db/keys [node]} config] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Observation" :id "0" :hash hash :refs [["Patient" "0"]]}]) @@ -389,6 +496,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Patient" :id "0" :hash (hash/generate patient-0) @@ -402,6 +510,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Patient" :id "0" :hash (hash/generate patient-0) @@ -415,6 +524,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "put" :type "Patient" :id "0" :hash (hash/generate patient-0) @@ -430,6 +540,7 @@ [[[:put patient-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0" :check-refs true} {:op "put" :type "Observation" :id "0" :hash hash :refs [["Patient" "0"]]}]) @@ -443,11 +554,12 @@ [:put observation-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0" :check-refs true} {:op "delete" :type "Observation" :id "0" :check-refs true}]) - count := 9 + count := 10 [0 0] := :resource-as-of-index [0 1 rao-tu/decode-key] := {:type "Patient" :id "0" :t 2} @@ -473,17 +585,21 @@ [5 1 sao-tu/decode-key] := {:t 2 :type "Observation" :id "0"} [5 2 rts-tu/decode-val] := {:hash hash/deleted-hash :num-changes 2 :op :delete} - [6 0] := :type-stats-index - [6 1 ts-tu/decode-key] := {:type "Patient" :t 2} - [6 2 ts-tu/decode-val] := {:total 0 :num-changes 2} + [6 0] := :patient-last-change-index + [6 1 plc-tu/decode-key] := {:patient-id "0" :t 2} + [6 2 bs/from-byte-array] := bs/empty [7 0] := :type-stats-index - [7 1 ts-tu/decode-key] := {:type "Observation" :t 2} + [7 1 ts-tu/decode-key] := {:type "Patient" :t 2} [7 2 ts-tu/decode-val] := {:total 0 :num-changes 2} - [8 0] := :system-stats-index - [8 1 ss-tu/decode-key] := {:t 2} - [8 2 ss-tu/decode-val] := {:total 0 :num-changes 4}))) + [8 0] := :type-stats-index + [8 1 ts-tu/decode-key] := {:type "Observation" :t 2} + [8 2 ts-tu/decode-val] := {:total 0 :num-changes 2} + + [9 0] := :system-stats-index + [9 1 ss-tu/decode-key] := {:t 2} + [9 2 ss-tu/decode-val] := {:total 0 :num-changes 4}))) (testing "deleting a patient which is referenced by a still existing observation" (with-system-data [{:blaze.db/keys [node]} config] @@ -491,6 +607,7 @@ [:put observation-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0" :check-refs true}]) @@ -503,6 +620,7 @@ [:put observation-0]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0"}]) @@ -535,6 +653,7 @@ [:put observation-1]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "0" :check-refs true}]) @@ -550,6 +669,7 @@ :birthDate #fhir/date"2020"}]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "create" :type "Patient" :id "foo" :hash (hash/generate patient-0) @@ -565,6 +685,7 @@ (is (empty? (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "create" :type "Patient" :id "0" :hash (hash/generate patient-0) @@ -575,6 +696,7 @@ [[[:put patient-2]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 2 [{:op "delete" :type "Patient" :id "2"} {:op "create" :type "Patient" :id "0" @@ -590,6 +712,7 @@ [[:delete "Patient" "0"]]] (given (verify/verify-tx-cmds + search-param-registry (d/db node) 3 [{:op "put" :type "Patient" :id "0" :hash hash}]) diff --git a/modules/db/test/blaze/db/node/tx_indexer_spec.clj b/modules/db/test/blaze/db/node/tx_indexer_spec.clj index beda8c044..703ab6d5a 100644 --- a/modules/db/test/blaze/db/node/tx_indexer_spec.clj +++ b/modules/db/test/blaze/db/node/tx_indexer_spec.clj @@ -3,11 +3,14 @@ [blaze.db.kv.spec] [blaze.db.node.tx-indexer :as tx-indexer] [blaze.db.node.tx-indexer.verify-spec] + [blaze.db.search-param-registry.spec] [blaze.db.spec] [blaze.db.tx-log.spec] [clojure.spec.alpha :as s] [cognitect.anomalies :as anom])) (s/fdef tx-indexer/index-tx - :args (s/cat :db-before :blaze.db/db :tx-data :blaze.db/tx-data) + :args (s/cat :search-param-registry :blaze.db/search-param-registry + :db-before :blaze.db/db + :tx-data :blaze.db/tx-data) :ret (s/or :entries (s/coll-of :blaze.db.kv/put-entry) :anomaly ::anom/anomaly)) diff --git a/modules/db/test/blaze/db/node_test.clj b/modules/db/test/blaze/db/node_test.clj index a11da85ff..3555d43ef 100644 --- a/modules/db/test/blaze/db/node_test.clj +++ b/modules/db/test/blaze/db/node_test.clj @@ -6,11 +6,14 @@ [blaze.db.api :as d] [blaze.db.api-spec] [blaze.db.impl.db-spec] + [blaze.db.impl.index.patient-last-change :as plc] + [blaze.db.impl.index.patient-last-change-spec] [blaze.db.impl.index.tx-success :as tx-success] [blaze.db.kv :as kv] [blaze.db.kv.mem-spec] [blaze.db.node :as node] [blaze.db.node-spec] + [blaze.db.node.patient-last-change-index-spec] [blaze.db.node.resource-indexer :as resource-indexer] [blaze.db.node.tx-indexer :as-alias tx-indexer] [blaze.db.node.version :as version] @@ -42,7 +45,7 @@ (set! *warn-on-reflection* true) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -271,3 +274,12 @@ (deftest existing-data-with-compatible-version (with-system [{:blaze.db/keys [node]} (with-index-store-version config 0)] (is node))) + +(deftest patient-last-change-index-state-test + (testing "the state is set to current on a fresh start of the node" + (with-system [{:blaze.db/keys [node]} config] + ;; Wait for index building finished + (Thread/sleep 100) + + (given (plc/state (:kv-store node)) + :type := :current)))) diff --git a/modules/db/test/blaze/db/search_param_registry_test.clj b/modules/db/test/blaze/db/search_param_registry_test.clj index eb5c6b1c5..fd40cdbef 100644 --- a/modules/db/test/blaze/db/search_param_registry_test.clj +++ b/modules/db/test/blaze/db/search_param_registry_test.clj @@ -17,7 +17,7 @@ [taoensso.timbre :as log])) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -219,10 +219,15 @@ (deftest compartment-resources-test (testing "Patient" (with-system [{:blaze.db/keys [search-param-registry]} config] - (given (sr/compartment-resources search-param-registry "Patient") - count := 66 - [0] := ["Account" ["subject"]] - [1] := ["AdverseEvent" ["subject"]] - [2] := ["AllergyIntolerance" ["patient" "recorder" "asserter"]] - [3] := ["Appointment" ["actor"]] - [65] := ["VisionPrescription" ["patient"]])))) + (testing "all resource types" + (given (sr/compartment-resources search-param-registry "Patient") + count := 66 + [0] := ["Account" ["subject"]] + [1] := ["AdverseEvent" ["subject"]] + [2] := ["AllergyIntolerance" ["patient" "recorder" "asserter"]] + [3] := ["Appointment" ["actor"]] + [65] := ["VisionPrescription" ["patient"]])) + + (testing "only Observation codes" + (is (= (sr/compartment-resources search-param-registry "Patient" "Observation") + ["subject" "performer"])))))) diff --git a/modules/db/test/blaze/db/test_util.clj b/modules/db/test/blaze/db/test_util.clj index 6ad63556e..168aef74d 100644 --- a/modules/db/test/blaze/db/test_util.clj +++ b/modules/db/test/blaze/db/test_util.clj @@ -28,6 +28,7 @@ :kv-store (ig/ref :blaze.db/index-kv-store) :resource-indexer (ig/ref ::node/resource-indexer) :search-param-registry search-param-registry + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} ::tx-log/local @@ -58,6 +59,7 @@ :resource-as-of-index nil :type-as-of-index nil :system-as-of-index nil + :patient-last-change-index nil :type-stats-index nil :system-stats-index nil}} @@ -76,7 +78,9 @@ :search-param-registry search-param-registry :executor (ig/ref :blaze.db.node.resource-indexer/executor)} - :blaze.db.node.resource-indexer/executor {}}) + :blaze.db.node.resource-indexer/executor {} + + :blaze/scheduler {}}) (defmacro with-system-data "Runs `body` inside a system that is initialized from `config`, bound to diff --git a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj index a7d9a5392..d5d4958ca 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj @@ -37,6 +37,9 @@ (s/def :fhir/Task #(s2/valid? :fhir/Task %)) +(s/def :fhir/Measure + #(s2/valid? :fhir/Measure %)) + (s/def :fhir.Measure/group #(s2/valid? :fhir.Measure/group %)) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 9530aab02..231125a5c 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1718,11 +1718,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2493,9 +2493,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, diff --git a/modules/frontend/src/lib/resource/json/array.svelte b/modules/frontend/src/lib/resource/json/array.svelte index 2d7d6ea2d..7cd00f626 100644 --- a/modules/frontend/src/lib/resource/json/array.svelte +++ b/modules/frontend/src/lib/resource/json/array.svelte @@ -6,8 +6,8 @@ export let values: (FhirObject | FhirPrimitive)[]; -{'[\n'}{#each values as value}{'[\n'}{#each values as value, index}{/each}{' '.repeat(indent)}{']'} + />{index < values.length - 1 ? ', ' : ''}{/each}{' '.repeat(indent)}{']'} diff --git a/modules/frontend/src/routes/__admin/+layout.svelte b/modules/frontend/src/routes/__admin/+layout.svelte index 4983c01b7..0a597e09e 100644 --- a/modules/frontend/src/routes/__admin/+layout.svelte +++ b/modules/frontend/src/routes/__admin/+layout.svelte @@ -1,6 +1,8 @@
    @@ -17,6 +19,13 @@ id="/__admin/jobs" label="Jobs" /> + {#if data.features.find((f) => f.key === 'cql-expression-cache')?.enabled} + + {/if}
    diff --git a/modules/frontend/src/routes/__admin/+page.ts b/modules/frontend/src/routes/__admin/+layout.ts similarity index 85% rename from modules/frontend/src/routes/__admin/+page.ts rename to modules/frontend/src/routes/__admin/+layout.ts index 6858ab2b8..d5ccce9ec 100644 --- a/modules/frontend/src/routes/__admin/+page.ts +++ b/modules/frontend/src/routes/__admin/+layout.ts @@ -1,4 +1,4 @@ -import type { PageLoad } from './$types'; +import type { LayoutLoad } from './$types'; import { base } from '$app/paths'; import { error, type NumericRange } from '@sveltejs/kit'; @@ -10,6 +10,7 @@ export interface Setting { } export interface Feature { + key: string; name: string; toggle: string; enabled: boolean; @@ -20,7 +21,7 @@ export interface Data { features: Feature[]; } -export const load: PageLoad = async ({ fetch }) => { +export const load: LayoutLoad = async ({ fetch }) => { const res = await fetch(`${base}/__admin`, { headers: { Accept: 'application/json' } }); diff --git a/modules/frontend/src/routes/__admin/+page.svelte b/modules/frontend/src/routes/__admin/+page.svelte index 7d2958fdb..45e0f4560 100644 --- a/modules/frontend/src/routes/__admin/+page.svelte +++ b/modules/frontend/src/routes/__admin/+page.svelte @@ -54,7 +54,7 @@ {#each data.features as feature} - + {/each} diff --git a/modules/frontend/src/routes/__admin/cql/+page.svelte b/modules/frontend/src/routes/__admin/cql/+page.svelte new file mode 100644 index 000000000..c94694bb5 --- /dev/null +++ b/modules/frontend/src/routes/__admin/cql/+page.svelte @@ -0,0 +1,45 @@ + + + + CQL - Admin - Blaze + + +
    + +
    +

    Bloom filters

    +

    + Used to improve CQL performance. Only the newest 100 are shown. +

    +
    + + + + + + + + + {#each data.bloomFilters as bloomFilter} + + {/each} +
    THash# PatientsMem Size
    +
    diff --git a/modules/frontend/src/routes/__admin/cql/+page.ts b/modules/frontend/src/routes/__admin/cql/+page.ts new file mode 100644 index 000000000..34a81a34b --- /dev/null +++ b/modules/frontend/src/routes/__admin/cql/+page.ts @@ -0,0 +1,29 @@ +import { base } from '$app/paths'; +import { error, type NumericRange } from '@sveltejs/kit'; + +export interface BloomFilter { + hash: string; + t: number; + patientCount: number; + exprForm: string; + memSize: number; +} + +export interface Data { + bloomFilters: BloomFilter[]; +} + +export async function load({ fetch }): Promise { + const res = await fetch(`${base}/__admin/cql/bloom-filters`, { + headers: { Accept: 'application/json' } + }); + + if (!res.ok) { + error(res.status as NumericRange<400, 599>, { + short: undefined, + message: `An error happened while loading CQL Bloom filters. Please try again later.` + }); + } + + return { bloomFilters: await res.json() }; +} diff --git a/modules/frontend/src/routes/__admin/cql/bloom-filter-row.svelte b/modules/frontend/src/routes/__admin/cql/bloom-filter-row.svelte new file mode 100644 index 000000000..6276446ac --- /dev/null +++ b/modules/frontend/src/routes/__admin/cql/bloom-filter-row.svelte @@ -0,0 +1,21 @@ + + + + {bloomFilter.t} + {bloomFilter.hash.substring(0, 8)} + {bloomFilter.patientCount} + {prettyBytes(bloomFilter.memSize, { binary: true, maximumFractionDigits: 1 })} + diff --git a/modules/frontend/src/routes/__admin/cql/bloom-filters/+server.ts b/modules/frontend/src/routes/__admin/cql/bloom-filters/+server.ts new file mode 100644 index 000000000..b9ea59870 --- /dev/null +++ b/modules/frontend/src/routes/__admin/cql/bloom-filters/+server.ts @@ -0,0 +1,9 @@ +import type { RequestHandler } from './$types'; +import { base } from '$app/paths'; + +export const GET: RequestHandler = async ({ fetch }) => { + const res = await fetch(`${base}/__admin/cql/bloom-filters`, { + headers: { Accept: 'application/json' } + }); + return new Response(await res.blob(), res); +}; diff --git a/modules/frontend/src/routes/__admin/feature-row.svelte b/modules/frontend/src/routes/__admin/feature-row.svelte index ecbe107eb..1a5b9503a 100644 --- a/modules/frontend/src/routes/__admin/feature-row.svelte +++ b/modules/frontend/src/routes/__admin/feature-row.svelte @@ -1,15 +1,15 @@ - {name} - {toggle} + {feature.name} + {feature.toggle} {enabled}{feature.enabled} diff --git a/modules/job-async-interaction/test/blaze/job/async_interaction_test.clj b/modules/job-async-interaction/test/blaze/job/async_interaction_test.clj index 1fa36dfc7..ea5d2bed8 100644 --- a/modules/job-async-interaction/test/blaze/job/async_interaction_test.clj +++ b/modules/job-async-interaction/test/blaze/job/async_interaction_test.clj @@ -130,6 +130,7 @@ :kv-store (ig/ref :blaze.db.main/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.main/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} :blaze.db.admin/node @@ -140,6 +141,7 @@ :kv-store (ig/ref :blaze.db.admin/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.admin/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} [::tx-log/local :blaze.db.main/tx-log] @@ -230,6 +232,8 @@ :blaze.db/search-param-registry {:structure-definition-repo structure-definition-repo} + :blaze/scheduler {} + :blaze.test/fixed-clock {} :blaze.test/fixed-rng-fn {}}) diff --git a/modules/job-re-index/test/blaze/job/re_index_test.clj b/modules/job-re-index/test/blaze/job/re_index_test.clj index dd2b40f26..289fab436 100644 --- a/modules/job-re-index/test/blaze/job/re_index_test.clj +++ b/modules/job-re-index/test/blaze/job/re_index_test.clj @@ -85,6 +85,7 @@ :kv-store (ig/ref :blaze.db.main/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.main/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} :blaze.db.admin/node @@ -95,6 +96,7 @@ :kv-store (ig/ref :blaze.db.admin/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.admin/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} [::tx-log/local :blaze.db.main/tx-log] @@ -185,6 +187,8 @@ :blaze.db/search-param-registry {:structure-definition-repo structure-definition-repo} + :blaze/scheduler {} + :blaze.test/fixed-clock {} :blaze.test/offset-clock diff --git a/modules/job-scheduler/test/blaze/job_scheduler_test.clj b/modules/job-scheduler/test/blaze/job_scheduler_test.clj index 75eb81ba8..f49c6ec42 100644 --- a/modules/job-scheduler/test/blaze/job_scheduler_test.clj +++ b/modules/job-scheduler/test/blaze/job_scheduler_test.clj @@ -125,6 +125,7 @@ :kv-store (ig/ref :blaze.db.main/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.main/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} :blaze.db.admin/node @@ -135,6 +136,7 @@ :kv-store (ig/ref :blaze.db.admin/index-kv-store) :resource-indexer (ig/ref :blaze.db.node.admin/resource-indexer) :search-param-registry (ig/ref :blaze.db/search-param-registry) + :scheduler (ig/ref :blaze/scheduler) :poll-timeout (time/millis 10)} [::tx-log/local :blaze.db.main/tx-log] @@ -225,6 +227,8 @@ :blaze.db/search-param-registry {:structure-definition-repo structure-definition-repo} + :blaze/scheduler {} + :blaze.test/fixed-clock {} ::incrementing-rng-fn {}}) diff --git a/modules/module-base/src/blaze/util.clj b/modules/module-base/src/blaze/util.clj index a90ac9274..adda60379 100644 --- a/modules/module-base/src/blaze/util.clj +++ b/modules/module-base/src/blaze/util.clj @@ -16,7 +16,7 @@ [x] (if (or (nil? x) (sequential? x)) x [x])) -(defn strip-leading-slash - "Strips a possible leading slash from `s`." +(defn strip-leading-slashes + "Strips all possible leading slashes from `s`." [s] - (if (str/starts-with? s "/") (subs s 1) s)) + (if (str/starts-with? s "/") (recur (subs s 1)) s)) diff --git a/modules/module-base/test/blaze/util_test.clj b/modules/module-base/test/blaze/util_test.clj index 8c6f823fd..0bf68b41b 100644 --- a/modules/module-base/test/blaze/util_test.clj +++ b/modules/module-base/test/blaze/util_test.clj @@ -27,6 +27,6 @@ (is (= [1] (u/to-seq [1]))))) (deftest strip-leading-slash-test - (satisfies-prop 1000 + (satisfies-prop 10000 (prop/for-all [s gen/string] - (not (str/starts-with? (u/strip-leading-slash s) "/"))))) + (not (str/starts-with? (u/strip-leading-slashes s) "/"))))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj index f643a6405..d64f0bd70 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj @@ -5,6 +5,8 @@ [blaze.async.comp :as ac] [blaze.coll.core :as coll] [blaze.db.api :as d] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.spec] [blaze.executors :as ex] [blaze.fhir.operation.evaluate-measure.measure :as measure] [blaze.fhir.operation.evaluate-measure.measure.spec] @@ -121,6 +123,7 @@ (defmethod m/pre-init-spec ::handler [_] (s/keys :req-un [:blaze.db/node ::executor :blaze/clock :blaze/rng-fn] + :opt [::expr/cache] :opt-un [::timeout :blaze/context-path])) (defmethod ig/init-key ::handler [_ context] 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 277a32ce3..d9db991b4 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 @@ -5,8 +5,8 @@ [blaze.coll.core :as coll] [blaze.db.api :as d] [blaze.elm.compiler :as c] - [blaze.elm.compiler.external-data :as ed] [blaze.elm.expression :as expr] + [blaze.elm.resource :as cr] [blaze.elm.util :as elm-util] [blaze.fhir.spec :as fhir-spec] [taoensso.timbre :as log])) @@ -77,7 +77,7 @@ [{:keys [db] :as context} {:keys [name expression]} subject-handles] (with-open [db (d/new-batch-db db)] (transduce - (comp (map (partial ed/mk-resource db)) + (comp (map (partial cr/mk-resource db)) (result-xf (assoc context :db db) name expression) (halt-when ba/anomaly?)) ((:reduce-op context) db) subject-handles))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj index 9fdeed1fb..abbf9062b 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj @@ -5,9 +5,11 @@ [blaze.coll.core :as coll] [blaze.cql-translator :as cql-translator] [blaze.db.api :as d] + [blaze.elm.compiler :as c] [blaze.elm.compiler-spec] - [blaze.elm.compiler.external-data :as ed] [blaze.elm.compiler.library :as library] + [blaze.elm.expression :as-alias expr] + [blaze.elm.resource :as cr] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.measure.group :as group] [blaze.fhir.operation.evaluate-measure.measure.population :as pop] @@ -127,6 +129,9 @@ :fhir/issue "value" :fhir.issue/expression "Measure.library")))) +(defn- remove-unused-defs [expression-defs measure] + (into {} (filter (comp (u/expression-names measure) key)) expression-defs)) + (defn- compile-primary-library "Compiles the CQL code from the first library resource which is referenced from `measure`. @@ -326,15 +331,30 @@ :unit #fhir/string"s" :value (bigdec duration)})})) +(defn- bloom-filter-ratio + "Creates an extension with a number of available Bloom filters over the total + number of requested Bloom filters." + [bloom-filters] + (type/extension + {:url "https://samply.github.io/blaze/fhir/StructureDefinition/bloom-filter-ratio" + :value + (type/ratio + {:numerator + (type/quantity {:value (bigdec (count (coll/eduction (remove ba/anomaly?) bloom-filters)))}) + :denominator + (type/quantity {:value (bigdec (count bloom-filters))})})})) + (defn- local-ref [handle] (str (name (fhir-spec/fhir-type handle)) "/" (:id handle))) (defn- measure-report - [{:keys [now report-type subject-handle] :as context} measure + [{:keys [now report-type subject-handle bloom-filters] :as context} measure {[start end] :period} [{:keys [result]} duration]] (cond-> {:fhir/type :fhir/MeasureReport - :extension [(eval-duration duration)] + :extension + [(eval-duration duration) + (bloom-filter-ratio bloom-filters)] :status #fhir/code"complete" :type (case report-type @@ -385,6 +405,18 @@ (format "Timeout of %d millis eclipsed while evaluating." (.toMillis ^Duration timeout))) +(defn- attach-cache* [cache context [name {:keys [expression] :as expr}]] + [name (c/form expression)] + (let [[expression bloom-filters] (c/attach-cache expression cache)] + (-> (assoc-in context [:expression-defs name] (assoc expr :expression expression)) + (update :bloom-filters into bloom-filters)))) + +(defn- attach-cache [{:keys [expression-defs] :as context} cache] + (reduce + (partial attach-cache* cache) + (assoc context :bloom-filters []) + expression-defs)) + (defn- eval-unfiltered-xf [context] (comp (filter (comp #{"Unfiltered"} :context val)) (map @@ -408,6 +440,7 @@ (defn- enhance-context [{:keys [clock db timeout] :blaze/keys [cancelled?] + ::expr/keys [cache] :or {timeout (time/hours 1)} :as context} measure {:keys [report-type subject-ref]}] @@ -432,10 +465,16 @@ ::luid/generator (luid-generator context)) function-defs (assoc :function-defs function-defs))] - (when-ok [expression-defs (eval-unfiltered context expression-defs)] - (cond-> (assoc context :expression-defs expression-defs) + (when-ok [expression-defs (eval-unfiltered context expression-defs) + expression-defs (library/resolve-all-refs expression-defs)] + (cond-> (assoc + context + :expression-defs + (-> (remove-unused-defs expression-defs measure) + (library/resolve-param-refs parameter-default-values))) + cache (attach-cache cache) subject-handle - (assoc :subject-handle (ed/mk-resource db subject-handle))))))))) + (assoc :subject-handle (cr/mk-resource db subject-handle))))))))) (defn evaluate-measure "Evaluates `measure` inside `context` with `params`. diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/spec.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/spec.clj index c2d00a904..6969de4bb 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/spec.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/spec.clj @@ -1,7 +1,7 @@ (ns blaze.fhir.operation.evaluate-measure.measure.spec (:require [blaze.db.spec] - [blaze.elm.compiler.external-data :as ed] + [blaze.elm.resource :as cr] [blaze.fhir.operation.evaluate-measure.measure :as-alias measure] [blaze.fhir.spec.spec] [clojure.spec.alpha :as s])) @@ -14,10 +14,10 @@ :local-ref :blaze.fhir/literal-ref-tuple)) (s/def ::measure/population-handle - ed/resource?) + cr/resource?) (s/def ::measure/subject-handle - ed/resource?) + cr/resource?) (s/def ::measure/handle (s/keys :req-un [::measure/population-handle ::measure/subject-handle])) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj index 37cf94b3f..840b6ac84 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj @@ -1,6 +1,7 @@ (ns blaze.fhir.operation.evaluate-measure.measure.util (:require [blaze.anomaly :as ba] + [blaze.coll.core :as coll] [blaze.fhir.spec.type :as type] [blaze.luid :as luid])) @@ -66,3 +67,25 @@ (-> result (ba/map (partial merge-result* ret)) (ba/exceptionally reduced))) + +(defn- expression-name-of-expression [{:keys [language expression]}] + (when (#{"text/cql" "text/cql-identifier"} (type/value language)) + (type/value expression))) + +(defn- expression-name-of-population [{:keys [criteria]}] + (expression-name-of-expression criteria)) + +(defn- expression-names-of-stratifier [{:keys [criteria component]}] + (if criteria + (some-> (expression-name-of-expression criteria) vector) + (coll/eduction (keep expression-name-of-population) component))) + +(defn- expression-names-of-group [{:keys [population stratifier]}] + (-> (into [] (keep expression-name-of-population) population) + (into (mapcat expression-names-of-stratifier) stratifier))) + +(defn expression-names + "Returns a set of alle CQL expression names found in `measure`." + {:arglists '([measure])} + [{:keys [group]}] + (into #{} (mapcat expression-names-of-group) group)) 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 47ef824c1..b2694f7c0 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 @@ -1,10 +1,10 @@ (ns blaze.fhir.operation.evaluate-measure.cql.spec (:require [blaze.elm.compiler :as-alias c] - [blaze.elm.compiler.external-data :as ed] [blaze.elm.compiler.library.spec] [blaze.elm.expression :as-alias expr] [blaze.elm.expression.spec] + [blaze.elm.resource :as cr] [blaze.fhir.operation.evaluate-measure :as-alias evaluate-measure] [blaze.fhir.operation.evaluate-measure.cql :as-alias cql] [blaze.fhir.operation.evaluate-measure.spec] @@ -35,10 +35,10 @@ :opt-un [::cql/population-basis]))) (s/def ::cql/subject-handle - ed/resource?) + cr/resource?) (s/def ::cql/population-handle - ed/resource?) + cr/resource?) (s/def ::cql/handle (s/keys :req-un [::cql/subject-handle ::cql/population-handle])) 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 7704316c1..e2497f718 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 @@ -2,9 +2,8 @@ (:require [blaze.async.comp :as ac] [blaze.elm.compiler :as-alias c] - [blaze.elm.compiler.external-data :as ed] - [blaze.elm.compiler.external-data-spec] [blaze.elm.compiler.spec] + [blaze.elm.resource :as cr] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.cql.spec] [blaze.fhir.spec] @@ -13,7 +12,7 @@ (s/fdef cql/evaluate-expression-1 :args (s/cat :context ::cql/context - :subject (s/nilable ed/resource?) + :subject (s/nilable cr/resource?) :name string? :expression ::c/expression) :ret (s/or :result any? :anomaly ::anom/anomaly)) @@ -25,7 +24,7 @@ (s/fdef cql/evaluate-individual-expression :args (s/cat :context ::cql/evaluate-expression-context - :subject ed/resource? + :subject cr/resource? :name string?) :ret ac/completable-future?) 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 65f31265d..7fdd823e7 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 @@ -18,6 +18,7 @@ [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] [cognitect.anomalies :as anom] + [integrant.core :as ig] [juxt.iota :refer [given]] [taoensso.timbre :as log]) (:import @@ -90,17 +91,25 @@ (fn [_ _ _] (throw (Exception. ^String msg)))) (defn- context - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock executor]} library] + [{:blaze.db/keys [node] + ::expr/keys [cache] + :blaze.test/keys [fixed-clock executor]} + library] (let [{:keys [expression-defs function-defs]} (compile-library node library)] {:db (d/db node) :now (now fixed-clock) + ::expr/cache cache :interrupted? (constantly nil) :expression-defs expression-defs :function-defs function-defs :executor executor})) (def ^:private config - (assoc mem-node-config :blaze.test/executor {})) + (assoc mem-node-config + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {})) (def ^:private conj-reduce-op (fn [_db] conj)) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj index b8c6f6379..0290e8865 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj @@ -8,3 +8,7 @@ (s/fdef u/expression-name :args (s/cat :population-path-fn fn? :criteria (s/nilable :fhir/Expression)) :ret (s/or :expression-name string? :anomaly ::anom/anomaly)) + +(s/fdef u/expression-names + :args (s/cat :measure :fhir/Measure) + :ret (s/coll-of string?)) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj index 2619183f7..33339fffd 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj @@ -5,7 +5,7 @@ [blaze.fhir.operation.evaluate-measure.measure.util-spec] [blaze.test-util :as tu :refer [satisfies-prop]] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [deftest testing]] + [clojure.test :as test :refer [are deftest testing]] [clojure.test.check.generators :as gen] [clojure.test.check.properties :as prop] [cognitect.anomalies :as anom] @@ -56,3 +56,43 @@ {:fhir/type :fhir/Expression :language #fhir/code"text/cql" :expression expression})))))) + +(defn- cql-expression [expr] + {:fhir/type :fhir/Expression + :language #fhir/code"text/cql-identifier" + :expression expr}) + +(deftest cql-definition-names-test + (are [measure names] (= names (u/expression-names measure)) + {:fhir/type :fhir/Measure :id "0" + :url #fhir/uri"measure-155502" + :library [#fhir/canonical"0"] + :group + [{:fhir/type :fhir.Measure/group + :population + [{:fhir/type :fhir.Measure.group/population + :criteria (cql-expression "InInitialPopulation")}] + :stratifier + [{:fhir/type :fhir.Measure.group/stratifier + :criteria (cql-expression "Gender")}]}]} + #{"InInitialPopulation" + "Gender"} + + {:fhir/type :fhir/Measure :id "0" + :url #fhir/uri"measure-155502" + :library [#fhir/canonical"0"] + :group + [{:fhir/type :fhir.Measure/group + :population + [{:fhir/type :fhir.Measure.group/population + :criteria (cql-expression "InInitialPopulation")}] + :stratifier + [{:fhir/type :fhir.Measure.group/stratifier + :component + [{:fhir/type :fhir.Measure.group.stratifier/component + :criteria (cql-expression "AgeClass")} + {:fhir/type :fhir.Measure.group.stratifier/component + :criteria (cql-expression "Gender")}]}]}]} + #{"InInitialPopulation" + "AgeClass" + "Gender"})) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj index 0527ab9a6..a9b170017 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj @@ -3,6 +3,7 @@ [blaze.async.comp :as ac] [blaze.cql-translator-spec] [blaze.db.spec] + [blaze.elm.expression :as-alias expr] [blaze.elm.expression.spec] [blaze.fhir.operation.evaluate-measure :as-alias evaluate-measure] [blaze.fhir.operation.evaluate-measure.cql-spec] @@ -18,7 +19,7 @@ (s/def ::context (s/keys :req [:blaze/base-url ::reitit/router] - :opt [:blaze/cancelled?] + :opt [:blaze/cancelled? ::expr/cache] :req-un [:blaze/clock :blaze/rng-fn :blaze.db/db ::evaluate-measure/executor] :opt-un [::evaluate-measure/timeout])) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj index f1bc41542..96ba87da2 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj @@ -1,8 +1,11 @@ (ns blaze.fhir.operation.evaluate-measure.measure-test (:require - [blaze.anomaly :as ba] + [blaze.anomaly :as ba :refer [when-ok]] [blaze.db.api :as d] [blaze.db.api-stub :refer [mem-node-config with-system-data]] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache :as ec] + [blaze.elm.expression.cache.bloom-filter :as-alias bloom-filter] [blaze.fhir.operation.evaluate-measure.measure :as measure] [blaze.fhir.operation.evaluate-measure.measure-spec] [blaze.fhir.operation.evaluate-measure.measure.group-spec] @@ -19,6 +22,7 @@ [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] [cognitect.anomalies :as anom] + [integrant.core :as ig] [java-time.api :as time] [juxt.iota :refer [given]] [reitit.core :as reitit] @@ -75,6 +79,9 @@ (def ^:private config (assoc mem-node-config + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} :blaze.test/fixed-rng-fn {} :blaze.test/executor {})) @@ -82,22 +89,30 @@ ([name] (evaluate name "population")) ([name report-type] + (evaluate name report-type false)) + ([name report-type twice-for-caching] (with-system-data [{:blaze.db/keys [node] + ::expr/keys [cache] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] [(tx-ops (:entry (read-data name)))] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - :blaze/base-url "" ::reitit/router router + ::expr/cache cache :blaze/base-url "" ::reitit/router router :executor executor} measure @(d/pull node (d/resource-handle db "Measure" "0")) - period [#system/date "2000" #system/date "2020"]] - (try - @(measure/evaluate-measure context measure - {:period period :report-type report-type}) - (catch Exception e - (ex-data (ex-cause e)))))))) + period [#system/date "2000" #system/date "2020"] + params {:period period :report-type report-type} + eval #(try + @(measure/evaluate-measure context measure params) + (catch Exception e + (ex-data (ex-cause e))))] + (if twice-for-caching + (when-ok [_ (eval)] + (Thread/sleep 200) + (eval)) + (eval)))))) (defn- first-population [result] (if (::anom/category result) @@ -172,6 +187,35 @@ define InInitialPopulation: [Encounter]") +(def library-exists-condition + "library Retrieve + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define InInitialPopulation: + exists [Condition]") + +(def library-medication + "library Retrieve + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + codesystem atc: 'http://fhir.de/CodeSystem/dimdi/atc' + + context Unfiltered + + define \"Temozolomid Refs\": + [Medication: Code 'L01AX03' from atc] M return 'Medication/' + M.id + + context Patient + + define InInitialPopulation: + Patient.gender = 'female' and + exists from [MedicationStatement] M + where M.medication.reference in \"Temozolomid Refs\"") + (def library-patient-encounter "library Retrieve using FHIR version '4.0.0' @@ -685,6 +729,122 @@ ::anom/category := ::anom/incorrect ::anom/message := "Subject with type `Patient` and id `0` was not found.")))))) +(def ^:private ^:const bloom-filter-ratio-url + "https://samply.github.io/blaze/fhir/StructureDefinition/bloom-filter-ratio") + +(defn- bloom-filter-ratio [extensions] + (some #(when (= bloom-filter-ratio-url (:url %)) %) extensions)) + +(defn- patient-condition-tx-ops [id] + (cond-> [[:put {:fhir/type :fhir/Patient :id (str id)}]] + (even? id) + (conj [:put {:fhir/type :fhir/Condition :id (str id) + :subject (type/reference {:reference (str "Patient/" id)})}]))) + +(defn- patient-medication-tx-ops [id] + (cond-> [[:put {:fhir/type :fhir/Patient :id (str id) + :gender (if (even? id) #fhir/code"female" #fhir/code"male")}]] + (zero? (rem id 4)) + (conj [:put {:fhir/type :fhir/MedicationStatement :id (str id) + :medication #fhir/Reference{:reference "Medication/0"} + :subject (type/reference {:reference (str "Patient/" id)})}]))) + +(deftest evaluate-measure-cache-test + (testing "Condition" + (with-system-data + [{:blaze.db/keys [node] + ::expr/keys [cache] + :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [(into [] (mapcat patient-condition-tx-ops) (range 2000)) + [[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" + :content [(library-content library-exists-condition)]}]]] + + (let [db (d/db node) + context {:clock fixed-clock :rng-fn fixed-rng-fn :db db + ::expr/cache cache + :executor executor :blaze/base-url "" ::reitit/router router} + measure {:fhir/type :fhir/Measure :id "0" + :library [#fhir/canonical"0"] + :group + [{:fhir/type :fhir.Measure/group + :population + [{:fhir/type :fhir.Measure.group/population + :code (population-concept "initial-population") + :criteria (cql-expression "InInitialPopulation")}]}]}] + + (testing "without bloom filter because it's not available yet" + (let [params {:period [#system/date "2000" #system/date "2100"] + :report-type "population"}] + (given (:resource @(measure/evaluate-measure context measure params)) + :fhir/type := :fhir/MeasureReport + [:extension bloom-filter-ratio :value :numerator :value] := 0M + [:extension bloom-filter-ratio :value :denominator :value] := 1M + [:group 0 :population 0 :count] := 1000))) + + (Thread/sleep 1000) + + (testing "with bloom filter" + (let [params {:period [#system/date "2000" #system/date "2100"] + :report-type "population"}] + (given (:resource @(measure/evaluate-measure context measure params)) + :fhir/type := :fhir/MeasureReport + [:extension bloom-filter-ratio :value :numerator :value] := 1M + [:extension bloom-filter-ratio :value :denominator :value] := 1M + [:group 0 :population 0 :count] := 1000)) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/expr-form] := "(exists (retrieve \"Condition\"))"))))) + + (testing "Medication" + (log/set-min-level! :info) + (with-system-data + [{:blaze.db/keys [node] + ::expr/keys [cache] + :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [[[:put {:fhir/type :fhir/Medication :id "0" + :code #fhir/CodeableConcept{:coding [#fhir/Coding{:system #fhir/uri"http://fhir.de/CodeSystem/dimdi/atc" :code #fhir/code"L01AX03"}]}}]] + (into [] (mapcat patient-medication-tx-ops) (range 2000)) + [[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" + :content [(library-content library-medication)]}]]] + + (let [db (d/db node) + context {:clock fixed-clock :rng-fn fixed-rng-fn :db db + ::expr/cache cache + :executor executor :blaze/base-url "" ::reitit/router router} + measure {:fhir/type :fhir/Measure :id "0" + :library [#fhir/canonical"0"] + :group + [{:fhir/type :fhir.Measure/group + :population + [{:fhir/type :fhir.Measure.group/population + :code (population-concept "initial-population") + :criteria (cql-expression "InInitialPopulation")}]}]}] + + (testing "without bloom filter because it's not available yet" + (let [params {:period [#system/date "2000" #system/date "2100"] + :report-type "population"}] + (given (:resource @(measure/evaluate-measure context measure params)) + :fhir/type := :fhir/MeasureReport + [:extension bloom-filter-ratio :value :numerator :value] := 0M + [:extension bloom-filter-ratio :value :denominator :value] := 1M + [:group 0 :population 0 :count] := 500))) + + (Thread/sleep 1000) + + (testing "with bloom filter" + (let [params {:period [#system/date "2000" #system/date "2100"] + :report-type "population"}] + (given (:resource @(measure/evaluate-measure context measure params)) + :fhir/type := :fhir/MeasureReport + [:extension bloom-filter-ratio :value :numerator :value] := 1M + [:extension bloom-filter-ratio :value :denominator :value] := 1M + [:group 0 :population 0 :count] := 500)) + + (given (into [] (ec/list-by-t cache)) + count := 1 + [0 ::bloom-filter/expr-form] := "(exists (eduction-query (comp (filter (fn [M] (contains [\"Medication/0\"] (call \"ToString\" (:reference (:medication M)))))) distinct) (retrieve \"MedicationStatement\")))")))))) + (defmacro testing-query [name count] `(testing ~name (is (= ~count (:count (first-population (evaluate ~name))))))) @@ -766,6 +926,12 @@ (testing-query "q53-population-basis-boolean" 2) + (testing "q57-mii-specimen-reference" + (given (evaluate "q57-mii-specimen-reference" "population" true) + [:resource :extension bloom-filter-ratio :value :numerator :value] := 6M + [:resource :extension bloom-filter-ratio :value :denominator :value] := 6M + [first-population :count] := 1)) + (let [result (evaluate "q1" "subject-list")] (testing "MeasureReport is valid" (is (s/valid? :blaze/resource (:resource result)))) @@ -973,7 +1139,7 @@ (given (first-stratifier-strata (evaluate "q52-sort-with-missing-values")) count := 1 - [0 :value :text] := "Condition[id = 0, t = 1]" + [0 :value :text] := "Condition[id = 0, t = 1, last-change-t = 1]" [0 :population 0 :count] := #fhir/integer 1) (given (first-stratifier-strata (evaluate "q54-stratifier-condition-code")) @@ -1020,6 +1186,12 @@ [1 :component 1 :extension 0 :value :code] := #fhir/code"cm" [1 :population 0 :count] := #fhir/integer 1)) +(deftest queries-with-errors-test + (testing "overly large non-parsable query" + (given (evaluate "q58-overly-large-nonparsable-query") + ::anom/category := ::anom/fault + ::anom/message := "Error while parsing the ELM representation of a CQL library: Could not convert library to JSON using JAXB serializer."))) + (comment (log/set-level! :debug) - (evaluate "q53-population-basis-boolean")) + (evaluate "q58-overly-large-nonparsable-query")) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q50-specimen-condition-reference.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q50-specimen-condition-reference.cql index c49d57a8f..04a88ae55 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q50-specimen-condition-reference.cql +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q50-specimen-condition-reference.cql @@ -12,4 +12,4 @@ context Patient define InInitialPopulation: exists [Specimen: "Serum Specimen"] S with [Condition: "Diabetes mellitus"] C - such that (S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value as Reference).reference = 'Condition/' + C.id + such that (S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value as Reference).reference = 'Condition/' + C.id diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.cql new file mode 100644 index 000000000..371a8d694 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.cql @@ -0,0 +1,40 @@ +library "q57-mii-specimen-reference" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem icd10: 'http://fhir.de/CodeSystem/bfarm/icd-10-gm' +codesystem snomed: 'http://snomed.info/sct' + +context Patient + +define "Criterion 1": + Patient.gender = 'female' + +define "Diagnose E13.9": + [Condition: Code 'E13.9' from icd10] union + [Condition: Code 'E13.91' from icd10] union + [Condition: Code 'E13.90' from icd10] + +define "Criterion 2": + exists (from [Specimen: Code '119364003' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) or + exists (from [Specimen: Code '258590006' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) or + exists (from [Specimen: Code '866034009' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) or + exists (from [Specimen: Code '866035005' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) or + exists (from [Specimen: Code '442427000' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) or + exists (from [Specimen: Code '737089009' from snomed] S + with "Diagnose E13.9" C + such that S.extension.where(url='https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose').first().value.as(Reference).reference = 'Condition/' + C.id) + +define InInitialPopulation: + "Criterion 1" and + "Criterion 2" diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.json new file mode 100644 index 000000000..1d88bb188 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q57-mii-specimen-reference.json @@ -0,0 +1,139 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "0", + "gender": "female" + }, + "request": { + "method": "PUT", + "url": "Patient/0" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "1", + "gender": "female" + }, + "request": { + "method": "PUT", + "url": "Patient/1" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "2" + }, + "request": { + "method": "PUT", + "url": "Patient/2" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "0", + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "code": "E13.91" + } + ] + }, + "subject": { + "reference": "Patient/0" + } + }, + "request": { + "method": "PUT", + "url": "Condition/0" + } + }, + { + "resource": { + "resourceType": "Specimen", + "id": "0", + "extension": [ + { + "url": "https://www.medizininformatik-initiative.de/fhir/ext/modul-biobank/StructureDefinition/Diagnose", + "valueReference": { + "reference": "Condition/0" + } + } + ], + "type": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "866035005" + } + ] + }, + "subject": { + "reference": "Patient/0" + } + }, + "request": { + "method": "PUT", + "url": "Specimen/0" + } + }, + { + "resource": { + "resourceType": "Measure", + "id": "0", + "url": "0", + "status": "active", + "subjectCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/resource-types", + "code": "Patient" + } + ] + }, + "library": [ + "0" + ], + "scoring": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Measure/0" + } + } + ] +} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.cql new file mode 100644 index 000000000..63caffc93 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.cql @@ -0,0 +1,509 @@ +library "q58-overly-large-nonparsable-query" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem sct: 'http://snomed.info/sct' + +context Patient + +define InInitialPopulation: + exists [Condition: Code '0' from sct] or + exists [Condition: Code '1' from sct] or + exists [Condition: Code '2' from sct] or + exists [Condition: Code '3' from sct] or + exists [Condition: Code '4' from sct] or + exists [Condition: Code '5' from sct] or + exists [Condition: Code '6' from sct] or + exists [Condition: Code '7' from sct] or + exists [Condition: Code '8' from sct] or + exists [Condition: Code '9' from sct] or + exists [Condition: Code '10' from sct] or + exists [Condition: Code '11' from sct] or + exists [Condition: Code '12' from sct] or + exists [Condition: Code '13' from sct] or + exists [Condition: Code '14' from sct] or + exists [Condition: Code '15' from sct] or + exists [Condition: Code '16' from sct] or + exists [Condition: Code '17' from sct] or + exists [Condition: Code '18' from sct] or + exists [Condition: Code '19' from sct] or + exists [Condition: Code '20' from sct] or + exists [Condition: Code '21' from sct] or + exists [Condition: Code '22' from sct] or + exists [Condition: Code '23' from sct] or + exists [Condition: Code '24' from sct] or + exists [Condition: Code '25' from sct] or + exists [Condition: Code '26' from sct] or + exists [Condition: Code '27' from sct] or + exists [Condition: Code '28' from sct] or + exists [Condition: Code '29' from sct] or + exists [Condition: Code '30' from sct] or + exists [Condition: Code '31' from sct] or + exists [Condition: Code '32' from sct] or + exists [Condition: Code '33' from sct] or + exists [Condition: Code '34' from sct] or + exists [Condition: Code '35' from sct] or + exists [Condition: Code '36' from sct] or + exists [Condition: Code '37' from sct] or + exists [Condition: Code '38' from sct] or + exists [Condition: Code '39' from sct] or + exists [Condition: Code '40' from sct] or + exists [Condition: Code '41' from sct] or + exists [Condition: Code '42' from sct] or + exists [Condition: Code '43' from sct] or + exists [Condition: Code '44' from sct] or + exists [Condition: Code '45' from sct] or + exists [Condition: Code '46' from sct] or + exists [Condition: Code '47' from sct] or + exists [Condition: Code '48' from sct] or + exists [Condition: Code '49' from sct] or + exists [Condition: Code '50' from sct] or + exists [Condition: Code '51' from sct] or + exists [Condition: Code '52' from sct] or + exists [Condition: Code '53' from sct] or + exists [Condition: Code '54' from sct] or + exists [Condition: Code '55' from sct] or + exists [Condition: Code '56' from sct] or + exists [Condition: Code '57' from sct] or + exists [Condition: Code '58' from sct] or + exists [Condition: Code '59' from sct] or + exists [Condition: Code '60' from sct] or + exists [Condition: Code '61' from sct] or + exists [Condition: Code '62' from sct] or + exists [Condition: Code '63' from sct] or + exists [Condition: Code '64' from sct] or + exists [Condition: Code '65' from sct] or + exists [Condition: Code '66' from sct] or + exists [Condition: Code '67' from sct] or + exists [Condition: Code '68' from sct] or + exists [Condition: Code '69' from sct] or + exists [Condition: Code '70' from sct] or + exists [Condition: Code '71' from sct] or + exists [Condition: Code '72' from sct] or + exists [Condition: Code '73' from sct] or + exists [Condition: Code '74' from sct] or + exists [Condition: Code '75' from sct] or + exists [Condition: Code '76' from sct] or + exists [Condition: Code '77' from sct] or + exists [Condition: Code '78' from sct] or + exists [Condition: Code '79' from sct] or + exists [Condition: Code '80' from sct] or + exists [Condition: Code '81' from sct] or + exists [Condition: Code '82' from sct] or + exists [Condition: Code '83' from sct] or + exists [Condition: Code '84' from sct] or + exists [Condition: Code '85' from sct] or + exists [Condition: Code '86' from sct] or + exists [Condition: Code '87' from sct] or + exists [Condition: Code '88' from sct] or + exists [Condition: Code '89' from sct] or + exists [Condition: Code '90' from sct] or + exists [Condition: Code '91' from sct] or + exists [Condition: Code '92' from sct] or + exists [Condition: Code '93' from sct] or + exists [Condition: Code '94' from sct] or + exists [Condition: Code '95' from sct] or + exists [Condition: Code '96' from sct] or + exists [Condition: Code '97' from sct] or + exists [Condition: Code '98' from sct] or + exists [Condition: Code '99' from sct] or + exists [Condition: Code '100' from sct] or + exists [Condition: Code '101' from sct] or + exists [Condition: Code '102' from sct] or + exists [Condition: Code '103' from sct] or + exists [Condition: Code '104' from sct] or + exists [Condition: Code '105' from sct] or + exists [Condition: Code '106' from sct] or + exists [Condition: Code '107' from sct] or + exists [Condition: Code '108' from sct] or + exists [Condition: Code '109' from sct] or + exists [Condition: Code '110' from sct] or + exists [Condition: Code '111' from sct] or + exists [Condition: Code '112' from sct] or + exists [Condition: Code '113' from sct] or + exists [Condition: Code '114' from sct] or + exists [Condition: Code '115' from sct] or + exists [Condition: Code '116' from sct] or + exists [Condition: Code '117' from sct] or + exists [Condition: Code '118' from sct] or + exists [Condition: Code '119' from sct] or + exists [Condition: Code '120' from sct] or + exists [Condition: Code '121' from sct] or + exists [Condition: Code '122' from sct] or + exists [Condition: Code '123' from sct] or + exists [Condition: Code '124' from sct] or + exists [Condition: Code '125' from sct] or + exists [Condition: Code '126' from sct] or + exists [Condition: Code '127' from sct] or + exists [Condition: Code '128' from sct] or + exists [Condition: Code '129' from sct] or + exists [Condition: Code '130' from sct] or + exists [Condition: Code '131' from sct] or + exists [Condition: Code '132' from sct] or + exists [Condition: Code '133' from sct] or + exists [Condition: Code '134' from sct] or + exists [Condition: Code '135' from sct] or + exists [Condition: Code '136' from sct] or + exists [Condition: Code '137' from sct] or + exists [Condition: Code '138' from sct] or + exists [Condition: Code '139' from sct] or + exists [Condition: Code '140' from sct] or + exists [Condition: Code '141' from sct] or + exists [Condition: Code '142' from sct] or + exists [Condition: Code '143' from sct] or + exists [Condition: Code '144' from sct] or + exists [Condition: Code '145' from sct] or + exists [Condition: Code '146' from sct] or + exists [Condition: Code '147' from sct] or + exists [Condition: Code '148' from sct] or + exists [Condition: Code '149' from sct] or + exists [Condition: Code '150' from sct] or + exists [Condition: Code '151' from sct] or + exists [Condition: Code '152' from sct] or + exists [Condition: Code '153' from sct] or + exists [Condition: Code '154' from sct] or + exists [Condition: Code '155' from sct] or + exists [Condition: Code '156' from sct] or + exists [Condition: Code '157' from sct] or + exists [Condition: Code '158' from sct] or + exists [Condition: Code '159' from sct] or + exists [Condition: Code '160' from sct] or + exists [Condition: Code '161' from sct] or + exists [Condition: Code '162' from sct] or + exists [Condition: Code '163' from sct] or + exists [Condition: Code '164' from sct] or + exists [Condition: Code '165' from sct] or + exists [Condition: Code '166' from sct] or + exists [Condition: Code '167' from sct] or + exists [Condition: Code '168' from sct] or + exists [Condition: Code '169' from sct] or + exists [Condition: Code '170' from sct] or + exists [Condition: Code '171' from sct] or + exists [Condition: Code '172' from sct] or + exists [Condition: Code '173' from sct] or + exists [Condition: Code '174' from sct] or + exists [Condition: Code '175' from sct] or + exists [Condition: Code '176' from sct] or + exists [Condition: Code '177' from sct] or + exists [Condition: Code '178' from sct] or + exists [Condition: Code '179' from sct] or + exists [Condition: Code '180' from sct] or + exists [Condition: Code '181' from sct] or + exists [Condition: Code '182' from sct] or + exists [Condition: Code '183' from sct] or + exists [Condition: Code '184' from sct] or + exists [Condition: Code '185' from sct] or + exists [Condition: Code '186' from sct] or + exists [Condition: Code '187' from sct] or + exists [Condition: Code '188' from sct] or + exists [Condition: Code '189' from sct] or + exists [Condition: Code '190' from sct] or + exists [Condition: Code '191' from sct] or + exists [Condition: Code '192' from sct] or + exists [Condition: Code '193' from sct] or + exists [Condition: Code '194' from sct] or + exists [Condition: Code '195' from sct] or + exists [Condition: Code '196' from sct] or + exists [Condition: Code '197' from sct] or + exists [Condition: Code '198' from sct] or + exists [Condition: Code '199' from sct] or + exists [Condition: Code '200' from sct] or + exists [Condition: Code '201' from sct] or + exists [Condition: Code '202' from sct] or + exists [Condition: Code '203' from sct] or + exists [Condition: Code '204' from sct] or + exists [Condition: Code '205' from sct] or + exists [Condition: Code '206' from sct] or + exists [Condition: Code '207' from sct] or + exists [Condition: Code '208' from sct] or + exists [Condition: Code '209' from sct] or + exists [Condition: Code '210' from sct] or + exists [Condition: Code '211' from sct] or + exists [Condition: Code '212' from sct] or + exists [Condition: Code '213' from sct] or + exists [Condition: Code '214' from sct] or + exists [Condition: Code '215' from sct] or + exists [Condition: Code '216' from sct] or + exists [Condition: Code '217' from sct] or + exists [Condition: Code '218' from sct] or + exists [Condition: Code '219' from sct] or + exists [Condition: Code '220' from sct] or + exists [Condition: Code '221' from sct] or + exists [Condition: Code '222' from sct] or + exists [Condition: Code '223' from sct] or + exists [Condition: Code '224' from sct] or + exists [Condition: Code '225' from sct] or + exists [Condition: Code '226' from sct] or + exists [Condition: Code '227' from sct] or + exists [Condition: Code '228' from sct] or + exists [Condition: Code '229' from sct] or + exists [Condition: Code '230' from sct] or + exists [Condition: Code '231' from sct] or + exists [Condition: Code '232' from sct] or + exists [Condition: Code '233' from sct] or + exists [Condition: Code '234' from sct] or + exists [Condition: Code '235' from sct] or + exists [Condition: Code '236' from sct] or + exists [Condition: Code '237' from sct] or + exists [Condition: Code '238' from sct] or + exists [Condition: Code '239' from sct] or + exists [Condition: Code '240' from sct] or + exists [Condition: Code '241' from sct] or + exists [Condition: Code '242' from sct] or + exists [Condition: Code '243' from sct] or + exists [Condition: Code '244' from sct] or + exists [Condition: Code '245' from sct] or + exists [Condition: Code '246' from sct] or + exists [Condition: Code '247' from sct] or + exists [Condition: Code '248' from sct] or + exists [Condition: Code '249' from sct] or + exists [Condition: Code '250' from sct] or + exists [Condition: Code '251' from sct] or + exists [Condition: Code '252' from sct] or + exists [Condition: Code '253' from sct] or + exists [Condition: Code '254' from sct] or + exists [Condition: Code '255' from sct] or + exists [Condition: Code '256' from sct] or + exists [Condition: Code '257' from sct] or + exists [Condition: Code '258' from sct] or + exists [Condition: Code '259' from sct] or + exists [Condition: Code '260' from sct] or + exists [Condition: Code '261' from sct] or + exists [Condition: Code '262' from sct] or + exists [Condition: Code '263' from sct] or + exists [Condition: Code '264' from sct] or + exists [Condition: Code '265' from sct] or + exists [Condition: Code '266' from sct] or + exists [Condition: Code '267' from sct] or + exists [Condition: Code '268' from sct] or + exists [Condition: Code '269' from sct] or + exists [Condition: Code '270' from sct] or + exists [Condition: Code '271' from sct] or + exists [Condition: Code '272' from sct] or + exists [Condition: Code '273' from sct] or + exists [Condition: Code '274' from sct] or + exists [Condition: Code '275' from sct] or + exists [Condition: Code '276' from sct] or + exists [Condition: Code '277' from sct] or + exists [Condition: Code '278' from sct] or + exists [Condition: Code '279' from sct] or + exists [Condition: Code '280' from sct] or + exists [Condition: Code '281' from sct] or + exists [Condition: Code '282' from sct] or + exists [Condition: Code '283' from sct] or + exists [Condition: Code '284' from sct] or + exists [Condition: Code '285' from sct] or + exists [Condition: Code '286' from sct] or + exists [Condition: Code '287' from sct] or + exists [Condition: Code '288' from sct] or + exists [Condition: Code '289' from sct] or + exists [Condition: Code '290' from sct] or + exists [Condition: Code '291' from sct] or + exists [Condition: Code '292' from sct] or + exists [Condition: Code '293' from sct] or + exists [Condition: Code '294' from sct] or + exists [Condition: Code '295' from sct] or + exists [Condition: Code '296' from sct] or + exists [Condition: Code '297' from sct] or + exists [Condition: Code '298' from sct] or + exists [Condition: Code '299' from sct] or + exists [Condition: Code '300' from sct] or + exists [Condition: Code '301' from sct] or + exists [Condition: Code '302' from sct] or + exists [Condition: Code '303' from sct] or + exists [Condition: Code '304' from sct] or + exists [Condition: Code '305' from sct] or + exists [Condition: Code '306' from sct] or + exists [Condition: Code '307' from sct] or + exists [Condition: Code '308' from sct] or + exists [Condition: Code '309' from sct] or + exists [Condition: Code '310' from sct] or + exists [Condition: Code '311' from sct] or + exists [Condition: Code '312' from sct] or + exists [Condition: Code '313' from sct] or + exists [Condition: Code '314' from sct] or + exists [Condition: Code '315' from sct] or + exists [Condition: Code '316' from sct] or + exists [Condition: Code '317' from sct] or + exists [Condition: Code '318' from sct] or + exists [Condition: Code '319' from sct] or + exists [Condition: Code '320' from sct] or + exists [Condition: Code '321' from sct] or + exists [Condition: Code '322' from sct] or + exists [Condition: Code '323' from sct] or + exists [Condition: Code '324' from sct] or + exists [Condition: Code '325' from sct] or + exists [Condition: Code '326' from sct] or + exists [Condition: Code '327' from sct] or + exists [Condition: Code '328' from sct] or + exists [Condition: Code '329' from sct] or + exists [Condition: Code '330' from sct] or + exists [Condition: Code '331' from sct] or + exists [Condition: Code '332' from sct] or + exists [Condition: Code '333' from sct] or + exists [Condition: Code '334' from sct] or + exists [Condition: Code '335' from sct] or + exists [Condition: Code '336' from sct] or + exists [Condition: Code '337' from sct] or + exists [Condition: Code '338' from sct] or + exists [Condition: Code '339' from sct] or + exists [Condition: Code '340' from sct] or + exists [Condition: Code '341' from sct] or + exists [Condition: Code '342' from sct] or + exists [Condition: Code '343' from sct] or + exists [Condition: Code '344' from sct] or + exists [Condition: Code '345' from sct] or + exists [Condition: Code '346' from sct] or + exists [Condition: Code '347' from sct] or + exists [Condition: Code '348' from sct] or + exists [Condition: Code '349' from sct] or + exists [Condition: Code '350' from sct] or + exists [Condition: Code '351' from sct] or + exists [Condition: Code '352' from sct] or + exists [Condition: Code '353' from sct] or + exists [Condition: Code '354' from sct] or + exists [Condition: Code '355' from sct] or + exists [Condition: Code '356' from sct] or + exists [Condition: Code '357' from sct] or + exists [Condition: Code '358' from sct] or + exists [Condition: Code '359' from sct] or + exists [Condition: Code '360' from sct] or + exists [Condition: Code '361' from sct] or + exists [Condition: Code '362' from sct] or + exists [Condition: Code '363' from sct] or + exists [Condition: Code '364' from sct] or + exists [Condition: Code '365' from sct] or + exists [Condition: Code '366' from sct] or + exists [Condition: Code '367' from sct] or + exists [Condition: Code '368' from sct] or + exists [Condition: Code '369' from sct] or + exists [Condition: Code '370' from sct] or + exists [Condition: Code '371' from sct] or + exists [Condition: Code '372' from sct] or + exists [Condition: Code '373' from sct] or + exists [Condition: Code '374' from sct] or + exists [Condition: Code '375' from sct] or + exists [Condition: Code '376' from sct] or + exists [Condition: Code '377' from sct] or + exists [Condition: Code '378' from sct] or + exists [Condition: Code '379' from sct] or + exists [Condition: Code '380' from sct] or + exists [Condition: Code '381' from sct] or + exists [Condition: Code '382' from sct] or + exists [Condition: Code '383' from sct] or + exists [Condition: Code '384' from sct] or + exists [Condition: Code '385' from sct] or + exists [Condition: Code '386' from sct] or + exists [Condition: Code '387' from sct] or + exists [Condition: Code '388' from sct] or + exists [Condition: Code '389' from sct] or + exists [Condition: Code '390' from sct] or + exists [Condition: Code '391' from sct] or + exists [Condition: Code '392' from sct] or + exists [Condition: Code '393' from sct] or + exists [Condition: Code '394' from sct] or + exists [Condition: Code '395' from sct] or + exists [Condition: Code '396' from sct] or + exists [Condition: Code '397' from sct] or + exists [Condition: Code '398' from sct] or + exists [Condition: Code '399' from sct] or + exists [Condition: Code '400' from sct] or + exists [Condition: Code '401' from sct] or + exists [Condition: Code '402' from sct] or + exists [Condition: Code '403' from sct] or + exists [Condition: Code '404' from sct] or + exists [Condition: Code '405' from sct] or + exists [Condition: Code '406' from sct] or + exists [Condition: Code '407' from sct] or + exists [Condition: Code '408' from sct] or + exists [Condition: Code '409' from sct] or + exists [Condition: Code '410' from sct] or + exists [Condition: Code '411' from sct] or + exists [Condition: Code '412' from sct] or + exists [Condition: Code '413' from sct] or + exists [Condition: Code '414' from sct] or + exists [Condition: Code '415' from sct] or + exists [Condition: Code '416' from sct] or + exists [Condition: Code '417' from sct] or + exists [Condition: Code '418' from sct] or + exists [Condition: Code '419' from sct] or + exists [Condition: Code '420' from sct] or + exists [Condition: Code '421' from sct] or + exists [Condition: Code '422' from sct] or + exists [Condition: Code '423' from sct] or + exists [Condition: Code '424' from sct] or + exists [Condition: Code '425' from sct] or + exists [Condition: Code '426' from sct] or + exists [Condition: Code '427' from sct] or + exists [Condition: Code '428' from sct] or + exists [Condition: Code '429' from sct] or + exists [Condition: Code '430' from sct] or + exists [Condition: Code '431' from sct] or + exists [Condition: Code '432' from sct] or + exists [Condition: Code '433' from sct] or + exists [Condition: Code '434' from sct] or + exists [Condition: Code '435' from sct] or + exists [Condition: Code '436' from sct] or + exists [Condition: Code '437' from sct] or + exists [Condition: Code '438' from sct] or + exists [Condition: Code '439' from sct] or + exists [Condition: Code '440' from sct] or + exists [Condition: Code '441' from sct] or + exists [Condition: Code '442' from sct] or + exists [Condition: Code '443' from sct] or + exists [Condition: Code '444' from sct] or + exists [Condition: Code '445' from sct] or + exists [Condition: Code '446' from sct] or + exists [Condition: Code '447' from sct] or + exists [Condition: Code '448' from sct] or + exists [Condition: Code '449' from sct] or + exists [Condition: Code '450' from sct] or + exists [Condition: Code '451' from sct] or + exists [Condition: Code '452' from sct] or + exists [Condition: Code '453' from sct] or + exists [Condition: Code '454' from sct] or + exists [Condition: Code '455' from sct] or + exists [Condition: Code '456' from sct] or + exists [Condition: Code '457' from sct] or + exists [Condition: Code '458' from sct] or + exists [Condition: Code '459' from sct] or + exists [Condition: Code '460' from sct] or + exists [Condition: Code '461' from sct] or + exists [Condition: Code '462' from sct] or + exists [Condition: Code '463' from sct] or + exists [Condition: Code '464' from sct] or + exists [Condition: Code '465' from sct] or + exists [Condition: Code '466' from sct] or + exists [Condition: Code '467' from sct] or + exists [Condition: Code '468' from sct] or + exists [Condition: Code '469' from sct] or + exists [Condition: Code '470' from sct] or + exists [Condition: Code '471' from sct] or + exists [Condition: Code '472' from sct] or + exists [Condition: Code '473' from sct] or + exists [Condition: Code '474' from sct] or + exists [Condition: Code '475' from sct] or + exists [Condition: Code '476' from sct] or + exists [Condition: Code '477' from sct] or + exists [Condition: Code '478' from sct] or + exists [Condition: Code '479' from sct] or + exists [Condition: Code '480' from sct] or + exists [Condition: Code '481' from sct] or + exists [Condition: Code '482' from sct] or + exists [Condition: Code '483' from sct] or + exists [Condition: Code '484' from sct] or + exists [Condition: Code '485' from sct] or + exists [Condition: Code '486' from sct] or + exists [Condition: Code '487' from sct] or + exists [Condition: Code '488' from sct] or + exists [Condition: Code '489' from sct] or + exists [Condition: Code '490' from sct] or + exists [Condition: Code '491' from sct] or + exists [Condition: Code '492' from sct] or + exists [Condition: Code '493' from sct] or + exists [Condition: Code '494' from sct] or + exists [Condition: Code '495' from sct] or + exists [Condition: Code '496' from sct] or + exists [Condition: Code '497' from sct] or + exists [Condition: Code '498' from sct] or + exists [Condition: Code '499' from sct] diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.json new file mode 100644 index 000000000..4ed75a355 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q58-overly-large-nonparsable-query.json @@ -0,0 +1,57 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Measure", + "id": "0", + "url": "0", + "status": "active", + "subjectCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/resource-types", + "code": "Patient" + } + ] + }, + "library": [ + "0" + ], + "scoring": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Measure/0" + } + } + ] +} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj index cbabea32e..f3d5d76b0 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj @@ -2,8 +2,8 @@ (:require [blaze.async.comp :as ac] [blaze.db.api :as d] - [blaze.elm.compiler.external-data :as ed] - [blaze.elm.compiler.external-data-spec] + [blaze.elm.resource :as cr] + [blaze.elm.resource-spec] [blaze.handler.util :as handler-util])) (defn wrap-error [handler] @@ -15,7 +15,7 @@ {:population-handle subject-handle :subject-handle subject-handle}) (defn handle-mapper [db] - (comp (ed/resource-mapper db) (map handle))) + (comp (cr/resource-mapper db) (map handle))) (defn resource [db type id] - (ed/mk-resource db (d/resource-handle db type id))) + (cr/mk-resource db (d/resource-handle db type id))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj index f013bb48e..4299c42f0 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj @@ -5,6 +5,7 @@ [blaze.db.api-stub :as api-stub :refer [with-system-data]] [blaze.db.node :refer [node?]] [blaze.db.resource-store :as rs] + [blaze.elm.expression :as-alias expr] [blaze.executors :as ex] [blaze.fhir.operation.evaluate-measure :as evaluate-measure] [blaze.fhir.operation.evaluate-measure.test-util :refer [wrap-error]] @@ -140,6 +141,7 @@ api-stub/mem-node-config ::evaluate-measure/handler {:node (ig/ref :blaze.db/node) + ::expr/cache (ig/ref ::expr/cache) :executor (ig/ref :blaze.test/executor) :clock (ig/ref :blaze.test/fixed-clock) :rng-fn (ig/ref :blaze.test/fixed-rng-fn)} @@ -147,6 +149,9 @@ {:node (ig/ref :blaze.db/node) :clock (ig/ref :blaze.test/fixed-clock) :rng-fn (ig/ref :blaze.test/fixed-rng-fn)} + ::expr/cache + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} :blaze.test/executor {} :blaze.test/fixed-rng-fn {})) diff --git a/modules/rest-util/src/blaze/handler/fhir/util.clj b/modules/rest-util/src/blaze/handler/fhir/util.clj index 0d8024fc5..e6c7312f4 100644 --- a/modules/rest-util/src/blaze/handler/fhir/util.clj +++ b/modules/rest-util/src/blaze/handler/fhir/util.clj @@ -201,7 +201,7 @@ if-match :ifMatch if-none-exist :ifNoneExist} :request :keys [resource]}] - (let [url (-> url type/value u/strip-leading-slash) + (let [url (-> url type/value u/strip-leading-slashes) [url query-string] (str/split url #"\?") method (keyword (str/lower-case (type/value method))) return-preference (or return-preference @@ -415,7 +415,7 @@ {:arglists '([idx entry])} [idx {:keys [request resource] :as entry}] (let [method (some-> request :method type/value) - [url] (some-> request :url type/value u/strip-leading-slash (str/split #"\?")) + [url] (some-> request :url type/value u/strip-leading-slashes (str/split #"\?")) {:keys [type id kind]} (some-> url match-url)] (cond (nil? request) diff --git a/modules/rocksdb/src/blaze/db/kv/rocksdb/metrics.clj b/modules/rocksdb/src/blaze/db/kv/rocksdb/metrics.clj index 9ed3d0757..5479801e7 100644 --- a/modules/rocksdb/src/blaze/db/kv/rocksdb/metrics.clj +++ b/modules/rocksdb/src/blaze/db/kv/rocksdb/metrics.clj @@ -203,13 +203,13 @@ (def ^:private bloom-filter-full-positive-total (counter-metric "blaze_rocksdb_bloom_filter_full_positive_total" - "Number of times bloom FullFilter has not avoided the reads." + "Number of times Bloom FullFilter has not avoided the reads." TickerType/BLOOM_FILTER_FULL_POSITIVE)) (def ^:private bloom-filter-full-true-positive-total (counter-metric "blaze_rocksdb_bloom_filter_full_true_positive_total" - "Number of times bloom FullFilter has not avoided the reads and data actually exist." + "Number of times Bloom FullFilter has not avoided the reads and data actually exist." TickerType/BLOOM_FILTER_FULL_TRUE_POSITIVE)) (def ^:private blocks-compressed-total diff --git a/modules/scheduler/src/blaze/scheduler.clj b/modules/scheduler/src/blaze/scheduler.clj index 00cc904f8..78beac8a9 100644 --- a/modules/scheduler/src/blaze/scheduler.clj +++ b/modules/scheduler/src/blaze/scheduler.clj @@ -11,6 +11,13 @@ (set! *warn-on-reflection* true) +(defn submit + "Submits the function `f` to be called later. + + Returns a future that can be used in `cancel`." + [scheduler f] + (p/-submit scheduler f)) + (defn schedule-at-fixed-rate "Schedules the function `f` to be called at a rate of `period` with an `initial-delay`. @@ -24,6 +31,9 @@ (extend-protocol p/Scheduler ScheduledExecutorService + (-submit [scheduler f] + (.submit scheduler ^Runnable f)) + (-schedule-at-fixed-rate [scheduler f initial-delay period] (.scheduleAtFixedRate scheduler diff --git a/modules/scheduler/src/blaze/scheduler_spec.clj b/modules/scheduler/src/blaze/scheduler_spec.clj index 9d87fdd47..e0d7eb908 100644 --- a/modules/scheduler/src/blaze/scheduler_spec.clj +++ b/modules/scheduler/src/blaze/scheduler_spec.clj @@ -5,6 +5,10 @@ [clojure.spec.alpha :as s] [java-time.api :as time])) +(s/fdef sched/submit + :args (s/cat :scheduler :blaze/scheduler :f fn?) + :ret :blaze.scheduler/future) + (s/fdef sched/schedule-at-fixed-rate :args (s/cat :scheduler :blaze/scheduler :f fn? :initial-delay time/duration? :period time/duration?) diff --git a/modules/thread-pool-executor-collector/test/blaze/thread_pool_executor_collector_test.clj b/modules/thread-pool-executor-collector/test/blaze/thread_pool_executor_collector_test.clj index b3de602c8..d9ef51831 100644 --- a/modules/thread-pool-executor-collector/test/blaze/thread_pool_executor_collector_test.clj +++ b/modules/thread-pool-executor-collector/test/blaze/thread_pool_executor_collector_test.clj @@ -17,7 +17,7 @@ (set! *warn-on-reflection* true) (st/instrument) -(log/set-level! :trace) +(log/set-min-level! :trace) (test/use-fixtures :each tu/fixture) @@ -86,7 +86,7 @@ [6 :samples 0 :value] := 0.0)) (testing "one active thread" - (ex/execute! pool #(Thread/sleep 1000)) + (ex/execute! pool #(Thread/sleep 2000)) (given (metrics/collect collector) [0 :name] := "thread_pool_executor_active_count" [0 :samples 0 :value] := 1.0 diff --git a/profiling/blaze/profiling.clj b/profiling/blaze/profiling.clj index e06c5dec5..49682c842 100644 --- a/profiling/blaze/profiling.clj +++ b/profiling/blaze/profiling.clj @@ -3,8 +3,12 @@ (:require [blaze.system :as system] [blaze.cache-collector :as cc] + [blaze.cache-collector.protocols :as ccp] [blaze.db.kv.rocksdb :as rocksdb] [blaze.db.resource-cache :as resource-cache] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.cache :as ec] + [blaze.system :as system] [clojure.tools.namespace.repl :refer [refresh]] [taoensso.timbre :as log])) @@ -43,16 +47,22 @@ ;; Transaction Cache (comment - (str (cc/-stats (:blaze.db/tx-cache system))) + (str (ccp/-stats (:blaze.db/tx-cache system))) (resource-cache/invalidate-all! (:blaze.db/tx-cache system)) ) ;; Resource Cache (comment - (str (cc/-stats (:blaze.db/resource-cache system))) + (str (ccp/-stats (:blaze.db/resource-cache system))) (resource-cache/invalidate-all! (:blaze.db/resource-cache system)) ) +;; CQL Expression Cache +(comment + (into [] (ec/list-by-t (::expr/cache system))) + (str (ccp/-stats (::expr/cache system))) + ) + ;; DB (comment (str (system [:blaze.db.kv.rocksdb/stats :blaze.db.index-kv-store/stats])) @@ -69,6 +79,7 @@ (rocksdb/property index-db :resource-as-of-index "rocksdb.stats") (rocksdb/property index-db :type-as-of-index "rocksdb.stats") (rocksdb/property index-db :system-as-of-index "rocksdb.stats") + (rocksdb/property index-db :patient-last-change-index "rocksdb.stats") (rocksdb/property index-db :type-stats-index "rocksdb.stats") (rocksdb/property index-db :system-stats-index "rocksdb.stats") diff --git a/resources/blaze.edn b/resources/blaze.edn index b65ae6282..bd56d7f34 100644 --- a/resources/blaze.edn +++ b/resources/blaze.edn @@ -188,6 +188,11 @@ :blaze.rest-api/async-status-cancel-handler {:job-scheduler #blaze/ref :blaze/job-scheduler} + ;; + ;; CQL Evaluation Engine + ;; + :blaze.cql/retrieve-total {} + ;; ;; FHIR Operation Evaluate Measure ;; @@ -276,6 +281,7 @@ :kv-store #blaze/ref :blaze.db.main/index-kv-store :resource-indexer #blaze/ref :blaze.db.node.main/resource-indexer :search-param-registry #blaze/ref :blaze.db/search-param-registry + :scheduler #blaze/ref :blaze/scheduler :enforce-referential-integrity #blaze/cfg ["ENFORCE_REFERENTIAL_INTEGRITY" boolean? true]} [:blaze.db.node/indexer-executor :blaze.db.node.main/indexer-executor] {} @@ -291,7 +297,8 @@ :resource-store #blaze/ref :blaze.db/resource-cache :kv-store #blaze/ref :blaze.db.admin/index-kv-store :resource-indexer #blaze/ref :blaze.db.node.admin/resource-indexer - :search-param-registry #blaze/ref :blaze.db/search-param-registry} + :search-param-registry #blaze/ref :blaze.db/search-param-registry + :scheduler #blaze/ref :blaze/scheduler} [:blaze.db.node/indexer-executor :blaze.db.node.admin/indexer-executor] {} @@ -403,7 +410,10 @@ :blaze.job/re-index {:main-node #blaze/ref :blaze.db.main/node :admin-node #blaze/ref :blaze.db.admin/node - :clock #blaze/ref :blaze/clock}} + :clock #blaze/ref :blaze/clock + :extra-bundle-file #blaze/cfg ["DB_SEARCH_PARAM_BUNDLE" string?]} + + :blaze/scheduler {}} :storage {:in-memory @@ -427,8 +437,11 @@ :resource-as-of-index nil :type-as-of-index nil :system-as-of-index nil + :patient-last-change-index nil :type-stats-index nil - :system-stats-index nil}} + :system-stats-index nil + :cql-bloom-filter nil + :cql-bloom-filter-by-t nil}} ;; ;; Admin In-Memory, Volatile Index Store @@ -449,8 +462,11 @@ :resource-as-of-index nil :type-as-of-index nil :system-as-of-index nil + :patient-last-change-index nil :type-stats-index nil - :system-stats-index nil}} + :system-stats-index nil + :cql-bloom-filter nil + :cql-bloom-filter-by-t nil}} ;; ;; Main Local Transaction Log for Single Node Deployments @@ -614,6 +630,12 @@ :target-file-size-base-in-mb 8 :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :patient-last-change-index + {:write-buffer-size-in-mb 8 + :max-bytes-for-level-base-in-mb 32 + :target-file-size-base-in-mb 8 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :type-stats-index {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 @@ -624,7 +646,22 @@ {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 :target-file-size-base-in-mb 2 - :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]}}} + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + + :cql-bloom-filter + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :enable-blob-files? true + :min-blob-size 512} + + :cql-bloom-filter-by-t + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :reverse-comparator? true}}} [:blaze.db.kv.rocksdb/stats :blaze.db.index-kv-store.main/stats] {} @@ -699,6 +736,12 @@ :target-file-size-base-in-mb 8 :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :patient-last-change-index + {:write-buffer-size-in-mb 8 + :max-bytes-for-level-base-in-mb 32 + :target-file-size-base-in-mb 8 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :type-stats-index {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 @@ -709,7 +752,22 @@ {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 :target-file-size-base-in-mb 2 - :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]}}} + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + + :cql-bloom-filter + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :enable-blob-files? true + :min-blob-size 512} + + :cql-bloom-filter-by-t + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :reverse-comparator? true}}} ;; ;; Main Local Transaction Log for Single Node Deployments @@ -926,6 +984,12 @@ :target-file-size-base-in-mb 8 :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :patient-last-change-index + {:write-buffer-size-in-mb 8 + :max-bytes-for-level-base-in-mb 32 + :target-file-size-base-in-mb 8 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :type-stats-index {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 @@ -936,7 +1000,22 @@ {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 :target-file-size-base-in-mb 2 - :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]}}} + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + + :cql-bloom-filter + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :enable-blob-files? true + :min-blob-size 512} + + :cql-bloom-filter-by-t + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :reverse-comparator? true}}} [:blaze.db.kv.rocksdb/stats :blaze.db.index-kv-store.main/stats] {} @@ -1011,6 +1090,12 @@ :target-file-size-base-in-mb 8 :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :patient-last-change-index + {:write-buffer-size-in-mb 8 + :max-bytes-for-level-base-in-mb 32 + :target-file-size-base-in-mb 8 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + :type-stats-index {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 @@ -1021,7 +1106,22 @@ {:write-buffer-size-in-mb 2 :max-bytes-for-level-base-in-mb 8 :target-file-size-base-in-mb 2 - :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]}}} + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384]} + + :cql-bloom-filter + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :enable-blob-files? true + :min-blob-size 512} + + :cql-bloom-filter-by-t + {:write-buffer-size-in-mb 2 + :max-bytes-for-level-base-in-mb 8 + :target-file-size-base-in-mb 2 + :block-size #blaze/cfg ["DB_BLOCK_SIZE" int? 16384] + :reverse-comparator? true}}} :blaze.db.kv.rocksdb/block-cache {:size-in-mb #blaze/cfg ["DB_BLOCK_CACHE_SIZE" int? 128]} @@ -1155,6 +1255,33 @@ [:blaze/http-client :blaze.openid-auth/http-client] {:trust-store #blaze/cfg ["OPENID_CLIENT_TRUST_STORE" string?] - :trust-store-pass #blaze/cfg ["OPENID_CLIENT_TRUST_STORE_PASS" string?]} + :trust-store-pass #blaze/cfg ["OPENID_CLIENT_TRUST_STORE_PASS" string?]}}} - :blaze/scheduler {}}}]} + {:key :cql-expression-cache + :name "CQL Expression Cache" + :toggle "CQL_EXPR_CACHE_SIZE" + :config + {:blaze.fhir.operation.evaluate-measure/handler + {:blaze.elm.expression/cache #blaze/ref :blaze.elm.expression/cache} + + :blaze.elm.expression/cache + {:node #blaze/ref :blaze.db.main/node + :max-size-in-mb #blaze/cfg ["CQL_EXPR_CACHE_SIZE" nat-int?] + :refresh #blaze/cfg ["CQL_EXPR_CACHE_REFRESH" java-time.api/duration? "PT24H"] + :executor #blaze/ref :blaze.elm.expression.cache/executor} + + :blaze.elm.expression.cache/executor + {:num-threads #blaze/cfg ["CQL_EXPR_CACHE_THREADS" pos-int? 4]} + + :blaze.elm.expression.cache/bloom-filter-creation-duration-seconds {} + :blaze.elm.expression.cache/bloom-filter-useful-total {} + :blaze.elm.expression.cache/bloom-filter-not-useful-total {} + :blaze.elm.expression.cache/bloom-filter-false-positive-total {} + :blaze.elm.expression.cache/bloom-filter-bytes {} + + :blaze/cache-collector + {:caches + {"cql-expr-cache" #blaze/ref :blaze.elm.expression/cache}} + + :blaze/admin-api + {:blaze.elm.expression/cache #blaze/ref :blaze.elm.expression/cache}}}]} diff --git a/src/blaze/system.clj b/src/blaze/system.clj index b232a3a23..0628fc714 100644 --- a/src/blaze/system.clj +++ b/src/blaze/system.clj @@ -163,9 +163,10 @@ (-> (assoc-in config [:base-config :blaze.db/storage] (keyword key)) (update :base-config (partial merge-with merge) (get storage (keyword key)))))) -(defn- conj-feature [config {:keys [name toggle]} enabled?] +(defn- conj-feature [config {:keys [key name toggle]} enabled?] (update-in config [:blaze/admin-api :features] (fnil conj []) - {:name name :toggle toggle :enabled enabled?})) + {:key (clojure.core/name key) :name name :toggle toggle + :enabled enabled?})) (defn- merge-features "Merges feature config portions of enabled features into `base-config`." @@ -185,7 +186,7 @@ (defn init! [{level "LOG_LEVEL" :or {level "info"} :as env}] (log/info "Set log level to:" (str/lower-case level)) - (log/set-level! (keyword (str/lower-case level))) + (log/set-min-level! (keyword (str/lower-case level))) (let [config (-> (read-blaze-edn) (merge-storage env) (merge-features env)) From 5014c17378b2e678789a5beb9cd66e2450374a47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:33:08 +0000 Subject: [PATCH 19/24] chore(deps): update actions/checkout digest to 692973e --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 534286a8a..042c360fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -340,7 +340,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh @@ -1707,7 +1707,7 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Blazectl run: .github/scripts/install-blazectl.sh From fc3ea18dfb33079c9bd9d7e7c571c02cfe5c1205 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Wed, 26 Jun 2024 14:40:56 +0200 Subject: [PATCH 20/24] Release v0.28.0-rc.1 --- CHANGELOG.md | 12 ++++++++++++ Dockerfile | 4 ++-- README.md | 4 ++-- build.clj | 2 +- docs/.vitepress/config.ts | 2 +- docs/deployment/manual-deployment.md | 12 ++++++------ docs/deployment/standalone-backend.md | 4 ++-- modules/frontend/package-lock.json | 4 ++-- modules/frontend/package.json | 2 +- src/blaze/system.clj | 4 ++-- 10 files changed, 31 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d620215f..19a4dcfe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v0.28.0-rc.1 + +### New Features + +* Cache Results of the Exists CQL Expression ([#1051](https://github.com/samply/blaze/issues/1051)) + +### Bugfixes + +* Fix JSON Rendering in UI ([#1813](https://github.com/samply/blaze/issues/1813)) + +The full changelog can be found [here](https://github.com/samply/blaze/milestone/91?closed=1). + ## v0.27.1 ### Enhancements diff --git a/Dockerfile b/Dockerfile index 66e4462ff..d82548de7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/ RUN mkdir -p /app/data && chown 1001:1001 /app/data -COPY target/blaze-0.27.1-standalone.jar /app/ +COPY target/blaze-0.28.0-rc.1-standalone.jar /app/ WORKDIR /app USER 1001 @@ -20,4 +20,4 @@ ENV RESOURCE_DB_DIR="/app/data/resource" ENV ADMIN_INDEX_DB_DIR="/app/data/admin-index" ENV ADMIN_TRANSACTION_DB_DIR="/app/data/admin-transaction" -CMD ["java", "-jar", "blaze-0.27.1-standalone.jar"] +CMD ["java", "-jar", "blaze-0.28.0-rc.1-standalone.jar"] diff --git a/README.md b/README.md index ec3b367a6..7200a0c6d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A demo installation can be found [here](https://blaze.life.uni-leipzig.de/fhir) Blaze is widely used in the [Medical Informatics Initiative](https://www.medizininformatik-initiative.de) in Germany and in [Biobanks](https://www.bbmri-eric.eu) across Europe. A 1.0 version is expected in the next months. -Latest release: [v0.27.1][5] +Latest release: [v0.28.0-rc.1][5] ## Quick Start @@ -75,7 +75,7 @@ Unless required by applicable law or agreed to in writing, software distributed [3]: [4]: -[5]: +[5]: [6]: [7]: [8]: diff --git a/build.clj b/build.clj index c2c1db77d..8a6e253b9 100644 --- a/build.clj +++ b/build.clj @@ -2,7 +2,7 @@ (:require [clojure.tools.build.api :as b])) (def lib 'samply/blaze) -(def version "0.27.1") +(def version "0.28.0-rc.1") (def class-dir "target/classes") (def basis (b/create-basis {:project "deps.edn"})) (def uber-file (format "target/%s-%s-standalone.jar" (name lib) version)) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 60b7f397c..11df2c1b7 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -27,7 +27,7 @@ export default defineConfig({ nav: [ {text: 'Home', link: '/'}, { - text: "v0.27.1", + text: "v0.28.0-rc.1", items: [ { text: 'Changelog', diff --git a/docs/deployment/manual-deployment.md b/docs/deployment/manual-deployment.md index c1a7edeb7..8cdbaab7c 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 or 17 with 17 recommended. Blaze is tested with [Eclipse Temurin][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.27.1). Look for `blaze-0.27.1-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.28.0-rc.1). Look for `blaze-0.28.0-rc.1-standalone.jar`. After the download, you can start blaze with the following command (Linux, macOS): ```sh -java -jar blaze-0.27.1-standalone.jar +java -jar blaze-0.28.0-rc.1-standalone.jar ``` 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.27.1-standalone.jar +STORAGE=standalone java -jar blaze-0.28.0-rc.1-standalone.jar ``` Under Windows, you need to set the Environment variables in the PowerShell before starting Blaze: ```powershell $Env:STORAGE="standalone" -java -jar blaze-0.27.1-standalone.jar +java -jar blaze-0.28.0-rc.1-standalone.jar ``` 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.27.1 in 8.2 seconds +2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.28.0-rc.1 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.27.1" + "version": "0.28.0-rc.1" } ``` diff --git a/docs/deployment/standalone-backend.md b/docs/deployment/standalone-backend.md index 6418eb93c..a938d9231 100644 --- a/docs/deployment/standalone-backend.md +++ b/docs/deployment/standalone-backend.md @@ -27,7 +27,7 @@ Blaze should log something like this: 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:67] - JVM version: 17.0.7 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:68] - Maximum available memory: 1738 MiB 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:69] - Number of available processors: 2 -2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:70] - Successfully started ๐Ÿ”ฅ Blaze version 0.27.1 in 9.0 seconds +2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:70] - Successfully started ๐Ÿ”ฅ Blaze version 0.28.0-rc.1 in 9.0 seconds ``` In order to test connectivity, query the health endpoint: @@ -47,7 +47,7 @@ that should return: ```json { "name": "Blaze", - "version": "0.27.1" + "version": "0.28.0-rc.1" } ``` diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 231125a5c..25012f119 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "blaze-frontend", - "version": "0.27.1", + "version": "0.28.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blaze-frontend", - "version": "0.27.1", + "version": "0.28.0-rc.1", "dependencies": { "@auth/sveltekit": "^1.0.0", "@fontsource-variable/inter": "^5.0.8", diff --git a/modules/frontend/package.json b/modules/frontend/package.json index d5cb9e517..05c8ba382 100644 --- a/modules/frontend/package.json +++ b/modules/frontend/package.json @@ -1,6 +1,6 @@ { "name": "blaze-frontend", - "version": "0.27.1", + "version": "0.28.0-rc.1", "private": true, "scripts": { "dev": "vite dev", diff --git a/src/blaze/system.clj b/src/blaze/system.clj index 0628fc714..dc203b624 100644 --- a/src/blaze/system.clj +++ b/src/blaze/system.clj @@ -92,9 +92,9 @@ (log/info "Loaded the following namespaces:" (str/join ", " loaded-ns)))) (def ^:private root-config - {:blaze/version "0.27.1" + {:blaze/version "0.28.0-rc.1" - :blaze/release-date "2024-06-14" + :blaze/release-date "2024-06-26" :blaze/clock {} From b1876cdbdf899dc393d98871b441027f8b7f8cc0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:47:19 +0000 Subject: [PATCH 21/24] chore(deps): update docker/build-push-action digest to 1556069 --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 042c360fd..6d0277e01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -251,7 +251,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6 with: context: . tags: blaze:latest @@ -283,7 +283,7 @@ jobs: uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3 - name: Build and Export to Docker - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6 with: context: modules/frontend tags: blaze-frontend:latest @@ -1916,7 +1916,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push to GHCR - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 @@ -1947,7 +1947,7 @@ jobs: - name: Build and push to DockerHub if: ${{ env.dockerhub_username != '' }} - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6 with: context: ${{ matrix.image.context }} platforms: linux/amd64,linux/arm64 From dd6252830dc33081a89bf0be356bfb166b00531d Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Wed, 26 Jun 2024 16:17:07 +0200 Subject: [PATCH 22/24] Update Documentation --- .../api/operation-measure-evaluate-measure.md | 3 + docs/cql-queries.md | 156 +----------------- docs/deployment/environment-variables.md | 2 +- 3 files changed, 7 insertions(+), 154 deletions(-) diff --git a/docs/api/operation-measure-evaluate-measure.md b/docs/api/operation-measure-evaluate-measure.md index cb805190b..60c1fa595 100644 --- a/docs/api/operation-measure-evaluate-measure.md +++ b/docs/api/operation-measure-evaluate-measure.md @@ -6,6 +6,9 @@ The official documentation can be found [here][1]. * [blazectl][2] supports async requests since [v0.15.0][3] * cancellation of async requests is fully supported. the evaluation is stopped almost immediately * the `FHIR_OPERATION_EVALUATE_MEASURE_TIMEOUT` is only applied to synchronous requests +* [`CQL_EXPR_CACHE_SIZE`](../deployment/environment-variables.md) can be set to enable a cache of certain CQL expressions that will speed up evaluations +* a detailed documentation how to use the $evaluate-measure API can be found [here](../cql-queries/api.md) +* a documentation how to use $evaluate-measure via blazectl can be found [here](../cql-queries/blazectl.md) [1]: [2]: diff --git a/docs/cql-queries.md b/docs/cql-queries.md index 7f8bb1273..eb85f01b6 100644 --- a/docs/cql-queries.md +++ b/docs/cql-queries.md @@ -13,158 +13,8 @@ If you like to use the command line, please look into [this section](cql-queries If you'd like to use the CQL Evaluation API directly, please read the [CQL API Documentation](cql-queries/api.md). -## Install the Quality Reporting UI +## Resources -The most accessible way to create and execute CQL queries is to use the Quality Reporting UI. The Quality Reporting UI is a desktop application which you can download [here](https://github.com/samply/blaze-quality-reporting-ui). +You can learn more about CQL queries in the [Author's Guide][1] at HL7. -## Run Blaze - -If you don't already have Blaze running, you can read about how to do it in [Deployment](deployment/README.md). If you have Docker available just run: - -``` -docker run -p 8080:8080 -v blaze-data:/app/data samply/blaze:latest -``` - -Start the Quality Reporting UI. You should see an empty measure list. - -![](cql-queries/measures.png) - -In the upper right corner, you see **Localhost 8080**. If Blaze runs on Localhost port 8080, you can continue, otherwise you have to go into Settings and add your Blaze servers location. - -### Add Your Blaze Server - -Go into **Settings** and click on **Add**. - -![](cql-queries/settings-server.png) - -Enter a **Name** and a **URL**. Please be aware that URLs of Blaze FHIR endpoints have the path `/fhir` in it by default. You can find your FHIR endpoint URL of Blaze in the logs in a line like this: - -```text -Init FHIR RESTful API with base URL: http://localhost:8080/fhir -``` - -### Create Your First Library - -Blaze uses the FHIR [Quality Reporting](https://www.hl7.org/fhir/clinicalreasoning-quality-reporting.html) Module, to execute CQL queries. In Quality Reporting, CQL Query Expressions reside in [Library](https://www.hl7.org/fhir/library.html) resources and are referenced in [Measure](https://www.hl7.org/fhir/measure.html) resources. In order to create a Library resource, go to **Libraries** and click on **New Library**. - -![](cql-queries/libraries-new.png) - -After you created your Library, you can give it a name by clicking at **Edit:** - -![](cql-queries/library-title-edit.png) - -After naming your Library, you have to give it a canonical URL. We just use localhost for our URL here: - -![](cql-queries/library-url-edit.png) - -Next we add the CQL source code for our single **InInitialPopulation** query expression: - -![](cql-queries/library-cql.png) - -```text -library Covid19 -using FHIR version '4.0.0' -include FHIRHelpers version '4.0.0' - -codesystem icd10: 'http://hl7.org/fhir/sid/icd-10' - -define InInitialPopulation: - exists([Condition: Code 'U07.1' from icd10]) -``` - -### Create Your First Measure - -You can create Measure resources under **Measures** by clicking on **New Measure**. After giving our Measure a name, we also have to give it a canonical URL: - -![](cql-queries/measure-url-edit.png) - -After that, we have to reference our previously created Library to our Measure by clicking on the **Edit** button in the right sidebar: - -![](cql-queries/measure-library-edit.png) - -Because the Measure comes with an initial population definition by default, we will only check it by clicking on **initial-population**: - -![](cql-queries/measure-initial-population.png) - -Here we see our CQL expression **InInitialPopulation** from our Library referenced: - -![](cql-queries/measure-initial-population-criteria.png) - -### Generating a First Report - -To generate a Report, we click on **Generate New Report**: - -![](cql-queries/measure-generate-report.png) - -After some time, a MeasureReport will appear in the list of reports of our Measure: - -![](cql-queries/measure-report-list.png) - -Please be patient, because currently there is no progress bar. If nothing appears for a long time, you can use the menu to go back to all measure, open our measure again and look if ou see a report with a fitting timestamp. - -All reports are persisted in Blaze and are shown in the UI with their creation timestamp. - -After you open the report, you will see that your **initial-population** has a count of zero. - -![](cql-queries/measure-report-1.png) - -### Import Patients with COVID-19 Diagnoses - -If you POST the following Bundle to the transaction endpoint of Blaze, you will have two patients, one with a COVID-19 condition and one without: - -```text -{ - "resourceType": "Bundle", - "type": "transaction", - "entry": [ - { - "request": { - "method": "PUT", - "url": "Patient/0" - }, - "resource": { - "resourceType": "Patient", - "id": "0" - } - }, - { - "request": { - "method": "PUT", - "url": "Condition/0-condition" - }, - "resource": { - "resourceType": "Condition", - "id": "0-condition", - "code": { - "coding": [ - { - "code": "U07.1", - "system": "http://hl7.org/fhir/sid/icd-10", - "version": "2016" - } - ] - }, - "subject": { - "reference": "Patient/0" - } - } - }, - { - "request": { - "method": "PUT", - "url": "Patient/1" - }, - "resource": { - "resourceType": "Patient", - "id": "1" - } - } - ] -} -``` - -After importing patients, the result of the initial population will be one: - -![](cql-queries/measure-report-2.png) - -You can learn more about CQL queries in the [Author's Guide](https://cql.hl7.org/02-authorsguide.html) at HL7. +[1]: diff --git a/docs/deployment/environment-variables.md b/docs/deployment/environment-variables.md index 074343370..80ac451ed 100644 --- a/docs/deployment/environment-variables.md +++ b/docs/deployment/environment-variables.md @@ -110,7 +110,7 @@ More information about distributed deployment are available [here](distributed-b | DB_SYNC_TIMEOUT | 10000 | v0.15 | โ€” | Timeout in milliseconds for all reading FHIR interactions acquiring the newest database state. | | DB_SEARCH_PARAM_BUNDLE | โ€” | v0.21 | โ€” | Name of a custom search parameter bundle file. | | ENABLE_ADMIN_API | โ€” | v0.26 | โ€” | Set to `true` if the optional Admin API should be enabled. Needed by the frontend. | -| CQL_EXPR_CACHE_SIZE | 128 (128 MiB) | v0.28 | โ€” | Size of the CQL expression cache. Will be disabled if not given. | +| CQL_EXPR_CACHE_SIZE | โ€” | v0.28 | โ€” | Size of the CQL expression cache in MiB. Will be disabled if not given. | | CQL_EXPR_CACHE_REFRESH | PT24H | v0.28 | โ€” | The duration after which a Bloom filter of the CQL expression cache will be refreshed. | | CQL_EXPR_CACHE_THREADS | 4 | v0.28 | โ€” | The maximum number of parallel Bloom filter calculations for the CQL expression cache. | From 71fb7409dc16767ac40375a8ed58b7fabf046681 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Wed, 26 Jun 2024 16:52:23 +0200 Subject: [PATCH 23/24] Fix JSON Rendering in UI Closes: #1813 --- modules/frontend/src/lib/resource/json/array.svelte | 2 +- modules/frontend/src/lib/resource/json/value.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/frontend/src/lib/resource/json/array.svelte b/modules/frontend/src/lib/resource/json/array.svelte index 7cd00f626..46dfc6186 100644 --- a/modules/frontend/src/lib/resource/json/array.svelte +++ b/modules/frontend/src/lib/resource/json/array.svelte @@ -10,4 +10,4 @@ indent={indent + 4} insideArray={true} {value} - />{index < values.length - 1 ? ', ' : ''}{/each}{' '.repeat(indent)}{']'} + />{index < values.length - 1 ? ',\n' : '\n'}{/each}{' '.repeat(indent)}{']'} diff --git a/modules/frontend/src/lib/resource/json/value.svelte b/modules/frontend/src/lib/resource/json/value.svelte index 4885b7179..b1045c12a 100644 --- a/modules/frontend/src/lib/resource/json/value.svelte +++ b/modules/frontend/src/lib/resource/json/value.svelte @@ -12,4 +12,4 @@ {#if isPrimitive(value.type)}{insideArray ? ' '.repeat(indent) : ''}{:else}{/if}{insideArray ? '\n' : ''} + />{:else}{/if} From 57d9e8009d5ab272f54b33cbfc27310273dd8fbf Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Fri, 28 Jun 2024 11:47:14 +0200 Subject: [PATCH 24/24] Release v0.28.0 --- .github/workflows/build.yml | 10 ++-- CHANGELOG.md | 22 +++++++++ Dockerfile | 4 +- README.md | 4 +- build.clj | 2 +- docs/.vitepress/config.ts | 2 +- docs/deployment/environment-variables.md | 58 ++++++++++++------------ docs/deployment/manual-deployment.md | 12 ++--- docs/deployment/standalone-backend.md | 4 +- modules/db/src/blaze/db/node.clj | 36 ++++++++------- modules/frontend/package-lock.json | 4 +- modules/frontend/package.json | 2 +- src/blaze/system.clj | 4 +- 13 files changed, 95 insertions(+), 69 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d0277e01..38b6a6d50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1701,6 +1701,8 @@ jobs: - name: Fetch Patient Expecting an Error run: .github/scripts/fetch-resource-0-with-missing-resource-content.sh + # This test ensures that older versions of Blaze will migrate successfully to + # the new database schema especially building the PatientLastChange index. build-patient-last-change-index-test: needs: build runs-on: ubuntu-22.04 @@ -1721,8 +1723,8 @@ jobs: - name: Load Blaze Image run: docker load --input /tmp/blaze.tar - - name: Run Blaze v0.27 - run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -p 8080:8080 -v blaze-data:/app/data samply/blaze:0.27 + - name: Run Blaze in the Version 0.27 before Introduction of the PatientLastChange Index + run: docker run --name blaze --rm -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -p 8080:8080 -v blaze-data:/app/data samply/blaze:0.27 - name: Wait for Blaze run: .github/scripts/wait-for-url.sh http://localhost:8080/health @@ -1734,9 +1736,9 @@ jobs: run: .github/scripts/check-patient-last-change-index-missing.sh - name: Shut down Blaze - run: docker stop blaze && docker rm blaze + run: docker stop blaze - - name: Run Latest Blaze + - name: Run Latest Blaze with the PatientLastChange Index run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -e ENABLE_ADMIN_API=true -e LOG_LEVEL=debug -p 8080:8080 -v blaze-data:/app/data blaze:latest - name: Wait for Blaze diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a4dcfe3..4829bd81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v0.28.0 + +**!!! IMPORTANT !!!** + +This release contains database schema additions, so that the database can't be opened with older versions of Blaze. Downgrades are not possible. + +### Notes + +A new index called `PatientLastChange` which will be automatically created and filled at the startup of Blaze. The index is used for the new CQL cache feature ([#1051](https://github.com/samply/blaze/issues/1051)). Except for the caching, Blaze will work fully during index buildup. The index will be ready once the log message "Finished building PatientLastChange index of main node." appears. + +After the index buildup, the CQL Cache can be activated by setting `CQL_EXPR_CACHE_SIZE` to a size of heap memory in MiB and restarting Blaze. + +### New Features + +* Cache Results of the Exists CQL Expression ([#1051](https://github.com/samply/blaze/issues/1051)) + +### Bugfixes + +* Fix JSON Rendering in UI ([#1813](https://github.com/samply/blaze/issues/1813)) + +The full changelog can be found [here](https://github.com/samply/blaze/milestone/91?closed=1). + ## v0.28.0-rc.1 ### New Features diff --git a/Dockerfile b/Dockerfile index d82548de7..122b0bfd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/ RUN mkdir -p /app/data && chown 1001:1001 /app/data -COPY target/blaze-0.28.0-rc.1-standalone.jar /app/ +COPY target/blaze-0.28.0-standalone.jar /app/ WORKDIR /app USER 1001 @@ -20,4 +20,4 @@ ENV RESOURCE_DB_DIR="/app/data/resource" ENV ADMIN_INDEX_DB_DIR="/app/data/admin-index" ENV ADMIN_TRANSACTION_DB_DIR="/app/data/admin-transaction" -CMD ["java", "-jar", "blaze-0.28.0-rc.1-standalone.jar"] +CMD ["java", "-jar", "blaze-0.28.0-standalone.jar"] diff --git a/README.md b/README.md index 7200a0c6d..26f39fb69 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A demo installation can be found [here](https://blaze.life.uni-leipzig.de/fhir) Blaze is widely used in the [Medical Informatics Initiative](https://www.medizininformatik-initiative.de) in Germany and in [Biobanks](https://www.bbmri-eric.eu) across Europe. A 1.0 version is expected in the next months. -Latest release: [v0.28.0-rc.1][5] +Latest release: [v0.28.0][5] ## Quick Start @@ -75,7 +75,7 @@ Unless required by applicable law or agreed to in writing, software distributed [3]: [4]: -[5]: +[5]: [6]: [7]: [8]: diff --git a/build.clj b/build.clj index 8a6e253b9..50c11f0c8 100644 --- a/build.clj +++ b/build.clj @@ -2,7 +2,7 @@ (:require [clojure.tools.build.api :as b])) (def lib 'samply/blaze) -(def version "0.28.0-rc.1") +(def version "0.28.0") (def class-dir "target/classes") (def basis (b/create-basis {:project "deps.edn"})) (def uber-file (format "target/%s-%s-standalone.jar" (name lib) version)) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 11df2c1b7..cdd9f9f12 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -27,7 +27,7 @@ export default defineConfig({ nav: [ {text: 'Home', link: '/'}, { - text: "v0.28.0-rc.1", + text: "v0.28.0", items: [ { text: 'Changelog', diff --git a/docs/deployment/environment-variables.md b/docs/deployment/environment-variables.md index 80ac451ed..cb62d4621 100644 --- a/docs/deployment/environment-variables.md +++ b/docs/deployment/environment-variables.md @@ -41,7 +41,7 @@ The three database directories must not exist on the first start of Blaze and wi | TRANSACTION_DB_WAL_DIR | \ | v0.18 | | The directory were the transaction log write ahead log (WAL) files are stored. Empty means same dir as database files. | | RESOURCE_DB_DIR | resource ยฒ | v0.8 | | The directory were the resource files are stored. This directory must not exist on the first start of Blaze and will be created by | | RESOURCE_DB_WAL_DIR | \ | v0.18 | | The directory were the resource write ahead log (WAL) files are stored. Empty means same dir as database files. | -| DB_BLOCK_CACHE_SIZE | 128 | v0.8 | | The size of the [block cache][2] of the DB in MB. | +| DB_BLOCK_CACHE_SIZE | 128 | v0.8 | | The size of the [block cache][2] of the DB in MiB. This cache is outside of the JVM heap. | | DB_RESOURCE_CACHE_SIZE | 100000 | v0.8 | | The size of the resource cache of the DB in number of resources. | | DB_MAX_BACKGROUND_JOBS | 4 | v0.8 | | The maximum number of the [background jobs][3] used for DB compactions. | | DB_RESOURCE_INDEXER_THREADS | 4 | v0.8 | | The number threads used for indexing resources. Try 8 or 16 depending on your hardware. | @@ -58,7 +58,7 @@ The distributed storage variant only uses the index database locally. |:-----------------------------------|:---------------|:------|:-------|:-----------------------------------------------------------------------------------------------------------------------------------------------------| | INDEX_DB_DIR | index ยฒ | v0.8 | | The directory were the index database files are stored. | | INDEX_DB_WAL_DIR | \ | v0.18 | | The directory were the index database write ahead log (WAL) files are stored. Empty means same dir as database files. | -| DB_BLOCK_CACHE_SIZE | 128 | v0.8 | | The size of the [block cache][2] of the DB in MB. | +| DB_BLOCK_CACHE_SIZE | 128 | v0.8 | | The size of the [block cache][2] of the DB in MiB. This cache is outside of the JVM heap. | | DB_RESOURCE_CACHE_SIZE | 100000 | v0.8 | | The size of the resource cache of the DB in number of resources. | | DB_MAX_BACKGROUND_JOBS | 4 | v0.8 | | The maximum number of the [background jobs][3] used for DB compactions. | | DB_RESOURCE_INDEXER_THREADS | 4 | v0.8 | | The number threads used for indexing resources. Try 8 or 16 depending on your hardware. | @@ -86,33 +86,33 @@ More information about distributed deployment are available [here](distributed-b ### Other Environment Variables -| Name | Default | Since | Depr ยน | Description | -|:----------------------------------------|:---------------------------|:-------|---------|:-----------------------------------------------------------------------------------------------| -| PROXY_HOST | โ€” | v0.6 | โ€” | REMOVED: use -Dhttp.proxyHost | -| PROXY_PORT | โ€” | v0.6 | โ€” | REMOVED: use -Dhttp.proxyPort | -| PROXY_USER | โ€” | v0.6.1 | โ€” | REMOVED: try [SOCKS Options][1] | -| PROXY_PASSWORD | โ€” | v0.6.1 | โ€” | REMOVED: try [SOCKS Options][1] | -| CONNECTION_TIMEOUT | 5 s | v0.6.3 | โ€” | connection timeout for outbound HTTP requests | -| REQUEST_TIMEOUT | 30 s | v0.6.3 | โ€” | REMOVED | -| TERM_SERVICE_URI | [http://tx.fhir.org/r4][6] | v0.6 | v0.11 | Base URI of the terminology service | -| BASE_URL | `http://localhost:8080` | โ€” | โ€” | The URL under which Blaze is accessible by clients. | -| CONTEXT_PATH | /fhir | v0.11 | โ€” | Context path under which the FHIR RESTful API will be accessible. | -| SERVER_PORT | 8080 | โ€” | โ€” | The port of the main HTTP server | -| METRICS_SERVER_PORT | 8081 | v0.6 | โ€” | The port of the Prometheus metrics server | -| LOG_LEVEL | info | v0.6 | โ€” | one of trace, debug, info, warn or error | -| JAVA_TOOL_OPTIONS | โ€” | โ€” | โ€” | JVM options \(Docker only\) | -| FHIR_OPERATION_EVALUATE_MEASURE_THREADS | โ€” | v0.8 | v0.23.3 | The number threads used for $evaluate-measure executions. | -| FHIR_OPERATION_EVALUATE_MEASURE_TIMEOUT | 3600000 (1h) | v0.19 | โ€” | Timeout in milliseconds for synchronous $evaluate-measure executions. | -| OPENID_PROVIDER_URL | โ€” | v0.11 | โ€” | [OpenID Connect][4] provider URL to enable [authentication][5] | -| OPENID_CLIENT_TRUST_STORE | โ€” | v0.26 | โ€” | A PKCS #12 trust store containing CA certificates needed for the [OpenID Connect][4] provider. | -| OPENID_CLIENT_TRUST_STORE_PASS | โ€” | v0.26 | โ€” | The password for the PKCS #12 trust store. | -| ENFORCE_REFERENTIAL_INTEGRITY | true | v0.14 | โ€” | Enforce referential integrity on resource create, update and delete. | -| DB_SYNC_TIMEOUT | 10000 | v0.15 | โ€” | Timeout in milliseconds for all reading FHIR interactions acquiring the newest database state. | -| DB_SEARCH_PARAM_BUNDLE | โ€” | v0.21 | โ€” | Name of a custom search parameter bundle file. | -| ENABLE_ADMIN_API | โ€” | v0.26 | โ€” | Set to `true` if the optional Admin API should be enabled. Needed by the frontend. | -| CQL_EXPR_CACHE_SIZE | โ€” | v0.28 | โ€” | Size of the CQL expression cache in MiB. Will be disabled if not given. | -| CQL_EXPR_CACHE_REFRESH | PT24H | v0.28 | โ€” | The duration after which a Bloom filter of the CQL expression cache will be refreshed. | -| CQL_EXPR_CACHE_THREADS | 4 | v0.28 | โ€” | The maximum number of parallel Bloom filter calculations for the CQL expression cache. | +| Name | Default | Since | Depr ยน | Description | +|:----------------------------------------|:---------------------------|:-------|---------|:------------------------------------------------------------------------------------------------------------| +| PROXY_HOST | โ€” | v0.6 | โ€” | REMOVED: use -Dhttp.proxyHost | +| PROXY_PORT | โ€” | v0.6 | โ€” | REMOVED: use -Dhttp.proxyPort | +| PROXY_USER | โ€” | v0.6.1 | โ€” | REMOVED: try [SOCKS Options][1] | +| PROXY_PASSWORD | โ€” | v0.6.1 | โ€” | REMOVED: try [SOCKS Options][1] | +| CONNECTION_TIMEOUT | 5 s | v0.6.3 | โ€” | connection timeout for outbound HTTP requests | +| REQUEST_TIMEOUT | 30 s | v0.6.3 | โ€” | REMOVED | +| TERM_SERVICE_URI | [http://tx.fhir.org/r4][6] | v0.6 | v0.11 | Base URI of the terminology service | +| BASE_URL | `http://localhost:8080` | โ€” | โ€” | The URL under which Blaze is accessible by clients. | +| CONTEXT_PATH | /fhir | v0.11 | โ€” | Context path under which the FHIR RESTful API will be accessible. | +| SERVER_PORT | 8080 | โ€” | โ€” | The port of the main HTTP server | +| METRICS_SERVER_PORT | 8081 | v0.6 | โ€” | The port of the Prometheus metrics server | +| LOG_LEVEL | info | v0.6 | โ€” | one of trace, debug, info, warn or error | +| JAVA_TOOL_OPTIONS | โ€” | โ€” | โ€” | JVM options \(Docker only\) | +| FHIR_OPERATION_EVALUATE_MEASURE_THREADS | โ€” | v0.8 | v0.23.3 | The number threads used for $evaluate-measure executions. | +| FHIR_OPERATION_EVALUATE_MEASURE_TIMEOUT | 3600000 (1h) | v0.19 | โ€” | Timeout in milliseconds for synchronous $evaluate-measure executions. | +| OPENID_PROVIDER_URL | โ€” | v0.11 | โ€” | [OpenID Connect][4] provider URL to enable [authentication][5] | +| OPENID_CLIENT_TRUST_STORE | โ€” | v0.26 | โ€” | A PKCS #12 trust store containing CA certificates needed for the [OpenID Connect][4] provider. | +| OPENID_CLIENT_TRUST_STORE_PASS | โ€” | v0.26 | โ€” | The password for the PKCS #12 trust store. | +| ENFORCE_REFERENTIAL_INTEGRITY | true | v0.14 | โ€” | Enforce referential integrity on resource create, update and delete. | +| DB_SYNC_TIMEOUT | 10000 | v0.15 | โ€” | Timeout in milliseconds for all reading FHIR interactions acquiring the newest database state. | +| DB_SEARCH_PARAM_BUNDLE | โ€” | v0.21 | โ€” | Name of a custom search parameter bundle file. | +| ENABLE_ADMIN_API | โ€” | v0.26 | โ€” | Set to `true` if the optional Admin API should be enabled. Needed by the frontend. | +| CQL_EXPR_CACHE_SIZE | โ€” | v0.28 | โ€” | Size of the CQL expression cache in MiB. This cache is part of the JVM heap. Will be disabled if not given. | +| CQL_EXPR_CACHE_REFRESH | PT24H | v0.28 | โ€” | The duration after which a Bloom filter of the CQL expression cache will be refreshed. | +| CQL_EXPR_CACHE_THREADS | 4 | v0.28 | โ€” | The maximum number of parallel Bloom filter calculations for the CQL expression cache. | ยน Deprecated diff --git a/docs/deployment/manual-deployment.md b/docs/deployment/manual-deployment.md index 8cdbaab7c..949aef0a3 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 or 17 with 17 recommended. Blaze is tested with [Eclipse Temurin][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.28.0-rc.1). Look for `blaze-0.28.0-rc.1-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.28.0). Look for `blaze-0.28.0-standalone.jar`. After the download, you can start blaze with the following command (Linux, macOS): ```sh -java -jar blaze-0.28.0-rc.1-standalone.jar +java -jar blaze-0.28.0-standalone.jar ``` 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.28.0-rc.1-standalone.jar +STORAGE=standalone java -jar blaze-0.28.0-standalone.jar ``` Under Windows, you need to set the Environment variables in the PowerShell before starting Blaze: ```powershell $Env:STORAGE="standalone" -java -jar blaze-0.28.0-rc.1-standalone.jar +java -jar blaze-0.28.0-standalone.jar ``` 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.28.0-rc.1 in 8.2 seconds +2021-06-27T11:02:37.836Z ee086ef908c1 main INFO [blaze.core:67] - Successfully started Blaze version 0.28.0 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.28.0-rc.1" + "version": "0.28.0" } ``` diff --git a/docs/deployment/standalone-backend.md b/docs/deployment/standalone-backend.md index a938d9231..dca3d57aa 100644 --- a/docs/deployment/standalone-backend.md +++ b/docs/deployment/standalone-backend.md @@ -27,7 +27,7 @@ Blaze should log something like this: 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:67] - JVM version: 17.0.7 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:68] - Maximum available memory: 1738 MiB 2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:69] - Number of available processors: 2 -2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:70] - Successfully started ๐Ÿ”ฅ Blaze version 0.28.0-rc.1 in 9.0 seconds +2023-06-09T08:30:30.126Z b45689460ff3 main INFO [blaze.core:70] - Successfully started ๐Ÿ”ฅ Blaze version 0.28.0 in 9.0 seconds ``` In order to test connectivity, query the health endpoint: @@ -47,7 +47,7 @@ that should return: ```json { "name": "Blaze", - "version": "0.28.0-rc.1" + "version": "0.28.0" } ``` diff --git a/modules/db/src/blaze/db/node.clj b/modules/db/src/blaze/db/node.clj index f07ce5c6f..fb071ee9e 100644 --- a/modules/db/src/blaze/db/node.clj +++ b/modules/db/src/blaze/db/node.clj @@ -392,9 +392,9 @@ :error-t 0}) (defn- init-msg - [{:keys [enforce-referential-integrity] - :or {enforce-referential-integrity true}}] - (log/info "Open local database node with" + [key {:keys [enforce-referential-integrity] + :or {enforce-referential-integrity true}}] + (log/info "Open" (node-util/component-name key "local database node") "with" (if enforce-referential-integrity "enabled" "disabled") "referential integrity checks")) @@ -453,24 +453,26 @@ (poll-tx-queue! queue poll-timeout))) (defn build-patient-last-change-index - [{:keys [tx-log kv-store run? state poll-timeout] :as node}] + [key {:keys [tx-log kv-store run? state poll-timeout] :as node}] (let [{:keys [type t]} (plc/state kv-store)] (when (identical? :building type) (let [start-t (inc t) end-t (:t @state) current-t (volatile! start-t)] - (log/info "Building PatientLastChange index starting at t =" start-t) + (log/info "Building PatientLastChange index of" (node-util/component-name key "node") "starting at t =" start-t) (with-open [queue (tx-log/new-queue tx-log start-t)] (while (and @run? (< @current-t end-t)) (try (poll-and-index-patient-last-change-index! node queue current-t poll-timeout) (catch Exception e - (log/error "Error while building the PatientLastChange index." e))))) + (log/error (format "Error while building the PatientLastChange index of %s." + (node-util/component-name key "node")) e))))) (if (>= @current-t end-t) (do (store-tx-entries! kv-store [(plc/state-index-entry {:type :current})]) - (log/info "Finished building PatientLastChange index.")) - (log/info "Partially build PatientLastChange index up to t =" @current-t + (log/info (format "Finished building PatientLastChange index of %s." (node-util/component-name key "node")))) + (log/info (format "Partially build PatientLastChange index of %s up to t =" + (node-util/component-name key "node")) @current-t "at a goal of t =" end-t "Will continue at next start.")))))) (defmethod m/pre-init-spec :blaze.db/node [_] @@ -488,11 +490,11 @@ [:blaze.db/enforce-referential-integrity])) (defmethod ig/init-key :blaze.db/node - [_ {:keys [storage tx-log tx-cache indexer-executor kv-store resource-indexer - resource-store search-param-registry scheduler poll-timeout] - :or {poll-timeout (time/seconds 1)} - :as config}] - (init-msg config) + [key {:keys [storage tx-log tx-cache indexer-executor kv-store resource-indexer + resource-store search-param-registry scheduler poll-timeout] + :or {poll-timeout (time/seconds 1)} + :as config}] + (init-msg key config) (check-version! kv-store) (let [node (->Node (ctx config) tx-log tx-cache kv-store resource-store (sync-fn storage) search-param-registry resource-indexer @@ -501,13 +503,13 @@ poll-timeout (ac/future))] (when (= :building (:type (plc/state kv-store))) - (sched/submit scheduler #(build-patient-last-change-index node))) + (sched/submit scheduler #(build-patient-last-change-index key node))) (execute node indexer-executor) node)) (defmethod ig/halt-key! :blaze.db/node [_ node] - (log/info "Close local database node") + (log/info "Close" (node-util/component-name key "local database node")) (.close ^AutoCloseable node)) (defmethod ig/init-key ::indexer-executor @@ -517,11 +519,11 @@ (defmethod ig/halt-key! ::indexer-executor [_ executor] - (log/info "Stopping indexer executor...") + (log/info "Stopping" (node-util/component-name key "indexer executor...")) (ex/shutdown! executor) (if (ex/await-termination executor 10 TimeUnit/SECONDS) (log/info "Indexer executor was stopped successfully") - (log/warn "Got timeout while stopping the indexer executor"))) + (log/warn "Got timeout while stopping the" (node-util/component-name key "indexer executor")))) (reg-collector ::duration-seconds duration-seconds) diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 25012f119..4f58911fb 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "blaze-frontend", - "version": "0.28.0-rc.1", + "version": "0.28.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blaze-frontend", - "version": "0.28.0-rc.1", + "version": "0.28.0", "dependencies": { "@auth/sveltekit": "^1.0.0", "@fontsource-variable/inter": "^5.0.8", diff --git a/modules/frontend/package.json b/modules/frontend/package.json index 05c8ba382..202db6d3a 100644 --- a/modules/frontend/package.json +++ b/modules/frontend/package.json @@ -1,6 +1,6 @@ { "name": "blaze-frontend", - "version": "0.28.0-rc.1", + "version": "0.28.0", "private": true, "scripts": { "dev": "vite dev", diff --git a/src/blaze/system.clj b/src/blaze/system.clj index dc203b624..d1da5f6b4 100644 --- a/src/blaze/system.clj +++ b/src/blaze/system.clj @@ -92,9 +92,9 @@ (log/info "Loaded the following namespaces:" (str/join ", " loaded-ns)))) (def ^:private root-config - {:blaze/version "0.28.0-rc.1" + {:blaze/version "0.28.0" - :blaze/release-date "2024-06-26" + :blaze/release-date "2024-06-28" :blaze/clock {}