mikeboone Claude Fable 5 commited on
Commit
abfed92
Β·
1 Parent(s): 47edf58

fix(model): omit join-key columns instead of hiding them (ISS-8 follow-up)

Browse files

The Aug 24 naming fix (3986a1e) marked surrogate/FK *_KEY columns
is_hidden: true, but live verification on TRI_08250424_PY3_mdl showed
hidden keys still display their raw junk names ("Dim Dim Broker Key")
in the ThoughtSpot model editor page β€” is_hidden only hides from
search/Spotter, not the editor stakeholders actually look at.

Fix: omit *_KEY columns from the model's columns list entirely in
_create_model_with_constraints(). Joins are unaffected since they're
defined at the table level (model_tables[].joins reference physical
TABLE::COLUMN names directly, independent of the columns list). All
liveboard tiles on the Triumph model confirmed still resolve with keys
fully removed.

New coverage: tests/test_model_key_column_omission.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

sprint_2026_04.md CHANGED
@@ -259,7 +259,7 @@ should tell. KPI targets, growth trends, and anomaly patterns live in the matrix
259
  - [x] **Model-quality fixes from TRI_08190447_DXO review (Aug 24)** β€” investigated mani's Triumph model (sebe); found four systemic pipeline defects and fixed all in code (NOT applied to the existing TRI objects β€” that demo stays as-is per boone):
260
  - **Join spanning-tree bug**: `_remove_diamond_joins()` reduced every model to Nβˆ’1 joins via undirected union-find, silently dropping legitimate factβ†’dim joins in galaxy schemas (TRI lost FACT_BROKER_PAYMENTβ†’DIM_BROKER and FACT_CARRIER_CAPACITYβ†’DIM_LANE β€” payment metrics unsliceable by broker). Rewritten to directed-reachability: only prunes true diamonds (two paths between same pair) and directed cycles; shared conformed dims are kept βœ…
261
  - **DIM_/FACT_ naming banned end-to-end**: blueprint prompt rule 2a + `_table_ident()` normalization in `blueprint.py` (strips prefixes even when the user request includes them; directives matching already prefix-tolerant); Example B updated. Deployer `_strip_dim_fact_prefix()` strips prefixes from display names; conflict resolution now gives the home table the clean name ("Broker Key") and others a readable suffix ("Broker Key (Broker Payment)") β€” no more "Dim Dim Broker Key" / "fact_Fact …" / "_2" βœ…
262
- - **Key columns hidden**: surrogate PKs + FKs get `is_hidden: true` in model TML (still present for joins) βœ…
263
  - **Column classification**: token-based matching in `_determine_column_type()` β€” INVOICES_PAID no longer an ATTRIBUTE (endswith-'ID' trap), CORPORATE/GENERATED no longer match 'RATE'; RATIO/PCT/DAYS_TO_* now AVERAGE instead of SUM βœ…
264
  - Verified offline against the exact TRI structure: `scratch/verify_fixes.py` β€” 25/25 pass. Repaired TRI TMLs staged in scratch for reference only (`model_tri_fixed.tml.yml`, `lb_*_fixed.tml.yml`), not imported.
265
 
 
259
  - [x] **Model-quality fixes from TRI_08190447_DXO review (Aug 24)** β€” investigated mani's Triumph model (sebe); found four systemic pipeline defects and fixed all in code (NOT applied to the existing TRI objects β€” that demo stays as-is per boone):
260
  - **Join spanning-tree bug**: `_remove_diamond_joins()` reduced every model to Nβˆ’1 joins via undirected union-find, silently dropping legitimate factβ†’dim joins in galaxy schemas (TRI lost FACT_BROKER_PAYMENTβ†’DIM_BROKER and FACT_CARRIER_CAPACITYβ†’DIM_LANE β€” payment metrics unsliceable by broker). Rewritten to directed-reachability: only prunes true diamonds (two paths between same pair) and directed cycles; shared conformed dims are kept βœ…
261
  - **DIM_/FACT_ naming banned end-to-end**: blueprint prompt rule 2a + `_table_ident()` normalization in `blueprint.py` (strips prefixes even when the user request includes them; directives matching already prefix-tolerant); Example B updated. Deployer `_strip_dim_fact_prefix()` strips prefixes from display names; conflict resolution now gives the home table the clean name ("Broker Key") and others a readable suffix ("Broker Key (Broker Payment)") β€” no more "Dim Dim Broker Key" / "fact_Fact …" / "_2" βœ…
262
+ - **Key columns omitted (updated Aug 25)**: surrogate PKs + FKs were initially given `is_hidden: true` in model TML, but live verification on `TRI_08250424_PY3_mdl` showed hidden keys still display their raw junk names ("Dim Dim Broker Key") in the ThoughtSpot model editor β€” `is_hidden` only hides from search/Spotter. Fix updated to omit `*_KEY` columns from the model's `columns` list entirely; joins are unaffected since they're defined at the table level (`model_tables[].joins` reference physical `TABLE::COLUMN` names, independent of the columns list). All liveboard tiles confirmed still resolve. New coverage: `tests/test_model_key_column_omission.py` βœ…
263
  - **Column classification**: token-based matching in `_determine_column_type()` β€” INVOICES_PAID no longer an ATTRIBUTE (endswith-'ID' trap), CORPORATE/GENERATED no longer match 'RATE'; RATIO/PCT/DAYS_TO_* now AVERAGE instead of SUM βœ…
