From 54dd858adfb0e4a4214817326ca04c1e6ce9146b Mon Sep 17 00:00:00 2001 From: Ildar Karymov Date: Mon, 10 Feb 2025 01:03:22 +0100 Subject: [PATCH] Add tilde (like) operator --- README.md | 11 ++--- dumbql.go | 13 +++--- query/ast.go | 3 ++ query/grammar.peg | 2 +- query/parser.gen.go | 90 ++++++++++++++++++++--------------------- query/parser_helpers.go | 2 + query/sql.go | 2 + query/sql_test.go | 7 ++++ 8 files changed, 73 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 509c425..3dbad55 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,12 @@ period_months < 4 ### Field expression operators -| Operator | Meaning | Supported types | -|----------------------|---------------|------------------------------| -| `:` or `=` | Equal, one of | `int64`, `float64`, `string` | -| `!=` or `!:` | Not equal | `int64`, `float64`, `string` | -| `>`, `>=`, `<`, `<=` | Comparison | `int64`, `float64` | +| Operator | Meaning | Supported types | +|----------------------|-------------------------------|------------------------------| +| `:` or `=` | Equal, one of | `int64`, `float64`, `string` | +| `!=` or `!:` | Not equal | `int64`, `float64`, `string` | +| `~` | “Like” or “contains” operator | `string` | +| `>`, `>=`, `<`, `<=` | Comparison | `int64`, `float64` | ### Boolean operators diff --git a/dumbql.go b/dumbql.go index e520ef8..3c36c0f 100644 --- a/dumbql.go +++ b/dumbql.go @@ -39,7 +39,7 @@ // SingleCharEscape <- ["\\/bfnrt] // UnicodeEscape <- 'u' HexDigit HexDigit HexDigit HexDigit // HexDigit <- [0-9a-f]i -// CmpOp <- ( ">=" / ">" / "<=" / "<" / "!:" / "!=" / ":" / "=" ) +// CmpOp <- ( ">=" / ">" / "<=" / "<" / "!:" / "!=" / ":" / "=" / "~" ) // OneOfExpr <- '[' _ values:(OneOfValues)? _ ']' // OneOfValues <- head:OneOfValue tail:(_ ',' _ OneOfValue)* // _ <- [ \t\r\n]* @@ -58,11 +58,12 @@ // // # Field expression operators // -// | Operator | Meaning | Supported types | -// |----------------------|---------------|------------------------------| -// | `:` or `=` | Equal, one of | `int64`, `float64`, `string` | -// | `!=` or `!:` | Not equal | `int64`, `float64`, `string` | -// | `>`, `>=`, `<`, `<=` | Comparison | `int64`, `float64` | +// | Operator | Meaning | Supported types | +// |----------------------|-------------------------------|------------------------------| +// | `:` or `=` | Equal, one of | `int64`, `float64`, `string` | +// | `!=` or `!:` | Not equal | `int64`, `float64`, `string` | +// | `~` | “Like” or “contains” operator | `string` | +// | `>`, `>=`, `<`, `<=` | Comparison | `int64`, `float64` | // // # Boolean operators // diff --git a/query/ast.go b/query/ast.go index 855c48e..bfa0344 100644 --- a/query/ast.go +++ b/query/ast.go @@ -122,6 +122,7 @@ const ( GreaterThanOrEqual LessThan LessThanOrEqual + Like ) func (c FieldOperator) String() string { @@ -138,6 +139,8 @@ func (c FieldOperator) String() string { return "<" case LessThanOrEqual: return "<=" + case Like: + return "~" default: return "unknown!" } diff --git a/query/grammar.peg b/query/grammar.peg index 9ba81cd..5cdd24b 100644 --- a/query/grammar.peg +++ b/query/grammar.peg @@ -27,7 +27,7 @@ EscapeSequence <- SingleCharEscape / UnicodeEscape SingleCharEscape <- ["\\/bfnrt] UnicodeEscape <- 'u' HexDigit HexDigit HexDigit HexDigit HexDigit <- [0-9a-f]i -CmpOp <- ( ">=" / ">" / "<=" / "<" / "!:" / "!=" / ":" / "=" ) +CmpOp <- ( ">=" / ">" / "<=" / "<" / "!:" / "!=" / ":" / "=" / "~" ) OneOfExpr <- '[' _ values:(OneOfValues)? _ ']' { return parseOneOfExpression(values) } OneOfValues <- head:OneOfValue tail:(_ ',' _ OneOfValue)* { return parseOneOfValues(head, tail) } _ <- [ \t\r\n]* diff --git a/query/parser.gen.go b/query/parser.gen.go index 60c1d92..87870e6 100644 --- a/query/parser.gen.go +++ b/query/parser.gen.go @@ -28,9 +28,9 @@ var g = &grammar{ pos: position{line: 5, col: 24, offset: 46}, exprs: []any{ &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -46,9 +46,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -85,9 +85,9 @@ var g = &grammar{ pos: position{line: 6, col: 43, offset: 160}, exprs: []any{ &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -112,9 +112,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -159,9 +159,9 @@ var g = &grammar{ pos: position{line: 8, col: 43, offset: 320}, exprs: []any{ &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -190,9 +190,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -241,9 +241,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -350,9 +350,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -403,8 +403,8 @@ var g = &grammar{ }, &charClassMatcher{ pos: position{line: 30, col: 66, offset: 1826}, - val: "[:=]", - chars: []rune{':', '='}, + val: "[:=~]", + chars: []rune{':', '=', '~'}, ignoreCase: false, inverted: false, }, @@ -412,9 +412,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -428,21 +428,21 @@ var g = &grammar{ pos: position{line: 15, col: 24, offset: 893}, alternatives: []any{ &actionExpr{ - pos: position{line: 31, col: 24, offset: 1861}, + pos: position{line: 31, col: 24, offset: 1867}, run: (*parser).callonPrimary32, expr: &seqExpr{ - pos: position{line: 31, col: 24, offset: 1861}, + pos: position{line: 31, col: 24, offset: 1867}, exprs: []any{ &litMatcher{ - pos: position{line: 31, col: 24, offset: 1861}, + pos: position{line: 31, col: 24, offset: 1867}, val: "[", ignoreCase: false, want: "\"[\"", }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -450,18 +450,18 @@ var g = &grammar{ }, }, &labeledExpr{ - pos: position{line: 31, col: 30, offset: 1867}, + pos: position{line: 31, col: 30, offset: 1873}, label: "values", expr: &zeroOrOneExpr{ - pos: position{line: 31, col: 37, offset: 1874}, + pos: position{line: 31, col: 37, offset: 1880}, expr: &actionExpr{ - pos: position{line: 32, col: 24, offset: 1978}, + pos: position{line: 32, col: 24, offset: 1984}, run: (*parser).callonPrimary39, expr: &seqExpr{ - pos: position{line: 32, col: 24, offset: 1978}, + pos: position{line: 32, col: 24, offset: 1984}, exprs: []any{ &labeledExpr{ - pos: position{line: 32, col: 24, offset: 1978}, + pos: position{line: 32, col: 24, offset: 1984}, label: "head", expr: &choiceExpr{ pos: position{line: 16, col: 24, offset: 957}, @@ -716,17 +716,17 @@ var g = &grammar{ }, }, &labeledExpr{ - pos: position{line: 32, col: 40, offset: 1994}, + pos: position{line: 32, col: 40, offset: 2000}, label: "tail", expr: &zeroOrMoreExpr{ - pos: position{line: 32, col: 45, offset: 1999}, + pos: position{line: 32, col: 45, offset: 2005}, expr: &seqExpr{ - pos: position{line: 32, col: 46, offset: 2000}, + pos: position{line: 32, col: 46, offset: 2006}, exprs: []any{ &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -734,15 +734,15 @@ var g = &grammar{ }, }, &litMatcher{ - pos: position{line: 32, col: 48, offset: 2002}, + pos: position{line: 32, col: 48, offset: 2008}, val: ",", ignoreCase: false, want: "\",\"", }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -1010,9 +1010,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -1020,7 +1020,7 @@ var g = &grammar{ }, }, &litMatcher{ - pos: position{line: 31, col: 54, offset: 1891}, + pos: position{line: 31, col: 54, offset: 1897}, val: "]", ignoreCase: false, want: "\"]\"", @@ -1299,9 +1299,9 @@ var g = &grammar{ want: "\"(\"", }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -1317,9 +1317,9 @@ var g = &grammar{ }, }, &zeroOrMoreExpr{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, expr: &charClassMatcher{ - pos: position{line: 33, col: 24, offset: 2095}, + pos: position{line: 33, col: 24, offset: 2101}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, diff --git a/query/parser_helpers.go b/query/parser_helpers.go index 3b36c8c..70908ce 100644 --- a/query/parser_helpers.go +++ b/query/parser_helpers.go @@ -30,6 +30,8 @@ func resolveFieldOperator(op any) (FieldOperator, error) { return NotEqual, nil case ":", "=": return Equal, nil + case "~": + return Like, nil default: return 0, fmt.Errorf("unknown compare operator %q", op) } diff --git a/query/sql.go b/query/sql.go index f8ae794..a905fe0 100644 --- a/query/sql.go +++ b/query/sql.go @@ -44,6 +44,8 @@ func (f *FieldExpr) ToSql() (string, []any, error) { //nolint:revive sqlizer = sq.Lt{field: value} case LessThanOrEqual: sqlizer = sq.LtOrEq{field: value} + case Like: + sqlizer = sq.Like{field: value} default: return "", nil, fmt.Errorf("unknown operator %q", f.Op) } diff --git a/query/sql_test.go b/query/sql_test.go index ba4bd24..4725ce8 100644 --- a/query/sql_test.go +++ b/query/sql_test.go @@ -81,6 +81,13 @@ func TestToSql(t *testing.T) { //nolint:funlen want: "SELECT * FROM dummy_table WHERE NOT (status = ? AND eps < ?)", wantArgs: []any{int64(200), 0.003}, }, + { + input: `name~"John"`, + want: "SELECT * FROM dummy_table WHERE name LIKE ?", + wantArgs: []any{ + "John", + }, + }, } for _, test := range tests {