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#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum Value {
114    /// String value.
115    String(String),
116    /// Numeric value.
117    Number(Decimal),
118    /// Integer value.
119    Integer(i64),
120    /// Date value.
121    Date(NaiveDate),
122    /// Boolean value.
123    Boolean(bool),
124    /// Amount (number + currency).
125    Amount(Amount),
126    /// Position (amount + optional cost).
127    Position(Position),
128    /// Inventory (aggregated positions).
129    Inventory(Inventory),
130    /// Set of strings (tags, links).
131    StringSet(Vec<String>),
132    /// Metadata dictionary.
133    Metadata(Metadata),
134    /// Interval for date arithmetic.
135    Interval(Interval),
136    /// Structured object (for entry, meta columns).
137    Object(BTreeMap<String, Self>),
138    /// NULL value.
139    Null,
140}
141
142impl Value {
143    /// Compute a hash for this value.
144    ///
145    /// Note: This is not the standard Hash trait because some contained types
146    /// (Decimal, Inventory) don't implement Hash. We use byte representations
147    /// for those types.
148    pub(crate) fn hash_value<H: Hasher>(&self, state: &mut H) {
149        std::mem::discriminant(self).hash(state);
150        match self {
151            Self::String(s) => s.hash(state),
152            Self::Number(d) => d.serialize().hash(state),
153            Self::Integer(i) => i.hash(state),
154            Self::Date(d) => {
155                d.year().hash(state);
156                d.month().hash(state);
157                d.day().hash(state);
158            }
159            Self::Boolean(b) => b.hash(state),
160            Self::Amount(a) => {
161                a.number.serialize().hash(state);
162                a.currency.as_str().hash(state);
163            }
164            Self::Position(p) => {
165                p.units.number.serialize().hash(state);
166                p.units.currency.as_str().hash(state);
167                if let Some(cost) = &p.cost {
168                    cost.number.serialize().hash(state);
169                    cost.currency.as_str().hash(state);
170                }
171            }
172            Self::Inventory(inv) => {
173                for pos in inv.positions() {
174                    pos.units.number.serialize().hash(state);
175                    pos.units.currency.as_str().hash(state);
176                    if let Some(cost) = &pos.cost {
177                        cost.number.serialize().hash(state);
178                        cost.currency.as_str().hash(state);
179                    }
180                }
181            }
182            Self::StringSet(ss) => {
183                // Hash StringSet in a canonical, order-independent way by sorting first.
184                let mut sorted = ss.clone();
185                sorted.sort();
186                for s in &sorted {
187                    s.hash(state);
188                }
189            }
190            Self::Metadata(meta) => {
191                // Hash metadata in canonical order by sorting keys
192                let mut keys: Vec<_> = meta.keys().collect();
193                keys.sort();
194                for key in keys {
195                    key.hash(state);
196                    // Hash the debug representation of the value
197                    format!("{:?}", meta.get(key)).hash(state);
198                }
199            }
200            Self::Interval(interval) => {
201                interval.count.hash(state);
202                interval.unit.hash(state);
203            }
204            Self::Object(obj) => {
205                // BTreeMap is already sorted by key, so iteration order is deterministic
206                for (k, v) in obj {
207                    k.hash(state);
208                    v.hash_value(state);
209                }
210            }
211            Self::Null => {}
212        }
213    }
214}
215
216/// A row of query results.
217pub type Row = Vec<Value>;
218
219/// Compute a hash for a row (for DISTINCT deduplication).
220pub fn hash_row(row: &Row) -> u64 {
221    use std::collections::hash_map::DefaultHasher;
222    let mut hasher = DefaultHasher::new();
223    for value in row {
224        value.hash_value(&mut hasher);
225    }
226    hasher.finish()
227}
228
229/// Compute a hash for a single value (for PIVOT lookups).
230pub fn hash_single_value(value: &Value) -> u64 {
231    use std::collections::hash_map::DefaultHasher;
232    let mut hasher = DefaultHasher::new();
233    value.hash_value(&mut hasher);
234    hasher.finish()
235}
236
237/// Query result containing column names and rows.
238#[derive(Debug, Clone)]
239pub struct QueryResult {
240    /// Column names.
241    pub columns: Vec<String>,
242    /// Result rows.
243    pub rows: Vec<Row>,
244}
245
246impl QueryResult {
247    /// Create a new empty result.
248    pub const fn new(columns: Vec<String>) -> Self {
249        Self {
250            columns,
251            rows: Vec::new(),
252        }
253    }
254
255    /// Add a row to the result.
256    pub fn add_row(&mut self, row: Row) {
257        self.rows.push(row);
258    }
259
260    /// Number of rows.
261    pub fn len(&self) -> usize {
262        self.rows.len()
263    }
264
265    /// Whether the result is empty.
266    pub fn is_empty(&self) -> bool {
267        self.rows.is_empty()
268    }
269}
270
271/// Context for a single posting being evaluated.
272#[derive(Debug)]
273pub struct PostingContext<'a> {
274    /// The transaction this posting belongs to.
275    pub transaction: &'a Transaction,
276    /// The posting index within the transaction.
277    pub posting_index: usize,
278    /// Running balance after this posting (optional).
279    pub balance: Option<Inventory>,
280    /// The directive index (for source location lookup).
281    pub directive_index: Option<usize>,
282}
283
284/// Context for window function evaluation.
285#[derive(Debug, Clone)]
286pub struct WindowContext {
287    /// Row number within the partition (1-based).
288    pub row_number: usize,
289    /// Rank within the partition (1-based, ties get same rank).
290    pub rank: usize,
291    /// Dense rank within the partition (1-based, no gaps after ties).
292    pub dense_rank: usize,
293}
294
295/// Account information cached from Open/Close directives.
296#[derive(Debug, Clone)]
297pub struct AccountInfo {
298    /// Date the account was opened.
299    pub open_date: Option<NaiveDate>,
300    /// Date the account was closed (if any).
301    pub close_date: Option<NaiveDate>,
302    /// Metadata from the Open directive.
303    pub open_meta: Metadata,
304}
305
306/// An in-memory table created by CREATE TABLE.
307#[derive(Debug, Clone)]
308pub struct Table {
309    /// Column names.
310    pub columns: Vec<String>,
311    /// Rows of data.
312    pub rows: Vec<Vec<Value>>,
313}
314
315impl Table {
316    /// Create a new empty table with the given column names.
317    #[allow(clippy::missing_const_for_fn)] // Vec::new() isn't const with owned columns
318    pub fn new(columns: Vec<String>) -> Self {
319        Self {
320            columns,
321            rows: Vec::new(),
322        }
323    }
324
325    /// Add a row to the table.
326    pub fn add_row(&mut self, row: Vec<Value>) {
327        self.rows.push(row);
328    }
329}