Compare commits

...
Author SHA1 Message Date
sadpandajoeandClaude Sonnet 5 11dfcb75ad refactor(postgres): collapse duplicate validate_parameters into a required_parameters hook
PostgresEngineSpec.validate_parameters was a near-verbatim copy of
BasicParametersMixin.validate_parameters, differing only in the required
set excluding "port" -- a duplicate-and-diverge that would silently miss
any future fix to the base method. Per maintainer review, add a
required_parameters class attribute to BasicParametersMixin (defaulting
to the base method's existing hardcoded set, so every other
BasicParametersMixin-based engine is unaffected) and have
validate_parameters read cls.required_parameters instead of a literal.
PostgresEngineSpec now just sets required_parameters = {"host",
"username", "database"} and inherits validate_parameters directly,
producing identical behavior to the deleted override.

build_sqlalchemy_uri and PostgresParametersSchema are unchanged. Retargeted
test mocks of is_hostname_valid/is_port_open back to
superset.db_engine_specs.base, since PostgresEngineSpec no longer imports
them into its own module namespace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:27:30 +00:00
Joe Li fba938d3e5 Merge branch 'master' into fix-postgres-dynamic-form-default-port 2026-08-20 13:17:21 -07:00
madhushreeagandmadhushree agarwal 271564cb0d feat(config): add EXTRA_PANDAS_POSTPROCESSING_OPS extension point (#43337)
Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
2026-08-20 13:15:41 -07:00
JUST.in DO ITandClaude Sonnet 5 bcfb4346f6 fix(mcp): honor use_cache and cache_timeout in get_chart_data (#43349)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:01:41 -07:00
Joe Li b5fe00b407 test(frontend): shrink flaky/misplaced recently-archived e2e coverage to Jest unit tests (#43264) 2026-08-20 11:48:02 -07: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
sadpandajoeandClaude Sonnet 5 28db79db4c fix(sqllab): fix stale PostgreSQL schema mock in DatabaseModal frontend test
Sibling of the two backend integration test mocks fixed previously:
DatabaseModal/index.test.tsx's fetch-mocked /api/v1/database/available/
response for PostgreSQL still listed port as required and was missing
port's nullable:true, both stale now that PostgresEngineSpec no longer
requires port. Verified byte-accurate against
PostgresEngineSpec.parameters_json_schema()'s actual output under the
repo's pinned marshmallow/apispec versions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 04:55:37 +00:00
sadpandajoeandClaude Sonnet 5 e08fc8905f fix(sqllab): fix two remaining stale Postgres schema test expectations from CI
CI caught two sibling test-expectation staleness issues missed in the prior
local round (a marshmallow==3.26.2/apispec==6.10.0 local mismatch vs. this
repo's pinned marshmallow==4.3.0/apispec==6.6.1 masked one of them):

- tests/integration_tests/databases/api_tests.py::test_available has its own
  hardcoded /api/v1/database/available/ snapshot; its PostgreSQL entry still
  listed port as required and was missing port's nullable:true.
- tests/integration_tests/db_engine_specs/postgres_tests.py::
  test_base_parameters_mixin asserted a top-level additionalProperties:False
  that PostgresEngineSpec.parameters_json_schema() does not actually
  produce under the pinned marshmallow/apispec versions.

Verified both fixes against the pinned versions installed locally, plus a
full regression pass of the previously-touched integration test files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 01:07:08 +00:00
sadpandajoe 8a8940557d fix(sqllab): default PostgreSQL port to 5432 in the dynamic connection form 2026-08-20 00:40:45 +00:00
202 changed files with 1793 additions and 482 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",
@@ -1,58 +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.
*/
/**
* With SOFT_DELETE enabled the delete-confirmation modal becomes recoverable:
* it explains the object is moved to the archive (and for how long), and drops
* the "type DELETE to confirm" friction. Non-destructive — the modal is opened
* and dismissed without deleting anything.
*/
import { test, expect } from '@playwright/test';
import { skipUnlessFeatureEnabled } from '../../helpers/featureFlags';
test.beforeEach(async ({ page }) => {
await skipUnlessFeatureEnabled(page, 'SOFT_DELETE');
});
test('chart delete confirmation reflects soft-delete (archive) semantics', async ({
page,
}) => {
await page.goto('chart/list/');
await page.locator('[data-test="chart-row-delete"]').first().waitFor();
await page.locator('[data-test="chart-row-delete"]').first().click();
// The action reads as "Archive", not "Delete". Scope to the dialog: with
// the flag on, every list row's delete action is also named "Archive", so
// an unscoped button query is a strict-mode violation (25 rows + modal).
const dialog = page.getByRole('dialog');
await expect(dialog.getByText(/^Archive .+\?$/)).toBeVisible();
await expect(dialog.getByRole('button', { name: 'Archive' })).toBeVisible();
// Recoverable copy instead of "Are you sure … permanently".
await expect(page.getByText(/moved to Recently Archived/i)).toBeVisible();
await expect(
page.getByText(/recover it there within \d+ days/i),
).toBeVisible();
// No "type DELETE to confirm" input in recoverable mode.
await expect(page.getByTestId('delete-modal-input')).toHaveCount(0);
// Dismiss without deleting.
await page.getByTestId('close-modal-btn').click();
});
@@ -29,7 +29,7 @@
* restore it and asserts — via the API — that it is live again.
*/
import { test, expect, Page } from '@playwright/test';
import { apiGet, apiPost } from '../../helpers/api/requests';
import { apiGet } from '../../helpers/api/requests';
import { extractIdFromResponse } from '../../helpers/api/assertions';
import {
apiPostChart,
@@ -188,58 +188,3 @@ test('permanently deletes an archived item from the view', async ({ page }) => {
await TYPES[0].softDelete(page, id).catch(() => {});
}
});
test('shows an empty message and no rows when the search matches nothing', async ({
page,
}) => {
await page.goto('archived/');
await expect(page.getByTestId('archived-list-view')).toBeVisible();
const search = page.getByPlaceholder(/type a value/i);
await search.click();
await search.fill(`e2e_nonexistent_${Date.now()}`);
await search.press('Enter');
await expect(
page.getByText('No results match your filter criteria'),
).toBeVisible();
await expect(page.getByTestId('archived-row-restore')).toHaveCount(0);
});
test('restoring an already-restored row surfaces an error without crashing', async ({
page,
}) => {
const name = `e2e_stale_${Date.now()}`;
const id = await TYPES[0].create(page, name);
// Capture the uuid before soft-delete (a soft-deleted GET returns 404).
const { uuid } = (await (await apiGetDashboard(page, id)).json()).result;
try {
expect((await apiDeleteDashboard(page, id)).ok()).toBeTruthy();
await openArchive(page, 'Dashboard', name);
await expect(page.getByText(name, { exact: false })).toBeVisible();
// Simulate another actor restoring the object out from under this view.
const restored = await apiPost(
page,
`api/v1/dashboard/${uuid}/restore`,
{},
);
expect(restored.ok()).toBeTruthy();
// Clicking the now-stale row's Restore yields a 404 → danger toast, no crash.
await page
.getByRole('row')
.filter({ hasText: name })
.getByTestId('archived-row-restore')
.click();
await expect(
page.getByText(`Failed to restore ${name}`, { exact: false }),
).toBeVisible({ timeout: 15000 });
// The page is still functional (the list view did not crash).
await expect(page.getByTestId('archived-list-view')).toBeVisible();
} finally {
// Re-archive the (possibly) restored dashboard, whatever happened above.
await apiDeleteDashboard(page, id).catch(() => {});
}
});
@@ -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);
});
});
@@ -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();
});
});
@@ -136,6 +136,7 @@ describe('DatabaseModal', () => {
format: 'int32',
maximum: 65536,
minimum: 0,
nullable: true,
type: 'integer',
},
query: {
@@ -153,7 +154,7 @@ describe('DatabaseModal', () => {
type: 'string',
},
},
required: ['database', 'host', 'port', 'username'],
required: ['database', 'host', 'username'],
type: 'object',
},
preferred: true,
@@ -141,6 +141,7 @@ const renderArchivedList = (withStore = store) =>
beforeEach(() => {
fetchMock.removeRoutes();
fetchMock.clearHistory();
mockAddDangerToast.mockClear();
});
test('renders archived rows with Name and Type columns', async () => {
@@ -204,6 +205,31 @@ test('restore failure surfaces an error and leaves the row in place', async () =
expect(screen.getByText('Deleted Chart One')).toBeInTheDocument();
});
test('restoring an already-restored row (404) surfaces an error without crashing', async () => {
// Simulates another actor having restored the object out from under this
// view: the server answers 404 to the now-stale row's restore request.
mockRoutes(404);
renderArchivedList();
await screen.findByTestId('archived-list-view');
const restoreButtons = await screen.findAllByTestId('archived-row-restore');
fireEvent.click(restoreButtons[0]);
await waitFor(() => {
expect(fetchMock.callHistory.calls(/chart\/uuid-1\/restore/)).toHaveLength(
1,
);
});
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
expect.stringContaining('Failed to restore Deleted Chart One'),
);
});
expect(mockAddDangerToast).toHaveBeenCalledTimes(1);
// The page is still functional -- the list view did not crash.
expect(screen.getByTestId('archived-list-view')).toBeInTheDocument();
});
test('row actions are keyboard-operable (Enter restores)', async () => {
mockRoutes();
renderArchivedList();
@@ -273,6 +299,45 @@ test('name search refetches with a contains filter on the name field', async ()
});
});
test('a search that matches nothing shows the empty-state and no restore actions', async () => {
// The initial load returns real rows; only the search-triggered request
// answers empty. If the list were empty from the start, this test could
// pass even if the search never fired a request at all -- so the request
// itself is asserted below before trusting the rendered empty state.
fetchMock.get(infoEndpoint, { permissions: ['can_read', 'can_write'] });
fetchMock.getOnce(listEndpoint, {
result: mockCharts,
count: mockCharts.length,
});
fetchMock.get(listEndpoint, { result: [], count: 0 });
renderArchivedList();
await screen.findByText('Deleted Chart One');
const searchInput = screen.getByPlaceholderText(/type a value/i);
fireEvent.change(searchInput, { target: { value: 'e2e_nonexistent' } });
fireEvent.keyDown(searchInput, { key: 'Enter', keyCode: 13 });
await waitFor(() => {
const hit = fetchMock.callHistory
.calls(/chart\/\?q/)
.find(call =>
call.url.includes(
'(col:slice_name,opr:chart_all_text,value:e2e_nonexistent)',
),
);
expect(hit).toBeTruthy();
});
// ListView renders this hardcoded copy whenever a filter is active and the
// result set is empty, overriding the page's own `emptyState` prop
// entirely (see ListView.tsx) -- so this is the actual rendered text, not
// the page's "No archived items" default.
expect(
await screen.findByText('No results match your filter criteria'),
).toBeInTheDocument();
expect(screen.queryAllByTestId('archived-row-restore')).toHaveLength(0);
});
test('switching Type fetches the newly selected resource with its deleted-state filter', async () => {
mockRoutes();
renderArchivedList();
@@ -239,6 +239,40 @@ describe('ChartList', () => {
screen.getByRole('button', { name: 'Bulk select' }),
).toBeInTheDocument();
});
test('archive (soft-delete) confirmation reflects recoverable semantics, not delete', async () => {
// With SOFT_DELETE on, the same delete affordance becomes reversible: the
// dialog reads "Archive", not "Delete", and drops the "type DELETE to
// confirm" gate -- that friction is reserved for the permanent purge in
// the Recently Archived view, not this one.
(
isFeatureEnabled as jest.MockedFunction<typeof isFeatureEnabled>
).mockImplementation((feature: string) => feature === 'SOFT_DELETE');
// isUserEditorOrAdmin requires `username` + `permissions` to recognize an
// Admin role (see src/types/bootstrapTypes.ts's isUserWithPermissionsAndRoles);
// mockUser lacks both, so row actions would otherwise render disabled.
const adminUser = { ...mockUser, username: 'admin', permissions: {} };
renderChartList(adminUser);
await screen.findByTestId('chart-list-view');
const deleteButtons = await screen.findAllByTestId('chart-row-delete');
fireEvent.click(deleteButtons[0]);
const dialog = await screen.findByRole('dialog');
expect(
within(dialog).getByText(`Archive ${mockCharts[0].slice_name}?`),
).toBeInTheDocument();
expect(
within(dialog).getByRole('button', { name: 'Archive' }),
).toBeInTheDocument();
expect(
within(dialog).getByText(/moved to Recently Archived/i),
).toBeInTheDocument();
expect(within(dialog).getByText(/recover it there/i)).toBeInTheDocument();
expect(screen.queryByTestId('delete-modal-input')).not.toBeInTheDocument();
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
+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",
+33 -10
View File
@@ -17,12 +17,19 @@
# pylint: disable=too-many-lines
from __future__ import annotations
import inspect
from typing import Any, TYPE_CHECKING
from flask import current_app
from flask_babel import gettext as _
from marshmallow import EXCLUDE, fields, post_load, Schema, validate
from marshmallow import (
EXCLUDE,
fields,
post_load,
Schema,
validate,
validates,
ValidationError,
)
from marshmallow.validate import Length, Range
from marshmallow_union import Union
@@ -972,21 +979,37 @@ class ChartDataGeodeticParseOptionsSchema(
class ChartDataPostProcessingOperationSchema(Schema):
_builtin_ops = pandas_postprocessing.__all__
operation = fields.String(
metadata={
"description": "Post processing operation type",
"example": "aggregate",
},
required=True,
validate=validate.OneOf(
choices=[
name
for name, value in inspect.getmembers(
pandas_postprocessing, inspect.isfunction
)
]
),
)
@validates("operation")
def validate_operation(self, value: str, **kwargs: object) -> None:
# Built-in operations validate without reading the config, so schemas can
# still be loaded outside of an app context.
if value in self._builtin_ops:
return
try:
extra = current_app.config.get("EXTRA_PANDAS_POSTPROCESSING_OPS", [])
except RuntimeError:
# Outside app context, only built-in operations are known
extra = []
allowed = set(self._builtin_ops) | set(
pandas_postprocessing.build_extra_ops_map(extra)
)
if value not in allowed:
raise ValidationError(
f"Must be one of: {sorted(allowed)!r}.",
)
options = fields.Dict(
metadata={
"description": "Options specifying how to perform the operation. Please "
-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:
+28 -11
View File
@@ -23,6 +23,7 @@ from datetime import datetime
from pprint import pformat
from typing import Any, NamedTuple, TYPE_CHECKING
from flask import current_app
from flask_babel import gettext as _
from jinja2.exceptions import TemplateError
from pandas import DataFrame
@@ -229,16 +230,23 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
Comparing against the signature avoids a hard-coded list of removed
option names, which would need extending at each release.
Only the built-in operations in ``pandas_postprocessing.__all__`` are
inspected. The module also exposes helpers, imported submodules and
typing aliases, none of which are operations; and options belonging to a
callable registered through ``EXTRA_PANDAS_POSTPROCESSING_OPS`` are the
operator's to manage, so both are passed through untouched.
"""
operation = post_proc.get("operation")
function = (
getattr(pandas_postprocessing, operation, None)
if isinstance(operation, str)
if isinstance(operation, str) and operation in pandas_postprocessing.__all__
else None
)
if function is None:
# A missing or unknown operation is left untouched, so that
# exec_post_processing reports it as InvalidPostProcessingError.
# A missing, unknown or operator-registered operation is left
# untouched, so that exec_post_processing either dispatches it or
# reports it as InvalidPostProcessingError.
return post_proc
parameters = inspect.signature(function).parameters
@@ -623,13 +631,22 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
raise InvalidPostProcessingError(
_("`operation` property of post processing object undefined")
)
if not hasattr(pandas_postprocessing, operation):
raise InvalidPostProcessingError(
_(
"Unsupported post processing operation: %(operation)s",
type=operation,
)
# ``__all__`` is the authoritative list of built-in operations.
# ``hasattr`` would also match module internals (helpers, imported
# submodules, typing aliases), shadowing a like-named custom op.
if operation in pandas_postprocessing.__all__:
func = getattr(pandas_postprocessing, operation)
else:
extra_ops = pandas_postprocessing.build_extra_ops_map(
current_app.config.get("EXTRA_PANDAS_POSTPROCESSING_OPS", [])
)
options = post_process.get("options", {})
df = getattr(pandas_postprocessing, operation)(df, **options)
if operation not in extra_ops:
raise InvalidPostProcessingError(
_(
"Unsupported post processing operation: %(operation)s",
operation=operation,
)
)
func = extra_ops[operation]
df = func(df, **post_process.get("options", {}))
return df
+11
View File
@@ -358,6 +358,17 @@ SQLALCHEMY_ENCRYPTED_FIELD_ENGINE: Literal["aes", "aes-gcm"] = "aes"
# Extends the default SQLGlot dialects with additional dialects
SQLGLOT_DIALECTS_EXTENSIONS: DialectExtensions | Callable[[], DialectExtensions] = {}
# Extra pandas post-processing operations to register alongside the built-in ones.
# Each entry must be a named callable (i.e. have a __name__ attribute) with the
# signature:
# def my_op(df: pandas.DataFrame, **options: Any) -> pandas.DataFrame
# The function is registered under its __name__ as the operation name. Callables
# without __name__ (e.g. functools.partial, lambda) are silently ignored.
# Example:
# from mypackage.ops import my_custom_op
# EXTRA_PANDAS_POSTPROCESSING_OPS = [my_custom_op]
EXTRA_PANDAS_POSTPROCESSING_OPS: list[Callable[..., Any]] = []
# The limit of queries fetched for query search
QUERY_SEARCH_LIMIT = 1000
+6 -1
View File
@@ -3010,6 +3010,11 @@ class BasicParametersMixin:
# for Databend this would be `{"sslmode": "disable"}`, eg.
encryption_disable_parameters: dict[str, str] = {}
# parameters that `validate_parameters` treats as mandatory; subclasses
# override this to relax a parameter (e.g. `port`) without duplicating
# the rest of `validate_parameters`
required_parameters: set[str] = {"host", "port", "username", "database"}
@classmethod
def build_sqlalchemy_uri( # pylint: disable=unused-argument
cls,
@@ -3081,7 +3086,7 @@ class BasicParametersMixin:
"""
errors: list[SupersetError] = []
required = {"host", "port", "username", "database"}
required = cls.required_parameters
parameters = properties.get("parameters", {})
present = {key for key in parameters if parameters.get(key, ())}
-1
View File
@@ -389,7 +389,6 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
}
}
},
future=True,
)
conn = engine.connect()
idx = 0
+65
View File
@@ -24,6 +24,8 @@ from re import Pattern
from typing import Any, Callable, Optional, TYPE_CHECKING
from flask_babel import gettext as __
from marshmallow import fields, pre_load
from marshmallow.validate import Range
from sqlalchemy import text, types
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON
from sqlalchemy.dialects.postgresql.base import PGInspector
@@ -37,6 +39,8 @@ from superset.db_engine_specs.base import (
AURORA_DATA_API_KNOWN_INCOMPATIBILITIES,
BaseEngineSpec,
BasicParametersMixin,
BasicParametersSchema,
BasicParametersType,
DatabaseCategory,
TimestampExpression,
)
@@ -298,6 +302,34 @@ class PostgresBaseEngineSpec(BaseEngineSpec):
return None
class PostgresParametersSchema(BasicParametersSchema):
"""
Same as ``BasicParametersSchema``, except ``port`` is optional: a blank
port falls back to Postgres's own default (5432) in
``PostgresEngineSpec.build_sqlalchemy_uri``.
"""
port = fields.Integer(
required=False,
allow_none=True,
metadata={"description": __("Database port")},
validate=Range(min=0, max=2**16, max_inclusive=False),
)
@pre_load
def blank_port_to_none(self, data: Any, **kwargs: Any) -> Any:
"""
A cleared number input in the Connect Database form submits ``""``
for ``port`` (HTML input values are always strings) rather than
omitting the key or sending ``null``. Normalize it to ``None`` so it
deserializes cleanly instead of failing with "Not a valid integer.",
and is treated as blank -- same as an omitted port -- downstream.
"""
if isinstance(data, dict) and data.get("port") == "":
data = {**data, "port": None}
return data
class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
engine = "postgresql"
engine_name = "PostgreSQL"
@@ -309,6 +341,11 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
supports_grouping_sets = True
default_driver = "psycopg2"
parameters_schema = PostgresParametersSchema()
# ``port`` is intentionally not required: a blank port falls back to
# Postgres's own default (``metadata["default_port"]``) in
# ``BasicParametersMixin.build_sqlalchemy_uri`` (overridden below).
required_parameters = {"host", "username", "database"}
sqlalchemy_uri_placeholder = (
"postgresql://user:password@host:port/dbname[?key=value&key=value...]"
)
@@ -674,6 +711,34 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
return uri, connect_args
@classmethod
def build_sqlalchemy_uri(
cls,
parameters: BasicParametersType,
encrypted_extra: dict[str, str] | None = None,
) -> str:
"""
Default a missing/blank port to Postgres's own default (5432) so the
dynamic form can connect without requiring the port to be filled in.
Only an absent key, ``None``, or ``""`` (what a cleared number input
submits, since this may be called directly with raw, non-schema-
loaded parameters -- see ``ValidateDatabaseParametersCommand``) are
treated as blank; an explicitly supplied port -- including ``0`` --
is preserved as-is rather than overwritten by a truthiness check.
"""
port = parameters.get("port")
resolved_port: int = (
cls.metadata["default_port"] if port is None or port == "" else port
)
parameters_with_default_port: BasicParametersType = {
**parameters,
"port": resolved_port,
}
return super().build_sqlalchemy_uri(
parameters_with_default_port, encrypted_extra
)
@staticmethod
def mutate_db_for_connection_test(database: Database) -> None:
"""
+17
View File
@@ -1365,6 +1365,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
self.configure_cache()
self.set_db_default_isolation()
self.configure_sqlglot_dialects()
self.configure_extra_post_processing_ops()
with self.superset_app.app_context():
self.init_app_in_ctx()
@@ -1438,6 +1439,22 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
SQLGLOT_DIALECTS.update(extensions)
def configure_extra_post_processing_ops(self) -> None:
from superset.utils.pandas_postprocessing import (
__all__ as builtin_ops,
build_extra_ops_map,
)
extra = self.config.get("EXTRA_PANDAS_POSTPROCESSING_OPS", [])
for name in build_extra_ops_map(extra):
if name in builtin_ops:
logger.warning(
"EXTRA_PANDAS_POSTPROCESSING_OPS: '%s' conflicts with a "
"built-in post-processing operation and will never fire. "
"Rename the custom function to avoid the conflict.",
name,
)
@transaction()
def configure_fab(self) -> None:
if self.config["SILENCE_FAB"]:
@@ -655,6 +655,7 @@ def build_query_context_from_form_data(
order_desc: bool | None = None,
result_type: Any = None,
force: bool = False,
custom_cache_timeout: int | None = None,
) -> Any:
"""Build a QueryContext from chart-type-aware Explore form_data."""
# avoid circular import
@@ -683,6 +684,7 @@ def build_query_context_from_form_data(
form_data=form_data,
result_type=result_type,
force=force,
custom_cache_timeout=custom_cache_timeout,
)
@@ -112,6 +112,11 @@ _VIZ_CATEGORY: dict[str, str] = {
_MAX_RECOMMENDATIONS = 4
def _compute_effective_force(request: GetChartDataRequest) -> bool:
"""use_cache=False must also bypass the cache, not just force_refresh=True."""
return request.force_refresh or not request.use_cache
def _coerce_row_limit(value: Any, default: int) -> int:
"""Coerce a row_limit (which may arrive as a str from chart.params) to int,
falling back to ``default`` when it is missing, non-numeric, or non-positive.
@@ -359,6 +364,7 @@ async def get_chart_data( # noqa: C901
request.cache_timeout,
)
)
effective_force = _compute_effective_force(request)
try:
await ctx.report_progress(1, 4, "Looking up chart")
@@ -570,7 +576,8 @@ async def get_chart_data( # noqa: C901
extra_form_data=request.extra_form_data,
row_limit=row_limit,
order_desc=cached_form_data_dict.get("order_desc", True),
force=request.force_refresh,
force=effective_force,
custom_cache_timeout=request.cache_timeout,
)
await ctx.debug(
"Built query_context from cached form_data (unsaved state)"
@@ -666,11 +673,14 @@ async def get_chart_data( # noqa: C901
},
queries=fallback_queries,
form_data=form_data,
force=request.force_refresh,
force=effective_force,
custom_cache_timeout=request.cache_timeout,
)
elif query_context_json is not None:
# Apply request overrides to the saved query_context
query_context_json["force"] = request.force_refresh
query_context_json["force"] = effective_force
if request.cache_timeout is not None:
query_context_json["custom_cache_timeout"] = request.cache_timeout
# Ignore a non-positive limit so it can't emit LIMIT -1 downstream.
if request.limit and request.limit > 0:
@@ -1054,6 +1064,7 @@ async def _query_from_form_data(
current_app.config["ROW_LIMIT"],
)
viz_type = form_data.get("viz_type", "unknown")
effective_force = _compute_effective_force(request)
try:
query_context = build_query_context_from_form_data(
@@ -1061,7 +1072,8 @@ async def _query_from_form_data(
extra_form_data=request.extra_form_data,
row_limit=row_limit,
order_desc=form_data.get("order_desc", True),
force=request.force_refresh,
force=effective_force,
custom_cache_timeout=request.cache_timeout,
)
await ctx.report_progress(3, 4, "Executing data query")
+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

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