1mod functions;
6mod types;
7
8use types::AccountInfo;
9pub use types::{
10 Interval, IntervalUnit, PostingContext, QueryResult, Row, SourceLocation, Table, Value,
11 WindowContext,
12};
13
14use std::sync::RwLock;
15
16use rustc_hash::FxHashMap;
17
18use regex::{Regex, RegexBuilder};
19use rust_decimal::Decimal;
20use rustledger_core::{Amount, Directive, InternedStr, Inventory, Metadata, Position};
21#[cfg(test)]
22use rustledger_core::{MetaValue, NaiveDate, Transaction};
23use rustledger_loader::SourceMap;
24use rustledger_parser::Spanned;
25
26use crate::ast::{Expr, FromClause, FunctionCall, Query, Target};
27use crate::error::QueryError;
28
29pub struct Executor<'a> {
31 directives: &'a [Directive],
33 spanned_directives: Option<&'a [Spanned<Directive>]>,
35 balances: FxHashMap<InternedStr, Inventory>,
37 price_db: crate::price::PriceDatabase,
39 target_currency: Option<String>,
41 query_date: rustledger_core::NaiveDate,
43 regex_cache: RwLock<FxHashMap<String, Option<Regex>>>,
45 account_info: FxHashMap<String, AccountInfo>,
47 source_locations: Option<Vec<SourceLocation>>,
49 tables: FxHashMap<String, Table>,
51}
52
53mod aggregation;
55mod evaluation;
56mod execution;
57mod operators;
58mod sort;
59mod window;
60
61pub const WILDCARD_COLUMNS: &[&str] =
64 &["date", "flag", "payee", "narration", "account", "position"];
65
66impl<'a> Executor<'a> {
67 pub fn new(directives: &'a [Directive]) -> Self {
69 let price_db = crate::price::PriceDatabase::from_directives(directives);
70
71 let mut account_info: FxHashMap<String, AccountInfo> = FxHashMap::default();
73 for directive in directives {
74 match directive {
75 Directive::Open(open) => {
76 let account = open.account.to_string();
77 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
78 open_date: None,
79 close_date: None,
80 open_meta: Metadata::default(),
81 });
82 info.open_date = Some(open.date);
83 info.open_meta.clone_from(&open.meta);
84 }
85 Directive::Close(close) => {
86 let account = close.account.to_string();
87 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
88 open_date: None,
89 close_date: None,
90 open_meta: Metadata::default(),
91 });
92 info.close_date = Some(close.date);
93 }
94 _ => {}
95 }
96 }
97
98 Self {
99 directives,
100 spanned_directives: None,
101 balances: FxHashMap::default(),
102 price_db,
103 target_currency: None,
104 query_date: jiff::Zoned::now().date(),
105 regex_cache: RwLock::new(FxHashMap::default()),
106 account_info,
107 source_locations: None,
108 tables: FxHashMap::default(),
109 }
110 }
111
112 pub fn new_with_sources(
117 spanned_directives: &'a [Spanned<Directive>],
118 source_map: &SourceMap,
119 ) -> Self {
120 let mut price_db = crate::price::PriceDatabase::new();
123 for spanned in spanned_directives {
124 match &spanned.value {
125 Directive::Price(p) => {
126 price_db.add_price(p);
127 }
128 Directive::Transaction(txn) => {
129 price_db.add_implicit_prices_from_transaction(txn);
130 }
131 _ => {}
132 }
133 }
134 price_db.sort_prices();
135
136 let source_locations: Vec<SourceLocation> = spanned_directives
138 .iter()
139 .map(|spanned| {
140 let file = source_map.get(spanned.file_id as usize);
141 let (line, _col) = file.map_or((0, 0), |f| f.line_col(spanned.span.start));
142 SourceLocation {
143 filename: file.map_or_else(String::new, |f| f.path.display().to_string()),
144 lineno: line,
145 }
146 })
147 .collect();
148
149 let mut account_info: FxHashMap<String, AccountInfo> = FxHashMap::default();
151 for spanned in spanned_directives {
152 match &spanned.value {
153 Directive::Open(open) => {
154 let account = open.account.to_string();
155 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
156 open_date: None,
157 close_date: None,
158 open_meta: Metadata::default(),
159 });
160 info.open_date = Some(open.date);
161 info.open_meta.clone_from(&open.meta);
162 }
163 Directive::Close(close) => {
164 let account = close.account.to_string();
165 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
166 open_date: None,
167 close_date: None,
168 open_meta: Metadata::default(),
169 });
170 info.close_date = Some(close.date);
171 }
172 _ => {}
173 }
174 }
175
176 Self {
177 directives: &[], spanned_directives: Some(spanned_directives),
179 balances: FxHashMap::default(),
180 price_db,
181 target_currency: None,
182 query_date: jiff::Zoned::now().date(),
183 regex_cache: RwLock::new(FxHashMap::default()),
184 account_info,
185 source_locations: Some(source_locations),
186 tables: FxHashMap::default(),
187 }
188 }
189
190 fn get_source_location(&self, directive_index: usize) -> Option<&SourceLocation> {
192 self.source_locations
193 .as_ref()
194 .and_then(|locs| locs.get(directive_index))
195 }
196
197 fn get_or_compile_regex(&self, pattern: &str) -> Option<Regex> {
202 {
204 let cache = match self.regex_cache.read() {
207 Ok(guard) => guard,
208 Err(poisoned) => poisoned.into_inner(),
209 };
210 if let Some(cached) = cache.get(pattern) {
211 return cached.clone();
212 }
213 }
214 let compiled = RegexBuilder::new(pattern)
217 .case_insensitive(true)
218 .build()
219 .ok();
220 let mut cache = match self.regex_cache.write() {
221 Ok(guard) => guard,
222 Err(poisoned) => poisoned.into_inner(),
223 };
224 if let Some(cached) = cache.get(pattern) {
226 return cached.clone();
227 }
228 cache.insert(pattern.to_string(), compiled.clone());
229 compiled
230 }
231
232 fn require_regex(&self, pattern: &str) -> Result<Regex, QueryError> {
234 self.get_or_compile_regex(pattern)
235 .ok_or_else(|| QueryError::Type(format!("invalid regex: {pattern}")))
236 }
237
238 pub fn set_target_currency(&mut self, currency: impl Into<String>) {
240 self.target_currency = Some(currency.into());
241 }
242
243 pub fn execute(&mut self, query: &Query) -> Result<QueryResult, QueryError> {
256 match query {
257 Query::Select(select) => self.execute_select(select),
258 Query::Journal(journal) => self.execute_journal(journal),
259 Query::Balances(balances) => self.execute_balances(balances),
260 Query::Print(print) => self.execute_print(print),
261 Query::CreateTable(create) => self.execute_create_table(create),
262 Query::Insert(insert) => self.execute_insert(insert),
263 }
264 }
265
266 fn build_balances_with_filter(&mut self, from: Option<&FromClause>) -> Result<(), QueryError> {
268 for directive in self.directives {
269 if let Directive::Transaction(txn) = directive {
270 if let Some(from_clause) = from
272 && let Some(filter) = &from_clause.filter
273 && !self.evaluate_from_filter(filter, txn)?
274 {
275 continue;
276 }
277
278 for posting in &txn.postings {
279 if let Some(units) = posting.amount() {
280 let balance = self.balances.entry(posting.account.clone()).or_default();
281
282 let pos = if let Some(cost_spec) = &posting.cost {
283 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
284 Position::with_cost(units.clone(), cost)
285 } else {
286 Position::simple(units.clone())
287 }
288 } else {
289 Position::simple(units.clone())
290 };
291 balance.add(pos);
292 }
293 }
294 }
295 }
296 Ok(())
297 }
298
299 fn collect_postings(
301 &self,
302 from: Option<&FromClause>,
303 where_clause: Option<&Expr>,
304 ) -> Result<Vec<PostingContext<'a>>, QueryError> {
305 let mut postings = Vec::new();
306 let mut running_balances: FxHashMap<InternedStr, Inventory> = FxHashMap::default();
308
309 let directive_iter: Vec<(usize, &Directive)> =
312 if let Some(spanned) = self.spanned_directives {
313 spanned
314 .iter()
315 .enumerate()
316 .map(|(i, s)| (i, &s.value))
317 .collect()
318 } else {
319 self.directives.iter().enumerate().collect()
320 };
321
322 for (directive_index, directive) in directive_iter {
323 if let Directive::Transaction(txn) = directive {
324 if let Some(from) = from {
326 if let Some(open_date) = from.open_on
328 && txn.date < open_date
329 {
330 for posting in &txn.postings {
332 if let Some(units) = posting.amount() {
333 let balance =
334 running_balances.entry(posting.account.clone()).or_default();
335 balance.add(Position::simple(units.clone()));
336 }
337 }
338 continue;
339 }
340 if let Some(close_date) = from.close_on
341 && txn.date > close_date
342 {
343 continue;
344 }
345 if let Some(filter) = &from.filter
347 && !self.evaluate_from_filter(filter, txn)?
348 {
349 continue;
350 }
351 }
352
353 for (i, posting) in txn.postings.iter().enumerate() {
355 if let Some(units) = posting.amount() {
357 let balance = running_balances.entry(posting.account.clone()).or_default();
358 balance.add(Position::simple(units.clone()));
359 }
360
361 let ctx = PostingContext {
362 transaction: txn,
363 posting_index: i,
364 balance: running_balances.get(&posting.account).cloned(),
365 directive_index: Some(directive_index),
366 };
367
368 if let Some(where_expr) = where_clause {
370 if self.evaluate_predicate(where_expr, &ctx)? {
371 postings.push(ctx);
372 }
373 } else {
374 postings.push(ctx);
375 }
376 }
377 }
378 }
379
380 Ok(postings)
381 }
382 fn evaluate_function(
383 &self,
384 func: &FunctionCall,
385 ctx: &PostingContext,
386 ) -> Result<Value, QueryError> {
387 let name = func.name.to_uppercase();
388 match name.as_str() {
389 "YEAR" | "MONTH" | "DAY" | "WEEKDAY" | "QUARTER" | "YMONTH" | "TODAY" => {
391 self.eval_date_function(&name, func, ctx)
392 }
393 "DATE" | "DATE_DIFF" | "DATE_ADD" | "DATE_TRUNC" | "DATE_PART" | "PARSE_DATE"
395 | "DATE_BIN" | "INTERVAL" => self.eval_extended_date_function(&name, func, ctx),
396 "LENGTH" | "UPPER" | "LOWER" | "SUBSTR" | "SUBSTRING" | "TRIM" | "STARTSWITH"
398 | "ENDSWITH" | "GREP" | "GREPN" | "SUBST" | "SPLITCOMP" | "JOINSTR" | "MAXWIDTH" => {
399 self.eval_string_function(&name, func, ctx)
400 }
401 "PARENT" | "LEAF" | "ROOT" | "ACCOUNT_DEPTH" | "ACCOUNT_SORTKEY" => {
403 self.eval_account_function(&name, func, ctx)
404 }
405 "OPEN_DATE" | "CLOSE_DATE" | "OPEN_META" => {
407 self.eval_account_meta_function(&name, func, ctx)
408 }
409 "ABS" | "NEG" | "ROUND" | "SAFEDIV" => self.eval_math_function(&name, func, ctx),
411 "NUMBER" | "CURRENCY" | "GETITEM" | "GET" | "UNITS" | "COST" | "WEIGHT" | "VALUE" => {
413 self.eval_position_function(&name, func, ctx)
414 }
415 "EMPTY" | "FILTER_CURRENCY" | "POSSIGN" => {
417 self.eval_inventory_function(&name, func, ctx)
418 }
419 "GETPRICE" => self.eval_getprice(func, ctx),
421 "COALESCE" => self.eval_coalesce(func, ctx),
423 "ONLY" => self.eval_only(func, ctx),
424 "META" | "ENTRY_META" | "ANY_META" | "POSTING_META" => {
426 self.eval_meta_function(&name, func, ctx)
427 }
428 "CONVERT" => self.eval_convert(func, ctx),
430 "INT" => self.eval_int(func, ctx),
432 "DECIMAL" => self.eval_decimal(func, ctx),
433 "STR" => self.eval_str(func, ctx),
434 "BOOL" => self.eval_bool(func, ctx),
435 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
438 _ => Err(QueryError::UnknownFunction(func.name.clone())),
439 }
440 }
441
442 fn evaluate_function_on_values(&self, name: &str, args: &[Value]) -> Result<Value, QueryError> {
444 let name_upper = name.to_uppercase();
445 match name_upper.as_str() {
446 "TODAY" => Ok(Value::Date(jiff::Zoned::now().date())),
448 "YEAR" => {
449 Self::require_args_count(&name_upper, args, 1)?;
450 match &args[0] {
451 Value::Date(d) => Ok(Value::Integer(d.year().into())),
452 _ => Err(QueryError::Type("YEAR expects a date".to_string())),
453 }
454 }
455 "MONTH" => {
456 Self::require_args_count(&name_upper, args, 1)?;
457 match &args[0] {
458 Value::Date(d) => Ok(Value::Integer(d.month().into())),
459 _ => Err(QueryError::Type("MONTH expects a date".to_string())),
460 }
461 }
462 "DAY" => {
463 Self::require_args_count(&name_upper, args, 1)?;
464 match &args[0] {
465 Value::Date(d) => Ok(Value::Integer(d.day().into())),
466 _ => Err(QueryError::Type("DAY expects a date".to_string())),
467 }
468 }
469 "LENGTH" => {
471 Self::require_args_count(&name_upper, args, 1)?;
472 match &args[0] {
473 Value::String(s) => Ok(Value::Integer(s.len() as i64)),
474 _ => Err(QueryError::Type("LENGTH expects a string".to_string())),
475 }
476 }
477 "UPPER" => {
478 Self::require_args_count(&name_upper, args, 1)?;
479 match &args[0] {
480 Value::String(s) => Ok(Value::String(s.to_uppercase())),
481 _ => Err(QueryError::Type("UPPER expects a string".to_string())),
482 }
483 }
484 "LOWER" => {
485 Self::require_args_count(&name_upper, args, 1)?;
486 match &args[0] {
487 Value::String(s) => Ok(Value::String(s.to_lowercase())),
488 _ => Err(QueryError::Type("LOWER expects a string".to_string())),
489 }
490 }
491 "TRIM" => {
492 Self::require_args_count(&name_upper, args, 1)?;
493 match &args[0] {
494 Value::String(s) => Ok(Value::String(s.trim().to_string())),
495 _ => Err(QueryError::Type("TRIM expects a string".to_string())),
496 }
497 }
498 "ABS" => {
500 Self::require_args_count(&name_upper, args, 1)?;
501 match &args[0] {
502 Value::Number(n) => Ok(Value::Number(n.abs())),
503 Value::Integer(i) => Ok(Value::Integer(i.abs())),
504 _ => Err(QueryError::Type("ABS expects a number".to_string())),
505 }
506 }
507 "ROUND" => {
508 if args.is_empty() || args.len() > 2 {
509 return Err(QueryError::InvalidArguments(
510 "ROUND".to_string(),
511 "expected 1 or 2 arguments".to_string(),
512 ));
513 }
514 match &args[0] {
515 Value::Number(n) => {
516 let scale = if args.len() == 2 {
517 match &args[1] {
518 Value::Integer(i) => *i as u32,
519 _ => 0,
520 }
521 } else {
522 0
523 };
524 Ok(Value::Number(n.round_dp(scale)))
525 }
526 Value::Integer(i) => Ok(Value::Integer(*i)),
527 _ => Err(QueryError::Type("ROUND expects a number".to_string())),
528 }
529 }
530 "COALESCE" => {
532 for arg in args {
533 if !matches!(arg, Value::Null) {
534 return Ok(arg.clone());
535 }
536 }
537 Ok(Value::Null)
538 }
539 "NUMBER" => {
541 Self::require_args_count(&name_upper, args, 1)?;
542 match &args[0] {
543 Value::Amount(a) => Ok(Value::Number(a.number)),
544 Value::Position(p) => Ok(Value::Number(p.units.number)),
545 Value::Number(n) => Ok(Value::Number(*n)),
546 Value::Integer(i) => Ok(Value::Number(Decimal::from(*i))),
547 Value::Inventory(inv) => {
548 let positions = inv.positions();
551 if positions.is_empty() {
552 return Ok(Value::Number(Decimal::ZERO));
553 }
554 let first_currency = &positions[0].units.currency;
555 let all_same_currency = positions
556 .iter()
557 .all(|p| &p.units.currency == first_currency);
558 if all_same_currency {
559 let total: Decimal = positions.iter().map(|p| p.units.number).sum();
560 Ok(Value::Number(total))
561 } else {
562 Ok(Value::Null)
564 }
565 }
566 Value::Null => Ok(Value::Null),
567 _ => Err(QueryError::Type(
568 "NUMBER expects an amount, position, or inventory".to_string(),
569 )),
570 }
571 }
572 "CURRENCY" => {
573 Self::require_args_count(&name_upper, args, 1)?;
574 match &args[0] {
575 Value::Amount(a) => Ok(Value::String(a.currency.to_string())),
576 Value::Position(p) => Ok(Value::String(p.units.currency.to_string())),
577 Value::Inventory(inv) => {
578 if let Some(pos) = inv.positions().first() {
580 Ok(Value::String(pos.units.currency.to_string()))
581 } else {
582 Ok(Value::Null)
583 }
584 }
585 Value::Null => Ok(Value::Null),
586 _ => Err(QueryError::Type(
587 "CURRENCY expects an amount or position".to_string(),
588 )),
589 }
590 }
591 "UNITS" => {
592 Self::require_args_count(&name_upper, args, 1)?;
593 match &args[0] {
594 Value::Position(p) => Ok(Value::Amount(p.units.clone())),
595 Value::Amount(a) => Ok(Value::Amount(a.clone())),
596 Value::Inventory(inv) => {
597 let mut units_inv = Inventory::new();
599 for pos in inv.positions() {
600 units_inv.add(Position::simple(pos.units.clone()));
601 }
602 Ok(Value::Inventory(Box::new(units_inv)))
603 }
604 Value::Null => Ok(Value::Null),
605 _ => Err(QueryError::Type(
606 "UNITS expects a position or inventory".to_string(),
607 )),
608 }
609 }
610 "COST" => {
611 Self::require_args_count(&name_upper, args, 1)?;
612 match &args[0] {
613 Value::Position(p) => {
614 if let Some(cost) = &p.cost {
615 let total = p.units.number * cost.number;
617 Ok(Value::Amount(Amount::new(total, cost.currency.clone())))
618 } else {
619 Ok(Value::Amount(p.units.clone()))
620 }
621 }
622 Value::Amount(a) => Ok(Value::Amount(a.clone())),
623 Value::Inventory(inv) => {
624 let mut total = Decimal::ZERO;
625 let mut currency: Option<InternedStr> = None;
626 for pos in inv.positions() {
627 if let Some(cost) = &pos.cost {
628 total += pos.units.number * cost.number;
629 if currency.is_none() {
630 currency = Some(cost.currency.clone());
631 }
632 } else {
633 total += pos.units.number;
634 if currency.is_none() {
635 currency = Some(pos.units.currency.clone());
636 }
637 }
638 }
639 if let Some(curr) = currency {
640 Ok(Value::Amount(Amount::new(total, curr)))
641 } else {
642 Ok(Value::Null)
643 }
644 }
645 Value::Null => Ok(Value::Null),
646 _ => Err(QueryError::Type(
647 "COST expects a position or inventory".to_string(),
648 )),
649 }
650 }
651 "VALUE" => {
652 if args.is_empty() || args.len() > 2 {
654 return Err(QueryError::InvalidArguments(
655 "VALUE".to_string(),
656 "expected 1-2 arguments".to_string(),
657 ));
658 }
659 let explicit_currency = if args.len() == 2 {
660 match &args[1] {
661 Value::String(s) => Some(s.as_str()),
662 _ => None,
663 }
664 } else {
665 None
666 };
667 self.convert_to_market_value(&args[0], explicit_currency)
668 }
669 "SAFEDIV" => {
671 Self::require_args_count(&name_upper, args, 2)?;
672 let (dividend, divisor) = (&args[0], &args[1]);
673 match (dividend, divisor) {
674 (Value::Number(a), Value::Number(b)) => {
675 if b.is_zero() {
676 Ok(Value::Null)
677 } else {
678 Ok(Value::Number(a / b))
679 }
680 }
681 (Value::Integer(a), Value::Integer(b)) => {
682 if *b == 0 {
683 Ok(Value::Null)
684 } else {
685 Ok(Value::Number(Decimal::from(*a) / Decimal::from(*b)))
686 }
687 }
688 (Value::Number(a), Value::Integer(b)) => {
689 if *b == 0 {
690 Ok(Value::Null)
691 } else {
692 Ok(Value::Number(a / Decimal::from(*b)))
693 }
694 }
695 (Value::Integer(a), Value::Number(b)) => {
696 if b.is_zero() {
697 Ok(Value::Null)
698 } else {
699 Ok(Value::Number(Decimal::from(*a) / b))
700 }
701 }
702 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
703 _ => Err(QueryError::Type(
704 "SAFEDIV expects numeric arguments".to_string(),
705 )),
706 }
707 }
708 "NEG" => {
709 Self::require_args_count(&name_upper, args, 1)?;
710 match &args[0] {
711 Value::Number(n) => Ok(Value::Number(-n)),
712 Value::Integer(i) => Ok(Value::Integer(-i)),
713 Value::Amount(a) => {
714 Ok(Value::Amount(Amount::new(-a.number, a.currency.clone())))
715 }
716 _ => Err(QueryError::Type(
717 "NEG expects a number or amount".to_string(),
718 )),
719 }
720 }
721 "ACCOUNT_SORTKEY" => {
723 Self::require_args_count(&name_upper, args, 1)?;
724 match &args[0] {
725 Value::String(s) => {
726 let type_index = Self::account_type_index(s);
727 Ok(Value::String(format!("{type_index}-{s}")))
728 }
729 _ => Err(QueryError::Type(
730 "ACCOUNT_SORTKEY expects an account string".to_string(),
731 )),
732 }
733 }
734 "PARENT" => {
735 Self::require_args_count(&name_upper, args, 1)?;
736 match &args[0] {
737 Value::String(s) => {
738 if let Some(idx) = s.rfind(':') {
739 Ok(Value::String(s[..idx].to_string()))
740 } else {
741 Ok(Value::Null)
742 }
743 }
744 _ => Err(QueryError::Type(
745 "PARENT expects an account string".to_string(),
746 )),
747 }
748 }
749 "LEAF" => {
750 Self::require_args_count(&name_upper, args, 1)?;
751 match &args[0] {
752 Value::String(s) => {
753 if let Some(idx) = s.rfind(':') {
754 Ok(Value::String(s[idx + 1..].to_string()))
755 } else {
756 Ok(Value::String(s.clone()))
757 }
758 }
759 _ => Err(QueryError::Type(
760 "LEAF expects an account string".to_string(),
761 )),
762 }
763 }
764 "ROOT" => {
765 if args.is_empty() || args.len() > 2 {
766 return Err(QueryError::InvalidArguments(
767 "ROOT".to_string(),
768 "expected 1 or 2 arguments".to_string(),
769 ));
770 }
771 let n = if args.len() == 2 {
772 match &args[1] {
773 Value::Integer(i) => *i as usize,
774 _ => 1,
775 }
776 } else {
777 1
778 };
779 match &args[0] {
780 Value::String(s) => {
781 let parts: Vec<&str> = s.split(':').collect();
782 if n >= parts.len() {
783 Ok(Value::String(s.clone()))
784 } else {
785 Ok(Value::String(parts[..n].join(":")))
786 }
787 }
788 _ => Err(QueryError::Type(
789 "ROOT expects an account string".to_string(),
790 )),
791 }
792 }
793 "ONLY" => {
795 Self::require_args_count(&name_upper, args, 2)?;
796 let currency = match &args[0] {
797 Value::String(s) => s.clone(),
798 _ => {
799 return Err(QueryError::Type(
800 "ONLY: first argument must be a currency string".to_string(),
801 ));
802 }
803 };
804 match &args[1] {
805 Value::Inventory(inv) => {
806 let total = inv.units(¤cy);
807 if total.is_zero() {
808 Ok(Value::Null)
809 } else {
810 Ok(Value::Amount(Amount::new(total, ¤cy)))
811 }
812 }
813 Value::Position(p) => {
814 if p.units.currency.as_str() == currency {
815 Ok(Value::Amount(p.units.clone()))
816 } else {
817 Ok(Value::Null)
818 }
819 }
820 Value::Amount(a) => {
821 if a.currency.as_str() == currency {
822 Ok(Value::Amount(a.clone()))
823 } else {
824 Ok(Value::Null)
825 }
826 }
827 Value::Null => Ok(Value::Null),
828 _ => Err(QueryError::Type(
829 "ONLY: second argument must be an inventory, position, or amount"
830 .to_string(),
831 )),
832 }
833 }
834 "GETPRICE" => {
836 if args.len() < 2 || args.len() > 3 {
837 return Err(QueryError::InvalidArguments(
838 "GETPRICE".to_string(),
839 "expected 2 or 3 arguments".to_string(),
840 ));
841 }
842 let base = match &args[0] {
844 Value::String(s) => s.clone(),
845 Value::Null => return Ok(Value::Null),
846 _ => {
847 return Err(QueryError::Type(
848 "GETPRICE: first argument must be a currency string".to_string(),
849 ));
850 }
851 };
852 let quote = match &args[1] {
853 Value::String(s) => s.clone(),
854 Value::Null => return Ok(Value::Null),
855 _ => {
856 return Err(QueryError::Type(
857 "GETPRICE: second argument must be a currency string".to_string(),
858 ));
859 }
860 };
861 let date = if args.len() == 3 {
862 match &args[2] {
863 Value::Date(d) => *d,
864 Value::Null => self.query_date,
865 _ => self.query_date,
866 }
867 } else {
868 self.query_date
869 };
870 match self.price_db.get_price(&base, "e, date) {
871 Some(price) => Ok(Value::Number(price)),
872 None => Ok(Value::Null),
873 }
874 }
875 "EMPTY" => {
877 Self::require_args_count(&name_upper, args, 1)?;
878 match &args[0] {
879 Value::Inventory(inv) => Ok(Value::Boolean(inv.is_empty())),
880 Value::Null => Ok(Value::Boolean(true)),
881 _ => Err(QueryError::Type("EMPTY expects an inventory".to_string())),
882 }
883 }
884 "FILTER_CURRENCY" => {
885 Self::require_args_count(&name_upper, args, 2)?;
886 let currency = match &args[1] {
887 Value::String(s) => s.clone(),
888 _ => {
889 return Err(QueryError::Type(
890 "FILTER_CURRENCY expects (inventory, string)".to_string(),
891 ));
892 }
893 };
894 match &args[0] {
895 Value::Inventory(inv) => {
896 let filtered: Vec<Position> = inv
897 .positions()
898 .iter()
899 .filter(|p| p.units.currency.as_str() == currency)
900 .cloned()
901 .collect();
902 let mut new_inv = Inventory::new();
903 for pos in filtered {
904 new_inv.add(pos);
905 }
906 Ok(Value::Inventory(Box::new(new_inv)))
907 }
908 Value::Null => Ok(Value::Null),
909 _ => Err(QueryError::Type(
910 "FILTER_CURRENCY expects (inventory, string)".to_string(),
911 )),
912 }
913 }
914 "POSSIGN" => {
915 Self::require_args_count(&name_upper, args, 2)?;
916 let account_str = match &args[1] {
917 Value::String(s) => s.clone(),
918 _ => {
919 return Err(QueryError::Type(
920 "POSSIGN expects (amount, account_string)".to_string(),
921 ));
922 }
923 };
924 let first_component = account_str.split(':').next().unwrap_or("");
925 let is_credit_normal =
926 matches!(first_component, "Liabilities" | "Equity" | "Income");
927 match &args[0] {
928 Value::Amount(a) => {
929 let mut amt = a.clone();
930 if is_credit_normal {
931 amt.number = -amt.number;
932 }
933 Ok(Value::Amount(amt))
934 }
935 Value::Number(n) => {
936 let adjusted = if is_credit_normal { -n } else { *n };
937 Ok(Value::Number(adjusted))
938 }
939 Value::Null => Ok(Value::Null),
940 _ => Err(QueryError::Type(
941 "POSSIGN expects (amount, account_string)".to_string(),
942 )),
943 }
944 }
945 "CONVERT" => {
947 if args.len() < 2 || args.len() > 3 {
948 return Err(QueryError::InvalidArguments(
949 "CONVERT".to_string(),
950 "expected 2 or 3 arguments: (value, currency[, date])".to_string(),
951 ));
952 }
953
954 let target_currency = match &args[1] {
955 Value::String(s) => s.clone(),
956 _ => {
957 return Err(QueryError::Type(
958 "CONVERT: second argument must be a currency string".to_string(),
959 ));
960 }
961 };
962
963 let date: Option<rustledger_core::NaiveDate> = if args.len() == 3 {
965 match &args[2] {
966 Value::Date(d) => Some(*d),
967 Value::Null => None, _ => {
969 return Err(QueryError::Type(
970 "CONVERT: third argument must be a date".to_string(),
971 ));
972 }
973 }
974 } else {
975 None
976 };
977
978 let convert_amount = |amt: &Amount| -> Option<Amount> {
980 if let Some(d) = date {
981 self.price_db.convert(amt, &target_currency, d)
982 } else {
983 self.price_db.convert_latest(amt, &target_currency)
984 }
985 };
986
987 match &args[0] {
988 Value::Position(p) => {
989 if p.units.currency == target_currency {
990 Ok(Value::Amount(p.units.clone()))
991 } else if let Some(converted) = convert_amount(&p.units) {
992 Ok(Value::Amount(converted))
993 } else {
994 Ok(Value::Amount(p.units.clone()))
995 }
996 }
997 Value::Amount(a) => {
998 if a.currency == target_currency {
999 Ok(Value::Amount(a.clone()))
1000 } else if let Some(converted) = convert_amount(a) {
1001 Ok(Value::Amount(converted))
1002 } else {
1003 Ok(Value::Amount(a.clone()))
1004 }
1005 }
1006 Value::Inventory(inv) => {
1007 let mut result = Inventory::default();
1010 for pos in inv.positions() {
1011 if pos.units.currency == target_currency {
1012 result.add(Position::simple(pos.units.clone()));
1013 } else if let Some(converted) = convert_amount(&pos.units) {
1014 result.add(Position::simple(converted));
1015 } else {
1016 result.add(Position::simple(pos.units.clone()));
1018 }
1019 }
1020 let positions = result.positions();
1023 if positions.is_empty() {
1024 Ok(Value::Amount(Amount::new(Decimal::ZERO, &target_currency)))
1025 } else if positions.len() == 1
1026 && positions[0].units.currency == target_currency
1027 {
1028 Ok(Value::Amount(positions[0].units.clone()))
1029 } else {
1030 Ok(Value::Inventory(Box::new(result)))
1031 }
1032 }
1033 Value::Number(n) => Ok(Value::Amount(Amount::new(*n, &target_currency))),
1034 Value::Null => {
1035 Ok(Value::Amount(Amount::new(Decimal::ZERO, &target_currency)))
1038 }
1039 _ => Err(QueryError::Type(
1040 "CONVERT expects a position, amount, inventory, or number".to_string(),
1041 )),
1042 }
1043 }
1044 "STR" => {
1046 Self::require_args_count(&name_upper, args, 1)?;
1047 Self::value_to_str(&args[0])
1048 }
1049 "INT" => {
1050 Self::require_args_count(&name_upper, args, 1)?;
1051 Self::value_to_int(&args[0])
1052 }
1053 "DECIMAL" => {
1054 Self::require_args_count(&name_upper, args, 1)?;
1055 Self::value_to_decimal(&args[0])
1056 }
1057 "BOOL" => {
1058 Self::require_args_count(&name_upper, args, 1)?;
1059 Self::value_to_bool(&args[0])
1060 }
1061 "QUARTER" => {
1063 Self::require_args_count(&name_upper, args, 1)?;
1064 match &args[0] {
1065 Value::Date(d) => Ok(Value::Integer(((d.month() - 1) / 3 + 1).into())),
1066 _ => Err(QueryError::Type("QUARTER expects a date".to_string())),
1067 }
1068 }
1069 "WEEKDAY" => {
1070 Self::require_args_count(&name_upper, args, 1)?;
1071 match &args[0] {
1072 Value::Date(d) => Ok(Value::Integer(
1073 (d.weekday().to_monday_zero_offset() as u32).into(),
1074 )),
1075 _ => Err(QueryError::Type("WEEKDAY expects a date".to_string())),
1076 }
1077 }
1078 "YMONTH" => {
1079 Self::require_args_count(&name_upper, args, 1)?;
1080 match &args[0] {
1081 Value::Date(d) => {
1082 Ok(Value::String(format!("{:04}-{:02}", d.year(), d.month())))
1083 }
1084 _ => Err(QueryError::Type("YMONTH expects a date".to_string())),
1085 }
1086 }
1087 "SUBSTR" | "SUBSTRING" => {
1089 if args.len() < 2 || args.len() > 3 {
1090 return Err(QueryError::InvalidArguments(
1091 name_upper,
1092 "expected 2 or 3 arguments".to_string(),
1093 ));
1094 }
1095 match (&args[0], &args[1], args.get(2)) {
1096 (Value::String(s), Value::Integer(start), None) => {
1097 let start = (*start).max(0) as usize;
1098 let result: String = s.chars().skip(start).collect();
1099 Ok(Value::String(result))
1100 }
1101 (Value::String(s), Value::Integer(start), Some(Value::Integer(len))) => {
1102 let start = (*start).max(0) as usize;
1103 let len = (*len).max(0) as usize;
1104 let result: String = s.chars().skip(start).take(len).collect();
1105 Ok(Value::String(result))
1106 }
1107 _ => Err(QueryError::Type(
1108 "SUBSTR expects (string, int, [int])".to_string(),
1109 )),
1110 }
1111 }
1112 "STARTSWITH" => {
1113 Self::require_args_count(&name_upper, args, 2)?;
1114 match (&args[0], &args[1]) {
1115 (Value::String(s), Value::String(prefix)) => {
1116 Ok(Value::Boolean(s.starts_with(prefix.as_str())))
1117 }
1118 _ => Err(QueryError::Type(
1119 "STARTSWITH expects two strings".to_string(),
1120 )),
1121 }
1122 }
1123 "ENDSWITH" => {
1124 Self::require_args_count(&name_upper, args, 2)?;
1125 match (&args[0], &args[1]) {
1126 (Value::String(s), Value::String(suffix)) => {
1127 Ok(Value::Boolean(s.ends_with(suffix.as_str())))
1128 }
1129 _ => Err(QueryError::Type("ENDSWITH expects two strings".to_string())),
1130 }
1131 }
1132 "MAXWIDTH" => {
1133 Self::require_args_count(&name_upper, args, 2)?;
1134 match (&args[0], &args[1]) {
1135 (Value::String(s), Value::Integer(max)) => {
1136 let n = *max as usize;
1137 if s.chars().count() <= n {
1138 Ok(Value::String(s.clone()))
1139 } else if n <= 3 {
1140 Ok(Value::String(s.chars().take(n).collect()))
1141 } else {
1142 let truncated: String = s.chars().take(n - 3).collect();
1143 Ok(Value::String(format!("{truncated}...")))
1144 }
1145 }
1146 _ => Err(QueryError::Type(
1147 "MAXWIDTH expects (string, integer)".to_string(),
1148 )),
1149 }
1150 }
1151 "ACCOUNT_DEPTH" => {
1153 Self::require_args_count(&name_upper, args, 1)?;
1154 match &args[0] {
1155 Value::String(s) => Ok(Value::Integer(s.matches(':').count() as i64 + 1)),
1156 _ => Err(QueryError::Type(
1157 "ACCOUNT_DEPTH expects an account string".to_string(),
1158 )),
1159 }
1160 }
1161 "GETITEM" | "GET" => {
1163 Self::require_args_count(&name_upper, args, 2)?;
1164 match (&args[0], &args[1]) {
1165 (Value::Inventory(inv), Value::String(currency)) => {
1166 let amount = inv.units(currency);
1167 if amount.is_zero() {
1168 Ok(Value::Null)
1169 } else {
1170 Ok(Value::Amount(Amount::new(amount, currency.as_str())))
1171 }
1172 }
1173 (Value::Null, _) => Ok(Value::Null),
1174 _ => Err(QueryError::Type(
1175 "GETITEM expects (inventory, string)".to_string(),
1176 )),
1177 }
1178 }
1179 "WEIGHT" => {
1180 Self::require_args_count(&name_upper, args, 1)?;
1181 match &args[0] {
1182 Value::Position(p) => {
1183 if let Some(cost) = &p.cost {
1184 let total = p.units.number * cost.number;
1185 Ok(Value::Amount(Amount::new(total, cost.currency.clone())))
1186 } else {
1187 Ok(Value::Amount(p.units.clone()))
1188 }
1189 }
1190 Value::Amount(a) => Ok(Value::Amount(a.clone())),
1191 Value::Inventory(inv) => {
1192 let mut result = Inventory::new();
1193 for pos in inv.positions() {
1194 if let Some(cost) = &pos.cost {
1195 let total = pos.units.number * cost.number;
1196 result.add(Position::simple(Amount::new(
1197 total,
1198 cost.currency.clone(),
1199 )));
1200 } else {
1201 result.add(Position::simple(pos.units.clone()));
1202 }
1203 }
1204 Ok(Value::Inventory(Box::new(result)))
1205 }
1206 Value::Null => Ok(Value::Null),
1207 _ => Err(QueryError::Type(
1208 "WEIGHT expects a position, amount, or inventory".to_string(),
1209 )),
1210 }
1211 }
1212 "DATE_DIFF" => {
1214 Self::require_args_count(&name_upper, args, 2)?;
1215 match (&args[0], &args[1]) {
1216 (Value::Date(d1), Value::Date(d2)) => Ok(Value::Integer(i64::from(
1217 d1.since(*d2).unwrap_or_default().get_days(),
1218 ))),
1219 _ => Err(QueryError::Type("DATE_DIFF expects two dates".to_string())),
1220 }
1221 }
1222 "GREP" => {
1224 Self::require_args_count(&name_upper, args, 2)?;
1225 match (&args[0], &args[1]) {
1226 (Value::String(pattern), Value::String(s)) => {
1227 let re = regex::Regex::new(pattern).map_err(|e| {
1228 QueryError::Type(format!("GREP: invalid regex '{pattern}': {e}"))
1229 })?;
1230 match re.find(s) {
1231 Some(m) => Ok(Value::String(m.as_str().to_string())),
1232 None => Ok(Value::Null),
1233 }
1234 }
1235 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1237 _ => Err(QueryError::Type("GREP expects two strings".to_string())),
1238 }
1239 }
1240 "GREPN" => {
1241 Self::require_args_count(&name_upper, args, 3)?;
1242 let n = match &args[2] {
1243 Value::Integer(i) => (*i).max(0) as usize,
1244 Value::Number(n) => {
1245 use rust_decimal::prelude::ToPrimitive;
1246 n.to_usize().unwrap_or(0)
1247 }
1248 _ => {
1249 return Err(QueryError::Type(
1250 "GREPN: third argument must be an integer".to_string(),
1251 ));
1252 }
1253 };
1254 match (&args[0], &args[1]) {
1255 (Value::String(pattern), Value::String(s)) => {
1256 let re = regex::Regex::new(pattern).map_err(|e| {
1257 QueryError::Type(format!("GREPN: invalid regex '{pattern}': {e}"))
1258 })?;
1259 match re.captures(s) {
1260 Some(caps) => match caps.get(n) {
1261 Some(m) => Ok(Value::String(m.as_str().to_string())),
1262 None => Ok(Value::Null),
1263 },
1264 None => Ok(Value::Null),
1265 }
1266 }
1267 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1268 _ => Err(QueryError::Type(
1269 "GREPN expects (pattern, string, int)".to_string(),
1270 )),
1271 }
1272 }
1273 "SUBST" => {
1274 Self::require_args_count(&name_upper, args, 3)?;
1275 match (&args[0], &args[1], &args[2]) {
1276 (Value::String(pattern), Value::String(replacement), Value::String(s)) => {
1277 let re = regex::Regex::new(pattern).map_err(|e| {
1278 QueryError::Type(format!("SUBST: invalid regex '{pattern}': {e}"))
1279 })?;
1280 Ok(Value::String(
1281 re.replace_all(s, replacement.as_str()).to_string(),
1282 ))
1283 }
1284 _ => Err(QueryError::Type(
1285 "SUBST expects (pattern, replacement, string)".to_string(),
1286 )),
1287 }
1288 }
1289 "SPLITCOMP" => {
1290 Self::require_args_count(&name_upper, args, 3)?;
1291 let n = match &args[2] {
1292 Value::Integer(i) => (*i).max(0) as usize,
1293 Value::Number(n) => {
1294 use rust_decimal::prelude::ToPrimitive;
1295 n.to_usize().unwrap_or(0)
1296 }
1297 _ => {
1298 return Err(QueryError::Type(
1299 "SPLITCOMP: third argument must be an integer".to_string(),
1300 ));
1301 }
1302 };
1303 match (&args[0], &args[1]) {
1304 (Value::String(s), Value::String(delim)) => {
1305 let parts: Vec<&str> = s.split(delim.as_str()).collect();
1306 match parts.get(n) {
1307 Some(part) => Ok(Value::String((*part).to_string())),
1308 None => Ok(Value::Null),
1309 }
1310 }
1311 _ => Err(QueryError::Type(
1312 "SPLITCOMP expects (string, delimiter, int)".to_string(),
1313 )),
1314 }
1315 }
1316 "JOINSTR" => {
1317 let mut parts = Vec::new();
1318 for v in args {
1319 match v {
1320 Value::String(s) => parts.push(s.clone()),
1321 Value::StringSet(ss) => parts.extend(ss.iter().cloned()),
1322 Value::Integer(i) => parts.push(i.to_string()),
1323 Value::Number(n) => parts.push(n.to_string()),
1324 Value::Null => {}
1325 _ => {}
1326 }
1327 }
1328 Ok(Value::String(parts.join(",")))
1329 }
1330 "OPEN_DATE" => {
1332 Self::require_args_count(&name_upper, args, 1)?;
1333 match &args[0] {
1334 Value::String(account) => Ok(self
1335 .account_info
1336 .get(account.as_str())
1337 .and_then(|info| info.open_date)
1338 .map_or(Value::Null, Value::Date)),
1339 Value::Null => Ok(Value::Null),
1340 _ => Err(QueryError::Type(
1341 "OPEN_DATE expects an account string".to_string(),
1342 )),
1343 }
1344 }
1345 "CLOSE_DATE" => {
1346 Self::require_args_count(&name_upper, args, 1)?;
1347 match &args[0] {
1348 Value::String(account) => Ok(self
1349 .account_info
1350 .get(account.as_str())
1351 .and_then(|info| info.close_date)
1352 .map_or(Value::Null, Value::Date)),
1353 Value::Null => Ok(Value::Null),
1354 _ => Err(QueryError::Type(
1355 "CLOSE_DATE expects an account string".to_string(),
1356 )),
1357 }
1358 }
1359 "OPEN_META" => {
1360 Self::require_args_count(&name_upper, args, 2)?;
1361 match (&args[0], &args[1]) {
1362 (Value::String(account), Value::String(key)) => Ok(self
1363 .account_info
1364 .get(account.as_str())
1365 .and_then(|info| info.open_meta.get(key))
1366 .map_or(Value::Null, |mv| Self::meta_value_to_value(Some(mv)))),
1367 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1368 _ => Err(QueryError::Type(
1369 "OPEN_META expects (account_string, key_string)".to_string(),
1370 )),
1371 }
1372 }
1373 "META" | "ENTRY_META" | "ANY_META" | "POSTING_META" => {
1377 Self::require_args_count(&name_upper, args, 1)?;
1378 match &args[0] {
1379 Value::String(_) | Value::Null => Ok(Value::Null),
1380 _ => Err(QueryError::Type(format!(
1381 "{name_upper}: argument must be a string key"
1382 ))),
1383 }
1384 }
1385 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
1387 _ => Err(QueryError::UnknownFunction(name.to_string())),
1388 }
1389 }
1390
1391 fn metadata_to_value(meta: &rustledger_core::Metadata) -> Value {
1393 if meta.is_empty() {
1394 return Value::Null;
1395 }
1396 let map: std::collections::BTreeMap<String, Value> = meta
1397 .iter()
1398 .map(|(k, v)| (k.clone(), Self::meta_value_to_value(Some(v))))
1399 .collect();
1400 Value::Object(Box::new(map))
1401 }
1402
1403 fn require_args_count(name: &str, args: &[Value], expected: usize) -> Result<(), QueryError> {
1405 if args.len() != expected {
1406 return Err(QueryError::InvalidArguments(
1407 name.to_string(),
1408 format!("expected {} argument(s), got {}", expected, args.len()),
1409 ));
1410 }
1411 Ok(())
1412 }
1413
1414 fn require_args(name: &str, func: &FunctionCall, expected: usize) -> Result<(), QueryError> {
1416 if func.args.len() != expected {
1417 return Err(QueryError::InvalidArguments(
1418 name.to_string(),
1419 format!("expected {expected} argument(s)"),
1420 ));
1421 }
1422 Ok(())
1423 }
1424
1425 pub(crate) fn convert_to_market_value(
1446 &self,
1447 val: &Value,
1448 explicit_currency: Option<&str>,
1449 ) -> Result<Value, QueryError> {
1450 let target_currency = if let Some(currency) = explicit_currency {
1455 currency.to_string()
1456 } else {
1457 let inferred = match val {
1459 Value::Position(p) => p.cost.as_ref().map(|c| c.currency.to_string()),
1460 Value::Inventory(inv) => inv
1461 .positions()
1462 .iter()
1463 .find_map(|p| p.cost.as_ref().map(|c| c.currency.to_string())),
1464 _ => None,
1465 };
1466
1467 match inferred.or_else(|| self.target_currency.clone()) {
1468 Some(c) => c,
1469 None => {
1470 return match val {
1473 Value::Position(p) => Ok(Value::Amount(p.units.clone())),
1474 Value::Amount(a) => Ok(Value::Amount(a.clone())),
1475 Value::Inventory(inv) => Ok(Value::Inventory(inv.clone())),
1476 Value::Null => Ok(Value::Null),
1477 _ => Err(QueryError::Type(
1478 "VALUE expects a position, amount, or inventory".to_string(),
1479 )),
1480 };
1481 }
1482 }
1483 };
1484
1485 match val {
1488 Value::Position(p) => {
1489 if p.units.currency == target_currency {
1490 Ok(Value::Amount(p.units.clone()))
1491 } else if let Some(converted) =
1492 self.price_db.convert_latest(&p.units, &target_currency)
1493 {
1494 Ok(Value::Amount(converted))
1495 } else {
1496 Ok(Value::Amount(p.units.clone()))
1497 }
1498 }
1499 Value::Amount(a) => {
1500 if a.currency == target_currency {
1501 Ok(Value::Amount(a.clone()))
1502 } else if let Some(converted) = self.price_db.convert_latest(a, &target_currency) {
1503 Ok(Value::Amount(converted))
1504 } else {
1505 Ok(Value::Amount(a.clone()))
1506 }
1507 }
1508 Value::Inventory(inv) => {
1509 let mut total = Decimal::ZERO;
1510 for pos in inv.positions() {
1511 if pos.units.currency == target_currency {
1512 total += pos.units.number;
1513 } else if let Some(converted) =
1514 self.price_db.convert_latest(&pos.units, &target_currency)
1515 {
1516 total += converted.number;
1517 }
1518 }
1519 Ok(Value::Amount(Amount::new(total, &target_currency)))
1520 }
1521 Value::Null => Ok(Value::Null),
1522 _ => Err(QueryError::Type(
1523 "VALUE expects a position, amount, or inventory".to_string(),
1524 )),
1525 }
1526 }
1527
1528 pub(super) const fn is_window_expr(expr: &Expr) -> bool {
1530 matches!(expr, Expr::Window(_))
1531 }
1532
1533 fn resolve_column_names(&self, targets: &[Target]) -> Result<Vec<String>, QueryError> {
1535 let mut names = Vec::new();
1536 for (i, target) in targets.iter().enumerate() {
1537 if matches!(target.expr, Expr::Wildcard) {
1538 if target.alias.is_some() {
1540 return Err(QueryError::Evaluation(
1541 "Cannot alias wildcard (*) - it expands to multiple columns".to_string(),
1542 ));
1543 }
1544 names.extend(WILDCARD_COLUMNS.iter().map(|s| (*s).to_string()));
1546 } else if let Some(alias) = &target.alias {
1547 names.push(alias.clone());
1548 } else {
1549 names.push(self.expr_to_name(&target.expr, i));
1550 }
1551 }
1552 Ok(names)
1553 }
1554
1555 fn expr_to_name(&self, expr: &Expr, index: usize) -> String {
1557 match expr {
1558 Expr::Wildcard => "*".to_string(),
1559 Expr::Column(name) => name.clone(),
1560 Expr::Function(func) => func.name.clone(),
1561 Expr::Window(wf) => wf.name.clone(),
1562 _ => format!("col{index}"),
1563 }
1564 }
1565
1566 pub(super) fn get_builtin_table(&self, table_name: &str) -> Option<Table> {
1585 let upper = table_name.to_uppercase();
1589 let normalized = upper.strip_prefix('#').unwrap_or(&upper);
1590
1591 match normalized {
1592 "PRICES" => Some(self.build_prices_table()),
1593 "BALANCES" => Some(self.build_balances_table()),
1594 "COMMODITIES" => Some(self.build_commodities_table()),
1595 "EVENTS" => Some(self.build_events_table()),
1596 "NOTES" => Some(self.build_notes_table()),
1597 "DOCUMENTS" => Some(self.build_documents_table()),
1598 "ACCOUNTS" => Some(self.build_accounts_table()),
1599 "TRANSACTIONS" => Some(self.build_transactions_table()),
1600 "ENTRIES" => Some(self.build_entries_table()),
1601 "POSTINGS" => Some(self.build_postings_table()),
1602 _ => None,
1603 }
1604 }
1605
1606 fn build_prices_table(&self) -> Table {
1613 let columns = vec![
1614 "date".to_string(),
1615 "currency".to_string(),
1616 "amount".to_string(),
1617 ];
1618 let mut table = Table::new(columns);
1619
1620 let mut entries: Vec<_> = self.price_db.iter_entries().collect();
1622 entries.sort_by(|(currency_a, date_a, _, _), (currency_b, date_b, _, _)| {
1624 date_a.cmp(date_b).then_with(|| currency_a.cmp(currency_b))
1625 });
1626
1627 for (base_currency, date, price_number, quote_currency) in entries {
1628 let row = vec![
1629 Value::Date(date),
1630 Value::String(base_currency.to_string()),
1631 Value::Amount(Amount::new(price_number, quote_currency)),
1632 ];
1633 table.add_row(row);
1634 }
1635
1636 table
1637 }
1638
1639 fn build_balances_table(&self) -> Table {
1646 let columns = vec![
1647 "date".to_string(),
1648 "account".to_string(),
1649 "amount".to_string(),
1650 ];
1651 let mut table = Table::new(columns);
1652
1653 let mut balances: Vec<_> = if let Some(spanned) = self.spanned_directives {
1655 spanned
1656 .iter()
1657 .filter_map(|s| {
1658 if let Directive::Balance(b) = &s.value {
1659 Some((b.date, b.account.as_ref(), b.amount.clone()))
1660 } else {
1661 None
1662 }
1663 })
1664 .collect()
1665 } else {
1666 self.directives
1667 .iter()
1668 .filter_map(|d| {
1669 if let Directive::Balance(b) = d {
1670 Some((b.date, b.account.as_ref(), b.amount.clone()))
1671 } else {
1672 None
1673 }
1674 })
1675 .collect()
1676 };
1677
1678 balances.sort_by(|(date_a, account_a, _), (date_b, account_b, _)| {
1680 date_a.cmp(date_b).then_with(|| account_a.cmp(account_b))
1681 });
1682
1683 for (date, account, amount) in balances {
1684 let row = vec![
1685 Value::Date(date),
1686 Value::String(account.to_string()),
1687 Value::Amount(amount),
1688 ];
1689 table.add_row(row);
1690 }
1691
1692 table
1693 }
1694
1695 fn build_commodities_table(&self) -> Table {
1701 let columns = vec!["date".to_string(), "name".to_string()];
1702 let mut table = Table::new(columns);
1703
1704 let mut commodities: Vec<_> = if let Some(spanned) = self.spanned_directives {
1706 spanned
1707 .iter()
1708 .filter_map(|s| {
1709 if let Directive::Commodity(c) = &s.value {
1710 Some((c.date, c.currency.as_ref()))
1711 } else {
1712 None
1713 }
1714 })
1715 .collect()
1716 } else {
1717 self.directives
1718 .iter()
1719 .filter_map(|d| {
1720 if let Directive::Commodity(c) = d {
1721 Some((c.date, c.currency.as_ref()))
1722 } else {
1723 None
1724 }
1725 })
1726 .collect()
1727 };
1728
1729 commodities.sort_by(|(date_a, name_a), (date_b, name_b)| {
1731 date_a.cmp(date_b).then_with(|| name_a.cmp(name_b))
1732 });
1733
1734 for (date, name) in commodities {
1735 let row = vec![Value::Date(date), Value::String(name.to_string())];
1736 table.add_row(row);
1737 }
1738
1739 table
1740 }
1741
1742 fn build_events_table(&self) -> Table {
1749 let columns = vec![
1750 "date".to_string(),
1751 "type".to_string(),
1752 "description".to_string(),
1753 ];
1754 let mut table = Table::new(columns);
1755
1756 let mut events: Vec<_> = if let Some(spanned) = self.spanned_directives {
1758 spanned
1759 .iter()
1760 .filter_map(|s| {
1761 if let Directive::Event(e) = &s.value {
1762 Some((e.date, e.event_type.as_str(), e.value.as_str()))
1763 } else {
1764 None
1765 }
1766 })
1767 .collect()
1768 } else {
1769 self.directives
1770 .iter()
1771 .filter_map(|d| {
1772 if let Directive::Event(e) = d {
1773 Some((e.date, e.event_type.as_str(), e.value.as_str()))
1774 } else {
1775 None
1776 }
1777 })
1778 .collect()
1779 };
1780
1781 events.sort_by(|(date_a, type_a, _), (date_b, type_b, _)| {
1783 date_a.cmp(date_b).then_with(|| type_a.cmp(type_b))
1784 });
1785
1786 for (date, event_type, description) in events {
1787 let row = vec![
1788 Value::Date(date),
1789 Value::String(event_type.to_string()),
1790 Value::String(description.to_string()),
1791 ];
1792 table.add_row(row);
1793 }
1794
1795 table
1796 }
1797
1798 fn build_notes_table(&self) -> Table {
1805 let columns = vec![
1806 "date".to_string(),
1807 "account".to_string(),
1808 "comment".to_string(),
1809 ];
1810 let mut table = Table::new(columns);
1811
1812 let mut notes: Vec<_> = if let Some(spanned) = self.spanned_directives {
1814 spanned
1815 .iter()
1816 .filter_map(|s| {
1817 if let Directive::Note(n) = &s.value {
1818 Some((n.date, n.account.as_ref(), n.comment.as_str()))
1819 } else {
1820 None
1821 }
1822 })
1823 .collect()
1824 } else {
1825 self.directives
1826 .iter()
1827 .filter_map(|d| {
1828 if let Directive::Note(n) = d {
1829 Some((n.date, n.account.as_ref(), n.comment.as_str()))
1830 } else {
1831 None
1832 }
1833 })
1834 .collect()
1835 };
1836
1837 notes.sort_by(|(date_a, account_a, _), (date_b, account_b, _)| {
1839 date_a.cmp(date_b).then_with(|| account_a.cmp(account_b))
1840 });
1841
1842 for (date, account, comment) in notes {
1843 let row = vec![
1844 Value::Date(date),
1845 Value::String(account.to_string()),
1846 Value::String(comment.to_string()),
1847 ];
1848 table.add_row(row);
1849 }
1850
1851 table
1852 }
1853
1854 fn build_documents_table(&self) -> Table {
1863 let columns = vec![
1864 "date".to_string(),
1865 "account".to_string(),
1866 "filename".to_string(),
1867 "tags".to_string(),
1868 "links".to_string(),
1869 ];
1870 let mut table = Table::new(columns);
1871
1872 let mut documents: Vec<_> = if let Some(spanned) = self.spanned_directives {
1874 spanned
1875 .iter()
1876 .filter_map(|s| {
1877 if let Directive::Document(d) = &s.value {
1878 Some((
1879 d.date,
1880 d.account.as_ref(),
1881 d.path.as_str(),
1882 &d.tags,
1883 &d.links,
1884 ))
1885 } else {
1886 None
1887 }
1888 })
1889 .collect()
1890 } else {
1891 self.directives
1892 .iter()
1893 .filter_map(|d| {
1894 if let Directive::Document(doc) = d {
1895 Some((
1896 doc.date,
1897 doc.account.as_ref(),
1898 doc.path.as_str(),
1899 &doc.tags,
1900 &doc.links,
1901 ))
1902 } else {
1903 None
1904 }
1905 })
1906 .collect()
1907 };
1908
1909 documents.sort_by(
1911 |(date_a, account_a, file_a, _, _), (date_b, account_b, file_b, _, _)| {
1912 date_a
1913 .cmp(date_b)
1914 .then_with(|| account_a.cmp(account_b))
1915 .then_with(|| file_a.cmp(file_b))
1916 },
1917 );
1918
1919 for (date, account, filename, tags, links) in documents {
1920 let tags_vec: Vec<String> = tags.iter().map(ToString::to_string).collect();
1921 let links_vec: Vec<String> = links.iter().map(ToString::to_string).collect();
1922 let row = vec![
1923 Value::Date(date),
1924 Value::String(account.to_string()),
1925 Value::String(filename.to_string()),
1926 Value::StringSet(tags_vec),
1927 Value::StringSet(links_vec),
1928 ];
1929 table.add_row(row);
1930 }
1931
1932 table
1933 }
1934
1935 fn build_accounts_table(&self) -> Table {
1944 let columns = vec![
1945 "account".to_string(),
1946 "open".to_string(),
1947 "close".to_string(),
1948 "currencies".to_string(),
1949 "booking".to_string(),
1950 ];
1951 let mut table = Table::new(columns);
1952
1953 let mut accounts: FxHashMap<
1955 &str,
1956 (
1957 Option<rustledger_core::NaiveDate>,
1958 Option<rustledger_core::NaiveDate>,
1959 Vec<String>,
1960 Option<&str>,
1961 ),
1962 > = FxHashMap::default();
1963
1964 let iter: Box<dyn Iterator<Item = &Directive>> =
1966 if let Some(spanned) = self.spanned_directives {
1967 Box::new(spanned.iter().map(|s| &s.value))
1968 } else {
1969 Box::new(self.directives.iter())
1970 };
1971
1972 for directive in iter {
1973 match directive {
1974 Directive::Open(open) => {
1975 let entry = accounts.entry(open.account.as_ref()).or_insert((
1976 None,
1977 None,
1978 Vec::new(),
1979 None,
1980 ));
1981 entry.0 = Some(open.date);
1982 entry.2 = open.currencies.iter().map(ToString::to_string).collect();
1983 entry.3 = open.booking.as_deref();
1984 }
1985 Directive::Close(close) => {
1986 let entry = accounts.entry(close.account.as_ref()).or_insert((
1987 None,
1988 None,
1989 Vec::new(),
1990 None,
1991 ));
1992 entry.1 = Some(close.date);
1993 }
1994 _ => {}
1995 }
1996 }
1997
1998 let mut account_list: Vec<_> = accounts.into_iter().collect();
2000 account_list.sort_by_key(|(a, _)| *a);
2001
2002 for (account, (open_date, close_date, currencies, booking)) in account_list {
2003 let row = vec![
2004 Value::String(account.to_string()),
2005 open_date.map_or(Value::Null, Value::Date),
2006 close_date.map_or(Value::Null, Value::Date),
2007 Value::StringSet(currencies),
2008 booking.map_or(Value::Null, |b| Value::String(b.to_string())),
2009 ];
2010 table.add_row(row);
2011 }
2012
2013 table
2014 }
2015
2016 fn build_transactions_table(&self) -> Table {
2027 let columns = vec![
2028 "date".to_string(),
2029 "flag".to_string(),
2030 "payee".to_string(),
2031 "narration".to_string(),
2032 "tags".to_string(),
2033 "links".to_string(),
2034 "accounts".to_string(),
2035 ];
2036 let mut table = Table::new(columns);
2037
2038 let iter: Box<dyn Iterator<Item = &Directive>> =
2040 if let Some(spanned) = self.spanned_directives {
2041 Box::new(spanned.iter().map(|s| &s.value))
2042 } else {
2043 Box::new(self.directives.iter())
2044 };
2045
2046 let mut transactions: Vec<_> = iter
2047 .filter_map(|d| {
2048 if let Directive::Transaction(txn) = d {
2049 Some(txn)
2050 } else {
2051 None
2052 }
2053 })
2054 .collect();
2055
2056 transactions.sort_by_key(|t| t.date);
2058
2059 for txn in transactions {
2060 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2061 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2062 let mut accounts: Vec<String> = txn
2063 .postings
2064 .iter()
2065 .map(|p| p.account.to_string())
2066 .collect::<std::collections::HashSet<_>>()
2067 .into_iter()
2068 .collect();
2069 accounts.sort(); let row = vec![
2072 Value::Date(txn.date),
2073 Value::String(txn.flag.to_string()),
2074 txn.payee
2075 .as_ref()
2076 .map_or(Value::Null, |p| Value::String(p.to_string())),
2077 Value::String(txn.narration.to_string()),
2078 Value::StringSet(tags),
2079 Value::StringSet(links),
2080 Value::StringSet(accounts),
2081 ];
2082 table.add_row(row);
2083 }
2084
2085 table
2086 }
2087
2088 fn build_entries_table(&self) -> Table {
2093 let columns = vec![
2094 "id".to_string(),
2095 "type".to_string(),
2096 "filename".to_string(),
2097 "lineno".to_string(),
2098 "date".to_string(),
2099 "flag".to_string(),
2100 "payee".to_string(),
2101 "narration".to_string(),
2102 "tags".to_string(),
2103 "links".to_string(),
2104 "accounts".to_string(),
2105 "_entry_meta".to_string(),
2106 ];
2107 let mut table = Table::new(columns);
2108
2109 if let Some(spanned) = self.spanned_directives {
2111 for (idx, spanned_dir) in spanned.iter().enumerate() {
2112 let directive = &spanned_dir.value;
2113 let source_loc = self.get_source_location(idx);
2114 let row = self.directive_to_entry_row(idx, directive, source_loc);
2115 table.add_row(row);
2116 }
2117 } else {
2118 for (idx, directive) in self.directives.iter().enumerate() {
2119 let row = self.directive_to_entry_row(idx, directive, None);
2120 table.add_row(row);
2121 }
2122 }
2123
2124 table
2125 }
2126
2127 fn directive_to_entry_row(
2129 &self,
2130 idx: usize,
2131 directive: &Directive,
2132 source_loc: Option<&SourceLocation>,
2133 ) -> Vec<Value> {
2134 let type_name = match directive {
2135 Directive::Transaction(_) => "transaction",
2136 Directive::Balance(_) => "balance",
2137 Directive::Open(_) => "open",
2138 Directive::Close(_) => "close",
2139 Directive::Commodity(_) => "commodity",
2140 Directive::Pad(_) => "pad",
2141 Directive::Event(_) => "event",
2142 Directive::Query(_) => "query",
2143 Directive::Note(_) => "note",
2144 Directive::Document(_) => "document",
2145 Directive::Price(_) => "price",
2146 Directive::Custom(_) => "custom",
2147 };
2148
2149 let date = match directive {
2150 Directive::Transaction(t) => Value::Date(t.date),
2151 Directive::Balance(b) => Value::Date(b.date),
2152 Directive::Open(o) => Value::Date(o.date),
2153 Directive::Close(c) => Value::Date(c.date),
2154 Directive::Commodity(c) => Value::Date(c.date),
2155 Directive::Pad(p) => Value::Date(p.date),
2156 Directive::Event(e) => Value::Date(e.date),
2157 Directive::Query(q) => Value::Date(q.date),
2158 Directive::Note(n) => Value::Date(n.date),
2159 Directive::Document(d) => Value::Date(d.date),
2160 Directive::Price(p) => Value::Date(p.date),
2161 Directive::Custom(c) => Value::Date(c.date),
2162 };
2163
2164 let (flag, payee, narration, tags, links, accounts) =
2165 if let Directive::Transaction(txn) = directive {
2166 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2167 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2168 let mut accounts: Vec<String> = txn
2169 .postings
2170 .iter()
2171 .map(|p| p.account.to_string())
2172 .collect::<std::collections::HashSet<_>>()
2173 .into_iter()
2174 .collect();
2175 accounts.sort(); (
2177 Value::String(txn.flag.to_string()),
2178 txn.payee
2179 .as_ref()
2180 .map_or(Value::Null, |p| Value::String(p.to_string())),
2181 Value::String(txn.narration.to_string()),
2182 Value::StringSet(tags),
2183 Value::StringSet(links),
2184 Value::StringSet(accounts),
2185 )
2186 } else {
2187 (
2188 Value::Null,
2189 Value::Null,
2190 Value::Null,
2191 Value::StringSet(vec![]),
2192 Value::StringSet(vec![]),
2193 Value::StringSet(vec![]),
2194 )
2195 };
2196
2197 let filename = source_loc.map_or(Value::Null, |loc| Value::String(loc.filename.clone()));
2198 let lineno = source_loc.map_or(Value::Null, |loc| Value::Integer(loc.lineno as i64));
2199
2200 vec![
2201 Value::Integer(idx as i64), Value::String(type_name.to_string()),
2203 filename,
2204 lineno,
2205 date,
2206 flag,
2207 payee,
2208 narration,
2209 tags,
2210 links,
2211 accounts,
2212 Self::metadata_to_value(directive.meta()),
2214 ]
2215 }
2216
2217 fn build_postings_table(&self) -> Table {
2221 let columns = vec![
2222 "type".to_string(),
2224 "id".to_string(),
2225 "date".to_string(),
2226 "year".to_string(),
2227 "month".to_string(),
2228 "day".to_string(),
2229 "filename".to_string(),
2230 "lineno".to_string(),
2231 "location".to_string(),
2232 "flag".to_string(),
2234 "payee".to_string(),
2235 "narration".to_string(),
2236 "description".to_string(),
2237 "tags".to_string(),
2238 "links".to_string(),
2239 "posting_flag".to_string(),
2241 "account".to_string(),
2242 "other_accounts".to_string(),
2243 "number".to_string(),
2244 "currency".to_string(),
2245 "cost_number".to_string(),
2246 "cost_currency".to_string(),
2247 "cost_date".to_string(),
2248 "cost_label".to_string(),
2249 "position".to_string(),
2250 "price".to_string(),
2251 "weight".to_string(),
2252 "balance".to_string(),
2253 "meta".to_string(),
2255 "accounts".to_string(),
2256 "_entry_meta".to_string(),
2258 "_posting_meta".to_string(),
2259 ];
2260 let mut table = Table::new(columns);
2261
2262 let mut running_balances: FxHashMap<InternedStr, Inventory> = FxHashMap::default();
2264
2265 let mut transactions: Vec<(usize, &rustledger_core::Transaction)> =
2267 if let Some(spanned) = self.spanned_directives {
2268 spanned
2269 .iter()
2270 .enumerate()
2271 .filter_map(|(idx, s)| {
2272 if let Directive::Transaction(txn) = &s.value {
2273 Some((idx, txn))
2274 } else {
2275 None
2276 }
2277 })
2278 .collect()
2279 } else {
2280 self.directives
2281 .iter()
2282 .enumerate()
2283 .filter_map(|(idx, d)| {
2284 if let Directive::Transaction(txn) = d {
2285 Some((idx, txn))
2286 } else {
2287 None
2288 }
2289 })
2290 .collect()
2291 };
2292 transactions.sort_by_key(|(_, t)| t.date);
2293
2294 for (dir_idx, txn) in &transactions {
2295 let source_loc = self.get_source_location(*dir_idx);
2297 let filename =
2298 source_loc.map_or(Value::Null, |loc| Value::String(loc.filename.clone()));
2299 let lineno = source_loc.map_or(Value::Null, |loc| Value::Integer(loc.lineno as i64));
2300 let location = source_loc.map_or(Value::Null, |loc| {
2301 Value::String(format!("{}:{}", loc.filename, loc.lineno))
2302 });
2303
2304 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2305 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2306
2307 let mut all_accounts: Vec<String> = txn
2308 .postings
2309 .iter()
2310 .map(|p| p.account.to_string())
2311 .collect::<std::collections::HashSet<_>>()
2312 .into_iter()
2313 .collect();
2314 all_accounts.sort();
2315
2316 let description = match &txn.payee {
2317 Some(payee) => format!("{} | {}", payee, txn.narration),
2318 None => txn.narration.to_string(),
2319 };
2320
2321 let year = Value::Integer(i64::from(txn.date.year()));
2322 let month = Value::Integer(i64::from(txn.date.month()));
2323 let day = Value::Integer(i64::from(txn.date.day()));
2324
2325 for posting in &txn.postings {
2326 if let Some(units) = posting.amount() {
2328 let balance = running_balances.entry(posting.account.clone()).or_default();
2329 let pos = if let Some(cost_spec) = &posting.cost {
2330 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
2331 Position::with_cost(units.clone(), cost)
2332 } else {
2333 Position::simple(units.clone())
2334 }
2335 } else {
2336 Position::simple(units.clone())
2337 };
2338 balance.add(pos);
2339 }
2340
2341 let (number, currency) = posting.amount().map_or((Value::Null, Value::Null), |a| {
2343 (
2344 Value::Number(a.number),
2345 Value::String(a.currency.to_string()),
2346 )
2347 });
2348
2349 let (cost_number, cost_currency, cost_date, cost_label) = if let Some(cost_spec) =
2350 &posting.cost
2351 {
2352 let units = posting.amount();
2353 if let Some(cost) = units.and_then(|u| cost_spec.resolve(u.number, txn.date)) {
2354 (
2355 Value::Number(cost.number),
2356 Value::String(cost.currency.to_string()),
2357 cost.date.map_or(Value::Null, Value::Date),
2358 cost.label
2359 .as_ref()
2360 .map_or(Value::Null, |l| Value::String(l.clone())),
2361 )
2362 } else {
2363 (Value::Null, Value::Null, Value::Null, Value::Null)
2364 }
2365 } else {
2366 (Value::Null, Value::Null, Value::Null, Value::Null)
2367 };
2368
2369 let position_val = if let Some(units) = posting.amount() {
2370 if let Some(cost_spec) = &posting.cost
2371 && let Some(cost) = cost_spec.resolve(units.number, txn.date)
2372 {
2373 Value::Position(Box::new(Position::with_cost(units.clone(), cost)))
2374 } else {
2375 Value::Position(Box::new(Position::simple(units.clone())))
2376 }
2377 } else {
2378 Value::Null
2379 };
2380
2381 let price_val = posting
2382 .price
2383 .as_ref()
2384 .and_then(|p| p.amount())
2385 .map_or(Value::Null, |a| Value::Amount(a.clone()));
2386
2387 let weight_val = if let Some(units) = posting.amount() {
2393 if let Some(cost_spec) = &posting.cost {
2394 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
2395 Value::Amount(Amount::new(units.number * cost.number, cost.currency))
2396 } else {
2397 Value::Amount(units.clone())
2398 }
2399 } else if let Some(price_ann) = &posting.price {
2400 if let Some(price_amt) = price_ann.amount() {
2401 if price_ann.is_unit() {
2402 Value::Amount(Amount::new(
2404 units.number * price_amt.number,
2405 price_amt.currency.clone(),
2406 ))
2407 } else {
2408 Value::Amount(price_amt.clone())
2410 }
2411 } else {
2412 Value::Amount(units.clone())
2413 }
2414 } else {
2415 Value::Amount(units.clone())
2416 }
2417 } else {
2418 Value::Null
2419 };
2420
2421 let balance_val = running_balances
2422 .get(&posting.account)
2423 .map_or(Value::Null, |inv| Value::Inventory(Box::new(inv.clone())));
2424
2425 let other_accounts: Vec<String> = all_accounts
2427 .iter()
2428 .filter(|a| a.as_str() != posting.account.as_ref())
2429 .cloned()
2430 .collect();
2431
2432 let posting_flag = posting
2433 .flag
2434 .map_or(Value::Null, |f| Value::String(f.to_string()));
2435
2436 let row = vec![
2437 Value::String("transaction".to_string()),
2439 Value::Integer(*dir_idx as i64),
2440 Value::Date(txn.date),
2441 year.clone(),
2442 month.clone(),
2443 day.clone(),
2444 filename.clone(),
2445 lineno.clone(),
2446 location.clone(),
2447 Value::String(txn.flag.to_string()),
2449 txn.payee
2450 .as_ref()
2451 .map_or(Value::Null, |p| Value::String(p.to_string())),
2452 Value::String(txn.narration.to_string()),
2453 Value::String(description.clone()),
2454 Value::StringSet(tags.clone()),
2455 Value::StringSet(links.clone()),
2456 posting_flag,
2458 Value::String(posting.account.to_string()),
2459 Value::StringSet(other_accounts),
2460 number,
2461 currency,
2462 cost_number,
2463 cost_currency,
2464 cost_date,
2465 cost_label,
2466 position_val,
2467 price_val,
2468 weight_val,
2469 balance_val,
2470 Value::Metadata(Box::new(posting.meta.clone())),
2472 Value::StringSet(all_accounts.clone()),
2473 Self::metadata_to_value(&txn.meta),
2475 Self::metadata_to_value(&posting.meta),
2476 ];
2477 table.add_row(row);
2478 }
2479 }
2480
2481 table
2482 }
2483}
2484#[cfg(test)]
2485mod tests {
2486 use super::types::{hash_row, hash_single_value};
2487 use super::*;
2488 use crate::parse;
2489 use rust_decimal_macros::dec;
2490 use rustledger_core::Posting;
2491
2492 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
2493 rustledger_core::naive_date(year, month, day).unwrap()
2494 }
2495
2496 fn sample_directives() -> Vec<Directive> {
2497 vec![
2498 Directive::Transaction(
2499 Transaction::new(date(2024, 1, 15), "Coffee")
2500 .with_flag('*')
2501 .with_payee("Coffee Shop")
2502 .with_posting(Posting::new(
2503 "Expenses:Food:Coffee",
2504 Amount::new(dec!(5.00), "USD"),
2505 ))
2506 .with_posting(Posting::new(
2507 "Assets:Bank:Checking",
2508 Amount::new(dec!(-5.00), "USD"),
2509 )),
2510 ),
2511 Directive::Transaction(
2512 Transaction::new(date(2024, 1, 16), "Groceries")
2513 .with_flag('*')
2514 .with_payee("Supermarket")
2515 .with_posting(Posting::new(
2516 "Expenses:Food:Groceries",
2517 Amount::new(dec!(50.00), "USD"),
2518 ))
2519 .with_posting(Posting::new(
2520 "Assets:Bank:Checking",
2521 Amount::new(dec!(-50.00), "USD"),
2522 )),
2523 ),
2524 ]
2525 }
2526
2527 #[test]
2528 fn test_simple_select() {
2529 let directives = sample_directives();
2530 let mut executor = Executor::new(&directives);
2531
2532 let query = parse("SELECT date, account").unwrap();
2533 let result = executor.execute(&query).unwrap();
2534
2535 assert_eq!(result.columns, vec!["date", "account"]);
2536 assert_eq!(result.len(), 4); }
2538
2539 #[test]
2540 fn test_where_clause() {
2541 let directives = sample_directives();
2542 let mut executor = Executor::new(&directives);
2543
2544 let query = parse("SELECT account WHERE account ~ \"Expenses:\"").unwrap();
2545 let result = executor.execute(&query).unwrap();
2546
2547 assert_eq!(result.len(), 2); }
2549
2550 #[test]
2551 fn test_balances() {
2552 let directives = sample_directives();
2553 let mut executor = Executor::new(&directives);
2554
2555 let query = parse("BALANCES").unwrap();
2556 let result = executor.execute(&query).unwrap();
2557
2558 assert_eq!(result.columns, vec!["account", "balance"]);
2559 assert!(result.len() >= 3); }
2561
2562 #[test]
2563 fn test_account_functions() {
2564 let directives = sample_directives();
2565 let mut executor = Executor::new(&directives);
2566
2567 let query = parse("SELECT DISTINCT LEAF(account) WHERE account ~ \"Expenses:\"").unwrap();
2569 let result = executor.execute(&query).unwrap();
2570 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT ROOT(account)").unwrap();
2574 let result = executor.execute(&query).unwrap();
2575 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT PARENT(account) WHERE account ~ \"Expenses:\"").unwrap();
2579 let result = executor.execute(&query).unwrap();
2580 assert!(!result.is_empty()); }
2582
2583 #[test]
2584 fn test_min_max_aggregate() {
2585 let directives = sample_directives();
2586 let mut executor = Executor::new(&directives);
2587
2588 let query = parse("SELECT MIN(date)").unwrap();
2590 let result = executor.execute(&query).unwrap();
2591 assert_eq!(result.len(), 1);
2592 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
2593
2594 let query = parse("SELECT MAX(date)").unwrap();
2596 let result = executor.execute(&query).unwrap();
2597 assert_eq!(result.len(), 1);
2598 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
2599 }
2600
2601 #[test]
2602 fn test_order_by() {
2603 let directives = sample_directives();
2604 let mut executor = Executor::new(&directives);
2605
2606 let query = parse("SELECT date, account ORDER BY date DESC").unwrap();
2607 let result = executor.execute(&query).unwrap();
2608
2609 assert_eq!(result.len(), 4);
2611 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
2613 }
2614
2615 #[test]
2616 fn test_hash_value_all_variants() {
2617 use rustledger_core::{Cost, Inventory, Position};
2618
2619 let values = vec![
2621 Value::String("test".to_string()),
2622 Value::Number(dec!(123.45)),
2623 Value::Integer(42),
2624 Value::Date(date(2024, 1, 15)),
2625 Value::Boolean(true),
2626 Value::Boolean(false),
2627 Value::Amount(Amount::new(dec!(100), "USD")),
2628 Value::Position(Box::new(Position::simple(Amount::new(dec!(10), "AAPL")))),
2629 Value::Position(Box::new(Position::with_cost(
2630 Amount::new(dec!(10), "AAPL"),
2631 Cost::new(dec!(150), "USD"),
2632 ))),
2633 Value::Inventory(Box::new(Inventory::new())),
2634 Value::StringSet(vec!["tag1".to_string(), "tag2".to_string()]),
2635 Value::Null,
2636 ];
2637
2638 for value in &values {
2640 let hash = hash_single_value(value);
2641 assert!(hash != 0 || matches!(value, Value::Null));
2642 }
2643
2644 let hash1 = hash_single_value(&Value::String("a".to_string()));
2646 let hash2 = hash_single_value(&Value::String("b".to_string()));
2647 assert_ne!(hash1, hash2);
2648
2649 let hash3 = hash_single_value(&Value::Integer(42));
2651 let hash4 = hash_single_value(&Value::Integer(42));
2652 assert_eq!(hash3, hash4);
2653 }
2654
2655 #[test]
2656 fn test_hash_row_distinct() {
2657 let row1 = vec![Value::String("a".to_string()), Value::Integer(1)];
2659 let row2 = vec![Value::String("a".to_string()), Value::Integer(1)];
2660 let row3 = vec![Value::String("b".to_string()), Value::Integer(1)];
2661
2662 assert_eq!(hash_row(&row1), hash_row(&row2));
2663 assert_ne!(hash_row(&row1), hash_row(&row3));
2664 }
2665
2666 #[test]
2667 fn test_string_set_hash_order_independent() {
2668 let set1 = Value::StringSet(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
2670 let set2 = Value::StringSet(vec!["c".to_string(), "a".to_string(), "b".to_string()]);
2671 let set3 = Value::StringSet(vec!["b".to_string(), "c".to_string(), "a".to_string()]);
2672
2673 let hash1 = hash_single_value(&set1);
2674 let hash2 = hash_single_value(&set2);
2675 let hash3 = hash_single_value(&set3);
2676
2677 assert_eq!(hash1, hash2);
2678 assert_eq!(hash2, hash3);
2679 }
2680
2681 #[test]
2682 fn test_inventory_hash_includes_cost() {
2683 use rustledger_core::{Cost, Inventory, Position};
2684
2685 let mut inv1 = Inventory::new();
2687 inv1.add(Position::with_cost(
2688 Amount::new(dec!(10), "AAPL"),
2689 Cost::new(dec!(100), "USD"),
2690 ));
2691
2692 let mut inv2 = Inventory::new();
2693 inv2.add(Position::with_cost(
2694 Amount::new(dec!(10), "AAPL"),
2695 Cost::new(dec!(200), "USD"),
2696 ));
2697
2698 let hash1 = hash_single_value(&Value::Inventory(Box::new(inv1)));
2699 let hash2 = hash_single_value(&Value::Inventory(Box::new(inv2)));
2700
2701 assert_ne!(hash1, hash2);
2702 }
2703
2704 #[test]
2705 fn test_distinct_deduplication() {
2706 let directives = sample_directives();
2707 let mut executor = Executor::new(&directives);
2708
2709 let query = parse("SELECT flag").unwrap();
2711 let result = executor.execute(&query).unwrap();
2712 assert_eq!(result.len(), 4); let query = parse("SELECT DISTINCT flag").unwrap();
2716 let result = executor.execute(&query).unwrap();
2717 assert_eq!(result.len(), 1); }
2719
2720 #[test]
2721 fn test_limit_clause() {
2722 let directives = sample_directives();
2723 let mut executor = Executor::new(&directives);
2724
2725 let query = parse("SELECT date, account LIMIT 2").unwrap();
2727 let result = executor.execute(&query).unwrap();
2728 assert_eq!(result.len(), 2);
2729
2730 let query = parse("SELECT date LIMIT 0").unwrap();
2732 let result = executor.execute(&query).unwrap();
2733 assert_eq!(result.len(), 0);
2734
2735 let query = parse("SELECT date LIMIT 100").unwrap();
2737 let result = executor.execute(&query).unwrap();
2738 assert_eq!(result.len(), 4);
2739 }
2740
2741 #[test]
2742 fn test_group_by_with_count() {
2743 let directives = sample_directives();
2744 let mut executor = Executor::new(&directives);
2745
2746 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
2748 let result = executor.execute(&query).unwrap();
2749
2750 assert_eq!(result.columns.len(), 2);
2751 assert_eq!(result.len(), 2);
2753 }
2754
2755 #[test]
2756 fn test_count_aggregate() {
2757 let directives = sample_directives();
2758 let mut executor = Executor::new(&directives);
2759
2760 let query = parse("SELECT COUNT(account)").unwrap();
2762 let result = executor.execute(&query).unwrap();
2763
2764 assert_eq!(result.len(), 1);
2765 assert_eq!(result.rows[0][0], Value::Integer(4));
2766
2767 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
2769 let result = executor.execute(&query).unwrap();
2770 assert_eq!(result.len(), 2); }
2772
2773 #[test]
2774 fn test_count_wildcard_direct() {
2775 let directives = sample_directives();
2777 let mut executor = Executor::new(&directives);
2778
2779 let query = parse("SELECT count(*)").unwrap();
2781 let result = executor.execute(&query).unwrap();
2782 assert_eq!(result.len(), 1);
2783 assert_eq!(result.rows[0][0], Value::Integer(4)); let query = parse("SELECT account, count(*) GROUP BY account").unwrap();
2788 let result = executor.execute(&query).unwrap();
2789 assert_eq!(result.len(), 3); }
2791
2792 #[test]
2793 fn test_count_wildcard_from_postings_table() {
2794 let directives = sample_directives();
2796 let mut executor = Executor::new(&directives);
2797
2798 let query = parse("SELECT account, count(*) FROM postings GROUP BY account").unwrap();
2800 let result = executor.execute(&query).unwrap();
2801 assert_eq!(result.len(), 3);
2803 }
2804
2805 #[test]
2806 fn test_count_wildcard_from_entries_table() {
2807 let directives = sample_directives();
2809 let mut executor = Executor::new(&directives);
2810
2811 let query = parse("SELECT type, count(*) FROM entries GROUP BY type").unwrap();
2812 let result = executor.execute(&query).unwrap();
2813 assert_eq!(result.len(), 1);
2815 assert_eq!(result.rows[0][0], Value::String("transaction".to_string()));
2816 assert_eq!(result.rows[0][1], Value::Integer(2));
2817 }
2818
2819 #[test]
2820 fn test_count_wildcard_having() {
2821 let directives = sample_directives();
2823 let mut executor = Executor::new(&directives);
2824
2825 let query = parse(
2827 "SELECT account, count(*) AS cnt FROM postings GROUP BY account HAVING count(*) > 0",
2828 )
2829 .unwrap();
2830 let result = executor.execute(&query).unwrap();
2831 assert_eq!(result.len(), 3);
2832
2833 let query = parse(
2835 "SELECT account, count(*) AS cnt FROM postings GROUP BY account HAVING count(*) > 1",
2836 )
2837 .unwrap();
2838 let result = executor.execute(&query).unwrap();
2839 assert_eq!(result.len(), 1);
2840 assert_eq!(
2841 result.rows[0][0],
2842 Value::String("Assets:Bank:Checking".to_string())
2843 );
2844 assert_eq!(result.rows[0][1], Value::Integer(2));
2845 }
2846
2847 #[test]
2848 fn test_journal_query() {
2849 let directives = sample_directives();
2850 let mut executor = Executor::new(&directives);
2851
2852 let query = parse("JOURNAL \"Expenses\"").unwrap();
2854 let result = executor.execute(&query).unwrap();
2855
2856 assert!(result.columns.contains(&"account".to_string()));
2858 assert_eq!(result.len(), 2);
2860 }
2861
2862 #[test]
2863 fn test_print_query() {
2864 let directives = sample_directives();
2865 let mut executor = Executor::new(&directives);
2866
2867 let query = parse("PRINT").unwrap();
2869 let result = executor.execute(&query).unwrap();
2870
2871 assert_eq!(result.columns.len(), 1);
2873 assert_eq!(result.columns[0], "directive");
2874 assert_eq!(result.len(), 2);
2876 }
2877
2878 #[test]
2879 fn test_empty_directives() {
2880 let directives: Vec<Directive> = vec![];
2881 let mut executor = Executor::new(&directives);
2882
2883 let query = parse("SELECT date, account").unwrap();
2885 let result = executor.execute(&query).unwrap();
2886 assert!(result.is_empty());
2887
2888 let query = parse("BALANCES").unwrap();
2890 let result = executor.execute(&query).unwrap();
2891 assert!(result.is_empty());
2892 }
2893
2894 #[test]
2895 fn test_comparison_operators() {
2896 let directives = sample_directives();
2897 let mut executor = Executor::new(&directives);
2898
2899 let query = parse("SELECT date WHERE date < 2024-01-16").unwrap();
2901 let result = executor.execute(&query).unwrap();
2902 assert_eq!(result.len(), 2); let query = parse("SELECT date WHERE year > 2023").unwrap();
2906 let result = executor.execute(&query).unwrap();
2907 assert_eq!(result.len(), 4); let query = parse("SELECT account WHERE day = 15").unwrap();
2911 let result = executor.execute(&query).unwrap();
2912 assert_eq!(result.len(), 2); }
2914
2915 #[test]
2916 fn test_logical_operators() {
2917 let directives = sample_directives();
2918 let mut executor = Executor::new(&directives);
2919
2920 let query = parse("SELECT account WHERE account ~ \"Expenses\" AND day > 14").unwrap();
2922 let result = executor.execute(&query).unwrap();
2923 assert_eq!(result.len(), 2); let query = parse("SELECT account WHERE day = 15 OR day = 16").unwrap();
2927 let result = executor.execute(&query).unwrap();
2928 assert_eq!(result.len(), 4); }
2930
2931 #[test]
2932 fn test_arithmetic_expressions() {
2933 let directives = sample_directives();
2934 let mut executor = Executor::new(&directives);
2935
2936 let query = parse("SELECT -day WHERE day = 15").unwrap();
2938 let result = executor.execute(&query).unwrap();
2939 assert_eq!(result.len(), 2);
2940 for row in &result.rows {
2942 if let Value::Integer(n) = &row[0] {
2943 assert_eq!(*n, -15);
2944 }
2945 }
2946 }
2947
2948 #[test]
2949 fn test_first_last_aggregates() {
2950 let directives = sample_directives();
2951 let mut executor = Executor::new(&directives);
2952
2953 let query = parse("SELECT FIRST(date)").unwrap();
2955 let result = executor.execute(&query).unwrap();
2956 assert_eq!(result.len(), 1);
2957 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
2958
2959 let query = parse("SELECT LAST(date)").unwrap();
2961 let result = executor.execute(&query).unwrap();
2962 assert_eq!(result.len(), 1);
2963 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
2964 }
2965
2966 #[test]
2967 fn test_wildcard_select() {
2968 let directives = sample_directives();
2969 let mut executor = Executor::new(&directives);
2970
2971 let query = parse("SELECT *").unwrap();
2973 let result = executor.execute(&query).unwrap();
2974
2975 assert_eq!(
2977 result.columns,
2978 vec!["date", "flag", "payee", "narration", "account", "position"]
2979 );
2980 assert_eq!(result.len(), 4);
2982 assert_eq!(result.rows[0].len(), 6);
2983 }
2984
2985 #[test]
2986 fn test_wildcard_alias_rejected() {
2987 let directives = sample_directives();
2988 let mut executor = Executor::new(&directives);
2989
2990 let query = parse("SELECT * AS data").unwrap();
2992 let result = executor.execute(&query);
2993
2994 assert!(result.is_err());
2995 let err = result.unwrap_err();
2996 assert!(
2997 err.to_string().contains("Cannot alias wildcard"),
2998 "Expected wildcard alias error, got: {err}"
2999 );
3000 }
3001
3002 #[test]
3003 fn test_query_result_methods() {
3004 let mut result = QueryResult::new(vec!["col1".to_string(), "col2".to_string()]);
3005
3006 assert!(result.is_empty());
3008 assert_eq!(result.len(), 0);
3009
3010 result.add_row(vec![Value::Integer(1), Value::String("a".to_string())]);
3012 assert!(!result.is_empty());
3013 assert_eq!(result.len(), 1);
3014
3015 result.add_row(vec![Value::Integer(2), Value::String("b".to_string())]);
3016 assert_eq!(result.len(), 2);
3017 }
3018
3019 #[test]
3020 fn test_type_cast_functions() {
3021 let directives = sample_directives();
3022 let mut executor = Executor::new(&directives);
3023
3024 let query = parse("SELECT int(5.7)").unwrap();
3026 let result = executor.execute(&query).unwrap();
3027 assert_eq!(result.rows[0][0], Value::Integer(5));
3028
3029 let query = parse("SELECT decimal(42)").unwrap();
3031 let result = executor.execute(&query).unwrap();
3032 assert_eq!(result.rows[0][0], Value::Number(dec!(42)));
3033
3034 let query = parse("SELECT str(123)").unwrap();
3036 let result = executor.execute(&query).unwrap();
3037 assert_eq!(result.rows[0][0], Value::String("123".to_string()));
3038
3039 let query = parse("SELECT bool(1)").unwrap();
3041 let result = executor.execute(&query).unwrap();
3042 assert_eq!(result.rows[0][0], Value::Boolean(true));
3043
3044 let query = parse("SELECT bool(0)").unwrap();
3045 let result = executor.execute(&query).unwrap();
3046 assert_eq!(result.rows[0][0], Value::Boolean(false));
3047 }
3048
3049 #[test]
3051 fn test_type_casting_in_aggregate_context() {
3052 let txn1 = Transaction::new(date(2024, 1, 15), "Item 1")
3053 .with_flag('*')
3054 .with_posting(Posting::new("Expenses:Food", Amount::new(dec!(10), "USD")))
3055 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-10), "USD")));
3056
3057 let txn2 = Transaction::new(date(2024, 1, 16), "Item 2")
3058 .with_flag('*')
3059 .with_posting(Posting::new("Expenses:Food", Amount::new(dec!(20), "USD")))
3060 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-20), "USD")));
3061
3062 let directives = vec![Directive::Transaction(txn1), Directive::Transaction(txn2)];
3063 let mut executor = Executor::new(&directives);
3064
3065 let query =
3068 parse("SELECT account, str(sum(number(units))) GROUP BY account ORDER BY account")
3069 .unwrap();
3070 let result = executor.execute(&query).unwrap();
3071 assert_eq!(result.rows.len(), 2);
3072 assert_eq!(result.rows[0][0], Value::String("Assets:Cash".to_string()));
3074 assert_eq!(result.rows[0][1], Value::String("-30".to_string()));
3075 assert_eq!(
3076 result.rows[1][0],
3077 Value::String("Expenses:Food".to_string())
3078 );
3079 assert_eq!(result.rows[1][1], Value::String("30".to_string()));
3080
3081 let query =
3083 parse("SELECT account, int(sum(number(units))) GROUP BY account ORDER BY account")
3084 .unwrap();
3085 let result = executor.execute(&query).unwrap();
3086 assert_eq!(result.rows[0][1], Value::Integer(-30));
3087 assert_eq!(result.rows[1][1], Value::Integer(30));
3088
3089 let query =
3091 parse("SELECT account, decimal(count(*)) GROUP BY account ORDER BY account").unwrap();
3092 let result = executor.execute(&query).unwrap();
3093 assert_eq!(result.rows[0][1], Value::Number(dec!(2))); assert_eq!(result.rows[1][1], Value::Number(dec!(2)));
3095
3096 let query =
3098 parse("SELECT account, bool(count(*)) GROUP BY account ORDER BY account").unwrap();
3099 let result = executor.execute(&query).unwrap();
3100 assert_eq!(result.rows[0][1], Value::Boolean(true));
3101 assert_eq!(result.rows[1][1], Value::Boolean(true));
3102 }
3103
3104 #[test]
3106 fn test_int_truncation() {
3107 let directives = sample_directives();
3108 let mut executor = Executor::new(&directives);
3109
3110 let query = parse("SELECT int(5.7)").unwrap();
3112 let result = executor.execute(&query).unwrap();
3113 assert_eq!(result.rows[0][0], Value::Integer(5));
3114
3115 let query = parse("SELECT int(-5.7)").unwrap();
3116 let result = executor.execute(&query).unwrap();
3117 assert_eq!(result.rows[0][0], Value::Integer(-5));
3118
3119 let query = parse("SELECT int(0.999)").unwrap();
3120 let result = executor.execute(&query).unwrap();
3121 assert_eq!(result.rows[0][0], Value::Integer(0));
3122 }
3123
3124 #[test]
3126 fn test_type_casting_errors() {
3127 let directives = sample_directives();
3128 let mut executor = Executor::new(&directives);
3129
3130 let query = parse("SELECT int('not-a-number')").unwrap();
3132 let result = executor.execute(&query);
3133 assert!(result.is_err());
3134 assert!(
3135 result
3136 .unwrap_err()
3137 .to_string()
3138 .contains("cannot parse 'not-a-number'")
3139 );
3140
3141 let query = parse("SELECT decimal('invalid')").unwrap();
3143 let result = executor.execute(&query);
3144 assert!(result.is_err());
3145 assert!(result.unwrap_err().to_string().contains("cannot parse"));
3146
3147 let query = parse("SELECT bool('maybe')").unwrap();
3149 let result = executor.execute(&query);
3150 assert!(result.is_err());
3151 assert!(result.unwrap_err().to_string().contains("cannot parse"));
3152 }
3153
3154 #[test]
3155 fn test_meta_functions() {
3156 let mut txn_meta: Metadata = Metadata::default();
3158 txn_meta.insert(
3159 "source".to_string(),
3160 MetaValue::String("bank_import".to_string()),
3161 );
3162
3163 let mut posting_meta: Metadata = Metadata::default();
3164 posting_meta.insert(
3165 "category".to_string(),
3166 MetaValue::String("food".to_string()),
3167 );
3168
3169 let txn = Transaction {
3170 date: date(2024, 1, 15),
3171 flag: '*',
3172 payee: Some("Coffee Shop".into()),
3173 narration: "Coffee".into(),
3174 tags: vec![],
3175 links: vec![],
3176 meta: txn_meta,
3177 postings: vec![
3178 Posting {
3179 account: "Expenses:Food".into(),
3180 units: Some(rustledger_core::IncompleteAmount::Complete(Amount::new(
3181 dec!(5),
3182 "USD",
3183 ))),
3184 cost: None,
3185 price: None,
3186 flag: None,
3187 meta: posting_meta,
3188 comments: Vec::new(),
3189 trailing_comments: Vec::new(),
3190 },
3191 Posting::new("Assets:Cash", Amount::new(dec!(-5), "USD")),
3192 ],
3193 trailing_comments: Vec::new(),
3194 };
3195
3196 let directives = vec![Directive::Transaction(txn)];
3197 let mut executor = Executor::new(&directives);
3198
3199 let query = parse("SELECT meta('category') WHERE account ~ 'Expenses'").unwrap();
3201 let result = executor.execute(&query).unwrap();
3202 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
3203
3204 let query = parse("SELECT entry_meta('source') WHERE account ~ 'Expenses'").unwrap();
3206 let result = executor.execute(&query).unwrap();
3207 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
3208
3209 let query = parse("SELECT any_meta('source') WHERE account ~ 'Expenses'").unwrap();
3211 let result = executor.execute(&query).unwrap();
3212 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
3213
3214 let query = parse("SELECT any_meta('category') WHERE account ~ 'Expenses'").unwrap();
3216 let result = executor.execute(&query).unwrap();
3217 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
3218
3219 let query = parse("SELECT meta('nonexistent') WHERE account ~ 'Expenses'").unwrap();
3221 let result = executor.execute(&query).unwrap();
3222 assert_eq!(result.rows[0][0], Value::Null);
3223 }
3224
3225 #[test]
3226 fn test_convert_function() {
3227 let price = rustledger_core::Price {
3229 date: date(2024, 1, 1),
3230 currency: "EUR".into(),
3231 amount: Amount::new(dec!(1.10), "USD"),
3232 meta: Metadata::default(),
3233 };
3234
3235 let txn = Transaction::new(date(2024, 1, 15), "Test")
3236 .with_flag('*')
3237 .with_posting(Posting::new("Assets:Euro", Amount::new(dec!(100), "EUR")))
3238 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-110), "USD")));
3239
3240 let directives = vec![Directive::Price(price), Directive::Transaction(txn)];
3241 let mut executor = Executor::new(&directives);
3242
3243 let query = parse("SELECT convert(position, 'USD') WHERE account ~ 'Euro'").unwrap();
3245 let result = executor.execute(&query).unwrap();
3246 match &result.rows[0][0] {
3248 Value::Amount(a) => {
3249 assert_eq!(a.number, dec!(110));
3250 assert_eq!(a.currency.as_ref(), "USD");
3251 }
3252 _ => panic!("Expected Amount, got {:?}", result.rows[0][0]),
3253 }
3254 }
3255
3256 #[test]
3257 fn test_date_functions() {
3258 let directives = sample_directives();
3259 let mut executor = Executor::new(&directives);
3260
3261 let query = parse("SELECT date('2024-06-15')").unwrap();
3263 let result = executor.execute(&query).unwrap();
3264 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3265
3266 let query = parse("SELECT date(2024, 6, 15)").unwrap();
3268 let result = executor.execute(&query).unwrap();
3269 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3270
3271 let query = parse("SELECT date_diff(date('2024-01-20'), date('2024-01-15'))").unwrap();
3273 let result = executor.execute(&query).unwrap();
3274 assert_eq!(result.rows[0][0], Value::Integer(5));
3275
3276 let query = parse("SELECT date_add(date('2024-01-15'), 10)").unwrap();
3278 let result = executor.execute(&query).unwrap();
3279 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 25)));
3280
3281 let query = parse("SELECT date_trunc('year', date('2024-06-15'))").unwrap();
3283 let result = executor.execute(&query).unwrap();
3284 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1)));
3285
3286 let query = parse("SELECT date_trunc('month', date('2024-06-15'))").unwrap();
3288 let result = executor.execute(&query).unwrap();
3289 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1)));
3290
3291 let query = parse("SELECT date_part('month', date('2024-06-15'))").unwrap();
3293 let result = executor.execute(&query).unwrap();
3294 assert_eq!(result.rows[0][0], Value::Integer(6));
3295
3296 let query = parse("SELECT parse_date('15/06/2024', '%d/%m/%Y')").unwrap();
3298 let result = executor.execute(&query).unwrap();
3299 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3300
3301 let query =
3303 parse("SELECT date_bin('7 days', date('2024-01-15'), date('2024-01-01'))").unwrap();
3304 let result = executor.execute(&query).unwrap();
3305 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
3309 parse("SELECT date_bin('1 week', date('2024-01-20'), date('2024-01-01'))").unwrap();
3310 let result = executor.execute(&query).unwrap();
3311 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
3315 parse("SELECT date_bin('1 month', date('2024-06-15'), date('2024-01-01'))").unwrap();
3316 let result = executor.execute(&query).unwrap();
3317 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1))); let query =
3321 parse("SELECT date_bin('1 year', date('2024-06-15'), date('2020-01-01'))").unwrap();
3322 let result = executor.execute(&query).unwrap();
3323 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1))); }
3325
3326 #[test]
3327 fn test_string_functions_extended() {
3328 let directives = sample_directives();
3329 let mut executor = Executor::new(&directives);
3330
3331 let query = parse("SELECT grep('Ex[a-z]+', 'Hello Expenses World')").unwrap();
3333 let result = executor.execute(&query).unwrap();
3334 assert_eq!(result.rows[0][0], Value::String("Expenses".to_string()));
3335
3336 let query = parse("SELECT grep('xyz', 'Hello World')").unwrap();
3338 let result = executor.execute(&query).unwrap();
3339 assert_eq!(result.rows[0][0], Value::Null);
3340
3341 let query = parse("SELECT grepn('([0-9]+)-([0-9]+)', '2024-01', 1)").unwrap();
3343 let result = executor.execute(&query).unwrap();
3344 assert_eq!(result.rows[0][0], Value::String("2024".to_string()));
3345
3346 let query = parse("SELECT subst('-', '/', '2024-01-15')").unwrap();
3348 let result = executor.execute(&query).unwrap();
3349 assert_eq!(result.rows[0][0], Value::String("2024/01/15".to_string()));
3350
3351 let query = parse("SELECT splitcomp('a:b:c', ':', 1)").unwrap();
3353 let result = executor.execute(&query).unwrap();
3354 assert_eq!(result.rows[0][0], Value::String("b".to_string()));
3355
3356 let query = parse("SELECT joinstr('hello', 'world')").unwrap();
3358 let result = executor.execute(&query).unwrap();
3359 assert_eq!(result.rows[0][0], Value::String("hello, world".to_string()));
3360
3361 let query = parse("SELECT maxwidth('hello', 10)").unwrap();
3363 let result = executor.execute(&query).unwrap();
3364 assert_eq!(result.rows[0][0], Value::String("hello".to_string()));
3365
3366 let query = parse("SELECT maxwidth('hello world', 8)").unwrap();
3368 let result = executor.execute(&query).unwrap();
3369 assert_eq!(result.rows[0][0], Value::String("hello...".to_string()));
3370 }
3371
3372 #[test]
3373 fn test_inventory_functions() {
3374 let directives = sample_directives();
3375 let mut executor = Executor::new(&directives);
3376
3377 let query = parse("SELECT empty(sum(position)) WHERE account ~ 'Assets'").unwrap();
3380 let result = executor.execute(&query).unwrap();
3381 assert!(matches!(result.rows[0][0], Value::Boolean(_)));
3383
3384 let query = parse("SELECT possign(100, 'Assets:Bank')").unwrap();
3389 let result = executor.execute(&query).unwrap();
3390 assert_eq!(
3391 result.rows[0][0],
3392 Value::Number(rust_decimal::Decimal::from(100))
3393 );
3394
3395 let query = parse("SELECT possign(100, 'Income:Salary')").unwrap();
3397 let result = executor.execute(&query).unwrap();
3398 assert_eq!(
3399 result.rows[0][0],
3400 Value::Number(rust_decimal::Decimal::from(-100))
3401 );
3402
3403 let query = parse("SELECT possign(50, 'Expenses:Food')").unwrap();
3405 let result = executor.execute(&query).unwrap();
3406 assert_eq!(
3407 result.rows[0][0],
3408 Value::Number(rust_decimal::Decimal::from(50))
3409 );
3410
3411 let query = parse("SELECT possign(200, 'Liabilities:CreditCard')").unwrap();
3413 let result = executor.execute(&query).unwrap();
3414 assert_eq!(
3415 result.rows[0][0],
3416 Value::Number(rust_decimal::Decimal::from(-200))
3417 );
3418
3419 let query = parse("SELECT possign(300, 'Equity:OpeningBalances')").unwrap();
3421 let result = executor.execute(&query).unwrap();
3422 assert_eq!(
3423 result.rows[0][0],
3424 Value::Number(rust_decimal::Decimal::from(-300))
3425 );
3426 }
3427
3428 #[test]
3429 fn test_account_meta_functions() {
3430 use rustledger_core::{Close, Metadata, Open};
3431
3432 let mut open_meta = Metadata::default();
3434 open_meta.insert(
3435 "category".to_string(),
3436 MetaValue::String("checking".to_string()),
3437 );
3438
3439 let directives = vec![
3440 Directive::Open(Open {
3441 date: date(2020, 1, 1),
3442 account: "Assets:Bank:Checking".into(),
3443 currencies: vec![],
3444 booking: None,
3445 meta: open_meta,
3446 }),
3447 Directive::Open(Open::new(date(2020, 2, 15), "Expenses:Food")),
3448 Directive::Close(Close::new(date(2024, 12, 31), "Assets:Bank:Checking")),
3449 Directive::Transaction(
3451 Transaction::new(date(2024, 1, 15), "Coffee")
3452 .with_posting(Posting::new(
3453 "Expenses:Food",
3454 Amount::new(dec!(5.00), "USD"),
3455 ))
3456 .with_posting(Posting::new(
3457 "Assets:Bank:Checking",
3458 Amount::new(dec!(-5.00), "USD"),
3459 )),
3460 ),
3461 ];
3462
3463 let mut executor = Executor::new(&directives);
3464
3465 let query = parse("SELECT open_date('Assets:Bank:Checking')").unwrap();
3467 let result = executor.execute(&query).unwrap();
3468 assert_eq!(result.rows[0][0], Value::Date(date(2020, 1, 1)));
3469
3470 let query = parse("SELECT close_date('Assets:Bank:Checking')").unwrap();
3472 let result = executor.execute(&query).unwrap();
3473 assert_eq!(result.rows[0][0], Value::Date(date(2024, 12, 31)));
3474
3475 let query = parse("SELECT close_date('Expenses:Food')").unwrap();
3477 let result = executor.execute(&query).unwrap();
3478 assert_eq!(result.rows[0][0], Value::Null);
3479
3480 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'category')").unwrap();
3482 let result = executor.execute(&query).unwrap();
3483 assert_eq!(result.rows[0][0], Value::String("checking".to_string()));
3484
3485 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'nonexistent')").unwrap();
3487 let result = executor.execute(&query).unwrap();
3488 assert_eq!(result.rows[0][0], Value::Null);
3489
3490 let query = parse("SELECT open_date('NonExistent:Account')").unwrap();
3492 let result = executor.execute(&query).unwrap();
3493 assert_eq!(result.rows[0][0], Value::Null);
3494 }
3495
3496 #[test]
3497 fn test_source_location_columns_return_null_without_sources() {
3498 let directives = vec![Directive::Transaction(Transaction {
3501 date: rustledger_core::naive_date(2024, 1, 15).unwrap(),
3502 flag: '*',
3503 payee: Some("Test".into()),
3504 narration: "Test transaction".into(),
3505 tags: vec![],
3506 links: vec![],
3507 meta: Metadata::default(),
3508 postings: vec![
3509 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
3510 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
3511 ],
3512 trailing_comments: Vec::new(),
3513 })];
3514
3515 let mut executor = Executor::new(&directives);
3516
3517 let query = parse("SELECT filename").unwrap();
3519 let result = executor.execute(&query).unwrap();
3520 assert_eq!(result.rows[0][0], Value::Null);
3521
3522 let query = parse("SELECT lineno").unwrap();
3524 let result = executor.execute(&query).unwrap();
3525 assert_eq!(result.rows[0][0], Value::Null);
3526
3527 let query = parse("SELECT location").unwrap();
3529 let result = executor.execute(&query).unwrap();
3530 assert_eq!(result.rows[0][0], Value::Null);
3531 }
3532
3533 #[test]
3534 fn test_source_location_columns_with_sources() {
3535 use rustledger_loader::SourceMap;
3536 use rustledger_parser::Spanned;
3537 use std::sync::Arc;
3538
3539 let mut source_map = SourceMap::new();
3541 let source: Arc<str> =
3542 "2024-01-15 * \"Test\"\n Assets:Bank 100 USD\n Expenses:Food".into();
3543 let file_id = source_map.add_file("test.beancount".into(), source);
3544
3545 let txn = Transaction {
3547 date: rustledger_core::naive_date(2024, 1, 15).unwrap(),
3548 flag: '*',
3549 payee: Some("Test".into()),
3550 narration: "Test transaction".into(),
3551 tags: vec![],
3552 links: vec![],
3553 meta: Metadata::default(),
3554 postings: vec![
3555 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
3556 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
3557 ],
3558 trailing_comments: Vec::new(),
3559 };
3560
3561 let spanned_directives = vec![Spanned {
3562 value: Directive::Transaction(txn),
3563 span: rustledger_parser::Span { start: 0, end: 50 },
3564 file_id: file_id as u16,
3565 }];
3566
3567 let mut executor = Executor::new_with_sources(&spanned_directives, &source_map);
3568
3569 let query = parse("SELECT filename").unwrap();
3571 let result = executor.execute(&query).unwrap();
3572 assert_eq!(
3573 result.rows[0][0],
3574 Value::String("test.beancount".to_string())
3575 );
3576
3577 let query = parse("SELECT lineno").unwrap();
3579 let result = executor.execute(&query).unwrap();
3580 assert_eq!(result.rows[0][0], Value::Integer(1));
3581
3582 let query = parse("SELECT location").unwrap();
3584 let result = executor.execute(&query).unwrap();
3585 assert_eq!(
3586 result.rows[0][0],
3587 Value::String("test.beancount:1".to_string())
3588 );
3589 }
3590
3591 #[test]
3592 fn test_interval_function() {
3593 let directives = sample_directives();
3594 let mut executor = Executor::new(&directives);
3595
3596 let query = parse("SELECT interval('month')").unwrap();
3598 let result = executor.execute(&query).unwrap();
3599 assert_eq!(
3600 result.rows[0][0],
3601 Value::Interval(Interval::new(1, IntervalUnit::Month))
3602 );
3603
3604 let query = parse("SELECT interval(3, 'day')").unwrap();
3606 let result = executor.execute(&query).unwrap();
3607 assert_eq!(
3608 result.rows[0][0],
3609 Value::Interval(Interval::new(3, IntervalUnit::Day))
3610 );
3611
3612 let query = parse("SELECT interval(-2, 'week')").unwrap();
3614 let result = executor.execute(&query).unwrap();
3615 assert_eq!(
3616 result.rows[0][0],
3617 Value::Interval(Interval::new(-2, IntervalUnit::Week))
3618 );
3619 }
3620
3621 #[test]
3622 fn test_date_add_with_interval() {
3623 let directives = sample_directives();
3624 let mut executor = Executor::new(&directives);
3625
3626 let query = parse("SELECT date_add(date(2024, 1, 15), interval(1, 'month'))").unwrap();
3628 let result = executor.execute(&query).unwrap();
3629 assert_eq!(
3630 result.rows[0][0],
3631 Value::Date(rustledger_core::naive_date(2024, 2, 15).unwrap())
3632 );
3633
3634 let query = parse("SELECT date(2024, 1, 15) + interval(1, 'year')").unwrap();
3636 let result = executor.execute(&query).unwrap();
3637 assert_eq!(
3638 result.rows[0][0],
3639 Value::Date(rustledger_core::naive_date(2025, 1, 15).unwrap())
3640 );
3641
3642 let query = parse("SELECT date(2024, 3, 15) - interval(2, 'month')").unwrap();
3644 let result = executor.execute(&query).unwrap();
3645 assert_eq!(
3646 result.rows[0][0],
3647 Value::Date(rustledger_core::naive_date(2024, 1, 15).unwrap())
3648 );
3649 }
3650}