Skip to content
Closed
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
482 changes: 86 additions & 396 deletions apps/framework-docs-v2/content/guides/performant-dashboards.mdx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
##### Checkpoint 1 — Capture test cases

Goal: capture 2–5 replayable test cases from the **existing** OLTP endpoint. These are your parity oracle for all later phases.

Steps:

1. Identify 2–5 representative requests that exercise different filters, time ranges, and edge cases (e.g. empty results, large result sets, boundary dates).
2. Run each request against the **live OLTP endpoint** and capture the exact response.
3. For each test case, create a file: `moosestack/context/dashboard-migration/<component>/test-cases/0N-<short-name>.md`.
4. Each file must include:
- the `curl` command (GET or POST)
- the **verbatim** JSON response — this must come from actually calling the running endpoint, not from approximation
5. Choose requests that cover a reasonable recent time window (e.g. last 7–30 days). You will seed your local ClickHouse to match this window after identifying the source tables in Checkpoint 2.

Use this template (keep only the request method that matches your endpoint):

````md
# Test case: <short-name>

## Request (curl)

```bash
# Method: GET|POST
# Path: /api/<endpoint>
# Expected: HTTP 200
# Auth: Bearer token via $API_TOKEN (do not paste secrets)
# Notes: <timezone/order/pagination assumptions if relevant>

# Set once in your shell:
# export API_BASE_URL="http://localhost:4000"
# export API_TOKEN="..."

# GET (query params)
curl -sS -G "$API_BASE_URL/api/<endpoint>" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
--data-urlencode "param1=value1" \
--data-urlencode "param2=value2" \
| jq .

# POST (JSON body)
curl -sS -X POST "$API_BASE_URL/api/<endpoint>" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"param1": "value1", "param2": "value2"}' \
| jq .
```

## Expected response

```json
{
"REPLACE_ME": "paste the full JSON response body here (verbatim from the running endpoint)"
}
```
````

**Done when:** 2–5 test case files exist under `test-cases/`, each with a runnable curl command and the actual JSON response from the OLTP endpoint. Record the file paths in `context-map.md`.

##### Checkpoint 2 — OLTP semantics

Goal: extract the query logic from the existing endpoint so you have a precise spec to translate.

Steps:

1. Locate the SQL query or stored procedure that powers the existing endpoint (typically in the backend handler or a database layer).
2. Document:
- Source tables and join conditions (including join type)
- Filter clauses (including implicit tenancy, soft-delete, or RBAC filters)
- Parameter substitution rules and defaults
- Group-by / aggregation logic
- Edge cases (null handling, division by zero, missing rows)
3. Record these findings in `context-map.md` under Phase 1 Notes.

**Done when:** `context-map.md` lists every source table, the join graph, all filter/aggregation logic, and any edge cases. You should be able to write the ClickHouse translation from this spec alone.

##### Checkpoint 3 — Seed local ClickHouse

Goal: seed your local ClickHouse with enough production data to fully cover the test cases from Checkpoint 1. You now know the source tables (from Checkpoint 2) and the time windows and filter values (from Checkpoint 1).

Prerequisites:
- Dev server is running (`moose dev`)
- Production ClickHouse HTTPS connection string is exported:

```shell
export CLICKHOUSE_PROD_URL="<YOUR_HTTPS_CONNECTION_STRING>"
```

The connection string should start with `https://` (for example, `https://username:password@host:8443/database`).

Steps:

1. For each source table from Checkpoint 2, generate and run a seed command that covers the test case time windows:

```shell
moose seed clickhouse \
--connection-string "$CLICKHOUSE_PROD_URL" \
--table <TABLE_NAME> \
--order-by '<TIMESTAMP_COLUMN> DESC' \
--limit 100000
```

2. Verify the seeded data covers every test case:

```shell
moose query -q "SELECT min(<TIMESTAMP_COLUMN>), max(<TIMESTAMP_COLUMN>), count() FROM <TABLE_NAME> FINAL"
```

If any test case falls outside the seeded range, re-seed with a larger `--limit` or use `--all`.

**Done when:** every source table has local data that spans the time windows used in all Checkpoint 1 test cases.

##### Checkpoint 4 — ClickHouse parity function

Goal: write a direct translation of the OLTP query into ClickHouse SQL that returns the exact same response shape.

Steps:

1. Create `moosestack/app/queries/<component>-olap-translation.ts`.
2. Write a function that:
- accepts the exact same parameters as the existing handler/query logic
- builds the translated query using the `sql` tagged template literal
- executes via the Moose ClickHouse client against local `moose dev`
3. Preserve column names, types, and ordering exactly. Add explicit `ORDER BY` and casts where needed.
4. Call out any ClickHouse-specific differences (null handling, decimal precision, timestamp bucketing) as comments in the code.
5. Record the file path in `context-map.md`.