264
  - Verified offline against the exact TRI structure: `scratch/verify_fixes.py` β€” 25/25 pass. Repaired TRI TMLs staged in scratch for reference only (`model_tri_fixed.tml.yml`, `lb_*_fixed.tml.yml`), not imported.
265
 
tests/test_model_key_column_omission.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Regression test for the join-key visibility fix on deploy/mcp (2026-08-25).
3
+
4
+ _create_model_with_constraints() previously marked surrogate/FK *_KEY columns
5
+ is_hidden: true. That hides them from search/Spotter but they still show up
6
+ with their raw physical names ("Dim Dim Broker Key") in the ThoughtSpot model
7
+ editor page β€” verified live on TRI_08250424_PY3_mdl, the surface stakeholders
8
+ actually look at. Fix: omit *_KEY columns from the model's columns list
9
+ entirely. Joins stay intact because they're defined at the table level
10
+ (model_tables[].joins reference physical TABLE::COLUMN names directly).
11
+ """
12
+ import re
13
+
14
+ import pytest
15
+ import yaml
16
+
17
+ import thoughtspot_deployer as tsd
18
+
19
+
20
+ @pytest.fixture
21
+ def deployer(monkeypatch):
22
+ monkeypatch.setattr(
23
+ tsd, 'get_admin_setting',
24
+ lambda key, required=True: f'test-{key.lower()}'
25
+ )
26
+ return tsd.ThoughtSpotDeployer(
27
+ base_url='https://unit-test.thoughtspot.cloud',
28
+ username='unit-test@example.com',
29
+ secret_key='unit-test-key',
30
+ )
31
+
32
+
33
+ def _star_schema():
34
+ tables = {
35
+ 'DIM_BROKER': [
36
+ {'name': 'DIM_BROKER_KEY', 'type': 'NUMBER(38,0)'},
37
+ {'name': 'DIM_BROKER_NAME', 'type': 'VARCHAR(120)'},
38
+ {'name': 'BROKER_TIER', 'type': 'VARCHAR(20)'},
39
+ ],
40
+ 'FACT_LOAD_TRANSACTION': [
41
+ {'name': 'FACT_LOAD_TRANSACTION_KEY', 'type': 'NUMBER(38,0)'},
42
+ {'name': 'DIM_BROKER_KEY', 'type': 'NUMBER(38,0)'},
43
+ {'name': 'LOAD_DATE', 'type': 'DATE'},
44
+ {'name': 'TOTAL_LOAD_REVENUE_USD', 'type': 'NUMBER(18,2)'},
45
+ ],
46
+ }
47
+ foreign_keys = [
48
+ {'from_table': 'FACT_LOAD_TRANSACTION', 'from_column': 'DIM_BROKER_KEY',
49
+ 'to_table': 'DIM_BROKER', 'to_column': 'DIM_BROKER_KEY'},
50
+ ]
51
+ table_guids = {name: f'guid-{name.lower()}' for name in tables}
52
+ return tables, foreign_keys, table_guids
53
+
54
+
55
+ def _build_model(deployer):
56
+ tables, foreign_keys, table_guids = _star_schema()
57
+ tml = deployer._create_model_with_constraints(
58
+ tables, foreign_keys, table_guids,
59
+ table_constraints={}, model_name='UNIT_TEST_mdl',
60
+ connection_name='unit_test_conn',
61
+ )
62
+ return yaml.safe_load(tml)
63
+
64
+
65
+ def test_join_key_columns_are_omitted_not_hidden(deployer):
66
+ model = _build_model(deployer)
67
+ columns = model['model']['columns']
68
+
69
+ key_backed = [c for c in columns if c['column_id'].upper().endswith('_KEY')]
70
+ assert key_backed == [], f"*_KEY columns must not appear in the model at all: {key_backed}"
71
+
72
+ hidden = [c for c in columns if c.get('properties', {}).get('is_hidden')]
73
+ assert hidden == [], "is_hidden is no longer how keys are handled β€” they should be absent, not hidden"
74
+
75
+ key_named = [c for c in columns if re.search(r'\bkey\b', c['name'], re.IGNORECASE)]
76
+ assert key_named == [], f"No column should be named like a key: {key_named}"
77
+
78
+ # 7 source columns minus 3 keys = 4 business columns
79
+ assert len(columns) == 4
80
+
81
+
82
+ def test_joins_still_reference_physical_key_columns(deployer):
83
+ """Keys leave the columns list but must stay in the table-level join clauses."""
84
+ model = _build_model(deployer)
85
+ fact = next(t for t in model['model']['model_tables']
86
+ if t['name'] == 'FACT_LOAD_TRANSACTION')
87
+ on_clauses = [j.get('on', j.get(True)) for j in fact['joins']]
88
+
89
+ assert '[FACT_LOAD_TRANSACTION::DIM_BROKER_KEY] = [DIM_BROKER::DIM_BROKER_KEY]' in on_clauses
90
+
91
+
92
+ def test_business_columns_unaffected(deployer):
93
+ model = _build_model(deployer)
94
+ names = {c['name'] for c in model['model']['columns']}
95
+ assert 'Broker Tier' in names
96
+ assert 'Load Date' in names
97
+ assert 'Total Load Revenue Usd' in names
thoughtspot_deployer.py CHANGED
@@ -1403,19 +1403,30 @@ class ThoughtSpotDeployer:
1403
  # Add columns with proper global conflict resolution (same as working version)
1404
  used_display_names = set()
1405
 
1406
- # Key columns (surrogate PKs and FKs) must be IN the model for joins to
1407
- # work, but they're join plumbing, not demo content β€” mark them hidden.
 
 
 
 
 
 
1408
  fk_columns = set()
1409
  for fk in foreign_keys or []:
1410
  fk_columns.add((fk.get('from_table', '').upper(), fk.get('from_column', '').upper()))
1411
  fk_columns.add((fk.get('to_table', '').upper(), fk.get('to_column', '').upper()))
1412
 
 
1413
  for table_name, columns in tables.items():
1414
  table_name_upper = table_name.upper()
1415
  for col in columns:
1416
  col_name = col['name'].upper()
1417
  original_col_name = col.get('original_name', col['name']) # Use original casing for display
1418
 
 
 
 
 
1419
  # Start with basic conflict resolution
1420
  display_name = self._resolve_column_name_conflict(
1421
  col_name, table_name_upper, column_name_counts,
@@ -1453,11 +1464,6 @@ class ThoughtSpotDeployer:
1453
  if aggregation:
1454
  column_def['properties']['aggregation'] = aggregation
1455
 
1456
- # Hide surrogate/foreign key columns β€” required for joins,
1457
- # but nobody demos "broker key 23455"
1458
- if col_name.endswith('_KEY') or (table_name_upper, col_name) in fk_columns:
1459
- column_def['properties']['is_hidden'] = True
1460
-
1461
  # Add calendar property for DATE columns so ThoughtSpot enables
1462
  # time bucketing (.weekly, .monthly, etc.) on them
1463
  if self._map_data_type(col['type']) == 'DATE':
@@ -1465,6 +1471,9 @@ class ThoughtSpotDeployer:
1465
 
1466
  model['model']['columns'].append(column_def)
1467
 
 
 
 
1468
  # Generate YAML output with validation
1469
  yaml_output = yaml.dump(model, default_flow_style=False, sort_keys=False,
1470
  default_style=None, indent=2, width=120)
 
1403
  # Add columns with proper global conflict resolution (same as working version)
1404
  used_display_names = set()
1405
 
1406
+ # Key columns (surrogate PKs and FKs) are needed in the physical tables
1407
+ # for joins, but joins are defined at the table level (model_tables[].joins
1408
+ # reference TABLE::COLUMN directly) β€” the model's columns list doesn't need
1409
+ # them. Previously these were kept with is_hidden: true, but that still
1410
+ # left the junk names ("Dim Dim Broker Key") visible in the ThoughtSpot
1411
+ # model editor β€” is_hidden only hides from search/Spotter. Verified live
1412
+ # on TRI_08250424_PY3_mdl (2026-08-25): every liveboard tile still
1413
+ # resolves with all key columns omitted entirely.
1414
  fk_columns = set()
1415
  for fk in foreign_keys or []:
1416
  fk_columns.add((fk.get('from_table', '').upper(), fk.get('from_column', '').upper()))
1417
  fk_columns.add((fk.get('to_table', '').upper(), fk.get('to_column', '').upper()))
1418
 
1419
+ omitted_keys = []
1420
  for table_name, columns in tables.items():
1421
  table_name_upper = table_name.upper()
1422
  for col in columns:
1423
  col_name = col['name'].upper()
1424
  original_col_name = col.get('original_name', col['name']) # Use original casing for display
1425
 
1426
+ if col_name.endswith('_KEY') or (table_name_upper, col_name) in fk_columns:
1427
+ omitted_keys.append(f"{table_name_upper}.{col_name}")
1428
+ continue
1429
+
1430
  # Start with basic conflict resolution
1431
  display_name = self._resolve_column_name_conflict(
1432
  col_name, table_name_upper, column_name_counts,
 
1464
  if aggregation:
1465
  column_def['properties']['aggregation'] = aggregation
1466
 
 
 
 
 
 
1467
  # Add calendar property for DATE columns so ThoughtSpot enables
1468
  # time bucketing (.weekly, .monthly, etc.) on them
1469
  if self._map_data_type(col['type']) == 'DATE':
 
1471
 
1472
  model['model']['columns'].append(column_def)
1473
 
1474
+ if omitted_keys:
1475
+ print(f" πŸ™ˆ Omitted {len(omitted_keys)} join-key columns from model: {', '.join(omitted_keys)}")
1476
+
1477
  # Generate YAML output with validation
1478
  yaml_output = yaml.dump(model, default_flow_style=False, sort_keys=False,
1479
  default_style=None, indent=2, width=120)