Remove the transfer flag
A transfer was a second verdict carried alongside the tag: a boolean set by transfer = true in a rule or by x in the TUI, kept in its own pair of rule_ and manual_ columns, whose one real effect was to hold the row out of the report. The rest of it was display -- a T column in the transaction list, in the rules screen and in money ls. Money moved between your own accounts is now tagged like anything else and counts like anything else. The leg leaving checking is an outflow and the leg arriving in savings is an inflow, so a report over the whole data root roughly nets out while one scoped to a single account or month does not. That is the price of one verdict per transaction instead of two. A rule now needs a tag, and one that set only transfer = true is refused by number on load. A leftover transfer key beside a tag is ignored, as unknown TOML keys always were, and an index built by an older binary drops both columns when it is opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
-50
@@ -1,6 +1,6 @@
|
||||
// Package store is the SQLite index over the statements. It is entirely
|
||||
// rebuildable: delete index.db and re-import to get it back, except for manual
|
||||
// tags and manual transfer overrides, which live only here.
|
||||
// rebuildable: delete index.db and re-import to get it back, except for the
|
||||
// tags set by hand, which live only here.
|
||||
package store
|
||||
|
||||
import (
|
||||
@@ -52,8 +52,6 @@ CREATE TABLE IF NOT EXISTS transactions (
|
||||
balance_minor INTEGER,
|
||||
rule_tag TEXT,
|
||||
manual_tag TEXT,
|
||||
rule_transfer INTEGER NOT NULL DEFAULT 0,
|
||||
manual_transfer INTEGER,
|
||||
UNIQUE(account_id, fingerprint)
|
||||
);
|
||||
|
||||
@@ -71,7 +69,7 @@ var migrations = []string{
|
||||
// dropped are columns an older build created that this one no longer reads.
|
||||
// Nothing indexes or constrains them, so they can simply go; leaving them
|
||||
// would keep a NOT NULL column alive that no INSERT here ever names.
|
||||
var dropped = []string{"counterparty"}
|
||||
var dropped = []string{"counterparty", "rule_transfer", "manual_transfer"}
|
||||
|
||||
// migrate brings an index created by an older build up to date, adding the
|
||||
// columns it lacks and removing the ones it should no longer have.
|
||||
@@ -237,11 +235,11 @@ func (d *DB) InsertTransaction(t model.Transaction) (bool, error) {
|
||||
res, err := d.sql.Exec(`
|
||||
INSERT INTO transactions
|
||||
(account_id, source_file_id, fingerprint, date, description, amount_minor,
|
||||
type, balance_minor, rule_tag, rule_transfer)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULLIF(?, ''), ?)
|
||||
type, balance_minor, rule_tag)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULLIF(?, ''))
|
||||
ON CONFLICT(account_id, fingerprint) DO NOTHING`,
|
||||
t.AccountID, t.SourceFileID, t.Fingerprint, t.Date, t.Description,
|
||||
t.AmountMinor, t.Type, balance, t.RuleTag, boolToInt(t.RuleTransfer))
|
||||
t.AmountMinor, t.Type, balance, t.RuleTag)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("insert transaction: %w", err)
|
||||
}
|
||||
@@ -267,8 +265,7 @@ func (d *DB) Transactions(f Filter) ([]model.Transaction, error) {
|
||||
SELECT t.id, t.account_id, a.slug, a.currency, a.minor_digits,
|
||||
t.fingerprint, t.date, t.description, t.amount_minor,
|
||||
COALESCE(s.path, ''), t.type, t.balance_minor,
|
||||
COALESCE(t.rule_tag, ''), COALESCE(t.manual_tag, ''),
|
||||
t.rule_transfer, t.manual_transfer
|
||||
COALESCE(t.rule_tag, ''), COALESCE(t.manual_tag, '')
|
||||
FROM transactions t
|
||||
JOIN accounts a ON a.id = t.account_id
|
||||
LEFT JOIN source_files s ON s.id = t.source_file_id
|
||||
@@ -299,26 +296,19 @@ func (d *DB) Transactions(f Filter) ([]model.Transaction, error) {
|
||||
var out []model.Transaction
|
||||
for rows.Next() {
|
||||
var (
|
||||
t model.Transaction
|
||||
ruleTransfer int
|
||||
manualTransfer sql.NullInt64
|
||||
balance sql.NullInt64
|
||||
t model.Transaction
|
||||
balance sql.NullInt64
|
||||
)
|
||||
if err := rows.Scan(&t.ID, &t.AccountID, &t.AccountSlug, &t.Currency, &t.MinorDigits,
|
||||
&t.Fingerprint, &t.Date, &t.Description, &t.AmountMinor, &t.SourcePath,
|
||||
&t.Type, &balance,
|
||||
&t.RuleTag, &t.ManualTag, &ruleTransfer, &manualTransfer); err != nil {
|
||||
&t.RuleTag, &t.ManualTag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.RuleTransfer = ruleTransfer != 0
|
||||
if balance.Valid {
|
||||
v := balance.Int64
|
||||
t.BalanceMinor = &v
|
||||
}
|
||||
if manualTransfer.Valid {
|
||||
v := manualTransfer.Int64 != 0
|
||||
t.ManualTransfer = &v
|
||||
}
|
||||
if needle != "" && !strings.Contains(model.NormalizeDescription(t.Description), needle) {
|
||||
continue
|
||||
}
|
||||
@@ -336,33 +326,21 @@ func (d *DB) SetManualTag(id int64, tag string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetManualTransfer sets the user's transfer override; nil restores the
|
||||
// rule-derived value.
|
||||
func (d *DB) SetManualTransfer(id int64, v *bool) error {
|
||||
var arg any
|
||||
if v != nil {
|
||||
arg = boolToInt(*v)
|
||||
}
|
||||
_, err := d.sql.Exec(`UPDATE transactions SET manual_transfer = ? WHERE id = ?`, arg, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ClearOverrides drops both manual overrides for a transaction.
|
||||
// ClearOverrides drops the manual tag for a transaction, so the rules decide
|
||||
// it again.
|
||||
func (d *DB) ClearOverrides(id int64) error {
|
||||
_, err := d.sql.Exec(
|
||||
`UPDATE transactions SET manual_tag = NULL, manual_transfer = NULL WHERE id = ?`, id)
|
||||
_, err := d.sql.Exec(`UPDATE transactions SET manual_tag = NULL WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// RuleAssignment is one row's recomputed rule verdict.
|
||||
type RuleAssignment struct {
|
||||
ID int64
|
||||
Tag string
|
||||
Transfer bool
|
||||
ID int64
|
||||
Tag string
|
||||
}
|
||||
|
||||
// ApplyRuleResults rewrites rule_tag and rule_transfer for every listed row in
|
||||
// a single transaction. Manual columns are never touched.
|
||||
// ApplyRuleResults rewrites rule_tag for every listed row in a single
|
||||
// transaction. The manual column is never touched.
|
||||
func (d *DB) ApplyRuleResults(rs []RuleAssignment) error {
|
||||
tx, err := d.sql.Begin()
|
||||
if err != nil {
|
||||
@@ -371,22 +349,21 @@ func (d *DB) ApplyRuleResults(rs []RuleAssignment) error {
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare(
|
||||
`UPDATE transactions SET rule_tag = NULLIF(?, ''), rule_transfer = ? WHERE id = ?`)
|
||||
`UPDATE transactions SET rule_tag = NULLIF(?, '') WHERE id = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, r := range rs {
|
||||
if _, err := stmt.Exec(r.Tag, boolToInt(r.Transfer), r.ID); err != nil {
|
||||
if _, err := stmt.Exec(r.Tag, r.ID); err != nil {
|
||||
return fmt.Errorf("apply rules to txn %d: %w", r.ID, err)
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Balance sums every transaction in an account, transfers included: a transfer
|
||||
// really does move the account's money.
|
||||
// Balance sums every transaction in an account.
|
||||
func (d *DB) Balance(accountID int64) (int64, error) {
|
||||
var v sql.NullInt64
|
||||
err := d.sql.QueryRow(
|
||||
@@ -424,10 +401,3 @@ func (d *DB) Tags() ([]string, error) {
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user