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