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;
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
62impl<'a> Executor<'a> {
63 pub fn new(directives: &'a [Directive]) -> Self {
65 let price_db = crate::price::PriceDatabase::from_directives(directives);
66
67 let mut account_info: FxHashMap<String, AccountInfo> = FxHashMap::default();
69 for directive in directives {
70 match directive {
71 Directive::Open(open) => {
72 let account = open.account.to_string();
73 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
74 open_date: None,
75 close_date: None,
76 open_meta: Metadata::default(),
77 });
78 info.open_date = Some(open.date);
79 info.open_meta.clone_from(&open.meta);
80 }
81 Directive::Close(close) => {
82 let account = close.account.to_string();
83 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
84 open_date: None,
85 close_date: None,
86 open_meta: Metadata::default(),
87 });
88 info.close_date = Some(close.date);
89 }
90 _ => {}
91 }
92 }
93
94 Self {
95 directives,
96 spanned_directives: None,
97 balances: FxHashMap::default(),
98 price_db,
99 target_currency: None,
100 query_date: chrono::Local::now().date_naive(),
101 regex_cache: RwLock::new(FxHashMap::default()),
102 account_info,
103 source_locations: None,
104 tables: FxHashMap::default(),
105 }
106 }
107
108 pub fn new_with_sources(
113 spanned_directives: &'a [Spanned<Directive>],
114 source_map: &SourceMap,
115 ) -> Self {
116 let mut price_db = crate::price::PriceDatabase::new();
118 for spanned in spanned_directives {
119 if let Directive::Price(p) = &spanned.value {
120 price_db.add_price(p);
121 }
122 }
123
124 let source_locations: Vec<SourceLocation> = spanned_directives
126 .iter()
127 .map(|spanned| {
128 let file = source_map.get(spanned.file_id as usize);
129 let (line, _col) = file.map_or((0, 0), |f| f.line_col(spanned.span.start));
130 SourceLocation {
131 filename: file.map_or_else(String::new, |f| f.path.display().to_string()),
132 lineno: line,
133 }
134 })
135 .collect();
136
137 let mut account_info: FxHashMap<String, AccountInfo> = FxHashMap::default();
139 for spanned in spanned_directives {
140 match &spanned.value {
141 Directive::Open(open) => {
142 let account = open.account.to_string();
143 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
144 open_date: None,
145 close_date: None,
146 open_meta: Metadata::default(),
147 });
148 info.open_date = Some(open.date);
149 info.open_meta.clone_from(&open.meta);
150 }
151 Directive::Close(close) => {
152 let account = close.account.to_string();
153 let info = account_info.entry(account).or_insert_with(|| AccountInfo {
154 open_date: None,
155 close_date: None,
156 open_meta: Metadata::default(),
157 });
158 info.close_date = Some(close.date);
159 }
160 _ => {}
161 }
162 }
163
164 Self {
165 directives: &[], spanned_directives: Some(spanned_directives),
167 balances: FxHashMap::default(),
168 price_db,
169 target_currency: None,
170 query_date: chrono::Local::now().date_naive(),
171 regex_cache: RwLock::new(FxHashMap::default()),
172 account_info,
173 source_locations: Some(source_locations),
174 tables: FxHashMap::default(),
175 }
176 }
177
178 fn get_source_location(&self, directive_index: usize) -> Option<&SourceLocation> {
180 self.source_locations
181 .as_ref()
182 .and_then(|locs| locs.get(directive_index))
183 }
184
185 fn get_or_compile_regex(&self, pattern: &str) -> Option<Regex> {
190 {
192 let cache = match self.regex_cache.read() {
195 Ok(guard) => guard,
196 Err(poisoned) => poisoned.into_inner(),
197 };
198 if let Some(cached) = cache.get(pattern) {
199 return cached.clone();
200 }
201 }
202 let compiled = Regex::new(pattern).ok();
204 let mut cache = match self.regex_cache.write() {
205 Ok(guard) => guard,
206 Err(poisoned) => poisoned.into_inner(),
207 };
208 if let Some(cached) = cache.get(pattern) {
210 return cached.clone();
211 }
212 cache.insert(pattern.to_string(), compiled.clone());
213 compiled
214 }
215
216 fn require_regex(&self, pattern: &str) -> Result<Regex, QueryError> {
218 self.get_or_compile_regex(pattern)
219 .ok_or_else(|| QueryError::Type(format!("invalid regex: {pattern}")))
220 }
221
222 pub fn set_target_currency(&mut self, currency: impl Into<String>) {
224 self.target_currency = Some(currency.into());
225 }
226
227 pub fn execute(&mut self, query: &Query) -> Result<QueryResult, QueryError> {
240 match query {
241 Query::Select(select) => self.execute_select(select),
242 Query::Journal(journal) => self.execute_journal(journal),
243 Query::Balances(balances) => self.execute_balances(balances),
244 Query::Print(print) => self.execute_print(print),
245 Query::CreateTable(create) => self.execute_create_table(create),
246 Query::Insert(insert) => self.execute_insert(insert),
247 }
248 }
249
250 fn build_balances_with_filter(&mut self, from: Option<&FromClause>) -> Result<(), QueryError> {
252 for directive in self.directives {
253 if let Directive::Transaction(txn) = directive {
254 if let Some(from_clause) = from
256 && let Some(filter) = &from_clause.filter
257 && !self.evaluate_from_filter(filter, txn)?
258 {
259 continue;
260 }
261
262 for posting in &txn.postings {
263 if let Some(units) = posting.amount() {
264 let balance = self.balances.entry(posting.account.clone()).or_default();
265
266 let pos = if let Some(cost_spec) = &posting.cost {
267 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
268 Position::with_cost(units.clone(), cost)
269 } else {
270 Position::simple(units.clone())
271 }
272 } else {
273 Position::simple(units.clone())
274 };
275 balance.add(pos);
276 }
277 }
278 }
279 }
280 Ok(())
281 }
282
283 fn collect_postings(
285 &self,
286 from: Option<&FromClause>,
287 where_clause: Option<&Expr>,
288 ) -> Result<Vec<PostingContext<'a>>, QueryError> {
289 let mut postings = Vec::new();
290 let mut running_balances: FxHashMap<InternedStr, Inventory> = FxHashMap::default();
292
293 let directive_iter: Vec<(usize, &Directive)> =
296 if let Some(spanned) = self.spanned_directives {
297 spanned
298 .iter()
299 .enumerate()
300 .map(|(i, s)| (i, &s.value))
301 .collect()
302 } else {
303 self.directives.iter().enumerate().collect()
304 };
305
306 for (directive_index, directive) in directive_iter {
307 if let Directive::Transaction(txn) = directive {
308 if let Some(from) = from {
310 if let Some(open_date) = from.open_on
312 && txn.date < open_date
313 {
314 for posting in &txn.postings {
316 if let Some(units) = posting.amount() {
317 let balance =
318 running_balances.entry(posting.account.clone()).or_default();
319 balance.add(Position::simple(units.clone()));
320 }
321 }
322 continue;
323 }
324 if let Some(close_date) = from.close_on
325 && txn.date > close_date
326 {
327 continue;
328 }
329 if let Some(filter) = &from.filter
331 && !self.evaluate_from_filter(filter, txn)?
332 {
333 continue;
334 }
335 }
336
337 for (i, posting) in txn.postings.iter().enumerate() {
339 if let Some(units) = posting.amount() {
341 let balance = running_balances.entry(posting.account.clone()).or_default();
342 balance.add(Position::simple(units.clone()));
343 }
344
345 let ctx = PostingContext {
346 transaction: txn,
347 posting_index: i,
348 balance: running_balances.get(&posting.account).cloned(),
349 directive_index: Some(directive_index),
350 };
351
352 if let Some(where_expr) = where_clause {
354 if self.evaluate_predicate(where_expr, &ctx)? {
355 postings.push(ctx);
356 }
357 } else {
358 postings.push(ctx);
359 }
360 }
361 }
362 }
363
364 Ok(postings)
365 }
366 fn evaluate_function(
367 &self,
368 func: &FunctionCall,
369 ctx: &PostingContext,
370 ) -> Result<Value, QueryError> {
371 let name = func.name.to_uppercase();
372 match name.as_str() {
373 "YEAR" | "MONTH" | "DAY" | "WEEKDAY" | "QUARTER" | "YMONTH" | "TODAY" => {
375 self.eval_date_function(&name, func, ctx)
376 }
377 "DATE" | "DATE_DIFF" | "DATE_ADD" | "DATE_TRUNC" | "DATE_PART" | "PARSE_DATE"
379 | "DATE_BIN" | "INTERVAL" => self.eval_extended_date_function(&name, func, ctx),
380 "LENGTH" | "UPPER" | "LOWER" | "SUBSTR" | "SUBSTRING" | "TRIM" | "STARTSWITH"
382 | "ENDSWITH" | "GREP" | "GREPN" | "SUBST" | "SPLITCOMP" | "JOINSTR" | "MAXWIDTH" => {
383 self.eval_string_function(&name, func, ctx)
384 }
385 "PARENT" | "LEAF" | "ROOT" | "ACCOUNT_DEPTH" | "ACCOUNT_SORTKEY" => {
387 self.eval_account_function(&name, func, ctx)
388 }
389 "OPEN_DATE" | "CLOSE_DATE" | "OPEN_META" => {
391 self.eval_account_meta_function(&name, func, ctx)
392 }
393 "ABS" | "NEG" | "ROUND" | "SAFEDIV" => self.eval_math_function(&name, func, ctx),
395 "NUMBER" | "CURRENCY" | "GETITEM" | "GET" | "UNITS" | "COST" | "WEIGHT" | "VALUE" => {
397 self.eval_position_function(&name, func, ctx)
398 }
399 "EMPTY" | "FILTER_CURRENCY" | "POSSIGN" => {
401 self.eval_inventory_function(&name, func, ctx)
402 }
403 "GETPRICE" => self.eval_getprice(func, ctx),
405 "COALESCE" => self.eval_coalesce(func, ctx),
407 "ONLY" => self.eval_only(func, ctx),
408 "META" | "ENTRY_META" | "ANY_META" | "POSTING_META" => {
410 self.eval_meta_function(&name, func, ctx)
411 }
412 "CONVERT" => self.eval_convert(func, ctx),
414 "INT" => self.eval_int(func, ctx),
416 "DECIMAL" => self.eval_decimal(func, ctx),
417 "STR" => self.eval_str(func, ctx),
418 "BOOL" => self.eval_bool(func, ctx),
419 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
422 _ => Err(QueryError::UnknownFunction(func.name.clone())),
423 }
424 }
425
426 fn evaluate_function_on_values(&self, name: &str, args: &[Value]) -> Result<Value, QueryError> {
428 let name_upper = name.to_uppercase();
429 match name_upper.as_str() {
430 "TODAY" => Ok(Value::Date(chrono::Local::now().date_naive())),
432 "YEAR" => {
433 Self::require_args_count(&name_upper, args, 1)?;
434 match &args[0] {
435 Value::Date(d) => Ok(Value::Integer(d.year().into())),
436 _ => Err(QueryError::Type("YEAR expects a date".to_string())),
437 }
438 }
439 "MONTH" => {
440 Self::require_args_count(&name_upper, args, 1)?;
441 match &args[0] {
442 Value::Date(d) => Ok(Value::Integer(d.month().into())),
443 _ => Err(QueryError::Type("MONTH expects a date".to_string())),
444 }
445 }
446 "DAY" => {
447 Self::require_args_count(&name_upper, args, 1)?;
448 match &args[0] {
449 Value::Date(d) => Ok(Value::Integer(d.day().into())),
450 _ => Err(QueryError::Type("DAY expects a date".to_string())),
451 }
452 }
453 "LENGTH" => {
455 Self::require_args_count(&name_upper, args, 1)?;
456 match &args[0] {
457 Value::String(s) => Ok(Value::Integer(s.len() as i64)),
458 _ => Err(QueryError::Type("LENGTH expects a string".to_string())),
459 }
460 }
461 "UPPER" => {
462 Self::require_args_count(&name_upper, args, 1)?;
463 match &args[0] {
464 Value::String(s) => Ok(Value::String(s.to_uppercase())),
465 _ => Err(QueryError::Type("UPPER expects a string".to_string())),
466 }
467 }
468 "LOWER" => {
469 Self::require_args_count(&name_upper, args, 1)?;
470 match &args[0] {
471 Value::String(s) => Ok(Value::String(s.to_lowercase())),
472 _ => Err(QueryError::Type("LOWER expects a string".to_string())),
473 }
474 }
475 "TRIM" => {
476 Self::require_args_count(&name_upper, args, 1)?;
477 match &args[0] {
478 Value::String(s) => Ok(Value::String(s.trim().to_string())),
479 _ => Err(QueryError::Type("TRIM expects a string".to_string())),
480 }
481 }
482 "ABS" => {
484 Self::require_args_count(&name_upper, args, 1)?;
485 match &args[0] {
486 Value::Number(n) => Ok(Value::Number(n.abs())),
487 Value::Integer(i) => Ok(Value::Integer(i.abs())),
488 _ => Err(QueryError::Type("ABS expects a number".to_string())),
489 }
490 }
491 "ROUND" => {
492 if args.is_empty() || args.len() > 2 {
493 return Err(QueryError::InvalidArguments(
494 "ROUND".to_string(),
495 "expected 1 or 2 arguments".to_string(),
496 ));
497 }
498 match &args[0] {
499 Value::Number(n) => {
500 let scale = if args.len() == 2 {
501 match &args[1] {
502 Value::Integer(i) => *i as u32,
503 _ => 0,
504 }
505 } else {
506 0
507 };
508 Ok(Value::Number(n.round_dp(scale)))
509 }
510 Value::Integer(i) => Ok(Value::Integer(*i)),
511 _ => Err(QueryError::Type("ROUND expects a number".to_string())),
512 }
513 }
514 "COALESCE" => {
516 for arg in args {
517 if !matches!(arg, Value::Null) {
518 return Ok(arg.clone());
519 }
520 }
521 Ok(Value::Null)
522 }
523 "NUMBER" => {
525 Self::require_args_count(&name_upper, args, 1)?;
526 match &args[0] {
527 Value::Amount(a) => Ok(Value::Number(a.number)),
528 Value::Position(p) => Ok(Value::Number(p.units.number)),
529 Value::Number(n) => Ok(Value::Number(*n)),
530 Value::Integer(i) => Ok(Value::Number(Decimal::from(*i))),
531 Value::Inventory(inv) => {
532 let positions = inv.positions();
535 if positions.is_empty() {
536 return Ok(Value::Number(Decimal::ZERO));
537 }
538 let first_currency = &positions[0].units.currency;
539 let all_same_currency = positions
540 .iter()
541 .all(|p| &p.units.currency == first_currency);
542 if all_same_currency {
543 let total: Decimal = positions.iter().map(|p| p.units.number).sum();
544 Ok(Value::Number(total))
545 } else {
546 Ok(Value::Null)
548 }
549 }
550 Value::Null => Ok(Value::Null),
551 _ => Err(QueryError::Type(
552 "NUMBER expects an amount, position, or inventory".to_string(),
553 )),
554 }
555 }
556 "CURRENCY" => {
557 Self::require_args_count(&name_upper, args, 1)?;
558 match &args[0] {
559 Value::Amount(a) => Ok(Value::String(a.currency.to_string())),
560 Value::Position(p) => Ok(Value::String(p.units.currency.to_string())),
561 Value::Inventory(inv) => {
562 if let Some(pos) = inv.positions().first() {
564 Ok(Value::String(pos.units.currency.to_string()))
565 } else {
566 Ok(Value::Null)
567 }
568 }
569 Value::Null => Ok(Value::Null),
570 _ => Err(QueryError::Type(
571 "CURRENCY expects an amount or position".to_string(),
572 )),
573 }
574 }
575 "UNITS" => {
576 Self::require_args_count(&name_upper, args, 1)?;
577 match &args[0] {
578 Value::Position(p) => Ok(Value::Amount(p.units.clone())),
579 Value::Amount(a) => Ok(Value::Amount(a.clone())),
580 Value::Inventory(inv) => {
581 let mut units_inv = Inventory::new();
583 for pos in inv.positions() {
584 units_inv.add(Position::simple(pos.units.clone()));
585 }
586 Ok(Value::Inventory(Box::new(units_inv)))
587 }
588 Value::Null => Ok(Value::Null),
589 _ => Err(QueryError::Type(
590 "UNITS expects a position or inventory".to_string(),
591 )),
592 }
593 }
594 "COST" => {
595 Self::require_args_count(&name_upper, args, 1)?;
596 match &args[0] {
597 Value::Position(p) => {
598 if let Some(cost) = &p.cost {
599 let total = p.units.number.abs() * cost.number;
600 Ok(Value::Amount(Amount::new(total, cost.currency.clone())))
601 } else {
602 Ok(Value::Null)
603 }
604 }
605 Value::Amount(a) => Ok(Value::Amount(a.clone())),
606 Value::Inventory(inv) => {
607 let mut total = Decimal::ZERO;
608 let mut currency: Option<InternedStr> = None;
609 for pos in inv.positions() {
610 if let Some(cost) = &pos.cost {
611 total += pos.units.number.abs() * cost.number;
612 if currency.is_none() {
613 currency = Some(cost.currency.clone());
614 }
615 }
616 }
617 if let Some(curr) = currency {
618 Ok(Value::Amount(Amount::new(total, curr)))
619 } else {
620 Ok(Value::Null)
621 }
622 }
623 Value::Null => Ok(Value::Null),
624 _ => Err(QueryError::Type(
625 "COST expects a position or inventory".to_string(),
626 )),
627 }
628 }
629 "VALUE" => {
630 if args.is_empty() || args.len() > 2 {
633 return Err(QueryError::InvalidArguments(
634 "VALUE".to_string(),
635 "expected 1-2 arguments".to_string(),
636 ));
637 }
638 let target_currency = if args.len() == 2 {
639 match &args[1] {
640 Value::String(s) => Some(s.clone()),
641 _ => None,
642 }
643 } else {
644 self.target_currency.clone()
645 };
646 match &args[0] {
647 Value::Position(p) => {
648 if let Some(ref target) = target_currency {
649 if p.units.currency.as_str() == target {
650 return Ok(Value::Amount(p.units.clone()));
651 }
652 if let Some(converted) =
654 self.price_db.convert(&p.units, target, self.query_date)
655 {
656 return Ok(Value::Amount(converted));
657 }
658 }
659 Ok(Value::Amount(p.units.clone()))
660 }
661 Value::Amount(a) => {
662 if let Some(ref target) = target_currency {
663 if a.currency.as_str() == target {
664 return Ok(Value::Amount(a.clone()));
665 }
666 if let Some(converted) =
667 self.price_db.convert(a, target, self.query_date)
668 {
669 return Ok(Value::Amount(converted));
670 }
671 }
672 Ok(Value::Amount(a.clone()))
673 }
674 Value::Inventory(inv) => {
675 if let Some(ref target) = target_currency {
676 let mut total = Decimal::ZERO;
677 for pos in inv.positions() {
678 if pos.units.currency.as_str() == target {
679 total += pos.units.number;
680 } else if let Some(converted) =
681 self.price_db.convert(&pos.units, target, self.query_date)
682 {
683 total += converted.number;
684 }
685 }
686 return Ok(Value::Amount(Amount::new(total, target)));
687 }
688 Ok(Value::Inventory(inv.clone()))
690 }
691 Value::Null => Ok(Value::Null),
692 _ => Err(QueryError::Type(
693 "VALUE expects a position or inventory".to_string(),
694 )),
695 }
696 }
697 "SAFEDIV" => {
699 Self::require_args_count(&name_upper, args, 2)?;
700 let (dividend, divisor) = (&args[0], &args[1]);
701 match (dividend, divisor) {
702 (Value::Number(a), Value::Number(b)) => {
703 if b.is_zero() {
704 Ok(Value::Null)
705 } else {
706 Ok(Value::Number(a / b))
707 }
708 }
709 (Value::Integer(a), Value::Integer(b)) => {
710 if *b == 0 {
711 Ok(Value::Null)
712 } else {
713 Ok(Value::Number(Decimal::from(*a) / Decimal::from(*b)))
714 }
715 }
716 (Value::Number(a), Value::Integer(b)) => {
717 if *b == 0 {
718 Ok(Value::Null)
719 } else {
720 Ok(Value::Number(a / Decimal::from(*b)))
721 }
722 }
723 (Value::Integer(a), Value::Number(b)) => {
724 if b.is_zero() {
725 Ok(Value::Null)
726 } else {
727 Ok(Value::Number(Decimal::from(*a) / b))
728 }
729 }
730 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
731 _ => Err(QueryError::Type(
732 "SAFEDIV expects numeric arguments".to_string(),
733 )),
734 }
735 }
736 "NEG" => {
737 Self::require_args_count(&name_upper, args, 1)?;
738 match &args[0] {
739 Value::Number(n) => Ok(Value::Number(-n)),
740 Value::Integer(i) => Ok(Value::Integer(-i)),
741 Value::Amount(a) => {
742 Ok(Value::Amount(Amount::new(-a.number, a.currency.clone())))
743 }
744 _ => Err(QueryError::Type(
745 "NEG expects a number or amount".to_string(),
746 )),
747 }
748 }
749 "ACCOUNT_SORTKEY" => {
751 Self::require_args_count(&name_upper, args, 1)?;
752 match &args[0] {
753 Value::String(s) => {
754 let type_index = Self::account_type_index(s);
755 Ok(Value::String(format!("{type_index}-{s}")))
756 }
757 _ => Err(QueryError::Type(
758 "ACCOUNT_SORTKEY expects an account string".to_string(),
759 )),
760 }
761 }
762 "PARENT" => {
763 Self::require_args_count(&name_upper, args, 1)?;
764 match &args[0] {
765 Value::String(s) => {
766 if let Some(idx) = s.rfind(':') {
767 Ok(Value::String(s[..idx].to_string()))
768 } else {
769 Ok(Value::Null)
770 }
771 }
772 _ => Err(QueryError::Type(
773 "PARENT expects an account string".to_string(),
774 )),
775 }
776 }
777 "LEAF" => {
778 Self::require_args_count(&name_upper, args, 1)?;
779 match &args[0] {
780 Value::String(s) => {
781 if let Some(idx) = s.rfind(':') {
782 Ok(Value::String(s[idx + 1..].to_string()))
783 } else {
784 Ok(Value::String(s.clone()))
785 }
786 }
787 _ => Err(QueryError::Type(
788 "LEAF expects an account string".to_string(),
789 )),
790 }
791 }
792 "ROOT" => {
793 if args.is_empty() || args.len() > 2 {
794 return Err(QueryError::InvalidArguments(
795 "ROOT".to_string(),
796 "expected 1 or 2 arguments".to_string(),
797 ));
798 }
799 let n = if args.len() == 2 {
800 match &args[1] {
801 Value::Integer(i) => *i as usize,
802 _ => 1,
803 }
804 } else {
805 1
806 };
807 match &args[0] {
808 Value::String(s) => {
809 let parts: Vec<&str> = s.split(':').collect();
810 if n >= parts.len() {
811 Ok(Value::String(s.clone()))
812 } else {
813 Ok(Value::String(parts[..n].join(":")))
814 }
815 }
816 _ => Err(QueryError::Type(
817 "ROOT expects an account string".to_string(),
818 )),
819 }
820 }
821 "ONLY" => {
823 Self::require_args_count(&name_upper, args, 2)?;
824 let currency = match &args[0] {
825 Value::String(s) => s.clone(),
826 _ => {
827 return Err(QueryError::Type(
828 "ONLY: first argument must be a currency string".to_string(),
829 ));
830 }
831 };
832 match &args[1] {
833 Value::Inventory(inv) => {
834 let total = inv.units(¤cy);
835 if total.is_zero() {
836 Ok(Value::Null)
837 } else {
838 Ok(Value::Amount(Amount::new(total, ¤cy)))
839 }
840 }
841 Value::Position(p) => {
842 if p.units.currency.as_str() == currency {
843 Ok(Value::Amount(p.units.clone()))
844 } else {
845 Ok(Value::Null)
846 }
847 }
848 Value::Amount(a) => {
849 if a.currency.as_str() == currency {
850 Ok(Value::Amount(a.clone()))
851 } else {
852 Ok(Value::Null)
853 }
854 }
855 Value::Null => Ok(Value::Null),
856 _ => Err(QueryError::Type(
857 "ONLY: second argument must be an inventory, position, or amount"
858 .to_string(),
859 )),
860 }
861 }
862 "GETPRICE" => {
864 if args.len() < 2 || args.len() > 3 {
865 return Err(QueryError::InvalidArguments(
866 "GETPRICE".to_string(),
867 "expected 2 or 3 arguments".to_string(),
868 ));
869 }
870 let base = match &args[0] {
872 Value::String(s) => s.clone(),
873 Value::Null => return Ok(Value::Null),
874 _ => {
875 return Err(QueryError::Type(
876 "GETPRICE: first argument must be a currency string".to_string(),
877 ));
878 }
879 };
880 let quote = match &args[1] {
881 Value::String(s) => s.clone(),
882 Value::Null => return Ok(Value::Null),
883 _ => {
884 return Err(QueryError::Type(
885 "GETPRICE: second argument must be a currency string".to_string(),
886 ));
887 }
888 };
889 let date = if args.len() == 3 {
890 match &args[2] {
891 Value::Date(d) => *d,
892 Value::Null => self.query_date,
893 _ => self.query_date,
894 }
895 } else {
896 self.query_date
897 };
898 match self.price_db.get_price(&base, "e, date) {
899 Some(price) => Ok(Value::Number(price)),
900 None => Ok(Value::Null),
901 }
902 }
903 "EMPTY" => {
905 Self::require_args_count(&name_upper, args, 1)?;
906 match &args[0] {
907 Value::Inventory(inv) => Ok(Value::Boolean(inv.is_empty())),
908 Value::Null => Ok(Value::Boolean(true)),
909 _ => Err(QueryError::Type("EMPTY expects an inventory".to_string())),
910 }
911 }
912 "FILTER_CURRENCY" => {
913 Self::require_args_count(&name_upper, args, 2)?;
914 let currency = match &args[1] {
915 Value::String(s) => s.clone(),
916 _ => {
917 return Err(QueryError::Type(
918 "FILTER_CURRENCY expects (inventory, string)".to_string(),
919 ));
920 }
921 };
922 match &args[0] {
923 Value::Inventory(inv) => {
924 let filtered: Vec<Position> = inv
925 .positions()
926 .iter()
927 .filter(|p| p.units.currency.as_str() == currency)
928 .cloned()
929 .collect();
930 let mut new_inv = Inventory::new();
931 for pos in filtered {
932 new_inv.add(pos);
933 }
934 Ok(Value::Inventory(Box::new(new_inv)))
935 }
936 Value::Null => Ok(Value::Null),
937 _ => Err(QueryError::Type(
938 "FILTER_CURRENCY expects (inventory, string)".to_string(),
939 )),
940 }
941 }
942 "POSSIGN" => {
943 Self::require_args_count(&name_upper, args, 2)?;
944 let account_str = match &args[1] {
945 Value::String(s) => s.clone(),
946 _ => {
947 return Err(QueryError::Type(
948 "POSSIGN expects (amount, account_string)".to_string(),
949 ));
950 }
951 };
952 let first_component = account_str.split(':').next().unwrap_or("");
953 let is_credit_normal =
954 matches!(first_component, "Liabilities" | "Equity" | "Income");
955 match &args[0] {
956 Value::Amount(a) => {
957 let mut amt = a.clone();
958 if is_credit_normal {
959 amt.number = -amt.number;
960 }
961 Ok(Value::Amount(amt))
962 }
963 Value::Number(n) => {
964 let adjusted = if is_credit_normal { -n } else { *n };
965 Ok(Value::Number(adjusted))
966 }
967 Value::Null => Ok(Value::Null),
968 _ => Err(QueryError::Type(
969 "POSSIGN expects (amount, account_string)".to_string(),
970 )),
971 }
972 }
973 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
975 _ => Err(QueryError::UnknownFunction(name.to_string())),
976 }
977 }
978
979 fn require_args_count(name: &str, args: &[Value], expected: usize) -> Result<(), QueryError> {
981 if args.len() != expected {
982 return Err(QueryError::InvalidArguments(
983 name.to_string(),
984 format!("expected {} argument(s), got {}", expected, args.len()),
985 ));
986 }
987 Ok(())
988 }
989
990 fn require_args(name: &str, func: &FunctionCall, expected: usize) -> Result<(), QueryError> {
992 if func.args.len() != expected {
993 return Err(QueryError::InvalidArguments(
994 name.to_string(),
995 format!("expected {expected} argument(s)"),
996 ));
997 }
998 Ok(())
999 }
1000 pub(super) const fn is_window_expr(expr: &Expr) -> bool {
1002 matches!(expr, Expr::Window(_))
1003 }
1004
1005 fn resolve_column_names(&self, targets: &[Target]) -> Result<Vec<String>, QueryError> {
1007 let mut names = Vec::new();
1008 for (i, target) in targets.iter().enumerate() {
1009 if let Some(alias) = &target.alias {
1010 names.push(alias.clone());
1011 } else {
1012 names.push(self.expr_to_name(&target.expr, i));
1013 }
1014 }
1015 Ok(names)
1016 }
1017
1018 fn expr_to_name(&self, expr: &Expr, index: usize) -> String {
1020 match expr {
1021 Expr::Wildcard => "*".to_string(),
1022 Expr::Column(name) => name.clone(),
1023 Expr::Function(func) => func.name.clone(),
1024 Expr::Window(wf) => wf.name.clone(),
1025 _ => format!("col{index}"),
1026 }
1027 }
1028}
1029#[cfg(test)]
1030mod tests {
1031 use super::types::{hash_row, hash_single_value};
1032 use super::*;
1033 use crate::parse;
1034 use rust_decimal_macros::dec;
1035 use rustledger_core::Posting;
1036
1037 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
1038 NaiveDate::from_ymd_opt(year, month, day).unwrap()
1039 }
1040
1041 fn sample_directives() -> Vec<Directive> {
1042 vec![
1043 Directive::Transaction(
1044 Transaction::new(date(2024, 1, 15), "Coffee")
1045 .with_flag('*')
1046 .with_payee("Coffee Shop")
1047 .with_posting(Posting::new(
1048 "Expenses:Food:Coffee",
1049 Amount::new(dec!(5.00), "USD"),
1050 ))
1051 .with_posting(Posting::new(
1052 "Assets:Bank:Checking",
1053 Amount::new(dec!(-5.00), "USD"),
1054 )),
1055 ),
1056 Directive::Transaction(
1057 Transaction::new(date(2024, 1, 16), "Groceries")
1058 .with_flag('*')
1059 .with_payee("Supermarket")
1060 .with_posting(Posting::new(
1061 "Expenses:Food:Groceries",
1062 Amount::new(dec!(50.00), "USD"),
1063 ))
1064 .with_posting(Posting::new(
1065 "Assets:Bank:Checking",
1066 Amount::new(dec!(-50.00), "USD"),
1067 )),
1068 ),
1069 ]
1070 }
1071
1072 #[test]
1073 fn test_simple_select() {
1074 let directives = sample_directives();
1075 let mut executor = Executor::new(&directives);
1076
1077 let query = parse("SELECT date, account").unwrap();
1078 let result = executor.execute(&query).unwrap();
1079
1080 assert_eq!(result.columns, vec!["date", "account"]);
1081 assert_eq!(result.len(), 4); }
1083
1084 #[test]
1085 fn test_where_clause() {
1086 let directives = sample_directives();
1087 let mut executor = Executor::new(&directives);
1088
1089 let query = parse("SELECT account WHERE account ~ \"Expenses:\"").unwrap();
1090 let result = executor.execute(&query).unwrap();
1091
1092 assert_eq!(result.len(), 2); }
1094
1095 #[test]
1096 fn test_balances() {
1097 let directives = sample_directives();
1098 let mut executor = Executor::new(&directives);
1099
1100 let query = parse("BALANCES").unwrap();
1101 let result = executor.execute(&query).unwrap();
1102
1103 assert_eq!(result.columns, vec!["account", "balance"]);
1104 assert!(result.len() >= 3); }
1106
1107 #[test]
1108 fn test_account_functions() {
1109 let directives = sample_directives();
1110 let mut executor = Executor::new(&directives);
1111
1112 let query = parse("SELECT DISTINCT LEAF(account) WHERE account ~ \"Expenses:\"").unwrap();
1114 let result = executor.execute(&query).unwrap();
1115 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT ROOT(account)").unwrap();
1119 let result = executor.execute(&query).unwrap();
1120 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT PARENT(account) WHERE account ~ \"Expenses:\"").unwrap();
1124 let result = executor.execute(&query).unwrap();
1125 assert!(!result.is_empty()); }
1127
1128 #[test]
1129 fn test_min_max_aggregate() {
1130 let directives = sample_directives();
1131 let mut executor = Executor::new(&directives);
1132
1133 let query = parse("SELECT MIN(date)").unwrap();
1135 let result = executor.execute(&query).unwrap();
1136 assert_eq!(result.len(), 1);
1137 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
1138
1139 let query = parse("SELECT MAX(date)").unwrap();
1141 let result = executor.execute(&query).unwrap();
1142 assert_eq!(result.len(), 1);
1143 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1144 }
1145
1146 #[test]
1147 fn test_order_by() {
1148 let directives = sample_directives();
1149 let mut executor = Executor::new(&directives);
1150
1151 let query = parse("SELECT date, account ORDER BY date DESC").unwrap();
1152 let result = executor.execute(&query).unwrap();
1153
1154 assert_eq!(result.len(), 4);
1156 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1158 }
1159
1160 #[test]
1161 fn test_hash_value_all_variants() {
1162 use rustledger_core::{Cost, Inventory, Position};
1163
1164 let values = vec![
1166 Value::String("test".to_string()),
1167 Value::Number(dec!(123.45)),
1168 Value::Integer(42),
1169 Value::Date(date(2024, 1, 15)),
1170 Value::Boolean(true),
1171 Value::Boolean(false),
1172 Value::Amount(Amount::new(dec!(100), "USD")),
1173 Value::Position(Box::new(Position::simple(Amount::new(dec!(10), "AAPL")))),
1174 Value::Position(Box::new(Position::with_cost(
1175 Amount::new(dec!(10), "AAPL"),
1176 Cost::new(dec!(150), "USD"),
1177 ))),
1178 Value::Inventory(Box::new(Inventory::new())),
1179 Value::StringSet(vec!["tag1".to_string(), "tag2".to_string()]),
1180 Value::Null,
1181 ];
1182
1183 for value in &values {
1185 let hash = hash_single_value(value);
1186 assert!(hash != 0 || matches!(value, Value::Null));
1187 }
1188
1189 let hash1 = hash_single_value(&Value::String("a".to_string()));
1191 let hash2 = hash_single_value(&Value::String("b".to_string()));
1192 assert_ne!(hash1, hash2);
1193
1194 let hash3 = hash_single_value(&Value::Integer(42));
1196 let hash4 = hash_single_value(&Value::Integer(42));
1197 assert_eq!(hash3, hash4);
1198 }
1199
1200 #[test]
1201 fn test_hash_row_distinct() {
1202 let row1 = vec![Value::String("a".to_string()), Value::Integer(1)];
1204 let row2 = vec![Value::String("a".to_string()), Value::Integer(1)];
1205 let row3 = vec![Value::String("b".to_string()), Value::Integer(1)];
1206
1207 assert_eq!(hash_row(&row1), hash_row(&row2));
1208 assert_ne!(hash_row(&row1), hash_row(&row3));
1209 }
1210
1211 #[test]
1212 fn test_string_set_hash_order_independent() {
1213 let set1 = Value::StringSet(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
1215 let set2 = Value::StringSet(vec!["c".to_string(), "a".to_string(), "b".to_string()]);
1216 let set3 = Value::StringSet(vec!["b".to_string(), "c".to_string(), "a".to_string()]);
1217
1218 let hash1 = hash_single_value(&set1);
1219 let hash2 = hash_single_value(&set2);
1220 let hash3 = hash_single_value(&set3);
1221
1222 assert_eq!(hash1, hash2);
1223 assert_eq!(hash2, hash3);
1224 }
1225
1226 #[test]
1227 fn test_inventory_hash_includes_cost() {
1228 use rustledger_core::{Cost, Inventory, Position};
1229
1230 let mut inv1 = Inventory::new();
1232 inv1.add(Position::with_cost(
1233 Amount::new(dec!(10), "AAPL"),
1234 Cost::new(dec!(100), "USD"),
1235 ));
1236
1237 let mut inv2 = Inventory::new();
1238 inv2.add(Position::with_cost(
1239 Amount::new(dec!(10), "AAPL"),
1240 Cost::new(dec!(200), "USD"),
1241 ));
1242
1243 let hash1 = hash_single_value(&Value::Inventory(Box::new(inv1)));
1244 let hash2 = hash_single_value(&Value::Inventory(Box::new(inv2)));
1245
1246 assert_ne!(hash1, hash2);
1247 }
1248
1249 #[test]
1250 fn test_distinct_deduplication() {
1251 let directives = sample_directives();
1252 let mut executor = Executor::new(&directives);
1253
1254 let query = parse("SELECT flag").unwrap();
1256 let result = executor.execute(&query).unwrap();
1257 assert_eq!(result.len(), 4); let query = parse("SELECT DISTINCT flag").unwrap();
1261 let result = executor.execute(&query).unwrap();
1262 assert_eq!(result.len(), 1); }
1264
1265 #[test]
1266 fn test_limit_clause() {
1267 let directives = sample_directives();
1268 let mut executor = Executor::new(&directives);
1269
1270 let query = parse("SELECT date, account LIMIT 2").unwrap();
1272 let result = executor.execute(&query).unwrap();
1273 assert_eq!(result.len(), 2);
1274
1275 let query = parse("SELECT date LIMIT 0").unwrap();
1277 let result = executor.execute(&query).unwrap();
1278 assert_eq!(result.len(), 0);
1279
1280 let query = parse("SELECT date LIMIT 100").unwrap();
1282 let result = executor.execute(&query).unwrap();
1283 assert_eq!(result.len(), 4);
1284 }
1285
1286 #[test]
1287 fn test_group_by_with_count() {
1288 let directives = sample_directives();
1289 let mut executor = Executor::new(&directives);
1290
1291 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
1293 let result = executor.execute(&query).unwrap();
1294
1295 assert_eq!(result.columns.len(), 2);
1296 assert_eq!(result.len(), 2);
1298 }
1299
1300 #[test]
1301 fn test_count_aggregate() {
1302 let directives = sample_directives();
1303 let mut executor = Executor::new(&directives);
1304
1305 let query = parse("SELECT COUNT(account)").unwrap();
1307 let result = executor.execute(&query).unwrap();
1308
1309 assert_eq!(result.len(), 1);
1310 assert_eq!(result.rows[0][0], Value::Integer(4));
1311
1312 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
1314 let result = executor.execute(&query).unwrap();
1315 assert_eq!(result.len(), 2); }
1317
1318 #[test]
1319 fn test_journal_query() {
1320 let directives = sample_directives();
1321 let mut executor = Executor::new(&directives);
1322
1323 let query = parse("JOURNAL \"Expenses\"").unwrap();
1325 let result = executor.execute(&query).unwrap();
1326
1327 assert!(result.columns.contains(&"account".to_string()));
1329 assert_eq!(result.len(), 2);
1331 }
1332
1333 #[test]
1334 fn test_print_query() {
1335 let directives = sample_directives();
1336 let mut executor = Executor::new(&directives);
1337
1338 let query = parse("PRINT").unwrap();
1340 let result = executor.execute(&query).unwrap();
1341
1342 assert_eq!(result.columns.len(), 1);
1344 assert_eq!(result.columns[0], "directive");
1345 assert_eq!(result.len(), 2);
1347 }
1348
1349 #[test]
1350 fn test_empty_directives() {
1351 let directives: Vec<Directive> = vec![];
1352 let mut executor = Executor::new(&directives);
1353
1354 let query = parse("SELECT date, account").unwrap();
1356 let result = executor.execute(&query).unwrap();
1357 assert!(result.is_empty());
1358
1359 let query = parse("BALANCES").unwrap();
1361 let result = executor.execute(&query).unwrap();
1362 assert!(result.is_empty());
1363 }
1364
1365 #[test]
1366 fn test_comparison_operators() {
1367 let directives = sample_directives();
1368 let mut executor = Executor::new(&directives);
1369
1370 let query = parse("SELECT date WHERE date < 2024-01-16").unwrap();
1372 let result = executor.execute(&query).unwrap();
1373 assert_eq!(result.len(), 2); let query = parse("SELECT date WHERE year > 2023").unwrap();
1377 let result = executor.execute(&query).unwrap();
1378 assert_eq!(result.len(), 4); let query = parse("SELECT account WHERE day = 15").unwrap();
1382 let result = executor.execute(&query).unwrap();
1383 assert_eq!(result.len(), 2); }
1385
1386 #[test]
1387 fn test_logical_operators() {
1388 let directives = sample_directives();
1389 let mut executor = Executor::new(&directives);
1390
1391 let query = parse("SELECT account WHERE account ~ \"Expenses\" AND day > 14").unwrap();
1393 let result = executor.execute(&query).unwrap();
1394 assert_eq!(result.len(), 2); let query = parse("SELECT account WHERE day = 15 OR day = 16").unwrap();
1398 let result = executor.execute(&query).unwrap();
1399 assert_eq!(result.len(), 4); }
1401
1402 #[test]
1403 fn test_arithmetic_expressions() {
1404 let directives = sample_directives();
1405 let mut executor = Executor::new(&directives);
1406
1407 let query = parse("SELECT -day WHERE day = 15").unwrap();
1409 let result = executor.execute(&query).unwrap();
1410 assert_eq!(result.len(), 2);
1411 for row in &result.rows {
1413 if let Value::Integer(n) = &row[0] {
1414 assert_eq!(*n, -15);
1415 }
1416 }
1417 }
1418
1419 #[test]
1420 fn test_first_last_aggregates() {
1421 let directives = sample_directives();
1422 let mut executor = Executor::new(&directives);
1423
1424 let query = parse("SELECT FIRST(date)").unwrap();
1426 let result = executor.execute(&query).unwrap();
1427 assert_eq!(result.len(), 1);
1428 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
1429
1430 let query = parse("SELECT LAST(date)").unwrap();
1432 let result = executor.execute(&query).unwrap();
1433 assert_eq!(result.len(), 1);
1434 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1435 }
1436
1437 #[test]
1438 fn test_wildcard_select() {
1439 let directives = sample_directives();
1440 let mut executor = Executor::new(&directives);
1441
1442 let query = parse("SELECT *").unwrap();
1444 let result = executor.execute(&query).unwrap();
1445
1446 assert_eq!(result.columns, vec!["*"]);
1448 assert_eq!(result.len(), 4);
1450 assert_eq!(result.rows[0].len(), 6); }
1452
1453 #[test]
1454 fn test_query_result_methods() {
1455 let mut result = QueryResult::new(vec!["col1".to_string(), "col2".to_string()]);
1456
1457 assert!(result.is_empty());
1459 assert_eq!(result.len(), 0);
1460
1461 result.add_row(vec![Value::Integer(1), Value::String("a".to_string())]);
1463 assert!(!result.is_empty());
1464 assert_eq!(result.len(), 1);
1465
1466 result.add_row(vec![Value::Integer(2), Value::String("b".to_string())]);
1467 assert_eq!(result.len(), 2);
1468 }
1469
1470 #[test]
1471 fn test_type_cast_functions() {
1472 let directives = sample_directives();
1473 let mut executor = Executor::new(&directives);
1474
1475 let query = parse("SELECT int(5.7)").unwrap();
1477 let result = executor.execute(&query).unwrap();
1478 assert_eq!(result.rows[0][0], Value::Integer(5));
1479
1480 let query = parse("SELECT decimal(42)").unwrap();
1482 let result = executor.execute(&query).unwrap();
1483 assert_eq!(result.rows[0][0], Value::Number(dec!(42)));
1484
1485 let query = parse("SELECT str(123)").unwrap();
1487 let result = executor.execute(&query).unwrap();
1488 assert_eq!(result.rows[0][0], Value::String("123".to_string()));
1489
1490 let query = parse("SELECT bool(1)").unwrap();
1492 let result = executor.execute(&query).unwrap();
1493 assert_eq!(result.rows[0][0], Value::Boolean(true));
1494
1495 let query = parse("SELECT bool(0)").unwrap();
1496 let result = executor.execute(&query).unwrap();
1497 assert_eq!(result.rows[0][0], Value::Boolean(false));
1498 }
1499
1500 #[test]
1501 fn test_meta_functions() {
1502 let mut txn_meta: Metadata = Metadata::default();
1504 txn_meta.insert(
1505 "source".to_string(),
1506 MetaValue::String("bank_import".to_string()),
1507 );
1508
1509 let mut posting_meta: Metadata = Metadata::default();
1510 posting_meta.insert(
1511 "category".to_string(),
1512 MetaValue::String("food".to_string()),
1513 );
1514
1515 let txn = Transaction {
1516 date: date(2024, 1, 15),
1517 flag: '*',
1518 payee: Some("Coffee Shop".into()),
1519 narration: "Coffee".into(),
1520 tags: vec![],
1521 links: vec![],
1522 meta: txn_meta,
1523 postings: vec![
1524 Posting {
1525 account: "Expenses:Food".into(),
1526 units: Some(rustledger_core::IncompleteAmount::Complete(Amount::new(
1527 dec!(5),
1528 "USD",
1529 ))),
1530 cost: None,
1531 price: None,
1532 flag: None,
1533 meta: posting_meta,
1534 },
1535 Posting::new("Assets:Cash", Amount::new(dec!(-5), "USD")),
1536 ],
1537 };
1538
1539 let directives = vec![Directive::Transaction(txn)];
1540 let mut executor = Executor::new(&directives);
1541
1542 let query = parse("SELECT meta('category') WHERE account ~ 'Expenses'").unwrap();
1544 let result = executor.execute(&query).unwrap();
1545 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
1546
1547 let query = parse("SELECT entry_meta('source') WHERE account ~ 'Expenses'").unwrap();
1549 let result = executor.execute(&query).unwrap();
1550 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
1551
1552 let query = parse("SELECT any_meta('source') WHERE account ~ 'Expenses'").unwrap();
1554 let result = executor.execute(&query).unwrap();
1555 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
1556
1557 let query = parse("SELECT any_meta('category') WHERE account ~ 'Expenses'").unwrap();
1559 let result = executor.execute(&query).unwrap();
1560 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
1561
1562 let query = parse("SELECT meta('nonexistent') WHERE account ~ 'Expenses'").unwrap();
1564 let result = executor.execute(&query).unwrap();
1565 assert_eq!(result.rows[0][0], Value::Null);
1566 }
1567
1568 #[test]
1569 fn test_convert_function() {
1570 let price = rustledger_core::Price {
1572 date: date(2024, 1, 1),
1573 currency: "EUR".into(),
1574 amount: Amount::new(dec!(1.10), "USD"),
1575 meta: Metadata::default(),
1576 };
1577
1578 let txn = Transaction::new(date(2024, 1, 15), "Test")
1579 .with_flag('*')
1580 .with_posting(Posting::new("Assets:Euro", Amount::new(dec!(100), "EUR")))
1581 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-110), "USD")));
1582
1583 let directives = vec![Directive::Price(price), Directive::Transaction(txn)];
1584 let mut executor = Executor::new(&directives);
1585
1586 let query = parse("SELECT convert(position, 'USD') WHERE account ~ 'Euro'").unwrap();
1588 let result = executor.execute(&query).unwrap();
1589 match &result.rows[0][0] {
1591 Value::Amount(a) => {
1592 assert_eq!(a.number, dec!(110));
1593 assert_eq!(a.currency.as_ref(), "USD");
1594 }
1595 _ => panic!("Expected Amount, got {:?}", result.rows[0][0]),
1596 }
1597 }
1598
1599 #[test]
1600 fn test_date_functions() {
1601 let directives = sample_directives();
1602 let mut executor = Executor::new(&directives);
1603
1604 let query = parse("SELECT date('2024-06-15')").unwrap();
1606 let result = executor.execute(&query).unwrap();
1607 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1608
1609 let query = parse("SELECT date(2024, 6, 15)").unwrap();
1611 let result = executor.execute(&query).unwrap();
1612 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1613
1614 let query = parse("SELECT date_diff(date('2024-01-20'), date('2024-01-15'))").unwrap();
1616 let result = executor.execute(&query).unwrap();
1617 assert_eq!(result.rows[0][0], Value::Integer(5));
1618
1619 let query = parse("SELECT date_add(date('2024-01-15'), 10)").unwrap();
1621 let result = executor.execute(&query).unwrap();
1622 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 25)));
1623
1624 let query = parse("SELECT date_trunc('year', date('2024-06-15'))").unwrap();
1626 let result = executor.execute(&query).unwrap();
1627 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1)));
1628
1629 let query = parse("SELECT date_trunc('month', date('2024-06-15'))").unwrap();
1631 let result = executor.execute(&query).unwrap();
1632 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1)));
1633
1634 let query = parse("SELECT date_part('month', date('2024-06-15'))").unwrap();
1636 let result = executor.execute(&query).unwrap();
1637 assert_eq!(result.rows[0][0], Value::Integer(6));
1638
1639 let query = parse("SELECT parse_date('15/06/2024', '%d/%m/%Y')").unwrap();
1641 let result = executor.execute(&query).unwrap();
1642 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1643
1644 let query =
1646 parse("SELECT date_bin('7 days', date('2024-01-15'), date('2024-01-01'))").unwrap();
1647 let result = executor.execute(&query).unwrap();
1648 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
1652 parse("SELECT date_bin('1 week', date('2024-01-20'), date('2024-01-01'))").unwrap();
1653 let result = executor.execute(&query).unwrap();
1654 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
1658 parse("SELECT date_bin('1 month', date('2024-06-15'), date('2024-01-01'))").unwrap();
1659 let result = executor.execute(&query).unwrap();
1660 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1))); let query =
1664 parse("SELECT date_bin('1 year', date('2024-06-15'), date('2020-01-01'))").unwrap();
1665 let result = executor.execute(&query).unwrap();
1666 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1))); }
1668
1669 #[test]
1670 fn test_string_functions_extended() {
1671 let directives = sample_directives();
1672 let mut executor = Executor::new(&directives);
1673
1674 let query = parse("SELECT grep('Ex[a-z]+', 'Hello Expenses World')").unwrap();
1676 let result = executor.execute(&query).unwrap();
1677 assert_eq!(result.rows[0][0], Value::String("Expenses".to_string()));
1678
1679 let query = parse("SELECT grep('xyz', 'Hello World')").unwrap();
1681 let result = executor.execute(&query).unwrap();
1682 assert_eq!(result.rows[0][0], Value::Null);
1683
1684 let query = parse("SELECT grepn('([0-9]+)-([0-9]+)', '2024-01', 1)").unwrap();
1686 let result = executor.execute(&query).unwrap();
1687 assert_eq!(result.rows[0][0], Value::String("2024".to_string()));
1688
1689 let query = parse("SELECT subst('-', '/', '2024-01-15')").unwrap();
1691 let result = executor.execute(&query).unwrap();
1692 assert_eq!(result.rows[0][0], Value::String("2024/01/15".to_string()));
1693
1694 let query = parse("SELECT splitcomp('a:b:c', ':', 1)").unwrap();
1696 let result = executor.execute(&query).unwrap();
1697 assert_eq!(result.rows[0][0], Value::String("b".to_string()));
1698
1699 let query = parse("SELECT joinstr('hello', 'world')").unwrap();
1701 let result = executor.execute(&query).unwrap();
1702 assert_eq!(result.rows[0][0], Value::String("hello, world".to_string()));
1703
1704 let query = parse("SELECT maxwidth('hello', 10)").unwrap();
1706 let result = executor.execute(&query).unwrap();
1707 assert_eq!(result.rows[0][0], Value::String("hello".to_string()));
1708
1709 let query = parse("SELECT maxwidth('hello world', 8)").unwrap();
1711 let result = executor.execute(&query).unwrap();
1712 assert_eq!(result.rows[0][0], Value::String("hello...".to_string()));
1713 }
1714
1715 #[test]
1716 fn test_inventory_functions() {
1717 let directives = sample_directives();
1718 let mut executor = Executor::new(&directives);
1719
1720 let query = parse("SELECT empty(sum(position)) WHERE account ~ 'Assets'").unwrap();
1723 let result = executor.execute(&query).unwrap();
1724 assert!(matches!(result.rows[0][0], Value::Boolean(_)));
1726
1727 let query = parse("SELECT possign(100, 'Assets:Bank')").unwrap();
1732 let result = executor.execute(&query).unwrap();
1733 assert_eq!(
1734 result.rows[0][0],
1735 Value::Number(rust_decimal::Decimal::from(100))
1736 );
1737
1738 let query = parse("SELECT possign(100, 'Income:Salary')").unwrap();
1740 let result = executor.execute(&query).unwrap();
1741 assert_eq!(
1742 result.rows[0][0],
1743 Value::Number(rust_decimal::Decimal::from(-100))
1744 );
1745
1746 let query = parse("SELECT possign(50, 'Expenses:Food')").unwrap();
1748 let result = executor.execute(&query).unwrap();
1749 assert_eq!(
1750 result.rows[0][0],
1751 Value::Number(rust_decimal::Decimal::from(50))
1752 );
1753
1754 let query = parse("SELECT possign(200, 'Liabilities:CreditCard')").unwrap();
1756 let result = executor.execute(&query).unwrap();
1757 assert_eq!(
1758 result.rows[0][0],
1759 Value::Number(rust_decimal::Decimal::from(-200))
1760 );
1761
1762 let query = parse("SELECT possign(300, 'Equity:OpeningBalances')").unwrap();
1764 let result = executor.execute(&query).unwrap();
1765 assert_eq!(
1766 result.rows[0][0],
1767 Value::Number(rust_decimal::Decimal::from(-300))
1768 );
1769 }
1770
1771 #[test]
1772 fn test_account_meta_functions() {
1773 use rustledger_core::{Close, Metadata, Open};
1774
1775 let mut open_meta = Metadata::default();
1777 open_meta.insert(
1778 "category".to_string(),
1779 MetaValue::String("checking".to_string()),
1780 );
1781
1782 let directives = vec![
1783 Directive::Open(Open {
1784 date: date(2020, 1, 1),
1785 account: "Assets:Bank:Checking".into(),
1786 currencies: vec![],
1787 booking: None,
1788 meta: open_meta,
1789 }),
1790 Directive::Open(Open::new(date(2020, 2, 15), "Expenses:Food")),
1791 Directive::Close(Close::new(date(2024, 12, 31), "Assets:Bank:Checking")),
1792 Directive::Transaction(
1794 Transaction::new(date(2024, 1, 15), "Coffee")
1795 .with_posting(Posting::new(
1796 "Expenses:Food",
1797 Amount::new(dec!(5.00), "USD"),
1798 ))
1799 .with_posting(Posting::new(
1800 "Assets:Bank:Checking",
1801 Amount::new(dec!(-5.00), "USD"),
1802 )),
1803 ),
1804 ];
1805
1806 let mut executor = Executor::new(&directives);
1807
1808 let query = parse("SELECT open_date('Assets:Bank:Checking')").unwrap();
1810 let result = executor.execute(&query).unwrap();
1811 assert_eq!(result.rows[0][0], Value::Date(date(2020, 1, 1)));
1812
1813 let query = parse("SELECT close_date('Assets:Bank:Checking')").unwrap();
1815 let result = executor.execute(&query).unwrap();
1816 assert_eq!(result.rows[0][0], Value::Date(date(2024, 12, 31)));
1817
1818 let query = parse("SELECT close_date('Expenses:Food')").unwrap();
1820 let result = executor.execute(&query).unwrap();
1821 assert_eq!(result.rows[0][0], Value::Null);
1822
1823 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'category')").unwrap();
1825 let result = executor.execute(&query).unwrap();
1826 assert_eq!(result.rows[0][0], Value::String("checking".to_string()));
1827
1828 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'nonexistent')").unwrap();
1830 let result = executor.execute(&query).unwrap();
1831 assert_eq!(result.rows[0][0], Value::Null);
1832
1833 let query = parse("SELECT open_date('NonExistent:Account')").unwrap();
1835 let result = executor.execute(&query).unwrap();
1836 assert_eq!(result.rows[0][0], Value::Null);
1837 }
1838
1839 #[test]
1840 fn test_source_location_columns_return_null_without_sources() {
1841 let directives = vec![Directive::Transaction(Transaction {
1844 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1845 flag: '*',
1846 payee: Some("Test".into()),
1847 narration: "Test transaction".into(),
1848 tags: vec![],
1849 links: vec![],
1850 meta: Metadata::default(),
1851 postings: vec![
1852 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
1853 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
1854 ],
1855 })];
1856
1857 let mut executor = Executor::new(&directives);
1858
1859 let query = parse("SELECT filename").unwrap();
1861 let result = executor.execute(&query).unwrap();
1862 assert_eq!(result.rows[0][0], Value::Null);
1863
1864 let query = parse("SELECT lineno").unwrap();
1866 let result = executor.execute(&query).unwrap();
1867 assert_eq!(result.rows[0][0], Value::Null);
1868
1869 let query = parse("SELECT location").unwrap();
1871 let result = executor.execute(&query).unwrap();
1872 assert_eq!(result.rows[0][0], Value::Null);
1873 }
1874
1875 #[test]
1876 fn test_source_location_columns_with_sources() {
1877 use rustledger_loader::SourceMap;
1878 use rustledger_parser::Spanned;
1879 use std::sync::Arc;
1880
1881 let mut source_map = SourceMap::new();
1883 let source: Arc<str> =
1884 "2024-01-15 * \"Test\"\n Assets:Bank 100 USD\n Expenses:Food".into();
1885 let file_id = source_map.add_file("test.beancount".into(), source);
1886
1887 let txn = Transaction {
1889 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1890 flag: '*',
1891 payee: Some("Test".into()),
1892 narration: "Test transaction".into(),
1893 tags: vec![],
1894 links: vec![],
1895 meta: Metadata::default(),
1896 postings: vec![
1897 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
1898 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
1899 ],
1900 };
1901
1902 let spanned_directives = vec![Spanned {
1903 value: Directive::Transaction(txn),
1904 span: rustledger_parser::Span { start: 0, end: 50 },
1905 file_id: file_id as u16,
1906 }];
1907
1908 let mut executor = Executor::new_with_sources(&spanned_directives, &source_map);
1909
1910 let query = parse("SELECT filename").unwrap();
1912 let result = executor.execute(&query).unwrap();
1913 assert_eq!(
1914 result.rows[0][0],
1915 Value::String("test.beancount".to_string())
1916 );
1917
1918 let query = parse("SELECT lineno").unwrap();
1920 let result = executor.execute(&query).unwrap();
1921 assert_eq!(result.rows[0][0], Value::Integer(1));
1922
1923 let query = parse("SELECT location").unwrap();
1925 let result = executor.execute(&query).unwrap();
1926 assert_eq!(
1927 result.rows[0][0],
1928 Value::String("test.beancount:1".to_string())
1929 );
1930 }
1931
1932 #[test]
1933 fn test_interval_function() {
1934 let directives = sample_directives();
1935 let mut executor = Executor::new(&directives);
1936
1937 let query = parse("SELECT interval('month')").unwrap();
1939 let result = executor.execute(&query).unwrap();
1940 assert_eq!(
1941 result.rows[0][0],
1942 Value::Interval(Interval::new(1, IntervalUnit::Month))
1943 );
1944
1945 let query = parse("SELECT interval(3, 'day')").unwrap();
1947 let result = executor.execute(&query).unwrap();
1948 assert_eq!(
1949 result.rows[0][0],
1950 Value::Interval(Interval::new(3, IntervalUnit::Day))
1951 );
1952
1953 let query = parse("SELECT interval(-2, 'week')").unwrap();
1955 let result = executor.execute(&query).unwrap();
1956 assert_eq!(
1957 result.rows[0][0],
1958 Value::Interval(Interval::new(-2, IntervalUnit::Week))
1959 );
1960 }
1961
1962 #[test]
1963 fn test_date_add_with_interval() {
1964 let directives = sample_directives();
1965 let mut executor = Executor::new(&directives);
1966
1967 let query = parse("SELECT date_add(date(2024, 1, 15), interval(1, 'month'))").unwrap();
1969 let result = executor.execute(&query).unwrap();
1970 assert_eq!(
1971 result.rows[0][0],
1972 Value::Date(NaiveDate::from_ymd_opt(2024, 2, 15).unwrap())
1973 );
1974
1975 let query = parse("SELECT date(2024, 1, 15) + interval(1, 'year')").unwrap();
1977 let result = executor.execute(&query).unwrap();
1978 assert_eq!(
1979 result.rows[0][0],
1980 Value::Date(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap())
1981 );
1982
1983 let query = parse("SELECT date(2024, 3, 15) - interval(2, 'month')").unwrap();
1985 let result = executor.execute(&query).unwrap();
1986 assert_eq!(
1987 result.rows[0][0],
1988 Value::Date(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
1989 );
1990 }
1991}