mirror of
https://github.com/apache/superset.git
synced 2026-08-20 23:21:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f015871ee1 |
@@ -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,7 +1,7 @@
|
||||
---
|
||||
name: Cosmetic Issue
|
||||
about: Describe a cosmetic issue with CSS, positioning, layout, labeling, or similar
|
||||
labels: "#bug:cosmetic"
|
||||
labels: "cosmetic-issue"
|
||||
---
|
||||
|
||||
## Screenshot
|
||||
|
||||
@@ -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)
|
||||
engine = create_engine(sqlalchemy_uri, future=True)
|
||||
Base = automap_base() # noqa: N806
|
||||
Base.prepare(engine, reflect=True)
|
||||
seen = set()
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 } from '../../helpers/api/requests';
|
||||
import { apiGet, apiPost } from '../../helpers/api/requests';
|
||||
import { extractIdFromResponse } from '../../helpers/api/assertions';
|
||||
import {
|
||||
apiPostChart,
|
||||
@@ -188,3 +188,58 @@ 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,14 +467,6 @@ 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)
|
||||
|
||||
+1
-69
@@ -20,14 +20,13 @@ 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 { StackControlsValue, TIMESERIES_CONSTANTS } from '../../src/constants';
|
||||
import { TIMESERIES_CONSTANTS } from '../../src/constants';
|
||||
import {
|
||||
LegendOrientation,
|
||||
EchartsTimeseriesChartProps,
|
||||
@@ -567,70 +566,3 @@ 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,11 +223,6 @@ 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,20 +91,4 @@ 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,17 +105,10 @@ export const SamplesPane = ({
|
||||
1,
|
||||
)
|
||||
.then(response => {
|
||||
// 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);
|
||||
setData(ensureIsArray(response.data));
|
||||
setColnames(ensureIsArray(response.colnames));
|
||||
setColtypes(ensureIsArray(response.coltypes));
|
||||
setRowCount(response.rowcount);
|
||||
setResponseError('');
|
||||
cache.set(queryFormData, true);
|
||||
if (queryForce) {
|
||||
|
||||
@@ -60,27 +60,6 @@ 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(() => {
|
||||
@@ -135,29 +114,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,9 @@ import { ControlFormItemComponents } from './ControlForm';
|
||||
* Column formatting configs.
|
||||
*/
|
||||
export type ColumnConfig = {
|
||||
[key in SharedColumnConfigProp]?: (typeof SHARED_COLUMN_CONFIG_PROPS)[key]['value'];
|
||||
[
|
||||
key in SharedColumnConfigProp
|
||||
]?: (typeof SHARED_COLUMN_CONFIG_PROPS)[key]['value'];
|
||||
} & Record<string, StrictJsonValue>;
|
||||
|
||||
/**
|
||||
|
||||
+5
-4
@@ -22,10 +22,11 @@ import FixedOrMetricControl from '.';
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
const createProps = () => ({
|
||||
|
||||
@@ -136,7 +136,6 @@ describe('DatabaseModal', () => {
|
||||
format: 'int32',
|
||||
maximum: 65536,
|
||||
minimum: 0,
|
||||
nullable: true,
|
||||
type: 'integer',
|
||||
},
|
||||
query: {
|
||||
@@ -154,7 +153,7 @@ describe('DatabaseModal', () => {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['database', 'host', 'username'],
|
||||
required: ['database', 'host', 'port', 'username'],
|
||||
type: 'object',
|
||||
},
|
||||
preferred: true,
|
||||
|
||||
+5
-4
@@ -38,10 +38,11 @@ import {
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
|
||||
+5
-4
@@ -29,10 +29,11 @@ import DatasetPanelWrapper from 'src/features/datasets/AddDataset/DatasetPanel';
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
const errorMessageRegistry = getErrorMessageComponentRegistry();
|
||||
|
||||
@@ -141,7 +141,6 @@ const renderArchivedList = (withStore = store) =>
|
||||
beforeEach(() => {
|
||||
fetchMock.removeRoutes();
|
||||
fetchMock.clearHistory();
|
||||
mockAddDangerToast.mockClear();
|
||||
});
|
||||
|
||||
test('renders archived rows with Name and Type columns', async () => {
|
||||
@@ -205,31 +204,6 @@ 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();
|
||||
@@ -299,45 +273,6 @@ 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,40 +239,6 @@ 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
|
||||
|
||||
+10
-33
@@ -17,19 +17,12 @@
|
||||
# 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,
|
||||
validates,
|
||||
ValidationError,
|
||||
)
|
||||
from marshmallow import EXCLUDE, fields, post_load, Schema, validate
|
||||
from marshmallow.validate import Length, Range
|
||||
from marshmallow_union import Union
|
||||
|
||||
@@ -979,37 +972,21 @@ 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 "
|
||||
|
||||
@@ -280,6 +280,9 @@ 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
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ class BaseStreamingCSVExportCommand(BaseCommand):
|
||||
delimiter = csv_export_config.get("sep", ",")
|
||||
decimal_separator = csv_export_config.get("decimal", ".")
|
||||
|
||||
with db.session() as session:
|
||||
with db.session(future=True) as session:
|
||||
# Merge database to prevent DetachedInstanceError
|
||||
merged_database = session.merge(database)
|
||||
|
||||
|
||||
@@ -291,30 +291,19 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
|
||||
),
|
||||
None,
|
||||
)
|
||||
# Point the x-axis at the overridden Time Column (granularity).
|
||||
# Replaces x-axis column values with 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:
|
||||
|
||||
@@ -23,7 +23,6 @@ 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
|
||||
@@ -230,23 +229,16 @@ 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) and operation in pandas_postprocessing.__all__
|
||||
if isinstance(operation, str)
|
||||
else None
|
||||
)
|
||||
if function is None:
|
||||
# A missing, unknown or operator-registered operation is left
|
||||
# untouched, so that exec_post_processing either dispatches it or
|
||||
# reports it as InvalidPostProcessingError.
|
||||
# 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
|
||||
@@ -631,22 +623,13 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
raise InvalidPostProcessingError(
|
||||
_("`operation` property of post processing object undefined")
|
||||
)
|
||||
# ``__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", [])
|
||||
)
|
||||
if operation not in extra_ops:
|
||||
raise InvalidPostProcessingError(
|
||||
_(
|
||||
"Unsupported post processing operation: %(operation)s",
|
||||
operation=operation,
|
||||
)
|
||||
if not hasattr(pandas_postprocessing, operation):
|
||||
raise InvalidPostProcessingError(
|
||||
_(
|
||||
"Unsupported post processing operation: %(operation)s",
|
||||
type=operation,
|
||||
)
|
||||
func = extra_ops[operation]
|
||||
df = func(df, **post_process.get("options", {}))
|
||||
)
|
||||
options = post_process.get("options", {})
|
||||
df = getattr(pandas_postprocessing, operation)(df, **options)
|
||||
return df
|
||||
|
||||
@@ -358,17 +358,6 @@ 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
|
||||
|
||||
|
||||
@@ -389,6 +389,7 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
|
||||
}
|
||||
}
|
||||
},
|
||||
future=True,
|
||||
)
|
||||
conn = engine.connect()
|
||||
idx = 0
|
||||
|
||||
@@ -24,8 +24,6 @@ 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
|
||||
@@ -39,9 +37,6 @@ from superset.db_engine_specs.base import (
|
||||
AURORA_DATA_API_KNOWN_INCOMPATIBILITIES,
|
||||
BaseEngineSpec,
|
||||
BasicParametersMixin,
|
||||
BasicParametersSchema,
|
||||
BasicParametersType,
|
||||
BasicPropertiesType,
|
||||
DatabaseCategory,
|
||||
TimestampExpression,
|
||||
)
|
||||
@@ -51,7 +46,6 @@ from superset.models.sql_lab import Query
|
||||
from superset.sql.parse import process_jinja_sql
|
||||
from superset.utils import core as utils, json
|
||||
from superset.utils.core import GenericDataType, QuerySource
|
||||
from superset.utils.network import is_hostname_valid, is_port_open
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.models.core import Database # pragma: no cover
|
||||
@@ -304,34 +298,6 @@ 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"
|
||||
@@ -343,7 +309,6 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
|
||||
supports_grouping_sets = True
|
||||
|
||||
default_driver = "psycopg2"
|
||||
parameters_schema = PostgresParametersSchema()
|
||||
sqlalchemy_uri_placeholder = (
|
||||
"postgresql://user:password@host:port/dbname[?key=value&key=value...]"
|
||||
)
|
||||
@@ -709,113 +674,6 @@ 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
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_parameters(
|
||||
cls, properties: BasicPropertiesType
|
||||
) -> list[SupersetError]:
|
||||
"""
|
||||
Validates any number of parameters, for progressive validation.
|
||||
|
||||
Same as ``BasicParametersMixin.validate_parameters``, except ``port``
|
||||
is not a required parameter: a blank port is valid, since
|
||||
``build_sqlalchemy_uri`` falls back to Postgres's own default. Port
|
||||
format/range/open checks still run whenever a port is present.
|
||||
"""
|
||||
errors: list[SupersetError] = []
|
||||
|
||||
required = {"host", "username", "database"}
|
||||
parameters = properties.get("parameters", {})
|
||||
present = {key for key in parameters if parameters.get(key, ())}
|
||||
|
||||
if missing := sorted(required - present):
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message=f"One or more parameters are missing: {', '.join(missing)}",
|
||||
error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
|
||||
level=ErrorLevel.WARNING,
|
||||
extra={"missing": missing},
|
||||
),
|
||||
)
|
||||
|
||||
host = parameters.get("host", None)
|
||||
if not host:
|
||||
return errors
|
||||
if not is_hostname_valid(host):
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message="The hostname provided can't be resolved.",
|
||||
error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
|
||||
level=ErrorLevel.ERROR,
|
||||
extra={"invalid": ["host"]},
|
||||
),
|
||||
)
|
||||
return errors
|
||||
|
||||
port = parameters.get("port", None)
|
||||
if not port:
|
||||
return errors
|
||||
try:
|
||||
port = int(port)
|
||||
except (ValueError, TypeError):
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message="Port must be a valid integer.",
|
||||
error_type=SupersetErrorType.CONNECTION_INVALID_PORT_ERROR,
|
||||
level=ErrorLevel.ERROR,
|
||||
extra={"invalid": ["port"]},
|
||||
),
|
||||
)
|
||||
if not (isinstance(port, int) and 0 <= port < 2**16):
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message=(
|
||||
"The port must be an integer between 0 and 65535 (inclusive)."
|
||||
),
|
||||
error_type=SupersetErrorType.CONNECTION_INVALID_PORT_ERROR,
|
||||
level=ErrorLevel.ERROR,
|
||||
extra={"invalid": ["port"]},
|
||||
),
|
||||
)
|
||||
elif not is_port_open(host, port):
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message="The port is closed.",
|
||||
error_type=SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
|
||||
level=ErrorLevel.ERROR,
|
||||
extra={"invalid": ["port"]},
|
||||
),
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@staticmethod
|
||||
def mutate_db_for_connection_test(database: Database) -> None:
|
||||
"""
|
||||
|
||||
@@ -1365,7 +1365,6 @@ 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()
|
||||
@@ -1439,22 +1438,6 @@ 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,7 +655,6 @@ 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
|
||||
@@ -684,7 +683,6 @@ def build_query_context_from_form_data(
|
||||
form_data=form_data,
|
||||
result_type=result_type,
|
||||
force=force,
|
||||
custom_cache_timeout=custom_cache_timeout,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -112,11 +112,6 @@ _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.
|
||||
@@ -364,7 +359,6 @@ 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")
|
||||
@@ -576,8 +570,7 @@ 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=effective_force,
|
||||
custom_cache_timeout=request.cache_timeout,
|
||||
force=request.force_refresh,
|
||||
)
|
||||
await ctx.debug(
|
||||
"Built query_context from cached form_data (unsaved state)"
|
||||
@@ -673,14 +666,11 @@ async def get_chart_data( # noqa: C901
|
||||
},
|
||||
queries=fallback_queries,
|
||||
form_data=form_data,
|
||||
force=effective_force,
|
||||
custom_cache_timeout=request.cache_timeout,
|
||||
force=request.force_refresh,
|
||||
)
|
||||
elif query_context_json is not None:
|
||||
# Apply request overrides to the saved query_context
|
||||
query_context_json["force"] = effective_force
|
||||
if request.cache_timeout is not None:
|
||||
query_context_json["custom_cache_timeout"] = request.cache_timeout
|
||||
query_context_json["force"] = request.force_refresh
|
||||
|
||||
# Ignore a non-positive limit so it can't emit LIMIT -1 downstream.
|
||||
if request.limit and request.limit > 0:
|
||||
@@ -1064,7 +1054,6 @@ 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(
|
||||
@@ -1072,8 +1061,7 @@ 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=effective_force,
|
||||
custom_cache_timeout=request.cache_timeout,
|
||||
force=request.force_refresh,
|
||||
)
|
||||
|
||||
await ctx.report_progress(3, 4, "Executing data query")
|
||||
|
||||
@@ -376,7 +376,7 @@ def upgrade_catalog_perms(engines: set[str] | None = None) -> None:
|
||||
|
||||
"""
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# See upgrade_catalog_perms: avoid eager-loading the ``ssh_tunnel`` backref so the
|
||||
# query stays schema-safe across migration revisions.
|
||||
|
||||
+1
-1
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# Use Slice class defined here instead of models.Slice
|
||||
for slc in session.query(Slice).all():
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ def upgrade():
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
for slc in session.query(Slice).all():
|
||||
if slc.datasource_type == "druid":
|
||||
slc.druid_datasource_id = slc.datasource_id
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ class Database(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for obj in session.query(Database).all():
|
||||
obj.allow_run_sync = True
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
slices = session.query(Slice).all()
|
||||
slice_len = len(slices)
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ class Url(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = session.query(Dashboard).all()
|
||||
for i, dashboard in enumerate(dashboards):
|
||||
|
||||
@@ -57,7 +57,7 @@ def upgrade():
|
||||
),
|
||||
)
|
||||
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# Use Slice class defined here instead of models.Slice
|
||||
for tbl in session.query(Table).all():
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
try:
|
||||
|
||||
+2
-2
@@ -68,7 +68,7 @@ class Database(Base):
|
||||
|
||||
|
||||
def replace(source, target):
|
||||
with db.Session(bind=op.get_bind()) as session:
|
||||
with db.Session(bind=op.get_bind(), future=True) 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)
|
||||
engine = create_engine(database.sqlalchemy_uri, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
try:
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).filter(Slice.viz_type == "pie").all():
|
||||
try:
|
||||
|
||||
+2
-2
@@ -134,7 +134,7 @@ def compute_time_compare(granularity, periods):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
tables = [
|
||||
Annotation,
|
||||
|
||||
@@ -59,7 +59,7 @@ class TableColumn(BaseColumnMixin, Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# Delete the orphaned metrics records.
|
||||
for record in session.query(DruidMetric).all():
|
||||
|
||||
+1
-1
@@ -579,7 +579,7 @@ def scan_dashboard_positions_data(positions):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = session.query(Dashboard).all()
|
||||
for i, dashboard in enumerate(dashboards): # noqa: B007
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ class Dashboard(Base):
|
||||
|
||||
def upgrade(): # noqa: C901
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
filter_box_slices = session.query(Slice).filter_by(viz_type="filter_box")
|
||||
for slc in filter_box_slices.all():
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ def upgrade():
|
||||
|
||||
bind = op.get_bind()
|
||||
insp = sa.engine.reflection.Inspector.from_engine(bind)
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
tables = [
|
||||
Annotation,
|
||||
|
||||
+2
-2
@@ -62,7 +62,7 @@ def add_parent_ids(node, layout):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
try:
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ class DashboardSlices(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# find dup records in dashboard_slices tbl
|
||||
dup_records = (
|
||||
|
||||
+1
-1
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
for t in session.query(Sqlatable).all():
|
||||
db_name = (
|
||||
t.database.verbose_name
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ down_revision = "11c737c17cc6"
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
tables = [
|
||||
Annotation,
|
||||
|
||||
@@ -48,7 +48,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
try:
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
if slc.params:
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class Dashboard(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = session.query(Dashboard).all()
|
||||
for i, dashboard in enumerate(dashboards):
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ def upgrade():
|
||||
"""
|
||||
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# Visualization types which support time granularity (hence negate).
|
||||
viz_types = [
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ def duration_by_name(database: Database):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
query = (
|
||||
session.query(Slice, Database)
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ def create_new_markdown_component(chart_position, url):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dash_to_migrate = defaultdict(list)
|
||||
iframe_urls = defaultdict(list)
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ def upgrade(): # noqa: C901
|
||||
"""
|
||||
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
session = orm.Session(bind=bind, future=True)
|
||||
|
||||
faulty_view_menus = (
|
||||
session.query(ViewMenu)
|
||||
|
||||
+2
-2
@@ -119,7 +119,7 @@ def update_dashboards(session, uuid_map):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# remove uuid from position_json
|
||||
update_dashboards(session, {})
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ def upgrade():
|
||||
),
|
||||
)
|
||||
# Migrate data
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
alerts = session.query(Alert).all()
|
||||
for a in alerts:
|
||||
if a.sql:
|
||||
|
||||
+2
-2
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# Add uuid column
|
||||
try:
|
||||
@@ -86,7 +86,7 @@ def upgrade():
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind) # noqa: F841
|
||||
session = db.Session(bind=bind, future=True) # noqa: F841
|
||||
|
||||
# Remove uuid column
|
||||
with op.batch_alter_table("saved_query") as batch_op:
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -65,7 +65,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
x_dateunit_in_since = DateRangeMigration.x_dateunit_in_since
|
||||
x_dateunit_in_until = DateRangeMigration.x_dateunit_in_until
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
slices_changed = 0
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
slices = (
|
||||
session.query(Slice)
|
||||
@@ -75,7 +75,7 @@ def upgrade():
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
slices = (
|
||||
session.query(Slice)
|
||||
|
||||
+2
-2
@@ -46,7 +46,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).filter(Slice.viz_type.like("graph_chart")):
|
||||
params = json.loads(slc.params)
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ def has_uuid_column(table_name, bind):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for table_name, model in models.items():
|
||||
# this script adds missing uuid columns
|
||||
|
||||
+2
-2
@@ -46,7 +46,7 @@ class Dashboard(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = (
|
||||
session.query(Dashboard)
|
||||
@@ -74,7 +74,7 @@ def upgrade():
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = (
|
||||
session.query(Dashboard)
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ class SqlaTable(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for datasource in session.query(SqlaTable):
|
||||
if datasource.extra:
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ def upgrade():
|
||||
Convert all country names to lowercase
|
||||
"""
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).filter(Slice.viz_type == "country_map").all():
|
||||
try:
|
||||
|
||||
+2
-2
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = (
|
||||
session.query(Dashboard)
|
||||
@@ -208,7 +208,7 @@ def upgrade():
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = (
|
||||
session.query(Dashboard)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
dashboards = (
|
||||
session.query(Dashboard)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for database in session.query(Database).all():
|
||||
try:
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ VALID_RENDERERS = (
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
slices = (
|
||||
session.query(Slice)
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ PVM_MAP = {
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# 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)
|
||||
session = Session(bind=bind, future=True)
|
||||
|
||||
# Add the old permissions on the migration itself
|
||||
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ def upgrade():
|
||||
|
||||
def remove_value_if_too_long():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
# 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
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ class Dashboard(Base):
|
||||
def upgrade():
|
||||
logger.info("[AddTypeToNativeFilter] Starting upgrade")
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for dashboard in session.query(Dashboard).all():
|
||||
logger.info(
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ class Slice(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
where_clause = and_(
|
||||
Slice.viz_type == "pivot_table_v2",
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ class Slice(Base):
|
||||
|
||||
def migrate(mapping: dict[str, str]) -> None:
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for slc in session.query(Slice).all():
|
||||
try:
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ class Database(Base):
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
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)
|
||||
session = db.Session(bind=bind, future=True)
|
||||
|
||||
for database in session.query(Database).all():
|
||||
try:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user