Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions .claude/skills/metric-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,15 +302,27 @@ declarative `*.test.yaml` rig replaced the CSV rig.

The seeder INSERTs into a table that MUST already exist (it reads
`system.columns` and fails otherwise — it does NOT create from the schema YAML).
Bronze tables come from `src/ingestion/scripts/create-bronze-placeholders.sh`
(the rig parses the `run_ch <<'SQL' … SQL` heredocs out of it). So to seed a
connector that isn't there yet:

1. Add `CREATE DATABASE IF NOT EXISTS bronze_<snake>;` to the database heredoc.
2. Add a `CREATE TABLE IF NOT EXISTS bronze_<snake>.<stream> (…)` block (inside a
`run_ch <<'SQL' … SQL` heredoc) with the columns your dbt model reads + the 4
`_airbyte_*` CDK columns. Real Airbyte overwrites it on first sync.
3. Add a matching `schemas/bronze_<snake>.<stream>.yaml` (every column;
Bronze tables come from `src/ingestion/scripts/connectors-ddl/*.sql`
(the generated DDL snapshot the rig applies verbatim). That snapshot is NOT
hand-edited — it is regenerated from real connectors + dbt by bootstrap-db, so
adding a table by hand to a `connectors-ddl/*.sql` (or to a bootstrap heredoc)
does not survive the next regeneration. To seed a connector whose bronze tables
aren't in the snapshot yet:

1. Make sure the connector is listed in `bootstrap-db/connectors-config.yaml`,
then regenerate and commit the snapshot (see
`src/ingestion/scripts/bootstrap-db/README.md`):

```bash
cd src/ingestion/scripts/bootstrap-db
set -a; source pins.env; source .env; set +a
./bootstrap-db.sh connectors-config.yaml # fresh ClickHouse 25.7.5
./dump-ddl.sh # writes ../connectors-ddl/*.sql
```

Commit the resulting `connectors-ddl/*.sql` diff so the new
`bronze_<snake>.<stream>` tables ship in the snapshot the rig applies.
2. Add a matching `schemas/bronze_<snake>.<stream>.yaml` (every column;
`additionalProperties: false`) and a base template covering all of them.

## Gotchas (rig operations + cross-test impact)
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/connectors-ddl-reminder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Reminds contributors to regenerate the committed connectors-ddl snapshot when a
# PR touches src/ingestion. The snapshot (scripts/connectors-ddl/*.sql) is
# regenerated MANUALLY (see src/ingestion/scripts/bootstrap-db/), so this workflow
# is only a nudge — it does NOT run the regeneration and pushes nothing.
#
# Uses pull_request_target so the token can comment on fork PRs too (a plain
# pull_request from a fork gets a read-only token). This is safe here because the
# job never checks out or executes PR code and uses no secrets — it only reads the
# changed-file list (via the paths filter) and posts a single sticky comment.
#
# NB: pull_request_target always uses the workflow file from the base branch, so
# this takes effect only once merged to main.
name: connectors-ddl reminder

on:
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- "src/ingestion/**"

permissions:
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
remind:
name: Remind to regenerate connectors-ddl
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
// Sticky marker: post the reminder at most once per PR (not per push).
const marker = '<!-- connectors-ddl-regen-reminder -->';
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
const existing = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number, per_page: 100,
});
if (existing.some((c) => c.body && c.body.includes(marker))) return;
const body = [
marker,
'### ⚠️ Regenerate the connectors-ddl snapshot',
'',
'This PR changes `src/ingestion/**`. If your change affects any',
'bronze / silver / gold schema, regenerate the committed DDL snapshot',
'and include it in this PR:',
'',
'```bash',
'cd src/ingestion/scripts/bootstrap-db',
'set -a; source pins.env; source .env; set +a',
'./bootstrap-db.sh connectors-config.yaml # fresh ClickHouse 25.7.5',
'./dump-ddl.sh # writes scripts/connectors-ddl/*.sql',
'```',
'',
'Commit the resulting `scripts/connectors-ddl/*.sql` diff. If nothing',
'changed, no snapshot update is needed. (Regeneration is manual for now.)',
].join('\n');
await github.rest.issues.createComment({ owner, repo, issue_number, body });
6 changes: 6 additions & 0 deletions charts/insight/templates/ingestion/dbt-run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ spec:
"send_receive_timeout": 1500,
"query_limit": 0,
"connect_timeout": 30,
# Correlated subqueries (LEFT ANTI JOIN in the identity
# seed models) are gated behind this experimental flag
# on CH 25.7. A model-level config() setting does NOT
# reach the SELECT plan in dbt-clickhouse, so it must be
# set at profile level. Parity with test/bootstrap.
"settings": {"allow_experimental_correlated_subqueries": 1},
}
}
}
Expand Down
13 changes: 2 additions & 11 deletions deploy/compose/clickhouse-user-defaults.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,13 @@
Setting join_use_nulls=1 as the profile default means views are both
CREATEd and queried under the same regime.

