Skip to main content

hawdb_expression/
lib.rs

1pub mod sql;
2
3use hawdb_core::{RelationshipDirection, ValidatedRegex, Value};
4
5mod null_rejection;
6
7pub use null_rejection::{
8    prove_null_rejecting, BindingId, BindingSet, BoundPredicate, BoundScalarExpression,
9    NullRejectionProof, ScalarNullability, TruthSet, TruthValue,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Predicate {
14    And(Vec<Predicate>),
15    Or(Vec<Predicate>),
16    Not(Box<Predicate>),
17    ConstantBool(bool),
18    RelationshipExists {
19        variable: String,
20        rel_type: String,
21        direction: RelationshipDirection,
22        target_label: String,
23    },
24    BoundRelationshipExists {
25        source_variable: String,
26        rel_type: String,
27        direction: RelationshipDirection,
28        target_variable: String,
29    },
30    IdEq {
31        variable: String,
32        value: Value,
33    },
34    IdNotEq {
35        variable: String,
36        value: Value,
37    },
38    IdCompare {
39        variable: String,
40        op: ComparisonOp,
41        value: Value,
42    },
43    IdIn {
44        variable: String,
45        values: Vec<Value>,
46    },
47    PropertyEq {
48        variable: String,
49        property: String,
50        value: Value,
51    },
52    PropertyNotEq {
53        variable: String,
54        property: String,
55        value: Value,
56    },
57    PropertyCompare {
58        variable: String,
59        property: String,
60        op: ComparisonOp,
61        value: Value,
62    },
63    ExpressionEq {
64        expression: ProjectionExpression,
65        value: ProjectionExpression,
66    },
67    ExpressionNotEq {
68        expression: ProjectionExpression,
69        value: ProjectionExpression,
70    },
71    ExpressionCompare {
72        expression: ProjectionExpression,
73        op: ComparisonOp,
74        value: ProjectionExpression,
75    },
76    ExpressionContains {
77        expression: ProjectionExpression,
78        value: ProjectionExpression,
79    },
80    ExpressionIn {
81        expression: ProjectionExpression,
82        values: Vec<Value>,
83    },
84    PropertyListContains {
85        variable: String,
86        property: String,
87        value: Value,
88    },
89    PropertyListContainsLower {
90        variable: String,
91        property: String,
92        value: String,
93    },
94    PropertyContains {
95        variable: String,
96        property: String,
97        value: String,
98    },
99    PropertyStartsWith {
100        variable: String,
101        property: String,
102        value: String,
103    },
104    PropertyEndsWith {
105        variable: String,
106        property: String,
107        value: String,
108    },
109    PropertyRegexMatch {
110        variable: String,
111        property: String,
112        pattern: ValidatedRegex,
113    },
114    PropertyIsNull {
115        variable: String,
116        property: String,
117    },
118    PropertyIsNotNull {
119        variable: String,
120        property: String,
121    },
122    PropertyIn {
123        variable: String,
124        property: String,
125        values: Vec<Value>,
126    },
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ComparisonOp {
131    Lt,
132    Lte,
133    Gt,
134    Gte,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum DatePart {
139    Year,
140    Month,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum ProjectionExpression {
145    Variable {
146        variable: String,
147    },
148    Property {
149        variable: String,
150        property: String,
151    },
152    Id {
153        variable: String,
154    },
155    RelationshipType {
156        variable: String,
157    },
158    Literal(Value),
159    Coalesce(Vec<ProjectionExpression>),
160    VectorDistance {
161        metric: hawdb_core::VectorDistance,
162        left: Box<ProjectionExpression>,
163        right: Box<ProjectionExpression>,
164    },
165    JsonValue {
166        expression: Box<ProjectionExpression>,
167        path: String,
168    },
169    Left {
170        expression: Box<ProjectionExpression>,
171        length: usize,
172    },
173    Lower(Box<ProjectionExpression>),
174    DatePart {
175        part: DatePart,
176        variable: String,
177        property: String,
178    },
179    DefaultIfNullOrEq {
180        variable: String,
181        property: String,
182        empty: Value,
183        default: Value,
184    },
185    DefaultIfNull {
186        variable: String,
187        property: String,
188        default: Value,
189    },
190    CasePropertyNotNullOrEq {
191        variable: String,
192        property: String,
193        empty: Value,
194        non_empty: Value,
195        null_or_empty: Value,
196    },
197    CasePropertyEqualsRank {
198        variable: String,
199        property: String,
200        branches: Vec<(Value, Value)>,
201        default: Value,
202    },
203    CaseLowerPropertyDefault {
204        variable: String,
205        property: String,
206        default: Value,
207    },
208    CaseCoalesceDifferenceFloorZero {
209        variable: String,
210        terms: Vec<CoalesceDifferenceProjectionTerm>,
211    },
212    Case {
213        operand: Option<Box<ProjectionExpression>>,
214        branches: Vec<(ProjectionExpression, ProjectionExpression)>,
215        otherwise: Option<Box<ProjectionExpression>>,
216    },
217    Binary {
218        left: Box<ProjectionExpression>,
219        op: ScalarBinaryOp,
220        right: Box<ProjectionExpression>,
221    },
222    Not(Box<ProjectionExpression>),
223    IsNull {
224        expression: Box<ProjectionExpression>,
225        negated: bool,
226    },
227    CaseEntitySearchRank(Box<CaseEntitySearchRankProjection>),
228    CaseColumnSearchRank(Box<CaseColumnSearchRankProjection>),
229    ColumnDefaultIfNullOrEq {
230        column: String,
231        property: String,
232        empty: Value,
233        default: Value,
234    },
235    ColumnValueDefaultIfNull {
236        column: String,
237        default: Value,
238    },
239    ColumnValueCasePropertyNotNullOrEq {
240        column: String,
241        empty: Value,
242        non_empty: Value,
243        null_or_empty: Value,
244    },
245    Column(String),
246    ColumnProperty {
247        column: String,
248        property: String,
249    },
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum ScalarBinaryOp {
254    Add,
255    Subtract,
256    Multiply,
257    Divide,
258    Remainder,
259    Eq,
260    NotEq,
261    Lt,
262    Lte,
263    Gt,
264    Gte,
265    Contains,
266    ListContains,
267    And,
268    Or,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct CaseEntitySearchRankProjection {
273    pub variable: String,
274    pub name_property: String,
275    pub aliases_property: String,
276    pub raw_query: Value,
277    pub normalized_query: Value,
278    pub raw_input: Value,
279    pub exact_rank: Value,
280    pub alias_rank: Value,
281    pub fallback_rank: Value,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct CaseColumnSearchRankProjection {
286    pub column: String,
287    pub raw_query: Value,
288    pub normalized_query: Value,
289    pub exact_rank: Value,
290    pub contains_rank: Value,
291    pub fallback_rank: Value,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct CoalesceDifferenceProjectionTerm {
296    pub property: String,
297    pub default: Value,
298}
299
300impl ProjectionExpression {
301    /// Visits immediate scalar children in evaluation order, stopping on false.
302    pub fn all_children(&self, mut visit: impl FnMut(&ProjectionExpression) -> bool) -> bool {
303        match self {
304            Self::Case {
305                operand,
306                branches,
307                otherwise,
308            } => operand
309                .iter()
310                .map(Box::as_ref)
311                .chain(
312                    branches
313                        .iter()
314                        .flat_map(|(condition, result)| [condition, result]),
315                )
316                .chain(otherwise.iter().map(Box::as_ref))
317                .all(visit),
318            Self::Binary { left, right, .. } | Self::VectorDistance { left, right, .. } => {
319                visit(left) && visit(right)
320            }
321            Self::Not(expression)
322            | Self::IsNull { expression, .. }
323            | Self::Lower(expression)
324            | Self::Left { expression, .. }
325            | Self::JsonValue { expression, .. } => visit(expression),
326            Self::Coalesce(expressions) => expressions.iter().all(visit),
327            _ => true,
328        }
329    }
330}
331
332impl ProjectionExpression {
333    /// Mutates immediate scalar children in evaluation order; errors are not rolled back.
334    pub fn try_for_each_child_mut<E>(
335        &mut self,
336        mut visit: impl FnMut(&mut Self) -> Result<(), E>,
337    ) -> Result<(), E> {
338        match self {
339            Self::Case {
340                operand,
341                branches,
342                otherwise,
343            } => {
344                for expression in operand
345                    .iter_mut()
346                    .map(Box::as_mut)
347                    .chain(
348                        branches
349                            .iter_mut()
350                            .flat_map(|(condition, result)| [condition, result]),
351                    )
352                    .chain(otherwise.iter_mut().map(Box::as_mut))
353                {
354                    visit(expression)?;
355                }
356            }
357            Self::Binary { left, right, .. } | Self::VectorDistance { left, right, .. } => {
358                visit(left)?;
359                visit(right)?;
360            }
361            Self::Not(expression)
362            | Self::IsNull { expression, .. }
363            | Self::Lower(expression)
364            | Self::Left { expression, .. }
365            | Self::JsonValue { expression, .. } => visit(expression)?,
366            Self::Coalesce(expressions) => {
367                for expression in expressions {
368                    visit(expression)?;
369                }
370            }
371            _ => {}
372        }
373        Ok(())
374    }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct SortItem {
379    pub key: SortKey,
380    pub direction: SortDirection,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum SortKey {
385    Property { variable: String, property: String },
386    Id { variable: String },
387    Expression(ProjectionExpression),
388    Column(String),
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub enum SortDirection {
393    Asc,
394    Desc,
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn expression_ir_composes_boolean_and_scalar_nodes() {
403        let predicate = Predicate::ExpressionCompare {
404            expression: ProjectionExpression::Property {
405                variable: "memory".to_string(),
406                property: "importance".to_string(),
407            },
408            op: ComparisonOp::Gte,
409            value: ProjectionExpression::Literal(Value::Float(0.75)),
410        };
411
412        assert!(matches!(
413            predicate,
414            Predicate::ExpressionCompare {
415                op: ComparisonOp::Gte,
416                ..
417            }
418        ));
419    }
420}