Repository navigation
fix: only split a query at a standalone SETTINGS keyword #3216
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
e7d9860
542bb85
ff0d6bd
62a13f7
25609fb
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,13 @@ | ||
| --- | ||
| '@hyperdx/common-utils': patch | ||
| '@hyperdx/api': patch | ||
| '@hyperdx/app': patch | ||
| --- | ||
|
|
||
| fix: only split a query at a standalone SETTINGS keyword | ||
|
|
||
| `extractSettingsClauseFromEnd` cut the query at the first "settings" anywhere in | ||
| it, including inside a string or an identifier. A multi-series metric chart with | ||
| a metric such as `app.settings.reloads` produced SQL with an unterminated | ||
| string, and a SQL filter on a column such as `AppSettings` was rewritten to | ||
| reference `App`. The keyword now has to stand alone outside quotes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1209,6 +1209,44 @@ export function isDateRangeEqual(range1: [Date, Date], range2: [Date, Date]) { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Index of the first standalone SETTINGS keyword, or -1. Occurrences inside | ||
| * quoted strings, comments or identifiers (e.g. `'app.settings.reloads'`, | ||
| * `AppSettings`, `LogAttributes.settings`) are not the clause and must not | ||
| * split the query. | ||
| */ | ||
| function findSettingsKeyword(sql: string): number { | ||
|
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 —
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 isWordChar = (c: string) => /\w/.test(c); | ||
| const nextNonSpace = (from: number) => sql.slice(from).trimStart().charAt(0); | ||
| let quote: string | undefined; | ||
| for (let i = 0; i < sql.length; i++) { | ||
| const c = sql.charAt(i); | ||
| if (quote) { | ||
| if (c === '\\') i++; | ||
| else if (c === quote) quote = undefined; | ||
| } else if (c === "'" || c === '"' || c === '`') { | ||
|
greptile-apps[bot] marked this conversation as resolved.
Resolved
|
||
| quote = c; | ||
| } else if (c === '-' && sql.charAt(i + 1) === '-') { | ||
| const lineEnd = sql.indexOf('\n', i + 2); | ||
| if (lineEnd === -1) break; | ||
| i = lineEnd; | ||
| } else if (c === '/' && sql.charAt(i + 1) === '*') { | ||
| const blockEnd = sql.indexOf('*/', i + 2); | ||
| if (blockEnd === -1) break; | ||
| i = blockEnd + 1; | ||
| } else if ( | ||
| sql.substring(i, i + 8).toUpperCase() === 'SETTINGS' && | ||
|
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 — A dotted path or map column named The word-boundary check uses 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
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. Fixed in 542bb85. A match is skipped when the previous non-space character is |
||
| !isWordChar(sql.charAt(i - 1)) && | ||
| !isWordChar(sql.charAt(i + 8)) && | ||
| sql.slice(0, i).trimEnd().slice(-1) !== '.' && | ||
| !['.', '['].includes(nextNonSpace(i + 8)) | ||
| ) { | ||
| return i; | ||
| } | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| /* | ||
| This function extracts the SETTINGS clause from the end(!) of the sql string. | ||
| */ | ||
|
|
@@ -1219,7 +1257,7 @@ export function extractSettingsClauseFromEnd( | |
| ? sqlInput.trim().slice(0, -1) | ||
| : sqlInput.trim(); | ||
|
|
||
| const settingsIndex = sql.toUpperCase().indexOf('SETTINGS'); | ||
| const settingsIndex = findSettingsKeyword(sql); | ||
|
|
||
| if (settingsIndex === -1) { | ||
| return [sql, undefined] as const; | ||
|
|
||
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 — A second quote-aware keyword scanner, and this one skips SQL comments
extractOuterSelectProjectioninpackages/common-utils/src/clickhouse/index.ts:855-937already does this: itsmatchesKeywordAt/isWordCharscanner tracks quotes and word boundaries and also skips--and/* */comments. The newfindSettingsKeywordhas no comment handling. So an apostrophe inside a comment (-- don't count retries\n... SETTINGS max_threads=1) opens a fake string, and the real SETTINGS is never found.chSqlToAliasMapandfastifySQLthen pass SETTINGS to SQLParser, which fails to parse it. Onmain, plainindexOffound that clause. Pull the scanner out into a shared keyword finder with comment skipping, and use it in both places.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
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.
Fixed in 542bb85. The scanner now skips
--and/* */comments. A newextractSettingsClauseFromEndcase ("apostrophe in a comment before SETTINGS") fails on e7d9860 and passes now. I didn't reuseextractOuterSelectProjection's scanner here, because that would touchclickhouse/index.ts, which this PR doesn't otherwise change.