Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

4.8 KiB

Materialization analysis

Finding properties worth materializing (to stop JSONExtract scans) and materialized columns worth dropping (to reclaim disk). Always run on both US and EU, since they have different materialized columns and workloads, so the candidate lists differ. Write the result to the private Insights/query-performance-analysis repo as analysis/<date>-materialization-candidates.md.

Run everything via query-datastore-via-metabase. Split wide column-list checks into batches of ~5-6 to stay under the Metabase response cutoff.

Step 1: Candidates to materialize

The candidate list is the JSON-extracted property breakdown from query-patterns.md §7: which property names slow queries pull out of a JSON blob, split by column (event properties, person_properties, or group_properties), with the teams driving each. Run that query (widen the window to ~30 days for materialization, since a property is worth materializing if it is hit consistently). Rank by slow_queries and teams; a property hit by many slow queries across several teams is the strongest candidate. The column tells you which blob it lives on, which the dagster job needs (table, table_column, property).

To recover the table + column for a specific candidate:

SELECT
    arrayDistinct(extractAll(query, '(\\w+)\\.\\w*properties,\\s*''<PROPERTY>''')) AS table_aliases,
    arrayDistinct(extractAll(query, '\\w+\\.(\\w*properties),\\s*''<PROPERTY>''')) AS columns
FROM insights.query_log_archive
WHERE event_time > now() - INTERVAL 7 DAY AND query LIKE '%<PROPERTY>%' LIMIT 100

Step 2: What is already materialized

SHOW CREATE TABLE sharded_events

Cross-reference against Step 1 to split "needs materializing" from "bypassing an existing mat column". A bypass (mat column exists but queries still JSONExtract) usually means the property is accessed as JSONExtractString(properties, '$foo') in user-written InsightsQL, which creates an ast.Call that skips visit_property_type(), rather than properties.$foo which materializes. Known sources: InsightsQL / DataVisualization nodes and some survey SQL.

Step 3: Drop candidates

A materialized column is only safe to drop when it has zero queries over 30 days and zero data. Usage must be checked across all queries, not just slow ones.

-- usage: does any query reference the column? (targeted countIf per column to avoid timeouts)
SELECT countIf(query LIKE '%mat_<col>%') AS uses
FROM insights.query_log_archive
WHERE event_time > now() - INTERVAL 30 DAY AND is_initial_query
-- disk size
SELECT name, formatReadableSize(sum(data_compressed_bytes)) AS compressed
FROM clusterAllReplicas(insights, system, columns)
WHERE table = 'sharded_events' AND name LIKE 'mat_%'
GROUP BY name ORDER BY sum(data_compressed_bytes) DESC
-- data presence before dropping (batch the column list)
SELECT
    countIf(mat_<col> != '') AS nonempty_<col>,
    uniqExactIf(team_id, mat_<col> != '') AS teams_<col>
FROM events
WHERE timestamp > now() - INTERVAL 7 DAY

Read data presence from the Distributed events table, not clusterAllReplicas(...) over the local sharded_events. The Distributed table fans out to one replica per shard, so each row is counted once; clusterAllReplicas hits every replica and multiplies the counts by the replication factor. Use clusterAllReplicas only for non-Distributed system tables (as the disk-size query above does with system.columns).

  • Zero queries + zero data → safe to drop.
  • Zero queries + active data → risky. Dropping does not lose data (it stays in the JSON blob) but re-materializing later needs an expensive backfill. Record as an optional drop with size + team info.

Step 4: Dagster jobs

These jobs must be run by a human operator with dagster access; you cannot run them yourself. Before running anything, the operator should sanity-check with #team-datastore that the cluster is not in a bad state (backfills and materializations add load, so they must not land while the cluster is already struggling). Hand the operator the configs below as a recommendation rather than executing them.

  • Create: create_materialized_column (team-datastore location, per region). Backfilling adds cluster load, so schedule for a weekend.
  • Drop: drop_materialized_column (defaults to dry_run: true; minimal impact).

Report format

Start with four recommendation tables, then dagster configs, then the investigation detail.

New materializations:

| Table | Column | Property | Slow queries (30d) | Teams | Timeouts | Avg read | Max read | Est. column size |

Drop candidates:

| Column | Compressed | Non-empty events | Teams | Safe to drop? |

Put US-new, EU-new, US-drop, EU-drop as separate tables. Optional drops (no queries but active data) go into the same dagster config as commented-out entries with their size and team info.