feat: extend expr

This commit is contained in:
dualfroz committed 2026-09-04 11:03:26 +02:00
1 parent 273fca5ec6
commit d06bc9de07
1 file changed
+163
+163
View File
@@ -315,3 +315,166 @@ test "nested parentheses and multiple not" {
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"));
}