Compare commits

..
Author SHA1 Message Date
sadpandajoe 901cb3de9d test(query-context): update normalize_df mock signature for new offset param
normalize_df now accepts an optional third sql_shifted_temporal_labels
parameter (passed positionally from processing_time_offsets). Three
pre-existing mocks in this file stubbed normalize_df with a fixed two-argument
lambda, which broke when the call site started passing a third positional
argument.
2026-08-20 18:06:05 +00:00
sadpandajoe 41233bcbe3 test(dataset): regression guards for Hours Offset bound and grain bugs
Covers both defects across an offset x grain x column-type matrix:

- DATE-column filter bounds stay on the requested whole-day window for
  offsets 0/+1/+24/+25/-1/-25 (the bug admitted/dropped a day for any
  sub-24h offset); TIMESTAMP columns keep exact-hour bounds as a control.
- Grained physical and adhoc BASE_AXIS temporal axes apply the offset
  exactly once, before grain truncation, matching the exact SQL literal
  and the resulting bucket for each combination -- including the case
  that previously applied the offset zero times for an ungrained axis,
  and negative sub-day DATE offsets that must not move the bucket.
  Physical and adhoc filter SQL are asserted byte-identical to pre-fix
  output for the explicitly out-of-scope grained-filter paths.
- The new per-engine capability gate correctly excludes every engine
  that doesn't support the SQL-side shift (falls back to the pre-fix
  pandas-only offset, verified against the real code path with no
  shift SQL emitted) and correctly includes Postgres/SQLite (compiled
  SQL asserted exactly, across signed and >=24h offsets).
2026-08-20 18:06:05 +00:00
sadpandajoe e023dbfa7c fix(dataset): correct Hours Offset filter bounds and grain-truncation order
The dataset "Hours offset" setting had two independent bugs:

1. Filter bounds shifted a DATE-typed column's literal bound by the raw
   offset before rendering it as a date-only value. A sub-24h offset could
   move which calendar day a bound truncated to, silently admitting an
   out-of-range day on one end and dropping the last requested day on the
   other. The offset is now quantized to whole days (truncating toward
   zero) before it's applied to a DATE column's bounds, so a sub-day offset
   never moves the selected day window.

2. The offset was applied only in pandas, after the database had already
   truncated a temporal axis to its time grain using the raw (unshifted)
   value. A row near a grain boundary could bucket to the wrong day, and
   the displayed label (computed via a different path) could disagree with
   the filter bounds. The offset is now applied in SQL, before grain
   truncation, at the axis-construction call sites -- gated behind a new
   `apply_dataset_offset` opt-in so filter construction and other unrelated
   callers are unaffected. A new `sql_shifted_temporal_labels` signal tells
   dataframe normalization which columns were already shifted in SQL so the
   legacy pandas-side offset isn't re-applied on top.

