Migrate transactions to SQLite (self-contained, on the volume)

- txns_db.py: SQLite store with the same list-of-dicts shape, ACID writes
  (no corruption / no lost-update race), indexes, and a fast unreviewed COUNT.
- app.py: get_transactions/save_transactions delegate to txns_db; one-time
  idempotent migration from transactions.json on startup (JSON kept as backup);
  nav badge uses count_unreviewed() instead of parsing the whole store (#33).
- Verified against the live 265-row dataset: counts, fields, splits, mutation.
- Other small JSON files intentionally stay JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:43:03 -05:00
parent 74d5e196c7
commit f3485fdc17
2 changed files with 171 additions and 5 deletions

17
app.py
View File

@@ -7,6 +7,7 @@ from datetime import datetime, date
import calendar as cal_module
from storage import load_json, save_json
from settings import load_settings, save_settings, SECRET_KEYS
import txns_db
from categories import CATEGORIES, CATEGORY_COLORS
from bills import load_bills, save_bills, bills_for_month, calendar_grid, get_week_num
from sinking import (load_funds, save_funds, FUND_COLORS, monthly_needed,
@@ -163,9 +164,8 @@ def compute_tax_estimate(gross_income, rates, se_income=None):
@app.context_processor
def inject_unreviewed_count():
transactions = get_transactions()
count = sum(1 for t in transactions if not t.get("reviewed"))
return dict(unreviewed_count=count)
# Fast COUNT query instead of parsing the whole transaction store per request.
return dict(unreviewed_count=txns_db.count_unreviewed())
DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data"))
TRANSACTIONS_FILE = os.path.join(DATA_DIR, "transactions.json")
@@ -173,6 +173,13 @@ RULES_FILE = os.path.join(DATA_DIR, "rules.json")
BUDGET_FILE = os.path.join(DATA_DIR, "budget.json")
NET_WORTH_FILE = os.path.join(DATA_DIR, "net_worth_accounts.json")
# Transactions live in SQLite (self-contained, on the volume). On first run this
# imports any existing transactions.json; it's a no-op once the DB has rows.
txns_db.init_db()
_migrated = txns_db.migrate_from_json(TRANSACTIONS_FILE)
if _migrated:
print(f"[startup] migrated {_migrated} transactions from JSON -> SQLite")
def load_nw_accounts():
return load_json(NET_WORTH_FILE, [])
@@ -183,11 +190,11 @@ def save_nw_accounts(accounts):
def get_transactions():
return load_json(TRANSACTIONS_FILE, [])
return txns_db.get_transactions()
def save_transactions(transactions):
save_json(TRANSACTIONS_FILE, transactions)
txns_db.save_transactions(transactions)
def get_rules():