Skip to content

Commit 01a84bb

Browse files
authored
feat: support CONTAINS operator (#5)
1 parent 8e408c6 commit 01a84bb

13 files changed

Lines changed: 308 additions & 26 deletions

src/analysis.rs

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ impl<'a> Analysis<'a> {
542542
&mut self,
543543
attrs: &Attrs,
544544
value: &Value,
545-
expect: Type,
545+
mut expect: Type,
546546
) -> AnalysisResult<Type> {
547547
match value {
548548
Value::Number(_) => expect.check(attrs, Type::Number),
@@ -566,19 +566,22 @@ impl<'a> Analysis<'a> {
566566
}
567567
}
568568

569-
this @ Value::Array(exprs) => {
569+
Value::Array(exprs) => {
570570
if matches!(expect, Type::Unspecified) {
571-
return Ok(self.project_type(this));
571+
for expr in exprs {
572+
expect = self.analyze_expr(expr, expect)?;
573+
}
574+
575+
return Ok(Type::Array(Box::new(expect)));
572576
}
573577

574578
match expect {
575-
Type::Array(mut types) if exprs.len() == types.len() => {
576-
for (expr, expect) in exprs.iter().zip(types.iter_mut()) {
577-
let tmp = mem::take(expect);
578-
*expect = self.analyze_expr(expr, tmp)?;
579+
Type::Array(mut expect) => {
580+
for expr in exprs {
581+
*expect = self.analyze_expr(expr, expect.as_ref().clone())?;
579582
}
580583

581-
Ok(Type::Array(types))
584+
Ok(Type::Array(expect))
582585
}
583586

584587
expect => Err(AnalysisError::TypeMismatch(
@@ -590,9 +593,22 @@ impl<'a> Analysis<'a> {
590593
}
591594
}
592595

593-
this @ Value::Record(fields) => {
596+
Value::Record(fields) => {
594597
if matches!(expect, Type::Unspecified) {
595-
return Ok(self.project_type(this));
598+
let mut record = BTreeMap::new();
599+
600+
for field in fields {
601+
record.insert(
602+
field.name.clone(),
603+
self.analyze_value(
604+
&field.value.attrs,
605+
&field.value.value,
606+
Type::Unspecified,
607+
)?,
608+
);
609+
}
610+
611+
return Ok(Type::Record(record));
596612
}
597613

598614
match expect {
@@ -692,6 +708,34 @@ impl<'a> Analysis<'a> {
692708
expect.check(attrs, Type::Bool)
693709
}
694710

711+
Operator::Contains => {
712+
let lhs_expect =
713+
self.analyze_expr(&binary.lhs, Type::Array(Box::new(Type::Unspecified)))?;
714+
715+
let lhs_assumption = match lhs_expect {
716+
Type::Array(inner) => *inner,
717+
other => {
718+
return Err(AnalysisError::ExpectArray(
719+
attrs.pos.line,
720+
attrs.pos.col,
721+
other,
722+
));
723+
}
724+
};
725+
726+
let rhs_expect = self.analyze_expr(&binary.rhs, lhs_assumption.clone())?;
727+
728+
// If the left side didn't have enough type information while the other did,
729+
// we replay another typecheck pass on the left side if the right side was conclusive
730+
if matches!(lhs_assumption, Type::Unspecified)
731+
&& !matches!(rhs_expect, Type::Unspecified)
732+
{
733+
self.analyze_expr(&binary.lhs, Type::Array(Box::new(rhs_expect)))?;
734+
}
735+
736+
expect.check(attrs, Type::Bool)
737+
}
738+
695739
Operator::And | Operator::Or | Operator::Xor => {
696740
self.analyze_expr(&binary.lhs, Type::Bool)?;
697741
self.analyze_expr(&binary.rhs, Type::Bool)?;
@@ -914,7 +958,18 @@ impl<'a> Analysis<'a> {
914958
}
915959
}
916960
Value::Array(exprs) => {
917-
Type::Array(exprs.iter().map(|v| self.project_type(&v.value)).collect())
961+
let mut project = Type::Unspecified;
962+
963+
for expr in exprs {
964+
let tmp = self.project_type(&expr.value);
965+
966+
if !matches!(tmp, Type::Unspecified) {
967+
project = tmp;
968+
break;
969+
}
970+
}
971+
972+
Type::Array(Box::new(project))
918973
}
919974
Value::Record(fields) => Type::Record(
920975
fields
@@ -951,7 +1006,8 @@ impl<'a> Analysis<'a> {
9511006
| Operator::And
9521007
| Operator::Or
9531008
| Operator::Xor
954-
| Operator::Not => Type::Bool,
1009+
| Operator::Not
1010+
| Operator::Contains => Type::Bool,
9551011
},
9561012
Value::Unary(unary) => match unary.operator {
9571013
Operator::Add | Operator::Sub => Type::Number,
@@ -966,7 +1022,8 @@ impl<'a> Analysis<'a> {
9661022
| Operator::And
9671023
| Operator::Or
9681024
| Operator::Xor
969-
| Operator::Not => unreachable!(),
1025+
| Operator::Not
1026+
| Operator::Contains => unreachable!(),
9701027
},
9711028
Value::Group(expr) => self.project_type(&expr.value),
9721029
}

src/ast.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ impl From<Token<'_>> for Pos {
5454
/// Type information for expressions.
5555
///
5656
/// This enum represents the type of an expression in the E
57-
5857
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize)]
5958
pub enum Type {
6059
/// Type has not been determined yet
@@ -67,7 +66,7 @@ pub enum Type {
6766
/// Boolean type
6867
Bool,
6968
/// Array type
70-
Array(Vec<Type>),
69+
Array(Box<Type>),
7170
/// Record (object) type
7271
Record(BTreeMap<String, Type>),
7372
/// Subject pattern type
@@ -104,17 +103,8 @@ impl Type {
104103
(Self::Number, Self::Number) => Ok(Self::Number),
105104
(Self::String, Self::String) => Ok(Self::String),
106105
(Self::Bool, Self::Bool) => Ok(Self::Bool),
107-
108-
(Self::Array(mut a), Self::Array(b)) if a.len() == b.len() => {
109-
if a.is_empty() {
110-
return Ok(Self::Array(a));
111-
}
112-
113-
for (a, b) in a.iter_mut().zip(b.into_iter()) {
114-
let tmp = mem::take(a);
115-
*a = tmp.check(attrs, b)?;
116-
}
117-
106+
(Self::Array(mut a), Self::Array(b)) => {
107+
*a = a.as_ref().clone().check(attrs, *b)?;
118108
Ok(Self::Array(a))
119109
}
120110

src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ pub enum AnalysisError {
153153
#[error("{0}:{1}: expected record but got {2:?}")]
154154
ExpectRecord(u32, u32, Type),
155155

156+
/// Expected an array type but found a different type.
157+
///
158+
/// Fields: `(line, column, actual_type)`
159+
///
160+
/// This occurs when an array type is required but a different type was found.
161+
#[error("{0}:{1}: expected an array but got {2:?}")]
162+
ExpectArray(u32, u32, Type),
163+
156164
/// Expected a field literal but found a different expression.
157165
///
158166
/// Fields: `(line, column)`

src/lexer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ fn ident(input: Text) -> IResult<Text, Token> {
145145
Sym::Operator(Operator::Xor)
146146
} else if value.fragment().eq_ignore_ascii_case("not") {
147147
Sym::Operator(Operator::Not)
148+
} else if value.fragment().eq_ignore_ascii_case("contains") {
149+
Sym::Operator(Operator::Contains)
148150
} else {
149151
Sym::Id(value.fragment())
150152
};

src/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ fn binding_pow(op: Operator) -> (u64, u64) {
476476
match op {
477477
Operator::Add | Operator::Sub => (20, 21),
478478
Operator::Mul | Operator::Div => (30, 31),
479+
Operator::Contains => (40, 39),
479480
Operator::Eq
480481
| Operator::Neq
481482
| Operator::Gt

src/tests/analysis.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,15 @@ fn test_rename_subquery() {
3232
let query = parse_query(include_str!("./resources/rename_subquery.eql")).unwrap();
3333
insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default()));
3434
}
35+
36+
#[test]
37+
fn test_analyze_valid_contains() {
38+
let query = parse_query(include_str!("./resources/valid_contains.eql")).unwrap();
39+
insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default()));
40+
}
41+
42+
#[test]
43+
fn test_analyze_invalid_type_contains() {
44+
let query = parse_query(include_str!("./resources/invalid_type_contains.eql")).unwrap();
45+
insta::assert_yaml_snapshot!(query.run_static_analysis(&Default::default()));
46+
}

src/tests/parser.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,9 @@ fn test_parser_from_events_with_distinct() {
6666
let tokens = tokenize(include_str!("./resources/from_events_with_distinct.eql")).unwrap();
6767
insta::assert_yaml_snapshot!(parse(tokens.as_slice()).unwrap());
6868
}
69+
70+
#[test]
71+
fn test_parser_valid_contains() {
72+
let tokens = tokenize(include_str!("./resources/valid_contains.eql")).unwrap();
73+
insta::assert_yaml_snapshot!(parse(tokens.as_slice()).unwrap());
74+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM e in events
2+
WHERE [1,2,3] CONTAINS e.source
3+
PROJECT INTO e
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM e in events
2+
WHERE [1,2,3] CONTAINS e.data.price
3+
PROJECT INTO e
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: src/tests/analysis.rs
3+
expression: "query.run_static_analysis(&Default::default())"
4+
---
5+
Err:
6+
Analysis:
7+
TypeMismatch:
8+
- 2
9+
- 24
10+
- String
11+
- Number

0 commit comments

Comments
 (0)