Skip to content

feat: only use pk + block ids to resolve a row - #2320

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
aaron/select-only-pks-in-where
May 21, 2026
Merged

kodiakhq[bot] merged 3 commits into
mainfrom
aaron/select-only-pks-in-where

Conversation

@knudtty

@knudtty knudtty commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Now that we use block numbers and offsets to enforce row uniqueness, we can just use that + a PK to identify a row. There is no need to specify Body or SeverityText and waste i/o and processing time.

Testing Locally

Steps:

  1. yarn dev
  2. Ensure your log source table contains the settings enable_block_number_column = 1, enable_block_offset_column = 1
  3. Select a row
  4. Verify the query that fired only has the PK and the block number & offset in the WHERE

References

  • Linear Issue: Closes HDX-4287

@changeset-bot

changeset-bot Bot commented May 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d71f347

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/app Patch
@hyperdx/cli Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 21, 2026 3:13pm

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label May 20, 2026
@github-actions

github-actions Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 4
  • Production lines changed: 77 (+ 264 in test files, excluded from tier calculation)
  • Branch: aaron/select-only-pks-in-where
  • Author: knudtty

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@knudtty
knudtty force-pushed the aaron/select-only-pks-in-where branch from e9e0e51 to a4f60f3 Compare May 20, 2026 21:59
@github-actions

github-actions Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 178 passed • 3 skipped • 1185s

Status Count
✅ Passed 178
❌ Failed 0
⚠️ Flaky 4
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

🔴 P0/P1 -- must fix

  • packages/app/src/components/DBRowTable.tsx:1444 -- On a Distributed source backed by ReplicatedMergeTree, TableMetadata.engine_full mirrors the local table per packages/common-utils/src/core/metadata.ts:96-108, so hasBlockColumns opens the optimization even though _block_number/_block_offset are per-shard and not globally unique; pre-PR, including Body/attributes in the WHERE accidentally disambiguated across shards, so the lookup can now silently land on a row from a different shard than the one the user clicked.
    • Fix: Detect Distributed engines via tableMetadata.engine and either disable rowKeyColumns or include a shard discriminator (_shard_num) in extraKeys and the row WHERE for those sources.
    • adversarial

🟡 P2 -- recommended

  • packages/app/src/hooks/useRowWhere.tsx:181 -- When primaryKeyColumns is provided and the row keys do not intersect it, filteredRow becomes {}, processRowToWhereClause returns '', the where: rowId ?? '0=1' guard in packages/app/src/components/DBRowDataPanel.tsx:148 doesn't catch empty string (only null/undefined), and isNonEmptyWhereExpr('') in packages/common-utils/src/core/renderChartConfig.ts then drops the WHERE entirely, so the side panel renders an arbitrary LIMIT 1 row; the CLI variant in packages/cli/src/api/eventQuery.ts:371 has no 0=1 fallback at all.
    • Fix: Either throw / return '0=1' from useRowWhere/getRowWhere when filteredRow is empty, or replace rowId ?? '0=1' with an explicit isNonEmptyWhereExpr check in both DBRowDataPanel.tsx and eventQuery.ts.
    • correctness, testing, adversarial, kieran-typescript
  • packages/cli/src/components/EventViewer/useEventData.ts:263 -- The CLI's only caller of buildFullRowQuery never sets primaryKeyColumns, so the parameter plumbed through packages/cli/src/api/eventQuery.ts:341 and packages/cli/src/shared/useRowWhere.ts:180 is dead and the CLI/agent surface still issues the wide WHERE clause the PR set out to eliminate.
    • Fix: Compute rowKeyColumns in the CLI (resolve primary_key/partition_key/engine_full via Metadata, derive extraKeys exactly as useConfigWithAdditionalSelect does, also append those columns through an equivalent of appendSelectWithAdditionalKeys in buildEventSearchQuery), then thread the set into buildFullRowQuery.
    • correctness, testing, performance, agent-native
  • packages/app/src/components/DBRowTable.tsx:1472 -- rowKeyColumns entries from splitAndTrimWithBracket(partitionKey/primaryKey) (e.g. toStartOfFiveMinutes(Timestamp), json.a.b.:Int64) are matched character-exactly against ClickHouse result column names, yet the same file at lines 1640-1644 already warns that "CH may rewrite column names in the result set (e.g. expression formatting)", so PK-expression entries can silently fail to match and the WHERE collapses to the block columns alone.
    • Fix: Build rowKeyColumns from the same source that produces the SELECT list (so column-name identity is preserved by construction), or alias the appended raw expressions so the result column name is deterministic.
    • correctness, adversarial
  • packages/app/src/hooks/__tests__/useRowWhere.test.tsx:611 -- New "otel_logs"/"otel_traces" tests rely on .toContain('Timestamp') / .not.toContain('Body') substring checks against the full WHERE string, which would pass even if a regression dropped every PK predicate except one, or if a column name appeared inside a value literal.
    • Fix: Replace substring assertions with toBe/toEqual against the full expected WHERE string, or assert the count and LHS of AND-joined predicates structurally.
    • testing
  • packages/cli/src/shared/useRowWhere.ts:176 -- getRowWhere and buildFullRowQuery have no test coverage in the CLI package; the new primaryKeyColumns parameter is wired through both files but no test guards the parameter plumbing, the mirror semantics versus the web hook, or future drift.
    • Fix: Add at minimum one test file under packages/cli/src/shared/__tests__/ that exercises getRowWhere with and without primaryKeyColumns, paralleling the web suite.
    • testing, project-standards
  • packages/app/src/components/DBRowTable.tsx:1472 -- rowKeyColumns is recomputed verbatim from splitAndTrimWithBracket(partitionKey), splitAndTrimWithBracket(primaryKey), extractColumnReferencesFromKey(partitionKey/primaryKey), and extraKeys, which is the exact union appendSelectWithAdditionalKeys already computes as allKeys at line 1376; the two will drift the next time the membership rules evolve.
    • Fix: Have appendSelectWithAdditionalKeys also return the union set (e.g. { select, additionalKeysLength, keyColumns }), or extract a single buildRowKeyColumns(partitionKey, primaryKey, extraKeys) helper used by both sites.
    • maintainability
  • packages/app/src/hooks/useRowWhere.tsx:135 -- The parameter is named primaryKeyColumns but the set actually contains primary-key columns, partition-key columns, raw PK/partition expressions, and _block_number/_block_offset; the misnomer is reinforced by the config-side name rowKeyColumns, which is more honest.
    • Fix: Rename the hook/function parameter (and the CLI mirror) to rowKeyColumns so both the construction site and the consumers share one accurate name.
    • maintainability
