568 lines
24 KiB
Zig
568 lines
24 KiB
Zig
const std = @import("std");
|
|
const rowmod = @import("row.zig");
|
|
const idx = @import("index.zig");
|
|
const querymod = @import("query.zig");
|
|
const dbmod = @import("db.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const Predicate = querymod.Predicate;
|
|
const Condition = querymod.Condition;
|
|
|
|
/// Errors a caller can act on when a `table select` expression is rejected.
|
|
/// Parsing never panics on malformed input - every failure mode a user can
|
|
/// trigger from the CLI maps to one of these.
|
|
pub const ParseError = error{
|
|
UnexpectedToken,
|
|
UnexpectedEnd,
|
|
InvalidIdentifier,
|
|
UnknownOperator,
|
|
InvalidValue,
|
|
} || Allocator.Error;
|
|
|
|
/// Owns every allocation a parsed Predicate tree depends on (field-name and
|
|
/// text-value copies, and_/or_ slices, not_ boxes) in one arena, so freeing
|
|
/// is a single `deinit()` regardless of how deep the tree is.
|
|
pub const Parsed = struct {
|
|
arena: std.heap.ArenaAllocator,
|
|
predicate: Predicate,
|
|
|
|
pub fn deinit(self: *Parsed) void {
|
|
self.arena.deinit();
|
|
}
|
|
};
|
|
|
|
const TokenKind = enum { lparen, rparen, word, eof };
|
|
|
|
const Token = struct {
|
|
kind: TokenKind,
|
|
text: []const u8,
|
|
};
|
|
|
|
const Lexer = struct {
|
|
src: []const u8,
|
|
pos: usize = 0,
|
|
|
|
fn next(self: *Lexer) Token {
|
|
while (self.pos < self.src.len and std.ascii.isWhitespace(self.src[self.pos])) : (self.pos += 1) {}
|
|
if (self.pos >= self.src.len) return .{ .kind = .eof, .text = "" };
|
|
|
|
const c = self.src[self.pos];
|
|
if (c == '(') {
|
|
self.pos += 1;
|
|
return .{ .kind = .lparen, .text = "(" };
|
|
}
|
|
if (c == ')') {
|
|
self.pos += 1;
|
|
return .{ .kind = .rparen, .text = ")" };
|
|
}
|
|
|
|
const start = self.pos;
|
|
while (self.pos < self.src.len) {
|
|
const ch = self.src[self.pos];
|
|
if (std.ascii.isWhitespace(ch) or ch == '(' or ch == ')') break;
|
|
self.pos += 1;
|
|
}
|
|
return .{ .kind = .word, .text = self.src[start..self.pos] };
|
|
}
|
|
};
|
|
|
|
fn isKeyword(tok: Token, kw: []const u8) bool {
|
|
return tok.kind == .word and std.mem.eql(u8, tok.text, kw);
|
|
}
|
|
|
|
/// Recursive-descent parser for the `table select` predicate language:
|
|
///
|
|
/// expr := and ("or" and)*
|
|
/// and := unary ("and" unary)*
|
|
/// unary := "not" unary | primary
|
|
/// primary := "(" expr ")" | field op value | field ("is_null" | "is_not_null")
|
|
/// op := eq | ne | lt | lte | gt | gte | like
|
|
///
|
|
/// Precedence (loosest to tightest) is or < and < not, matching every other
|
|
/// C-family boolean expression language, so `a eq 1 or b eq 2 and not c eq 3`
|
|
/// parses as `a eq 1 or (b eq 2 and (not (c eq 3)))`.
|
|
const Parser = struct {
|
|
lexer: Lexer,
|
|
allocator: Allocator,
|
|
cur: Token,
|
|
|
|
fn advance(self: *Parser) void {
|
|
self.cur = self.lexer.next();
|
|
}
|
|
|
|
fn parseOr(self: *Parser) ParseError!Predicate {
|
|
var list = std.ArrayList(Predicate).init(self.allocator);
|
|
try list.append(try self.parseAnd());
|
|
while (isKeyword(self.cur, "or")) {
|
|
self.advance();
|
|
try list.append(try self.parseAnd());
|
|
}
|
|
if (list.items.len == 1) return list.items[0];
|
|
return .{ .or_ = try list.toOwnedSlice() };
|
|
}
|
|
|
|
fn parseAnd(self: *Parser) ParseError!Predicate {
|
|
var list = std.ArrayList(Predicate).init(self.allocator);
|
|
try list.append(try self.parseUnary());
|
|
while (isKeyword(self.cur, "and")) {
|
|
self.advance();
|
|
try list.append(try self.parseUnary());
|
|
}
|
|
if (list.items.len == 1) return list.items[0];
|
|
return .{ .and_ = try list.toOwnedSlice() };
|
|
}
|
|
|
|
fn parseUnary(self: *Parser) ParseError!Predicate {
|
|
if (isKeyword(self.cur, "not")) {
|
|
self.advance();
|
|
const inner = try self.parseUnary();
|
|
const boxed = try self.allocator.create(Predicate);
|
|
boxed.* = inner;
|
|
return .{ .not_ = boxed };
|
|
}
|
|
return self.parsePrimary();
|
|
}
|
|
|
|
fn parsePrimary(self: *Parser) ParseError!Predicate {
|
|
if (self.cur.kind == .lparen) {
|
|
self.advance();
|
|
const inner = try self.parseOr();
|
|
if (self.cur.kind != .rparen) return error.UnexpectedToken;
|
|
self.advance();
|
|
return inner;
|
|
}
|
|
return self.parseCondition();
|
|
}
|
|
|
|
fn parseCondition(self: *Parser) ParseError!Predicate {
|
|
if (self.cur.kind != .word) return if (self.cur.kind == .eof) error.UnexpectedEnd else error.UnexpectedToken;
|
|
const field = self.cur.text;
|
|
if (!idx.validIdentifier(field)) return error.InvalidIdentifier;
|
|
const fieldCopy = try self.allocator.dupe(u8, field);
|
|
self.advance();
|
|
|
|
if (self.cur.kind != .word) return error.UnexpectedEnd;
|
|
const op = self.cur.text;
|
|
self.advance();
|
|
|
|
if (std.mem.eql(u8, op, "in")) return self.parseIn(fieldCopy);
|
|
if (std.mem.eql(u8, op, "between")) return self.parseBetween(fieldCopy);
|
|
if (std.mem.eql(u8, op, "is_null")) return .{ .is_null = fieldCopy };
|
|
if (std.mem.eql(u8, op, "is_not_null")) return .{ .is_not_null = fieldCopy };
|
|
|
|
if (self.cur.kind != .word) return error.UnexpectedEnd;
|
|
const rawValue = rowmod.Value.parseCli(self.cur.text) catch return error.InvalidValue;
|
|
const value = try rawValue.dupe(self.allocator);
|
|
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));
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !dbmod.Db {
|
|
const dbPath = try tmp.dir.realpathAlloc(allocator, ".");
|
|
defer allocator.free(dbPath);
|
|
return dbmod.Db.open(allocator, dbPath, .{});
|
|
}
|
|
|
|
test "a single-value in reaches Table.select through an index like eq does" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
var db = try openTemp(&tmp, allocator);
|
|
defer db.close();
|
|
|
|
var table = try querymod.Table.open(&db, allocator, "cities");
|
|
defer table.deinit();
|
|
try table.createIndex("city");
|
|
try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "oslo" } }});
|
|
try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "bergen" } }});
|
|
|
|
var parsed = try parse(allocator, "city in (s:oslo)");
|
|
defer parsed.deinit();
|
|
|
|
var plan = try table.explain(allocator, parsed.predicate);
|
|
defer plan.deinit(allocator);
|
|
try std.testing.expectEqual(querymod.PlanKind.index_eq, plan.kind);
|
|
try std.testing.expectEqualStrings("city", plan.indexName.?);
|
|
|
|
const results = try table.select(parsed.predicate);
|
|
defer querymod.freeRows(allocator, results);
|
|
try std.testing.expectEqual(@as(usize, 1), results.len);
|
|
try std.testing.expectEqual(@as(i64, 1), results[0].get(querymod.pkField).?.int);
|
|
}
|
|
|
|
test "a multi-value in falls back to a full scan and still matches every listed value" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
var db = try openTemp(&tmp, allocator);
|
|
defer db.close();
|
|
|
|
var table = try querymod.Table.open(&db, allocator, "cities");
|
|
defer table.deinit();
|
|
try table.createIndex("city");
|
|
try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "oslo" } }});
|
|
try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "bergen" } }});
|
|
try table.put(.{ .int = 3 }, &.{.{ .name = "city", .value = .{ .text = "trondheim" } }});
|
|
|
|
var parsed = try parse(allocator, "city in (s:oslo s:trondheim)");
|
|
defer parsed.deinit();
|
|
|
|
var plan = try table.explain(allocator, parsed.predicate);
|
|
defer plan.deinit(allocator);
|
|
try std.testing.expectEqual(querymod.PlanKind.full_scan, plan.kind);
|
|
|
|
const results = try table.select(parsed.predicate);
|
|
defer querymod.freeRows(allocator, results);
|
|
try std.testing.expectEqual(@as(usize, 2), results.len);
|
|
}
|
|
|
|
test "between reaches Table.select through an index range like gte/lte does" {
|
|
const allocator = std.testing.allocator;
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
var db = try openTemp(&tmp, allocator);
|
|
defer db.close();
|
|
|
|
var table = try querymod.Table.open(&db, allocator, "people");
|
|
defer table.deinit();
|
|
try table.createIndex("age");
|
|
var i: i64 = 0;
|
|
while (i < 10) : (i += 1) {
|
|
try table.put(.{ .int = i }, &.{.{ .name = "age", .value = .{ .int = i * 10 } }});
|
|
}
|
|
|
|
var parsed = try parse(allocator, "age between i:20 and i:50");
|
|
defer parsed.deinit();
|
|
|
|
var plan = try table.explain(allocator, parsed.predicate);
|
|
defer plan.deinit(allocator);
|
|
try std.testing.expectEqual(querymod.PlanKind.index_range, plan.kind);
|
|
try std.testing.expectEqualStrings("age", plan.indexName.?);
|
|
|
|
const results = try table.select(parsed.predicate);
|
|
defer querymod.freeRows(allocator, results);
|
|
// ages 20, 30, 40, 50 - inclusive both ends.
|
|
try std.testing.expectEqual(@as(usize, 4), results.len);
|
|
}
|