Skip to content

Commit 306f4c1

Browse files
authored
fix(sql): quote column names with spaces to prevent SQLGlot parsing errors (apache#35553)
1 parent 310dcd7 commit 306f4c1

File tree

7 files changed

+77
-31
lines changed

7 files changed

+77
-31
lines changed

superset-frontend/packages/superset-ui-core/src/query/normalizeTimeColumn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function normalizeTimeColumn(
6767
sqlExpression: formData.x_axis,
6868
label: formData.x_axis,
6969
expressionType: 'SQL',
70+
isColumnReference: true,
7071
};
7172
}
7273

superset-frontend/packages/superset-ui-core/src/query/types/Column.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface AdhocColumn {
2727
optionName?: string;
2828
sqlExpression: string;
2929
expressionType: 'SQL';
30+
isColumnReference?: boolean;
3031
columnType?: 'BASE_AXIS' | 'SERIES';
3132
timeGrain?: string;
3233
datasourceWarning?: boolean;
@@ -74,6 +75,10 @@ export function isAdhocColumn(column?: any): column is AdhocColumn {
7475
);
7576
}
7677

78+
export function isAdhocColumnReference(column?: any): column is AdhocColumn {
79+
return isAdhocColumn(column) && column?.isColumnReference === true;
80+
}
81+
7782
export function isQueryFormColumn(column: any): column is QueryFormColumn {
7883
return isPhysicalColumn(column) || isAdhocColumn(column);
7984
}

superset-frontend/packages/superset-ui-core/test/query/normalizeTimeColumn.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ test('should support different columns for x-axis and granularity', () => {
8686
{
8787
timeGrain: 'P1Y',
8888
columnType: 'BASE_AXIS',
89+
isColumnReference: true,
8990
sqlExpression: 'time_column_in_x_axis',
9091
label: 'time_column_in_x_axis',
9192
expressionType: 'SQL',

superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -101,36 +101,35 @@ describe('queryObject conversion', () => {
101101

102102
it('should convert queryObject', () => {
103103
const { queries } = buildQuery({ ...formData, x_axis: 'time_column' });
104-
expect(queries[0]).toEqual(
105-
expect.objectContaining({
106-
granularity: 'time_column',
107-
time_range: '1 year ago : 2013',
108-
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
109-
columns: [
110-
{
111-
columnType: 'BASE_AXIS',
112-
expressionType: 'SQL',
113-
label: 'time_column',
114-
sqlExpression: 'time_column',
115-
timeGrain: 'P1Y',
116-
},
117-
'col1',
118-
],
119-
series_columns: ['col1'],
120-
metrics: ['count(*)'],
121-
post_processing: [
122-
{
123-
operation: 'pivot',
124-
options: {
125-
aggregates: { 'count(*)': { operator: 'mean' } },
126-
columns: ['col1'],
127-
drop_missing_columns: true,
128-
index: ['time_column'],
129-
},
104+
expect(queries[0]).toMatchObject({
105+
granularity: 'time_column',
106+
time_range: '1 year ago : 2013',
107+
extras: { having: '', where: '', time_grain_sqla: 'P1Y' },
108+
columns: [
109+
{
110+
columnType: 'BASE_AXIS',
111+
expressionType: 'SQL',
112+
label: 'time_column',
113+
sqlExpression: 'time_column',
114+
timeGrain: 'P1Y',
115+
isColumnReference: true,
116+
},
117+
'col1',
118+
],
119+
series_columns: ['col1'],
120+
metrics: ['count(*)'],
121+
post_processing: [
122+
{
123+
operation: 'pivot',
124+
options: {
125+
aggregates: { 'count(*)': { operator: 'mean' } },
126+
columns: ['col1'],
127+
drop_missing_columns: true,
128+
index: ['time_column'],
130129
},
131-
{ operation: 'flatten' },
132-
],
133-
}),
134-
);
130+
},
131+
{ operation: 'flatten' },
132+
],
133+
});
135134
});
136135
});

superset/connectors/sqla/models.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1502,8 +1502,14 @@ def adhoc_column_to_sqla( # pylint: disable=too-many-locals
15021502
"""
15031503
label = utils.get_column_name(col)
15041504
try:
1505+
sql_expression = col["sqlExpression"]
1506+
1507+
# For column references, conditionally quote identifiers that need it
1508+
if col.get("isColumnReference"):
1509+
sql_expression = self.database.quote_identifier(sql_expression)
1510+
15051511
expression = self._process_select_expression(
1506-
expression=col["sqlExpression"],
1512+
expression=sql_expression,
15071513
database_id=self.database_id,
15081514
engine=self.database.backend,
15091515
schema=self.schema,

superset/superset_typing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class AdhocColumn(TypedDict, total=False):
5959
hasCustomLabel: Optional[bool]
6060
label: str
6161
sqlExpression: str
62+
isColumnReference: Optional[bool]
6263
columnType: Optional[Literal["BASE_AXIS", "SERIES"]]
6364
timeGrain: Optional[str]
6465

tests/unit_tests/models/helpers_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
from sqlalchemy.orm.session import Session
3030
from sqlalchemy.pool import StaticPool
3131

32+
from superset.superset_typing import AdhocColumn
33+
3234
if TYPE_CHECKING:
3335
from superset.models.core import Database
3436

@@ -1125,3 +1127,34 @@ def test_process_select_expression_end_to_end(database: Database) -> None:
11251127
assert expected.replace(" ", "").lower() in result.replace(" ", "").lower(), (
11261128
f"Expected '{expected}' to be in result '{result}' for input '{expression}'"
11271129
)
1130+
1131+
1132+
def test_adhoc_column_to_sqla_with_column_reference(database: Database) -> None:
1133+
"""
1134+
Test that adhoc_column_to_sqla
1135+
properly quotes column identifiers when isColumnReference is true.
1136+
1137+
This tests the fix for column names with spaces being properly quoted
1138+
before being processed by SQLGlot to prevent "column AS alias" misinterpretation.
1139+
"""
1140+
from superset.connectors.sqla.models import SqlaTable
1141+
1142+
table = SqlaTable(
1143+
table_name="test_table",
1144+
database=database,
1145+
)
1146+
1147+
# Test 1: Column reference with spaces should be quoted
1148+
col_with_spaces: AdhocColumn = {
1149+
"sqlExpression": "Customer Name",
1150+
"label": "Customer Name",
1151+
"isColumnReference": True,
1152+
}
1153+
1154+
result = table.adhoc_column_to_sqla(col_with_spaces)
1155+
1156+
# Should contain the quoted column name
1157+
assert result is not None
1158+
result_str = str(result)
1159+
1160+
assert '"Customer Name"' in result_str

0 commit comments

Comments
 (0)