allow_experimental_refreshable_materialized_view=1: the task-delivery
migrations (e.g. 20260429000000_task-delivery-silver-rewrite.sql) create
refreshable MATERIALIZED VIEWs. They enable the flag with a leading
`SET allow_experimental_refreshable_materialized_view = 1;`, but the
deploy migration runner (scripts/lib/ch-exec.sh `run_ch`) sends each
`;`-separated statement as a SEPARATE, stateless HTTP request, so the
SET does not persist to the later CREATE. Production ClickHouse allows
refreshable MVs at the server level; enabling it here as a profile
default gives the compose CH the same regime so the real deploy scripts
(apply-ch-migrations.sh) run unchanged.
(Refreshable materialized views used to need an experimental flag here;
they are GA on the pinned ClickHouse 25.x, so the flag is gone.)
-->
<clickhouse>
<profiles>
<default>
<join_use_nulls>1</join_use_nulls>
<allow_experimental_refreshable_materialized_view>1</allow_experimental_refreshable_materialized_view>
</default>
</profiles>
</clickhouse>
24 changes: 13 additions & 11 deletions deploy/seed/silver.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
k8s clickhouse-migrate Hook Job runs, from the ingestion tree bind-mounted
at /ingestion (docker-compose.yml `seed-sample.volumes`):

