mirror of
https://github.com/apache/superset.git
synced 2026-06-15 12:39:16 +00:00
Compare commits
21 Commits
example_th
...
refactor_q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398842a4d8 | ||
|
|
aff7f54b1a | ||
|
|
288da4a050 | ||
|
|
2d8ae42d42 | ||
|
|
990174bb1c | ||
|
|
6f8a79693d | ||
|
|
b8a71e4754 | ||
|
|
13e7ba18ed | ||
|
|
c5887630ab | ||
|
|
c11efecdad | ||
|
|
3dc97b11f8 | ||
|
|
b81487e177 | ||
|
|
72e33ba811 | ||
|
|
b0715bd8bb | ||
|
|
0348b6c313 | ||
|
|
453b3da9f6 | ||
|
|
7c6c0c0451 | ||
|
|
bf43704200 | ||
|
|
fef0676954 | ||
|
|
7485af5e6c | ||
|
|
825b9e784a |
53
superset-frontend/package-lock.json
generated
53
superset-frontend/package-lock.json
generated
@@ -61,7 +61,7 @@
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
@@ -140,7 +140,7 @@
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
@@ -175,7 +175,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -263,7 +263,7 @@
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.99.9",
|
||||
@@ -1086,9 +1086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
|
||||
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3989,18 +3989,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@deck.gl/core": {
|
||||
"version": "9.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.1.14.tgz",
|
||||
"integrity": "sha512-tXakSSvi5g+EvxSsnnjoRO8z3XxHxISTRzzIqcs3AZuWHnDptK28y9iD0Da21ILop1IYLaWE1QTUe6IAdp/Wag==",
|
||||
"version": "9.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.1.13.tgz",
|
||||
"integrity": "sha512-c15DpwUEvDjmt3+/azSjcfhVQ5L5HiIj6LJob1KAwQOnB5zgVdKWukN/21ELQ7ekppEkfT0x4byRv5k4QVocqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@loaders.gl/core": "^4.2.0",
|
||||
"@loaders.gl/images": "^4.2.0",
|
||||
"@luma.gl/constants": "~9.1.9",
|
||||
"@luma.gl/core": "~9.1.9",
|
||||
"@luma.gl/engine": "~9.1.9",
|
||||
"@luma.gl/shadertools": "~9.1.9",
|
||||
"@luma.gl/webgl": "~9.1.9",
|
||||
"@luma.gl/constants": "^9.1.5",
|
||||
"@luma.gl/core": "^9.1.5",
|
||||
"@luma.gl/engine": "^9.1.5",
|
||||
"@luma.gl/shadertools": "^9.1.5",
|
||||
"@luma.gl/webgl": "^9.1.5",
|
||||
"@math.gl/core": "^4.1.0",
|
||||
"@math.gl/sun": "^4.1.0",
|
||||
"@math.gl/types": "^4.1.0",
|
||||
@@ -15253,10 +15253,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/classnames": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.4.tgz",
|
||||
"integrity": "sha512-dwmfrMMQb9ujX1uYGvB5ERDlOzBNywnZAZBtOe107/hORWP05ESgU4QyaanZMWYYfd2BzrG78y13/Bju8IQcMQ==",
|
||||
"deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.0.tgz",
|
||||
"integrity": "sha512-3GsbOoDYteFShlrBTKzI2Eii4vPg/jAf7LXRIn0WQePKlmhpkV0KoTMuawA7gZJkrbPrZGwv9IEAfIWaOaQK8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"classnames": "*"
|
||||
@@ -24129,9 +24128,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-to-image-more": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.6.0.tgz",
|
||||
"integrity": "sha512-0BB0M9gRRP7znKBNLRAvNyWnkDIzSgMSDcS7WdPDzPnWhW2YJqxUR/dCHiJ2HdCV3K2rVky5Vba8UF31mvrCuQ==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-to-image-more/-/dom-to-image-more-3.5.0.tgz",
|
||||
"integrity": "sha512-VF/vwfHsPNMHJb5W/5sAmco3UIlEWSEFLppInQwqwN4joUvBULDwE3CqVcUDkUWleke/nZ5KwIVSrrFlGw7WPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-to-pdf": {
|
||||
@@ -54977,9 +54976,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"version": "4.19.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
|
||||
"integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -61644,7 +61643,7 @@
|
||||
"@storybook/types": "8.4.7",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"core-js": "3.40.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"memoize-one": "^5.2.1",
|
||||
"react": "^17.0.2",
|
||||
@@ -62585,7 +62584,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/core": "^9.1.14",
|
||||
"@deck.gl/core": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.13",
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
@@ -208,7 +208,7 @@
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
@@ -243,7 +243,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -331,7 +331,7 @@
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.99.9",
|
||||
|
||||
@@ -57,206 +57,5 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
|
||||
},
|
||||
algorithm: ThemeAlgorithm.DARK,
|
||||
},
|
||||
claudette: {
|
||||
algorithm: 'dark',
|
||||
token: {
|
||||
colorPrimary: '#C15F3C',
|
||||
colorPrimaryHover: '#d16b48',
|
||||
colorPrimaryActive: '#a84f30',
|
||||
colorBgBase: '#1a1a1a',
|
||||
colorBgContainer: '#2a2a2a',
|
||||
colorBgElevated: '#323232',
|
||||
colorBgLayout: '#0f0f0f',
|
||||
colorBgSpotlight: '#323232',
|
||||
colorText: '#F4F3EE',
|
||||
colorTextSecondary: '#B1ADA1',
|
||||
colorTextTertiary: '#8a8680',
|
||||
colorTextQuaternary: '#6b6862',
|
||||
colorTextPlaceholder: '#B1ADA1',
|
||||
colorTextLightSolid: '#F4F3EE',
|
||||
colorBorder: '#404040',
|
||||
colorBorderSecondary: '#303030',
|
||||
colorFill: '#404040',
|
||||
colorFillSecondary: '#353535',
|
||||
colorFillTertiary: '#2a2a2a',
|
||||
colorFillQuaternary: '#1f1f1f',
|
||||
colorFillAlter: '#323232',
|
||||
colorIcon: '#B1ADA1',
|
||||
colorIconHover: '#F4F3EE',
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu'",
|
||||
fontFamilyCode:
|
||||
"'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||
fontSize: 14,
|
||||
borderRadius: 6,
|
||||
borderRadiusLG: 8,
|
||||
wireframe: false,
|
||||
},
|
||||
},
|
||||
figmate: {
|
||||
algorithm: 'light',
|
||||
token: {
|
||||
colorPrimary: '#0d99ff',
|
||||
colorPrimaryHover: '#3dadff',
|
||||
colorPrimaryActive: '#0085e6',
|
||||
colorInfo: '#4c74f4',
|
||||
colorInfoHover: '#6b8af5',
|
||||
colorInfoActive: '#2d5af2',
|
||||
colorSuccess: '#00d2aa',
|
||||
colorSuccessHover: '#1adbba',
|
||||
colorSuccessActive: '#00c299',
|
||||
colorWarning: '#ffad33',
|
||||
colorWarningHover: '#ffbf5c',
|
||||
colorWarningActive: '#ff9900',
|
||||
colorError: '#ff5757',
|
||||
colorErrorHover: '#ff7a7a',
|
||||
colorErrorActive: '#ff3333',
|
||||
colorBgBase: '#ffffff',
|
||||
colorBgContainer: '#fafafa',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f5f5f5',
|
||||
colorText: '#1a1a1a',
|
||||
colorTextSecondary: '#666666',
|
||||
colorTextTertiary: '#999999',
|
||||
colorTextQuaternary: '#cccccc',
|
||||
colorTextPlaceholder: '#999999',
|
||||
colorBorder: '#e6e6e6',
|
||||
colorBorderSecondary: '#f0f0f0',
|
||||
colorFill: '#f0f0f0',
|
||||
colorFillSecondary: '#f5f5f5',
|
||||
colorFillTertiary: '#fafafa',
|
||||
colorFillQuaternary: '#ffffff',
|
||||
colorIcon: '#666666',
|
||||
colorIconHover: '#333333',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 6,
|
||||
lineHeight: 1.5,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
hubert: {
|
||||
algorithm: 'dark',
|
||||
token: {
|
||||
colorPrimary: '#276EF1',
|
||||
colorPrimaryHover: '#4285f4',
|
||||
colorPrimaryActive: '#1557d6',
|
||||
colorInfo: '#276EF1',
|
||||
colorInfoHover: '#4285f4',
|
||||
colorInfoActive: '#1557d6',
|
||||
colorSuccess: '#3AA76D',
|
||||
colorSuccessHover: '#4db87d',
|
||||
colorSuccessActive: '#2d9657',
|
||||
colorWarning: '#FFC043',
|
||||
colorWarningHover: '#ffcd66',
|
||||
colorWarningActive: '#e6ac26',
|
||||
colorError: '#D44333',
|
||||
colorErrorHover: '#dd5a4c',
|
||||
colorErrorActive: '#bf3526',
|
||||
colorBgBase: '#000000',
|
||||
colorBgContainer: '#1a1a1a',
|
||||
colorBgElevated: '#2a2a2a',
|
||||
colorBgLayout: '#000000',
|
||||
colorBgSpotlight: '#333333',
|
||||
colorText: '#ffffff',
|
||||
colorTextSecondary: '#cccccc',
|
||||
colorTextTertiary: '#999999',
|
||||
colorTextQuaternary: '#666666',
|
||||
colorTextPlaceholder: '#999999',
|
||||
colorTextLightSolid: '#ffffff',
|
||||
colorBorder: '#333333',
|
||||
colorBorderSecondary: '#1a1a1a',
|
||||
colorFill: '#1a1a1a',
|
||||
colorFillSecondary: '#2a2a2a',
|
||||
colorFillTertiary: '#333333',
|
||||
colorFillQuaternary: '#404040',
|
||||
colorFillAlter: '#2a2a2a',
|
||||
colorIcon: '#cccccc',
|
||||
colorIconHover: '#ffffff',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
fontWeightStrong: 600,
|
||||
borderRadius: 4,
|
||||
borderRadiusLG: 6,
|
||||
borderRadiusSM: 2,
|
||||
lineHeight: 1.4,
|
||||
lineWidthBold: 2,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
bnb: {
|
||||
algorithm: 'light',
|
||||
token: {
|
||||
colorPrimary: '#FF5A5F',
|
||||
colorPrimaryHover: '#FF7479',
|
||||
colorPrimaryActive: '#E5464B',
|
||||
colorInfo: '#29696B',
|
||||
colorInfoHover: '#3D7D7F',
|
||||
colorInfoActive: '#1F5557',
|
||||
colorSuccess: '#5BCACE',
|
||||
colorSuccessHover: '#7DD4D8',
|
||||
colorSuccessActive: '#49B6BA',
|
||||
colorWarning: '#F4B02A',
|
||||
colorWarningHover: '#F6C054',
|
||||
colorWarningActive: '#E2A01E',
|
||||
colorError: '#C32F0E',
|
||||
colorErrorHover: '#D54428',
|
||||
colorErrorActive: '#A8280C',
|
||||
colorBgBase: '#ffffff',
|
||||
colorBgContainer: '#fafafa',
|
||||
colorBgElevated: '#ffffff',
|
||||
colorBgLayout: '#f7f7f7',
|
||||
colorText: '#222222',
|
||||
colorTextSecondary: '#484848',
|
||||
colorTextTertiary: '#767676',
|
||||
colorTextQuaternary: '#b0b0b0',
|
||||
colorTextPlaceholder: '#767676',
|
||||
colorBorder: '#e8e8e8',
|
||||
colorBorderSecondary: '#f0f0f0',
|
||||
colorFill: '#f7f7f7',
|
||||
colorFillSecondary: '#fafafa',
|
||||
colorFillTertiary: '#fcfcfc',
|
||||
colorFillQuaternary: '#ffffff',
|
||||
colorIcon: '#767676',
|
||||
colorIconHover: '#484848',
|
||||
fontFamily:
|
||||
'Circular, Circular Air Pro, Airbnb Cereal, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto',
|
||||
fontFamilyCode:
|
||||
'"SF Mono", Monaco, Inconsolata, "Roboto Mono", monospace',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
fontSizeXL: 20,
|
||||
fontWeightStrong: 600,
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 4,
|
||||
lineHeight: 1.5,
|
||||
wireframe: false,
|
||||
motion: true,
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
},
|
||||
};
|
||||
export default exampleThemes;
|
||||
|
||||
@@ -49,21 +49,6 @@ describe('isProbablyHTML', () => {
|
||||
const trickyText = 'a <= 10 and b > 10';
|
||||
expect(isProbablyHTML(trickyText)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for strings with angle brackets that are not HTML', () => {
|
||||
// Test case from issue #25561
|
||||
expect(isProbablyHTML('<abcdef:12345>')).toBe(false);
|
||||
|
||||
// Other similar cases
|
||||
expect(isProbablyHTML('<foo:bar>')).toBe(false);
|
||||
expect(isProbablyHTML('<123>')).toBe(false);
|
||||
expect(isProbablyHTML('<test@example.com>')).toBe(false);
|
||||
expect(isProbablyHTML('<custom-element>')).toBe(false);
|
||||
|
||||
// Mathematical expressions
|
||||
expect(isProbablyHTML('if x < 5 and y > 10')).toBe(false);
|
||||
expect(isProbablyHTML('price < $100')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeHtmlIfNeeded', () => {
|
||||
|
||||
@@ -68,87 +68,9 @@ export function isProbablyHTML(text: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the string contains common HTML patterns
|
||||
if (!hasHtmlTagPattern(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(cleanedStr, 'text/html');
|
||||
|
||||
// Check if parsing created actual HTML elements (not just text nodes)
|
||||
const elements = Array.from(doc.body.childNodes).filter(
|
||||
node => node.nodeType === 1,
|
||||
) as Element[];
|
||||
|
||||
// If no elements were created, it's not HTML
|
||||
if (elements.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the elements are known HTML tags (not custom/unknown tags)
|
||||
// This prevents strings like "<abcdef:12345>" from being treated as HTML
|
||||
return elements.some(element => {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
// List of common HTML tags we want to recognize
|
||||
const knownHtmlTags = [
|
||||
'div',
|
||||
'span',
|
||||
'p',
|
||||
'a',
|
||||
'b',
|
||||
'i',
|
||||
'u',
|
||||
'em',
|
||||
'strong',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'th',
|
||||
'tbody',
|
||||
'thead',
|
||||
'tfoot',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'img',
|
||||
'br',
|
||||
'hr',
|
||||
'pre',
|
||||
'code',
|
||||
'blockquote',
|
||||
'section',
|
||||
'article',
|
||||
'nav',
|
||||
'header',
|
||||
'footer',
|
||||
'form',
|
||||
'input',
|
||||
'button',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'label',
|
||||
'fieldset',
|
||||
'legend',
|
||||
'video',
|
||||
'audio',
|
||||
'canvas',
|
||||
'iframe',
|
||||
'script',
|
||||
'style',
|
||||
'link',
|
||||
'meta',
|
||||
'title',
|
||||
];
|
||||
return knownHtmlTags.includes(tagName);
|
||||
});
|
||||
return Array.from(doc.body.childNodes).some(({ nodeType }) => nodeType === 1);
|
||||
}
|
||||
|
||||
export function sanitizeHtmlIfNeeded(htmlString: string) {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@storybook/types": "8.4.7",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"core-js": "3.40.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"memoize-one": "^5.2.1",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/core": "^9.1.14",
|
||||
"@deck.gl/core": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/layers": "^9.1.13",
|
||||
"@deck.gl/react": "^9.1.13",
|
||||
|
||||
@@ -145,7 +145,7 @@ def _get_samples(
|
||||
query_obj = copy.copy(query_obj)
|
||||
query_obj.is_timeseries = False
|
||||
query_obj.orderby = []
|
||||
query_obj.metrics = None
|
||||
query_obj.metrics = []
|
||||
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 = None
|
||||
query_obj.metrics = []
|
||||
query_obj.post_processing = []
|
||||
qry_obj_cols = []
|
||||
for o in datasource.columns:
|
||||
|
||||
@@ -86,6 +86,8 @@ 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
|
||||
@@ -94,7 +96,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
inner_to_dttm: datetime | None
|
||||
is_rowcount: bool
|
||||
is_timeseries: bool
|
||||
metrics: list[Metric] | None
|
||||
metrics: list[Metric]
|
||||
order_desc: bool
|
||||
orderby: list[OrderBy]
|
||||
post_processing: list[dict[str, Any]]
|
||||
@@ -141,6 +143,30 @@ 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
|
||||
@@ -192,9 +218,12 @@ 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)
|
||||
|
||||
self.metrics = metrics and [
|
||||
# Track whether metrics was originally None (for need_groupby logic)
|
||||
self._metrics_is_not_none = metrics is not None
|
||||
|
||||
self.metrics = [
|
||||
x if is_str_or_adhoc(x) else x["label"] # type: ignore
|
||||
for x in metrics
|
||||
for x in (metrics or [])
|
||||
]
|
||||
|
||||
def _set_post_processing(
|
||||
@@ -226,15 +255,20 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
field.new_name,
|
||||
)
|
||||
value = kwargs[field.old_name]
|
||||
if value:
|
||||
if hasattr(self, field.new_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
|
||||
logger.warning(
|
||||
"The field `%s` is already populated, "
|
||||
"replacing value with contents from `%s`.",
|
||||
"not replacing with contents from deprecated `%s`.",
|
||||
field.new_name,
|
||||
field.old_name,
|
||||
)
|
||||
setattr(self, field.new_name, value)
|
||||
else:
|
||||
setattr(self, field.new_name, value)
|
||||
|
||||
def _move_deprecated_extra_fields(self, kwargs: dict[str, Any]) -> None:
|
||||
# move deprecated extras fields to extras
|
||||
@@ -247,8 +281,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
field.new_name,
|
||||
)
|
||||
value = kwargs[field.old_name]
|
||||
if value:
|
||||
if hasattr(self.extras, field.new_name):
|
||||
if value is not None and value != "": # Don't add empty string values
|
||||
if field.new_name in self.extras:
|
||||
logger.warning(
|
||||
"The field `%s` is already populated in "
|
||||
"`extras`, replacing value with contents "
|
||||
@@ -262,7 +296,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 or [],
|
||||
self.metrics,
|
||||
(
|
||||
self.datasource.verbose_map
|
||||
if self.datasource and hasattr(self.datasource, "verbose_map")
|
||||
@@ -276,6 +310,428 @@ 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:
|
||||
@@ -350,7 +806,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,
|
||||
"metrics": self.metrics if self.metrics else None,
|
||||
"order_desc": self.order_desc,
|
||||
"orderby": self.orderby,
|
||||
"row_limit": self.row_limit,
|
||||
|
||||
@@ -71,6 +71,7 @@ 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,
|
||||
@@ -720,7 +721,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["filter"][0]["val"])
|
||||
qry = qry.filter(Annotation.layer_id == query_obj["filters"][0]["val"])
|
||||
if query_obj["from_dttm"]:
|
||||
qry = qry.filter(Annotation.start_dttm >= query_obj["from_dttm"])
|
||||
if query_obj["to_dttm"]:
|
||||
@@ -1514,18 +1515,19 @@ class SqlaTable(
|
||||
def _get_series_orderby(
|
||||
self,
|
||||
series_limit_metric: Metric,
|
||||
metrics_by_name: dict[str, SqlMetric],
|
||||
columns_by_name: dict[str, TableColumn],
|
||||
query_obj: QueryObject,
|
||||
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, columns_by_name)
|
||||
ob = self.adhoc_metric_to_sqla(
|
||||
series_limit_metric, query_obj.columns_by_name
|
||||
)
|
||||
elif (
|
||||
isinstance(series_limit_metric, str)
|
||||
and series_limit_metric in metrics_by_name
|
||||
and series_limit_metric in query_obj.metrics_by_name
|
||||
):
|
||||
ob = metrics_by_name[series_limit_metric].get_sqla_col(
|
||||
ob = query_obj.metrics_by_name[series_limit_metric].get_sqla_col(
|
||||
template_processor=template_processor
|
||||
)
|
||||
else:
|
||||
@@ -1857,7 +1859,10 @@ class SqlaTable(
|
||||
"""
|
||||
extra_cache_keys = super().get_extra_cache_keys(query_obj)
|
||||
if self.has_extra_cache_key_calls(query_obj):
|
||||
sqla_query = self.get_sqla_query(**query_obj)
|
||||
from superset.common.query_object import QueryObject
|
||||
|
||||
query_object = QueryObject(datasource=self, **query_obj)
|
||||
sqla_query = self.get_sqla_query(query_object)
|
||||
extra_cache_keys += sqla_query.extra_cache_keys
|
||||
return list(set(extra_cache_keys))
|
||||
|
||||
|
||||
@@ -940,7 +940,7 @@ def dataset_macro(
|
||||
metrics = [metric.metric_name for metric in dataset.metrics]
|
||||
query_obj = {
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"metrics": metrics if include_metrics else None,
|
||||
"columns": columns,
|
||||
"from_dttm": from_dttm,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,7 @@ SQLGLOT_DIALECTS = {
|
||||
# "denodo": ???
|
||||
"dremio": Dremio,
|
||||
"drill": Dialects.DRILL,
|
||||
"druid": Dialects.DRUID,
|
||||
# "druid": Dialects.DRUID, # DRUID dialect not available in current sqlglot version
|
||||
"duckdb": Dialects.DUCKDB,
|
||||
# "dynamodb": ???
|
||||
# "elasticsearch": ???
|
||||
|
||||
@@ -411,7 +411,7 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||
"groupby": groupby,
|
||||
"metrics": metrics,
|
||||
"row_limit": row_limit,
|
||||
"filter": self.form_data.get("filters", []),
|
||||
"filters": self.form_data.get("filters", []),
|
||||
"timeseries_limit": limit,
|
||||
"extras": extras,
|
||||
"timeseries_limit_metric": timeseries_limit_metric,
|
||||
|
||||
@@ -29,8 +29,8 @@ query_birth_names = {
|
||||
"row_limit": 100,
|
||||
"granularity": "ds",
|
||||
"time_range": "100 years ago : now",
|
||||
"timeseries_limit": 0,
|
||||
"timeseries_limit_metric": None,
|
||||
"series_limit": 0,
|
||||
"series_limit_metric": None,
|
||||
"order_desc": True,
|
||||
"filters": [
|
||||
{"col": "gender", "op": "==", "val": "boy"},
|
||||
|
||||
@@ -902,6 +902,9 @@ 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,
|
||||
@@ -1012,7 +1015,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
@@ -1065,7 +1068,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
@@ -1119,7 +1122,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
|
||||
@@ -124,17 +124,6 @@ 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,
|
||||
@@ -149,13 +138,28 @@ 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):
|
||||
@@ -584,7 +588,11 @@ 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
|
||||
assert rv.json["errors"][0]["error_type"] == "INVALID_SQL_ERROR"
|
||||
# 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
|
||||
|
||||
|
||||
@with_feature_flags(ALLOW_ADHOC_SUBQUERY=True)
|
||||
|
||||
@@ -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,"timeseries_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,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,"timeseries_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,"series_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",
|
||||
|
||||
@@ -294,14 +294,17 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload = get_query_context("birth_names")
|
||||
columns = payload["queries"][0]["columns"]
|
||||
payload["queries"][0]["groupby"] = columns
|
||||
payload["queries"][0]["timeseries_limit"] = 99
|
||||
payload["queries"][0]["timeseries_limit_metric"] = "sum__num"
|
||||
payload["queries"][0]["series_limit"] = 99
|
||||
payload["queries"][0]["series_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
|
||||
@@ -520,7 +523,7 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["name"]
|
||||
payload["queries"][0]["is_timeseries"] = True
|
||||
payload["queries"][0]["timeseries_limit"] = 5
|
||||
payload["queries"][0]["series_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)
|
||||
@@ -556,7 +559,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]["timeseries_limit"] = 5
|
||||
payload["queries"][0]["series_limit"] = 5
|
||||
payload["queries"][0]["time_offsets"] = []
|
||||
payload["queries"][0]["time_range"] = "1990 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -609,7 +612,7 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["state"]
|
||||
payload["queries"][0]["is_timeseries"] = True
|
||||
payload["queries"][0]["timeseries_limit"] = 5
|
||||
payload["queries"][0]["series_limit"] = 5
|
||||
payload["queries"][0]["time_offsets"] = []
|
||||
payload["queries"][0]["time_range"] = "1980 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -638,9 +641,11 @@ class TestQueryContext(SupersetTestCase):
|
||||
def test_time_offsets_accuracy(self):
|
||||
payload = get_query_context("birth_names")
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["state"]
|
||||
payload["queries"][0]["columns"] = [
|
||||
"state"
|
||||
] # Use columns instead of deprecated groupby
|
||||
payload["queries"][0]["is_timeseries"] = True
|
||||
payload["queries"][0]["timeseries_limit"] = 5
|
||||
payload["queries"][0]["series_limit"] = 5
|
||||
payload["queries"][0]["time_offsets"] = []
|
||||
payload["queries"][0]["time_range"] = "1980 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -713,10 +718,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
|
||||
|
||||
@@ -33,6 +33,7 @@ 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
|
||||
@@ -162,7 +163,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"count_timegrain",
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {"time_grain_sqla": "P1D"},
|
||||
}
|
||||
|
||||
@@ -186,7 +187,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
sqla_query = table.get_sqla_query(**base_query_obj)
|
||||
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||
sqla_query = table.get_sqla_query(query_object)
|
||||
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||
|
||||
# assert virtual dataset
|
||||
@@ -234,12 +236,13 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
},
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {"time_grain_sqla": "P1D"},
|
||||
}
|
||||
mock_dataset_id_from_context.return_value = table.id
|
||||
|
||||
sqla_query = table.get_sqla_query(**base_query_obj)
|
||||
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||
sqla_query = table.get_sqla_query(query_object)
|
||||
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||
|
||||
database = table.database
|
||||
@@ -267,7 +270,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
}
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -275,8 +278,9 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||
with pytest.raises(QueryObjectValidationError):
|
||||
table.get_sqla_query(**base_query_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
# Cleanup
|
||||
db.session.delete(table)
|
||||
db.session.commit()
|
||||
@@ -310,7 +314,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["gender"],
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": filter_.column,
|
||||
"op": filter_.operator,
|
||||
@@ -319,7 +323,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
],
|
||||
"extras": {},
|
||||
}
|
||||
sqla_query = table.get_sqla_query(**query_obj)
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
sqla_query = table.get_sqla_query(query_object)
|
||||
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
|
||||
@@ -344,7 +349,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["boolean_gender"],
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "boolean_gender",
|
||||
"op": FilterOperator.IN,
|
||||
@@ -353,7 +358,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
],
|
||||
"extras": {},
|
||||
}
|
||||
sqla_query = table.get_sqla_query(**query_obj)
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
sqla_query = table.get_sqla_query(query_object)
|
||||
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||
dialect = table.database.get_dialect()
|
||||
operand = "(true, false)"
|
||||
@@ -371,7 +377,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["user"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -383,8 +389,9 @@ 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_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
|
||||
def test_query_format_strip_trailing_semicolon(self):
|
||||
query_obj = {
|
||||
@@ -394,7 +401,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["user"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -403,7 +410,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
sql="SELECT * from test_table;",
|
||||
database=get_example_database(),
|
||||
)
|
||||
sqlaq = table.get_sqla_query(**query_obj)
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
sqlaq = table.get_sqla_query(query_object)
|
||||
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
||||
assert sql[-1] != ";"
|
||||
|
||||
@@ -415,7 +423,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["grp"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -425,8 +433,9 @@ 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_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
|
||||
def test_dml_statement_raises_exception(self):
|
||||
base_query_obj = {
|
||||
@@ -436,7 +445,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["grp"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -446,8 +455,9 @@ 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_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
|
||||
def test_fetch_metadata_for_updated_virtual_table(self):
|
||||
table = SqlaTable(
|
||||
@@ -507,7 +517,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
}
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -516,7 +526,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
db.session.add(database)
|
||||
db.session.add(table)
|
||||
db.session.commit()
|
||||
sqlaq = table.get_sqla_query(**query_obj)
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
sqlaq = table.get_sqla_query(query_object)
|
||||
assert sqlaq.labels_expected == ["user", "COUNT_DISTINCT(user)"]
|
||||
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
||||
assert "COUNT_DISTINCT_user__00db1" in sql
|
||||
@@ -585,7 +596,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -595,7 +606,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [None], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [None], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -605,7 +616,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -615,7 +626,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [""], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [""], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -625,7 +636,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": [EMPTY_STRING, NULL_STRING, "null", "foo"],
|
||||
@@ -641,7 +652,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ['"text in double quotes"'],
|
||||
@@ -657,7 +668,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ["'text in single quotes'"],
|
||||
@@ -673,7 +684,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ['double quotes " in text'],
|
||||
@@ -689,7 +700,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ["single quotes ' in text"],
|
||||
@@ -726,7 +737,7 @@ def test_should_generate_closed_and_open_time_filter_range(login_as_admin):
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"from_dttm": datetime(2022, 1, 1),
|
||||
"to_dttm": datetime(2023, 1, 1),
|
||||
"granularity": "datetime_col",
|
||||
@@ -763,7 +774,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
||||
result = physical_dataset.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
||||
"filters": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -782,7 +793,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
||||
physical_dataset.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "col4", "val": None, "op": flt.value}],
|
||||
"filters": [{"col": "col4", "val": None, "op": flt.value}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -871,7 +882,7 @@ def test_extra_cache_keys(
|
||||
"groupby": ["id", "username", "email"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
query_obj = dict(**base_query_obj, extras={})
|
||||
@@ -917,7 +928,7 @@ def test_extra_cache_keys_in_sql_expression(
|
||||
"groupby": ["id", "username", "email"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
query_obj = dict(**base_query_obj, extras={"where": sql_expression})
|
||||
@@ -960,7 +971,7 @@ def test_extra_cache_keys_in_adhoc_metrics_and_columns(
|
||||
"metrics": [],
|
||||
"columns": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
items: dict[str, Any] = {
|
||||
@@ -1014,7 +1025,7 @@ def test_extra_cache_keys_in_dataset_metrics_and_columns(
|
||||
"columns": ["username"],
|
||||
"metrics": ["variable_profit"],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
extra_cache_keys = table.get_extra_cache_keys(query_obj)
|
||||
@@ -1118,7 +1129,7 @@ def test__temporal_range_operator_in_adhoc_filter(physical_dataset):
|
||||
result = physical_dataset.query(
|
||||
{
|
||||
"columns": ["col1", "col2"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "col5",
|
||||
"val": "2000-01-05 : 2000-01-06",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
115
tests/unit_tests/queries/test_query_object_prequery.py
Normal file
115
tests/unit_tests/queries/test_query_object_prequery.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# 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"]
|
||||
Reference in New Issue
Block a user