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