1. `create-bronze-placeholders.sh` — CREATE DATABASE + bronze/silver
placeholder tables (CREATE TABLE IF NOT EXISTS; each silver placeholder
carries the INSIGHT_PLACEHOLDER_v1 marker). This gives the generators
real tables to write into.
1. `create-bronze-placeholders.sh` — applies the CI-generated DDL snapshot
from scripts/connectors-ddl/*.sql (CREATE DATABASE + every bronze/silver/
insight relation, all IF NOT EXISTS / OR REPLACE). This gives the
generators the real production schemas to write into.

2. Generate per-team activity rows via `generators/*.py` INTO those silver
tables. Volumes scale by team profile + persona; per-day caps live in
Expand Down Expand Up @@ -91,7 +91,10 @@ def _ch_client() -> clickhouse_connect.driver.client.Client:
# (deploy/compose/clickhouse-user-defaults.xml) so those CREATE VIEWs
# type-check server-side. This client only INSERTs silver rows.
return clickhouse_connect.get_client(
host=host, port=port, username=user, password=pwd,
host=host,
port=port,
username=user,
password=pwd,
)


Expand Down Expand Up @@ -137,15 +140,15 @@ def generate_rows(
client: clickhouse_connect.driver.client.Client,
) -> None:
"""Populate silver tables with per-team activity for the demo roster."""
tenant_uuid = os.environ.get(
"TENANT_DEFAULT_ID", "00000000-df51-5b42-9538-d2b56b7ee953"
)
tenant_uuid = os.environ.get("TENANT_DEFAULT_ID", "00000000-df51-5b42-9538-d2b56b7ee953")
dev_email = get_dev_user_email()
roster = build_roster(dev_email)
days = int(os.environ.get("SEED_DAYS", DEFAULT_DAYS))
LOG.info(
"generating silver rows: tenant=%s days=%d persons=%d",
tenant_uuid, days, len(roster),
tenant_uuid,
days,
len(roster),
)

totals: dict[str, int] = {}
Expand All @@ -160,8 +163,7 @@ def generate_rows(

for table, n in sorted(totals.items()):
LOG.info(" %-46s %6d rows", table, n)
LOG.info("silver rows: %d total across %d tables",
sum(totals.values()), len(totals))
LOG.info("silver rows: %d total across %d tables", sum(totals.values()), len(totals))


def run() -> None:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ services:
# ClickHouse, point CLICKHOUSE_HOST / CLICKHOUSE_INTERNAL_HTTP_PORT at
# the real host and leave this profile inactive.
profiles: ["local-clickhouse"]
image: clickhouse/clickhouse-server:24.8
image: clickhouse/clickhouse-server:25.7.5
container_name: insight-clickhouse
networks: [insight]
environment:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
---
id: cpt-ingestion-adr-fresh-cluster-placeholders
status: accepted
status: superseded
date: 2026-05-05
---

# ADR-0007 — Fresh-cluster placeholders for silver / bronze tables

> **Superseded (2026-07, issue #1831).** Hand-written minimum-viable
> placeholders are gone. `scripts/create-bronze-placeholders.sh` now applies
> `scripts/connectors-ddl/*.sql` — a CI-generated `SHOW CREATE` snapshot of
> every relation the full bootstrap-db pipeline (real connector `discover`,
> real destination-clickhouse, real dbt models) produces on a throwaway
> ClickHouse (see `.github/workflows/connectors-ddl.yml` and
> `scripts/bootstrap-db/`). Schemas can no longer drift from the connectors:
> the snapshot is regenerated whenever connector/dbt sources change. The
> "extend the placeholder list by hand" rule below and the
> drop-before-first-sync caveat no longer apply; the historical context and
> the marker/drop mechanism for pre-snapshot clusters remain documented
> below.

## Context

ClickHouse-side gold-view migrations in
Expand Down
4 changes: 2 additions & 2 deletions docs/domain/ingestion/specs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,8 @@ Key deployment decisions:
- Airbyte port-forward uses `nohup ... & disown` to avoid blocking the terminal.
- Argo `dbt-run` WorkflowTemplate uses locally-built `insight-toolbox:local` image (with `imagePullPolicy: IfNotPresent`) — not `ghcr.io/constructorfabric/insight-toolbox:latest`. Local builds via `tools/toolbox/build.sh` pick up dbt model changes without requiring a registry push. Template also accepts `full_refresh` parameter (pass `--full-refresh` to recreate tables from scratch).
- CoreDNS is patched to use public DNS upstream (`8.8.8.8`, `8.8.4.4`) — WSL's `/etc/resolv.conf` points to an internal WSL nameserver that cannot reliably resolve external domains (e.g. `login.microsoftonline.com`). Patch lives in `scripts/dev/patch-coredns-wsl.sh` (idempotent, opt-out via `SKIP_COREDNS_PATCH=1`); Windows/WSL+Kind operators run it manually against their cluster after bootstrap.
- Gold views migration (`20260422000000_gold-views.sql`) references bronze tables from optional connectors (jira, m365, zoom). When the corresponding bronze table does not yet exist (no real connector data ingested yet), `scripts/create-bronze-placeholders.sh` creates empty placeholder tables with a minimal compatible schema so the gold-views migration succeeds on a partial install.
- **Placeholder handoff caveat**: Airbyte ClickHouse destination v2.0.8+ throws an error on the first sync if the target bronze table exists with a schema that does not match the destination's expected schema for that stream. The placeholder schemas in `create-bronze-placeholders.sh` are intentionally minimal (only the columns referenced by gold views) — they are **not** a drop-in replacement for a native Airbyte-generated table. Before enabling a previously-placeholdered connector, the operator should manually `DROP TABLE` the placeholder(s) in ClickHouse so Airbyte can create them fresh on its first sync.
- Gold views migrations reference bronze/silver tables that do not exist yet on a fresh cluster. `scripts/create-bronze-placeholders.sh` pre-creates every such relation by applying `scripts/connectors-ddl/*.sql` — a CI-generated `SHOW CREATE` snapshot of everything the full bootstrap-db pipeline (real connector `discover` → real destination-clickhouse write → dbt) produces (issue #1831, supersedes the hand-written ADR-0007 placeholders).
- The snapshot schemas are byte-identical to what a real Airbyte sync / dbt run would create, so there is no handoff caveat: the destination's `ensureSchemaMatches` accepts the pre-created bronze tables and dbt continues into the pre-created silver tables. The snapshot is regenerated by `.github/workflows/connectors-ddl.yml` whenever connector or dbt sources change in a PR.
- Runs automatically inside the `clickhouse-migrate` Helm Hook Job, immediately before the gold-view migrations (see §4.4.1).
- All credentials managed via Kubernetes Secrets (see §4.1.1).
- Service access via Ingress (PR #224): the umbrella chart configures `ingress-nginx` routes for Frontend, API Gateway, Airbyte UI and Argo UI; on a local Kubernetes cluster the Kind config maps host ports 80/443 to the ingress controller. Direct port-forwards (`kubectl port-forward`) remain available for debugging.
Expand Down
3 changes: 2 additions & 1 deletion src/ingestion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ src/ingestion/
├── scripts/ # ClickHouse migrations + in-toolbox helpers
│ ├── migrations/ # gold-view migrations (*.sql)
│ ├── apply-ch-migrations.sh # migration runner (clickhouse-migrate Hook Job)
│ ├── create-bronze-placeholders.sh # ADR-0007 fresh-cluster placeholders
│ ├── create-bronze-placeholders.sh # applies connectors-ddl/ DDL snapshot
│ ├── connectors-ddl/ # CI-generated SHOW CREATE snapshot (#1831)
│ ├── lib/ch-exec.sh # ClickHouse HTTP exec helpers
│ └── wait-for-services.sh # kubectl wait for pods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ SELECT
CAST(u.collected_at_max AS Nullable(DateTime64(3))) AS collected_at,
-- _version: aggregating model uses max(collected_at) as version proxy (epoch-ms).
-- NULL collected_at falls back to 0 to keep _version non-nullable.
coalesce(toUnixTimestamp64Milli(u.collected_at_max), toUInt64(0)) AS _version
coalesce(toUnixTimestamp64Milli(u.collected_at_max), toInt64(0)) AS _version
FROM usage_agg u
LEFT JOIN api_keys k
ON u.actor_type = 'api_actor'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ SELECT
num_model_requests,
coalesce(batch, false) AS is_batch,
service_tier,
NULL AS person_id,
CAST(NULL, 'Nullable(String)') AS person_id,
'openai' AS provider,
'openai_api' AS client,
'insight_openai' AS data_source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,22 @@ SELECT
lower(userPrincipalName),
'') AS person_key,
toDate(reportRefreshDate) AS date,
privateChatMessageCount AS direct_messages,
toInt64(privateChatMessageCount) AS direct_messages,
-- #431: teamChatMessageCount is channel-post activity (not group DMs)
-- per Microsoft Graph docs. Group-chat counts are not surfaced by this
-- report endpoint. Emit NULL rather than the mislabeled channel count.
CAST(NULL AS Nullable(Int64)) AS group_chat_messages,
-- #266: for m365, only the DM portion of "direct + group" is available.
-- Group chats unsurfaced — see header.
privateChatMessageCount AS direct_and_group_messages,
toInt64(privateChatMessageCount) AS direct_and_group_messages,
-- total_chat_messages retains the existing semantics
-- (DMs + team-channel messages) so existing Gold consumers do not see a
-- discontinuity. This is "user engagement across DM + channel surfaces",
-- not "DM + group DM". Documented in silver schema.
COALESCE(privateChatMessageCount, 0) + COALESCE(teamChatMessageCount, 0) AS total_chat_messages,
postMessages AS channel_posts,
replyMessages AS channel_replies,
urgentMessages AS urgent_messages,
toInt64(COALESCE(privateChatMessageCount, 0) + COALESCE(teamChatMessageCount, 0)) AS total_chat_messages,
toInt64(postMessages) AS channel_posts,
toInt64(replyMessages) AS channel_replies,
toInt64(urgentMessages) AS urgent_messages,
reportPeriod AS report_period,
now() AS collected_at,
'insight_m365' AS data_source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ WITH src AS (
-- Real pipeline semantics are derived at Silver from StageName.
ForecastCategory AS forecast_category,
StageName AS stage,
Amount AS amount,
toFloat64(Amount) AS amount,
-- SF's `Amount` is in record-currency. Single-currency orgs treat it
-- as home; multi-currency orgs surface ConvertedAmount but we don't
-- assume that mode here. Aliasing keeps gold per-rep aggregates
-- comparable across connectors; tenants on multi-currency setups can
-- swap this for ConvertedAmount at silver. `acv/tcv/arr` have no
-- native SF equivalent (HubSpot computes them from line items).
Amount AS amount_home,
toFloat64(Amount) AS amount_home,
CAST(NULL AS Nullable(Float64)) AS acv,
CAST(NULL AS Nullable(Float64)) AS tcv,
CAST(NULL AS Nullable(Float64)) AS arr,
Expand All @@ -41,7 +41,7 @@ WITH src AS (
toInt64(IsClosed = true) AS is_closed,
toInt64(IsWon = true) AS is_won,
LeadSource AS lead_source,
Probability AS probability,
toFloat64(Probability) AS probability,
Type AS deal_type,
-- SF has no built-in "closed lost reason" — orgs use a custom field
-- (e.g. LossReason__c) that varies per tenant. Expose NULL; tenants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ SELECT
unique_key,
COALESCE(repo_owner, '') AS project_key,
COALESCE(repo_name, '') AS repo_slug,
COALESCE(oid, '') AS commit_hash,
COALESCE(sha, '') AS commit_hash,
COALESCE(branch_name, '') AS branch,
COALESCE(author_name, '') AS author_name,
COALESCE(author_email, '') AS author_email,
Expand All @@ -24,7 +24,11 @@ SELECT
COALESCE(changed_files, 0) AS files_changed,
COALESCE(additions, 0) AS lines_added,
COALESCE(deletions, 0) AS lines_removed,
if(length(parent_hashes) > 1, 1, 0) AS is_merge_commit,
-- parent_hashes arrives as a JSON-array string (Airbyte serializes the
-- connector's array field into the Nullable(String) bronze column), so
-- count elements with JSONLength — plain length() would count characters
-- and flag every commit with a parent (e.g. `["sha"]`) as a merge.
if(JSONLength(COALESCE(parent_hashes, '')) > 1, 1, 0) AS is_merge_commit,
'insight_github' AS data_source,
toUnixTimestamp64Milli(now64()) AS _version,
_airbyte_extracted_at
Expand Down
Loading
Loading