Compare commits

..

1 Commits

Author SHA1 Message Date
Evan Rusackas
4026ac9134 fix(charts): handle MSSQL datetime values outside JavaScript's safe range
MSSQL datetime values outside JavaScript's Date range (1938-2286) were causing charts to display "0NaN-NaN-NaN". This fix returns ISO strings for dates outside the safe range instead of epoch milliseconds that overflow.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-01 15:14:22 -07:00
48 changed files with 807 additions and 2395 deletions

View File

@@ -28,9 +28,6 @@ const globals = require('globals');
const { defineConfig, globalIgnores } = require('eslint/config');
module.exports = defineConfig([
{
files: ['**/*.{js,jsx,ts,tsx}'],
},
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
js.configs.recommended,
...ts.configs.recommended,
@@ -39,7 +36,7 @@ module.exports = defineConfig([
files: ['eslint.config.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
}
},
{
languageOptions: {
@@ -71,5 +68,5 @@ module.exports = defineConfig([
version: 'detect',
},
},
},
]);
}
])

View File

@@ -15,7 +15,7 @@
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"eslint": "eslint ."
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
@@ -26,33 +26,33 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"antd": "^5.26.7",
"antd": "^5.26.3",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.4.0",
"less": "^4.3.0",
"less-loader": "^12.3.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.27.1"
"swagger-ui-react": "^5.26.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@eslint/js": "^9.32.0",
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.8",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.37.0",
"webpack": "^5.101.0"
"webpack": "^5.99.9"
},
"browserslist": {
"production": [

View File

@@ -2205,16 +2205,11 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.31.0":
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/js@^9.32.0":
version "9.32.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
@@ -2517,10 +2512,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -3427,10 +3422,10 @@
dependencies:
"@types/estree" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.6"
@@ -3971,11 +3966,6 @@ accepts@~1.3.4, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-import-phases@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -3988,7 +3978,12 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -4112,10 +4107,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.7:
version "5.26.7"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/cssinjs" "^1.23.0"
@@ -4128,7 +4123,7 @@ antd@^5.26.7:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.3.0"
"@rc-component/trigger" "^2.2.7"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4158,7 +4153,7 @@ antd@^5.26.7:
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-tabs "~15.6.1"
rc-textarea "~1.10.1"
rc-textarea "~1.10.0"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
@@ -4513,7 +4508,17 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
version "4.24.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
dependencies:
caniuse-lite "^1.0.30001688"
electron-to-chromium "^1.5.73"
node-releases "^2.0.19"
update-browserslist-db "^1.1.1"
browserslist@^4.25.0:
version "4.25.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
@@ -4615,7 +4620,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
version "1.0.30001714"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
@@ -5898,6 +5903,11 @@ electron-to-chromium@^1.5.160:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
electron-to-chromium@^1.5.73:
version "1.5.138"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -5942,10 +5952,10 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.17.2:
version "5.18.2"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
enhanced-resolve@^5.17.1:
version "5.18.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -6151,10 +6161,10 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-config-prettier@^10.1.8:
version "10.1.8"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
eslint-config-prettier@^10.1.5:
version "10.1.5"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-plugin-prettier@^5.5.1:
version "5.5.1"
@@ -8053,10 +8063,10 @@ less-loader@^12.3.0:
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
less@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
less@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
dependencies:
copy-anything "^2.0.1"
parse-node-version "^1.0.1"
@@ -10704,10 +10714,10 @@ rc-tabs@~15.6.1:
rc-resize-observer "^1.0.0"
rc-util "^5.34.1"
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
version "1.10.2"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
rc-textarea@~1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
@@ -12090,10 +12100,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"
@@ -12515,7 +12525,7 @@ unraw@^3.0.0:
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
update-browserslist-db@^1.1.3:
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
@@ -12784,27 +12794,26 @@ webpack-merge@^6.0.1:
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.101.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
version "5.99.9"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.15.0"
acorn-import-phases "^1.0.3"
acorn "^8.14.0"
browserslist "^4.24.0"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.2"
enhanced-resolve "^5.17.1"
es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -12818,7 +12827,7 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
tapable "^2.1.1"
terser-webpack-plugin "^5.3.11"
watchpack "^2.4.1"
webpack-sources "^3.3.3"
webpack-sources "^3.2.3"
webpackbar@^6.0.1:
version "6.0.1"

View File

@@ -174,8 +174,6 @@ function Echart(
if (!chartRef.current) {
chartRef.current = init(divRef.current, null, { locale });
}
// did mount
handleSizeChange({ width, height });
setDidMount(true);
});
}, [locale]);
@@ -237,6 +235,9 @@ function Echart(
echartOptions,
);
chartRef.current?.setOption(themedEchartOptions, true);
// did mount
handleSizeChange({ width, height });
}
}, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme]);

View File

