feat: extend expr
This commit is contained in:
1 parent
273fca5ec6
commit
d06bc9de07
1 file changed
+163
+163
@@ -315,3 +315,166 @@ test "nested parentheses and multiple not" {
|
|||||||
try std.testing.expect(try evalStr(allocator, "((x eq i:5))", &row));
|
try std.testing.expect(try evalStr(allocator, "((x eq i:5))", &row));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "range and inequality operators parse and evaluate" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("score", .{ .int = 42 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "score gt i:10", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "score gte i:42", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "score lt i:100", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "score lte i:42", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "score ne i:0", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "like matches SQL-style wildcards and rejects non-text patterns" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("name", .{ .text = "Ada Lovelace" });
|
||||||
|
try row.set("age", .{ .int = 36 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "name like s:Ada%", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "name like s:%Lovelace", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "name like s:%Love%", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "name like s:Boris%", &row));
|
||||||
|
// like against a non-text row field never matches, it does not error.
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "age like s:3%", &row));
|
||||||
|
// like combines with and/or/not/parens exactly like every other operator.
|
||||||
|
try std.testing.expect(try evalStr(allocator, "name like s:Ada% and age eq i:36", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "not name like s:Ada%", &row));
|
||||||
|
|
||||||
|
try std.testing.expectError(error.InvalidValue, parse(allocator, "name like i:5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "is_null matches a row missing the field, is_not_null matches a row that has it" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var withCity = rowmod.Row.init(allocator);
|
||||||
|
defer withCity.deinit();
|
||||||
|
try withCity.set("city", .{ .text = "warsaw" });
|
||||||
|
|
||||||
|
var withoutCity = rowmod.Row.init(allocator);
|
||||||
|
defer withoutCity.deinit();
|
||||||
|
try withoutCity.set("age", .{ .int = 30 });
|
||||||
|
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city is_null", &withCity));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city is_not_null", &withCity));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city is_null", &withoutCity));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city is_not_null", &withoutCity));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "is_null treats an explicit Value.none the same as a missing field" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("nickname", .{ .none = {} });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "nickname is_null", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "nickname is_not_null", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "is_null and is_not_null take no trailing value and combine with and/or/not" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("age", .{ .int = 30 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city is_null and age eq i:30", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city is_not_null or age eq i:99", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "not city is_not_null", &row));
|
||||||
|
|
||||||
|
var parsed = try parse(allocator, "city is_null");
|
||||||
|
defer parsed.deinit();
|
||||||
|
try std.testing.expect(parsed.predicate == .is_null);
|
||||||
|
try std.testing.expectEqualStrings("city", parsed.predicate.is_null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "field name copy and value copy outlive the source text" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("name", .{ .text = "ada" });
|
||||||
|
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
const src = try std.fmt.bufPrint(&buf, "name eq s:ada", .{});
|
||||||
|
var parsed = try parse(allocator, src);
|
||||||
|
// Overwrite the source buffer entirely: if the parser had kept slices
|
||||||
|
// into `src` instead of arena-owned copies, this would corrupt the
|
||||||
|
// predicate's field name/value in place.
|
||||||
|
@memset(&buf, 'X');
|
||||||
|
try std.testing.expect(parsed.predicate.evaluate(&row));
|
||||||
|
parsed.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rejects malformed expressions" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
try std.testing.expectError(error.UnexpectedEnd, parse(allocator, "age eq"));
|
||||||
|
try std.testing.expectError(error.UnexpectedEnd, parse(allocator, "age"));
|
||||||
|
try std.testing.expectError(error.UnknownOperator, parse(allocator, "age foo i:1"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, parse(allocator, "age eq i:notanumber"));
|
||||||
|
try std.testing.expectError(error.UnexpectedToken, parse(allocator, "(age eq i:1"));
|
||||||
|
try std.testing.expectError(error.UnexpectedToken, parse(allocator, "age eq i:1)"));
|
||||||
|
try std.testing.expectError(error.UnexpectedEnd, parse(allocator, ""));
|
||||||
|
try std.testing.expectError(error.UnexpectedToken, parse(allocator, "age eq i:1 age eq i:2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "in matches any value in the list" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("city", .{ .text = "krakow" });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city in (s:warsaw s:krakow s:gdansk)", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city in (s:warsaw s:gdansk)", &row));
|
||||||
|
// Empty list is always false, mirroring SQL's `IN ()`.
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "city in ()", &row));
|
||||||
|
// Combines with and/or/not/parens like every other operator.
|
||||||
|
try std.testing.expect(try evalStr(allocator, "city in (s:krakow) and not city eq s:warsaw", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "not city in (s:warsaw s:krakow)", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "in with a single value collapses to a bare eq leaf" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var parsed = try parse(allocator, "city in (s:warsaw)");
|
||||||
|
defer parsed.deinit();
|
||||||
|
try std.testing.expect(parsed.predicate == .eq);
|
||||||
|
try std.testing.expectEqualStrings("city", parsed.predicate.eq.field);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "between is inclusive on both ends" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var row = rowmod.Row.init(allocator);
|
||||||
|
defer row.deinit();
|
||||||
|
try row.set("age", .{ .int = 30 });
|
||||||
|
|
||||||
|
try std.testing.expect(try evalStr(allocator, "age between i:30 and i:40", &row));
|
||||||
|
try std.testing.expect(try evalStr(allocator, "age between i:20 and i:30", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "age between i:31 and i:40", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "age between i:10 and i:29", &row));
|
||||||
|
// Combines with and/or/not/parens, and the leaf's own "and" does not
|
||||||
|
// leak into the outer and/or grammar as a second top-level conjunct.
|
||||||
|
try std.testing.expect(try evalStr(allocator, "age between i:25 and i:35 and age ne i:0", &row));
|
||||||
|
try std.testing.expect(!try evalStr(allocator, "not age between i:25 and i:35", &row));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "between desugars to a two-leaf and_ of gte/lte" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var parsed = try parse(allocator, "age between i:1 and i:2");
|
||||||
|
defer parsed.deinit();
|
||||||
|
try std.testing.expect(parsed.predicate == .and_);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), parsed.predicate.and_.len);
|
||||||
|
try std.testing.expect(parsed.predicate.and_[0] == .gte);
|
||||||
|
try std.testing.expect(parsed.predicate.and_[1] == .lte);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "in and between reject malformed input" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
try std.testing.expectError(error.UnexpectedToken, parse(allocator, "city in s:warsaw"));
|
||||||
|
try std.testing.expectError(error.UnexpectedEnd, parse(allocator, "city in (s:warsaw"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, parse(allocator, "age in (i:notanumber)"));
|
||||||
|
try std.testing.expectError(error.UnexpectedEnd, parse(allocator, "age between i:1"));
|
||||||
|
try std.testing.expectError(error.UnexpectedToken, parse(allocator, "age between i:1 or i:2"));
|
||||||
|
try std.testing.expectError(error.InvalidValue, parse(allocator, "age between i:1 and i:notanumber"));
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user