3.9 KiB
SQLite entity-relationship diagram
Local database: ApplicationDocumentsDirectory/FuelTaxTracker/fuel_tax_tracker.db
Schema version: 5 (defined in lib/services/database_service.dart / lib/services/db_schema.dart)
erDiagram
VEHICLES ||--o{ FUEL_ENTRIES : "owns (vehicle_id)"
VEHICLES {
TEXT id PK "UUID, hidden, immutable"
TEXT vin "required; unique among active rows, app-enforced"
TEXT nickname "nullable display label"
INTEGER updated_at "UTC millis; newest-wins merge key"
INTEGER deleted_at "nullable soft-delete tombstone"
INTEGER dirty "local-only; 1 = not yet pushed"
}
FUEL_ENTRIES {
TEXT id PK "UUID"
TEXT vehicle_id FK "references vehicles.id — no SQLite FK"
INTEGER date "purchase datetime, local millis"
REAL gallons
REAL price_per_gallon
REAL total_cost
TEXT receipt_image_path "nullable on-device path"
TEXT receipt_drive_file_id "nullable cloud file id"
INTEGER updated_at "UTC millis; newest-wins merge key"
INTEGER deleted_at "nullable soft-delete tombstone"
INTEGER dirty "local-only; 1 = not yet pushed"
}
AD_FREE_ENTITLEMENT {
INTEGER id PK "always 1 (CHECK id = 1)"
INTEGER ad_free_until "nullable UTC millis"
INTEGER updated_at "UTC millis; newest-wins merge key"
}
Relationships
| From | To | Cardinality | Enforced by |
|---|---|---|---|
vehicles |
fuel_entries |
1 : 0..n | Application (FuelEntry.vehicleId → Vehicle.id). There is no FOREIGN KEY clause. |
ad_free_entitlement |
— | singleton | PRIMARY KEY CHECK (id = 1) |
VIN is not the foreign key. Fuel entries reference the hidden vehicle id so the user can edit a VIN without breaking receipts or sync history.
Indexes and constraints
vehicles.id—PRIMARY KEYfuel_entries.id—PRIMARY KEYidx_fuel_entries_vehicle_idonfuel_entries(vehicle_id)ad_free_entitlement.id—PRIMARY KEY CHECK (id = 1)- VIN uniqueness among active (
deleted_at IS NULL) vehicles is enforced inDatabaseService.vinExists/AppState.addVehicle/AppState.updateVehicle, not by a unique index. Two offline devices can independently add the same VIN; that rare conflict is accepted rather than merged field-by-field.
Sync and lifecycle columns
Every synced table carries updated_at. Cloud merge is last-write-wins on that timestamp (INSERT OR REPLACE of a remote row that is new or newer). See mergeVehiclesSql, mergeFuelEntriesSql, and mergeAdFreeEntitlementSql in db_schema.dart.
| Column | Scope | Meaning |
|---|---|---|
updated_at |
all three tables | Newest value wins when merging a remote copy. |
deleted_at |
vehicles, fuel_entries |
Soft-delete tombstone. Rows are never hard-deleted, so a deletion can propagate to other devices instead of being resurrected by a stale remote copy. |
dirty |
vehicles, fuel_entries only |
Local bookkeeping: 1 = not yet pushed. Cleared to 0 on merge-in. Not mapped onto the Dart models. |
receipt_image_path |
fuel_entries |
Device-local filesystem path. Forced to NULL when merging a remote row — a path from another phone is never valid here. |
receipt_drive_file_id |
fuel_entries |
Opaque id of the uploaded receipt on the active cloud provider. Presence means “already uploaded”; a pending upload is path IS NOT NULL AND drive_file_id IS NULL. |
ad_free_entitlement has no dirty or deleted_at. It is a single row that exists so a consumable Play Store purchase (which cannot be restored after consume) can survive reinstall via the same cloud merge as vehicles and fuel entries.
What is not in SQLite
Preferences live in SharedPreferences, not this database: theme, onboarding/agreement flags, cloud provider + folder ids, keep-photos-locally, stale-lock timeout, estimated-refund toggle.