codeant-ai-for-open-source[bot] commented on code in PR #38402:
URL: https://github.com/apache/superset/pull/38402#discussion_r2930720646
##########
superset/mcp_service/chart/chart_utils.py:
##########
@@ -916,12 +958,24 @@ def _mixed_timeseries_what(config:
MixedTimeseriesChartConfig) -> str:
return f"{primary} + {secondary}"
+def _handlebars_chart_what(config: HandlebarsChartConfig) -> str:
+ """Build the 'what' portion for a handlebars chart name."""
+ if config.query_mode == "raw" and config.columns:
+ cols = ", ".join(col.name for col in config.columns[:3])
+ return f"Handlebars – {cols}"
+ elif config.metrics:
+ metrics = ", ".join(col.name for col in config.metrics[:3])
+ return f"Handlebars – {metrics}"
Review Comment:
**Suggestion:** The chart-name fragment for Handlebars already contains an
en-dash delimiter, but `generate_chart_name` and `_truncate` also use the same
delimiter to append/split context. This can produce malformed names and
incorrect truncation when filters are present. Use a different separator in the
Handlebars-specific fragment to avoid delimiter collisions. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Saved chart names lose metric/column specificity.
- ⚠️ Preview alt_text becomes less descriptive for users.
- ⚠️ Chart management UX worsens with ambiguous names.
```
</details>
```suggestion
if config.query_mode == "raw" and config.columns:
cols = ", ".join(col.name for col in config.columns[:3])
return f"Handlebars: {cols}"
elif config.metrics:
metrics = ", ".join(col.name for col in config.metrics[:3])
return f"Handlebars: {metrics}"
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Call MCP tool `generate_chart` (entrypoint
`superset/mcp_service/chart/tool/generate_chart.py:123`) with a Handlebars
config
(`chart_type=\"handlebars\"`) and long column/metric names plus filters;
this is valid
because `ColumnRef.name` allows up to 255 chars and Handlebars supports
filters
(`superset/mcp_service/chart/schemas.py:60-66`, `:667-720`).
2. In the normal chart-creation flow, `generate_chart` computes the name via
`generate_chart_name(request.config, ...)`
(`superset/mcp_service/chart/tool/generate_chart.py:347`), which calls
`_handlebars_chart_what()` and returns a base string already containing `" –
"`
(`superset/mcp_service/chart/chart_utils.py:961-968`).
3. `generate_chart_name` then appends context using the same delimiter
(`name = f\"{what}
– {context}\"` at `superset/mcp_service/chart/chart_utils.py:1015-1017`),
producing names
with multiple `" – "` separators.
4. When name length exceeds 60, `_truncate()` splits at the first `" – "`
(`superset/mcp_service/chart/chart_utils.py:872-880`), so Handlebars names
can collapse to
just `"Handlebars"` and lose identifying details; that degraded name is
persisted/returned
in user-facing flows (`slice_name` in create payload at
`generate_chart.py:347-343/37`,
and also used in preview/update metadata at `update_chart_preview.py:98-103`
and
`update_chart.py:142,170`).
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/mcp_service/chart/chart_utils.py
**Line:** 963:968
**Comment:**
*Logic Error: The chart-name fragment for Handlebars already contains
an en-dash delimiter, but `generate_chart_name` and `_truncate` also use the
same delimiter to append/split context. This can produce malformed names and
incorrect truncation when filters are present. Use a different separator in the
Handlebars-specific fragment to avoid delimiter collisions.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=c0645c2adbab82153a665b9fcfb8f2971d2f246ca565908f4ce48fd39759b976&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=c0645c2adbab82153a665b9fcfb8f2971d2f246ca565908f4ce48fd39759b976&reaction=dislike'>👎</a>
##########
superset/mcp_service/chart/schemas.py:
##########
@@ -664,6 +664,88 @@ class MixedTimeseriesChartConfig(BaseModel):
filters: List[FilterConfig] | None = Field(None, description="Filters to
apply")
+class HandlebarsChartConfig(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ chart_type: Literal["handlebars"] = Field(
+ ...,
+ description=(
+ "Chart type discriminator - MUST be 'handlebars' for custom HTML "
+ "template charts. Handlebars charts render query results using "
+ "Handlebars templates, enabling fully custom layouts like KPI
cards, "
+ "leaderboards, and formatted reports."
+ ),
+ )
+ handlebars_template: str = Field(
+ ...,
+ description=(
+ "Handlebars HTML template string. Data is available as {{data}}
array. "
+ "Built-in helpers: {{dateFormat val format='YYYY-MM-DD'}}, "
+ "{{formatNumber val}}, {{stringify obj}}. "
+ "Example: '<ul>{{#each data}}<li>{{this.name}}:
{{this.value}}</li>"
+ "{{/each}}</ul>'"
+ ),
+ min_length=1,
+ max_length=50000,
+ )
+ query_mode: Literal["aggregate", "raw"] = Field(
+ "aggregate",
+ description=(
+ "Query mode: 'aggregate' groups data with metrics, "
+ "'raw' returns individual rows"
+ ),
+ )
+ columns: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Columns to display in raw mode (query_mode='raw'). "
+ "Each column specifies a column name to include in the query
results."
+ ),
+ )
+ groupby: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Columns to group by in aggregate mode (query_mode='aggregate'). "
+ "These become the dimensions for aggregation."
+ ),
+ )
+ metrics: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Metrics to aggregate in aggregate mode. "
+ "Each must have an aggregate function (e.g., SUM, COUNT)."
+ ),
+ )
+ filters: list[FilterConfig] | None = Field(None, description="Filters to
apply")
+ row_limit: int = Field(
+ 1000,
+ description="Maximum number of rows",
+ ge=1,
+ le=50000,
+ )
+ order_desc: bool = Field(True, description="Sort in descending order")
+ style_template: str | None = Field(
+ None,
+ description="Optional CSS styles to apply to the rendered template",
+ max_length=10000,
+ )
+
+ @model_validator(mode="after")
+ def validate_query_fields(self) -> "HandlebarsChartConfig":
+ """Validate that the right fields are provided for the query mode."""
+ if self.query_mode == "raw" and not self.columns:
+ raise ValueError(
+ "Handlebars chart in 'raw' query mode requires 'columns'
field. "
+ "Specify which columns to include in the query results."
+ )
+ if self.query_mode == "aggregate" and not self.metrics:
+ raise ValueError(
+ "Handlebars chart in 'aggregate' query mode requires 'metrics'
field. "
+ "Specify at least one metric with an aggregate function."
+ )
Review Comment:
**Suggestion:** Aggregate-mode metrics are only checked for presence, not
for an aggregate function. Because downstream mapping defaults missing
aggregates to `SUM`, non-numeric metric columns can generate invalid SQL at
runtime. Validate that every metric in aggregate mode includes an explicit
aggregate. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Handlebars aggregate config silently changes metric semantics.
- ❌ Non-numeric SUM metrics can fail chart compilation.
- ⚠️ Affects generate_chart, update_chart, generate_explore_link flows.
```
</details>
```suggestion
if self.query_mode == "aggregate":
if not self.metrics:
raise ValueError(
"Handlebars chart in 'aggregate' query mode requires
'metrics' field. "
"Specify at least one metric with an aggregate function."
)
if any(metric.aggregate is None for metric in self.metrics):
raise ValueError(
"Handlebars chart metrics in 'aggregate' query mode must
include "
"an explicit 'aggregate' function for each metric."
)
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Call MCP `generate_chart`
(`superset/mcp_service/chart/tool/generate_chart.py:121-125`)
with `config.chart_type="handlebars"`, `query_mode="aggregate"`, and
`metrics=[{"name":"product_name"}]` (no `aggregate`).
2. Request parsing via `@parse_request(GenerateChartRequest)`
(`generate_chart.py:122`)
builds `HandlebarsChartConfig`; `validate_query_fields()` in
`superset/mcp_service/chart/schemas.py:734-746` only checks metrics
presence, so this
payload passes.
3. `generate_chart()` maps config through `map_config_to_form_data()`
(`generate_chart.py:252-254`) → `map_handlebars_config()`
(`superset/mcp_service/chart/chart_utils.py:638-660`) →
`create_metric_object()`
(`chart_utils.py:414-444`), which silently sets missing aggregates to `SUM`
(`aggregate =
col.aggregate or "SUM"`).
4. Resulting query semantics differ from submitted config; and for
non-numeric columns
this can fail at compile/query time (`_compile_chart()` in
`generate_chart.py:63-118`)
because Handlebars aggregation compatibility is not checked in dataset
validation
(`dataset_validator.py:106-112` only handles `TableChartConfig |
XYChartConfig`).
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/mcp_service/chart/schemas.py
**Line:** 741:745
**Comment:**
*Logic Error: Aggregate-mode metrics are only checked for presence, not
for an aggregate function. Because downstream mapping defaults missing
aggregates to `SUM`, non-numeric metric columns can generate invalid SQL at
runtime. Validate that every metric in aggregate mode includes an explicit
aggregate.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=d0df770d04817ca128c47f4e1dc8378fb14d48e5091181bc0d7a40dd469a1800&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=d0df770d04817ca128c47f4e1dc8378fb14d48e5091181bc0d7a40dd469a1800&reaction=dislike'>👎</a>
##########
superset/mcp_service/chart/schemas.py:
##########
@@ -664,6 +664,88 @@ class MixedTimeseriesChartConfig(BaseModel):
filters: List[FilterConfig] | None = Field(None, description="Filters to
apply")
+class HandlebarsChartConfig(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ chart_type: Literal["handlebars"] = Field(
+ ...,
+ description=(
+ "Chart type discriminator - MUST be 'handlebars' for custom HTML "
+ "template charts. Handlebars charts render query results using "
+ "Handlebars templates, enabling fully custom layouts like KPI
cards, "
+ "leaderboards, and formatted reports."
+ ),
+ )
+ handlebars_template: str = Field(
+ ...,
+ description=(
+ "Handlebars HTML template string. Data is available as {{data}}
array. "
+ "Built-in helpers: {{dateFormat val format='YYYY-MM-DD'}}, "
+ "{{formatNumber val}}, {{stringify obj}}. "
+ "Example: '<ul>{{#each data}}<li>{{this.name}}:
{{this.value}}</li>"
+ "{{/each}}</ul>'"
+ ),
+ min_length=1,
+ max_length=50000,
+ )
+ query_mode: Literal["aggregate", "raw"] = Field(
+ "aggregate",
+ description=(
+ "Query mode: 'aggregate' groups data with metrics, "
+ "'raw' returns individual rows"
+ ),
+ )
+ columns: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Columns to display in raw mode (query_mode='raw'). "
+ "Each column specifies a column name to include in the query
results."
+ ),
+ )
+ groupby: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Columns to group by in aggregate mode (query_mode='aggregate'). "
+ "These become the dimensions for aggregation."
+ ),
+ )
+ metrics: list[ColumnRef] | None = Field(
+ None,
+ description=(
+ "Metrics to aggregate in aggregate mode. "
+ "Each must have an aggregate function (e.g., SUM, COUNT)."
+ ),
+ )
+ filters: list[FilterConfig] | None = Field(None, description="Filters to
apply")
+ row_limit: int = Field(
+ 1000,
+ description="Maximum number of rows",
+ ge=1,
+ le=50000,
+ )
+ order_desc: bool = Field(True, description="Sort in descending order")
+ style_template: str | None = Field(
+ None,
+ description="Optional CSS styles to apply to the rendered template",
+ max_length=10000,
+ )
+
+ @model_validator(mode="after")
+ def validate_query_fields(self) -> "HandlebarsChartConfig":
+ """Validate that the right fields are provided for the query mode."""
+ if self.query_mode == "raw" and not self.columns:
+ raise ValueError(
+ "Handlebars chart in 'raw' query mode requires 'columns'
field. "
+ "Specify which columns to include in the query results."
+ )
Review Comment:
**Suggestion:** Raw mode currently accepts `metrics` and `groupby` but
silently ignores them during form-data mapping, which can produce a chart that
does not match the submitted config. Reject these aggregate-only fields when
`query_mode` is `raw` to prevent incorrect behavior. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Raw-mode payload accepts unsupported aggregate-only fields.
- ❌ Returned form_data ignores submitted metrics/groupby silently.
- ⚠️ Confuses users across explore and chart update flows.
```
</details>
```suggestion
if self.query_mode == "raw":
if not self.columns:
raise ValueError(
"Handlebars chart in 'raw' query mode requires 'columns'
field. "
"Specify which columns to include in the query results."
)
if self.metrics or self.groupby:
raise ValueError(
"Handlebars chart in 'raw' query mode does not support
'metrics' "
"or 'groupby'. Use 'columns' only, or switch to
'aggregate' mode."
)
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Call `generate_explore_link`
(`superset/mcp_service/explore/tool/generate_explore_link.py:42-46`) with
`chart_type="handlebars"`, `query_mode="raw"`, valid `columns`, plus extra
`metrics`/`groupby`.
2. `HandlebarsChartConfig.validate_query_fields()`
(`superset/mcp_service/chart/schemas.py:734-746`) accepts this because raw
mode currently
only requires `columns`.
3. Mapping in `map_handlebars_config()`
(`superset/mcp_service/chart/chart_utils.py:650-659`) takes raw branch and
only emits
`all_columns`; `metrics` and `groupby` are ignored.
4. Tool returns `form_data` (`generate_explore_link.py:153-157`) that does
not reflect
submitted config, producing a silent user-visible mismatch between requested
and executed
query settings.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/mcp_service/chart/schemas.py
**Line:** 736:740
**Comment:**
*Logic Error: Raw mode currently accepts `metrics` and `groupby` but
silently ignores them during form-data mapping, which can produce a chart that
does not match the submitted config. Reject these aggregate-only fields when
`query_mode` is `raw` to prevent incorrect behavior.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=c20ccb84b841e2e78422599be2be3a7ece1c32b793b07c36aa3f5316511559bd&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38402&comment_hash=c20ccb84b841e2e78422599be2be3a7ece1c32b793b07c36aa3f5316511559bd&reaction=dislike'>👎</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]