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`: **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 ```bash
python scripts/create_metric.py --sdm <SDM> --name <Metric_mtc> --verify-only # --verify-only ignores --label/--calculated-field/--time-* but argparse still requires them; pass any values.
python scripts/create_calc_field.py --sdm <SDM> --type measurement --name <Field_clc> --verify-only 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). 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 python scripts/update_sdm.py {{SDM_NAME}} --agent-enabled --dry-run
# Confirm via discovery (stored state, not CLI success message) # 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`. 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 ## 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 ```json
{ {
"error": { "errorCode": "INVALID_SESSION_ID",
"code": "INVALID_FIELD", "message": "Session expired or invalid.",
"message": "Field 'Amount' not found in semantic model 'Sales_Cloud12_backward'", "fields": []
"details": {
"fieldName": "Amount",
"availableFields": ["Total_Amount", "Close_Date", "Stage"]
}
}
} }
``` ```
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:** **Common Error Codes:**
- `INVALID_TOKEN`: Authentication token is invalid or expired - `INVALID_TOKEN`: Authentication token is invalid or expired
- `INSUFFICIENT_PERMISSIONS`: User lacks required permissions - `INSUFFICIENT_PERMISSIONS`: User lacks required permissions

View File

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

View File

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

View File

@ -139,7 +139,7 @@ def main() -> int:
# Measurement-specific # Measurement-specific
parser.add_argument("--aggregation", default="UserAgg", 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)") help="Aggregation type (for measurements, default: UserAgg)")
parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"], parser.add_argument("--data-type", choices=["Number", "Text", "Boolean", "DateTime"],
help="Data type (default: Number for measurements, inferred for dimensions)") help="Data type (default: Number for measurements, inferred for dimensions)")