Skip to content

feat(storage): use embedded DuckDB for primary history - #1129

Merged
frahlg merged 20 commits into
masterfrom
feat/duckdb-history
Sep 8, 2026
Merged

feat(storage): use embedded DuckDB for primary history#1129
frahlg merged 20 commits into
masterfrom
feat/duckdb-history

Conversation

@frahlg

@frahlg frahlg commented Sep 8, 2026

Copy link
Copy Markdown
Member

Core now writes and reads time-series data through embedded DuckDB. SQLite keeps configuration, device identity, learned state and cache. This removes the FTWDB shadow client and the live SQLite/Parquet query split.

History snapshots, metric samples and energy accounting commit together. A bounded queue keeps history I/O outside the control tick; health distinguishes accepted, committed and rejected ticks. The serial writer retires an earlier retry receipt only after observing its successful commit, retaining the current receipt across an uncertain result. Repeated samples keep their first value, history corrections keep their last value, and invalid floating-point values fail at the storage boundary.

Startup imports SQLite from one read snapshot in bounded transactions, checks every table's count and value hash, then binds the completed DuckDB generation to configuration. Legacy sample Parquet files use staging that can spill to disk, an ordered window check for duplicate keys, and verified 2,048-row commits. Checkpoints run between files and after 131,072 imported rows within a file. Each new file opens a fresh native DuckDB instance on the same durable primary to release retained table buffers. The production constructor finishes this import before it exposes the Store or starts the writer; verified restarts keep one instance. A persistent source hash prevents a changed or missing file from completing an interrupted import; full backups reject pending imports. Full backups export a consistent DuckDB snapshot to portable SQLite and omit the imported sample files so older Core does not read duplicates. Schema 3 requires a full backup. Image-only downgrade and incomplete config-only restore are refused.

Core and backup builds use official DuckDB Go bindings with CGO on Linux amd64/arm64 and Windows amd64. The Linux images include the C++ runtime. Windows uses the pinned MinGW 14.2.0 UCRT toolchain used for the upstream DuckDB archive. Linked-library notices have been checked against the actual artifacts.

Validation completed locally:

  • make verify after rebasing on the planner fix and adding receipt retention; focused import tests after the staging/checkpoint changes.
  • Race tests for the writer, retry receipts, concurrent readers and shutdown.
  • Functional write/read test on the full database: 60 committed ticks across 161 series, 9,660 new samples, no rejected or pending ticks and no reader errors. All new values matched after checkpoint/reopen; one retry receipt remained. The raw 30-day query read 507,963 rows; p95 was 188 ms. Process peak RSS was 451.8 MiB, which includes native query results outside the 128 MB buffer setting.
  • Failed-write retry, queue overflow, signed zero, finite-value rejection, query cancellation, retention and duplicate precedence.
  • Offline backup, old-timestamp correction, restore beside an existing primary and old-Core writes followed by a new import.
  • Independent 889,978-row migration/readback and interrupted-import recovery.
  • Full import from the fresh verified box backup: 20,998,953 SQLite samples plus all 131 Parquet files, yielding 160,585,961 samples. A separate reader verified every key and value across 147 days against the original sources. All ten other SQLite history tables passed count and value-hash verification.
  • Browser check of 7/30-day synthetic history through the new Core and a verified full backup of that fixture. This is separate from physical candidate validation.

Readback now uses 2,048-row keyset pages. On the same 20.8-million-sample box source, startup with profiling completed in 47.2 seconds with 221 MiB peak RSS; an independent reader verified all eleven tables. Earlier materialized readback used about 852 MiB. These are individual Mac host runs, not Raspberry Pi timings, and DuckDB's 128 MB buffer setting is not a process memory limit.

All 17 CI checks passed on 1bd31f6, including Windows config/state/backup tests, all command builds, Core/backup startup with only System32 in PATH, Linux amd64/arm64 builds, Go, web, drivers and full-stack tests. The expanded database fixtures exceeded the old two-minute Go package deadline on shared Linux runners; the candidate gives each package five minutes and retains focused latency/cancellation assertions.

The full fresh-source import on that same commit completed in 387.49 seconds with 249.6 MiB peak process RSS and under 9 MiB Go heap. It used the production 128 MB DuckDB setting with two threads and no persistent native observer. After the final reopen, tracked ART memory was 0.75 MiB and base-table memory was 2.75 MiB. These are Mac host measurements, not Raspberry Pi timings. The independent union verifier used a separate 512 MB setting and its memory is not part of the production result.

Ready for physical beta validation at the repository owner's request. The complete source import, independent data comparison and final-head CI have passed. A fresh verified full backup of the current physical box is preserved. Full-scale portable export/restore and a long writer soak remain unverified; they are not claimed as completed. The next step is the normal beta build and installation, followed by physical writes, queries, forecast, plan and browser checks. CODEOWNERS review still applies to the running beta before stable promotion. Earlier full-source tests exposed memory growth in staging indexes and native table buffers; the staging, checkpoint and session changes above fixed the import failure on this fresh source. The primary key remains: separate checkpoint/reopen probes released its ART buffers. Beta v3.2.0-beta.1 is published from version commit 5b37898. Its normal updater is now creating the pre-install rollback point on the physical box; installation is in progress.

This branch is based on master after the planner fix in #1130 and its private Energyplan counterpart merged. The next beta must validate both changes together on the physical box.

Dependency overlap: #1085 also touches go.mod/go.sum; this change retains the existing direct dependency versions apart from adding DuckDB. #1033 proposes an Alpine Go builder update; this change needs a glibc builder for the official DuckDB archive.

Raspberry Pi evidence: an isolated f659 build passed Core start/hold/restart and the anonymous SQLite migration with eleven-table comparison. Its five-minute workload committed all 60 ticks without rejection, but 3 of 2,746 concurrent queries exceeded their three-second context under a deliberate 0.5-CPU quota. The test stopped there, so its subsequent checkpoint/readback and ARM64 backup/restore steps did not run. Peak combined QA RSS was 172.1 MiB. The QA container was removed and the existing Core was unchanged. Normal-CPU candidate checks follow during physical installation.

@frahlg
frahlg force-pushed the feat/duckdb-history branch from 1f5deb8 to a512755 Compare September 8, 2026 07:22
@frahlg
frahlg marked this pull request as ready for review September 8, 2026 08:22
@frahlg
frahlg merged commit a62b082 into master Sep 8, 2026
17 checks passed
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_22e14306-0d5a-492c-8736-fe81a2e33466)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T08:34:48.509276Z 1bd31f6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bd31f6b3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if s.history == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the fixed deadline from full-history exports

On installations with large histories or slow Raspberry Pi storage, the mandatory full backup will abort after 30 minutes even if it is making progress. This export performs an individual SQLite ExecContext for every DuckDB row and then rereads each destination table for verification, so mature databases containing tens or hundreds of millions of samples can legitimately exceed this fixed deadline; the resulting backup failure also blocks update flows that require a full backup. Use the caller's cancellable context or a configurable operation deadline rather than imposing an unconditional 30-minute cap.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant