Skip to main content

uni_query_functions/
function_props.rs

1// M-CANONICAL-DOCS: This module defines property requirements for Cypher functions
2// M-CANONICAL-DOCS: to enable pushdown hydration optimization
3//
4// Pushdown hydration analyzes which properties a query needs and loads them during
5// the initial scan, transforming property loading from O(N*M) to O(N) complexity.
6
7use std::collections::HashMap;
8use std::sync::LazyLock;
9
10/// Specification of property requirements for a Cypher function.
11///
12/// This helps the query planner understand which properties need to be loaded
13/// for entity arguments to a function, enabling pushdown hydration.
14#[derive(Debug, Clone, Copy)]
15pub struct FunctionPropertySpec {
16    /// Argument positions containing entity references (0-indexed).
17    /// For example, in `validAt(entity, start, end, ts)`, position 0 is the entity.
18    pub entity_args: &'static [usize],
19
20    /// (arg_index, entity_arg_index) pairs for property name arguments.
21    /// For example, in `validAt(entity, 'start', 'end', ts)`:
22    /// - (1, 0) means argument 1 is a property name for entity at position 0
23    /// - (2, 0) means argument 2 is a property name for entity at position 0
24    pub property_name_args: &'static [(usize, usize)],
25
26    /// If true, requires all properties of entity (e.g., keys(), properties()).
27    pub needs_full_entity: bool,
28}
29
30/// Static registry of function property specifications.
31/// Function names are uppercase for case-insensitive lookup.
32static FUNCTION_SPECS: LazyLock<HashMap<&'static str, FunctionPropertySpec>> =
33    LazyLock::new(|| {
34        // Helper specs for common patterns
35        let full_entity = FunctionPropertySpec {
36            entity_args: &[0],
37            property_name_args: &[],
38            needs_full_entity: true,
39        };
40        let entity_arg_only = FunctionPropertySpec {
41            entity_args: &[0],
42            property_name_args: &[],
43            needs_full_entity: false,
44        };
45        let no_entity = FunctionPropertySpec {
46            entity_args: &[],
47            property_name_args: &[],
48            needs_full_entity: false,
49        };
50
51        HashMap::from([
52            // uni.temporal.validAt(entity, start_prop, end_prop, timestamp)
53            (
54                "UNI.TEMPORAL.VALIDAT",
55                FunctionPropertySpec {
56                    entity_args: &[0],
57                    property_name_args: &[(1, 0), (2, 0)],
58                    needs_full_entity: false,
59                },
60            ),
61            // Functions that need full entity materialization
62            ("KEYS", full_entity),
63            ("PROPERTIES", full_entity),
64            ("LABELS", full_entity),
65            ("NODES", full_entity),
66            ("RELATIONSHIPS", full_entity),
67            // Identity / structural functions: take an entity arg but only
68            // need its vid / type metadata, not its property bag. Without
69            // these entries the unknown-function fallback marks the entity
70            // as "*" (full materialization), which makes per-label `Scan`
71            // branches under a label-disjunction Union resolve to *different*
72            // property sets and crash `UnionExec::try_new`. See issue #62.
73            ("ID", entity_arg_only),
74            ("ELEMENTID", entity_arg_only),
75            ("TYPE", entity_arg_only),
76            // Functions that take entity arg but don't need full entity
77            ("COUNT", entity_arg_only),
78            // Functions where properties are extracted from PropertyAccess
79            ("COALESCE", no_entity),
80            ("SUM", no_entity),
81            ("AVG", no_entity),
82            ("MIN", no_entity),
83            ("MAX", no_entity),
84            ("COLLECT", no_entity),
85            ("PERCENTILEDISC", no_entity),
86            ("PERCENTILECONT", no_entity),
87        ])
88    });
89
90/// Look up the property specification for a function by name (case-insensitive).
91pub fn get_function_spec(name: &str) -> Option<&'static FunctionPropertySpec> {
92    let name_upper = name.to_uppercase();
93    FUNCTION_SPECS.get(name_upper.as_str())
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_validat_spec() {
102        let spec = get_function_spec("uni.temporal.validAt").unwrap();
103        assert_eq!(spec.entity_args, &[0]);
104        assert_eq!(spec.property_name_args, &[(1, 0), (2, 0)]);
105        assert!(!spec.needs_full_entity);
106    }
107
108    #[test]
109    fn test_keys_spec() {
110        let spec = get_function_spec("keys").unwrap();
111        assert_eq!(spec.entity_args, &[0]);
112        assert!(spec.needs_full_entity);
113    }
114
115    #[test]
116    fn test_properties_spec() {
117        let spec = get_function_spec("PROPERTIES").unwrap();
118        assert_eq!(spec.entity_args, &[0]);
119        assert!(spec.needs_full_entity);
120    }
121
122    #[test]
123    fn test_unknown_function_returns_none() {
124        assert!(get_function_spec("unknownFunction").is_none());
125    }
126
127    #[test]
128    fn test_count_spec_exists() {
129        let spec = get_function_spec("COUNT").unwrap();
130        assert!(!spec.needs_full_entity);
131        assert_eq!(spec.entity_args, &[0]);
132    }
133
134    #[test]
135    fn test_all_aggregates_registered() {
136        for func in ["COUNT", "SUM", "AVG", "MIN", "MAX", "COLLECT"] {
137            let spec = get_function_spec(func);
138            assert!(
139                spec.is_some(),
140                "Aggregate function {} should be registered",
141                func
142            );
143            assert!(
144                !spec.unwrap().needs_full_entity,
145                "Aggregate function {} should not need full entity",
146                func
147            );
148        }
149    }
150
151    #[test]
152    fn test_aggregate_case_insensitive() {
153        // Test that aggregate functions work with different case
154        assert!(get_function_spec("count").is_some());
155        assert!(get_function_spec("Count").is_some());
156        assert!(get_function_spec("COUNT").is_some());
157    }
158}