Since the SQL-side shift depends on dialect-specific interval syntax, it's
gated behind a new `BaseEngineSpec.supports_temporal_column_shift` capability
flag (default False), enabled explicitly on the two engines it's been
verified against (Postgres, SQLite). Engines that don't opt in keep the
prior pandas-only behavior for the grain-truncation-order bug -- no new
regression -- while gaining the DATE-bound quantization fix unconditionally
(that part is engine-agnostic).
2026-08-20 18:06:05 +00:00
Parman MohammadalizadehandEvan Rusackas 148ffaff50 fix(plugin-chart-echarts): omit stacked value labels on zero-height segments (#42756)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-20 11:04:19 -07:00
Ankit 83c6ea4e03 fix(explore): show the empty state when Samples returns no result payload (#43115) 2026-08-20 11:00:54 -07:00
Hans Yu 1ca5e61f54 chore: Unset future flag in engines and sessions. (#43273) 2026-08-20 10:58:37 -07:00
rlei 1ef12580a7 fix(chart): ignore chart actions for a chart no longer in state (#43228) 2026-08-20 10:57:21 -07:00
c0884c0f0c fix(explore): keep x-axis label when overriding Time Column with time comparison (#42875)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-20 10:52:22 -07:00
Sepuri Sai KrishnaandClaude Opus 5 2ff79bd495 fix(github): point the issue templates at labels that exist (#43357)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:13:36 -07:00
e7dccd44a7 fix(reports): prevent blank/partial report PDFs from virtualized charts (#43348)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-08-20 09:10:30 -04:00
dependabot[bot] fdd3945dde chore(deps): bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#43361)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 13:58:31 +03:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8b67fb4d30 chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /superset-websocket (#43360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:49 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b288db91f9 chore(deps): bump github/codeql-action/init from 4.37.6 to 4.37.7 (#43362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:43 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 80bba12d0f chore(deps-dev): bump globals from 17.10.0 to 17.11.0 in /docs (#43363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:38 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5d4358a149 chore(deps): bump google-auth-library from 11.0.1 to 11.0.2 in /superset-frontend (#43364)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 00:41:25 -07:00
f9cedf84e2 fix: drop post-processing options the operation no longer accepts (#42927)
Signed-off-by: Arya Ketan <aryaketan@sharechat.co>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-19 18:15:15 -07:00
195 changed files with 2077 additions and 381 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: Bug report
description: Report a bug to improve Superset's stability
labels: ["bug"]
labels: ["#bug"]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Cosmetic Issue
about: Describe a cosmetic issue with CSS, positioning, layout, labeling, or similar
labels: "cosmetic-issue"
labels: "#bug:cosmetic"
---
## Screenshot
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -78,6 +78,6 @@ jobs:
# queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -93,7 +93,7 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.10.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.67.0",
+4 -4
View File
@@ -9174,10 +9174,10 @@ globals@^14.0.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^17.10.0:
version "17.10.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.10.0.tgz#f9dbd847ae99e236f98b13095e2426ac3b25a45c"
integrity sha512-V0kztuWST2k8A/VbxAY8+L+7+Rgo3fyA24IHRLrZp7HOzJjV0gHSaZUjK9lpP/IrBSNite2tZ1prhRkinRu1CA==
globals@^17.11.0:
version "17.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87"
integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==
globalthis@^1.0.4:
version "1.0.4"
+1 -1
View File
@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
# where the current model is out-of-sync with the existing table after a
# downgrade
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
engine = create_engine(sqlalchemy_uri, future=True)
engine = create_engine(sqlalchemy_uri)
Base = automap_base() # noqa: N806
Base.prepare(engine, reflect=True)
seen = set()
+6 -15
View File
@@ -99,7 +99,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.1",
"google-auth-library": "^11.0.2",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -20612,7 +20612,7 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/expect-playwright/-/expect-playwright-0.8.0.tgz",
"integrity": "sha512-+kn8561vHAY+dt+0gMqqj1oY+g5xWrsuGMk4QGxotT2WS545nVqqjs37z6hrYfIuucwqthzwJfCJUEYqixyljg==",
"deprecated": "\u26a0\ufe0f The 'expect-playwright' package is deprecated. The Playwright core assertions (via @playwright/test) now cover the same functionality. Please migrate to built-in expect. See https://playwright.dev/docs/test-assertions for migration.",
"deprecated": "⚠️ The 'expect-playwright' package is deprecated. The Playwright core assertions (via @playwright/test) now cover the same functionality. Please migrate to built-in expect. See https://playwright.dev/docs/test-assertions for migration.",
"dev": true,
"license": "MIT"
},
@@ -22783,9 +22783,9 @@
"license": "MIT"
},
"node_modules/google-auth-library": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.1.tgz",
"integrity": "sha512-ZqfaYduu9ASUaFuUk5dF9g9QvufdhhSj7jFiEnCrTQcH57sFPKYetM0iU4dcKkQk6CqC1xpSrVr5uQ9NhqjNOg==",
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.2.tgz",
"integrity": "sha512-vzpgPutxrghPsnjrjpzLX2bdv8IOL719Rh0oEjGnQu8YCIbnbMuTTQ5zU9LcKvLdOPgCxBwppbvnhgW90Qna5Q==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
@@ -26023,7 +26023,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/jest-process-manager/-/jest-process-manager-0.4.0.tgz",
"integrity": "sha512-80Y6snDyb0p8GG83pDxGI/kQzwVTkCxc7ep5FPe/F6JYdvRDhwr6RzRmPSP7SEwuLhxo80lBS/NqOdUIbHIfhw==",
"deprecated": "\u26a0\ufe0f The 'jest-process-manager' package is deprecated. Please migrate to Playwright's built-in test runner (@playwright/test) which now includes full Jest-style features and parallel testing. See https://playwright.dev/docs/intro for details.",
"deprecated": "⚠️ The 'jest-process-manager' package is deprecated. Please migrate to Playwright's built-in test runner (@playwright/test) which now includes full Jest-style features and parallel testing. See https://playwright.dev/docs/intro for details.",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -43073,15 +43073,6 @@
"node": ">=12"
}
},
"packages/superset-ui-core/node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"packages/superset-ui-core/node_modules/react-ace": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz",
+1 -1
View File
@@ -176,7 +176,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.1",
"google-auth-library": "^11.0.2",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -467,6 +467,14 @@ export function transformSeries(
return formatter(numericValue);
}
if (!onlyTotal) {
// A stacked segment with no height begins and ends at the same
// coordinate as the top of the segment beneath it, so its label is
// drawn over that segment's label. Zero and null have no height, so
// they carry no label. The rich tooltip omits zero observations from
// a stacked series for the same reason.
if (stack && !numericValue) {
return '';
}
if (
numericValue >=
(thresholdValues[dataIndex] || Number.MIN_SAFE_INTEGER)
@@ -20,13 +20,14 @@ import {
CategoricalColorScale,
ChartProps,
TimeGranularity,
getNumberFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { supersetTheme } from '@apache-superset/core/theme';
import type { SeriesOption } from 'echarts';
import type { ScatterSeriesOption } from 'echarts/charts';
import { EchartsTimeseriesSeriesType } from '../../src';
import { TIMESERIES_CONSTANTS } from '../../src/constants';
import { StackControlsValue, TIMESERIES_CONSTANTS } from '../../src/constants';
import {
LegendOrientation,
EchartsTimeseriesChartProps,
@@ -566,3 +567,70 @@ test('getPadding should handle Left position with zero margin correctly', () =>
getChartPaddingSpy.mockRestore();
}
});
/**
* #42702: a stacked segment with no height starts and ends at the same
* coordinate as the top of the segment beneath it, so a value label on it is
* drawn over that segment's label. `percentage_threshold` does not filter these
* out: it defaults to 0, and `thresholdValues[dataIndex] || MIN_SAFE_INTEGER`
* turns a 0 threshold into "no filtering", which is intentional.
*/
const stackedLabel = (
numericValue: number | null,
opts: Record<string, unknown> = {},
) => {
const series = transformSeries(
{ id: 'B', name: 'B', data: [[1, numericValue]] } as SeriesOption,
mockColorScale,
'B',
{
seriesType: EchartsTimeseriesSeriesType.Bar,
stack: StackControlsValue.Stack,
showValue: true,
onlyTotal: false,
formatter: getNumberFormatter(),
thresholdValues: [0],
...opts,
},
) as SeriesOption & {
label: { formatter: (params: unknown) => string };
};
return series.label.formatter({
value: [1, numericValue],
dataIndex: 0,
seriesIndex: 1,
seriesName: 'B',
});
};
test('stacked value labels are omitted for a zero-height segment', () => {
expect(stackedLabel(0)).toBe('');
expect(stackedLabel(null)).toBe('');
});
test('stacked value labels are kept for segments that have height', () => {
expect(stackedLabel(32)).toBe('32');
expect(stackedLabel(-5)).toBe('-5');
});
test('a zero value keeps its label when the series is not stacked', () => {
// Without a stack the label sits on the bar itself, so there is nothing for
// it to collide with.
expect(stackedLabel(0, { stack: undefined })).toBe('0');
});
test('percentage_threshold still filters values below the threshold', () => {
// 10% of a 100 total. The zero-height guard must not swallow this rule.
expect(stackedLabel(5, { thresholdValues: [10] })).toBe('');
expect(stackedLabel(50, { thresholdValues: [10] })).toBe('50');
});
test('only-total labels are unaffected by the zero-height guard', () => {
expect(
stackedLabel(0, {
onlyTotal: true,
showValueIndexes: [1],
totalStackedValues: [32],
}),
).toBe('32');
});
@@ -223,6 +223,11 @@ export default function chartReducer(
}
if (action.type in actionHandlers) {
// ADD_CHART creates the entry, so it runs without prior state; every other
// handler reads state that is absent once the chart has been removed
if (action.type !== actions.ADD_CHART && !charts[action.key]) {
return charts;
}
return {
...charts,
[action.key]: actionHandlers[action.type](charts[action.key]),
@@ -91,4 +91,20 @@ describe('chart reducers', () => {
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
expect(newState[chartKey].chartStatus).toEqual('failed');
});
test('ignores an action for a chart that is no longer in state', () => {
const action = actions.chartUpdateStopped(999, new AbortController());
expect(() => chartReducer(charts, action)).not.toThrow();
expect(chartReducer(charts, action)).toEqual(charts);
});
test('still adds a chart that is not yet in state', () => {
const newChartKey = 2;
const newState = chartReducer(
charts,
actions.addChart({ ...chart, id: newChartKey }, newChartKey),
);
expect(newState[newChartKey].id).toEqual(newChartKey);
expect(newState[chartKey]).toEqual(testChart);
});
});
-31
View File
@@ -1,31 +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.
*/
import {
URL_PARAMS,
RESERVED_CHART_URL_PARAMS,
RESERVED_DASHBOARD_URL_PARAMS,
} from 'src/constants';
test('permalinkKey is reserved on both the chart and dashboard URL param lists', () => {
// Dashboard and explore permalinks resolve against different backend
// KV resources/salts, so a key from one must never leak into the other's
// URL via the reserved-params passthrough logic.
expect(RESERVED_DASHBOARD_URL_PARAMS).toContain(URL_PARAMS.permalinkKey.name);
expect(RESERVED_CHART_URL_PARAMS).toContain(URL_PARAMS.permalinkKey.name);
});
-1
View File
@@ -123,7 +123,6 @@ export const RESERVED_CHART_URL_PARAMS: string[] = [
URL_PARAMS.datasourceId.name,
URL_PARAMS.datasourceType.name,
URL_PARAMS.datasetId.name,
URL_PARAMS.permalinkKey.name,
URL_PARAMS.versionHistory.name,
];
export const RESERVED_DASHBOARD_URL_PARAMS: string[] = [
@@ -105,10 +105,17 @@ export const SamplesPane = ({
1,
)
.then(response => {
setData(ensureIsArray(response.data));
setColnames(ensureIsArray(response.colnames));
setColtypes(ensureIsArray(response.coltypes));
setRowCount(response.rowcount);
// A 200 that carries no `result` payload resolves to undefined here.
// Read through it so the pane falls back to its empty state instead
// of throwing a TypeError that surfaces as an internal error message.
const rows = ensureIsArray(response?.data);
setData(rows);
setColnames(ensureIsArray(response?.colnames));
setColtypes(ensureIsArray(response?.coltypes));
// Fall back to the rows actually returned rather than to zero: the
// controls only render when there are rows, and a hardcoded 0 would
// label a populated table as "0 rows".
setRowCount(response?.rowcount ?? rows.length);
setResponseError('');
cache.set(queryFormData, true);
if (queryForce) {
@@ -60,6 +60,27 @@ describe('SamplesPane', () => {
400,
);
// A 200 response that carries no `result` payload, as reported in #36840.
fetchMock.post(
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=37&per_page=100&page=1',
{},
);
// A 200 whose result carries rows but omits `rowcount`.
fetchMock.post(
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=38&per_page=100&page=1',
{
result: {
data: [
{ __timestamp: 1230768000000, genre: 'Action' },
{ __timestamp: 1230768000010, genre: 'Horror' },
],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
},
);
const setForceQuery = jest.fn();
afterAll(() => {
@@ -114,4 +135,29 @@ describe('SamplesPane', () => {
expect(queryByText('Action')).toBeVisible();
expect(queryByText('Horror')).toBeVisible();
});
test('renders the empty state when the response carries no result payload', async () => {
const props = createSamplesPaneProps({ datasourceId: 37 });
const { findByText, queryByRole } = render(<SamplesPane {...props} />, {
useRedux: true,
});
expect(
await findByText('No samples were returned for this dataset'),
).toBeVisible();
// The pane should not leak an internal TypeError through the error alert.
expect(queryByRole('alert')).not.toBeInTheDocument();
});
test('counts the returned rows when the response omits rowcount', async () => {
const props = createSamplesPaneProps({ datasourceId: 38 });
const { findByText, queryByText } = render(<SamplesPane {...props} />, {
useRedux: true,
});
expect(await findByText('Action')).toBeVisible();
// Falling back to 0 here would label a populated table as "0 rows".
expect(queryByText('0 rows')).not.toBeInTheDocument();
expect(queryByText('2 rows')).toBeVisible();
});
});
+4 -4
View File
@@ -28,7 +28,7 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.10.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
@@ -2053,9 +2053,9 @@
}
},
"node_modules/globals": {
"version": "17.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.10.0.tgz",
"integrity": "sha512-V0kztuWST2k8A/VbxAY8+L+7+Rgo3fyA24IHRLrZp7HOzJjV0gHSaZUjK9lpP/IrBSNite2tZ1prhRkinRu1CA==",
"version": "17.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
"integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
"dev": true,
"license": "MIT",
"engines": {
+1 -1
View File
@@ -36,7 +36,7 @@
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.10.0",
"globals": "^17.11.0",
"oxfmt": "^0.63.0",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
-3
View File
@@ -280,9 +280,6 @@ def test_sqlalchemy_dialect(
"""
Test the SQLAlchemy dialect, making sure it supports everything Superset needs.
"""
if "future" not in engine_kwargs:
engine_kwargs["future"] = True
engine = create_engine(sqlalchemy_uri, **engine_kwargs)
dialect = engine.dialect
+1 -1
View File
@@ -227,7 +227,7 @@ class BaseStreamingCSVExportCommand(BaseCommand):
delimiter = csv_export_config.get("sep", ",")
decimal_separator = csv_export_config.get("decimal", ".")
with db.session(future=True) as session:
with db.session() as session:
# Merge database to prevent DetachedInstanceError
merged_database = session.merge(database)
+16 -5
View File
@@ -291,19 +291,30 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
),
None,
)
# Replaces x-axis column values with granularity
# Point the x-axis at the overridden Time Column (granularity).
if x_axis_column:
if isinstance(x_axis_column, dict):
# Only swap the underlying expression, keeping the
# column's original label. The temporal offset join
# (``processing_time_offsets``), the post-processing
# pivot ``index`` and the frontend all reference this
# column by its label; renaming it to the granularity
# here desynchronizes those consumers from the label
# the saved chart still advertises, which — with a Time
# Comparison offset — collapses the series into a single
# point.
x_axis_column["sqlExpression"] = granularity
x_axis_column["label"] = granularity
else:
# A bare string x-axis has no distinct label, so it is
# replaced wholesale and the pivot ``index`` must be
# realigned to the overridden column.
query_object.columns = [
granularity if column == x_axis_column else column
for column in query_object.columns
]
for post_processing in query_object.post_processing:
if post_processing.get("operation") == "pivot":
post_processing["options"]["index"] = [granularity]
for post_processing in query_object.post_processing:
if post_processing.get("operation") == "pivot":
post_processing["options"]["index"] = [granularity]
# If no temporal x-axis, then get the default temporal filter
if not filter_to_remove:
+81 -2
View File
@@ -17,6 +17,7 @@
# pylint: disable=invalid-name
from __future__ import annotations
import inspect
import logging
from datetime import datetime
from pprint import pformat
@@ -205,8 +206,86 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
def _set_post_processing(
self, post_processing: list[dict[str, Any] | None] | None
) -> None:
post_processing = post_processing or []
self.post_processing = [post_proc for post_proc in post_processing if post_proc]
self.post_processing = [
self._drop_unsupported_options(post_proc)
for post_proc in post_processing or []
if post_proc
]
@staticmethod
def _drop_unsupported_options(post_proc: dict[str, Any]) -> dict[str, Any]:
"""
Drop options that the post-processing operation no longer accepts.
A chart's ``query_context`` is written when the chart is saved and is
never rewritten afterwards, while Explore rebuilds the query from
``form_data`` at every render. A chart saved by an older version of
Superset can therefore reference an option that has since been removed
from the operation. ``exec_post_processing`` passes the stored options
as keyword arguments, so that option raises a bare ``TypeError`` on
every path that replays the stored ``query_context`` -- the chart data
endpoint, alerts and reports, thumbnails, CSV export -- while the same
chart still renders correctly in Explore.
Comparing against the signature avoids a hard-coded list of removed
option names, which would need extending at each release.
"""
operation = post_proc.get("operation")
function = (
getattr(pandas_postprocessing, operation, None)
if isinstance(operation, str)
else None
)
if function is None:
# A missing or unknown operation is left untouched, so that
# exec_post_processing reports it as InvalidPostProcessingError.
return post_proc
parameters = inspect.signature(function).parameters
if any(
parameter.kind is inspect.Parameter.VAR_KEYWORD
for parameter in parameters.values()
):
return post_proc
# `exec_post_processing` calls the operation as `operation(df, **options)`,
# so an option can only reach a parameter that a caller may fill by
# keyword. That excludes the first parameter, which receives the
# DataFrame positionally, and any positional-only or `*args` parameter.
keyword_parameters = {
name
for position, (name, parameter) in enumerate(parameters.items())
if position > 0
and parameter.kind
in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
}
options = post_proc.get("options") or {}
unsupported = {key for key in options if key not in keyword_parameters}
if not unsupported:
return post_proc
# Logged at info: a chart saved before the option was removed hits this
# on every render, so a warning would repeat for as long as the chart
# is not resaved, without anything new to report.
logger.info(
"Dropping unsupported option(s) %s of post-processing operation "
"`%s`. The chart's stored query_context predates the current "
"signature of that operation.",
sorted(unsupported),
operation,
)
return {
**post_proc,
"options": {
key: value
for key, value in options.items()
if key in keyword_parameters
},
}
def _init_series_columns(
self,
+65
View File
@@ -99,6 +99,7 @@ from superset.models.helpers import (
AuditMixinNullable,
CertificationMixin,
ExploreMixin,
get_effective_hours_offset,
ImportExportMixin,
QueryResult,
SoftDeleteMixin,
@@ -1247,6 +1248,8 @@ class TableColumn(AuditMixinNullable, ImportExportMixin, CertificationMixin, Mod
time_grain: str | None,
label: str | None = None,
template_processor: BaseTemplateProcessor | None = None,
apply_dataset_offset: bool = False,
sql_shifted_temporal_labels: set[str] | None = None,
) -> TimestampExpression | Label:
"""
Return a SQLAlchemy Core element representation of self to be used in a query.
@@ -1254,6 +1257,8 @@ class TableColumn(AuditMixinNullable, ImportExportMixin, CertificationMixin, Mod
:param time_grain: Optional time grain, e.g. P1Y
:param label: alias/label that column is expected to have
:param template_processor: template processor
:param apply_dataset_offset: shift the selected axis before truncation
:param sql_shifted_temporal_labels: labels shifted before truncation
:return: A TimeExpression object wrapped in a Label if supported by db
"""
label = label or utils.DTTM_ALIAS
@@ -1291,6 +1296,27 @@ class TableColumn(AuditMixinNullable, ImportExportMixin, CertificationMixin, Mod
col = literal_column(expression, type_=type_)
else:
col = column(self.column_name, type_=type_)
if (
apply_dataset_offset
and time_grain
and self.table
and self.db_engine_spec.supports_temporal_column_shift
and (offset_hours := self.table.offset or 0)
and not self.table.get_dataset_timezone()
):
effective_offset_hours = get_effective_hours_offset(
self.db_engine_spec,
self.type,
offset_hours,
db_extra=self.db_extra,
)
if effective_offset_hours:
col = self.db_engine_spec.get_temporal_column_shift_expr(
col,
effective_offset_hours,
)
if sql_shifted_temporal_labels is not None:
sql_shifted_temporal_labels.add(label)
time_expr = self.db_engine_spec.get_timestamp_expr(col, pdf, time_grain)
return self.database.make_sqla_column_compatible(time_expr, label)
@@ -1960,11 +1986,26 @@ class SqlaTable(
)
) from ex
def _shift_temporal_column_if_needed(
self,
sqla_column: ColumnClause,
effective_offset_hours: int,
) -> ColumnClause:
"""Apply a nonzero effective dataset offset to a temporal expression."""
if not effective_offset_hours:
return sqla_column
return self.db_engine_spec.get_temporal_column_shift_expr(
sqla_column,
effective_offset_hours,
)
def adhoc_column_to_sqla( # pylint: disable=too-many-locals
self,
col: AdhocColumn,
force_type_check: bool = False,
template_processor: BaseTemplateProcessor | None = None,
apply_dataset_offset: bool = False,
sql_shifted_temporal_labels: set[str] | None = None,
) -> tuple[ColumnElement, utils.GenericDataType | None]:
"""
Turn an adhoc column into a sqlalchemy column.
@@ -1974,6 +2015,8 @@ class SqlaTable(
This is needed to validate if a filter with an adhoc column
is applicable.
:param template_processor: template_processor instance
:param apply_dataset_offset: shift the selected axis before truncation
:param sql_shifted_temporal_labels: labels shifted before truncation
:returns: A tuple of (SQLAlchemy column, generic column type). The
generic type is populated when the column type is resolved
(either because the adhoc column matches a physical column, or
@@ -1990,6 +2033,7 @@ class SqlaTable(
pdf = None
is_column_reference = col.get("isColumnReference", False)
generic_type: utils.GenericDataType | None = None
native_type: str | None = None
metadata_lookup_key = self._render_adhoc_expression_for_metadata_lookup(
sql_expression, template_processor
@@ -2004,6 +2048,7 @@ class SqlaTable(
is_dttm = col_in_metadata.is_temporal
pdf = col_in_metadata.python_date_format
generic_type = col_in_metadata.type_generic
native_type = col_in_metadata.type
else:
# Column doesn't exist in metadata or is not a reference - treat as ad-hoc
# expression Note: If isColumnReference=true but column not found, we still
@@ -2066,8 +2111,28 @@ class SqlaTable(
# stay unquoted for numeric adhoc expressions like
# CAST(... AS BIGINT)).
generic_type = col_desc[0].get("type_generic")
probed_type = col_desc[0].get("type")
native_type = str(probed_type) if probed_type is not None else None
if is_dttm and has_timegrain:
if (
apply_dataset_offset
and self.db_engine_spec.supports_temporal_column_shift
and (offset_hours := self.offset or 0)
and not self.get_dataset_timezone()
):
effective_offset_hours = get_effective_hours_offset(
self.db_engine_spec,
native_type,
offset_hours,
db_extra=self.db_extra,
)
sqla_column = self._shift_temporal_column_if_needed(
sqla_column,
effective_offset_hours,
)
if sql_shifted_temporal_labels is not None:
sql_shifted_temporal_labels.add(label)
sqla_column = self.db_engine_spec.get_timestamp_expr(
col=sqla_column,
pdf=pdf,
+14
View File
@@ -539,6 +539,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
# the ``array_*`` capability methods below must be implemented. Defaults to
# False so engines that have not opted in keep treating arrays as strings.
supports_multivalue_columns = False
supports_temporal_column_shift: bool = False
allows_joins = True
allows_subqueries = True
allows_alias_in_select = True
@@ -1215,6 +1216,19 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
return TimestampExpression(time_expr, col, type_=col.type)
@classmethod
def get_temporal_column_shift_expr(
cls,
col: ColumnClause,
offset_hours: int,
) -> TimestampExpression:
"""Shift a temporal SQL expression by a bounded number of hours."""
return TimestampExpression(
f"{{col}} + INTERVAL '{offset_hours}' HOUR",
col,
type_=col.type,
)
@classmethod
def _apply_year_to_dttm(cls, time_expr: str) -> str:
"""
-1
View File
@@ -389,7 +389,6 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
}
}
},
future=True,
)
conn = engine.connect()
idx = 0
+1
View File
@@ -307,6 +307,7 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
supports_catalog = True
supports_dynamic_catalog = True
supports_grouping_sets = True
supports_temporal_column_shift = True
default_driver = "psycopg2"
sqlalchemy_uri_placeholder = (
+21 -1
View File
@@ -25,9 +25,14 @@ from typing import Any, TYPE_CHECKING
from flask_babel import gettext as __
from sqlalchemy import types
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql.elements import ColumnClause
from superset.constants import TimeGrain
from superset.db_engine_specs.base import BaseEngineSpec, DatabaseCategory
from superset.db_engine_specs.base import (
BaseEngineSpec,
DatabaseCategory,
TimestampExpression,
)
from superset.errors import SupersetErrorType
if TYPE_CHECKING:
@@ -43,6 +48,7 @@ class SqliteEngineSpec(BaseEngineSpec):
disable_ssh_tunneling = True
supports_multivalues_insert = True
supports_temporal_column_shift = True
metadata = {
"description": "SQLite is a self-contained, serverless SQL database engine.",
@@ -140,6 +146,20 @@ class SqliteEngineSpec(BaseEngineSpec):
"ELSE printf('%04d-01-01', CAST({col} AS INTEGER)) END)"
)
@classmethod
def get_temporal_column_shift_expr(
cls,
col: ColumnClause,
offset_hours: int,
) -> TimestampExpression:
"""Shift a temporal expression with SQLite's datetime modifier syntax."""
modifier = f"{offset_hours:+d} hours"
return TimestampExpression(
f"DATETIME({{col}}, '{modifier}')",
col,
type_=col.type,
)
@classmethod
def convert_dttm(
cls, target_type: str, dttm: datetime, db_extra: dict[str, Any] | None = None
+2 -2
View File
@@ -376,7 +376,7 @@ def upgrade_catalog_perms(engines: set[str] | None = None) -> None:
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# The Database model has an eager-loaded (``lazy="joined"``) ``ssh_tunnel``
# backref. Eager-loading it here would SELECT every column on ``ssh_tunnels``,
@@ -581,7 +581,7 @@ def downgrade_catalog_perms(engines: set[str] | None = None) -> None:
WARNING: models (datasets and charts) not in the default catalog are deleted!
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# See upgrade_catalog_perms: avoid eager-loading the ``ssh_tunnel`` backref so the
# query stays schema-safe across migration revisions.
@@ -51,7 +51,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
op.add_column("slices", sa.Column("perm", sa.String(length=2000), nullable=True))
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Use Slice class defined here instead of models.Slice
for slc in session.query(Slice).all():
@@ -59,7 +59,7 @@ def upgrade():
)
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# don't use models.DruidMetric
# because it assumes the context is consistent with the application
@@ -94,7 +94,7 @@ class Dashboard(AuditMixin, Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
objects = session.query(Slice).all()
objects += session.query(Dashboard).all()
@@ -50,7 +50,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
op.add_column("slices", sa.Column("datasource_id", sa.Integer()))
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
if slc.druid_datasource_id:
@@ -63,7 +63,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
if slc.datasource_type == "druid":
slc.druid_datasource_id = slc.datasource_id
@@ -45,7 +45,7 @@ class Database(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for obj in session.query(Database).all():
obj.allow_run_sync = True
@@ -48,7 +48,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = session.query(Slice).all()
slice_len = len(slices)
@@ -61,7 +61,7 @@ class Url(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
urls = session.query(Url).all()
urls_len = len(urls)
@@ -45,7 +45,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type.like("deck_%")):
params = json.loads(slc.params)
@@ -45,7 +45,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(
or_(Slice.viz_type.like("line"), Slice.viz_type.like("bar"))
@@ -75,7 +75,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(
or_(Slice.viz_type.like("line"), Slice.viz_type.like("bar"))
@@ -46,7 +46,7 @@ class Dashboard(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -68,7 +68,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -57,7 +57,7 @@ def upgrade():
),
)
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Use Slice class defined here instead of models.Slice
for tbl in session.query(Table).all():
@@ -49,7 +49,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = session.query(Slice).filter_by(viz_type="cal_heatmap").all()
slice_len = len(slices)
@@ -45,7 +45,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -63,7 +63,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -45,7 +45,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -68,7 +68,7 @@ class Database(Base):
def replace(source, target):
with db.Session(bind=op.get_bind(), future=True) as session:
with db.Session(bind=op.get_bind()) as session:
with session.begin():
query = (
session.query(Slice, Database)
@@ -80,7 +80,7 @@ def replace(source, target):
for slc, database in query:
try:
engine = create_engine(database.sqlalchemy_uri, future=True)
engine = create_engine(database.sqlalchemy_uri)
if engine.dialect.identifier_preparer._double_percents:
params = json.loads(slc.params)
@@ -50,7 +50,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -66,7 +66,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -47,7 +47,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type == "pie").all():
try:
@@ -68,7 +68,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type == "pie").all():
try:
@@ -134,7 +134,7 @@ def compute_time_compare(granularity, periods):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for chart in session.query(Slice):
params = json.loads(chart.params or "{}")
@@ -163,7 +163,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for chart in session.query(Slice):
params = json.loads(chart.params or "{}")
@@ -159,7 +159,7 @@ class TableColumn(BaseColumnMixin, Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
tables = [
Annotation,
@@ -59,7 +59,7 @@ class TableColumn(BaseColumnMixin, Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Delete the orphaned columns records.
for record in session.query(DruidColumn).all():
@@ -59,7 +59,7 @@ class SqlMetric(BaseMetricMixin, Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Delete the orphaned metrics records.
for record in session.query(DruidMetric).all():
@@ -579,7 +579,7 @@ def scan_dashboard_positions_data(positions):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -55,7 +55,7 @@ def is_v2_dash(positions):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards): # noqa: B007
@@ -46,7 +46,7 @@ class Dashboard(Base):
def upgrade(): # noqa: C901
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -75,7 +75,7 @@ def upgrade_slice(slc):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
filter_box_slices = session.query(Slice).filter_by(viz_type="filter_box")
for slc in filter_box_slices.all():
@@ -90,7 +90,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
filter_box_slices = session.query(Slice).filter_by(viz_type="filter_box")
for slc in filter_box_slices.all():
@@ -87,7 +87,7 @@ def upgrade():
bind = op.get_bind()
insp = sa.engine.reflection.Inspector.from_engine(bind)
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
tables = session.query(SqlaTable).all()
for table in tables:
@@ -159,7 +159,7 @@ class TableColumn(BaseColumnMixin, Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
tables = [
Annotation,
@@ -62,7 +62,7 @@ def add_parent_ids(node, layout):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -88,7 +88,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -47,7 +47,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -94,7 +94,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -46,7 +46,7 @@ class DashboardSlices(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# find dup records in dashboard_slices tbl
dup_records = (
@@ -71,7 +71,7 @@ def upgrade():
op.add_column("tables", Column("schema_perm", String(length=1000), nullable=True))
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for t in session.query(Sqlatable).all():
db_name = (
t.database.verbose_name
@@ -161,7 +161,7 @@ down_revision = "11c737c17cc6"
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
tables = [
Annotation,
@@ -48,7 +48,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -63,7 +63,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -53,7 +53,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
if slc.params:
@@ -69,7 +69,7 @@ class Dashboard(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = session.query(Dashboard).all()
for i, dashboard in enumerate(dashboards):
@@ -56,7 +56,7 @@ def upgrade():
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Visualization types which support time granularity (hence negate).
viz_types = [
@@ -74,7 +74,7 @@ def duration_by_name(database: Database):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
query = (
session.query(Slice, Database)
@@ -89,7 +89,7 @@ def create_new_markdown_component(chart_position, url):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dash_to_migrate = defaultdict(list)
iframe_urls = defaultdict(list)
@@ -152,7 +152,7 @@ def upgrade(): # noqa: C901
"""
bind = op.get_bind()
session = orm.Session(bind=bind, future=True)
session = orm.Session(bind=bind)
faulty_view_menus = (
session.query(ViewMenu)
@@ -119,7 +119,7 @@ def update_dashboards(session, uuid_map):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for table_name, model in models.items():
with op.batch_alter_table(table_name) as batch_op:
@@ -152,7 +152,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# remove uuid from position_json
update_dashboards(session, {})
@@ -135,7 +135,7 @@ def upgrade():
),
)
# Migrate data
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
alerts = session.query(Alert).all()
for a in alerts:
if a.sql_observer:
@@ -224,7 +224,7 @@ def downgrade():
)
# Migrate data
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
alerts = session.query(Alert).all()
for a in alerts:
if a.sql:
@@ -56,7 +56,7 @@ default_batch_size = int(os.environ.get("BATCH_SIZE", 200))
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# Add uuid column
try:
@@ -86,7 +86,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True) # noqa: F841
session = db.Session(bind=bind) # noqa: F841
# Remove uuid column
with op.batch_alter_table("saved_query") as batch_op:
@@ -93,7 +93,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -107,7 +107,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -65,7 +65,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -79,7 +79,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -77,7 +77,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -91,7 +91,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -101,7 +101,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -115,7 +115,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -87,7 +87,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -101,7 +101,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -101,7 +101,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -115,7 +115,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -89,7 +89,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -103,7 +103,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -80,7 +80,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -94,7 +94,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -55,7 +55,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -69,7 +69,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -47,7 +47,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -61,7 +61,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -53,7 +53,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
x_dateunit_in_since = DateRangeMigration.x_dateunit_in_since
x_dateunit_in_until = DateRangeMigration.x_dateunit_in_until
@@ -74,7 +74,7 @@ def upgrade():
- If no dttm columns exist in the dataset, don't change the chart.
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices_changed = 0
@@ -47,7 +47,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = (
session.query(Slice)
@@ -75,7 +75,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = (
session.query(Slice)
@@ -46,7 +46,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type.like("directed_force")):
params = json.loads(slc.params)
@@ -75,7 +75,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type.like("graph_chart")):
params = json.loads(slc.params)
@@ -62,7 +62,7 @@ def has_uuid_column(table_name, bind):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for table_name, model in models.items():
# this script adds missing uuid columns
@@ -46,7 +46,7 @@ class Dashboard(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -74,7 +74,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -47,7 +47,7 @@ class SqlaTable(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for datasource in session.query(SqlaTable):
if datasource.extra:
@@ -49,7 +49,7 @@ def upgrade():
Convert all country names to lowercase
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type == "country_map").all():
try:
@@ -69,7 +69,7 @@ def downgrade():
Convert all country names to sentence case
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).filter(Slice.viz_type == "country_map").all():
try:
@@ -172,7 +172,7 @@ def downgrade_filter_set(filter_set: dict[str, Any]) -> int:
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -208,7 +208,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -94,7 +94,7 @@ def upgrade_dashboard(dashboard: dict[str, Any]) -> tuple[int, int]:
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -136,7 +136,7 @@ def downgrade_dashboard(dashboard: dict[str, Any]) -> tuple[int, int]:
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
dashboards = (
session.query(Dashboard)
@@ -52,7 +52,7 @@ def upgrade():
Fix databases with ``schemas_allowed_for_csv_upload`` stored as string.
"""
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for database in session.query(Database).all():
try:
@@ -56,7 +56,7 @@ VALID_RENDERERS = (
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = (
session.query(Slice)
@@ -47,7 +47,7 @@ PVM_MAP = {
def upgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
@@ -61,7 +61,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = Session(bind=bind, future=True)
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
@@ -50,7 +50,7 @@ def upgrade():
def remove_value_if_too_long():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
# it will be easier for users to notice that their field has been deleted rather than truncated # noqa: E501
# so just remove it if it won't fit back into the 1000 string length column
@@ -49,7 +49,7 @@ class Dashboard(Base):
def upgrade():
logger.info("[AddTypeToNativeFilter] Starting upgrade")
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for dashboard in session.query(Dashboard).all():
logger.info("[AddTypeToNativeFilter] Updating Dashboard<pk:%s> ", dashboard.id)
@@ -87,7 +87,7 @@ def upgrade():
def downgrade():
logger.info("[RemoveTypeToNativeFilter] Starting downgrade")
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for dashboard in session.query(Dashboard).all():
logger.info(
@@ -47,7 +47,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
where_clause = and_(
Slice.viz_type == "pivot_table_v2",
@@ -45,7 +45,7 @@ class Slice(Base):
def migrate(mapping: dict[str, str]) -> None:
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for slc in session.query(Slice).all():
try:
@@ -48,7 +48,7 @@ class Database(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for database in session.query(Database).all():
try:
@@ -70,7 +70,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for database in session.query(Database).all():
try:
@@ -49,7 +49,7 @@ def upgrade():
batch_op.add_column(sa.Column("force_screenshot", sa.Boolean(), default=False))
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
for report in session.query(ReportSchedule).all():
# Update existing alerts that send chart screenshots so that the cache is
@@ -50,7 +50,7 @@ class Slice(Base):
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = session.query(Slice).filter(Slice.viz_type == "big_number_total").all()
for slc in slices:
@@ -77,7 +77,7 @@ def upgrade():
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind, future=True)
session = db.Session(bind=bind)
slices = session.query(Slice).filter(Slice.viz_type == "big_number_total").all()
for slc in slices:

Some files were not shown because too many files have changed in this diff Show More