@@ -145,7 +145,7 @@ def _get_samples(
query_obj = copy.copy(query_obj)
query_obj.is_timeseries = False
query_obj.orderby = []
query_obj.metrics = []
query_obj.metrics = None
query_obj.post_processing = []
qry_obj_cols = []
for o in datasource.columns:
@@ -168,7 +168,7 @@ def _get_drill_detail(
query_obj = copy.copy(query_obj)
query_obj.is_timeseries = False
query_obj.orderby = []
query_obj.metrics = []
query_obj.metrics = None
query_obj.post_processing = []
qry_obj_cols = []
for o in datasource.columns:

View File

@@ -86,8 +86,6 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
apply_fetch_values_predicate: bool
columns: list[Column]
datasource: BaseDatasource | None
columns_by_name: dict[str, Any]
metrics_by_name: dict[str, Any]
extras: dict[str, Any]
filter: list[QueryObjectFilterClause]
from_dttm: datetime | None
@@ -96,7 +94,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
inner_to_dttm: datetime | None
is_rowcount: bool
is_timeseries: bool
metrics: list[Metric]
metrics: list[Metric] | None
order_desc: bool
orderby: list[OrderBy]
post_processing: list[dict[str, Any]]
@@ -143,30 +141,6 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
self.apply_fetch_values_predicate = apply_fetch_values_predicate or False
self.columns = columns or []
self.datasource = datasource
# Build datasource mappings for easy lookup
self.columns_by_name: dict[str, Any] = {}
self.metrics_by_name: dict[str, Any] = {}
if datasource:
try:
if hasattr(datasource, "columns") and datasource.columns is not None:
self.columns_by_name = {
col.column_name: col for col in datasource.columns
}
except (TypeError, AttributeError):
# Handle mocked datasources or other non-iterable cases
pass
try:
if hasattr(datasource, "metrics") and datasource.metrics is not None:
self.metrics_by_name = {
metric.metric_name: metric for metric in datasource.metrics
}
except (TypeError, AttributeError):
# Handle mocked datasources or other non-iterable cases
pass
self.extras = extras or {}
self.filter = filters or []
self.granularity = granularity
@@ -218,12 +192,9 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
def is_str_or_adhoc(metric: Metric) -> bool:
return isinstance(metric, str) or is_adhoc_metric(metric)
# Track whether metrics was originally None (for need_groupby logic)
self._metrics_is_not_none = metrics is not None
self.metrics = [
self.metrics = metrics and [
x if is_str_or_adhoc(x) else x["label"] # type: ignore
for x in (metrics or [])
for x in metrics
]
def _set_post_processing(
@@ -255,20 +226,15 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
field.new_name,
)
value = kwargs[field.old_name]
if value is not None:
# Only override if the new field is not already populated with data
current_value = getattr(self, field.new_name, None)
if (
current_value
): # If field already has truthy data, don't override
if value:
if hasattr(self, field.new_name):
logger.warning(
"The field `%s` is already populated, "
"not replacing with contents from deprecated `%s`.",
"replacing value with contents from `%s`.",
field.new_name,
field.old_name,
)
else:
setattr(self, field.new_name, value)
setattr(self, field.new_name, value)
def _move_deprecated_extra_fields(self, kwargs: dict[str, Any]) -> None:
# move deprecated extras fields to extras
@@ -281,8 +247,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
field.new_name,
)
value = kwargs[field.old_name]
if value is not None and value != "": # Don't add empty string values
if field.new_name in self.extras:
if value:
if hasattr(self.extras, field.new_name):
logger.warning(
"The field `%s` is already populated in "
"`extras`, replacing value with contents "
@@ -296,7 +262,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
def metric_names(self) -> list[str]:
"""Return metrics names (labels), coerce adhoc metrics to strings."""
return get_metric_names(
self.metrics,
self.metrics or [],
(
self.datasource.verbose_map
if self.datasource and hasattr(self.datasource, "verbose_map")
@@ -310,428 +276,6 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
and metrics are non-empty, otherwise returns column labels."""
return get_column_names(self.columns)
@property
def time_grain(self) -> str | None:
"""Get time grain from extras."""
return (self.extras or {}).get("time_grain_sqla")
@property
def need_groupby(self) -> bool:
"""Determine if GROUP BY is needed based on metrics and columns."""
# GROUP BY is needed when there are metrics or when metrics is explicitly
# provided (even as empty list). When metrics=None, columns are just for
# selection without aggregation, so no GROUP BY needed.
return self._metrics_is_not_none
@property
def groupby(self) -> list[Column]:
"""Alias for columns (for backward compatibility/clarity)."""
return self.columns or []
def get_series_limit_prequery_obj(
self,
granularity: str | None,
inner_from_dttm: datetime | None,
inner_to_dttm: datetime | None,
orderby: list[OrderBy] | None = None,
) -> dict[str, Any]:
"""Build prequery object for series limit queries.
This is used to determine top groups when series_limit is set.
Args:
granularity: The time column name
inner_from_dttm: Inner from datetime (if different from main query)
inner_to_dttm: Inner to datetime (if different from main query)
orderby: Optional orderby to override (for series_limit_metric)
Returns:
Dictionary suitable for passing to query()
"""
from superset.utils.core import get_non_base_axis_columns
return {
"is_timeseries": False,
"row_limit": self.series_limit,
"metrics": self.metrics,
"granularity": granularity,
"groupby": self.groupby,
"from_dttm": inner_from_dttm or self.from_dttm,
"to_dttm": inner_to_dttm or self.to_dttm,
"filter": self.filter,
"orderby": orderby or [],
"extras": self.extras or {},
"columns": get_non_base_axis_columns(self.columns),
"order_desc": True,
}
def build_select_expressions( # noqa: C901
self,
granularity: str | None,
series_column_labels: set[str],
datasource: Any, # BaseDatasource
template_processor: Any,
) -> tuple[list[Any], dict[str, Any], dict[str, Any]]:
"""Build SELECT expressions for the query.
Args:
granularity: The time column name
series_column_labels: Labels of series columns
datasource: The datasource being queried
template_processor: Template processor for SQL templating
Returns:
Tuple of (select_exprs, groupby_all_columns, groupby_series_columns)
"""
from sqlalchemy import literal_column
from superset.utils.core import (
DTTM_ALIAS,
is_adhoc_column,
)
select_exprs = []
groupby_all_columns = {}
groupby_series_columns = {}
# Filter out the pseudo column __timestamp from columns
columns = [col for col in self.columns if col != DTTM_ALIAS]
if self.need_groupby:
# dedup columns while preserving order
columns = self.groupby or self.columns
for selected in columns:
if isinstance(selected, str):
# if groupby field/expr equals granularity field/expr
if selected == granularity:
table_col = self.columns_by_name[selected]
outer = table_col.get_timestamp_expression(
time_grain=self.time_grain,
label=selected,
template_processor=template_processor,
)
# if groupby field equals a selected column
elif selected in self.columns_by_name:
outer = datasource.convert_tbl_column_to_sqla_col(
self.columns_by_name[selected],
template_processor=template_processor,
)
else:
# Import here to avoid circular imports
from superset.models.helpers import validate_adhoc_subquery
selected = validate_adhoc_subquery(
selected,
datasource.database,
datasource.catalog,
datasource.schema,
datasource.database.db_engine_spec.engine,
)
outer = literal_column(f"({selected})")
outer = datasource.make_sqla_column_compatible(outer, selected)
else:
outer = datasource.adhoc_column_to_sqla(
col=selected,
template_processor=template_processor,
)
groupby_all_columns[outer.name] = outer
if (
self.is_timeseries and not series_column_labels
) or outer.name in series_column_labels:
groupby_series_columns[outer.name] = outer
select_exprs.append(outer)
elif self.columns:
with datasource.database.get_sqla_engine() as engine:
quote = engine.dialect.identifier_preparer.quote
for selected in self.columns:
if is_adhoc_column(selected):
_sql = selected["sqlExpression"]
_column_label = selected["label"]
elif isinstance(selected, str):
_sql = quote(selected)
_column_label = selected
# Import here to avoid circular imports
from superset.models.helpers import validate_adhoc_subquery
selected = validate_adhoc_subquery(
_sql,
datasource.database,
datasource.catalog,
datasource.schema,
datasource.database.db_engine_spec.engine,
)
select_exprs.append(
datasource.convert_tbl_column_to_sqla_col(
self.columns_by_name[selected],
template_processor=template_processor,
label=_column_label,
)
if selected in self.columns_by_name
else datasource.make_sqla_column_compatible(
literal_column(selected), _column_label
)
)
return select_exprs, groupby_all_columns, groupby_series_columns
def build_filter_clauses( # noqa: C901
self,
datasource: Any, # BaseDatasource
template_processor: Any,
time_filters: list[Any],
removed_filters: set[str],
applied_adhoc_filters_columns: list[Any],
rejected_adhoc_filters_columns: list[Any],
is_timeseries: bool,
dttm_col: Any,
) -> tuple[list[Any], list[Any]]:
"""Build WHERE and HAVING filter clauses for the query.
Args:
datasource: The datasource being queried
template_processor: Template processor for SQL templating
time_filters: Time-based filters to apply
removed_filters: Set of filter column names handled by Jinja templates
applied_adhoc_filters_columns: List to track applied adhoc filters
rejected_adhoc_filters_columns: List to track rejected adhoc filters
is_timeseries: Whether this is a timeseries query
dttm_col: The datetime column object
Returns:
Tuple of (where_clause_and, having_clause_and)
"""
from flask import current_app
from sqlalchemy import or_
from superset import feature_flag_manager
from superset.common.utils.time_range_utils import (
get_since_until_from_time_range,
)
from superset.exceptions import QueryObjectValidationError
from superset.utils.core import (
DTTM_ALIAS,
FilterOperator,
GenericDataType,
get_column_name,
is_adhoc_column,
)
where_clause_and = []
having_clause_and = []
# Process regular filters
for flt in self.filter:
if not all(flt.get(s) for s in ["col", "op"]):
continue
flt_col = flt["col"]
val = flt.get("val")
flt_grain = flt.get("grain")
op = FilterOperator(flt["op"].upper())
col_obj = None
sqla_col = None
if flt_col == DTTM_ALIAS and is_timeseries and dttm_col:
col_obj = dttm_col
elif is_adhoc_column(flt_col):
try:
sqla_col = datasource.adhoc_column_to_sqla(
flt_col, force_type_check=True
)
applied_adhoc_filters_columns.append(flt_col)
except Exception: # ColumnNotFoundException
rejected_adhoc_filters_columns.append(flt_col)
continue
else:
col_obj = self.columns_by_name.get(str(flt_col))
filter_grain = flt.get("grain")
if get_column_name(flt_col) in removed_filters:
# Skip generating SQLA filter when the jinja template handles it.
continue
if col_obj or sqla_col is not None:
db_engine_spec = datasource.database.db_engine_spec
if sqla_col is not None:
pass
elif col_obj and filter_grain:
sqla_col = col_obj.get_timestamp_expression(
time_grain=filter_grain, template_processor=template_processor
)
elif col_obj:
sqla_col = datasource.convert_tbl_column_to_sqla_col(
tbl_column=col_obj, template_processor=template_processor
)
col_type = col_obj.type if col_obj else None
col_spec = db_engine_spec.get_column_spec(native_type=col_type)
is_list_target = op in (
FilterOperator.IN,
FilterOperator.NOT_IN,
)
col_advanced_data_type = col_obj.advanced_data_type if col_obj else ""
if col_spec and not col_advanced_data_type:
target_generic_type = col_spec.generic_type
else:
target_generic_type = GenericDataType.STRING
eq = datasource.filter_values_handler(
values=val,
operator=op,
target_generic_type=target_generic_type,
target_native_type=col_type,
is_list_target=is_list_target,
db_engine_spec=db_engine_spec,
)
# Get ADVANCED_DATA_TYPES from config when needed
ADVANCED_DATA_TYPES = current_app.config.get("ADVANCED_DATA_TYPES", {}) # noqa: N806
if (
col_advanced_data_type != ""
and feature_flag_manager.is_feature_enabled(
"ENABLE_ADVANCED_DATA_TYPES"
)
and col_advanced_data_type in ADVANCED_DATA_TYPES
and eq is not None
):
where_clause_and.append(
datasource._apply_advanced_data_type_filter(
sqla_col, col_advanced_data_type, op, eq
)
)
elif is_list_target:
assert isinstance(eq, (tuple, list))
if len(eq) == 0:
raise QueryObjectValidationError(
"Filter value list cannot be empty"
)
if len(eq) > len(
eq_without_none := [x for x in eq if x is not None]
):
is_null_cond = sqla_col.is_(None)
if eq:
cond = or_(is_null_cond, sqla_col.in_(eq_without_none))
else:
cond = is_null_cond
else:
cond = sqla_col.in_(eq)
if op == FilterOperator.NOT_IN:
cond = ~cond
where_clause_and.append(cond)
elif op in {
FilterOperator.IS_NULL,
FilterOperator.IS_NOT_NULL,
}:
where_clause_and.append(
db_engine_spec.handle_null_filter(sqla_col, op)
)
elif op == FilterOperator.IS_TRUE:
where_clause_and.append(
db_engine_spec.handle_boolean_filter(sqla_col, op, True)
)
elif op == FilterOperator.IS_FALSE:
where_clause_and.append(
db_engine_spec.handle_boolean_filter(sqla_col, op, False)
)
else:
if (
op
not in {
FilterOperator.EQUALS,
FilterOperator.NOT_EQUALS,
}
and eq is None
):
raise QueryObjectValidationError(
"Must specify a value for filters with comparison operators"
)
if op in {
FilterOperator.EQUALS,
FilterOperator.NOT_EQUALS,
FilterOperator.GREATER_THAN,
FilterOperator.LESS_THAN,
FilterOperator.GREATER_THAN_OR_EQUALS,
FilterOperator.LESS_THAN_OR_EQUALS,
}:
where_clause_and.append(
db_engine_spec.handle_comparison_filter(sqla_col, op, eq)
)
elif op in {
FilterOperator.ILIKE,
FilterOperator.LIKE,
}:
if target_generic_type != GenericDataType.STRING:
import sqlalchemy as sa
sqla_col = sa.cast(sqla_col, sa.String)
if op == FilterOperator.LIKE:
where_clause_and.append(sqla_col.like(eq))
else:
where_clause_and.append(sqla_col.ilike(eq))
elif op in {FilterOperator.NOT_LIKE}:
if target_generic_type != GenericDataType.STRING:
import sqlalchemy as sa
sqla_col = sa.cast(sqla_col, sa.String)
where_clause_and.append(sqla_col.not_like(eq))
elif (
op == FilterOperator.TEMPORAL_RANGE
and isinstance(eq, str)
and col_obj is not None
):
_since, _until = get_since_until_from_time_range(
time_range=eq,
time_shift=self.time_shift,
extras=self.extras or {},
)
where_clause_and.append(
datasource.get_time_filter(
time_col=col_obj,
start_dttm=_since,
end_dttm=_until,
time_grain=flt_grain,
label=sqla_col.key,
template_processor=template_processor,
)
)
else:
raise QueryObjectValidationError(
f"Invalid filter operation type: {op}"
)
# Process WHERE and HAVING extras
if self.extras:
where = self.extras.get("where")
if where:
where = datasource._process_sql_expression(
expression=where,
database_id=datasource.database_id,
engine=datasource.database.backend,
schema=datasource.schema,
template_processor=template_processor,
)
where_clause_and += [datasource.text(where)]
having = self.extras.get("having")
if having:
having = datasource._process_sql_expression(
expression=having,
database_id=datasource.database_id,
engine=datasource.database.backend,
schema=datasource.schema,
template_processor=template_processor,
)
having_clause_and += [datasource.text(having)]
return where_clause_and, having_clause_and
def validate(
self, raise_exceptions: bool | None = True
) -> QueryObjectValidationError | None:
@@ -806,7 +350,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
"inner_to_dttm": self.inner_to_dttm,
"is_rowcount": self.is_rowcount,
"is_timeseries": self.is_timeseries,
"metrics": self.metrics if self.metrics else None,
"metrics": self.metrics,
"order_desc": self.order_desc,
"orderby": self.orderby,
"row_limit": self.row_limit,

View File

@@ -71,7 +71,6 @@ from sqlalchemy.types import JSON
from superset import db, is_feature_enabled, security_manager
from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.common.db_query_status import QueryStatus
from superset.common.query_object import QueryObject
from superset.connectors.sqla.utils import (
get_columns_description,
get_physical_table_metadata,
@@ -721,7 +720,7 @@ class AnnotationDatasource(BaseDatasource):
def query(self, query_obj: QueryObjectDict) -> QueryResult:
error_message = None
qry = db.session.query(Annotation)
qry = qry.filter(Annotation.layer_id == query_obj["filters"][0]["val"])
qry = qry.filter(Annotation.layer_id == query_obj["filter"][0]["val"])
if query_obj["from_dttm"]:
qry = qry.filter(Annotation.start_dttm >= query_obj["from_dttm"])
if query_obj["to_dttm"]:
@@ -1515,19 +1514,18 @@ class SqlaTable(
def _get_series_orderby(
self,
series_limit_metric: Metric,
query_obj: QueryObject,
metrics_by_name: dict[str, SqlMetric],
columns_by_name: dict[str, TableColumn],
template_processor: BaseTemplateProcessor | None = None,
) -> Column:
if utils.is_adhoc_metric(series_limit_metric):
assert isinstance(series_limit_metric, dict)
ob = self.adhoc_metric_to_sqla(
series_limit_metric, query_obj.columns_by_name
)
ob = self.adhoc_metric_to_sqla(series_limit_metric, columns_by_name)
elif (
isinstance(series_limit_metric, str)
and series_limit_metric in query_obj.metrics_by_name
and series_limit_metric in metrics_by_name
):
ob = query_obj.metrics_by_name[series_limit_metric].get_sqla_col(
ob = metrics_by_name[series_limit_metric].get_sqla_col(
template_processor=template_processor
)
else:
@@ -1859,10 +1857,7 @@ class SqlaTable(
"""
extra_cache_keys = super().get_extra_cache_keys(query_obj)
if self.has_extra_cache_key_calls(query_obj):
from superset.common.query_object import QueryObject
query_object = QueryObject(datasource=self, **query_obj)
sqla_query = self.get_sqla_query(query_object)
sqla_query = self.get_sqla_query(**query_obj)
extra_cache_keys += sqla_query.extra_cache_keys
return list(set(extra_cache_keys))

View File

@@ -940,7 +940,7 @@ def dataset_macro(
metrics = [metric.metric_name for metric in dataset.metrics]
query_obj = {
"is_timeseries": False,
"filters": [],
"filter": [],
"metrics": metrics if include_metrics else None,
"columns": columns,
"from_dttm": from_dttm,

View File

@@ -37,7 +37,7 @@ from superset.migrations.shared.security_converge import (
)
from superset.models.core import Database
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
Base: Type[Any] = declarative_base()

View File

@@ -28,7 +28,7 @@ from superset.migrations.shared.utils import paginated_update, try_load_json
from superset.utils import json
from superset.utils.date_parser import get_since_until
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
Base = declarative_base()

View File

@@ -29,7 +29,7 @@ from sqlalchemy import (
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Load, relationship, Session
logger = logging.getLogger("alembic.env")
logger = logging.getLogger(__name__)
Base = declarative_base()

View File

@@ -49,7 +49,7 @@ YELLOW = "\033[33m"
RED = "\033[31m"
LRED = "\033[91m"
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
DEFAULT_BATCH_SIZE = int(os.environ.get("BATCH_SIZE", 1000))

View File

@@ -33,8 +33,6 @@ from superset.utils.core import generic_find_constraint_name
revision = "1226819ee0e3"
down_revision = "956a063c52b3"
logger = logging.getLogger("alembic.env")
naming_convention = {
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s"
@@ -63,7 +61,7 @@ def upgrade():
["datasource_name"],
)
except: # noqa: E722
logger.warning("Could not find or drop constraint on `columns`")
logging.warning("Could not find or drop constraint on `columns`")
def downgrade():

View File

@@ -38,8 +38,6 @@ from superset.utils.core import generic_find_constraint_name
revision = "3b626e2a6783"
down_revision = "eca4694defa7"
logger = logging.getLogger("alembic.env")
def upgrade():
# cleanup after: https://github.com/airbnb/superset/pull/1078
@@ -62,7 +60,7 @@ def upgrade():
batch_op.drop_column("druid_datasource_id")
batch_op.drop_column("table_id")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
# fixed issue: https://github.com/airbnb/superset/issues/466
try:
@@ -71,18 +69,18 @@ def upgrade():
None, "datasources", ["datasource_name"], ["datasource_name"]
)
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
try:
with op.batch_alter_table("query") as batch_op:
batch_op.create_unique_constraint("client_id", ["client_id"])
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
try:
with op.batch_alter_table("query") as batch_op:
batch_op.drop_column("name")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
def downgrade():
@@ -90,7 +88,7 @@ def downgrade():
with op.batch_alter_table("tables") as batch_op:
batch_op.create_index("table_name", ["table_name"], unique=True)
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
try:
with op.batch_alter_table("slices") as batch_op:
@@ -115,7 +113,7 @@ def downgrade():
)
batch_op.create_foreign_key("slices_ibfk_2", "tables", ["table_id"], ["id"])
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
try:
fk_columns = generic_find_constraint_name(
@@ -127,11 +125,11 @@ def downgrade():
with op.batch_alter_table("columns") as batch_op:
batch_op.drop_constraint(fk_columns, type_="foreignkey")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))
op.add_column("query", sa.Column("name", sa.String(length=256), nullable=True))
try:
with op.batch_alter_table("query") as batch_op:
batch_op.drop_constraint("client_id", type_="unique")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))

View File

@@ -31,8 +31,6 @@ import logging # noqa: E402
import sqlalchemy as sa # noqa: E402
from alembic import op # noqa: E402
logger = logging.getLogger("alembic.env")
def upgrade():
op.add_column("tables", sa.Column("params", sa.Text(), nullable=True))
@@ -42,4 +40,4 @@ def downgrade():
try:
op.drop_column("tables", "params")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex))

View File

@@ -31,8 +31,6 @@ import logging # noqa: E402
import sqlalchemy as sa # noqa: E402
from alembic import op # noqa: E402
logger = logging.getLogger("alembic.env")
def upgrade():
op.add_column(
@@ -46,7 +44,7 @@ def upgrade():
op.create_unique_constraint(None, "dbs", ["verbose_name"])
op.create_unique_constraint(None, "clusters", ["verbose_name"])
except Exception:
logger.info("Constraint not created, expected when using sqlite")
logging.info("Constraint not created, expected when using sqlite")
def downgrade():
@@ -54,4 +52,4 @@ def downgrade():
op.drop_column("dbs", "verbose_name")
op.drop_column("clusters", "verbose_name")
except Exception as ex:
logger.exception(ex)
logging.exception(ex)

View File

@@ -37,8 +37,6 @@ from superset.utils.core import (
revision = "4736ec66ce19"
down_revision = "f959a6652acd"
logger = logging.getLogger("alembic.env")
conv = {
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
@@ -121,13 +119,13 @@ def upgrade():
type_="unique",
)
except Exception as ex:
logger.warning(
logging.warning(
"Constraint drop failed, you may want to do this "
"manually on your database. For context, this is a known "
"issue around nondeterministic constraint names on Postgres "
"and perhaps more databases through SQLAlchemy."
)
logger.exception(ex)
logging.exception(ex)
def downgrade():

View File

@@ -22,16 +22,12 @@ Create Date: 2018-05-09 23:45:14.296283
"""
import logging
import sqlalchemy as sa # noqa: E402
from alembic import op # noqa: E402
# revision identifiers, used by Alembic.
revision = "e502db2af7be"
down_revision = "5ccf602336a0"
logger = logging.getLogger("alembic.env")
import sqlalchemy as sa # noqa: E402
from alembic import op # noqa: E402
def upgrade():
@@ -42,4 +38,4 @@ def downgrade():
try:
op.drop_column("tables", "template_params")
except Exception as ex:
logger.warning(str(ex))
logging.warning(str(ex)) # noqa: F821

View File

@@ -35,8 +35,6 @@ from superset.utils import json
revision = "fb13d49b72f9"
down_revision = "de021a1ca60d"
logger = logging.getLogger("alembic.env")
Base = declarative_base()
@@ -51,7 +49,7 @@ class Slice(Base):
def upgrade_slice(slc):
params = json.loads(slc.params)
logger.info(f"Upgrading {slc.slice_name}")
logging.info(f"Upgrading {slc.slice_name}")
cols = params.get("groupby")
metric = params.get("metric")
if cols:
@@ -81,8 +79,8 @@ def upgrade():
for slc in filter_box_slices.all():
try:
upgrade_slice(slc)
except Exception as e:
logger.exception(e)
except Exception:
logging.exception(e) # noqa: F821
session.commit()
session.close()
@@ -96,7 +94,7 @@ def downgrade():
for slc in filter_box_slices.all():
try:
params = json.loads(slc.params)
logger.info(f"Downgrading {slc.slice_name}")
logging.info(f"Downgrading {slc.slice_name}")
flts = params.get("filter_configs")
if not flts:
continue
@@ -104,7 +102,7 @@ def downgrade():
params["groupby"] = [o.get("column") for o in flts]
slc.params = json.dumps(params, sort_keys=True)
except Exception as ex:
logger.exception(ex)
logging.exception(ex)
session.commit()
session.close()

View File

@@ -41,8 +41,6 @@ from superset.utils.core import ( # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
class Slice(Base):
__tablename__ = "slices"
@@ -65,7 +63,7 @@ def upgrade():
if source != target:
slc.params = json.dumps(target, sort_keys=True)
except Exception as ex:
logger.warning(ex)
logging.warn(ex)
session.commit()
session.close()

View File

@@ -37,8 +37,6 @@ from superset.utils.dashboard_filter_scopes_converter import convert_filter_scop
revision = "3325d4caccc8"
down_revision = "e96dbf2cfef0"
logger = logging.getLogger("alembic.env")
Base = declarative_base()
@@ -88,7 +86,7 @@ def upgrade():
if filters:
filter_scopes = convert_filter_scopes(json_metadata, filters)
json_metadata["filter_scopes"] = filter_scopes
logger.info(
logging.info(
f"Adding filter_scopes for dashboard {dashboard.id}: {json.dumps(filter_scopes)}" # noqa: E501
)
@@ -102,7 +100,7 @@ def upgrade():
else:
dashboard.json_metadata = None
except Exception as ex:
logger.exception(f"dashboard {dashboard.id} has error: {ex}")
logging.exception(f"dashboard {dashboard.id} has error: {ex}")
session.commit()
session.close()

View File

@@ -38,8 +38,6 @@ from sqlalchemy_utils import UUIDType # noqa: E402
from superset import db # noqa: E402
logger = logging.getLogger("alembic.env")
add_uuid_column_to_import_mixin = import_module(
"superset.migrations.versions."
"2020-09-28_17-57_b56500de1855_add_uuid_column_to_import_mixin",
@@ -54,9 +52,9 @@ def has_uuid_column(table_name, bind):
columns = {column["name"] for column in inspector.get_columns(table_name)}
has_uuid_column = "uuid" in columns
if has_uuid_column:
logger.info("Table %s already has uuid column, skipping...", table_name)
logging.info("Table %s already has uuid column, skipping...", table_name)
else:
logger.info("Table %s doesn't have uuid column, adding...", table_name)
logging.info("Table %s doesn't have uuid column, adding...", table_name)
return has_uuid_column

View File

@@ -38,8 +38,6 @@ down_revision = "31b2a1039d4a"
Base = declarative_base()
logger = logging.getLogger("alembic.env")
class Database(Base):
__tablename__ = "dbs"
@@ -58,7 +56,7 @@ def upgrade():
try:
extra = json.loads(database.extra)
except json.JSONDecodeError as ex:
logger.warning(str(ex))
logging.warning(str(ex))
continue
schemas_allowed_for_csv_upload = extra.get("schemas_allowed_for_csv_upload")

View File

@@ -35,8 +35,6 @@ from sqlalchemy import func # noqa: E402
from superset import db # noqa: E402
from superset.connectors.sqla.models import SqlaTable # noqa: E402
logger = logging.getLogger("alembic.env")
def upgrade():
with op.batch_alter_table("tables") as batch_op:
@@ -64,12 +62,12 @@ def remove_value_if_too_long():
for row in rows:
row.fetch_values_predicate = None
logger.info("%d values deleted", len(rows))
logging.info("%d values deleted", len(rows))
session.commit()
session.close()
except Exception as ex:
logger.warning(ex)
logging.warning(ex)
def downgrade():

View File

@@ -37,7 +37,7 @@ from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
class Dashboard(Base):

View File

@@ -37,8 +37,6 @@ from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
class Database(Base):
__tablename__ = "dbs"
@@ -54,7 +52,7 @@ def upgrade():
try:
extra = json.loads(database.extra)
except json.JSONDecodeError as ex:
logger.warning(str(ex))
logging.warning(str(ex))
continue
if "schemas_allowed_for_csv_upload" in extra:
@@ -76,7 +74,7 @@ def downgrade():
try:
extra = json.loads(database.extra)
except json.JSONDecodeError as ex:
logger.warning(str(ex))
logging.warning(str(ex))
continue
if "schemas_allowed_for_file_upload" in extra:

View File

@@ -37,7 +37,7 @@ from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
class Slice(Base):

View File

@@ -38,7 +38,7 @@ from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
class Slice(Base):

View File

@@ -37,7 +37,7 @@ from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
logger = logging.getLogger(__name__)
class Slice(Base): # type: ignore

View File

@@ -38,7 +38,7 @@ from superset.migrations.shared.utils import paginated_update # noqa: E402
from superset.utils import json # noqa: E402
Base = declarative_base()
logger = logging.getLogger("alembic.env")
logger = logging.getLogger(__name__)
class Dashboard(Base):

View File

@@ -41,7 +41,7 @@ from superset.utils.date_parser import get_since_until
revision = "f84fde59123a"
down_revision = "9621c6d56ffb"
logger = logging.getLogger("alembic.env")
logger = logging.getLogger(__name__)
Base = declarative_base()

View File

@@ -34,7 +34,7 @@ from superset.utils.core import generic_find_uq_constraint_name
revision = "df3d7e2eb9a4"
down_revision = "48cbb571fa3a"
logger = logging.getLogger("alembic.env")
logger = logging.getLogger(__name__)
def upgrade():

View File

@@ -32,7 +32,8 @@ from sqlalchemy.ext.declarative import declarative_base
from superset import db
from superset.migrations.shared.utils import paginated_update
logger = logging.getLogger("alembic.env")
logger = logging.getLogger("alembic")
logger.setLevel(logging.INFO)
# revision identifiers, used by Alembic.
revision = "363a9b1e8992"

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,7 @@ SQLGLOT_DIALECTS = {
# "denodo": ???
"dremio": Dremio,
"drill": Dialects.DRILL,
# "druid": Dialects.DRUID, # DRUID dialect not available in current sqlglot version
"druid": Dialects.DRUID,
"duckdb": Dialects.DUCKDB,
# "dynamodb": ???
# "elasticsearch": ???

View File

@@ -19,6 +19,11 @@ from datetime import datetime
import pytz
EPOCH = datetime(1970, 1, 1)
# JavaScript's Date object can safely represent dates within this range
# These are the min/max values that can be represented as milliseconds since epoch
# without overflowing JavaScript's Number type (±2^53)
JS_DATE_RANGE_MIN = datetime(1938, 4, 24, 22, 13, 20, 0)
JS_DATE_RANGE_MAX = datetime(2286, 11, 20, 17, 46, 39, 999000)
def datetime_to_epoch(dttm: datetime) -> float:

View File

@@ -29,7 +29,12 @@ from jsonpath_ng import parse
from simplejson import JSONDecodeError
from superset.constants import PASSWORD_MASK
from superset.utils.dates import datetime_to_epoch, EPOCH
from superset.utils.dates import (
datetime_to_epoch,
EPOCH,
JS_DATE_RANGE_MAX,
JS_DATE_RANGE_MIN,
)
logging.getLogger("MARKDOWN").setLevel(logging.INFO)
logger = logging.getLogger(__name__)
@@ -155,9 +160,27 @@ def json_int_dttm_ser(obj: Any) -> Any:
"""
if isinstance(obj, (datetime, pd.Timestamp)):
# Check if datetime is within JavaScript's safe date range
# If not, return ISO string instead of epoch milliseconds
if isinstance(obj, pd.Timestamp):
dttm = obj.to_pydatetime()
else:
dttm = obj
# Remove timezone info for comparison
dttm_no_tz = dttm.replace(tzinfo=None) if dttm.tzinfo else dttm
if dttm_no_tz < JS_DATE_RANGE_MIN or dttm_no_tz > JS_DATE_RANGE_MAX:
# Return ISO string for dates outside JavaScript's safe range
return obj.isoformat()
return datetime_to_epoch(obj)
if isinstance(obj, date):
# Check if date is within JavaScript's safe date range
date_as_datetime = datetime.combine(obj, datetime.min.time())
if date_as_datetime < JS_DATE_RANGE_MIN or date_as_datetime > JS_DATE_RANGE_MAX:
return obj.isoformat()
return (obj - EPOCH.date()).total_seconds() * 1000
return base_json_conv(obj)

View File

@@ -411,7 +411,7 @@ class BaseViz: # pylint: disable=too-many-public-methods
"groupby": groupby,
"metrics": metrics,
"row_limit": row_limit,
"filters": self.form_data.get("filters", []),
"filter": self.form_data.get("filters", []),
"timeseries_limit": limit,
"extras": extras,
"timeseries_limit_metric": timeseries_limit_metric,

View File

@@ -29,8 +29,8 @@ query_birth_names = {
"row_limit": 100,
"granularity": "ds",
"time_range": "100 years ago : now",
"series_limit": 0,
"series_limit_metric": None,
"timeseries_limit": 0,
"timeseries_limit_metric": None,
"order_desc": True,
"filters": [
{"col": "gender", "op": "==", "val": "boy"},

View File

@@ -902,9 +902,6 @@ class TestPostChartDataApi(BaseTestChartDataApi):
request_payload["queries"][0]["columns"] = ["foo", "bar", "state"]
request_payload["queries"][0]["where"] = "':abc' != ':xyz:qwerty'"
request_payload["queries"][0]["orderby"] = None
request_payload["queries"][0]["granularity"] = (
None # Virtual table has no time column
)
request_payload["queries"][0]["metrics"] = [
{
"expressionType": AdhocMetricExpressionType.SQL,
@@ -1015,7 +1012,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
"orderby": [["sum__num", False]],
"annotation_layers": [],
"row_limit": 50000,
"series_limit": 0,
"timeseries_limit": 0,
"order_desc": True,
"url_params": {},
"custom_params": {},
@@ -1068,7 +1065,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
"orderby": [["sum__num", False]],
"annotation_layers": [],
"row_limit": 50000,
"series_limit": 0,
"timeseries_limit": 0,
"order_desc": True,
"url_params": {},
"custom_params": {},
@@ -1122,7 +1119,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
"orderby": [["sum__num", False]],
"annotation_layers": [],
"row_limit": 50000,
"series_limit": 0,
"timeseries_limit": 0,
"order_desc": True,
"url_params": {},
"custom_params": {},

View File

@@ -124,6 +124,17 @@ class TestDatasource(SupersetTestCase):
else:
return
query_obj = {
"columns": ["metric"],
"filter": [],
"from_dttm": datetime.now() - timedelta(days=1),
"granularity": "additional_dttm",
"orderby": [],
"to_dttm": datetime.now() + timedelta(days=1),
"series_columns": [],
"row_limit": 1000,
"row_offset": 0,
}
table = SqlaTable(
table_name="dummy_sql_table",
database=database,
@@ -138,28 +149,13 @@ class TestDatasource(SupersetTestCase):
sql=sql,
)
from superset.common.query_object import QueryObject
query_obj = QueryObject(
columns=["metric"],
filters=[],
from_dttm=datetime.now() - timedelta(days=1),
granularity="additional_dttm",
orderby=[],
to_dttm=datetime.now() + timedelta(days=1),
series_columns=[],
row_limit=1000,
row_offset=0,
datasource=table,
)
with create_and_cleanup_table(table):
table.always_filter_main_dttm = False
result = str(table.get_sqla_query(query_obj).sqla_query.whereclause)
result = str(table.get_sqla_query(**query_obj).sqla_query.whereclause)
assert "default_dttm" not in result and "additional_dttm" in result # noqa: PT018
table.always_filter_main_dttm = True
result = str(table.get_sqla_query(query_obj).sqla_query.whereclause)
result = str(table.get_sqla_query(**query_obj).sqla_query.whereclause)
assert "default_dttm" in result and "additional_dttm" in result # noqa: PT018
def test_external_metadata_for_virtual_table(self):
@@ -588,11 +584,7 @@ def test_get_samples_with_incorrect_cc(test_client, login_as_admin, virtual_data
)
rv = test_client.post(uri, json={})
assert rv.status_code == 422
# The error handling returns a simple error message for CommandInvalidError
assert "error" in rv.json
assert (
"DUMMY CC" in rv.json["error"]
) # Check the error mentions the problematic column
assert rv.json["errors"][0]["error_type"] == "INVALID_SQL_ERROR"
@with_feature_flags(ALLOW_ADHOC_SUBQUERY=True)

View File

@@ -186,6 +186,6 @@ def _get_energy_slices():
"xscale_interval": "1",
"yscale_interval": "1",
},
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"timeseries_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
},
]

View File

@@ -591,7 +591,7 @@ chart_config: dict[str, Any] = {
},
"viz_type": "deck_path",
},
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"timeseries_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
"cache_timeout": None,
"uuid": "0c23747a-6528-4629-97bf-e4b78d3b9df1",
"version": "1.0.0",

View File

@@ -294,17 +294,14 @@ class TestQueryContext(SupersetTestCase):
payload = get_query_context("birth_names")
columns = payload["queries"][0]["columns"]
payload["queries"][0]["groupby"] = columns
payload["queries"][0]["series_limit"] = 99
payload["queries"][0]["series_limit_metric"] = "sum__num"
payload["queries"][0]["timeseries_limit"] = 99
payload["queries"][0]["timeseries_limit_metric"] = "sum__num"
del payload["queries"][0]["columns"]
# Remove granularity so granularity_sqla can be used
del payload["queries"][0]["granularity"]
payload["queries"][0]["granularity_sqla"] = "timecol"
payload["queries"][0]["having_filters"] = [{"col": "a", "op": "==", "val": "b"}]
query_context = ChartDataQueryContextSchema().load(payload)
assert len(query_context.queries) == 1
query_object = query_context.queries[0]
# granularity should be set from granularity_sqla since granularity was not set
assert query_object.granularity == "timecol"
assert query_object.columns == columns
assert query_object.series_limit == 99
@@ -523,7 +520,7 @@ class TestQueryContext(SupersetTestCase):
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["groupby"] = ["name"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["series_limit"] = 5
payload["queries"][0]["timeseries_limit"] = 5
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
payload["queries"][0]["time_range"] = "1990 : 1991"
query_context = ChartDataQueryContextSchema().load(payload)
@@ -559,7 +556,7 @@ class TestQueryContext(SupersetTestCase):
# due to "name" is random generated, each time_offset slice will be empty
payload["queries"][0]["groupby"] = ["name"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["series_limit"] = 5
payload["queries"][0]["timeseries_limit"] = 5
payload["queries"][0]["time_offsets"] = []
payload["queries"][0]["time_range"] = "1990 : 1991"
payload["queries"][0]["granularity"] = "ds"
@@ -612,7 +609,7 @@ class TestQueryContext(SupersetTestCase):
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["groupby"] = ["state"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["series_limit"] = 5
payload["queries"][0]["timeseries_limit"] = 5
payload["queries"][0]["time_offsets"] = []
payload["queries"][0]["time_range"] = "1980 : 1991"
payload["queries"][0]["granularity"] = "ds"
@@ -641,11 +638,9 @@ class TestQueryContext(SupersetTestCase):
def test_time_offsets_accuracy(self):
payload = get_query_context("birth_names")
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["columns"] = [
"state"
] # Use columns instead of deprecated groupby
payload["queries"][0]["groupby"] = ["state"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["series_limit"] = 5
payload["queries"][0]["timeseries_limit"] = 5
payload["queries"][0]["time_offsets"] = []
payload["queries"][0]["time_range"] = "1980 : 1991"
payload["queries"][0]["granularity"] = "ds"
@@ -718,10 +713,10 @@ class TestQueryContext(SupersetTestCase):
"sqlExpression": "ds",
"label": "ds",
"expressionType": "SQL",
},
"name", # Add name to columns instead of using deprecated groupby
}
]
payload["queries"][0]["metrics"] = ["sum__num"]
payload["queries"][0]["groupby"] = ["name"]
payload["queries"][0]["is_timeseries"] = True
payload["queries"][0]["row_limit"] = 100
payload["queries"][0]["row_offset"] = 10

View File

@@ -33,7 +33,6 @@ from sqlalchemy.sql import text
from sqlalchemy.sql.elements import TextClause
from superset import db
from superset.common.query_object import QueryObject
from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric
from superset.constants import EMPTY_STRING, NULL_STRING
from superset.db_engine_specs.bigquery import BigQueryEngineSpec
@@ -163,7 +162,7 @@ class TestDatabaseModel(SupersetTestCase):
"count_timegrain",
],
"is_timeseries": False,
"filters": [],
"filter": [],
"extras": {"time_grain_sqla": "P1D"},
}
@@ -187,8 +186,7 @@ class TestDatabaseModel(SupersetTestCase):
)
db.session.commit()
query_object = QueryObject(datasource=table, **base_query_obj)
sqla_query = table.get_sqla_query(query_object)
sqla_query = table.get_sqla_query(**base_query_obj)
query = table.database.compile_sqla_query(sqla_query.sqla_query)
# assert virtual dataset
@@ -236,13 +234,12 @@ class TestDatabaseModel(SupersetTestCase):
},
],
"is_timeseries": False,
"filters": [],
"filter": [],
"extras": {"time_grain_sqla": "P1D"},
}
mock_dataset_id_from_context.return_value = table.id
query_object = QueryObject(datasource=table, **base_query_obj)
sqla_query = table.get_sqla_query(query_object)
sqla_query = table.get_sqla_query(**base_query_obj)
query = table.database.compile_sqla_query(sqla_query.sqla_query)
database = table.database
@@ -270,7 +267,7 @@ class TestDatabaseModel(SupersetTestCase):
}
],
"is_timeseries": False,
"filters": [],
"filter": [],
}
table = SqlaTable(
@@ -278,9 +275,8 @@ class TestDatabaseModel(SupersetTestCase):
)
db.session.commit()
query_object = QueryObject(datasource=table, **base_query_obj)
with pytest.raises(QueryObjectValidationError):
table.get_sqla_query(query_object)
table.get_sqla_query(**base_query_obj)
# Cleanup
db.session.delete(table)
db.session.commit()
@@ -314,7 +310,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["gender"],
"metrics": ["count"],
"is_timeseries": False,
"filters": [
"filter": [
{
"col": filter_.column,
"op": filter_.operator,
@@ -323,8 +319,7 @@ class TestDatabaseModel(SupersetTestCase):
],
"extras": {},
}
query_object = QueryObject(datasource=table, **query_obj)
sqla_query = table.get_sqla_query(query_object)
sqla_query = table.get_sqla_query(**query_obj)
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
if isinstance(filter_.expected, list):
assert any([candidate in sql for candidate in filter_.expected]) # noqa: C419
@@ -349,7 +344,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["boolean_gender"],
"metrics": ["count"],
"is_timeseries": False,
"filters": [
"filter": [
{
"col": "boolean_gender",
"op": FilterOperator.IN,
@@ -358,8 +353,7 @@ class TestDatabaseModel(SupersetTestCase):
],
"extras": {},
}
query_object = QueryObject(datasource=table, **query_obj)
sqla_query = table.get_sqla_query(query_object)
sqla_query = table.get_sqla_query(**query_obj)
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
dialect = table.database.get_dialect()
operand = "(true, false)"
@@ -377,7 +371,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["user"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
"extras": {},
}
@@ -389,9 +383,8 @@ class TestDatabaseModel(SupersetTestCase):
)
# TODO(villebro): make it work with presto
if get_example_database().backend != "presto":
query_object = QueryObject(datasource=table, **query_obj)
with pytest.raises(QueryObjectValidationError):
table.get_sqla_query(query_object)
table.get_sqla_query(**query_obj)
def test_query_format_strip_trailing_semicolon(self):
query_obj = {
@@ -401,7 +394,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["user"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
"extras": {},
}
@@ -410,8 +403,7 @@ class TestDatabaseModel(SupersetTestCase):
sql="SELECT * from test_table;",
database=get_example_database(),
)
query_object = QueryObject(datasource=table, **query_obj)
sqlaq = table.get_sqla_query(query_object)
sqlaq = table.get_sqla_query(**query_obj)
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
assert sql[-1] != ";"
@@ -423,7 +415,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["grp"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
}
table = SqlaTable(
@@ -433,9 +425,8 @@ class TestDatabaseModel(SupersetTestCase):
)
query_obj = dict(**base_query_obj, extras={})
query_object = QueryObject(datasource=table, **query_obj)
with pytest.raises(QueryObjectValidationError):
table.get_sqla_query(query_object)
table.get_sqla_query(**query_obj)
def test_dml_statement_raises_exception(self):
base_query_obj = {
@@ -445,7 +436,7 @@ class TestDatabaseModel(SupersetTestCase):
"groupby": ["grp"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
}
table = SqlaTable(
@@ -455,9 +446,8 @@ class TestDatabaseModel(SupersetTestCase):
)
query_obj = dict(**base_query_obj, extras={})
query_object = QueryObject(datasource=table, **query_obj)
with pytest.raises(QueryObjectValidationError):
table.get_sqla_query(query_object)
table.get_sqla_query(**query_obj)
def test_fetch_metadata_for_updated_virtual_table(self):
table = SqlaTable(
@@ -517,7 +507,7 @@ class TestDatabaseModel(SupersetTestCase):
}
],
"is_timeseries": False,
"filters": [],
"filter": [],
"extras": {},
}
@@ -526,8 +516,7 @@ class TestDatabaseModel(SupersetTestCase):
db.session.add(database)
db.session.add(table)
db.session.commit()
query_object = QueryObject(datasource=table, **query_obj)
sqlaq = table.get_sqla_query(query_object)
sqlaq = table.get_sqla_query(**query_obj)
assert sqlaq.labels_expected == ["user", "COUNT_DISTINCT(user)"]
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
assert "COUNT_DISTINCT_user__00db1" in sql
@@ -596,7 +585,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
"filter": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
"is_timeseries": False,
}
)
@@ -606,7 +595,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [{"col": "foo", "val": [None], "op": "IN"}],
"filter": [{"col": "foo", "val": [None], "op": "IN"}],
"is_timeseries": False,
}
)
@@ -616,7 +605,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
"filter": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
"is_timeseries": False,
}
)
@@ -626,7 +615,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [{"col": "foo", "val": [""], "op": "IN"}],
"filter": [{"col": "foo", "val": [""], "op": "IN"}],
"is_timeseries": False,
}
)
@@ -636,7 +625,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [
"filter": [
{
"col": "foo",
"val": [EMPTY_STRING, NULL_STRING, "null", "foo"],
@@ -652,7 +641,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [
"filter": [
{
"col": "foo",
"val": ['"text in double quotes"'],
@@ -668,7 +657,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [
"filter": [
{
"col": "foo",
"val": ["'text in single quotes'"],
@@ -684,7 +673,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [
"filter": [
{
"col": "foo",
"val": ['double quotes " in text'],
@@ -700,7 +689,7 @@ def test_filter_on_text_column(text_column_table):
result_object = table.query(
{
"metrics": ["count"],
"filters": [
"filter": [
{
"col": "foo",
"val": ["single quotes ' in text"],
@@ -737,7 +726,7 @@ def test_should_generate_closed_and_open_time_filter_range(login_as_admin):
{
"metrics": ["count"],
"is_timeseries": False,
"filters": [],
"filter": [],
"from_dttm": datetime(2022, 1, 1),
"to_dttm": datetime(2023, 1, 1),
"granularity": "datetime_col",
@@ -774,7 +763,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
result = physical_dataset.query(
{
"metrics": ["count"],
"filters": [{"col": "col4", "val": None, "op": expected["operator"]}],
"filter": [{"col": "col4", "val": None, "op": expected["operator"]}],
"is_timeseries": False,
}
)
@@ -793,7 +782,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
physical_dataset.query(
{
"metrics": ["count"],
"filters": [{"col": "col4", "val": None, "op": flt.value}],
"filter": [{"col": "col4", "val": None, "op": flt.value}],
"is_timeseries": False,
}
)
@@ -882,7 +871,7 @@ def test_extra_cache_keys(
"groupby": ["id", "username", "email"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
}
query_obj = dict(**base_query_obj, extras={})
@@ -928,7 +917,7 @@ def test_extra_cache_keys_in_sql_expression(
"groupby": ["id", "username", "email"],
"metrics": [],
"is_timeseries": False,
"filters": [],
"filter": [],
}
query_obj = dict(**base_query_obj, extras={"where": sql_expression})
@@ -971,7 +960,7 @@ def test_extra_cache_keys_in_adhoc_metrics_and_columns(
"metrics": [],
"columns": [],
"is_timeseries": False,
"filters": [],
"filter": [],
}
items: dict[str, Any] = {
@@ -1025,7 +1014,7 @@ def test_extra_cache_keys_in_dataset_metrics_and_columns(
"columns": ["username"],
"metrics": ["variable_profit"],
"is_timeseries": False,
"filters": [],
"filter": [],
}
extra_cache_keys = table.get_extra_cache_keys(query_obj)
@@ -1129,7 +1118,7 @@ def test__temporal_range_operator_in_adhoc_filter(physical_dataset):
result = physical_dataset.query(
{
"columns": ["col1", "col2"],
"filters": [
"filter": [
{
"col": "col5",
"val": "2000-01-05 : 2000-01-06",

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +0,0 @@
# 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 datetime import datetime
from superset.common.query_object import QueryObject
def test_get_series_limit_prequery_obj():
"""
Test get_series_limit_prequery_obj method
"""
# Create a QueryObject with series limit settings
query_object = QueryObject(
columns=["country", "year"],
metrics=["sum__sales"],
series_limit=10,
from_dttm=datetime(2020, 1, 1),
to_dttm=datetime(2021, 1, 1),
filters=[{"col": "region", "op": "IN", "val": ["US", "EU"]}],
extras={"time_grain_sqla": "P1D"},
order_desc=False,
)
# Test basic prequery object creation
prequery_obj = query_object.get_series_limit_prequery_obj(
granularity="ds",
inner_from_dttm=None,
inner_to_dttm=None,
)
assert prequery_obj["is_timeseries"] is False
assert prequery_obj["row_limit"] == 10
assert prequery_obj["metrics"] == ["sum__sales"]
assert prequery_obj["granularity"] == "ds"
assert prequery_obj["groupby"] == ["country", "year"]
assert prequery_obj["from_dttm"] == datetime(2020, 1, 1)
assert prequery_obj["to_dttm"] == datetime(2021, 1, 1)
assert prequery_obj["filter"] == [
{"col": "region", "op": "IN", "val": ["US", "EU"]}
]
assert prequery_obj["orderby"] == []
assert prequery_obj["extras"] == {"time_grain_sqla": "P1D"}
assert prequery_obj["order_desc"] is True # Always True for prequery
def test_get_series_limit_prequery_obj_with_overrides():
"""
Test get_series_limit_prequery_obj with inner dates and orderby override
"""
query_object = QueryObject(
columns=["country"],
metrics=["count"],
series_limit=5,
from_dttm=datetime(2020, 1, 1),
to_dttm=datetime(2021, 1, 1),
)
# Test with inner dates and custom orderby
inner_from = datetime(2020, 6, 1)
inner_to = datetime(2020, 12, 31)
custom_orderby = [("sum__revenue", False)]
prequery_obj = query_object.get_series_limit_prequery_obj(
granularity="date_col",
inner_from_dttm=inner_from,
inner_to_dttm=inner_to,
orderby=custom_orderby,
)
assert prequery_obj["from_dttm"] == inner_from
assert prequery_obj["to_dttm"] == inner_to
assert prequery_obj["orderby"] == custom_orderby
def test_get_series_limit_prequery_obj_base_axis_filtering():
"""
Test that base axis columns are filtered out in prequery
"""
# Mock the x-axis column with proper structure for base axis
query_object = QueryObject(
columns=[
{
"label": "__timestamp",
"sqlExpression": "__timestamp",
"columnType": "BASE_AXIS",
},
"country",
"city",
],
metrics=["revenue"],
series_limit=20,
)
prequery_obj = query_object.get_series_limit_prequery_obj(
granularity=None,
inner_from_dttm=None,
inner_to_dttm=None,
)
# The columns in prequery should exclude the base axis column
assert prequery_obj["columns"] == ["country", "city"]

View File

@@ -254,6 +254,23 @@ def test_json_int_dttm_ser():
assert json.json_int_dttm_ser(dttm + timedelta(milliseconds=1)) == (ts + 1)
assert json.json_int_dttm_ser(np.int64(1)) == 1
# Test edge cases for JavaScript Date range
# Dates outside JavaScript's safe range should return ISO strings
assert (
json.json_int_dttm_ser(datetime(1938, 4, 24, 22, 13, 19))
== "1938-04-24T22:13:19"
)
assert (
json.json_int_dttm_ser(datetime(2286, 11, 20, 17, 46, 40))
== "2286-11-20T17:46:40"
)
assert json.json_int_dttm_ser(date(1938, 4, 23)) == "1938-04-23"
assert json.json_int_dttm_ser(date(2286, 11, 21)) == "2286-11-21"
# Dates within JavaScript's safe range should return epoch milliseconds
assert isinstance(json.json_int_dttm_ser(datetime(2000, 1, 1)), (int, float))
assert isinstance(json.json_int_dttm_ser(date(2000, 1, 1)), (int, float))
with pytest.raises(TypeError):
json.json_int_dttm_ser(np.datetime64())