diff --git a/.changeset/fix-sql-where-kv-textindex.md b/.changeset/fix-sql-where-kv-textindex.md new file mode 100644 index 0000000000..759702e2dc --- /dev/null +++ b/.changeset/fix-sql-where-kv-textindex.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/common-utils': patch +--- + +fix: apply KV text-index rewrite to the SQL `where` search box, not just `sql`-type filters (#2927) \ No newline at end of file diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 0e7e074497..c407c65923 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -1489,6 +1489,28 @@ describe('renderChartConfig', () => { ); expect(sql).toContain("SeverityText = 'error'"); }); + it('rewrites `Map[key] = value` in chartConfig.where when whereLanguage is sql', async () => { + stubKvItemsMetadata(); + const config: ChartConfigWithOptDateRange = { + connection: 'test-connection', + from: { databaseName: 'default', tableName: 'otel_logs' }, + select: [{ aggFn: 'count', valueExpression: '' }], + where: "LogAttributes['service.name'] = 'api'", + whereLanguage: 'sql', + timestampValueExpression: 'Timestamp', + dateRange: [new Date('2025-01-01'), new Date('2025-01-02')], + granularity: '1 minute', + }; + + const sql = parameterizedQueryToSql( + await renderChartConfig(config, mockMetadata, querySettings), + ); + + expect(sql).toContain( + "has(`LogAttributeItems`, concat('service.name', '=', 'api'))", + ); + expect(sql).not.toContain("LogAttributes['service.name'] = 'api'"); + }); }); describe('k8s semantic convention migrations', () => { diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 1c35ca8c23..3c166c2f9c 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -1151,11 +1151,34 @@ async function renderWhere( chartConfig: BuilderChartConfigWithOptDateRangeEx, metadata: Metadata, ): Promise { + // Build the lookup first — it's needed by both the `where` block and the + // `filters` block below. + const hasSqlFilter = + (isNonEmptyWhereExpr(chartConfig.where) && + (chartConfig.whereLanguage ?? 'sql') === 'sql') || + (chartConfig.filters?.some(f => f.type === 'sql') ?? false); + const textIndexInfoLookup: TextIndexInfoLookup = + hasSqlFilter && + chartConfig.from.databaseName && + chartConfig.from.tableName && + !hasSubqueryCte(chartConfig.with) + ? await buildTextIndexInfoLookup({ + metadata, + databaseName: chartConfig.from.databaseName, + tableName: chartConfig.from.tableName, + connectionId: chartConfig.connection, + }) + : new Map(); + let whereSearchCondition: ChSql | [] = []; if (isNonEmptyWhereExpr(chartConfig.where)) { + const rewrittenWhere = + (chartConfig.whereLanguage ?? 'sql') === 'sql' + ? rewriteSqlFilterWithKvItems(chartConfig.where, textIndexInfoLookup) + : chartConfig.where; whereSearchCondition = wrapChSqlIfNotEmpty( await renderWhereExpression({ - condition: chartConfig.where, + condition: rewrittenWhere, from: chartConfig.from, language: chartConfig.whereLanguage ?? 'sql', implicitColumnExpression: chartConfig.implicitColumnExpression, @@ -1201,21 +1224,6 @@ async function renderWhere( ).filter(v => v !== null) as ChSql[]; } - const hasSqlFilter = - chartConfig.filters?.some(f => f.type === 'sql') ?? false; - const textIndexInfoLookup: TextIndexInfoLookup = - hasSqlFilter && - chartConfig.from.databaseName && - chartConfig.from.tableName && - !hasSubqueryCte(chartConfig.with) - ? await buildTextIndexInfoLookup({ - metadata, - databaseName: chartConfig.from.databaseName, - tableName: chartConfig.from.tableName, - connectionId: chartConfig.connection, - }) - : new Map(); - const filterConditions = await Promise.all( (chartConfig.filters ?? []).map(async filter => { if (filter.type === 'sql_ast') { @@ -1246,7 +1254,6 @@ async function renderWhere( ')', ); } - throw new Error(`Unknown filter type: ${filter.type}`); }), );