fix(journey): a place kept across two days is two days of the journal

The sync engine keyed a skeleton by source_place_id alone, so a stop standing
on two days — the city you land in at dusk and walk through the next morning —
got one entry on the first of them and nothing on the rest.

Key on the day assignment instead. The assignment row rather than the day,
because moving a stop updates day_assignments.day_id in place: keyed this way
an entry follows the move instead of reading as one stop leaving the plan and
another arriving. Existing rows are backfilled to the place's earliest
assignment, and reconciliation adopts an unlinked row rather than annotating
it out from under its author.

Fixes #2329
This commit is contained in:
Konstantinos Thermos
2026-09-15 15:31:59 +03:00
parent 2efa843306
commit 4eaf90fd1b
6 changed files with 348 additions and 71 deletions
+44
View File
@@ -4900,6 +4900,50 @@ function runMigrations(db: Database.Database): void {
END
`);
},
/*
* A journal skeleton belongs to a day, not just to a place (#2329).
*
* The sync engine keyed a skeleton by `source_place_id` alone, so the same
* place standing on two days — the city you land in at dusk and walk through
* the next morning, the hotel you sleep in three nights — produced one entry
* on the first of them and nothing on the rest. There was nowhere to put the
* second evening's photographs but the first evening's entry.
*
* The assignment row, not the day, is the missing half of that key: moving a
* stop to another day updates `day_assignments.day_id` in place, so keying on
* the assignment lets an entry follow the move the way it always has, while
* still telling two days of the same place apart.
*
* Deliberately no REFERENCES clause. Unassigning a stop has to leave the id
* stale rather than NULL, because reconciliation reads a key that no longer
* matches as "this stop left the plan" — which is what happened — and a NULL
* would be indistinguishable from a row this migration could not resolve. The
* id is safe to dangle: `day_assignments.id` is AUTOINCREMENT, so it is never
* handed out twice.
*
* Existing rows are backfilled to the place's earliest assignment, which is
* the one the old engine would have picked, so reconciliation matches what is
* already there instead of writing a duplicate beside it.
*/
() => {
db.exec('ALTER TABLE journey_entries ADD COLUMN source_assignment_id INTEGER');
db.exec(`
UPDATE journey_entries
SET source_assignment_id = (
SELECT da.id
FROM day_assignments da
JOIN days d ON d.id = da.day_id
WHERE da.place_id = journey_entries.source_place_id
ORDER BY d.day_number ASC, d.date ASC, da.order_index ASC, da.id ASC
LIMIT 1
)
WHERE source_place_id IS NOT NULL
`);
db.exec(
'CREATE INDEX IF NOT EXISTS idx_journey_entries_source_assignment ON journey_entries(source_place_id, source_assignment_id)',
);
},
];
if (currentVersion < migrations.length) {