Compare commits

..

1 Commits

Author SHA1 Message Date
Elizabeth Thompson
7d5e8ad785 fix(sqllab): roll back session before retrying get_query after a broken transaction
`get_query` catches any exception from the ORM lookup and relies on the
`backoff` decorator to retry up to 5 times. When the underlying failure is
(or causes) a SQLAlchemy `PendingRollbackError` - e.g. a `PendingRollbackError`
chained under an `OperationalError`/`QueryCanceled` from a dropped connection
or statement timeout - the session is left in a broken state that SQLAlchemy
refuses to use again until `.rollback()` is called explicitly. Since the
session was never rolled back, every one of the 5 retries reused the same
poisoned session and failed identically, so the retry loop never had a
chance to recover from what may be a transient connection blip.

Roll back the session in the except block before raising `SqlLabException`
so each `backoff` retry starts from a clean session. The exception raised
and logged is unchanged.

Fixes SUPERSET-PYTHON-WDZ

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 15:12:43 +00:00
26 changed files with 114 additions and 627 deletions

View File

@@ -1,54 +0,0 @@
name: Label Merge Conflicts
# Sweeps every open PR and labels the ones GitHub reports as CONFLICTING with
# `requires:rebase` (removing it once a rebase makes the PR mergeable again),
# so the label can be used to filter the PR backlog for the ones that need a
# rebase before they can be reviewed/merged.
#
# The action itself always re-checks *every* open PR via GraphQL on each run
# regardless of what triggered it (see eps1lon/actions-label-merge-conflict's
# sources/main.ts) - there's no way to scope it to "just this PR". The
# project's own README suggests triggering on `push` (to the default branch)
# plus `pull_request_target: [synchronize]`, but on a repo with Superset's PR
# volume that combination would re-sweep the entire open-PR list on every
# merge to master *and* every push to *any* open PR - many times an hour.
# A schedule bounds that to a fixed, predictable cadence instead; adjust it
# if 2 hours turns out to be too slow or too chatty in practice.
on:
schedule:
- cron: "0 */2 * * *"
workflow_dispatch:
# Avoid two full backlog sweeps racing (a manual workflow_dispatch landing
# mid-schedule-tick, say); queue rather than cancel so an in-progress
# paginated sweep always runs to completion.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions: {}
jobs:
label-merge-conflicts:
# Scheduled/dispatch workflows still run on forks that carry this file;
# skip anywhere but the canonical repo.
if: github.repository == 'apache/superset'
name: Label Merge Conflicts
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # to add/remove requires:rebase and need:merge
steps:
# ASF Infra allowlists this whole action via a wildcard
# (eps1lon/actions-label-merge-conflict@*), so any pinned SHA/version
# is already fine here - no Infra ticket needed for future bumps.
- uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
with:
dirtyLabel: "requires:rebase"
# A conflicting PR isn't actually ready to merge; strip that signal
# if it was previously set so reviewers don't act on a stale one.
removeOnDirtyLabel: "need:merge"
repoToken: ${{ secrets.GITHUB_TOKEN }}
# Intentionally no commentOnDirty/commentOnClean: the label alone is
# the signal (matches the label's existing description, and avoids
# a one-time comment storm across the whole backlog on first run).

View File

@@ -216,12 +216,6 @@ test('should render the error', async () => {
.spyOn(SupersetClient, 'post')
.mockRejectedValue(new Error('Something went wrong'));
await waitForRender();
// The error is wrapped in an Alert component with a stable headline and the
// raw error text in the description — no more bare ``<pre>`` elements.
expect(await screen.findByRole('alert')).toBeVisible();
expect(
await screen.findByText('Failed to load drill-to-detail rows'),
).toBeVisible();
expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument();
});

View File