🔵 P3 nitpicks (10)
  • packages/app/src/components/DBRowTable.tsx:1468 -- The hasBlockColumns gate bypasses the optimization entirely for older __hdx_id-only schemas even though __hdx_id is itself row-unique.
    • Fix: Build rowKeyColumns for the __hdx_id branch too if you intend to accelerate those schemas; otherwise leave a comment recording the deliberate scope.
  • packages/app/src/components/DBRowTable.tsx:1472 -- rowKeyColumns is allocated as a fresh Set on every memo run, which flips useRowWhere's useCallback (deps include primaryKeyColumns), which in turn invalidates dedupedRows in RawLogTable for every parent re-render that touches config identity.
    • Fix: Memoize rowKeyColumns on primitive deps [primaryKey, partitionKey, hasBlockColumns, extraKeys.join(',')] rather than the full config object.
  • packages/app/src/components/DBRowTable.tsx:1482 -- { ...config, select, additionalKeysLength, rowKeyColumns } exposes rowKeyColumns only via inferred return type, so any future typed boundary that re-narrows to BuilderChartConfigWithDateRange will silently strip the field and degrade to the full-scan path with no compile signal.
    • Fix: Declare an explicit MergedRowTableConfig interface extending the base config with select, additionalKeysLength, and rowKeyColumns, and use it for useConfigWithAdditionalSelect's return.
  • packages/app/src/hooks/useRowWhere.tsx:135 -- Accepting Set<string> directly forces every caller to memoize the set themselves or silently lose useCallback stability.
    • Fix: Accept readonly string[] (or a sorted comma-joined key) and build/memoize the Set inside the hook.
  • packages/cli/src/shared/useRowWhere.ts:176 -- The CLI mirror takes primaryKeyColumns as the 4th positional argument while the web hook takes it as a named field on the options object, leaving an unflagged stylistic drift between the two @source-linked copies.
    • Fix: Either match the named-arg shape now or document the intentional divergence in the @source header for the eventual consolidation into @hyperdx/common-utils.
  • packages/app/src/components/DBRowTable.tsx:1393 -- The array branch of appendSelectWithAdditionalKeys skips the dedup-against-existing-select that the string branch performs, so the larger allKeys set now produced for block-column tables can emit duplicate { valueExpression } entries.
    • Fix: Apply the same selectColumns.has(k) filter to the array branch before mapping to { valueExpression }.
  • packages/app/src/components/DBRowTable.tsx:1376 -- A source whose PK or partition key contains an expensive expression (heavy hash, UDF, JSON extraction) now pays that cost per row in the list-render query because the raw expression is appended to the SELECT alongside the bare column references.
    • Fix: Skip appending a raw expression whose extracted column references are already in the SELECT, or add the raw expression only when no extracted refs cover the partitioning.
  • packages/app/src/components/DBRowTable.tsx:1448 -- No unit test covers the new hasBlockColumns branch in useConfigWithAdditionalSelect; a regression that always sets or never sets rowKeyColumns would be invisible.
    • Fix: Add tests that mount useConfigWithAdditionalSelect with engine_full strings that do and do not include both enable_block_*_column = 1 settings.
  • packages/app/src/hooks/__tests__/useRowWhere.test.tsx:611 -- No test covers an aliased PK column in the SELECT (Timestamp AS ts) paired with primaryKeyColumns: Set(['Timestamp']), which is the natural way the filter under-constrains the WHERE.
    • Fix: Add a test that produces such a meta/row pair and asserts the WHERE is not silently empty.
  • packages/app/src/hooks/useRowWhere.tsx:181 -- Object.fromEntries(Object.entries(dbRow).filter(...)) returns { [k: string]: any }, widening the typed rest-spread; the CLI mirror uses Record<string, unknown> end-to-end and is the better template.
    • Fix: Track toward Record<string, unknown> so this branch stops being an any ingress.

