Basic implementation of DBBalances

This commit is contained in:
RunasSudo 2025-05-22 00:25:51 +10:00
parent 0eb583d028
commit ec470f8ced
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 1776 additions and 15 deletions

1654
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,4 +9,7 @@ downcast-rs = "2.0.1"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
dyn-eq = "0.1.3" dyn-eq = "0.1.3"
dyn-hash = "0.2.2" dyn-hash = "0.2.2"
futures = "0.3.31"
indexmap = "2.9.0" indexmap = "2.9.0"
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] }
tokio = { version = "1.45.0", features = ["full"] }

85
src/db.rs Normal file
View File

@ -0,0 +1,85 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use std::ops::DerefMut;
use std::{cell::RefCell, future::Future};
use chrono::NaiveDate;
use sqlx::{Connection, Row, SqliteConnection};
use tokio::runtime::Runtime;
use crate::{util::format_date, QuantityInt};
pub struct DbConnection {
sqlx_connection: RefCell<SqliteConnection>,
}
fn run_blocking<F: Future>(future: F) -> F::Output {
let rt = Runtime::new().unwrap();
rt.block_on(future)
}
impl DbConnection {
/// Connect to the given Sqlite database
pub fn connect(url: &str) -> Self {
Self {
sqlx_connection: RefCell::new(run_blocking(Self::connect_async(url))),
}
}
async fn connect_async(url: &str) -> SqliteConnection {
SqliteConnection::connect(url).await.expect("SQL error")
}
/// Get account balances from the database
pub fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> {
run_blocking(self.get_balances_async(date))
}
async fn get_balances_async(&self, date: NaiveDate) -> HashMap<String, QuantityInt> {
let mut connection = self.sqlx_connection.borrow_mut();
let rows = sqlx::query(
"-- Get last transaction for each account
WITH max_dt_by_account AS (
SELECT account, max(dt) AS max_dt
FROM joined_transactions
WHERE DATE(dt) <= DATE($1)
GROUP BY account
),
max_tid_by_account AS (
SELECT max_dt_by_account.account, max(transaction_id) AS max_tid
FROM max_dt_by_account
JOIN joined_transactions ON max_dt_by_account.account = joined_transactions.account AND max_dt_by_account.max_dt = joined_transactions.dt
GROUP BY max_dt_by_account.account
)
-- Get running balance at last transaction for each account
SELECT max_tid_by_account.account, running_balance AS quantity
FROM max_tid_by_account
JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account"
).bind(format_date(date)).fetch_all(connection.deref_mut()).await.unwrap();
let mut balances = HashMap::new();
for row in rows {
balances.insert(row.get("account"), row.get("quantity"));
}
balances
}
}

View File

@ -1,5 +1,6 @@
pub mod db;
pub mod reporting; pub mod reporting;
pub mod transaction; pub mod transaction;
pub mod util; pub mod util;
pub type QuantityInt = u64; pub type QuantityInt = i64;

View File

@ -17,6 +17,7 @@
*/ */
use chrono::NaiveDate; use chrono::NaiveDate;
use libdrcr::db::DbConnection;
use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::generate_report; use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::{ use libdrcr::reporting::steps::{
@ -29,7 +30,13 @@ use libdrcr::reporting::types::{
}; };
fn main() { fn main() {
let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); // Connect to database
let db_connection = DbConnection::connect("sqlite:drcr_testing.db");
// Initialise ReportingContext
let mut context =
ReportingContext::new(db_connection, NaiveDate::from_ymd_opt(2025, 6, 30).unwrap());
register_lookup_fns(&mut context); register_lookup_fns(&mut context);
register_dynamic_builders(&mut context); register_dynamic_builders(&mut context);
@ -82,5 +89,6 @@ fn main() {
}) })
.unwrap(); .unwrap();
//println!("{}", products);
println!("{:?}", result); println!("{:?}", result);
} }

View File

@ -548,7 +548,7 @@ impl ReportingStep for UpdateBalancesAt {
products.insert( products.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()), args: Box::new(self.args.clone()),
}, },
Box::new(balances), Box::new(balances),

View File

@ -510,15 +510,14 @@ impl ReportingStep for DBBalances {
fn execute( fn execute(
&self, &self,
_context: &ReportingContext, context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts, products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> { ) -> Result<(), ReportingExecutionError> {
eprintln!("Stub: DBBalances.execute"); // Get balances from DB
let balances = BalancesAt { let balances = BalancesAt {
balances: HashMap::new(), balances: context.db_connection.get_balances(self.args.date),
}; };
products.insert( products.insert(

View File

@ -26,6 +26,7 @@ use dyn_eq::DynEq;
use dyn_hash::DynHash; use dyn_hash::DynHash;
use indexmap::IndexMap; use indexmap::IndexMap;
use crate::db::DbConnection;
use crate::transaction::TransactionWithPostings; use crate::transaction::TransactionWithPostings;
use crate::QuantityInt; use crate::QuantityInt;
@ -37,7 +38,11 @@ use super::executor::ReportingExecutionError;
/// Records the context for a single reporting job /// Records the context for a single reporting job
pub struct ReportingContext { pub struct ReportingContext {
// Configuration
pub db_connection: DbConnection,
pub eofy_date: NaiveDate, pub eofy_date: NaiveDate,
// State
pub(crate) step_lookup_fn: HashMap< pub(crate) step_lookup_fn: HashMap<
(&'static str, &'static [ReportingProductKind]), (&'static str, &'static [ReportingProductKind]),
(ReportingStepTakesArgsFn, ReportingStepFromArgsFn), (ReportingStepTakesArgsFn, ReportingStepFromArgsFn),
@ -47,8 +52,9 @@ pub struct ReportingContext {
impl ReportingContext { impl ReportingContext {
/// Initialise a new [ReportingContext] /// Initialise a new [ReportingContext]
pub fn new(eofy_date: NaiveDate) -> Self { pub fn new(db_connection: DbConnection, eofy_date: NaiveDate) -> Self {
Self { Self {
db_connection: db_connection,
eofy_date: eofy_date, eofy_date: eofy_date,
step_lookup_fn: HashMap::new(), step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::new(), step_dynamic_builders: Vec::new(),

View File

@ -20,5 +20,10 @@ use chrono::{Datelike, NaiveDate};
/// Return the start date of the financial year, given the end date of the financial year /// Return the start date of the financial year, given the end date of the financial year
pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate {
return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap()
}
/// Format the [NaiveDate] as a string
pub fn format_date(date: NaiveDate) -> String {
date.format("%Y-%m-%d 00:00:00.000000").to_string()
} }