Skip to main content

rustledger_query/executor/
types.rs

1//! Types used by the BQL query executor.
2
3use std::collections::BTreeMap;
4use std::hash::{Hash, Hasher};
5
6use rust_decimal::Decimal;
7use rustledger_core::{Amount, Inventory, Metadata, NaiveDate, Position, Transaction};
8
9/// Source location information for a directive.
10#[derive(Debug, Clone)]
11pub struct SourceLocation {
12    /// File path.
13    pub filename: String,
14    /// Line number (1-based).
15    pub lineno: usize,
16}
17
18/// An interval unit for date arithmetic.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum IntervalUnit {
21    /// Days.
22    Day,
23    /// Weeks.
24    Week,
25    /// Months.
26    Month,
27    /// Quarters.
28    Quarter,
29    /// Years.
30    Year,
31}
32
33impl IntervalUnit {
34    /// Parse an interval unit from a string.
35    pub fn parse_unit(s: &str) -> Option<Self> {
36        match s.to_uppercase().as_str() {
37            "DAY" | "DAYS" | "D" => Some(Self::Day),
38            "WEEK" | "WEEKS" | "W" => Some(Self::Week),
39            "MONTH" | "MONTHS" | "M" => Some(Self::Month),
40            "QUARTER" | "QUARTERS" | "Q" => Some(Self::Quarter),
41            "YEAR" | "YEARS" | "Y" => Some(Self::Year),
42            _ => None,
43        }
44    }
45}
46
47/// An interval value for date arithmetic.
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct Interval {
50    /// The count (can be negative).
51    pub count: i64,
52    /// The unit.
53    pub unit: IntervalUnit,
54}
55
56impl Interval {
57    /// Create a new interval.
58    pub const fn new(count: i64, unit: IntervalUnit) -> Self {
59        Self { count, unit }
60    }
61
62    /// Convert interval to an approximate number of days for comparison.
63    /// Uses: Day=1, Week=7, Month=30, Quarter=91, Year=365.
64    pub(crate) const fn to_approx_days(&self) -> i64 {
65        let days_per_unit = match self.unit {
66            IntervalUnit::Day => 1,
67            IntervalUnit::Week => 7,
68            IntervalUnit::Month => 30,
69            IntervalUnit::Quarter => 91,
70            IntervalUnit::Year => 365,
71        };
72        self.count.saturating_mul(days_per_unit)
73    }
74
75    /// Add this interval to a date.
76    pub fn add_to_date(&self, date: NaiveDate) -> Option<NaiveDate> {
77        use jiff::ToSpan;
78
79        let span = match self.unit {
80            IntervalUnit::Day => self.count.days(),
81            IntervalUnit::Week => self.count.weeks(),
82            IntervalUnit::Month => self.count.months(),
83            IntervalUnit::Quarter => (self.count * 3).months(),
84            IntervalUnit::Year => self.count.years(),
85        };
86        date.checked_add(span).ok()
87    }
88}
89
90/// A value that can result from evaluating a BQL expression.
91///
92/// Heavy variants (Inventory, Position, Metadata, Object) are boxed to reduce
93/// the size of the enum from 120 bytes to 32 bytes, improving cache efficiency
94/// when processing large result sets.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum Value {
97    /// String value.
98    String(String),
99    /// Numeric value.
100    Number(Decimal),
101    /// Integer value.
102    Integer(i64),
103    /// Date value.
104    Date(NaiveDate),
105    /// Boolean value.
106    Boolean(bool),
107    /// Amount (number + currency).
108    Amount(Amount),
109    /// Position (amount + optional cost). Boxed to reduce enum size.
110    Position(Box<Position>),
111    /// Inventory (aggregated positions). Boxed to reduce enum size.
112    Inventory(Box<Inventory>),
113    /// Set of strings (tags, links).
114    StringSet(Vec<String>),
115    /// Generic set of values for IN operator (supports mixed types).
116    Set(Vec<Self>),
117    /// Metadata dictionary. Boxed to reduce enum size.
118    Metadata(Box<Metadata>),
119    /// Interval for date arithmetic.
120    Interval(Interval),
121    /// Structured object (for entry, meta columns). Boxed to reduce enum size.
122    Object(Box<BTreeMap<String, Self>>),
123    /// NULL value.
124    Null,
125}
126
127impl Value {
128    /// Compute a hash for this value.
129    ///
130    /// Note: This is not the standard Hash trait because some contained types
131    /// (Decimal, Inventory) don't implement Hash. We use byte representations
132    /// for those types.
133    pub(crate) fn hash_value<H: Hasher>(&self, state: &mut H) {
134        std::mem::discriminant(self).hash(state);
135        match self {
136            Self::String(s) => s.hash(state),
137            Self::Number(d) => d.serialize().hash(state),
138            Self::Integer(i) => i.hash(state),
139            Self::Date(d) => {
140                d.year().hash(state);
141                d.month().hash(state);
142                d.day().hash(state);
143            }
144            Self::Boolean(b) => b.hash(state),
145            Self::Amount(a) => {
146                a.number.serialize().hash(state);
147                a.currency.as_str().hash(state);
148            }
149            Self::Position(p) => {
150                // Dereference boxed position
151                p.units.number.serialize().hash(state);
152                p.units.currency.as_str().hash(state);
153                if let Some(cost) = &p.cost {
154                    cost.number.serialize().hash(state);
155                    cost.currency.as_str().hash(state);
156                }
157            }
158            Self::Inventory(inv) => {
159                // Dereference boxed inventory
160                for pos in inv.positions() {
161                    pos.units.number.serialize().hash(state);
162                    pos.units.currency.as_str().hash(state);
163                    if let Some(cost) = &pos.cost {
164                        cost.number.serialize().hash(state);
165                        cost.currency.as_str().hash(state);
166                    }
167                }
168            }
169            Self::StringSet(ss) => {
170                // Hash StringSet in a canonical, order-independent way by sorting first.
171                let mut sorted = ss.clone();
172                sorted.sort();
173                for s in &sorted {
174                    s.hash(state);
175                }
176            }
177            Self::Set(values) => {
178                // Hash each value in order (sets from literals maintain order)
179                for v in values {
180                    v.hash_value(state);
181                }
182            }
183            Self::Metadata(meta) => {
184                // Hash metadata in canonical order by sorting keys (boxed)
185                let mut keys: Vec<_> = meta.keys().collect();
186                keys.sort();
187                for key in keys {
188                    key.hash(state);
189                    // Hash the debug representation of the value
190                    format!("{:?}", meta.get(key)).hash(state);
191                }
192            }
193            Self::Interval(interval) => {
194                interval.count.hash(state);
195                interval.unit.hash(state);
196            }
197            Self::Object(obj) => {
198                // BTreeMap is already sorted by key, so iteration order is deterministic (boxed)
199                for (k, v) in obj.as_ref() {
200                    k.hash(state);
201                    v.hash_value(state);
202                }
203            }
204            Self::Null => {}
205        }
206    }
207}
208
209/// A row of query results.
210pub type Row = Vec<Value>;
211
212/// Compute a hash for a row (for DISTINCT deduplication).
213pub fn hash_row(row: &Row) -> u64 {
214    use std::collections::hash_map::DefaultHasher;
215    let mut hasher = DefaultHasher::new();
216    for value in row {
217        value.hash_value(&mut hasher);
218    }
219    hasher.finish()
220}
221
222/// Compute a hash for a single value (for PIVOT lookups).
223pub fn hash_single_value(value: &Value) -> u64 {
224    use std::collections::hash_map::DefaultHasher;
225    let mut hasher = DefaultHasher::new();
226    value.hash_value(&mut hasher);
227    hasher.finish()
228}
229
230/// Query result containing column names and rows.
231#[derive(Debug, Clone)]
232pub struct QueryResult {
233    /// Column names.
234    pub columns: Vec<String>,
235    /// Result rows.
236    pub rows: Vec<Row>,
237}
238
239impl QueryResult {
240    /// Create a new empty result.
241    pub const fn new(columns: Vec<String>) -> Self {
242        Self {
243            columns,
244            rows: Vec::new(),
245        }
246    }
247
248    /// Add a row to the result.
249    pub fn add_row(&mut self, row: Row) {
250        self.rows.push(row);
251    }
252
253    /// Number of rows.
254    pub const fn len(&self) -> usize {
255        self.rows.len()
256    }
257
258    /// Whether the result is empty.
259    pub const fn is_empty(&self) -> bool {
260        self.rows.is_empty()
261    }
262}
263
264/// Context for a single posting being evaluated.
265#[derive(Debug)]
266pub struct PostingContext<'a> {
267    /// The transaction this posting belongs to.
268    pub transaction: &'a Transaction,
269    /// The posting index within the transaction.
270    pub posting_index: usize,
271    /// Running balance after this posting (optional).
272    pub balance: Option<Inventory>,
273    /// The directive index (for source location lookup).
274    pub directive_index: Option<usize>,
275}
276
277/// Context for window function evaluation.
278#[derive(Debug, Clone)]
279pub struct WindowContext {
280    /// Row number within the partition (1-based).
281    pub row_number: usize,
282    /// Rank within the partition (1-based, ties get same rank).
283    pub rank: usize,
284    /// Dense rank within the partition (1-based, no gaps after ties).
285    pub dense_rank: usize,
286}
287
288/// Account information cached from Open/Close directives.
289#[derive(Debug, Clone)]
290pub struct AccountInfo {
291    /// Date the account was opened.
292    pub open_date: Option<NaiveDate>,
293    /// Date the account was closed (if any).
294    pub close_date: Option<NaiveDate>,
295    /// Metadata from the Open directive.
296    pub open_meta: Metadata,
297}
298
299/// An in-memory table created by CREATE TABLE.
300#[derive(Debug, Clone)]
301pub struct Table {
302    /// Column names.
303    pub columns: Vec<String>,
304    /// Rows of data.
305    pub rows: Vec<Vec<Value>>,
306}
307
308impl Table {
309    /// Create a new empty table with the given column names.
310    #[allow(clippy::missing_const_for_fn)] // Vec::new() isn't const with owned columns
311    pub fn new(columns: Vec<String>) -> Self {
312        Self {
313            columns,
314            rows: Vec::new(),
315        }
316    }
317
318    /// Add a row to the table.
319    pub fn add_row(&mut self, row: Vec<Value>) {
320        self.rows.push(row);
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    /// Verify Value enum size is reasonable after boxing heavy variants.
329    /// Previously 120 bytes, now 40 bytes (67% reduction).
330    #[test]
331    fn test_value_size() {
332        use std::mem::size_of;
333        // Value should be ~40 bytes with boxed variants (vs 120 unboxed)
334        assert!(
335            size_of::<Value>() <= 48,
336            "Value enum too large: {} bytes",
337            size_of::<Value>()
338        );
339    }
340}