diff --git a/pkg/tsdb/influxdb/models/query.go b/pkg/tsdb/influxdb/models/query.go index 4fb0a6e863f..e8e6837aae7 100644 --- a/pkg/tsdb/influxdb/models/query.go +++ b/pkg/tsdb/influxdb/models/query.go @@ -67,6 +67,45 @@ func (query *Query) renderTags() []string { } } + isOperatorTypeHandler := func(tag *Tag) (string, string) { + // Attempt to identify the type of the supplied value + var lowerValue = strings.ToLower(tag.Value) + var r = regexp.MustCompile(`^(-?)[0-9\.]+$`) + var textValue string + var operator string + + // Perform operator replacements + switch tag.Operator { + case "Is": + operator = "=" + case "Is Not": + operator = "!=" + default: + // This should never happen + operator = "=" + } + + // Always quote tag values + if strings.HasSuffix(tag.Key, "::tag") { + textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`)) + return textValue, operator + } + + // Try and discern the type of fields + if lowerValue == "true" || lowerValue == "false" { + // boolean, don't quote, but make lowercase + textValue = lowerValue + } else if r.MatchString(tag.Value) { + // Integer or float, don't quote + textValue = tag.Value + } else { + // String (or unknown) - quote + textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`)) + } + + return textValue, operator + } + // quote value unless regex or number var textValue string switch tag.Operator { @@ -74,6 +113,8 @@ func (query *Query) renderTags() []string { textValue = tag.Value case "<", ">", ">=", "<=": textValue = tag.Value + case "Is", "Is Not": + textValue, tag.Operator = isOperatorTypeHandler(tag) default: textValue = fmt.Sprintf("'%s'", strings.ReplaceAll(tag.Value, `\`, `\\`)) } diff --git a/pkg/tsdb/influxdb/models/query_test.go b/pkg/tsdb/influxdb/models/query_test.go index 2cc6026c4c5..c590e829d16 100644 --- a/pkg/tsdb/influxdb/models/query_test.go +++ b/pkg/tsdb/influxdb/models/query_test.go @@ -239,6 +239,47 @@ func TestInfluxdbQueryBuilder(t *testing.T) { require.Equal(t, strings.Join(query.renderTags(), ""), `"key" <= 10001`) }) + t.Run("can render boolean equality tags", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "false", Key: "key"}}} + + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = false`) + }) + + t.Run("can render boolean inequality tags", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is Not", Value: "true", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" != true`) + }) + + t.Run("can correct case of boolean tags", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "False", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = false`) + }) + + t.Run("can use strings with Is", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "A string", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = 'A string'`) + }) + + t.Run("can use integers with Is", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "123", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = 123`) + }) + + t.Run("can use negative integers with Is", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "-123", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = -123`) + }) + + t.Run("can use floats with Is", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "1.23", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = 1.23`) + }) + + t.Run("can use negative floats with Is", func(t *testing.T) { + query := &Query{Tags: []*Tag{{Operator: "Is", Value: "-1.23", Key: "key"}}} + require.Equal(t, strings.Join(query.renderTags(), ""), `"key" = -1.23`) + }) + t.Run("can render string tags", func(t *testing.T) { query := &Query{Tags: []*Tag{{Operator: "=", Value: "value", Key: "key"}}} diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx index bcc17db289d..b290a231bb2 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/TagsSection.tsx @@ -10,8 +10,8 @@ import { toSelectableValue } from '../utils/toSelectableValue'; import { AddButton } from './AddButton'; import { Seg } from './Seg'; -type KnownOperator = '=' | '!=' | '<>' | '<' | '>' | '>=' | '<=' | '=~' | '!~'; -const knownOperators: KnownOperator[] = ['=', '!=', '<>', '<', '>', '>=', '<=', '=~', '!~']; +type KnownOperator = '=' | '!=' | '<>' | '<' | '>' | '>=' | '<=' | '=~' | '!~' | 'Is' | 'Is Not'; +const knownOperators: KnownOperator[] = ['=', '!=', '<>', '<', '>', '>=', '<=', '=~', '!~', 'Is', 'Is Not']; type KnownCondition = 'AND' | 'OR'; const knownConditions: KnownCondition[] = ['AND', 'OR']; diff --git a/public/app/plugins/datasource/influxdb/influx_query_model.test.ts b/public/app/plugins/datasource/influxdb/influx_query_model.test.ts index b88cf7eeef6..6f52956da38 100644 --- a/public/app/plugins/datasource/influxdb/influx_query_model.test.ts +++ b/public/app/plugins/datasource/influxdb/influx_query_model.test.ts @@ -205,7 +205,6 @@ describe('InfluxQuery', () => { expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" > 5) AND $timeFilter'); }); }); - describe('query with greater-than-or-equal-to condition', () => { it('should use >=', () => { const query = new InfluxQueryModel( @@ -244,6 +243,158 @@ describe('InfluxQuery', () => { }); }); + describe('query with Is operator', () => { + it('should use =', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: 'False', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = false) AND $timeFilter'); + }); + }); + + describe('query with Is Not operator', () => { + it('should use !=', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: 'False', operator: 'Is Not' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" != false) AND $timeFilter'); + }); + }); + + describe('query with Is boolean condition', () => { + it('should convert to lowercase and not quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: 'True', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = true) AND $timeFilter'); + }); + }); + + describe('query with Is string condition', () => { + it('should quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: 'Server2', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = \'Server2\') AND $timeFilter'); + }); + }); + + describe('query with Is integer condition', () => { + it('should not quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: '5', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = 5) AND $timeFilter'); + }); + }); + + describe('query with Is float condition', () => { + it('should not quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: '1.234', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = 1.234) AND $timeFilter'); + }); + }); + + describe('query with Is negative float condition', () => { + it('should not quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: '-1.234', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = -1.234) AND $timeFilter'); + }); + }); + + describe('query with Is boolean condition', () => { + it('should convert to lowercase and not quote value', () => { + const query = new InfluxQueryModel( + { + refId: 'A', + measurement: 'cpu', + policy: 'autogen', + groupBy: [], + tags: [{ key: 'value', value: 'True', operator: 'Is' }], + }, + templateSrv, + {} + ); + + const queryText = query.render(); + expect(queryText).toBe('SELECT mean("value") FROM "autogen"."cpu" WHERE ("value" = true) AND $timeFilter'); + }); + }); + describe('series with groupByTag', () => { it('should generate correct query', () => { const query = new InfluxQueryModel( diff --git a/public/app/plugins/datasource/influxdb/influx_query_model.ts b/public/app/plugins/datasource/influxdb/influx_query_model.ts index 337ef905f76..1be91772f0e 100644 --- a/public/app/plugins/datasource/influxdb/influx_query_model.ts +++ b/public/app/plugins/datasource/influxdb/influx_query_model.ts @@ -141,6 +141,42 @@ export default class InfluxQueryModel { this.updatePersistedParts(); } + private isOperatorTypeHandler(operator: string, value: string, fieldName: string) { + let textValue; + if (operator === 'Is Not') { + operator = '!='; + } else { + operator = '='; + } + + // Tags should always quote + if (fieldName.endsWith('::tag')) { + textValue = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'"; + return { + operator: operator, + value: textValue, + }; + } + + let lowerValue = value.toLowerCase(); + + // Try and discern type + if (!isNaN(parseFloat(value))) { + // Integer or float, don't quote + textValue = value; + } else if (['true', 'false'].includes(lowerValue)) { + // It's a boolean, don't quite + textValue = lowerValue; + } else { + // String or unrecognised: quote + textValue = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'"; + } + return { + operator: operator, + value: textValue, + }; + } + private renderTagCondition(tag: InfluxQueryTag, index: number, interpolate?: boolean) { // FIXME: merge this function with query_builder/renderTagCondition let str = ''; @@ -163,7 +199,12 @@ export default class InfluxQueryModel { if (interpolate) { value = this.templateSrv.replace(value, this.scopedVars); } - if ((!operator.startsWith('>') && !operator.startsWith('<')) || operator === '<>') { + + if (operator.startsWith('Is')) { + let r = this.isOperatorTypeHandler(operator, value, tag.key); + operator = r.operator; + value = r.value; + } else if ((!operator.startsWith('>') && !operator.startsWith('<')) || operator === '<>') { value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'"; } } else if (interpolate) { diff --git a/public/app/plugins/datasource/influxdb/queryUtils.test.ts b/public/app/plugins/datasource/influxdb/queryUtils.test.ts index 26c61368e30..89762072922 100644 --- a/public/app/plugins/datasource/influxdb/queryUtils.test.ts +++ b/public/app/plugins/datasource/influxdb/queryUtils.test.ts @@ -147,6 +147,18 @@ describe('InfluxDB query utils', () => { operator: '!~', value: '/cpu0/', }, + { + condition: 'AND', + key: 'cpu', + operator: 'Is', + value: 'false', + }, + { + condition: 'AND', + key: 'cpu', + operator: 'Is Not', + value: 'false', + }, ], groupBy: [], }) @@ -154,7 +166,7 @@ describe('InfluxDB query utils', () => { `SELECT "value" ` + `FROM "autogen"."measurement" ` + `WHERE ("cpu" = 'cpu0' AND "cpu" != 'cpu0' AND "cpu" <> 'cpu0' AND "cpu" < cpu0 AND ` + - `"cpu" > cpu0 AND "cpu" =~ /cpu0/ AND "cpu" !~ /cpu0/) AND $timeFilter` + `"cpu" > cpu0 AND "cpu" =~ /cpu0/ AND "cpu" !~ /cpu0/ AND "cpu" = false AND "cpu" != false) AND $timeFilter` ); }); it('should handle a complex query', () => {