Compare commits

...

8 Commits

Author SHA1 Message Date
dependabot[bot]
d64eaf9cdb chore(deps-dev): bump @testing-library/jest-dom from 6.9.1 to 7.0.0 in /superset-frontend (#42525)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-08-03 09:00:44 +07:00
Luc Verdier
4915a3e8c7 fix(mcp): report the committed chart when its instance is detached (#42621) 2026-08-02 14:56:46 -07:00
Evan Rusackas
7ce56d369a ci: add Kesin11/actions-timeline to the heavy CI workflows (#42507)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 14:53:27 -07:00
Evan Rusackas
b6d8c830e7 test(jinja): pin filter_values() drill-to-detail native-filter fallback for virtual datasets (#35263) (#42595)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-02 14:44:38 -07:00
Đỗ Trọng Hải
4e3ff37137 chore(ci): resolve shellcheck-flagged errors (#42430)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-08-03 00:09:04 +07:00
Joe Li
120b4420b9 fix(versioning): narrow UUIDs in restore tests (#42654) 2026-08-02 23:54:30 +07:00
dependabot[bot]
0628b0b813 chore(deps-dev): update thrift requirement from <1.0.0,>=0.23.0 to >=0.24.0,<1.0.0 (#42667)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-02 23:45:06 +07:00
dependabot[bot]
378634cceb chore(deps-dev): update pyathena requirement from <4,>=2 to >=3.35.2,<4 (#42665)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-02 23:44:48 +07:00
46 changed files with 539 additions and 240 deletions

View File

@@ -11,6 +11,7 @@ on:
permissions:
contents: read
pull-requests: read
actions: read
# cancel previous workflow jobs for PRs
concurrency:
@@ -21,6 +22,10 @@ jobs:
check-python-deps:
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

View File

@@ -20,6 +20,7 @@ concurrency:
permissions:
contents: read
actions: read
jobs:
dependency-review:
@@ -49,6 +50,10 @@ jobs:
# You cannot use a liccheck.ini file in this workflow.
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: "Checkout Repository"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

View File

@@ -213,3 +213,14 @@ jobs:
shell: bash
run: |
docker compose -f docker-compose-image-tag.yml up superset-init --exit-code-from superset-init
actions-timeline:
needs: [docker-build, docker-compose-image-tag]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -15,6 +15,7 @@ on:
permissions:
contents: read
actions: read
# cancel previous workflow jobs for PRs
concurrency:
@@ -188,3 +189,12 @@ jobs:
echo "📖 More details here: https://superset.apache.org/docs/contributing/development#git-hooks"
exit 1
fi
actions-timeline:
needs: pre-commit
if: always()
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -11,6 +11,7 @@ on:
permissions:
contents: read
pull-requests: read
actions: read
# cancel previous workflow jobs for PRs
concurrency:
@@ -40,6 +41,10 @@ jobs:
ports:
- 16379:6379
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

View File

@@ -30,6 +30,7 @@ concurrency:
permissions:
contents: read
actions: read
jobs:
config:
@@ -59,6 +60,10 @@ jobs:
name: Build & Deploy
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

View File

@@ -340,3 +340,14 @@ jobs:
exit 1
fi
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
actions-timeline:
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -11,6 +11,7 @@ on:
permissions:
contents: read
pull-requests: read
actions: read
# cancel previous workflow jobs for PRs
concurrency:
@@ -69,3 +70,12 @@ jobs:
with:
name: superset-extensions-cli-coverage-html
path: htmlcov/
actions-timeline:
needs: test-superset-extensions-cli-package
if: always()
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -201,3 +201,14 @@ jobs:
run: |
docker run --rm $TAG bash -c \
"npm run build-storybook && npx playwright install-deps && npx playwright install chromium && npm run test-storybook:ci"
actions-timeline:
needs: [report-coverage, lint-frontend, validate-frontend, test-storybook]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: read
# Serialize runs per PR without cancelling: when a first-time contributor's
# queued runs are approved together, cancel-in-progress lets an older run
@@ -21,6 +22,10 @@ jobs:
lint-test:
runs-on: ubuntu-26.04
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

View File

@@ -170,3 +170,14 @@ jobs:
${{ github.workspace }}/superset-frontend/playwright-results/
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-experimental-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
actions-timeline:
needs: playwright-tests-experimental
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -255,3 +255,14 @@ jobs:
exit 1
fi
echo "test-postgres result: $RESULT"
actions-timeline:
needs: [test-mysql, test-postgres, test-sqlite, test-postgres-required]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -158,3 +158,14 @@ jobs:
verbose: true
use_oidc: true
slug: apache/superset
actions-timeline:
needs: [test-postgres-presto, test-postgres-hive]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -101,7 +101,14 @@ jobs:
if: always()
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true
- name: Check unit-tests result
env:
RESULT: ${{ needs.unit-tests.result }}

View File

@@ -153,3 +153,14 @@ jobs:
- name: Fail if regression detected
if: steps.regression.outcome == 'failure'
run: exit 1
actions-timeline:
needs: [frontend-check-translations, babel-extract]
if: always()
runs-on: ubuntu-26.04
permissions:
actions: read
steps:
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
with:
expand-composite-actions: true

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env bash
# 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
@@ -15,8 +17,6 @@
# specific language governing permissions and limitations
# under the License.
#!/bin/bash
# Function to determine Python command
get_python_command() {
if command -v python3 &>/dev/null; then

View File

@@ -38,7 +38,7 @@ RESET='\033[0m'
echo -e "${GREEN}Updating package lists...${RESET}"
apt-get update -qq
echo -e "${GREEN}Installing packages: $@${RESET}"
echo -e "${GREEN}Installing packages: $*${RESET}"
apt-get install -yqq --no-install-recommends "$@"
echo -e "${GREEN}Autoremoving unnecessary packages...${RESET}"

View File

@@ -163,10 +163,10 @@ do
# Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater"
break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser"
break
fi

View File

@@ -122,7 +122,7 @@ dependencies = [
[project.optional-dependencies]
athena = ["pyathena[pandas]>=2, <4"]
athena = ["pyathena[pandas]>=3.35.2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.35.0",
@@ -173,7 +173,7 @@ hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
hive = [
"pyhive[hive_pure_sasl]>=0.7.0",
"tableschema",
"thrift>=0.23.0, <1.0.0",
"thrift>=0.24.0, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>=0.24.0, <0.25"]
@@ -206,7 +206,7 @@ sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
spark = [
"pyhive[hive_pure_sasl]>=0.7",
"tableschema",
"thrift>=0.23.0, <1",
"thrift>=0.24.0, <1",
]
tdengine = [
"taospy>=2.8.9",

View File

@@ -35,7 +35,7 @@ acquire_rat_jar () {
wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
else
printf "You do not have curl or wget installed, please install rat manually.\n"
exit -1
exit 255
fi
fi
@@ -44,7 +44,7 @@ acquire_rat_jar () {
# We failed to download
rm "$JAR"
printf "Our attempt to download rat locally to ${JAR} failed. Please install rat manually.\n"
exit -1
exit 255
fi
printf "Done downloading.\n"
}

View File

@@ -163,10 +163,10 @@ do
# Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater"
break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser"
break
fi

View File

@@ -190,9 +190,9 @@
"@swc/core": "^1.15.46",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -11433,28 +11433,28 @@
}
},
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.1.3",
"chalk": "^4.1.0",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz",
"integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==",
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
@@ -11465,9 +11465,12 @@
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"node": ">=22",
"npm": ">=6",
"yarn": ">=1"
},
"peerDependencies": {
"@testing-library/dom": ">=10 <11"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
@@ -11477,21 +11480,27 @@
"license": "MIT"
},
"node_modules/@testing-library/react": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
"integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz",
"integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^9.0.0",
"@testing-library/dom": "^10.0.0",
"@types/react-dom": "^18.0.0"
},
"engines": {
"node": ">=14"
"node": ">=18"
},
"peerDependencies": {
"@types/react": "^18.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@testing-library/user-event": {
@@ -14434,6 +14443,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -14601,12 +14611,12 @@
}
},
"node_modules/aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"license": "Apache-2.0",
"dependencies": {
"deep-equal": "^2.0.5"
"dequal": "^2.0.3"
}
},
"node_modules/arr-union": {
@@ -16177,6 +16187,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -18172,38 +18183,6 @@
"node": ">=6"
}
},
"node_modules/deep-equal": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.5",
"es-get-iterator": "^1.1.3",
"get-intrinsic": "^1.2.2",
"is-arguments": "^1.1.1",
"is-array-buffer": "^3.0.2",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"isarray": "^2.0.5",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.1",
"side-channel": "^1.0.4",
"which-boxed-primitive": "^1.0.2",
"which-collection": "^1.0.1",
"which-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -19215,26 +19194,6 @@
"node": ">= 0.4"
}
},
"node_modules/es-get-iterator": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"has-symbols": "^1.0.3",
"is-arguments": "^1.1.1",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.7",
"isarray": "^2.0.5",
"stop-iteration-iterator": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -22777,6 +22736,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -23985,22 +23945,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -31769,22 +31713,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -38501,26 +38429,33 @@
}
}
},
"node_modules/storybook/node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"node_modules/storybook/node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
"node": ">=18"
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/storybook/node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
},
"node_modules/storybook/node_modules/@testing-library/user-event": {
"version": "14.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
@@ -38535,16 +38470,6 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/storybook/node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/storybook/node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -39311,6 +39236,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -43686,9 +43612,9 @@
"@babel/preset-react": "^7.29.7",
"@babel/preset-typescript": "^7.29.7",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/lodash": "^4.17.24",
"@types/react": "*",
@@ -43728,9 +43654,9 @@
"@ant-design/icons": "^5.6.1 || ^6.0.0",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
@@ -43813,9 +43739,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -44088,7 +44014,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
@@ -44184,9 +44110,9 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.3.0",
@@ -44326,9 +44252,9 @@
"license": "Apache-2.0",
"devDependencies": {
"@babel/types": "^7.29.7",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/jest": "^30.0.0",
"jest": "^30.4.2"
@@ -44384,9 +44310,9 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",

View File

@@ -275,9 +275,9 @@
"@swc/core": "^1.15.46",
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",

View File

@@ -93,9 +93,9 @@
"typescript": "^5.0.0",
"@emotion/styled": "^11.14.1",
"@types/lodash": "^4.17.24",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",

View File

@@ -34,9 +34,9 @@
"@ant-design/icons": "^5.6.1 || ^6.0.0",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",

View File

@@ -92,9 +92,9 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",

View File

@@ -32,7 +32,7 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},

View File

@@ -40,9 +40,9 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.3.0",

View File

@@ -38,9 +38,9 @@
},
"devDependencies": {
"@babel/types": "^7.29.7",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/jest": "^30.0.0",
"jest": "^30.4.2"

View File

@@ -40,9 +40,9 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@testing-library/dom": "^9.3.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",

View File

@@ -868,7 +868,7 @@ def check_chart_data_access(chart: Any) -> "DatasetValidationResult":
"""
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
return validate_chart_dataset(chart, check_access=True)
return validate_chart_dataset(chart.datasource_id, check_access=True)
def _log_user_resolution_failure(exc: ValueError | PermissionError) -> None:

View File

@@ -70,7 +70,7 @@ class DatasetValidationResult:
def validate_chart_dataset(
chart: Any,
datasource_id: int | None,
check_access: bool = True,
) -> DatasetValidationResult:
"""
@@ -79,8 +79,12 @@ def validate_chart_dataset(
This shared utility should be called by MCP tools after creating or retrieving
charts to detect issues like missing or deleted datasets early.
Takes the datasource id rather than the chart so that callers holding an ORM
instance read it while that instance is attached; reading it here can raise
``DetachedInstanceError`` when a concurrent request has torn down the session.
Args:
chart: A chart-like object with datasource_id, datasource_type attributes
datasource_id: The chart's ``datasource_id``, or None if it has none
check_access: Whether to also check user permissions (default True)
Returns:
@@ -92,7 +96,6 @@ def validate_chart_dataset(
from superset.mcp_service.auth import has_dataset_access
warnings: list[str] = []
datasource_id = getattr(chart, "datasource_id", None)
# Check if chart has a datasource reference
if datasource_id is None:
@@ -1524,11 +1527,9 @@ def get_table_chart_type_label(viz_type: str | None) -> str | None:
return TABLE_VIZ_TYPE_LABELS.get(viz_type) if viz_type is not None else None
def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilities:
def analyze_chart_capabilities(viz_type: str | None, config: Any) -> ChartCapabilities:
"""Analyze chart capabilities based on type and configuration."""
if chart:
viz_type = getattr(chart, "viz_type", "unknown")
else:
if not viz_type:
viz_type = _resolve_viz_type(config)
# Determine interaction capabilities based on chart type
@@ -1574,11 +1575,9 @@ def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilit
)
def analyze_chart_semantics(chart: Any | None, config: Any) -> ChartSemantics:
def analyze_chart_semantics(viz_type: str | None, config: Any) -> ChartSemantics:
"""Generate semantic understanding of the chart."""
if chart:
viz_type = getattr(chart, "viz_type", "unknown")
else:
if not viz_type:
viz_type = _resolve_viz_type(config)
# Generate primary insight based on chart type

View File

@@ -317,6 +317,9 @@ async def generate_chart( # noqa: C901
chart = None
chart_id = None
chart_slice_name = None
chart_viz_type = None
chart_uuid = None
explore_url = None
form_data_key = None
response_warnings: list[str] = form_data.pop("_mcp_warnings", [])
@@ -424,7 +427,6 @@ async def generate_chart( # noqa: C901
)
chart = command.run()
chart_id = chart.id
# Ensure chart was created successfully before committing
if not chart or not chart.id:
@@ -432,6 +434,19 @@ async def generate_chart( # noqa: C901
"Chart creation failed - no chart ID returned"
)
# Snapshot the scalar fields now, while the instance is
# known to be attached. The chart is already committed at
# this point, and every read further down happens after an
# await: under concurrency another in-flight request can
# tear down the shared session in between, which detaches
# this instance and turns any attribute access into a
# DetachedInstanceError.
chart_id = chart.id
chart_slice_name = chart.slice_name
chart_viz_type = chart.viz_type
chart_uuid = str(chart.uuid) if chart.uuid else None
chart_datasource_id = chart.datasource_id
# Reload server-generated timestamps (created_on,
# changed_on) so the serializer sees real values.
from superset import db
@@ -442,20 +457,22 @@ async def generate_chart( # noqa: C901
logger.warning(
"Chart %s created but refresh failed; "
"continuing with current values",
chart.id,
chart_id,
exc_info=True,
)
await ctx.info(
"Chart created successfully: chart_id=%s, chart_name=%s"
% (
chart.id,
chart.slice_name,
chart_id,
chart_slice_name,
)
)
# Post-creation validation: verify the chart's dataset is accessible
dataset_check = validate_chart_dataset(chart, check_access=True)
dataset_check = validate_chart_dataset(
chart_datasource_id, check_access=True
)
if not dataset_check.is_valid:
# Dataset validation failed - warn but don't fail the operation
await ctx.warning(
@@ -464,7 +481,7 @@ async def generate_chart( # noqa: C901
)
logger.warning(
"Chart %s created but dataset validation failed: %s",
chart.id,
chart_id,
dataset_check.error,
)
if dataset_check.error:
@@ -482,7 +499,7 @@ async def generate_chart( # noqa: C901
# Query failed — delete the broken chart and return an error
logger.warning(
"Compile check failed for chart %s: %s",
chart.id,
chart_id,
compile_result.error,
)
await ctx.warning(
@@ -537,7 +554,7 @@ async def generate_chart( # noqa: C901
await ctx.error("Chart creation failed: error=%s" % (str(e),))
raise
# Update explore URL to use saved chart
explore_url = f"{get_superset_base_url()}/explore/?slice_id={chart.id}"
explore_url = f"{get_superset_base_url()}/explore/?slice_id={chart_id}"
# Generate form_data_key for saved charts (needed for chatbot rendering)
try:
@@ -561,7 +578,7 @@ async def generate_chart( # noqa: C901
cmd_params = CommandParameters(
datasource_type=DatasourceType.TABLE,
datasource_id=dataset.id,
chart_id=chart.id,
chart_id=chart_id,
tab_id=None,
form_data=json.dumps(form_data_with_datasource),
)
@@ -666,8 +683,8 @@ async def generate_chart( # noqa: C901
response_warnings.extend(compile_result.warnings)
# Generate semantic analysis
capabilities = analyze_chart_capabilities(chart, config)
semantics = analyze_chart_semantics(chart, config)
capabilities = analyze_chart_capabilities(chart_viz_type, config)
semantics = analyze_chart_semantics(chart_viz_type, config)
# Create performance metadata
execution_time = int((time.time() - start_time) * 1000)
@@ -678,11 +695,7 @@ async def generate_chart( # noqa: C901
)
# Create accessibility metadata
chart_name = (
chart.slice_name
if chart and hasattr(chart, "slice_name")
else generate_chart_name(config)
)
chart_name = chart_slice_name or generate_chart_name(config)
accessibility = AccessibilityMetadata(
color_blind_safe=True, # Would need actual analysis
alt_text=f"Chart showing {chart_name}",
@@ -775,7 +788,7 @@ async def generate_chart( # noqa: C901
# Build chart info using serialize_chart_object for saved charts
chart_info = None
chart_data = None
if request.save_chart and chart:
if request.save_chart and chart_id:
from sqlalchemy.orm import joinedload
from superset import db
@@ -793,7 +806,7 @@ async def generate_chart( # noqa: C901
try:
chart = (
ChartDAO.find_by_id(
chart.id,
chart_id,
query_options=[
joinedload(Slice.editors),
joinedload(Slice.tags),
@@ -804,7 +817,7 @@ async def generate_chart( # noqa: C901
except SQLAlchemyError:
logger.warning(
"Re-fetch of chart %s failed; returning minimal response",
chart.id,
chart_id,
exc_info=True,
)
try:
@@ -815,11 +828,11 @@ async def generate_chart( # noqa: C901
exc_info=True,
)
chart_data = {
"id": chart.id,
"slice_name": chart.slice_name,
"viz_type": chart.viz_type,
"id": chart_id,
"slice_name": chart_slice_name,
"viz_type": chart_viz_type,
"url": explore_url,
"uuid": str(chart.uuid) if chart.uuid else None,
"uuid": chart_uuid,
}
if chart_data is None:
@@ -849,14 +862,10 @@ async def generate_chart( # noqa: C901
"form_data": _sanitize_generate_chart_form_data_for_llm_context(form_data),
"form_data_key": form_data_key,
"api_endpoints": {
"data": f"{get_superset_base_url()}/api/v1/chart/{chart.id}/data/"
if chart
else None,
"export": f"{get_superset_base_url()}/api/v1/chart/{chart.id}/export/"
if chart
else None,
"data": f"{get_superset_base_url()}/api/v1/chart/{chart_id}/data/",
"export": f"{get_superset_base_url()}/api/v1/chart/{chart_id}/export/",
}
if chart
if chart_id
else {},
"performance": performance.model_dump() if performance else None,
"accessibility": accessibility.model_dump() if accessibility else None,
@@ -870,7 +879,7 @@ async def generate_chart( # noqa: C901
await ctx.info(
"Chart generation completed successfully: chart_id=%s, execution_time_ms=%s"
% (
chart.id if chart else None,
chart_id,
int((time.time() - start_time) * 1000),
)
)

View File

@@ -416,7 +416,9 @@ async def get_chart_data( # noqa: C901
# Skip the dataset RBAC pre-check for guests (see guest_scope.is_guest_read).
if not guest_scope.is_guest_read():
validation_result = validate_chart_dataset(chart, check_access=True)
validation_result = validate_chart_dataset(
chart.datasource_id, check_access=True
)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s"

View File

@@ -116,7 +116,7 @@ async def _validate_chart_dataset_access(
chart = ChartDAO.find_by_id(result.id)
if not chart:
return None
validation_result = validate_chart_dataset(chart, check_access=True)
validation_result = validate_chart_dataset(chart.datasource_id, check_access=True)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s" % (validation_result.error,)

View File

@@ -1262,7 +1262,9 @@ async def _get_chart_preview_internal( # noqa: C901
from superset.mcp_service import guest_scope
if getattr(chart, "id", None) is not None and not guest_scope.is_guest_read():
validation_result = validate_chart_dataset(chart, check_access=True)
validation_result = validate_chart_dataset(
chart.datasource_id, check_access=True
)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s"

View File

@@ -421,7 +421,7 @@ async def _handle_chart_sql_request(
)
# Validate the chart's dataset is accessible
validation_result = validate_chart_dataset(chart, check_access=True)
validation_result = validate_chart_dataset(chart.datasource_id, check_access=True)
if not validation_result.is_valid:
await ctx.warning(
"Chart found but dataset is not accessible: %s" % (validation_result.error,)

View File

@@ -579,8 +579,9 @@ async def update_chart( # noqa: C901
)
chart_for_analysis = updated_chart if saved else chart
capabilities = analyze_chart_capabilities(chart_for_analysis, parsed_config)
semantics = analyze_chart_semantics(chart_for_analysis, parsed_config)
viz_type_for_analysis = getattr(chart_for_analysis, "viz_type", None)
capabilities = analyze_chart_capabilities(viz_type_for_analysis, parsed_config)
semantics = analyze_chart_semantics(viz_type_for_analysis, parsed_config)
execution_time = int((time.time() - start_time) * 1000)
performance = PerformanceMetadata(

View File

@@ -224,6 +224,7 @@ class TestChartRestoreApi(SupersetTestCase):
chart_id = chart.id
chart_uuid = str(chart.uuid)
entity_uuid = chart.uuid
assert entity_uuid is not None
original_name = chart.slice_name
original_created_by = chart.created_by_fk
before_changed_on = chart.changed_on
@@ -329,6 +330,8 @@ class TestChartRestoreApi(SupersetTestCase):
assert alpha not in chart.editors
ver_cls = version_class(Slice)
entity_uuid = chart.uuid
assert entity_uuid is not None
first_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == chart.id)
@@ -337,7 +340,7 @@ class TestChartRestoreApi(SupersetTestCase):
.scalar()
)
assert first_tx is not None
target_uuid = str(derive_version_uuid(chart.uuid, first_tx))
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
self.login(ALPHA_USERNAME)
rv = self._restore(str(chart.uuid), target_uuid)
@@ -387,6 +390,8 @@ class TestChartRestoreApi(SupersetTestCase):
assert boys is not None
ver_cls = version_class(Slice)
boys_uuid = boys.uuid
assert boys_uuid is not None
boys_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == boys.id)
@@ -395,7 +400,7 @@ class TestChartRestoreApi(SupersetTestCase):
.scalar()
)
assert boys_tx is not None
boys_version_uuid = str(derive_version_uuid(boys.uuid, boys_tx))
boys_version_uuid = str(derive_version_uuid(boys_uuid, boys_tx))
self.login(ADMIN_USERNAME)
rv = self._restore(str(girls.uuid), boys_version_uuid)
@@ -421,6 +426,8 @@ class TestChartRestoreApi(SupersetTestCase):
db.session.commit()
ver_cls = version_class(Slice)
entity_uuid = chart.uuid
assert entity_uuid is not None
first_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == chart_id)
@@ -428,7 +435,7 @@ class TestChartRestoreApi(SupersetTestCase):
.limit(1)
.scalar()
)
target_uuid = str(derive_version_uuid(chart.uuid, first_tx))
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
self.login(ADMIN_USERNAME)
rv = self._restore(str(chart.uuid), target_uuid)

View File

@@ -90,6 +90,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
original_title = dashboard.dashboard_title
dashboard_id = dashboard.id
entity_uuid = dashboard.uuid
assert entity_uuid is not None
# Make two more edits so we have a known non-trivial history to
# navigate: [initial, v1, v2].
@@ -151,6 +152,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
dashboard_uuid = str(dashboard.uuid)
dashboard_id = dashboard.id
entity_uuid = dashboard.uuid
assert entity_uuid is not None
original_slice_ids = sorted(s.id for s in dashboard.slices)
assert len(original_slice_ids) >= 2, (
@@ -225,6 +227,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
db.session.commit()
ver_cls = version_class(Dashboard)
entity_uuid = dashboard.uuid
assert entity_uuid is not None
target_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == dashboard_id)
@@ -232,7 +236,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
.limit(1)
.scalar()
)
target_uuid = str(derive_version_uuid(dashboard.uuid, target_tx))
target_uuid = str(derive_version_uuid(entity_uuid, target_tx))
# Edit the member chart AFTER the snapshot.
member = db.session.query(Slice).filter(Slice.id == member_id).one()
@@ -282,6 +286,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
db.session.commit()
ver_cls = version_class(Dashboard)
entity_uuid = dashboard.uuid
assert entity_uuid is not None
target_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == dashboard_id)
@@ -289,7 +295,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
.limit(1)
.scalar()
)
target_uuid = str(derive_version_uuid(dashboard.uuid, target_tx))
target_uuid = str(derive_version_uuid(entity_uuid, target_tx))
# Detach, then hard-delete the victim via raw SQL so no live row
# remains (bypasses the soft-delete listener deliberately — the
@@ -340,6 +346,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
assert alpha not in dashboard.editors
ver_cls = version_class(Dashboard)
entity_uuid = dashboard.uuid
assert entity_uuid is not None
first_tx = (
db.session.query(ver_cls.transaction_id)
.filter(ver_cls.id == dashboard.id)
@@ -348,7 +356,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
.scalar()
)
assert first_tx is not None
target_uuid = str(derive_version_uuid(dashboard.uuid, first_tx))
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
self.login(ALPHA_USERNAME)
rv = self._restore(str(dashboard.uuid), target_uuid)

View File

@@ -109,6 +109,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
assert table is not None
table_uuid = str(table.uuid)
entity_uuid = table.uuid
assert entity_uuid is not None
table_id = table.id
original_description = table.description
@@ -164,6 +165,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
assert table is not None
table_uuid = str(table.uuid)
entity_uuid = table.uuid
assert entity_uuid is not None
table_id = table.id
col = table.columns[0]
@@ -219,6 +221,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
table_id = table.id
table_uuid = str(table.uuid)
entity_uuid = table.uuid
assert entity_uuid is not None
original_col_names = sorted(c.column_name for c in table.columns)
removed_name = table.columns[0].column_name
@@ -285,6 +288,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
table_id = table.id
table_uuid = str(table.uuid)
entity_uuid = table.uuid
assert entity_uuid is not None
removed_name = table.columns[0].column_name
added_name = "__restore_full_diff_test__"
@@ -399,6 +403,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
table_id = table.id
table_uuid = str(table.uuid)
entity_uuid = table.uuid
assert entity_uuid is not None
original_description = table.description
original_col_names = sorted(c.column_name for c in table.columns)

View File

@@ -1945,9 +1945,8 @@ class TestValidateChartDataset:
def test_validate_chart_dataset_no_datasource_id(
self, mock_find: MagicMock, mock_access: MagicMock
) -> None:
"""Chart with no datasource_id returns invalid result."""
chart = MagicMock(spec=[]) # no datasource_id attribute
result = validate_chart_dataset(chart)
"""A chart with no datasource_id returns invalid result."""
result = validate_chart_dataset(None)
assert not result.is_valid
assert result.dataset_id is None
assert "no dataset reference" in (result.error or "").lower()
@@ -1959,9 +1958,7 @@ class TestValidateChartDataset:
self, mock_find: MagicMock, mock_access: MagicMock
) -> None:
"""Chart whose dataset was deleted returns invalid result."""
chart = MagicMock()
chart.datasource_id = 42
result = validate_chart_dataset(chart)
result = validate_chart_dataset(42)
assert not result.is_valid
assert result.dataset_id == 42
assert "deleted" in (result.error or "").lower()
@@ -1976,9 +1973,7 @@ class TestValidateChartDataset:
dataset.table_name = "my_table"
dataset.sql = None
mock_find.return_value = dataset
chart = MagicMock()
chart.datasource_id = 7
result = validate_chart_dataset(chart)
result = validate_chart_dataset(7)
assert result.is_valid
assert result.dataset_id == 7
assert result.dataset_name == "my_table"
@@ -1994,9 +1989,7 @@ class TestValidateChartDataset:
dataset.table_name = "virt_ds"
dataset.sql = "SELECT 1"
mock_find.return_value = dataset
chart = MagicMock()
chart.datasource_id = 10
result = validate_chart_dataset(chart)
result = validate_chart_dataset(10)
assert result.is_valid
assert len(result.warnings) == 1
assert "virtual" in result.warnings[0].lower()
@@ -2010,9 +2003,7 @@ class TestValidateChartDataset:
from sqlalchemy.exc import SQLAlchemyError
mock_find.side_effect = SQLAlchemyError("connection lost")
chart = MagicMock()
chart.datasource_id = 99
result = validate_chart_dataset(chart)
result = validate_chart_dataset(99)
assert not result.is_valid
assert result.dataset_id == 99
assert "error" in (result.error or "").lower()

View File

@@ -19,9 +19,11 @@
Unit tests for MCP generate_chart tool
"""
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.exc import DetachedInstanceError
from superset.mcp_service.chart.schemas import (
@@ -438,6 +440,154 @@ def _make_mock_chart(chart_id: int = 42) -> Mock:
return chart
class _DetachableSlice:
"""A Slice stand-in that starts attached and can be detached at will.
Once detached, every attribute read raises ``DetachedInstanceError``, which
is what SQLAlchemy does when a concurrent request tears down the session
this instance was loaded in.
"""
def __init__(self, chart_id: int = 42) -> None:
self._values = {
"id": chart_id,
"slice_name": "Concurrent chart",
"viz_type": "table",
"uuid": "2a0e0e0e-0000-4000-8000-000000000042",
"datasource_id": 1,
}
self._detached = False
def detach(self) -> None:
self._detached = True
def __getattr__(self, name: str) -> Any:
if self._detached:
raise DetachedInstanceError(
f"Instance <Slice> is not bound to a Session; "
f"attribute refresh operation cannot proceed ({name})"
)
try:
return self._values[name]
except KeyError as ex:
raise AttributeError(name) from ex
async def _generate_saved_chart(
refetch: Any,
) -> tuple[Any, _DetachableSlice]:
"""Run generate_chart(save_chart=True) with a chart that detaches on commit.
``refetch`` is used as the ``ChartDAO.find_by_id`` behaviour of the
serialization path.
"""
request = GenerateChartRequest(
dataset_id="1",
config=TableChartConfig(chart_type="table", columns=[ColumnRef(name="region")]),
save_chart=True,
generate_preview=False,
)
ctx = MagicMock()
ctx.info = AsyncMock()
ctx.debug = AsyncMock()
ctx.warning = AsyncMock()
ctx.error = AsyncMock()
ctx.report_progress = AsyncMock()
chart = _DetachableSlice()
dataset = Mock(
id=1, datasource_name="test_table", table_name="test_table", sql=None
)
validation_result = Mock(is_valid=True, request=request, warnings={}, error=None)
session = MagicMock()
# The instance is detached right after the commit, before any of the reads
# that build the response.
session.refresh.side_effect = lambda _chart: chart.detach()
with (
patch(
"superset.mcp_service.auth.get_user_from_request",
return_value=Mock(id=1, username="admin", roles=[], groups=[]),
),
patch(
"superset.mcp_service.chart.validation.ValidationPipeline."
"validate_request_with_warnings",
return_value=validation_result,
),
patch("superset.daos.dataset.DatasetDAO.find_by_id", return_value=dataset),
patch(
"superset.mcp_service.chart.tool.generate_chart.has_dataset_access",
return_value=True,
),
# validate_chart_dataset is deliberately not mocked: it runs for real
# against the detached instance, which is where it used to raise.
patch("superset.mcp_service.auth.has_dataset_access", return_value=True),
patch(
"superset.commands.chart.create.CreateChartCommand",
return_value=Mock(run=Mock(return_value=chart)),
),
patch("superset.db.session", session),
patch(
"superset.mcp_service.chart.tool.generate_chart._compile_chart",
return_value=CompileResult(success=True, warnings=[]),
),
patch("superset.daos.chart.ChartDAO", Mock(find_by_id=refetch)),
patch(
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand",
return_value=Mock(run=Mock(return_value="form-data-key")),
),
patch(
"superset.mcp_service.chart.tool.generate_chart.get_superset_base_url",
return_value="http://localhost:8088",
),
):
result = await generate_chart(request, ctx=ctx)
return result, chart
class TestGenerateChartDetachedInstance:
"""The committed chart must be reported even if its instance is detached.
Regression tests for https://github.com/apache/superset/issues/42567: under
concurrency the chart was written to the database and the tool still
returned an error, because the response was built by reading attributes off
an instance another request had detached.
"""
@pytest.mark.asyncio
async def test_detached_chart_is_reported_as_created(self) -> None:
"""A detached instance no longer turns a committed chart into an error."""
refetched = _make_mock_chart()
result, chart = await _generate_saved_chart(
refetch=Mock(return_value=refetched)
)
assert chart._detached is True
assert result.success is True
assert result.error is None
assert result.chart is not None
assert result.chart.id == 42
assert result.explore_url == "http://localhost:8088/explore/?slice_id=42"
assert result.api_endpoints["data"].endswith("/api/v1/chart/42/data/")
@pytest.mark.asyncio
async def test_detached_chart_falls_back_to_captured_scalars(self) -> None:
"""The minimal fallback response never reads the detached instance."""
result, chart = await _generate_saved_chart(
refetch=Mock(side_effect=SQLAlchemyError("session is gone"))
)
assert chart._detached is True
assert result.success is True
assert result.error is None
assert result.chart is not None
assert result.chart.id == 42
assert result.chart.slice_name == "Concurrent chart"
assert result.chart.viz_type == "table"
class TestChartSerializationEagerLoading:
"""Tests for eager loading fix in generate_chart serialization path."""

View File

@@ -2405,6 +2405,76 @@ def test_get_sqla_query_allows_jinja_templated_custom_sql_metric_with_columns(
assert "{{" not in sql
def test_get_sqla_query_virtual_dataset_filter_values_drill_to_detail(
database: Database,
) -> None:
"""
Regression for #35263: a Jinja-templated virtual dataset that calls
``filter_values()`` in its own SQL must see filters sent in the native
``{col, op, val}`` format that Drill to Detail/Drill by use, not just
the ``adhoc_filters`` format used by ordinary chart/explore requests.
Without this, Jinja-based datasets return zero rows when drilled into,
even though the parent chart shows data for the selected value.
"""
from superset.connectors.sqla.models import SqlaTable, TableColumn
table = SqlaTable(
database=database,
schema=None,
table_name="t",
sql=(
"SELECT a, b FROM t WHERE 1=1 "
"{% if filter_values('b') %} "
"AND b IN {{ filter_values('b') | where_in }} "
"{% endif %}"
),
columns=[
TableColumn(column_name="a", type="INTEGER"),
TableColumn(column_name="b", type="TEXT"),
],
)
result = table.get_sqla_query(
columns=["a", "b"],
metrics=[],
extras={},
filter=[{"col": "b", "op": "IN", "val": ["Alice"]}],
granularity=None,
is_timeseries=False,
)
assert result is not None
with database.get_sqla_engine() as engine:
sql = str(
result.sqla_query.compile(
dialect=engine.dialect,
compile_kwargs={"literal_binds": True},
)
)
assert "'Alice'" in sql, (
"filter_values() should resolve native drill-to-detail-style "
f"filters inside a virtual dataset's own SQL. Generated SQL: {sql}"
)
# The assertion above can pass even when filter_values() itself is
# broken, because get_sqla_query() independently applies the native
# filter as an outer WHERE predicate on top of whatever the virtual
# dataset's own SQL renders to. Pull the virtual dataset's own rendered
# SQL directly out of the compiled query (rather than re-rendering it
# via a separately constructed template processor, which would not
# catch get_sqla_query() failing to forward the filter to the template
# processor it builds internally) to confirm filter_values() actually
# resolved the native filter *inside* the templated subquery.
virtual_table_from = result.sqla_query.get_final_froms()[0]
rendered_inner_sql = virtual_table_from.element.element.text
assert "'Alice'" in rendered_inner_sql, (
"filter_values() should render the native drill-to-detail-style "
"filter directly into the virtual dataset's own templated SQL, "
f"not just the outer query. Rendered SQL: {rendered_inner_sql}"
)
def test_extras_where_is_parenthesized(
database: Database,
) -> None: