From d06bc9de07086961c0765a685eb6dde489b130f2 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 11:03:26 +0200 Subject: [PATCH] feat: extend expr --- src/expr.zig | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/src/expr.zig b/src/expr.zig index 059d221..0d9060e 100644 --- a/src/expr.zig +++ b/src/expr.zig @@ -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")); +} +