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, NaiveDate, Position};
21#[cfg(test)]
22use rustledger_core::{MetaValue, 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 {
656 return Err(QueryError::InvalidArguments(
657 "VALUE".to_string(),
658 "expected 1-2 arguments".to_string(),
659 ));
660 }
661 let (explicit_currency, at_date) = if args.len() == 2 {
662 match &args[1] {
663 Value::Date(d) => (None, Some(*d)),
664 Value::String(s) => (Some(s.as_str()), None),
665 Value::Null => {
666 return Err(QueryError::Type(
667 concat!(
668 "VALUE: second argument evaluated to NULL; ",
669 "expected a date or currency string ",
670 "(this often means an aggregate expression couldn't ",
671 "evaluate against an empty group — see issue #902)",
672 )
673 .to_string(),
674 ));
675 }
676 _ => {
677 return Err(QueryError::Type(
678 "VALUE second argument must be a date or currency string"
679 .to_string(),
680 ));
681 }
682 }
683 } else {
684 (None, None)
685 };
686 self.convert_to_market_value(&args[0], explicit_currency, at_date)
687 }
688 "SAFEDIV" => {
690 Self::require_args_count(&name_upper, args, 2)?;
691 let (dividend, divisor) = (&args[0], &args[1]);
692 match (dividend, divisor) {
693 (Value::Number(a), Value::Number(b)) => {
694 if b.is_zero() {
695 Ok(Value::Null)
696 } else {
697 Ok(Value::Number(a / b))
698 }
699 }
700 (Value::Integer(a), Value::Integer(b)) => {
701 if *b == 0 {
702 Ok(Value::Null)
703 } else {
704 Ok(Value::Number(Decimal::from(*a) / Decimal::from(*b)))
705 }
706 }
707 (Value::Number(a), Value::Integer(b)) => {
708 if *b == 0 {
709 Ok(Value::Null)
710 } else {
711 Ok(Value::Number(a / Decimal::from(*b)))
712 }
713 }
714 (Value::Integer(a), Value::Number(b)) => {
715 if b.is_zero() {
716 Ok(Value::Null)
717 } else {
718 Ok(Value::Number(Decimal::from(*a) / b))
719 }
720 }
721 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
722 _ => Err(QueryError::Type(
723 "SAFEDIV expects numeric arguments".to_string(),
724 )),
725 }
726 }
727 "NEG" => {
728 Self::require_args_count(&name_upper, args, 1)?;
729 match &args[0] {
730 Value::Number(n) => Ok(Value::Number(-n)),
731 Value::Integer(i) => Ok(Value::Integer(-i)),
732 Value::Amount(a) => {
733 Ok(Value::Amount(Amount::new(-a.number, a.currency.clone())))
734 }
735 _ => Err(QueryError::Type(
736 "NEG expects a number or amount".to_string(),
737 )),
738 }
739 }
740 "ACCOUNT_SORTKEY" => {
742 Self::require_args_count(&name_upper, args, 1)?;
743 match &args[0] {
744 Value::String(s) => {
745 let type_index = Self::account_type_index(s);
746 Ok(Value::String(format!("{type_index}-{s}")))
747 }
748 _ => Err(QueryError::Type(
749 "ACCOUNT_SORTKEY expects an account string".to_string(),
750 )),
751 }
752 }
753 "PARENT" => {
754 Self::require_args_count(&name_upper, args, 1)?;
755 match &args[0] {
756 Value::String(s) => {
757 if let Some(idx) = s.rfind(':') {
758 Ok(Value::String(s[..idx].to_string()))
759 } else {
760 Ok(Value::Null)
761 }
762 }
763 _ => Err(QueryError::Type(
764 "PARENT expects an account string".to_string(),
765 )),
766 }
767 }
768 "LEAF" => {
769 Self::require_args_count(&name_upper, args, 1)?;
770 match &args[0] {
771 Value::String(s) => {
772 if let Some(idx) = s.rfind(':') {
773 Ok(Value::String(s[idx + 1..].to_string()))
774 } else {
775 Ok(Value::String(s.clone()))
776 }
777 }
778 _ => Err(QueryError::Type(
779 "LEAF expects an account string".to_string(),
780 )),
781 }
782 }
783 "ROOT" => {
784 if args.is_empty() || args.len() > 2 {
785 return Err(QueryError::InvalidArguments(
786 "ROOT".to_string(),
787 "expected 1 or 2 arguments".to_string(),
788 ));
789 }
790 let n = if args.len() == 2 {
791 match &args[1] {
792 Value::Integer(i) => *i as usize,
793 _ => 1,
794 }
795 } else {
796 1
797 };
798 match &args[0] {
799 Value::String(s) => {
800 let parts: Vec<&str> = s.split(':').collect();
801 if n >= parts.len() {
802 Ok(Value::String(s.clone()))
803 } else {
804 Ok(Value::String(parts[..n].join(":")))
805 }
806 }
807 _ => Err(QueryError::Type(
808 "ROOT expects an account string".to_string(),
809 )),
810 }
811 }
812 "ONLY" => {
814 Self::require_args_count(&name_upper, args, 2)?;
815 let currency = match &args[0] {
816 Value::String(s) => s.clone(),
817 _ => {
818 return Err(QueryError::Type(
819 "ONLY: first argument must be a currency string".to_string(),
820 ));
821 }
822 };
823 match &args[1] {
824 Value::Inventory(inv) => {
825 let total = inv.units(¤cy);
826 if total.is_zero() {
827 Ok(Value::Null)
828 } else {
829 Ok(Value::Amount(Amount::new(total, ¤cy)))
830 }
831 }
832 Value::Position(p) => {
833 if p.units.currency.as_str() == currency {
834 Ok(Value::Amount(p.units.clone()))
835 } else {
836 Ok(Value::Null)
837 }
838 }
839 Value::Amount(a) => {
840 if a.currency.as_str() == currency {
841 Ok(Value::Amount(a.clone()))
842 } else {
843 Ok(Value::Null)
844 }
845 }
846 Value::Null => Ok(Value::Null),
847 _ => Err(QueryError::Type(
848 "ONLY: second argument must be an inventory, position, or amount"
849 .to_string(),
850 )),
851 }
852 }
853 "GETPRICE" => {
855 if args.len() < 2 || args.len() > 3 {
856 return Err(QueryError::InvalidArguments(
857 "GETPRICE".to_string(),
858 "expected 2 or 3 arguments".to_string(),
859 ));
860 }
861 let base = match &args[0] {
863 Value::String(s) => s.clone(),
864 Value::Null => return Ok(Value::Null),
865 _ => {
866 return Err(QueryError::Type(
867 "GETPRICE: first argument must be a currency string".to_string(),
868 ));
869 }
870 };
871 let quote = match &args[1] {
872 Value::String(s) => s.clone(),
873 Value::Null => return Ok(Value::Null),
874 _ => {
875 return Err(QueryError::Type(
876 "GETPRICE: second argument must be a currency string".to_string(),
877 ));
878 }
879 };
880 let date = if args.len() == 3 {
881 match &args[2] {
882 Value::Date(d) => *d,
883 Value::Null => self.query_date,
884 _ => self.query_date,
885 }
886 } else {
887 self.query_date
888 };
889 match self.price_db.get_price(&base, "e, date) {
890 Some(price) => Ok(Value::Number(price)),
891 None => Ok(Value::Null),
892 }
893 }
894 "EMPTY" => {
896 Self::require_args_count(&name_upper, args, 1)?;
897 match &args[0] {
898 Value::Inventory(inv) => Ok(Value::Boolean(inv.is_empty())),
899 Value::Null => Ok(Value::Boolean(true)),
900 _ => Err(QueryError::Type("EMPTY expects an inventory".to_string())),
901 }
902 }
903 "FILTER_CURRENCY" => {
904 Self::require_args_count(&name_upper, args, 2)?;
905 let currency = match &args[1] {
906 Value::String(s) => s.clone(),
907 _ => {
908 return Err(QueryError::Type(
909 "FILTER_CURRENCY expects (inventory, string)".to_string(),
910 ));
911 }
912 };
913 match &args[0] {
914 Value::Inventory(inv) => {
915 let filtered: Vec<Position> = inv
916 .positions()
917 .iter()
918 .filter(|p| p.units.currency.as_str() == currency)
919 .cloned()
920 .collect();
921 let mut new_inv = Inventory::new();
922 for pos in filtered {
923 new_inv.add(pos);
924 }
925 Ok(Value::Inventory(Box::new(new_inv)))
926 }
927 Value::Null => Ok(Value::Null),
928 _ => Err(QueryError::Type(
929 "FILTER_CURRENCY expects (inventory, string)".to_string(),
930 )),
931 }
932 }
933 "POSSIGN" => {
934 Self::require_args_count(&name_upper, args, 2)?;
935 let account_str = match &args[1] {
936 Value::String(s) => s.clone(),
937 _ => {
938 return Err(QueryError::Type(
939 "POSSIGN expects (amount, account_string)".to_string(),
940 ));
941 }
942 };
943 let first_component = account_str.split(':').next().unwrap_or("");
944 let is_credit_normal =
945 matches!(first_component, "Liabilities" | "Equity" | "Income");
946 match &args[0] {
947 Value::Amount(a) => {
948 let mut amt = a.clone();
949 if is_credit_normal {
950 amt.number = -amt.number;
951 }
952 Ok(Value::Amount(amt))
953 }
954 Value::Number(n) => {
955 let adjusted = if is_credit_normal { -n } else { *n };
956 Ok(Value::Number(adjusted))
957 }
958 Value::Null => Ok(Value::Null),
959 _ => Err(QueryError::Type(
960 "POSSIGN expects (amount, account_string)".to_string(),
961 )),
962 }
963 }
964 "CONVERT" => {
966 if args.len() < 2 || args.len() > 3 {
967 return Err(QueryError::InvalidArguments(
968 "CONVERT".to_string(),
969 "expected 2 or 3 arguments: (value, currency[, date])".to_string(),
970 ));
971 }
972
973 let target_currency = match &args[1] {
974 Value::String(s) => s.clone(),
975 Value::Null => {
976 return Err(QueryError::Type(
977 concat!(
978 "CONVERT: second argument evaluated to NULL; ",
979 "expected a currency string ",
980 "(this often means an aggregate expression couldn't ",
981 "evaluate against an empty group — see issue #902)",
982 )
983 .to_string(),
984 ));
985 }
986 _ => {
987 return Err(QueryError::Type(
988 "CONVERT: second argument must be a currency string".to_string(),
989 ));
990 }
991 };
992
993 let date: Option<rustledger_core::NaiveDate> = if args.len() == 3 {
995 match &args[2] {
996 Value::Date(d) => Some(*d),
997 Value::Null => None, _ => {
999 return Err(QueryError::Type(
1000 "CONVERT: third argument must be a date".to_string(),
1001 ));
1002 }
1003 }
1004 } else {
1005 None
1006 };
1007
1008 let convert_amount = |amt: &Amount| -> Option<Amount> {
1010 if let Some(d) = date {
1011 self.price_db.convert(amt, &target_currency, d)
1012 } else {
1013 self.price_db.convert_latest(amt, &target_currency)
1014 }
1015 };
1016
1017 match &args[0] {
1018 Value::Position(p) => {
1019 if p.units.currency == target_currency {
1020 Ok(Value::Amount(p.units.clone()))
1021 } else if let Some(converted) = convert_amount(&p.units) {
1022 Ok(Value::Amount(converted))
1023 } else {
1024 Ok(Value::Amount(p.units.clone()))
1025 }
1026 }
1027 Value::Amount(a) => {
1028 if a.currency == target_currency {
1029 Ok(Value::Amount(a.clone()))
1030 } else if let Some(converted) = convert_amount(a) {
1031 Ok(Value::Amount(converted))
1032 } else {
1033 Ok(Value::Amount(a.clone()))
1034 }
1035 }
1036 Value::Inventory(inv) => {
1037 let mut result = Inventory::default();
1040 for pos in inv.positions() {
1041 if pos.units.currency == target_currency {
1042 result.add(Position::simple(pos.units.clone()));
1043 } else if let Some(converted) = convert_amount(&pos.units) {
1044 result.add(Position::simple(converted));
1045 } else {
1046 result.add(Position::simple(pos.units.clone()));
1048 }
1049 }
1050 let positions = result.positions();
1053 if positions.is_empty() {
1054 Ok(Value::Amount(Amount::new(Decimal::ZERO, &target_currency)))
1055 } else if positions.len() == 1
1056 && positions[0].units.currency == target_currency
1057 {
1058 Ok(Value::Amount(positions[0].units.clone()))
1059 } else {
1060 Ok(Value::Inventory(Box::new(result)))
1061 }
1062 }
1063 Value::Number(n) => Ok(Value::Amount(Amount::new(*n, &target_currency))),
1064 Value::Null => {
1065 Ok(Value::Amount(Amount::new(Decimal::ZERO, &target_currency)))
1068 }
1069 _ => Err(QueryError::Type(
1070 "CONVERT expects a position, amount, inventory, or number".to_string(),
1071 )),
1072 }
1073 }
1074 "STR" => {
1076 Self::require_args_count(&name_upper, args, 1)?;
1077 Self::value_to_str(&args[0])
1078 }
1079 "INT" => {
1080 Self::require_args_count(&name_upper, args, 1)?;
1081 Self::value_to_int(&args[0])
1082 }
1083 "DECIMAL" => {
1084 Self::require_args_count(&name_upper, args, 1)?;
1085 Self::value_to_decimal(&args[0])
1086 }
1087 "BOOL" => {
1088 Self::require_args_count(&name_upper, args, 1)?;
1089 Self::value_to_bool(&args[0])
1090 }
1091 "QUARTER" => {
1093 Self::require_args_count(&name_upper, args, 1)?;
1094 match &args[0] {
1095 Value::Date(d) => Ok(Value::Integer(((d.month() - 1) / 3 + 1).into())),
1096 _ => Err(QueryError::Type("QUARTER expects a date".to_string())),
1097 }
1098 }
1099 "WEEKDAY" => {
1100 Self::require_args_count(&name_upper, args, 1)?;
1101 match &args[0] {
1102 Value::Date(d) => Ok(Value::Integer(
1103 (d.weekday().to_monday_zero_offset() as u32).into(),
1104 )),
1105 _ => Err(QueryError::Type("WEEKDAY expects a date".to_string())),
1106 }
1107 }
1108 "YMONTH" => {
1109 Self::require_args_count(&name_upper, args, 1)?;
1110 match &args[0] {
1111 Value::Date(d) => {
1112 Ok(Value::String(format!("{:04}-{:02}", d.year(), d.month())))
1113 }
1114 _ => Err(QueryError::Type("YMONTH expects a date".to_string())),
1115 }
1116 }
1117 "SUBSTR" | "SUBSTRING" => {
1119 if args.len() < 2 || args.len() > 3 {
1120 return Err(QueryError::InvalidArguments(
1121 name_upper,
1122 "expected 2 or 3 arguments".to_string(),
1123 ));
1124 }
1125 match (&args[0], &args[1], args.get(2)) {
1126 (Value::String(s), Value::Integer(start), None) => {
1127 let start = (*start).max(0) as usize;
1128 let result: String = s.chars().skip(start).collect();
1129 Ok(Value::String(result))
1130 }
1131 (Value::String(s), Value::Integer(start), Some(Value::Integer(len))) => {
1132 let start = (*start).max(0) as usize;
1133 let len = (*len).max(0) as usize;
1134 let result: String = s.chars().skip(start).take(len).collect();
1135 Ok(Value::String(result))
1136 }
1137 _ => Err(QueryError::Type(
1138 "SUBSTR expects (string, int, [int])".to_string(),
1139 )),
1140 }
1141 }
1142 "STARTSWITH" => {
1143 Self::require_args_count(&name_upper, args, 2)?;
1144 match (&args[0], &args[1]) {
1145 (Value::String(s), Value::String(prefix)) => {
1146 Ok(Value::Boolean(s.starts_with(prefix.as_str())))
1147 }
1148 _ => Err(QueryError::Type(
1149 "STARTSWITH expects two strings".to_string(),
1150 )),
1151 }
1152 }
1153 "ENDSWITH" => {
1154 Self::require_args_count(&name_upper, args, 2)?;
1155 match (&args[0], &args[1]) {
1156 (Value::String(s), Value::String(suffix)) => {
1157 Ok(Value::Boolean(s.ends_with(suffix.as_str())))
1158 }
1159 _ => Err(QueryError::Type("ENDSWITH expects two strings".to_string())),
1160 }
1161 }
1162 "MAXWIDTH" => {
1163 Self::require_args_count(&name_upper, args, 2)?;
1164 match (&args[0], &args[1]) {
1165 (Value::String(s), Value::Integer(max)) => {
1166 let n = *max as usize;
1167 if s.chars().count() <= n {
1168 Ok(Value::String(s.clone()))
1169 } else if n <= 3 {
1170 Ok(Value::String(s.chars().take(n).collect()))
1171 } else {
1172 let truncated: String = s.chars().take(n - 3).collect();
1173 Ok(Value::String(format!("{truncated}...")))
1174 }
1175 }
1176 _ => Err(QueryError::Type(
1177 "MAXWIDTH expects (string, integer)".to_string(),
1178 )),
1179 }
1180 }
1181 "ACCOUNT_DEPTH" => {
1183 Self::require_args_count(&name_upper, args, 1)?;
1184 match &args[0] {
1185 Value::String(s) => Ok(Value::Integer(s.matches(':').count() as i64 + 1)),
1186 _ => Err(QueryError::Type(
1187 "ACCOUNT_DEPTH expects an account string".to_string(),
1188 )),
1189 }
1190 }
1191 "GETITEM" | "GET" => {
1193 Self::require_args_count(&name_upper, args, 2)?;
1194 match (&args[0], &args[1]) {
1195 (Value::Inventory(inv), Value::String(currency)) => {
1196 let amount = inv.units(currency);
1197 if amount.is_zero() {
1198 Ok(Value::Null)
1199 } else {
1200 Ok(Value::Amount(Amount::new(amount, currency.as_str())))
1201 }
1202 }
1203 (Value::Null, _) => Ok(Value::Null),
1204 _ => Err(QueryError::Type(
1205 "GETITEM expects (inventory, string)".to_string(),
1206 )),
1207 }
1208 }
1209 "WEIGHT" => {
1210 Self::require_args_count(&name_upper, args, 1)?;
1211 match &args[0] {
1212 Value::Position(p) => {
1213 if let Some(cost) = &p.cost {
1214 let total = p.units.number * cost.number;
1215 Ok(Value::Amount(Amount::new(total, cost.currency.clone())))
1216 } else {
1217 Ok(Value::Amount(p.units.clone()))
1218 }
1219 }
1220 Value::Amount(a) => Ok(Value::Amount(a.clone())),
1221 Value::Inventory(inv) => {
1222 let mut result = Inventory::new();
1223 for pos in inv.positions() {
1224 if let Some(cost) = &pos.cost {
1225 let total = pos.units.number * cost.number;
1226 result.add(Position::simple(Amount::new(
1227 total,
1228 cost.currency.clone(),
1229 )));
1230 } else {
1231 result.add(Position::simple(pos.units.clone()));
1232 }
1233 }
1234 Ok(Value::Inventory(Box::new(result)))
1235 }
1236 Value::Null => Ok(Value::Null),
1237 _ => Err(QueryError::Type(
1238 "WEIGHT expects a position, amount, or inventory".to_string(),
1239 )),
1240 }
1241 }
1242 "DATE_DIFF" => {
1244 Self::require_args_count(&name_upper, args, 2)?;
1245 match (&args[0], &args[1]) {
1246 (Value::Date(d1), Value::Date(d2)) => Ok(Value::Integer(i64::from(
1247 d1.since(*d2).unwrap_or_default().get_days(),
1248 ))),
1249 _ => Err(QueryError::Type("DATE_DIFF expects two dates".to_string())),
1250 }
1251 }
1252 "GREP" => {
1254 Self::require_args_count(&name_upper, args, 2)?;
1255 match (&args[0], &args[1]) {
1256 (Value::String(pattern), Value::String(s)) => {
1257 let re = regex::Regex::new(pattern).map_err(|e| {
1258 QueryError::Type(format!("GREP: invalid regex '{pattern}': {e}"))
1259 })?;
1260 match re.find(s) {
1261 Some(m) => Ok(Value::String(m.as_str().to_string())),
1262 None => Ok(Value::Null),
1263 }
1264 }
1265 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1267 _ => Err(QueryError::Type("GREP expects two strings".to_string())),
1268 }
1269 }
1270 "GREPN" => {
1271 Self::require_args_count(&name_upper, args, 3)?;
1272 let n = match &args[2] {
1273 Value::Integer(i) => (*i).max(0) as usize,
1274 Value::Number(n) => {
1275 use rust_decimal::prelude::ToPrimitive;
1276 n.to_usize().unwrap_or(0)
1277 }
1278 _ => {
1279 return Err(QueryError::Type(
1280 "GREPN: third argument must be an integer".to_string(),
1281 ));
1282 }
1283 };
1284 match (&args[0], &args[1]) {
1285 (Value::String(pattern), Value::String(s)) => {
1286 let re = regex::Regex::new(pattern).map_err(|e| {
1287 QueryError::Type(format!("GREPN: invalid regex '{pattern}': {e}"))
1288 })?;
1289 match re.captures(s) {
1290 Some(caps) => match caps.get(n) {
1291 Some(m) => Ok(Value::String(m.as_str().to_string())),
1292 None => Ok(Value::Null),
1293 },
1294 None => Ok(Value::Null),
1295 }
1296 }
1297 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1298 _ => Err(QueryError::Type(
1299 "GREPN expects (pattern, string, int)".to_string(),
1300 )),
1301 }
1302 }
1303 "SUBST" => {
1304 Self::require_args_count(&name_upper, args, 3)?;
1305 match (&args[0], &args[1], &args[2]) {
1306 (Value::String(pattern), Value::String(replacement), Value::String(s)) => {
1307 let re = regex::Regex::new(pattern).map_err(|e| {
1308 QueryError::Type(format!("SUBST: invalid regex '{pattern}': {e}"))
1309 })?;
1310 Ok(Value::String(
1311 re.replace_all(s, replacement.as_str()).to_string(),
1312 ))
1313 }
1314 _ => Err(QueryError::Type(
1315 "SUBST expects (pattern, replacement, string)".to_string(),
1316 )),
1317 }
1318 }
1319 "SPLITCOMP" => {
1320 Self::require_args_count(&name_upper, args, 3)?;
1321 let n = match &args[2] {
1322 Value::Integer(i) => (*i).max(0) as usize,
1323 Value::Number(n) => {
1324 use rust_decimal::prelude::ToPrimitive;
1325 n.to_usize().unwrap_or(0)
1326 }
1327 _ => {
1328 return Err(QueryError::Type(
1329 "SPLITCOMP: third argument must be an integer".to_string(),
1330 ));
1331 }
1332 };
1333 match (&args[0], &args[1]) {
1334 (Value::String(s), Value::String(delim)) => {
1335 let parts: Vec<&str> = s.split(delim.as_str()).collect();
1336 match parts.get(n) {
1337 Some(part) => Ok(Value::String((*part).to_string())),
1338 None => Ok(Value::Null),
1339 }
1340 }
1341 _ => Err(QueryError::Type(
1342 "SPLITCOMP expects (string, delimiter, int)".to_string(),
1343 )),
1344 }
1345 }
1346 "JOINSTR" => {
1347 let mut parts = Vec::new();
1348 for v in args {
1349 match v {
1350 Value::String(s) => parts.push(s.clone()),
1351 Value::StringSet(ss) => parts.extend(ss.iter().cloned()),
1352 Value::Integer(i) => parts.push(i.to_string()),
1353 Value::Number(n) => parts.push(n.to_string()),
1354 Value::Null => {}
1355 _ => {}
1356 }
1357 }
1358 Ok(Value::String(parts.join(",")))
1359 }
1360 "OPEN_DATE" => {
1362 Self::require_args_count(&name_upper, args, 1)?;
1363 match &args[0] {
1364 Value::String(account) => Ok(self
1365 .account_info
1366 .get(account.as_str())
1367 .and_then(|info| info.open_date)
1368 .map_or(Value::Null, Value::Date)),
1369 Value::Null => Ok(Value::Null),
1370 _ => Err(QueryError::Type(
1371 "OPEN_DATE expects an account string".to_string(),
1372 )),
1373 }
1374 }
1375 "CLOSE_DATE" => {
1376 Self::require_args_count(&name_upper, args, 1)?;
1377 match &args[0] {
1378 Value::String(account) => Ok(self
1379 .account_info
1380 .get(account.as_str())
1381 .and_then(|info| info.close_date)
1382 .map_or(Value::Null, Value::Date)),
1383 Value::Null => Ok(Value::Null),
1384 _ => Err(QueryError::Type(
1385 "CLOSE_DATE expects an account string".to_string(),
1386 )),
1387 }
1388 }
1389 "OPEN_META" => {
1390 Self::require_args_count(&name_upper, args, 2)?;
1391 match (&args[0], &args[1]) {
1392 (Value::String(account), Value::String(key)) => Ok(self
1393 .account_info
1394 .get(account.as_str())
1395 .and_then(|info| info.open_meta.get(key))
1396 .map_or(Value::Null, |mv| Self::meta_value_to_value(Some(mv)))),
1397 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
1398 _ => Err(QueryError::Type(
1399 "OPEN_META expects (account_string, key_string)".to_string(),
1400 )),
1401 }
1402 }
1403 "META" | "ENTRY_META" | "ANY_META" | "POSTING_META" => {
1407 Self::require_args_count(&name_upper, args, 1)?;
1408 match &args[0] {
1409 Value::String(_) | Value::Null => Ok(Value::Null),
1410 _ => Err(QueryError::Type(format!(
1411 "{name_upper}: argument must be a string key"
1412 ))),
1413 }
1414 }
1415 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
1417 _ => Err(QueryError::UnknownFunction(name.to_string())),
1418 }
1419 }
1420
1421 fn metadata_to_value(meta: &rustledger_core::Metadata) -> Value {
1423 if meta.is_empty() {
1424 return Value::Null;
1425 }
1426 let map: std::collections::BTreeMap<String, Value> = meta
1427 .iter()
1428 .map(|(k, v)| (k.clone(), Self::meta_value_to_value(Some(v))))
1429 .collect();
1430 Value::Object(Box::new(map))
1431 }
1432
1433 fn require_args_count(name: &str, args: &[Value], expected: usize) -> Result<(), QueryError> {
1435 if args.len() != expected {
1436 return Err(QueryError::InvalidArguments(
1437 name.to_string(),
1438 format!("expected {} argument(s), got {}", expected, args.len()),
1439 ));
1440 }
1441 Ok(())
1442 }
1443
1444 fn require_args(name: &str, func: &FunctionCall, expected: usize) -> Result<(), QueryError> {
1446 if func.args.len() != expected {
1447 return Err(QueryError::InvalidArguments(
1448 name.to_string(),
1449 format!("expected {expected} argument(s)"),
1450 ));
1451 }
1452 Ok(())
1453 }
1454
1455 pub(crate) fn convert_to_market_value(
1490 &self,
1491 val: &Value,
1492 explicit_currency: Option<&str>,
1493 at_date: Option<NaiveDate>,
1494 ) -> Result<Value, QueryError> {
1495 let target_currency = if let Some(currency) = explicit_currency {
1500 currency.to_string()
1501 } else {
1502 let inferred = match val {
1504 Value::Position(p) => p.cost.as_ref().map(|c| c.currency.to_string()),
1505 Value::Inventory(inv) => inv
1506 .positions()
1507 .iter()
1508 .find_map(|p| p.cost.as_ref().map(|c| c.currency.to_string())),
1509 _ => None,
1510 };
1511
1512 match inferred.or_else(|| self.target_currency.clone()) {
1513 Some(c) => c,
1514 None => {
1515 return match val {
1520 Value::Position(p) => Ok(Value::Amount(p.units.clone())),
1521 Value::Amount(a) => Ok(Value::Amount(a.clone())),
1522 Value::Inventory(inv) => Ok(Value::Inventory(inv.clone())),
1523 Value::Null => Ok(Value::Null),
1524 _ => Err(QueryError::Type(
1525 "VALUE expects a position, amount, or inventory".to_string(),
1526 )),
1527 };
1528 }
1529 }
1530 };
1531
1532 let convert_one = |amount: &Amount| -> Option<Amount> {
1537 match at_date {
1538 Some(d) => self.price_db.convert(amount, &target_currency, d),
1539 None => self.price_db.convert_latest(amount, &target_currency),
1540 }
1541 };
1542
1543 match val {
1544 Value::Position(p) => {
1545 if p.units.currency == target_currency {
1546 Ok(Value::Amount(p.units.clone()))
1547 } else if let Some(converted) = convert_one(&p.units) {
1548 Ok(Value::Amount(converted))
1549 } else {
1550 Ok(Value::Amount(p.units.clone()))
1551 }
1552 }
1553 Value::Amount(a) => {
1554 if a.currency == target_currency {
1555 Ok(Value::Amount(a.clone()))
1556 } else if let Some(converted) = convert_one(a) {
1557 Ok(Value::Amount(converted))
1558 } else {
1559 Ok(Value::Amount(a.clone()))
1560 }
1561 }
1562 Value::Inventory(inv) => {
1563 let mut total = Decimal::ZERO;
1564 for pos in inv.positions() {
1565 if pos.units.currency == target_currency {
1566 total += pos.units.number;
1567 } else if let Some(converted) = convert_one(&pos.units) {
1568 total += converted.number;
1569 }
1570 }
1571 Ok(Value::Amount(Amount::new(total, &target_currency)))
1572 }
1573 Value::Null => Ok(Value::Null),
1574 _ => Err(QueryError::Type(
1575 "VALUE expects a position, amount, or inventory".to_string(),
1576 )),
1577 }
1578 }
1579
1580 pub(super) const fn is_window_expr(expr: &Expr) -> bool {
1582 matches!(expr, Expr::Window(_))
1583 }
1584
1585 fn resolve_column_names(&self, targets: &[Target]) -> Result<Vec<String>, QueryError> {
1587 let mut names = Vec::new();
1588 for (i, target) in targets.iter().enumerate() {
1589 if matches!(target.expr, Expr::Wildcard) {
1590 if target.alias.is_some() {
1592 return Err(QueryError::Evaluation(
1593 "Cannot alias wildcard (*) - it expands to multiple columns".to_string(),
1594 ));
1595 }
1596 names.extend(WILDCARD_COLUMNS.iter().map(|s| (*s).to_string()));
1598 } else if let Some(alias) = &target.alias {
1599 names.push(alias.clone());
1600 } else {
1601 names.push(self.expr_to_name(&target.expr, i));
1602 }
1603 }
1604 Ok(names)
1605 }
1606
1607 fn expr_to_name(&self, expr: &Expr, index: usize) -> String {
1609 match expr {
1610 Expr::Wildcard => "*".to_string(),
1611 Expr::Column(name) => name.clone(),
1612 Expr::Function(func) => func.name.clone(),
1613 Expr::Window(wf) => wf.name.clone(),
1614 _ => format!("col{index}"),
1615 }
1616 }
1617
1618 pub(super) fn get_builtin_table(&self, table_name: &str) -> Option<Table> {
1637 let upper = table_name.to_uppercase();
1641 let normalized = upper.strip_prefix('#').unwrap_or(&upper);
1642
1643 match normalized {
1644 "PRICES" => Some(self.build_prices_table()),
1645 "BALANCES" => Some(self.build_balances_table()),
1646 "COMMODITIES" => Some(self.build_commodities_table()),
1647 "EVENTS" => Some(self.build_events_table()),
1648 "NOTES" => Some(self.build_notes_table()),
1649 "DOCUMENTS" => Some(self.build_documents_table()),
1650 "ACCOUNTS" => Some(self.build_accounts_table()),
1651 "TRANSACTIONS" => Some(self.build_transactions_table()),
1652 "ENTRIES" => Some(self.build_entries_table()),
1653 "POSTINGS" => Some(self.build_postings_table()),
1654 _ => None,
1655 }
1656 }
1657
1658 fn build_prices_table(&self) -> Table {
1665 let columns = vec![
1666 "date".to_string(),
1667 "currency".to_string(),
1668 "amount".to_string(),
1669 ];
1670 let mut table = Table::new(columns);
1671
1672 let mut entries: Vec<_> = self.price_db.iter_entries().collect();
1674 entries.sort_by(|(currency_a, date_a, _, _), (currency_b, date_b, _, _)| {
1676 date_a.cmp(date_b).then_with(|| currency_a.cmp(currency_b))
1677 });
1678
1679 for (base_currency, date, price_number, quote_currency) in entries {
1680 let row = vec![
1681 Value::Date(date),
1682 Value::String(base_currency.to_string()),
1683 Value::Amount(Amount::new(price_number, quote_currency)),
1684 ];
1685 table.add_row(row);
1686 }
1687
1688 table
1689 }
1690
1691 fn build_balances_table(&self) -> Table {
1698 let columns = vec![
1699 "date".to_string(),
1700 "account".to_string(),
1701 "amount".to_string(),
1702 ];
1703 let mut table = Table::new(columns);
1704
1705 let mut balances: Vec<_> = if let Some(spanned) = self.spanned_directives {
1707 spanned
1708 .iter()
1709 .filter_map(|s| {
1710 if let Directive::Balance(b) = &s.value {
1711 Some((b.date, b.account.as_ref(), b.amount.clone()))
1712 } else {
1713 None
1714 }
1715 })
1716 .collect()
1717 } else {
1718 self.directives
1719 .iter()
1720 .filter_map(|d| {
1721 if let Directive::Balance(b) = d {
1722 Some((b.date, b.account.as_ref(), b.amount.clone()))
1723 } else {
1724 None
1725 }
1726 })
1727 .collect()
1728 };
1729
1730 balances.sort_by(|(date_a, account_a, _), (date_b, account_b, _)| {
1732 date_a.cmp(date_b).then_with(|| account_a.cmp(account_b))
1733 });
1734
1735 for (date, account, amount) in balances {
1736 let row = vec![
1737 Value::Date(date),
1738 Value::String(account.to_string()),
1739 Value::Amount(amount),
1740 ];
1741 table.add_row(row);
1742 }
1743
1744 table
1745 }
1746
1747 fn build_commodities_table(&self) -> Table {
1753 let columns = vec!["date".to_string(), "name".to_string()];
1754 let mut table = Table::new(columns);
1755
1756 let mut commodities: Vec<_> = if let Some(spanned) = self.spanned_directives {
1758 spanned
1759 .iter()
1760 .filter_map(|s| {
1761 if let Directive::Commodity(c) = &s.value {
1762 Some((c.date, c.currency.as_ref()))
1763 } else {
1764 None
1765 }
1766 })
1767 .collect()
1768 } else {
1769 self.directives
1770 .iter()
1771 .filter_map(|d| {
1772 if let Directive::Commodity(c) = d {
1773 Some((c.date, c.currency.as_ref()))
1774 } else {
1775 None
1776 }
1777 })
1778 .collect()
1779 };
1780
1781 commodities.sort_by(|(date_a, name_a), (date_b, name_b)| {
1783 date_a.cmp(date_b).then_with(|| name_a.cmp(name_b))
1784 });
1785
1786 for (date, name) in commodities {
1787 let row = vec![Value::Date(date), Value::String(name.to_string())];
1788 table.add_row(row);
1789 }
1790
1791 table
1792 }
1793
1794 fn build_events_table(&self) -> Table {
1801 let columns = vec![
1802 "date".to_string(),
1803 "type".to_string(),
1804 "description".to_string(),
1805 ];
1806 let mut table = Table::new(columns);
1807
1808 let mut events: Vec<_> = if let Some(spanned) = self.spanned_directives {
1810 spanned
1811 .iter()
1812 .filter_map(|s| {
1813 if let Directive::Event(e) = &s.value {
1814 Some((e.date, e.event_type.as_str(), e.value.as_str()))
1815 } else {
1816 None
1817 }
1818 })
1819 .collect()
1820 } else {
1821 self.directives
1822 .iter()
1823 .filter_map(|d| {
1824 if let Directive::Event(e) = d {
1825 Some((e.date, e.event_type.as_str(), e.value.as_str()))
1826 } else {
1827 None
1828 }
1829 })
1830 .collect()
1831 };
1832
1833 events.sort_by(|(date_a, type_a, _), (date_b, type_b, _)| {
1835 date_a.cmp(date_b).then_with(|| type_a.cmp(type_b))
1836 });
1837
1838 for (date, event_type, description) in events {
1839 let row = vec![
1840 Value::Date(date),
1841 Value::String(event_type.to_string()),
1842 Value::String(description.to_string()),
1843 ];
1844 table.add_row(row);
1845 }
1846
1847 table
1848 }
1849
1850 fn build_notes_table(&self) -> Table {
1857 let columns = vec![
1858 "date".to_string(),
1859 "account".to_string(),
1860 "comment".to_string(),
1861 ];
1862 let mut table = Table::new(columns);
1863
1864 let mut notes: Vec<_> = if let Some(spanned) = self.spanned_directives {
1866 spanned
1867 .iter()
1868 .filter_map(|s| {
1869 if let Directive::Note(n) = &s.value {
1870 Some((n.date, n.account.as_ref(), n.comment.as_str()))
1871 } else {
1872 None
1873 }
1874 })
1875 .collect()
1876 } else {
1877 self.directives
1878 .iter()
1879 .filter_map(|d| {
1880 if let Directive::Note(n) = d {
1881 Some((n.date, n.account.as_ref(), n.comment.as_str()))
1882 } else {
1883 None
1884 }
1885 })
1886 .collect()
1887 };
1888
1889 notes.sort_by(|(date_a, account_a, _), (date_b, account_b, _)| {
1891 date_a.cmp(date_b).then_with(|| account_a.cmp(account_b))
1892 });
1893
1894 for (date, account, comment) in notes {
1895 let row = vec![
1896 Value::Date(date),
1897 Value::String(account.to_string()),
1898 Value::String(comment.to_string()),
1899 ];
1900 table.add_row(row);
1901 }
1902
1903 table
1904 }
1905
1906 fn build_documents_table(&self) -> Table {
1915 let columns = vec![
1916 "date".to_string(),
1917 "account".to_string(),
1918 "filename".to_string(),
1919 "tags".to_string(),
1920 "links".to_string(),
1921 ];
1922 let mut table = Table::new(columns);
1923
1924 let mut documents: Vec<_> = if let Some(spanned) = self.spanned_directives {
1926 spanned
1927 .iter()
1928 .filter_map(|s| {
1929 if let Directive::Document(d) = &s.value {
1930 Some((
1931 d.date,
1932 d.account.as_ref(),
1933 d.path.as_str(),
1934 &d.tags,
1935 &d.links,
1936 ))
1937 } else {
1938 None
1939 }
1940 })
1941 .collect()
1942 } else {
1943 self.directives
1944 .iter()
1945 .filter_map(|d| {
1946 if let Directive::Document(doc) = d {
1947 Some((
1948 doc.date,
1949 doc.account.as_ref(),
1950 doc.path.as_str(),
1951 &doc.tags,
1952 &doc.links,
1953 ))
1954 } else {
1955 None
1956 }
1957 })
1958 .collect()
1959 };
1960
1961 documents.sort_by(
1963 |(date_a, account_a, file_a, _, _), (date_b, account_b, file_b, _, _)| {
1964 date_a
1965 .cmp(date_b)
1966 .then_with(|| account_a.cmp(account_b))
1967 .then_with(|| file_a.cmp(file_b))
1968 },
1969 );
1970
1971 for (date, account, filename, tags, links) in documents {
1972 let tags_vec: Vec<String> = tags.iter().map(ToString::to_string).collect();
1973 let links_vec: Vec<String> = links.iter().map(ToString::to_string).collect();
1974 let row = vec![
1975 Value::Date(date),
1976 Value::String(account.to_string()),
1977 Value::String(filename.to_string()),
1978 Value::StringSet(tags_vec),
1979 Value::StringSet(links_vec),
1980 ];
1981 table.add_row(row);
1982 }
1983
1984 table
1985 }
1986
1987 fn build_accounts_table(&self) -> Table {
1996 let columns = vec![
1997 "account".to_string(),
1998 "open".to_string(),
1999 "close".to_string(),
2000 "currencies".to_string(),
2001 "booking".to_string(),
2002 ];
2003 let mut table = Table::new(columns);
2004
2005 let mut accounts: FxHashMap<
2007 &str,
2008 (
2009 Option<rustledger_core::NaiveDate>,
2010 Option<rustledger_core::NaiveDate>,
2011 Vec<String>,
2012 Option<&str>,
2013 ),
2014 > = FxHashMap::default();
2015
2016 let iter: Box<dyn Iterator<Item = &Directive>> =
2018 if let Some(spanned) = self.spanned_directives {
2019 Box::new(spanned.iter().map(|s| &s.value))
2020 } else {
2021 Box::new(self.directives.iter())
2022 };
2023
2024 for directive in iter {
2025 match directive {
2026 Directive::Open(open) => {
2027 let entry = accounts.entry(open.account.as_ref()).or_insert((
2028 None,
2029 None,
2030 Vec::new(),
2031 None,
2032 ));
2033 entry.0 = Some(open.date);
2034 entry.2 = open.currencies.iter().map(ToString::to_string).collect();
2035 entry.3 = open.booking.as_deref();
2036 }
2037 Directive::Close(close) => {
2038 let entry = accounts.entry(close.account.as_ref()).or_insert((
2039 None,
2040 None,
2041 Vec::new(),
2042 None,
2043 ));
2044 entry.1 = Some(close.date);
2045 }
2046 _ => {}
2047 }
2048 }
2049
2050 let mut account_list: Vec<_> = accounts.into_iter().collect();
2052 account_list.sort_by_key(|(a, _)| *a);
2053
2054 for (account, (open_date, close_date, currencies, booking)) in account_list {
2055 let row = vec![
2056 Value::String(account.to_string()),
2057 open_date.map_or(Value::Null, Value::Date),
2058 close_date.map_or(Value::Null, Value::Date),
2059 Value::StringSet(currencies),
2060 booking.map_or(Value::Null, |b| Value::String(b.to_string())),
2061 ];
2062 table.add_row(row);
2063 }
2064
2065 table
2066 }
2067
2068 fn build_transactions_table(&self) -> Table {
2079 let columns = vec![
2080 "date".to_string(),
2081 "flag".to_string(),
2082 "payee".to_string(),
2083 "narration".to_string(),
2084 "tags".to_string(),
2085 "links".to_string(),
2086 "accounts".to_string(),
2087 ];
2088 let mut table = Table::new(columns);
2089
2090 let iter: Box<dyn Iterator<Item = &Directive>> =
2092 if let Some(spanned) = self.spanned_directives {
2093 Box::new(spanned.iter().map(|s| &s.value))
2094 } else {
2095 Box::new(self.directives.iter())
2096 };
2097
2098 let mut transactions: Vec<_> = iter
2099 .filter_map(|d| {
2100 if let Directive::Transaction(txn) = d {
2101 Some(txn)
2102 } else {
2103 None
2104 }
2105 })
2106 .collect();
2107
2108 transactions.sort_by_key(|t| t.date);
2110
2111 for txn in transactions {
2112 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2113 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2114 let mut accounts: Vec<String> = txn
2115 .postings
2116 .iter()
2117 .map(|p| p.account.to_string())
2118 .collect::<std::collections::HashSet<_>>()
2119 .into_iter()
2120 .collect();
2121 accounts.sort(); let row = vec![
2124 Value::Date(txn.date),
2125 Value::String(txn.flag.to_string()),
2126 txn.payee
2127 .as_ref()
2128 .map_or(Value::Null, |p| Value::String(p.to_string())),
2129 Value::String(txn.narration.to_string()),
2130 Value::StringSet(tags),
2131 Value::StringSet(links),
2132 Value::StringSet(accounts),
2133 ];
2134 table.add_row(row);
2135 }
2136
2137 table
2138 }
2139
2140 fn build_entries_table(&self) -> Table {
2145 let columns = vec![
2146 "id".to_string(),
2147 "type".to_string(),
2148 "filename".to_string(),
2149 "lineno".to_string(),
2150 "date".to_string(),
2151 "flag".to_string(),
2152 "payee".to_string(),
2153 "narration".to_string(),
2154 "tags".to_string(),
2155 "links".to_string(),
2156 "accounts".to_string(),
2157 "_entry_meta".to_string(),
2158 ];
2159 let mut table = Table::new(columns);
2160
2161 if let Some(spanned) = self.spanned_directives {
2163 for (idx, spanned_dir) in spanned.iter().enumerate() {
2164 let directive = &spanned_dir.value;
2165 let source_loc = self.get_source_location(idx);
2166 let row = self.directive_to_entry_row(idx, directive, source_loc);
2167 table.add_row(row);
2168 }
2169 } else {
2170 for (idx, directive) in self.directives.iter().enumerate() {
2171 let row = self.directive_to_entry_row(idx, directive, None);
2172 table.add_row(row);
2173 }
2174 }
2175
2176 table
2177 }
2178
2179 fn directive_to_entry_row(
2181 &self,
2182 idx: usize,
2183 directive: &Directive,
2184 source_loc: Option<&SourceLocation>,
2185 ) -> Vec<Value> {
2186 let type_name = match directive {
2187 Directive::Transaction(_) => "transaction",
2188 Directive::Balance(_) => "balance",
2189 Directive::Open(_) => "open",
2190 Directive::Close(_) => "close",
2191 Directive::Commodity(_) => "commodity",
2192 Directive::Pad(_) => "pad",
2193 Directive::Event(_) => "event",
2194 Directive::Query(_) => "query",
2195 Directive::Note(_) => "note",
2196 Directive::Document(_) => "document",
2197 Directive::Price(_) => "price",
2198 Directive::Custom(_) => "custom",
2199 };
2200
2201 let date = match directive {
2202 Directive::Transaction(t) => Value::Date(t.date),
2203 Directive::Balance(b) => Value::Date(b.date),
2204 Directive::Open(o) => Value::Date(o.date),
2205 Directive::Close(c) => Value::Date(c.date),
2206 Directive::Commodity(c) => Value::Date(c.date),
2207 Directive::Pad(p) => Value::Date(p.date),
2208 Directive::Event(e) => Value::Date(e.date),
2209 Directive::Query(q) => Value::Date(q.date),
2210 Directive::Note(n) => Value::Date(n.date),
2211 Directive::Document(d) => Value::Date(d.date),
2212 Directive::Price(p) => Value::Date(p.date),
2213 Directive::Custom(c) => Value::Date(c.date),
2214 };
2215
2216 let (flag, payee, narration, tags, links, accounts) =
2217 if let Directive::Transaction(txn) = directive {
2218 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2219 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2220 let mut accounts: Vec<String> = txn
2221 .postings
2222 .iter()
2223 .map(|p| p.account.to_string())
2224 .collect::<std::collections::HashSet<_>>()
2225 .into_iter()
2226 .collect();
2227 accounts.sort(); (
2229 Value::String(txn.flag.to_string()),
2230 txn.payee
2231 .as_ref()
2232 .map_or(Value::Null, |p| Value::String(p.to_string())),
2233 Value::String(txn.narration.to_string()),
2234 Value::StringSet(tags),
2235 Value::StringSet(links),
2236 Value::StringSet(accounts),
2237 )
2238 } else {
2239 (
2240 Value::Null,
2241 Value::Null,
2242 Value::Null,
2243 Value::StringSet(vec![]),
2244 Value::StringSet(vec![]),
2245 Value::StringSet(vec![]),
2246 )
2247 };
2248
2249 let filename = source_loc.map_or(Value::Null, |loc| Value::String(loc.filename.clone()));
2250 let lineno = source_loc.map_or(Value::Null, |loc| Value::Integer(loc.lineno as i64));
2251
2252 vec![
2253 Value::Integer(idx as i64), Value::String(type_name.to_string()),
2255 filename,
2256 lineno,
2257 date,
2258 flag,
2259 payee,
2260 narration,
2261 tags,
2262 links,
2263 accounts,
2264 Self::metadata_to_value(directive.meta()),
2266 ]
2267 }
2268
2269 fn build_postings_table(&self) -> Table {
2273 let columns = vec![
2274 "type".to_string(),
2276 "id".to_string(),
2277 "date".to_string(),
2278 "year".to_string(),
2279 "month".to_string(),
2280 "day".to_string(),
2281 "filename".to_string(),
2282 "lineno".to_string(),
2283 "location".to_string(),
2284 "flag".to_string(),
2286 "payee".to_string(),
2287 "narration".to_string(),
2288 "description".to_string(),
2289 "tags".to_string(),
2290 "links".to_string(),
2291 "posting_flag".to_string(),
2293 "account".to_string(),
2294 "other_accounts".to_string(),
2295 "number".to_string(),
2296 "currency".to_string(),
2297 "cost_number".to_string(),
2298 "cost_currency".to_string(),
2299 "cost_date".to_string(),
2300 "cost_label".to_string(),
2301 "position".to_string(),
2302 "price".to_string(),
2303 "weight".to_string(),
2304 "balance".to_string(),
2305 "meta".to_string(),
2307 "accounts".to_string(),
2308 "_entry_meta".to_string(),
2310 "_posting_meta".to_string(),
2311 ];
2312 let mut table = Table::new(columns);
2313
2314 let mut running_balances: FxHashMap<InternedStr, Inventory> = FxHashMap::default();
2316
2317 let mut transactions: Vec<(usize, &rustledger_core::Transaction)> =
2319 if let Some(spanned) = self.spanned_directives {
2320 spanned
2321 .iter()
2322 .enumerate()
2323 .filter_map(|(idx, s)| {
2324 if let Directive::Transaction(txn) = &s.value {
2325 Some((idx, txn))
2326 } else {
2327 None
2328 }
2329 })
2330 .collect()
2331 } else {
2332 self.directives
2333 .iter()
2334 .enumerate()
2335 .filter_map(|(idx, d)| {
2336 if let Directive::Transaction(txn) = d {
2337 Some((idx, txn))
2338 } else {
2339 None
2340 }
2341 })
2342 .collect()
2343 };
2344 transactions.sort_by_key(|(_, t)| t.date);
2345
2346 for (dir_idx, txn) in &transactions {
2347 let source_loc = self.get_source_location(*dir_idx);
2349 let filename =
2350 source_loc.map_or(Value::Null, |loc| Value::String(loc.filename.clone()));
2351 let lineno = source_loc.map_or(Value::Null, |loc| Value::Integer(loc.lineno as i64));
2352 let location = source_loc.map_or(Value::Null, |loc| {
2353 Value::String(format!("{}:{}", loc.filename, loc.lineno))
2354 });
2355
2356 let tags: Vec<String> = txn.tags.iter().map(ToString::to_string).collect();
2357 let links: Vec<String> = txn.links.iter().map(ToString::to_string).collect();
2358
2359 let mut all_accounts: Vec<String> = txn
2360 .postings
2361 .iter()
2362 .map(|p| p.account.to_string())
2363 .collect::<std::collections::HashSet<_>>()
2364 .into_iter()
2365 .collect();
2366 all_accounts.sort();
2367
2368 let description = match &txn.payee {
2369 Some(payee) => format!("{} | {}", payee, txn.narration),
2370 None => txn.narration.to_string(),
2371 };
2372
2373 let year = Value::Integer(i64::from(txn.date.year()));
2374 let month = Value::Integer(i64::from(txn.date.month()));
2375 let day = Value::Integer(i64::from(txn.date.day()));
2376
2377 for posting in &txn.postings {
2378 if let Some(units) = posting.amount() {
2380 let balance = running_balances.entry(posting.account.clone()).or_default();
2381 let pos = if let Some(cost_spec) = &posting.cost {
2382 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
2383 Position::with_cost(units.clone(), cost)
2384 } else {
2385 Position::simple(units.clone())
2386 }
2387 } else {
2388 Position::simple(units.clone())
2389 };
2390 balance.add(pos);
2391 }
2392
2393 let (number, currency) = posting.amount().map_or((Value::Null, Value::Null), |a| {
2395 (
2396 Value::Number(a.number),
2397 Value::String(a.currency.to_string()),
2398 )
2399 });
2400
2401 let (cost_number, cost_currency, cost_date, cost_label) = if let Some(cost_spec) =
2402 &posting.cost
2403 {
2404 let units = posting.amount();
2405 if let Some(cost) = units.and_then(|u| cost_spec.resolve(u.number, txn.date)) {
2406 (
2407 Value::Number(cost.number),
2408 Value::String(cost.currency.to_string()),
2409 cost.date.map_or(Value::Null, Value::Date),
2410 cost.label
2411 .as_ref()
2412 .map_or(Value::Null, |l| Value::String(l.clone())),
2413 )
2414 } else {
2415 (Value::Null, Value::Null, Value::Null, Value::Null)
2416 }
2417 } else {
2418 (Value::Null, Value::Null, Value::Null, Value::Null)
2419 };
2420
2421 let position_val = if let Some(units) = posting.amount() {
2422 if let Some(cost_spec) = &posting.cost
2423 && let Some(cost) = cost_spec.resolve(units.number, txn.date)
2424 {
2425 Value::Position(Box::new(Position::with_cost(units.clone(), cost)))
2426 } else {
2427 Value::Position(Box::new(Position::simple(units.clone())))
2428 }
2429 } else {
2430 Value::Null
2431 };
2432
2433 let price_val = posting
2434 .price
2435 .as_ref()
2436 .and_then(|p| p.amount())
2437 .map_or(Value::Null, |a| Value::Amount(a.clone()));
2438
2439 let weight_val = if let Some(units) = posting.amount() {
2445 if let Some(cost_spec) = &posting.cost {
2446 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
2447 Value::Amount(Amount::new(units.number * cost.number, cost.currency))
2448 } else {
2449 Value::Amount(units.clone())
2450 }
2451 } else if let Some(price_ann) = &posting.price {
2452 if let Some(price_amt) = price_ann.amount() {
2453 if price_ann.is_unit() {
2454 Value::Amount(Amount::new(
2456 units.number * price_amt.number,
2457 price_amt.currency.clone(),
2458 ))
2459 } else {
2460 Value::Amount(price_amt.clone())
2462 }
2463 } else {
2464 Value::Amount(units.clone())
2465 }
2466 } else {
2467 Value::Amount(units.clone())
2468 }
2469 } else {
2470 Value::Null
2471 };
2472
2473 let balance_val = running_balances
2474 .get(&posting.account)
2475 .map_or(Value::Null, |inv| Value::Inventory(Box::new(inv.clone())));
2476
2477 let other_accounts: Vec<String> = all_accounts
2479 .iter()
2480 .filter(|a| a.as_str() != posting.account.as_ref())
2481 .cloned()
2482 .collect();
2483
2484 let posting_flag = posting
2485 .flag
2486 .map_or(Value::Null, |f| Value::String(f.to_string()));
2487
2488 let row = vec![
2489 Value::String("transaction".to_string()),
2491 Value::Integer(*dir_idx as i64),
2492 Value::Date(txn.date),
2493 year.clone(),
2494 month.clone(),
2495 day.clone(),
2496 filename.clone(),
2497 lineno.clone(),
2498 location.clone(),
2499 Value::String(txn.flag.to_string()),
2501 txn.payee
2502 .as_ref()
2503 .map_or(Value::Null, |p| Value::String(p.to_string())),
2504 Value::String(txn.narration.to_string()),
2505 Value::String(description.clone()),
2506 Value::StringSet(tags.clone()),
2507 Value::StringSet(links.clone()),
2508 posting_flag,
2510 Value::String(posting.account.to_string()),
2511 Value::StringSet(other_accounts),
2512 number,
2513 currency,
2514 cost_number,
2515 cost_currency,
2516 cost_date,
2517 cost_label,
2518 position_val,
2519 price_val,
2520 weight_val,
2521 balance_val,
2522 Value::Metadata(Box::new(posting.meta.clone())),
2524 Value::StringSet(all_accounts.clone()),
2525 Self::metadata_to_value(&txn.meta),
2527 Self::metadata_to_value(&posting.meta),
2528 ];
2529 table.add_row(row);
2530 }
2531 }
2532
2533 table
2534 }
2535}
2536#[cfg(test)]
2537mod tests {
2538 use super::types::{hash_row, hash_single_value};
2539 use super::*;
2540 use crate::parse;
2541 use rust_decimal_macros::dec;
2542 use rustledger_core::Posting;
2543
2544 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
2545 rustledger_core::naive_date(year, month, day).unwrap()
2546 }
2547
2548 fn sample_directives() -> Vec<Directive> {
2549 vec![
2550 Directive::Transaction(
2551 Transaction::new(date(2024, 1, 15), "Coffee")
2552 .with_flag('*')
2553 .with_payee("Coffee Shop")
2554 .with_posting(Posting::new(
2555 "Expenses:Food:Coffee",
2556 Amount::new(dec!(5.00), "USD"),
2557 ))
2558 .with_posting(Posting::new(
2559 "Assets:Bank:Checking",
2560 Amount::new(dec!(-5.00), "USD"),
2561 )),
2562 ),
2563 Directive::Transaction(
2564 Transaction::new(date(2024, 1, 16), "Groceries")
2565 .with_flag('*')
2566 .with_payee("Supermarket")
2567 .with_posting(Posting::new(
2568 "Expenses:Food:Groceries",
2569 Amount::new(dec!(50.00), "USD"),
2570 ))
2571 .with_posting(Posting::new(
2572 "Assets:Bank:Checking",
2573 Amount::new(dec!(-50.00), "USD"),
2574 )),
2575 ),
2576 ]
2577 }
2578
2579 #[test]
2580 fn test_simple_select() {
2581 let directives = sample_directives();
2582 let mut executor = Executor::new(&directives);
2583
2584 let query = parse("SELECT date, account").unwrap();
2585 let result = executor.execute(&query).unwrap();
2586
2587 assert_eq!(result.columns, vec!["date", "account"]);
2588 assert_eq!(result.len(), 4); }
2590
2591 #[test]
2592 fn test_where_clause() {
2593 let directives = sample_directives();
2594 let mut executor = Executor::new(&directives);
2595
2596 let query = parse("SELECT account WHERE account ~ \"Expenses:\"").unwrap();
2597 let result = executor.execute(&query).unwrap();
2598
2599 assert_eq!(result.len(), 2); }
2601
2602 #[test]
2603 fn test_balances() {
2604 let directives = sample_directives();
2605 let mut executor = Executor::new(&directives);
2606
2607 let query = parse("BALANCES").unwrap();
2608 let result = executor.execute(&query).unwrap();
2609
2610 assert_eq!(result.columns, vec!["account", "balance"]);
2611 assert!(result.len() >= 3); }
2613
2614 #[test]
2615 fn test_account_functions() {
2616 let directives = sample_directives();
2617 let mut executor = Executor::new(&directives);
2618
2619 let query = parse("SELECT DISTINCT LEAF(account) WHERE account ~ \"Expenses:\"").unwrap();
2621 let result = executor.execute(&query).unwrap();
2622 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT ROOT(account)").unwrap();
2626 let result = executor.execute(&query).unwrap();
2627 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT PARENT(account) WHERE account ~ \"Expenses:\"").unwrap();
2631 let result = executor.execute(&query).unwrap();
2632 assert!(!result.is_empty()); }
2634
2635 #[test]
2636 fn test_min_max_aggregate() {
2637 let directives = sample_directives();
2638 let mut executor = Executor::new(&directives);
2639
2640 let query = parse("SELECT MIN(date)").unwrap();
2642 let result = executor.execute(&query).unwrap();
2643 assert_eq!(result.len(), 1);
2644 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
2645
2646 let query = parse("SELECT MAX(date)").unwrap();
2648 let result = executor.execute(&query).unwrap();
2649 assert_eq!(result.len(), 1);
2650 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
2651 }
2652
2653 #[test]
2654 fn test_order_by() {
2655 let directives = sample_directives();
2656 let mut executor = Executor::new(&directives);
2657
2658 let query = parse("SELECT date, account ORDER BY date DESC").unwrap();
2659 let result = executor.execute(&query).unwrap();
2660
2661 assert_eq!(result.len(), 4);
2663 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
2665 }
2666
2667 #[test]
2668 fn test_hash_value_all_variants() {
2669 use rustledger_core::{Cost, Inventory, Position};
2670
2671 let values = vec![
2673 Value::String("test".to_string()),
2674 Value::Number(dec!(123.45)),
2675 Value::Integer(42),
2676 Value::Date(date(2024, 1, 15)),
2677 Value::Boolean(true),
2678 Value::Boolean(false),
2679 Value::Amount(Amount::new(dec!(100), "USD")),
2680 Value::Position(Box::new(Position::simple(Amount::new(dec!(10), "AAPL")))),
2681 Value::Position(Box::new(Position::with_cost(
2682 Amount::new(dec!(10), "AAPL"),
2683 Cost::new(dec!(150), "USD"),
2684 ))),
2685 Value::Inventory(Box::new(Inventory::new())),
2686 Value::StringSet(vec!["tag1".to_string(), "tag2".to_string()]),
2687 Value::Null,
2688 ];
2689
2690 for value in &values {
2692 let hash = hash_single_value(value);
2693 assert!(hash != 0 || matches!(value, Value::Null));
2694 }
2695
2696 let hash1 = hash_single_value(&Value::String("a".to_string()));
2698 let hash2 = hash_single_value(&Value::String("b".to_string()));
2699 assert_ne!(hash1, hash2);
2700
2701 let hash3 = hash_single_value(&Value::Integer(42));
2703 let hash4 = hash_single_value(&Value::Integer(42));
2704 assert_eq!(hash3, hash4);
2705 }
2706
2707 #[test]
2708 fn test_hash_row_distinct() {
2709 let row1 = vec![Value::String("a".to_string()), Value::Integer(1)];
2711 let row2 = vec![Value::String("a".to_string()), Value::Integer(1)];
2712 let row3 = vec![Value::String("b".to_string()), Value::Integer(1)];
2713
2714 assert_eq!(hash_row(&row1), hash_row(&row2));
2715 assert_ne!(hash_row(&row1), hash_row(&row3));
2716 }
2717
2718 #[test]
2719 fn test_string_set_hash_order_independent() {
2720 let set1 = Value::StringSet(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
2722 let set2 = Value::StringSet(vec!["c".to_string(), "a".to_string(), "b".to_string()]);
2723 let set3 = Value::StringSet(vec!["b".to_string(), "c".to_string(), "a".to_string()]);
2724
2725 let hash1 = hash_single_value(&set1);
2726 let hash2 = hash_single_value(&set2);
2727 let hash3 = hash_single_value(&set3);
2728
2729 assert_eq!(hash1, hash2);
2730 assert_eq!(hash2, hash3);
2731 }
2732
2733 #[test]
2734 fn test_inventory_hash_includes_cost() {
2735 use rustledger_core::{Cost, Inventory, Position};
2736
2737 let mut inv1 = Inventory::new();
2739 inv1.add(Position::with_cost(
2740 Amount::new(dec!(10), "AAPL"),
2741 Cost::new(dec!(100), "USD"),
2742 ));
2743
2744 let mut inv2 = Inventory::new();
2745 inv2.add(Position::with_cost(
2746 Amount::new(dec!(10), "AAPL"),
2747 Cost::new(dec!(200), "USD"),
2748 ));
2749
2750 let hash1 = hash_single_value(&Value::Inventory(Box::new(inv1)));
2751 let hash2 = hash_single_value(&Value::Inventory(Box::new(inv2)));
2752
2753 assert_ne!(hash1, hash2);
2754 }
2755
2756 #[test]
2757 fn test_distinct_deduplication() {
2758 let directives = sample_directives();
2759 let mut executor = Executor::new(&directives);
2760
2761 let query = parse("SELECT flag").unwrap();
2763 let result = executor.execute(&query).unwrap();
2764 assert_eq!(result.len(), 4); let query = parse("SELECT DISTINCT flag").unwrap();
2768 let result = executor.execute(&query).unwrap();
2769 assert_eq!(result.len(), 1); }
2771
2772 #[test]
2773 fn test_limit_clause() {
2774 let directives = sample_directives();
2775 let mut executor = Executor::new(&directives);
2776
2777 let query = parse("SELECT date, account LIMIT 2").unwrap();
2779 let result = executor.execute(&query).unwrap();
2780 assert_eq!(result.len(), 2);
2781
2782 let query = parse("SELECT date LIMIT 0").unwrap();
2784 let result = executor.execute(&query).unwrap();
2785 assert_eq!(result.len(), 0);
2786
2787 let query = parse("SELECT date LIMIT 100").unwrap();
2789 let result = executor.execute(&query).unwrap();
2790 assert_eq!(result.len(), 4);
2791 }
2792
2793 #[test]
2794 fn test_group_by_with_count() {
2795 let directives = sample_directives();
2796 let mut executor = Executor::new(&directives);
2797
2798 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
2800 let result = executor.execute(&query).unwrap();
2801
2802 assert_eq!(result.columns.len(), 2);
2803 assert_eq!(result.len(), 2);
2805 }
2806
2807 #[test]
2808 fn test_count_aggregate() {
2809 let directives = sample_directives();
2810 let mut executor = Executor::new(&directives);
2811
2812 let query = parse("SELECT COUNT(account)").unwrap();
2814 let result = executor.execute(&query).unwrap();
2815
2816 assert_eq!(result.len(), 1);
2817 assert_eq!(result.rows[0][0], Value::Integer(4));
2818
2819 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
2821 let result = executor.execute(&query).unwrap();
2822 assert_eq!(result.len(), 2); }
2824
2825 #[test]
2826 fn test_count_wildcard_direct() {
2827 let directives = sample_directives();
2829 let mut executor = Executor::new(&directives);
2830
2831 let query = parse("SELECT count(*)").unwrap();
2833 let result = executor.execute(&query).unwrap();
2834 assert_eq!(result.len(), 1);
2835 assert_eq!(result.rows[0][0], Value::Integer(4)); let query = parse("SELECT account, count(*) GROUP BY account").unwrap();
2840 let result = executor.execute(&query).unwrap();
2841 assert_eq!(result.len(), 3); }
2843
2844 #[test]
2845 fn test_count_wildcard_from_postings_table() {
2846 let directives = sample_directives();
2848 let mut executor = Executor::new(&directives);
2849
2850 let query = parse("SELECT account, count(*) FROM postings GROUP BY account").unwrap();
2852 let result = executor.execute(&query).unwrap();
2853 assert_eq!(result.len(), 3);
2855 }
2856
2857 #[test]
2858 fn test_count_wildcard_from_entries_table() {
2859 let directives = sample_directives();
2861 let mut executor = Executor::new(&directives);
2862
2863 let query = parse("SELECT type, count(*) FROM entries GROUP BY type").unwrap();
2864 let result = executor.execute(&query).unwrap();
2865 assert_eq!(result.len(), 1);
2867 assert_eq!(result.rows[0][0], Value::String("transaction".to_string()));
2868 assert_eq!(result.rows[0][1], Value::Integer(2));
2869 }
2870
2871 #[test]
2872 fn test_count_wildcard_having() {
2873 let directives = sample_directives();
2875 let mut executor = Executor::new(&directives);
2876
2877 let query = parse(
2879 "SELECT account, count(*) AS cnt FROM postings GROUP BY account HAVING count(*) > 0",
2880 )
2881 .unwrap();
2882 let result = executor.execute(&query).unwrap();
2883 assert_eq!(result.len(), 3);
2884
2885 let query = parse(
2887 "SELECT account, count(*) AS cnt FROM postings GROUP BY account HAVING count(*) > 1",
2888 )
2889 .unwrap();
2890 let result = executor.execute(&query).unwrap();
2891 assert_eq!(result.len(), 1);
2892 assert_eq!(
2893 result.rows[0][0],
2894 Value::String("Assets:Bank:Checking".to_string())
2895 );
2896 assert_eq!(result.rows[0][1], Value::Integer(2));
2897 }
2898
2899 #[test]
2900 fn test_journal_query() {
2901 let directives = sample_directives();
2902 let mut executor = Executor::new(&directives);
2903
2904 let query = parse("JOURNAL \"Expenses\"").unwrap();
2906 let result = executor.execute(&query).unwrap();
2907
2908 assert!(result.columns.contains(&"account".to_string()));
2910 assert_eq!(result.len(), 2);
2912 }
2913
2914 #[test]
2915 fn test_print_query() {
2916 let directives = sample_directives();
2917 let mut executor = Executor::new(&directives);
2918
2919 let query = parse("PRINT").unwrap();
2921 let result = executor.execute(&query).unwrap();
2922
2923 assert_eq!(result.columns.len(), 1);
2925 assert_eq!(result.columns[0], "directive");
2926 assert_eq!(result.len(), 2);
2928 }
2929
2930 #[test]
2931 fn test_empty_directives() {
2932 let directives: Vec<Directive> = vec![];
2933 let mut executor = Executor::new(&directives);
2934
2935 let query = parse("SELECT date, account").unwrap();
2937 let result = executor.execute(&query).unwrap();
2938 assert!(result.is_empty());
2939
2940 let query = parse("BALANCES").unwrap();
2942 let result = executor.execute(&query).unwrap();
2943 assert!(result.is_empty());
2944 }
2945
2946 #[test]
2947 fn test_comparison_operators() {
2948 let directives = sample_directives();
2949 let mut executor = Executor::new(&directives);
2950
2951 let query = parse("SELECT date WHERE date < 2024-01-16").unwrap();
2953 let result = executor.execute(&query).unwrap();
2954 assert_eq!(result.len(), 2); let query = parse("SELECT date WHERE year > 2023").unwrap();
2958 let result = executor.execute(&query).unwrap();
2959 assert_eq!(result.len(), 4); let query = parse("SELECT account WHERE day = 15").unwrap();
2963 let result = executor.execute(&query).unwrap();
2964 assert_eq!(result.len(), 2); }
2966
2967 #[test]
2968 fn test_logical_operators() {
2969 let directives = sample_directives();
2970 let mut executor = Executor::new(&directives);
2971
2972 let query = parse("SELECT account WHERE account ~ \"Expenses\" AND day > 14").unwrap();
2974 let result = executor.execute(&query).unwrap();
2975 assert_eq!(result.len(), 2); let query = parse("SELECT account WHERE day = 15 OR day = 16").unwrap();
2979 let result = executor.execute(&query).unwrap();
2980 assert_eq!(result.len(), 4); }
2982
2983 #[test]
2984 fn test_arithmetic_expressions() {
2985 let directives = sample_directives();
2986 let mut executor = Executor::new(&directives);
2987
2988 let query = parse("SELECT -day WHERE day = 15").unwrap();
2990 let result = executor.execute(&query).unwrap();
2991 assert_eq!(result.len(), 2);
2992 for row in &result.rows {
2994 if let Value::Integer(n) = &row[0] {
2995 assert_eq!(*n, -15);
2996 }
2997 }
2998 }
2999
3000 #[test]
3001 fn test_first_last_aggregates() {
3002 let directives = sample_directives();
3003 let mut executor = Executor::new(&directives);
3004
3005 let query = parse("SELECT FIRST(date)").unwrap();
3007 let result = executor.execute(&query).unwrap();
3008 assert_eq!(result.len(), 1);
3009 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
3010
3011 let query = parse("SELECT LAST(date)").unwrap();
3013 let result = executor.execute(&query).unwrap();
3014 assert_eq!(result.len(), 1);
3015 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
3016 }
3017
3018 #[test]
3019 fn test_wildcard_select() {
3020 let directives = sample_directives();
3021 let mut executor = Executor::new(&directives);
3022
3023 let query = parse("SELECT *").unwrap();
3025 let result = executor.execute(&query).unwrap();
3026
3027 assert_eq!(
3029 result.columns,
3030 vec!["date", "flag", "payee", "narration", "account", "position"]
3031 );
3032 assert_eq!(result.len(), 4);
3034 assert_eq!(result.rows[0].len(), 6);
3035 }
3036
3037 #[test]
3038 fn test_wildcard_alias_rejected() {
3039 let directives = sample_directives();
3040 let mut executor = Executor::new(&directives);
3041
3042 let query = parse("SELECT * AS data").unwrap();
3044 let result = executor.execute(&query);
3045
3046 assert!(result.is_err());
3047 let err = result.unwrap_err();
3048 assert!(
3049 err.to_string().contains("Cannot alias wildcard"),
3050 "Expected wildcard alias error, got: {err}"
3051 );
3052 }
3053
3054 #[test]
3055 fn test_query_result_methods() {
3056 let mut result = QueryResult::new(vec!["col1".to_string(), "col2".to_string()]);
3057
3058 assert!(result.is_empty());
3060 assert_eq!(result.len(), 0);
3061
3062 result.add_row(vec![Value::Integer(1), Value::String("a".to_string())]);
3064 assert!(!result.is_empty());
3065 assert_eq!(result.len(), 1);
3066
3067 result.add_row(vec![Value::Integer(2), Value::String("b".to_string())]);
3068 assert_eq!(result.len(), 2);
3069 }
3070
3071 #[test]
3072 fn test_type_cast_functions() {
3073 let directives = sample_directives();
3074 let mut executor = Executor::new(&directives);
3075
3076 let query = parse("SELECT int(5.7)").unwrap();
3078 let result = executor.execute(&query).unwrap();
3079 assert_eq!(result.rows[0][0], Value::Integer(5));
3080
3081 let query = parse("SELECT decimal(42)").unwrap();
3083 let result = executor.execute(&query).unwrap();
3084 assert_eq!(result.rows[0][0], Value::Number(dec!(42)));
3085
3086 let query = parse("SELECT str(123)").unwrap();
3088 let result = executor.execute(&query).unwrap();
3089 assert_eq!(result.rows[0][0], Value::String("123".to_string()));
3090
3091 let query = parse("SELECT bool(1)").unwrap();
3093 let result = executor.execute(&query).unwrap();
3094 assert_eq!(result.rows[0][0], Value::Boolean(true));
3095
3096 let query = parse("SELECT bool(0)").unwrap();
3097 let result = executor.execute(&query).unwrap();
3098 assert_eq!(result.rows[0][0], Value::Boolean(false));
3099 }
3100
3101 #[test]
3103 fn test_type_casting_in_aggregate_context() {
3104 let txn1 = Transaction::new(date(2024, 1, 15), "Item 1")
3105 .with_flag('*')
3106 .with_posting(Posting::new("Expenses:Food", Amount::new(dec!(10), "USD")))
3107 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-10), "USD")));
3108
3109 let txn2 = Transaction::new(date(2024, 1, 16), "Item 2")
3110 .with_flag('*')
3111 .with_posting(Posting::new("Expenses:Food", Amount::new(dec!(20), "USD")))
3112 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-20), "USD")));
3113
3114 let directives = vec![Directive::Transaction(txn1), Directive::Transaction(txn2)];
3115 let mut executor = Executor::new(&directives);
3116
3117 let query =
3120 parse("SELECT account, str(sum(number(units))) GROUP BY account ORDER BY account")
3121 .unwrap();
3122 let result = executor.execute(&query).unwrap();
3123 assert_eq!(result.rows.len(), 2);
3124 assert_eq!(result.rows[0][0], Value::String("Assets:Cash".to_string()));
3126 assert_eq!(result.rows[0][1], Value::String("-30".to_string()));
3127 assert_eq!(
3128 result.rows[1][0],
3129 Value::String("Expenses:Food".to_string())
3130 );
3131 assert_eq!(result.rows[1][1], Value::String("30".to_string()));
3132
3133 let query =
3135 parse("SELECT account, int(sum(number(units))) GROUP BY account ORDER BY account")
3136 .unwrap();
3137 let result = executor.execute(&query).unwrap();
3138 assert_eq!(result.rows[0][1], Value::Integer(-30));
3139 assert_eq!(result.rows[1][1], Value::Integer(30));
3140
3141 let query =
3143 parse("SELECT account, decimal(count(*)) GROUP BY account ORDER BY account").unwrap();
3144 let result = executor.execute(&query).unwrap();
3145 assert_eq!(result.rows[0][1], Value::Number(dec!(2))); assert_eq!(result.rows[1][1], Value::Number(dec!(2)));
3147
3148 let query =
3150 parse("SELECT account, bool(count(*)) GROUP BY account ORDER BY account").unwrap();
3151 let result = executor.execute(&query).unwrap();
3152 assert_eq!(result.rows[0][1], Value::Boolean(true));
3153 assert_eq!(result.rows[1][1], Value::Boolean(true));
3154 }
3155
3156 #[test]
3158 fn test_int_truncation() {
3159 let directives = sample_directives();
3160 let mut executor = Executor::new(&directives);
3161
3162 let query = parse("SELECT int(5.7)").unwrap();
3164 let result = executor.execute(&query).unwrap();
3165 assert_eq!(result.rows[0][0], Value::Integer(5));
3166
3167 let query = parse("SELECT int(-5.7)").unwrap();
3168 let result = executor.execute(&query).unwrap();
3169 assert_eq!(result.rows[0][0], Value::Integer(-5));
3170
3171 let query = parse("SELECT int(0.999)").unwrap();
3172 let result = executor.execute(&query).unwrap();
3173 assert_eq!(result.rows[0][0], Value::Integer(0));
3174 }
3175
3176 #[test]
3178 fn test_type_casting_errors() {
3179 let directives = sample_directives();
3180 let mut executor = Executor::new(&directives);
3181
3182 let query = parse("SELECT int('not-a-number')").unwrap();
3184 let result = executor.execute(&query);
3185 assert!(result.is_err());
3186 assert!(
3187 result
3188 .unwrap_err()
3189 .to_string()
3190 .contains("cannot parse 'not-a-number'")
3191 );
3192
3193 let query = parse("SELECT decimal('invalid')").unwrap();
3195 let result = executor.execute(&query);
3196 assert!(result.is_err());
3197 assert!(result.unwrap_err().to_string().contains("cannot parse"));
3198
3199 let query = parse("SELECT bool('maybe')").unwrap();
3201 let result = executor.execute(&query);
3202 assert!(result.is_err());
3203 assert!(result.unwrap_err().to_string().contains("cannot parse"));
3204 }
3205
3206 #[test]
3207 fn test_meta_functions() {
3208 let mut txn_meta: Metadata = Metadata::default();
3210 txn_meta.insert(
3211 "source".to_string(),
3212 MetaValue::String("bank_import".to_string()),
3213 );
3214
3215 let mut posting_meta: Metadata = Metadata::default();
3216 posting_meta.insert(
3217 "category".to_string(),
3218 MetaValue::String("food".to_string()),
3219 );
3220
3221 let txn = Transaction {
3222 date: date(2024, 1, 15),
3223 flag: '*',
3224 payee: Some("Coffee Shop".into()),
3225 narration: "Coffee".into(),
3226 tags: vec![],
3227 links: vec![],
3228 meta: txn_meta,
3229 postings: vec![
3230 Posting {
3231 account: "Expenses:Food".into(),
3232 units: Some(rustledger_core::IncompleteAmount::Complete(Amount::new(
3233 dec!(5),
3234 "USD",
3235 ))),
3236 cost: None,
3237 price: None,
3238 flag: None,
3239 meta: posting_meta,
3240 comments: Vec::new(),
3241 trailing_comments: Vec::new(),
3242 },
3243 Posting::new("Assets:Cash", Amount::new(dec!(-5), "USD")),
3244 ],
3245 trailing_comments: Vec::new(),
3246 };
3247
3248 let directives = vec![Directive::Transaction(txn)];
3249 let mut executor = Executor::new(&directives);
3250
3251 let query = parse("SELECT meta('category') WHERE account ~ 'Expenses'").unwrap();
3253 let result = executor.execute(&query).unwrap();
3254 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
3255
3256 let query = parse("SELECT entry_meta('source') WHERE account ~ 'Expenses'").unwrap();
3258 let result = executor.execute(&query).unwrap();
3259 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
3260
3261 let query = parse("SELECT any_meta('source') WHERE account ~ 'Expenses'").unwrap();
3263 let result = executor.execute(&query).unwrap();
3264 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
3265
3266 let query = parse("SELECT any_meta('category') WHERE account ~ 'Expenses'").unwrap();
3268 let result = executor.execute(&query).unwrap();
3269 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
3270
3271 let query = parse("SELECT meta('nonexistent') WHERE account ~ 'Expenses'").unwrap();
3273 let result = executor.execute(&query).unwrap();
3274 assert_eq!(result.rows[0][0], Value::Null);
3275 }
3276
3277 #[test]
3278 fn test_convert_function() {
3279 let price = rustledger_core::Price {
3281 date: date(2024, 1, 1),
3282 currency: "EUR".into(),
3283 amount: Amount::new(dec!(1.10), "USD"),
3284 meta: Metadata::default(),
3285 };
3286
3287 let txn = Transaction::new(date(2024, 1, 15), "Test")
3288 .with_flag('*')
3289 .with_posting(Posting::new("Assets:Euro", Amount::new(dec!(100), "EUR")))
3290 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-110), "USD")));
3291
3292 let directives = vec![Directive::Price(price), Directive::Transaction(txn)];
3293 let mut executor = Executor::new(&directives);
3294
3295 let query = parse("SELECT convert(position, 'USD') WHERE account ~ 'Euro'").unwrap();
3297 let result = executor.execute(&query).unwrap();
3298 match &result.rows[0][0] {
3300 Value::Amount(a) => {
3301 assert_eq!(a.number, dec!(110));
3302 assert_eq!(a.currency.as_ref(), "USD");
3303 }
3304 _ => panic!("Expected Amount, got {:?}", result.rows[0][0]),
3305 }
3306 }
3307
3308 #[test]
3309 fn test_date_functions() {
3310 let directives = sample_directives();
3311 let mut executor = Executor::new(&directives);
3312
3313 let query = parse("SELECT date('2024-06-15')").unwrap();
3315 let result = executor.execute(&query).unwrap();
3316 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3317
3318 let query = parse("SELECT date(2024, 6, 15)").unwrap();
3320 let result = executor.execute(&query).unwrap();
3321 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3322
3323 let query = parse("SELECT date_diff(date('2024-01-20'), date('2024-01-15'))").unwrap();
3325 let result = executor.execute(&query).unwrap();
3326 assert_eq!(result.rows[0][0], Value::Integer(5));
3327
3328 let query = parse("SELECT date_add(date('2024-01-15'), 10)").unwrap();
3330 let result = executor.execute(&query).unwrap();
3331 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 25)));
3332
3333 let query = parse("SELECT date_trunc('year', date('2024-06-15'))").unwrap();
3335 let result = executor.execute(&query).unwrap();
3336 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1)));
3337
3338 let query = parse("SELECT date_trunc('month', date('2024-06-15'))").unwrap();
3340 let result = executor.execute(&query).unwrap();
3341 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1)));
3342
3343 let query = parse("SELECT date_part('month', date('2024-06-15'))").unwrap();
3345 let result = executor.execute(&query).unwrap();
3346 assert_eq!(result.rows[0][0], Value::Integer(6));
3347
3348 let query = parse("SELECT parse_date('15/06/2024', '%d/%m/%Y')").unwrap();
3350 let result = executor.execute(&query).unwrap();
3351 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
3352
3353 let query =
3355 parse("SELECT date_bin('7 days', date('2024-01-15'), date('2024-01-01'))").unwrap();
3356 let result = executor.execute(&query).unwrap();
3357 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
3361 parse("SELECT date_bin('1 week', date('2024-01-20'), date('2024-01-01'))").unwrap();
3362 let result = executor.execute(&query).unwrap();
3363 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
3367 parse("SELECT date_bin('1 month', date('2024-06-15'), date('2024-01-01'))").unwrap();
3368 let result = executor.execute(&query).unwrap();
3369 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1))); let query =
3373 parse("SELECT date_bin('1 year', date('2024-06-15'), date('2020-01-01'))").unwrap();
3374 let result = executor.execute(&query).unwrap();
3375 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1))); }
3377
3378 #[test]
3379 fn test_string_functions_extended() {
3380 let directives = sample_directives();
3381 let mut executor = Executor::new(&directives);
3382
3383 let query = parse("SELECT grep('Ex[a-z]+', 'Hello Expenses World')").unwrap();
3385 let result = executor.execute(&query).unwrap();
3386 assert_eq!(result.rows[0][0], Value::String("Expenses".to_string()));
3387
3388 let query = parse("SELECT grep('xyz', 'Hello World')").unwrap();
3390 let result = executor.execute(&query).unwrap();
3391 assert_eq!(result.rows[0][0], Value::Null);
3392
3393 let query = parse("SELECT grepn('([0-9]+)-([0-9]+)', '2024-01', 1)").unwrap();
3395 let result = executor.execute(&query).unwrap();
3396 assert_eq!(result.rows[0][0], Value::String("2024".to_string()));
3397
3398 let query = parse("SELECT subst('-', '/', '2024-01-15')").unwrap();
3400 let result = executor.execute(&query).unwrap();
3401 assert_eq!(result.rows[0][0], Value::String("2024/01/15".to_string()));
3402
3403 let query = parse("SELECT splitcomp('a:b:c', ':', 1)").unwrap();
3405 let result = executor.execute(&query).unwrap();
3406 assert_eq!(result.rows[0][0], Value::String("b".to_string()));
3407
3408 let query = parse("SELECT joinstr('hello', 'world')").unwrap();
3410 let result = executor.execute(&query).unwrap();
3411 assert_eq!(result.rows[0][0], Value::String("hello, world".to_string()));
3412
3413 let query = parse("SELECT maxwidth('hello', 10)").unwrap();
3415 let result = executor.execute(&query).unwrap();
3416 assert_eq!(result.rows[0][0], Value::String("hello".to_string()));
3417
3418 let query = parse("SELECT maxwidth('hello world', 8)").unwrap();
3420 let result = executor.execute(&query).unwrap();
3421 assert_eq!(result.rows[0][0], Value::String("hello...".to_string()));
3422 }
3423
3424 #[test]
3425 fn test_inventory_functions() {
3426 let directives = sample_directives();
3427 let mut executor = Executor::new(&directives);
3428
3429 let query = parse("SELECT empty(sum(position)) WHERE account ~ 'Assets'").unwrap();
3432 let result = executor.execute(&query).unwrap();
3433 assert!(matches!(result.rows[0][0], Value::Boolean(_)));
3435
3436 let query = parse("SELECT possign(100, 'Assets:Bank')").unwrap();
3441 let result = executor.execute(&query).unwrap();
3442 assert_eq!(
3443 result.rows[0][0],
3444 Value::Number(rust_decimal::Decimal::from(100))
3445 );
3446
3447 let query = parse("SELECT possign(100, 'Income:Salary')").unwrap();
3449 let result = executor.execute(&query).unwrap();
3450 assert_eq!(
3451 result.rows[0][0],
3452 Value::Number(rust_decimal::Decimal::from(-100))
3453 );
3454
3455 let query = parse("SELECT possign(50, 'Expenses:Food')").unwrap();
3457 let result = executor.execute(&query).unwrap();
3458 assert_eq!(
3459 result.rows[0][0],
3460 Value::Number(rust_decimal::Decimal::from(50))
3461 );
3462
3463 let query = parse("SELECT possign(200, 'Liabilities:CreditCard')").unwrap();
3465 let result = executor.execute(&query).unwrap();
3466 assert_eq!(
3467 result.rows[0][0],
3468 Value::Number(rust_decimal::Decimal::from(-200))
3469 );
3470
3471 let query = parse("SELECT possign(300, 'Equity:OpeningBalances')").unwrap();
3473 let result = executor.execute(&query).unwrap();
3474 assert_eq!(
3475 result.rows[0][0],
3476 Value::Number(rust_decimal::Decimal::from(-300))
3477 );
3478 }
3479
3480 #[test]
3481 fn test_account_meta_functions() {
3482 use rustledger_core::{Close, Metadata, Open};
3483
3484 let mut open_meta = Metadata::default();
3486 open_meta.insert(
3487 "category".to_string(),
3488 MetaValue::String("checking".to_string()),
3489 );
3490
3491 let directives = vec![
3492 Directive::Open(Open {
3493 date: date(2020, 1, 1),
3494 account: "Assets:Bank:Checking".into(),
3495 currencies: vec![],
3496 booking: None,
3497 meta: open_meta,
3498 }),
3499 Directive::Open(Open::new(date(2020, 2, 15), "Expenses:Food")),
3500 Directive::Close(Close::new(date(2024, 12, 31), "Assets:Bank:Checking")),
3501 Directive::Transaction(
3503 Transaction::new(date(2024, 1, 15), "Coffee")
3504 .with_posting(Posting::new(
3505 "Expenses:Food",
3506 Amount::new(dec!(5.00), "USD"),
3507 ))
3508 .with_posting(Posting::new(
3509 "Assets:Bank:Checking",
3510 Amount::new(dec!(-5.00), "USD"),
3511 )),
3512 ),
3513 ];
3514
3515 let mut executor = Executor::new(&directives);
3516
3517 let query = parse("SELECT open_date('Assets:Bank:Checking')").unwrap();
3519 let result = executor.execute(&query).unwrap();
3520 assert_eq!(result.rows[0][0], Value::Date(date(2020, 1, 1)));
3521
3522 let query = parse("SELECT close_date('Assets:Bank:Checking')").unwrap();
3524 let result = executor.execute(&query).unwrap();
3525 assert_eq!(result.rows[0][0], Value::Date(date(2024, 12, 31)));
3526
3527 let query = parse("SELECT close_date('Expenses:Food')").unwrap();
3529 let result = executor.execute(&query).unwrap();
3530 assert_eq!(result.rows[0][0], Value::Null);
3531
3532 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'category')").unwrap();
3534 let result = executor.execute(&query).unwrap();
3535 assert_eq!(result.rows[0][0], Value::String("checking".to_string()));
3536
3537 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'nonexistent')").unwrap();
3539 let result = executor.execute(&query).unwrap();
3540 assert_eq!(result.rows[0][0], Value::Null);
3541
3542 let query = parse("SELECT open_date('NonExistent:Account')").unwrap();
3544 let result = executor.execute(&query).unwrap();
3545 assert_eq!(result.rows[0][0], Value::Null);
3546 }
3547
3548 #[test]
3549 fn test_source_location_columns_return_null_without_sources() {
3550 let directives = vec![Directive::Transaction(Transaction {
3553 date: rustledger_core::naive_date(2024, 1, 15).unwrap(),
3554 flag: '*',
3555 payee: Some("Test".into()),
3556 narration: "Test transaction".into(),
3557 tags: vec![],
3558 links: vec![],
3559 meta: Metadata::default(),
3560 postings: vec![
3561 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
3562 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
3563 ],
3564 trailing_comments: Vec::new(),
3565 })];
3566
3567 let mut executor = Executor::new(&directives);
3568
3569 let query = parse("SELECT filename").unwrap();
3571 let result = executor.execute(&query).unwrap();
3572 assert_eq!(result.rows[0][0], Value::Null);
3573
3574 let query = parse("SELECT lineno").unwrap();
3576 let result = executor.execute(&query).unwrap();
3577 assert_eq!(result.rows[0][0], Value::Null);
3578
3579 let query = parse("SELECT location").unwrap();
3581 let result = executor.execute(&query).unwrap();
3582 assert_eq!(result.rows[0][0], Value::Null);
3583 }
3584
3585 #[test]
3586 fn test_source_location_columns_with_sources() {
3587 use rustledger_loader::SourceMap;
3588 use rustledger_parser::Spanned;
3589 use std::sync::Arc;
3590
3591 let mut source_map = SourceMap::new();
3593 let source: Arc<str> =
3594 "2024-01-15 * \"Test\"\n Assets:Bank 100 USD\n Expenses:Food".into();
3595 let file_id = source_map.add_file("test.beancount".into(), source);
3596
3597 let txn = Transaction {
3599 date: rustledger_core::naive_date(2024, 1, 15).unwrap(),
3600 flag: '*',
3601 payee: Some("Test".into()),
3602 narration: "Test transaction".into(),
3603 tags: vec![],
3604 links: vec![],
3605 meta: Metadata::default(),
3606 postings: vec![
3607 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
3608 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
3609 ],
3610 trailing_comments: Vec::new(),
3611 };
3612
3613 let spanned_directives = vec![Spanned {
3614 value: Directive::Transaction(txn),
3615 span: rustledger_parser::Span { start: 0, end: 50 },
3616 file_id: file_id as u16,
3617 }];
3618
3619 let mut executor = Executor::new_with_sources(&spanned_directives, &source_map);
3620
3621 let query = parse("SELECT filename").unwrap();
3623 let result = executor.execute(&query).unwrap();
3624 assert_eq!(
3625 result.rows[0][0],
3626 Value::String("test.beancount".to_string())
3627 );
3628
3629 let query = parse("SELECT lineno").unwrap();
3631 let result = executor.execute(&query).unwrap();
3632 assert_eq!(result.rows[0][0], Value::Integer(1));
3633
3634 let query = parse("SELECT location").unwrap();
3636 let result = executor.execute(&query).unwrap();
3637 assert_eq!(
3638 result.rows[0][0],
3639 Value::String("test.beancount:1".to_string())
3640 );
3641 }
3642
3643 #[test]
3644 fn test_interval_function() {
3645 let directives = sample_directives();
3646 let mut executor = Executor::new(&directives);
3647
3648 let query = parse("SELECT interval('month')").unwrap();
3650 let result = executor.execute(&query).unwrap();
3651 assert_eq!(
3652 result.rows[0][0],
3653 Value::Interval(Interval::new(1, IntervalUnit::Month))
3654 );
3655
3656 let query = parse("SELECT interval(3, 'day')").unwrap();
3658 let result = executor.execute(&query).unwrap();
3659 assert_eq!(
3660 result.rows[0][0],
3661 Value::Interval(Interval::new(3, IntervalUnit::Day))
3662 );
3663
3664 let query = parse("SELECT interval(-2, 'week')").unwrap();
3666 let result = executor.execute(&query).unwrap();
3667 assert_eq!(
3668 result.rows[0][0],
3669 Value::Interval(Interval::new(-2, IntervalUnit::Week))
3670 );
3671 }
3672
3673 #[test]
3674 fn test_date_add_with_interval() {
3675 let directives = sample_directives();
3676 let mut executor = Executor::new(&directives);
3677
3678 let query = parse("SELECT date_add(date(2024, 1, 15), interval(1, 'month'))").unwrap();
3680 let result = executor.execute(&query).unwrap();
3681 assert_eq!(
3682 result.rows[0][0],
3683 Value::Date(rustledger_core::naive_date(2024, 2, 15).unwrap())
3684 );
3685
3686 let query = parse("SELECT date(2024, 1, 15) + interval(1, 'year')").unwrap();
3688 let result = executor.execute(&query).unwrap();
3689 assert_eq!(
3690 result.rows[0][0],
3691 Value::Date(rustledger_core::naive_date(2025, 1, 15).unwrap())
3692 );
3693
3694 let query = parse("SELECT date(2024, 3, 15) - interval(2, 'month')").unwrap();
3696 let result = executor.execute(&query).unwrap();
3697 assert_eq!(
3698 result.rows[0][0],
3699 Value::Date(rustledger_core::naive_date(2024, 1, 15).unwrap())
3700 );
3701 }
3702}