afv-library/skills/tableau-next-semantic-model-generate/scripts/add_relationship.py
Antoine Laviron b26d254871 feat: add tableau-next-semantic-model-generate skill
Adds a skill for authoring Tableau Next semantic models (SDMs) on Data 360: build from scratch, add data objects, define joins, enrich with calculated fields and metrics, and make models AI-ready.

Smoke-tested against a live Data 360 org: SDM discovery, AI-readiness flip, dimension creation, metric creation, and description backfill all exercised end-to-end.
2026-07-26 13:33:29 +02:00

124 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Add a model-level relationship (join) between two SDM data objects.
Builds the relationship: joinType="Auto",
cardinality (default ManyToOne, fact→dimension), criteria[] with
leftFieldType/rightFieldType="TableField", and a REQUIRED label. The join-key
field references MUST be the resolved semantic apiNames (printed by
create_sdm.py / add_data_object.py, or from `discover_sdm.py --json`) —
NOT the raw "__c" source column, which fails with "field could not be found"
(the #1 relationship error).
Usage:
# Single-key join (most common): employee.position_id2 -> position.position_id3
python scripts/add_relationship.py \\
--sdm Join_SDM \\
--left-object qb_hw_employee --right-object qb_hw_position \\
--left-field position_id2 --right-field position_id3 \\
--label "Employee : Position"
# Explicit cardinality + apiName
python scripts/add_relationship.py \\
--sdm Join_SDM --api-name emp_dept \\
--left-object Employee --right-object Department \\
--left-field dept_id1 --right-field dept_id \\
--cardinality ManyToOne --label "Employee : Department"
# Dry-run
python scripts/add_relationship.py \\
--sdm Join_SDM --left-object A --right-object B \\
--left-field a1 --right-field b1 --label "A : B" --dry-run
After a successful add, verify the join with a cross-object semantic query
(see references/sdm-creation-api.md, "Proof the join works").
"""
import argparse
import json
import sys
from _shared.sdm_structure_templates import (
build_join_criterion,
build_relationship,
validate_relationship,
)
from _shared.sf_api import (
get_credentials,
sdm_relationships_endpoint,
sf_post,
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Add a model-level relationship (join) between two SDM data objects.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--sdm", required=True, help="Target SDM apiName")
parser.add_argument("--left-object", required=True, help="Left data-object apiName (e.g. the fact)")
parser.add_argument("--right-object", required=True, help="Right data-object apiName (e.g. the dimension)")
parser.add_argument("--left-field", required=True,
help="Left join-key: the RESOLVED semantic apiName (NOT the __c source name)")
parser.add_argument("--right-field", required=True,
help="Right join-key: the RESOLVED semantic apiName (NOT the __c source name)")
parser.add_argument("--label", required=True, help="Relationship display label (REQUIRED)")
parser.add_argument("--api-name", help="Relationship apiName (default: <left>_<right>)")
parser.add_argument(
"--cardinality", default="ManyToOne",
choices=["OneToOne", "OneToMany", "ManyToOne", "ManyToMany", "Unspecified"],
help="Join cardinality (default ManyToOne, fact->dimension)",
)
parser.add_argument("--join-operator", default="Equals", help="Criterion operator (default Equals)")
parser.add_argument("--dry-run", action="store_true", help="Print payload without POSTing")
args = parser.parse_args()
api_name = args.api_name or f"{args.left_object}_{args.right_object}"
criterion = build_join_criterion(
left_field_api_name=args.left_field,
right_field_api_name=args.right_field,
join_operator=args.join_operator,
)
payload = build_relationship(
api_name=api_name,
label=args.label,
left_object=args.left_object,
right_object=args.right_object,
criteria=[criterion],
cardinality=args.cardinality,
)
# Validate pre-POST — catches empty label, non-Auto join, bad cardinality,
# and the #1 error: a raw "__c" source-column join key.
ok, errors = validate_relationship(payload)
if not ok:
print("Validation errors:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
sys.exit(1)
print(json.dumps(payload, indent=2))
if args.dry_run:
print("\n[Dry-run mode - payload shown above, not POSTed]", file=sys.stderr)
return
token, instance = get_credentials()
resp, err = sf_post(token, instance, sdm_relationships_endpoint(args.sdm), payload)
if err:
print(f"Error: {err}", file=sys.stderr)
sys.exit(1)
queryable = (resp or {}).get("isQueryable", "")
print(f"\n✓ Added relationship: {api_name}", file=sys.stderr)
if queryable:
print(f" isQueryable: {queryable}", file=sys.stderr)
print(
"\nVerify the join with a cross-object query (a field from each object "
"in one query). See references/sdm-creation-api.md.",
file=sys.stderr,
)
if __name__ == "__main__":
main()