mirror of
https://github.com/apache/superset.git
synced 2026-05-13 11:55:16 +00:00
Compare commits
20 Commits
fix-asyncs
...
sl-cache
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a476d45e | ||
|
|
b2160f8d4e | ||
|
|
2392c8e624 | ||
|
|
39ad6b200f | ||
|
|
3363b48180 | ||
|
|
c9fb1bc10f | ||
|
|
658907a0a6 | ||
|
|
4a79896bb2 | ||
|
|
b0c5b061c5 | ||
|
|
c394405fc1 | ||
|
|
d2ae5fb275 | ||
|
|
460992d89b | ||
|
|
85935b0b88 | ||
|
|
fa168fcc8a | ||
|
|
a6ad0bf169 | ||
|
|
fed29b3017 | ||
|
|
24d76b4249 | ||
|
|
5ab8583cd0 | ||
|
|
e66fbc91c2 | ||
|
|
187bb416e7 |
10
.github/workflows/claude.yml
vendored
10
.github/workflows/claude.yml
vendored
@@ -17,13 +17,12 @@ jobs:
|
||||
steps:
|
||||
- name: Check if user is allowed
|
||||
id: check
|
||||
env:
|
||||
COMMENTER: ${{ github.event.comment.user.login }}
|
||||
run: |
|
||||
# List of allowed users
|
||||
ALLOWED_USERS="mistercrunch,rusackas"
|
||||
|
||||
# Get the commenter's username
|
||||
COMMENTER="${{ github.event.comment.user.login }}"
|
||||
|
||||
echo "Checking permissions for user: $COMMENTER"
|
||||
|
||||
# Check if user is in allowed list
|
||||
@@ -45,9 +44,12 @@ jobs:
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
COMMENTER_LOGIN: ${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
const commenter = process.env.COMMENTER_LOGIN;
|
||||
const message = `👋 Hi @${commenter}!
|
||||
|
||||
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
|
||||
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -53,6 +53,6 @@ jobs:
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/latest-release-tag.yml
vendored
2
.github/workflows/latest-release-tag.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: Run latest-tag
|
||||
uses: ./.github/actions/latest-tag
|
||||
if: (! ${{ steps.latest-tag.outputs.SKIP_TAG }} )
|
||||
if: steps.latest-tag.outputs.SKIP_TAG != 'true'
|
||||
with:
|
||||
description: Superset latest release
|
||||
tag-name: latest
|
||||
|
||||
@@ -328,7 +328,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
|
||||
There's a migration added that can potentially affect a significant number of existing charts.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the beginning of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [32432](https://github.com/apache/superset/pull/32432) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
|
||||
|
||||
@@ -343,7 +343,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
|
||||
- [31582](https://github.com/apache/superset/pull/31582) Removed the legacy Area, Bar, Event Flow, Heatmap, Histogram, Line, Sankey, and Sankey Loop charts. They were all automatically migrated to their ECharts counterparts with the exception of the Event Flow and Sankey Loop charts which were removed as they were not actively maintained and not widely used. If you were using the Event Flow or Sankey Loop charts, you will need to find an alternative solution.
|
||||
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
|
||||
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the intial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
|
||||
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the initial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
|
||||
- [30021](https://github.com/apache/superset/pull/30021) The `dev` layer in our Dockerfile no long includes firefox binaries, only Chromium to reduce bloat/docker-build-time.
|
||||
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSLATIONS=true`.
|
||||
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
|
||||
@@ -356,7 +356,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
- [25166](https://github.com/apache/superset/pull/25166) Changed the default configuration of `UPLOAD_FOLDER` from `/app/static/uploads/` to `/static/uploads/`. It also removed the unused `IMG_UPLOAD_FOLDER` and `IMG_UPLOAD_URL` configuration options.
|
||||
- [30284](https://github.com/apache/superset/pull/30284) Deprecated GLOBAL_ASYNC_QUERIES_REDIS_CONFIG in favor of the new GLOBAL_ASYNC_QUERIES_CACHE_BACKEND configuration. To leverage Redis Sentinel, set CACHE_TYPE to RedisSentinelCache, or use RedisCache for standalone Redis
|
||||
- [31961](https://github.com/apache/superset/pull/31961) Upgraded React from version 16.13.1 to 17.0.2. If you are using custom frontend extensions or plugins, you may need to update them to be compatible with React 17.
|
||||
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python envrionment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
|
||||
- [31260](https://github.com/apache/superset/pull/31260) Docker images now use `uv pip install` instead of `pip install` to manage the python environment. Most docker-based deployments will be affected, whether you derive one of the published images, or have custom bootstrap script that install python libraries (drivers)
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
@@ -433,7 +433,7 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
- [26462](https://github.com/apache/superset/issues/26462): Removes the Profile feature given that it's not actively maintained and not widely used.
|
||||
- [26377](https://github.com/apache/superset/pull/26377): Removes the deprecated Redirect API that supported short URLs used before the permalink feature.
|
||||
- [26329](https://github.com/apache/superset/issues/26329): Removes the deprecated `DASHBOARD_NATIVE_FILTERS` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
|
||||
- [25510](https://github.com/apache/superset/pull/25510): Reenforces that any newly defined Python data format (other than epoch) must adhere to the ISO 8601 standard (enforced by way of validation at the API and database level) after a previous relaxation to include slashes in addition to dashes. From now on when specifying new columns, dataset owners will need to use a SQL expression instead to convert their string columns of the form %Y/%m/%d etc. to a `DATE`, `DATETIME`, etc. type.
|
||||
- [25510](https://github.com/apache/superset/pull/25510): Reinforces that any newly defined Python data format (other than epoch) must adhere to the ISO 8601 standard (enforced by way of validation at the API and database level) after a previous relaxation to include slashes in addition to dashes. From now on when specifying new columns, dataset owners will need to use a SQL expression instead to convert their string columns of the form %Y/%m/%d etc. to a `DATE`, `DATETIME`, etc. type.
|
||||
- [26372](https://github.com/apache/superset/issues/26372): Removes the deprecated `GENERIC_CHART_AXES` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled.
|
||||
|
||||
### Potential Downtime
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
@@ -106,7 +106,7 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"webpack": "^5.106.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
142
docs/yarn.lock
142
docs/yarn.lock
@@ -5088,100 +5088,100 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.59.2", "@typescript-eslint/eslint-plugin@^8.52.0":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz#f37b2c189a0177141fe3de3b08f2a83991bfdbfa"
|
||||
integrity sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==
|
||||
"@typescript-eslint/eslint-plugin@8.59.3", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz#5d6da7e7b236b46452fa00d3904bb6f59615bfde"
|
||||
integrity sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/type-utils" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.3"
|
||||
"@typescript-eslint/type-utils" "8.59.3"
|
||||
"@typescript-eslint/utils" "8.59.3"
|
||||
"@typescript-eslint/visitor-keys" "8.59.3"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.59.2", "@typescript-eslint/parser@^8.59.0":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.2.tgz#e2fd0084baa5dd0c24cd789af1c72cbc3a7a1c62"
|
||||
integrity sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==
|
||||
"@typescript-eslint/parser@8.59.3", "@typescript-eslint/parser@^8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.3.tgz#f46cbc70ae0a25119ef94eac9ecd46714788e1a1"
|
||||
integrity sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.3"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
"@typescript-eslint/typescript-estree" "8.59.3"
|
||||
"@typescript-eslint/visitor-keys" "8.59.3"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz#f8b8cbf8692e3a51c2c394acf8cf6900f7e755af"
|
||||
integrity sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==
|
||||
"@typescript-eslint/project-service@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz#1be5ae152aad987a156c9a1a9b4256e75cfbbe0c"
|
||||
integrity sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.2"
|
||||
"@typescript-eslint/types" "^8.59.2"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.59.3"
|
||||
"@typescript-eslint/types" "^8.59.3"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz#63cbd0af2e3180949d6be81122cc555bc71e736d"
|
||||
integrity sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==
|
||||
"@typescript-eslint/scope-manager@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz#91a60f66803fe9dae0696fbab2451f5723f119d2"
|
||||
integrity sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
"@typescript-eslint/visitor-keys" "8.59.3"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.59.2", "@typescript-eslint/tsconfig-utils@^8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz#6e92bc412083753185a79c9f1431e78169d9232f"
|
||||
integrity sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==
|
||||
"@typescript-eslint/tsconfig-utils@8.59.3", "@typescript-eslint/tsconfig-utils@^8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz#88ca9036b42ccdd1e630cfdafd2e042c2ca6a835"
|
||||
integrity sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==
|
||||
|
||||
"@typescript-eslint/type-utils@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz#a60a1192a804fa472a92c41656853ac6a9ba7176"
|
||||
integrity sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==
|
||||
"@typescript-eslint/type-utils@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz#421fb2448bdfeb301d134a01cd02503f67fd8192"
|
||||
integrity sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
"@typescript-eslint/typescript-estree" "8.59.3"
|
||||
"@typescript-eslint/utils" "8.59.3"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.59.2", "@typescript-eslint/types@^8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.2.tgz#01caabcd7e4715c33ad5e11cab260829714d6b9c"
|
||||
integrity sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==
|
||||
"@typescript-eslint/types@8.59.3", "@typescript-eslint/types@^8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.3.tgz#b7ca539c5e302fdde9a7cadb73caed107ef8f2cd"
|
||||
integrity sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz#6a217ef65b18dbd12c718fc86a675d1d7a1414cc"
|
||||
integrity sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==
|
||||
"@typescript-eslint/typescript-estree@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz#e6bb1408e00b47e431427a40268db4e86cb121ab"
|
||||
integrity sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.59.2"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/visitor-keys" "8.59.2"
|
||||
"@typescript-eslint/project-service" "8.59.3"
|
||||
"@typescript-eslint/tsconfig-utils" "8.59.3"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
"@typescript-eslint/visitor-keys" "8.59.3"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/utils@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.2.tgz#ff619a6a3075f4017fa91b8610b752a8ca3366aa"
|
||||
integrity sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==
|
||||
"@typescript-eslint/utils@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.3.tgz#f693f979deb4dc3994de03ff8b23976d625c36c5"
|
||||
integrity sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/scope-manager" "8.59.3"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
"@typescript-eslint/typescript-estree" "8.59.3"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.59.2":
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz#5ccc486913cd347883d69158836b1189a660bfe6"
|
||||
integrity sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==
|
||||
"@typescript-eslint/visitor-keys@8.59.3":
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz#820843b1b5ca4290009cf189382abcf6fe00dfa6"
|
||||
integrity sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.59.2"
|
||||
"@typescript-eslint/types" "8.59.3"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -14763,15 +14763,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.59.2:
|
||||
version "8.59.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz#e24b4f7232e20112e40572dba162a829a738ce98"
|
||||
integrity sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==
|
||||
typescript-eslint@^8.59.3:
|
||||
version "8.59.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz#4a41d9007faa539a66292189e2795eeb0b9fca29"
|
||||
integrity sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.59.2"
|
||||
"@typescript-eslint/parser" "8.59.2"
|
||||
"@typescript-eslint/typescript-estree" "8.59.2"
|
||||
"@typescript-eslint/utils" "8.59.2"
|
||||
"@typescript-eslint/eslint-plugin" "8.59.3"
|
||||
"@typescript-eslint/parser" "8.59.3"
|
||||
"@typescript-eslint/typescript-estree" "8.59.3"
|
||||
"@typescript-eslint/utils" "8.59.3"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -92,6 +92,26 @@ class Dimension:
|
||||
grain: Grain | None = None
|
||||
|
||||
|
||||
class AggregationType(str, enum.Enum):
|
||||
"""
|
||||
Aggregation function applied by a metric.
|
||||
|
||||
Additivity (across an arbitrary set of grouping dimensions):
|
||||
* ``SUM``, ``COUNT``: fully additive — sub-group sums roll up via ``sum``.
|
||||
* ``MIN``, ``MAX``: roll up via ``min`` / ``max`` of sub-group values.
|
||||
* ``AVG``, ``COUNT_DISTINCT``, ``OTHER``: not safely roll-uppable from
|
||||
sub-aggregates without auxiliary data.
|
||||
"""
|
||||
|
||||
SUM = "SUM"
|
||||
COUNT = "COUNT"
|
||||
MIN = "MIN"
|
||||
MAX = "MAX"
|
||||
AVG = "AVG"
|
||||
COUNT_DISTINCT = "COUNT_DISTINCT"
|
||||
OTHER = "OTHER"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Metric:
|
||||
id: str
|
||||
@@ -100,6 +120,7 @@ class Metric:
|
||||
|
||||
definition: str
|
||||
description: str | None = None
|
||||
aggregation: AggregationType | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -77,6 +77,10 @@ const restrictedImportsRules = {
|
||||
name: 'query-string',
|
||||
message: 'Please use the URLSearchParams API instead of query-string.',
|
||||
},
|
||||
'no-jest-mock-console': {
|
||||
name: 'jest-mock-console',
|
||||
message: 'Please use native Jest spies, i.e. jest.spyOn(console, "warn")',
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
249
superset-frontend/package-lock.json
generated
249
superset-frontend/package-lock.json
generated
@@ -222,8 +222,8 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
@@ -14358,17 +14358,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
|
||||
"integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
|
||||
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/type-utils": "8.59.2",
|
||||
"@typescript-eslint/utils": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/type-utils": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -14381,20 +14381,20 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
|
||||
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.3",
|
||||
"@typescript-eslint/types": "^8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14409,14 +14409,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
|
||||
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14427,9 +14427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14444,9 +14444,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
|
||||
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14458,16 +14458,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
|
||||
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/project-service": "8.59.3",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -14486,16 +14486,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
|
||||
"integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
|
||||
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2"
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14510,13 +14510,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
|
||||
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14590,16 +14590,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
|
||||
"integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
|
||||
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14615,14 +14615,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
|
||||
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.3",
|
||||
"@typescript-eslint/types": "^8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14637,14 +14637,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
|
||||
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14655,9 +14655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14672,9 +14672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
|
||||
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14686,16 +14686,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
|
||||
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/project-service": "8.59.3",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -14714,13 +14714,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
|
||||
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14855,15 +14855,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -14880,14 +14880,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
|
||||
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.3",
|
||||
"@typescript-eslint/types": "^8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -14902,14 +14902,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
|
||||
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -14920,9 +14920,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14937,9 +14937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
|
||||
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14951,16 +14951,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
|
||||
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/project-service": "8.59.3",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -14979,16 +14979,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
|
||||
"integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
|
||||
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2"
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -15003,13 +15003,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
|
||||
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -30223,16 +30223,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-mock-console": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-2.0.0.tgz",
|
||||
"integrity": "sha512-7zrKtXVut+6doalosFxw/2O9spLepQJ9VukODtyLIub2fFkWKe1TyQrxr/GyQogTQcdkHfhvFJdx1OEzLqf/mw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"jest": ">= 22.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-pnp-resolver": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
|
||||
@@ -47934,9 +47924,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vm2": {
|
||||
"version": "3.10.5",
|
||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.10.5.tgz",
|
||||
"integrity": "sha512-3P/2QDccVFBcujfCOeP8vVNuGfuBJHEuvGR8eMmI10p/iwLL2UwF5PDaNaoOS2pRGQEDmJRyeEcc8kmm2Z59RA==",
|
||||
"version": "3.11.3",
|
||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.11.3.tgz",
|
||||
"integrity": "sha512-DO1TTKuOc+veL11VNOvJwRab80mghFKE40Av3bl6pdXs11bdiDMuR73owy+dS2EsTZEvRUeBkkBuDVRjV/RgEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
@@ -50216,7 +50206,6 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"jest-mock-console": "^2.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"timezone-mock": "^1.4.2"
|
||||
},
|
||||
|
||||
@@ -303,8 +303,8 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"babel-jest": "^30.0.2",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"jest-mock-console": "^2.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"timezone-mock": "^1.4.2"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Label(props: LabelProps) {
|
||||
} = props;
|
||||
|
||||
const baseColor = getColorVariants(theme, type);
|
||||
const color = baseColor.active;
|
||||
const color = baseColor.text;
|
||||
const borderColor = baseColor.border;
|
||||
const backgroundColor = baseColor.bg;
|
||||
|
||||
|
||||
@@ -507,7 +507,7 @@ const Select = forwardRef(
|
||||
|
||||
const bulkSelectComponent = useMemo(
|
||||
() => (
|
||||
<StyledBulkActionsContainer justify="space-between">
|
||||
<StyledBulkActionsContainer justify="center" gap="small" wrap>
|
||||
<Button
|
||||
type="link"
|
||||
buttonStyle="link"
|
||||
@@ -519,7 +519,7 @@ const Select = forwardRef(
|
||||
handleSelectAll();
|
||||
}}
|
||||
>
|
||||
{`${t('Select all')} (${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@@ -536,7 +536,7 @@ const Select = forwardRef(
|
||||
handleDeselectAll();
|
||||
}}
|
||||
>
|
||||
{`${t('Clear')} (${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
|
||||
</Button>
|
||||
</StyledBulkActionsContainer>
|
||||
),
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@superset-ui/core/spec';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { triggerResizeObserver } from 'resize-observer-polyfill';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
@@ -66,8 +65,6 @@ function getDimensionText(container: HTMLElement) {
|
||||
describe('SuperChart', () => {
|
||||
jest.setTimeout(5000);
|
||||
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
const plugins = [
|
||||
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }),
|
||||
new BuggyChartPlugin().configure({ key: ChartKeys.BUGGY }),
|
||||
@@ -80,14 +77,9 @@ describe('SuperChart', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
restoreConsole = mockConsole();
|
||||
triggerResizeObserver([]); // Reset any pending resize observers
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
describe('includes ErrorBoundary', () => {
|
||||
let expectedErrors = 0;
|
||||
let actualErrors = 0;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import { render, screen, waitFor } from '@superset-ui/core/spec';
|
||||
@@ -38,8 +37,6 @@ describe('SuperChartCore', () => {
|
||||
new SlowChartPlugin().configure({ key: ChartKeys.SLOW }),
|
||||
];
|
||||
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(30000);
|
||||
plugins.forEach(p => {
|
||||
@@ -53,14 +50,6 @@ describe('SuperChartCore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
restoreConsole = mockConsole();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
describe('registered charts', () => {
|
||||
test('renders registered chart', async () => {
|
||||
const { container } = render(
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { ComponentType } from 'react';
|
||||
import mockConsole, { RestoreConsole } from 'jest-mock-console';
|
||||
import { render as renderTestComponent, screen } from '@testing-library/react';
|
||||
import createLoadableRenderer, {
|
||||
LoadableRenderer as LoadableRendererType,
|
||||
@@ -33,10 +32,8 @@ describe('createLoadableRenderer', () => {
|
||||
let render: (loaded: { Chart: ComponentType }) => JSX.Element;
|
||||
let loading: () => JSX.Element;
|
||||
let LoadableRenderer: LoadableRendererType<{}>;
|
||||
let restoreConsole: RestoreConsole;
|
||||
|
||||
beforeEach(() => {
|
||||
restoreConsole = mockConsole();
|
||||
loadChartSuccess = jest.fn(() => Promise.resolve(TestComponent));
|
||||
render = jest.fn(loaded => {
|
||||
const { Chart } = loaded;
|
||||
@@ -54,10 +51,6 @@ describe('createLoadableRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
describe('returns a LoadableRenderer class', () => {
|
||||
test('LoadableRenderer.preload() preloads the lazy-load components', () => {
|
||||
expect(LoadableRenderer.preload).toBeInstanceOf(Function);
|
||||
|
||||
@@ -18,11 +18,18 @@
|
||||
*/
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
import mockConsole from 'jest-mock-console';
|
||||
import { Registry, OverwritePolicy } from '@superset-ui/core';
|
||||
|
||||
const loader = () => 'testValue';
|
||||
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn');
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error');
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy.mockClear();
|
||||
consoleWarnSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('Registry', () => {
|
||||
test('exists', () => {
|
||||
expect(Registry !== undefined).toBe(true);
|
||||
@@ -308,18 +315,15 @@ describe('Registry', () => {
|
||||
describe('=ALLOW', () => {
|
||||
describe('.registerValue(key, value)', () => {
|
||||
test('registers normally', () => {
|
||||
const restoreConsole = mockConsole();
|
||||
const registry = new Registry();
|
||||
registry.registerValue('a', 'testValue');
|
||||
expect(() => registry.registerValue('a', 'testValue2')).not.toThrow();
|
||||
expect(registry.get('a')).toEqual('testValue2');
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
describe('.registerLoader(key, loader)', () => {
|
||||
test('registers normally', () => {
|
||||
const restoreConsole = mockConsole();
|
||||
const registry = new Registry();
|
||||
registry.registerLoader('a', () => 'testValue');
|
||||
expect(() =>
|
||||
@@ -327,14 +331,12 @@ describe('Registry', () => {
|
||||
).not.toThrow();
|
||||
expect(registry.get('a')).toEqual('testValue2');
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('=WARN', () => {
|
||||
describe('.registerValue(key, value)', () => {
|
||||
test('warns when overwrite', () => {
|
||||
const restoreConsole = mockConsole();
|
||||
const registry = new Registry({
|
||||
overwritePolicy: OverwritePolicy.Warn,
|
||||
});
|
||||
@@ -342,12 +344,10 @@ describe('Registry', () => {
|
||||
expect(() => registry.registerValue('a', 'testValue2')).not.toThrow();
|
||||
expect(registry.get('a')).toEqual('testValue2');
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
describe('.registerLoader(key, loader)', () => {
|
||||
test('warns when overwrite', () => {
|
||||
const restoreConsole = mockConsole();
|
||||
const registry = new Registry({
|
||||
overwritePolicy: OverwritePolicy.Warn,
|
||||
});
|
||||
@@ -357,7 +357,6 @@ describe('Registry', () => {
|
||||
).not.toThrow();
|
||||
expect(registry.get('a')).toEqual('testValue2');
|
||||
expect(console.warn).toHaveBeenCalled();
|
||||
restoreConsole();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -438,14 +437,6 @@ describe('Registry', () => {
|
||||
});
|
||||
|
||||
describe('with a broken listener', () => {
|
||||
let restoreConsole: any;
|
||||
beforeEach(() => {
|
||||
restoreConsole = mockConsole();
|
||||
});
|
||||
afterEach(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
test('keeps working', () => {
|
||||
const errorListener = jest.fn().mockImplementation(() => {
|
||||
throw new Error('test error');
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ValueGetterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import htmlTextFilterValueGetter, {
|
||||
htmlTextComparator,
|
||||
} from './htmlTextFilterValueGetter';
|
||||
|
||||
const makeParams = (value: unknown): ValueGetterParams =>
|
||||
({
|
||||
data: { foo: value },
|
||||
colDef: { field: 'foo' },
|
||||
}) as unknown as ValueGetterParams;
|
||||
|
||||
test('htmlTextFilterValueGetter extracts visible text from HTML anchor', () => {
|
||||
expect(
|
||||
htmlTextFilterValueGetter(
|
||||
makeParams(
|
||||
'<a href="https://jira.example.com/123/S18_3232">S18_3232</a>',
|
||||
),
|
||||
),
|
||||
).toBe('S18_3232');
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter strips nested HTML markup', () => {
|
||||
expect(
|
||||
htmlTextFilterValueGetter(
|
||||
makeParams('<div><strong>Hello</strong> <em>World</em></div>'),
|
||||
),
|
||||
).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter passes plain strings through', () => {
|
||||
expect(htmlTextFilterValueGetter(makeParams('plain value'))).toBe(
|
||||
'plain value',
|
||||
);
|
||||
});
|
||||
|
||||
test('htmlTextFilterValueGetter passes non-string values through', () => {
|
||||
expect(htmlTextFilterValueGetter(makeParams(42))).toBe(42);
|
||||
expect(htmlTextFilterValueGetter(makeParams(null))).toBeNull();
|
||||
expect(htmlTextFilterValueGetter(makeParams(undefined))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('htmlTextComparator orders by visible text, not raw HTML', () => {
|
||||
// URL prefixes (zzz vs bbb) would flip the order under raw-HTML sort,
|
||||
// but the visible labels (S700_4002 vs S72_3212) sort the other way.
|
||||
const left = '<a href="https://jira.example.com/zzz/S700_4002">S700_4002</a>';
|
||||
const right = '<a href="https://jira.example.com/bbb/S72_3212">S72_3212</a>';
|
||||
expect(htmlTextComparator(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test('htmlTextComparator handles nulls and numbers', () => {
|
||||
expect(htmlTextComparator(null, null)).toBe(0);
|
||||
expect(htmlTextComparator(null, 'x')).toBeLessThan(0);
|
||||
expect(htmlTextComparator('x', null)).toBeGreaterThan(0);
|
||||
expect(htmlTextComparator(1, 2)).toBeLessThan(0);
|
||||
expect(htmlTextComparator(2, 1)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('htmlTextComparator preserves default codepoint ordering for plain strings', () => {
|
||||
// AG Grid's default string comparator orders by codepoint, so 'Z' (90)
|
||||
// sorts before 'a' (97). A locale-aware comparator would flip this —
|
||||
// verify we match the default so plain string columns are unaffected.
|
||||
expect(htmlTextComparator('Z', 'a')).toBeLessThan(0);
|
||||
expect(htmlTextComparator('a', 'Z')).toBeGreaterThan(0);
|
||||
expect(htmlTextComparator('apple', 'banana')).toBeLessThan(0);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { isProbablyHTML, sanitizeHtml } from '@superset-ui/core';
|
||||
import { ValueGetterParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
|
||||
const stripHtmlToText = (html: string): string => {
|
||||
const doc = new DOMParser().parseFromString(sanitizeHtml(html), 'text/html');
|
||||
return (doc.body.textContent || '').trim();
|
||||
};
|
||||
|
||||
// Cache the comparator-ready form per raw string. Both the HTML-detection
|
||||
// step (`isProbablyHTML`, which itself invokes DOMParser for HTML-looking
|
||||
// values) and the extraction step (`stripHtmlToText`, also DOMParser) are
|
||||
// expensive; sort runs `O(n log n)` comparator calls against the same set
|
||||
// of cell values. Memoizing the combined detection + extraction means each
|
||||
// unique cell value pays the cost once per session. Module-level scope;
|
||||
// bounded by the count of unique string cell values seen.
|
||||
const comparableTextCache = new Map<string, string>();
|
||||
|
||||
const toComparableText = (raw: string): string => {
|
||||
const cached = comparableTextCache.get(raw);
|
||||
if (cached !== undefined) return cached;
|
||||
const normalized = isProbablyHTML(raw) ? stripHtmlToText(raw) : raw;
|
||||
comparableTextCache.set(raw, normalized);
|
||||
return normalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the visible-text representation of an HTML cell value so AG Grid
|
||||
* filters and sort operate on what the user sees, not the underlying markup.
|
||||
* Pass-through for non-HTML values.
|
||||
*/
|
||||
const htmlTextFilterValueGetter = (params: ValueGetterParams) => {
|
||||
const raw = params.data?.[params.colDef.field as string];
|
||||
return typeof raw === 'string' ? toComparableText(raw) : raw;
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparator that mirrors AG Grid's default string comparator (codepoint
|
||||
* order, nulls first), but extracts visible text from HTML values first
|
||||
* so HTML cells sort by their displayed label. Plain (non-HTML) values
|
||||
* pass through unchanged, preserving default ordering — e.g. 'Z' still
|
||||
* sorts before 'a' as it does under the default comparator.
|
||||
*/
|
||||
export const htmlTextComparator = (a: unknown, b: unknown): number => {
|
||||
const toText = (v: unknown) =>
|
||||
typeof v === 'string' ? toComparableText(v) : v;
|
||||
const aT = toText(a);
|
||||
const bT = toText(b);
|
||||
if (aT == null && bT == null) return 0;
|
||||
if (aT == null) return -1;
|
||||
if (bT == null) return 1;
|
||||
if (typeof aT === 'number' && typeof bT === 'number') return aT - bT;
|
||||
if (aT === bT) return 0;
|
||||
return aT < bT ? -1 : 1;
|
||||
};
|
||||
|
||||
export default htmlTextFilterValueGetter;
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
} from '../types';
|
||||
import getCellClass from './getCellClass';
|
||||
import filterValueGetter from './filterValueGetter';
|
||||
import htmlTextFilterValueGetter, {
|
||||
htmlTextComparator,
|
||||
} from './htmlTextFilterValueGetter';
|
||||
import dateFilterComparator from './dateFilterComparator';
|
||||
import DateWithFormatter from './DateWithFormatter';
|
||||
import { getAggFunc } from './getAggFunc';
|
||||
@@ -317,6 +320,24 @@ export const useColDefs = ({
|
||||
...(isPercentMetric && {
|
||||
filterValueGetter,
|
||||
}),
|
||||
...(dataType === GenericDataType.String &&
|
||||
!serverPagination && {
|
||||
// HTML cells (e.g. anchor markup) are rendered by TextCellRenderer
|
||||
// via dangerouslySetInnerHTML; without these the filter and sort
|
||||
// operate on raw HTML so the URL inside the markup dictates order
|
||||
// and the "Contains" filter matches against the raw HTML string.
|
||||
//
|
||||
// Gated on !serverPagination: in server-pagination mode sort and
|
||||
// filter are both delegated to the backend (which sees raw HTML
|
||||
// in the database), so applying the visible-text getter only on
|
||||
// the client would create a mismatch where the typed filter
|
||||
// value is stripped client-side but the server query still
|
||||
// operates on the raw HTML. Server-paginated tables with HTML
|
||||
// columns are out of scope for this fix and would require
|
||||
// server-side handling.
|
||||
filterValueGetter: htmlTextFilterValueGetter,
|
||||
comparator: htmlTextComparator,
|
||||
}),
|
||||
...(dataType === GenericDataType.Temporal && {
|
||||
// Use dateFilterValueGetter so AG Grid correctly identifies null dates for blank filter
|
||||
filterValueGetter: dateFilterValueGetter,
|
||||
|
||||
@@ -285,8 +285,6 @@ export default function transformProps(
|
||||
}
|
||||
const labelProps = {
|
||||
color: theme.colorText,
|
||||
textBorderColor: theme.colorBgBase,
|
||||
textBorderWidth: 1,
|
||||
};
|
||||
const traverse = (
|
||||
treeNodes: TreeNode[],
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import { EchartsSunburstChartProps } from '../../src/Sunburst/types';
|
||||
import transformProps from '../../src/Sunburst/transformProps';
|
||||
|
||||
const formData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
groupby: ['category'],
|
||||
metric: 'sum__value',
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{ category: 'A', sum__value: 10 },
|
||||
{ category: 'B', sum__value: 20 },
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
test('series label has no textBorderColor or textBorderWidth', () => {
|
||||
const { echartOptions } = transformProps(
|
||||
chartProps as EchartsSunburstChartProps,
|
||||
);
|
||||
const series = (echartOptions as any).series[0];
|
||||
expect(series.label).not.toHaveProperty('textBorderColor');
|
||||
expect(series.label).not.toHaveProperty('textBorderWidth');
|
||||
});
|
||||
@@ -44,5 +44,4 @@ export const ModalResultSetWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-height: 50vh;
|
||||
`;
|
||||
|
||||
@@ -22,12 +22,18 @@ export const ExplorePopoverContent = styled.div`
|
||||
.edit-popover-resize {
|
||||
transform: scaleX(-1);
|
||||
float: right;
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
margin-right: ${({ theme }) => theme.sizeUnit * -1}px;
|
||||
margin-top: ${({ theme }) => theme.margin}px;
|
||||
margin-right: ${({ theme }) => theme.marginXXS * -1}px;
|
||||
color: ${({ theme }) => theme.colorIcon};
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.filter-sql-editor {
|
||||
border: ${({ theme }) => theme.colorBorder} solid thin;
|
||||
}
|
||||
&& .ant-tabs-nav {
|
||||
margin-bottom: ${({ theme }) => theme.marginSM}px;
|
||||
}
|
||||
&& .ant-form-item {
|
||||
margin-bottom: ${({ theme }) => theme.marginXS}px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -78,14 +78,6 @@ interface AdhocFilterEditPopoverState {
|
||||
}
|
||||
|
||||
const FilterPopoverContentContainer = styled.div`
|
||||
.adhoc-filter-edit-tabs > .nav-tabs {
|
||||
margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
|
||||
& > li > a {
|
||||
padding: ${({ theme }) => theme.sizeUnit}px;
|
||||
}
|
||||
}
|
||||
|
||||
#filter-edit-popover {
|
||||
max-width: none;
|
||||
}
|
||||
@@ -97,21 +89,17 @@ const FilterPopoverContentContainer = styled.div`
|
||||
.filter-edit-clause-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.sizeUnit * 5}px;
|
||||
}
|
||||
|
||||
.adhoc-filter-simple-column-dropdown {
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 5}px;
|
||||
gap: ${({ theme }) => theme.marginMD}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterActionsContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
margin-top: ${({ theme }) => theme.marginXS}px;
|
||||
`;
|
||||
|
||||
const LayerSelectContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
margin-bottom: ${({ theme }) => theme.sizeUnit * 12}px;
|
||||
margin-top: ${({ theme }) => theme.marginXS}px;
|
||||
margin-bottom: ${({ theme }) => theme.marginXXL}px;
|
||||
`;
|
||||
|
||||
export default class AdhocFilterEditPopover extends Component<
|
||||
|
||||
@@ -523,8 +523,7 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
const subjectComponent = (
|
||||
<Select
|
||||
css={{
|
||||
marginTop: theme.sizeUnit * 4,
|
||||
marginBottom: theme.sizeUnit * 4,
|
||||
marginBottom: theme.marginXS,
|
||||
}}
|
||||
data-test="select-element"
|
||||
options={columns.map(column => ({
|
||||
@@ -565,7 +564,7 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
>
|
||||
<SelectWithLabel
|
||||
css={css`
|
||||
margin-top: ${theme.sizeUnit * 4}px;
|
||||
margin-top: ${theme.marginXS}px;
|
||||
`}
|
||||
labelText={labelText}
|
||||
options={suggestions}
|
||||
@@ -581,7 +580,7 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${theme.sizeUnit * 4}px;
|
||||
margin-top: ${theme.marginXS}px;
|
||||
`}
|
||||
/>
|
||||
<Input
|
||||
|
||||
@@ -64,7 +64,6 @@ export const StyledCloseButton = styled(Button)`
|
||||
color: ${theme.colorPrimaryText};
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
text-transform: uppercase;
|
||||
min-width: ${theme.sizeUnit * 36};
|
||||
min-height: ${theme.sizeUnit * 8};
|
||||
box-shadow: none;
|
||||
@@ -113,7 +112,6 @@ export const StyledSaveButton = styled(Button)`
|
||||
color: ${theme.colorTextLightSolid};
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
text-transform: uppercase;
|
||||
min-width: ${theme.sizeUnit * 36};
|
||||
min-height: ${theme.sizeUnit * 8};
|
||||
box-shadow: none;
|
||||
|
||||
@@ -36,7 +36,6 @@ export const StyledExtentButton = styled(Button)`
|
||||
color: ${theme.colorPrimaryText};
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
text-transform: uppercase;
|
||||
min-width: ${theme.sizeUnit * 36};
|
||||
min-height: ${theme.sizeUnit * 8};
|
||||
box-shadow: none;
|
||||
|
||||
562
superset-websocket/package-lock.json
generated
562
superset-websocket/package-lock.json
generated
@@ -25,8 +25,8 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.59.1"
|
||||
"typescript-eslint": "^8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.0",
|
||||
@@ -1844,17 +1844,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
|
||||
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/type-utils": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -1883,16 +1883,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
|
||||
"integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
|
||||
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1907,184 +1907,15 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
|
||||
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.3",
|
||||
"@typescript-eslint/types": "^8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2099,14 +1930,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
|
||||
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2117,9 +1948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2134,15 +1965,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2159,9 +1990,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
|
||||
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2173,16 +2004,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
|
||||
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/project-service": "8.59.3",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2211,9 +2042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2240,16 +2071,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
|
||||
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2264,13 +2095,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
|
||||
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6368,41 +6199,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
|
||||
"integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
|
||||
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/eslint-plugin": "8.59.3",
|
||||
"@typescript-eslint/parser": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -8132,16 +7938,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
|
||||
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/type-utils": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -8156,168 +7962,75 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
|
||||
"integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
|
||||
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"debug": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/project-service": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
|
||||
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.3",
|
||||
"@typescript-eslint/types": "^8.59.3",
|
||||
"debug": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
|
||||
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
|
||||
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
|
||||
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
|
||||
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/project-service": "8.59.3",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/visitor-keys": "8.59.3",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -8332,9 +8045,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -8352,24 +8065,24 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
|
||||
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
"@typescript-eslint/scope-manager": "8.59.3",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
|
||||
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.3",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -11331,30 +11044,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
|
||||
"integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
|
||||
"version": "8.59.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
|
||||
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"debug": "^4.4.3"
|
||||
}
|
||||
}
|
||||
"@typescript-eslint/eslint-plugin": "8.59.3",
|
||||
"@typescript-eslint/parser": "8.59.3",
|
||||
"@typescript-eslint/typescript-estree": "8.59.3",
|
||||
"@typescript-eslint/utils": "8.59.3"
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.59.3",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.59.1"
|
||||
"typescript-eslint": "^8.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.0",
|
||||
|
||||
@@ -71,22 +71,6 @@ def create_app(
|
||||
# value of app_root so things work out of the box
|
||||
if not app.config["STATIC_ASSETS_PREFIX"]:
|
||||
app.config["STATIC_ASSETS_PREFIX"] = app_root
|
||||
# Prefix APP_ICON path with subdirectory root for subdirectory deployments
|
||||
if (
|
||||
app.config.get("APP_ICON", "").startswith("/static/")
|
||||
and app_root != "/"
|
||||
):
|
||||
app.config["APP_ICON"] = f"{app_root}{app.config['APP_ICON']}"
|
||||
# Also update theme tokens for subdirectory deployments
|
||||
for theme_key in ("THEME_DEFAULT", "THEME_DARK"):
|
||||
theme = app.config[theme_key]
|
||||
token = theme.get("token", {})
|
||||
# Update brandLogoUrl if it points to /static/
|
||||
if token.get("brandLogoUrl", "").startswith("/static/"):
|
||||
token["brandLogoUrl"] = f"{app_root}{token['brandLogoUrl']}"
|
||||
# Update brandLogoHref if it's the default "/"
|
||||
if token.get("brandLogoHref") == "/":
|
||||
token["brandLogoHref"] = app_root
|
||||
if app.config["APPLICATION_ROOT"] == "/":
|
||||
app.config["APPLICATION_ROOT"] = app_root
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.dialects import registry
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import DatabaseCategory
|
||||
from superset.db_engine_specs.hive import HiveEngineSpec
|
||||
@@ -40,6 +42,8 @@ time_grain_expressions: dict[str | None, str] = {
|
||||
|
||||
|
||||
class SparkEngineSpec(HiveEngineSpec):
|
||||
engine = "spark"
|
||||
registry.register("spark", "pyhive.sqlalchemy_hive", "HiveDialect")
|
||||
_time_grain_expressions = time_grain_expressions
|
||||
engine_name = "Apache Spark SQL"
|
||||
|
||||
@@ -53,6 +57,6 @@ class SparkEngineSpec(HiveEngineSpec):
|
||||
DatabaseCategory.OPEN_SOURCE,
|
||||
],
|
||||
"pypi_packages": ["pyhive"],
|
||||
"connection_string": "hive://hive@{hostname}:{port}/{database}",
|
||||
"connection_string": "spark://hive@{hostname}:{port}/{database}",
|
||||
"default_port": 10000,
|
||||
}
|
||||
|
||||
@@ -516,6 +516,47 @@ def _cleanup_session_on_error() -> None:
|
||||
logger.warning("Error cleaning up session after exception: %s", e)
|
||||
|
||||
|
||||
def _remove_session_safe() -> None:
|
||||
"""Remove the scoped SQLAlchemy session, tolerating SSL/connection errors.
|
||||
|
||||
Thread-pool workers reuse threads across requests. Before each tool call
|
||||
the session is removed to prevent a prior request's thread-local session
|
||||
from leaking into the next one. If the underlying DBAPI connection died
|
||||
between requests (e.g. RDS SSL idle-timeout or max-connection-age), the
|
||||
rollback implicit in ``session.close()`` raises a ``DBAPIError`` subclass
|
||||
(``OperationalError`` for psycopg2, ``InterfaceError`` for some other
|
||||
drivers).
|
||||
|
||||
When that happens:
|
||||
1. Invalidate the dead connection so the pool discards it (rather than
|
||||
returning a broken connection to the next caller).
|
||||
2. Retry ``remove()`` to deregister the session from the scoped registry.
|
||||
|
||||
The tool call still proceeds because a fresh connection will be obtained
|
||||
on the next DB access.
|
||||
"""
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
|
||||
from superset.extensions import db
|
||||
|
||||
try:
|
||||
db.session.remove()
|
||||
except DBAPIError as exc:
|
||||
logger.warning(
|
||||
"Connection error during pre-call session cleanup "
|
||||
"(likely SSL/idle timeout); invalidating connection and retrying: %s",
|
||||
exc,
|
||||
)
|
||||
try:
|
||||
db.session.invalidate()
|
||||
except Exception as invalidate_exc:
|
||||
logger.debug(
|
||||
"Could not invalidate session after connection error: %s",
|
||||
invalidate_exc,
|
||||
)
|
||||
db.session.remove() # retry: session deregisters cleanly after invalidation
|
||||
|
||||
|
||||
def mcp_auth_hook(tool_func: F) -> F: # noqa: C901
|
||||
"""
|
||||
Authentication and authorization decorator for MCP tools.
|
||||
@@ -638,9 +679,7 @@ def mcp_auth_hook(tool_func: F) -> F: # noqa: C901
|
||||
# still be bound to a different tenant's DB engine. Removing it here
|
||||
# ensures the next DB access creates a fresh session bound to the
|
||||
# correct engine for the current request.
|
||||
from superset.extensions import db
|
||||
|
||||
db.session.remove()
|
||||
_remove_session_safe()
|
||||
user = _setup_user_context()
|
||||
|
||||
# No Flask context - this is a FastMCP internal operation
|
||||
|
||||
@@ -46,7 +46,10 @@ from superset.mcp_service.chart.schemas import (
|
||||
GetChartDataRequest,
|
||||
PerformanceMetadata,
|
||||
)
|
||||
from superset.mcp_service.utils import sanitize_for_llm_context
|
||||
from superset.mcp_service.utils import (
|
||||
escape_llm_context_delimiters,
|
||||
sanitize_for_llm_context,
|
||||
)
|
||||
from superset.mcp_service.utils.cache_utils import get_cache_status_from_result
|
||||
from superset.mcp_service.utils.oauth2_utils import (
|
||||
build_oauth2_redirect_message,
|
||||
@@ -199,8 +202,12 @@ async def get_chart_data( # noqa: C901
|
||||
|
||||
if not chart:
|
||||
await ctx.warning("Chart not found: identifier=%s" % (request.identifier,))
|
||||
safe_id = escape_llm_context_delimiters(str(request.identifier)[:200])
|
||||
return ChartError(
|
||||
error=f"No chart found with identifier: {request.identifier}",
|
||||
error=(
|
||||
f"No chart found with identifier: {safe_id}."
|
||||
" Use list_charts to get valid chart IDs."
|
||||
),
|
||||
error_type="NotFound",
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,10 @@ from superset.mcp_service.chart.schemas import (
|
||||
URLPreview,
|
||||
VegaLitePreview,
|
||||
)
|
||||
from superset.mcp_service.utils import sanitize_for_llm_context
|
||||
from superset.mcp_service.utils import (
|
||||
escape_llm_context_delimiters,
|
||||
sanitize_for_llm_context,
|
||||
)
|
||||
from superset.mcp_service.utils.oauth2_utils import (
|
||||
build_oauth2_redirect_message,
|
||||
OAUTH2_CONFIG_ERROR_MESSAGE,
|
||||
@@ -1201,8 +1204,22 @@ async def _get_chart_preview_internal( # noqa: C901
|
||||
|
||||
if not chart:
|
||||
await ctx.warning("Chart not found: identifier=%s" % (request.identifier,))
|
||||
is_form_data_key = (
|
||||
isinstance(request.identifier, str)
|
||||
and len(request.identifier) > 8
|
||||
and not request.identifier.isdigit()
|
||||
)
|
||||
if is_form_data_key:
|
||||
recovery = (
|
||||
"If using a form_data_key, it may have expired — "
|
||||
"use generate_explore_link to get a fresh key, "
|
||||
"or use list_charts to find a saved chart by ID."
|
||||
)
|
||||
else:
|
||||
recovery = "Use list_charts to get valid chart IDs."
|
||||
safe_id = escape_llm_context_delimiters(str(request.identifier)[:200])
|
||||
return ChartError(
|
||||
error=f"No chart found with identifier: {request.identifier}",
|
||||
error=f"No chart found with identifier: {safe_id}. {recovery}",
|
||||
error_type="NotFound",
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ from superset.mcp_service.chart.schemas import (
|
||||
PerformanceMetadata,
|
||||
UpdateChartRequest,
|
||||
)
|
||||
from superset.mcp_service.utils import escape_llm_context_delimiters
|
||||
from superset.mcp_service.utils.oauth2_utils import (
|
||||
build_oauth2_redirect_message,
|
||||
OAUTH2_CONFIG_ERROR_MESSAGE,
|
||||
@@ -337,17 +338,18 @@ async def update_chart( # noqa: C901
|
||||
chart = find_chart_by_identifier(request.identifier)
|
||||
|
||||
if not chart:
|
||||
safe_id = escape_llm_context_delimiters(str(request.identifier)[:200])
|
||||
not_found_msg = (
|
||||
f"No chart found with identifier: {safe_id}."
|
||||
" Use list_charts to get valid chart IDs."
|
||||
)
|
||||
return GenerateChartResponse.model_validate(
|
||||
{
|
||||
"chart": None,
|
||||
"error": {
|
||||
"error_type": "NotFound",
|
||||
"message": (
|
||||
f"No chart found with identifier: {request.identifier}"
|
||||
),
|
||||
"details": (
|
||||
f"No chart found with identifier: {request.identifier}"
|
||||
),
|
||||
"message": not_found_msg,
|
||||
"details": not_found_msg,
|
||||
},
|
||||
"success": False,
|
||||
"schema_version": "2.0",
|
||||
|
||||
@@ -334,7 +334,10 @@ def _find_and_authorize_dashboard(
|
||||
dashboard=None,
|
||||
dashboard_url=None,
|
||||
position=None,
|
||||
error=f"Dashboard with ID {dashboard_id} not found",
|
||||
error=(
|
||||
f"Dashboard with ID {dashboard_id} not found."
|
||||
" Use list_dashboards to get valid dashboard IDs."
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -392,7 +395,10 @@ def add_chart_to_existing_dashboard(
|
||||
dashboard=None,
|
||||
dashboard_url=None,
|
||||
position=None,
|
||||
error=f"Chart with ID {request.chart_id} not found",
|
||||
error=(
|
||||
f"Chart with ID {request.chart_id} not found."
|
||||
" Use list_charts to get valid chart IDs."
|
||||
),
|
||||
)
|
||||
|
||||
# Validate dataset access for the chart.
|
||||
|
||||
@@ -230,7 +230,10 @@ def generate_dashboard( # noqa: C901
|
||||
return GenerateDashboardResponse(
|
||||
dashboard=None,
|
||||
dashboard_url=None,
|
||||
error=f"Charts not found: {list(missing_chart_ids)}",
|
||||
error=(
|
||||
f"Charts not found: {sorted(missing_chart_ids)}."
|
||||
" Use list_charts to get valid chart IDs."
|
||||
),
|
||||
)
|
||||
|
||||
# Validate dataset access for each chart.
|
||||
|
||||
@@ -183,7 +183,10 @@ async def query_dataset( # noqa: C901
|
||||
if dataset is None:
|
||||
await ctx.error("Dataset not found: identifier=%s" % (request.dataset_id,))
|
||||
return DatasetError.create(
|
||||
error=f"No dataset found with identifier: {request.dataset_id}",
|
||||
error=(
|
||||
f"No dataset found with identifier: {request.dataset_id}."
|
||||
" Use list_datasets to get valid dataset IDs."
|
||||
),
|
||||
error_type="NotFound",
|
||||
)
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
|
||||
)
|
||||
return ExecuteSqlResponse(
|
||||
success=False,
|
||||
error=f"Database with ID {request.database_id} not found",
|
||||
error=(
|
||||
f"Database with ID {request.database_id} not found."
|
||||
" Use list_databases to get valid database IDs."
|
||||
),
|
||||
error_type=SupersetErrorType.DATABASE_NOT_FOUND_ERROR.value,
|
||||
)
|
||||
|
||||
|
||||
@@ -103,7 +103,8 @@ def open_sql_lab_with_context(
|
||||
database = DatabaseDAO.find_by_id(request.database_connection_id)
|
||||
if not database:
|
||||
error_message = (
|
||||
f"Database with ID {request.database_connection_id} not found"
|
||||
f"Database with ID {request.database_connection_id} not found."
|
||||
" Use list_databases to get valid database IDs."
|
||||
)
|
||||
return _sanitize_sql_lab_response_for_llm_context(
|
||||
SqlLabResponse(
|
||||
|
||||
709
superset/semantic_layers/cache.py
Normal file
709
superset/semantic_layers/cache.py
Normal file
@@ -0,0 +1,709 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Containment-aware cache for semantic view queries.
|
||||
|
||||
A broader cached result can satisfy a narrower new query: when the new query's
|
||||
filters and limit are strictly more restrictive than a cached entry's, the cached
|
||||
DataFrame is post-filtered and re-limited rather than re-executing the underlying
|
||||
query.
|
||||
|
||||
See ``docs/`` and the plan file for the design rationale; the rules summary:
|
||||
|
||||
* Same metrics and dimensions (shape).
|
||||
* Each cached filter must be implied by a new-query filter on the same column.
|
||||
* New filters on columns with no cached constraint are applied post-fetch as
|
||||
"leftovers" — provided the column is in the projection.
|
||||
* Cached ``limit`` must be at least the new ``limit``; if a cached ``limit`` is
|
||||
present, the orderings must match (otherwise the cached "top N" is not the
|
||||
true top of the new query).
|
||||
* ``ADHOC`` and ``HAVING`` filters require exact-set equality.
|
||||
* ``offset != 0`` and mismatching ``group_limit`` skip the cache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time as _time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import Any, Iterable
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
from flask import current_app
|
||||
from superset_core.semantic_layers.types import (
|
||||
AdhocExpression,
|
||||
AggregationType,
|
||||
Dimension,
|
||||
Filter,
|
||||
Metric,
|
||||
Operator,
|
||||
OrderDirection,
|
||||
OrderTuple,
|
||||
PredicateType,
|
||||
SemanticQuery,
|
||||
SemanticRequest,
|
||||
SemanticResult,
|
||||
)
|
||||
|
||||
from superset.extensions import cache_manager
|
||||
from superset.utils import json
|
||||
from superset.utils.hashing import hash_from_str
|
||||
from superset.utils.pandas_postprocessing.aggregate import aggregate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INDEX_KEY_PREFIX = "sv:idx:"
|
||||
VALUE_KEY_PREFIX = "sv:val:"
|
||||
MAX_ENTRIES_PER_SHAPE = 32
|
||||
|
||||
_AGGREGATION_TO_PANDAS: dict[AggregationType, str] = {
|
||||
AggregationType.SUM: "sum",
|
||||
AggregationType.COUNT: "sum",
|
||||
AggregationType.MIN: "min",
|
||||
AggregationType.MAX: "max",
|
||||
}
|
||||
ADDITIVE_AGGREGATIONS = frozenset(_AGGREGATION_TO_PANDAS)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViewMeta:
|
||||
"""Identity/freshness/TTL info pulled from the SemanticView ORM row."""
|
||||
|
||||
uuid: str
|
||||
changed_on_iso: str
|
||||
cache_timeout: int | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CachedEntry:
|
||||
filters: frozenset[Filter]
|
||||
dimension_keys: frozenset[str]
|
||||
limit: int | None
|
||||
offset: int
|
||||
order_key: str
|
||||
group_limit_key: str
|
||||
value_key: str
|
||||
timestamp: float = field(default_factory=_time.time)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public surface
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def try_serve_from_cache(
|
||||
view_meta: ViewMeta,
|
||||
query: SemanticQuery,
|
||||
) -> SemanticResult | None:
|
||||
"""Return a cached ``SemanticResult`` that satisfies ``query`` if any."""
|
||||
try:
|
||||
cache = cache_manager.data_cache
|
||||
idx_key = shape_key(view_meta, query)
|
||||
entries: list[CachedEntry] | None = cache.get(idx_key)
|
||||
if not entries:
|
||||
return None
|
||||
|
||||
pruned: list[CachedEntry] = []
|
||||
served: SemanticResult | None = None
|
||||
for entry in entries:
|
||||
if served is None:
|
||||
ok, leftovers, projection_needed = can_satisfy(entry, query)
|
||||
if ok:
|
||||
payload = cache.get(entry.value_key)
|
||||
if payload is None:
|
||||
# value evicted but index entry survived; drop it
|
||||
continue
|
||||
pruned.append(entry)
|
||||
served = _apply_post_processing(
|
||||
payload, query, leftovers, projection_needed
|
||||
)
|
||||
continue
|
||||
# keep entry; verify its value is still alive
|
||||
if cache.get(entry.value_key) is not None:
|
||||
pruned.append(entry)
|
||||
|
||||
if len(pruned) != len(entries):
|
||||
cache.set(idx_key, pruned, timeout=_timeout(view_meta))
|
||||
return served
|
||||
except Exception: # pragma: no cover - defensive
|
||||
logger.warning("Semantic view cache lookup failed", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def store_result(
|
||||
view_meta: ViewMeta,
|
||||
query: SemanticQuery,
|
||||
result: SemanticResult,
|
||||
) -> None:
|
||||
"""Persist ``result`` under a fresh value key and register a descriptor."""
|
||||
try:
|
||||
cache = cache_manager.data_cache
|
||||
timeout = _timeout(view_meta)
|
||||
vkey = value_key(view_meta, query)
|
||||
cache.set(vkey, result, timeout=timeout)
|
||||
|
||||
idx_key = shape_key(view_meta, query)
|
||||
entries: list[CachedEntry] = list(cache.get(idx_key) or [])
|
||||
entry = CachedEntry(
|
||||
filters=frozenset(query.filters or set()),
|
||||
dimension_keys=frozenset(_dimension_key(d) for d in query.dimensions),
|
||||
limit=query.limit,
|
||||
offset=query.offset or 0,
|
||||
order_key=_order_key(query.order),
|
||||
group_limit_key=_group_limit_key(query.group_limit),
|
||||
value_key=vkey,
|
||||
)
|
||||
entries = [e for e in entries if e.value_key != vkey]
|
||||
entries.append(entry)
|
||||
if len(entries) > MAX_ENTRIES_PER_SHAPE:
|
||||
entries = sorted(entries, key=lambda e: e.timestamp)[
|
||||
-MAX_ENTRIES_PER_SHAPE:
|
||||
]
|
||||
cache.set(idx_key, entries, timeout=timeout)
|
||||
except Exception: # pragma: no cover - defensive
|
||||
logger.warning("Semantic view cache store failed", exc_info=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Keys
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def shape_key(view_meta: ViewMeta, query: SemanticQuery) -> str:
|
||||
# The shape key buckets entries by metric set only; dimensions live on each
|
||||
# ``CachedEntry`` so we can find broader (dimension-superset) entries via the
|
||||
# projection path.
|
||||
shape = {"m": sorted(m.id for m in query.metrics)}
|
||||
digest = hash_from_str(json.dumps(shape, sort_keys=True))[:16]
|
||||
return f"{INDEX_KEY_PREFIX}{view_meta.uuid}:{view_meta.changed_on_iso}:{digest}"
|
||||
|
||||
|
||||
def value_key(view_meta: ViewMeta, query: SemanticQuery) -> str:
|
||||
digest = hash_from_str(json.dumps(_canonicalize(query), sort_keys=True))[:32]
|
||||
return f"{VALUE_KEY_PREFIX}{view_meta.uuid}:{view_meta.changed_on_iso}:{digest}"
|
||||
|
||||
|
||||
def _dimension_key(dim: Dimension) -> str:
|
||||
grain = dim.grain.representation if dim.grain else "_"
|
||||
return f"{dim.id}@{grain}"
|
||||
|
||||
|
||||
def _canonicalize(query: SemanticQuery) -> dict[str, Any]:
|
||||
return {
|
||||
"m": sorted(m.id for m in query.metrics),
|
||||
"d": sorted(_dimension_key(d) for d in query.dimensions),
|
||||
"f": sorted(_filter_to_jsonable(f) for f in (query.filters or [])),
|
||||
"o": _order_key(query.order),
|
||||
"l": query.limit,
|
||||
"off": query.offset or 0,
|
||||
"gl": _group_limit_key(query.group_limit),
|
||||
}
|
||||
|
||||
|
||||
def _filter_to_jsonable(f: Filter) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"t": f.type.value,
|
||||
"c": f.column.id if f.column is not None else None,
|
||||
"o": f.operator.value,
|
||||
"v": _value_to_jsonable(f.value),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _value_to_jsonable(value: Any) -> Any:
|
||||
if isinstance(value, frozenset):
|
||||
return sorted(_value_to_jsonable(v) for v in value)
|
||||
if isinstance(value, (datetime, date, time)):
|
||||
return value.isoformat()
|
||||
if isinstance(value, timedelta):
|
||||
return value.total_seconds()
|
||||
return value
|
||||
|
||||
|
||||
def _order_key(order: list[OrderTuple] | None) -> str:
|
||||
if not order:
|
||||
return ""
|
||||
return json.dumps(
|
||||
[(_orderable_id(element), direction.value) for element, direction in order]
|
||||
)
|
||||
|
||||
|
||||
def _orderable_id(element: Metric | Dimension | AdhocExpression) -> str:
|
||||
return element.id
|
||||
|
||||
|
||||
def _group_limit_key(group_limit: Any) -> str:
|
||||
if group_limit is None:
|
||||
return ""
|
||||
return json.dumps(
|
||||
{
|
||||
"dims": sorted(d.id for d in group_limit.dimensions),
|
||||
"top": group_limit.top,
|
||||
"metric": group_limit.metric.id if group_limit.metric else None,
|
||||
"direction": group_limit.direction.value,
|
||||
"group_others": group_limit.group_others,
|
||||
"filters": sorted(
|
||||
_filter_to_jsonable(f) for f in (group_limit.filters or [])
|
||||
),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _timeout(view_meta: ViewMeta) -> int | None:
|
||||
if view_meta.cache_timeout is not None:
|
||||
return view_meta.cache_timeout
|
||||
config = current_app.config.get("DATA_CACHE_CONFIG") or {}
|
||||
return config.get("CACHE_DEFAULT_TIMEOUT")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Containment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def can_satisfy( # noqa: C901
|
||||
entry: CachedEntry,
|
||||
query: SemanticQuery,
|
||||
) -> tuple[bool, set[Filter], bool]:
|
||||
"""
|
||||
Return ``(reusable, leftover_filters, projection_needed)`` for ``entry`` vs
|
||||
``query``. ``projection_needed`` is True when the cached entry has a strict
|
||||
superset of the new dimensions and a pandas rollup is required.
|
||||
"""
|
||||
new_dim_keys = frozenset(_dimension_key(d) for d in query.dimensions)
|
||||
cached_dim_keys = entry.dimension_keys
|
||||
|
||||
if cached_dim_keys == new_dim_keys:
|
||||
projection_needed = False
|
||||
elif cached_dim_keys > new_dim_keys:
|
||||
projection_needed = True
|
||||
if not _projection_allowed(entry, query, new_dim_keys, cached_dim_keys):
|
||||
return False, set(), False
|
||||
else:
|
||||
return False, set(), False
|
||||
|
||||
new_filters = frozenset(query.filters or set())
|
||||
|
||||
c_adhoc, c_having, c_where = _split(entry.filters)
|
||||
n_adhoc, n_having, n_where = _split(new_filters)
|
||||
|
||||
if c_adhoc != n_adhoc:
|
||||
return False, set(), False
|
||||
if c_having != n_having:
|
||||
return False, set(), False
|
||||
|
||||
c_by_col = _group_by_column(c_where)
|
||||
n_by_col = _group_by_column(n_where)
|
||||
|
||||
for c_list in c_by_col.values():
|
||||
for c in c_list:
|
||||
n_list = n_by_col.get(_filter_col_id(c), [])
|
||||
if not any(_implies(n, c) for n in n_list):
|
||||
return False, set(), False
|
||||
|
||||
leftovers: set[Filter] = set()
|
||||
for col_id, n_list in n_by_col.items():
|
||||
c_list = c_by_col.get(col_id, [])
|
||||
for n in n_list:
|
||||
if not any(_implies(c, n) for c in c_list):
|
||||
if n.column is None or n.operator == Operator.ADHOC:
|
||||
return False, set(), False
|
||||
leftovers.add(n)
|
||||
|
||||
# Leftover filters are applied to the cached DataFrame BEFORE the optional
|
||||
# rollup, so their columns must be present in the cached projection.
|
||||
allowed_ids = _cached_column_ids(entry, query)
|
||||
for leftover in leftovers:
|
||||
if leftover.column is None or leftover.column.id not in allowed_ids:
|
||||
return False, set(), False
|
||||
|
||||
if entry.offset != 0 or (query.offset or 0) != 0:
|
||||
return False, set(), False
|
||||
|
||||
if projection_needed:
|
||||
# Re-aggregation will re-order by ``query.order`` after rollup, so the
|
||||
# cached order is irrelevant. We do require the new order (if any) to
|
||||
# reference only surviving columns; otherwise sort would fail post-rollup.
|
||||
if not _order_uses_only(query.order, _projection_ids(query)):
|
||||
return False, set(), False
|
||||
else:
|
||||
if entry.limit is not None:
|
||||
if query.limit is None or query.limit > entry.limit:
|
||||
return False, set(), False
|
||||
if entry.order_key != _order_key(query.order):
|
||||
return False, set(), False
|
||||
|
||||
if entry.group_limit_key != _group_limit_key(query.group_limit):
|
||||
return False, set(), False
|
||||
|
||||
return True, leftovers, projection_needed
|
||||
|
||||
|
||||
def _projection_allowed(
|
||||
entry: CachedEntry,
|
||||
query: SemanticQuery,
|
||||
new_dim_keys: frozenset[str],
|
||||
cached_dim_keys: frozenset[str],
|
||||
) -> bool:
|
||||
"""
|
||||
Gates for the projection path (above and beyond filter containment).
|
||||
"""
|
||||
if any(m.aggregation not in ADDITIVE_AGGREGATIONS for m in query.metrics):
|
||||
return False
|
||||
# Cached truncation makes the rollup unsafe (we're missing rows).
|
||||
if entry.limit is not None:
|
||||
return False
|
||||
if entry.group_limit_key:
|
||||
return False
|
||||
# Cached HAVING dropped sub-aggregate rows; the rolled-up totals would be
|
||||
# off. Conservative: skip the projection path when cached has any HAVING.
|
||||
if any(f.type == PredicateType.HAVING for f in entry.filters):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _filter_col_id(f: Filter) -> str | None:
|
||||
return f.column.id if f.column is not None else None
|
||||
|
||||
|
||||
def _order_uses_only(
|
||||
order: list[OrderTuple] | None,
|
||||
allowed_ids: set[str],
|
||||
) -> bool:
|
||||
if not order:
|
||||
return True
|
||||
return all(_orderable_id(element) in allowed_ids for element, _ in order)
|
||||
|
||||
|
||||
def _split(
|
||||
filters: Iterable[Filter],
|
||||
) -> tuple[frozenset[Filter], frozenset[Filter], frozenset[Filter]]:
|
||||
adhoc: set[Filter] = set()
|
||||
having: set[Filter] = set()
|
||||
where: set[Filter] = set()
|
||||
for f in filters:
|
||||
if f.operator == Operator.ADHOC:
|
||||
adhoc.add(f)
|
||||
elif f.type == PredicateType.HAVING:
|
||||
having.add(f)
|
||||
else:
|
||||
where.add(f)
|
||||
return frozenset(adhoc), frozenset(having), frozenset(where)
|
||||
|
||||
|
||||
def _group_by_column(filters: Iterable[Filter]) -> dict[str | None, list[Filter]]:
|
||||
out: dict[str | None, list[Filter]] = {}
|
||||
for f in filters:
|
||||
col_id = f.column.id if f.column is not None else None
|
||||
out.setdefault(col_id, []).append(f)
|
||||
return out
|
||||
|
||||
|
||||
def _projection_ids(query: SemanticQuery) -> set[str]:
|
||||
return {d.id for d in query.dimensions} | {m.id for m in query.metrics}
|
||||
|
||||
|
||||
def _cached_column_ids(entry: CachedEntry, query: SemanticQuery) -> set[str]:
|
||||
"""Column ids available in the cached DataFrame (cached dims + shared metrics)."""
|
||||
cached_dim_ids = {key.rsplit("@", 1)[0] for key in entry.dimension_keys}
|
||||
return cached_dim_ids | {m.id for m in query.metrics}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pairwise implication
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
def _implies(new: Filter, cached: Filter) -> bool: # noqa: C901
|
||||
"""True iff every row matching ``new`` also matches ``cached``.
|
||||
|
||||
Both filters are assumed to be on the same column (caller groups by column).
|
||||
"""
|
||||
if new == cached:
|
||||
return True
|
||||
|
||||
nop, nval = new.operator, new.value
|
||||
cop, cval = cached.operator, cached.value
|
||||
|
||||
if cop == Operator.IS_NULL:
|
||||
if nop == Operator.IS_NULL:
|
||||
return True
|
||||
if nop == Operator.EQUALS and nval is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
if cop == Operator.IS_NOT_NULL:
|
||||
if nop == Operator.IS_NOT_NULL:
|
||||
return True
|
||||
if nop == Operator.EQUALS:
|
||||
return nval is not None
|
||||
if nop in _RANGE_OPS:
|
||||
return True
|
||||
if nop == Operator.IN:
|
||||
return isinstance(nval, frozenset) and all(v is not None for v in nval)
|
||||
return False
|
||||
|
||||
if cop == Operator.EQUALS:
|
||||
if nop == Operator.EQUALS:
|
||||
return nval == cval
|
||||
if nop == Operator.IN and isinstance(nval, frozenset):
|
||||
return nval == frozenset({cval})
|
||||
return False
|
||||
|
||||
if cop == Operator.NOT_EQUALS:
|
||||
if nop == Operator.NOT_EQUALS:
|
||||
return nval == cval
|
||||
if nop == Operator.EQUALS:
|
||||
return nval != cval
|
||||
if nop == Operator.IN and isinstance(nval, frozenset):
|
||||
return cval not in nval
|
||||
return False
|
||||
|
||||
if cop == Operator.IN and isinstance(cval, frozenset):
|
||||
if nop == Operator.IN and isinstance(nval, frozenset):
|
||||
return nval.issubset(cval)
|
||||
if nop == Operator.EQUALS:
|
||||
return nval in cval
|
||||
return False
|
||||
|
||||
if cop == Operator.NOT_IN and isinstance(cval, frozenset):
|
||||
if nop == Operator.NOT_IN and isinstance(nval, frozenset):
|
||||
return cval.issubset(nval)
|
||||
if nop == Operator.NOT_EQUALS:
|
||||
return cval.issubset({nval})
|
||||
if nop == Operator.EQUALS:
|
||||
return nval not in cval
|
||||
if nop == Operator.IN and isinstance(nval, frozenset):
|
||||
return cval.isdisjoint(nval)
|
||||
return False
|
||||
|
||||
if cop in _RANGE_OPS:
|
||||
return _implies_range(nop, nval, cop, cval)
|
||||
|
||||
# LIKE / NOT_LIKE / ADHOC: only the exact-match path at the top.
|
||||
return False
|
||||
|
||||
|
||||
_RANGE_OPS = frozenset(
|
||||
{
|
||||
Operator.GREATER_THAN,
|
||||
Operator.GREATER_THAN_OR_EQUAL,
|
||||
Operator.LESS_THAN,
|
||||
Operator.LESS_THAN_OR_EQUAL,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _implies_range( # noqa: C901
|
||||
nop: Operator,
|
||||
nval: Any,
|
||||
cop: Operator,
|
||||
cval: Any,
|
||||
) -> bool:
|
||||
if isinstance(nval, frozenset):
|
||||
return nop == Operator.IN and all(_scalar_in_range(v, cop, cval) for v in nval)
|
||||
if nop == Operator.EQUALS:
|
||||
return _scalar_in_range(nval, cop, cval)
|
||||
if nop not in _RANGE_OPS:
|
||||
return False
|
||||
if not _comparable(nval, cval):
|
||||
return False
|
||||
|
||||
# Same direction (both upper or both lower bounds) required.
|
||||
cached_is_lower = cop in (Operator.GREATER_THAN, Operator.GREATER_THAN_OR_EQUAL)
|
||||
new_is_lower = nop in (Operator.GREATER_THAN, Operator.GREATER_THAN_OR_EQUAL)
|
||||
if cached_is_lower != new_is_lower:
|
||||
return False
|
||||
|
||||
if cached_is_lower:
|
||||
# cached: a > cval or a >= cval
|
||||
# new: a > nval or a >= nval
|
||||
# need rows(new) ⊆ rows(cached)
|
||||
if cop == Operator.GREATER_THAN and nop == Operator.GREATER_THAN:
|
||||
return nval >= cval
|
||||
if cop == Operator.GREATER_THAN and nop == Operator.GREATER_THAN_OR_EQUAL:
|
||||
return nval > cval
|
||||
if cop == Operator.GREATER_THAN_OR_EQUAL and nop == Operator.GREATER_THAN:
|
||||
return nval >= cval
|
||||
if (
|
||||
cop == Operator.GREATER_THAN_OR_EQUAL
|
||||
and nop == Operator.GREATER_THAN_OR_EQUAL
|
||||
):
|
||||
return nval >= cval
|
||||
return False
|
||||
else:
|
||||
if cop == Operator.LESS_THAN and nop == Operator.LESS_THAN:
|
||||
return nval <= cval
|
||||
if cop == Operator.LESS_THAN and nop == Operator.LESS_THAN_OR_EQUAL:
|
||||
return nval < cval
|
||||
if cop == Operator.LESS_THAN_OR_EQUAL and nop == Operator.LESS_THAN:
|
||||
return nval <= cval
|
||||
if cop == Operator.LESS_THAN_OR_EQUAL and nop == Operator.LESS_THAN_OR_EQUAL:
|
||||
return nval <= cval
|
||||
return False
|
||||
|
||||
|
||||
def _scalar_in_range(value: Any, cop: Operator, cval: Any) -> bool:
|
||||
if not _comparable(value, cval):
|
||||
return False
|
||||
if cop == Operator.GREATER_THAN:
|
||||
return value > cval
|
||||
if cop == Operator.GREATER_THAN_OR_EQUAL:
|
||||
return value >= cval
|
||||
if cop == Operator.LESS_THAN:
|
||||
return value < cval
|
||||
if cop == Operator.LESS_THAN_OR_EQUAL:
|
||||
return value <= cval
|
||||
return False
|
||||
|
||||
|
||||
def _comparable(a: Any, b: Any) -> bool:
|
||||
if a is None or b is None:
|
||||
return False
|
||||
if isinstance(a, bool) or isinstance(b, bool):
|
||||
return isinstance(a, bool) and isinstance(b, bool)
|
||||
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
||||
return True
|
||||
if isinstance(a, str) and isinstance(b, str):
|
||||
return True
|
||||
if isinstance(a, (datetime, date, time)) and isinstance(b, type(a)):
|
||||
return True
|
||||
if isinstance(a, type(b)) and isinstance(a, (datetime, date, time, timedelta)):
|
||||
return True
|
||||
return type(a) == type(b) # noqa: E721
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post-processing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _apply_post_processing(
|
||||
cached: SemanticResult,
|
||||
query: SemanticQuery,
|
||||
leftovers: set[Filter],
|
||||
projection_needed: bool,
|
||||
) -> SemanticResult:
|
||||
"""Apply leftover filters, projection (re-aggregation), order, and limit."""
|
||||
if not leftovers and not projection_needed and query.limit is None:
|
||||
return cached
|
||||
|
||||
df = cached.results.to_pandas()
|
||||
if leftovers:
|
||||
mask = pd.Series(True, index=df.index)
|
||||
for f in leftovers:
|
||||
mask &= _mask_for(df, f)
|
||||
df = df[mask]
|
||||
|
||||
note_def = "Served from semantic view smart cache (post-processed locally)"
|
||||
if projection_needed:
|
||||
groupby = [d.name for d in query.dimensions]
|
||||
aggregates = {
|
||||
m.name: {
|
||||
"column": m.name,
|
||||
"operator": _AGGREGATION_TO_PANDAS[
|
||||
# Guarded by ``_projection_allowed`` — non-None and additive.
|
||||
m.aggregation # type: ignore[index]
|
||||
],
|
||||
}
|
||||
for m in query.metrics
|
||||
}
|
||||
df = aggregate(df, groupby=groupby, aggregates=aggregates)
|
||||
df = _apply_order(df, query.order)
|
||||
note_def = "Served from semantic view smart cache (re-aggregated locally)"
|
||||
|
||||
if query.limit is not None:
|
||||
df = df.head(query.limit)
|
||||
|
||||
table = pa.Table.from_pandas(df, preserve_index=False)
|
||||
note = SemanticRequest(type="cache", definition=note_def)
|
||||
return SemanticResult(requests=list(cached.requests) + [note], results=table)
|
||||
|
||||
|
||||
def _apply_order(
|
||||
df: pd.DataFrame,
|
||||
order: list[OrderTuple] | None,
|
||||
) -> pd.DataFrame:
|
||||
if not order:
|
||||
return df
|
||||
available: list[tuple[str, bool]] = []
|
||||
for element, direction in order:
|
||||
col = _orderable_id_name(element)
|
||||
if col in df.columns:
|
||||
available.append((col, direction == OrderDirection.ASC))
|
||||
if not available:
|
||||
return df
|
||||
cols = [col for col, _ in available]
|
||||
asc = [a for _, a in available]
|
||||
return df.sort_values(by=cols, ascending=asc).reset_index(drop=True)
|
||||
|
||||
|
||||
def _orderable_id_name(element: Metric | Dimension | AdhocExpression) -> str:
|
||||
return getattr(element, "name", element.id)
|
||||
|
||||
|
||||
def _mask_for(df: pd.DataFrame, f: Filter) -> pd.Series: # noqa: C901
|
||||
if f.column is None:
|
||||
return pd.Series(True, index=df.index)
|
||||
series = df[f.column.name]
|
||||
op = f.operator
|
||||
val = f.value
|
||||
if op == Operator.EQUALS:
|
||||
return series == val if val is not None else series.isna()
|
||||
if op == Operator.NOT_EQUALS:
|
||||
return series != val if val is not None else series.notna()
|
||||
if op == Operator.GREATER_THAN:
|
||||
return series > val
|
||||
if op == Operator.GREATER_THAN_OR_EQUAL:
|
||||
return series >= val
|
||||
if op == Operator.LESS_THAN:
|
||||
return series < val
|
||||
if op == Operator.LESS_THAN_OR_EQUAL:
|
||||
return series <= val
|
||||
if op == Operator.IN:
|
||||
return series.isin(list(val) if isinstance(val, frozenset) else [val])
|
||||
if op == Operator.NOT_IN:
|
||||
return ~series.isin(list(val) if isinstance(val, frozenset) else [val])
|
||||
if op == Operator.IS_NULL:
|
||||
return series.isna()
|
||||
if op == Operator.IS_NOT_NULL:
|
||||
return series.notna()
|
||||
if op == Operator.LIKE:
|
||||
return series.astype(str).str.match(_sql_like_to_regex(str(val)))
|
||||
if op == Operator.NOT_LIKE:
|
||||
return ~series.astype(str).str.match(_sql_like_to_regex(str(val)))
|
||||
return pd.Series(True, index=df.index)
|
||||
|
||||
|
||||
def _sql_like_to_regex(pattern: str) -> str:
|
||||
out = []
|
||||
for ch in pattern:
|
||||
if ch == "%":
|
||||
out.append(".*")
|
||||
elif ch == "_":
|
||||
out.append(".")
|
||||
else:
|
||||
out.append(re.escape(ch))
|
||||
return f"^{''.join(out)}$"
|
||||
@@ -26,7 +26,7 @@ single dataframe.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from time import time
|
||||
from typing import Any, cast, Sequence, TypeGuard
|
||||
from typing import Any, Callable, cast, Sequence, TypeGuard
|
||||
|
||||
import isodate
|
||||
import numpy as np
|
||||
@@ -55,6 +55,11 @@ from superset.common.utils.time_range_utils import get_since_until_from_query_ob
|
||||
from superset.connectors.sqla.models import BaseDatasource
|
||||
from superset.constants import NO_TIME_RANGE
|
||||
from superset.models.helpers import QueryResult
|
||||
from superset.semantic_layers.cache import (
|
||||
store_result,
|
||||
try_serve_from_cache,
|
||||
ViewMeta,
|
||||
)
|
||||
from superset.superset_typing import AdhocColumn
|
||||
from superset.utils.core import (
|
||||
FilterOperator,
|
||||
@@ -112,13 +117,15 @@ def get_results(query_object: QueryObject) -> QueryResult:
|
||||
else semantic_view.get_table
|
||||
)
|
||||
|
||||
cached_dispatch = _make_cached_dispatch(query_object, dispatcher)
|
||||
|
||||
# Step 1: Convert QueryObject to list of SemanticQuery objects
|
||||
# The first query is the main query, subsequent queries are for time offsets
|
||||
queries = map_query_object(query_object)
|
||||
|
||||
# Step 2: Execute the main query (first in the list)
|
||||
main_query = queries[0]
|
||||
main_result = dispatcher(main_query)
|
||||
main_result = cached_dispatch(main_query)
|
||||
|
||||
main_df = main_result.results.to_pandas()
|
||||
|
||||
@@ -149,7 +156,7 @@ def get_results(query_object: QueryObject) -> QueryResult:
|
||||
strict=False,
|
||||
):
|
||||
# Execute the offset query
|
||||
result = dispatcher(offset_query)
|
||||
result = cached_dispatch(offset_query)
|
||||
|
||||
# Add this query's requests to the collection
|
||||
all_requests.extend(result.requests)
|
||||
@@ -205,6 +212,37 @@ def get_results(query_object: QueryObject) -> QueryResult:
|
||||
)
|
||||
|
||||
|
||||
def _make_cached_dispatch(
|
||||
query_object: ValidatedQueryObject,
|
||||
dispatcher: Callable[[SemanticQuery], SemanticResult],
|
||||
) -> Callable[[SemanticQuery], SemanticResult]:
|
||||
"""
|
||||
Wrap the semantic view dispatcher with a containment-aware cache.
|
||||
|
||||
Row-count queries bypass the cache. Cache failures are logged and the
|
||||
dispatcher is called as if the cache were absent.
|
||||
"""
|
||||
if query_object.is_rowcount:
|
||||
return dispatcher
|
||||
|
||||
view = query_object.datasource
|
||||
changed_on = getattr(view, "changed_on", None)
|
||||
view_meta = ViewMeta(
|
||||
uuid=str(view.uuid),
|
||||
changed_on_iso=changed_on.isoformat() if changed_on else "",
|
||||
cache_timeout=getattr(view, "cache_timeout", None),
|
||||
)
|
||||
|
||||
def cached_dispatch(query: SemanticQuery) -> SemanticResult:
|
||||
if (hit := try_serve_from_cache(view_meta, query)) is not None:
|
||||
return hit
|
||||
result = dispatcher(query)
|
||||
store_result(view_meta, query, result)
|
||||
return result
|
||||
|
||||
return cached_dispatch
|
||||
|
||||
|
||||
def map_semantic_result_to_query_result(
|
||||
semantic_result: SemanticResult,
|
||||
query_object: ValidatedQueryObject,
|
||||
|
||||
@@ -99,14 +99,29 @@ class SecretsMigrator:
|
||||
|
||||
def discover_encrypted_fields(self) -> dict[str, dict[str, EncryptedType]]:
|
||||
"""
|
||||
Iterates over SqlAlchemy's metadata, looking for EncryptedType
|
||||
columns along the way. Builds up a dict of
|
||||
Iterates over ORM-mapped tables, looking for EncryptedType columns
|
||||
along the way. Builds up a dict of
|
||||
table_name -> dict of col_name: enc type instance
|
||||
:return:
|
||||
|
||||
Superset's ORM models inherit from Flask-AppBuilder's declarative base
|
||||
(`flask_appbuilder.Model`), whose MetaData is distinct from
|
||||
`db.metadata`. We combine both sources so encrypted columns are found
|
||||
regardless of which base a model uses. FAB's metadata takes precedence
|
||||
when a table name appears in both registries.
|
||||
|
||||
:return: mapping of table name to a dict of {column name: EncryptedType}
|
||||
"""
|
||||
from flask_appbuilder import ( # pylint: disable=import-outside-toplevel
|
||||
Model as FABModel,
|
||||
)
|
||||
|
||||
meta_info: dict[str, Any] = {}
|
||||
|
||||
tables: dict[str, Any] = dict(FABModel.metadata.tables)
|
||||
for table_name, table in self._db.metadata.tables.items():
|
||||
tables.setdefault(table_name, table)
|
||||
|
||||
for table_name, table in tables.items():
|
||||
for col_name, col in table.columns.items():
|
||||
if isinstance(col.type, EncryptedType):
|
||||
cols = meta_info.get(table_name, {})
|
||||
|
||||
@@ -89,6 +89,21 @@ class EncryptedFieldTest(SupersetTestCase):
|
||||
" encrypted_field_factory"
|
||||
)
|
||||
|
||||
def test_discover_encrypted_fields_finds_dbs_table(self):
|
||||
"""
|
||||
Ensure discover_encrypted_fields finds the encrypted columns on the
|
||||
dbs table (password, encrypted_extra, server_cert). This guards
|
||||
against db.metadata diverging from db.Model.metadata.
|
||||
"""
|
||||
migrator = SecretsMigrator("")
|
||||
encrypted_fields = migrator.discover_encrypted_fields()
|
||||
assert "dbs" in encrypted_fields, (
|
||||
"dbs table not found in encrypted fields — "
|
||||
"discover_encrypted_fields may be using the wrong MetaData instance"
|
||||
)
|
||||
dbs_cols = set(encrypted_fields["dbs"].keys())
|
||||
assert {"password", "encrypted_extra", "server_cert"}.issubset(dbs_cols)
|
||||
|
||||
def test_lazy_key_resolution(self):
|
||||
"""
|
||||
Verify that the encryption key is resolved lazily at runtime,
|
||||
|
||||
@@ -298,7 +298,8 @@ class TestOpenSqlLabWithContext:
|
||||
field_path=("title",),
|
||||
)
|
||||
assert response.error == sanitize_for_llm_context(
|
||||
"Database with ID 404 not found",
|
||||
"Database with ID 404 not found."
|
||||
" Use list_databases to get valid database IDs.",
|
||||
field_path=("error",),
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -409,6 +409,50 @@ def test_mcp_auth_hook_removes_stale_db_session_in_sync_wrapper(app) -> None:
|
||||
assert result == "fresh"
|
||||
|
||||
|
||||
def test_sync_wrapper_handles_ssl_error_on_pre_call_remove(app) -> None:
|
||||
"""sync_wrapper tolerates OperationalError from db.session.remove() before the call.
|
||||
|
||||
If the underlying DBAPI connection died between requests (e.g. RDS SSL
|
||||
idle-timeout), the rollback implicit in session.close() raises
|
||||
OperationalError. _remove_session_safe() should:
|
||||
- Log a warning
|
||||
- Call session.invalidate() to mark the dead connection for pool discard
|
||||
- Retry session.remove() so the registry is clean
|
||||
- Allow the tool to run successfully
|
||||
"""
|
||||
from sqlalchemy.exc import OperationalError as SAOperationalError
|
||||
|
||||
fresh_user = _make_mock_user("fresh")
|
||||
|
||||
def dummy_tool() -> str:
|
||||
"""Dummy sync tool."""
|
||||
return g.user.username
|
||||
|
||||
wrapped = mcp_auth_hook(dummy_tool)
|
||||
|
||||
with app.test_request_context():
|
||||
g.user = fresh_user
|
||||
with patch("superset.extensions.db") as mock_db:
|
||||
mock_db.session.remove.side_effect = [
|
||||
SAOperationalError(
|
||||
"SSL connection has been closed unexpectedly", None, None
|
||||
),
|
||||
None, # second call succeeds
|
||||
]
|
||||
|
||||
with patch(
|
||||
"superset.mcp_service.auth.get_user_from_request",
|
||||
return_value=fresh_user,
|
||||
):
|
||||
result = wrapped()
|
||||
|
||||
assert result == "fresh"
|
||||
assert mock_db.session.invalidate.called, "invalidate() must be called on SSL error"
|
||||
assert mock_db.session.remove.call_count == 2, (
|
||||
"remove() must be retried after SSL error"
|
||||
)
|
||||
|
||||
|
||||
# -- default_user_resolver --
|
||||
|
||||
|
||||
|
||||
295
tests/unit_tests/semantic_layers/cache_integration_test.py
Normal file
295
tests/unit_tests/semantic_layers/cache_integration_test.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""End-to-end test that exercises ``mapper.get_results`` with a live cache."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from superset_core.semantic_layers.types import (
|
||||
AggregationType,
|
||||
Dimension,
|
||||
Metric,
|
||||
SemanticRequest,
|
||||
SemanticResult,
|
||||
)
|
||||
|
||||
from superset.semantic_layers import cache as cache_module
|
||||
from superset.semantic_layers.mapper import get_results, ValidatedQueryObject
|
||||
|
||||
|
||||
class _InMemoryCache:
|
||||
"""Minimal flask-caching compatible cache used to isolate tests."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, Any] = {}
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
return self._store.get(key)
|
||||
|
||||
def set(self, key: str, value: Any, timeout: int | None = None) -> bool:
|
||||
self._store[key] = value
|
||||
return True
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
return self._store.pop(key, None) is not None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_cache(mocker: MockerFixture) -> _InMemoryCache:
|
||||
fake = _InMemoryCache()
|
||||
mocker.patch.object(
|
||||
type(cache_module.cache_manager),
|
||||
"data_cache",
|
||||
property(lambda self: fake),
|
||||
)
|
||||
return fake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def view_implementation() -> Any:
|
||||
"""SemanticView implementation stub with one metric and one dimension."""
|
||||
dim_a = Dimension(id="t.a", name="a", type=pa.int64())
|
||||
metric_x = Metric(id="t.x", name="x", type=pa.float64(), definition="sum(x)")
|
||||
|
||||
impl = MagicMock()
|
||||
impl.metrics = {metric_x}
|
||||
impl.dimensions = {dim_a}
|
||||
impl.features = frozenset()
|
||||
impl.get_metrics = MagicMock(return_value={metric_x})
|
||||
impl.get_dimensions = MagicMock(return_value={dim_a})
|
||||
return impl
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def datasource(view_implementation: Any) -> MagicMock:
|
||||
ds = MagicMock()
|
||||
ds.implementation = view_implementation
|
||||
ds.uuid = "view-uuid-stable"
|
||||
ds.changed_on = datetime(2026, 1, 1, 12, 0, 0)
|
||||
ds.cache_timeout = 60
|
||||
ds.fetch_values_predicate = None
|
||||
return ds
|
||||
|
||||
|
||||
def _result(rows: list[tuple[int, float]]) -> SemanticResult:
|
||||
df = pd.DataFrame(rows, columns=["a", "x"])
|
||||
return SemanticResult(
|
||||
requests=[SemanticRequest(type="SQL", definition="select a, x")],
|
||||
results=pa.Table.from_pandas(df, preserve_index=False),
|
||||
)
|
||||
|
||||
|
||||
def _qo(
|
||||
datasource: MagicMock,
|
||||
filter_op: str | None = None,
|
||||
filter_val: Any = None,
|
||||
limit: int | None = None,
|
||||
) -> ValidatedQueryObject:
|
||||
qo_filters: list[dict[str, Any]] = (
|
||||
[{"col": "a", "op": filter_op, "val": filter_val}] if filter_op else []
|
||||
)
|
||||
return ValidatedQueryObject(
|
||||
datasource=datasource,
|
||||
metrics=["x"],
|
||||
columns=["a"],
|
||||
filters=qo_filters, # type: ignore[arg-type]
|
||||
row_limit=limit,
|
||||
)
|
||||
|
||||
|
||||
def test_narrower_filter_reuses_cache(
|
||||
fake_cache: _InMemoryCache,
|
||||
view_implementation: Any,
|
||||
datasource: MagicMock,
|
||||
) -> None:
|
||||
# The dispatcher returns rows already filtered by `a > 1` (in production it
|
||||
# would; here we hand-feed the result). The second query (a > 2) is a subset
|
||||
# and must be served from the cached DataFrame.
|
||||
cached = _result([(2, 2.0), (3, 3.0), (5, 5.0)])
|
||||
view_implementation.get_table = MagicMock(return_value=cached)
|
||||
|
||||
first = get_results(_qo(datasource, ">", 1))
|
||||
assert view_implementation.get_table.call_count == 1
|
||||
assert sorted(first.df["a"].tolist()) == [2, 3, 5]
|
||||
|
||||
second = get_results(_qo(datasource, ">", 2))
|
||||
assert view_implementation.get_table.call_count == 1 # cache hit
|
||||
assert sorted(second.df["a"].tolist()) == [3, 5]
|
||||
|
||||
|
||||
def test_smaller_limit_reuses_cache(
|
||||
fake_cache: _InMemoryCache,
|
||||
view_implementation: Any,
|
||||
datasource: MagicMock,
|
||||
) -> None:
|
||||
# First call has no limit; second asks for 2 rows — should be served from cache.
|
||||
full = _result([(0, 1.0), (1, 2.0), (2, 3.0), (3, 4.0)])
|
||||
view_implementation.get_table = MagicMock(return_value=full)
|
||||
|
||||
get_results(_qo(datasource, limit=None))
|
||||
assert view_implementation.get_table.call_count == 1
|
||||
|
||||
result = get_results(_qo(datasource, limit=2))
|
||||
assert view_implementation.get_table.call_count == 1 # cache hit
|
||||
assert len(result.df) == 2
|
||||
|
||||
|
||||
def test_broader_filter_misses_cache(
|
||||
fake_cache: _InMemoryCache,
|
||||
view_implementation: Any,
|
||||
datasource: MagicMock,
|
||||
) -> None:
|
||||
view_implementation.get_table = MagicMock(
|
||||
side_effect=[
|
||||
_result([(2, 1.0), (3, 2.0)]),
|
||||
_result([(0, 1.0), (2, 2.0), (3, 3.0)]),
|
||||
]
|
||||
)
|
||||
|
||||
get_results(_qo(datasource, ">", 1))
|
||||
assert view_implementation.get_table.call_count == 1
|
||||
|
||||
# Broader filter — must re-execute.
|
||||
get_results(_qo(datasource, ">", 0))
|
||||
assert view_implementation.get_table.call_count == 2
|
||||
|
||||
|
||||
def test_changed_on_invalidates_cache(
|
||||
fake_cache: _InMemoryCache,
|
||||
view_implementation: Any,
|
||||
datasource: MagicMock,
|
||||
) -> None:
|
||||
view_implementation.get_table = MagicMock(return_value=_result([(2, 1.0)]))
|
||||
|
||||
get_results(_qo(datasource, ">", 1))
|
||||
assert view_implementation.get_table.call_count == 1
|
||||
|
||||
# Bumping changed_on yields a different shape key — cache misses.
|
||||
datasource.changed_on = datetime(2026, 2, 1, 0, 0, 0)
|
||||
get_results(_qo(datasource, ">", 1))
|
||||
assert view_implementation.get_table.call_count == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Projection (v2) — dropping a dimension and re-aggregating
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_view(metric_aggregation: AggregationType | None) -> tuple[Any, MagicMock]:
|
||||
dim_b = Dimension(id="t.b", name="b", type=pa.utf8())
|
||||
dim_c = Dimension(id="t.c", name="c", type=pa.utf8())
|
||||
metric_x = Metric(
|
||||
id="t.x",
|
||||
name="x",
|
||||
type=pa.float64(),
|
||||
definition="sum(x)",
|
||||
aggregation=metric_aggregation,
|
||||
)
|
||||
impl = MagicMock()
|
||||
impl.metrics = {metric_x}
|
||||
impl.dimensions = {dim_b, dim_c}
|
||||
impl.features = frozenset()
|
||||
impl.get_metrics = MagicMock(return_value={metric_x})
|
||||
impl.get_dimensions = MagicMock(return_value={dim_b, dim_c})
|
||||
|
||||
ds = MagicMock()
|
||||
ds.implementation = impl
|
||||
ds.uuid = "proj-view"
|
||||
ds.changed_on = datetime(2026, 3, 1, 0, 0, 0)
|
||||
ds.cache_timeout = 60
|
||||
ds.fetch_values_predicate = None
|
||||
return impl, ds
|
||||
|
||||
|
||||
def _qo_dims(ds: MagicMock, columns: list[str]) -> ValidatedQueryObject:
|
||||
return ValidatedQueryObject(
|
||||
datasource=ds,
|
||||
metrics=["x"],
|
||||
columns=columns, # type: ignore[arg-type]
|
||||
filters=[],
|
||||
)
|
||||
|
||||
|
||||
def _result_bc(rows: list[tuple[str, str, float]]) -> SemanticResult:
|
||||
df = pd.DataFrame(rows, columns=["b", "c", "x"])
|
||||
return SemanticResult(
|
||||
requests=[SemanticRequest(type="SQL", definition="select b,c,sum(x)")],
|
||||
results=pa.Table.from_pandas(df, preserve_index=False),
|
||||
)
|
||||
|
||||
|
||||
def test_projection_reuses_cached_for_dropped_dim(
|
||||
fake_cache: _InMemoryCache,
|
||||
) -> None:
|
||||
impl, ds = _make_view(AggregationType.SUM)
|
||||
impl.get_table = MagicMock(
|
||||
return_value=_result_bc(
|
||||
[("b1", "c1", 5.0), ("b1", "c2", 3.0), ("b2", "c1", 4.0)]
|
||||
)
|
||||
)
|
||||
|
||||
first = get_results(_qo_dims(ds, ["b", "c"]))
|
||||
assert impl.get_table.call_count == 1
|
||||
assert len(first.df) == 3
|
||||
|
||||
second = get_results(_qo_dims(ds, ["b"]))
|
||||
assert impl.get_table.call_count == 1 # served via projection
|
||||
df = second.df.sort_values("b").reset_index(drop=True)
|
||||
assert df["b"].tolist() == ["b1", "b2"]
|
||||
assert df["x"].tolist() == [8.0, 4.0]
|
||||
|
||||
|
||||
def test_projection_skipped_when_aggregation_unknown(
|
||||
fake_cache: _InMemoryCache,
|
||||
) -> None:
|
||||
impl, ds = _make_view(None) # metric has no aggregation declared
|
||||
impl.get_table = MagicMock(
|
||||
side_effect=[
|
||||
_result_bc([("b1", "c1", 5.0), ("b1", "c2", 3.0)]),
|
||||
_result_bc([("b1", "c1", 5.0)]), # what the SV would compute for [b]
|
||||
]
|
||||
)
|
||||
|
||||
get_results(_qo_dims(ds, ["b", "c"]))
|
||||
assert impl.get_table.call_count == 1
|
||||
|
||||
get_results(_qo_dims(ds, ["b"]))
|
||||
assert impl.get_table.call_count == 2 # cannot project, re-executed
|
||||
|
||||
|
||||
def test_projection_skipped_for_avg(
|
||||
fake_cache: _InMemoryCache,
|
||||
) -> None:
|
||||
impl, ds = _make_view(AggregationType.AVG)
|
||||
impl.get_table = MagicMock(
|
||||
side_effect=[
|
||||
_result_bc([("b1", "c1", 5.0), ("b1", "c2", 3.0)]),
|
||||
_result_bc([("b1", "c1", 4.0)]),
|
||||
]
|
||||
)
|
||||
|
||||
get_results(_qo_dims(ds, ["b", "c"]))
|
||||
get_results(_qo_dims(ds, ["b"]))
|
||||
assert impl.get_table.call_count == 2
|
||||
671
tests/unit_tests/semantic_layers/cache_test.py
Normal file
671
tests/unit_tests/semantic_layers/cache_test.py
Normal file
@@ -0,0 +1,671 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pytest
|
||||
from superset_core.semantic_layers.types import (
|
||||
AggregationType,
|
||||
Dimension,
|
||||
Filter,
|
||||
Metric,
|
||||
Operator,
|
||||
OrderDirection,
|
||||
PredicateType,
|
||||
SemanticQuery,
|
||||
SemanticRequest,
|
||||
SemanticResult,
|
||||
)
|
||||
|
||||
from superset.semantic_layers.cache import (
|
||||
_apply_post_processing,
|
||||
_implies,
|
||||
CachedEntry,
|
||||
can_satisfy,
|
||||
shape_key,
|
||||
value_key,
|
||||
ViewMeta,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def dim(id_: str, name: str | None = None) -> Dimension:
|
||||
return Dimension(id=id_, name=name or id_, type=pa.utf8())
|
||||
|
||||
|
||||
def met(
|
||||
id_: str,
|
||||
name: str | None = None,
|
||||
aggregation: AggregationType | None = None,
|
||||
) -> Metric:
|
||||
return Metric(
|
||||
id=id_,
|
||||
name=name or id_,
|
||||
type=pa.float64(),
|
||||
definition="x",
|
||||
aggregation=aggregation,
|
||||
)
|
||||
|
||||
|
||||
COL_A = dim("col.a", "a")
|
||||
COL_B = dim("col.b", "b")
|
||||
M_X = met("met.x", "x")
|
||||
M_Y = met("met.y", "y")
|
||||
|
||||
VIEW = ViewMeta(uuid="view-1", changed_on_iso="2026-05-01T00:00:00", cache_timeout=None)
|
||||
|
||||
|
||||
def where(column: Dimension | Metric | None, op: Operator, value: Any) -> Filter:
|
||||
return Filter(type=PredicateType.WHERE, column=column, operator=op, value=value)
|
||||
|
||||
|
||||
def having(column: Metric, op: Operator, value: Any) -> Filter:
|
||||
return Filter(type=PredicateType.HAVING, column=column, operator=op, value=value)
|
||||
|
||||
|
||||
def adhoc(definition: str, type_: PredicateType = PredicateType.WHERE) -> Filter:
|
||||
return Filter(type=type_, column=None, operator=Operator.ADHOC, value=definition)
|
||||
|
||||
|
||||
def query(
|
||||
filters: set[Filter] | None = None,
|
||||
limit: int | None = None,
|
||||
order: Any = None,
|
||||
dimensions: list[Dimension] | None = None,
|
||||
metrics: list[Metric] | None = None,
|
||||
) -> SemanticQuery:
|
||||
return SemanticQuery(
|
||||
metrics=metrics if metrics is not None else [M_X],
|
||||
dimensions=dimensions if dimensions is not None else [COL_A, COL_B],
|
||||
filters=filters,
|
||||
order=order,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
def entry_from(q: SemanticQuery, value_key_: str = "vk") -> CachedEntry:
|
||||
from superset.semantic_layers.cache import (
|
||||
_dimension_key,
|
||||
_group_limit_key,
|
||||
_order_key,
|
||||
)
|
||||
|
||||
return CachedEntry(
|
||||
filters=frozenset(q.filters or set()),
|
||||
dimension_keys=frozenset(_dimension_key(d) for d in q.dimensions),
|
||||
limit=q.limit,
|
||||
offset=q.offset or 0,
|
||||
order_key=_order_key(q.order),
|
||||
group_limit_key=_group_limit_key(q.group_limit),
|
||||
value_key=value_key_,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _implies: scalar range pairs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"new_op,new_val,cached_op,cached_val,expected",
|
||||
[
|
||||
# narrower lower bound
|
||||
(Operator.GREATER_THAN, 20, Operator.GREATER_THAN, 10, True),
|
||||
(Operator.GREATER_THAN, 10, Operator.GREATER_THAN, 20, False),
|
||||
(Operator.GREATER_THAN_OR_EQUAL, 11, Operator.GREATER_THAN, 10, True),
|
||||
(Operator.GREATER_THAN_OR_EQUAL, 10, Operator.GREATER_THAN, 10, False),
|
||||
(Operator.GREATER_THAN, 10, Operator.GREATER_THAN_OR_EQUAL, 10, True),
|
||||
(Operator.GREATER_THAN, 9, Operator.GREATER_THAN_OR_EQUAL, 10, False),
|
||||
# narrower upper bound
|
||||
(Operator.LESS_THAN, 5, Operator.LESS_THAN, 10, True),
|
||||
(Operator.LESS_THAN_OR_EQUAL, 9, Operator.LESS_THAN, 10, True),
|
||||
(Operator.LESS_THAN_OR_EQUAL, 10, Operator.LESS_THAN, 10, False),
|
||||
# cross-direction — never implies
|
||||
(Operator.LESS_THAN, 5, Operator.GREATER_THAN, 10, False),
|
||||
(Operator.GREATER_THAN, 5, Operator.LESS_THAN, 10, False),
|
||||
# equals fits in range
|
||||
(Operator.EQUALS, 15, Operator.GREATER_THAN, 10, True),
|
||||
(Operator.EQUALS, 10, Operator.GREATER_THAN, 10, False),
|
||||
(Operator.EQUALS, 10, Operator.GREATER_THAN_OR_EQUAL, 10, True),
|
||||
],
|
||||
)
|
||||
def test_implies_range(
|
||||
new_op: Operator,
|
||||
new_val: Any,
|
||||
cached_op: Operator,
|
||||
cached_val: Any,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
assert (
|
||||
_implies(where(COL_A, new_op, new_val), where(COL_A, cached_op, cached_val))
|
||||
is expected
|
||||
)
|
||||
|
||||
|
||||
def test_implies_in_subset() -> None:
|
||||
cached = where(COL_A, Operator.IN, frozenset({"a", "b", "c"}))
|
||||
assert _implies(where(COL_A, Operator.IN, frozenset({"a", "b"})), cached) is True
|
||||
assert _implies(where(COL_A, Operator.IN, frozenset({"a", "d"})), cached) is False
|
||||
# equals to a value in the cached IN set
|
||||
assert _implies(where(COL_A, Operator.EQUALS, "b"), cached) is True
|
||||
assert _implies(where(COL_A, Operator.EQUALS, "z"), cached) is False
|
||||
|
||||
|
||||
def test_implies_in_all_in_range() -> None:
|
||||
cached = where(COL_A, Operator.GREATER_THAN, 10)
|
||||
assert _implies(where(COL_A, Operator.IN, frozenset({11, 12})), cached) is True
|
||||
assert _implies(where(COL_A, Operator.IN, frozenset({10, 12})), cached) is False
|
||||
|
||||
|
||||
def test_implies_equals_exact() -> None:
|
||||
cached = where(COL_A, Operator.EQUALS, 5)
|
||||
assert _implies(where(COL_A, Operator.EQUALS, 5), cached) is True
|
||||
assert _implies(where(COL_A, Operator.EQUALS, 6), cached) is False
|
||||
|
||||
|
||||
def test_implies_is_not_null() -> None:
|
||||
cached = where(COL_A, Operator.IS_NOT_NULL, None)
|
||||
assert _implies(where(COL_A, Operator.GREATER_THAN, 0), cached) is True
|
||||
assert _implies(where(COL_A, Operator.IS_NOT_NULL, None), cached) is True
|
||||
assert _implies(where(COL_A, Operator.IS_NULL, None), cached) is False
|
||||
|
||||
|
||||
def test_implies_like_exact_match_only() -> None:
|
||||
a = where(COL_A, Operator.LIKE, "foo%")
|
||||
b = where(COL_A, Operator.LIKE, "foo%")
|
||||
c = where(COL_A, Operator.LIKE, "bar%")
|
||||
assert _implies(a, b) is True
|
||||
assert _implies(c, b) is False
|
||||
assert _implies(where(COL_A, Operator.EQUALS, "fooz"), b) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# can_satisfy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_can_satisfy_empty_cached_returns_all_as_leftovers() -> None:
|
||||
cached_q = query(filters=None)
|
||||
new_q = query(filters={where(COL_A, Operator.GREATER_THAN, 5)})
|
||||
ok, leftovers, projection = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is True
|
||||
assert projection is False
|
||||
assert leftovers == {where(COL_A, Operator.GREATER_THAN, 5)}
|
||||
|
||||
|
||||
def test_can_satisfy_narrower_filter() -> None:
|
||||
cached_q = query(filters={where(COL_A, Operator.GREATER_THAN, 1)})
|
||||
new_q = query(filters={where(COL_A, Operator.GREATER_THAN, 2)})
|
||||
ok, leftovers, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is True
|
||||
assert leftovers == {where(COL_A, Operator.GREATER_THAN, 2)}
|
||||
|
||||
|
||||
def test_can_satisfy_broader_filter_fails() -> None:
|
||||
cached_q = query(filters={where(COL_A, Operator.GREATER_THAN, 2)})
|
||||
new_q = query(filters={where(COL_A, Operator.GREATER_THAN, 1)})
|
||||
ok, leftovers, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
assert leftovers == set()
|
||||
|
||||
|
||||
def test_can_satisfy_missing_constraint_fails() -> None:
|
||||
cached_q = query(filters={where(COL_A, Operator.GREATER_THAN, 1)})
|
||||
new_q = query(filters=None)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_can_satisfy_new_filter_on_extra_column() -> None:
|
||||
cached_q = query(filters={where(COL_A, Operator.GREATER_THAN, 1)})
|
||||
new_q = query(
|
||||
filters={
|
||||
where(COL_A, Operator.GREATER_THAN, 2),
|
||||
where(COL_B, Operator.EQUALS, "x"),
|
||||
}
|
||||
)
|
||||
ok, leftovers, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is True
|
||||
assert leftovers == {
|
||||
where(COL_A, Operator.GREATER_THAN, 2),
|
||||
where(COL_B, Operator.EQUALS, "x"),
|
||||
}
|
||||
|
||||
|
||||
def test_can_satisfy_leftover_on_non_projected_column_fails() -> None:
|
||||
other = dim("col.other", "other")
|
||||
cached_q = query(filters=None)
|
||||
new_q = query(
|
||||
filters={where(other, Operator.EQUALS, "x")},
|
||||
dimensions=[COL_A, COL_B],
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_can_satisfy_having_requires_exact_set() -> None:
|
||||
cached_q = query(filters={having(M_X, Operator.GREATER_THAN, 100)})
|
||||
same = query(filters={having(M_X, Operator.GREATER_THAN, 100)})
|
||||
tighter = query(filters={having(M_X, Operator.GREATER_THAN, 200)})
|
||||
ok_same, _, _ = can_satisfy(entry_from(cached_q), same)
|
||||
ok_tight, _, _ = can_satisfy(entry_from(cached_q), tighter)
|
||||
assert ok_same is True
|
||||
assert ok_tight is False
|
||||
|
||||
|
||||
def test_can_satisfy_adhoc_requires_exact_set() -> None:
|
||||
cached_q = query(filters={adhoc("col_a > 1")})
|
||||
same = query(filters={adhoc("col_a > 1")})
|
||||
different = query(filters={adhoc("col_a > 2")})
|
||||
ok_same, _, _ = can_satisfy(entry_from(cached_q), same)
|
||||
ok_diff, _, _ = can_satisfy(entry_from(cached_q), different)
|
||||
assert ok_same is True
|
||||
assert ok_diff is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Limit / order / offset
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_can_satisfy_unlimited_cached_satisfies_any_limit() -> None:
|
||||
cached_q = query(filters=None, limit=None)
|
||||
new_q = query(filters=None, limit=10)
|
||||
ok, leftovers, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is True
|
||||
assert leftovers == set()
|
||||
|
||||
|
||||
def test_can_satisfy_smaller_limit_with_matching_order() -> None:
|
||||
order = [(M_X, OrderDirection.DESC)]
|
||||
cached_q = query(filters=None, limit=100, order=order)
|
||||
new_q = query(filters=None, limit=10, order=order)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is True
|
||||
|
||||
|
||||
def test_can_satisfy_smaller_limit_different_order_fails() -> None:
|
||||
cached_q = query(filters=None, limit=100, order=[(M_X, OrderDirection.DESC)])
|
||||
new_q = query(filters=None, limit=10, order=[(M_X, OrderDirection.ASC)])
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_can_satisfy_larger_limit_fails() -> None:
|
||||
cached_q = query(filters=None, limit=10)
|
||||
new_q = query(filters=None, limit=100)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_can_satisfy_no_new_limit_when_cached_has_one_fails() -> None:
|
||||
cached_q = query(filters=None, limit=100)
|
||||
new_q = query(filters=None, limit=None)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_can_satisfy_offset_never_reused() -> None:
|
||||
cached_q = SemanticQuery(metrics=[M_X], dimensions=[COL_A], offset=5)
|
||||
new_q = SemanticQuery(metrics=[M_X], dimensions=[COL_A], offset=5)
|
||||
ok, _, _ = can_satisfy(entry_from(cached_q), new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post-processing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_apply_post_processing_filters_and_limits() -> None:
|
||||
df = pd.DataFrame({"a": [1, 3, 5, 7, 9], "x": [10, 20, 30, 40, 50]})
|
||||
cached = SemanticResult(
|
||||
requests=[SemanticRequest(type="SQL", definition="select ...")],
|
||||
results=pa.Table.from_pandas(df, preserve_index=False),
|
||||
)
|
||||
new_q = query(
|
||||
filters={where(COL_A, Operator.GREATER_THAN, 2)},
|
||||
limit=2,
|
||||
)
|
||||
result = _apply_post_processing(
|
||||
cached, new_q, {where(COL_A, Operator.GREATER_THAN, 2)}, False
|
||||
)
|
||||
result_df = result.results.to_pandas()
|
||||
assert list(result_df["a"]) == [3, 5]
|
||||
# the cache annotates the requests with a marker
|
||||
assert any(req.type == "cache" for req in result.requests)
|
||||
|
||||
|
||||
def test_apply_post_processing_no_leftovers_no_limit_returns_original() -> None:
|
||||
df = pd.DataFrame({"a": [1, 2]})
|
||||
cached = SemanticResult(
|
||||
requests=[], results=pa.Table.from_pandas(df, preserve_index=False)
|
||||
)
|
||||
new_q = query(filters=None, limit=None)
|
||||
out = _apply_post_processing(cached, new_q, set(), False)
|
||||
# same object reference is OK; we explicitly return the input
|
||||
assert out is cached
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hash stability
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_value_key_stable_across_metric_order() -> None:
|
||||
q1 = SemanticQuery(metrics=[M_X, M_Y], dimensions=[COL_A])
|
||||
q2 = SemanticQuery(metrics=[M_Y, M_X], dimensions=[COL_A])
|
||||
assert value_key(VIEW, q1) == value_key(VIEW, q2)
|
||||
|
||||
|
||||
def test_shape_key_stable_across_dimension_order() -> None:
|
||||
q1 = SemanticQuery(metrics=[M_X], dimensions=[COL_A, COL_B])
|
||||
q2 = SemanticQuery(metrics=[M_X], dimensions=[COL_B, COL_A])
|
||||
assert shape_key(VIEW, q1) == shape_key(VIEW, q2)
|
||||
|
||||
|
||||
def test_shape_key_changes_with_changed_on() -> None:
|
||||
q = SemanticQuery(metrics=[M_X], dimensions=[COL_A])
|
||||
other = ViewMeta(uuid=VIEW.uuid, changed_on_iso="2099-01-01", cache_timeout=None)
|
||||
assert shape_key(VIEW, q) != shape_key(other, q)
|
||||
|
||||
|
||||
def test_value_key_changes_with_filter_value() -> None:
|
||||
q1 = SemanticQuery(
|
||||
metrics=[M_X],
|
||||
dimensions=[COL_A],
|
||||
filters={where(COL_A, Operator.GREATER_THAN, 1)},
|
||||
)
|
||||
q2 = SemanticQuery(
|
||||
metrics=[M_X],
|
||||
dimensions=[COL_A],
|
||||
filters={where(COL_A, Operator.GREATER_THAN, 2)},
|
||||
)
|
||||
assert value_key(VIEW, q1) != value_key(VIEW, q2)
|
||||
|
||||
|
||||
def test_value_key_with_datetime_filter() -> None:
|
||||
f = where(COL_A, Operator.GREATER_THAN_OR_EQUAL, datetime(2025, 1, 1))
|
||||
q = SemanticQuery(metrics=[M_X], dimensions=[COL_A], filters={f})
|
||||
# should not raise
|
||||
assert value_key(VIEW, q).startswith("sv:val:")
|
||||
|
||||
|
||||
def test_shape_key_independent_of_dimensions() -> None:
|
||||
# The v2 shape key buckets entries by metric set only; different dimension
|
||||
# sets share the same shape so the projection path can find broader entries.
|
||||
q1 = SemanticQuery(metrics=[M_X], dimensions=[COL_A, COL_B])
|
||||
q2 = SemanticQuery(metrics=[M_X], dimensions=[COL_A])
|
||||
assert shape_key(VIEW, q1) == shape_key(VIEW, q2)
|
||||
# Value keys still differ.
|
||||
assert value_key(VIEW, q1) != value_key(VIEW, q2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Projection (v2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
M_SUM = met("met.sum", "sum_x", aggregation=AggregationType.SUM)
|
||||
M_COUNT = met("met.count", "count_x", aggregation=AggregationType.COUNT)
|
||||
M_MIN = met("met.min", "min_x", aggregation=AggregationType.MIN)
|
||||
M_MAX = met("met.max", "max_x", aggregation=AggregationType.MAX)
|
||||
M_AVG = met("met.avg", "avg_x", aggregation=AggregationType.AVG)
|
||||
M_UNKNOWN = met("met.unknown", "unknown_x", aggregation=None)
|
||||
|
||||
|
||||
def _projection_query(
|
||||
metrics: list[Metric],
|
||||
new_dimensions: list[Dimension],
|
||||
cached_dimensions: list[Dimension],
|
||||
cached_filters: set[Filter] | None = None,
|
||||
cached_limit: int | None = None,
|
||||
new_filters: set[Filter] | None = None,
|
||||
new_limit: int | None = None,
|
||||
new_order: Any = None,
|
||||
) -> tuple[CachedEntry, SemanticQuery]:
|
||||
cached_q = SemanticQuery(
|
||||
metrics=metrics,
|
||||
dimensions=cached_dimensions,
|
||||
filters=cached_filters,
|
||||
limit=cached_limit,
|
||||
)
|
||||
new_q = SemanticQuery(
|
||||
metrics=metrics,
|
||||
dimensions=new_dimensions,
|
||||
filters=new_filters,
|
||||
limit=new_limit,
|
||||
order=new_order,
|
||||
)
|
||||
return entry_from(cached_q), new_q
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metric,operator",
|
||||
[
|
||||
(M_SUM, "sum"),
|
||||
(M_COUNT, "sum"),
|
||||
(M_MIN, "min"),
|
||||
(M_MAX, "max"),
|
||||
],
|
||||
)
|
||||
def test_can_satisfy_projection_each_additive_op(metric: Metric, operator: str) -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[metric],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
)
|
||||
ok, leftovers, projection = can_satisfy(entry, new_q)
|
||||
assert ok is True
|
||||
assert projection is True
|
||||
assert leftovers == set()
|
||||
|
||||
|
||||
def test_projection_rolls_up_sum() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
)
|
||||
cached_df = pd.DataFrame(
|
||||
{"a": ["x", "x", "y", "y"], "b": [1, 2, 1, 2], "sum_x": [10, 20, 30, 40]}
|
||||
)
|
||||
cached = SemanticResult(
|
||||
requests=[SemanticRequest(type="SQL", definition="select ...")],
|
||||
results=pa.Table.from_pandas(cached_df, preserve_index=False),
|
||||
)
|
||||
out = _apply_post_processing(cached, new_q, set(), True)
|
||||
out_df = out.results.to_pandas().sort_values("a").reset_index(drop=True)
|
||||
assert list(out_df["a"]) == ["x", "y"]
|
||||
assert list(out_df["sum_x"]) == [30, 70]
|
||||
|
||||
|
||||
def test_projection_rolls_up_min_max_count() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_MIN, M_MAX, M_COUNT],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
)
|
||||
cached_df = pd.DataFrame(
|
||||
{
|
||||
"a": ["x", "x", "y", "y"],
|
||||
"b": [1, 2, 1, 2],
|
||||
"min_x": [5, 2, 9, 8],
|
||||
"max_x": [50, 60, 70, 80],
|
||||
"count_x": [1, 1, 2, 3],
|
||||
}
|
||||
)
|
||||
cached = SemanticResult(
|
||||
requests=[],
|
||||
results=pa.Table.from_pandas(cached_df, preserve_index=False),
|
||||
)
|
||||
out = _apply_post_processing(cached, new_q, set(), True)
|
||||
df = out.results.to_pandas().sort_values("a").reset_index(drop=True)
|
||||
assert list(df["min_x"]) == [2, 8]
|
||||
assert list(df["max_x"]) == [60, 80]
|
||||
assert list(df["count_x"]) == [2, 5]
|
||||
|
||||
|
||||
def test_projection_drops_multiple_dims() -> None:
|
||||
col_c = dim("col.c", "c")
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B, col_c],
|
||||
)
|
||||
cached_df = pd.DataFrame(
|
||||
{
|
||||
"a": ["x", "x", "x", "y"],
|
||||
"b": [1, 1, 2, 1],
|
||||
"c": [10, 20, 10, 10],
|
||||
"sum_x": [1, 2, 3, 4],
|
||||
}
|
||||
)
|
||||
cached = SemanticResult(
|
||||
requests=[], results=pa.Table.from_pandas(cached_df, preserve_index=False)
|
||||
)
|
||||
out = _apply_post_processing(cached, new_q, set(), True)
|
||||
df = out.results.to_pandas().sort_values("a").reset_index(drop=True)
|
||||
assert list(df["sum_x"]) == [6, 4]
|
||||
|
||||
|
||||
def test_projection_with_leftover_filter_then_rollup() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
new_filters={where(COL_B, Operator.GREATER_THAN, 1)},
|
||||
)
|
||||
cached_df = pd.DataFrame(
|
||||
{"a": ["x", "x", "y"], "b": [1, 2, 2], "sum_x": [10, 20, 30]}
|
||||
)
|
||||
cached = SemanticResult(
|
||||
requests=[], results=pa.Table.from_pandas(cached_df, preserve_index=False)
|
||||
)
|
||||
ok, leftovers, projection = can_satisfy(entry, new_q)
|
||||
assert ok is True
|
||||
assert projection is True
|
||||
out = _apply_post_processing(cached, new_q, leftovers, projection)
|
||||
df = out.results.to_pandas().sort_values("a").reset_index(drop=True)
|
||||
# b > 1 removes the (x,1) row; x sums to 20, y to 30
|
||||
assert list(df["sum_x"]) == [20, 30]
|
||||
|
||||
|
||||
def test_projection_with_order_and_limit() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
new_order=[(M_SUM, OrderDirection.DESC)],
|
||||
new_limit=1,
|
||||
)
|
||||
cached_df = pd.DataFrame(
|
||||
{"a": ["x", "x", "y"], "b": [1, 2, 1], "sum_x": [1, 2, 100]}
|
||||
)
|
||||
cached = SemanticResult(
|
||||
requests=[], results=pa.Table.from_pandas(cached_df, preserve_index=False)
|
||||
)
|
||||
out = _apply_post_processing(cached, new_q, set(), True)
|
||||
df = out.results.to_pandas()
|
||||
assert len(df) == 1
|
||||
assert df["a"].tolist() == ["y"]
|
||||
assert df["sum_x"].tolist() == [100]
|
||||
|
||||
|
||||
def test_projection_rejected_when_metric_aggregation_unknown() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_UNKNOWN],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_for_avg() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_AVG],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_when_cached_has_limit() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
cached_limit=10,
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_when_cached_has_having() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
cached_filters={having(M_SUM, Operator.GREATER_THAN, 10)},
|
||||
new_filters={having(M_SUM, Operator.GREATER_THAN, 10)},
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_when_order_references_dropped_dim() -> None:
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
new_order=[(COL_B, OrderDirection.ASC)],
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_when_cached_has_filter_on_dropped_dim() -> None:
|
||||
# cached restricts c; rolling up to [a] would miss rows we'd need
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A],
|
||||
cached_dimensions=[COL_A, COL_B],
|
||||
cached_filters={where(COL_B, Operator.GREATER_THAN, 5)},
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_projection_rejected_when_cached_dims_subset_not_superset() -> None:
|
||||
# cached has just [a]; new wants [a, b] — finer-grained data unavailable
|
||||
entry, new_q = _projection_query(
|
||||
metrics=[M_SUM],
|
||||
new_dimensions=[COL_A, COL_B],
|
||||
cached_dimensions=[COL_A],
|
||||
)
|
||||
ok, _, _ = can_satisfy(entry, new_q)
|
||||
assert ok is False
|
||||
94
tests/unit_tests/sql/test_spark_dialect.py
Normal file
94
tests/unit_tests/sql/test_spark_dialect.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for Spark dialect support in sqlglot.
|
||||
|
||||
Verifies that a spark:// SQLAlchemy connection resolves to SparkEngineSpec,
|
||||
which uses the sqlglot Spark dialect and preserves Spark SQL functions like
|
||||
BOOL_OR (instead of rewriting them to LOGICAL_OR as the Hive dialect does).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.engine.url import make_url
|
||||
|
||||
from superset.db_engine_specs import get_engine_spec
|
||||
from superset.db_engine_specs.spark import SparkEngineSpec
|
||||
from superset.sql.parse import LimitMethod, SQLScript, SQLStatement
|
||||
|
||||
|
||||
def test_spark_url_resolves_to_spark_engine_spec() -> None:
|
||||
"""A spark:// SQLAlchemy URI should resolve to SparkEngineSpec."""
|
||||
url = make_url("spark://localhost:10009/default")
|
||||
backend = url.get_backend_name()
|
||||
engine_spec = get_engine_spec(backend)
|
||||
assert engine_spec is SparkEngineSpec
|
||||
|
||||
|
||||
def test_spark_engine_spec_engine_attribute() -> None:
|
||||
"""SparkEngineSpec.engine should be 'spark', not inherited 'hive'."""
|
||||
assert SparkEngineSpec.engine == "spark"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("sql", "expected"),
|
||||
[
|
||||
(
|
||||
"SELECT BOOL_OR(col) FROM my_table",
|
||||
"SELECT\n BOOL_OR(col)\nFROM my_table",
|
||||
),
|
||||
(
|
||||
"SELECT BOOL_OR('test_value' IN ('test', 'test_value'))",
|
||||
"SELECT\n BOOL_OR('test_value' IN ('test', 'test_value'))",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_spark_preserves_bool_or(sql: str, expected: str) -> None:
|
||||
"""BOOL_OR should be preserved when using the Spark engine.
|
||||
|
||||
The Hive dialect rewrites BOOL_OR to LOGICAL_OR via sqlglot, but Spark SQL
|
||||
supports BOOL_OR natively so it must remain unchanged.
|
||||
"""
|
||||
script = SQLScript(sql, SparkEngineSpec.engine)
|
||||
result = script.statements[0].format()
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_spark_preserves_bool_or_with_limit() -> None:
|
||||
"""BOOL_OR should be preserved after applying a LIMIT (the SQLLab flow).
|
||||
|
||||
In SQLLab, Superset parses the user's SQL, applies a LIMIT, and regenerates
|
||||
the SQL using the engine's sqlglot dialect. This test replicates that full
|
||||
flow for a spark:// connection.
|
||||
"""
|
||||
sql = "SELECT BOOL_OR('test_value' IN ('test', 'test_value'))"
|
||||
statement = SQLStatement(sql, SparkEngineSpec.engine)
|
||||
statement.set_limit_value(1001, LimitMethod.FORCE_LIMIT)
|
||||
result = statement.format()
|
||||
|
||||
expected = "SELECT\n BOOL_OR('test_value' IN ('test', 'test_value'))\nLIMIT 1001"
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_hive_rewrites_bool_or_to_logical_or() -> None:
|
||||
"""Contrast: the Hive dialect rewrites BOOL_OR to LOGICAL_OR."""
|
||||
sql = "SELECT BOOL_OR('test_value' IN ('test', 'test_value'))"
|
||||
statement = SQLStatement(sql, "hive")
|
||||
statement.set_limit_value(1001, LimitMethod.FORCE_LIMIT)
|
||||
result = statement.format()
|
||||
|
||||
assert "LOGICAL_OR" in result
|
||||
assert "BOOL_OR" not in result
|
||||
Reference in New Issue
Block a user