Repository navigation
fix: label Map attribute columns by their key instead of arrayElement(...) #3163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb876b0
19292b5
3a81184
e27c5a0
a7a5fa9
cdc3047
d230f1b
eb8cc31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| '@hyperdx/app': patch | ||
| --- | ||
|
|
||
| fix: label Map attribute columns added from the search sidebars by their key | ||
|
|
||
| Adding a Map attribute such as `ResourceAttributes['service.name']` as a column | ||
| from the filters sidebar or the row side panel now writes it into the SELECT as | ||
| `ResourceAttributes['service.name'] AS "service.name"`, so the results column | ||
| reads `service.name` instead of | ||
| `arrayElement(ResourceAttributes, 'service.name')`. The alias is visible and | ||
| editable in the SELECT, and queries typed by hand are not changed. A key named | ||
| like a table column, or one already used as a name in the SELECT, is added | ||
| without an alias. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,9 +130,12 @@ import { | |
| } from '@/timeQuery'; | ||
| import { | ||
| formatDurationMs, | ||
| orderByAfterRemovingSelectItem, | ||
| QUERY_LOCAL_STORAGE, | ||
| selectItemExpression, | ||
| useLocalStorage, | ||
| usePrevious, | ||
| withMapKeyAlias, | ||
| } from '@/utils'; | ||
|
|
||
| import ChartSQLPreview, { SQLPreview } from './components/ChartSQLPreview'; | ||
|
|
@@ -1104,7 +1107,7 @@ export function DBSearchPage() { | |
| [sources, lastSelectedSourceId], | ||
| ); | ||
|
|
||
| const { control, setValue, reset, handleSubmit, formState } = | ||
| const { control, setValue, getValues, reset, handleSubmit, formState } = | ||
| useForm<SearchConfigFromSchema>({ | ||
| values: { | ||
| select: searchedConfig.select || '', | ||
|
|
@@ -1737,13 +1740,38 @@ export function DBSearchPage() { | |
|
|
||
| const toggleColumn = useCallback( | ||
| (column: string) => { | ||
| const newSelectArray = displayedColumns.includes(column) | ||
| ? displayedColumns.filter(s => s !== column) | ||
| : [...displayedColumns, column]; | ||
| // A column added from the UI can carry an alias, so match the expression too | ||
| const selected = displayedColumns.find( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — toggleColumn repeats the alias-aware match that isColumnInSelect already does Add a Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — toggleColumn re-implements the isColumnInSelect match inline The Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — toggleColumn copies the matching rule that isColumnInSelect already implements The Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| s => s === column || selectItemExpression(s) === column, | ||
| ); | ||
| const newSelectArray = selected | ||
| ? displayedColumns.filter(s => s !== selected) | ||
| : [ | ||
| ...displayedColumns, | ||
| withMapKeyAlias(column, displayedColumns, knownColumns), | ||
|
greptile-apps[bot] marked this conversation as resolved.
Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 major — Sorting an aliased Map column, then removing it, leaves a dangling Reachable with UI clicks only: add Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed. The form's |
||
| ]; | ||
| setValue('select', newSelectArray.join(', ')); | ||
| if (selected) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — The new toggleColumn logic (removing an aliased item, resetting the sort) has no test Add a DBSearchPage or DBRowJsonViewer test that adds a Map subscript, checks the SELECT gets Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — New toggleColumn orderBy reset and aliasing have no test The tests only cover the pure helpers. Nothing checks that removing an aliased column that is currently sorted resets the Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| const orderBy = getValues('orderBy') ?? ''; | ||
| const nextOrderBy = orderByAfterRemovingSelectItem( | ||
| selected, | ||
| orderBy, | ||
| defaultSearchConfig.orderBy ?? '', | ||
| ); | ||
| if (nextOrderBy !== orderBy) { | ||
| setValue('orderBy', nextOrderBy); | ||
| } | ||
| } | ||
| onSubmit(); | ||
| }, | ||
| [displayedColumns, setValue, onSubmit], | ||
| [ | ||
| displayedColumns, | ||
| knownColumns, | ||
| setValue, | ||
| getValues, | ||
| defaultSearchConfig.orderBy, | ||
| onSubmit, | ||
| ], | ||
| ); | ||
|
|
||
| const generateSearchUrl = useCallback( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,10 @@ import { formatDistanceToNowStrict } from 'date-fns'; | |
| import numbro from 'numbro'; | ||
| import type { SetStateAction } from 'react'; | ||
| import TimestampNano from 'timestamp-nano'; | ||
| import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata'; | ||
| import { | ||
| TableConnection, | ||
| unquoteIdentifier, | ||
| } from '@hyperdx/common-utils/dist/core/metadata'; | ||
| import { | ||
| CATEGORICAL_PALETTE_TOKENS, | ||
| ChartPaletteToken, | ||
|
|
@@ -1201,6 +1204,93 @@ export const mergePath = ( | |
| .join('')}`; | ||
| }; | ||
|
|
||
| // A bare Map subscript as the UI builds it: `LogAttributes['key']` or `` `LogAttributes`['key'] `` | ||
| const MAP_SUBSCRIPT_REGEX = | ||
| /^(`[^`]+`|[A-Za-z_][A-Za-z0-9_]*)\['((?:[^'\\]|\\.)*)'\]$/; | ||
| // A trailing `AS alias` on a SELECT item, with the alias quoted or bare | ||
| const SELECT_ALIAS_REGEX = | ||
| /\s+AS\s+("(?:[^"\\]|\\.)*"|`[^`]*`|[A-Za-z_][A-Za-z0-9_]*)\s*$/i; | ||
|
|
||
| /** | ||
| * The expression of a SELECT item, without its `AS alias` | ||
| */ | ||
| export const selectItemExpression = (item: string): string => | ||
| item.replace(SELECT_ALIAS_REGEX, '').trim(); | ||
|
|
||
| /** | ||
| * The alias of a SELECT item, unquoted, or undefined if it has none | ||
| */ | ||
| const selectItemAlias = (item: string): string | undefined => { | ||
| const alias = SELECT_ALIAS_REGEX.exec(item)?.[1]; | ||
| return alias == null ? undefined : unquoteIdentifier(alias); | ||
| }; | ||
|
|
||
| /** | ||
| * Whether a column picked in the UI is in the SELECT list, aliased or not | ||
| */ | ||
| export const isColumnInSelect = ( | ||
| displayedColumns: string[] | undefined, | ||
| column: string, | ||
| ): boolean => | ||
| displayedColumns?.some( | ||
| item => item === column || selectItemExpression(item) === column, | ||
| ) ?? false; | ||
|
|
||
| /** | ||
| * Label a Map subscript picked in the UI with its key, so the results column | ||
| * reads `service.name` instead of `arrayElement(LogAttributes, 'service.name')`. | ||
| * The alias is left off whenever it could change the query. | ||
| */ | ||
| export const withMapKeyAlias = ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Header labelling is done by rewriting persisted SQL when a presentational rename already exists
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A fair alternative, but @pulpdrew asked specifically for the alias to be written into the SELECT so it's visible and editable (#3163 (comment)), so I've kept that. Happy to switch to a presentational |
||
| column: string, | ||
| selectItems: string[], | ||
| tableColumns: Set<string>, | ||
| ): string => { | ||
| const match = MAP_SUBSCRIPT_REGEX.exec(column); | ||
| if (!match) { | ||
| return column; | ||
| } | ||
| // Inverse of escapeSqlSingleQuoted, which mergePath uses to build the subscript | ||
| const key = match[2].replace(/\\(.)/g, '$1'); | ||
| const selectedNames = selectItems.map( | ||
| item => selectItemAlias(item) ?? item.trim(), | ||
| ); | ||
| if ( | ||
| // Nothing to label | ||
| key === '' || | ||
| // Would need escaping inside the quoted alias | ||
| /["\\]/.test(key) || | ||
| // Table columns not loaded yet, so a clash with one cannot be ruled out | ||
| tableColumns.size === 0 || | ||
| // ClickHouse resolves an alias before a column of the same name | ||
| tableColumns.has(key) || | ||
| // Two selected columns cannot share a name | ||
| selectedNames.includes(key) | ||
| ) { | ||
| return column; | ||
| } | ||
| return `${column} AS "${key}"`; | ||
| }; | ||
|
|
||
| /** | ||
| * The sort to keep once a SELECT item is removed. Sorting on an aliased column | ||
| * from the table header orders by the alias itself (`"service.name" DESC`), | ||
| * which stops resolving once the item is gone, so that falls back to the | ||
| * default sort. | ||
| */ | ||
| export const orderByAfterRemovingSelectItem = ( | ||
| item: string, | ||
| orderBy: string, | ||
| defaultOrderBy: string, | ||
| ): string => { | ||
| const alias = selectItemAlias(item); | ||
| if (alias == null) { | ||
| return orderBy; | ||
| } | ||
| const sortedBy = orderBy.trim().replace(/\s+(ASC|DESC)$/i, ''); | ||
| return sortedBy === `"${alias}"` ? defaultOrderBy : orderBy; | ||
| }; | ||
|
|
||
| const _useTry = <T>(fn: () => T): [null | Error | unknown, null | T] => { | ||
| let output = null; | ||
| let error = null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 minor — The new toggleColumn wiring has no test
The helpers are unit-tested, but nothing checks toggleColumn itself. No test removes an aliased item when it is given the bare expression, which is what the sidebar, side panel and header all send, or resets the form's
orderBytodefaultSearchConfig.orderBy. Add a DBSearchPage test that togglesResourceAttributes['service.name']on, sorts by"service.name", toggles it off, and asserts the resultingselectandorderBy.Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond