snapshot of https://github.com/ActivityWatch/aw-server-rust.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled
This commit is contained in:
26
aw-query/Cargo.toml
Normal file
26
aw-query/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "aw-query"
|
||||
version = "0.1.0"
|
||||
authors = ["Johan Bjäreholt <johan@bjareho.lt>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
plex = "0.3.0"
|
||||
log = "0.4"
|
||||
fancy-regex = "0.17.0"
|
||||
aw-datastore = { path = "../aw-datastore" }
|
||||
aw-models = { path = "../aw-models" }
|
||||
aw-transform = { path = "../aw-transform" }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5.1"
|
||||
proptest = "1.4"
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
127
aw-query/benches/benchmark.rs
Normal file
127
aw-query/benches/benchmark.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use criterion::{criterion_group, criterion_main};
|
||||
|
||||
// TODO: Move me to an appropriate place
|
||||
#[macro_export]
|
||||
macro_rules! json_map {
|
||||
{ $( $key:literal : $value:expr),* } => {{
|
||||
use serde_json::Value;
|
||||
use serde_json::map::Map;
|
||||
#[allow(unused_mut)]
|
||||
let mut map : Map<String, Value> = Map::new();
|
||||
$(
|
||||
map.insert( $key.to_string(), json!($value) );
|
||||
)*
|
||||
map
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod query_benchmarks {
|
||||
use chrono::Duration;
|
||||
use criterion::Criterion;
|
||||
use serde_json::json;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
|
||||
use aw_datastore::Datastore;
|
||||
use aw_models::Bucket;
|
||||
use aw_models::BucketMetadata;
|
||||
use aw_models::Event;
|
||||
use aw_models::TimeInterval;
|
||||
|
||||
static BUCKETNAME: &str = "testbucket";
|
||||
static TIME_INTERVAL: &str = "1980-01-01T00:00:00Z/2080-01-02T00:00:00Z";
|
||||
|
||||
fn setup_datastore() -> Datastore {
|
||||
Datastore::new_in_memory(false)
|
||||
}
|
||||
|
||||
fn create_bucket(ds: &Datastore, bucketname: String) {
|
||||
let bucket = Bucket {
|
||||
bid: None,
|
||||
id: bucketname,
|
||||
_type: "testtype".to_string(),
|
||||
client: "testclient".to_string(),
|
||||
hostname: "testhost".to_string(),
|
||||
created: Some(chrono::Utc::now()),
|
||||
data: json_map! {},
|
||||
metadata: BucketMetadata::default(),
|
||||
events: None,
|
||||
last_updated: None,
|
||||
};
|
||||
ds.create_bucket(&bucket).unwrap();
|
||||
}
|
||||
|
||||
fn insert_events(ds: &Datastore, bucketname: &str, num_events: i64) {
|
||||
let mut possible_data = Vec::<Map<String, Value>>::new();
|
||||
for i in 0..20 {
|
||||
possible_data.push(json_map! {"number": i});
|
||||
}
|
||||
let mut event_list = Vec::new();
|
||||
for i in 0..num_events {
|
||||
let e = Event {
|
||||
id: None,
|
||||
timestamp: chrono::Utc::now() + Duration::seconds(i),
|
||||
duration: Duration::seconds(10),
|
||||
data: possible_data[i as usize % 20].clone(),
|
||||
};
|
||||
event_list.push(e);
|
||||
}
|
||||
ds.insert_events(bucketname, &event_list).unwrap();
|
||||
}
|
||||
|
||||
pub fn bench_assign(c: &mut Criterion) {
|
||||
let ds = setup_datastore();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
c.bench_function("bench assign", |b| {
|
||||
b.iter(|| {
|
||||
let code = String::from("return a=1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::None() => (),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn bench_many_events(c: &mut Criterion) {
|
||||
let ds = setup_datastore();
|
||||
create_bucket(&ds, BUCKETNAME.to_string());
|
||||
insert_events(&ds, BUCKETNAME, 5000);
|
||||
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
c.bench_function("bench many events", |b| {
|
||||
b.iter(|| {
|
||||
let code = String::from(
|
||||
"
|
||||
events = query_bucket(\"testbucket\");
|
||||
return events;
|
||||
",
|
||||
);
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn bench_read_only_references(c: &mut Criterion) {
|
||||
let ds = setup_datastore();
|
||||
create_bucket(&ds, BUCKETNAME.to_string());
|
||||
insert_events(&ds, BUCKETNAME, 50_000);
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
let code = format!(
|
||||
"events = query_bucket(\"testbucket\"); total = 0; {} return total;",
|
||||
"total = total + sum_durations(events);".repeat(20)
|
||||
);
|
||||
c.bench_function("read-only references 50000 events", |b| {
|
||||
b.iter(|| aw_query::query(&code, &interval, &ds).unwrap())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
query_benchmarks::bench_assign,
|
||||
query_benchmarks::bench_many_events,
|
||||
query_benchmarks::bench_read_only_references
|
||||
);
|
||||
criterion_main!(benches);
|
||||
38
aw-query/src/ast.rs
Normal file
38
aw-query/src/ast.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::lexer::Span;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
pub stmts: Vec<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Expr {
|
||||
#[allow(dead_code)]
|
||||
pub span: Span,
|
||||
pub node: Expr_,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Expr_ {
|
||||
Add(Box<Expr>, Box<Expr>),
|
||||
Sub(Box<Expr>, Box<Expr>),
|
||||
Mul(Box<Expr>, Box<Expr>),
|
||||
Div(Box<Expr>, Box<Expr>),
|
||||
Mod(Box<Expr>, Box<Expr>),
|
||||
|
||||
Equal(Box<Expr>, Box<Expr>),
|
||||
|
||||
Var(String),
|
||||
Assign(String, Box<Expr>),
|
||||
Function(String, Box<Expr>),
|
||||
If(Vec<(Box<Expr>, Vec<Expr>)>),
|
||||
Return(Box<Expr>),
|
||||
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
List(Vec<Expr>),
|
||||
Dict(HashMap<String, Expr>),
|
||||
}
|
||||
479
aw-query/src/datatype.rs
Normal file
479
aw-query/src/datatype.rs
Normal file
@@ -0,0 +1,479 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
use super::functions;
|
||||
use super::QueryError;
|
||||
use aw_models::Event;
|
||||
use aw_transform::classify::{CategoryRule, RegexRule, Rule};
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::value::Value;
|
||||
use serde_json::Number;
|
||||
|
||||
// TODO: greater/less comparisons
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[non_exhaustive]
|
||||
pub enum DataType {
|
||||
None(),
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Event(Event),
|
||||
List(Vec<DataType>),
|
||||
Dict(HashMap<String, DataType>),
|
||||
#[serde(serialize_with = "serialize_function")]
|
||||
Function(String, functions::QueryFn),
|
||||
#[serde(serialize_with = "serialize_function")]
|
||||
ReadOnlyFunction(String, functions::ReadOnlyQueryFn),
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn serialize_function<S, F>(_element: &str, _fun: &F, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
panic!("Query function was unevaluated and was attempted to be serialized, panic!");
|
||||
//element.id.serialize(serializer)
|
||||
}
|
||||
|
||||
// Needed because of a limitation in rust where you cannot derive(Debug) on a
|
||||
// enum which has a fn with reference parameters which our QueryFn has
|
||||
// https://stackoverflow.com/questions/53380040/function-pointer-with-a-reference-argument-cannot-derive-debug
|
||||
impl fmt::Debug for DataType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DataType::None() => write!(f, "None()"),
|
||||
DataType::Bool(b) => write!(f, "Bool({b})"),
|
||||
DataType::Number(n) => write!(f, "Number({n})"),
|
||||
DataType::String(s) => write!(f, "String({s})"),
|
||||
DataType::Event(e) => write!(f, "Event({e:?})"),
|
||||
DataType::List(l) => write!(f, "List({l:?})"),
|
||||
DataType::Dict(d) => write!(f, "Dict({d:?})"),
|
||||
DataType::Function(name, _fun) => write!(f, "Function({name})"),
|
||||
DataType::ReadOnlyFunction(name, _fun) => write!(f, "Function({name})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Like eq, but raises an error when comparing between different types.
|
||||
* Should be used as often as possible */
|
||||
impl DataType {
|
||||
pub fn query_eq(&self, other: &DataType) -> Result<bool, QueryError> {
|
||||
match (self, other) {
|
||||
// TODO: Comparisons of bool == num, bool == str
|
||||
// None is equal to itself, matching the PartialEq impl below.
|
||||
(DataType::None(), DataType::None()) => Ok(true),
|
||||
(DataType::Bool(b1), DataType::Bool(b2)) => Ok(b1 == b2),
|
||||
(DataType::Number(n1), DataType::Number(n2)) => Ok(n1 == n2),
|
||||
(DataType::String(s1), DataType::String(s2)) => Ok(s1 == s2),
|
||||
(DataType::Event(e1), DataType::Event(e2)) => Ok(e1 == e2),
|
||||
(DataType::List(l1), DataType::List(l2)) => Ok(l1 == l2),
|
||||
(DataType::Dict(d1), DataType::Dict(d2)) => Ok(d1 == d2),
|
||||
// We do not care about comparing functions
|
||||
_ => Err(QueryError::InvalidType(format!(
|
||||
"Cannot compare values of different types {self:?} and {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Required for query_eq when comparing two dicts */
|
||||
impl PartialEq for DataType {
|
||||
fn eq(&self, other: &DataType) -> bool {
|
||||
match (self, other) {
|
||||
(DataType::None(), DataType::None()) => true,
|
||||
// TODO: Comparisons of bool == num, bool == str
|
||||
(DataType::Bool(b1), DataType::Bool(b2)) => b1 == b2,
|
||||
(DataType::Number(n1), DataType::Number(n2)) => n1 == n2,
|
||||
(DataType::String(s1), DataType::String(s2)) => s1 == s2,
|
||||
(DataType::Event(e1), DataType::Event(e2)) => e1 == e2,
|
||||
(DataType::List(l1), DataType::List(l2)) => l1 == l2,
|
||||
(DataType::Dict(d1), DataType::Dict(d2)) => d1 == d2,
|
||||
// We do not care about comparing functions
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Conversions out of DataType.
|
||||
*
|
||||
* The by-value impls move the contained data out and are what query functions
|
||||
* should use, since functions own their argument vector. The by-ref impls
|
||||
* delegate via a clone and only exist for callers that cannot give up
|
||||
* ownership — going through them deep-copies every contained event. */
|
||||
|
||||
impl TryFrom<DataType> for Vec<DataType> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DataType::List(s) => Ok(s),
|
||||
invalid_type => Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type List, got {invalid_type:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<DataType> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for String {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DataType::String(s) => Ok(s),
|
||||
invalid_type => Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type String, list contains {invalid_type:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for String {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Vec<String> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
let tagged_strings: Vec<DataType> = value.try_into()?;
|
||||
let mut strings = Vec::with_capacity(tagged_strings.len());
|
||||
for string in tagged_strings {
|
||||
strings.push(string.try_into()?);
|
||||
}
|
||||
Ok(strings)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<String> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Vec<Event> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
let tagged_events: Vec<DataType> = value.try_into()?;
|
||||
let mut events = Vec::with_capacity(tagged_events.len());
|
||||
for event in tagged_events {
|
||||
match event {
|
||||
DataType::Event(e) => events.push(e),
|
||||
invalid_type => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type List of Events, list contains {invalid_type:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<Event> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<(String, Rule)> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Vec<(String, Rule)> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
let tagged_lists: Vec<DataType> = value.try_into()?;
|
||||
let mut lists: Vec<(String, Rule)> = Vec::with_capacity(tagged_lists.len());
|
||||
for list in tagged_lists {
|
||||
match list {
|
||||
DataType::List(ref l) => {
|
||||
let tag: String = match l.first() {
|
||||
Some(tag) => tag.try_into()?,
|
||||
None => return Err(QueryError::InvalidFunctionParameters(
|
||||
format!("Expected function parameter of type list of (tag, rule) tuples, list contains {l:?}")))
|
||||
};
|
||||
let rule: Rule = match l.get(1) {
|
||||
Some(rule) => rule.try_into()?,
|
||||
None => return Err(QueryError::InvalidFunctionParameters(
|
||||
format!("Expected function parameter of type list of (tag, rule) tuples, list contains {l:?}")))
|
||||
};
|
||||
lists.push((tag, rule));
|
||||
}
|
||||
ref invalid_type => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type list of (tag, rule) tuples, got {invalid_type:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(lists)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<CategoryRule> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_optional_priority(rule: &DataType) -> Result<Option<i64>, QueryError> {
|
||||
let obj = match rule {
|
||||
DataType::Dict(dict) => dict,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let val = match obj.get("priority").or_else(|| obj.get("weight")) {
|
||||
Some(v) => v,
|
||||
None => return Ok(None),
|
||||
};
|
||||
match val {
|
||||
DataType::Number(n) => {
|
||||
if !n.is_finite() {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"priority/weight must be a finite integer".to_string(),
|
||||
));
|
||||
}
|
||||
let as_int = *n as i64;
|
||||
if (as_int as f64) != *n {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"priority/weight must be an integer".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Some(as_int))
|
||||
}
|
||||
_ => Err(QueryError::InvalidFunctionParameters(
|
||||
"priority/weight must be an integer".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Vec<CategoryRule> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
let tagged_lists: Vec<DataType> = value.try_into()?;
|
||||
let mut lists: Vec<CategoryRule> = Vec::with_capacity(tagged_lists.len());
|
||||
for list in tagged_lists {
|
||||
match list {
|
||||
DataType::List(ref l) => {
|
||||
let category: Vec<String> = match l.first() {
|
||||
Some(category) => category.try_into()?,
|
||||
None => return Err(QueryError::InvalidFunctionParameters(
|
||||
format!("Expected function parameter of type list of (category, rule) tuples, list contains {l:?}")))
|
||||
};
|
||||
let rule_data = match l.get(1) {
|
||||
Some(rule) => rule,
|
||||
None => return Err(QueryError::InvalidFunctionParameters(
|
||||
format!("Expected function parameter of type list of (category, rule) tuples, list contains {l:?}")))
|
||||
};
|
||||
let priority = parse_optional_priority(rule_data)?;
|
||||
let rule: Rule = rule_data.try_into()?;
|
||||
lists.push(CategoryRule {
|
||||
category,
|
||||
rule,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
ref invalid_type => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type list of (category, rule) tuples, got {invalid_type:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(lists)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for f64 {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DataType::Number(f) => Ok(*f),
|
||||
ref invalid_type => Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type Number, got {invalid_type:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for f64 {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
(&value).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for usize {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
let f: f64 = value.try_into()?;
|
||||
Ok(f as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for usize {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
(&value).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Value {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DataType::None() => Ok(Value::Null),
|
||||
DataType::Bool(b) => Ok(Value::Bool(b)),
|
||||
DataType::Number(n) => Ok(Value::Number(Number::from_f64(n).unwrap())),
|
||||
DataType::String(s) => Ok(Value::String(s)),
|
||||
DataType::List(l) => {
|
||||
let mut values: Vec<Value> = Vec::with_capacity(l.len());
|
||||
for value in l {
|
||||
values.push(value.try_into()?);
|
||||
}
|
||||
Ok(Value::Array(values))
|
||||
}
|
||||
invalid_type => Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Query2 support for parsing values is limited, does not support parsing {invalid_type:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Value {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataType> for Vec<Value> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: DataType) -> Result<Self, Self::Error> {
|
||||
let tagged_values: Vec<DataType> = value.try_into()?;
|
||||
let mut values: Vec<Value> = Vec::with_capacity(tagged_values.len());
|
||||
for value in tagged_values {
|
||||
values.push(value.try_into()?);
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Vec<Value> {
|
||||
type Error = QueryError;
|
||||
fn try_from(value: &DataType) -> Result<Self, Self::Error> {
|
||||
value.clone().try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DataType> for Rule {
|
||||
type Error = QueryError;
|
||||
|
||||
fn try_from(data: &DataType) -> Result<Self, Self::Error> {
|
||||
let obj = match data {
|
||||
DataType::Dict(dict) => dict,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected rule dict, got {data:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let rtype_val = match obj.get("type") {
|
||||
Some(rtype) => rtype,
|
||||
None => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"rule does not have a type".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let rtype = match rtype_val {
|
||||
DataType::String(s) => s,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"rule type is not a string".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
if rtype == "none" {
|
||||
Ok(Self::None)
|
||||
} else if rtype == "regex" {
|
||||
let regex_val = match obj.get("regex") {
|
||||
Some(regex_val) => regex_val,
|
||||
None => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"regex rule is missing the 'regex' field".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let regex_str = match regex_val {
|
||||
DataType::String(s) => s,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"the regex field of the regex rule is not a string".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let ignore_case_val = match obj.get("ignore_case") {
|
||||
Some(case_val) => case_val,
|
||||
None => &DataType::Bool(false),
|
||||
};
|
||||
let ignore_case = match ignore_case_val {
|
||||
DataType::Bool(b) => b,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"the ignore_case field of the regex rule is not a bool".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let select_keys = match obj.get("select_keys") {
|
||||
Some(DataType::List(keys)) => {
|
||||
let mut select_keys = Vec::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
match key {
|
||||
DataType::String(key) => select_keys.push(key.clone()),
|
||||
_ => return Err(QueryError::InvalidFunctionParameters(
|
||||
"the select_keys field of the regex rule must contain only strings"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Some(select_keys)
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(QueryError::InvalidFunctionParameters(
|
||||
"the select_keys field of the regex rule is not a list".to_string(),
|
||||
))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let regex_rule = match RegexRule::new(regex_str, *ignore_case, select_keys) {
|
||||
Ok(regex_rule) => regex_rule,
|
||||
Err(err) => {
|
||||
return Err(QueryError::RegexCompileError(format!(
|
||||
"Failed to compile regex string '{regex_str}': '{err:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(Self::Regex(regex_rule))
|
||||
} else {
|
||||
Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Unknown rule type '{rtype}'"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
651
aw-query/src/functions.rs
Normal file
651
aw-query/src/functions.rs
Normal file
@@ -0,0 +1,651 @@
|
||||
use crate::DataType;
|
||||
use crate::QueryError;
|
||||
use crate::VarEnv;
|
||||
use aw_datastore::Datastore;
|
||||
|
||||
pub type QueryFn =
|
||||
fn(args: Vec<DataType>, env: &VarEnv, ds: &Datastore) -> Result<DataType, QueryError>;
|
||||
|
||||
pub type ReadOnlyQueryFn =
|
||||
fn(args: &[&DataType], env: &VarEnv, ds: &Datastore) -> Result<DataType, QueryError>;
|
||||
|
||||
pub fn fill_env(env: &mut VarEnv) {
|
||||
env.insert(
|
||||
"print".to_string(),
|
||||
DataType::ReadOnlyFunction("print".to_string(), qfunctions::print),
|
||||
);
|
||||
env.insert(
|
||||
"query_bucket".to_string(),
|
||||
DataType::Function("query_bucket".to_string(), qfunctions::query_bucket),
|
||||
);
|
||||
env.insert(
|
||||
"query_bucket_names".to_string(),
|
||||
DataType::Function(
|
||||
"query_bucket_names".to_string(),
|
||||
qfunctions::query_bucket_names,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"sort_by_duration".to_string(),
|
||||
DataType::Function("sort_by_duration".to_string(), qfunctions::sort_by_duration),
|
||||
);
|
||||
env.insert(
|
||||
"sort_by_timestamp".to_string(),
|
||||
DataType::Function(
|
||||
"sort_by_timestamp".to_string(),
|
||||
qfunctions::sort_by_timestamp,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"sum_durations".to_string(),
|
||||
DataType::ReadOnlyFunction("sum_durations".to_string(), qfunctions::sum_durations),
|
||||
);
|
||||
env.insert(
|
||||
"limit_events".to_string(),
|
||||
DataType::Function("limit_events".to_string(), qfunctions::limit_events),
|
||||
);
|
||||
env.insert(
|
||||
"contains".to_string(),
|
||||
DataType::ReadOnlyFunction("contains".to_string(), qfunctions::contains),
|
||||
);
|
||||
env.insert(
|
||||
"flood".to_string(),
|
||||
DataType::Function("flood".to_string(), qfunctions::flood),
|
||||
);
|
||||
env.insert(
|
||||
"find_bucket".to_string(),
|
||||
DataType::Function("find_bucket".to_string(), qfunctions::find_bucket),
|
||||
);
|
||||
env.insert(
|
||||
"merge_events_by_keys".to_string(),
|
||||
DataType::Function(
|
||||
"merge_events_by_keys".to_string(),
|
||||
qfunctions::merge_events_by_keys,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"chunk_events_by_key".to_string(),
|
||||
DataType::Function(
|
||||
"chunk_events_by_key".to_string(),
|
||||
qfunctions::chunk_events_by_key,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"exclude_keyvals".to_string(),
|
||||
DataType::Function("exclude_keyvals".to_string(), qfunctions::exclude_keyvals),
|
||||
);
|
||||
env.insert(
|
||||
"filter_keyvals".to_string(),
|
||||
DataType::Function("filter_keyvals".to_string(), qfunctions::filter_keyvals),
|
||||
);
|
||||
env.insert(
|
||||
"filter_keyvals_regex".to_string(),
|
||||
DataType::Function(
|
||||
"filter_keyvals_regex".to_string(),
|
||||
qfunctions::filter_keyvals_regex,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"filter_period_intersect".to_string(),
|
||||
DataType::Function(
|
||||
"filter_period_intersect".to_string(),
|
||||
qfunctions::filter_period_intersect,
|
||||
),
|
||||
);
|
||||
env.insert(
|
||||
"split_url_events".to_string(),
|
||||
DataType::Function("split_url_events".to_string(), qfunctions::split_url_events),
|
||||
);
|
||||
env.insert(
|
||||
"concat".to_string(),
|
||||
DataType::Function("concat".to_string(), qfunctions::concat),
|
||||
);
|
||||
env.insert(
|
||||
"categorize".to_string(),
|
||||
DataType::Function("categorize".into(), qfunctions::categorize),
|
||||
);
|
||||
env.insert(
|
||||
"tag".to_string(),
|
||||
DataType::Function("tag".into(), qfunctions::tag),
|
||||
);
|
||||
env.insert(
|
||||
"period_union".to_string(),
|
||||
DataType::Function("period_union".into(), qfunctions::period_union),
|
||||
);
|
||||
env.insert(
|
||||
"union_no_overlap".to_string(),
|
||||
DataType::Function("union_no_overlap".into(), qfunctions::union_no_overlap),
|
||||
);
|
||||
}
|
||||
|
||||
mod qfunctions {
|
||||
use aw_datastore::Datastore;
|
||||
use aw_models::Event;
|
||||
use aw_transform::classify::{CategoryRule, Rule};
|
||||
|
||||
use super::validate;
|
||||
use crate::DataType;
|
||||
use crate::QueryError;
|
||||
use crate::VarEnv;
|
||||
|
||||
pub fn print(
|
||||
args: &[&DataType],
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
for arg in args {
|
||||
info!("{:?}", arg);
|
||||
}
|
||||
Ok(DataType::None())
|
||||
}
|
||||
|
||||
pub fn query_bucket(
|
||||
args: Vec<DataType>,
|
||||
env: &VarEnv,
|
||||
ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// Typecheck
|
||||
validate::args_length(&args, 1)?;
|
||||
|
||||
let bucket_id: String = args.into_iter().next().unwrap().try_into()?;
|
||||
let interval = validate::get_timeinterval(env)?;
|
||||
|
||||
let events = match ds.get_events(
|
||||
bucket_id.as_str(),
|
||||
Some(*interval.start()),
|
||||
Some(*interval.end()),
|
||||
None,
|
||||
) {
|
||||
Ok(events) => events,
|
||||
Err(aw_datastore::DatastoreError::NoSuchBucket(_)) => {
|
||||
return Err(QueryError::BucketNotFound(format!(
|
||||
"Failed to find bucket '{bucket_id}'"
|
||||
)))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(QueryError::BucketQueryError(format!(
|
||||
"Failed to query bucket: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut ret = Vec::new();
|
||||
for event in events {
|
||||
ret.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(ret))
|
||||
}
|
||||
|
||||
pub fn query_bucket_names(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
validate::args_length(&args, 0)?;
|
||||
let mut bucketnames: Vec<DataType> = Vec::new();
|
||||
let buckets = match ds.get_buckets() {
|
||||
Ok(buckets) => buckets,
|
||||
Err(e) => {
|
||||
return Err(QueryError::BucketQueryError(format!(
|
||||
"Failed to query bucket names: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
for bucketname in buckets.keys() {
|
||||
bucketnames.push(DataType::String(bucketname.to_string()));
|
||||
}
|
||||
Ok(DataType::List(bucketnames))
|
||||
}
|
||||
|
||||
pub fn find_bucket(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
validate::args_length(&args, 1).or_else(|_| validate::args_length(&args, 2))?;
|
||||
|
||||
let mut args = args.into_iter();
|
||||
let bucket_filter: String = args.next().unwrap().try_into()?;
|
||||
let hostname_filter: Option<String> = match args.next() {
|
||||
Some(arg) => Some(arg.try_into()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let buckets = match ds.get_buckets() {
|
||||
Ok(buckets) => buckets,
|
||||
Err(e) => {
|
||||
return Err(QueryError::BucketQueryError(format!(
|
||||
"Failed to query bucket names: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let bucketname = match aw_transform::find_bucket(
|
||||
&bucket_filter,
|
||||
&hostname_filter,
|
||||
buckets.values(),
|
||||
) {
|
||||
Some(bucketname) => bucketname,
|
||||
None => {
|
||||
return Err(QueryError::BucketNotFound(match hostname_filter {
|
||||
None => {
|
||||
format!("Failed to find bucket matching filter '{bucket_filter}'")
|
||||
}
|
||||
Some(hostname_filter) => format!(
|
||||
"Failed to find bucket matching filter '{bucket_filter}' and hostname '{hostname_filter}'"
|
||||
),
|
||||
}));
|
||||
}
|
||||
};
|
||||
Ok(DataType::String(bucketname))
|
||||
}
|
||||
|
||||
pub fn contains(
|
||||
args: &[&DataType],
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(args, 2)?;
|
||||
match args[0] {
|
||||
DataType::List(list) => Ok(DataType::Bool(list.contains(args[1]))),
|
||||
DataType::Dict(ref dict) => {
|
||||
let s = match &args[1] {
|
||||
DataType::String(s) => s.as_str(),
|
||||
_ => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"function contains got second argument {:?}, expected type String",
|
||||
args[0]
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(DataType::Bool(dict.contains_key(s)))
|
||||
}
|
||||
_ => Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"function contains got first argument {:?}, expected type List or Dict",
|
||||
args[0]
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flood(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 1)?;
|
||||
let events: Vec<Event> = args.into_iter().next().unwrap().try_into()?;
|
||||
// Run flood
|
||||
let mut flooded_events = aw_transform::flood(events, chrono::Duration::seconds(5));
|
||||
// Put events back into DataType::Event container
|
||||
let mut tagged_flooded_events = Vec::new();
|
||||
for event in flooded_events.drain(..) {
|
||||
tagged_flooded_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_flooded_events))
|
||||
}
|
||||
|
||||
pub fn categorize(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let rules: Vec<CategoryRule> = args.next().unwrap().try_into()?;
|
||||
// Run categorize
|
||||
let mut flooded_events = aw_transform::classify::categorize(events, &rules);
|
||||
// Put events back into DataType::Event container
|
||||
let mut tagged_flooded_events = Vec::new();
|
||||
for event in flooded_events.drain(..) {
|
||||
tagged_flooded_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_flooded_events))
|
||||
}
|
||||
|
||||
pub fn tag(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let rules: Vec<(String, Rule)> = args.next().unwrap().try_into()?;
|
||||
// Run categorize
|
||||
let mut flooded_events = aw_transform::classify::tag(events, &rules);
|
||||
// Put events back into DataType::Event container
|
||||
let mut tagged_flooded_events = Vec::new();
|
||||
for event in flooded_events.drain(..) {
|
||||
tagged_flooded_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_flooded_events))
|
||||
}
|
||||
|
||||
pub fn sort_by_duration(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 1)?;
|
||||
let events: Vec<Event> = args.into_iter().next().unwrap().try_into()?;
|
||||
|
||||
// Sort by duration
|
||||
let mut sorted_events = aw_transform::sort_by_duration(events);
|
||||
// Put events back into DataType::Event container
|
||||
let mut tagged_sorted_events = Vec::new();
|
||||
for event in sorted_events.drain(..) {
|
||||
tagged_sorted_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_sorted_events))
|
||||
}
|
||||
|
||||
pub fn limit_events(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let mut events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let mut limit: usize = args.next().unwrap().try_into()?;
|
||||
|
||||
if events.len() < limit {
|
||||
limit = events.len()
|
||||
}
|
||||
let mut limited_tagged_events = Vec::new();
|
||||
for event in events.drain(0..limit) {
|
||||
limited_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(limited_tagged_events))
|
||||
}
|
||||
|
||||
pub fn sort_by_timestamp(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 1)?;
|
||||
let events: Vec<Event> = args.into_iter().next().unwrap().try_into()?;
|
||||
|
||||
// Sort by duration
|
||||
let mut sorted_events = aw_transform::sort_by_timestamp(events);
|
||||
// Put events back into DataType::Event container
|
||||
let mut tagged_sorted_events = Vec::new();
|
||||
for event in sorted_events.drain(..) {
|
||||
tagged_sorted_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_sorted_events))
|
||||
}
|
||||
|
||||
pub fn sum_durations(
|
||||
args: &[&DataType],
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(args, 1)?;
|
||||
let events = match args[0] {
|
||||
DataType::List(events) => events,
|
||||
invalid_type => {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type List, got {invalid_type:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut sum_durations = chrono::Duration::zero();
|
||||
for event in events {
|
||||
match event {
|
||||
DataType::Event(event) => sum_durations += event.duration,
|
||||
invalid_type => return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected function parameter of type List of Events, list contains {invalid_type:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
Ok(DataType::Number(
|
||||
(sum_durations.num_milliseconds() as f64) / 1000.0,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn merge_events_by_keys(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let keys: Vec<String> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut merged_events = aw_transform::merge_events_by_keys(events, keys);
|
||||
let mut merged_tagged_events = Vec::new();
|
||||
for event in merged_events.drain(..) {
|
||||
merged_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(merged_tagged_events))
|
||||
}
|
||||
|
||||
pub fn chunk_events_by_key(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let key: String = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut merged_events = aw_transform::chunk_events_by_key(events, &key);
|
||||
let mut merged_tagged_events = Vec::new();
|
||||
for event in merged_events.drain(..) {
|
||||
merged_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(merged_tagged_events))
|
||||
}
|
||||
|
||||
pub fn filter_keyvals(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 3)?;
|
||||
let mut args = args.into_iter();
|
||||
let events = args.next().unwrap().try_into()?;
|
||||
let key: String = args.next().unwrap().try_into()?;
|
||||
let vals: Vec<_> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut filtered_events = aw_transform::filter_keyvals(events, &key, &vals);
|
||||
let mut filtered_tagged_events = Vec::new();
|
||||
for event in filtered_events.drain(..) {
|
||||
filtered_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(filtered_tagged_events))
|
||||
}
|
||||
|
||||
use fancy_regex::RegexBuilder;
|
||||
|
||||
pub fn filter_keyvals_regex(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 3)?;
|
||||
let mut args = args.into_iter();
|
||||
let events = args.next().unwrap().try_into()?;
|
||||
let key: String = args.next().unwrap().try_into()?;
|
||||
let regex_str: String = args.next().unwrap().try_into()?;
|
||||
let regex = match RegexBuilder::new(®ex_str).build() {
|
||||
Ok(regex) => regex,
|
||||
Err(e) => {
|
||||
return Err(QueryError::RegexCompileError(format!(
|
||||
"Failed to compile regex string '{regex_str}': {e}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut filtered_events = aw_transform::filter_keyvals_regex(events, &key, ®ex);
|
||||
let mut filtered_tagged_events = Vec::new();
|
||||
for event in filtered_events.drain(..) {
|
||||
filtered_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(filtered_tagged_events))
|
||||
}
|
||||
|
||||
pub fn exclude_keyvals(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 3)?;
|
||||
let mut args = args.into_iter();
|
||||
let events = args.next().unwrap().try_into()?;
|
||||
let key: String = args.next().unwrap().try_into()?;
|
||||
let vals: Vec<_> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut filtered_events = aw_transform::exclude_keyvals(events, &key, &vals);
|
||||
let mut filtered_tagged_events = Vec::new();
|
||||
for event in filtered_events.drain(..) {
|
||||
filtered_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(filtered_tagged_events))
|
||||
}
|
||||
|
||||
pub fn filter_period_intersect(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let filter_events: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut filtered_events = aw_transform::filter_period_intersect(events, filter_events);
|
||||
let mut filtered_tagged_events = Vec::new();
|
||||
for event in filtered_events.drain(..) {
|
||||
filtered_tagged_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(filtered_tagged_events))
|
||||
}
|
||||
|
||||
pub fn split_url_events(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 1)?;
|
||||
let mut events: Vec<Event> = args.into_iter().next().unwrap().try_into()?;
|
||||
|
||||
let mut tagged_split_url_events = Vec::new();
|
||||
for mut event in events.drain(..) {
|
||||
aw_transform::split_url_event(&mut event);
|
||||
tagged_split_url_events.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(tagged_split_url_events))
|
||||
}
|
||||
|
||||
pub fn concat(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
let mut event_list = Vec::new();
|
||||
for arg in args {
|
||||
let mut events: Vec<Event> = arg.try_into()?;
|
||||
for event in events.drain(..) {
|
||||
event_list.push(DataType::Event(event));
|
||||
}
|
||||
}
|
||||
Ok(DataType::List(event_list))
|
||||
}
|
||||
|
||||
pub fn period_union(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events1: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let events2: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut result = aw_transform::period_union(&events1, &events2);
|
||||
let mut result_tagged = Vec::new();
|
||||
for event in result.drain(..) {
|
||||
result_tagged.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(result_tagged))
|
||||
}
|
||||
|
||||
pub fn union_no_overlap(
|
||||
args: Vec<DataType>,
|
||||
_env: &VarEnv,
|
||||
_ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
// typecheck
|
||||
validate::args_length(&args, 2)?;
|
||||
let mut args = args.into_iter();
|
||||
let events1: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
let events2: Vec<Event> = args.next().unwrap().try_into()?;
|
||||
|
||||
let mut result = aw_transform::union_no_overlap(events1, events2);
|
||||
let mut result_tagged = Vec::new();
|
||||
for event in result.drain(..) {
|
||||
result_tagged.push(DataType::Event(event));
|
||||
}
|
||||
Ok(DataType::List(result_tagged))
|
||||
}
|
||||
}
|
||||
|
||||
mod validate {
|
||||
use crate::{DataType, QueryError, VarEnv};
|
||||
use aw_models::TimeInterval;
|
||||
|
||||
pub fn args_length<T>(args: &[T], len: usize) -> Result<(), QueryError> {
|
||||
if args.len() != len {
|
||||
return Err(QueryError::InvalidFunctionParameters(format!(
|
||||
"Expected {} parameters in function, got {}",
|
||||
len,
|
||||
args.len()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_timeinterval(env: &VarEnv) -> Result<TimeInterval, QueryError> {
|
||||
let interval_str = match env.get("TIMEINTERVAL") {
|
||||
Some(data_ti) => match data_ti {
|
||||
DataType::String(ti_str) => ti_str,
|
||||
_ => {
|
||||
return Err(QueryError::TimeIntervalError(
|
||||
"TIMEINTERVAL is not of type string!".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(QueryError::TimeIntervalError(
|
||||
"TIMEINTERVAL not defined!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
match TimeInterval::new_from_string(interval_str) {
|
||||
Ok(ti) => Ok(ti),
|
||||
Err(_e) => Err(QueryError::TimeIntervalError(format!(
|
||||
"Failed to parse TIMEINTERVAL: {interval_str}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
0
aw-query/src/grammar.rs
Normal file
0
aw-query/src/grammar.rs
Normal file
403
aw-query/src/interpret.rs
Normal file
403
aw-query/src/interpret.rs
Normal file
@@ -0,0 +1,403 @@
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crate::functions;
|
||||
|
||||
use aw_datastore::Datastore;
|
||||
use aw_models::TimeInterval;
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::DataType;
|
||||
use crate::QueryError;
|
||||
|
||||
pub type VarEnv = HashMap<String, DataType>;
|
||||
|
||||
fn init_env(ti: &TimeInterval) -> VarEnv {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("TIMEINTERVAL".to_string(), DataType::String(ti.to_string()));
|
||||
functions::fill_env(&mut env);
|
||||
env
|
||||
}
|
||||
|
||||
pub fn interpret_prog(
|
||||
p: Program,
|
||||
ti: &TimeInterval,
|
||||
ds: &Datastore,
|
||||
) -> Result<DataType, QueryError> {
|
||||
let mut env = init_env(ti);
|
||||
for expr in p.stmts {
|
||||
interpret_expr(&mut env, ds, expr)?;
|
||||
}
|
||||
match env.remove("RETURN") {
|
||||
Some(ret) => Ok(ret),
|
||||
None => Err(QueryError::EmptyQuery()),
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_expr(
|
||||
env: &mut HashMap<String, DataType>,
|
||||
ds: &Datastore,
|
||||
expr: Expr,
|
||||
) -> Result<DataType, QueryError> {
|
||||
use crate::ast::Expr_::*;
|
||||
match expr.node {
|
||||
Add(a, b) => {
|
||||
let a_res = interpret_expr(env, ds, *a)?;
|
||||
let b_res = interpret_expr(env, ds, *b)?;
|
||||
let res = match a_res {
|
||||
DataType::Number(n1) => match b_res {
|
||||
DataType::Number(n2) => DataType::Number(n1 + n2),
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot use + on something that is not a number with a number!"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
DataType::List(mut l1) => match b_res {
|
||||
DataType::List(mut l2) => {
|
||||
l1.append(&mut l2);
|
||||
DataType::List(l1)
|
||||
}
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot use + on something that is not a list with a list!".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
DataType::String(s1) => match b_res {
|
||||
DataType::String(s2) => {
|
||||
let mut new_string = s1;
|
||||
new_string.push_str(&s2);
|
||||
DataType::String(new_string)
|
||||
}
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot use + on something that is not a list with a list!".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot use + on something that is not a number, list or string!"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
Sub(a, b) => {
|
||||
let a_res = interpret_expr(env, ds, *a)?;
|
||||
let b_res = interpret_expr(env, ds, *b)?;
|
||||
let a_num = match a_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let b_num = match b_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(DataType::Number(a_num - b_num))
|
||||
}
|
||||
Mul(a, b) => {
|
||||
let a_res = interpret_expr(env, ds, *a)?;
|
||||
let b_res = interpret_expr(env, ds, *b)?;
|
||||
let a_num = match a_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let b_num = match b_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(DataType::Number(a_num * b_num))
|
||||
}
|
||||
Div(a, b) => {
|
||||
let a_res = interpret_expr(env, ds, *a)?;
|
||||
let b_res = interpret_expr(env, ds, *b)?;
|
||||
let a_num = match a_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let b_num = match b_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
if b_num == 0.0 {
|
||||
return Err(QueryError::MathError(
|
||||
"Tried to divide by zero!".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(DataType::Number(a_num / b_num))
|
||||
}
|
||||
Mod(a, b) => {
|
||||
let a_res = interpret_expr(env, ds, *a)?;
|
||||
let b_res = interpret_expr(env, ds, *b)?;
|
||||
let a_num = match a_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let b_num = match b_res {
|
||||
DataType::Number(n) => n,
|
||||
_ => {
|
||||
return Err(QueryError::InvalidType(
|
||||
"Cannot sub something that is not a number!".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(DataType::Number(a_num % b_num))
|
||||
}
|
||||
Equal(lhs, rhs) => {
|
||||
let lhs_res = interpret_expr(env, ds, *lhs)?;
|
||||
let rhs_res = interpret_expr(env, ds, *rhs)?;
|
||||
Ok(DataType::Bool(lhs_res.query_eq(&rhs_res)?))
|
||||
}
|
||||
Assign(var, b) => {
|
||||
let val = interpret_expr(env, ds, *b)?;
|
||||
env.insert(var, val);
|
||||
Ok(DataType::None())
|
||||
}
|
||||
// This clone is the one necessary copy per variable reference: the env
|
||||
// must retain the value since it may be referenced again, while
|
||||
// function arguments and transforms need owned events.
|
||||
Var(var) => match env.get(&var) {
|
||||
Some(v) => Ok(v.clone()),
|
||||
None => Err(QueryError::VariableNotDefined(var.to_string())),
|
||||
},
|
||||
Bool(lit) => Ok(DataType::Bool(lit)),
|
||||
Number(lit) => Ok(DataType::Number(lit)),
|
||||
String(litstr) => Ok(DataType::String(litstr)),
|
||||
Return(e) => {
|
||||
let val = interpret_expr(env, ds, *e)?;
|
||||
// TODO: Once RETURN is deprecated we can fix this
|
||||
env.insert("RETURN".to_string(), val);
|
||||
Ok(DataType::None())
|
||||
}
|
||||
If(ifs) => {
|
||||
for (cond, block) in ifs {
|
||||
let c = interpret_expr(env, ds, *cond)?;
|
||||
if c.query_eq(&DataType::Bool(true))? {
|
||||
for expr in block {
|
||||
interpret_expr(env, ds, expr)?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(DataType::None())
|
||||
}
|
||||
Function(fname, e) => {
|
||||
// Borrow variables only when every argument is an atom. Evaluating
|
||||
// arbitrary arguments may assign to the environment, including the
|
||||
// function binding itself, so those retain the owned evaluation path.
|
||||
if let Expr_::List(exprs) = &e.node {
|
||||
if exprs.iter().all(|expr| {
|
||||
matches!(
|
||||
expr.node,
|
||||
Expr_::Var(_) | Expr_::Bool(_) | Expr_::Number(_) | Expr_::String(_)
|
||||
)
|
||||
}) {
|
||||
if let Some(DataType::ReadOnlyFunction(_, fun)) = env.get(&fname) {
|
||||
let args: Result<Vec<Cow<'_, DataType>>, QueryError> = exprs
|
||||
.iter()
|
||||
.map(|expr| {
|
||||
Ok(match &expr.node {
|
||||
Expr_::Var(name) => {
|
||||
Cow::Borrowed(env.get(name).ok_or_else(|| {
|
||||
QueryError::VariableNotDefined(name.clone())
|
||||
})?)
|
||||
}
|
||||
Expr_::Bool(value) => Cow::Owned(DataType::Bool(*value)),
|
||||
Expr_::Number(value) => Cow::Owned(DataType::Number(*value)),
|
||||
Expr_::String(value) => {
|
||||
Cow::Owned(DataType::String(value.clone()))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let args = args?;
|
||||
let refs: Vec<_> = args.iter().map(|arg| arg.as_ref()).collect();
|
||||
return fun(&refs, env, ds);
|
||||
}
|
||||
}
|
||||
}
|
||||
let args = match interpret_expr(env, ds, *e)? {
|
||||
DataType::List(l) => l,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let var = match env.get(&fname[..]) {
|
||||
Some(v) => v,
|
||||
None => return Err(QueryError::VariableNotDefined(fname.clone())),
|
||||
};
|
||||
match var {
|
||||
DataType::Function(_, fun) => fun(args, env, ds),
|
||||
DataType::ReadOnlyFunction(_, fun) => {
|
||||
let refs: Vec<_> = args.iter().collect();
|
||||
fun(&refs, env, ds)
|
||||
}
|
||||
_ => Err(QueryError::InvalidType(fname.to_string())),
|
||||
}
|
||||
}
|
||||
List(list) => {
|
||||
let mut l = Vec::new();
|
||||
for entry in list {
|
||||
let res = interpret_expr(env, ds, entry)?;
|
||||
l.push(res);
|
||||
}
|
||||
Ok(DataType::List(l))
|
||||
}
|
||||
Dict(d) => {
|
||||
let mut dict = HashMap::new();
|
||||
for (key, val_uninterpreted) in d {
|
||||
let val = interpret_expr(env, ds, val_uninterpreted)?;
|
||||
dict.insert(key, val);
|
||||
}
|
||||
Ok(DataType::Dict(dict))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(code: &str, env: &mut VarEnv, ds: &Datastore) -> DataType {
|
||||
let program = crate::parser::parse(crate::lexer::Lexer::new(code)).unwrap();
|
||||
for expr in program.stmts {
|
||||
interpret_expr(env, ds, expr).unwrap();
|
||||
}
|
||||
env.remove("RETURN").unwrap_or(DataType::None())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_arguments_borrow_the_original_value_and_support_aliases() {
|
||||
fn check(args: &[&DataType], env: &VarEnv, _: &Datastore) -> Result<DataType, QueryError> {
|
||||
assert!(std::ptr::eq(args[0], env.get("events").unwrap()));
|
||||
Ok(DataType::Bool(true))
|
||||
}
|
||||
let ds = Datastore::new_in_memory(false);
|
||||
let mut env = VarEnv::new();
|
||||
env.insert(
|
||||
"check".into(),
|
||||
DataType::ReadOnlyFunction("check".into(), check),
|
||||
);
|
||||
env.insert(
|
||||
"events".into(),
|
||||
DataType::List(vec![DataType::Event(aw_models::Event::default())]),
|
||||
);
|
||||
assert_eq!(
|
||||
run("alias = check; return alias(events);", &mut env, &ds),
|
||||
DataType::Bool(true)
|
||||
);
|
||||
assert!(env.contains_key("events"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_effecting_arguments_keep_snapshots_and_resolve_the_function_after_evaluation() {
|
||||
fn check(args: &[&DataType], env: &VarEnv, _: &Datastore) -> Result<DataType, QueryError> {
|
||||
assert_eq!(args[0], &DataType::List(vec![DataType::Number(1.0)]));
|
||||
assert_eq!(env["items"], DataType::List(vec![DataType::Number(2.0)]));
|
||||
Ok(DataType::Bool(true))
|
||||
}
|
||||
let ds = Datastore::new_in_memory(false);
|
||||
let mut env = VarEnv::new();
|
||||
env.insert(
|
||||
"check".into(),
|
||||
DataType::ReadOnlyFunction("check".into(), check),
|
||||
);
|
||||
// Assignment nodes are supported by the interpreter, although the
|
||||
// current parser only emits them at statement level.
|
||||
let expr = |node| Expr {
|
||||
span: crate::lexer::Span {
|
||||
lo: 0,
|
||||
hi: 0,
|
||||
line: 1,
|
||||
},
|
||||
node,
|
||||
};
|
||||
for rebind in [false, true] {
|
||||
env.insert("items".into(), DataType::List(vec![DataType::Number(1.0)]));
|
||||
env.insert(
|
||||
"f".into(),
|
||||
if rebind {
|
||||
DataType::Number(0.0)
|
||||
} else {
|
||||
env["check"].clone()
|
||||
},
|
||||
);
|
||||
let mut args = vec![
|
||||
expr(Expr_::Var("items".into())),
|
||||
expr(Expr_::Assign(
|
||||
"items".into(),
|
||||
Box::new(expr(Expr_::List(vec![expr(Expr_::Number(2.0))]))),
|
||||
)),
|
||||
];
|
||||
if rebind {
|
||||
args.push(expr(Expr_::Assign(
|
||||
"f".into(),
|
||||
Box::new(expr(Expr_::Var("check".into()))),
|
||||
)));
|
||||
}
|
||||
let call = expr(Expr_::Function(
|
||||
"f".into(),
|
||||
Box::new(expr(Expr_::List(args))),
|
||||
));
|
||||
assert_eq!(
|
||||
interpret_expr(&mut env, &ds, call).unwrap(),
|
||||
DataType::Bool(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_builtins_keep_values_available_and_accept_computed_arguments() {
|
||||
let ds = Datastore::new_in_memory(false);
|
||||
let mut env = VarEnv::new();
|
||||
functions::fill_env(&mut env);
|
||||
let mut event = aw_models::Event::default();
|
||||
event.duration = chrono::Duration::seconds(3);
|
||||
env.insert(
|
||||
"events".into(),
|
||||
DataType::List(vec![DataType::Event(event)]),
|
||||
);
|
||||
assert_eq!(run("total = sum_durations; a = total(events); b = total(events + events); return a + b;", &mut env, &ds), DataType::Number(9.0));
|
||||
assert_eq!(
|
||||
run(
|
||||
"values = [1, 2]; f = contains; a = f(values, 2); return [a, values];",
|
||||
&mut env,
|
||||
&ds
|
||||
),
|
||||
DataType::List(vec![
|
||||
DataType::Bool(true),
|
||||
DataType::List(vec![DataType::Number(1.0), DataType::Number(2.0)])
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
144
aw-query/src/lexer.rs
Normal file
144
aw-query/src/lexer.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use plex::lexer;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Token {
|
||||
Ident(String),
|
||||
|
||||
If,
|
||||
ElseIf,
|
||||
Else,
|
||||
Return,
|
||||
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
Percent,
|
||||
Equals,
|
||||
Assign,
|
||||
LParen,
|
||||
RParen,
|
||||
LBracket,
|
||||
RBracket,
|
||||
LBrace,
|
||||
RBrace,
|
||||
Comma,
|
||||
Colon,
|
||||
Semi,
|
||||
|
||||
Whitespace,
|
||||
Newline,
|
||||
Comment,
|
||||
}
|
||||
|
||||
lexer! {
|
||||
fn next_token(text: 'a) -> (Token, &'a str);
|
||||
|
||||
r#"[ \t\r]+"# => (Token::Whitespace, text),
|
||||
r#"\n"# => (Token::Newline, text),
|
||||
// Python-style comments (# ...)
|
||||
r#"#[^\n]*"# => (Token::Comment, text),
|
||||
|
||||
r#"if"# => (Token::If, text),
|
||||
r#"elif"# => (Token::ElseIf, text),
|
||||
r#"else"# => (Token::Else, text),
|
||||
r#"return"# => (Token::Return, text),
|
||||
|
||||
r#"true"# => (Token::Bool(true), text),
|
||||
r#"false"# => (Token::Bool(false), text),
|
||||
// TODO: Deprecate/Remove?
|
||||
r#"True"# => (Token::Bool(true), text),
|
||||
r#"False"# => (Token::Bool(false), text),
|
||||
|
||||
r#"\"([^\"]|(\\\"))*\""# => (
|
||||
Token::String(text.to_owned()[1..text.len()-1].replace("\\\"", "\"")),
|
||||
text
|
||||
),
|
||||
r#"[0-9]+[\.]?[0-9]*"# => {
|
||||
let tok = match text.parse() {
|
||||
Ok(n) => Token::Number(n),
|
||||
Err(e) => panic!("Integer {text} is out of range: {e}"),
|
||||
};
|
||||
(tok, text)
|
||||
}
|
||||
|
||||
r#"[a-zA-Z_][a-zA-Z0-9_]*"# => (Token::Ident(text.to_owned()), text),
|
||||
|
||||
r#"=="# => (Token::Equals, text),
|
||||
r#"="# => (Token::Assign, text),
|
||||
r#"\+"# => (Token::Plus, text),
|
||||
r#"-"# => (Token::Minus, text),
|
||||
r#"\*"# => (Token::Star, text),
|
||||
r#"/"# => (Token::Slash, text),
|
||||
r#"%"# => (Token::Percent, text),
|
||||
r#"\("# => (Token::LParen, text),
|
||||
r#"\)"# => (Token::RParen, text),
|
||||
r#"\["# => (Token::LBracket, text),
|
||||
r#"\]"# => (Token::RBracket, text),
|
||||
r#"\{"# => (Token::LBrace, text),
|
||||
r#"\}"# => (Token::RBrace, text),
|
||||
r#","# => (Token::Comma, text),
|
||||
r#":"# => (Token::Colon, text),
|
||||
r#";"# => (Token::Semi, text),
|
||||
}
|
||||
|
||||
pub struct Lexer<'a> {
|
||||
original: &'a str,
|
||||
remaining: &'a str,
|
||||
line: usize,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(s: &'a str) -> Lexer<'a> {
|
||||
Lexer {
|
||||
original: s,
|
||||
remaining: s,
|
||||
line: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Span {
|
||||
pub lo: usize,
|
||||
pub hi: usize,
|
||||
pub line: usize,
|
||||
}
|
||||
|
||||
fn span_in(s: &str, t: &str, l: usize) -> Span {
|
||||
let lo = s.as_ptr() as usize - t.as_ptr() as usize;
|
||||
Span {
|
||||
lo,
|
||||
hi: lo + s.len(),
|
||||
line: l,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Lexer<'a> {
|
||||
type Item = (Token, Span);
|
||||
fn next(&mut self) -> Option<(Token, Span)> {
|
||||
loop {
|
||||
let tok = if let Some((tok, new_remaining)) = next_token(self.remaining) {
|
||||
self.remaining = new_remaining;
|
||||
tok
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
match tok {
|
||||
(Token::Whitespace, _) | (Token::Comment, _) => {
|
||||
continue;
|
||||
}
|
||||
(Token::Newline, _) => {
|
||||
self.line += 1;
|
||||
continue;
|
||||
}
|
||||
(tok, span) => {
|
||||
return Some((tok, span_in(span, self.original, self.line)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
aw-query/src/lib.rs
Normal file
65
aw-query/src/lib.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use aw_models::TimeInterval;
|
||||
|
||||
use aw_datastore::Datastore;
|
||||
|
||||
pub mod datatype;
|
||||
|
||||
mod ast;
|
||||
mod functions;
|
||||
mod interpret;
|
||||
mod lexer;
|
||||
#[allow(
|
||||
clippy::match_single_binding,
|
||||
clippy::redundant_closure_call,
|
||||
unused_braces
|
||||
)]
|
||||
mod parser;
|
||||
|
||||
pub use crate::datatype::DataType;
|
||||
pub use crate::interpret::VarEnv;
|
||||
|
||||
// TODO: add line numbers to errors
|
||||
// (works during lexing, but not during parsing I believe)
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum QueryError {
|
||||
// Parser
|
||||
ParsingError(String),
|
||||
|
||||
// Execution
|
||||
EmptyQuery(),
|
||||
VariableNotDefined(String),
|
||||
MathError(String),
|
||||
InvalidType(String),
|
||||
InvalidFunctionParameters(String),
|
||||
TimeIntervalError(String),
|
||||
BucketNotFound(String),
|
||||
BucketQueryError(String),
|
||||
RegexCompileError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for QueryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query(code: &str, ti: &TimeInterval, ds: &Datastore) -> Result<DataType, QueryError> {
|
||||
let lexer = lexer::Lexer::new(code);
|
||||
let program = match parser::parse(lexer) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
// TODO: Improve parsing error message
|
||||
warn!("ParsingError: {:?}", e);
|
||||
return Err(QueryError::ParsingError(format!("{e:?}")));
|
||||
}
|
||||
};
|
||||
interpret::interpret_prog(program, ti, ds)
|
||||
}
|
||||
272
aw-query/src/parser.rs
Normal file
272
aw-query/src/parser.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
#![allow(clippy::ptr_arg)]
|
||||
#![allow(clippy::vec_init_then_push)]
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::lexer::Token::*;
|
||||
use crate::lexer::*;
|
||||
use plex::parser;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn merge_if_vecs(lhs: Expr_, rhs: Expr_) -> Expr_ {
|
||||
let mut ifs = match lhs {
|
||||
Expr_::If(_ifs) => _ifs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
match rhs {
|
||||
Expr_::If(mut _ifs) => ifs.append(&mut _ifs),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Expr_::If(ifs)
|
||||
}
|
||||
|
||||
parser! {
|
||||
fn parse_(Token, Span);
|
||||
|
||||
// combine two spans
|
||||
(a, b) {
|
||||
Span {
|
||||
lo: a.lo,
|
||||
hi: b.hi,
|
||||
line: a.line,
|
||||
}
|
||||
}
|
||||
|
||||
program: Program {
|
||||
statements[s] => Program { stmts: s }
|
||||
}
|
||||
|
||||
statements: Vec<Expr> {
|
||||
=> vec![],
|
||||
statements[mut st] statement[x] => {
|
||||
st.push(x);
|
||||
st
|
||||
},
|
||||
statements[st] Semi => st,
|
||||
}
|
||||
|
||||
statement: Expr {
|
||||
ifs[x] => x,
|
||||
ret[x] Semi => x,
|
||||
}
|
||||
|
||||
ifs: Expr {
|
||||
_if[l_ifs] => l_ifs,
|
||||
_elif[l_ifs] => l_ifs,
|
||||
_else[l_ifs] => l_ifs,
|
||||
}
|
||||
|
||||
_cond_block: Expr {
|
||||
binop[cond] LBrace statements[block] RBrace => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let mut ifs = Vec::new();
|
||||
ifs.push((Box::new(cond), block));
|
||||
Expr_::If(ifs)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_if: Expr {
|
||||
If _cond_block[x] => x
|
||||
}
|
||||
|
||||
_elif: Expr {
|
||||
// Else if
|
||||
_if[l_ifs] ElseIf _cond_block[l_preceding_ifs] => Expr {
|
||||
span: span!(),
|
||||
node: merge_if_vecs(l_ifs.node, l_preceding_ifs.node),
|
||||
},
|
||||
// Else if else if
|
||||
_elif[l_ifs] ElseIf _cond_block[l_preceding_ifs] => Expr {
|
||||
span: span!(),
|
||||
node: merge_if_vecs(l_ifs.node, l_preceding_ifs.node),
|
||||
},
|
||||
}
|
||||
|
||||
_else: Expr {
|
||||
// if else
|
||||
_if[l_ifs] Else LBrace statements[l_else_block] RBrace => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let mut l_new = match l_ifs.node {
|
||||
Expr_::If(l_ifs) => l_ifs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let true_expr = Expr { span: span!(), node: Expr_::Bool(true) };
|
||||
l_new.push((Box::new(true_expr), l_else_block));
|
||||
Expr_::If(l_new)
|
||||
}
|
||||
},
|
||||
// else if else
|
||||
_elif[l_ifs] Else LBrace statements[l_else_block] RBrace => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let mut l_new = match l_ifs.node {
|
||||
Expr_::If(l_ifs) => l_ifs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let true_expr = Expr { span: span!(), node: Expr_::Bool(true) };
|
||||
l_new.push((Box::new(true_expr), l_else_block));
|
||||
Expr_::If(l_new)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ret: Expr {
|
||||
Return assign[a] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Return(Box::new(a)),
|
||||
},
|
||||
assign[x] => x,
|
||||
}
|
||||
|
||||
assign: Expr {
|
||||
Ident(var) Assign binop[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Assign(var, Box::new(rhs)),
|
||||
},
|
||||
binop[x] => x
|
||||
}
|
||||
|
||||
binop: Expr {
|
||||
binop[lhs] Plus func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Add(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
binop[lhs] Minus func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Sub(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
binop[lhs] Star func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Mul(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
binop[lhs] Slash func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Div(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
binop[lhs] Percent func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Mod(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
binop[lhs] Equals func[rhs] => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Equal(Box::new(lhs), Box::new(rhs)),
|
||||
},
|
||||
func[x] => x
|
||||
}
|
||||
|
||||
func: Expr {
|
||||
Ident(fname) LParen _inner_list[l] RParen => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Function(fname, Box::new(l)),
|
||||
},
|
||||
Ident(fname) LParen RParen => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let empty_expr_list = Expr {
|
||||
span: span!(),
|
||||
node: Expr_::List(Vec::new())
|
||||
};
|
||||
Expr_::Function(fname, Box::new(empty_expr_list))
|
||||
},
|
||||
},
|
||||
object[o] => o,
|
||||
}
|
||||
|
||||
object: Expr {
|
||||
LBrace dict[d] RBrace => d,
|
||||
LBrace RBrace => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
Expr_::Dict(HashMap::new())
|
||||
}
|
||||
},
|
||||
list[l] => l
|
||||
}
|
||||
|
||||
list: Expr {
|
||||
LBracket _inner_list[l] RBracket => l,
|
||||
LBracket RBracket => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
Expr_::List(Vec::new())
|
||||
}
|
||||
},
|
||||
atom[a] => a
|
||||
}
|
||||
|
||||
_inner_list: Expr {
|
||||
binop[o] => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let mut list = Vec::new();
|
||||
list.push(o);
|
||||
Expr_::List(list)
|
||||
}
|
||||
},
|
||||
_inner_list[l] Comma binop[o] => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
match l.node {
|
||||
Expr_::List(mut l) => {
|
||||
l.push(o);
|
||||
Expr_::List(l)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
dict: Expr {
|
||||
String(k) Colon binop[v] => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
let mut dict = HashMap::new();
|
||||
dict.insert(k, v);
|
||||
Expr_::Dict(dict)
|
||||
}
|
||||
},
|
||||
dict[d] Comma String(k) Colon binop[v] => Expr {
|
||||
span: span!(),
|
||||
node: {
|
||||
match d.node {
|
||||
Expr_::Dict(mut d) => {
|
||||
d.insert(k, v);
|
||||
Expr_::Dict(d)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
atom: Expr {
|
||||
// round brackets to destructure tokens
|
||||
Ident(v) => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Var(v),
|
||||
},
|
||||
Bool(b) => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Bool(b),
|
||||
},
|
||||
Number(i) => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::Number(i),
|
||||
},
|
||||
String(s) => Expr {
|
||||
span: span!(),
|
||||
node: Expr_::String(s),
|
||||
},
|
||||
LParen binop[x] RParen => x
|
||||
}
|
||||
}
|
||||
|
||||
pub type ParseError = (Option<(Token, Span)>, &'static str);
|
||||
|
||||
pub fn parse<I: Iterator<Item = (Token, Span)>>(i: I) -> Result<Program, ParseError> {
|
||||
parse_(i)
|
||||
}
|
||||
778
aw-query/tests/query.rs
Normal file
778
aw-query/tests/query.rs
Normal file
@@ -0,0 +1,778 @@
|
||||
extern crate chrono;
|
||||
extern crate serde_json;
|
||||
|
||||
extern crate aw_query;
|
||||
|
||||
// TODO: Move me to an appropriate place
|
||||
#[macro_export]
|
||||
macro_rules! json_map {
|
||||
{ $( $key:literal : $value:expr),* } => {{
|
||||
use serde_json::Value;
|
||||
use serde_json::map::Map;
|
||||
#[allow(unused_mut)]
|
||||
let mut map : Map<String, Value> = Map::new();
|
||||
$(
|
||||
map.insert( $key.to_string(), json!($value) );
|
||||
)*
|
||||
map
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod query_tests {
|
||||
|
||||
use chrono::Duration;
|
||||
use serde_json::json;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use aw_query::DataType;
|
||||
use aw_query::QueryError;
|
||||
|
||||
use aw_datastore::Datastore;
|
||||
|
||||
use aw_models::Bucket;
|
||||
use aw_models::BucketMetadata;
|
||||
use aw_models::Event;
|
||||
use aw_models::TimeInterval;
|
||||
|
||||
static TIME_INTERVAL: &str = "1980-01-01T00:00:00Z/2080-01-02T00:00:00Z";
|
||||
static BUCKET_ID: &str = "testid";
|
||||
|
||||
fn setup_datastore_empty() -> Datastore {
|
||||
Datastore::new_in_memory(false)
|
||||
}
|
||||
|
||||
fn setup_datastore_with_bucket() -> Datastore {
|
||||
let ds = setup_datastore_empty();
|
||||
// Create bucket
|
||||
let bucket = Bucket {
|
||||
bid: None,
|
||||
id: BUCKET_ID.to_string(),
|
||||
_type: "testtype".to_string(),
|
||||
client: "testclient".to_string(),
|
||||
hostname: "testhost".to_string(),
|
||||
created: Some(chrono::Utc::now()),
|
||||
data: json_map! {},
|
||||
metadata: BucketMetadata::default(),
|
||||
events: None,
|
||||
last_updated: None,
|
||||
};
|
||||
ds.create_bucket(&bucket).unwrap();
|
||||
ds
|
||||
}
|
||||
|
||||
fn setup_datastore_populated() -> Datastore {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
// Insert events
|
||||
let e1 = Event {
|
||||
id: None,
|
||||
timestamp: chrono::Utc::now(),
|
||||
duration: Duration::seconds(0),
|
||||
data: json_map! {"key": json!("value")},
|
||||
};
|
||||
let mut e2 = e1.clone();
|
||||
e2.timestamp = chrono::Utc::now();
|
||||
let mut e_replace = e2.clone();
|
||||
e_replace.data = json_map! {"key": json!("value2")};
|
||||
e_replace.duration = Duration::seconds(2);
|
||||
|
||||
let event_list = vec![e1, e2];
|
||||
|
||||
ds.insert_events(BUCKET_ID, &event_list).unwrap();
|
||||
|
||||
ds
|
||||
}
|
||||
|
||||
macro_rules! assert_err_type {
|
||||
($v:expr, $p:pat) => {
|
||||
match $v {
|
||||
Ok(_) => panic!("Expected an error, got {:?}", $v),
|
||||
Err(e) => match e {
|
||||
$p => (),
|
||||
_ => panic!("Expected an error of another type, got {:?}", e),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("True;False;a=True;return True;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("1;1.;return 1.1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 1.1),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_equals() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// number comparison true
|
||||
let code = String::from("return 1==1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// number comparison false
|
||||
let code = String::from("return 2==1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(!b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// string comparison true
|
||||
let code = String::from(r#"return "a"=="a";"#);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// string comparison false
|
||||
let code = String::from(r#"return "a"=="b";"#);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(!b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// bool comparison true
|
||||
let code = String::from("return True==True;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// bool comparison false
|
||||
let code = String::from("return False==True;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(!b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// different types comparison (should raise an error)
|
||||
let code = String::from("return True==1;");
|
||||
let res = aw_query::query(&code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidType(_));
|
||||
|
||||
// None comparison, consistent with the PartialEq impl on DataType.
|
||||
// print() returns None, so this compares two None values.
|
||||
let code = String::from(r#"a=print("a"); b=print("b"); return a==b;"#);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Bool(b) => assert!(b),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return 1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 1.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test with old RETURN method
|
||||
let code = String::from("RETURN=1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 1.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 1+1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// Test hardcoded True
|
||||
let code = String::from(
|
||||
"
|
||||
n=1;
|
||||
if True { n=2; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test hardcoded False
|
||||
let code = String::from(
|
||||
"
|
||||
n=1;
|
||||
if False { n=2; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 1.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test expression True
|
||||
let code = String::from(
|
||||
"
|
||||
a=True; n=1;
|
||||
if a { n=2; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test expression False
|
||||
let code = String::from(
|
||||
"
|
||||
a=False; n=1;
|
||||
if a { n=2; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 1.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test if else
|
||||
let code = String::from(
|
||||
"
|
||||
a=False; n=1;
|
||||
if a { }
|
||||
else { n=3; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 3.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test if else if
|
||||
let code = String::from(
|
||||
"
|
||||
a=False; b=True; n=1;
|
||||
if a { n=2; }
|
||||
elif b { n=3; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 3.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test if else if else
|
||||
let code = String::from(
|
||||
"
|
||||
a=False; b=True; n=1;
|
||||
if a { n=2; }
|
||||
elif a { n=3; }
|
||||
else { n=4; }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 4.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
|
||||
// Test if inside if
|
||||
let code = String::from(
|
||||
"
|
||||
a=True; n=1;
|
||||
if a { if a { n = 2; } }
|
||||
return n;",
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return print(1);");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return print(1, 2);");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return no_such_function(1);");
|
||||
match aw_query::query(&code, &interval, &ds) {
|
||||
Ok(ok) => panic!("Expected QueryError, got {ok:?}"),
|
||||
Err(e) => match e {
|
||||
QueryError::VariableNotDefined(qe) => assert_eq!(qe, "no_such_function"),
|
||||
qe => panic!("Expected QueryError::VariableNotDefined, got {qe:?}"),
|
||||
},
|
||||
}
|
||||
|
||||
let code = String::from("invalid_type=1; return invalid_type(1);");
|
||||
match aw_query::query(&code, &interval, &ds) {
|
||||
Ok(ok) => panic!("Expected QueryError, got {ok:?}"),
|
||||
Err(e) => match e {
|
||||
QueryError::InvalidType(qe) => assert_eq!(qe, "invalid_type"),
|
||||
qe => panic!("Expected QueryError::VariableNotDefined, got {qe:?}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_functions() {
|
||||
let ds = setup_datastore_populated();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return query_bucket(\"testid\");");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket(find_bucket("{}", "testhost"));
|
||||
events = flood(events);
|
||||
events = sort_by_duration(events);
|
||||
events = limit_events(events, 10000);
|
||||
events = sort_by_timestamp(events);
|
||||
events = concat(events, query_bucket("{}"));
|
||||
events = categorize(events, [[["test"], {{ "type": "regex", "regex": "value$" }}], [["test", "testing"], {{ "type": "regex", "regex": "value$" }}]]);
|
||||
events = tag(events, [["testtag", {{ "type": "regex", "regex": "test$" }}], ["another testtag", {{ "type": "regex", "regex": "test-pat$" }}]]);
|
||||
total_duration = sum_durations(events);
|
||||
bucketnames = query_bucket_names();
|
||||
print("test", "test2");
|
||||
url_events = split_url_events (events);
|
||||
filtered_events = filter_period_intersect(events, events);
|
||||
filtered_events = filter_keyvals(events, "$category", [["Uncategorized"]]);
|
||||
filtered_events = filter_keyvals_regex(events, "key", "regex");
|
||||
chunked_events = chunk_events_by_key(events, "key");
|
||||
merged_events = merge_events_by_keys(events, ["key"]);
|
||||
return merged_events;"#,
|
||||
"testid", "testid"
|
||||
);
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::List(l) => l,
|
||||
ref data => panic!("Wrong datatype, {data:?}"),
|
||||
};
|
||||
// TODO: assert_eq result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_categorize() {
|
||||
let ds = setup_datastore_populated();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = categorize(events, [[["Test", "Subtest"], {{ "type": "regex", "regex": "^value$" }}]]);
|
||||
return events;"#,
|
||||
"testid"
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
|
||||
let event = events.first().unwrap();
|
||||
let cats = event.data.get("$category").unwrap();
|
||||
assert_eq!(cats, &serde_json::json!(vec!["Test", "Subtest"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_categorize_priority_overrides_depth() {
|
||||
let ds = setup_datastore_populated();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// Without priority, the deeper rule wins (existing contract).
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = categorize(events, [[["A"], {{ "type": "regex", "regex": "^value$" }}], [["B", "B1"], {{ "type": "regex", "regex": "^value$" }}]]);
|
||||
return events;"#,
|
||||
"testid"
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
assert_eq!(
|
||||
events.first().unwrap().data.get("$category").unwrap(),
|
||||
&serde_json::json!(vec!["B", "B1"])
|
||||
);
|
||||
|
||||
// With an explicit priority on A, organizational depth no longer wins.
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = categorize(events, [[["A"], {{ "type": "regex", "regex": "^value$", "priority": 25 }}], [["B", "B1"], {{ "type": "regex", "regex": "^value$" }}]]);
|
||||
return events;"#,
|
||||
"testid"
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
assert_eq!(
|
||||
events.first().unwrap().data.get("$category").unwrap(),
|
||||
&serde_json::json!(vec!["A"])
|
||||
);
|
||||
|
||||
// `weight` is accepted as an alias for `priority`.
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = categorize(events, [[["A"], {{ "type": "regex", "regex": "^value$", "weight": 25 }}], [["B", "B1"], {{ "type": "regex", "regex": "^value$" }}]]);
|
||||
return events;"#,
|
||||
"testid"
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
assert_eq!(
|
||||
events.first().unwrap().data.get("$category").unwrap(),
|
||||
&serde_json::json!(vec!["A"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag() {
|
||||
let ds = setup_datastore_populated();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = tag(events, [["testtag", {{ "type": "regex", "regex": "value$" }}], ["another testtag", {{ "type": "regex", "regex": "value$" }}]]);
|
||||
return events;"#,
|
||||
"testid"
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
|
||||
let event = events.first().unwrap();
|
||||
let tags = event.data.get("$tags").unwrap().as_array().unwrap();
|
||||
assert_eq!(tags.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_select_keys() {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let event = Event {
|
||||
id: None,
|
||||
timestamp: chrono::Utc::now(),
|
||||
duration: Duration::seconds(0),
|
||||
data: json_map! {
|
||||
"app": json!("terminal"),
|
||||
"title": json!("just a test"),
|
||||
"pid": json!(123)
|
||||
},
|
||||
};
|
||||
ds.insert_events(BUCKET_ID, &[event]).unwrap();
|
||||
|
||||
let code = format!(
|
||||
r#"
|
||||
events = query_bucket("{}");
|
||||
events = tag(events, [["title-match", {{ "type": "regex", "regex": "test$", "select_keys": ["title"] }}], ["app-match", {{ "type": "regex", "regex": "test$", "select_keys": ["app"] }}], ["pid-match", {{ "type": "regex", "regex": "123", "select_keys": ["pid"] }}], ["missing-match", {{ "type": "regex", "regex": "test", "select_keys": ["missing"] }}]]);
|
||||
return events;"#,
|
||||
BUCKET_ID
|
||||
);
|
||||
let result: DataType = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let events: Vec<Event> = Vec::try_from(&result).unwrap();
|
||||
|
||||
let event = events.first().unwrap();
|
||||
let tags = event.data.get("$tags").unwrap();
|
||||
assert_eq!(tags, &serde_json::json!(vec!["title-match"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rule_parsing() {
|
||||
let ds = setup_datastore_populated();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// Test rule where rule is not a dict
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, ["test", false]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test rule without type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test invalid rule type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": false }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test invalid rule name
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "rgex" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test "none" rule type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "none" }]]);
|
||||
return events;"#;
|
||||
aw_query::query(code, &interval, &ds).unwrap();
|
||||
|
||||
// Test regex rule where regex field has wrong type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": true }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test regex rule where regex field is not set
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test regex rule with ignore_case field
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "test", "ignore_case": false }]]);
|
||||
return events;"#;
|
||||
aw_query::query(code, &interval, &ds).unwrap();
|
||||
|
||||
// Test regex rule with valid select_keys field
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "test", "select_keys": ["key"] }]]);
|
||||
return events;"#;
|
||||
aw_query::query(code, &interval, &ds).unwrap();
|
||||
|
||||
// Test regex rule where ignore_case field is of invalid type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "test", "ignore_case": "" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test regex rule where select_keys field is of invalid type
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "test", "select_keys": "key" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test regex rule where select_keys contains non-string values
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "test", "select_keys": ["key", false] }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test regex rule where uncompilable regex is supplied
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = tag(events, [["testtag", { "type": "regex", "regex": "!#¤%&/(=" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::RegexCompileError(_));
|
||||
|
||||
// Test categorize rule with non-integer priority
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = categorize(events, [[["test"], { "type": "regex", "regex": "test", "priority": 1.5 }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
|
||||
// Test categorize rule with non-numeric priority
|
||||
let code = r#"
|
||||
events = [];
|
||||
events = categorize(events, [[["test"], { "type": "regex", "regex": "test", "priority": "high" }]]);
|
||||
return events;"#;
|
||||
let res = aw_query::query(code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::InvalidFunctionParameters(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return \"test \\\" with escaped quote\";");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
aw_query::DataType::String(s) => assert_eq!(s, "test \" with escaped quote"),
|
||||
_ => panic!("Wrong datatype"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return [];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return [1];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return [1+1];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return [1,1];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return [1,1+2];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return [1,1+1,1+2+3,4/3,[1+2]];");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comment() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return 1;# testing 123");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dict() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return {};");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return {\"test\": 2};");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return {\"test\": 2, \"test2\": \"teststr\"};");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
|
||||
let code = String::from("return {\"test\": {\"test\": \"test\"}};");
|
||||
aw_query::query(&code, &interval, &ds).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// Append lists
|
||||
let code = String::from("return [1]+[2];");
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
let v = vec![DataType::Number(1.0), DataType::Number(2.0)];
|
||||
assert_eq!(res, DataType::List(v));
|
||||
|
||||
// Append strings
|
||||
let code = String::from(r#"return "a"+"b";"#);
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
assert_eq!(res, DataType::String("ab".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
// test list true
|
||||
let code = String::from(r#"a = ["b", "a"]; return contains(a, "a");"#);
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
assert_eq!(res, DataType::Bool(true));
|
||||
|
||||
// test list false
|
||||
let code = String::from(r#"a = ["b", "a"]; return contains(a, "c");"#);
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
assert_eq!(res, DataType::Bool(false));
|
||||
|
||||
// test dict true
|
||||
let code = String::from(r#"a = {"a": 1}; return contains(a, "a");"#);
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
assert_eq!(res, DataType::Bool(true));
|
||||
|
||||
// test dict false
|
||||
let code = String::from(r#"a = {"b": 1}; return contains(a, "a");"#);
|
||||
let res = aw_query::query(&code, &interval, &ds).unwrap();
|
||||
assert_eq!(res, DataType::Bool(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math() {
|
||||
let ds = setup_datastore_empty();
|
||||
let interval = TimeInterval::new_from_string(TIME_INTERVAL).unwrap();
|
||||
|
||||
let code = String::from("return 1+1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 1-1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 0.0),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 3*5;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 15.0),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 4/2;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 2.0),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 1/0;");
|
||||
let res = aw_query::query(&code, &interval, &ds);
|
||||
assert_err_type!(res, QueryError::MathError(_));
|
||||
|
||||
let code = String::from("return 2.5%1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 0.5),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
|
||||
let code = String::from("return 1+1+0+1;");
|
||||
match aw_query::query(&code, &interval, &ds).unwrap() {
|
||||
DataType::Number(n) => assert_eq!(n, 3.0),
|
||||
num => panic!("Expected number, got {num:?}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
229
aw-query/tests/regex_fuzz.rs
Normal file
229
aw-query/tests/regex_fuzz.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
//! Property-based fuzz tests for regex compilation in aw-query.
|
||||
//!
|
||||
//! These tests verify that invalid regex patterns fed into the query language
|
||||
//! (via categorize, tag, or filter_keyvals_regex) are rejected with
|
||||
//! QueryError::RegexCompileError rather than panicking or returning a
|
||||
//! server-side 500-equivalent error.
|
||||
|
||||
use aw_datastore::Datastore;
|
||||
use aw_models::{Bucket, BucketMetadata, TimeInterval};
|
||||
use aw_query::QueryError;
|
||||
use proptest::prelude::*;
|
||||
use serde_json::json;
|
||||
|
||||
static TIME_INTERVAL: &str = "1980-01-01T00:00:00Z/2080-01-02T00:00:00Z";
|
||||
|
||||
fn setup_datastore_with_bucket() -> Datastore {
|
||||
let ds = Datastore::new_in_memory(false);
|
||||
let bucket = Bucket {
|
||||
bid: None,
|
||||
id: "testid".to_string(),
|
||||
_type: "testtype".to_string(),
|
||||
client: "testclient".to_string(),
|
||||
hostname: "testhost".to_string(),
|
||||
created: Some(chrono::Utc::now()),
|
||||
data: json!({}).as_object().unwrap().clone(),
|
||||
metadata: BucketMetadata::default(),
|
||||
events: None,
|
||||
last_updated: None,
|
||||
};
|
||||
ds.create_bucket(&bucket).unwrap();
|
||||
ds
|
||||
}
|
||||
|
||||
fn setup_interval() -> TimeInterval {
|
||||
TimeInterval::new_from_string(TIME_INTERVAL).unwrap()
|
||||
}
|
||||
|
||||
/// Strategy that generates regex strings known to be invalid for fancy_regex.
|
||||
fn invalid_regex_strategy() -> impl Strategy<Value = String> {
|
||||
prop_oneof![
|
||||
// Unclosed groups and character classes
|
||||
Just("(".to_string()),
|
||||
Just("[".to_string()),
|
||||
Just("(?P<name".to_string()),
|
||||
Just("(?i".to_string()),
|
||||
Just("(?<".to_string()),
|
||||
Just("(?P<".to_string()),
|
||||
Just("[abc".to_string()),
|
||||
// Lone or incomplete escape sequences
|
||||
Just("\\".to_string()),
|
||||
Just("\\k".to_string()),
|
||||
Just("\\x".to_string()),
|
||||
Just("\\u".to_string()),
|
||||
Just("\\Q".to_string()),
|
||||
// Quantifiers without targets (these are rejected at parse time)
|
||||
Just("*".to_string()),
|
||||
Just("?".to_string()),
|
||||
Just("+".to_string()),
|
||||
Just("***".to_string()),
|
||||
Just("???".to_string()),
|
||||
Just("+++".to_string()),
|
||||
Just("**?".to_string()),
|
||||
// Unclosed alternation / group combinations
|
||||
Just("(a|".to_string()),
|
||||
Just("[a-z".to_string()),
|
||||
Just("(?i)[".to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Strategy that generates arbitrary strings (including potentially valid regex).
|
||||
/// Used for the "must not panic" property.
|
||||
fn arbitrary_regex_strategy() -> impl Strategy<Value = String> {
|
||||
proptest::collection::vec(any::<char>(), 0..64).prop_map(|v| v.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Build a query that uses categorize() with the given regex pattern.
|
||||
///
|
||||
/// Only quotes are escaped; backslashes are passed through as-is because
|
||||
/// the aw-query lexer only handles `\"` escapes, not `\\`.
|
||||
fn categorize_query(pattern: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
events = query_bucket("testid");
|
||||
events = categorize(events, [[["TestCat"], {{ "type": "regex", "regex": "{}" }}]]);
|
||||
return events;
|
||||
"#,
|
||||
pattern.replace('"', "\\\"")
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a query that uses tag() with the given regex pattern.
|
||||
fn tag_query(pattern: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
events = query_bucket("testid");
|
||||
events = tag(events, [["testtag", {{ "type": "regex", "regex": "{}" }}]]);
|
||||
return events;
|
||||
"#,
|
||||
pattern.replace('"', "\\\"")
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a query that uses filter_keyvals_regex() with the given regex pattern.
|
||||
fn filter_keyvals_regex_query(pattern: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
events = query_bucket("testid");
|
||||
events = filter_keyvals_regex(events, "key", "{}");
|
||||
return events;
|
||||
"#,
|
||||
pattern.replace('"', "\\\"")
|
||||
)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
/// Any invalid regex pattern passed to categorize() must produce
|
||||
/// QueryError::RegexCompileError, never panic.
|
||||
#[test]
|
||||
fn categorize_rejects_invalid_regex(pattern in invalid_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = categorize_query(&pattern);
|
||||
|
||||
let result = aw_query::query(&code, &interval, &ds);
|
||||
prop_assert!(
|
||||
matches!(result, Err(QueryError::RegexCompileError(_))),
|
||||
"Expected RegexCompileError for pattern {:?}, got {:?}",
|
||||
pattern, result
|
||||
);
|
||||
}
|
||||
|
||||
/// Any invalid regex pattern passed to tag() must produce
|
||||
/// QueryError::RegexCompileError, never panic.
|
||||
#[test]
|
||||
fn tag_rejects_invalid_regex(pattern in invalid_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = tag_query(&pattern);
|
||||
|
||||
let result = aw_query::query(&code, &interval, &ds);
|
||||
prop_assert!(
|
||||
matches!(result, Err(QueryError::RegexCompileError(_))),
|
||||
"Expected RegexCompileError for pattern {:?}, got {:?}",
|
||||
pattern, result
|
||||
);
|
||||
}
|
||||
|
||||
/// Any invalid regex pattern passed to filter_keyvals_regex() must produce
|
||||
/// QueryError::RegexCompileError, never panic.
|
||||
#[test]
|
||||
fn filter_keyvals_regex_rejects_invalid_regex(pattern in invalid_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = filter_keyvals_regex_query(&pattern);
|
||||
|
||||
let result = aw_query::query(&code, &interval, &ds);
|
||||
prop_assert!(
|
||||
matches!(result, Err(QueryError::RegexCompileError(_))),
|
||||
"Expected RegexCompileError for pattern {:?}, got {:?}",
|
||||
pattern, result
|
||||
);
|
||||
}
|
||||
|
||||
/// Arbitrary strings passed to categorize() must never panic.
|
||||
/// The result may be Ok or Err, but it must not crash.
|
||||
#[test]
|
||||
fn categorize_never_panics(pattern in arbitrary_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = categorize_query(&pattern);
|
||||
|
||||
// We only assert that this does not panic; the result itself is allowed
|
||||
// to be either Ok or Err (some patterns are valid regex).
|
||||
let _result = aw_query::query(&code, &interval, &ds);
|
||||
}
|
||||
|
||||
/// Arbitrary strings passed to tag() must never panic.
|
||||
#[test]
|
||||
fn tag_never_panics(pattern in arbitrary_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = tag_query(&pattern);
|
||||
|
||||
let _result = aw_query::query(&code, &interval, &ds);
|
||||
}
|
||||
|
||||
/// Arbitrary strings passed to filter_keyvals_regex() must never panic.
|
||||
#[test]
|
||||
fn filter_keyvals_regex_never_panics(pattern in arbitrary_regex_strategy()) {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = filter_keyvals_regex_query(&pattern);
|
||||
|
||||
let _result = aw_query::query(&code, &interval, &ds);
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test: the specific pattern that caused ActivityWatch#1340
|
||||
/// (`++` in category pattern). In aw-server-rust this is a valid possessive
|
||||
/// quantifier, so it should NOT produce RegexCompileError. The original bug
|
||||
/// only affected the Python aw-server.
|
||||
#[test]
|
||||
fn regression_notepad_plus_plus_is_valid_in_rust() {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = categorize_query("Notepad++");
|
||||
|
||||
let result = aw_query::query(&code, &interval, &ds);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Expected 'Notepad++' to be accepted as valid possessive quantifier, got {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test: the literal-escaped form of the #1340 pattern must also work.
|
||||
#[test]
|
||||
fn regression_notepad_escaped_plus_is_valid() {
|
||||
let ds = setup_datastore_with_bucket();
|
||||
let interval = setup_interval();
|
||||
let code = categorize_query(r"Notepad\+\+");
|
||||
|
||||
let result = aw_query::query(&code, &interval, &ds);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Expected 'Notepad\\+\\+' to be accepted, got {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user