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

This commit is contained in:
upstream-mirror
2026-09-16 00:19:17 +05:00
commit da934b693c
120 changed files with 26659 additions and 0 deletions

26
aw-query/Cargo.toml Normal file
View 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

View 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
View 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
View 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
View 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(&regex_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, &regex);
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
View File

403
aw-query/src/interpret.rs Normal file
View 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
View 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
View 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
View 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
View 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:?}"),
};
}
}

View 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
);
}