Basic implementation of DBBalances
This commit is contained in:
parent
0eb583d028
commit
ec470f8ced
1654
Cargo.lock
generated
1654
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
85
src/db.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
10
src/main.rs
10
src/main.rs
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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(
|
||||||
|
@ -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(),
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user