Spaces:
Running
fix(model): omit join-key columns instead of hiding them (ISS-8 follow-up)
Browse filesThe 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 +1 -1
- tests/test_model_key_column_omission.py +97 -0
- thoughtspot_deployer.py +16 -7
|
@@ -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
|
| 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 |
|
|
@@ -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
|
|
@@ -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)
|
| 1407 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|