MO-Fuel-Tax-Back/lib/services/db_schema.dart

135 lines
5.9 KiB
Dart

const createVehiclesTableSql = '''
CREATE TABLE vehicles (
id TEXT PRIMARY KEY,
vin TEXT NOT NULL,
nickname TEXT,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
dirty INTEGER NOT NULL DEFAULT 1
)
''';
const createFuelEntriesTableSql = '''
CREATE TABLE fuel_entries (
id TEXT PRIMARY KEY,
vehicle_id TEXT NOT NULL,
date INTEGER NOT NULL,
gallons REAL NOT NULL,
price_per_gallon REAL NOT NULL,
total_cost REAL NOT NULL,
receipt_image_path TEXT,
receipt_drive_file_id TEXT,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
dirty INTEGER NOT NULL DEFAULT 1
)
''';
const createFuelEntriesIndexSql =
'CREATE INDEX idx_fuel_entries_vehicle_id ON fuel_entries(vehicle_id)';
/// A single-row table (`id` is always `1`) holding the furthest-known
/// "ads disabled until" date from the one-time "remove ads for a year"
/// purchase — see `PurchaseService`/`AppState.adsCurrentlyDisabled`.
///
/// This exists as its own synced table (rather than, say, a
/// SharedPreferences value) specifically so it survives an app
/// reinstall: the purchase itself is a *consumable* Play Store product
/// (needed so it can be bought again once the year lapses), and Play
/// Store has no memory of a consumed purchase to "restore" — this table,
/// merged through the same cloud sync as vehicles/fuel_entries, is what
/// lets a user with cloud sync configured get their remaining ad-free
/// time back after reinstalling, instead of losing it.
const createAdFreeEntitlementTableSql = '''
CREATE TABLE ad_free_entitlement (
id INTEGER PRIMARY KEY CHECK (id = 1),
ad_free_until INTEGER,
updated_at INTEGER NOT NULL
)
''';
/// A single-row table (`id` is always `1`) holding the earliest-known
/// moment this app was ever used — see `AppState.firstUsedAt` and
/// `DatabaseService.getOrCreateFirstUsedAt`. Synced like
/// `ad_free_entitlement`, for the same reason: without it, reinstalling
/// would reset the new-user ad grace period every time.
const createAppUsageTableSql = '''
CREATE TABLE app_usage (
id INTEGER PRIMARY KEY CHECK (id = 1),
first_used_at INTEGER NOT NULL
)
''';
/// Unlike every other merge statement here, this keeps whichever row has
/// the *smaller* `first_used_at` — merging in a device that's had the app
/// longer can only push "first use" earlier, never later.
const mergeAppUsageSql = '''
INSERT OR REPLACE INTO main.app_usage (id, first_used_at)
SELECT r.id, r.first_used_at
FROM remote_db.app_usage r
LEFT JOIN main.app_usage l ON l.id = r.id
WHERE l.id IS NULL OR r.first_used_at < l.first_used_at
''';
/// Selects fuel entries whose receipt photo still needs uploading to
/// Drive. Deliberately requires `receipt_drive_file_id IS NULL`, not just
/// "has a local path": once a row is uploaded, its local copy may still be
/// kept around (see the "keep photos on this phone" setting) — matching on
/// the local path alone would re-upload that same photo as a duplicate
/// Drive file on every subsequent sync.
const pendingReceiptUploadWhereClause = 'dirty = 1 AND receipt_image_path IS NOT NULL '
'AND receipt_drive_file_id IS NULL AND deleted_at IS NULL';
/// Merges attached `remote_db` rows into `main` (the live local database):
/// any remote row that's new to us, or newer than our copy, replaces ours.
/// Rows this doesn't touch — including our own not-yet-pushed edits — are
/// left alone, since the WHERE clause only matches rows remote should win;
/// no separate "keep local" statement is needed.
///
/// Merges on `id` (the hidden, immutable identifier), not `vin` — VIN is
/// user-editable, so it can't be relied on as a stable merge key. VIN
/// uniqueness among active vehicles is enforced at the app layer instead
/// (see `DatabaseService.vinExists`), not by a database constraint here,
/// since two devices could in principle each independently add a vehicle
/// with the same VIN while offline; that's an accepted rare-conflict edge
/// case (see the "field-level conflicts aren't merged" caveat in the
/// README) rather than something this merge statement tries to resolve.
const mergeVehiclesSql = '''
INSERT OR REPLACE INTO main.vehicles
(id, vin, nickname, updated_at, deleted_at, dirty)
SELECT r.id, r.vin, r.nickname, r.updated_at, r.deleted_at, 0
FROM remote_db.vehicles r
LEFT JOIN main.vehicles l ON l.id = r.id
WHERE l.id IS NULL OR r.updated_at > l.updated_at
''';
/// Same rule as [mergeVehiclesSql]. `receipt_image_path` is always forced
/// to NULL on the merged-in copy — it's a local filesystem path, meaningless
/// (and never valid) on another device, and rows are only ever pushed to
/// Drive once their pending receipt has already been uploaded there, so a
/// remote row should never legitimately have one anyway.
const mergeFuelEntriesSql = '''
INSERT OR REPLACE INTO main.fuel_entries
(id, vehicle_id, date, gallons, price_per_gallon, total_cost,
receipt_image_path, receipt_drive_file_id, updated_at, deleted_at, dirty)
SELECT r.id, r.vehicle_id, r.date, r.gallons, r.price_per_gallon, r.total_cost,
NULL, r.receipt_drive_file_id, r.updated_at, r.deleted_at, 0
FROM remote_db.fuel_entries r
LEFT JOIN main.fuel_entries l ON l.id = r.id
WHERE l.id IS NULL OR r.updated_at > l.updated_at
''';
/// Same "newest `updated_at` wins" rule as [mergeVehiclesSql], applied to
/// the single `id = 1` entitlement row instead of many rows — whichever
/// device most recently purchased (or already had) a further-out
/// `ad_free_until` date wins, so this can never make an entitlement
/// *shorter* by merging in a stale copy from a device that hasn't synced
/// in a while.
const mergeAdFreeEntitlementSql = '''
INSERT OR REPLACE INTO main.ad_free_entitlement
(id, ad_free_until, updated_at)
SELECT r.id, r.ad_free_until, r.updated_at
FROM remote_db.ad_free_entitlement r
LEFT JOIN main.ad_free_entitlement l ON l.id = r.id
WHERE l.id IS NULL OR r.updated_at > l.updated_at
''';