mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
InfluxDB: Add new truthiness operators (Is
and Is Not
) to InfluxQL Query Builder (#77923)
* InfluxDB: Add new truthiness operators (`Is` and `Is Not`) to InfluxQL Editor for use with boolean fields * InfluxDB: Make the front-end aware of the new operators so that translation between raw and builder works * Chore: Add tests * feat: identify type of field value to allow other types to work with Is/Is Not Tags: always quote Integer: Don't quote Float: Don't quote Boolean: Don't quote String: Quote * Chore: Add test-cases for type inference * Update front-end to infer type for operators Is and Is Not * chore: Add front-end tests * chore: add additional front-end tests * chore: fix failing lint test * chore: fix tests (run prettier)
This commit is contained in:
parent
a94acf4b63
commit
f38f657f87
@ -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, `\`, `\\`))
|
||||
}
|
||||
|
@ -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"}}}
|
||||
|
||||
|
@ -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'];
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user