okane/lib/database/sqlite.dart

670 lines
19 KiB
Dart

import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:okane/ui/utils.dart';
import 'package:path_provider/path_provider.dart';
part 'sqlite.g.dart';
class Accounts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
enum BeneficiaryType { account, other }
class Beneficiaries extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().unique()();
TextColumn get type => textEnum<BeneficiaryType>()();
IntColumn get accountId => integer().nullable().references(Accounts, #id)();
TextColumn get imagePath => text().nullable()();
}
class ExpenseCategories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
enum BudgetPeriod { month }
class BudgetItems extends Table {
IntColumn get id => integer().autoIncrement()();
RealColumn get amount => real()();
IntColumn get expenseCategoryId =>
integer().references(ExpenseCategories, #id)();
IntColumn get budgetId => integer().references(Budgets, #id)();
}
class BudgetItemDto {
final BudgetItem item;
final ExpenseCategory expenseCategory;
BudgetItemDto({required this.item, required this.expenseCategory});
}
class Budgets extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get period => textEnum<BudgetPeriod>()();
TextColumn get name => text()();
RealColumn get income => real()();
BoolColumn get includeOtherSpendings => boolean()();
IntColumn get accountId => integer().references(Accounts, #id)();
}
class BudgetsDto {
final Budget budget;
BudgetsDto({required this.budget});
}
class Loans extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
}
class LoanChanges extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get loanId => integer().references(Loans, #id)();
RealColumn get amount => real()();
DateTimeColumn get date => dateTime()();
}
class LoanDto {
final Loan loan;
final Beneficiary beneficiary;
final List<LoanChange> changes;
LoanDto({
required this.loan,
required this.beneficiary,
required this.changes,
});
}
class RecurringTransactions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get days => integer()();
DateTimeColumn get lastExecution => dateTime().nullable()();
IntColumn get templateId => integer().references(TransactionTemplates, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
}
typedef RecurringTransactionDto =
({
RecurringTransaction recurring,
Beneficiary beneficiary,
TransactionTemplate template,
});
class TransactionTemplates extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
RealColumn get amount => real()();
BoolColumn get recurring => boolean()();
IntColumn get expenseCategoryId =>
integer().nullable().references(ExpenseCategories, #id)();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
}
typedef TransactionTemplateDto =
({
TransactionTemplate template,
Beneficiary beneficiary,
ExpenseCategory? expenseCategory,
});
class Transactions extends Table {
IntColumn get id => integer().autoIncrement()();
RealColumn get amount => real()();
// TODO: tags
DateTimeColumn get date => dateTime()();
IntColumn get expenseCategoryId =>
integer().nullable().references(ExpenseCategories, #id)();
IntColumn get accountId => integer().references(Accounts, #id)();
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
}
class TransactionDto {
final Transaction transaction;
final Beneficiary beneficiary;
final ExpenseCategory? expenseCategory;
TransactionDto({
required this.transaction,
required this.beneficiary,
required this.expenseCategory,
});
}
@DriftDatabase(
tables: [
Accounts,
Beneficiaries,
Budgets,
BudgetItems,
ExpenseCategories,
Loans,
LoanChanges,
RecurringTransactions,
TransactionTemplates,
Transactions,
],
daos: [
AccountsDao,
BeneficiariesDao,
BudgetsDao,
ExpenseCategoriesDao,
LoansDao,
RecurringTransactionsDao,
TransactionTemplatesDao,
TransactionsDao,
],
)
class OkaneDatabase extends _$OkaneDatabase {
OkaneDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: "okane",
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
}
@DriftAccessor(tables: [Accounts])
class AccountsDao extends DatabaseAccessor<OkaneDatabase>
with _$AccountsDaoMixin {
AccountsDao(OkaneDatabase db) : super(db);
Stream<List<Account>> accountsStream() {
return select(accounts).watch();
}
Future<List<Account>> getAccounts() {
return select(accounts).get();
}
Future<int> upsertAccount(AccountsCompanion account) {
return into(accounts).insertOnConflictUpdate(account);
}
}
enum TransactionQueryDateOption { thisMonth }
@DriftAccessor(tables: [Transactions, Beneficiaries, ExpenseCategories])
class TransactionsDao extends DatabaseAccessor<OkaneDatabase>
with _$TransactionsDaoMixin {
TransactionsDao(OkaneDatabase db) : super(db);
JoinedSelectStatement _transactionQuery(Account account) {
return (select(transactions)
..where((t) => t.accountId.equals(account.id))).join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
]);
}
TransactionDto _mapToDto(TypedResult row) {
return TransactionDto(
transaction: row.readTable(transactions),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTableOrNull(expenseCategories),
);
}
Stream<List<TransactionDto>> transactionsStream(Account account) {
return _transactionQuery(account).watch().map((rows) {
return rows.map(_mapToDto).toList();
});
}
Future<List<TransactionDto>> getTransactions(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return _transactionQuery(
account,
).get().then((rows) => rows.map(_mapToDto).toList());
}
Future<List<TransactionDto>> getLastTransactions(
Account account,
DateTime today,
int days,
) async {
return (select(transactions)..where(
(t) =>
t.accountId.equals(account.id) &
t.date.isBiggerThanValue(
toMidnight(today.subtract(Duration(days: days))),
),
))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
])
.get()
.then((rows) => rows.map(_mapToDto).toList());
}
Future<double> getTotalBalance(Iterable<int> accountIds) async {
final sum = transactions.amount.sum();
final query =
selectOnly(transactions)
..where(transactions.accountId.isIn(accountIds))
..addColumns([sum]);
return query
.map((row) => row.read(sum))
.getSingleOrNull()
.then((v) => v ?? 0);
}
Future<List<TransactionDto>> getTransactionsInTimeframe(
Account account,
DateTime today,
TransactionQueryDateOption option,
) {
final lower = switch (option) {
TransactionQueryDateOption.thisMonth => DateTime(
today.year,
today.month,
0,
),
};
final upper = switch (option) {
TransactionQueryDateOption.thisMonth => monthEnding(today),
};
return (select(transactions)..where(
(t) =>
t.accountId.equals(account.id) &
t.date.isBetweenValues(lower, upper),
))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactions.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
),
])
.get()
.then((rows) => rows.map(_mapToDto).toList());
}
Future<Transaction> upsertTransaction(TransactionsCompanion t) {
return into(
transactions,
).insertReturning(t, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Beneficiaries])
class BeneficiariesDao extends DatabaseAccessor<OkaneDatabase>
with _$BeneficiariesDaoMixin {
BeneficiariesDao(OkaneDatabase db) : super(db);
Stream<List<Beneficiary>> beneficiariesStream() {
return select(beneficiaries).watch();
}
Future<List<Beneficiary>> getBeneficiaries() {
return select(beneficiaries).get();
}
Future<Beneficiary> upsertBeneficiary(BeneficiariesCompanion beneficiary) {
return into(
beneficiaries,
).insertReturning(beneficiary, mode: InsertMode.insertOrReplace);
}
Future<Beneficiary> getAccountBeneficiary(Account account) {
return (select(beneficiaries)
..where((b) => b.accountId.equals(account.id))).getSingle();
}
Stream<Beneficiary> watchBeneficiary(int id) {
return (select(beneficiaries)..where((b) => b.id.equals(id))).watchSingle();
}
}
@DriftAccessor(tables: [ExpenseCategories])
class ExpenseCategoriesDao extends DatabaseAccessor<OkaneDatabase>
with _$ExpenseCategoriesDaoMixin {
ExpenseCategoriesDao(OkaneDatabase db) : super(db);
Stream<List<ExpenseCategory>> expenseCategoriesStream(Account account) {
return select(expenseCategories).watch();
}
Future<List<ExpenseCategory>> getExpenseCategories(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return select(expenseCategories).get();
}
Future<ExpenseCategory> upsertCategory(ExpenseCategoriesCompanion category) {
return into(
expenseCategories,
).insertReturning(category, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Budgets, BudgetItems])
class BudgetsDao extends DatabaseAccessor<OkaneDatabase>
with _$BudgetsDaoMixin {
BudgetsDao(OkaneDatabase db) : super(db);
Stream<List<BudgetsDto>> budgetsStream(Account account) {
return (select(budgets)
..where((b) => b.accountId.equals(account.id))).watch().map((rows) {
return rows.map((row) {
return BudgetsDto(budget: row);
}).toList();
});
}
Future<List<BudgetsDto>> getBudgets(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return (select(budgets)
..where((b) => b.accountId.equals(account.id))).get().then((rows) {
return rows.map((row) {
return BudgetsDto(budget: row);
}).toList();
});
}
Stream<List<BudgetItemDto>> watchBudgetItems(Budget budget) {
return (select(budgetItems)..where((b) => b.budgetId.equals(budget.id)))
.join([
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(budgetItems.expenseCategoryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return BudgetItemDto(
expenseCategory: row.readTable(expenseCategories),
item: row.readTable(budgetItems),
);
}).toList();
});
}
Future<Budget> upsertBudget(BudgetsCompanion budget) {
return into(
budgets,
).insertReturning(budget, mode: InsertMode.insertOrReplace);
}
Future<BudgetItem> upsertBudgetItem(BudgetItemsCompanion item) {
return into(
budgetItems,
).insertReturning(item, mode: InsertMode.insertOrReplace);
}
}
@DriftAccessor(tables: [Loans, LoanChanges, Beneficiaries])
class LoansDao extends DatabaseAccessor<OkaneDatabase> with _$LoansDaoMixin {
LoansDao(OkaneDatabase db) : super(db);
Stream<List<LoanDto>> loansStream(Account account) {
return select(loans)
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(loans.beneficiaryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return (
loan: row.readTable(loans),
beneficiary: row.readTable(beneficiaries),
changes: List.empty(),
)
as LoanDto;
}).toList();
});
}
Future<List<LoanDto>> getLoans(Account? account) {
if (account == null) {
return Future.value(List.empty());
}
return select(loans)
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(loans.beneficiaryId),
),
])
.get()
.then((rows) {
return rows.map((row) {
return (
loan: row.readTable(loans),
beneficiary: row.readTable(beneficiaries),
changes: List.empty(),
)
as LoanDto;
}).toList();
});
}
Future<double> getTotalLoanSum() async {
final count = loanChanges.amount.sum();
final query = selectOnly(loanChanges)..addColumns([count]);
return query
.map((row) => row.read(count))
.getSingleOrNull()
.then((v) => v ?? 0);
}
Future<Loan> upsertLoan(LoansCompanion loan) {
return into(loans).insertReturning(loan, mode: InsertMode.insertOrReplace);
}
Future<LoanChange> upsertLoanChange(LoanChangesCompanion loanChange) {
return into(
loanChanges,
).insertReturning(loanChange, mode: InsertMode.insertOrReplace);
}
Future<void> deleteLoanChange(int id) {
return (delete(loanChanges)..where((c) => c.id.equals(id))).go();
}
}
@DriftAccessor(tables: [TransactionTemplates, ExpenseCategories, Beneficiaries])
class TransactionTemplatesDao extends DatabaseAccessor<OkaneDatabase>
with _$TransactionTemplatesDaoMixin {
TransactionTemplatesDao(OkaneDatabase db) : super(db);
Stream<List<TransactionTemplateDto>> transactionTemplatesStream(
Account account,
) {
return (select(transactionTemplates)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(
transactionTemplates.expenseCategoryId,
),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return (
template: row.readTable(transactionTemplates),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTable(expenseCategories),
);
}).toList();
});
}
Future<List<TransactionTemplateDto>> getTransactionTemplates(
Account? account,
) {
if (account == null) {
return Future.value(List.empty());
}
return (select(transactionTemplates)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
leftOuterJoin(
expenseCategories,
expenseCategories.id.equalsExp(
transactionTemplates.expenseCategoryId,
),
),
])
.get()
.then((rows) {
return rows.map((row) {
return (
template: row.readTable(transactionTemplates),
beneficiary: row.readTable(beneficiaries),
expenseCategory: row.readTable(expenseCategories),
);
}).toList();
});
}
Future<TransactionTemplate> upsertTemplate(
TransactionTemplatesCompanion template,
) {
return into(
transactionTemplates,
).insertReturning(template, mode: InsertMode.insertOrReplace);
}
Future<void> deleteTemplate(TransactionTemplate template) {
return (delete(transactionTemplates)
..where((t) => t.id.equals(template.id))).go();
}
}
@DriftAccessor(tables: [TransactionTemplates, RecurringTransactions])
class RecurringTransactionsDao extends DatabaseAccessor<OkaneDatabase>
with _$RecurringTransactionsDaoMixin {
RecurringTransactionsDao(OkaneDatabase db) : super(db);
Stream<List<RecurringTransactionDto>> recurringTransactionsStream(
Account account,
) {
return (select(recurringTransactions)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
transactionTemplates,
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
),
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
])
.watch()
.map((rows) {
return rows.map((row) {
return (
recurring: row.readTable(recurringTransactions),
beneficiary: row.readTable(beneficiaries),
template: row.readTable(transactionTemplates),
);
}).toList();
});
}
Future<List<RecurringTransactionDto>> getRecurringTransactions(
Account? account,
) {
if (account == null) {
return Future.value(List.empty());
}
return (select(recurringTransactions)
..where((b) => b.accountId.equals(account.id)))
.join([
leftOuterJoin(
transactionTemplates,
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
),
leftOuterJoin(
beneficiaries,
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
),
])
.get()
.then((rows) {
return rows.map((row) {
return (
recurring: row.readTable(recurringTransactions),
beneficiary: row.readTable(beneficiaries),
template: row.readTable(transactionTemplates),
);
}).toList();
});
}
Future<int> upsertRecurringTransaction(RecurringTransactionsCompanion r) {
return into(recurringTransactions).insertOnConflictUpdate(r);
}
Future<void> deleteTemplate(RecurringTransactionDto dto) async {
await db.transactionTemplatesDao.deleteTemplate(dto.template);
await (delete(recurringTransactions)
..where((t) => t.id.equals(dto.recurring.id))).go();
}
}