The refusal property gained the converse law (an accepted command journals something; only batch runs may find nothing to do) and checks the store's class and payroll listings. It found a schedule registered with an out-of-range total whose first month was refused after its entry was begun: the live ledger kept a draft the journal never had. - Core: compound commands (post_schedule_month, reverse_entry, close_period_to, create_invoice, create_bill, receive_payment, pay_bill, run_payroll, remit_payroll_taxes) run inside Ledger::atomically, so a refusal partway leaves the ledger exactly as it was. - Core: a schedule total no line could carry, or finer than a cent, and an asset cost beyond the line range, are refused at registration. Replay keeps older schedules (replay_register_schedule). - Store: add_line, add_line_fx, mark_cleared, set_fx_rate, post_schedule_month and close_period_to changed the live ledger before journaling; they now run on a copy, journal what it produced, then swap it in, so a failed write leaves the live books as journaled. New spec-blind suite tests/spec_store.rs (57 tests written from the store's specs alone). Its two findings, adjudicated: - A file filed twice under different names listed the first name and type for both (the projection kept them per blob). Schema v8 gives each attachment its own; older databases backfill and heal by verify. - Withdrawn attachments stay in the store's listing, marked (the MCP listing hides them): the spec and doc said "omit" and are corrected. Also removes journal::truncate_to_last_good_seq: unused, and it dropped everything after the first bad complete line, which the books now treat as corruption for a person, never repaired automatically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
212 lines
6.4 KiB
Rust
212 lines
6.4 KiB
Rust
//! SQLite schema and migrations.
|
|
//!
|
|
//! The projection schema. Amounts are TEXT (decimal strings) — never REAL.
|
|
//! Migrations are numbered SQL scripts applied in order; `SCHEMA_VERSION`
|
|
//! is the current version.
|
|
|
|
/// Current schema version.
|
|
pub const SCHEMA_VERSION: u32 = 8;
|
|
|
|
/// Migrations, in order. `MIGRATIONS[i]` migrates from version `i` to `i+1`.
|
|
pub const MIGRATIONS: &[&str] = &[
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS meta(
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS accounts(
|
|
id INTEGER PRIMARY KEY,
|
|
code TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
parent INTEGER NULL,
|
|
placeholder INTEGER NOT NULL,
|
|
currency TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS entries(
|
|
id INTEGER PRIMARY KEY,
|
|
date TEXT NOT NULL,
|
|
description TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
reversal_of INTEGER NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS lines(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
entry_id INTEGER NOT NULL,
|
|
account_id INTEGER NOT NULL,
|
|
side TEXT NOT NULL,
|
|
amount TEXT NOT NULL,
|
|
currency TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS blobs(
|
|
sha256 TEXT PRIMARY KEY,
|
|
size_bytes INTEGER NOT NULL,
|
|
mime_type TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS attachments(
|
|
id INTEGER PRIMARY KEY,
|
|
blob_sha TEXT NOT NULL,
|
|
entry_id INTEGER NULL,
|
|
category TEXT NOT NULL,
|
|
description TEXT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS events(
|
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_lines_entry ON lines(entry_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_date ON entries(date);
|
|
CREATE INDEX IF NOT EXISTS idx_attachments_entry ON attachments(entry_id);
|
|
"#,
|
|
r#"
|
|
-- v2: fixed-asset register and depreciation runs.
|
|
CREATE TABLE IF NOT EXISTS assets(
|
|
id INTEGER PRIMARY KEY,
|
|
description TEXT NOT NULL,
|
|
purchase_date TEXT NOT NULL,
|
|
cost_amount TEXT NOT NULL,
|
|
cost_currency TEXT NOT NULL,
|
|
salvage_amount TEXT NOT NULL,
|
|
salvage_currency TEXT NOT NULL,
|
|
useful_life_months INTEGER NOT NULL,
|
|
method TEXT NOT NULL,
|
|
cost_account INTEGER NOT NULL,
|
|
accum_depr_account INTEGER NOT NULL,
|
|
expense_account INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
disposed_date TEXT NULL,
|
|
proceeds_amount TEXT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS asset_depreciation(
|
|
asset_id INTEGER NOT NULL,
|
|
period TEXT NOT NULL,
|
|
entry_id INTEGER NOT NULL,
|
|
amount TEXT NOT NULL,
|
|
PRIMARY KEY (asset_id, period)
|
|
);
|
|
"#,
|
|
r#"
|
|
-- v3: accounting periods, A/R + A/P, cleared-line flags, FX rates, and
|
|
-- foreign-currency columns on lines. Amounts are TEXT, never REAL.
|
|
CREATE TABLE IF NOT EXISTS periods(
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
start TEXT NOT NULL,
|
|
end TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
closing_entry INTEGER NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS customers(
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS vendors(
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS invoices(
|
|
id INTEGER PRIMARY KEY,
|
|
customer_id INTEGER NOT NULL,
|
|
date TEXT NOT NULL,
|
|
due_date TEXT NOT NULL,
|
|
lines_json TEXT NOT NULL,
|
|
amount_paid TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
entry_id INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS bills(
|
|
id INTEGER PRIMARY KEY,
|
|
vendor_id INTEGER NOT NULL,
|
|
date TEXT NOT NULL,
|
|
due_date TEXT NOT NULL,
|
|
lines_json TEXT NOT NULL,
|
|
amount_paid TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
entry_id INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS cleared_lines(
|
|
entry_id INTEGER NOT NULL,
|
|
line_index INTEGER NOT NULL,
|
|
PRIMARY KEY (entry_id, line_index)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS fx_rates(
|
|
currency TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
rate TEXT NOT NULL,
|
|
PRIMARY KEY (currency, date)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_invoices_customer ON invoices(customer_id);
|
|
CREATE INDEX IF NOT EXISTS idx_bills_vendor ON bills(vendor_id);
|
|
ALTER TABLE lines ADD COLUMN foreign_currency TEXT;
|
|
ALTER TABLE lines ADD COLUMN foreign_amount TEXT;
|
|
"#,
|
|
r#"
|
|
-- v4: classes, payroll runs, and class_id on lines. Amounts are TEXT,
|
|
-- never REAL.
|
|
CREATE TABLE IF NOT EXISTS classes(
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS payroll_runs(
|
|
id INTEGER PRIMARY KEY,
|
|
date TEXT NOT NULL,
|
|
entry_id INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS payroll_lines(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
run_id INTEGER NOT NULL,
|
|
employee_name TEXT NOT NULL,
|
|
gross_pay TEXT NOT NULL,
|
|
federal_withholding TEXT NOT NULL,
|
|
state_withholding TEXT NOT NULL,
|
|
ss_employee TEXT NOT NULL,
|
|
medicare_employee TEXT NOT NULL,
|
|
ss_employer TEXT NOT NULL,
|
|
medicare_employer TEXT NOT NULL,
|
|
other_deductions TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_payroll_lines_run ON payroll_lines(run_id);
|
|
ALTER TABLE lines ADD COLUMN class_id INTEGER;
|
|
"#,
|
|
r#"
|
|
-- v5: withdrawn attachments. Withdrawal retires a receipt or document from
|
|
-- listings and linking; the bytes stay in the blob store and the journal
|
|
-- keeps the filing and the withdrawal.
|
|
ALTER TABLE attachments ADD COLUMN withdrawn_at TEXT;
|
|
ALTER TABLE attachments ADD COLUMN withdrawn_reason TEXT;
|
|
"#,
|
|
r#"
|
|
-- v6: tax categories on accounts (where each account lands on the return,
|
|
-- and how much of it is deductible). Percent is TEXT, never REAL.
|
|
ALTER TABLE accounts ADD COLUMN tax_category TEXT;
|
|
ALTER TABLE accounts ADD COLUMN deductible_percent TEXT;
|
|
"#,
|
|
r#"
|
|
-- v7: 1099 tracking: which return each vendor gets, whether its W-9 is on
|
|
-- file (never the TIN itself), and the vendor each entry paid.
|
|
ALTER TABLE vendors ADD COLUMN form_1099 TEXT;
|
|
ALTER TABLE vendors ADD COLUMN tin_on_file INTEGER NOT NULL DEFAULT 0;
|
|
CREATE TABLE IF NOT EXISTS entry_vendors(
|
|
entry_id INTEGER PRIMARY KEY,
|
|
vendor_id INTEGER NOT NULL
|
|
);
|
|
"#,
|
|
r#"
|
|
-- v8: each filing keeps its own file name and type. Before, they came from
|
|
-- the blob, so filing the same bytes again under another name listed the
|
|
-- first name (found by the spec-blind store suite, 2026-10-13). Existing
|
|
-- rows take the blob's; a database whose filings differed then fails
|
|
-- verify's table check and is rebuilt from the journal.
|
|
ALTER TABLE attachments ADD COLUMN file_name TEXT;
|
|
ALTER TABLE attachments ADD COLUMN mime_type TEXT;
|
|
UPDATE attachments SET
|
|
file_name = (SELECT original_name FROM blobs WHERE sha256 = attachments.blob_sha),
|
|
mime_type = (SELECT mime_type FROM blobs WHERE sha256 = attachments.blob_sha);
|
|
"#,
|
|
];
|