fix(database): make delete_by_datetime(None, None) a true full wipe (#1476)

delete_by_datetime() with no bounds was documented as "delete all
records", but db_delete_records() assumed that
_db_ensure_loaded(start=None, end=None) loads every record into memory
before building the delete list. That is wrong: _db_ensure_loaded()
implements a NONE -> INITIAL -> FULL phase machine, and with both bounds
None in the NONE phase it loads only an *initial window* and leaves the
phase at INITIAL. Records outside that window never enter
_db_sorted_timestamps, so they are never marked for deletion and survive
in the physical store. Symptom: after a "wipe", db_count_records() still
returned N records (observed: 4 leftovers from a previous dense fixture).
A second delete_by_datetime() call happened to escalate INITIAL -> FULL
and deleted the rest, which is why the bug looked intermittent and why
stacked test runs accumulated rows.

Fix: in db_delete_records(), when both bounds are None, call
self._db_load_full() directly; otherwise keep the existing
_db_ensure_loaded(start, end) path. The delete list is then complete and
the "wipe" fixture actually wipes.

delete_by_datetime() remains a soft delete (rows are only added to
_db_deleted_timestamps; the physical DELETE happens on the next flush),
unchanged by this commit.

Besides this an additional fix was added:

* fix(tests): assert UTC-instant ordering in test_timestamps_remain_sorted

The test asserted `dts == sorted(dts)` on seq.records[].date_time, which
are tz-aware DateTime values in Europe/Berlin. Compaction buckets on a
UTC grid but records are materialized in local time, so across a DST
fall-back the local wall-clock sequence is non-monotonic even though the
UTC instants are ordered (e.g. 00:45 UTC -> 02:45 CEST, 01:00 UTC ->
02:00 CET). sorted() compares wall-clock fields, so it flags the pair at
the seam (index 1248 on 2026-10-25 for Europe/Berlin) as out of order.
The assertion is unsatisfiable for any 15-min grid spanning a fall-back
boundary when records are labelled in local time.

The actual invariant is "ascending by UTC instant", which is what
_db_sorted_timestamps guarantees and what the adjacent alignment tests
already assert via int(rec.date_time.timestamp()). Change the assertion
to compare integer UTC epochs:

    epochs = [int(r.date_time.timestamp()) for r in seq.records if r.date_time]
    assert epochs == sorted(epochs)

Note: this test was seasonally flaky. It only fails while a DST
transition lies inside the [now - 4 weeks, now] window used by
dense_energy_seq, which is why it newly started failing without any code
change. Upstream should ideally emit compacted records in UTC (or order
by UTC instant) so that wall-clock ordering is not silently assumed.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-10-29 15:27:41 +01:00
committed by GitHub
parent c425fd58c4
commit 7554e2dc47
7 changed files with 62 additions and 23 deletions
+22 -8
View File
@@ -8,7 +8,7 @@
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "v0.3.0.dev2610261491542381"
"version": "v0.3.0.dev2610291327175670"
},
"paths": {
"/v1/measurement/battery-capacity/{battery_id}": {
@@ -1164,7 +1164,7 @@
"content": {
"application/json": {
"schema": {
"anyOf": [
"oneOf": [
{
"$ref": "#/components/schemas/PowerMeasurement"
},
@@ -1193,6 +1193,20 @@
"$ref": "#/components/schemas/DDBCActuatorStatus"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"PowerMeasurement": "#/components/schemas/PowerMeasurement",
"EnergyMeasurement": "#/components/schemas/EnergyMeasurement",
"PPBCPowerProfileStatus": "#/components/schemas/PPBCPowerProfileStatus",
"OMBCStatus": "#/components/schemas/OMBCStatus",
"FRBCActuatorStatus": "#/components/schemas/FRBCActuatorStatus",
"FRBCEnergyStatus": "#/components/schemas/FRBCEnergyStatus",
"FRBCStorageStatus": "#/components/schemas/FRBCStorageStatus",
"FRBCTimerStatus": "#/components/schemas/FRBCTimerStatus",
"DDBCActuatorStatus": "#/components/schemas/DDBCActuatorStatus"
}
},
"description": "Resource Status.",
"title": "Status"
}
@@ -10992,11 +11006,11 @@
}
],
"title": "Device Measurement Entity Ids",
"description": "Mapping of EOS measurement keys used by device (resource) simulations to Home Assistant entity IDs.",
"description": "Mapping of EOS device measurement keys to Home Assistant entity IDs. Registered keys such as 'battery1-soc-factor' are accepted; legacy underscore spellings are resolved when they uniquely match a registered key.",
"examples": [
{
"battery1_soc_factor": "sensor.battery1_soc_factor",
"ev11_soc_factor": "sensor.ev11_soc_factor"
"battery1-soc-factor": "sensor.battery1_soc_factor",
"ev11-soc-factor": "sensor.ev11_soc_factor"
}
]
},
@@ -11168,11 +11182,11 @@
}
],
"title": "Device Measurement Entity Ids",
"description": "Mapping of EOS measurement keys used by device (resource) simulations to Home Assistant entity IDs.",
"description": "Mapping of EOS device measurement keys to Home Assistant entity IDs. Registered keys such as 'battery1-soc-factor' are accepted; legacy underscore spellings are resolved when they uniquely match a registered key.",
"examples": [
{
"battery1_soc_factor": "sensor.battery1_soc_factor",
"ev11_soc_factor": "sensor.ev11_soc_factor"
"battery1-soc-factor": "sensor.battery1_soc_factor",
"ev11-soc-factor": "sensor.ev11_soc_factor"
}
]
},