fix(tableau-next-semantic-model-generate): correct broken CLI examples, unqualified syntax, aggregation choices, error docs

This commit is contained in:
Antoine Laviron 2026-07-30 18:11:14 +02:00
parent 2fe32896e4
commit 8018d30d9d
5 changed files with 44 additions and 27 deletions

View File

@ -330,8 +330,12 @@ Format: `fieldApiName:tableApiName` (repeat `--additional-dimension` for multipl
**Creation success is not data success.** Empty results (empty source, filter excludes everything, expression resolves to nothing) → dashboard shows "No results to show." Check with `--verify-only`:
```bash
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> --verify-only
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --verify-only
# --verify-only ignores --label/--calculated-field/--time-* but argparse still requires them; pass any values.
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> \
--label "verify" --calculated-field <ANY_clc> \
--time-field <ANY_TimeField> --time-table <ANY_TimeTable> \
--verify-only
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --label "verify" --verify-only
```
Non-zero exit + **NOT shippable** = don't proceed to dashboards. Full protocol (why not to re-POST, `query_data.py --count` for raw objects, empty-source guidance) in **[references/concepts.md](references/concepts.md)** and [references/empty-source-handling.md](references/empty-source-handling.md).
@ -353,7 +357,7 @@ python scripts/update_sdm.py {{SDM_NAME}} \
python scripts/update_sdm.py {{SDM_NAME}} --agent-enabled --dry-run
# Confirm via discovery (stored state, not CLI success message)
python scripts/discover_sdm.py {{SDM_NAME}} --json
python scripts/discover_sdm.py --sdm {{SDM_NAME}} --json
```
The update is a PATCH (partial body — server merges), idempotent. `--categories` takes controlled "Semantic Category" values only — the server rejects unknown values with `Invalid Semantic Category`.

View File

@ -418,21 +418,34 @@ Downstream builders that consume this contract should enforce this pre-POST and
## Error Responses
All endpoints return structured error responses:
Salesforce API error responses come in two shapes depending on the endpoint.
**Array form (most common):** a top-level JSON array of one or more error objects.
```json
[
{
"errorCode": "INVALID_API_INPUT",
"message": "Field 'Amount' is not found in the semantic model.",
"fields": ["dataObjectFields"]
}
]
```
**Single-object form:** a bare JSON object (used by some endpoints and by the platform for auth/session errors).
```json
{
"error": {
"code": "INVALID_FIELD",
"message": "Field 'Amount' not found in semantic model 'Sales_Cloud12_backward'",
"details": {
"fieldName": "Amount",
"availableFields": ["Total_Amount", "Close_Date", "Stage"]
}
}
"errorCode": "INVALID_SESSION_ID",
"message": "Session expired or invalid.",
"fields": []
}
```
A `localizedMessage` key may appear in place of (or alongside) `message` in platform-level errors.
The `fields` array is present when the error is scoped to specific request fields; it is omitted or empty otherwise.
**Common Error Codes:**
- `INVALID_TOKEN`: Authentication token is invalid or expired
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions

View File

@ -26,7 +26,7 @@ python scripts/create_calc_field.py \
--type measurement \
--name Total_Revenue_clc \
--label "Total Revenue" \
--expression "SUM([Amount])" \
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount])" \
--aggregation Sum
# Step 2: Create metric
@ -141,7 +141,7 @@ python scripts/create_calc_field.py \
--type measurement \
--name Win_Rate_clc \
--label "Win Rate" \
--expression "SUM([Won_Count]) / SUM([Total_Count])" \
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Won_Count]) / SUM([Opportunity_TAB_Sales_Cloud].[Total_Count])" \
--aggregation UserAgg
# Step 2: Create metric
@ -169,7 +169,7 @@ python scripts/create_calc_field.py \
--type measurement \
--name Weighted_Pipeline_clc \
--label "Weighted Pipeline Value" \
--expression "SUM([Amount] * [Probability])" \
--expression "SUM([Opportunity_TAB_Sales_Cloud].[Amount] * [Opportunity_TAB_Sales_Cloud].[Probability])" \
--aggregation Sum
# Step 2: Create metric
@ -197,7 +197,7 @@ python scripts/create_calc_field.py \
--type measurement \
--name Avg_Sales_Cycle_clc \
--label "Average Sales Cycle (Days)" \
--expression "AVG(DATEDIFF('day', [Created_Date], [Close_Date]))" \
--expression "AVG(DATEDIFF('day', [Opportunity_TAB_Sales_Cloud].[Created_Date], [Opportunity_TAB_Sales_Cloud].[Close_Date]))" \
--aggregation Avg
# Step 2: Create metric
@ -375,12 +375,12 @@ After creating a metric, test it by:
| Pattern | Calc Field Expression | Aggregation | Use Case |
|---------|----------------------|-------------|----------|
| Simple sum | `SUM([Field])` | Sum | Total revenue, total count |
| Average | `AVG([Field])` | Avg | Average deal size, average duration |
| Ratio | `SUM([A]) / SUM([B])` | UserAgg | Win rate, conversion rate |
| Weighted | `SUM([A] * [B])` | Sum | Weighted pipeline, risk-adjusted |
| Time calc | `AVG(DATEDIFF('day', [Start], [End]))` | Avg | Sales cycle, time to close |
| Conditional sum | `SUM(IF condition THEN [Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
| Simple sum | `SUM([Table].[Field])` | Sum | Total revenue, total count |
| Average | `AVG([Table].[Field])` | Avg | Average deal size, average duration |
| Ratio | `SUM([Table].[A]) / SUM([Table].[B])` | UserAgg | Win rate, conversion rate |
| Weighted | `SUM([Table].[A] * [Table].[B])` | Sum | Weighted pipeline, risk-adjusted |
| Time calc | `AVG(DATEDIFF('day', [Table].[Start], [Table].[End]))` | Avg | Sales cycle, time to close |
| Conditional sum | `SUM(IF condition THEN [Table].[Field] ELSE 0 END)` | Sum | Won revenue, qualified leads |
## Metric Lifecycle

View File

@ -119,7 +119,7 @@ def build_calculated_measurement(
api_name: API name (must end with _clc)
label: Display label
expression: Tableau formula expression
aggregation_type: Aggregation type (Sum, Avg, Count, Min, Max, UserAgg, Median)
aggregation_type: Aggregation type (Sum, Avg, Count, CountDistinct, Min, Max, UserAgg)
data_type: Data type (Number, Text, Boolean, DateTime)
decimal_place: Decimal places for Number fields
description: Field description
@ -155,11 +155,11 @@ def build_calculated_measurement(
final_agg_type = aggregation_type
level = "Row"
# Validate explicit aggregation types are allowed
if final_agg_type not in ("Sum", "Avg", "Count", "Min", "Max", "Median", "UserAgg"):
if final_agg_type not in ("Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"):
import warnings
warnings.warn(
f"Explicit aggregation type '{final_agg_type}' may not be supported. "
f"Common types: Sum, Avg, Count, Min, Max, Median"
f"Common types: Sum, Avg, Count, CountDistinct, Min, Max"
)
payload = {
@ -262,7 +262,7 @@ def validate_calc_field(
# Check aggregation type
if field_type == "measurement" and aggregation_type:
valid_agg = ["Sum", "Avg", "Count", "Min", "Max", "UserAgg", "Median"]
valid_agg = ["Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"]
if aggregation_type not in valid_agg:
errors.append(f"aggregationType must be one of: {', '.join(valid_agg)}")

View File

@ -139,7 +139,7 @@ def main() -> int:
# Measurement-specific
parser.add_argument("--aggregation", default="UserAgg",
choices=["Sum", "Avg", "Count", "Min", "Max", "UserAgg", "Median"],
choices=["Sum", "Avg", "Count", "CountDistinct", "Min", "Max", "UserAgg"],
help="Aggregation type (for measurements, default: UserAgg)")
parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"],
help="Data type (default: Number for measurements, inferred for dimensions)")