• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

flowlight0 / simpledb-rs / #113

20 Jun 2025 04:16PM UTC coverage: 78.834% (-1.0%) from 79.857%
#113

push

web-flow
Implement arithmetic operators in expressions (#118)

* Support arithmetic expressions

* Update README.md

* Minor visibility change

2 of 49 new or added lines in 1 file covered. (4.08%)

1 existing line in 1 file now uncovered.

2894 of 3671 relevant lines covered (78.83%)

1.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.93
/src/parser/predicate.rs
1
use crate::{
2
    errors::TransactionError,
3
    plan::{Plan, PlanControl},
4
    record::{field::Value, schema::Schema},
5
    scan::{Scan, ScanControl},
6
};
7

8
#[derive(Debug, Clone, PartialEq, Eq)]
9
pub enum Expression {
10
    NullConstant,
11
    I32Constant(i32),
12
    StringConstant(String),
13
    Field(String),
14
    Add(Box<Expression>, Box<Expression>),
15
    Sub(Box<Expression>, Box<Expression>),
16
    Mul(Box<Expression>, Box<Expression>),
17
    Div(Box<Expression>, Box<Expression>),
18
}
19

20
impl Expression {
21
    /// Evaluates the expression against the given [`Scan`].
22
    ///
23
    /// The resulting [`Value`] is returned in a `Result` since accessing a
24
    /// field may fail if the underlying scan encounters an error.
25
    pub fn evaluate(&self, scan: &mut Scan) -> Result<Value, TransactionError> {
3✔
26
        match self {
3✔
27
            Expression::NullConstant => Ok(Value::Null),
×
28
            Expression::I32Constant(value) => Ok(Value::I32(*value)),
1✔
29
            Expression::StringConstant(value) => Ok(Value::String(value.clone())),
2✔
30
            Expression::Field(field_name) => {
3✔
31
                if scan.has_field(field_name) {
3✔
32
                    scan.get_value(field_name)
3✔
33
                } else {
34
                    panic!("Field {} not found", field_name)
×
35
                }
36
            }
NEW
37
            Expression::Add(lhs, rhs) => {
×
NEW
38
                let l = lhs.evaluate(scan)?;
×
NEW
39
                let r = rhs.evaluate(scan)?;
×
NEW
40
                match (l, r) {
×
NEW
41
                    (Value::I32(a), Value::I32(b)) => Ok(Value::I32(a + b)),
×
NEW
42
                    (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
×
NEW
43
                    _ => panic!("Type mismatch in addition"),
×
44
                }
45
            }
NEW
46
            Expression::Sub(lhs, rhs) => {
×
NEW
47
                let l = lhs.evaluate(scan)?;
×
NEW
48
                let r = rhs.evaluate(scan)?;
×
NEW
49
                match (l, r) {
×
NEW
50
                    (Value::I32(a), Value::I32(b)) => Ok(Value::I32(a - b)),
×
NEW
51
                    (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
×
NEW
52
                    _ => panic!("Type mismatch in subtraction"),
×
53
                }
54
            }
NEW
55
            Expression::Mul(lhs, rhs) => {
×
NEW
56
                let l = lhs.evaluate(scan)?;
×
NEW
57
                let r = rhs.evaluate(scan)?;
×
NEW
58
                match (l, r) {
×
NEW
59
                    (Value::I32(a), Value::I32(b)) => Ok(Value::I32(a * b)),
×
NEW
60
                    (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
×
NEW
61
                    _ => panic!("Type mismatch in multiplication"),
×
62
                }
63
            }
NEW
64
            Expression::Div(lhs, rhs) => {
×
NEW
65
                let l = lhs.evaluate(scan)?;
×
NEW
66
                let r = rhs.evaluate(scan)?;
×
NEW
67
                match (l, r) {
×
NEW
68
                    (Value::I32(_), Value::I32(0)) => Ok(Value::Null),
×
NEW
69
                    (Value::I32(a), Value::I32(b)) => Ok(Value::I32(a / b)),
×
NEW
70
                    (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
×
NEW
71
                    _ => panic!("Type mismatch in division"),
×
72
                }
73
            }
74
        }
75
    }
76

77
    fn try_get_field(&self) -> Option<&str> {
1✔
78
        match self {
1✔
79
            Expression::Field(field_name) => Some(field_name),
1✔
80
            _ => None,
1✔
81
        }
82
    }
83

84
    fn try_get_constant(&self) -> Option<Value> {
1✔
85
        match self {
1✔
86
            Expression::NullConstant => Some(Value::Null),
×
87
            Expression::I32Constant(value) => Some(Value::I32(*value)),
1✔
88
            Expression::StringConstant(value) => Some(Value::String(value.clone())),
×
NEW
89
            Expression::Add(lhs, rhs) => match (lhs.try_get_constant(), rhs.try_get_constant()) {
×
NEW
90
                (Some(Value::I32(a)), Some(Value::I32(b))) => Some(Value::I32(a + b)),
×
NEW
91
                (Some(Value::Null), _) | (_, Some(Value::Null)) => Some(Value::Null),
×
NEW
92
                _ => None,
×
93
            },
NEW
94
            Expression::Sub(lhs, rhs) => match (lhs.try_get_constant(), rhs.try_get_constant()) {
×
NEW
95
                (Some(Value::I32(a)), Some(Value::I32(b))) => Some(Value::I32(a - b)),
×
NEW
96
                (Some(Value::Null), _) | (_, Some(Value::Null)) => Some(Value::Null),
×
NEW
97
                _ => None,
×
98
            },
NEW
99
            Expression::Mul(lhs, rhs) => match (lhs.try_get_constant(), rhs.try_get_constant()) {
×
NEW
100
                (Some(Value::I32(a)), Some(Value::I32(b))) => Some(Value::I32(a * b)),
×
NEW
101
                (Some(Value::Null), _) | (_, Some(Value::Null)) => Some(Value::Null),
×
NEW
102
                _ => None,
×
103
            },
NEW
104
            Expression::Div(lhs, rhs) => match (lhs.try_get_constant(), rhs.try_get_constant()) {
×
NEW
105
                (Some(Value::I32(_)), Some(Value::I32(0))) => Some(Value::Null),
×
NEW
106
                (Some(Value::I32(a)), Some(Value::I32(b))) => Some(Value::I32(a / b)),
×
NEW
107
                (Some(Value::Null), _) | (_, Some(Value::Null)) => Some(Value::Null),
×
NEW
108
                _ => None,
×
109
            },
110
            _ => None,
1✔
111
        }
112
    }
113

114
    /// Returns `true` if the expression can be applied to the given schema.
115
    ///
116
    /// Field references must exist in the schema while constant expressions are
117
    /// always applicable.
118
    fn is_applied_to(&self, schema: &Schema) -> bool {
1✔
119
        match self {
1✔
120
            Expression::Field(field_name) => schema.has_field(field_name),
1✔
NEW
121
            Expression::Add(lhs, rhs)
×
122
            | Expression::Sub(lhs, rhs)
123
            | Expression::Mul(lhs, rhs)
124
            | Expression::Div(lhs, rhs) => lhs.is_applied_to(schema) && rhs.is_applied_to(schema),
UNCOV
125
            _ => true,
×
126
        }
127
    }
128
}
129

130
#[derive(Debug, Clone, PartialEq, Eq)]
131
pub enum Term {
132
    Equality(Expression, Expression),
133
    IsNull(Expression),
134
}
135

136
impl Term {
137
    /// Evaluates the term for the current row of the given [`Scan`].
138
    ///
139
    /// Returns `Ok(true)` if the term is satisfied, `Ok(false)` otherwise. Any
140
    /// I/O errors produced while reading values from the scan are forwarded.
141
    pub fn is_satisfied(&self, scan: &mut Scan) -> Result<bool, TransactionError> {
2✔
142
        match self {
3✔
143
            Term::Equality(lhs, rhs) => {
2✔
144
                let lhs = lhs.evaluate(scan)?;
3✔
145
                let rhs = rhs.evaluate(scan)?;
4✔
146
                Ok(lhs == rhs)
4✔
147
            }
148
            Term::IsNull(expr) => {
1✔
149
                let value = expr.evaluate(scan)?;
1✔
150
                Ok(value == Value::Null)
2✔
151
            }
152
        }
153
    }
154

155
    /// Estimates the reduction factor of the term for query planning.
156
    ///
157
    /// The reduction factor is based on the distinct value counts provided by
158
    /// the [`Plan`] and follows the same heuristics as the Java version from
159
    /// the "SimpleDB" textbook.
160
    pub fn get_reduction_factor(&self, plan: &Plan) -> usize {
1✔
161
        match self {
1✔
162
            Term::Equality(lhs, rhs) => {
1✔
163
                let lhs_field = lhs.try_get_field();
1✔
164
                let rhs_field = rhs.try_get_field();
1✔
165
                match (lhs_field, rhs_field) {
1✔
166
                    (Some(lhs_field), Some(rhs_field)) => {
×
167
                        let l_distinct = plan.num_distinct_values(lhs_field);
×
168
                        let r_distinct = plan.num_distinct_values(rhs_field);
×
169
                        return std::cmp::max(l_distinct, r_distinct);
×
170
                    }
171
                    (Some(lhs_field), None) => {
1✔
172
                        return plan.num_distinct_values(lhs_field);
1✔
173
                    }
174
                    (None, Some(rhs_field)) => {
×
175
                        return plan.num_distinct_values(rhs_field);
×
176
                    }
177
                    _ => {}
178
                }
179
                if lhs == rhs {
×
180
                    return 1;
×
181
                } else {
182
                    // Just return some relatively large number.
183
                    // Integer.MAX_VALUE was used in the book, but I don't use it to avoid overflow.
184
                    return 100;
×
185
                }
186
            }
187
            Term::IsNull(expr) => {
×
188
                if let Some(field) = expr.try_get_field() {
×
189
                    return plan.num_distinct_values(field);
×
190
                }
191
                if let Some(value) = expr.try_get_constant() {
×
192
                    return if value == Value::Null { 1 } else { 100 };
×
193
                }
194
                100
×
195
            }
196
        }
197
    }
198

199
    /// If this term equates the given field with a constant, return that constant.
200
    fn equates_with_constant(&self, field_name: &str) -> Option<Value> {
1✔
201
        match self {
1✔
202
            Term::Equality(lhs, rhs) => {
1✔
203
                if let Some(lhs_field) = lhs.try_get_field() {
1✔
204
                    if lhs_field == field_name {
1✔
205
                        if let Some(rhs_constant) = rhs.try_get_constant() {
1✔
206
                            return Some(rhs_constant);
1✔
207
                        }
208
                    }
209
                }
210
                if let Some(rhs_field) = rhs.try_get_field() {
2✔
211
                    if rhs_field == field_name {
1✔
212
                        if let Some(lhs_constant) = lhs.try_get_constant() {
1✔
213
                            return Some(lhs_constant);
×
214
                        }
215
                    }
216
                }
217
            }
218
            Term::IsNull(expr) => {
×
219
                if let Some(field) = expr.try_get_field() {
×
220
                    if field == field_name {
×
221
                        return Some(Value::Null);
×
222
                    }
223
                }
224
            }
225
        }
226
        return None;
1✔
227
    }
228

229
    /// If this term equates the given field with another field, return the other
230
    /// field name.
231
    fn equates_with_field(&self, field_name: &str) -> Option<String> {
1✔
232
        match self {
1✔
233
            Term::Equality(lhs, rhs) => {
1✔
234
                if let Some(lhs_field) = lhs.try_get_field() {
1✔
235
                    if lhs_field == field_name {
1✔
236
                        if let Some(rhs_field) = rhs.try_get_field() {
×
237
                            return Some(rhs_field.to_string());
×
238
                        }
239
                    }
240
                }
241
                if let Some(rhs_field) = rhs.try_get_field() {
2✔
242
                    if rhs_field == field_name {
1✔
243
                        if let Some(lhs_field) = lhs.try_get_field() {
1✔
244
                            return Some(lhs_field.to_string());
1✔
245
                        }
246
                    }
247
                }
248
            }
249
            Term::IsNull(_) => {}
250
        }
251
        return None;
×
252
    }
253

254
    /// Returns `true` if all fields referenced by the term are present in the
255
    /// provided schema.
256
    fn is_applied_to(&self, schema: &Schema) -> bool {
1✔
257
        match self {
1✔
258
            Term::Equality(lhs, rhs) => lhs.is_applied_to(schema) && rhs.is_applied_to(schema),
1✔
259
            Term::IsNull(expr) => expr.is_applied_to(schema),
×
260
        }
261
    }
262
}
263

264
#[derive(Debug, Clone, PartialEq, Eq)]
265
pub struct Predicate {
266
    terms: Vec<Term>,
267
}
268

269
impl Predicate {
270
    /// Creates a new predicate composed of the given terms.
271
    pub fn new(terms: Vec<Term>) -> Self {
2✔
272
        Predicate { terms }
273
    }
274

275
    /// Checks whether all terms are satisfied for the current record of the scan.
276
    pub fn is_satisfied(&self, scan: &mut Scan) -> Result<bool, TransactionError> {
2✔
277
        for term in &self.terms {
4✔
278
            if !term.is_satisfied(scan)? {
2✔
279
                return Ok(false);
2✔
280
            }
281
        }
282
        Ok(true)
2✔
283
    }
284

285
    /// Computes the combined reduction factor of all terms.
286
    ///
287
    /// This is the product of the individual reduction factors and is used by
288
    /// the planner to estimate result sizes.
289
    pub fn get_reduction_factor(&self, plan: &Plan) -> usize {
1✔
290
        let mut factor = 1;
1✔
291
        for term in &self.terms {
2✔
292
            factor *= term.get_reduction_factor(plan);
2✔
293
        }
294
        factor
1✔
295
    }
296

297
    /// Returns a predicate containing only the terms that can be applied to the
298
    /// provided schema.
299
    ///
300
    /// If no such terms exist, `None` is returned.
301
    pub(crate) fn select_sub_predicates(&self, schema: &Schema) -> Option<Predicate> {
1✔
302
        let mut terms = Vec::new();
1✔
303
        for term in &self.terms {
2✔
304
            if term.is_applied_to(schema) {
2✔
305
                terms.push(term.clone());
1✔
306
            }
307
        }
308

309
        if terms.is_empty() {
1✔
310
            return None;
1✔
311
        } else {
312
            Some(Predicate::new(terms))
2✔
313
        }
314
    }
315

316
    /// Extracts the join predicate that references fields from both schemas.
317
    ///
318
    /// Terms that reference only one of the schemas are ignored. If none remain
319
    /// after filtering, `None` is returned.
320
    pub(crate) fn join_sub_predicates(
1✔
321
        &self,
322
        schema1: &Schema,
323
        schema2: &Schema,
324
    ) -> Option<Predicate> {
325
        let mut join_schema = Schema::new();
1✔
326
        join_schema.add_all(schema1);
1✔
327
        join_schema.add_all(schema2);
1✔
328

329
        let mut terms = vec![];
1✔
330
        for term in &self.terms {
2✔
331
            if term.is_applied_to(schema1) || term.is_applied_to(schema2) {
2✔
332
                continue;
333
            }
334
            if term.is_applied_to(&join_schema) {
1✔
335
                terms.push(term.clone());
1✔
336
            }
337
        }
338
        if terms.is_empty() {
1✔
339
            return None;
×
340
        } else {
341
            Some(Predicate::new(terms))
2✔
342
        }
343
    }
344

345
    /// Searches for a term equating `field_name` with a constant value.
346
    pub(crate) fn equates_with_constant(&self, field_name: &str) -> Option<Value> {
1✔
347
        for term in &self.terms {
2✔
348
            let t = term.equates_with_constant(field_name);
1✔
349
            if t.is_some() {
2✔
350
                return t;
1✔
351
            }
352
        }
353
        None
1✔
354
    }
355

356
    /// Searches for a term equating `field_name` with another field.
357
    pub(crate) fn equates_with_field(&self, field_name: &str) -> Option<String> {
1✔
358
        for term in &self.terms {
2✔
359
            let t = term.equates_with_field(field_name);
1✔
360
            if t.is_some() {
2✔
361
                return t;
1✔
362
            }
363
        }
364
        None
×
365
    }
366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc