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 if let Some(filter) = &from_clause.filter {
239 if !self.evaluate_from_filter(filter, txn)? {
240 continue;
241 }
242 }
243 }
244
245 for posting in &txn.postings {
246 if let Some(units) = posting.amount() {
247 let balance = self.balances.entry(posting.account.clone()).or_default();
248
249 let pos = if let Some(cost_spec) = &posting.cost {
250 if let Some(cost) = cost_spec.resolve(units.number, txn.date) {
251 Position::with_cost(units.clone(), cost)
252 } else {
253 Position::simple(units.clone())
254 }
255 } else {
256 Position::simple(units.clone())
257 };
258 balance.add(pos);
259 }
260 }
261 }
262 }
263 Ok(())
264 }
265
266 fn collect_postings(
268 &self,
269 from: Option<&FromClause>,
270 where_clause: Option<&Expr>,
271 ) -> Result<Vec<PostingContext<'a>>, QueryError> {
272 let mut postings = Vec::new();
273 let mut running_balances: HashMap<InternedStr, Inventory> = HashMap::new();
275
276 let directive_iter: Vec<(usize, &Directive)> =
279 if let Some(spanned) = self.spanned_directives {
280 spanned
281 .iter()
282 .enumerate()
283 .map(|(i, s)| (i, &s.value))
284 .collect()
285 } else {
286 self.directives.iter().enumerate().collect()
287 };
288
289 for (directive_index, directive) in directive_iter {
290 if let Directive::Transaction(txn) = directive {
291 if let Some(from) = from {
293 if let Some(open_date) = from.open_on {
295 if txn.date < open_date {
296 for posting in &txn.postings {
298 if let Some(units) = posting.amount() {
299 let balance = running_balances
300 .entry(posting.account.clone())
301 .or_default();
302 balance.add(Position::simple(units.clone()));
303 }
304 }
305 continue;
306 }
307 }
308 if let Some(close_date) = from.close_on {
309 if txn.date > close_date {
310 continue;
311 }
312 }
313 if let Some(filter) = &from.filter {
315 if !self.evaluate_from_filter(filter, txn)? {
316 continue;
317 }
318 }
319 }
320
321 for (i, posting) in txn.postings.iter().enumerate() {
323 if let Some(units) = posting.amount() {
325 let balance = running_balances.entry(posting.account.clone()).or_default();
326 balance.add(Position::simple(units.clone()));
327 }
328
329 let ctx = PostingContext {
330 transaction: txn,
331 posting_index: i,
332 balance: running_balances.get(&posting.account).cloned(),
333 directive_index: Some(directive_index),
334 };
335
336 if let Some(where_expr) = where_clause {
338 if self.evaluate_predicate(where_expr, &ctx)? {
339 postings.push(ctx);
340 }
341 } else {
342 postings.push(ctx);
343 }
344 }
345 }
346 }
347
348 Ok(postings)
349 }
350 fn evaluate_function(
351 &self,
352 func: &FunctionCall,
353 ctx: &PostingContext,
354 ) -> Result<Value, QueryError> {
355 let name = func.name.to_uppercase();
356 match name.as_str() {
357 "YEAR" | "MONTH" | "DAY" | "WEEKDAY" | "QUARTER" | "YMONTH" | "TODAY" => {
359 self.eval_date_function(&name, func, ctx)
360 }
361 "DATE" | "DATE_DIFF" | "DATE_ADD" | "DATE_TRUNC" | "DATE_PART" | "PARSE_DATE"
363 | "DATE_BIN" | "INTERVAL" => self.eval_extended_date_function(&name, func, ctx),
364 "LENGTH" | "UPPER" | "LOWER" | "SUBSTR" | "SUBSTRING" | "TRIM" | "STARTSWITH"
366 | "ENDSWITH" | "GREP" | "GREPN" | "SUBST" | "SPLITCOMP" | "JOINSTR" | "MAXWIDTH" => {
367 self.eval_string_function(&name, func, ctx)
368 }
369 "PARENT" | "LEAF" | "ROOT" | "ACCOUNT_DEPTH" | "ACCOUNT_SORTKEY" => {
371 self.eval_account_function(&name, func, ctx)
372 }
373 "OPEN_DATE" | "CLOSE_DATE" | "OPEN_META" => {
375 self.eval_account_meta_function(&name, func, ctx)
376 }
377 "ABS" | "NEG" | "ROUND" | "SAFEDIV" => self.eval_math_function(&name, func, ctx),
379 "NUMBER" | "CURRENCY" | "GETITEM" | "GET" | "UNITS" | "COST" | "WEIGHT" | "VALUE" => {
381 self.eval_position_function(&name, func, ctx)
382 }
383 "EMPTY" | "FILTER_CURRENCY" | "POSSIGN" => {
385 self.eval_inventory_function(&name, func, ctx)
386 }
387 "GETPRICE" => self.eval_getprice(func, ctx),
389 "COALESCE" => self.eval_coalesce(func, ctx),
391 "ONLY" => self.eval_only(func, ctx),
392 "META" | "ENTRY_META" | "ANY_META" | "POSTING_META" => {
394 self.eval_meta_function(&name, func, ctx)
395 }
396 "CONVERT" => self.eval_convert(func, ctx),
398 "INT" => self.eval_int(func, ctx),
400 "DECIMAL" => self.eval_decimal(func, ctx),
401 "STR" => self.eval_str(func, ctx),
402 "BOOL" => self.eval_bool(func, ctx),
403 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
406 _ => Err(QueryError::UnknownFunction(func.name.clone())),
407 }
408 }
409
410 fn evaluate_function_on_values(&self, name: &str, args: &[Value]) -> Result<Value, QueryError> {
412 let name_upper = name.to_uppercase();
413 match name_upper.as_str() {
414 "TODAY" => Ok(Value::Date(chrono::Local::now().date_naive())),
416 "YEAR" => {
417 Self::require_args_count(&name_upper, args, 1)?;
418 match &args[0] {
419 Value::Date(d) => Ok(Value::Integer(d.year().into())),
420 _ => Err(QueryError::Type("YEAR expects a date".to_string())),
421 }
422 }
423 "MONTH" => {
424 Self::require_args_count(&name_upper, args, 1)?;
425 match &args[0] {
426 Value::Date(d) => Ok(Value::Integer(d.month().into())),
427 _ => Err(QueryError::Type("MONTH expects a date".to_string())),
428 }
429 }
430 "DAY" => {
431 Self::require_args_count(&name_upper, args, 1)?;
432 match &args[0] {
433 Value::Date(d) => Ok(Value::Integer(d.day().into())),
434 _ => Err(QueryError::Type("DAY expects a date".to_string())),
435 }
436 }
437 "LENGTH" => {
439 Self::require_args_count(&name_upper, args, 1)?;
440 match &args[0] {
441 Value::String(s) => Ok(Value::Integer(s.len() as i64)),
442 _ => Err(QueryError::Type("LENGTH expects a string".to_string())),
443 }
444 }
445 "UPPER" => {
446 Self::require_args_count(&name_upper, args, 1)?;
447 match &args[0] {
448 Value::String(s) => Ok(Value::String(s.to_uppercase())),
449 _ => Err(QueryError::Type("UPPER expects a string".to_string())),
450 }
451 }
452 "LOWER" => {
453 Self::require_args_count(&name_upper, args, 1)?;
454 match &args[0] {
455 Value::String(s) => Ok(Value::String(s.to_lowercase())),
456 _ => Err(QueryError::Type("LOWER expects a string".to_string())),
457 }
458 }
459 "TRIM" => {
460 Self::require_args_count(&name_upper, args, 1)?;
461 match &args[0] {
462 Value::String(s) => Ok(Value::String(s.trim().to_string())),
463 _ => Err(QueryError::Type("TRIM expects a string".to_string())),
464 }
465 }
466 "ABS" => {
468 Self::require_args_count(&name_upper, args, 1)?;
469 match &args[0] {
470 Value::Number(n) => Ok(Value::Number(n.abs())),
471 Value::Integer(i) => Ok(Value::Integer(i.abs())),
472 _ => Err(QueryError::Type("ABS expects a number".to_string())),
473 }
474 }
475 "ROUND" => {
476 if args.is_empty() || args.len() > 2 {
477 return Err(QueryError::InvalidArguments(
478 "ROUND".to_string(),
479 "expected 1 or 2 arguments".to_string(),
480 ));
481 }
482 match &args[0] {
483 Value::Number(n) => {
484 let scale = if args.len() == 2 {
485 match &args[1] {
486 Value::Integer(i) => *i as u32,
487 _ => 0,
488 }
489 } else {
490 0
491 };
492 Ok(Value::Number(n.round_dp(scale)))
493 }
494 Value::Integer(i) => Ok(Value::Integer(*i)),
495 _ => Err(QueryError::Type("ROUND expects a number".to_string())),
496 }
497 }
498 "COALESCE" => {
500 for arg in args {
501 if !matches!(arg, Value::Null) {
502 return Ok(arg.clone());
503 }
504 }
505 Ok(Value::Null)
506 }
507 "NUMBER" => {
509 Self::require_args_count(&name_upper, args, 1)?;
510 match &args[0] {
511 Value::Amount(a) => Ok(Value::Number(a.number)),
512 Value::Position(p) => Ok(Value::Number(p.units.number)),
513 Value::Number(n) => Ok(Value::Number(*n)),
514 Value::Integer(i) => Ok(Value::Number(Decimal::from(*i))),
515 Value::Inventory(inv) => {
516 let positions = inv.positions();
519 if positions.is_empty() {
520 return Ok(Value::Number(Decimal::ZERO));
521 }
522 let first_currency = &positions[0].units.currency;
523 let all_same_currency = positions
524 .iter()
525 .all(|p| &p.units.currency == first_currency);
526 if all_same_currency {
527 let total: Decimal = positions.iter().map(|p| p.units.number).sum();
528 Ok(Value::Number(total))
529 } else {
530 Ok(Value::Null)
532 }
533 }
534 Value::Null => Ok(Value::Null),
535 _ => Err(QueryError::Type(
536 "NUMBER expects an amount, position, or inventory".to_string(),
537 )),
538 }
539 }
540 "CURRENCY" => {
541 Self::require_args_count(&name_upper, args, 1)?;
542 match &args[0] {
543 Value::Amount(a) => Ok(Value::String(a.currency.to_string())),
544 Value::Position(p) => Ok(Value::String(p.units.currency.to_string())),
545 Value::Inventory(inv) => {
546 if let Some(pos) = inv.positions().first() {
548 Ok(Value::String(pos.units.currency.to_string()))
549 } else {
550 Ok(Value::Null)
551 }
552 }
553 Value::Null => Ok(Value::Null),
554 _ => Err(QueryError::Type(
555 "CURRENCY expects an amount or position".to_string(),
556 )),
557 }
558 }
559 "UNITS" => {
560 Self::require_args_count(&name_upper, args, 1)?;
561 match &args[0] {
562 Value::Position(p) => Ok(Value::Amount(p.units.clone())),
563 Value::Amount(a) => Ok(Value::Amount(a.clone())),
564 Value::Inventory(inv) => {
565 let mut units_inv = Inventory::new();
567 for pos in inv.positions() {
568 units_inv.add(Position::simple(pos.units.clone()));
569 }
570 Ok(Value::Inventory(Box::new(units_inv)))
571 }
572 Value::Null => Ok(Value::Null),
573 _ => Err(QueryError::Type(
574 "UNITS expects a position or inventory".to_string(),
575 )),
576 }
577 }
578 "COST" => {
579 Self::require_args_count(&name_upper, args, 1)?;
580 match &args[0] {
581 Value::Position(p) => {
582 if let Some(cost) = &p.cost {
583 let total = p.units.number.abs() * cost.number;
584 Ok(Value::Amount(Amount::new(total, cost.currency.clone())))
585 } else {
586 Ok(Value::Null)
587 }
588 }
589 Value::Amount(a) => Ok(Value::Amount(a.clone())),
590 Value::Inventory(inv) => {
591 let mut total = Decimal::ZERO;
592 let mut currency: Option<InternedStr> = None;
593 for pos in inv.positions() {
594 if let Some(cost) = &pos.cost {
595 total += pos.units.number.abs() * cost.number;
596 if currency.is_none() {
597 currency = Some(cost.currency.clone());
598 }
599 }
600 }
601 if let Some(curr) = currency {
602 Ok(Value::Amount(Amount::new(total, curr)))
603 } else {
604 Ok(Value::Null)
605 }
606 }
607 Value::Null => Ok(Value::Null),
608 _ => Err(QueryError::Type(
609 "COST expects a position or inventory".to_string(),
610 )),
611 }
612 }
613 "VALUE" => {
614 if args.is_empty() || args.len() > 2 {
617 return Err(QueryError::InvalidArguments(
618 "VALUE".to_string(),
619 "expected 1-2 arguments".to_string(),
620 ));
621 }
622 let target_currency = if args.len() == 2 {
623 match &args[1] {
624 Value::String(s) => Some(s.clone()),
625 _ => None,
626 }
627 } else {
628 self.target_currency.clone()
629 };
630 match &args[0] {
631 Value::Position(p) => {
632 if let Some(ref target) = target_currency {
633 if p.units.currency.as_str() == target {
634 return Ok(Value::Amount(p.units.clone()));
635 }
636 if let Some(converted) =
638 self.price_db.convert(&p.units, target, self.query_date)
639 {
640 return Ok(Value::Amount(converted));
641 }
642 }
643 Ok(Value::Amount(p.units.clone()))
644 }
645 Value::Amount(a) => {
646 if let Some(ref target) = target_currency {
647 if a.currency.as_str() == target {
648 return Ok(Value::Amount(a.clone()));
649 }
650 if let Some(converted) =
651 self.price_db.convert(a, target, self.query_date)
652 {
653 return Ok(Value::Amount(converted));
654 }
655 }
656 Ok(Value::Amount(a.clone()))
657 }
658 Value::Inventory(inv) => {
659 if let Some(ref target) = target_currency {
660 let mut total = Decimal::ZERO;
661 for pos in inv.positions() {
662 if pos.units.currency.as_str() == target {
663 total += pos.units.number;
664 } else if let Some(converted) =
665 self.price_db.convert(&pos.units, target, self.query_date)
666 {
667 total += converted.number;
668 }
669 }
670 return Ok(Value::Amount(Amount::new(total, target)));
671 }
672 Ok(Value::Inventory(inv.clone()))
674 }
675 Value::Null => Ok(Value::Null),
676 _ => Err(QueryError::Type(
677 "VALUE expects a position or inventory".to_string(),
678 )),
679 }
680 }
681 "SAFEDIV" => {
683 Self::require_args_count(&name_upper, args, 2)?;
684 let (dividend, divisor) = (&args[0], &args[1]);
685 match (dividend, divisor) {
686 (Value::Number(a), Value::Number(b)) => {
687 if b.is_zero() {
688 Ok(Value::Null)
689 } else {
690 Ok(Value::Number(a / b))
691 }
692 }
693 (Value::Integer(a), Value::Integer(b)) => {
694 if *b == 0 {
695 Ok(Value::Null)
696 } else {
697 Ok(Value::Number(Decimal::from(*a) / Decimal::from(*b)))
698 }
699 }
700 (Value::Number(a), Value::Integer(b)) => {
701 if *b == 0 {
702 Ok(Value::Null)
703 } else {
704 Ok(Value::Number(a / Decimal::from(*b)))
705 }
706 }
707 (Value::Integer(a), Value::Number(b)) => {
708 if b.is_zero() {
709 Ok(Value::Null)
710 } else {
711 Ok(Value::Number(Decimal::from(*a) / b))
712 }
713 }
714 (Value::Null, _) | (_, Value::Null) => Ok(Value::Null),
715 _ => Err(QueryError::Type(
716 "SAFEDIV expects numeric arguments".to_string(),
717 )),
718 }
719 }
720 "NEG" => {
721 Self::require_args_count(&name_upper, args, 1)?;
722 match &args[0] {
723 Value::Number(n) => Ok(Value::Number(-n)),
724 Value::Integer(i) => Ok(Value::Integer(-i)),
725 Value::Amount(a) => {
726 Ok(Value::Amount(Amount::new(-a.number, a.currency.clone())))
727 }
728 _ => Err(QueryError::Type(
729 "NEG expects a number or amount".to_string(),
730 )),
731 }
732 }
733 "ACCOUNT_SORTKEY" => {
735 Self::require_args_count(&name_upper, args, 1)?;
736 match &args[0] {
737 Value::String(s) => {
738 let type_index = Self::account_type_index(s);
739 Ok(Value::String(format!("{type_index}-{s}")))
740 }
741 _ => Err(QueryError::Type(
742 "ACCOUNT_SORTKEY expects an account string".to_string(),
743 )),
744 }
745 }
746 "PARENT" => {
747 Self::require_args_count(&name_upper, args, 1)?;
748 match &args[0] {
749 Value::String(s) => {
750 if let Some(idx) = s.rfind(':') {
751 Ok(Value::String(s[..idx].to_string()))
752 } else {
753 Ok(Value::Null)
754 }
755 }
756 _ => Err(QueryError::Type(
757 "PARENT expects an account string".to_string(),
758 )),
759 }
760 }
761 "LEAF" => {
762 Self::require_args_count(&name_upper, args, 1)?;
763 match &args[0] {
764 Value::String(s) => {
765 if let Some(idx) = s.rfind(':') {
766 Ok(Value::String(s[idx + 1..].to_string()))
767 } else {
768 Ok(Value::String(s.clone()))
769 }
770 }
771 _ => Err(QueryError::Type(
772 "LEAF expects an account string".to_string(),
773 )),
774 }
775 }
776 "ROOT" => {
777 if args.is_empty() || args.len() > 2 {
778 return Err(QueryError::InvalidArguments(
779 "ROOT".to_string(),
780 "expected 1 or 2 arguments".to_string(),
781 ));
782 }
783 let n = if args.len() == 2 {
784 match &args[1] {
785 Value::Integer(i) => *i as usize,
786 _ => 1,
787 }
788 } else {
789 1
790 };
791 match &args[0] {
792 Value::String(s) => {
793 let parts: Vec<&str> = s.split(':').collect();
794 if n >= parts.len() {
795 Ok(Value::String(s.clone()))
796 } else {
797 Ok(Value::String(parts[..n].join(":")))
798 }
799 }
800 _ => Err(QueryError::Type(
801 "ROOT expects an account string".to_string(),
802 )),
803 }
804 }
805 "ONLY" => {
807 Self::require_args_count(&name_upper, args, 2)?;
808 let currency = match &args[0] {
809 Value::String(s) => s.clone(),
810 _ => {
811 return Err(QueryError::Type(
812 "ONLY: first argument must be a currency string".to_string(),
813 ));
814 }
815 };
816 match &args[1] {
817 Value::Inventory(inv) => {
818 let total = inv.units(¤cy);
819 if total.is_zero() {
820 Ok(Value::Null)
821 } else {
822 Ok(Value::Amount(Amount::new(total, ¤cy)))
823 }
824 }
825 Value::Position(p) => {
826 if p.units.currency.as_str() == currency {
827 Ok(Value::Amount(p.units.clone()))
828 } else {
829 Ok(Value::Null)
830 }
831 }
832 Value::Amount(a) => {
833 if a.currency.as_str() == currency {
834 Ok(Value::Amount(a.clone()))
835 } else {
836 Ok(Value::Null)
837 }
838 }
839 Value::Null => Ok(Value::Null),
840 _ => Err(QueryError::Type(
841 "ONLY: second argument must be an inventory, position, or amount"
842 .to_string(),
843 )),
844 }
845 }
846 "GETPRICE" => {
848 if args.len() < 2 || args.len() > 3 {
849 return Err(QueryError::InvalidArguments(
850 "GETPRICE".to_string(),
851 "expected 2 or 3 arguments".to_string(),
852 ));
853 }
854 let base = match &args[0] {
856 Value::String(s) => s.clone(),
857 Value::Null => return Ok(Value::Null),
858 _ => {
859 return Err(QueryError::Type(
860 "GETPRICE: first argument must be a currency string".to_string(),
861 ));
862 }
863 };
864 let quote = match &args[1] {
865 Value::String(s) => s.clone(),
866 Value::Null => return Ok(Value::Null),
867 _ => {
868 return Err(QueryError::Type(
869 "GETPRICE: second argument must be a currency string".to_string(),
870 ));
871 }
872 };
873 let date = if args.len() == 3 {
874 match &args[2] {
875 Value::Date(d) => *d,
876 Value::Null => self.query_date,
877 _ => self.query_date,
878 }
879 } else {
880 self.query_date
881 };
882 match self.price_db.get_price(&base, "e, date) {
883 Some(price) => Ok(Value::Number(price)),
884 None => Ok(Value::Null),
885 }
886 }
887 "EMPTY" => {
889 Self::require_args_count(&name_upper, args, 1)?;
890 match &args[0] {
891 Value::Inventory(inv) => Ok(Value::Boolean(inv.is_empty())),
892 Value::Null => Ok(Value::Boolean(true)),
893 _ => Err(QueryError::Type("EMPTY expects an inventory".to_string())),
894 }
895 }
896 "FILTER_CURRENCY" => {
897 Self::require_args_count(&name_upper, args, 2)?;
898 let currency = match &args[1] {
899 Value::String(s) => s.clone(),
900 _ => {
901 return Err(QueryError::Type(
902 "FILTER_CURRENCY expects (inventory, string)".to_string(),
903 ));
904 }
905 };
906 match &args[0] {
907 Value::Inventory(inv) => {
908 let filtered: Vec<Position> = inv
909 .positions()
910 .iter()
911 .filter(|p| p.units.currency.as_str() == currency)
912 .cloned()
913 .collect();
914 let mut new_inv = Inventory::new();
915 for pos in filtered {
916 new_inv.add(pos);
917 }
918 Ok(Value::Inventory(Box::new(new_inv)))
919 }
920 Value::Null => Ok(Value::Null),
921 _ => Err(QueryError::Type(
922 "FILTER_CURRENCY expects (inventory, string)".to_string(),
923 )),
924 }
925 }
926 "POSSIGN" => {
927 Self::require_args_count(&name_upper, args, 2)?;
928 let account_str = match &args[1] {
929 Value::String(s) => s.clone(),
930 _ => {
931 return Err(QueryError::Type(
932 "POSSIGN expects (amount, account_string)".to_string(),
933 ));
934 }
935 };
936 let first_component = account_str.split(':').next().unwrap_or("");
937 let is_credit_normal =
938 matches!(first_component, "Liabilities" | "Equity" | "Income");
939 match &args[0] {
940 Value::Amount(a) => {
941 let mut amt = a.clone();
942 if is_credit_normal {
943 amt.number = -amt.number;
944 }
945 Ok(Value::Amount(amt))
946 }
947 Value::Number(n) => {
948 let adjusted = if is_credit_normal { -n } else { *n };
949 Ok(Value::Number(adjusted))
950 }
951 Value::Null => Ok(Value::Null),
952 _ => Err(QueryError::Type(
953 "POSSIGN expects (amount, account_string)".to_string(),
954 )),
955 }
956 }
957 "SUM" | "COUNT" | "MIN" | "MAX" | "FIRST" | "LAST" | "AVG" => Ok(Value::Null),
959 _ => Err(QueryError::UnknownFunction(name.to_string())),
960 }
961 }
962
963 fn require_args_count(name: &str, args: &[Value], expected: usize) -> Result<(), QueryError> {
965 if args.len() != expected {
966 return Err(QueryError::InvalidArguments(
967 name.to_string(),
968 format!("expected {} argument(s), got {}", expected, args.len()),
969 ));
970 }
971 Ok(())
972 }
973
974 fn require_args(name: &str, func: &FunctionCall, expected: usize) -> Result<(), QueryError> {
976 if func.args.len() != expected {
977 return Err(QueryError::InvalidArguments(
978 name.to_string(),
979 format!("expected {expected} argument(s)"),
980 ));
981 }
982 Ok(())
983 }
984 pub(super) const fn is_window_expr(expr: &Expr) -> bool {
986 matches!(expr, Expr::Window(_))
987 }
988
989 fn resolve_column_names(&self, targets: &[Target]) -> Result<Vec<String>, QueryError> {
991 let mut names = Vec::new();
992 for (i, target) in targets.iter().enumerate() {
993 if let Some(alias) = &target.alias {
994 names.push(alias.clone());
995 } else {
996 names.push(self.expr_to_name(&target.expr, i));
997 }
998 }
999 Ok(names)
1000 }
1001
1002 fn expr_to_name(&self, expr: &Expr, index: usize) -> String {
1004 match expr {
1005 Expr::Wildcard => "*".to_string(),
1006 Expr::Column(name) => name.clone(),
1007 Expr::Function(func) => func.name.clone(),
1008 Expr::Window(wf) => wf.name.clone(),
1009 _ => format!("col{index}"),
1010 }
1011 }
1012}
1013#[cfg(test)]
1014mod tests {
1015 use super::types::{hash_row, hash_single_value};
1016 use super::*;
1017 use crate::parse;
1018 use rust_decimal_macros::dec;
1019 use rustledger_core::Posting;
1020
1021 fn date(year: i32, month: u32, day: u32) -> NaiveDate {
1022 NaiveDate::from_ymd_opt(year, month, day).unwrap()
1023 }
1024
1025 fn sample_directives() -> Vec<Directive> {
1026 vec![
1027 Directive::Transaction(
1028 Transaction::new(date(2024, 1, 15), "Coffee")
1029 .with_flag('*')
1030 .with_payee("Coffee Shop")
1031 .with_posting(Posting::new(
1032 "Expenses:Food:Coffee",
1033 Amount::new(dec!(5.00), "USD"),
1034 ))
1035 .with_posting(Posting::new(
1036 "Assets:Bank:Checking",
1037 Amount::new(dec!(-5.00), "USD"),
1038 )),
1039 ),
1040 Directive::Transaction(
1041 Transaction::new(date(2024, 1, 16), "Groceries")
1042 .with_flag('*')
1043 .with_payee("Supermarket")
1044 .with_posting(Posting::new(
1045 "Expenses:Food:Groceries",
1046 Amount::new(dec!(50.00), "USD"),
1047 ))
1048 .with_posting(Posting::new(
1049 "Assets:Bank:Checking",
1050 Amount::new(dec!(-50.00), "USD"),
1051 )),
1052 ),
1053 ]
1054 }
1055
1056 #[test]
1057 fn test_simple_select() {
1058 let directives = sample_directives();
1059 let mut executor = Executor::new(&directives);
1060
1061 let query = parse("SELECT date, account").unwrap();
1062 let result = executor.execute(&query).unwrap();
1063
1064 assert_eq!(result.columns, vec!["date", "account"]);
1065 assert_eq!(result.len(), 4); }
1067
1068 #[test]
1069 fn test_where_clause() {
1070 let directives = sample_directives();
1071 let mut executor = Executor::new(&directives);
1072
1073 let query = parse("SELECT account WHERE account ~ \"Expenses:\"").unwrap();
1074 let result = executor.execute(&query).unwrap();
1075
1076 assert_eq!(result.len(), 2); }
1078
1079 #[test]
1080 fn test_balances() {
1081 let directives = sample_directives();
1082 let mut executor = Executor::new(&directives);
1083
1084 let query = parse("BALANCES").unwrap();
1085 let result = executor.execute(&query).unwrap();
1086
1087 assert_eq!(result.columns, vec!["account", "balance"]);
1088 assert!(result.len() >= 3); }
1090
1091 #[test]
1092 fn test_account_functions() {
1093 let directives = sample_directives();
1094 let mut executor = Executor::new(&directives);
1095
1096 let query = parse("SELECT DISTINCT LEAF(account) WHERE account ~ \"Expenses:\"").unwrap();
1098 let result = executor.execute(&query).unwrap();
1099 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT ROOT(account)").unwrap();
1103 let result = executor.execute(&query).unwrap();
1104 assert_eq!(result.len(), 2); let query = parse("SELECT DISTINCT PARENT(account) WHERE account ~ \"Expenses:\"").unwrap();
1108 let result = executor.execute(&query).unwrap();
1109 assert!(!result.is_empty()); }
1111
1112 #[test]
1113 fn test_min_max_aggregate() {
1114 let directives = sample_directives();
1115 let mut executor = Executor::new(&directives);
1116
1117 let query = parse("SELECT MIN(date)").unwrap();
1119 let result = executor.execute(&query).unwrap();
1120 assert_eq!(result.len(), 1);
1121 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
1122
1123 let query = parse("SELECT MAX(date)").unwrap();
1125 let result = executor.execute(&query).unwrap();
1126 assert_eq!(result.len(), 1);
1127 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1128 }
1129
1130 #[test]
1131 fn test_order_by() {
1132 let directives = sample_directives();
1133 let mut executor = Executor::new(&directives);
1134
1135 let query = parse("SELECT date, account ORDER BY date DESC").unwrap();
1136 let result = executor.execute(&query).unwrap();
1137
1138 assert_eq!(result.len(), 4);
1140 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1142 }
1143
1144 #[test]
1145 fn test_hash_value_all_variants() {
1146 use rustledger_core::{Cost, Inventory, Position};
1147
1148 let values = vec![
1150 Value::String("test".to_string()),
1151 Value::Number(dec!(123.45)),
1152 Value::Integer(42),
1153 Value::Date(date(2024, 1, 15)),
1154 Value::Boolean(true),
1155 Value::Boolean(false),
1156 Value::Amount(Amount::new(dec!(100), "USD")),
1157 Value::Position(Box::new(Position::simple(Amount::new(dec!(10), "AAPL")))),
1158 Value::Position(Box::new(Position::with_cost(
1159 Amount::new(dec!(10), "AAPL"),
1160 Cost::new(dec!(150), "USD"),
1161 ))),
1162 Value::Inventory(Box::new(Inventory::new())),
1163 Value::StringSet(vec!["tag1".to_string(), "tag2".to_string()]),
1164 Value::Null,
1165 ];
1166
1167 for value in &values {
1169 let hash = hash_single_value(value);
1170 assert!(hash != 0 || matches!(value, Value::Null));
1171 }
1172
1173 let hash1 = hash_single_value(&Value::String("a".to_string()));
1175 let hash2 = hash_single_value(&Value::String("b".to_string()));
1176 assert_ne!(hash1, hash2);
1177
1178 let hash3 = hash_single_value(&Value::Integer(42));
1180 let hash4 = hash_single_value(&Value::Integer(42));
1181 assert_eq!(hash3, hash4);
1182 }
1183
1184 #[test]
1185 fn test_hash_row_distinct() {
1186 let row1 = vec![Value::String("a".to_string()), Value::Integer(1)];
1188 let row2 = vec![Value::String("a".to_string()), Value::Integer(1)];
1189 let row3 = vec![Value::String("b".to_string()), Value::Integer(1)];
1190
1191 assert_eq!(hash_row(&row1), hash_row(&row2));
1192 assert_ne!(hash_row(&row1), hash_row(&row3));
1193 }
1194
1195 #[test]
1196 fn test_string_set_hash_order_independent() {
1197 let set1 = Value::StringSet(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
1199 let set2 = Value::StringSet(vec!["c".to_string(), "a".to_string(), "b".to_string()]);
1200 let set3 = Value::StringSet(vec!["b".to_string(), "c".to_string(), "a".to_string()]);
1201
1202 let hash1 = hash_single_value(&set1);
1203 let hash2 = hash_single_value(&set2);
1204 let hash3 = hash_single_value(&set3);
1205
1206 assert_eq!(hash1, hash2);
1207 assert_eq!(hash2, hash3);
1208 }
1209
1210 #[test]
1211 fn test_inventory_hash_includes_cost() {
1212 use rustledger_core::{Cost, Inventory, Position};
1213
1214 let mut inv1 = Inventory::new();
1216 inv1.add(Position::with_cost(
1217 Amount::new(dec!(10), "AAPL"),
1218 Cost::new(dec!(100), "USD"),
1219 ));
1220
1221 let mut inv2 = Inventory::new();
1222 inv2.add(Position::with_cost(
1223 Amount::new(dec!(10), "AAPL"),
1224 Cost::new(dec!(200), "USD"),
1225 ));
1226
1227 let hash1 = hash_single_value(&Value::Inventory(Box::new(inv1)));
1228 let hash2 = hash_single_value(&Value::Inventory(Box::new(inv2)));
1229
1230 assert_ne!(hash1, hash2);
1231 }
1232
1233 #[test]
1234 fn test_distinct_deduplication() {
1235 let directives = sample_directives();
1236 let mut executor = Executor::new(&directives);
1237
1238 let query = parse("SELECT flag").unwrap();
1240 let result = executor.execute(&query).unwrap();
1241 assert_eq!(result.len(), 4); let query = parse("SELECT DISTINCT flag").unwrap();
1245 let result = executor.execute(&query).unwrap();
1246 assert_eq!(result.len(), 1); }
1248
1249 #[test]
1250 fn test_limit_clause() {
1251 let directives = sample_directives();
1252 let mut executor = Executor::new(&directives);
1253
1254 let query = parse("SELECT date, account LIMIT 2").unwrap();
1256 let result = executor.execute(&query).unwrap();
1257 assert_eq!(result.len(), 2);
1258
1259 let query = parse("SELECT date LIMIT 0").unwrap();
1261 let result = executor.execute(&query).unwrap();
1262 assert_eq!(result.len(), 0);
1263
1264 let query = parse("SELECT date LIMIT 100").unwrap();
1266 let result = executor.execute(&query).unwrap();
1267 assert_eq!(result.len(), 4);
1268 }
1269
1270 #[test]
1271 fn test_group_by_with_count() {
1272 let directives = sample_directives();
1273 let mut executor = Executor::new(&directives);
1274
1275 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
1277 let result = executor.execute(&query).unwrap();
1278
1279 assert_eq!(result.columns.len(), 2);
1280 assert_eq!(result.len(), 2);
1282 }
1283
1284 #[test]
1285 fn test_count_aggregate() {
1286 let directives = sample_directives();
1287 let mut executor = Executor::new(&directives);
1288
1289 let query = parse("SELECT COUNT(account)").unwrap();
1291 let result = executor.execute(&query).unwrap();
1292
1293 assert_eq!(result.len(), 1);
1294 assert_eq!(result.rows[0][0], Value::Integer(4));
1295
1296 let query = parse("SELECT ROOT(account), COUNT(account) GROUP BY ROOT(account)").unwrap();
1298 let result = executor.execute(&query).unwrap();
1299 assert_eq!(result.len(), 2); }
1301
1302 #[test]
1303 fn test_journal_query() {
1304 let directives = sample_directives();
1305 let mut executor = Executor::new(&directives);
1306
1307 let query = parse("JOURNAL \"Expenses\"").unwrap();
1309 let result = executor.execute(&query).unwrap();
1310
1311 assert!(result.columns.contains(&"account".to_string()));
1313 assert_eq!(result.len(), 2);
1315 }
1316
1317 #[test]
1318 fn test_print_query() {
1319 let directives = sample_directives();
1320 let mut executor = Executor::new(&directives);
1321
1322 let query = parse("PRINT").unwrap();
1324 let result = executor.execute(&query).unwrap();
1325
1326 assert_eq!(result.columns.len(), 1);
1328 assert_eq!(result.columns[0], "directive");
1329 assert_eq!(result.len(), 2);
1331 }
1332
1333 #[test]
1334 fn test_empty_directives() {
1335 let directives: Vec<Directive> = vec![];
1336 let mut executor = Executor::new(&directives);
1337
1338 let query = parse("SELECT date, account").unwrap();
1340 let result = executor.execute(&query).unwrap();
1341 assert!(result.is_empty());
1342
1343 let query = parse("BALANCES").unwrap();
1345 let result = executor.execute(&query).unwrap();
1346 assert!(result.is_empty());
1347 }
1348
1349 #[test]
1350 fn test_comparison_operators() {
1351 let directives = sample_directives();
1352 let mut executor = Executor::new(&directives);
1353
1354 let query = parse("SELECT date WHERE date < 2024-01-16").unwrap();
1356 let result = executor.execute(&query).unwrap();
1357 assert_eq!(result.len(), 2); let query = parse("SELECT date WHERE year > 2023").unwrap();
1361 let result = executor.execute(&query).unwrap();
1362 assert_eq!(result.len(), 4); let query = parse("SELECT account WHERE day = 15").unwrap();
1366 let result = executor.execute(&query).unwrap();
1367 assert_eq!(result.len(), 2); }
1369
1370 #[test]
1371 fn test_logical_operators() {
1372 let directives = sample_directives();
1373 let mut executor = Executor::new(&directives);
1374
1375 let query = parse("SELECT account WHERE account ~ \"Expenses\" AND day > 14").unwrap();
1377 let result = executor.execute(&query).unwrap();
1378 assert_eq!(result.len(), 2); let query = parse("SELECT account WHERE day = 15 OR day = 16").unwrap();
1382 let result = executor.execute(&query).unwrap();
1383 assert_eq!(result.len(), 4); }
1385
1386 #[test]
1387 fn test_arithmetic_expressions() {
1388 let directives = sample_directives();
1389 let mut executor = Executor::new(&directives);
1390
1391 let query = parse("SELECT -day WHERE day = 15").unwrap();
1393 let result = executor.execute(&query).unwrap();
1394 assert_eq!(result.len(), 2);
1395 for row in &result.rows {
1397 if let Value::Integer(n) = &row[0] {
1398 assert_eq!(*n, -15);
1399 }
1400 }
1401 }
1402
1403 #[test]
1404 fn test_first_last_aggregates() {
1405 let directives = sample_directives();
1406 let mut executor = Executor::new(&directives);
1407
1408 let query = parse("SELECT FIRST(date)").unwrap();
1410 let result = executor.execute(&query).unwrap();
1411 assert_eq!(result.len(), 1);
1412 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15)));
1413
1414 let query = parse("SELECT LAST(date)").unwrap();
1416 let result = executor.execute(&query).unwrap();
1417 assert_eq!(result.len(), 1);
1418 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 16)));
1419 }
1420
1421 #[test]
1422 fn test_wildcard_select() {
1423 let directives = sample_directives();
1424 let mut executor = Executor::new(&directives);
1425
1426 let query = parse("SELECT *").unwrap();
1428 let result = executor.execute(&query).unwrap();
1429
1430 assert_eq!(result.columns, vec!["*"]);
1432 assert_eq!(result.len(), 4);
1434 assert_eq!(result.rows[0].len(), 6); }
1436
1437 #[test]
1438 fn test_query_result_methods() {
1439 let mut result = QueryResult::new(vec!["col1".to_string(), "col2".to_string()]);
1440
1441 assert!(result.is_empty());
1443 assert_eq!(result.len(), 0);
1444
1445 result.add_row(vec![Value::Integer(1), Value::String("a".to_string())]);
1447 assert!(!result.is_empty());
1448 assert_eq!(result.len(), 1);
1449
1450 result.add_row(vec![Value::Integer(2), Value::String("b".to_string())]);
1451 assert_eq!(result.len(), 2);
1452 }
1453
1454 #[test]
1455 fn test_type_cast_functions() {
1456 let directives = sample_directives();
1457 let mut executor = Executor::new(&directives);
1458
1459 let query = parse("SELECT int(5.7)").unwrap();
1461 let result = executor.execute(&query).unwrap();
1462 assert_eq!(result.rows[0][0], Value::Integer(5));
1463
1464 let query = parse("SELECT decimal(42)").unwrap();
1466 let result = executor.execute(&query).unwrap();
1467 assert_eq!(result.rows[0][0], Value::Number(dec!(42)));
1468
1469 let query = parse("SELECT str(123)").unwrap();
1471 let result = executor.execute(&query).unwrap();
1472 assert_eq!(result.rows[0][0], Value::String("123".to_string()));
1473
1474 let query = parse("SELECT bool(1)").unwrap();
1476 let result = executor.execute(&query).unwrap();
1477 assert_eq!(result.rows[0][0], Value::Boolean(true));
1478
1479 let query = parse("SELECT bool(0)").unwrap();
1480 let result = executor.execute(&query).unwrap();
1481 assert_eq!(result.rows[0][0], Value::Boolean(false));
1482 }
1483
1484 #[test]
1485 fn test_meta_functions() {
1486 use std::collections::HashMap;
1487
1488 let mut txn_meta: HashMap<String, MetaValue> = HashMap::new();
1490 txn_meta.insert(
1491 "source".to_string(),
1492 MetaValue::String("bank_import".to_string()),
1493 );
1494
1495 let mut posting_meta: HashMap<String, MetaValue> = HashMap::new();
1496 posting_meta.insert(
1497 "category".to_string(),
1498 MetaValue::String("food".to_string()),
1499 );
1500
1501 let txn = Transaction {
1502 date: date(2024, 1, 15),
1503 flag: '*',
1504 payee: Some("Coffee Shop".into()),
1505 narration: "Coffee".into(),
1506 tags: vec![],
1507 links: vec![],
1508 meta: txn_meta,
1509 postings: vec![
1510 Posting {
1511 account: "Expenses:Food".into(),
1512 units: Some(rustledger_core::IncompleteAmount::Complete(Amount::new(
1513 dec!(5),
1514 "USD",
1515 ))),
1516 cost: None,
1517 price: None,
1518 flag: None,
1519 meta: posting_meta,
1520 },
1521 Posting::new("Assets:Cash", Amount::new(dec!(-5), "USD")),
1522 ],
1523 };
1524
1525 let directives = vec![Directive::Transaction(txn)];
1526 let mut executor = Executor::new(&directives);
1527
1528 let query = parse("SELECT meta('category') WHERE account ~ 'Expenses'").unwrap();
1530 let result = executor.execute(&query).unwrap();
1531 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
1532
1533 let query = parse("SELECT entry_meta('source') WHERE account ~ 'Expenses'").unwrap();
1535 let result = executor.execute(&query).unwrap();
1536 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
1537
1538 let query = parse("SELECT any_meta('source') WHERE account ~ 'Expenses'").unwrap();
1540 let result = executor.execute(&query).unwrap();
1541 assert_eq!(result.rows[0][0], Value::String("bank_import".to_string()));
1542
1543 let query = parse("SELECT any_meta('category') WHERE account ~ 'Expenses'").unwrap();
1545 let result = executor.execute(&query).unwrap();
1546 assert_eq!(result.rows[0][0], Value::String("food".to_string()));
1547
1548 let query = parse("SELECT meta('nonexistent') WHERE account ~ 'Expenses'").unwrap();
1550 let result = executor.execute(&query).unwrap();
1551 assert_eq!(result.rows[0][0], Value::Null);
1552 }
1553
1554 #[test]
1555 fn test_convert_function() {
1556 let price = rustledger_core::Price {
1558 date: date(2024, 1, 1),
1559 currency: "EUR".into(),
1560 amount: Amount::new(dec!(1.10), "USD"),
1561 meta: HashMap::new(),
1562 };
1563
1564 let txn = Transaction::new(date(2024, 1, 15), "Test")
1565 .with_flag('*')
1566 .with_posting(Posting::new("Assets:Euro", Amount::new(dec!(100), "EUR")))
1567 .with_posting(Posting::new("Assets:Cash", Amount::new(dec!(-110), "USD")));
1568
1569 let directives = vec![Directive::Price(price), Directive::Transaction(txn)];
1570 let mut executor = Executor::new(&directives);
1571
1572 let query = parse("SELECT convert(position, 'USD') WHERE account ~ 'Euro'").unwrap();
1574 let result = executor.execute(&query).unwrap();
1575 match &result.rows[0][0] {
1577 Value::Amount(a) => {
1578 assert_eq!(a.number, dec!(110));
1579 assert_eq!(a.currency.as_ref(), "USD");
1580 }
1581 _ => panic!("Expected Amount, got {:?}", result.rows[0][0]),
1582 }
1583 }
1584
1585 #[test]
1586 fn test_date_functions() {
1587 let directives = sample_directives();
1588 let mut executor = Executor::new(&directives);
1589
1590 let query = parse("SELECT date('2024-06-15')").unwrap();
1592 let result = executor.execute(&query).unwrap();
1593 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1594
1595 let query = parse("SELECT date(2024, 6, 15)").unwrap();
1597 let result = executor.execute(&query).unwrap();
1598 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1599
1600 let query = parse("SELECT date_diff(date('2024-01-20'), date('2024-01-15'))").unwrap();
1602 let result = executor.execute(&query).unwrap();
1603 assert_eq!(result.rows[0][0], Value::Integer(5));
1604
1605 let query = parse("SELECT date_add(date('2024-01-15'), 10)").unwrap();
1607 let result = executor.execute(&query).unwrap();
1608 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 25)));
1609
1610 let query = parse("SELECT date_trunc('year', date('2024-06-15'))").unwrap();
1612 let result = executor.execute(&query).unwrap();
1613 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1)));
1614
1615 let query = parse("SELECT date_trunc('month', date('2024-06-15'))").unwrap();
1617 let result = executor.execute(&query).unwrap();
1618 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1)));
1619
1620 let query = parse("SELECT date_part('month', date('2024-06-15'))").unwrap();
1622 let result = executor.execute(&query).unwrap();
1623 assert_eq!(result.rows[0][0], Value::Integer(6));
1624
1625 let query = parse("SELECT parse_date('15/06/2024', '%d/%m/%Y')").unwrap();
1627 let result = executor.execute(&query).unwrap();
1628 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 15)));
1629
1630 let query =
1632 parse("SELECT date_bin('7 days', date('2024-01-15'), date('2024-01-01'))").unwrap();
1633 let result = executor.execute(&query).unwrap();
1634 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
1638 parse("SELECT date_bin('1 week', date('2024-01-20'), date('2024-01-01'))").unwrap();
1639 let result = executor.execute(&query).unwrap();
1640 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 15))); let query =
1644 parse("SELECT date_bin('1 month', date('2024-06-15'), date('2024-01-01'))").unwrap();
1645 let result = executor.execute(&query).unwrap();
1646 assert_eq!(result.rows[0][0], Value::Date(date(2024, 6, 1))); let query =
1650 parse("SELECT date_bin('1 year', date('2024-06-15'), date('2020-01-01'))").unwrap();
1651 let result = executor.execute(&query).unwrap();
1652 assert_eq!(result.rows[0][0], Value::Date(date(2024, 1, 1))); }
1654
1655 #[test]
1656 fn test_string_functions_extended() {
1657 let directives = sample_directives();
1658 let mut executor = Executor::new(&directives);
1659
1660 let query = parse("SELECT grep('Ex[a-z]+', 'Hello Expenses World')").unwrap();
1662 let result = executor.execute(&query).unwrap();
1663 assert_eq!(result.rows[0][0], Value::String("Expenses".to_string()));
1664
1665 let query = parse("SELECT grep('xyz', 'Hello World')").unwrap();
1667 let result = executor.execute(&query).unwrap();
1668 assert_eq!(result.rows[0][0], Value::Null);
1669
1670 let query = parse("SELECT grepn('([0-9]+)-([0-9]+)', '2024-01', 1)").unwrap();
1672 let result = executor.execute(&query).unwrap();
1673 assert_eq!(result.rows[0][0], Value::String("2024".to_string()));
1674
1675 let query = parse("SELECT subst('-', '/', '2024-01-15')").unwrap();
1677 let result = executor.execute(&query).unwrap();
1678 assert_eq!(result.rows[0][0], Value::String("2024/01/15".to_string()));
1679
1680 let query = parse("SELECT splitcomp('a:b:c', ':', 1)").unwrap();
1682 let result = executor.execute(&query).unwrap();
1683 assert_eq!(result.rows[0][0], Value::String("b".to_string()));
1684
1685 let query = parse("SELECT joinstr('hello', 'world')").unwrap();
1687 let result = executor.execute(&query).unwrap();
1688 assert_eq!(result.rows[0][0], Value::String("hello, world".to_string()));
1689
1690 let query = parse("SELECT maxwidth('hello', 10)").unwrap();
1692 let result = executor.execute(&query).unwrap();
1693 assert_eq!(result.rows[0][0], Value::String("hello".to_string()));
1694
1695 let query = parse("SELECT maxwidth('hello world', 8)").unwrap();
1697 let result = executor.execute(&query).unwrap();
1698 assert_eq!(result.rows[0][0], Value::String("hello...".to_string()));
1699 }
1700
1701 #[test]
1702 fn test_inventory_functions() {
1703 let directives = sample_directives();
1704 let mut executor = Executor::new(&directives);
1705
1706 let query = parse("SELECT empty(sum(position)) WHERE account ~ 'Assets'").unwrap();
1709 let result = executor.execute(&query).unwrap();
1710 assert!(matches!(result.rows[0][0], Value::Boolean(_)));
1712
1713 let query = parse("SELECT possign(100, 'Assets:Bank')").unwrap();
1718 let result = executor.execute(&query).unwrap();
1719 assert_eq!(
1720 result.rows[0][0],
1721 Value::Number(rust_decimal::Decimal::from(100))
1722 );
1723
1724 let query = parse("SELECT possign(100, 'Income:Salary')").unwrap();
1726 let result = executor.execute(&query).unwrap();
1727 assert_eq!(
1728 result.rows[0][0],
1729 Value::Number(rust_decimal::Decimal::from(-100))
1730 );
1731
1732 let query = parse("SELECT possign(50, 'Expenses:Food')").unwrap();
1734 let result = executor.execute(&query).unwrap();
1735 assert_eq!(
1736 result.rows[0][0],
1737 Value::Number(rust_decimal::Decimal::from(50))
1738 );
1739
1740 let query = parse("SELECT possign(200, 'Liabilities:CreditCard')").unwrap();
1742 let result = executor.execute(&query).unwrap();
1743 assert_eq!(
1744 result.rows[0][0],
1745 Value::Number(rust_decimal::Decimal::from(-200))
1746 );
1747
1748 let query = parse("SELECT possign(300, 'Equity:OpeningBalances')").unwrap();
1750 let result = executor.execute(&query).unwrap();
1751 assert_eq!(
1752 result.rows[0][0],
1753 Value::Number(rust_decimal::Decimal::from(-300))
1754 );
1755 }
1756
1757 #[test]
1758 fn test_account_meta_functions() {
1759 use rustledger_core::{Close, Metadata, Open};
1760
1761 let mut open_meta = Metadata::new();
1763 open_meta.insert(
1764 "category".to_string(),
1765 MetaValue::String("checking".to_string()),
1766 );
1767
1768 let directives = vec![
1769 Directive::Open(Open {
1770 date: date(2020, 1, 1),
1771 account: "Assets:Bank:Checking".into(),
1772 currencies: vec![],
1773 booking: None,
1774 meta: open_meta,
1775 }),
1776 Directive::Open(Open::new(date(2020, 2, 15), "Expenses:Food")),
1777 Directive::Close(Close::new(date(2024, 12, 31), "Assets:Bank:Checking")),
1778 Directive::Transaction(
1780 Transaction::new(date(2024, 1, 15), "Coffee")
1781 .with_posting(Posting::new(
1782 "Expenses:Food",
1783 Amount::new(dec!(5.00), "USD"),
1784 ))
1785 .with_posting(Posting::new(
1786 "Assets:Bank:Checking",
1787 Amount::new(dec!(-5.00), "USD"),
1788 )),
1789 ),
1790 ];
1791
1792 let mut executor = Executor::new(&directives);
1793
1794 let query = parse("SELECT open_date('Assets:Bank:Checking')").unwrap();
1796 let result = executor.execute(&query).unwrap();
1797 assert_eq!(result.rows[0][0], Value::Date(date(2020, 1, 1)));
1798
1799 let query = parse("SELECT close_date('Assets:Bank:Checking')").unwrap();
1801 let result = executor.execute(&query).unwrap();
1802 assert_eq!(result.rows[0][0], Value::Date(date(2024, 12, 31)));
1803
1804 let query = parse("SELECT close_date('Expenses:Food')").unwrap();
1806 let result = executor.execute(&query).unwrap();
1807 assert_eq!(result.rows[0][0], Value::Null);
1808
1809 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'category')").unwrap();
1811 let result = executor.execute(&query).unwrap();
1812 assert_eq!(result.rows[0][0], Value::String("checking".to_string()));
1813
1814 let query = parse("SELECT open_meta('Assets:Bank:Checking', 'nonexistent')").unwrap();
1816 let result = executor.execute(&query).unwrap();
1817 assert_eq!(result.rows[0][0], Value::Null);
1818
1819 let query = parse("SELECT open_date('NonExistent:Account')").unwrap();
1821 let result = executor.execute(&query).unwrap();
1822 assert_eq!(result.rows[0][0], Value::Null);
1823 }
1824
1825 #[test]
1826 fn test_source_location_columns_return_null_without_sources() {
1827 let directives = vec![Directive::Transaction(Transaction {
1830 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1831 flag: '*',
1832 payee: Some("Test".into()),
1833 narration: "Test transaction".into(),
1834 tags: vec![],
1835 links: vec![],
1836 meta: Metadata::new(),
1837 postings: vec![
1838 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
1839 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
1840 ],
1841 })];
1842
1843 let mut executor = Executor::new(&directives);
1844
1845 let query = parse("SELECT filename").unwrap();
1847 let result = executor.execute(&query).unwrap();
1848 assert_eq!(result.rows[0][0], Value::Null);
1849
1850 let query = parse("SELECT lineno").unwrap();
1852 let result = executor.execute(&query).unwrap();
1853 assert_eq!(result.rows[0][0], Value::Null);
1854
1855 let query = parse("SELECT location").unwrap();
1857 let result = executor.execute(&query).unwrap();
1858 assert_eq!(result.rows[0][0], Value::Null);
1859 }
1860
1861 #[test]
1862 fn test_source_location_columns_with_sources() {
1863 use rustledger_loader::SourceMap;
1864 use rustledger_parser::Spanned;
1865 use std::sync::Arc;
1866
1867 let mut source_map = SourceMap::new();
1869 let source: Arc<str> =
1870 "2024-01-15 * \"Test\"\n Assets:Bank 100 USD\n Expenses:Food".into();
1871 let file_id = source_map.add_file("test.beancount".into(), source);
1872
1873 let txn = Transaction {
1875 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1876 flag: '*',
1877 payee: Some("Test".into()),
1878 narration: "Test transaction".into(),
1879 tags: vec![],
1880 links: vec![],
1881 meta: Metadata::new(),
1882 postings: vec![
1883 Posting::new("Assets:Bank", Amount::new(dec!(100), "USD")),
1884 Posting::new("Expenses:Food", Amount::new(dec!(-100), "USD")),
1885 ],
1886 };
1887
1888 let spanned_directives = vec![Spanned {
1889 value: Directive::Transaction(txn),
1890 span: rustledger_parser::Span { start: 0, end: 50 },
1891 file_id: file_id as u16,
1892 }];
1893
1894 let mut executor = Executor::new_with_sources(&spanned_directives, &source_map);
1895
1896 let query = parse("SELECT filename").unwrap();
1898 let result = executor.execute(&query).unwrap();
1899 assert_eq!(
1900 result.rows[0][0],
1901 Value::String("test.beancount".to_string())
1902 );
1903
1904 let query = parse("SELECT lineno").unwrap();
1906 let result = executor.execute(&query).unwrap();
1907 assert_eq!(result.rows[0][0], Value::Integer(1));
1908
1909 let query = parse("SELECT location").unwrap();
1911 let result = executor.execute(&query).unwrap();
1912 assert_eq!(
1913 result.rows[0][0],
1914 Value::String("test.beancount:1".to_string())
1915 );
1916 }
1917
1918 #[test]
1919 fn test_interval_function() {
1920 let directives = sample_directives();
1921 let mut executor = Executor::new(&directives);
1922
1923 let query = parse("SELECT interval('month')").unwrap();
1925 let result = executor.execute(&query).unwrap();
1926 assert_eq!(
1927 result.rows[0][0],
1928 Value::Interval(Interval::new(1, IntervalUnit::Month))
1929 );
1930
1931 let query = parse("SELECT interval(3, 'day')").unwrap();
1933 let result = executor.execute(&query).unwrap();
1934 assert_eq!(
1935 result.rows[0][0],
1936 Value::Interval(Interval::new(3, IntervalUnit::Day))
1937 );
1938
1939 let query = parse("SELECT interval(-2, 'week')").unwrap();
1941 let result = executor.execute(&query).unwrap();
1942 assert_eq!(
1943 result.rows[0][0],
1944 Value::Interval(Interval::new(-2, IntervalUnit::Week))
1945 );
1946 }
1947
1948 #[test]
1949 fn test_date_add_with_interval() {
1950 let directives = sample_directives();
1951 let mut executor = Executor::new(&directives);
1952
1953 let query = parse("SELECT date_add(date(2024, 1, 15), interval(1, 'month'))").unwrap();
1955 let result = executor.execute(&query).unwrap();
1956 assert_eq!(
1957 result.rows[0][0],
1958 Value::Date(NaiveDate::from_ymd_opt(2024, 2, 15).unwrap())
1959 );
1960
1961 let query = parse("SELECT date(2024, 1, 15) + interval(1, 'year')").unwrap();
1963 let result = executor.execute(&query).unwrap();
1964 assert_eq!(
1965 result.rows[0][0],
1966 Value::Date(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap())
1967 );
1968
1969 let query = parse("SELECT date(2024, 3, 15) - interval(2, 'month')").unwrap();
1971 let result = executor.execute(&query).unwrap();
1972 assert_eq!(
1973 result.rows[0][0],
1974 Value::Date(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
1975 );
1976 }
1977}