InfluxDB: Update InfluxQL querybuilder to qualify identifiers (#62230)
This commit is contained in:
@@ -78,7 +78,17 @@ func (query *Query) renderTags() []string {
|
|||||||
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`))
|
textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`))
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, textValue))
|
escapedKey := fmt.Sprintf(`"%s"`, tag.Key)
|
||||||
|
|
||||||
|
if strings.HasSuffix(tag.Key, "::tag") {
|
||||||
|
escapedKey = fmt.Sprintf(`"%s"::tag`, strings.TrimSuffix(tag.Key, "::tag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(tag.Key, "::field") {
|
||||||
|
escapedKey = fmt.Sprintf(`"%s"::field`, strings.TrimSuffix(tag.Key, "::field"))
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, fmt.Sprintf(`%s%s %s %s`, str, escapedKey, tag.Operator, textValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -98,10 +98,23 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fieldRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
func fieldRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
||||||
if part.Params[0] == "*" {
|
param := part.Params[0]
|
||||||
|
|
||||||
|
if param == "*" {
|
||||||
return "*"
|
return "*"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`"%s"`, part.Params[0])
|
|
||||||
|
escapedParam := fmt.Sprintf(`"%s"`, param)
|
||||||
|
|
||||||
|
if strings.HasSuffix(param, "::tag") {
|
||||||
|
escapedParam = fmt.Sprintf(`"%s"::tag`, strings.TrimSuffix(param, "::tag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(param, "::field") {
|
||||||
|
escapedParam = fmt.Sprintf(`"%s"::field`, strings.TrimSuffix(param, "::field"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedParam
|
||||||
}
|
}
|
||||||
|
|
||||||
func functionRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
func functionRenderer(query *Query, queryContext *backend.QueryDataRequest, part *QueryPart, innerExpr string) string {
|
||||||
|
|||||||
+33
-12
@@ -1,4 +1,4 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@@ -10,11 +10,16 @@ import { Editor } from './Editor';
|
|||||||
|
|
||||||
jest.mock('../../influxQLMetadataQuery', () => {
|
jest.mock('../../influxQLMetadataQuery', () => {
|
||||||
return {
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
getFieldKeysForMeasurement: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(Promise.resolve(['free', 'total']))
|
||||||
|
.mockReturnValueOnce(Promise.resolve([])),
|
||||||
getTagKeysForMeasurementAndTags: jest
|
getTagKeysForMeasurementAndTags: jest
|
||||||
.fn()
|
.fn()
|
||||||
// first time we are called when the widget mounts,
|
// first time we are called when the widget mounts,
|
||||||
// we respond by saying `cpu, host` are the real tags
|
// we respond by saying `cpu, host, device` are the real tags
|
||||||
.mockReturnValueOnce(Promise.resolve(['cpu', 'host']))
|
.mockReturnValueOnce(Promise.resolve(['cpu', 'host', 'device']))
|
||||||
// afterwards we will be called once when we click
|
// afterwards we will be called once when we click
|
||||||
// on a tag-key in the WHERE section.
|
// on a tag-key in the WHERE section.
|
||||||
// it does not matter what we return, as long as it is
|
// it does not matter what we return, as long as it is
|
||||||
@@ -43,6 +48,7 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mockClear();
|
(mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mockClear();
|
||||||
|
(mockedMeta.getFieldKeysForMeasurement as jest.Mock).mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
const ONLY_TAGS = [
|
const ONLY_TAGS = [
|
||||||
@@ -57,6 +63,12 @@ const ONLY_TAGS = [
|
|||||||
operator: '=',
|
operator: '=',
|
||||||
value: 'host2',
|
value: 'host2',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
condition: 'AND',
|
||||||
|
key: 'device::tag',
|
||||||
|
operator: '=',
|
||||||
|
value: 'sdd',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const query: InfluxQuery = {
|
const query: InfluxQuery = {
|
||||||
@@ -76,10 +88,22 @@ const query: InfluxQuery = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: 'AND',
|
condition: 'AND',
|
||||||
key: 'field1',
|
key: 'device::tag',
|
||||||
|
operator: '=',
|
||||||
|
value: 'sdd',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: 'AND',
|
||||||
|
key: 'free',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
value: '45',
|
value: '45',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
condition: 'AND',
|
||||||
|
key: 'total::field',
|
||||||
|
operator: '=',
|
||||||
|
value: '200',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
select: [
|
select: [
|
||||||
[
|
[
|
||||||
@@ -101,17 +125,14 @@ describe('InfluxDB InfluxQL Visual Editor field-filtering', () => {
|
|||||||
} as unknown as InfluxDatasource;
|
} as unknown as InfluxDatasource;
|
||||||
render(<Editor query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />);
|
render(<Editor query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
await waitFor(() => {});
|
||||||
|
|
||||||
|
// when the editor-widget mounts, it calls getFieldKeysForMeasurement
|
||||||
|
expect(mockedMeta.getFieldKeysForMeasurement).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// when the editor-widget mounts, it calls getTagKeysForMeasurementAndTags
|
// when the editor-widget mounts, it calls getTagKeysForMeasurementAndTags
|
||||||
expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(1);
|
expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// we click the WHERE/cpu button
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'cpu' }));
|
|
||||||
|
|
||||||
// and verify getTagKeysForMeasurementAndTags was called again,
|
|
||||||
// and in the tags-param we did not receive the `field1` part.
|
|
||||||
expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(2);
|
|
||||||
expect((mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mock.calls[1][2]).toStrictEqual(ONLY_TAGS);
|
|
||||||
|
|
||||||
// now we click on the WHERE/host2 button
|
// now we click on the WHERE/host2 button
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'host2' }));
|
await userEvent.click(screen.getByRole('button', { name: 'host2' }));
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function withTemplateVariableOptions(optionsPromise: Promise<string[]>, filter?:
|
|||||||
// it is possible to add fields into the `InfluxQueryTag` structures, and they do work,
|
// it is possible to add fields into the `InfluxQueryTag` structures, and they do work,
|
||||||
// but in some cases, when we do metadata queries, we have to remove them from the queries.
|
// but in some cases, when we do metadata queries, we have to remove them from the queries.
|
||||||
function filterTags(parts: InfluxQueryTag[], allTagKeys: Set<string>): InfluxQueryTag[] {
|
function filterTags(parts: InfluxQueryTag[], allTagKeys: Set<string>): InfluxQueryTag[] {
|
||||||
return parts.filter((t) => allTagKeys.has(t.key));
|
return parts.filter((t) => t.key.endsWith('::tag') || allTagKeys.has(t.key + '::tag'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = (props: Props): JSX.Element => {
|
export const Editor = (props: Props): JSX.Element => {
|
||||||
@@ -76,10 +76,16 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
const { datasource } = props;
|
const { datasource } = props;
|
||||||
const { measurement, policy } = query;
|
const { measurement, policy } = query;
|
||||||
|
|
||||||
const allTagKeys = useMemo(() => {
|
const allTagKeys = useMemo(async () => {
|
||||||
return getTagKeysForMeasurementAndTags(measurement, policy, [], datasource).then((tags) => {
|
const tagKeys = (await getTagKeysForMeasurementAndTags(measurement, policy, [], datasource)).map(
|
||||||
return new Set(tags);
|
(tag) => `${tag}::tag`
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const fieldKeys = (await getFieldKeysForMeasurement(measurement || '', policy, datasource)).map(
|
||||||
|
(field) => `${field}::field`
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Set([...tagKeys, ...fieldKeys]);
|
||||||
}, [measurement, policy, datasource]);
|
}, [measurement, policy, datasource]);
|
||||||
|
|
||||||
const selectLists = useMemo(() => {
|
const selectLists = useMemo(() => {
|
||||||
@@ -98,12 +104,14 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
|
|
||||||
// the following function is not complicated enough to memoize, but it's result
|
// the following function is not complicated enough to memoize, but it's result
|
||||||
// is used in both memoized and un-memoized parts, so we have no choice
|
// is used in both memoized and un-memoized parts, so we have no choice
|
||||||
const getTagKeys = useMemo(() => {
|
const getTagKeys = useMemo(
|
||||||
return () =>
|
() => async () => {
|
||||||
allTagKeys.then((keys) =>
|
const selectedTagKeys = new Set(query.tags?.map((tag) => tag.key));
|
||||||
getTagKeysForMeasurementAndTags(measurement, policy, filterTags(query.tags ?? [], keys), datasource)
|
|
||||||
);
|
return [...(await allTagKeys)].filter((tagKey) => !selectedTagKeys.has(tagKey));
|
||||||
}, [measurement, policy, query.tags, datasource, allTagKeys]);
|
},
|
||||||
|
[query.tags, allTagKeys]
|
||||||
|
);
|
||||||
|
|
||||||
const groupByList = useMemo(() => {
|
const groupByList = useMemo(() => {
|
||||||
const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]);
|
const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]);
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export async function getTagValues(
|
|||||||
datasource: InfluxDatasource
|
datasource: InfluxDatasource
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const target = { tags, measurement, policy };
|
const target = { tags, measurement, policy };
|
||||||
|
|
||||||
|
if (tagKey.endsWith('::field')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const data = await runExploreQuery('TAG_VALUES', tagKey, undefined, target, datasource);
|
const data = await runExploreQuery('TAG_VALUES', tagKey, undefined, target, datasource);
|
||||||
return data.map((item) => item.text);
|
return data.map((item) => item.text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,17 @@ export default class InfluxQueryModel {
|
|||||||
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
|
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
|
||||||
}
|
}
|
||||||
|
|
||||||
return str + '"' + tag.key + '" ' + operator + ' ' + value;
|
let escapedKey = `"${tag.key}"`;
|
||||||
|
|
||||||
|
if (tag.key.endsWith('::tag')) {
|
||||||
|
escapedKey = `"${tag.key.slice(0, -5)}"::tag`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.key.endsWith('::field')) {
|
||||||
|
escapedKey = `"${tag.key.slice(0, -7)}"::field`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str + escapedKey + ' ' + operator + ' ' + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMeasurementAndPolicy(interpolate: any) {
|
getMeasurementAndPolicy(interpolate: any) {
|
||||||
|
|||||||
@@ -25,7 +25,17 @@ function renderTagCondition(tag: { operator: any; value: string; condition: any;
|
|||||||
value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'";
|
value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
return str + '"' + tag.key + '" ' + operator + ' ' + value;
|
let escapedKey = `"${tag.key}"`;
|
||||||
|
|
||||||
|
if (tag.key.endsWith('::tag')) {
|
||||||
|
escapedKey = `"${tag.key.slice(0, -5)}"::tag`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.key.endsWith('::field')) {
|
||||||
|
escapedKey = `"${tag.key.slice(0, -7)}"::field`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str + escapedKey + ' ' + operator + ' ' + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InfluxQueryBuilder {
|
export class InfluxQueryBuilder {
|
||||||
@@ -83,7 +93,13 @@ export class InfluxQueryBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (withKey) {
|
if (withKey) {
|
||||||
query += ' WITH KEY = "' + withKey + '"';
|
let keyIdentifier = withKey;
|
||||||
|
|
||||||
|
if (keyIdentifier.endsWith('::tag')) {
|
||||||
|
keyIdentifier = keyIdentifier.slice(0, -5);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' WITH KEY = "' + keyIdentifier + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.target.tags && this.target.tags.length > 0) {
|
if (this.target.tags && this.target.tags.length > 0) {
|
||||||
|
|||||||
@@ -34,10 +34,23 @@ function aliasRenderer(part: { params: string[] }, innerExpr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fieldRenderer(part: { params: string[] }, innerExpr: any) {
|
function fieldRenderer(part: { params: string[] }, innerExpr: any) {
|
||||||
if (part.params[0] === '*') {
|
const param = part.params[0];
|
||||||
|
|
||||||
|
if (param === '*') {
|
||||||
return '*';
|
return '*';
|
||||||
}
|
}
|
||||||
return '"' + part.params[0] + '"';
|
|
||||||
|
let escapedParam = `"${param}"`;
|
||||||
|
|
||||||
|
if (param.endsWith('::tag')) {
|
||||||
|
escapedParam = `"${param.slice(0, -5)}"::tag`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.endsWith('::field')) {
|
||||||
|
escapedParam = `"${param.slice(0, -7)}"::field`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAggregationAddStrategy(selectParts: any[], partModel: { def: { type: string } }) {
|
function replaceAggregationAddStrategy(selectParts: any[], partModel: { def: { type: string } }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user