Refer to the [ClickHouse SQL reference](https://clickhouse.com/docs/en/sql-reference) as you translate.

Example (adapt to your component — do not copy names literally):

```ts
// File: moosestack/app/queries/<component>-olap-translation.ts

import { sql, MooseClient } from "@514labs/moose-lib";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if MooseClient is exported from moose-lib
rg -n "export.*MooseClient" --type ts -g "*.ts"

Repository: 514-labs/moosestack

Length of output: 504


🏁 Script executed:

# Find the example file mentioned
fd "order-fulfillment-handler-olap-translation.ts" --type f

Repository: 514-labs/moosestack

Length of output: 168


🏁 Script executed:

# Check the documentation file to understand context
head -150 "apps/framework-docs-v2/content/shared/guides/performant-dashboards/phase-1-checkpoints.mdx" | tail -30

Repository: 514-labs/moosestack

Length of output: 1108


🏁 Script executed:

# Examine the actual example file to see what it imports
cat -n "examples/dashboard-migration/example-order-fulfillment/code/order-fulfillment-handler-olap-translation.ts" | head -30

Repository: 514-labs/moosestack

Length of output: 1066


Update documentation example to match actual code: remove MooseClient import and define locally; fix package name.

The documentation imports MooseClient from "@514labs/moose-lib" and uses "@514labs/moose-lib", but the actual example file defines MooseClient as a local interface and imports sql from "@514-labs/moose-lib" (with hyphen). Align the documentation example with the actual example file pattern.

🤖 Prompt for AI Agents
In
`@apps/framework-docs-v2/content/shared/guides/performant-dashboards/phase-1-checkpoints.mdx`
at line 135, Update the import line to match the real example: remove the
MooseClient named import and change the package name to "@514-labs/moose-lib" so
only sql is imported (e.g., import { sql } from "@514-labs/moose-lib";), and
add/keep a local MooseClient interface definition in this MDX file (referencing
the MooseClient identifier) rather than importing it from the package; ensure
any usage of MooseClient in the example refers to the locally defined interface.

import { Orders } from "./models/Orders.model";

interface ParityInput {
merchantId: string;
startDate: string;
endDate: string;
}

interface ParityRow {
day: string;
fulfilled: number;
total: number;
}

export async function runParity(
params: ParityInput,
client: MooseClient,
): Promise<ParityRow[]> {
const statement = sql`
SELECT
toDate(order_ts) AS day,
sumIf(1, status = 'fulfilled') AS fulfilled,
count() AS total
FROM ${Orders}
WHERE merchant_id = ${params.merchantId}
AND order_ts >= toDateTime(${params.startDate})
AND order_ts < toDateTime(${params.endDate})
GROUP BY day
ORDER BY day ASC
`;

return client.query<ParityRow>(statement);
}
```

**Done when:** the parity function compiles, runs against local `moose dev`, and returns results. The file path is recorded in `context-map.md`.

##### Checkpoint 5 — Verification

Goal: prove the parity function returns the exact same JSON as the OLTP endpoint for every test case.

Steps:

1. For each Checkpoint 1 test case, call the parity function with the same parameters and capture the output:

```bash
pnpm tsx -e "
import { runParity } from './app/queries/<component>-olap-translation';
import { getMooseUtils } from '@514labs/moose-lib';
const { client } = await getMooseUtils();
const result = await runParity(
{ /* same params as the test case curl */ },
client,
);
console.log(JSON.stringify(result));
" | jq -S '.' > actual.json
```

2. Extract the expected response from the test case file:

```bash
awk 'f{print} /^```json/{f=1; next} /^```$/{if(f){exit}}' \
moosestack/context/dashboard-migration/<component>/test-cases/01-<short-name>.md \
| jq -S '.' > expected.json
```

3. Diff:

```bash
diff expected.json actual.json
```

4. If there are differences, check:
- Column names (case-sensitive in ClickHouse)
- Data types (timestamps, decimals, integers vs floats)
- Sort order (add explicit `ORDER BY` if needed)
- Missing rows (seed slice too small — re-seed with a larger `--limit`)

**Done when:** `diff` produces no output for every test case. Once all test cases pass, Phase 1 is complete — proceed to Phase 2.
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
##### Checkpoint 1 — Serving table design

Goal: define the serving table's **grain**, **columns**, and **orderByFields**. This table will be populated by the Materialized View and becomes the table your API reads from.

Steps:

1. **Output columns**: take the final `SELECT` list from the Phase 1 parity query function. Those columns become the serving table schema.
2. **Grain**: decide what uniquely identifies one row (e.g. `merchant_id` + `day`). This is the table's grain.
3. **orderByFields**: start with the grain columns, then append the most common filter/group-by fields from your access patterns.
4. Record the grain, columns, and `orderByFields` in `context-map.md`.

Example (adapt to your component — do not copy names literally):

If your parity query selects `merchant_id, merchant_name, day, total_orders, fulfilled_orders, fulfillment_rate`, then the serving table mirrors those columns and the `orderByFields` match the grain:

```ts
// File: moosestack/app/models/<ServingTable>-mv.ts

import { OlapTable, Int64 } from "@514labs/moose-lib";

interface ServingTableSchema {
merchant_id: string;
merchant_name: string;
day: Date;
total_orders: Int64;
fulfilled_orders: Int64;
fulfillment_rate: number;
}

export const ServingTable = new OlapTable<ServingTableSchema>("<ServingTable>", {
orderByFields: ["merchant_id", "day"],
});
```

**Done when:** `context-map.md` documents the grain, column list, and `orderByFields`. You have a clear mapping from every parity query output column to a serving table column.

##### Checkpoint 2 — Materialized View plan

Goal: decide how data flows into the serving table before writing code.

Rule of thumb:

- **Single MV** — 1 source table, no CTEs, straightforward aggregation
- **Staged pipeline** — multiple CTEs, 3+ joined tables, or cascading aggregations (create intermediate staging tables/MVs that feed the final serving table)

Everything not driven by request parameters belongs in write-time MVs. Only truly dynamic filters (from the API request) stay in the final read query.

Steps:

1. Decide single MV vs staged pipeline using the heuristic above. If it is ambiguous, present both options with tradeoffs and ask.
2. List every source table model that must be imported and included in `selectTables`.
3. Sketch the `selectStatement` logic: which joins, aggregations, and transformations move from read-time to write-time.
4. Record the MV plan in `context-map.md`: chosen approach (single or staged), source table list, and a summary of the write-time logic.

**Done when:** `context-map.md` contains the MV plan — approach, source table list, and a description of the write-time logic. You should be able to implement the MV from this plan alone.

##### Checkpoint 3 — Implement the serving table + MV

Goal: write the MooseStack file that defines the serving table and the MV that populates it.

Steps:

1. Put the serving table and MV definition in the same file: `moosestack/app/models/<ServingTable>-mv.ts`.
2. Ensure column names and types align exactly between the MV `selectStatement` output and the serving table schema.
3. If implementing a staged pipeline, ensure intermediate tables/views also align.
4. Use the `sql` tagged template literal to build the `selectStatement`.
5. Import every source table from the paths listed in `context-map.md` and include them in `selectTables`.
6. Record the file path in `context-map.md`.

Example (adapt to your component — do not copy names literally):

```ts
// File: moosestack/app/models/<ServingTable>-mv.ts

import { Int64, MaterializedView, OlapTable, sql } from "@514labs/moose-lib";
import { SourceTableA } from "./SourceTableA.model";
import { SourceTableB } from "./SourceTableB.model";

// ... serving table interface and OlapTable definition from Checkpoint 1 ...

export const ServingMV = new MaterializedView<ServingTableSchema>({
selectStatement: sql`
SELECT
a.id,
b.name,
toDate(a.created_at) AS day,
count() AS total,
sumIf(1, a.status = 'complete') AS completed
FROM ${SourceTableA} a
JOIN ${SourceTableB} b ON b.id = a.ref_id
GROUP BY a.id, b.name, day
`,
targetTable: ServingTable,
materializedViewName: "<ServingTable>MV",
selectTables: [SourceTableA, SourceTableB],
});
```

**Done when:** the file compiles and is saved. The file path is recorded in `context-map.md`.

##### Checkpoint 4 — Verification

Goal: confirm the serving table and MV are created and populated correctly.

Steps:

1. Save your files and watch the `moose dev` logs for successful creation:

```
[INFO] Created table: <ServingTable>
[INFO] Created materialized view: <ServingTable>MV
```

If you see errors, check:
- Column names match between the `selectStatement` and the serving table schema
- Column types from `selectTables` align with the casts/expressions in the `selectStatement`
- Every source table in `selectTables` exists and is registered

2. Verify the serving table is populated. The `moose dev` server automatically runs the `selectStatement` when the MV is created — do not backfill manually.

```bash
# Row count
moose query -q "SELECT count() FROM <ServingTable> FINAL"

# Spot-check a slice that matches a Phase 1 test case
moose query -q "SELECT * FROM <ServingTable> FINAL WHERE <filter_from_test_case> LIMIT 5"
```

3. Confirm the serving table has data for every Phase 1 test case time window. If rows are missing, check the MV logic and source table data.

**Done when:** `moose dev` logs show no errors, the serving table has rows, and spot-checks against Phase 1 test case filters return data. Proceed to Phase 3.
Loading
Loading