@@ -42,7 +42,6 @@ import BooleanCell from '@superset-ui/core/components/Table/cell-renderers/Boole
import NullCell from '@superset-ui/core/components/Table/cell-renderers/NullCell';
import TimeCell from '@superset-ui/core/components/Table/cell-renderers/TimeCell';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { Alert } from '@apache-superset/core/components';
import { getDatasourceSamples } from 'src/components/Chart/chartAction';
import Table, {
ColumnsType,
@@ -363,18 +362,13 @@ export default function DrillDetailPane({
if (responseError) {
// Render error if page download failed
tableContent = (
<div
<pre
css={css`
margin-top: ${theme.sizeUnit * 4}px;
`}
>
<Alert
type="error"
showIcon
message={t('Failed to load drill-to-detail rows')}
description={responseError}
/>
</div>
{responseError}
</pre>
);
} else if (bootstrapping) {
// Render loading if first page hasn't loaded

View File

@@ -50,7 +50,6 @@ const DISABLED_REASONS = {
DATABASE: t(
'Drill to detail is disabled for this database. Change the database settings to enable it.',
),
DATASOURCE: t('Drill to detail is not available for this datasource type.'),
NO_AGGREGATIONS: t(
'Drill to detail is disabled because this chart does not group data by dimension value.',
),
@@ -117,17 +116,6 @@ export const useDrillDetailMenuItems = ({
datasources[formData.datasource]?.database?.disable_drill_to_detail,
);
// Capability flag on the datasource itself. Datasources that don't model
// raw rows (e.g. semantic views) opt out via ``supports_drill_to_detail``
// in the explore data payload.
const datasourceSupportsDrillToDetail = useSelector<
RootState,
boolean | undefined
>(
({ datasources }) =>
datasources[formData.datasource]?.supports_drill_to_detail,
);
const openModal = useCallback(
(filters: BinaryQueryObjectFilterClause[], event: MouseEvent) => {
onClick(event);
@@ -170,10 +158,7 @@ export const useDrillDetailMenuItems = ({
let drillDisabled;
let drillByDisabled;
if (datasourceSupportsDrillToDetail === false) {
drillDisabled = DISABLED_REASONS.DATASOURCE;
drillByDisabled = DISABLED_REASONS.DATASOURCE;
} else if (drillToDetailDisabled) {
if (drillToDetailDisabled) {
drillDisabled = DISABLED_REASONS.DATABASE;
drillByDisabled = DISABLED_REASONS.DATABASE;
} else if (handlesDimensionContextMenu) {

View File

@@ -444,45 +444,3 @@ test('context menu renders <NULL> for null dimension values', async () => {
await expectDrillToDetailByEnabled();
await expectDrillToDetailByDimension(filterNull);
});
const buildStateWithUnsupportedDatasource = () => {
const baseState = getMockStoreWithNativeFilters().getState();
const datasourceKey = defaultFormData.datasource as string;
return {
...baseState,
datasources: {
...baseState.datasources,
[datasourceKey]: {
...baseState.datasources[datasourceKey],
supports_drill_to_detail: false,
},
},
};
};
test('dropdown menu when datasource opts out via supports_drill_to_detail=false', async () => {
cleanup();
render(<MockRenderChart formData={defaultFormData} />, {
useRouter: true,
useRedux: true,
initialState: buildStateWithUnsupportedDatasource(),
});
await expectDrillToDetailDisabled(
'Drill to detail is not available for this datasource type.',
);
await expectNoDrillToDetailBy();
});
test('context menu when datasource opts out via supports_drill_to_detail=false', async () => {
cleanup();
render(<MockRenderChart formData={defaultFormData} isContextMenu />, {
useRouter: true,
useRedux: true,
initialState: buildStateWithUnsupportedDatasource(),
});
const message = 'Drill to detail is not available for this datasource type.';
await expectDrillToDetailDisabled(message);
await expectDrillToDetailByDisabled(message);
});

View File

@@ -566,31 +566,3 @@ test('should pass filterState from dataMask to ChartContainer', () => {
mockFilterState,
);
});
test('should pass chartStackTrace to ChartContainer so dashboard chart errors stay expandable', () => {
// Regression guard for #31858: the dashboard chart wrapper stopped forwarding
// the stack trace, so failed charts rendered a flat error with no "See more"
// affordance while the same error in Explore stayed expandable.
const stackTrace = 'Traceback (most recent call last): ValueError: boom';
setup(
{},
{
...defaultState,
charts: {
...defaultState.charts,
[queryId]: {
...defaultState.charts[queryId],
chartStatus: 'failed',
chartAlert: 'Something went wrong',
chartStackTrace: stackTrace,
},
},
},
);
expect(capturedChartContainerProps).toHaveProperty(
'chartStackTrace',
stackTrace,
);
});

View File

@@ -789,7 +789,6 @@ const Chart = (props: ChartProps) => {
chartAlert={chart.chartAlert ?? undefined}
chartId={props.id}
chartStatus={chartStatus ?? undefined}
chartStackTrace={chart.chartStackTrace ?? undefined}
datasource={datasource}
dashboardId={props.dashboardId}
initialValues={EMPTY_OBJECT}

View File

@@ -232,10 +232,6 @@ export type Datasource = Dataset & {
// Populated by the dashboard datasets API alongside ``type``; declared here
// so callers can rely on structural typing instead of casting.
datasource_type?: DatasourceType;
/** False when the datasource can't return row samples (e.g. semantic views). */
supports_samples?: boolean;
/** False when the datasource can't answer drill-to-detail requests. */
supports_drill_to_detail?: boolean;
};
export type DatasourcesState = {
[key: string]: Datasource;

View File

@@ -241,44 +241,25 @@ export const DataTablesPane = ({
}
}, [resultsTabFallback]);
// Hide the Samples tab for datasources that don't expose raw rows
// (e.g. semantic views). The check is intentionally ``=== false`` so that
// datasources from older backends that don't send the flag still show the
// tab and preserve current behavior.
const showSamplesTab = datasource?.supports_samples !== false;
// If the datasource swaps to one that doesn't support samples while the
// Samples tab is active (e.g. the user picks a semantic view), the tab
// disappears from ``tabItems`` and ``activeTabKey`` is orphaned. Fall back
// to Results so the panel keeps rendering content.
useEffect(() => {
if (!showSamplesTab && activeTabKey === ResultTypes.Samples) {
setActiveTabKey(ResultTypes.Results);
}
}, [showSamplesTab, activeTabKey]);
const tabItems = [
...queryResultsPanes,
...(showSamplesTab
? [
{
key: ResultTypes.Samples,
label: t('Samples'),
children: (
<StyledDiv>
<SamplesPane
datasource={datasource}
queryFormData={queryFormData}
queryForce={queryForce}
isRequest={isRequest.samples}
setForceQuery={setForceQuery}
isVisible={ResultTypes.Samples === activeTabKey}
canDownload={canDownload}
/>
</StyledDiv>
),
},
]
: []),
{
key: ResultTypes.Samples,
label: t('Samples'),
children: (
<StyledDiv>
<SamplesPane
datasource={datasource}
queryFormData={queryFormData}
queryForce={queryForce}
isRequest={isRequest.samples}
setForceQuery={setForceQuery}
isVisible={ResultTypes.Samples === activeTabKey}
canDownload={canDownload}
/>
</StyledDiv>
),
},
];
return (

View File

@@ -21,7 +21,6 @@ import { t } from '@apache-superset/core/translation';
import { ensureIsArray } from '@superset-ui/core';
import { datasetLabelLower } from 'src/features/semanticLayers/label';
import { styled } from '@apache-superset/core/theme';
import { Alert } from '@apache-superset/core/components';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { GenericDataType } from '@apache-superset/core/common';
import { GridTable } from 'src/components/GridTable';
@@ -36,7 +35,7 @@ import {
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
import { SamplesPaneProps } from '../types';
const ErrorAlertWrapper = styled.div`
const Error = styled.pre`
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
`;
@@ -156,14 +155,7 @@ export const SamplesPane = ({
rowLimitOptions={ROW_LIMIT_OPTIONS}
onRowLimitChange={handleRowLimitChange}
/>
<ErrorAlertWrapper>
<Alert
type="error"
showIcon
message={t('Failed to load samples')}
description={responseError}
/>
</ErrorAlertWrapper>
<Error>{responseError}</Error>
</>
);
}

View File

@@ -27,14 +27,13 @@ import {
QueryData,
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { Alert } from '@apache-superset/core/components';
import { EmptyState, Loading } from '@superset-ui/core/components';
import { getChartDataRequest } from 'src/components/Chart/chartAction';
import { ResultsPaneProps, QueryResultInterface } from '../types';
import { SingleQueryResultPane } from './SingleQueryResultPane';
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
const ErrorAlertWrapper = styled.div`
const Error = styled.pre`
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
`;
@@ -200,14 +199,7 @@ export const useResultsPane = ({
isLoading={false}
canDownload={canDownload}
/>
<ErrorAlertWrapper>
<Alert
type="error"
showIcon
message={t('Failed to load results')}
description={responseError}
/>
</ErrorAlertWrapper>
<Error>{responseError}</Error>
</>
);
return Array(queryCount).fill(err);

View File

@@ -19,12 +19,7 @@
import fetchMock from 'fetch-mock';
import { FeatureFlag } from '@superset-ui/core';
import * as copyUtils from 'src/utils/copy';
import {
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { render, screen, userEvent } from 'spec/helpers/testing-library';
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { DataTablesPane } from '..';
@@ -94,48 +89,6 @@ describe('DataTablesPane', () => {
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
});
test('Hides Samples tab when datasource opts out via supports_samples=false', async () => {
const props = createDataTablesPaneProps(0);
const propsWithoutSamples = {
...props,
datasource: { ...props.datasource, supports_samples: false },
};
render(<DataTablesPane {...propsWithoutSamples} />, { useRedux: true });
expect(await screen.findByText('Results')).toBeVisible();
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
});
test('Falls back to Results when active Samples tab disappears mid-session', async () => {
// Regression for codeant Major finding on PR #41509: a datasource swap
// that hides the Samples tab while it was the active tab used to leave
// ``activeTabKey === 'samples'`` orphaned, rendering a blank panel.
const props = createDataTablesPaneProps(0);
const { rerender } = render(<DataTablesPane {...props} />, {
useRedux: true,
});
// Open the panel and pick the Samples tab.
userEvent.click(screen.getByLabelText('Expand data panel'));
userEvent.click(await screen.findByText('Samples'));
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
// Swap to a datasource that doesn't support samples (e.g. a semantic
// view). The Samples tab should disappear and the panel should land on
// Results with content still rendered.
rerender(
<DataTablesPane
{...props}
datasource={{ ...props.datasource, supports_samples: false }}
/>,
);
await waitFor(() => {
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
});
expect(screen.getByText('Results')).toBeVisible();
// Panel stays expanded and renders Results content rather than going blank.
expect(screen.getByLabelText('Collapse data panel')).toBeVisible();
});
test('Should copy data table content correctly', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',

View File

@@ -84,14 +84,10 @@ describe('SamplesPane', () => {
const props = createSamplesPaneProps({
datasourceId: 36,
});
const { findByText, findByRole } = render(<SamplesPane {...props} />, {
const { findByText } = render(<SamplesPane {...props} />, {
useRedux: true,
});
// The error is now rendered inside an Alert component, with a clear
// headline message and the raw error text as the description.
expect(await findByRole('alert')).toBeVisible();
expect(await findByText('Failed to load samples')).toBeVisible();
expect(await findByText('Error: Bad request')).toBeVisible();
});

View File

@@ -74,18 +74,6 @@ export type Datasource = Dataset & {
schema?: string;
is_sqllab_view?: boolean;
extra?: string | object;
/**
* False when the datasource (e.g. a semantic view) doesn't model raw rows
* and therefore can't return a row sample. Defaults to true on the server
* side; missing here means the explore UI keeps current behavior.
*/
supports_samples?: boolean;
/**
* False when the datasource doesn't model raw rows and therefore can't
* answer a drill-to-detail query. Tracked separately from
* ``supports_samples`` so the two capabilities can diverge.
*/
supports_drill_to_detail?: boolean;
};
export interface ExplorePageInitialData {

View File

@@ -235,15 +235,6 @@ def _get_drill_detail(
# todo(yongjie): Remove this function,
# when determining whether samples should be applied to the time filter.
datasource = _get_datasource(query_context, query_obj)
# Refuse for datasource types that don't model raw rows (e.g. semantic
# views). Mirrors the ``supports_samples`` gate on the ``/samples``
# endpoint so drill-detail is hard-blocked on the backend, not just
# hidden in the frontend menu. Defaults to ``True`` for any datasource
# class that doesn't explicitly opt out.
if not getattr(datasource, "supports_drill_to_detail", True):
raise QueryObjectValidationError(
_("Drill to detail is not available for this datasource type.")
)
query_obj = copy.copy(query_obj)
query_obj.is_timeseries = False
query_obj.metrics = None

View File

@@ -193,16 +193,6 @@ class BaseDatasource(
# Only some datasources support Row Level Security
is_rls_supported: bool = False
# Datasources that can return raw row samples (anything backed by a SQL
# table can; semantic-layer abstractions cannot, since they only expose
# pre-defined metrics and dimensions).
supports_samples: bool = True
# Datasources that can answer "drill to detail" requests — i.e. fetch the
# raw rows underlying a chart cell. Conceptually similar to ``samples``
# but kept as a separate capability so the two can diverge.
supports_drill_to_detail: bool = True
@property
def name(self) -> str:
# can be a Column or a property pointing to one
@@ -496,8 +486,6 @@ class BaseDatasource(
"order_by_choices": self.order_by_choices,
"verbose_map": self.verbose_map,
"select_star": self.select_star,
"supports_samples": self.supports_samples,
"supports_drill_to_detail": self.supports_drill_to_detail,
}
def data_for_slices( # pylint: disable=too-many-locals # noqa: C901

View File

@@ -113,9 +113,6 @@ class PinotEngineSpec(BaseEngineSpec):
) -> str:
# Pinot driver infers TIMESTAMP column as LONG, so make the quick fix.
# When the Pinot driver fix this bug, current method could be removed.
#
# TODO: remove this override once startreedata/pinot-dbapi#224 is
# merged and released, and pinotdb is bumped past that version.
if isinstance(sqla_column_type, types.TIMESTAMP):
return sqla_column_type.compile().upper()

View File

@@ -200,12 +200,6 @@ class SemanticView(AuditMixinNullable, Model):
__tablename__ = "semantic_views"
# Semantic views expose pre-defined metrics and dimensions, not raw rows,
# so neither the "Samples" tab in Explore nor the "Drill to detail"
# affordance from the chart 3-dots menu can return anything meaningful.
supports_samples: bool = False
supports_drill_to_detail: bool = False
# Use integer as the primary key for cross-database auto-increment
# compatibility (sa.Identity() is not supported in MySQL or SQLite).
# The uuid column is a secondary unique identifier used in URLs and perms.
@@ -431,8 +425,6 @@ class SemanticView(AuditMixinNullable, Model):
"sql": None,
"select_star": None,
"editors": [],
"supports_samples": self.supports_samples,
"supports_drill_to_detail": self.supports_drill_to_detail,
"description": self.description,
"table_name": self.name,
"column_types": [

View File

@@ -161,6 +161,9 @@ def get_query(query_id: int) -> Query:
try:
return db.session.query(Query).filter_by(id=query_id).one()
except Exception as ex:
# roll back so a poisoned session (e.g. PendingRollbackError after a
# failed flush) doesn't fail every subsequent backoff retry identically
db.session.rollback()
raise SqlLabException("Failed at getting query") from ex

View File

@@ -346,11 +346,6 @@ class ExplorableData(TypedDict, total=False):
always_filter_main_dttm: bool
normalize_columns: bool
rls_filters: list[dict[str, Any]]
# Set by datasources that cannot return raw row samples (e.g. semantic
# views, which only expose pre-defined metrics and dimensions).
supports_samples: bool
# Set by datasources that cannot answer drill-to-detail requests.
supports_drill_to_detail: bool
VizData: TypeAlias = list[Any] | dict[Any, Any] | None

View File

@@ -223,7 +223,7 @@ msgstr "% du total"
#, python-format
msgid "%(alertType)s \"%(alertName)s\" triggered successfully"
msgstr "%(alertType)s « %(alertName)s » déclenché avec succès"
msgstr ""
#, python-format
msgid "%(dialect)s cannot be used as a data source for security reasons."
@@ -1390,10 +1390,10 @@ msgstr ""
" ou négative par rapport à la valeur de comparaison."
msgid "Adhoc metric SQL expression is invalid"
msgstr "L'expression SQL de la mesure ad hoc est invalide"
msgstr ""
msgid "Adhoc metric aggregate is invalid"
msgstr "L'agrégat de la mesure ad hoc est invalide"
msgstr ""
msgid "Adjust how this database will interact with SQL Lab."
msgstr "Ajuster la façon dont cette base de données interagira avec SQL Lab."
@@ -2018,8 +2018,6 @@ msgid ""
"Angle at which the first slice begins, in degrees. 90° starts at the top,"
" 0°/360° at the right, 270° at the bottom, and 180° at the left."
msgstr ""
"Angle auquel commence la première part, en degrés. 90° démarre en haut, "
"0°/360° à droite, 270° en bas et 180° à gauche."
msgid "Angle at which to end progress axis"
msgstr "Angle de fin de l'axe de progression"
@@ -2280,7 +2278,7 @@ msgid "Are you sure you want to delete the selected layers?"
msgstr "Voulez-vous vraiment supprimer les couches sélectionnées?"
msgid "Are you sure you want to delete the selected queries?"
msgstr "Voulez-vous vraiment supprimer les requêtes sélectionnées ?"
msgstr ""
msgid "Are you sure you want to delete the selected roles?"
msgstr "Voulez-vous vraiment supprimer les rôles sélectionnés ?"
@@ -3529,7 +3527,7 @@ msgid "Clear local theme"
msgstr "Supprimer le thème local"
msgid "Clear search"
msgstr "Effacer la recherche"
msgstr ""
msgid "Clear the selection to revert to the system default theme"
msgstr "Effacer la sélection pour revenir au thème par défaut du système"
@@ -4058,7 +4056,7 @@ msgid "Connection failed, please check your connection settings."
msgstr "La connexion a échoué, veuillez vérifier vos paramètres de connexion"
msgid "Connection looks good!"
msgstr "La connexion fonctionne !"
msgstr ""
msgid "Contains"
msgstr "Contient"
@@ -4146,7 +4144,7 @@ msgid "Copy query"
msgstr "Copier la requête"
msgid "Copy query URL"
msgstr "Copier l'URL de la requête"
msgstr ""
msgid "Copy query link to your clipboard"
msgstr "Copier le lien de la requête vers le presse-papier"
@@ -4276,8 +4274,6 @@ msgid ""
"Create a new tag and assign it to existing entities like charts or "
"dashboards"
msgstr ""
"Créer une balise et l'affecter à des entités existantes comme des "
"graphiques ou des tableaux de bord"
msgid "Create and explore dataset"
msgstr "Créer et explorer un jeu de données"
@@ -4306,7 +4302,7 @@ msgstr "Créé par"
msgid "Created by me"
msgstr "Créé par moi"
#, python-format
, python-format
msgid "Created by: %s"
msgstr "Créé par : %s"
@@ -4601,9 +4597,6 @@ msgid ""
"Dashboard cannot be restored because its slug is now used by another "
"active dashboard. Rename one of the dashboards and retry."
msgstr ""
"Le tableau de bord ne peut pas être restauré car son slug est désormais "
"utilisé par un autre tableau de bord actif. Renommez l'un des deux "
"tableaux de bord et réessayez."
msgid "Dashboard cannot be unfavorited."
msgstr "Le tableau de bord n'a pas pu être retiré des favoris."
@@ -5290,7 +5283,7 @@ msgid "Delete item"
msgstr "Supprimer l'élément"
msgid "Delete query"
msgstr "Supprimer la requête"
msgstr ""
msgid "Delete role"
msgstr "Supprimer le rôle"
@@ -5602,9 +5595,6 @@ msgid ""
"Display charts on a map. For using this plugin, users first have to "
"create any other chart that can then be placed on the map."
msgstr ""
"Affiche des graphiques sur une carte. Pour utiliser ce module, il faut "
"d'abord créer un autre graphique, qui pourra ensuite être placé sur la "
"carte."
msgid "Display column in the chart"
msgstr "Afficher la colonne dans le graphique"
@@ -6000,7 +5990,7 @@ msgstr "ERREUR"
#, python-format
msgid "ERROR: %s"
msgstr "ERREUR : %s"
msgstr ""
msgid "Edge length"
msgstr "Longueur du bord"
@@ -6092,7 +6082,7 @@ msgid "Edit properties"
msgstr "Modifier les propriétés"
msgid "Edit query"
msgstr "Modifier la requête"
msgstr ""
msgid "Edit report"
msgstr "Modifier le rapport"
@@ -6182,7 +6172,7 @@ msgid "Email link"
msgstr "Lien par courriel"
msgid "Email recipients"
msgstr "Destinataires du courriel"
msgstr ""
msgid "Email reports active"
msgstr "Rapports par courriel actifs"
@@ -6435,7 +6425,7 @@ msgid "Entity"
msgstr "Entité"
msgid "Entries per page"
msgstr "Entrées par page"
msgstr ""
msgid "Equal Date Sizes"
msgstr "Taille des dates égales"
@@ -6703,7 +6693,7 @@ msgid "Export as Example"
msgstr "Exporter comme exemple"
msgid "Export as PDF"
msgstr "Exporter en PDF"
msgstr ""
msgid "Export cancelled"
msgstr "Export annulé"
@@ -6722,7 +6712,7 @@ msgid "Export failed: %s"
msgstr "Export échoué : %s"
msgid "Export query"
msgstr "Exporter la requête"
msgstr ""
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: cs, de, ja,
# lv, ro, ru, sk, sr, sr_Latn, tr, uk]
@@ -6730,7 +6720,7 @@ msgid "Export screenshot (jpeg)"
msgstr "Exporter la capture d'écran (jpeg)"
msgid "Export screenshot (png)"
msgstr "Exporter la capture d'écran (png)"
msgstr ""
#, python-format
msgid "Export successful: %s"
@@ -6774,15 +6764,11 @@ msgid ""
"Exporting semantic views is not supported yet — %s semantic-view row(s) "
"were skipped."
msgstr ""
"L'export des vues sémantiques n'est pas encore pris en charge — %s "
"ligne(s) de vue sémantique ont été ignorées."
msgid ""
"Exporting semantic views is not supported yet. Deselect the semantic-view"
" rows and try again."
msgstr ""
"L'export des vues sémantiques n'est pas encore pris en charge. "
"Désélectionnez les lignes de vue sémantique et réessayez."
msgid "Expose database in SQL Lab"
msgstr "Exposer la base de données dans SQL Lab"
@@ -6895,8 +6881,6 @@ msgid ""
"Failed to export chart data. Please try again or contact your "
"administrator."
msgstr ""
"Échec de l'export des données du graphique. Réessayez ou contactez votre "
"administrateur."
msgid "Failed to fetch API keys"
msgstr "Tout Dé-Sélectionner"
@@ -6972,7 +6956,7 @@ msgstr "Échec du marquage des éléments"
#, python-format
msgid "Failed to trigger %(alertType)s \"%(alertName)s\": %(error)s"
msgstr "Échec du déclenchement de %(alertType)s « %(alertName)s » : %(error)s"
msgstr ""
msgid "Failed to update report"
msgstr "Échec de la mise à jour du rapport"
@@ -7331,7 +7315,7 @@ msgid "Forecast periods"
msgstr "Périodes de prévision"
msgid "Forecast requires at least 2 data points"
msgstr "La prévision nécessite au moins 2 points de données"
msgstr ""
msgid "Foreign key"
msgstr "Clé étrangère"
@@ -7387,10 +7371,10 @@ msgstr ""
" sont présentes, le formatage revient aux nombres neutres."
msgid "Formatted CSV attached in email"
msgstr "Fichier CSV mis en forme joint au courriel"
msgstr "CSV formatté attaché dans le courriel"
msgid "Formatted Excel attached in email"
msgstr "Fichier Excel mis en forme joint au courriel"
msgstr ""
msgid "Formatted date"
msgstr "Date formatée"
@@ -8334,7 +8318,7 @@ msgid "Label for your query"
msgstr "Label pour votre requête"
msgid "Label must not be empty."
msgstr "L'étiquette ne doit pas être vide."
msgstr ""
msgid "Label position"
msgstr "Position de l'étiquette"
@@ -8635,7 +8619,7 @@ msgid "Lines encoding"
msgstr "Codage des lignes"
msgid "Link Copied!"
msgstr "Lien copié !"
msgstr ""
msgid "List"
msgstr "Liste"
@@ -9572,7 +9556,7 @@ msgstr ""
"enregistrement temporel"
msgid "No data found"
msgstr "Aucune donnée trouvée"
msgstr ""
msgid "No data in file"
msgstr "Pas de données dans le fichier"
@@ -9988,7 +9972,7 @@ msgstr "Une ou plusieurs mesures n'existent pas"
#, python-format
msgid "One or more parameters are missing: %(missing)s"
msgstr "Un ou plusieurs paramètres sont manquants : %(missing)s"
msgstr ""
msgid "One or more parameters needed to configure a database are missing."
msgstr ""
@@ -10691,11 +10675,6 @@ msgid ""
"period (e.g. today so far) against complete prior periods (e.g. all of "
"yesterday)."
msgstr ""
"Tracer chaque série décalée dans le temps sur toute sa plage temporelle "
"au lieu de la tronquer à celle de la série principale. Utile pour "
"comparer une période en cours partielle (par exemple aujourd'hui jusqu'à "
"maintenant) à des périodes antérieures complètes (par exemple la journée "
"d'hier entière)."
msgid "Plot the distance (like flight paths) between origin and destination."
msgstr ""
@@ -11156,12 +11135,6 @@ msgid ""
"except the subjects defined in the filter, and can be used to define what"
" users can see if no RLS filters within a filter group apply to them."
msgstr ""
"Les filtres classiques ajoutent des clauses WHERE aux requêtes lorsqu'un "
"utilisateur correspond à un sujet référencé par le filtre. Les filtres de"
" base appliquent des filtres à toutes les requêtes sauf pour les sujets "
"définis dans le filtre ; ils permettent de définir ce que voient les "
"utilisateurs auxquels aucun filtre RLS d'un groupe de filtres ne "
"s'applique."
msgid "Relational"
msgstr "Relationnel"
@@ -11202,7 +11175,7 @@ msgid "Remove customization"
msgstr "Supprimer la personnalisation"
msgid "Remove dependency"
msgstr "Supprimer la dépendance"
msgstr ""
msgid "Remove filter"
msgstr "Supprimer le filtre"
@@ -11211,13 +11184,13 @@ msgid "Remove item"
msgstr "Supprimer lélément"
msgid "Remove notification method"
msgstr "Supprimer le mode de notification"
msgstr ""
msgid "Remove query from log"
msgstr "Supprimer la requête des journaux"
msgid "Remove sheet"
msgstr "Supprimer la feuille"
msgstr ""
#, python-format
msgid "Removed 1 column from the virtual dataset"
@@ -11261,7 +11234,7 @@ msgid "Report Schedule delete failed."
msgstr "La planification de rapport n'a pas être supprimée."
msgid "Report Schedule execute now failed."
msgstr "L'exécution immédiate de la planification de rapport a échoué."
msgstr ""
msgid "Report Schedule execution failed when generating a csv."
msgstr ""
@@ -11285,8 +11258,6 @@ msgstr ""
msgid "Report Schedule execution failed when generating an Excel file."
msgstr ""
"L'exécution de la planification de rapport a échoué lors de la génération"
" du fichier Excel."
msgid "Report Schedule execution got an unexpected error."
msgstr ""
@@ -11298,15 +11269,10 @@ msgid ""
"Please configure a Celery broker (Redis or RabbitMQ) and worker "
"processes."
msgstr ""
"L'exécution de la planification de rapport nécessite un backend Celery "
"configuré. Configurez un broker Celery (Redis ou RabbitMQ) et des "
"processus worker."
#, python-format
msgid "Report Schedule executor user %(username)s was not found."
msgstr ""
"L'utilisateur %(username)s exécutant la planification de rapport est "
"introuvable."
msgid "Report Schedule is still working, refusing to re-compute."
msgstr ""
@@ -12015,7 +11981,7 @@ msgid "Search Metrics & Columns"
msgstr "Rechercher les mesures et les colonnes"
msgid "Search a channel by name, or paste a channel ID"
msgstr "Rechercher un canal par son nom, ou coller un identifiant de canal"
msgstr ""
msgid "Search all charts"
msgstr "Rechercher tous les graphiques"
@@ -12060,7 +12026,7 @@ msgid "Search owners"
msgstr "Rechercher des propriétaires"
msgid "Search records"
msgstr "Rechercher des enregistrements"
msgstr ""
msgid "Search roles"
msgstr "Recherche de rôles"
@@ -12076,8 +12042,6 @@ msgstr "Rechercher…"
msgid "Searches all text fields: Name, Description, Database & Schema"
msgstr ""
"Recherche dans tous les champs texte : nom, description, base de données "
"et schéma"
msgid "Second"
msgstr "Seconde"
@@ -12120,7 +12084,7 @@ msgid "See all %(tableName)s"
msgstr "Voir tout %(tableName)s"
msgid "See all dashboards"
msgstr "Voir tous les tableaux de bord"
msgstr ""
msgid "See less"
msgstr "Voir moins"
@@ -12411,10 +12375,10 @@ msgid "Select operator"
msgstr "Sélectionner l'opérateur"
msgid "Select or type BCC recipients"
msgstr "Sélectionner ou saisir les destinataires en Cci"
msgstr ""
msgid "Select or type CC recipients"
msgstr "Sélectionner ou saisir les destinataires en Cc"
msgstr ""
msgid "Select or type a custom value..."
msgstr "Sélectionner ou renseigner une valeur personnalisé..."
@@ -12426,7 +12390,7 @@ msgid "Select or type dataset name"
msgstr "Sélectionner la base de données ou taper le nom du jeu de données"
msgid "Select or type email recipients"
msgstr "Sélectionner ou saisir les destinataires du courriel"
msgstr ""
msgid "Select page size"
msgstr "Sélectionner la taille de la page"
@@ -12675,7 +12639,7 @@ msgid "Send as CSV"
msgstr "Envoyer comme CSV"
msgid "Send as Excel"
msgstr "Envoyer comme Excel"
msgstr ""
msgid "Send as PDF"
msgstr "Envoyer comme PDF"
@@ -12907,7 +12871,7 @@ msgid "Show Metric Names"
msgstr "Afficher les noms de mesure"
msgid "Show Null Values"
msgstr "Afficher les valeurs nulles"
msgstr ""
msgid "Show Range Filter"
msgstr "Afficher l'intervalle de filtre"
@@ -12948,17 +12912,13 @@ msgstr ""
"autrement min/max dans les données."
msgid "Show a draggable slider to control the visible range of the Y-axis."
msgstr "Afficher un curseur déplaçable pour contrôler la plage visible de l'axe Y."
msgstr ""
msgid ""
"Show a summary row of total aggregations: the selected metrics in "
"aggregate mode, or the sum of numeric columns in raw records mode. Note "
"that row limit does not apply to the result."
msgstr ""
"Afficher une ligne de synthèse des agrégats totaux : les mesures "
"sélectionnées en mode agrégé, ou la somme des colonnes numériques en mode"
" enregistrements bruts. Notez que la limite de lignes ne s'applique pas "
"au résultat."
msgid "Show all columns"
msgstr "Afficher toutes les colonnes"
@@ -12999,7 +12959,7 @@ msgid "Show entries per page"
msgstr "Afficher le nombre d'éléments par page"
msgid "Show full range for time shift"
msgstr "Afficher la plage complète pour le décalage temporel"
msgstr ""
msgid ""
"Show hierarchical relationships of data, with the value represented by "
@@ -13086,8 +13046,6 @@ msgid ""
"Showcases a metric along with a comparison of value, change, and percent "
"change for a selected time period."
msgstr ""
"Met en avant une mesure avec une comparaison de la valeur, de l'écart et "
"de l'écart en pourcentage sur une période sélectionnée."
msgid ""
"Showcases a single metric front-and-center. Big number is best used to "
@@ -13231,7 +13189,7 @@ msgid "Solid"
msgstr "Solide"
msgid "Solid background"
msgstr "Fond uni"
msgstr ""
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr,
# sr_Latn]
@@ -13255,14 +13213,12 @@ msgstr ""
"seront pas effacés"
msgid "Some tables are not shown. Refine your search."
msgstr "Certaines tables ne sont pas affichées. Affinez votre recherche."
msgstr ""
msgid ""
"Something went wrong loading the dashboard. Check the dev console for "
"details."
msgstr ""
"Un problème est survenu lors du chargement du tableau de bord. Consultez "
"la console développeur pour plus de détails."
msgid "Something went wrong while saving the user info"
msgstr "Une erreur s'est produite. Réessayez plus tard."
@@ -13672,7 +13628,7 @@ msgid "Success"
msgstr "Réussite"
msgid "Success message"
msgstr "Message de succès"
msgstr ""
#, python-format
msgid "Successfully changed %s!"
@@ -13746,7 +13702,7 @@ msgid "Swap rows and columns"
msgstr "Échanger les rangées et les colonnes"
msgid "Sweep angle"
msgstr "Angle de balayage"
msgstr ""
msgid ""
"Swiss army knife for visualizing data. Choose between step, line, "
@@ -13914,7 +13870,7 @@ msgid "Tag created"
msgstr "Balise créée"
msgid "Tag description"
msgstr "Description de la balise"
msgstr ""
msgid "Tag name"
msgstr "Nom de la balise"
@@ -14044,13 +14000,11 @@ msgid "Text align"
msgstr "Alignement du texte"
msgid "Text embedded in email"
msgstr "Texte encapsulé dans le courriel"
msgstr "Text encapsulé dans le courriel"
#, python-format
msgid "The %(key)s in metadata_cache_timeout must be a non-negative integer."
msgstr ""
"La valeur de %(key)s dans metadata_cache_timeout doit être un entier "
"positif ou nul."
#, python-format
msgid "The %s"
@@ -14170,9 +14124,6 @@ msgid ""
"The chart data is too large to download. Please try reducing the date "
"range, limiting rows, or using fewer columns."
msgstr ""
"Les données du graphique sont trop volumineuses pour être téléchargées. "
"Réduisez la plage de dates, limitez le nombre de lignes ou utilisez moins"
" de colonnes."
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, sr,
# sr_Latn]
@@ -14190,7 +14141,7 @@ msgstr ""
" ou mettez à jour le rapport pour pointer vers un graphique actif."
msgid "The chat failed to load."
msgstr "La conversation n'a pas pu être chargée."
msgstr ""
msgid ""
"The classic. Great for showing how much of a company each investor gets, "
@@ -14288,8 +14239,6 @@ msgstr ""
msgid "The dashboard you are looking for may have been deleted or moved."
msgstr ""
"Le tableau de bord que vous recherchez a peut-être été supprimé ou "
"déplacé."
msgid "The data source seems to have been deleted"
msgstr "La source de données semble avoir été effacée"
@@ -14574,8 +14523,6 @@ msgid ""
"The metadata_cache_timeout must be a mapping from string keys to non-"
"negative integer values."
msgstr ""
"metadata_cache_timeout doit être une correspondance entre des clés de "
"type chaîne et des entiers positifs ou nuls."
#, python-format
msgid ""
@@ -14812,8 +14759,6 @@ msgstr "Cette requête contient un ou plusieurs paramètres de modèle malformé
msgid "The query context datasource does not match the chart datasource"
msgstr ""
"La source de données du contexte de requête ne correspond pas à celle du "
"graphique"
msgid "The query couldn't be loaded"
msgstr "La requête ne peut pas être chargée"
@@ -15255,7 +15200,7 @@ msgid "There was an error fetching the filtered charts and dashboards:"
msgstr "Une erreur sest produite lors de la récupération des graphiques et tableaux de bord filtrés :"
msgid "There was an error generating the permalink."
msgstr "Une erreur s'est produite lors de la génération du lien permanent."
msgstr ""
msgid "There was an error loading groups."
msgstr "Une erreur s'est produite lors du chargement des groupes."
@@ -15360,8 +15305,6 @@ msgstr ""
#, python-format
msgid "There was an issue deleting the selected queries: %s"
msgstr ""
"Un problème est survenu lors de la suppression des requêtes sélectionnées"
" : %s"
#, python-format
msgid "There was an issue deleting the selected templates: %s"
@@ -15408,7 +15351,7 @@ msgid "There was an issue exporting the selected dashboards"
msgstr "Il y a eu un problème lors de l'export des tableaux de bord sélectionnés"
msgid "There was an issue exporting the selected queries"
msgstr "Un problème est survenu lors de l'export des requêtes sélectionnées"
msgstr ""
msgid "There was an issue exporting the selected themes"
msgstr "Il y a eu un problème lors de l'export des thèmes sélectionnés"
@@ -15575,7 +15518,7 @@ msgstr ""
"être transmis au graphique contenant les données d'annotation."
msgid "This dashboard does not exist"
msgstr "Ce tableau de bord n'existe pas"
msgstr ""
msgid "This dashboard is managed externally, and can't be edited in Superset"
msgstr ""
@@ -15963,8 +15906,6 @@ msgstr "Fragment de temps"
msgid "Time Grain must be specified when using Time Comparison."
msgstr ""
"Le fragment de temps doit être précisé lors de l'utilisation de la "
"comparaison de temps."
msgid "Time Granularity"
msgstr "Fragmentation de Temps"
@@ -16254,10 +16195,6 @@ msgid ""
" angle is a multiple of 90°, the chart is automatically re-centered to "
"make use of the empty space."
msgstr ""
"Angle total couvert par le graphique, en degrés. 360° dessine un cercle "
"complet et 180° un demi-anneau. Lorsque le balayage est inférieur ou égal"
" à 180° et que l'angle de départ est un multiple de 90°, le graphique est"
" automatiquement recentré pour exploiter l'espace vide."
msgid "Total color"
msgstr "Couleur du total"
@@ -16282,7 +16219,7 @@ msgid "Transparent"
msgstr "Transparent"
msgid "Transparent background"
msgstr "Fond transparent"
msgstr ""
msgid "Transpose pivot"
msgstr "Pivot de transposition"
@@ -16312,7 +16249,7 @@ msgid "Trigger Alert If..."
msgstr "Déclencher une alerte si…"
msgid "Trigger now"
msgstr "Déclencher maintenant"
msgstr ""
msgid "True"
msgstr "Est vrai"
@@ -16431,7 +16368,7 @@ msgid "URL parameters"
msgstr "Paramètres URL"
msgid "UUID to track the execution status"
msgstr "UUID permettant de suivre l'état de l'exécution"
msgstr ""
msgid "Unable to calculate such a date delta"
msgstr "Impossible de calculer un delta de date comme celui-ci"
@@ -16498,7 +16435,7 @@ msgstr "Impossible de générer les données de téléchargement"
#, python-format
msgid "Unable to generate forecast: %(error)s"
msgstr "Impossible de générer la prévision : %(error)s"
msgstr ""
msgid ""
"Unable to identify temporal column for date range time comparison.Please "
@@ -16513,8 +16450,6 @@ msgid ""
"Unable to interpret the time offset: %(offset)s. Use a relative time such"
" as \"1 month ago\"."
msgstr ""
"Impossible d'interpréter le décalage temporel : %(offset)s. Utilisez une "
"expression relative telle que « 1 month ago »."
msgid ""
"Unable to load columns for the selected table. Please select a different "
@@ -17562,8 +17497,6 @@ msgstr "Affichage ou non des bulles au-dessus des pays"
msgid "Whether to display entries with null values in the hierarchy"
msgstr ""
"Afficher ou non les entrées dont les valeurs sont nulles dans la "
"hiérarchie"
msgid "Whether to display in the chart"
msgstr "Afficher ou non dans le graphique"
@@ -17699,9 +17632,6 @@ msgid ""
"Whether to sort tooltip by the selected metric in descending order. On "
"stacked charts, values are shown in ascending order."
msgstr ""
"Trier ou non l'infobulle par la mesure sélectionnée dans l'ordre "
"décroissant. Sur les graphiques empilés, les valeurs sont affichées dans "
"l'ordre croissant."
msgid "Whether to truncate metrics"
msgstr "Tronquer ou non les mesures"
@@ -17861,7 +17791,7 @@ msgid "Y-axis bounds"
msgstr "Limites de laxe des ordonnées"
msgid "Y-axis range slider"
msgstr "Curseur de plage de l'axe Y"
msgstr ""
msgid "Y-scale interval"
msgstr "Intervalle d'échelle Y"
@@ -18136,41 +18066,26 @@ msgid ""
"You must be a chart editor in order to delete. Please reach out to a "
"chart editor to request modifications or edit access."
msgstr ""
"Vous devez être éditeur du graphique pour pouvoir supprimer. Contactez un"
" éditeur du graphique pour demander des modifications ou un accès en "
"modification."
msgid ""
"You must be a chart editor in order to edit. Please reach out to a chart "
"editor to request modifications or edit access."
msgstr ""
"Vous devez être éditeur du graphique pour pouvoir modifier. Contactez un "
"éditeur du graphique pour demander des modifications ou un accès en "
"modification."
msgid ""
"You must be a dashboard editor in order to delete. Please reach out to a "
"dashboard editor to request modifications or edit access."
msgstr ""
"Vous devez être éditeur du tableau de bord pour pouvoir supprimer. "
"Contactez un éditeur du tableau de bord pour demander des modifications "
"ou un accès en modification."
msgid ""
"You must be a dashboard editor in order to edit. Please reach out to a "
"dashboard editor to request modifications or edit access."
msgstr ""
"Vous devez être éditeur du tableau de bord pour pouvoir modifier. "
"Contactez un éditeur du tableau de bord pour demander des modifications "
"ou un accès en modification."
msgid ""
"You must be a dataset editor in order to delete. Please reach out to a "
"dataset editor to request modifications or edit access."
msgstr ""
"Vous devez être éditeur de l'ensemble de données pour pouvoir supprimer. "
"Contactez un éditeur de l'ensemble de données pour demander des "
"modifications ou un accès en modification."
msgid ""
"You must be a dataset editor in order to edit. Please reach out to a "
@@ -18257,11 +18172,6 @@ msgid ""
"into multiple dashboards) or raise the "
"SUPERSET_DASHBOARD_POSITION_DATA_LIMIT config setting."
msgstr ""
"Votre tableau de bord est trop volumineux pour être enregistré : la "
"longueur sérialisée de la disposition est de %s alors que la limite est "
"de %s. Réduisez la taille du tableau de bord (par exemple en le scindant "
"en plusieurs tableaux de bord) ou augmentez le paramètre de configuration"
" SUPERSET_DASHBOARD_POSITION_DATA_LIMIT."
msgid "Your query could not be saved"
msgstr "Votre requête n'a pas pu être enregistrée"
@@ -19116,7 +19026,7 @@ msgid "quarter"
msgstr "trimestre"
msgid "queries"
msgstr "requêtes"
msgstr ""
msgid "query"
msgstr "requête"
@@ -19162,7 +19072,7 @@ msgid "seconds"
msgstr "secondes"
msgid "semantic layer"
msgstr "couche sémantique"
msgstr ""
msgid "series"
msgstr "série"

View File

@@ -202,23 +202,6 @@ class Datasource(BaseSupersetView):
payload = SamplesPayloadSchema().load(request.json)
except ValidationError as err:
return json_error_response(err.messages, status=400)
# Refuse early for datasource types that don't model raw rows
# (e.g. semantic views, which only expose pre-defined metrics and
# dimensions). Without this gate the request would still go through
# the standard query pipeline and fail with an opaque 500.
# ``supports_samples`` defaults to True for any datasource class that
# doesn't explicitly opt out, so SqlaTable/Query/SavedQuery continue
# to work without needing the attribute declared on each class.
ds_class = DatasourceDAO.sources.get(
DatasourceType(params["datasource_type"]),
)
if ds_class is not None and not getattr(ds_class, "supports_samples", True):
return json_error_response(
_("Samples are not available for this datasource type."),
status=400,
)
dashboard_id = None
if security_manager.is_guest_user():
if not params["dashboard_id"]:

View File

@@ -1,70 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from unittest.mock import MagicMock, patch
import pytest
from superset.common.query_actions import _get_drill_detail
from superset.exceptions import QueryObjectValidationError
def test_get_drill_detail_refuses_datasource_that_opts_out() -> None:
"""
A datasource with ``supports_drill_to_detail = False`` (e.g. semantic
views) must be hard-blocked on the server. Without this gate the request
would fall through to ``_get_full`` and fail with an opaque error, and
the flag would only be enforced by the frontend menu — leaving the
chart-data API endpoint accepting drill-detail requests it shouldn't.
"""
datasource = MagicMock()
datasource.supports_drill_to_detail = False
query_obj = MagicMock()
query_obj.datasource = datasource
query_context = MagicMock()
with pytest.raises(
QueryObjectValidationError,
match="Drill to detail is not available",
):
_get_drill_detail(query_context, query_obj)
def test_get_drill_detail_allows_datasource_without_flag() -> None:
"""
Datasources that don't declare the flag (e.g. legacy ``SqlaTable``
subclasses via ``getattr`` default) must continue to work — the gate
only fires when the flag is explicitly ``False``.
"""
datasource = MagicMock(spec=["columns"])
column = MagicMock()
column.column_name = "id"
datasource.columns = [column]
query_obj = MagicMock()
query_obj.datasource = datasource
query_obj.columns = []
query_context = MagicMock()
expected_payload: dict[str, list[dict[str, str]]] = {"data": []}
with patch(
"superset.common.query_actions._get_full", return_value=expected_payload
) as mock_get_full:
assert _get_drill_detail(query_context, query_obj) is expected_payload
mock_get_full.assert_called_once()

View File

@@ -653,15 +653,6 @@ def test_semantic_view_data(
assert data["table_name"] == "Orders View"
assert data["datasource_name"] == "Orders View"
assert data["offset"] == 0
# Semantic views don't model raw rows, so neither samples nor
# drill-to-detail are available.
assert data["supports_samples"] is False
assert data["supports_drill_to_detail"] is False
def test_semantic_view_supports_samples_is_false() -> None:
"""The class-level flag opts SemanticView out of the Samples affordance."""
assert SemanticView.supports_samples is False
@pytest.fixture
@@ -776,11 +767,6 @@ def test_semantic_view_data_populates_time_grain_sqla(
assert grain_durations == sorted(["PT1H", "P1D", "P1M"])
def test_semantic_view_supports_drill_to_detail_is_false() -> None:
"""The class-level flag opts SemanticView out of Drill to detail."""
assert SemanticView.supports_drill_to_detail is False
def test_semantic_view_get_query_result(
mock_implementation: MagicMock,
) -> None:

View File

@@ -35,6 +35,7 @@ from superset.sql.parse import SQLStatement, Table
from superset.sql_lab import (
execute_query,
execute_sql_statements,
get_query,
get_sql_results,
)
from superset.utils.rls import apply_rls, get_predicates_for_table
@@ -71,6 +72,34 @@ def test_execute_query(mocker: MockerFixture, app: None) -> None:
SupersetResultSet.assert_called_with([(42,)], cursor.description, db_engine_spec)
def test_get_query_rolls_back_session_before_retrying(
mocker: MockerFixture, app: SupersetApp
) -> None:
"""
A broken transaction (e.g. `PendingRollbackError` following a failed flush)
leaves the session unusable until `session.rollback()` is called, so without
it every `backoff` retry would reuse the same poisoned session and fail
identically. `get_query` must roll back on failure so each retry gets a
clean session and has a real chance to succeed.
"""
# avoid actually sleeping through the `backoff` decorator's retry interval
mocker.patch("backoff._sync.time.sleep")
expected_query = mocker.MagicMock()
mock_one = mocker.patch("superset.sql_lab.db.session.query")
mock_one.return_value.filter_by.return_value.one.side_effect = [
Exception("session is broken"),
expected_query,
]
mock_rollback = mocker.patch("superset.sql_lab.db.session.rollback")
result = get_query(query_id=1)
assert result is expected_query
assert mock_one.return_value.filter_by.return_value.one.call_count == 2
mock_rollback.assert_called_once()
@with_config(
{
"SQLLAB_PAYLOAD_MAX_MB": 50,

View File

@@ -310,66 +310,3 @@ def test_save_non_editor_with_editors_field_is_rejected(
raw_save(_view_self())
mock_security_manager.raise_for_editorship.assert_called_once_with(mock_orm)
# ---------------------------------------------------------------------------
# Datasource.samples
# ---------------------------------------------------------------------------
@patch("superset.views.datasource.views._", lambda s: s)
@patch("superset.views.datasource.views.get_samples")
@patch("superset.views.datasource.views.json_error_response")
@patch("superset.views.datasource.views.security_manager", new_callable=MagicMock)
def test_samples_returns_400_for_unsupported_datasource_type(
mock_security_manager: MagicMock,
mock_json_error_response: MagicMock,
mock_get_samples: MagicMock,
) -> None:
"""Semantic views can't return raw samples — endpoint should refuse with 400."""
from flask import Flask
mock_security_manager.is_guest_user.return_value = False
mock_json_error_response.return_value = "error-response"
raw_samples = _get_view_func("samples")
app = Flask(__name__)
with app.test_request_context(
"/datasource/samples?datasource_type=semantic_view&datasource_id=1",
method="POST",
json={},
):
result = raw_samples(_view_self())
assert result == "error-response"
mock_json_error_response.assert_called_once()
_, kwargs = mock_json_error_response.call_args
assert kwargs.get("status") == 400
# The bail-out must happen before any sample fetching is attempted.
mock_get_samples.assert_not_called()
@patch("superset.views.datasource.views.get_samples")
@patch("superset.views.datasource.views.security_manager", new_callable=MagicMock)
def test_samples_proceeds_for_supported_datasource_type(
mock_security_manager: MagicMock,
mock_get_samples: MagicMock,
) -> None:
"""A `query` datasource (supports_samples=True) bypasses the 400 short-circuit."""
from flask import Flask
mock_security_manager.is_guest_user.return_value = False
mock_get_samples.return_value = {"rows": []}
view = _view_self()
raw_samples = _get_view_func("samples")
app = Flask(__name__)
with app.test_request_context(
"/datasource/samples?datasource_type=query&datasource_id=1",
method="POST",
json={},
):
raw_samples(view)
mock_get_samples.assert_called_once()
view.json_response.assert_called_once_with({"result": {"rows": []}})