Compare commits

...

5 Commits

Author SHA1 Message Date
Maxime Beauchemin
59ddd52789 fix(sqllab): Set explicit Content-Type headers to prevent HTTP 406 errors
Fixes #36072 where SQL Lab queries with WHERE clauses failed with
"Database error: Not acceptable" in Superset v4.1+.

Root cause: Flask 2.3+ (upgraded in v4.1.0) has stricter content
negotiation that could return HTTP 406 when Content-Type headers
aren't explicitly set, particularly with ENABLE_PROXY_FIX or certain
Accept header configurations.

Changes:
- Add explicit Content-Type headers to /api/v1/sqllab/execute/ and
  /api/v1/sqllab/results/ endpoints
- Improve error handling with try-except blocks for result fetching
  and JSON serialization
- Add targeted integration test for WHERE clause queries

The fix ensures Flask 2.3+ doesn't attempt content negotiation that
could fail, while maintaining backward compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 10:47:44 -08:00
Antonio Rivero
60f29ba6fb chore(logs): Add is_cached in sync AND async results (#36102) 2025-11-13 16:04:49 +01:00
Mehmet Salih Yavuz
306f4c14cf fix(sql): quote column names with spaces to prevent SQLGlot parsing errors (#35553) 2025-11-13 17:47:16 +03:00
Juan Manuel Cárdenas
310dcd7b94 chore: 🌐 Translations added to ES .po file (#30759) 2025-11-13 08:53:12 +03:00
Joe Li
008c7c6517 fix(table-chart): fix missing table header IDs (#35968)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-12 13:15:55 -08:00
14 changed files with 597 additions and 158 deletions

View File

@@ -67,6 +67,7 @@ export function normalizeTimeColumn(
sqlExpression: formData.x_axis,
label: formData.x_axis,
expressionType: 'SQL',
isColumnReference: true,
};
}

View File

@@ -27,6 +27,7 @@ export interface AdhocColumn {
optionName?: string;
sqlExpression: string;
expressionType: 'SQL';
isColumnReference?: boolean;
columnType?: 'BASE_AXIS' | 'SERIES';
timeGrain?: string;
datasourceWarning?: boolean;
@@ -74,6 +75,10 @@ export function isAdhocColumn(column?: any): column is AdhocColumn {
);
}
export function isAdhocColumnReference(column?: any): column is AdhocColumn {
return isAdhocColumn(column) && column?.isColumnReference === true;
}
export function isQueryFormColumn(column: any): column is QueryFormColumn {
return isPhysicalColumn(column) || isAdhocColumn(column);
}

View File

@@ -86,6 +86,7 @@ test('should support different columns for x-axis and granularity', () => {
{
timeGrain: 'P1Y',
columnType: 'BASE_AXIS',
isColumnReference: true,
sqlExpression: 'time_column_in_x_axis',
label: 'time_column_in_x_axis',
expressionType: 'SQL',

View File

@@ -101,36 +101,35 @@ describe('queryObject conversion', () => {
it('should convert queryObject', () => {
const { queries } = buildQuery({ ...formData, x_axis: 'time_column' });
expect(queries[0]).toEqual(
expect.objectContaining({
granularity: 'time_column',
time_range: '1 year ago : 2013',
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
columns: [
{
columnType: 'BASE_AXIS',
expressionType: 'SQL',
label: 'time_column',
sqlExpression: 'time_column',
timeGrain: 'P1Y',
expect(queries[0]).toMatchObject({
granularity: 'time_column',
time_range: '1 year ago : 2013',
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
columns: [
{
columnType: 'BASE_AXIS',
expressionType: 'SQL',
label: 'time_column',
sqlExpression: 'time_column',
timeGrain: 'P1Y',
isColumnReference: true,
},
'col1',
],
series_columns: ['col1'],
metrics: ['count(*)'],
post_processing: [
{
operation: 'pivot',
options: {
aggregates: { 'count(*)': { operator: 'mean' } },
columns: ['col1'],
drop_missing_columns: true,
index: ['time_column'],
},
'col1',
],
series_columns: ['col1'],
metrics: ['count(*)'],
post_processing: [
{
operation: 'pivot',
options: {
aggregates: { 'count(*)': { operator: 'mean' } },
columns: ['col1'],
drop_missing_columns: true,
index: ['time_column'],
},
},
{ operation: 'flatten' },
],
}),
);
},
{ operation: 'flatten' },
],
});
});
});

View File

@@ -139,6 +139,31 @@ function cellWidth({
return perc2;
}
/**
* Sanitize a column identifier for use in HTML id attributes and CSS selectors.
* Replaces characters that are invalid in CSS selectors with safe alternatives.
*
* Note: The returned value should be prefixed with a string (e.g., "header-")
* to ensure it forms a valid HTML ID (IDs cannot start with a digit).
*
* Exported for testing.
*/
export function sanitizeHeaderId(columnId: string): string {
return (
columnId
// Semantic replacements first: preserve meaning in IDs for readability
// (e.g., '%pct_nice' → 'percentpct_nice' instead of '_pct_nice')
.replace(/%/g, 'percent')
.replace(/#/g, 'hash')
.replace(/△/g, 'delta')
// Generic sanitization for remaining special characters
.replace(/\s+/g, '_')
.replace(/[^a-zA-Z0-9_-]/g, '_')
.replace(/_+/g, '_') // Collapse consecutive underscores
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
);
}
/**
* Cell left margin (offset) calculation for horizontal bar chart elements
* when alignPositiveNegative is not set
@@ -844,6 +869,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
}
// Cache sanitized header ID to avoid recomputing it multiple times
const headerId = sanitizeHeaderId(column.originalLabel ?? column.key);
return {
id: String(i), // to allow duplicate column keys
// must use custom accessor to allow `.` in column names
@@ -969,7 +997,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
const cellProps = {
'aria-labelledby': `header-${column.key}`,
'aria-labelledby': `header-${headerId}`,
role: 'cell',
// show raw number in title in case of numeric values
title: typeof value === 'number' ? String(value) : undefined,
@@ -1056,7 +1084,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
},
Header: ({ column: col, onClick, style, onDragStart, onDrop }) => (
<th
id={`header-${column.originalLabel}`}
id={`header-${headerId}`}
title={t('Shift + Click to sort by multiple columns')}
className={[className, col.isSorted ? 'is-sorted' : ''].join(' ')}
style={{

View File

@@ -18,15 +18,93 @@
*/
import '@testing-library/jest-dom';
import { render, screen } from '@superset-ui/core/spec';
import TableChart from '../src/TableChart';
import { cloneDeep } from 'lodash';
import TableChart, { sanitizeHeaderId } from '../src/TableChart';
import transformProps from '../src/transformProps';
import DateWithFormatter from '../src/utils/DateWithFormatter';
import testData from './testData';
import { ProviderWrapper } from './testHelpers';
test('sanitizeHeaderId should sanitize percent sign', () => {
expect(sanitizeHeaderId('%pct_nice')).toBe('percentpct_nice');
});
test('sanitizeHeaderId should sanitize hash/pound sign', () => {
expect(sanitizeHeaderId('# metric_1')).toBe('hash_metric_1');
});
test('sanitizeHeaderId should sanitize delta symbol', () => {
expect(sanitizeHeaderId('△ delta')).toBe('delta_delta');
});
test('sanitizeHeaderId should replace spaces with underscores', () => {
expect(sanitizeHeaderId('Main metric_1')).toBe('Main_metric_1');
expect(sanitizeHeaderId('multiple spaces')).toBe('multiple_spaces');
});
test('sanitizeHeaderId should handle multiple special characters', () => {
expect(sanitizeHeaderId('% #△ test')).toBe('percent_hashdelta_test');
expect(sanitizeHeaderId('% # △ test')).toBe('percent_hash_delta_test');
});
test('sanitizeHeaderId should preserve alphanumeric, underscore, and hyphen', () => {
expect(sanitizeHeaderId('valid-name_123')).toBe('valid-name_123');
});
test('sanitizeHeaderId should replace other special characters with underscore', () => {
expect(sanitizeHeaderId('col@name!test')).toBe('col_name_test');
expect(sanitizeHeaderId('test.column')).toBe('test_column');
});
test('sanitizeHeaderId should handle edge cases', () => {
expect(sanitizeHeaderId('')).toBe('');
expect(sanitizeHeaderId('simple')).toBe('simple');
});
test('sanitizeHeaderId should collapse consecutive underscores', () => {
expect(sanitizeHeaderId('test @@ space')).toBe('test_space');
expect(sanitizeHeaderId('col___name')).toBe('col_name');
expect(sanitizeHeaderId('a b c')).toBe('a_b_c');
expect(sanitizeHeaderId('test@@name')).toBe('test_name');
});
test('sanitizeHeaderId should remove leading underscores', () => {
expect(sanitizeHeaderId('@col')).toBe('col');
expect(sanitizeHeaderId('!revenue')).toBe('revenue');
expect(sanitizeHeaderId('@@test')).toBe('test');
expect(sanitizeHeaderId(' leading_spaces')).toBe('leading_spaces');
});
test('sanitizeHeaderId should remove trailing underscores', () => {
expect(sanitizeHeaderId('col@')).toBe('col');
expect(sanitizeHeaderId('revenue!')).toBe('revenue');
expect(sanitizeHeaderId('test@@')).toBe('test');
expect(sanitizeHeaderId('trailing_spaces ')).toBe('trailing_spaces');
});
test('sanitizeHeaderId should remove leading and trailing underscores', () => {
expect(sanitizeHeaderId('@col@')).toBe('col');
expect(sanitizeHeaderId('!test!')).toBe('test');
expect(sanitizeHeaderId(' spaced ')).toBe('spaced');
expect(sanitizeHeaderId('@@multiple@@')).toBe('multiple');
});
test('sanitizeHeaderId should handle inputs with only special characters', () => {
expect(sanitizeHeaderId('@')).toBe('');
expect(sanitizeHeaderId('@@')).toBe('');
expect(sanitizeHeaderId(' ')).toBe('');
expect(sanitizeHeaderId('!@$')).toBe('');
expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # is replaced with 'hash' (semantic replacement)
// Semantic replacements produce readable output even when alone
expect(sanitizeHeaderId('%')).toBe('percent');
expect(sanitizeHeaderId('#')).toBe('hash');
expect(sanitizeHeaderId('△')).toBe('delta');
expect(sanitizeHeaderId('% # △')).toBe('percent_hash_delta');
});
describe('plugin-chart-table', () => {
describe('transformProps', () => {
it('should parse pageLength to pageSize', () => {
test('should parse pageLength to pageSize', () => {
expect(transformProps(testData.basic).pageSize).toBe(20);
expect(
transformProps({
@@ -42,13 +120,13 @@ describe('plugin-chart-table', () => {
).toBe(0);
});
it('should memoize data records', () => {
test('should memoize data records', () => {
expect(transformProps(testData.basic).data).toBe(
transformProps(testData.basic).data,
);
});
it('should memoize columns meta', () => {
test('should memoize columns meta', () => {
expect(transformProps(testData.basic).columns).toBe(
transformProps({
...testData.basic,
@@ -57,14 +135,14 @@ describe('plugin-chart-table', () => {
);
});
it('should format timestamp', () => {
test('should format timestamp', () => {
// eslint-disable-next-line no-underscore-dangle
const parsedDate = transformProps(testData.basic).data[0]
.__timestamp as DateWithFormatter;
expect(String(parsedDate)).toBe('2020-01-01 12:34:56');
expect(parsedDate.getTime()).toBe(1577882096000);
});
it('should process comparison columns when time_compare and comparison_type are set', () => {
test('should process comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
const comparisonColumns = transformedProps.columns.filter(
col =>
@@ -86,7 +164,7 @@ describe('plugin-chart-table', () => {
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
});
it('should not process comparison columns when time_compare is empty', () => {
test('should not process comparison columns when time_compare is empty', () => {
const propsWithoutTimeCompare = {
...testData.comparison,
rawFormData: {
@@ -109,7 +187,7 @@ describe('plugin-chart-table', () => {
expect(comparisonColumns.length).toBe(0);
});
it('should correctly apply column configuration for comparison columns', () => {
test('should correctly apply column configuration for comparison columns', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
@@ -147,7 +225,7 @@ describe('plugin-chart-table', () => {
expect(percentMetricConfig?.config).toEqual({ d3NumberFormat: '.3f' });
});
it('should correctly format comparison columns using getComparisonColFormatter', () => {
test('should correctly format comparison columns using getComparisonColFormatter', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
col =>
@@ -178,7 +256,7 @@ describe('plugin-chart-table', () => {
expect(formattedPercentMetric).toBe('0.123');
});
it('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
test('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
@@ -265,7 +343,7 @@ describe('plugin-chart-table', () => {
});
describe('TableChart', () => {
it('render basic data', () => {
test('render basic data', () => {
render(
<TableChart {...transformProps(testData.basic)} sticky={false} />,
);
@@ -284,12 +362,9 @@ describe('plugin-chart-table', () => {
expect(cells[8]).toHaveTextContent('N/A');
});
it('render advanced data', () => {
test('render advanced data', () => {
render(
<>
<TableChart {...transformProps(testData.advanced)} sticky={false} />
,
</>,
<TableChart {...transformProps(testData.advanced)} sticky={false} />,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
@@ -304,7 +379,7 @@ describe('plugin-chart-table', () => {
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
test('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
@@ -324,7 +399,7 @@ describe('plugin-chart-table', () => {
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render data with a bigint value in a raw record mode', () => {
test('render data with a bigint value in a raw record mode', () => {
render(
ProviderWrapper({
children: (
@@ -345,7 +420,7 @@ describe('plugin-chart-table', () => {
expect(cells[3]).toHaveTextContent('1234567890123456789');
});
it('render raw data', () => {
test('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
@@ -362,7 +437,7 @@ describe('plugin-chart-table', () => {
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
test('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
@@ -387,7 +462,7 @@ describe('plugin-chart-table', () => {
expect(cells[2]).toHaveTextContent('$ 0');
});
it('render small formatted data with currencies', () => {
test('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
@@ -429,14 +504,14 @@ describe('plugin-chart-table', () => {
expect(cells[2]).toHaveTextContent('$ 0.61');
});
it('render empty data', () => {
test('render empty data', () => {
render(
<TableChart {...transformProps(testData.empty)} sticky={false} />,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
it('render color with column color formatter', () => {
test('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
@@ -466,8 +541,8 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
test('render cell without color', () => {
const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]);
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
@@ -507,7 +582,7 @@ describe('plugin-chart-table', () => {
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display original label in grouped headers', () => {
test('should display original label in grouped headers', () => {
const props = transformProps(testData.comparison);
render(<TableChart {...props} sticky={false} />);
@@ -522,7 +597,142 @@ describe('plugin-chart-table', () => {
expect(hasMetricHeaders).toBe(true);
});
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
test('should set meaningful header IDs for time-comparison columns', () => {
// Test time-comparison columns have proper IDs
// Uses originalLabel (e.g., "metric_1") which is sanitized for CSS safety
const props = transformProps(testData.comparison);
const { container } = render(<TableChart {...props} sticky={false} />);
const headers = screen.getAllByRole('columnheader');
// All headers should have IDs
const headersWithIds = headers.filter(header => header.id);
expect(headersWithIds.length).toBeGreaterThan(0);
// None should have "header-undefined"
const undefinedHeaders = headersWithIds.filter(header =>
header.id.includes('undefined'),
);
expect(undefinedHeaders).toHaveLength(0);
// Should have IDs based on sanitized originalLabel (e.g., "metric_1")
const hasMetricHeaders = headersWithIds.some(
header =>
header.id.includes('metric_1') || header.id.includes('metric_2'),
);
expect(hasMetricHeaders).toBe(true);
// CRITICAL: Verify sanitization - no spaces or special chars in any header ID
headersWithIds.forEach(header => {
// IDs must not contain spaces (would break CSS selectors and ARIA)
expect(header.id).not.toMatch(/\s/);
// IDs must not contain special chars like %, #, △
expect(header.id).not.toMatch(/[%#△]/);
// IDs should only contain valid characters: alphanumeric, underscore, hyphen
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
});
// CRITICAL: Verify ALL cells reference valid headers (no broken ARIA)
const cellsWithLabels = container.querySelectorAll(
'td[aria-labelledby]',
);
cellsWithLabels.forEach(cell => {
const labelledBy = cell.getAttribute('aria-labelledby');
if (labelledBy) {
// Check that the ID doesn't contain spaces (would be interpreted as multiple IDs)
expect(labelledBy).not.toMatch(/\s/);
// Check that the ID doesn't contain special characters
expect(labelledBy).not.toMatch(/[%#△]/);
// Verify the referenced header actually exists
const referencedHeader = container.querySelector(
`#${CSS.escape(labelledBy)}`,
);
expect(referencedHeader).toBeTruthy();
}
});
});
test('should set meaningful header IDs for regular table columns', () => {
// Test regular (non-time-comparison) columns have proper IDs
// Uses fallback to column.key since originalLabel is undefined
const props = transformProps(testData.advanced);
const { container } = render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const headers = screen.getAllByRole('columnheader');
// Test 1: "name" column (regular string column)
const nameHeader = headers.find(header =>
header.textContent?.includes('name'),
);
expect(nameHeader).toBeDefined();
expect(nameHeader?.id).toBe('header-name'); // Falls back to column.key
// Verify cells reference this header correctly
const nameCells = container.querySelectorAll(
'td[aria-labelledby="header-name"]',
);
expect(nameCells.length).toBeGreaterThan(0);
// Test 2: "sum__num" column (metric with verbose map "Sum of Num")
const sumHeader = headers.find(header =>
header.textContent?.includes('Sum of Num'),
);
expect(sumHeader).toBeDefined();
expect(sumHeader?.id).toBe('header-sum_num'); // Falls back to column.key, consecutive underscores collapsed
// Verify cells reference this header correctly
const sumCells = container.querySelectorAll(
'td[aria-labelledby="header-sum_num"]',
);
expect(sumCells.length).toBeGreaterThan(0);
// Test 3: Verify NO headers have "undefined" in their ID
const undefinedHeaders = headers.filter(header =>
header.id?.includes('undefined'),
);
expect(undefinedHeaders).toHaveLength(0);
// Test 4: Verify ALL headers have proper IDs (no missing IDs)
const headersWithIds = headers.filter(header => header.id);
expect(headersWithIds.length).toBe(headers.length);
// Test 5: Verify ALL header IDs are properly sanitized
headersWithIds.forEach(header => {
// IDs must not contain spaces
expect(header.id).not.toMatch(/\s/);
// IDs must not contain special chars like % (from %pct_nice column)
expect(header.id).not.toMatch(/[%#△]/);
// IDs should only contain valid CSS selector characters
expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/);
});
// Test 6: Verify ALL cells reference valid headers (no broken ARIA)
const cellsWithLabels = container.querySelectorAll(
'td[aria-labelledby]',
);
cellsWithLabels.forEach(cell => {
const labelledBy = cell.getAttribute('aria-labelledby');
if (labelledBy) {
// Verify no spaces (would be interpreted as multiple IDs)
expect(labelledBy).not.toMatch(/\s/);
// Verify no special characters
expect(labelledBy).not.toMatch(/[%#△]/);
// Verify the referenced header actually exists
const referencedHeader = container.querySelector(
`#${CSS.escape(labelledBy)}`,
);
expect(referencedHeader).toBeTruthy();
}
});
});
test('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
@@ -572,7 +782,7 @@ describe('plugin-chart-table', () => {
cells = document.querySelectorAll('td');
});
it('render color with string column color formatter(operator begins with)', () => {
test('render color with string column color formatter(operator begins with)', () => {
render(
ProviderWrapper({
children: (
@@ -604,7 +814,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator ends with)', () => {
test('render color with string column color formatter (operator ends with)', () => {
render(
ProviderWrapper({
children: (
@@ -633,7 +843,7 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator containing)', () => {
test('render color with string column color formatter (operator containing)', () => {
render(
ProviderWrapper({
children: (
@@ -662,7 +872,7 @@ describe('plugin-chart-table', () => {
expect(getComputedStyle(screen.getByText('Joe')).background).toBe('');
});
it('render color with string column color formatter (operator not containing)', () => {
test('render color with string column color formatter (operator not containing)', () => {
render(
ProviderWrapper({
children: (
@@ -693,7 +903,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator =)', () => {
test('render color with string column color formatter (operator =)', () => {
render(
ProviderWrapper({
children: (
@@ -724,7 +934,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with string column color formatter (operator None)', () => {
test('render color with string column color formatter (operator None)', () => {
render(
ProviderWrapper({
children: (
@@ -757,7 +967,7 @@ describe('plugin-chart-table', () => {
);
});
it('render color with column color formatter to entire row', () => {
test('render color with column color formatter to entire row', () => {
render(
ProviderWrapper({
children: (
@@ -793,7 +1003,7 @@ describe('plugin-chart-table', () => {
);
});
it('display text color using column color formatter', () => {
test('display text color using column color formatter', () => {
render(
ProviderWrapper({
children: (
@@ -826,7 +1036,7 @@ describe('plugin-chart-table', () => {
);
});
it('display text color using column color formatter for entire row', () => {
test('display text color using column color formatter for entire row', () => {
render(
ProviderWrapper({
children: (

View File

@@ -171,7 +171,7 @@ class ChartDataRestApi(ChartRestApi):
and query_context.result_format == ChartDataResultFormat.JSON
and query_context.result_type == ChartDataResultType.FULL
):
return self._run_async(json_body, command)
return self._run_async(json_body, command, add_extra_log_payload)
try:
form_data = json.loads(chart.params)
@@ -265,7 +265,7 @@ class ChartDataRestApi(ChartRestApi):
and query_context.result_format == ChartDataResultFormat.JSON
and query_context.result_type == ChartDataResultType.FULL
):
return self._run_async(json_body, command)
return self._run_async(json_body, command, add_extra_log_payload)
form_data = json_body.get("form_data")
return self._get_data_response(
@@ -334,7 +334,10 @@ class ChartDataRestApi(ChartRestApi):
return self._get_data_response(command, True)
def _run_async(
self, form_data: dict[str, Any], command: ChartDataCommand
self,
form_data: dict[str, Any],
command: ChartDataCommand,
add_extra_log_payload: Callable[..., None] | None = None,
) -> Response:
"""
Execute command as an async query.
@@ -343,6 +346,10 @@ class ChartDataRestApi(ChartRestApi):
with contextlib.suppress(ChartDataCacheLoadError):
result = command.run(force_cached=True)
if result is not None:
# Log is_cached if extra payload callback is provided.
# This indicates no async job was triggered - data was already cached
# and a synchronous response is being returned immediately.
self._log_is_cached(result, add_extra_log_payload)
return self._send_chart_response(result)
# Otherwise, kick off a background job to run the chart query.
# Clients will either poll or be notified of query completion,
@@ -424,6 +431,25 @@ class ChartDataRestApi(ChartRestApi):
return self.response_400(message=f"Unsupported result_format: {result_format}")
def _log_is_cached(
self,
result: dict[str, Any],
add_extra_log_payload: Callable[..., None] | None,
) -> None:
"""
Log is_cached values from query results to event logger.
Extracts is_cached from each query in the result and logs it.
If there's a single query, logs the boolean value directly.
If multiple queries, logs as a list.
"""
if add_extra_log_payload and result and "queries" in result:
is_cached_values = [query.get("is_cached") for query in result["queries"]]
if len(is_cached_values) == 1:
add_extra_log_payload(is_cached=is_cached_values[0])
elif is_cached_values:
add_extra_log_payload(is_cached=is_cached_values)
@event_logger.log_this
def _get_data_response(
self,
@@ -442,12 +468,7 @@ class ChartDataRestApi(ChartRestApi):
return self.response_400(message=exc.message)
# Log is_cached if extra payload callback is provided
if add_extra_log_payload and result and "queries" in result:
is_cached_values = [query.get("is_cached") for query in result["queries"]]
if len(is_cached_values) == 1:
add_extra_log_payload(is_cached=is_cached_values[0])
elif is_cached_values:
add_extra_log_payload(is_cached=is_cached_values)
self._log_is_cached(result, add_extra_log_payload)
return self._send_chart_response(result, form_data, datasource)

View File

@@ -1502,8 +1502,14 @@ class SqlaTable(
"""
label = utils.get_column_name(col)
try:
sql_expression = col["sqlExpression"]
# For column references, conditionally quote identifiers that need it
if col.get("isColumnReference"):
sql_expression = self.database.quote_identifier(sql_expression)
expression = self._process_select_expression(
expression=col["sqlExpression"],
expression=sql_expression,
database_id=self.database_id,
engine=self.database.backend,
schema=self.schema,

View File

@@ -337,16 +337,32 @@ class SqlLabRestApi(BaseSupersetApi):
params = kwargs["rison"]
key = params.get("key")
rows = params.get("rows")
result = SqlExecutionResultsCommand(key=key, rows=rows).run()
try:
result = SqlExecutionResultsCommand(key=key, rows=rows).run()
except Exception as ex:
logger.exception("Error fetching query results for key=%s", key)
return self.response_500(message=str(ex))
# Using pessimistic json serialization since some database drivers can return
# unserializeable types at times
payload = json.dumps(
result,
default=json.pessimistic_json_iso_dttm_ser,
ignore_nan=True,
)
return json_success(payload, 200)
try:
payload = json.dumps(
result,
default=json.pessimistic_json_iso_dttm_ser,
ignore_nan=True,
)
except Exception as ex:
logger.exception("Error serializing query results for key=%s", key)
return self.response_500(message="Unable to serialize query results")
# Use json_success with explicit Content-Type to ensure Flask 2.3+ correctly
# handles the response and doesn't trigger HTTP 406 errors due to content
# negotiation issues with Accept headers or proxy configurations
response = json_success(payload, 200)
# Explicitly set Content-Type as a safeguard against content negotiation issues
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@expose("/execute/", methods=("POST",))
@protect()
@@ -410,8 +426,11 @@ class SqlLabRestApi(BaseSupersetApi):
if command_result["status"] == SqlJsonExecutionStatus.QUERY_IS_RUNNING
else 200
)
# return the execution result without special encoding
return json_success(command_result["payload"], response_status)
# Return the execution result without special encoding
# Set explicit Content-Type to prevent Flask 2.3+ content negotiation issues
response = json_success(command_result["payload"], response_status)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
except SqlLabException as ex:
payload = {"errors": [ex.to_dict()]}

View File

@@ -59,6 +59,7 @@ class AdhocColumn(TypedDict, total=False):
hasCustomLabel: Optional[bool]
label: str
sqlExpression: str
isColumnReference: Optional[bool]
columnType: Optional[Literal["BASE_AXIS", "SERIES"]]
timeGrain: Optional[str]

View File

@@ -20,8 +20,8 @@ msgstr ""
"POT-Creation-Date: 2025-04-29 12:34+0330\n"
"PO-Revision-Date: 2016-05-02 08:49-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Language: es\n"
"Language-Team: Español; Castellano <>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -91,6 +91,10 @@ msgstr " uno nuevo"
msgid " at line %(line)d"
msgstr " en la línea %(line)d"
#, python-format
msgid " at line %(line)d"
msgstr ""
msgid " expression which needs to adhere to the "
msgstr " expresión que debe adherirse al "
@@ -184,7 +188,14 @@ msgstr "la frecuencia de %(report_type)s programación excede el límite. Config
#, python-format
msgid "%(rows)d rows returned"
msgstr "%(rows)d filas devueltas"
msgstr "líneas obtenidas"
#, python-format
msgid ""
"%(subtitle)s\n"
"This may be triggered by:\n"
" %(issue)s"
msgstr ""
#, fuzzy, python-format
msgid "%(suggestion)s instead of \"%(undefinedParameter)s?\""
@@ -255,7 +266,7 @@ msgstr "%s elementos no se han podido etiquetar porque no tienes permisos de edi
msgid "%s operator(s)"
msgstr "%s operador(es)"
#, fuzzy, python-format
#, python-format
msgid "%s option"
msgid_plural "%s options"
msgstr[0] "%s opción"
@@ -269,7 +280,7 @@ msgstr "%s opción(es)"
msgid "%s recipients"
msgstr "%s destinatarios"
#, fuzzy, python-format
#, python-format
msgid "%s row"
msgid_plural "%s rows"
msgstr[0] "%s fila"
@@ -1065,14 +1076,14 @@ msgstr "La consulta de alerta ha devuelto más de una columna."
#, python-format
msgid "Alert query returned more than one column. %(num_cols)s columns returned"
msgstr "La consulta de alerta ha devuelto más de una columna. %(num_cols)s columnas devueltas"
msgstr "La consulta de alerta devolvió más de una columna. %(num_cols)s columnas devueltas"
msgid "Alert query returned more than one row."
msgstr "La consulta de alerta ha devuelto más de una fila."
#, python-format
msgid "Alert query returned more than one row. %(num_rows)s rows returned"
msgstr "La consulta de alerta ha devuelto más de una fila. %(num_rows)s filas devueltas"
msgstr "La consulta de alerta devolvió más de una fila. %(num_rows)s filas devueltas"
msgid "Alert running"
msgstr "Alerta en ejecución"
@@ -1242,7 +1253,7 @@ msgid "An error occurred while creating %ss: %s"
msgstr "Se ha producido un error al crear %ss: %s"
msgid "An error occurred while creating the copy link."
msgstr "Se ha producido un error al crear el enlace de copia."
msgstr "Se produjo un error en la creación %ss: %s"
msgid "An error occurred while creating the data source"
msgstr "Se ha producido un error al crear la fuente de datos"
@@ -1339,7 +1350,7 @@ msgstr "Se ha producido un error al recuperar los valores del usuario: %s"
#, python-format
msgid "An error occurred while importing %s: %s"
msgstr "Se ha producido un error al importar %s: %s"
msgstr "Se produjo un error importando %s: %s"
msgid "An error occurred while loading dashboard information."
msgstr "Se ha producido un error al cargar la información del panel de control."
@@ -1535,7 +1546,7 @@ msgstr "Filtros aplicados (%s)"
#, python-format
msgid "Applied filters: %s"
msgstr "Filtros aplicados: %s"
msgstr "Filtros aplicados %s"
msgid ""
"Applied rolling window did not return any data. Please make sure the "
@@ -1577,7 +1588,7 @@ msgstr "¿Seguro que quieres eliminar?"
#, python-format
msgid "Are you sure you want to delete %s?"
msgstr "¿Seguro que quieres eliminar %s?"
msgstr "¿Está seguro de que desea eliminar %s?"
#, python-format
msgid "Are you sure you want to delete the selected %s?"
@@ -2238,7 +2249,7 @@ msgstr "Opciones del gráfico"
msgid "Chart Orientation"
msgstr "Orientación del gráfico"
#, fuzzy, python-format
#, python-format
msgid "Chart Owner: %s"
msgid_plural "Chart Owners: %s"
msgstr[0] "Propietario del gráfico: %s"
@@ -2252,15 +2263,15 @@ msgstr "Título del gráfico"
#, python-format
msgid "Chart [%s] has been overwritten"
msgstr "El gráfico [%s] se ha sobrescrito"
msgstr "El gráfico [%s] ha sido sobreescrito"
#, python-format
msgid "Chart [%s] has been saved"
msgstr "El gráfico [%s] se ha guardado"
msgstr "El gráfico [%s] ha sido guardado"
#, python-format
msgid "Chart [%s] was added to dashboard [%s]"
msgstr "El gráfico [%s] se ha añadido al panel de control [%s]"
msgstr "El gráfico [%s] ha sido añadido al panel de control [%s]"
msgid "Chart [{}] has been overwritten"
msgstr "El gráfico [{}] se ha sobrescrito"
@@ -2796,7 +2807,7 @@ msgid "Configuration"
msgstr "Configuración"
msgid "Configure Advanced Time Range "
msgstr "Configurar el intervalo de tiempo avanzado "
msgstr "Configuración avanzada de rango de tiempo "
msgid "Configure Time Range: Current..."
msgstr "Configurar el intervalo de tiempo: actual..."
@@ -2805,7 +2816,7 @@ msgid "Configure Time Range: Last..."
msgstr "Configurar el intervalo de tiempo: último..."
msgid "Configure Time Range: Previous..."
msgstr "Configurar el intervalo de tiempo: anterior..."
msgstr "Configurar Rango de Tiempo: Anteriores..."
msgid "Configure custom time range"
msgstr "Configurar intervalo de tiempo personalizado"
@@ -3814,7 +3825,7 @@ msgid_plural "Deleted %(num)d report schedules"
msgstr[0] "Se ha eliminado%(num)d programación de informe"
msgstr[1] "Se han eliminado%(num)d programaciones de informe"
#, fuzzy, python-format
#, python-format
msgid "Deleted %(num)d rules"
msgid_plural "Deleted %(num)d rules"
msgstr[0] "Se han eliminado%(num)d reglas"
@@ -4112,13 +4123,11 @@ msgstr "El desglose en detalle está deshabilitado para esta base de datos. Camb
msgid "Drill to detail: %s"
msgstr "Desglosar en detalle: %s"
#, fuzzy
msgid "Drop a column here or click"
msgid_plural "Drop columns here or click"
msgstr[0] "Suelta una columna aquí o haz clic"
msgstr[1] "Suelta las columnas aquí o haz clic"
#, fuzzy
msgid "Drop a column/metric here or click"
msgid_plural "Drop columns/metrics here or click"
msgstr[0] "Suelta una columna/métrica aquí o haz clic"
@@ -4185,10 +4194,10 @@ msgid "Duration in ms (1.40008 => 1ms 400µs 80ns)"
msgstr "Duración en ms (1,40008 => 1 ms 400 µs 80 ns)"
msgid "Duration in ms (100.40008 => 100ms 400µs 80ns)"
msgstr "Duración en ms (100,40008 => 100 ms 400 µs 80 ns)"
msgstr "Duración en ms (100.40008 => 100 ms 400 µs 80 ns)"
msgid "Duration in ms (10500 => 0:10.5)"
msgstr "Duración en ms (10 500 => 0:10,5)"
msgstr "Duración en ms (10500 => 0:10.5)"
msgid "Duration in ms (66000 => 1m 6s)"
msgstr "Duración en ms (66 000 => 1 m 6 s)"
@@ -4235,6 +4244,9 @@ msgstr "Editar CSS"
msgid "Edit CSS template properties"
msgstr "Editar propiedades de la plantilla CSS"
msgid "Edit Chart"
msgstr "Editar Gráfico"
msgid "Edit Chart Properties"
msgstr "Editar propiedades del gráfico"
@@ -4369,7 +4381,7 @@ msgid "Embed dashboard"
msgstr "Incrustar panel de control"
msgid "Embedded dashboard could not be deleted."
msgstr "No se ha podido eliminar el panel de control incrustado."
msgstr "El panel de control no pudo ser eliminado."
msgid "Embedding deactivated."
msgstr "Incrustación desactivada."
@@ -4453,7 +4465,7 @@ msgid "End date"
msgstr "Fecha final"
msgid "End date excluded from time range"
msgstr "Fecha final excluida del intervalo de tiempo"
msgstr "Fecha final excluida del rango de tiempo"
msgid "End date must be after start date"
msgstr "La fecha final debe ser posterior a la fecha inicial"
@@ -4572,9 +4584,6 @@ msgstr "Error al leer el archivo de Excel"
msgid "Error saving dataset"
msgstr "Error al guardar el conjunto de datos"
msgid "Error unfaving chart"
msgstr "Error al quitar el gráfico de favoritos"
msgid "Error while adding role!"
msgstr "Error al añadir el rol"
@@ -4584,6 +4593,9 @@ msgstr "Error al añadir el usuario"
msgid "Error while duplicating role!"
msgstr "Error al duplicar el rol"
msgid "Error unfaving chart"
msgstr "Error al quitar el gráfico de favoritos"
msgid "Error while fetching charts"
msgstr "Error al recuperar los gráficos"
@@ -5218,10 +5230,10 @@ msgid "Geometry Column"
msgstr "Columna de geometría"
msgid "Get the last date by the date unit."
msgstr "Obtener la última fecha por la unidad de fecha."
msgstr "Obtiene la última fecha para la unidad de fecha especificada."
msgid "Get the specify date for the holiday"
msgstr "Obtener la fecha especificada para el día festivo"
msgstr "Obtiene la fecha del día feriado especificado"
msgid "Give access to multiple catalogs in a single database connection."
msgstr "Da acceso a múltiples catálogos en una sola conexión de base de datos."
@@ -5496,7 +5508,7 @@ msgstr "Incluye una descripción que se enviará con tu informe"
#, python-format
msgid "Include description to be sent with %s"
msgstr "Incluye una descripción para enviarse con %s"
msgstr "Incluye una descripción para ser enviada con %s"
msgid "Include series name as an axis"
msgstr "Incluir el nombre de la serie como eje"
@@ -5584,7 +5596,7 @@ msgstr "JSON no válido"
#, python-format
msgid "Invalid advanced data type: %(advanced_data_type)s"
msgstr "Tipo de datos avanzados no válido: %(advanced_data_type)s"
msgstr "Tipo de información avanzada inválida: %(advanced_data_type)s"
msgid "Invalid certificate"
msgstr "Certificado no válido"
@@ -5657,7 +5669,7 @@ msgstr "Referencia no válida a la columna: «%(column)s»"
#, python-format
msgid "Invalid result type: %(result_type)s"
msgstr "Tipo de resultado no válido: %(result_type)s"
msgstr "Tipo de resultado inválido: %(result_type)s"
#, python-format
msgid "Invalid rolling_type: %(type)s"
@@ -5665,7 +5677,7 @@ msgstr "Tipo móvil no válido: %(type)s "
#, python-format
msgid "Invalid spatial point encountered: %(latlong)s"
msgstr "Se ha encontrado un punto espacial no válido: %(latlong)s"
msgstr "Se encontró un punto espacial inválido: %(latlong)s"
msgid "Invalid state."
msgstr "Estado no válido."
@@ -6451,7 +6463,7 @@ msgid "Middle"
msgstr "Medio"
msgid "Midnight"
msgstr "Medianoche"
msgstr "Media noche"
msgid "Miles"
msgstr "Millas"
@@ -6562,7 +6574,7 @@ msgstr "Mes"
#, python-format
msgid "Months %s"
msgstr "Meses %s "
msgstr "Meses %s"
msgid "More"
msgstr "Más"
@@ -6577,7 +6589,7 @@ msgid "Move only"
msgstr "Solo mover"
msgid "Moves the given set of dates by a specified interval."
msgstr "Mueve el conjunto de fechas en cuestión por un intervalo especificado."
msgstr "Desplaza el conjunto de fechas dado en un intervalo especificado."
msgid "Multi-Dimensions"
msgstr "Multidimensional"
@@ -6797,7 +6809,7 @@ msgid "No entities have this tag currently assigned"
msgstr "Ninguna entidad tiene esta etiqueta asignada actualmente"
msgid "No filter"
msgstr "No hay ningún filtro"
msgstr "Sin filtro"
msgid "No filter is selected."
msgstr "No se ha seleccionado ningún filtro."
@@ -6821,7 +6833,7 @@ msgid "No records found"
msgstr "No se han encontrado registros"
msgid "No results"
msgstr "No hay resultados"
msgstr "Sin resultados"
msgid "No results found"
msgstr "No se han encontrado resultados"
@@ -6830,7 +6842,7 @@ msgid "No results match your filter criteria"
msgstr "No hay resultados que coincidan con tus criterios de filtro"
msgid "No results were returned for this query"
msgstr "No se han devuelto resultados para esta consulta"
msgstr "No se obtuvieron resultados para esta consulta"
msgid ""
"No results were returned for this query. If you expected results to be "
@@ -7073,7 +7085,7 @@ msgid "One or many metrics to display"
msgstr "Una o varias métricas a mostrar"
msgid "One or more annotation layers failed loading."
msgstr "No se han podido cargar una o más capas de anotación."
msgstr "Una o más capas de anotación fallaron al cargar."
msgid "One or more columns already exist"
msgstr "Una o más columnas ya existen"
@@ -7489,7 +7501,7 @@ msgid "Pie Chart"
msgstr "Gráfico tipo pastel"
msgid "Pie charts on a map"
msgstr "Gráficos tipo pastel en un mapa"
msgstr "Mapa con gráficos tipo pastel"
msgid "Pie shape"
msgstr "Forma de pastel"
@@ -7590,7 +7602,6 @@ msgstr "Vuelve a introducir tu contraseña."
msgid "Please re-export your file and try importing again"
msgstr "Vuelve a exportar tu archivo e intenta importarlo de nuevo"
#, fuzzy
msgid "Please reach out to the Chart Owner for assistance."
msgid_plural "Please reach out to the Chart Owners for assistance."
msgstr[0] "Ponte en contacto con el propietario del gráfico para obtener ayuda."
@@ -8007,7 +8018,7 @@ msgid "Relationships between community channels"
msgstr "Relaciones entre canales comunitarios"
msgid "Relative Date/Time"
msgstr "Fecha/hora relativa"
msgstr "Fecha/Hora Relativa"
msgid "Relative period"
msgstr "Periodo relativo"
@@ -8752,6 +8763,9 @@ msgstr "Selecciona el método de entrega"
msgid "Select Tags"
msgstr "Seleccionar etiquetas"
msgid "Select Viz Type"
msgstr "Selecciona un tipo de visualización"
msgid "Select chart type"
msgstr "Seleccionar tipo de visualización"
@@ -9141,12 +9155,21 @@ msgstr "Mostrar burbujas"
msgid "Show CREATE VIEW statement"
msgstr "Mostrar instrucción CREAR VISTA"
msgid "Show cell bars"
msgstr "Mostrar barras de celda"
msgid "Show Cell bars"
msgstr "Todos los gráficos"
msgid "Show Chart"
msgstr "Mostrar Gráfico"
msgid "Show Column"
msgstr "Mostrar Columna"
msgid "Show Dashboard"
msgstr "Mostrar el panel de control"
msgid "Show Database"
msgstr "Mostrar Base de Datos"
msgid "Show Labels"
msgstr "Mostrar etiquetas"
@@ -9156,6 +9179,9 @@ msgstr "Mostrar registro"
msgid "Show Markers"
msgstr "Mostrar marcadores"
msgid "Show Metric"
msgstr "Mostrar Métrica"
msgid "Show Metric Names"
msgstr "Mostrar nombres de las métricas"
@@ -9423,7 +9449,7 @@ msgstr "Lo sentimos, se ha producido un error. Inténtalo de nuevo más tarde."
#, python-format
msgid "Sorry, there was an error saving this %s: %s"
msgstr "Lo sentimos, se ha producido un error al guardar este %s: %s"
msgstr "Lo sentimos, se ha producido un error al guardar esto %s: %s"
#, python-format
msgid "Sorry, there was an error saving this dashboard: %s"
@@ -9512,7 +9538,7 @@ msgid "Spatial"
msgstr "Espacial"
msgid "Specific Date/Time"
msgstr "Fecha/hora específica"
msgstr "Fecha/Hora Específica"
msgid "Specify name to CREATE TABLE AS schema in: public"
msgstr "Especifica el nombre para el esquema CREAR TABLA COMO en: público"
@@ -9577,7 +9603,7 @@ msgid "Start date"
msgstr "Fecha de inicio"
msgid "Start date included in time range"
msgstr "Fecha de inicio incluida en el intervalo de tiempo"
msgstr "Fecha inicial incluida en el rango de tiempo"
msgid "Start y-axis at 0"
msgstr "Iniciar eje Y en 0"
@@ -9733,6 +9759,9 @@ msgstr "Documentación del SDK integrado de Superset."
msgid "Superset chart"
msgstr "Gráfico Superset"
msgid "Superset dashboard"
msgstr "Dashboard Superset"
msgid "Superset encountered an error while running a command."
msgstr "Superset ha encontrado un error al ejecutar un comando."
@@ -9850,7 +9879,7 @@ msgstr "No se ha definido el nombre de la tabla"
#, python-format
msgid "Table or View \"%(table)s\" does not exist."
msgstr "La tabla o la vista «%(table)s» no existen."
msgstr "La tabla o vista \"%(table)s\" no existe"
msgid ""
"Table that visualizes paired t-tests, which are used to understand "
@@ -10717,7 +10746,9 @@ msgid "There was an error loading the tables"
msgstr "Se ha producido un error al cargar las tablas"
msgid "There was an error retrieving dashboard tabs."
msgstr "Se ha producido un error al recuperar las pestañas del panel."
msgstr ""
"Lo sentimos, hubo un error al obtener la información de la base de datos:"
" %s"
#, python-format
msgid "There was an error saving the favorite status: %s"
@@ -10736,7 +10767,7 @@ msgstr "Ha habido un problema al eliminar %s: %s"
#, python-format
msgid "There was an issue deleting rules: %s"
msgstr "Ha habido un problema al eliminar las reglas: %s"
msgstr "Hubo un problema eliminando las reglas: %s"
#, python-format
msgid "There was an issue deleting the selected %s: %s"
@@ -10795,11 +10826,11 @@ msgstr "Ha habido un problema al recuperar tu gráfico: %s "
#, python-format
msgid "There was an issue fetching your dashboards: %s"
msgstr "Ha habido un problema al recuperar tus paneles de control: %s"
msgstr "Hubo un problema al obtener tus dashboards: %s"
#, python-format
msgid "There was an issue fetching your recent activity: %s"
msgstr "Ha habido un problema al recuperar tu actividad reciente: %s"
msgstr "Hubo un error al obtener tu actividad reciente: %s"
#, python-format
msgid "There was an issue fetching your saved queries: %s"
@@ -10839,7 +10870,7 @@ msgid "This action will permanently delete the template."
msgstr "Esta acción eliminará permanentemente la plantilla."
msgid "This action will permanently delete the user."
msgstr "Esta acción eliminará permanentemente el uduario."
msgstr "Esta acción eliminará permanentemente el usuario."
msgid ""
"This can be either an IP address (e.g. 127.0.0.1) or a domain name (e.g. "
@@ -11048,7 +11079,6 @@ msgstr "Este tipo de visualización no admite el filtro cruzado."
msgid "This visualization type is not supported."
msgstr "Este tipo de visualización no se admite."
#, fuzzy
msgid "This was triggered by:"
msgid_plural "This may be triggered by:"
msgstr[0] "La causa de esto ha sido:"
@@ -11327,7 +11357,33 @@ msgid "Tree layout"
msgstr "Diseño del árbol"
msgid "Tree orientation"
msgstr "Orientación del árbol"
Findings (brief):
- No git merge conflict markers found (no <<<<<<< / ======= / >>>>>>>).
- PO header mismatch: "Language: en" — this is an Spanish file; set to "es".
- Duplicate msgid entries with conflicting/empty translations:
- " at line %(line)d" — one entry has " en la línea %(line)d", another has an empty msgstr.
- "Dashboard cannot be copied due to invalid parameters." — appears multiple times with different/empty msgstr values.
- "%(subtitle)s\nThis may be triggered by:\n %(issue)s" — msgstr is empty in one occurrence.
- There are other repeated msgids with one occurrence left untranslated (examples: search for repeated msgid strings with one msgstr == "").
- Empty translations (examples):
- msgid "%(subtitle)s\nThis may be triggered by:\n %(issue)s" → msgstr "".
- Several other msgid entries have msgstr "" (scan for msgstr "" occurrences).
- Fuzzy entries present (e.g. entries annotated "#, fuzzy") — these need review and removal of the fuzzy flag after correction.
- Typo in a translation: msgid "This action will permanently delete the user." → msgstr contains "uduario." (should be "usuario.").
Recommended next steps:
- Fix header Language to "es".
- Remove/fix duplicate msgids: consolidate into a single entry and keep the correct translation.
- Fill in missing msgstr values (or mark as untranslated intentionally).
- Review and resolve fuzzy entries, then remove the "fuzzy" flag.
- Fix obvious typos (e.g., "uduario" → "usuario").
If you want, I can produce a patch that:
- updates header Language to "es",
- removes duplicate entries by keeping the first translated occurrence,
- lists all msgids with empty msgstr for you to translate,
or show exact locations (line ranges) for each problem. Which would you prefer?
msgid "Treemap"
msgstr "Diagrama de árbol"
@@ -11983,7 +12039,7 @@ msgstr "WMS"
#, python-format
msgid "Waiting on %s"
msgstr "Esperando a %s"
msgstr "Esperando por %s"
msgid "Waiting on database..."
msgstr "Esperando a la base de datos..."
@@ -12083,7 +12139,7 @@ msgstr "Semanas %s"
msgid "Weight"
msgstr "Peso"
#, fuzzy, python-format
#, python-format
msgid ""
"Were having trouble loading these results. Queries are set to timeout "
"after %s second."
@@ -12093,7 +12149,7 @@ msgid_plural ""
msgstr[0] "Estamos teniendo problemas para cargar estos resultados. Se considera que una consulta ha superado el tiempo de espera después de %s segundo."
msgstr[1] "Estamos teniendo problemas para cargar estos resultados. Se considera que una consulta ha superado el tiempo de espera después de %s segundos."
#, fuzzy, python-format
#, python-format
msgid ""
"Were having trouble loading this visualization. Queries are set to "
"timeout after %s second."
@@ -12576,7 +12632,7 @@ msgstr "No puedes utilizar el diseño de marca de 45° con el filtro de interval
#, python-format
msgid "You do not have permission to edit this %s"
msgstr "No tienes permisos para editar este %s"
msgstr "No tienes permisos para editar esto %s"
msgid "You do not have permission to edit this chart"
msgstr "No tienes permisos para editar este gráfico"
@@ -12841,7 +12897,7 @@ msgid "background"
msgstr "fondo"
msgid "Basic conditional formatting"
msgstr "formato condicional básico"
msgstr "Formato condicional básico"
msgid "basis"
msgstr "base"
@@ -13164,13 +13220,18 @@ msgid ""
"is linked to %s charts that appear on %s dashboards and users have %s SQL"
" Lab tabs using this database open. Are you sure you want to continue? "
"Deleting the database will break those objects."
msgstr "está vinculado a %s gráficos que aparecen en %s paneles de control y los usuarios tienen %s pestañas de SQL Lab usando esta base de datos abierta. ¿Seguro que quieres continuar? Eliminar la base de datos descompondrá esos objetos."
msgstr ""
"La base de datos %s está vinculada a %s gráficos que aparecen en %s "
"dashboards. ¿Estás seguro de que quieres continuar? Eliminar la base de "
"datos dejará inutilizables esos objetos."
#, python-format
msgid ""
"is linked to %s charts that appear on %s dashboards. Are you sure you "
"want to continue? Deleting the dataset will break those objects."
msgstr "está vinculado a %s gráficos que aparecen en %s paneles de control. ¿Seguro que quieres continuar? Eliminar el conjunto de datos descompondrá esos objetos."
msgstr ""
"esta linkeado a %s gráficos que aparecen en %s tableros. ¿Está seguro"
"de que desea continuar? Eliminar el conjunto de datos romperá esos objetos."
msgid "is not"
msgstr "no es"
@@ -13310,16 +13371,19 @@ msgid "pixels"
msgstr "píxeles"
msgid "previous calendar month"
msgstr "mes natural anterior"
msgstr "mes anterior"
msgid "previous calendar quarter"
msgstr "trimestre natural anterior"
msgstr "trimestre anterior"
msgid "previous calendar week"
msgstr "semana natural anterior"
msgstr "semana anterior"
msgid "previous calendar year"
msgstr "año natural anterior"
msgstr "año anterior"
msgid "published"
msgstr "No publicado"
msgid "quarter"
msgstr "trimestre"
@@ -13339,6 +13403,9 @@ msgstr "reiniciar"
msgid "recent"
msgstr "reciente"
msgid "recents"
msgstr "Recientes"
msgid "recipients"
msgstr "destinatarios"
@@ -13360,6 +13427,9 @@ msgstr "rowlevelsecurity"
msgid "running"
msgstr "en ejecución"
msgid "saved queries"
msgstr "Consultas Guardadas"
msgid "save"
msgstr "guardar"

View File

@@ -753,10 +753,11 @@ class TestPostChartDataApi(BaseTestChartDataApi):
@with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_chart_data_async_cached_sync_response(self):
@mock.patch("superset.extensions.event_logger.log")
def test_chart_data_async_cached_sync_response(self, mock_event_logger):
"""
Chart data API: Test chart data query returns results synchronously
when results are already cached.
when results are already cached, and that is_cached is logged.
"""
app._got_first_request = False
async_query_manager_factory.init_app(app)
@@ -767,7 +768,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
cmd_run_val = {
"query_context": QueryContext(),
"queries": [{"query": "select * from foo"}],
"queries": [{"query": "select * from foo", "is_cached": True}],
}
with mock.patch.object(
@@ -780,7 +781,16 @@ class TestPostChartDataApi(BaseTestChartDataApi):
assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
patched_run.assert_called_once_with(force_cached=True)
assert data == {"result": [{"query": "select * from foo"}]}
assert data == {
"result": [{"query": "select * from foo", "is_cached": True}]
}
# Verify that is_cached was logged to event logger
call_kwargs = mock_event_logger.call_args[1]
records = call_kwargs.get("records", [])
assert len(records) > 0
# is_cached should be True when retrieved from cache in async path
assert records[0]["is_cached"] is True
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch("superset.extensions.event_logger.log")

View File

@@ -126,6 +126,41 @@ class TestSqlLab(SupersetTestCase):
"engine_name": engine_name,
}
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_sql_json_where_clause_content_type(self):
"""
Test that queries with WHERE clauses return proper Content-Type headers.
This test addresses issue #36072 where Flask 2.3+ content negotiation
could cause HTTP 406 errors for queries with WHERE clauses, particularly
when using ENABLE_PROXY_FIX or certain Accept header configurations.
"""
self.login(ADMIN_USERNAME)
# Test query with WHERE clause
resp = self.client.post(
"/api/v1/sqllab/execute/",
json={
"database_id": self.get_database_by_name("examples").id,
"sql": "SELECT * FROM birth_names WHERE name = 'John' LIMIT 5",
"client_id": "test_where_1",
},
)
# Verify response is successful
assert resp.status_code in (200, 202), f"Expected 200/202, got {resp.status_code}"
# Verify Content-Type header is explicitly set to prevent 406 errors
assert "application/json" in resp.headers.get("Content-Type", "")
# Verify response body is valid JSON
data = resp.json
assert isinstance(data, dict)
# If query ran synchronously (200), verify it has data
if resp.status_code == 200:
assert "data" in data or "query_id" in data
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_sql_json_dml_disallowed(self):
self.login(ADMIN_USERNAME)

View File

@@ -29,6 +29,8 @@ from sqlalchemy import create_engine
from sqlalchemy.orm.session import Session
from sqlalchemy.pool import StaticPool
from superset.superset_typing import AdhocColumn
if TYPE_CHECKING:
from superset.models.core import Database
@@ -1125,3 +1127,34 @@ def test_process_select_expression_end_to_end(database: Database) -> None:
assert expected.replace(" ", "").lower() in result.replace(" ", "").lower(), (
f"Expected '{expected}' to be in result '{result}' for input '{expression}'"
)
def test_adhoc_column_to_sqla_with_column_reference(database: Database) -> None:
"""
Test that adhoc_column_to_sqla
properly quotes column identifiers when isColumnReference is true.
This tests the fix for column names with spaces being properly quoted
before being processed by SQLGlot to prevent "column AS alias" misinterpretation.
"""
from superset.connectors.sqla.models import SqlaTable
table = SqlaTable(
table_name="test_table",
database=database,
)
# Test 1: Column reference with spaces should be quoted
col_with_spaces: AdhocColumn = {
"sqlExpression": "Customer Name",
"label": "Customer Name",
"isColumnReference": True,
}
result = table.adhoc_column_to_sqla(col_with_spaces)
# Should contain the quoted column name
assert result is not None
result_str = str(result)
assert '"Customer Name"' in result_str