use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use super::types::{
AggregateFunc, ChangeType, Column, Filter, OrderBy, TimeError, TimeQuery, TimeSpec, Value,
};
#[derive(Debug, Clone, PartialEq)]
enum Token {
Select,
From,
As,
Of,
Where,
And,
Or,
Not,
In,
Between,
Like,
Is,
Null,
Limit,
OrderBy,
Asc,
Desc,
Diff,
Versions,
Restore,
To,
Show,
Snapshots,
For,
Snapshot,
Txg,
Now,
Count,
Sum,
Avg,
Min,
Max,
Eq, Ne, Lt, Le, Gt, Ge, Star, Comma, LParen, RParen, Semicolon,
String(String),
Integer(i64),
Identifier(String),
Eof,
}
struct Tokenizer {
input: Vec<char>,
pos: usize,
}
impl Tokenizer {
fn new(input: &str) -> Self {
Self {
input: input.chars().collect(),
pos: 0,
}
}
fn peek(&self) -> Option<char> {
self.input.get(self.pos).copied()
}
fn advance(&mut self) -> Option<char> {
let c = self.peek();
if c.is_some() {
self.pos += 1;
}
c
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek() {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn read_string(&mut self, quote: char) -> Result<String, TimeError> {
let mut s = String::new();
self.advance();
while let Some(c) = self.peek() {
if c == quote {
self.advance(); return Ok(s);
} else if c == '\\' {
self.advance();
if let Some(escaped) = self.advance() {
match escaped {
'n' => s.push('\n'),
't' => s.push('\t'),
'r' => s.push('\r'),
'\\' => s.push('\\'),
'\'' => s.push('\''),
'"' => s.push('"'),
_ => s.push(escaped),
}
}
} else {
s.push(c);
self.advance();
}
}
Err(TimeError::ParseError("unterminated string".into()))
}
fn read_number(&mut self) -> i64 {
let mut s = String::new();
let negative = if self.peek() == Some('-') {
self.advance();
true
} else {
false
};
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
s.push(c);
self.advance();
} else {
break;
}
}
let n: i64 = s.parse().unwrap_or(0);
if negative { -n } else { n }
}
fn read_identifier(&mut self) -> String {
let mut s = String::new();
if let Some(c) = self.peek() {
if c.is_alphabetic() || c == '_' || c == '/' {
s.push(c);
self.advance();
}
}
while let Some(c) = self.peek() {
if c.is_alphanumeric() || c == '_' || c == '/' || c == '.' || c == '-' {
s.push(c);
self.advance();
} else {
break;
}
}
s
}
fn next_token(&mut self) -> Result<Token, TimeError> {
self.skip_whitespace();
let c = match self.peek() {
Some(c) => c,
None => return Ok(Token::Eof),
};
match c {
'*' => {
self.advance();
return Ok(Token::Star);
}
',' => {
self.advance();
return Ok(Token::Comma);
}
'(' => {
self.advance();
return Ok(Token::LParen);
}
')' => {
self.advance();
return Ok(Token::RParen);
}
';' => {
self.advance();
return Ok(Token::Semicolon);
}
'=' => {
self.advance();
return Ok(Token::Eq);
}
'<' => {
self.advance();
if self.peek() == Some('=') {
self.advance();
return Ok(Token::Le);
} else if self.peek() == Some('>') {
self.advance();
return Ok(Token::Ne);
}
return Ok(Token::Lt);
}
'>' => {
self.advance();
if self.peek() == Some('=') {
self.advance();
return Ok(Token::Ge);
}
return Ok(Token::Gt);
}
'!' => {
self.advance();
if self.peek() == Some('=') {
self.advance();
return Ok(Token::Ne);
}
return Err(TimeError::ParseError("expected '=' after '!'".into()));
}
'\'' | '"' => {
let s = self.read_string(c)?;
return Ok(Token::String(s));
}
_ => {}
}
if c.is_ascii_digit()
|| (c == '-'
&& self
.input
.get(self.pos + 1)
.is_some_and(|c| c.is_ascii_digit()))
{
let n = self.read_number();
return Ok(Token::Integer(n));
}
if c.is_alphabetic() || c == '_' || c == '/' {
let ident = self.read_identifier();
let upper = ident.to_uppercase();
let token = match upper.as_str() {
"SELECT" => Token::Select,
"FROM" => Token::From,
"AS" => Token::As,
"OF" => Token::Of,
"WHERE" => Token::Where,
"AND" => Token::And,
"OR" => Token::Or,
"NOT" => Token::Not,
"IN" => Token::In,
"BETWEEN" => Token::Between,
"LIKE" => Token::Like,
"IS" => Token::Is,
"NULL" => Token::Null,
"LIMIT" => Token::Limit,
"ORDER" => {
self.skip_whitespace();
let next = self.read_identifier();
if next.to_uppercase() == "BY" {
Token::OrderBy
} else {
Token::Identifier(ident)
}
}
"ASC" => Token::Asc,
"DESC" => Token::Desc,
"DIFF" => Token::Diff,
"VERSIONS" => Token::Versions,
"RESTORE" => Token::Restore,
"TO" => Token::To,
"SHOW" => Token::Show,
"SNAPSHOTS" => Token::Snapshots,
"FOR" => Token::For,
"SNAPSHOT" => Token::Snapshot,
"TXG" => Token::Txg,
"NOW" => Token::Now,
"COUNT" => Token::Count,
"SUM" => Token::Sum,
"AVG" => Token::Avg,
"MIN" => Token::Min,
"MAX" => Token::Max,
_ => Token::Identifier(ident),
};
return Ok(token);
}
Err(TimeError::ParseError(alloc::format!(
"unexpected character: '{}'",
c
)))
}
fn tokenize(&mut self) -> Result<Vec<Token>, TimeError> {
let mut tokens = Vec::new();
loop {
let token = self.next_token()?;
if token == Token::Eof {
tokens.push(token);
break;
}
tokens.push(token);
}
Ok(tokens)
}
}
pub struct QueryParser {
tokens: Vec<Token>,
pos: usize,
}
impl QueryParser {
pub fn parse(sql: &str) -> Result<TimeQuery, TimeError> {
let mut tokenizer = Tokenizer::new(sql);
let tokens = tokenizer.tokenize()?;
let mut parser = Self { tokens, pos: 0 };
parser.parse_query()
}
fn peek(&self) -> &Token {
self.tokens.get(self.pos).unwrap_or(&Token::Eof)
}
fn advance(&mut self) -> &Token {
let token = self.tokens.get(self.pos).unwrap_or(&Token::Eof);
if self.pos < self.tokens.len() {
self.pos += 1;
}
token
}
fn expect(&mut self, expected: Token) -> Result<(), TimeError> {
let actual = self.advance().clone();
if actual == expected {
Ok(())
} else {
Err(TimeError::ParseError(alloc::format!(
"expected {:?}, got {:?}",
expected,
actual
)))
}
}
fn parse_query(&mut self) -> Result<TimeQuery, TimeError> {
match self.peek().clone() {
Token::Select => self.parse_select(),
Token::Diff => self.parse_diff(),
Token::Versions => self.parse_versions(),
Token::Restore => self.parse_restore(),
Token::Show => self.parse_show(),
_ => Err(TimeError::ParseError(
"expected SELECT, DIFF, VERSIONS, RESTORE, or SHOW".into(),
)),
}
}
fn parse_select(&mut self) -> Result<TimeQuery, TimeError> {
self.expect(Token::Select)?;
if matches!(
self.peek(),
Token::Count | Token::Sum | Token::Avg | Token::Min | Token::Max
) {
return self.parse_aggregate();
}
let columns = self.parse_columns()?;
self.expect(Token::From)?;
let path = self.parse_path()?;
self.expect(Token::As)?;
self.expect(Token::Of)?;
let time = self.parse_time_spec()?;
let filter = if matches!(self.peek(), Token::Where) {
self.advance();
Some(self.parse_filter()?)
} else {
None
};
let order_by = if matches!(self.peek(), Token::OrderBy) {
self.advance();
Some(self.parse_order_by()?)
} else {
None
};
let limit = if matches!(self.peek(), Token::Limit) {
self.advance();
Some(self.parse_limit()?)
} else {
None
};
Ok(TimeQuery::Select {
columns,
path,
time,
filter,
limit,
order_by,
})
}
fn parse_aggregate(&mut self) -> Result<TimeQuery, TimeError> {
let mut functions = Vec::new();
loop {
let func = match self.peek().clone() {
Token::Count => {
self.advance();
self.expect(Token::LParen)?;
let col = if matches!(self.peek(), Token::Star) {
self.advance();
None
} else if let Token::Identifier(name) = self.peek().clone() {
self.advance();
Some(name)
} else {
None
};
self.expect(Token::RParen)?;
AggregateFunc::Count(col)
}
Token::Sum => {
self.advance();
self.expect(Token::LParen)?;
let col = self.parse_identifier()?;
self.expect(Token::RParen)?;
AggregateFunc::Sum(col)
}
Token::Avg => {
self.advance();
self.expect(Token::LParen)?;
let col = self.parse_identifier()?;
self.expect(Token::RParen)?;
AggregateFunc::Avg(col)
}
Token::Min => {
self.advance();
self.expect(Token::LParen)?;
let col = self.parse_identifier()?;
self.expect(Token::RParen)?;
AggregateFunc::Min(col)
}
Token::Max => {
self.advance();
self.expect(Token::LParen)?;
let col = self.parse_identifier()?;
self.expect(Token::RParen)?;
AggregateFunc::Max(col)
}
_ => break,
};
functions.push(func);
if !matches!(self.peek(), Token::Comma) {
break;
}
self.advance(); }
self.expect(Token::From)?;
let path = self.parse_path()?;
self.expect(Token::As)?;
self.expect(Token::Of)?;
let time = self.parse_time_spec()?;
let filter = if matches!(self.peek(), Token::Where) {
self.advance();
Some(self.parse_filter()?)
} else {
None
};
Ok(TimeQuery::Aggregate {
functions,
path,
time,
filter,
})
}
fn parse_diff(&mut self) -> Result<TimeQuery, TimeError> {
self.expect(Token::Diff)?;
let path = self.parse_path()?;
let (from, to) = if matches!(self.peek(), Token::As) {
self.expect(Token::As)?;
self.expect(Token::Of)?;
let from = self.parse_time_spec()?;
self.expect(Token::And)?;
let to = self.parse_time_spec()?;
(from, to)
} else {
self.expect(Token::Between)?;
let from = self.parse_time_spec()?;
self.expect(Token::And)?;
let to = self.parse_time_spec()?;
(from, to)
};
let change_types = if matches!(self.peek(), Token::Where) {
self.advance();
let _col = self.parse_identifier()?;
self.expect(Token::In)?;
self.expect(Token::LParen)?;
let mut types = Vec::new();
loop {
if let Token::String(s) = self.peek().clone() {
self.advance();
if let Some(ct) = ChangeType::from_str(&s) {
types.push(ct);
}
} else if let Token::Identifier(s) = self.peek().clone() {
self.advance();
if let Some(ct) = ChangeType::from_str(&s) {
types.push(ct);
}
}
if !matches!(self.peek(), Token::Comma) {
break;
}
self.advance();
}
self.expect(Token::RParen)?;
Some(types)
} else {
None
};
Ok(TimeQuery::Diff {
path,
from,
to,
change_types,
})
}
fn parse_versions(&mut self) -> Result<TimeQuery, TimeError> {
self.expect(Token::Versions)?;
let path = self.parse_path()?;
let limit = if matches!(self.peek(), Token::Limit) {
self.advance();
Some(self.parse_limit()?)
} else {
None
};
Ok(TimeQuery::Versions { path, limit })
}
fn parse_restore(&mut self) -> Result<TimeQuery, TimeError> {
self.expect(Token::Restore)?;
let path = self.parse_path()?;
self.expect(Token::To)?;
let time = self.parse_time_spec()?;
let dest_path = if matches!(self.peek(), Token::As) {
self.advance();
Some(self.parse_path()?)
} else {
None
};
Ok(TimeQuery::Restore {
path,
time,
dest_path,
})
}
fn parse_show(&mut self) -> Result<TimeQuery, TimeError> {
self.expect(Token::Show)?;
self.expect(Token::Snapshots)?;
let path = if matches!(self.peek(), Token::For) {
self.advance();
self.parse_path()?
} else {
"/".into()
};
Ok(TimeQuery::ShowSnapshots { path })
}
fn parse_columns(&mut self) -> Result<Vec<Column>, TimeError> {
let mut columns = Vec::new();
loop {
let col = match self.peek().clone() {
Token::Star => {
self.advance();
Column::All
}
Token::Identifier(name) => {
self.advance();
if matches!(self.peek(), Token::As) {
self.advance();
let alias = self.parse_identifier()?;
Column::Aliased { name, alias }
} else {
Column::Named(name)
}
}
_ => break,
};
columns.push(col);
if !matches!(self.peek(), Token::Comma) {
break;
}
self.advance(); }
if columns.is_empty() {
columns.push(Column::All);
}
Ok(columns)
}
fn parse_path(&mut self) -> Result<String, TimeError> {
match self.peek().clone() {
Token::Identifier(path) => {
self.advance();
Ok(path)
}
Token::String(path) => {
self.advance();
Ok(path)
}
_ => Err(TimeError::ParseError("expected path".into())),
}
}
fn parse_identifier(&mut self) -> Result<String, TimeError> {
match self.peek().clone() {
Token::Identifier(name) => {
self.advance();
Ok(name)
}
_ => Err(TimeError::ParseError("expected identifier".into())),
}
}
fn parse_time_spec(&mut self) -> Result<TimeSpec, TimeError> {
match self.peek().clone() {
Token::String(s) => {
self.advance();
if s.contains('-') && (s.contains(':') || s.len() == 10) {
Ok(TimeSpec::DateTime(s))
} else if s.contains("ago") || s == "yesterday" || s == "today" {
Ok(TimeSpec::Relative(s))
} else {
Ok(TimeSpec::Snapshot(s))
}
}
Token::Integer(n) => {
self.advance();
Ok(TimeSpec::Timestamp(n as u64))
}
Token::Snapshot => {
self.advance();
let name = match self.peek().clone() {
Token::String(s) => {
self.advance();
s
}
Token::Identifier(s) => {
self.advance();
s
}
_ => return Err(TimeError::ParseError("expected snapshot name".into())),
};
Ok(TimeSpec::Snapshot(name))
}
Token::Txg => {
self.advance();
match self.peek().clone() {
Token::Integer(n) => {
self.advance();
Ok(TimeSpec::Txg(n as u64))
}
_ => Err(TimeError::ParseError("expected TXG number".into())),
}
}
Token::Now => {
self.advance();
Ok(TimeSpec::Now)
}
_ => Err(TimeError::ParseError("expected time specification".into())),
}
}
fn parse_filter(&mut self) -> Result<Filter, TimeError> {
self.parse_or_filter()
}
fn parse_or_filter(&mut self) -> Result<Filter, TimeError> {
let mut left = self.parse_and_filter()?;
while matches!(self.peek(), Token::Or) {
self.advance();
let right = self.parse_and_filter()?;
left = Filter::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_and_filter(&mut self) -> Result<Filter, TimeError> {
let mut left = self.parse_not_filter()?;
while matches!(self.peek(), Token::And) {
self.advance();
let right = self.parse_not_filter()?;
left = Filter::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_not_filter(&mut self) -> Result<Filter, TimeError> {
if matches!(self.peek(), Token::Not) {
self.advance();
let inner = self.parse_primary_filter()?;
Ok(Filter::Not(Box::new(inner)))
} else {
self.parse_primary_filter()
}
}
fn parse_primary_filter(&mut self) -> Result<Filter, TimeError> {
if matches!(self.peek(), Token::LParen) {
self.advance();
let filter = self.parse_filter()?;
self.expect(Token::RParen)?;
return Ok(filter);
}
let column = self.parse_identifier()?;
if matches!(self.peek(), Token::Is) {
self.advance();
if matches!(self.peek(), Token::Not) {
self.advance();
self.expect(Token::Null)?;
return Ok(Filter::IsNotNull { column });
} else {
self.expect(Token::Null)?;
return Ok(Filter::IsNull { column });
}
}
if matches!(self.peek(), Token::In) {
self.advance();
self.expect(Token::LParen)?;
let mut values = Vec::new();
loop {
values.push(self.parse_value()?);
if !matches!(self.peek(), Token::Comma) {
break;
}
self.advance();
}
self.expect(Token::RParen)?;
return Ok(Filter::In { column, values });
}
if matches!(self.peek(), Token::Between) {
self.advance();
let low = self.parse_value()?;
self.expect(Token::And)?;
let high = self.parse_value()?;
return Ok(Filter::Between { column, low, high });
}
if matches!(self.peek(), Token::Like) {
self.advance();
let pattern = match self.peek().clone() {
Token::String(s) => {
self.advance();
s
}
_ => return Err(TimeError::ParseError("expected pattern string".into())),
};
return Ok(Filter::Like { column, pattern });
}
let op = self.peek().clone();
match op {
Token::Eq => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Eq { column, value })
}
Token::Ne => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Ne { column, value })
}
Token::Lt => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Lt { column, value })
}
Token::Le => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Le { column, value })
}
Token::Gt => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Gt { column, value })
}
Token::Ge => {
self.advance();
let value = self.parse_value()?;
Ok(Filter::Ge { column, value })
}
_ => Err(TimeError::ParseError(alloc::format!(
"expected comparison operator, got {:?}",
op
))),
}
}
fn parse_value(&mut self) -> Result<Value, TimeError> {
match self.peek().clone() {
Token::String(s) => {
self.advance();
Ok(Value::String(s))
}
Token::Integer(n) => {
self.advance();
if n >= 0 {
Ok(Value::Unsigned(n as u64))
} else {
Ok(Value::Integer(n))
}
}
Token::Null => {
self.advance();
Ok(Value::Null)
}
Token::Identifier(s) => {
self.advance();
match s.to_uppercase().as_str() {
"TRUE" => Ok(Value::Bool(true)),
"FALSE" => Ok(Value::Bool(false)),
_ => Ok(Value::String(s)),
}
}
_ => Err(TimeError::ParseError("expected value".into())),
}
}
fn parse_order_by(&mut self) -> Result<OrderBy, TimeError> {
let column = self.parse_identifier()?;
let ascending = match self.peek() {
Token::Asc => {
self.advance();
true
}
Token::Desc => {
self.advance();
false
}
_ => true, };
Ok(OrderBy { column, ascending })
}
fn parse_limit(&mut self) -> Result<usize, TimeError> {
match self.peek().clone() {
Token::Integer(n) => {
self.advance();
if n < 0 {
Err(TimeError::ParseError("LIMIT must be positive".into()))
} else {
Ok(n as usize)
}
}
_ => Err(TimeError::ParseError("expected LIMIT value".into())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_select() {
let query = QueryParser::parse("SELECT * FROM /data AS OF '2024-01-15'").unwrap();
match query {
TimeQuery::Select {
columns,
path,
time,
filter,
limit,
..
} => {
assert_eq!(columns.len(), 1);
assert!(matches!(columns[0], Column::All));
assert_eq!(path, "/data");
assert!(matches!(time, TimeSpec::DateTime(_)));
assert!(filter.is_none());
assert!(limit.is_none());
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_select_with_columns() {
let query =
QueryParser::parse("SELECT path, size, mtime FROM /pool/data AS OF '2024-01-15'")
.unwrap();
match query {
TimeQuery::Select { columns, .. } => {
assert_eq!(columns.len(), 3);
assert!(matches!(&columns[0], Column::Named(n) if n == "path"));
assert!(matches!(&columns[1], Column::Named(n) if n == "size"));
assert!(matches!(&columns[2], Column::Named(n) if n == "mtime"));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_select_with_where() {
let query =
QueryParser::parse("SELECT * FROM /data AS OF '2024-01-15' WHERE extension = 'pdf'")
.unwrap();
match query {
TimeQuery::Select { filter, .. } => {
assert!(filter.is_some());
let f = filter.unwrap();
match f {
Filter::Eq { column, value } => {
assert_eq!(column, "extension");
assert!(matches!(value, Value::String(ref s) if s == "pdf"));
}
_ => panic!("expected Eq filter"),
}
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_select_with_limit() {
let query = QueryParser::parse("SELECT * FROM /data AS OF '2024-01-15' LIMIT 100").unwrap();
match query {
TimeQuery::Select { limit, .. } => {
assert_eq!(limit, Some(100));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_diff() {
let query = QueryParser::parse("DIFF /data BETWEEN '2024-01-01' AND '2024-06-01'").unwrap();
match query {
TimeQuery::Diff { path, from, to, .. } => {
assert_eq!(path, "/data");
assert!(matches!(from, TimeSpec::DateTime(_)));
assert!(matches!(to, TimeSpec::DateTime(_)));
}
_ => panic!("expected DIFF query"),
}
}
#[test]
fn test_parse_versions() {
let query = QueryParser::parse("VERSIONS /data/config.yaml LIMIT 50").unwrap();
match query {
TimeQuery::Versions { path, limit } => {
assert_eq!(path, "/data/config.yaml");
assert_eq!(limit, Some(50));
}
_ => panic!("expected VERSIONS query"),
}
}
#[test]
fn test_parse_restore() {
let query = QueryParser::parse("RESTORE /data/file.txt TO '2024-03-15'").unwrap();
match query {
TimeQuery::Restore { path, time, .. } => {
assert_eq!(path, "/data/file.txt");
assert!(matches!(time, TimeSpec::DateTime(_)));
}
_ => panic!("expected RESTORE query"),
}
}
#[test]
fn test_parse_snapshot_timespec() {
let query =
QueryParser::parse("SELECT * FROM /data AS OF SNAPSHOT 'daily-backup'").unwrap();
match query {
TimeQuery::Select { time, .. } => {
assert!(matches!(time, TimeSpec::Snapshot(n) if n == "daily-backup"));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_txg_timespec() {
let query = QueryParser::parse("SELECT * FROM /data AS OF TXG 12345").unwrap();
match query {
TimeQuery::Select { time, .. } => {
assert!(matches!(time, TimeSpec::Txg(12345)));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_complex_filter() {
let query = QueryParser::parse(
"SELECT * FROM /data AS OF '2024-01-15' WHERE size > 1000 AND extension = 'txt'",
)
.unwrap();
match query {
TimeQuery::Select { filter, .. } => {
assert!(filter.is_some());
assert!(matches!(filter.unwrap(), Filter::And(_, _)));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_aggregate() {
let query =
QueryParser::parse("SELECT COUNT(*), SUM(size) FROM /data AS OF '2024-01-15'").unwrap();
match query {
TimeQuery::Aggregate { functions, .. } => {
assert_eq!(functions.len(), 2);
assert!(matches!(functions[0], AggregateFunc::Count(None)));
assert!(matches!(&functions[1], AggregateFunc::Sum(col) if col == "size"));
}
_ => panic!("expected AGGREGATE query"),
}
}
#[test]
fn test_parse_show_snapshots() {
let query = QueryParser::parse("SHOW SNAPSHOTS FOR /pool/data").unwrap();
match query {
TimeQuery::ShowSnapshots { path } => {
assert_eq!(path, "/pool/data");
}
_ => panic!("expected SHOW SNAPSHOTS query"),
}
}
#[test]
fn test_parse_in_filter() {
let query = QueryParser::parse(
"SELECT * FROM /data AS OF '2024-01-15' WHERE type IN ('file', 'directory')",
)
.unwrap();
match query {
TimeQuery::Select { filter, .. } => {
assert!(matches!(filter, Some(Filter::In { .. })));
}
_ => panic!("expected SELECT query"),
}
}
#[test]
fn test_parse_relative_time() {
let query = QueryParser::parse("SELECT * FROM /data AS OF '1 hour ago'").unwrap();
match query {
TimeQuery::Select { time, .. } => {
assert!(matches!(time, TimeSpec::Relative(s) if s == "1 hour ago"));
}
_ => panic!("expected SELECT query"),
}
}
}