mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Chore/added_ilike : added ilike and notIlike filter operator (#8595)
* chore(added-ilike): added ilike operator in qbv5 * chore(added-ilike): added test cases
This commit is contained in:
parent
7df5c33ce9
commit
771ba45d01
@ -333,6 +333,8 @@ export const OPERATORS = {
|
|||||||
'<': '<',
|
'<': '<',
|
||||||
HAS: 'HAS',
|
HAS: 'HAS',
|
||||||
NHAS: 'NHAS',
|
NHAS: 'NHAS',
|
||||||
|
ILIKE: 'ILIKE',
|
||||||
|
NOTILIKE: 'NOT_ILIKE',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||||
@ -349,6 +351,8 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
|||||||
OPERATORS.NOT_EXISTS,
|
OPERATORS.NOT_EXISTS,
|
||||||
OPERATORS.REGEX,
|
OPERATORS.REGEX,
|
||||||
OPERATORS.NREGEX,
|
OPERATORS.NREGEX,
|
||||||
|
OPERATORS.ILIKE,
|
||||||
|
OPERATORS.NOTILIKE,
|
||||||
],
|
],
|
||||||
int64: [
|
int64: [
|
||||||
OPERATORS['='],
|
OPERATORS['='],
|
||||||
@ -389,6 +393,8 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
|||||||
OPERATORS.NOT_EXISTS,
|
OPERATORS.NOT_EXISTS,
|
||||||
OPERATORS.LIKE,
|
OPERATORS.LIKE,
|
||||||
OPERATORS.NLIKE,
|
OPERATORS.NLIKE,
|
||||||
|
OPERATORS.ILIKE,
|
||||||
|
OPERATORS.NOTILIKE,
|
||||||
OPERATORS['>='],
|
OPERATORS['>='],
|
||||||
OPERATORS['>'],
|
OPERATORS['>'],
|
||||||
OPERATORS['<='],
|
OPERATORS['<='],
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { orderByValueDelimiter } from '../OrderByFilter/utils';
|
import { orderByValueDelimiter } from '../OrderByFilter/utils';
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
export const tagRegexp = /^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bLIKE\b|\bNOT_LIKE\b|\bREGEX\b|\bNOT_REGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
|
export const tagRegexp = /^\s*(.*?)\s*(\bIN\b|\bNOT_IN\b|\bLIKE\b|\bNOT_LIKE\b|\bILIKE\b|\bNOT_ILIKE\b|\bREGEX\b|\bNOT_REGEX\b|=|!=|\bEXISTS\b|\bNOT_EXISTS\b|\bCONTAINS\b|\bNOT_CONTAINS\b|>=|>|<=|<|\bHAS\b|\bNHAS\b)\s*(.*)$/gi;
|
||||||
|
|
||||||
export function isInNInOperator(value: string): boolean {
|
export function isInNInOperator(value: string): boolean {
|
||||||
return value === OPERATORS.IN || value === OPERATORS.NIN;
|
return value === OPERATORS.IN || value === OPERATORS.NIN;
|
||||||
@ -79,6 +79,10 @@ export function getOperatorValue(op: string): string {
|
|||||||
return 'contains';
|
return 'contains';
|
||||||
case 'NOT_CONTAINS':
|
case 'NOT_CONTAINS':
|
||||||
return 'ncontains';
|
return 'ncontains';
|
||||||
|
case 'ILIKE':
|
||||||
|
return 'ilike';
|
||||||
|
case 'NOT_ILIKE':
|
||||||
|
return 'notilike';
|
||||||
default:
|
default:
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export const operatorTypeMapper: Record<string, OperatorType> = {
|
|||||||
[OPERATORS['>']]: 'SINGLE_VALUE',
|
[OPERATORS['>']]: 'SINGLE_VALUE',
|
||||||
[OPERATORS.LIKE]: 'SINGLE_VALUE',
|
[OPERATORS.LIKE]: 'SINGLE_VALUE',
|
||||||
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
|
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
|
||||||
|
[OPERATORS.ILIKE]: 'SINGLE_VALUE',
|
||||||
|
[OPERATORS.NOTILIKE]: 'SINGLE_VALUE',
|
||||||
[OPERATORS.REGEX]: 'SINGLE_VALUE',
|
[OPERATORS.REGEX]: 'SINGLE_VALUE',
|
||||||
[OPERATORS.NREGEX]: 'SINGLE_VALUE',
|
[OPERATORS.NREGEX]: 'SINGLE_VALUE',
|
||||||
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',
|
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',
|
||||||
|
|||||||
@ -28,6 +28,8 @@ var logOperators = map[v3.FilterOperator]string{
|
|||||||
v3.FilterOperatorNotIn: "NOT IN",
|
v3.FilterOperatorNotIn: "NOT IN",
|
||||||
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
|
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
|
||||||
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
|
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
|
||||||
|
v3.FilterOperatorILike: "ILIKE",
|
||||||
|
v3.FilterOperatorNotILike: "NOT ILIKE",
|
||||||
}
|
}
|
||||||
|
|
||||||
var skipExistsFilter = map[v3.FilterOperator]struct{}{
|
var skipExistsFilter = map[v3.FilterOperator]struct{}{
|
||||||
@ -175,6 +177,9 @@ func buildAttributeFilter(item v3.FilterItem) (string, error) {
|
|||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil
|
return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil
|
||||||
}
|
}
|
||||||
|
case v3.FilterOperatorILike, v3.FilterOperatorNotILike:
|
||||||
|
val := utils.QuoteEscapedString(fmt.Sprintf("%s", item.Value))
|
||||||
|
return fmt.Sprintf("%s %s '%s'", keyName, logsOp, val), nil
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("%s %s %s", keyName, logsOp, fmtVal), nil
|
return fmt.Sprintf("%s %s %s", keyName, logsOp, fmtVal), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -297,6 +297,36 @@ func Test_buildAttributeFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: "lower(body) LIKE lower('test')",
|
want: "lower(body) LIKE lower('test')",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build attribute filter like-body",
|
||||||
|
args: args{
|
||||||
|
item: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
Operator: v3.FilterOperatorILike,
|
||||||
|
Value: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "body ILIKE 'test'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build attribute filter not ilike-body",
|
||||||
|
args: args{
|
||||||
|
item: v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "body",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
Operator: v3.FilterOperatorNotILike,
|
||||||
|
Value: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "body NOT ILIKE 'test'",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|||||||
@ -327,6 +327,10 @@ func PrepareTimeseriesFilterQuery(start, end int64, mq *v3.BuilderQuery) (string
|
|||||||
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", item.Key.Key))
|
conditions = append(conditions, fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", item.Key.Key))
|
||||||
case v3.FilterOperatorNotExists:
|
case v3.FilterOperatorNotExists:
|
||||||
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", item.Key.Key))
|
conditions = append(conditions, fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", item.Key.Key))
|
||||||
|
case v3.FilterOperatorILike:
|
||||||
|
conditions = append(conditions, fmt.Sprintf("ilike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
|
||||||
|
case v3.FilterOperatorNotILike:
|
||||||
|
conditions = append(conditions, fmt.Sprintf("notILike(JSONExtractString(labels, '%s'), %s)", item.Key.Key, fmtVal))
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported filter operator")
|
return "", fmt.Errorf("unsupported filter operator")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,6 +140,54 @@ func TestPrepareTimeseriesFilterQuery(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedQueryContains: "SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1706428800000 AND unix_milli < 1706434026000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']",
|
expectedQueryContains: "SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1706428800000 AND unix_milli < 1706434026000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test prepare time series with filters and multiple group by",
|
||||||
|
builderQuery: &v3.BuilderQuery{
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
DataSource: v3.DataSourceMetrics,
|
||||||
|
AggregateAttribute: v3.AttributeKey{
|
||||||
|
Key: "http_requests",
|
||||||
|
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||||
|
Type: v3.AttributeKeyTypeUnspecified,
|
||||||
|
IsColumn: true,
|
||||||
|
IsJSON: false,
|
||||||
|
},
|
||||||
|
Temporality: v3.Cumulative,
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Operator: "AND",
|
||||||
|
Items: []v3.FilterItem{
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "service_name",
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
},
|
||||||
|
Operator: v3.FilterOperatorILike,
|
||||||
|
Value: "payment_service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: v3.AttributeKey{
|
||||||
|
Key: "endpoint",
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
},
|
||||||
|
Operator: v3.FilterOperatorNotILike,
|
||||||
|
Value: "payment_service",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBy: []v3.AttributeKey{{
|
||||||
|
Key: "service_name",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
}},
|
||||||
|
Expression: "A",
|
||||||
|
Disabled: false,
|
||||||
|
// remaining struct fields are not needed here
|
||||||
|
},
|
||||||
|
expectedQueryContains: "SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1706428800000 AND unix_milli < 1706434026000 AND ilike(JSONExtractString(labels, 'service_name'), 'payment_service') AND notILike(JSONExtractString(labels, 'endpoint'), 'payment_service')",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@ -25,6 +25,8 @@ var resourceLogOperators = map[v3.FilterOperator]string{
|
|||||||
v3.FilterOperatorNotIn: "NOT IN",
|
v3.FilterOperatorNotIn: "NOT IN",
|
||||||
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
|
v3.FilterOperatorExists: "mapContains(%s_%s, '%s')",
|
||||||
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
|
v3.FilterOperatorNotExists: "not mapContains(%s_%s, '%s')",
|
||||||
|
v3.FilterOperatorILike: "ILIKE",
|
||||||
|
v3.FilterOperatorNotILike: "NOT ILIKE",
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildResourceFilter builds a clickhouse filter string for resource labels
|
// buildResourceFilter builds a clickhouse filter string for resource labels
|
||||||
@ -51,7 +53,7 @@ func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value
|
|||||||
// we also want to treat %, _ as literals for contains
|
// we also want to treat %, _ as literals for contains
|
||||||
escapedStringValue := utils.QuoteEscapedStringForContains(lowerValue, false)
|
escapedStringValue := utils.QuoteEscapedStringForContains(lowerValue, false)
|
||||||
return fmt.Sprintf("%s %s '%%%s%%'", lowerSearchKey, logsOp, escapedStringValue)
|
return fmt.Sprintf("%s %s '%%%s%%'", lowerSearchKey, logsOp, escapedStringValue)
|
||||||
case v3.FilterOperatorLike, v3.FilterOperatorNotLike:
|
case v3.FilterOperatorLike, v3.FilterOperatorNotLike, v3.FilterOperatorILike, v3.FilterOperatorNotILike:
|
||||||
// this is required as clickhouseFormattedValue add's quotes to the string
|
// this is required as clickhouseFormattedValue add's quotes to the string
|
||||||
escapedStringValue := utils.QuoteEscapedString(lowerValue)
|
escapedStringValue := utils.QuoteEscapedString(lowerValue)
|
||||||
return fmt.Sprintf("%s %s '%s'", lowerSearchKey, logsOp, escapedStringValue)
|
return fmt.Sprintf("%s %s '%s'", lowerSearchKey, logsOp, escapedStringValue)
|
||||||
@ -120,9 +122,9 @@ func buildResourceIndexFilter(key string, op v3.FilterOperator, value interface{
|
|||||||
return fmt.Sprintf("labels like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
|
return fmt.Sprintf("labels like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
|
||||||
case v3.FilterOperatorNotEqual:
|
case v3.FilterOperatorNotEqual:
|
||||||
return fmt.Sprintf("labels not like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
|
return fmt.Sprintf("labels not like '%%%s\":\"%s%%'", key, fmtValEscapedForContains)
|
||||||
case v3.FilterOperatorLike:
|
case v3.FilterOperatorLike, v3.FilterOperatorILike:
|
||||||
return fmt.Sprintf("lower(labels) like '%%%s%%%s%%'", key, fmtValEscapedLower)
|
return fmt.Sprintf("lower(labels) like '%%%s%%%s%%'", key, fmtValEscapedLower)
|
||||||
case v3.FilterOperatorNotLike:
|
case v3.FilterOperatorNotLike, v3.FilterOperatorNotILike:
|
||||||
// cannot apply not contains x%y as y can be somewhere else
|
// cannot apply not contains x%y as y can be somewhere else
|
||||||
return ""
|
return ""
|
||||||
case v3.FilterOperatorContains:
|
case v3.FilterOperatorContains:
|
||||||
|
|||||||
@ -208,6 +208,15 @@ func Test_buildResourceIndexFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: `lower(labels) like '%service.name%application%_test%'`,
|
want: `lower(labels) like '%service.name%application%_test%'`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test not like with % and _",
|
||||||
|
args: args{
|
||||||
|
key: "service.name",
|
||||||
|
op: v3.FilterOperatorILike,
|
||||||
|
value: "application%_test",
|
||||||
|
},
|
||||||
|
want: `lower(labels) like '%service.name%application%_test%'`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "test contains",
|
name: "test contains",
|
||||||
args: args{
|
args: args{
|
||||||
|
|||||||
@ -30,6 +30,8 @@ var tracesOperatorMappingV3 = map[v3.FilterOperator]string{
|
|||||||
v3.FilterOperatorNotContains: "NOT ILIKE",
|
v3.FilterOperatorNotContains: "NOT ILIKE",
|
||||||
v3.FilterOperatorExists: "mapContains(%s, '%s')",
|
v3.FilterOperatorExists: "mapContains(%s, '%s')",
|
||||||
v3.FilterOperatorNotExists: "NOT mapContains(%s, '%s')",
|
v3.FilterOperatorNotExists: "NOT mapContains(%s, '%s')",
|
||||||
|
v3.FilterOperatorILike: "ILIKE",
|
||||||
|
v3.FilterOperatorNotILike: "NOT ILIKE",
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClickHouseTracesColumnType(columnType v3.AttributeKeyType) string {
|
func getClickHouseTracesColumnType(columnType v3.AttributeKeyType) string {
|
||||||
|
|||||||
@ -284,7 +284,7 @@ func Test_buildTracesFilterQuery(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test contains, ncontains, like, nlike, regex, nregex",
|
name: "Test contains, ncontains, like, nlike, regex, nregex, ilike, nilike",
|
||||||
args: args{
|
args: args{
|
||||||
fs: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
fs: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.%", Operator: v3.FilterOperatorContains},
|
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.%", Operator: v3.FilterOperatorContains},
|
||||||
@ -293,10 +293,12 @@ func Test_buildTracesFilterQuery(t *testing.T) {
|
|||||||
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102", Operator: v3.FilterOperatorNotLike},
|
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102", Operator: v3.FilterOperatorNotLike},
|
||||||
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/mypath", Operator: v3.FilterOperatorRegex},
|
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/mypath", Operator: v3.FilterOperatorRegex},
|
||||||
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/health.*", Operator: v3.FilterOperatorNotRegex},
|
{Key: v3.AttributeKey{Key: "path", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "/health.*", Operator: v3.FilterOperatorNotRegex},
|
||||||
|
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: v3.FilterOperatorILike},
|
||||||
|
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102", Operator: v3.FilterOperatorNotLike},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
want: "attributes_string['host'] ILIKE '%102.\\%%' AND attributes_string['host'] NOT ILIKE '%103\\_%' AND attributes_string['host'] ILIKE '102.' AND attributes_string['host'] NOT ILIKE '102' AND " +
|
want: "attributes_string['host'] ILIKE '%102.\\%%' AND attributes_string['host'] NOT ILIKE '%103\\_%' AND attributes_string['host'] ILIKE '102.' AND attributes_string['host'] NOT ILIKE '102' AND " +
|
||||||
"match(`attribute_string_path`, '/mypath') AND NOT match(`attribute_string_path`, '/health.*')",
|
"match(`attribute_string_path`, '/mypath') AND NOT match(`attribute_string_path`, '/health.*') AND attributes_string['host'] ILIKE '102.' AND attributes_string['host'] NOT ILIKE '102'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test exists, nexists",
|
name: "Test exists, nexists",
|
||||||
|
|||||||
@ -1227,6 +1227,9 @@ const (
|
|||||||
|
|
||||||
FilterOperatorHas FilterOperator = "has"
|
FilterOperatorHas FilterOperator = "has"
|
||||||
FilterOperatorNotHas FilterOperator = "nhas"
|
FilterOperatorNotHas FilterOperator = "nhas"
|
||||||
|
|
||||||
|
FilterOperatorILike FilterOperator = "ilike"
|
||||||
|
FilterOperatorNotILike FilterOperator = "notilike"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FilterItem struct {
|
type FilterItem struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user