Compare commits

...

4 Commits

Author SHA1 Message Date
dependabot[bot]
2f9b0c8305 chore(deps): bump actions/setup-java from 5.2.0 to 5.3.0
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](be666c2fcd...ad2b38190b)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 07:03:55 +00:00
alex
5916ec4876 fix(plugin-chart-echarts): cross-filter horizontal bars on category not metric (#41104)
Signed-off-by: alex-poor <alex@karo.co.nz>
2026-06-22 20:29:29 -07:00
Harshit-Tiwary
36781fbf47 fix(i18n): wrap table access error message with gettext for translation (#38489)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:28:05 -07:00
Shaitan
215b207ae4 fix(sql): detect set operations and nested selects in subquery check (#38452)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:27:32 -07:00
9 changed files with 145 additions and 23 deletions

View File

@@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -71,7 +71,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "zulu"
java-version: "21"

View File

@@ -396,3 +396,102 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
expect(setDataMaskMock).not.toHaveBeenCalled();
}
});
// Test for issue #41102: horizontal bar cross-filter must use the category
// value, not the metric. For horizontal bars the data tuple is value-first
// (e.g. [100, 'Product A']), so relying on data[0] emitted the metric value.
test('emits cross-filter on the category value for a horizontal categorical bar', async () => {
const setDataMaskMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
setDataMask: setDataMaskMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const clickHandler = props.eventHandlers?.click;
if (clickHandler) {
clickHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
dataIndex: 0,
});
await waitFor(
() => {
expect(setDataMaskMock).toHaveBeenCalled();
},
{ timeout: 500 },
);
// Must filter on the category ('Product A'), not the metric value (100)
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
expect(dataMaskCall.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});
// Test for issue #41102: the context-menu ("Add cross-filter") path must also
// use the category value, not the metric, for a horizontal categorical bar.
test('context menu cross-filter uses the category value for a horizontal categorical bar', async () => {
const onContextMenuMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
onContextMenu: onContextMenuMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const contextMenuHandler = props.eventHandlers?.contextmenu;
expect(contextMenuHandler).toBeDefined();
if (contextMenuHandler) {
await contextMenuHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
event: { stop: jest.fn(), event: { clientX: 10, clientY: 20 } },
});
await waitFor(() => {
expect(onContextMenuMock).toHaveBeenCalled();
});
// The cross-filter must use the category ('Product A'), not the metric (100)
const { crossFilter } = onContextMenuMock.mock.calls[0][2];
expect(crossFilter.dataMask.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});

View File

@@ -234,9 +234,12 @@ export default function EchartsTimeseries({
// Cross-filter by dimension (original behavior)
const { seriesName: name } = props;
handleChange(name);
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334)
handleXAxisChange(props.data[0]);
} else if (canCrossFilterByXAxis && props.name != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334).
// Use `name` (the category-axis value) instead of `data[0]`: for
// horizontal bars the data tuple is value-first, so `data[0]` would
// be the metric value rather than the category (issue #41102).
handleXAxisChange(props.name);
}
}, TIMER_DURATION);
},
@@ -318,8 +321,10 @@ export default function EchartsTimeseries({
let crossFilter;
if (hasDimensions) {
crossFilter = getCrossFilterDataMask(seriesName);
} else if (canCrossFilterByXAxis && data?.[0] != null) {
crossFilter = getXAxisCrossFilterDataMask(data[0]);
} else if (canCrossFilterByXAxis && eventParams.name != null) {
// Use `name` (the category-axis value), not `data[0]`, so horizontal
// bars cross-filter on the category and not the metric (issue #41102).
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {

View File

@@ -1289,9 +1289,13 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:returns: The error message
"""
quoted_tables = [f"`{table}`" for table in tables]
return f"""You need access to the following tables: {", ".join(quoted_tables)},
`all_database_access` or `all_datasource_access` permission"""
quoted_tables = [f'"{table}"' for table in tables]
return _(
"You need access to the following tables: %(tables)s, "
"'all_database_access' or 'all_datasource_access' permission"
) % {
"tables": ",".join(quoted_tables),
}
def get_table_access_error_object(self, tables: set["Table"]) -> SupersetError:
"""

View File

@@ -1123,14 +1123,17 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
"""
Check if the statement has a subquery.
Covers explicit subqueries, set operations (``UNION``/``INTERSECT``/
``EXCEPT``), and any nested ``SELECT`` regardless of the top-level node
type (e.g. when wrapped in parentheses or a set operation).
:return: True if the statement has a subquery.
"""
return bool(self._parsed.find(exp.Subquery)) or (
isinstance(self._parsed, exp.Select)
and any(
isinstance(expression, exp.Select)
for expression in self._parsed.walk()
if expression != self._parsed
return (
self.is_set_operation()
or bool(self._parsed.find(exp.Subquery))
or any(
select != self._parsed for select in self._parsed.find_all(exp.Select)
)
)

View File

@@ -402,8 +402,8 @@ def test_raise_for_access_query_default_schema(
)
assert (
str(excinfo.value)
== """You need access to the following tables: `public.ab_user`,
`all_database_access` or `all_datasource_access` permission"""
== 'You need access to the following tables: "public.ab_user", '
"'all_database_access' or 'all_datasource_access' permission"
)
@@ -1454,8 +1454,8 @@ def test_raise_for_access_catalog(
sm.raise_for_access(query=query)
assert (
str(excinfo.value)
== """You need access to the following tables: `db1.public.ab_user`,
`all_database_access` or `all_datasource_access` permission"""
== 'You need access to the following tables: "db1.public.ab_user", '
"'all_database_access' or 'all_datasource_access' permission"
)
query.sql = "SELECT * FROM db2.public.ab_user"
@@ -1463,8 +1463,8 @@ def test_raise_for_access_catalog(
sm.raise_for_access(query=query)
assert (
str(excinfo.value)
== """You need access to the following tables: `db2.public.ab_user`,
`all_database_access` or `all_datasource_access` permission"""
== 'You need access to the following tables: "db2.public.ab_user", '
"'all_database_access' or 'all_datasource_access' permission"
)

View File

@@ -3549,6 +3549,17 @@ def test_tokenize_kql(kql: str, expected: list[tuple[KQLTokenType, str]]) -> Non
"postgresql",
True,
),
# Set operations: a top-level UNION/INTERSECT/EXCEPT is not an
# exp.Subquery, so it must be detected explicitly. A predicate fragment
# that introduces one (e.g. supplied through a chart filter) must be
# flagged.
("true UNION SELECT name FROM other_table", "postgresql", True),
("1 = 1 UNION ALL SELECT password FROM users", "postgresql", True),
("SELECT 1 INTERSECT SELECT 2", "postgresql", True),
("SELECT 1 EXCEPT SELECT 2", "postgresql", True),
# Nested SELECT under non-Select top-level nodes (e.g. extra
# parentheses) must still be detected.
("name IN (((SELECT secret FROM s)))", "postgresql", True),
],
)
def test_has_subquery(sql: str, engine: str, expected: bool) -> None: