Repository navigation
fix(flows): validate full schema on cache hit + make store init atomic - #5715
mysma-9403 wants to merge 2 commits into
Conversation
… field The vendored `tinyflows::observability::ExecutionStep` gained a `transcript` field, but four in-repo `#[cfg(test)]` constructors still use exhaustive struct literals that name every field, so the flows test tree fails to compile (E0063 ×4) — which blocks `cargo test` for the whole crate under the default `flows` feature. The library itself is unaffected (the break is test-only), so pushes to `main` — where the Rust test lanes don't run — did not catch it. Append `..Default::default()` to each literal, which is exactly the migration path the crate's own `ExecutionStep` doc prescribes for "when this struct gains a field" (it derives `Default` for this reason). This is a prerequisite for the schema-validation change in the next commit: a new `flows::store` test can't compile until the flows test tree does. Claude-Session: https://claude.ai/code/session_01ACB4Ugi5pJMQqoCbZnVo6f
Follow-up to tinyhumansai#5708 (cron) / tinyhumansai#5709 (task-sources). `flows::store` is the store those two were derived from (R-m8), and it still carried the same two gaps CodeRabbit + Codex flagged and I fixed there. 1. Complete-schema validation. The cache-hit verify probed only for the `flow_definitions` table via `sqlite_master`, so a database replaced at runtime with an older/partial schema (that table present but missing a migrated column such as `require_approval`/`graph_hash`, or one of the other tables) was trusted and later failed with `no such column`. Replace the single-table probe with a `PRAGMA user_version` check: `init_schema` now stamps `FLOWS_SCHEMA_VERSION` after a full, successful migration, and a cache hit is honoured only when the on-disk version matches. Any deleted (fresh file => version 0) or drifted database re-runs the idempotent `init_schema`, restoring the pre-gating self-heal for column drift as well as a missing table. On a hit the critical section is a single `PRAGMA user_version` read — cheaper than, and equivalent in locking to, the `sqlite_master` query it replaces, so the hot per-node `upsert_flow_run_step` path is not slowed. 2. Atomic per-path init. The `INITIALIZED_SCHEMAS` guard was released before `init_schema`, so two first callers for the same path could both run the DDL. Hold the guard across the verify + `init_schema` + insert so initialization happens exactly once per process per database file. New `older_on_disk_schema_under_a_cached_path_is_remigrated` test drops a migrated column and clears the version stamp under a cached path, then asserts the next store op re-migrates rather than failing `no such column`. The existing deleted-file self-heal test still passes. Claude-Session: https://claude.ai/code/session_01ACB4Ugi5pJMQqoCbZnVo6f
How this change flows2 changed behaviours across 13 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 44 further behaviours left out to keep the diagram readable. flowchart LR
n0["observer_persists_each_step_incrementally<br/>changed"]:::changed
n1["...n_the_database_file_is_deleted_at_runtime<br/>changed"]:::changed
n2["format"]:::impacted
n3["concurrent_step_upserts_do_not_lose_a_step"]:::impacted
n4["with_connection"]:::impacted
n5["create_get_list_delete_roundtrip"]:::impacted
n6["get_flow"]:::impacted
n7["create_flow"]:::impacted
n0 -->|calls| n2
n0 -->|tests| n2
n1 -->|calls| n7
n1 -->|tests| n7
n3 -->|calls| n7
n3 -->|tests| n7
n4 -->|calls| n2
n5 -->|calls| n6
n5 -->|tests| n6
n5 -->|calls| n7
n5 -->|tests| n7
n6 -->|calls| n2
n6 -->|calls| n4
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe flow store now tracks and validates SQLite schema versions on cached database access. It reruns migrations for older schemas and stamps successful initialization. Tests simulate schema drift, and ChangesFlow schema migration
ExecutionStep test fixtures
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This PR hardens flow-store schema recovery and initialization without changing normal user-visible behavior; no actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f953881e12
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if version == FLOWS_SCHEMA_VERSION { | ||
| return Ok(()); |
There was a problem hiding this comment.
Validate schema objects in addition to the version stamp
When a cached database is partially damaged or replaced while retaining user_version = 1—for example, if flow_definitions is dropped from an otherwise current database—this returns success without checking any tables or columns, and the subsequent operation fails with no such table until restart. The previous sqlite_master probe repaired at least the missing-flow_definitions case, so using the version as the sole integrity check regresses that recovery path; retain structural checks for the required schema objects in addition to the migration version.
Useful? React with 👍 / 👎.
|
Thanks for this, @mysma-9403 — closing because the code it guards was extracted to another crate.
So the premise — an Worth saying plainly: this was killed by a refactor, not by quality. Your #5708 and #5709 are good and are staying open. |
|
Follow-up push ( The on-disk
This mirrors the exact change CodeRabbit verified on #5708 ("the original hot-path serialization finding is addressed; a path-specific lock registry is not required" — #5708 (comment)). Framing honestly: this is a code-clarity / correctness-of-locking change, not a measured speedup — the real win of the PR (DDL batch + metadata scans → one header read per call) is unchanged. |
|
You're completely right, @M3gA-Mind — thanks for the clear write-up, and apologies for the noisy follow-up push above: that came from a sync automation that pushed the lock-free variant to all three sibling branches without first re-checking this PR's state or that |
Summary
flows::storeis the store those two were derived from (R-m8), and it still carries the same two gaps CodeRabbit + Codex flagged and I fixed there: the cache-hit verify trusted a single table, and initialization was not atomic per path. This closes both here.sqlite_masterprobe with aPRAGMA user_versioncheck:init_schemastampsFLOWS_SCHEMA_VERSIONafter a full, successful migration, and a cache hit is honoured only when the on-disk version matches — so a database replaced at runtime with an older/partial schema (missing a migrated column such asrequire_approval/graph_hash, or one of the other tables) is re-migrated instead of trusted and then failed withno such column.INITIALIZED_SCHEMASguard across the verify +init_schema+ insert so two first callers for the same path can't both run the DDL.Problem
flows::store::with_connectionfunnels every store op — includingupsert_flow_run_step, called once per node per live run. Its schema-init gating (R-m8) was the template for the cron/task-sources gating, and it has the two defects review found in those copies:flow_definitionsviasqlite_master. A database deleted at runtime yields a fresh empty file (no table → re-init, fine), but one replaced with an older/partial schema —flow_definitionspresent but missingrequire_approval/graph_hash, or missingflow_runs/flow_suggestions/flow_revisions— was trusted, and laterSELECTs failed withno such column/no such table. The pre-gating code (full idempotent DDL every call) self-healed that; the single-table probe does not.init_schemaran with the guard released, so two first callers for the same path could both run the DDL.Solution
The same fix that landed on cron/task-sources, applied to the original:
init_schemastampsPRAGMA user_version = FLOWS_SCHEMA_VERSIONafter the DDL +add_column_if_missingmigrations succeed (persistent db-file setting, like the existing WAL pragma).ensure_schema_initializedholds the guard across the whole verify + init, and on a cache hit readsPRAGMA user_version; a hit is honoured only when it matches, otherwise it falls through to the idempotentinit_schema(re-migrating a drifted or fresh db). On a hit the critical section is a singlePRAGMA user_versionread — cheaper than, and equivalent in locking to, thesqlite_masterquery it replaces, so the hotupsert_flow_run_steppath is not slowed.busy_timeout,foreign_keys = ON) and the persistentjournal_mode = WALare unchanged.New
older_on_disk_schema_under_a_cached_path_is_remigratedtest dropsrequire_approvaland resetsuser_versionunder a cached path, then asserts the next store op re-migrates rather than failingno such column. The existingschema_reinitializes_when_the_database_file_is_deleted_at_runtimetest still pins the deleted-file case.Incidental prerequisite (first commit)
The vendored
tinyflows::observability::ExecutionStepgained atranscriptfield, but four in-repo#[cfg(test)]constructors used exhaustive struct literals, so the flows test tree doesn't compile onmain(E0063 ×4) — the library is fine, so pushes tomain(Rust test lanes don't run there) didn't catch it. The first commit appends..Default::default()to each literal — exactly the migration path the crate's ownExecutionStepdoc prescribes (it derivesDefaultfor this). It's a prerequisite: the newflows::storetest can't compile until the flows test tree does. Kept as a separate commit so it's easy to see / cherry-pick.Impact
flowsfeature). No wire, schema, or config change; no migration.Known tradeoff:
FLOWS_SCHEMA_VERSIONmust be bumped whenever a table/migration is added toinit_schema(documented on the const). A future migration added without bumping it would silently skip drift-detection for older DBs. The alternative — enumerating expected columns on each hit — carries the same discipline with more surface, souser_versionis the right call.Submission Checklist
older_on_disk_schema_under_a_cached_path_is_remigratedpins the column-drift branch; the existing deleted-file self-heal test and the fullflows::storesuite still route throughwith_connection.flows::storesuite. Rancargo test --lib flows::storelocally (now that the test tree compiles again).docs/TEST-COVERAGE-MATRIX.md.docs/RELEASE-MANUAL-SMOKE.md.Related
flows::storeprecedent they were derived from.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
perf/flows-store-schema-validationf953881e1(hardening);79f3f8497(test-tree unblock)Validation Run
app/frontend changes —pnpm --filter openhuman-app format:checknot applicable.pnpm typechecknot applicable.GGML_NATIVE=OFF cargo test --lib flows::store(defaultflowsfeature) — passes, incl. both self-heal tests.cargo fmt(clean).flowsis a default feature, so — unlike the cron/task-sources PRs — this compiles and tests under default features directly (the first commit is what makes the flows test tree compile again).app/src-taurishell changes — Tauri fmt/check not applicable.Validation Blocked
command:N/Aerror:N/A — the only blocker (theExecutionSteptest-tree break) is fixed by this PR's first commit.impact:N/ABehavior Changes