Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changeset/tidy-pets-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperdx/api': patch
'@hyperdx/app': patch
---

fix: use block_number/block_offset to uniquely identify log rows
2 changes: 1 addition & 1 deletion docker/otel-collector/schema/seed/00002_otel_logs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ PARTITION BY toDate(TimestampTime)
PRIMARY KEY (ServiceName, TimestampTime)
ORDER BY (ServiceName, TimestampTime, Timestamp)
TTL TimestampTime + ${TABLES_TTL}
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1, enable_block_number_column = 1, enable_block_offset_column = 1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is great! Although, I wonder if the multiple replica would return different block numbers, though. Maybe something to think about later.


1 change: 0 additions & 1 deletion packages/api/src/models/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export const LogSource = Source.discriminator<ILogSource>(
traceIdExpression: String,
spanIdExpression: String,
implicitColumnExpression: String,
uniqueRowIdExpression: String,
/** @deprecated See LogSourceSchema in @hyperdx/common-utils/types.ts. */
tableFilterExpression: String,
highlightedTraceAttributeExpressions: {
Expand Down
58 changes: 43 additions & 15 deletions packages/app/src/components/DBRowTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
useRenderedSqlChartConfig,
} from '@/hooks/useChartConfig';
import { useCsvExport } from '@/hooks/useCsvExport';
import { useTableMetadata } from '@/hooks/useMetadata';
import { useColumns, useTableMetadata } from '@/hooks/useMetadata';
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
import { useGroupedPatterns } from '@/hooks/usePatterns';
import useRowWhere, {
Expand Down Expand Up @@ -1362,14 +1362,15 @@ export const RawLogTable = memo(
},
);

export function appendSelectWithPrimaryAndPartitionKey(
export function appendSelectWithAdditionalKeys(
select: SelectList,
primaryKeys: string,
partitionKey: string,
extraKeys: string[] = [],
): { select: SelectList; additionalKeysLength: number } {
const partitionKeyArr = extractColumnReferencesFromKey(partitionKey);
const primaryKeyArr = extractColumnReferencesFromKey(primaryKeys);
const allKeys = new Set([...partitionKeyArr, ...primaryKeyArr]);
const allKeys = new Set([...partitionKeyArr, ...primaryKeyArr, ...extraKeys]);
if (typeof select === 'string') {
const selectSplit = splitAndTrimWithBracket(select);
const selectColumns = new Set(selectSplit);
Expand All @@ -1395,33 +1396,60 @@ function getSelectLength(select: SelectList): number {
}
}

export function useConfigWithPrimaryAndPartitionKey(
export function useConfigWithAdditionalSelect(
config: BuilderChartConfigWithDateRange,
sourceId?: string,
) {
const { data: tableMetadata } = useTableMetadata({
databaseName: config.from.databaseName,
tableName: config.from.tableName,
connectionId: config.connection,
});

// Only check for row-ID columns for row-level queries (sourceId present).
// Skip for aggregate queries (e.g. patterns) where extra keys are irrelevant.
const { data: columns } = useColumns(
{
databaseName: config.from.databaseName,
tableName: config.from.tableName,
connectionId: config.connection,
},
{ enabled: !!sourceId },
);

const primaryKey = tableMetadata?.primary_key;
const partitionKey = tableMetadata?.partition_key;

const mergedConfig = useMemo(() => {
return useMemo(() => {
if (primaryKey == null || partitionKey == null) {
return undefined;
}

const { select, additionalKeysLength } =
appendSelectWithPrimaryAndPartitionKey(
config.select,
primaryKey,
partitionKey,
);
return { ...config, select, additionalKeysLength };
}, [primaryKey, partitionKey, config]);
let extraKeys: string[] = [];

if (sourceId) {
const engineFull = tableMetadata?.engine_full ?? '';

return mergedConfig;
const hasBlockColumns =
engineFull.includes('enable_block_number_column = 1') &&
engineFull.includes('enable_block_offset_column = 1');

if (hasBlockColumns) {
extraKeys = ['_block_number', '_block_offset'];
} else if (columns?.some(c => c.name === '__hdx_id')) {
extraKeys = ['__hdx_id'];
}
}

const { select, additionalKeysLength } = appendSelectWithAdditionalKeys(
config.select,
primaryKey,
partitionKey,
extraKeys,
);

return { ...config, select, additionalKeysLength };
}, [primaryKey, partitionKey, config, tableMetadata, columns, sourceId]);
}

function selectColumnMapWithoutAdditionalKeys(
Expand Down Expand Up @@ -1552,7 +1580,7 @@ function DBSqlRowTableComponent({
return base;
}, [me, config, orderByArray]);

const mergedConfig = useConfigWithPrimaryAndPartitionKey(mergedConfigObj);
const mergedConfig = useConfigWithAdditionalSelect(mergedConfigObj, sourceId);

const { data, fetchNextPage, hasNextPage, isFetching, isError, error } =
useOffsetPaginatedQuery(mergedConfig ?? config, {
Expand Down
15 changes: 0 additions & 15 deletions packages/app/src/components/Sources/SourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1256,21 +1256,6 @@ function LogTableModelForm(props: TableModelProps) {
</FormRow>

<Divider />
{/* <FormRow
label={'Unique Row ID Expression'}
helpText="Unique identifier for a given row, will be primary key if not specified. Used for showing full row details in search results."
>
<SQLInlineEditorControlled
tableConnection={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="uniqueRowIdExpression"
placeholder="Timestamp, ServiceName, Body"
/>
</FormRow> */}
{/* <FormRow label={'Table Filter Expression'}>
<SQLInlineEditorControlled
tableConnection={{
Expand Down
73 changes: 61 additions & 12 deletions packages/app/src/components/__tests__/DBRowTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {
appendSelectWithPrimaryAndPartitionKey,
appendSelectWithAdditionalKeys,
RawLogTable,
} from '@/components/DBRowTable';
import { RowWhereResult } from '@/hooks/useRowWhere';
Expand Down Expand Up @@ -145,9 +145,9 @@ describe('RawLogTable', () => {
});
});

describe('appendSelectWithPrimaryAndPartitionKey', () => {
describe('appendSelectWithAdditionalKeys', () => {
it('should extract columns from partition key with nested function call', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
'id, created_at',
' toStartOfInterval(timestamp, toIntervalDay(3))',
Expand All @@ -159,15 +159,15 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract no columns from empty primary key and partition key', () => {
const result = appendSelectWithPrimaryAndPartitionKey('col1, col2', '', '');
const result = appendSelectWithAdditionalKeys('col1, col2', '', '', []);
expect(result).toEqual({
additionalKeysLength: 0,
select: 'col1,col2',
});
});

it('should extract columns from complex primary key', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
'id, timestamp, toStartOfInterval(timestamp2, toIntervalDay(3))',
"toStartOfInterval(timestamp, toIntervalDay(3)), date_diff('DAY', col3, col4), now(), toDate(col5 + INTERVAL 1 DAY)",
Expand All @@ -179,7 +179,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract map columns', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`map['key']`,
`map2['key'], map1['key3 ']`,
Expand All @@ -191,7 +191,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract map columns', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
``,
`map2['key.2']`,
Expand All @@ -203,7 +203,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract array columns', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`array[1]`,
`array[2], array[3]`,
Expand All @@ -215,7 +215,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract json columns', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`json.b`,
`json.a, json.b.c, toStartOfDay(timestamp, json_2.d)`,
Expand All @@ -227,7 +227,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should extract json columns with type specifiers', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`json.b.:Int64`,
`toStartOfDay(json.a.b.:DateTime)`,
Expand All @@ -239,7 +239,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should skip json columns with hard-to-parse type specifiers', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`json.b.:Array(String), col3`,
``,
Expand All @@ -251,7 +251,7 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
});

it('should skip nested map references', () => {
const result = appendSelectWithPrimaryAndPartitionKey(
const result = appendSelectWithAdditionalKeys(
'col1, col2',
`map['key']['key2'], col3`,
``,
Expand All @@ -261,4 +261,53 @@ describe('appendSelectWithPrimaryAndPartitionKey', () => {
select: `col1,col2,col3`,
});
});

it('should append extraKeys to string select', () => {
const result = appendSelectWithAdditionalKeys('col1, col2', 'id', '', [
'__hdx_id',
]);
expect(result).toEqual({
additionalKeysLength: 2,
select: 'col1,col2,id,__hdx_id',
});
});

it('should not duplicate extraKeys already in select', () => {
const result = appendSelectWithAdditionalKeys('col1, __hdx_id', 'id', '', [
'__hdx_id',
]);
expect(result).toEqual({
additionalKeysLength: 1,
select: 'col1,__hdx_id,id',
});
});

it('should deduplicate extraKeys that overlap with primary/partition keys', () => {
const result = appendSelectWithAdditionalKeys('col1, col2', 'id', '', [
'id',
'__hdx_id',
]);
expect(result).toEqual({
additionalKeysLength: 2,
select: 'col1,col2,id,__hdx_id',
});
});

it('should append extraKeys to array-style select', () => {
const result = appendSelectWithAdditionalKeys(
[{ valueExpression: 'col1' }, { valueExpression: 'col2' }],
'id',
'',
['__hdx_id'],
);
expect(result).toEqual({
additionalKeysLength: 2,
select: [
{ valueExpression: 'col1' },
{ valueExpression: 'col2' },
{ valueExpression: 'id' },
{ valueExpression: '__hdx_id' },
],
});
});
});
4 changes: 2 additions & 2 deletions packages/app/src/hooks/usePatterns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BuilderChartConfigWithDateRange } from '@hyperdx/common-utils/dist/type
import { useQuery } from '@tanstack/react-query';

import { timeBucketByGranularity, toStartOfInterval } from '@/ChartUtils';
import { useConfigWithPrimaryAndPartitionKey } from '@/components/DBRowTable';
import { useConfigWithAdditionalSelect } from '@/components/DBRowTable';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { getFirstTimestampValueExpression } from '@/source';

Expand Down Expand Up @@ -134,7 +134,7 @@ function usePatterns({
statusCodeExpression?: string;
enabled?: boolean;
}) {
const configWithPrimaryAndPartitionKey = useConfigWithPrimaryAndPartitionKey({
const configWithPrimaryAndPartitionKey = useConfigWithAdditionalSelect({
...config,
// TODO: User-configurable pattern columns and non-pattern/group by columns
select: [
Expand Down
1 change: 0 additions & 1 deletion packages/common-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,6 @@ export const LogSourceSchema = BaseSourceSchema.extend({
traceIdExpression: z.string().optional(),
spanIdExpression: z.string().optional(),
implicitColumnExpression: z.string().optional(),
uniqueRowIdExpression: z.string().optional(),
/**
* @deprecated Application-side SQL predicate AND'd into every query against
* the source. Not a security boundary — bypassable by direct table SELECT.
Expand Down
Loading