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