feat: add parseIn and parseBetween to expr
This commit is contained in:
1 parent
d4a836ad27
commit
273fca5ec6
1 file changed
+160
+160
@@ -155,3 +155,163 @@ const Parser = struct {
|
|||||||
const value = try rawValue.dupe(self.allocator);
|
const value = try rawValue.dupe(self.allocator);
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
|
const cond = Condition{ .field = fieldCopy, .value = value };
|
||||||
|
if (std.mem.eql(u8, op, "eq")) return .{ .eq = cond };
|
||||||
|
if (std.mem.eql(u8, op, "ne")) return .{ .ne = cond };
|
||||||
|
if (std.mem.eql(u8, op, "lt")) return .{ .lt = cond };
|
||||||
|
if (std.mem.eql(u8, op, "lte")) return .{ .lte = cond };
|
||||||
|
if (std.mem.eql(u8, op, "gt")) return .{ .gt = cond };
|
||||||
|
if (std.mem.eql(u8, op, "gte")) return .{ .gte = cond };
|
||||||
|
if (std.mem.eql(u8, op, "like")) {
|
||||||
|
if (value != .text) return error.InvalidValue;
|
||||||
|
return .{ .like = cond };
|
||||||
|
}
|
||||||
|
return error.UnknownOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `field in (v1 v2 ...)`: desugars to an `or_` of per-value `eq`
|
||||||
|
/// leaves against the same field, sharing the one field-name copy the
|
||||||
|
/// caller already made (Condition never owns `field`, so aliasing it
|
||||||
|
/// across every leaf is safe). A single value collapses to a bare `eq`
|
||||||
|
/// leaf, the same collapse `parseAnd`/`parseOr` already do for a
|
||||||
|
/// one-element and/or chain - lets a one-value `in` use an index like
|
||||||
|
/// `eq` does, since `findIndexableLeaf`/`findCompositeMatch` never look
|
||||||
|
/// inside `or_`. An empty list, `field in ()`, is not a syntax error:
|
||||||
|
/// it desugars to an `or_` over zero leaves, which `Predicate.evaluate`
|
||||||
|
/// already folds to `false` for an empty `preds` slice - matching SQL's
|
||||||
|
/// "IN () is always false" without a special case here.
|
||||||
|
fn parseIn(self: *Parser, field: []const u8) ParseError!Predicate {
|
||||||
|
if (self.cur.kind != .lparen) return if (self.cur.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
var list = std.ArrayList(Predicate).init(self.allocator);
|
||||||
|
while (self.cur.kind != .rparen) {
|
||||||
|
if (self.cur.kind != .word) return if (self.cur.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
|
||||||
|
const rawValue = rowmod.Value.parseCli(self.cur.text) catch return error.InvalidValue;
|
||||||
|
const value = try rawValue.dupe(self.allocator);
|
||||||
|
self.advance();
|
||||||
|
try list.append(.{ .eq = .{ .field = field, .value = value } });
|
||||||
|
}
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
if (list.items.len == 1) return list.items[0];
|
||||||
|
return .{ .or_ = try list.toOwnedSlice() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `field between lo and hi`: desugars to `field gte lo and field lte
|
||||||
|
/// hi` (inclusive both ends, matching SQL BETWEEN). The `and` here is
|
||||||
|
/// consumed as part of this leaf's own grammar, not the outer
|
||||||
|
/// `parseAnd` binary connective, so it never nests a further `and_`
|
||||||
|
/// level - by the time control returns to the caller, `self.cur` is
|
||||||
|
/// whatever token follows `hi` (rparen/eof/"and"/"or"), decided purely
|
||||||
|
/// by having advanced past every token this leaf owns.
|
||||||
|
fn parseBetween(self: *Parser, field: []const u8) ParseError!Predicate {
|
||||||
|
if (self.cur.kind != .word) return error.UnexpectedEnd;
|
||||||
|
const rawLow = rowmod.Value.parseCli(self.cur.text) catch return error.InvalidValue;
|
||||||
|
const low = try rawLow.dupe(self.allocator);
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
if (!isKeyword(self.cur, "and")) return if (self.cur.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
if (self.cur.kind != .word) return error.UnexpectedEnd;
|
||||||
|
const rawHigh = rowmod.Value.parseCli(self.cur.text) catch return error.InvalidValue;
|
||||||
|
const high = try rawHigh.dupe(self.allocator);
|
||||||
|
self.advance();
|
||||||
|
|
||||||
|
const bounds = try self.allocator.alloc(Predicate, 2);
|
||||||
|
bounds[0] = .{ .gte = .{ .field = field, .value = low } };
|
||||||
|
bounds[1] = .{ .lte = .{ .field = field, .value = high } };
|
||||||
|
return .{ .and_ = bounds };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parses a full `table select` predicate expression, e.g.
|
||||||
|
/// `city eq s:warsaw and (age gte i:30 or not age lt i:10)`. The returned
|
||||||
|
/// Parsed owns every slice/pointer the Predicate tree references - `text`
|
||||||
|
/// itself does not need to outlive the call.
|
||||||
|
pub fn parse(backing: Allocator, text: []const u8) ParseError!Parsed {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(backing);
|
||||||
|
errdefer arena.deinit();
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
var p = Parser{ .lexer = .{ .src = text }, .allocator = allocator, .cur = undefined };
|
||||||
|
p.cur = p.lexer.next();
|
||||||
|
|
||||||
|
const pred = try p.parseOr();
|
||||||
|
if (p.cur.kind != .eof) return error.UnexpectedToken;
|
||||||
|
|
||||||
|
return .{ .arena = arena, .predicate = pred };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evalStr(allocator: Allocator, text: []const u8, row: *const rowmod.Row) !bool {
|
||||||
|
var parsed = try parse(allocator, text);
|
||||||
|
defer parsed.deinit();
|
||||||
|
return parsed.predicate.evaluate(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parses a single condition" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("city", .{ .text = "warsaw" });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city eq s:warsaw", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city eq s:krakow", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "and binds tighter than or" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("a", .{ .int = 1 });
|
||||||
|
try row.set("b", .{ .int = 2 });
|
||||||
|
try row.set("c", .{ .int = 99 });
|
||||||
|
|
||||||
|
// a eq 1 or b eq 2 and c eq 3 == (a eq 1) or ((b eq 2) and (c eq 3))
|
||||||
|
// b eq 2 is true but c eq 3 is false, so the and-branch is false; the
|
||||||
|
// overall result must still be true because of the leading `a eq 1`.
|
||||||
|
try std.testing.expect(try evalStr(allocator, "a eq i:1 or b eq i:2 and c eq i:3", &row));
|
||||||
|
|
||||||
|
var row2 = rowmod.Row.init(allocator);
|
||||||
|
defer row2.deinit();
|
||||||
|
try row2.set("a", .{ .int = 0 });
|
||||||
|
try row2.set("b", .{ .int = 2 });
|
||||||
|
try row2.set("c", .{ .int = 99 });
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "a eq i:1 or b eq i:2 and c eq i:3", &row2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parentheses override default precedence" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("a", .{ .int = 0 });
|
||||||
|
try row.set("b", .{ .int = 2 });
|
||||||
|
try row.set("c", .{ .int = 5 });
|
||||||
|
|
||||||
|
// (a eq 1 or b eq 2) and c eq 5 - forces the or to evaluate before and.
|
||||||
|
try std.testing.expect(try evalStr(allocator, "(a eq i:1 or b eq i:2) and c eq i:5", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "(a eq i:1 or b eq i:2) and c eq i:6", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "not negates the following term" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("age", .{ .int = 20 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "not age eq i:10", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "not age eq i:20", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "not (age eq i:10 or age eq i:20)", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "nested parentheses and multiple not" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("x", .{ .int = 5 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "not not x eq i:5", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "((x eq i:5))", &row));
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user