Reviewers (10): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, kieran-typescript, performance, api-contract, adversarial.

Testing gaps:

  • No CLI test exercises getRowWhere/buildFullRowQuery with or without primaryKeyColumns.
  • No test covers filteredRow becoming empty after the Set intersection (the empty-WHERE cascade).
  • No test covers useConfigWithAdditionalSelect's hasBlockColumns branch directly.
  • No integration test asserts the Set entries built JS-side actually byte-match ClickHouse result column names for the otel schemas.

@knudtty
knudtty requested review from a team and fleon and removed request for a team May 20, 2026 23:37
@kodiakhq
kodiakhq Bot merged commit 19cd7c9 into main May 21, 2026
18 checks passed
@kodiakhq
kodiakhq Bot deleted the aaron/select-only-pks-in-where branch May 21, 2026 15:19
kodiakhq Bot pushed a commit that referenced this pull request Oct 13, 2026
…ble persists (#3364)

## Summary

Opening log details on large text-log tables is much slower than the search itself (HDX-5696). The side panel already has a fast lookup that filters only on primary key + partition + block columns, but it was gated on the table persisting **both** `_block_number` and `_block_offset`. The affected table only sets `enable_block_offset_column = 1`, so every click fell back to the full-row WHERE, which filters on every selected column (including `Body`) and triggers expensive index loading.

This PR relaxes the gate: the fast path is taken when the table persists **either** block column, and the WHERE uses whichever of `_block_number` / `_block_offset` is available. Combined with the primary key, either one is unique enough in practice: two rows would need identical key values (including a nanosecond timestamp) *and* the same insert block or the same offset within their blocks. Tables that persist both columns keep the current behavior; tables with neither still fall back to `__hdx_id` / the full row.

Changes:

- `getPersistedBlockColumns(engineFull)` in `DBRowTable.tsx` decides which block columns to inject; `useConfigWithAdditionalSelect` uses it for both the extra SELECT keys and `rowKeyColumns`.
- Unit tests for the helper and `renderHook` coverage of `useConfigWithAdditionalSelect` (offset only, number only, both, `__hdx_id` fallback, no `sourceId`).
- Changeset for `@hyperdx/app`.

Known trade-off: rows with identical PK that also share the block number (or the offset) still collide and `LIMIT 1` picks one; the thread on the Linear issue accepted that given nanosecond timestamps.

Verified against ClickHouse 26.8 with tables that set only one of the two settings: `engine_full` exposes the setting as matched here, the persisted column survives `OPTIMIZE FINAL` unchanged, and the WHERE shape HyperDX now generates (PK expressions + the block column, no `Body`) returns the clicked row.

### How to test on Vercel preview

N/A — the demo tables persist both block columns, so the fast path was already taken there; this change only affects tables that persist one of them. To verify locally, create a table with `SETTINGS enable_block_offset_column = 1` (or `enable_block_number_column = 1`) alone, add it as a log source, click a row and check the side-panel query filters on PK columns + that block column only.

### References

- Linear Issue: [HDX-5696](https://linear.app/clickhouse/issue/HDX-5696/speed-up-log-details-lookup-using-primary-key-and-block-offset)
- Related PRs: #2071 (introduced block-column detection), #2320 (restricted the row WHERE to PK + block ids)



Linear Issue: [HDX-5696](https://linear.app/clickhouse/issue/HDX-5696/speed-up-log-details-lookup-using-primary-key-and-block-offset)

<div><a href="https://cursor.com/agents/bc-5fdae508-3c5f-4b0b-a60e-03f5e5eed062?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-5fdae508-3c5f-4b0b-a60e-03f5e5eed062&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>



Co-authored-by: Cursor Agent <199161495+cursoragent@users.noreply.github.com>
kodiakhq Bot pushed a commit that referenced this pull request Nov 5, 2026
…kup (#3566)

## Summary

Clicking a search result could open the wrong row's details when the table persists only one of `_block_number` / `_block_offset`.

- Since #3364 the side panel used a key-only lookup (primary key + partition + whichever block column exists) when at least one block column was persisted. One block column is not unique: `_block_offset` repeats across insert blocks, `_block_number` across the rows of one block, and the primary key often doesn't break the tie (the OTel exporter default keys on the second-precision `TimestampTime`).
- Since #3530 the block columns are also detected from server-level `merge_tree_settings`, so on servers that enable only one of them, every table without explicit settings takes this path.
- The key-only lookup now requires both block columns. A lone block column is still appended to the full-row lookup as a tiebreaker.
- Tables that persist only one block column return to the full-row lookup and lose the #3364 speed-up. A `_part` + `_part_offset` lookup is the follow-up to restore it.

### How to reproduce

**Starting point:** `yarn dev`. Set the server default on the dev ClickHouse and restart it:

```sh
docker exec <ch-server> sh -c 'echo "<clickhouse><merge_tree><enable_block_offset_column>1</enable_block_offset_column></merge_tree></clickhouse>" > /etc/clickhouse-server/config.d/block_offset.xml'
docker restart <ch-server>
```

Create an exporter-shaped table and insert three rows as separate inserts, two in the same second:

```sql
CREATE TABLE default.repro_logs (
  Timestamp DateTime64(9), TimestampTime DateTime DEFAULT toDateTime(Timestamp),
  ServiceName LowCardinality(String), SeverityText LowCardinality(String), Body String,
  LogAttributes Map(LowCardinality(String), String)
) ENGINE = MergeTree PARTITION BY toDate(TimestampTime)
PRIMARY KEY (ServiceName, TimestampTime) ORDER BY (ServiceName, TimestampTime, Timestamp);

INSERT INTO default.repro_logs (Timestamp, ServiceName, SeverityText, Body) VALUES ('2026-11-05 09:00:39.845', 'saruman', 'INFO', 'Response x_span_id=ESLMARS7L7');
INSERT INTO default.repro_logs (Timestamp, ServiceName, SeverityText, Body) VALUES ('2026-11-05 09:00:39.100', 'saruman', 'INFO', 'Request x_span_id=ESOPOZ7FCV');
INSERT INTO default.repro_logs (Timestamp, ServiceName, SeverityText, Body) VALUES ('2026-11-05 09:00:36.909', 'saruman', 'INFO', 'Request x_span_id=ESLMARS7L7');
```

Add a log source for `default.repro_logs` (timestamp column `TimestampTime`), then search it for `"ESLMARS7L7"` over a range covering 2026-11-05 09:00 UTC.

**On `main`:**

1. Click the `Response x_span_id=ESLMARS7L7` row a few times.
2. The side panel shows `Request x_span_id=ESOPOZ7FCV` about half the time. The query in `system.query_log` is `WHERE ServiceName='saruman' AND TimestampTime=... AND toDate(TimestampTime)=... AND _block_offset='0' LIMIT 1`.

**On this branch:**

1. Click the same row. The panel always shows `Response x_span_id=ESLMARS7L7`; the query also filters on `Timestamp`, `SeverityText` and `Body`.

### How to test on Vercel preview

N/A — needs a ClickHouse server default; the demo server's user cannot read `system.merge_tree_settings`.

### References

- Related PRs: #3530, #3364, #2320, #2071

This branch was successfully deployed

1 active deployment
Preview — d71f3471 Deployed May 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants