diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_01_REPO_AND_SCOPE_GUARDRAILS.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_01_REPO_AND_SCOPE_GUARDRAILS.md new file mode 100644 index 0000000..4f78f65 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_01_REPO_AND_SCOPE_GUARDRAILS.md @@ -0,0 +1,228 @@ + + + +# Persistence Plan 1 Block 01 — Repo and Scope Guardrails + +**Status:** Complete on 2026-07-02. +**Goal:** Establish the exact implementation boundary before replacing the +seeded in-memory runtime. + +--- + +## Block outcome + +After this block, the executor should know exactly which existing files are +runtime truth, which demo files are temporary, which SQLite schema path to use, +and what package-boundary rules must remain enforced. + +No app behavior should change in this block unless a documentation correction is +needed. + +--- + +## Chunk 1.1 — Reconcile current repo state + +### Tasks + +1. Read `AGENTS.md` before coding. +2. Read these current plan/context files: + - `Codex Documentation/Current Software Plan/README.md` + - `Codex Documentation/Completed Plans/V1_BLOCK_20_SQLite_Persistence_Adapter_Package.md` + - `Codex Documentation/Completed Plans/V1_BLOCK_29_Flutter_UI_Foundation.md` + - `Codex Documentation/Completed Plans/UI Plan 1 - Compact Today Timeline Mockup/README.md` +3. Inspect current Flutter startup and composition files: + - `apps/focus_flow_flutter/lib/main.dart` + - `apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart` + - `apps/focus_flow_flutter/lib/app/focus_flow_app.dart` + - `apps/focus_flow_flutter/lib/controllers/scheduler_command_controller.dart` + - `apps/focus_flow_flutter/lib/controllers/today_screen_controller.dart` +4. Inspect current SQLite adapter files: + - `packages/scheduler_persistence_sqlite/lib/sqlite.dart` + - `packages/scheduler_persistence_sqlite/lib/src/scheduler_db.dart` + - `packages/scheduler_persistence_sqlite/lib/src/sqlite_repositories.dart` +5. Inspect current application-layer repository interfaces exported by + `scheduler_core`: + - `ApplicationUnitOfWork` + - `ApplicationUnitOfWorkRepositories` + - task, project, locked-time, snapshot, owner-settings, task-activity, + project-statistics, notice-acknowledgement, and operation repository + surfaces. +6. Note any discrepancy between the existing SQLite schema and the repository + methods invoked by `GetTodayStateQuery`, `V1ApplicationManagementUseCases`, + `V1ApplicationCommandUseCases`, and `V1AppOpenRecoveryUseCases`. + +### Acceptance criteria + +- The executor can name every current in-memory/demo entry point. +- The executor can name every current SQLite persistence package entry point. +- Any missing table/repository required by current app commands is listed before + implementation begins. +- No code-level architecture assumptions are carried forward from older MongoDB + docs. + +--- + +## Chunk 1.2 — Define the persistence slice precisely + +### User-visible target + +Implement only this runtime proof: + +```text +start app -> capture task -> schedule task if needed -> mark done/not done +-> close app -> reopen app -> same task/state appears +``` + +### Tasks + +1. Keep the current compact Today UI layout intact. +2. Keep sidebar/date/toggle/settings/no-op controls out of scope unless they are + already wired. +3. Keep task creation limited to the current quick-capture path unless the + current app has a real task editor by the time this block executes. +4. Keep persisted runtime owner scope fixed to the existing owner id until a + future account/settings plan changes it. +5. Persist the minimum real state required for the current command paths: + - owner settings, + - default project, + - tasks, + - scheduled task placement, + - completion/uncompletion state, + - command operation records, + - task activities emitted by completion flows, + - project statistics touched by completion flows, + - snapshots/notices only where current command/recovery flows already save + them. +6. Do not persist static mockup tasks during normal app startup. + +### Acceptance criteria + +- Scope explicitly excludes broad UI editing and future surfaces. +- The first persistence proof does not depend on manually seeding fake timeline + tasks. +- Any temporary fallback path is test-only or opt-in demo-only. + +--- + +## Chunk 1.3 — Decide schema migration strategy + +### Context + +The accepted SQLite V1 documentation mentions app-layer rows such as activities, +but the current generated Drift database may not include every row required by +application-layer use cases. This must be resolved before the Flutter app is +allowed to use SQLite as runtime truth. + +### Tasks + +1. Compare actual Drift tables against application-layer write/read needs. +2. If no real user database must be preserved yet, either: + - patch schema version 1 before persistent runtime launch, or + - still bump to schema version 2 to exercise migration discipline. +3. If any dev/user database may already exist and should survive this plan, bump + Drift `schemaVersion` and add an explicit migration. +4. Document the chosen strategy in the block completion notes. +5. Do not leave `scripts/bootstrap_dev.sh` creating an empty placeholder file + that prevents Drift from creating its schema correctly. + +### Recommended default + +Use a Drift migration to schema version 2 if new tables are needed. This is more +work, but it keeps the plan safe for existing local development databases and +aligns with the migration-test guardrail. + +### Acceptance criteria + +- The chosen schema strategy is documented before table changes. +- Migration tests are planned if `schemaVersion` changes. +- No executor silently adds tables without either patching generated schema + consistently or writing a migration. + +BREAKPOINT: Stop if schema drift requires choosing between patching V1 and +migrating to V2 and the repo docs do not clearly permit one path. + +--- + +## Chunk 1.4 — Lock package-boundary rules + +### Tasks + +1. Keep `scheduler_core` free of Flutter, Drift, SQLite, `dart:io`, platform + notifications, and backup/export implementation imports. +2. Keep widgets and controllers free of Drift/SQL APIs and scheduler `src/` + imports. +3. Choose one of these composition-boundary options: + - **Option A:** expose `SqliteApplicationUnitOfWork` from + `scheduler_persistence_sqlite`, and allow only the Flutter app composition + root to import `package:scheduler_persistence_sqlite/sqlite.dart`. + - **Option B:** add a tiny pure-Dart runtime package, for example + `scheduler_runtime`, that opens SQLite and exposes a scheduler composition; + Flutter imports only the runtime package. +4. Update `apps/focus_flow_flutter/test/forbidden_imports_test.dart` to enforce + the chosen boundary precisely. +5. Do not loosen the forbidden import scan globally. + +### Recommended default + +Use Option B if the current app test should keep banning direct SQLite adapter +imports from the Flutter app. Use Option A if a small app composition file is +accepted as the local embedded runtime boundary. + +### Acceptance criteria + +- Boundary tests describe the intended rule after this plan, not the old UI Plan + 1 rule. +- Flutter widgets still cannot import Drift, scheduler internals, or direct OS + file APIs. +- Any allowed persistence import is restricted to the composition root or runtime + package. + +--- + +## Chunk 1.5 — Establish validation commands + +### Tasks + +1. Run or prepare to run the current backend gate: + + ```sh + scripts/test.sh + ``` + +2. Run or prepare to run Flutter app gates: + + ```sh + cd apps/focus_flow_flutter + flutter analyze + flutter test + ``` + +3. If Flutter is unavailable in the execution environment, document that as + unverified and still run all Dart package tests that are available. +4. Ensure future block notes include exact commands run and failures. + +### Acceptance criteria + +- Baseline validation status is known before code changes. +- The executor does not claim Flutter validation passed unless it actually ran. + +--- + +## Block acceptance criteria + +- The repo has been inspected against the current SQLite-first architecture. +- The persistence slice target is narrow and testable. +- Demo data retirement is explicitly scoped. +- Schema migration strategy is decided or paused at the breakpoint. +- Package-boundary rule is chosen and ready to enforce. +- Baseline validation commands are known. + +## Validation after block + +Documentation-only block. Run at least: + +```sh +dart analyze +``` + +Run broader gates if the environment supports them. diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_02_SQLITE_SCHEMA_APP_RECORDS.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_02_SQLITE_SCHEMA_APP_RECORDS.md new file mode 100644 index 0000000..73bbc1b --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_02_SQLITE_SCHEMA_APP_RECORDS.md @@ -0,0 +1,220 @@ + + + +# Persistence Plan 1 Block 02 — SQLite Schema for App-Layer Records + +**Status:** Complete on 2026-07-02. +**Goal:** Make the SQLite database capable of storing every record touched by the +current application read/command flows. + +--- + +## Block outcome + +After this block, `SchedulerDb` should expose all tables required for a durable +`ApplicationUnitOfWork`. Drift generation and migration/schema tests should prove +that a database can be created or upgraded and can store the current app-layer +record set. + +This block does not yet wire Flutter to SQLite. + +--- + +## Chunk 2.1 — Audit existing Drift tables against app repository needs + +### Tasks + +1. Confirm existing tables for: + - `tasks`, + - `projects`, + - `locked_blocks`, + - `locked_overrides`, + - `settings`, + - `snapshots`. +2. Compare with `ApplicationUnitOfWorkRepositories`, which also requires: + - task activity records, + - project statistics records, + - owner settings records, + - notice acknowledgement records, + - operation/idempotency records. +3. Compare with current command use cases: + - quick capture writes tasks and operation records, + - schedule-from-backlog writes tasks, activities, possibly snapshots/notices, + - complete/uncomplete writes tasks, task activities, project statistics, and + operation records, + - app-open recovery can read/write snapshots and operation-like state. +4. Write a short implementation note in the eventual block completion section + listing which tables were missing and which were already present. + +### Acceptance criteria + +- Missing app-layer tables are identified before implementation. +- No application repository method is ignored because it is not needed by the + first manual happy path; use cases should remain semantically durable. + +--- + +## Chunk 2.2 — Add application operation table + +### Tasks + +1. Add a Drift table for committed application operations. +2. Include fields needed by `ApplicationOperationRecord` and idempotency lookup: + - `operation_id TEXT PRIMARY KEY`, + - `owner_id TEXT`, + - `operation_name TEXT`, + - `committed_at_utc DATETIME`, + - optional `idempotency_key` only if the current core record/interface needs + it, + - `revision INT` only if the repository convention requires it for this row. +3. Add an index for owner + operation id/idempotency lookup if the table does not + use a composite key. +4. Map through existing core document mapping helpers where available. +5. Do not expose raw Drift rows outside the SQLite adapter/runtime boundary. + +### Acceptance criteria + +- A committed operation can be inserted once and found on a later app run. +- Duplicate operation inserts are detected consistently. +- Operation records are owner-scoped. + +--- + +## Chunk 2.3 — Add task activity table + +### Tasks + +1. Add a Drift table for append-only task activity records. +2. Include fields needed by `TaskActivity` and app queries: + - `id TEXT PRIMARY KEY`, + - `owner_id TEXT`, + - `task_id TEXT`, + - `project_id TEXT`, + - `operation_id TEXT`, + - `code TEXT`, + - `occurred_at_utc DATETIME`, + - metadata/details JSON if represented by the current document mapping, + - `created_at_utc` / `updated_at_utc` only if mapping/common fields require + them. +3. Add indexes for owner, task, project, and operation lookups. +4. Preserve append-only semantics; do not design an update/delete flow unless the + current application repository requires it. + +### Acceptance criteria + +- Completion-generated activities survive close/reopen. +- `findByTaskId`, `findByProjectId`, `findByOperationId`, `findByOwner`, and + `findAll` can be implemented deterministically. +- Activity rows do not require Flutter or UI metadata. + +--- + +## Chunk 2.4 — Add project statistics table + +### Tasks + +1. Add a Drift table for project statistics aggregates. +2. Use `project_id` as the stable aggregate key, owner scoped. +3. Store counters and applied activity ids using explicit JSON columns if the + current domain object is map/list heavy. +4. Include `revision INT` because current repository semantics use optimistic + save for aggregates. +5. Add index/unique constraint for `owner_id + project_id`. + +### Acceptance criteria + +- Project statistics can be read, saved, and saved with expected revision. +- Completion after restart does not double-apply already recorded activities. +- JSON fields round-trip through core document mapping helpers. + +--- + +## Chunk 2.5 — Add notice acknowledgement table if needed by current app layer + +### Tasks + +1. Add a Drift table for acknowledged notices if current application management + or recovery use cases read/write notice acknowledgements. +2. Key by owner + notice id. +3. Store acknowledged/created timestamp fields required by the domain record. +4. Add deterministic owner paging. + +### Acceptance criteria + +- Notice acknowledgement reads/writes can be implemented without in-memory + fallback. +- Acknowledged notices remain acknowledged after app restart. + +--- + +## Chunk 2.6 — Update Drift database wiring and generation + +### Tasks + +1. Add table files under descriptive `scheduler_db/tables/...` subfolders. +2. Add DAO/accessor files only where useful; do not add empty accessors if direct + queries are clearer and consistent with existing code. +3. Update `SchedulerDb` table list. +4. Apply the schema strategy chosen in Block 01: + - patch version 1 only if explicitly permitted, or + - bump `schemaVersion` and write migration steps. +5. Run Drift generation: + + ```sh + cd packages/scheduler_persistence_sqlite + dart run build_runner build --delete-conflicting-outputs + ``` + +6. Format generated and handwritten files. + +### Acceptance criteria + +- Drift generated files are updated. +- `SchedulerDb` opens a new database with all expected tables. +- Existing table names and columns remain compatible unless a migration test + covers the change. + +BREAKPOINT: Stop if Drift generation would require Flutter SDK dependencies in a +pure Dart package. + +--- + +## Chunk 2.7 — Add schema and migration tests + +### Required tests + +1. New-database schema smoke test includes all current tables. +2. Migration test from the previous schema creates the new tables and preserves + existing task/project/settings rows. +3. Date/time persistence test confirms UTC values remain UTC after round-trip. +4. JSON-column smoke test confirms non-empty task activity/project statistics + payloads round-trip. +5. Generated schema/table-name test stays deterministic across platforms. + +### Acceptance criteria + +- `cd packages/scheduler_persistence_sqlite && dart test` passes. +- `cd packages/scheduler_persistence_sqlite && dart analyze` passes. +- Root `dart test` still passes. + +--- + +## Block acceptance criteria + +- SQLite schema includes every table needed by current application read/command + flows. +- Drift generation is current. +- Migration behavior is tested if `schemaVersion` changed. +- No Flutter or widget code imports Drift or SQL. +- No command path must fall back to in-memory-only app records. + +## Validation after block + +```sh +cd packages/scheduler_persistence_sqlite +dart analyze +dart test +cd ../.. +dart analyze +dart test +``` diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_03_SQLITE_APPLICATION_UNIT_OF_WORK.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_03_SQLITE_APPLICATION_UNIT_OF_WORK.md new file mode 100644 index 0000000..7e53263 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_03_SQLITE_APPLICATION_UNIT_OF_WORK.md @@ -0,0 +1,227 @@ + + + +# Persistence Plan 1 Block 03 — SQLite Application Unit of Work + +**Status:** Complete on 2026-07-02. +**Goal:** Implement a SQLite-backed `ApplicationUnitOfWork` that current +scheduler application use cases can use without knowing about Drift. + +--- + +## Block outcome + +After this block, scheduler application read/command use cases should run +against SQLite in tests. The adapter must support current reads and writes for +Today, Backlog, quick capture, schedule-from-backlog, done, and uncomplete. + +Flutter still remains unwired until Block 04. + +--- + +## Chunk 3.1 — Choose implementation location and public API + +### Tasks + +1. Implement the application-unit-of-work adapter in the SQLite adapter layer or + a tiny runtime package, as chosen in Block 01. +2. Preferred direct adapter names if implemented in + `scheduler_persistence_sqlite`: + - `SqliteApplicationUnitOfWork`, + - `SqliteApplicationRepositories`, + - `SqliteApplicationRuntime` only if it owns file opening/lifecycle. +3. Export the public entry point from `packages/scheduler_persistence_sqlite/lib/sqlite.dart` + or from the chosen runtime package. +4. Keep raw Drift database classes available for tests but do not require + Flutter widgets to instantiate them. + +### Acceptance criteria + +- Application code can create a SQLite-backed `ApplicationUnitOfWork` through a + public package API. +- `scheduler_core` remains storage-free. +- Existing repository conformance APIs remain intact. + +--- + +## Chunk 3.2 — Implement transaction semantics + +### Tasks + +1. Implement `ApplicationUnitOfWork.run` using a Drift transaction. +2. Implement `ApplicationUnitOfWork.read` as a read-only repository view. +3. Preserve current in-memory semantics: + - duplicate `operationId` returns duplicate-operation failure, + - domain validation exceptions map to validation failures, + - application persistence exceptions map to persistence failure, + - unexpected errors map to unexpected failure, + - operation records are written only after the action succeeds. +4. Ensure operation record commit and all repository writes happen atomically. +5. Ensure failed actions do not partially save tasks or activities. +6. Ensure nested transaction behavior is either not used or explicitly safe. + +### Acceptance criteria + +- A command failure leaves the database unchanged. +- A successful command writes changed tasks, activities/statistics, and operation + record in one transaction. +- Duplicate operation id is rejected across app restarts. + +--- + +## Chunk 3.3 — Implement SQLite app task repository + +### Required methods + +Implement the `scheduler_core` application-layer task repository methods used by +current use cases, including at minimum: + +1. `findById` +2. `findRecordById` +3. `findAll` +4. `findByStatus` +5. `findByOwner` +6. `findByProjectId` +7. `findByParentTaskId` +8. `findBacklogCandidates` +9. `findScheduledInWindow` +10. `findScheduledInWindowForOwner` +11. `findForLocalDay` +12. `save` +13. `saveAll` +14. `insert` +15. `saveIfRevision` + +### Implementation notes + +- Reuse existing task document/row mapping helpers where possible. +- Preserve owner scoping. +- Preserve deterministic ordering used by Today and Backlog read models. +- Treat UTC task instants as stored UTC values. +- `save`/`saveAll` should insert if absent only if current in-memory semantics + require that behavior; otherwise use explicit repository mutation failures. + Document the chosen behavior because current command code calls `saveAll` for + changed tasks. +- If `saveAll` needs compare-and-set behavior, load current records first and + write each changed row in the enclosing transaction. + +### Acceptance criteria + +- Quick-captured tasks are stored as backlog tasks. +- Scheduled tasks reload with schedule placement. +- Completed tasks reload with `TaskStatus.completed`, actual times, and + `completedAt` where present. +- Stale revision tests cover `saveIfRevision`. + +--- + +## Chunk 3.4 — Implement SQLite app project/settings/locked/snapshot repositories + +### Tasks + +1. Implement app-layer `ProjectRepository` using existing project SQLite rows. +2. Implement app-layer `OwnerSettingsRepository` using existing settings rows. +3. Implement app-layer `LockedBlockRepository` using existing locked block and + override rows. +4. Implement app-layer `SchedulingSnapshotRepository` using existing snapshot + rows. +5. Preserve domain object mapping and owner scoping. +6. Support repository page semantics where the app-layer interface requires + pages. +7. Convert lower-level repository failures into application-layer repository + failures consistently. + +### Acceptance criteria + +- `GetTodayStateQuery` can run against an empty SQLite database plus bootstrap + owner settings/project. +- Locked time and snapshots do not crash current app reads. +- Snapshot retention/delete behavior remains consistent with ADR policy. + +--- + +## Chunk 3.5 — Implement SQLite app activity/statistics/notice/operation repositories + +### Tasks + +1. Implement `TaskActivityRepository` over the new task activity table. +2. Implement `ProjectStatisticsRepository` over the new project statistics table. +3. Implement `NoticeAcknowledgementRepository` over the new notice table, if it + exists after Block 02. +4. Implement `ApplicationOperationRepository` over the new operation table. +5. Make append-only writes idempotent where current interfaces require it. +6. Add owner filtering to every owner-scoped query. +7. Avoid storing derived UI-only strings. + +### Acceptance criteria + +- Completing a task writes activity records durably. +- Project statistics survive close/reopen. +- Operation records survive close/reopen and prevent duplicate operation replay. +- Notice acknowledgement behavior, if used, survives close/reopen. + +--- + +## Chunk 3.6 — Add adapter-level tests for current command/read flows + +### Required tests + +1. Empty database read: + - bootstrap owner settings/project if using bootstrap helper, + - `GetTodayStateQuery` returns empty Today without failure. +2. Quick capture persistence: + - run `V1ApplicationCommandUseCases.quickCaptureToBacklog`, + - close the first database handle, + - reopen the same file, + - read backlog and find the task. +3. Schedule persistence: + - quick capture or insert a backlog task, + - schedule it to next available slot, + - reopen, + - Today state contains the scheduled task and Backlog does not. +4. Completion persistence: + - schedule a task, + - complete it, + - reopen, + - Today state shows completed/done state. +5. Uncomplete persistence: + - complete then uncomplete, + - reopen, + - Today state shows planned/not-done state. +6. Atomic failure: + - trigger a validation or stale write failure, + - assert no partial task/activity/statistics/operation writes persisted. +7. Duplicate operation: + - run one command, + - attempt same operation id again after reopen, + - assert duplicate-operation failure. + +### Acceptance criteria + +- Tests use temporary file SQLite databases, not only `NativeDatabase.memory()`. +- Tests explicitly close database handles before reopen assertions. +- Tests do not depend on Flutter. + +--- + +## Block acceptance criteria + +- A SQLite-backed `ApplicationUnitOfWork` exists and is exported through the + chosen adapter/runtime boundary. +- Current scheduler reads and commands run against SQLite in package tests. +- Transactions prevent partial persistence. +- Close/reopen behavior is proven outside Flutter. +- Existing repository conformance tests still pass. + +## Validation after block + +```sh +cd packages/scheduler_persistence_sqlite +dart analyze +dart test +cd ../.. +scripts/test.sh +``` + +If `scripts/test.sh` is unavailable, run `dart analyze` and `dart test` from the +workspace root and document the gap. diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_04_FLUTTER_RUNTIME_COMPOSITION.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_04_FLUTTER_RUNTIME_COMPOSITION.md new file mode 100644 index 0000000..aaf8809 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_04_FLUTTER_RUNTIME_COMPOSITION.md @@ -0,0 +1,203 @@ + + + +# Persistence Plan 1 Block 04 — Flutter Runtime Composition + +**Status:** Complete on 2026-07-02. +**Goal:** Build a production-like Flutter composition root that opens SQLite, +bootstraps required owner state, creates scheduler use cases, and disposes the +database cleanly. + +--- + +## Block outcome + +After this block, the Flutter app should be able to choose between a persistent +SQLite composition for normal runtime and a deterministic demo/test composition +for widget tests. Normal `main()` should be ready to use the persistent +composition, but the final demo-seed retirement happens in Block 05. + +--- + +## Chunk 4.1 — Add runtime dependencies without polluting widgets + +### Tasks + +1. Add only the dependencies needed by the chosen boundary: + - If using direct app composition, add app path dependency on + `scheduler_persistence_sqlite` and any SQLite native support package needed + by Flutter desktop. + - If using a runtime package, add app path dependency on that runtime package + only. +2. Keep widgets/controllers importing public scheduler app/controller APIs only. +3. Keep `package:drift/`, `package:scheduler_persistence_sqlite/`, and `dart:io` + out of widgets/controllers. +4. Update forbidden import tests to enforce the exact allowed boundary. + +### Acceptance criteria + +- `flutter pub get` resolves. +- Forbidden import test still prevents SQL/Drift imports outside the approved + composition/runtime boundary. +- Backend packages remain in the root Dart workspace; Flutter app remains outside + the root workspace unless a separate plan changes that. + +--- + +## Chunk 4.2 — Read SQLite path from configuration + +### Tasks + +1. Read the configured SQLite path from: + + ```dart + const String.fromEnvironment('SCHEDULER_SQLITE_PATH') + ``` + +2. If no dart define is present, use the existing default scheduler SQLite path + convention, but keep path resolution in the runtime/composition layer. +3. In tests, allow injecting a temporary path or in-memory executor. +4. Do not call `DateTime.now()` or path APIs inside widgets. +5. Ensure parent directories are created before opening the database. +6. Ensure an empty placeholder file created by `scripts/bootstrap_dev.sh` does + not break Drift schema creation. + +### Acceptance criteria + +- `scripts/dev.dart --sqlite /tmp/scheduler.sqlite` controls the app DB path. +- Tests can provide isolated temp database files. +- Runtime path resolution is not duplicated in UI widgets. + +--- + +## Chunk 4.3 — Build persistent scheduler composition + +### Suggested files + +Use names that fit the chosen boundary. Possible direct-app layout: + +```text +apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart +apps/focus_flow_flutter/lib/app/runtime/sqlite_path_config.dart +apps/focus_flow_flutter/lib/app/runtime/scheduler_runtime_lifecycle.dart +``` + +Possible runtime-package layout: + +```text +packages/scheduler_runtime/lib/scheduler_runtime.dart +packages/scheduler_runtime/lib/src/sqlite_scheduler_runtime.dart +apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart +``` + +### Tasks + +1. Create a persistent composition that provides the same high-level factory + surface currently expected by `FocusFlowApp`/`FocusFlowHome`: + - today screen controller, + - command controller, + - operation context factory, + - selected date/date label, + - read refresh callbacks. +2. Internally use: + - SQLite-backed `ApplicationUnitOfWork`, + - `GetTodayStateQuery`, + - `V1ApplicationManagementUseCases`, + - `V1ApplicationCommandUseCases`, + - existing timezone resolver policy. +3. Preserve a fixed V1 owner id until a settings/account plan changes it. +4. Keep the current app interface small. Prefer defining an abstract composition + protocol if both demo and persistent compositions need to satisfy it. + +### Acceptance criteria + +- `FocusFlowApp` no longer needs to know whether its composition is in-memory or + SQLite-backed. +- The persistent composition can load Today state from SQLite without seeded + tasks. +- Widget code still consumes controllers/read models. + +--- + +## Chunk 4.4 — Bootstrap required owner state + +### Tasks + +1. Add a startup bootstrap step that runs before first read. +2. If owner settings do not exist, insert defaults: + - owner id matching current app owner, + - timezone id matching current runtime resolver, + - compact mode enabled if this remains the current UI mode. +3. If no default project exists, create a stable default project. +4. Decide the default project id for V1 quick capture: + - Preferred: move current UI hardcoded `home` project id into the composition + or settings/defaults boundary. + - Alternative: use `inbox` if that is the canonical app-layer default, and + update quick capture wiring and tests consistently. +5. Do not bootstrap demo timeline tasks. +6. Make bootstrap idempotent across app restarts. +7. Run bootstrap inside the SQLite unit-of-work transaction boundary. + +### Acceptance criteria + +- First app launch on an empty DB shows an empty real Today state, not a failure. +- Quick capture has a valid project id without hardcoded widget knowledge. +- Reopening the app does not duplicate default projects/settings. + +--- + +## Chunk 4.5 — Add lifecycle disposal + +### Tasks + +1. Add a `dispose` or `close` method to the persistent composition/runtime. +2. Ensure `FocusFlowHome` or the app root disposes controllers first and DB last. +3. Ensure tests can close the database deterministically before reopening the + same path. +4. Do not leave long-lived SQLite handles open across tests. + +### Acceptance criteria + +- App shutdown closes the database cleanly. +- Close/reopen tests do not flake due to locked files. +- No widget owns a raw database handle. + +--- + +## Chunk 4.6 — Add composition-level tests + +### Required tests + +1. Persistent composition opens an empty temp DB and loads an empty Today state. +2. Persistent composition bootstraps exactly one settings row and one default + project row. +3. Persistent composition quick captures a task and refreshes read controllers. +4. Persistent composition can be disposed and reopened against the same file. +5. Demo composition remains available for deterministic visual tests, if kept. + +### Acceptance criteria + +- Tests run under Flutter only if they need Flutter bindings; otherwise prefer + pure Dart package tests. +- No tests rely on static mockup tasks for persistence proof. + +--- + +## Block acceptance criteria + +- Persistent composition exists and can open/configure SQLite. +- Owner settings/default project bootstrap is idempotent. +- Runtime DB path is controlled by `SCHEDULER_SQLITE_PATH` or injected test path. +- Composition lifecycle closes DB resources. +- Widget/controller package boundaries remain enforced. + +## Validation after block + +```sh +cd apps/focus_flow_flutter +flutter pub get +flutter analyze +flutter test +cd ../.. +scripts/test.sh +``` diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_05_RETIRE_DEMO_SEED_AND_WIRE_COMMANDS.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_05_RETIRE_DEMO_SEED_AND_WIRE_COMMANDS.md new file mode 100644 index 0000000..b1c7750 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_05_RETIRE_DEMO_SEED_AND_WIRE_COMMANDS.md @@ -0,0 +1,195 @@ + + + +# Persistence Plan 1 Block 05 — Retire Demo Seed and Wire Persistent Commands + +**Status:** Complete on 2026-07-02. +**Goal:** Make normal app startup use persisted SQLite state and prove the +current capture/schedule/done controls mutate durable state. + +--- + +## Block outcome + +After this block, launching the app normally should no longer recreate arbitrary +demo tasks. The app should load real persisted tasks, allow the current command +paths to change them, and refresh Today from SQLite-backed read models. + +--- + +## Chunk 5.1 — Introduce an app composition protocol if needed + +### Context + +`FocusFlowApp` currently depends on `DemoSchedulerComposition`. To switch normal +runtime to SQLite without throwing away deterministic widget tests, the app +should depend on a small composition surface instead of one concrete demo class. + +### Tasks + +1. Define a protocol/interface for the composition used by the app shell if the + current class shape makes that useful. It should expose only: + - owner/date label data needed by UI, + - Today screen controller factory, + - command controller factory, + - operation context factory if still needed externally, + - dispose/close if the runtime owns resources. +2. Make the demo composition implement the protocol or adapt it behind a wrapper. +3. Make the persistent composition implement the protocol. +4. Keep the protocol free of Drift, SQL, and row types. + +### Acceptance criteria + +- `FocusFlowApp` can be given either demo or persistent composition. +- Widget tests can still use deterministic demo/fake state where visual coverage + needs it. +- Normal `main()` can switch to persistent composition without altering widgets. + +--- + +## Chunk 5.2 — Change normal app startup to persistent SQLite + +### Tasks + +1. Update `apps/focus_flow_flutter/lib/main.dart` so normal startup opens the + persistent SQLite composition. +2. Keep an explicit demo entry point only if needed, for example: + - `main_demo.dart`, + - test-only factory, + - `--dart-define=FOCUS_FLOW_DEMO_SEED=true` if an opt-in demo mode is useful. +3. Do not keep static demo task insertion on the normal code path. +4. Show a calm failure state if the DB cannot open instead of silently falling + back to fake data. +5. Keep startup asynchronous if required, using a small loading shell before the + persistent composition is ready. + +### Acceptance criteria + +- Running the app with no demo flags starts from the SQLite file. +- A database-open failure is visible and diagnosable. +- No arbitrary UI mockup tasks are inserted into user data. + +BREAKPOINT: Stop if the app cannot initialize asynchronous composition without a +small root-shell refactor. Keep the refactor minimal and do not redesign the UI. + +--- + +## Chunk 5.3 — Remove hardcoded demo assumptions from command wiring + +### Tasks + +1. Replace hardcoded quick-capture project id `'home'` in + `SchedulerCommandController` with a composition-provided/default project id. +2. Keep owner id/timezone id in the operation context factory, not widgets. +3. Ensure scheduled commands pass the expected updated timestamp/revision-related + metadata currently available from read models. +4. If read models do not expose enough information for stale-write protection, + add the smallest read-model field needed rather than bypassing stale checks. +5. Keep command labels/messages calm and stable. + +### Acceptance criteria + +- Quick capture works on a fresh SQLite DB with bootstrapped project defaults. +- Scheduling from Backlog does not depend on demo project ids. +- Completion/uncompletion commands target real persisted task ids. + +--- + +## Chunk 5.4 — Ensure Today and Backlog refresh from SQLite after commands + +### Tasks + +1. Verify the current refresh callback reloads all read controllers that should + reflect command changes. +2. If the compact Today screen currently only refreshes one controller, ensure + Backlog/TODAY surfaces used in this slice refresh together or derive from a + shared reload boundary. +3. After quick capture, Backlog reflects the new row. +4. After scheduling, Today reflects the planned placement and Backlog no longer + shows the task. +5. After done/uncomplete, Today reflects the new status. +6. Avoid optimistic UI as canonical state for this plan. It may show command + running/success, but the rendered task state must come from SQLite-backed + read models after refresh. + +### Acceptance criteria + +- UI state after every command is read back from SQLite-backed application state. +- No widget directly mutates task status or schedule placement as durable truth. + +--- + +## Chunk 5.5 — Preserve demo composition only where useful + +### Tasks + +1. Keep deterministic seeded demo data only for: + - visual/widget tests, + - mockup comparison tests, + - explicit demo mode if retained. +2. Rename files/classes if needed to clarify they are not normal runtime, for + example `SeededDemoSchedulerComposition`. +3. Update comments and README text that still say all controls are no-op if the + current code now supports command flows. +4. Do not delete tests that protect UI Plan 1 visuals unless they are replaced + with equal coverage. + +### Acceptance criteria + +- Normal runtime and demo/test runtime are visibly separate in code. +- Future contributors cannot accidentally reintroduce demo seed into user data. +- README status matches actual command wiring. + +--- + +## Chunk 5.6 — Add focused Flutter tests for persistent command wiring + +### Required tests + +1. Fresh persistent app starts empty or with only bootstrapped non-task state. +2. Quick capture through the UI writes a backlog task to temp-file SQLite. +3. Reopening the app against the same temp file still shows the captured backlog + task. +4. Scheduling through the UI writes planned schedule placement. +5. Reopening shows the scheduled task in Today. +6. Tapping done writes completed state. +7. Reopening shows the task as completed/done. +8. Tapping done/status again uncompletes if that interaction is currently wired. +9. Reopening shows planned/not-done state. +10. Demo visual tests still run with deterministic seeded data. + +### Test constraints + +- Use temporary SQLite files. +- Close/dispose every persistent composition before reopening. +- Avoid relying on wall-clock current date; inject fixed clock/date where the + app composition allows it. +- Keep tests resilient to UI text changes by using existing value keys where + practical. + +### Acceptance criteria + +- Flutter tests prove the exact user lifecycle requested by the plan. +- Failures identify whether the bug is read, write, scheduling, or lifecycle. + +--- + +## Block acceptance criteria + +- Normal app startup uses SQLite-backed persisted state. +- Static demo task insertion is removed from normal runtime. +- Existing quick capture/schedule/done/uncomplete commands operate on persisted + application state. +- Today/Backlog render from post-command SQLite reads. +- Persistent Flutter tests cover close/reopen task state. +- Demo/test seed remains isolated if retained. + +## Validation after block + +```sh +cd apps/focus_flow_flutter +flutter analyze +flutter test +cd ../.. +scripts/test.sh +``` diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_06_LIFECYCLE_VALIDATION_HANDOFF.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_06_LIFECYCLE_VALIDATION_HANDOFF.md new file mode 100644 index 0000000..39caa24 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_BLOCK_06_LIFECYCLE_VALIDATION_HANDOFF.md @@ -0,0 +1,244 @@ + + + +# Persistence Plan 1 Block 06 — Lifecycle Validation and Handoff + +**Status:** Complete on 2026-07-02. +**Goal:** Prove the requested close/reopen persistence behavior, run validation, +and document what changed for the next functional plan. + +--- + +## Block outcome + +After this block, the repo should have a validated SQLite-backed runtime slice +with clear handoff notes. The next plan can safely build richer task add/edit +flows or wire more modal controls without first solving persistence. + +--- + +## Chunk 6.1 — Add pure Dart lifecycle integration tests + +### Required tests + +Create or extend package-level integration tests so the persistence proof does +not depend only on Flutter widgets. + +1. Open temp-file SQLite database. +2. Bootstrap owner settings and default project. +3. Run `quickCaptureToBacklog` through `V1ApplicationCommandUseCases`. +4. Close DB. +5. Reopen DB. +6. Read Backlog and assert the task exists. +7. Schedule task to next available slot. +8. Close/reopen. +9. Read Today and assert schedule placement exists. +10. Complete task. +11. Close/reopen. +12. Read Today and assert completed state exists. +13. Uncomplete task. +14. Close/reopen. +15. Read Today and assert planned/not-done state exists. + +### Acceptance criteria + +- Tests exercise the same application use cases used by Flutter. +- Tests do not inspect raw rows except for targeted diagnostics. +- Tests close every database handle before reopen assertions. + +--- + +## Chunk 6.2 — Add Flutter lifecycle smoke tests + +### Required tests + +1. Pump app with persistent temp-file composition. +2. Verify empty first-run state. +3. Capture a task through the visible quick-capture UI. +4. Dispose app/composition and pump a new app with the same DB path. +5. Verify the task appears. +6. Schedule the task if current UI exposes the action. +7. Dispose/reopen. +8. Verify planned Today card appears. +9. Mark done. +10. Dispose/reopen. +11. Verify done/completed visual state appears. + +### Acceptance criteria + +- The requested user path is proven at widget level. +- Test names describe the close/reopen behavior explicitly. +- Demo widget tests remain separate from persistent lifecycle tests. + +--- + +## Chunk 6.3 — Manual verification script/checklist + +### Tasks + +1. Add a short manual checklist to app README or a plan handoff file: + + ```sh + scripts/bootstrap_dev.sh + scripts/dev.sh --sqlite /tmp/focus_flow_persistence_plan_1.sqlite + ``` + +2. Manual path: + - start app, + - capture a task, + - schedule it if visible, + - mark it done, + - close app, + - restart with the same `--sqlite` path, + - confirm task and done state persist. +3. Include how to reset the local dev DB safely. +4. Include the actual default DB path used by scripts. + +### Acceptance criteria + +- A developer can manually verify persistence without reading code. +- Reset instructions distinguish deleting dev data from backup/restore. + +--- + +## Chunk 6.4 — Update documentation to match reality + +### Files to review/update + +1. `apps/focus_flow_flutter/README.md` +2. Root `README.md` only if commands or behavior changed. +3. `Codex Documentation/Current Software Plan/README.md` +4. This plan's block files after execution, moving completed files if that is the + current project convention. +5. Any completed-plan index if the project archives completed plans after the + implementation is done. + +### Documentation updates + +1. Remove or revise claims that all visible controls are intentionally no-op. +2. Document that normal runtime uses SQLite. +3. Document demo/test seed boundaries. +4. Document current persistence proof and known limits. +5. Document validation commands and results. +6. Keep backup/restore and export/import boundaries unchanged. + +### Acceptance criteria + +- Docs do not imply demo seed is normal runtime. +- Docs identify exactly what persists now and what still does not. +- Docs do not promise future UI/control behavior that is still unwired. + +--- + +## Chunk 6.5 — Run full validation gates + +### Required commands + +Run the broadest available set: + +```sh +scripts/bootstrap_dev.sh +scripts/test.sh +cd apps/focus_flow_flutter +flutter pub get +flutter analyze +flutter test +``` + +If Flutter is unavailable in the execution environment, run all available Dart +validation and document Flutter as unverified. + +### Extra focused commands + +```sh +cd packages/scheduler_persistence_sqlite +dart analyze +dart test +``` + +If a new runtime package was added: + +```sh +cd packages/scheduler_runtime +dart analyze +dart test +``` + +### Acceptance criteria + +- Every changed package has package-local analyze/test results. +- Root backend gate passes. +- Flutter gate passes when the Flutter SDK is available. +- Coverage remains above the current project threshold or any gap is documented + and fixed before handoff. + +--- + +## Chunk 6.6 — Final handoff notes + +### Required content + +At the end of implementation, add a concise handoff note with: + +1. Files changed. +2. New runtime/composition entry points. +3. SQLite schema/migration version decision. +4. What state now persists. +5. What state is still intentionally not in scope. +6. Commands run and results. +7. Known risks or follow-up plan suggestions. +8. Manual close/reopen verification result if it was run. + +### Acceptance criteria + +- Handoff is specific enough for the next assistant/developer to continue + without re-discovering the persistence boundary. +- No unverified claim is written as passed validation. + +--- + +## Block acceptance criteria + +- Pure Dart lifecycle tests prove add/schedule/done/uncomplete close/reopen. +- Flutter lifecycle tests prove the visible user path. +- Manual verification instructions exist. +- Docs match the new SQLite runtime behavior. +- Validation results are recorded honestly. + +## Validation after block + +```sh +scripts/test.sh +cd apps/focus_flow_flutter +flutter analyze +flutter test +``` + +## Completion statement template + +Use this shape in the final implementation handoff: + +```text +Persistence Plan 1 is complete. + +Changed: +- ... + +Now persists: +- tasks captured from the UI +- schedule placement +- done/not-done state +- owner settings/default project bootstrap +- application operation/activity/statistics records needed by current commands + +Validation: +- scripts/test.sh: passed/failed/unavailable +- flutter analyze: passed/failed/unavailable +- flutter test: passed/failed/unavailable + +Not in scope: +- full task editor +- backup/restore UI +- export/import UI +- sync/calendar/notifications +``` diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_EXECUTION_ORDER.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_EXECUTION_ORDER.md new file mode 100644 index 0000000..a8d0dba --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_EXECUTION_ORDER.md @@ -0,0 +1,46 @@ + + + +# Persistence Plan 1 Execution Order + +**Status:** Complete on 2026-07-02. + +Run these files in order. All chunks are intended for `xhigh` execution unless a +chunk explicitly says otherwise. + +1. `PERSISTENCE_PLAN_1_BLOCK_01_REPO_AND_SCOPE_GUARDRAILS.md` + - Reconcile current repo state. + - Confirm demo seed retirement scope. + - Confirm schema migration strategy. + - Lock package-boundary rules for this persistence slice. + +2. `PERSISTENCE_PLAN_1_BLOCK_02_SQLITE_SCHEMA_APP_RECORDS.md` + - Add or verify missing SQLite tables for application-layer records. + - Add Drift migration/schema tests. + - Keep table rows behind adapter mapping helpers. + +3. `PERSISTENCE_PLAN_1_BLOCK_03_SQLITE_APPLICATION_UNIT_OF_WORK.md` + - Implement a SQLite-backed `ApplicationUnitOfWork`. + - Implement application repository adapters needed by current read/command + use cases. + - Add transaction, idempotency, and close/reopen tests. + +4. `PERSISTENCE_PLAN_1_BLOCK_04_FLUTTER_RUNTIME_COMPOSITION.md` + - Build persistent runtime composition. + - Open the configured SQLite file. + - Bootstrap owner settings and the default project. + - Dispose the database cleanly. + +5. `PERSISTENCE_PLAN_1_BLOCK_05_RETIRE_DEMO_SEED_AND_WIRE_COMMANDS.md` + - Stop normal app startup from inserting arbitrary demo tasks. + - Preserve demo/test composition only for tests or explicit demo mode. + - Wire quick capture, schedule, done, and uncomplete against SQLite state. + +6. `PERSISTENCE_PLAN_1_BLOCK_06_LIFECYCLE_VALIDATION_HANDOFF.md` + - Add temp-file lifecycle tests for add/schedule/done/reopen. + - Run backend and Flutter validation gates. + - Update handoff docs and README status. + +Do not jump ahead into broader task editing, backup UI, export UI, navigation, +calendar sync, notifications, or Shield/Recovery screens while executing this +plan. diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_SUMMARY.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_SUMMARY.md new file mode 100644 index 0000000..3798e98 --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/PERSISTENCE_PLAN_1_SUMMARY.md @@ -0,0 +1,104 @@ + + + +# Persistence Plan 1 Summary — SQLite Runtime Persistence + +**Status:** Complete on 2026-07-02. +**Scope level:** XHIGH implementation plan. +**Primary outcome:** The Flutter desktop app persists real task state to an +on-disk SQLite database instead of recreating arbitrary demo seed data at launch. + +--- + +## Current repo facts this plan is based on + +1. The project is SQLite-first and V1 has no MongoDB runtime. +2. `packages/scheduler_persistence_sqlite` already contains a Drift database and + repository implementations for task/project/locked-time/settings/snapshot + persistence. +3. The Flutter app still starts through `DemoSchedulerComposition.seeded()` and + an `InMemoryApplicationUnitOfWork`. +4. The dev runner already passes `SCHEDULER_SQLITE_PATH` into Flutter as a Dart + define, but the Flutter app does not yet consume that path. +5. `V1ApplicationCommandUseCases` and `GetTodayStateQuery` already operate + against the public `ApplicationUnitOfWork` boundary. +6. Existing command flows write more than tasks: completion can also write task + activities, project statistics, and operation records. +7. Current SQLite schema files do not yet expose all app-layer repository data + needed for durable application semantics across restarts. + +--- + +## Definition of done + +Persistence Plan 1 is complete when all of the following are true: + +1. `main.dart` no longer boots the normal app through static seeded demo data. +2. App startup opens an on-disk SQLite database at the configured path. +3. The composition root builds scheduler queries/use cases with a SQLite-backed + `ApplicationUnitOfWork`. +4. The app creates or loads required owner bootstrap data, including owner + settings and a default Inbox/Home project, without inserting arbitrary demo + timeline tasks. +5. Quick capture persists a task to SQLite. +6. Scheduling a backlog task persists `status`, `durationMinutes`, + `scheduledStart`, and `scheduledEnd`. +7. Completing and uncompleting a task persist `status`, actual/completion + timestamps, and updated metadata. +8. Closing and reopening the app against the same database reloads the task, + schedule placement, and done/not-done state. +9. Widgets/controllers still consume read models/controllers and do not contain + scheduling, Drift, SQL, or OS path logic. +10. Temp-file lifecycle tests prove close/reopen persistence. +11. Existing backend gates and Flutter gates pass, or every unavailable command + is documented with the reason. + +--- + +## Non-goals + +1. No UI redesign beyond empty/loading/error text needed for real persisted data. +2. No new scheduling algorithm behavior. +3. No manual SQL in Flutter widgets. +4. No backup/restore UI. +5. No JSON/CSV export UI. +6. No calendar sync. +7. No notifications wiring. +8. No week/month views. +9. No Shield/Recovery UX beyond preserving existing backend recovery contracts. + +--- + +## Architecture rule + +The SQLite runtime must sit behind the existing application boundaries: + +```text +Flutter widgets/controllers + -> app composition/root runtime object + -> V1ApplicationCommandUseCases / GetTodayStateQuery + -> ApplicationUnitOfWork + -> SQLite application repositories / Drift database +``` + +The UI may request capture/schedule/done actions. The scheduler core remains the +source of truth for schedule state and task transitions. + +--- + +## Important implementation decision + +The current `scheduler_persistence_sqlite` adapter package implements +repository-conformance contracts, but the app command layer needs the richer +`scheduler_core` application repository surface. This plan therefore adds a +SQLite-backed `ApplicationUnitOfWork` adapter and the missing app-layer rows it +requires instead of wiring Flutter directly to low-level persistence +repositories. + +Recommended default: implement the application unit of work in +`packages/scheduler_persistence_sqlite` and expose it from `sqlite.dart`. + +Optional stricter boundary: create a small pure-Dart runtime package that owns +SQLite file opening and depends on `scheduler_persistence_sqlite`; Flutter then +imports only that runtime package. Use this option only if keeping all SQLite +package imports out of `apps/focus_flow_flutter/lib/` remains a hard boundary. diff --git a/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/README.md b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/README.md new file mode 100644 index 0000000..7113ecd --- /dev/null +++ b/Codex Documentation/Completed Plans/Persistence Plan 1 - SQLite Runtime Persistence/README.md @@ -0,0 +1,63 @@ + + + +# Persistence Plan 1 - SQLite Runtime Persistence + +**Status:** Complete on 2026-07-02. + +Start with `PERSISTENCE_PLAN_1_SUMMARY.md`, then execute the block files in +`PERSISTENCE_PLAN_1_EXECUTION_ORDER.md` order. + +This plan replaces the current seeded/in-memory Flutter runtime with a real +SQLite-backed runtime slice. The target user-visible proof is narrow: + +1. Start the desktop app. +2. Add/capture a task. +3. Optionally schedule it into Today. +4. Mark it done or not done. +5. Close the app. +6. Reopen the app against the same SQLite file. +7. See the task, placement, and completion state exactly as persisted. + +The plan does **not** redesign the UI, add sync, add week/month views, add +Shield/Recovery surfaces, or move scheduling rules into Flutter widgets. + +## Completion Handoff + +Changed: + +- Added Drift schema version 2 app-layer tables for application operations, + task activities, project statistics, and notice acknowledgements. +- Added `SqliteApplicationUnitOfWork` and `SqliteApplicationRuntime` in + `packages/scheduler_persistence_sqlite`. +- Switched normal Flutter startup to `PersistentSchedulerComposition`. +- Kept `DemoSchedulerComposition` for deterministic widget and visual tests. +- Updated boundary tests so only the persistent composition root can import the + SQLite adapter and `dart:io`. + +Now persists: + +- tasks captured through current command flows, +- planned schedule placement, +- completed and reopened/not-done state, +- owner settings and default Home project bootstrap, +- operation, activity, and project statistics records needed by current + commands. + +Validation recorded during implementation: + +- `cd packages/scheduler_persistence_sqlite && dart analyze`: passed. +- `cd packages/scheduler_persistence_sqlite && dart test`: passed. +- `cd apps/focus_flow_flutter && flutter pub get`: passed. +- `cd apps/focus_flow_flutter && flutter analyze`: passed. +- `cd apps/focus_flow_flutter && flutter test`: passed. + +Not in scope: + +- full task editor, +- visible Backlog navigation in the compact shell, +- backup/restore UI, +- export/import UI, +- sync/calendar/notifications, +- week/month views, +- Shield/Recovery UX. diff --git a/Codex Documentation/Completed Plans/README.md b/Codex Documentation/Completed Plans/README.md index e70f58e..3b8142d 100644 --- a/Codex Documentation/Completed Plans/README.md +++ b/Codex Documentation/Completed Plans/README.md @@ -12,6 +12,8 @@ Included documents cover: ADRs, public API baseline, and requirements traceability matrix. - UI Plan 1, the implemented compact Today timeline mockup plan, including its source mockups and completed block handoff notes. +- Persistence Plan 1, the implemented SQLite runtime persistence slice for + normal Flutter startup and close/reopen task lifecycle proof. Excluded documents include superseded originals, MongoDB-targeted persistence or runtime plans, planned/blocked UI handoff plans, old archive indexes, and review diff --git a/Codex Documentation/Current Software Plan/README.md b/Codex Documentation/Current Software Plan/README.md index 21ca233..174263c 100644 --- a/Codex Documentation/Current Software Plan/README.md +++ b/Codex Documentation/Current Software Plan/README.md @@ -1,7 +1,17 @@ + + + + # Current Software Plan -No active implementation plans are queued here after UI Plan 1 completed on -2026-06-30. +Ordered implementation queue: -Completed implementation plans live in `../Archived plans/`. Curated copies -that still match the app's current state live in `../Completed Plans/`. +1. `Date Selection 01 - Day Navigation and Date Picker/` +2. `Task Pushing 01 - Past Task Push Controls/` + +Execute only the first incomplete plan. Do not implement Task Pushing 01 until +Date Selection 01 is complete. + +Start each plan with its `*_SUMMARY.md`, then follow its `*_EXECUTION_ORDER.md`. +Completed implementation plans live in `../Archived plans/`. Curated copies that +still match the app's current state live in `../Completed Plans/`. diff --git a/README.md b/README.md index 59d2141..2eab03d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ dart run scripts/build.dart --platform current --output build/releases The dev script prints the SQLite path, starts `dart run build_runner watch`, and launches Flutter desktop with `SCHEDULER_SQLITE_PATH` passed as a dart define. +Normal Flutter startup uses that SQLite file for scheduler state rather than +inserting seeded demo tasks. Prerequisites: diff --git a/apps/focus_flow_flutter/README.md b/apps/focus_flow_flutter/README.md index 4d618a8..25397b3 100644 --- a/apps/focus_flow_flutter/README.md +++ b/apps/focus_flow_flutter/README.md @@ -7,18 +7,28 @@ Flutter desktop app target for the FocusFlow UI work. ## Current Scope -UI Plan 1 implements a compact Today timeline mockup and selected-task modal. -The app target already existed before UI Plan 1 Block 1, so this package keeps -the existing Flutter desktop scaffolding and local scheduler dependency. +The app launches into the compact Today timeline. Normal startup now opens an +on-disk SQLite database and bootstraps owner settings plus the default Home +project when they are missing. -The app launches into the compact Today screen. All visible controls are -intentionally no-op except selecting a task card and closing the modal. +The compact Today screen can toggle completion for currently rendered task +cards. Backlog widgets and quick-capture controls exist for tests and focused +composition paths, but the sidebar Backlog navigation remains out of scope for +this screen. -## Seed Data +## Runtime Data -Static demo data lives in `lib/app/demo_scheduler_composition.dart`. It creates -domain `ProjectProfile`, `OwnerSettings`, and `Task` records in an in-memory -application store, then reads `TodayState` through `GetTodayStateQuery`. +Normal runtime uses `lib/app/persistent_scheduler_composition.dart`, which reads +`SCHEDULER_SQLITE_PATH` from a Dart define. If no define is present, it uses the +local dev convention: + +```text +~/ADHD_Scheduler/scheduler.sqlite +``` + +Static demo data still lives in `lib/app/demo_scheduler_composition.dart`, but +it is for deterministic widget/visual tests and explicit demo composition only. +It is not inserted during normal startup. Widgets consume `TodayScreenData` from `lib/models/today_screen_models.dart`. Scheduler core remains the source of truth for task state and Today ordering. @@ -31,9 +41,12 @@ Allowed app imports include public scheduler APIs such as: import 'package:scheduler_core/scheduler_core.dart'; ``` -The app must not import scheduler `src/` files, SQLite/Drift adapters, -desktop notification implementations, backup/export packages, or direct OS APIs -for this UI slice. `test/forbidden_imports_test.dart` enforces that boundary. +The persistent composition root may import +`package:scheduler_persistence_sqlite/sqlite.dart` and `dart:io` for runtime +path resolution. Widgets/controllers must not import scheduler `src/` files, +Drift, SQLite adapters, desktop notification implementations, backup/export +packages, or direct OS APIs. `test/forbidden_imports_test.dart` enforces that +boundary. ## Intentionally No-op @@ -43,7 +56,28 @@ for this UI slice. `test/forbidden_imports_test.dart` enforces that boundary. - Settings button. - Show upcoming. - Timeline card action icons. -- Modal Done, Push, Move to Backlog, and Break up buttons. +- Modal Push, Move to Backlog, and Break up buttons. + +## Persistence Proof + +Current focused tests prove: + +- first run opens an empty SQLite file without seeded task rows, +- quick capture writes a backlog task, +- scheduling writes planned Today placement, +- done and not-done state persists, +- closing and reopening the same SQLite file reloads the task state. + +Manual verification: + +```sh +scripts/bootstrap_dev.sh +scripts/dev.sh --sqlite /tmp/focus_flow_persistence_plan_1.sqlite +``` + +Use the same `--sqlite` path after closing the app to confirm state persists. To +reset local dev data, close the app and delete the chosen `.sqlite` file. This is +only a dev reset; backup/restore UI remains out of scope. ## Commands @@ -57,8 +91,7 @@ Run these from `apps/focus_flow_flutter/`. ## Next Plans -- Wire functional Done, Push, Move to Backlog, and Break up actions. -- Add Backlog and quick-capture UI. -- Add persistent SQLite-backed runtime composition. +- Wire visible Backlog navigation and quick-capture UI into the main shell. +- Wire functional Push, Move to Backlog, and Break up actions. - Add real navigation/settings. - Add Shield/Recovery only in a later scope. diff --git a/apps/focus_flow_flutter/lib/app/demo/demo_date_formatter.dart b/apps/focus_flow_flutter/lib/app/demo/demo_date_formatter.dart index c78bdc1..11d77d3 100644 --- a/apps/focus_flow_flutter/lib/app/demo/demo_date_formatter.dart +++ b/apps/focus_flow_flutter/lib/app/demo/demo_date_formatter.dart @@ -4,7 +4,7 @@ part of '../demo_scheduler_composition.dart'; /// Formats a civil date as a long month/day/year label. -String _formatDateLabel(CivilDate date) { +String formatSchedulerDateLabel(CivilDate date) { const months = [ 'January', 'February', diff --git a/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart b/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart index a614426..0d3a169 100644 --- a/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart +++ b/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart @@ -9,12 +9,13 @@ import 'package:scheduler_core/scheduler_core.dart'; import '../controllers/scheduler_command_controller.dart'; import '../controllers/scheduler_read_controller.dart'; import '../controllers/today_screen_controller.dart'; +import 'scheduler_app_composition.dart'; part 'demo/demo_date_formatter.dart'; part 'demo/demo_seed_tasks.dart'; /// Wires the Flutter demo UI to in-memory scheduler application use cases. -class DemoSchedulerComposition { +class DemoSchedulerComposition implements SchedulerAppComposition { /// Creates a `DemoSchedulerComposition._` instance with the values required by its invariants. /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code. DemoSchedulerComposition._({ @@ -54,7 +55,12 @@ class DemoSchedulerComposition { final DateTime readAt; /// Human-readable label for [date]. - String get dateLabel => _formatDateLabel(date); + @override + String get dateLabel => formatSchedulerDateLabel(date); + + /// Default project id used by quick capture. + @override + String get defaultProjectId => projectId; /// Creates a deterministic seeded composition for the demo app and tests. factory DemoSchedulerComposition.seeded({ @@ -115,6 +121,7 @@ class DemoSchedulerComposition { } /// Creates the controller used by the compact Today screen mockup. + @override TodayScreenController createTodayScreenController() { return TodayScreenController( read: () { @@ -163,6 +170,7 @@ class DemoSchedulerComposition { } /// Creates the command controller used by interactive scheduler controls. + @override SchedulerCommandController createCommandController({ required ReadRefresh refreshReads, }) { @@ -171,6 +179,7 @@ class DemoSchedulerComposition { contextFor: context, localDate: date, refreshReads: refreshReads, + quickCaptureProjectId: defaultProjectId, ); } @@ -186,4 +195,8 @@ class DemoSchedulerComposition { idGenerator: SequentialIdGenerator(prefix: operationId), ); } + + /// Releases demo composition resources. + @override + Future dispose() async {} } diff --git a/apps/focus_flow_flutter/lib/app/focus_flow_app.dart b/apps/focus_flow_flutter/lib/app/focus_flow_app.dart index f9be60f..482df64 100644 --- a/apps/focus_flow_flutter/lib/app/focus_flow_app.dart +++ b/apps/focus_flow_flutter/lib/app/focus_flow_app.dart @@ -4,6 +4,8 @@ /// Composes the FocusFlow Flutter application around Focus Flow App. library; +import 'dart:async'; + import 'package:flutter/material.dart'; import '../controllers/scheduler_command_controller.dart'; @@ -17,7 +19,7 @@ import '../widgets/sidebar.dart'; import '../widgets/task_selection_modal.dart'; import '../widgets/timeline/timeline_view.dart'; import '../widgets/top_bar.dart'; -import 'demo_scheduler_composition.dart'; +import 'scheduler_app_composition.dart'; part 'home/focus_flow_home.dart'; part 'home/focus_flow_home_state.dart'; @@ -31,7 +33,7 @@ class FocusFlowApp extends StatelessWidget { const FocusFlowApp({required this.composition, super.key}); /// Factory and controller bundle used by the app tree. - final DemoSchedulerComposition composition; + final SchedulerAppComposition composition; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. diff --git a/apps/focus_flow_flutter/lib/app/home/focus_flow_home.dart b/apps/focus_flow_flutter/lib/app/home/focus_flow_home.dart index eb92ebc..87d92ec 100644 --- a/apps/focus_flow_flutter/lib/app/home/focus_flow_home.dart +++ b/apps/focus_flow_flutter/lib/app/home/focus_flow_home.dart @@ -9,7 +9,7 @@ class FocusFlowHome extends StatefulWidget { const FocusFlowHome({required this.composition, super.key}); /// Factory and controller bundle used by the home screen. - final DemoSchedulerComposition composition; + final SchedulerAppComposition composition; /// Creates the mutable state object used by this stateful widget. /// Flutter calls this once for each mounted widget instance before lifecycle callbacks and builds begin. diff --git a/apps/focus_flow_flutter/lib/app/home/focus_flow_home_state.dart b/apps/focus_flow_flutter/lib/app/home/focus_flow_home_state.dart index 11eaa7a..da785c0 100644 --- a/apps/focus_flow_flutter/lib/app/home/focus_flow_home_state.dart +++ b/apps/focus_flow_flutter/lib/app/home/focus_flow_home_state.dart @@ -32,6 +32,7 @@ class _FocusFlowHomeState extends State { void dispose() { commandController.dispose(); controller.dispose(); + unawaited(widget.composition.dispose()); super.dispose(); } diff --git a/apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart b/apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart new file mode 100644 index 0000000..857f81c --- /dev/null +++ b/apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +/// Composes the FocusFlow app around persistent SQLite scheduler state. +library; + +import 'dart:io'; + +import 'package:scheduler_core/scheduler_core.dart'; +import 'package:scheduler_persistence_sqlite/sqlite.dart'; + +import '../controllers/scheduler_command_controller.dart'; +import '../controllers/today_screen_controller.dart'; +import 'demo_scheduler_composition.dart' show formatSchedulerDateLabel; +import 'scheduler_app_composition.dart'; + +/// Persistent SQLite-backed scheduler composition for normal app runtime. +final class PersistentSchedulerComposition implements SchedulerAppComposition { + /// Creates a persistent composition from already-open collaborators. + PersistentSchedulerComposition._({ + required this.runtime, + required this.todayQuery, + required this.managementUseCases, + required this.commandUseCases, + required this.date, + required this.readAt, + }); + + /// Opens the configured SQLite runtime and bootstraps required owner state. + static Future open({ + String? sqlitePath, + Clock? clock, + CivilDate? selectedDate, + }) async { + final runtime = await SqliteApplicationRuntime.open( + path: sqlitePath ?? sqlitePathFromEnvironment(), + ); + final bootstrap = await runtime.bootstrap(); + if (bootstrap.isFailure) { + await runtime.close(); + throw StateError( + 'SQLite bootstrap failed: ${bootstrap.failure!.code.name}', + ); + } + final resolvedClock = clock ?? const SystemClock(); + final date = selectedDate ?? CivilDate.fromDateTime(resolvedClock.now()); + final readAt = resolvedClock.now().toUtc(); + + return PersistentSchedulerComposition._( + runtime: runtime, + todayQuery: GetTodayStateQuery( + applicationStore: runtime.applicationStore, + timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), + ), + managementUseCases: V1ApplicationManagementUseCases( + applicationStore: runtime.applicationStore, + ), + commandUseCases: V1ApplicationCommandUseCases( + applicationStore: runtime.applicationStore, + timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), + ), + date: date, + readAt: readAt, + ); + } + + /// Resolves the SQLite path from Dart defines or the local dev default. + static String sqlitePathFromEnvironment() { + const configured = String.fromEnvironment('SCHEDULER_SQLITE_PATH'); + final trimmed = configured.trim(); + if (trimmed.isNotEmpty) { + return trimmed; + } + final home = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (home == null || home.trim().isEmpty) { + return 'scheduler.sqlite'; + } + return '$home/ADHD_Scheduler/scheduler.sqlite'; + } + + /// SQLite runtime owning the database handle. + final SqliteApplicationRuntime runtime; + + /// Query object used to read the Today state. + final GetTodayStateQuery todayQuery; + + /// Management use cases used by backlog-facing UI surfaces. + final V1ApplicationManagementUseCases managementUseCases; + + /// Command use cases used by interactive UI controls. + final V1ApplicationCommandUseCases commandUseCases; + + /// Local date represented by the app. + final CivilDate date; + + /// Stable read clock instant for operation contexts. + final DateTime readAt; + + /// Pending close operation, if disposal has started. + Future? _disposeFuture; + + /// Default project id used by quick capture. + @override + String get defaultProjectId => SqliteApplicationRuntime.defaultV1ProjectId; + + /// Human-readable label for [date]. + @override + String get dateLabel => formatSchedulerDateLabel(date); + + /// Creates the controller used by the compact Today screen mockup. + @override + TodayScreenController createTodayScreenController() { + return TodayScreenController( + read: () { + return todayQuery.execute( + GetTodayStateRequest( + context: context('ui-plan-1-read-today'), + date: date, + includeNextFlexibleItem: true, + ), + ); + }, + ); + } + + /// Creates the command controller used by interactive scheduler controls. + @override + SchedulerCommandController createCommandController({ + required ReadRefresh refreshReads, + }) { + return SchedulerCommandController( + commands: commandUseCases, + contextFor: context, + localDate: date, + refreshReads: refreshReads, + quickCaptureProjectId: defaultProjectId, + ); + } + + /// Creates an application operation context for [operationId]. + ApplicationOperationContext context(String operationId, {DateTime? now}) { + return ApplicationOperationContext.start( + operationId: operationId, + ownerTimeZone: OwnerTimeZoneContext( + ownerId: SqliteApplicationRuntime.defaultV1OwnerId, + timeZoneId: SqliteApplicationRuntime.defaultV1TimeZoneId, + ), + clock: FixedClock(now ?? readAt), + idGenerator: SequentialIdGenerator(prefix: operationId), + ); + } + + /// Closes the persistent runtime. + @override + Future dispose() { + return _disposeFuture ??= runtime.close(); + } +} diff --git a/apps/focus_flow_flutter/lib/app/scheduler_app_composition.dart b/apps/focus_flow_flutter/lib/app/scheduler_app_composition.dart new file mode 100644 index 0000000..9fa817b --- /dev/null +++ b/apps/focus_flow_flutter/lib/app/scheduler_app_composition.dart @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +/// Defines the app composition protocol used by FocusFlow widgets. +library; + +import '../controllers/scheduler_command_controller.dart'; +import '../controllers/today_screen_controller.dart'; + +/// Runtime-independent composition surface consumed by the app shell. +abstract interface class SchedulerAppComposition { + /// Human-readable label for the selected local date. + String get dateLabel; + + /// Default project id used by quick-capture commands. + String get defaultProjectId; + + /// Creates the controller used by the compact Today screen. + TodayScreenController createTodayScreenController(); + + /// Creates the command controller used by interactive scheduler controls. + SchedulerCommandController createCommandController({ + required ReadRefresh refreshReads, + }); + + /// Releases resources owned by the composition. + Future dispose(); +} diff --git a/apps/focus_flow_flutter/lib/controllers/command/scheduler_command_controller_impl.dart b/apps/focus_flow_flutter/lib/controllers/command/scheduler_command_controller_impl.dart index 1d3d5fa..0fde5e6 100644 --- a/apps/focus_flow_flutter/lib/controllers/command/scheduler_command_controller_impl.dart +++ b/apps/focus_flow_flutter/lib/controllers/command/scheduler_command_controller_impl.dart @@ -11,6 +11,7 @@ class SchedulerCommandController extends ChangeNotifier { required this.contextFor, required this.localDate, required this.refreshReads, + required this.quickCaptureProjectId, DateTime Function()? now, }) : now = now ?? _utcNow; @@ -26,6 +27,9 @@ class SchedulerCommandController extends ChangeNotifier { /// Refresh callback invoked after commands that mutate read state. final ReadRefresh refreshReads; + /// Project id used by quick-capture commands. + final String quickCaptureProjectId; + /// Clock used by direct UI commands such as marking a task complete. final DateTime Function() now; @@ -50,7 +54,7 @@ class SchedulerCommandController extends ChangeNotifier { return commands.quickCaptureToBacklog( context: contextFor(operationId), title: title, - projectId: 'home', + projectId: quickCaptureProjectId, ); }, ); diff --git a/apps/focus_flow_flutter/lib/main.dart b/apps/focus_flow_flutter/lib/main.dart index 20c19ae..31f44a3 100644 --- a/apps/focus_flow_flutter/lib/main.dart +++ b/apps/focus_flow_flutter/lib/main.dart @@ -6,13 +6,47 @@ library; import 'package:flutter/material.dart'; -import 'app/demo_scheduler_composition.dart'; import 'app/focus_flow_app.dart'; +import 'app/persistent_scheduler_composition.dart'; export 'app/demo_scheduler_composition.dart'; export 'app/focus_flow_app.dart'; +export 'app/persistent_scheduler_composition.dart'; -/// Starts the FocusFlow app with the seeded demo scheduler composition. -void main() { - runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded())); +/// Starts the FocusFlow app with the persistent SQLite scheduler composition. +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + final composition = await PersistentSchedulerComposition.open(); + runApp(FocusFlowApp(composition: composition)); + } on Object catch (error) { + runApp(_StartupFailureApp(message: error.toString())); + } +} + +/// Minimal failure app shown when persistent startup cannot finish. +class _StartupFailureApp extends StatelessWidget { + /// Creates a startup failure app with [message]. + const _StartupFailureApp({required this.message}); + + /// Diagnostic startup failure message. + final String message; + + /// Builds the widget subtree for this component. + /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'FocusFlow', + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text('Unable to open local schedule data.\n$message'), + ), + ), + ), + ); + } } diff --git a/apps/focus_flow_flutter/pubspec.lock b/apps/focus_flow_flutter/pubspec.lock index 703a56b..593d5be 100644 --- a/apps/focus_flow_flutter/pubspec.lock +++ b/apps/focus_flow_flutter/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" collection: dependency: transitive description: @@ -41,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -49,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + drift: + dependency: transitive + description: + name: drift + sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c" + url: "https://pub.dev" + source: hosted + version: "2.34.0" fake_async: dependency: transitive description: @@ -57,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +123,22 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" leak_tracker: dependency: transitive description: @@ -107,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -131,6 +203,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72 + url: "https://pub.dev" + source: hosted + version: "0.19.2" path: dependency: transitive description: @@ -139,6 +219,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" scheduler_core: dependency: "direct main" description: @@ -146,6 +242,20 @@ packages: relative: true source: path version: "0.1.0" + scheduler_persistence: + dependency: "direct overridden" + description: + path: "../../packages/scheduler_persistence" + relative: true + source: path + version: "0.1.0" + scheduler_persistence_sqlite: + dependency: "direct main" + description: + path: "../../packages/scheduler_persistence_sqlite" + relative: true + source: path + version: "0.1.0" sky_engine: dependency: transitive description: flutter @@ -159,6 +269,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "752d9d746052359a2022f588bb979f2e7c4e0f9e4b6a1c3121f7626a1574974b" + url: "https://pub.dev" + source: hosted + version: "3.3.4" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad + url: "https://pub.dev" + source: hosted + version: "0.5.42" stack_trace: dependency: transitive description: @@ -199,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -215,6 +349,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/apps/focus_flow_flutter/pubspec.yaml b/apps/focus_flow_flutter/pubspec.yaml index 6e6cf68..f3c070b 100644 --- a/apps/focus_flow_flutter/pubspec.yaml +++ b/apps/focus_flow_flutter/pubspec.yaml @@ -14,6 +14,9 @@ dependencies: sdk: flutter scheduler_core: path: ../../packages/scheduler_core + scheduler_persistence_sqlite: + path: ../../packages/scheduler_persistence_sqlite + sqlite3_flutter_libs: ^0.5.24 cupertino_icons: ^1.0.8 dev_dependencies: @@ -21,5 +24,11 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 +dependency_overrides: + scheduler_core: + path: ../../packages/scheduler_core + scheduler_persistence: + path: ../../packages/scheduler_persistence + flutter: uses-material-design: true diff --git a/apps/focus_flow_flutter/test/forbidden_imports_test.dart b/apps/focus_flow_flutter/test/forbidden_imports_test.dart index 02ead75..3815853 100644 --- a/apps/focus_flow_flutter/test/forbidden_imports_test.dart +++ b/apps/focus_flow_flutter/test/forbidden_imports_test.dart @@ -10,30 +10,33 @@ import 'package:flutter_test/flutter_test.dart'; /// Runs app package-boundary tests. void main() { - test('Flutter UI imports stay inside the UI Plan 1 package boundary', () { - final appRoot = Directory.current; - final dartFiles = [ - ..._dartFiles(Directory('${appRoot.path}/lib')), - ..._dartFiles(Directory('${appRoot.path}/test')), - ]; + test( + 'Flutter UI imports stay inside the SQLite runtime package boundary', + () { + final appRoot = Directory.current; + final dartFiles = [ + ..._dartFiles(Directory('${appRoot.path}/lib')), + ..._dartFiles(Directory('${appRoot.path}/test')), + ]; - final violations = []; - for (final file in dartFiles) { - final relativePath = file.path.substring(appRoot.path.length + 1); - final contents = file.readAsStringSync(); - for (final forbidden in _forbiddenImportsFor(relativePath)) { - if (forbidden.matches(contents)) { - violations.add('$relativePath imports ${forbidden.label}'); + final violations = []; + for (final file in dartFiles) { + final relativePath = file.path.substring(appRoot.path.length + 1); + final contents = file.readAsStringSync(); + for (final forbidden in _forbiddenImportsFor(relativePath)) { + if (forbidden.matches(contents)) { + violations.add('$relativePath imports ${forbidden.label}'); + } } } - } - expect( - violations, - isEmpty, - reason: 'Forbidden imports found:\n${violations.join('\n')}', - ); - }); + expect( + violations, + isEmpty, + reason: 'Forbidden imports found:\n${violations.join('\n')}', + ); + }, + ); } /// Top-level helper that performs the `_dartFiles` operation for this file. @@ -56,10 +59,11 @@ List<_ForbiddenImport> _forbiddenImportsFor(String relativePath) { "package:scheduler_core/src/", 'scheduler_core internals', ), - const _ForbiddenImport( - "package:scheduler_persistence_sqlite/", - 'SQLite persistence adapter', - ), + if (!_isPersistentComposition(relativePath)) + const _ForbiddenImport( + "package:scheduler_persistence_sqlite/", + 'SQLite persistence adapter', + ), const _ForbiddenImport("package:drift/", 'Drift'), const _ForbiddenImport( "package:scheduler_notifications_desktop/", @@ -71,11 +75,18 @@ List<_ForbiddenImport> _forbiddenImportsFor(String relativePath) { "package:scheduler_export_json/", 'JSON export package', ), - if (relativePath != 'test/forbidden_imports_test.dart') + if (relativePath != 'test/forbidden_imports_test.dart' && + relativePath != 'test/persistent_composition_test.dart' && + !_isPersistentComposition(relativePath)) const _ForbiddenImport("dart:io", 'dart:io'), ]; } +/// Whether [relativePath] is the approved persistent runtime composition root. +bool _isPersistentComposition(String relativePath) { + return relativePath == 'lib/app/persistent_scheduler_composition.dart'; +} + /// Private implementation type for `_ForbiddenImport` in this library. /// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly. class _ForbiddenImport { diff --git a/apps/focus_flow_flutter/test/persistent_composition_test.dart b/apps/focus_flow_flutter/test/persistent_composition_test.dart new file mode 100644 index 0000000..dacf0bf --- /dev/null +++ b/apps/focus_flow_flutter/test/persistent_composition_test.dart @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +/// Tests persistent SQLite composition behavior in the Flutter app. +library; + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:scheduler_core/scheduler_core.dart'; + +import 'package:focus_flow_flutter/app/focus_flow_app.dart'; +import 'package:focus_flow_flutter/app/persistent_scheduler_composition.dart'; +import 'package:focus_flow_flutter/controllers/scheduler_command_controller.dart'; + +/// Runs persistent composition smoke tests. +void main() { + testWidgets( + 'persistent app starts without seeded demo tasks and reopens capture', + (tester) async { + final path = await _runAsync(tester, _tempDatabasePath); + final composition = await _runAsync( + tester, + () => PersistentSchedulerComposition.open( + sqlitePath: path, + selectedDate: _date, + clock: FixedClock(_instant(8)), + ), + ); + + await tester.pumpWidget(FocusFlowApp(composition: composition)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('January 2, 2026'), findsOneWidget); + expect(find.text('Clean coffee maker'), findsNothing); + expect(find.text('Water plants'), findsNothing); + + final commandController = composition.createCommandController( + refreshReads: () async {}, + ); + addTearDown(commandController.dispose); + await tester.runAsync( + () => commandController.quickCaptureToBacklog( + 'Captured from Flutter test', + ), + ); + expect(commandController.state, isA()); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + await tester.runAsync(composition.dispose); + + final reopened = await _runAsync( + tester, + () => PersistentSchedulerComposition.open( + sqlitePath: path, + selectedDate: _date, + clock: FixedClock(_instant(8, 10)), + ), + ); + addTearDown(() => tester.runAsync(reopened.dispose)); + final backlog = await _runAsync( + tester, + () => reopened.managementUseCases.getBacklog( + GetBacklogRequest( + context: reopened.context('flutter-read-backlog'), + sortKey: BacklogSortKey.age, + ), + ), + ); + + expect(backlog.requireValue.items, hasLength(1)); + expect( + backlog.requireValue.items.single.task.title, + 'Captured from Flutter test', + ); + }, + ); +} + +/// Fixed date used by persistent Flutter tests. +final _date = CivilDate(2026, 1, 2); + +/// Returns an instant on the fixed lifecycle date. +DateTime _instant(int hour, [int minute = 0]) { + return DateTime.utc(_date.year, _date.month, _date.day, hour, minute); +} + +/// Returns a fresh temporary SQLite path. +Future _tempDatabasePath() async { + final directory = await Directory.systemTemp.createTemp( + 'focus-flow-flutter-persistence-', + ); + addTearDown(() => directory.delete(recursive: true)); + return '${directory.path}/scheduler.sqlite'; +} + +/// Runs real async work outside the widget-test fake async zone. +Future _runAsync( + WidgetTester tester, + Future Function() callback, +) async { + final result = await tester.runAsync(callback); + if (result == null) { + throw StateError('runAsync returned null.'); + } + return result; +} diff --git a/apps/focus_flow_flutter/windows/flutter/generated_plugin_registrant.cc b/apps/focus_flow_flutter/windows/flutter/generated_plugin_registrant.cc index 8b6d468..988f3c8 100644 --- a/apps/focus_flow_flutter/windows/flutter/generated_plugin_registrant.cc +++ b/apps/focus_flow_flutter/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); } diff --git a/apps/focus_flow_flutter/windows/flutter/generated_plugins.cmake b/apps/focus_flow_flutter/windows/flutter/generated_plugins.cmake index b93c4c3..8abff95 100644 --- a/apps/focus_flow_flutter/windows/flutter/generated_plugins.cmake +++ b/apps/focus_flow_flutter/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + sqlite3_flutter_libs ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.dart index 66b8cd7..3bbe7eb 100644 --- a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.dart +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.dart @@ -11,11 +11,15 @@ import 'package:drift/drift.dart'; part 'scheduler_db.g.dart'; part 'scheduler_db/tables/tasks/tasks.dart'; +part 'scheduler_db/tables/tasks/task_activities.dart'; part 'scheduler_db/tables/projects/projects.dart'; +part 'scheduler_db/tables/projects/project_statistics.dart'; part 'scheduler_db/tables/locked_time/locked_blocks.dart'; part 'scheduler_db/tables/locked_time/locked_overrides.dart'; part 'scheduler_db/tables/settings/settings_table.dart'; part 'scheduler_db/tables/snapshots/snapshots.dart'; +part 'scheduler_db/tables/application/application_operations.dart'; +part 'scheduler_db/tables/application/notice_acknowledgements.dart'; part 'scheduler_db/daos/task_dao.dart'; part 'scheduler_db/daos/project_dao.dart'; part 'scheduler_db/daos/locked_block_dao.dart'; diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.g.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.g.dart index b597c1f..81028f6 100644 --- a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.g.dart +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db.g.dart @@ -465,29 +465,100 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, TaskRow> { } class TaskRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `title` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String title; + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String projectId; + + /// Returns the derived `parentId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? parentId; + + /// Returns the derived `type` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String type; + + /// Returns the derived `status` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String status; + + /// Returns the derived `priority` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? priority; + + /// Returns the derived `reward` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String reward; + + /// Returns the derived `difficulty` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String difficulty; + + /// Returns the derived `durationMinutes` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int? durationMinutes; + + /// Returns the derived `scheduledStartUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? scheduledStartUtc; + + /// Returns the derived `scheduledEndUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? scheduledEndUtc; + + /// Returns the derived `actualStartUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? actualStartUtc; + + /// Returns the derived `actualEndUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? actualEndUtc; + + /// Returns the derived `completedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? completedAtUtc; + + /// Converts scheduler data for `backlogTagsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String backlogTagsJson; + + /// Returns the derived `reminderOverride` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? reminderOverride; + + /// Converts scheduler data for `statsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String statsJson; + + /// Returns the derived `backlogEnteredAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? backlogEnteredAtUtc; + + /// Returns the derived `backlogEnteredProvenance` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? backlogEnteredProvenance; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const TaskRow( {required this.id, @@ -1213,6 +1284,467 @@ class TasksCompanion extends UpdateCompanion { } } +class $TaskActivitiesTable extends TaskActivities + with TableInfo<$TaskActivitiesTable, TaskActivityRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TaskActivitiesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _ownerIdMeta = + const VerificationMeta('ownerId'); + @override + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _taskIdMeta = const VerificationMeta('taskId'); + @override + late final GeneratedColumn taskId = GeneratedColumn( + 'task_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _projectIdMeta = + const VerificationMeta('projectId'); + @override + late final GeneratedColumn projectId = GeneratedColumn( + 'project_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _operationIdMeta = + const VerificationMeta('operationId'); + @override + late final GeneratedColumn operationId = GeneratedColumn( + 'operation_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _codeMeta = const VerificationMeta('code'); + @override + late final GeneratedColumn code = GeneratedColumn( + 'code', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _occurredAtUtcMeta = + const VerificationMeta('occurredAtUtc'); + @override + late final GeneratedColumn occurredAtUtc = + GeneratedColumn('occurred_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + static const VerificationMeta _metadataJsonMeta = + const VerificationMeta('metadataJson'); + @override + late final GeneratedColumn metadataJson = GeneratedColumn( + 'metadata_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [ + id, + ownerId, + taskId, + projectId, + operationId, + code, + occurredAtUtc, + metadataJson + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'task_activities'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('task_id')) { + context.handle(_taskIdMeta, + taskId.isAcceptableOrUnknown(data['task_id']!, _taskIdMeta)); + } else if (isInserting) { + context.missing(_taskIdMeta); + } + if (data.containsKey('project_id')) { + context.handle(_projectIdMeta, + projectId.isAcceptableOrUnknown(data['project_id']!, _projectIdMeta)); + } else if (isInserting) { + context.missing(_projectIdMeta); + } + if (data.containsKey('operation_id')) { + context.handle( + _operationIdMeta, + operationId.isAcceptableOrUnknown( + data['operation_id']!, _operationIdMeta)); + } else if (isInserting) { + context.missing(_operationIdMeta); + } + if (data.containsKey('code')) { + context.handle( + _codeMeta, code.isAcceptableOrUnknown(data['code']!, _codeMeta)); + } else if (isInserting) { + context.missing(_codeMeta); + } + if (data.containsKey('occurred_at_utc')) { + context.handle( + _occurredAtUtcMeta, + occurredAtUtc.isAcceptableOrUnknown( + data['occurred_at_utc']!, _occurredAtUtcMeta)); + } else if (isInserting) { + context.missing(_occurredAtUtcMeta); + } + if (data.containsKey('metadata_json')) { + context.handle( + _metadataJsonMeta, + metadataJson.isAcceptableOrUnknown( + data['metadata_json']!, _metadataJsonMeta)); + } else if (isInserting) { + context.missing(_metadataJsonMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TaskActivityRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TaskActivityRow( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + taskId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}task_id'])!, + projectId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}project_id'])!, + operationId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}operation_id'])!, + code: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}code'])!, + occurredAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}occurred_at_utc'])!, + metadataJson: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}metadata_json'])!, + ); + } + + @override + $TaskActivitiesTable createAlias(String alias) { + return $TaskActivitiesTable(attachedDatabase, alias); + } +} + +class TaskActivityRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String ownerId; + + /// Returns the derived `taskId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String taskId; + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String projectId; + + /// Returns the derived `operationId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String operationId; + + /// Returns the derived `code` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String code; + + /// Returns the derived `occurredAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime occurredAtUtc; + + /// Converts scheduler data for `metadataJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String metadataJson; + const TaskActivityRow( + {required this.id, + required this.ownerId, + required this.taskId, + required this.projectId, + required this.operationId, + required this.code, + required this.occurredAtUtc, + required this.metadataJson}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['owner_id'] = Variable(ownerId); + map['task_id'] = Variable(taskId); + map['project_id'] = Variable(projectId); + map['operation_id'] = Variable(operationId); + map['code'] = Variable(code); + map['occurred_at_utc'] = Variable(occurredAtUtc); + map['metadata_json'] = Variable(metadataJson); + return map; + } + + TaskActivitiesCompanion toCompanion(bool nullToAbsent) { + return TaskActivitiesCompanion( + id: Value(id), + ownerId: Value(ownerId), + taskId: Value(taskId), + projectId: Value(projectId), + operationId: Value(operationId), + code: Value(code), + occurredAtUtc: Value(occurredAtUtc), + metadataJson: Value(metadataJson), + ); + } + + factory TaskActivityRow.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TaskActivityRow( + id: serializer.fromJson(json['id']), + ownerId: serializer.fromJson(json['ownerId']), + taskId: serializer.fromJson(json['taskId']), + projectId: serializer.fromJson(json['projectId']), + operationId: serializer.fromJson(json['operationId']), + code: serializer.fromJson(json['code']), + occurredAtUtc: serializer.fromJson(json['occurredAtUtc']), + metadataJson: serializer.fromJson(json['metadataJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'ownerId': serializer.toJson(ownerId), + 'taskId': serializer.toJson(taskId), + 'projectId': serializer.toJson(projectId), + 'operationId': serializer.toJson(operationId), + 'code': serializer.toJson(code), + 'occurredAtUtc': serializer.toJson(occurredAtUtc), + 'metadataJson': serializer.toJson(metadataJson), + }; + } + + TaskActivityRow copyWith( + {String? id, + String? ownerId, + String? taskId, + String? projectId, + String? operationId, + String? code, + DateTime? occurredAtUtc, + String? metadataJson}) => + TaskActivityRow( + id: id ?? this.id, + ownerId: ownerId ?? this.ownerId, + taskId: taskId ?? this.taskId, + projectId: projectId ?? this.projectId, + operationId: operationId ?? this.operationId, + code: code ?? this.code, + occurredAtUtc: occurredAtUtc ?? this.occurredAtUtc, + metadataJson: metadataJson ?? this.metadataJson, + ); + TaskActivityRow copyWithCompanion(TaskActivitiesCompanion data) { + return TaskActivityRow( + id: data.id.present ? data.id.value : this.id, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + taskId: data.taskId.present ? data.taskId.value : this.taskId, + projectId: data.projectId.present ? data.projectId.value : this.projectId, + operationId: + data.operationId.present ? data.operationId.value : this.operationId, + code: data.code.present ? data.code.value : this.code, + occurredAtUtc: data.occurredAtUtc.present + ? data.occurredAtUtc.value + : this.occurredAtUtc, + metadataJson: data.metadataJson.present + ? data.metadataJson.value + : this.metadataJson, + ); + } + + @override + String toString() { + return (StringBuffer('TaskActivityRow(') + ..write('id: $id, ') + ..write('ownerId: $ownerId, ') + ..write('taskId: $taskId, ') + ..write('projectId: $projectId, ') + ..write('operationId: $operationId, ') + ..write('code: $code, ') + ..write('occurredAtUtc: $occurredAtUtc, ') + ..write('metadataJson: $metadataJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, ownerId, taskId, projectId, operationId, + code, occurredAtUtc, metadataJson); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TaskActivityRow && + other.id == this.id && + other.ownerId == this.ownerId && + other.taskId == this.taskId && + other.projectId == this.projectId && + other.operationId == this.operationId && + other.code == this.code && + other.occurredAtUtc == this.occurredAtUtc && + other.metadataJson == this.metadataJson); +} + +class TaskActivitiesCompanion extends UpdateCompanion { + final Value id; + final Value ownerId; + final Value taskId; + final Value projectId; + final Value operationId; + final Value code; + final Value occurredAtUtc; + final Value metadataJson; + final Value rowid; + const TaskActivitiesCompanion({ + this.id = const Value.absent(), + this.ownerId = const Value.absent(), + this.taskId = const Value.absent(), + this.projectId = const Value.absent(), + this.operationId = const Value.absent(), + this.code = const Value.absent(), + this.occurredAtUtc = const Value.absent(), + this.metadataJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + TaskActivitiesCompanion.insert({ + required String id, + required String ownerId, + required String taskId, + required String projectId, + required String operationId, + required String code, + required DateTime occurredAtUtc, + required String metadataJson, + this.rowid = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + taskId = Value(taskId), + projectId = Value(projectId), + operationId = Value(operationId), + code = Value(code), + occurredAtUtc = Value(occurredAtUtc), + metadataJson = Value(metadataJson); + static Insertable custom({ + Expression? id, + Expression? ownerId, + Expression? taskId, + Expression? projectId, + Expression? operationId, + Expression? code, + Expression? occurredAtUtc, + Expression? metadataJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (ownerId != null) 'owner_id': ownerId, + if (taskId != null) 'task_id': taskId, + if (projectId != null) 'project_id': projectId, + if (operationId != null) 'operation_id': operationId, + if (code != null) 'code': code, + if (occurredAtUtc != null) 'occurred_at_utc': occurredAtUtc, + if (metadataJson != null) 'metadata_json': metadataJson, + if (rowid != null) 'rowid': rowid, + }); + } + + TaskActivitiesCompanion copyWith( + {Value? id, + Value? ownerId, + Value? taskId, + Value? projectId, + Value? operationId, + Value? code, + Value? occurredAtUtc, + Value? metadataJson, + Value? rowid}) { + return TaskActivitiesCompanion( + id: id ?? this.id, + ownerId: ownerId ?? this.ownerId, + taskId: taskId ?? this.taskId, + projectId: projectId ?? this.projectId, + operationId: operationId ?? this.operationId, + code: code ?? this.code, + occurredAtUtc: occurredAtUtc ?? this.occurredAtUtc, + metadataJson: metadataJson ?? this.metadataJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (taskId.present) { + map['task_id'] = Variable(taskId.value); + } + if (projectId.present) { + map['project_id'] = Variable(projectId.value); + } + if (operationId.present) { + map['operation_id'] = Variable(operationId.value); + } + if (code.present) { + map['code'] = Variable(code.value); + } + if (occurredAtUtc.present) { + map['occurred_at_utc'] = Variable(occurredAtUtc.value); + } + if (metadataJson.present) { + map['metadata_json'] = Variable(metadataJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TaskActivitiesCompanion(') + ..write('id: $id, ') + ..write('ownerId: $ownerId, ') + ..write('taskId: $taskId, ') + ..write('projectId: $projectId, ') + ..write('operationId: $operationId, ') + ..write('code: $code, ') + ..write('occurredAtUtc: $occurredAtUtc, ') + ..write('metadataJson: $metadataJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $ProjectsTable extends Projects with TableInfo<$ProjectsTable, ProjectRow> { @override @@ -1456,18 +1988,56 @@ class $ProjectsTable extends Projects } class ProjectRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `name` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String name; + + /// Returns the derived `colorKey` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String colorKey; + + /// Returns the derived `defaultPriority` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String defaultPriority; + + /// Returns the derived `defaultReward` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String defaultReward; + + /// Returns the derived `defaultDifficulty` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String defaultDifficulty; + + /// Returns the derived `defaultReminderProfile` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String defaultReminderProfile; + + /// Returns the derived `defaultDurationMinutes` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int? defaultDurationMinutes; + + /// Returns the derived `archivedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? archivedAtUtc; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const ProjectRow( {required this.id, @@ -1891,6 +2461,776 @@ class ProjectsCompanion extends UpdateCompanion { } } +class $ProjectStatisticsTableTable extends ProjectStatisticsTable + with TableInfo<$ProjectStatisticsTableTable, ProjectStatisticsRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ProjectStatisticsTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _projectIdMeta = + const VerificationMeta('projectId'); + @override + late final GeneratedColumn projectId = GeneratedColumn( + 'project_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _ownerIdMeta = + const VerificationMeta('ownerId'); + @override + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _completedTaskCountMeta = + const VerificationMeta('completedTaskCount'); + @override + late final GeneratedColumn completedTaskCount = GeneratedColumn( + 'completed_task_count', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _durationMinuteCountsJsonMeta = + const VerificationMeta('durationMinuteCountsJson'); + @override + late final GeneratedColumn durationMinuteCountsJson = + GeneratedColumn('duration_minute_counts_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _completionTimeBucketCountsJsonMeta = + const VerificationMeta('completionTimeBucketCountsJson'); + @override + late final GeneratedColumn completionTimeBucketCountsJson = + GeneratedColumn( + 'completion_time_bucket_counts_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _totalPushesBeforeCompletionMeta = + const VerificationMeta('totalPushesBeforeCompletion'); + @override + late final GeneratedColumn totalPushesBeforeCompletion = + GeneratedColumn('total_pushes_before_completion', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _completedAfterPushCountMeta = + const VerificationMeta('completedAfterPushCount'); + @override + late final GeneratedColumn completedAfterPushCount = + GeneratedColumn('completed_after_push_count', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _rewardCountsJsonMeta = + const VerificationMeta('rewardCountsJson'); + @override + late final GeneratedColumn rewardCountsJson = GeneratedColumn( + 'reward_counts_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _difficultyCountsJsonMeta = + const VerificationMeta('difficultyCountsJson'); + @override + late final GeneratedColumn difficultyCountsJson = + GeneratedColumn('difficulty_counts_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _reminderProfileCountsJsonMeta = + const VerificationMeta('reminderProfileCountsJson'); + @override + late final GeneratedColumn reminderProfileCountsJson = + GeneratedColumn( + 'reminder_profile_counts_json', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _revisionMeta = + const VerificationMeta('revision'); + @override + late final GeneratedColumn revision = GeneratedColumn( + 'revision', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _createdAtUtcMeta = + const VerificationMeta('createdAtUtc'); + @override + late final GeneratedColumn createdAtUtc = GeneratedColumn( + 'created_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + static const VerificationMeta _updatedAtUtcMeta = + const VerificationMeta('updatedAtUtc'); + @override + late final GeneratedColumn updatedAtUtc = GeneratedColumn( + 'updated_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [ + projectId, + ownerId, + completedTaskCount, + durationMinuteCountsJson, + completionTimeBucketCountsJson, + totalPushesBeforeCompletion, + completedAfterPushCount, + rewardCountsJson, + difficultyCountsJson, + reminderProfileCountsJson, + revision, + createdAtUtc, + updatedAtUtc + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'project_statistics'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('project_id')) { + context.handle(_projectIdMeta, + projectId.isAcceptableOrUnknown(data['project_id']!, _projectIdMeta)); + } else if (isInserting) { + context.missing(_projectIdMeta); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('completed_task_count')) { + context.handle( + _completedTaskCountMeta, + completedTaskCount.isAcceptableOrUnknown( + data['completed_task_count']!, _completedTaskCountMeta)); + } else if (isInserting) { + context.missing(_completedTaskCountMeta); + } + if (data.containsKey('duration_minute_counts_json')) { + context.handle( + _durationMinuteCountsJsonMeta, + durationMinuteCountsJson.isAcceptableOrUnknown( + data['duration_minute_counts_json']!, + _durationMinuteCountsJsonMeta)); + } else if (isInserting) { + context.missing(_durationMinuteCountsJsonMeta); + } + if (data.containsKey('completion_time_bucket_counts_json')) { + context.handle( + _completionTimeBucketCountsJsonMeta, + completionTimeBucketCountsJson.isAcceptableOrUnknown( + data['completion_time_bucket_counts_json']!, + _completionTimeBucketCountsJsonMeta)); + } else if (isInserting) { + context.missing(_completionTimeBucketCountsJsonMeta); + } + if (data.containsKey('total_pushes_before_completion')) { + context.handle( + _totalPushesBeforeCompletionMeta, + totalPushesBeforeCompletion.isAcceptableOrUnknown( + data['total_pushes_before_completion']!, + _totalPushesBeforeCompletionMeta)); + } else if (isInserting) { + context.missing(_totalPushesBeforeCompletionMeta); + } + if (data.containsKey('completed_after_push_count')) { + context.handle( + _completedAfterPushCountMeta, + completedAfterPushCount.isAcceptableOrUnknown( + data['completed_after_push_count']!, + _completedAfterPushCountMeta)); + } else if (isInserting) { + context.missing(_completedAfterPushCountMeta); + } + if (data.containsKey('reward_counts_json')) { + context.handle( + _rewardCountsJsonMeta, + rewardCountsJson.isAcceptableOrUnknown( + data['reward_counts_json']!, _rewardCountsJsonMeta)); + } else if (isInserting) { + context.missing(_rewardCountsJsonMeta); + } + if (data.containsKey('difficulty_counts_json')) { + context.handle( + _difficultyCountsJsonMeta, + difficultyCountsJson.isAcceptableOrUnknown( + data['difficulty_counts_json']!, _difficultyCountsJsonMeta)); + } else if (isInserting) { + context.missing(_difficultyCountsJsonMeta); + } + if (data.containsKey('reminder_profile_counts_json')) { + context.handle( + _reminderProfileCountsJsonMeta, + reminderProfileCountsJson.isAcceptableOrUnknown( + data['reminder_profile_counts_json']!, + _reminderProfileCountsJsonMeta)); + } else if (isInserting) { + context.missing(_reminderProfileCountsJsonMeta); + } + if (data.containsKey('revision')) { + context.handle(_revisionMeta, + revision.isAcceptableOrUnknown(data['revision']!, _revisionMeta)); + } else if (isInserting) { + context.missing(_revisionMeta); + } + if (data.containsKey('created_at_utc')) { + context.handle( + _createdAtUtcMeta, + createdAtUtc.isAcceptableOrUnknown( + data['created_at_utc']!, _createdAtUtcMeta)); + } else if (isInserting) { + context.missing(_createdAtUtcMeta); + } + if (data.containsKey('updated_at_utc')) { + context.handle( + _updatedAtUtcMeta, + updatedAtUtc.isAcceptableOrUnknown( + data['updated_at_utc']!, _updatedAtUtcMeta)); + } else if (isInserting) { + context.missing(_updatedAtUtcMeta); + } + return context; + } + + @override + Set get $primaryKey => {projectId}; + @override + ProjectStatisticsRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ProjectStatisticsRow( + projectId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}project_id'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + completedTaskCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}completed_task_count'])!, + durationMinuteCountsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}duration_minute_counts_json'])!, + completionTimeBucketCountsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}completion_time_bucket_counts_json'])!, + totalPushesBeforeCompletion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_pushes_before_completion'])!, + completedAfterPushCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}completed_after_push_count'])!, + rewardCountsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}reward_counts_json'])!, + difficultyCountsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}difficulty_counts_json'])!, + reminderProfileCountsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reminder_profile_counts_json'])!, + revision: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}revision'])!, + createdAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}created_at_utc'])!, + updatedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}updated_at_utc'])!, + ); + } + + @override + $ProjectStatisticsTableTable createAlias(String alias) { + return $ProjectStatisticsTableTable(attachedDatabase, alias); + } +} + +class ProjectStatisticsRow extends DataClass + implements Insertable { + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String projectId; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String ownerId; + + /// Returns the derived `completedTaskCount` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final int completedTaskCount; + + /// Converts scheduler data for `durationMinuteCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String durationMinuteCountsJson; + + /// Converts scheduler data for `completionTimeBucketCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String completionTimeBucketCountsJson; + + /// Returns the derived `totalPushesBeforeCompletion` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final int totalPushesBeforeCompletion; + + /// Returns the derived `completedAfterPushCount` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final int completedAfterPushCount; + + /// Converts scheduler data for `rewardCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String rewardCountsJson; + + /// Converts scheduler data for `difficultyCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String difficultyCountsJson; + + /// Converts scheduler data for `reminderProfileCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + final String reminderProfileCountsJson; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime updatedAtUtc; + const ProjectStatisticsRow( + {required this.projectId, + required this.ownerId, + required this.completedTaskCount, + required this.durationMinuteCountsJson, + required this.completionTimeBucketCountsJson, + required this.totalPushesBeforeCompletion, + required this.completedAfterPushCount, + required this.rewardCountsJson, + required this.difficultyCountsJson, + required this.reminderProfileCountsJson, + required this.revision, + required this.createdAtUtc, + required this.updatedAtUtc}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['project_id'] = Variable(projectId); + map['owner_id'] = Variable(ownerId); + map['completed_task_count'] = Variable(completedTaskCount); + map['duration_minute_counts_json'] = + Variable(durationMinuteCountsJson); + map['completion_time_bucket_counts_json'] = + Variable(completionTimeBucketCountsJson); + map['total_pushes_before_completion'] = + Variable(totalPushesBeforeCompletion); + map['completed_after_push_count'] = Variable(completedAfterPushCount); + map['reward_counts_json'] = Variable(rewardCountsJson); + map['difficulty_counts_json'] = Variable(difficultyCountsJson); + map['reminder_profile_counts_json'] = + Variable(reminderProfileCountsJson); + map['revision'] = Variable(revision); + map['created_at_utc'] = Variable(createdAtUtc); + map['updated_at_utc'] = Variable(updatedAtUtc); + return map; + } + + ProjectStatisticsTableCompanion toCompanion(bool nullToAbsent) { + return ProjectStatisticsTableCompanion( + projectId: Value(projectId), + ownerId: Value(ownerId), + completedTaskCount: Value(completedTaskCount), + durationMinuteCountsJson: Value(durationMinuteCountsJson), + completionTimeBucketCountsJson: Value(completionTimeBucketCountsJson), + totalPushesBeforeCompletion: Value(totalPushesBeforeCompletion), + completedAfterPushCount: Value(completedAfterPushCount), + rewardCountsJson: Value(rewardCountsJson), + difficultyCountsJson: Value(difficultyCountsJson), + reminderProfileCountsJson: Value(reminderProfileCountsJson), + revision: Value(revision), + createdAtUtc: Value(createdAtUtc), + updatedAtUtc: Value(updatedAtUtc), + ); + } + + factory ProjectStatisticsRow.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ProjectStatisticsRow( + projectId: serializer.fromJson(json['projectId']), + ownerId: serializer.fromJson(json['ownerId']), + completedTaskCount: serializer.fromJson(json['completedTaskCount']), + durationMinuteCountsJson: + serializer.fromJson(json['durationMinuteCountsJson']), + completionTimeBucketCountsJson: + serializer.fromJson(json['completionTimeBucketCountsJson']), + totalPushesBeforeCompletion: + serializer.fromJson(json['totalPushesBeforeCompletion']), + completedAfterPushCount: + serializer.fromJson(json['completedAfterPushCount']), + rewardCountsJson: serializer.fromJson(json['rewardCountsJson']), + difficultyCountsJson: + serializer.fromJson(json['difficultyCountsJson']), + reminderProfileCountsJson: + serializer.fromJson(json['reminderProfileCountsJson']), + revision: serializer.fromJson(json['revision']), + createdAtUtc: serializer.fromJson(json['createdAtUtc']), + updatedAtUtc: serializer.fromJson(json['updatedAtUtc']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'projectId': serializer.toJson(projectId), + 'ownerId': serializer.toJson(ownerId), + 'completedTaskCount': serializer.toJson(completedTaskCount), + 'durationMinuteCountsJson': + serializer.toJson(durationMinuteCountsJson), + 'completionTimeBucketCountsJson': + serializer.toJson(completionTimeBucketCountsJson), + 'totalPushesBeforeCompletion': + serializer.toJson(totalPushesBeforeCompletion), + 'completedAfterPushCount': + serializer.toJson(completedAfterPushCount), + 'rewardCountsJson': serializer.toJson(rewardCountsJson), + 'difficultyCountsJson': serializer.toJson(difficultyCountsJson), + 'reminderProfileCountsJson': + serializer.toJson(reminderProfileCountsJson), + 'revision': serializer.toJson(revision), + 'createdAtUtc': serializer.toJson(createdAtUtc), + 'updatedAtUtc': serializer.toJson(updatedAtUtc), + }; + } + + ProjectStatisticsRow copyWith( + {String? projectId, + String? ownerId, + int? completedTaskCount, + String? durationMinuteCountsJson, + String? completionTimeBucketCountsJson, + int? totalPushesBeforeCompletion, + int? completedAfterPushCount, + String? rewardCountsJson, + String? difficultyCountsJson, + String? reminderProfileCountsJson, + int? revision, + DateTime? createdAtUtc, + DateTime? updatedAtUtc}) => + ProjectStatisticsRow( + projectId: projectId ?? this.projectId, + ownerId: ownerId ?? this.ownerId, + completedTaskCount: completedTaskCount ?? this.completedTaskCount, + durationMinuteCountsJson: + durationMinuteCountsJson ?? this.durationMinuteCountsJson, + completionTimeBucketCountsJson: completionTimeBucketCountsJson ?? + this.completionTimeBucketCountsJson, + totalPushesBeforeCompletion: + totalPushesBeforeCompletion ?? this.totalPushesBeforeCompletion, + completedAfterPushCount: + completedAfterPushCount ?? this.completedAfterPushCount, + rewardCountsJson: rewardCountsJson ?? this.rewardCountsJson, + difficultyCountsJson: difficultyCountsJson ?? this.difficultyCountsJson, + reminderProfileCountsJson: + reminderProfileCountsJson ?? this.reminderProfileCountsJson, + revision: revision ?? this.revision, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + ); + ProjectStatisticsRow copyWithCompanion(ProjectStatisticsTableCompanion data) { + return ProjectStatisticsRow( + projectId: data.projectId.present ? data.projectId.value : this.projectId, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + completedTaskCount: data.completedTaskCount.present + ? data.completedTaskCount.value + : this.completedTaskCount, + durationMinuteCountsJson: data.durationMinuteCountsJson.present + ? data.durationMinuteCountsJson.value + : this.durationMinuteCountsJson, + completionTimeBucketCountsJson: + data.completionTimeBucketCountsJson.present + ? data.completionTimeBucketCountsJson.value + : this.completionTimeBucketCountsJson, + totalPushesBeforeCompletion: data.totalPushesBeforeCompletion.present + ? data.totalPushesBeforeCompletion.value + : this.totalPushesBeforeCompletion, + completedAfterPushCount: data.completedAfterPushCount.present + ? data.completedAfterPushCount.value + : this.completedAfterPushCount, + rewardCountsJson: data.rewardCountsJson.present + ? data.rewardCountsJson.value + : this.rewardCountsJson, + difficultyCountsJson: data.difficultyCountsJson.present + ? data.difficultyCountsJson.value + : this.difficultyCountsJson, + reminderProfileCountsJson: data.reminderProfileCountsJson.present + ? data.reminderProfileCountsJson.value + : this.reminderProfileCountsJson, + revision: data.revision.present ? data.revision.value : this.revision, + createdAtUtc: data.createdAtUtc.present + ? data.createdAtUtc.value + : this.createdAtUtc, + updatedAtUtc: data.updatedAtUtc.present + ? data.updatedAtUtc.value + : this.updatedAtUtc, + ); + } + + @override + String toString() { + return (StringBuffer('ProjectStatisticsRow(') + ..write('projectId: $projectId, ') + ..write('ownerId: $ownerId, ') + ..write('completedTaskCount: $completedTaskCount, ') + ..write('durationMinuteCountsJson: $durationMinuteCountsJson, ') + ..write( + 'completionTimeBucketCountsJson: $completionTimeBucketCountsJson, ') + ..write('totalPushesBeforeCompletion: $totalPushesBeforeCompletion, ') + ..write('completedAfterPushCount: $completedAfterPushCount, ') + ..write('rewardCountsJson: $rewardCountsJson, ') + ..write('difficultyCountsJson: $difficultyCountsJson, ') + ..write('reminderProfileCountsJson: $reminderProfileCountsJson, ') + ..write('revision: $revision, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + projectId, + ownerId, + completedTaskCount, + durationMinuteCountsJson, + completionTimeBucketCountsJson, + totalPushesBeforeCompletion, + completedAfterPushCount, + rewardCountsJson, + difficultyCountsJson, + reminderProfileCountsJson, + revision, + createdAtUtc, + updatedAtUtc); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ProjectStatisticsRow && + other.projectId == this.projectId && + other.ownerId == this.ownerId && + other.completedTaskCount == this.completedTaskCount && + other.durationMinuteCountsJson == this.durationMinuteCountsJson && + other.completionTimeBucketCountsJson == + this.completionTimeBucketCountsJson && + other.totalPushesBeforeCompletion == + this.totalPushesBeforeCompletion && + other.completedAfterPushCount == this.completedAfterPushCount && + other.rewardCountsJson == this.rewardCountsJson && + other.difficultyCountsJson == this.difficultyCountsJson && + other.reminderProfileCountsJson == this.reminderProfileCountsJson && + other.revision == this.revision && + other.createdAtUtc == this.createdAtUtc && + other.updatedAtUtc == this.updatedAtUtc); +} + +class ProjectStatisticsTableCompanion + extends UpdateCompanion { + final Value projectId; + final Value ownerId; + final Value completedTaskCount; + final Value durationMinuteCountsJson; + final Value completionTimeBucketCountsJson; + final Value totalPushesBeforeCompletion; + final Value completedAfterPushCount; + final Value rewardCountsJson; + final Value difficultyCountsJson; + final Value reminderProfileCountsJson; + final Value revision; + final Value createdAtUtc; + final Value updatedAtUtc; + final Value rowid; + const ProjectStatisticsTableCompanion({ + this.projectId = const Value.absent(), + this.ownerId = const Value.absent(), + this.completedTaskCount = const Value.absent(), + this.durationMinuteCountsJson = const Value.absent(), + this.completionTimeBucketCountsJson = const Value.absent(), + this.totalPushesBeforeCompletion = const Value.absent(), + this.completedAfterPushCount = const Value.absent(), + this.rewardCountsJson = const Value.absent(), + this.difficultyCountsJson = const Value.absent(), + this.reminderProfileCountsJson = const Value.absent(), + this.revision = const Value.absent(), + this.createdAtUtc = const Value.absent(), + this.updatedAtUtc = const Value.absent(), + this.rowid = const Value.absent(), + }); + ProjectStatisticsTableCompanion.insert({ + required String projectId, + required String ownerId, + required int completedTaskCount, + required String durationMinuteCountsJson, + required String completionTimeBucketCountsJson, + required int totalPushesBeforeCompletion, + required int completedAfterPushCount, + required String rewardCountsJson, + required String difficultyCountsJson, + required String reminderProfileCountsJson, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + this.rowid = const Value.absent(), + }) : projectId = Value(projectId), + ownerId = Value(ownerId), + completedTaskCount = Value(completedTaskCount), + durationMinuteCountsJson = Value(durationMinuteCountsJson), + completionTimeBucketCountsJson = Value(completionTimeBucketCountsJson), + totalPushesBeforeCompletion = Value(totalPushesBeforeCompletion), + completedAfterPushCount = Value(completedAfterPushCount), + rewardCountsJson = Value(rewardCountsJson), + difficultyCountsJson = Value(difficultyCountsJson), + reminderProfileCountsJson = Value(reminderProfileCountsJson), + revision = Value(revision), + createdAtUtc = Value(createdAtUtc), + updatedAtUtc = Value(updatedAtUtc); + static Insertable custom({ + Expression? projectId, + Expression? ownerId, + Expression? completedTaskCount, + Expression? durationMinuteCountsJson, + Expression? completionTimeBucketCountsJson, + Expression? totalPushesBeforeCompletion, + Expression? completedAfterPushCount, + Expression? rewardCountsJson, + Expression? difficultyCountsJson, + Expression? reminderProfileCountsJson, + Expression? revision, + Expression? createdAtUtc, + Expression? updatedAtUtc, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (projectId != null) 'project_id': projectId, + if (ownerId != null) 'owner_id': ownerId, + if (completedTaskCount != null) + 'completed_task_count': completedTaskCount, + if (durationMinuteCountsJson != null) + 'duration_minute_counts_json': durationMinuteCountsJson, + if (completionTimeBucketCountsJson != null) + 'completion_time_bucket_counts_json': completionTimeBucketCountsJson, + if (totalPushesBeforeCompletion != null) + 'total_pushes_before_completion': totalPushesBeforeCompletion, + if (completedAfterPushCount != null) + 'completed_after_push_count': completedAfterPushCount, + if (rewardCountsJson != null) 'reward_counts_json': rewardCountsJson, + if (difficultyCountsJson != null) + 'difficulty_counts_json': difficultyCountsJson, + if (reminderProfileCountsJson != null) + 'reminder_profile_counts_json': reminderProfileCountsJson, + if (revision != null) 'revision': revision, + if (createdAtUtc != null) 'created_at_utc': createdAtUtc, + if (updatedAtUtc != null) 'updated_at_utc': updatedAtUtc, + if (rowid != null) 'rowid': rowid, + }); + } + + ProjectStatisticsTableCompanion copyWith( + {Value? projectId, + Value? ownerId, + Value? completedTaskCount, + Value? durationMinuteCountsJson, + Value? completionTimeBucketCountsJson, + Value? totalPushesBeforeCompletion, + Value? completedAfterPushCount, + Value? rewardCountsJson, + Value? difficultyCountsJson, + Value? reminderProfileCountsJson, + Value? revision, + Value? createdAtUtc, + Value? updatedAtUtc, + Value? rowid}) { + return ProjectStatisticsTableCompanion( + projectId: projectId ?? this.projectId, + ownerId: ownerId ?? this.ownerId, + completedTaskCount: completedTaskCount ?? this.completedTaskCount, + durationMinuteCountsJson: + durationMinuteCountsJson ?? this.durationMinuteCountsJson, + completionTimeBucketCountsJson: + completionTimeBucketCountsJson ?? this.completionTimeBucketCountsJson, + totalPushesBeforeCompletion: + totalPushesBeforeCompletion ?? this.totalPushesBeforeCompletion, + completedAfterPushCount: + completedAfterPushCount ?? this.completedAfterPushCount, + rewardCountsJson: rewardCountsJson ?? this.rewardCountsJson, + difficultyCountsJson: difficultyCountsJson ?? this.difficultyCountsJson, + reminderProfileCountsJson: + reminderProfileCountsJson ?? this.reminderProfileCountsJson, + revision: revision ?? this.revision, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (projectId.present) { + map['project_id'] = Variable(projectId.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (completedTaskCount.present) { + map['completed_task_count'] = Variable(completedTaskCount.value); + } + if (durationMinuteCountsJson.present) { + map['duration_minute_counts_json'] = + Variable(durationMinuteCountsJson.value); + } + if (completionTimeBucketCountsJson.present) { + map['completion_time_bucket_counts_json'] = + Variable(completionTimeBucketCountsJson.value); + } + if (totalPushesBeforeCompletion.present) { + map['total_pushes_before_completion'] = + Variable(totalPushesBeforeCompletion.value); + } + if (completedAfterPushCount.present) { + map['completed_after_push_count'] = + Variable(completedAfterPushCount.value); + } + if (rewardCountsJson.present) { + map['reward_counts_json'] = Variable(rewardCountsJson.value); + } + if (difficultyCountsJson.present) { + map['difficulty_counts_json'] = + Variable(difficultyCountsJson.value); + } + if (reminderProfileCountsJson.present) { + map['reminder_profile_counts_json'] = + Variable(reminderProfileCountsJson.value); + } + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (createdAtUtc.present) { + map['created_at_utc'] = Variable(createdAtUtc.value); + } + if (updatedAtUtc.present) { + map['updated_at_utc'] = Variable(updatedAtUtc.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ProjectStatisticsTableCompanion(') + ..write('projectId: $projectId, ') + ..write('ownerId: $ownerId, ') + ..write('completedTaskCount: $completedTaskCount, ') + ..write('durationMinuteCountsJson: $durationMinuteCountsJson, ') + ..write( + 'completionTimeBucketCountsJson: $completionTimeBucketCountsJson, ') + ..write('totalPushesBeforeCompletion: $totalPushesBeforeCompletion, ') + ..write('completedAfterPushCount: $completedAfterPushCount, ') + ..write('rewardCountsJson: $rewardCountsJson, ') + ..write('difficultyCountsJson: $difficultyCountsJson, ') + ..write('reminderProfileCountsJson: $reminderProfileCountsJson, ') + ..write('revision: $revision, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $LockedBlocksTable extends LockedBlocks with TableInfo<$LockedBlocksTable, LockedBlockRow> { @override @@ -2125,18 +3465,56 @@ class $LockedBlocksTable extends LockedBlocks } class LockedBlockRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `name` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String name; + + /// Returns the derived `date` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? date; + + /// Returns the derived `startTime` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String startTime; + + /// Returns the derived `endTime` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String endTime; + + /// Converts scheduler data for `recurrenceJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String? recurrenceJson; + + /// Returns the derived `hiddenByDefault` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final bool hiddenByDefault; + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? projectId; + + /// Returns the derived `archivedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime? archivedAtUtc; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const LockedBlockRow( {required this.id, @@ -2777,18 +4155,56 @@ class $LockedOverridesTable extends LockedOverrides class LockedOverrideRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `lockedBlockId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? lockedBlockId; + + /// Returns the derived `date` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String date; + + /// Returns the derived `type` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String type; + + /// Returns the derived `name` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? name; + + /// Returns the derived `startTime` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? startTime; + + /// Returns the derived `endTime` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? endTime; + + /// Returns the derived `hiddenByDefault` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final bool hiddenByDefault; + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? projectId; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const LockedOverrideRow( {required this.id, @@ -3390,14 +4806,40 @@ class $SettingsTableTable extends SettingsTable } class SettingsRow extends DataClass implements Insertable { + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `timeZoneId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String timeZoneId; + + /// Returns the derived `dayStartMinutes` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int dayStartMinutes; + + /// Returns the derived `dayEndMinutes` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int dayEndMinutes; + + /// Returns the derived `compactMode` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final bool compactMode; + + /// Converts scheduler data for `backlogStalenessJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String backlogStalenessJson; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const SettingsRow( {required this.ownerId, @@ -4043,23 +5485,76 @@ class $SnapshotsTable extends Snapshots } class SnapshotRow extends DataClass implements Insertable { + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String id; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String ownerId; + + /// Returns the derived `capturedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime capturedAtUtc; + + /// Returns the derived `operationName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? operationName; + + /// Returns the derived `sourceDate` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? sourceDate; + + /// Returns the derived `targetDate` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final String? targetDate; + + /// Converts scheduler data for `windowJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String windowJson; + + /// Converts scheduler data for `tasksJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String tasksJson; + + /// Converts scheduler data for `lockedIntervalsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String lockedIntervalsJson; + + /// Converts scheduler data for `requiredVisibleIntervalsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String requiredVisibleIntervalsJson; + + /// Converts scheduler data for `noticesJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String noticesJson; + + /// Converts scheduler data for `changesJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String changesJson; + + /// Converts scheduler data for `overlapsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. final String overlapsJson; + + /// Returns the derived `retentionExpiresUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime retentionExpiresUtc; + + /// Returns the derived `truncated` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final bool truncated; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. final DateTime updatedAtUtc; const SnapshotRow( {required this.id, @@ -4610,16 +6105,716 @@ class SnapshotsCompanion extends UpdateCompanion { } } +class $ApplicationOperationsTable extends ApplicationOperations + with TableInfo<$ApplicationOperationsTable, ApplicationOperationRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ApplicationOperationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _operationIdMeta = + const VerificationMeta('operationId'); + @override + late final GeneratedColumn operationId = GeneratedColumn( + 'operation_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _ownerIdMeta = + const VerificationMeta('ownerId'); + @override + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _operationNameMeta = + const VerificationMeta('operationName'); + @override + late final GeneratedColumn operationName = GeneratedColumn( + 'operation_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _committedAtUtcMeta = + const VerificationMeta('committedAtUtc'); + @override + late final GeneratedColumn committedAtUtc = + GeneratedColumn('committed_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => + [operationId, ownerId, operationName, committedAtUtc]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'application_operations'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('operation_id')) { + context.handle( + _operationIdMeta, + operationId.isAcceptableOrUnknown( + data['operation_id']!, _operationIdMeta)); + } else if (isInserting) { + context.missing(_operationIdMeta); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('operation_name')) { + context.handle( + _operationNameMeta, + operationName.isAcceptableOrUnknown( + data['operation_name']!, _operationNameMeta)); + } else if (isInserting) { + context.missing(_operationNameMeta); + } + if (data.containsKey('committed_at_utc')) { + context.handle( + _committedAtUtcMeta, + committedAtUtc.isAcceptableOrUnknown( + data['committed_at_utc']!, _committedAtUtcMeta)); + } else if (isInserting) { + context.missing(_committedAtUtcMeta); + } + return context; + } + + @override + Set get $primaryKey => {operationId}; + @override + ApplicationOperationRow map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ApplicationOperationRow( + operationId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}operation_id'])!, + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + operationName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}operation_name'])!, + committedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}committed_at_utc'])!, + ); + } + + @override + $ApplicationOperationsTable createAlias(String alias) { + return $ApplicationOperationsTable(attachedDatabase, alias); + } +} + +class ApplicationOperationRow extends DataClass + implements Insertable { + /// Returns the derived `operationId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String operationId; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String ownerId; + + /// Returns the derived `operationName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String operationName; + + /// Returns the derived `committedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime committedAtUtc; + const ApplicationOperationRow( + {required this.operationId, + required this.ownerId, + required this.operationName, + required this.committedAtUtc}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['operation_id'] = Variable(operationId); + map['owner_id'] = Variable(ownerId); + map['operation_name'] = Variable(operationName); + map['committed_at_utc'] = Variable(committedAtUtc); + return map; + } + + ApplicationOperationsCompanion toCompanion(bool nullToAbsent) { + return ApplicationOperationsCompanion( + operationId: Value(operationId), + ownerId: Value(ownerId), + operationName: Value(operationName), + committedAtUtc: Value(committedAtUtc), + ); + } + + factory ApplicationOperationRow.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ApplicationOperationRow( + operationId: serializer.fromJson(json['operationId']), + ownerId: serializer.fromJson(json['ownerId']), + operationName: serializer.fromJson(json['operationName']), + committedAtUtc: serializer.fromJson(json['committedAtUtc']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'operationId': serializer.toJson(operationId), + 'ownerId': serializer.toJson(ownerId), + 'operationName': serializer.toJson(operationName), + 'committedAtUtc': serializer.toJson(committedAtUtc), + }; + } + + ApplicationOperationRow copyWith( + {String? operationId, + String? ownerId, + String? operationName, + DateTime? committedAtUtc}) => + ApplicationOperationRow( + operationId: operationId ?? this.operationId, + ownerId: ownerId ?? this.ownerId, + operationName: operationName ?? this.operationName, + committedAtUtc: committedAtUtc ?? this.committedAtUtc, + ); + ApplicationOperationRow copyWithCompanion( + ApplicationOperationsCompanion data) { + return ApplicationOperationRow( + operationId: + data.operationId.present ? data.operationId.value : this.operationId, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + operationName: data.operationName.present + ? data.operationName.value + : this.operationName, + committedAtUtc: data.committedAtUtc.present + ? data.committedAtUtc.value + : this.committedAtUtc, + ); + } + + @override + String toString() { + return (StringBuffer('ApplicationOperationRow(') + ..write('operationId: $operationId, ') + ..write('ownerId: $ownerId, ') + ..write('operationName: $operationName, ') + ..write('committedAtUtc: $committedAtUtc') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(operationId, ownerId, operationName, committedAtUtc); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ApplicationOperationRow && + other.operationId == this.operationId && + other.ownerId == this.ownerId && + other.operationName == this.operationName && + other.committedAtUtc == this.committedAtUtc); +} + +class ApplicationOperationsCompanion + extends UpdateCompanion { + final Value operationId; + final Value ownerId; + final Value operationName; + final Value committedAtUtc; + final Value rowid; + const ApplicationOperationsCompanion({ + this.operationId = const Value.absent(), + this.ownerId = const Value.absent(), + this.operationName = const Value.absent(), + this.committedAtUtc = const Value.absent(), + this.rowid = const Value.absent(), + }); + ApplicationOperationsCompanion.insert({ + required String operationId, + required String ownerId, + required String operationName, + required DateTime committedAtUtc, + this.rowid = const Value.absent(), + }) : operationId = Value(operationId), + ownerId = Value(ownerId), + operationName = Value(operationName), + committedAtUtc = Value(committedAtUtc); + static Insertable custom({ + Expression? operationId, + Expression? ownerId, + Expression? operationName, + Expression? committedAtUtc, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (operationId != null) 'operation_id': operationId, + if (ownerId != null) 'owner_id': ownerId, + if (operationName != null) 'operation_name': operationName, + if (committedAtUtc != null) 'committed_at_utc': committedAtUtc, + if (rowid != null) 'rowid': rowid, + }); + } + + ApplicationOperationsCompanion copyWith( + {Value? operationId, + Value? ownerId, + Value? operationName, + Value? committedAtUtc, + Value? rowid}) { + return ApplicationOperationsCompanion( + operationId: operationId ?? this.operationId, + ownerId: ownerId ?? this.ownerId, + operationName: operationName ?? this.operationName, + committedAtUtc: committedAtUtc ?? this.committedAtUtc, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (operationId.present) { + map['operation_id'] = Variable(operationId.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (operationName.present) { + map['operation_name'] = Variable(operationName.value); + } + if (committedAtUtc.present) { + map['committed_at_utc'] = Variable(committedAtUtc.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ApplicationOperationsCompanion(') + ..write('operationId: $operationId, ') + ..write('ownerId: $ownerId, ') + ..write('operationName: $operationName, ') + ..write('committedAtUtc: $committedAtUtc, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $NoticeAcknowledgementsTable extends NoticeAcknowledgements + with TableInfo<$NoticeAcknowledgementsTable, NoticeAcknowledgementRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $NoticeAcknowledgementsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _ownerIdMeta = + const VerificationMeta('ownerId'); + @override + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _noticeIdMeta = + const VerificationMeta('noticeId'); + @override + late final GeneratedColumn noticeId = GeneratedColumn( + 'notice_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _acknowledgedAtUtcMeta = + const VerificationMeta('acknowledgedAtUtc'); + @override + late final GeneratedColumn acknowledgedAtUtc = + GeneratedColumn('acknowledged_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + static const VerificationMeta _revisionMeta = + const VerificationMeta('revision'); + @override + late final GeneratedColumn revision = GeneratedColumn( + 'revision', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _createdAtUtcMeta = + const VerificationMeta('createdAtUtc'); + @override + late final GeneratedColumn createdAtUtc = GeneratedColumn( + 'created_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + static const VerificationMeta _updatedAtUtcMeta = + const VerificationMeta('updatedAtUtc'); + @override + late final GeneratedColumn updatedAtUtc = GeneratedColumn( + 'updated_at_utc', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [ + ownerId, + noticeId, + acknowledgedAtUtc, + revision, + createdAtUtc, + updatedAtUtc + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'notice_acknowledgements'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('notice_id')) { + context.handle(_noticeIdMeta, + noticeId.isAcceptableOrUnknown(data['notice_id']!, _noticeIdMeta)); + } else if (isInserting) { + context.missing(_noticeIdMeta); + } + if (data.containsKey('acknowledged_at_utc')) { + context.handle( + _acknowledgedAtUtcMeta, + acknowledgedAtUtc.isAcceptableOrUnknown( + data['acknowledged_at_utc']!, _acknowledgedAtUtcMeta)); + } else if (isInserting) { + context.missing(_acknowledgedAtUtcMeta); + } + if (data.containsKey('revision')) { + context.handle(_revisionMeta, + revision.isAcceptableOrUnknown(data['revision']!, _revisionMeta)); + } else if (isInserting) { + context.missing(_revisionMeta); + } + if (data.containsKey('created_at_utc')) { + context.handle( + _createdAtUtcMeta, + createdAtUtc.isAcceptableOrUnknown( + data['created_at_utc']!, _createdAtUtcMeta)); + } else if (isInserting) { + context.missing(_createdAtUtcMeta); + } + if (data.containsKey('updated_at_utc')) { + context.handle( + _updatedAtUtcMeta, + updatedAtUtc.isAcceptableOrUnknown( + data['updated_at_utc']!, _updatedAtUtcMeta)); + } else if (isInserting) { + context.missing(_updatedAtUtcMeta); + } + return context; + } + + @override + Set get $primaryKey => {ownerId, noticeId}; + @override + NoticeAcknowledgementRow map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return NoticeAcknowledgementRow( + ownerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + noticeId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}notice_id'])!, + acknowledgedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}acknowledged_at_utc'])!, + revision: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}revision'])!, + createdAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}created_at_utc'])!, + updatedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}updated_at_utc'])!, + ); + } + + @override + $NoticeAcknowledgementsTable createAlias(String alias) { + return $NoticeAcknowledgementsTable(attachedDatabase, alias); + } +} + +class NoticeAcknowledgementRow extends DataClass + implements Insertable { + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String ownerId; + + /// Returns the derived `noticeId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final String noticeId; + + /// Returns the derived `acknowledgedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime acknowledgedAtUtc; + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final int revision; + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime createdAtUtc; + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + final DateTime updatedAtUtc; + const NoticeAcknowledgementRow( + {required this.ownerId, + required this.noticeId, + required this.acknowledgedAtUtc, + required this.revision, + required this.createdAtUtc, + required this.updatedAtUtc}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['owner_id'] = Variable(ownerId); + map['notice_id'] = Variable(noticeId); + map['acknowledged_at_utc'] = Variable(acknowledgedAtUtc); + map['revision'] = Variable(revision); + map['created_at_utc'] = Variable(createdAtUtc); + map['updated_at_utc'] = Variable(updatedAtUtc); + return map; + } + + NoticeAcknowledgementsCompanion toCompanion(bool nullToAbsent) { + return NoticeAcknowledgementsCompanion( + ownerId: Value(ownerId), + noticeId: Value(noticeId), + acknowledgedAtUtc: Value(acknowledgedAtUtc), + revision: Value(revision), + createdAtUtc: Value(createdAtUtc), + updatedAtUtc: Value(updatedAtUtc), + ); + } + + factory NoticeAcknowledgementRow.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return NoticeAcknowledgementRow( + ownerId: serializer.fromJson(json['ownerId']), + noticeId: serializer.fromJson(json['noticeId']), + acknowledgedAtUtc: + serializer.fromJson(json['acknowledgedAtUtc']), + revision: serializer.fromJson(json['revision']), + createdAtUtc: serializer.fromJson(json['createdAtUtc']), + updatedAtUtc: serializer.fromJson(json['updatedAtUtc']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'ownerId': serializer.toJson(ownerId), + 'noticeId': serializer.toJson(noticeId), + 'acknowledgedAtUtc': serializer.toJson(acknowledgedAtUtc), + 'revision': serializer.toJson(revision), + 'createdAtUtc': serializer.toJson(createdAtUtc), + 'updatedAtUtc': serializer.toJson(updatedAtUtc), + }; + } + + NoticeAcknowledgementRow copyWith( + {String? ownerId, + String? noticeId, + DateTime? acknowledgedAtUtc, + int? revision, + DateTime? createdAtUtc, + DateTime? updatedAtUtc}) => + NoticeAcknowledgementRow( + ownerId: ownerId ?? this.ownerId, + noticeId: noticeId ?? this.noticeId, + acknowledgedAtUtc: acknowledgedAtUtc ?? this.acknowledgedAtUtc, + revision: revision ?? this.revision, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + ); + NoticeAcknowledgementRow copyWithCompanion( + NoticeAcknowledgementsCompanion data) { + return NoticeAcknowledgementRow( + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + noticeId: data.noticeId.present ? data.noticeId.value : this.noticeId, + acknowledgedAtUtc: data.acknowledgedAtUtc.present + ? data.acknowledgedAtUtc.value + : this.acknowledgedAtUtc, + revision: data.revision.present ? data.revision.value : this.revision, + createdAtUtc: data.createdAtUtc.present + ? data.createdAtUtc.value + : this.createdAtUtc, + updatedAtUtc: data.updatedAtUtc.present + ? data.updatedAtUtc.value + : this.updatedAtUtc, + ); + } + + @override + String toString() { + return (StringBuffer('NoticeAcknowledgementRow(') + ..write('ownerId: $ownerId, ') + ..write('noticeId: $noticeId, ') + ..write('acknowledgedAtUtc: $acknowledgedAtUtc, ') + ..write('revision: $revision, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(ownerId, noticeId, acknowledgedAtUtc, + revision, createdAtUtc, updatedAtUtc); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is NoticeAcknowledgementRow && + other.ownerId == this.ownerId && + other.noticeId == this.noticeId && + other.acknowledgedAtUtc == this.acknowledgedAtUtc && + other.revision == this.revision && + other.createdAtUtc == this.createdAtUtc && + other.updatedAtUtc == this.updatedAtUtc); +} + +class NoticeAcknowledgementsCompanion + extends UpdateCompanion { + final Value ownerId; + final Value noticeId; + final Value acknowledgedAtUtc; + final Value revision; + final Value createdAtUtc; + final Value updatedAtUtc; + final Value rowid; + const NoticeAcknowledgementsCompanion({ + this.ownerId = const Value.absent(), + this.noticeId = const Value.absent(), + this.acknowledgedAtUtc = const Value.absent(), + this.revision = const Value.absent(), + this.createdAtUtc = const Value.absent(), + this.updatedAtUtc = const Value.absent(), + this.rowid = const Value.absent(), + }); + NoticeAcknowledgementsCompanion.insert({ + required String ownerId, + required String noticeId, + required DateTime acknowledgedAtUtc, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + this.rowid = const Value.absent(), + }) : ownerId = Value(ownerId), + noticeId = Value(noticeId), + acknowledgedAtUtc = Value(acknowledgedAtUtc), + revision = Value(revision), + createdAtUtc = Value(createdAtUtc), + updatedAtUtc = Value(updatedAtUtc); + static Insertable custom({ + Expression? ownerId, + Expression? noticeId, + Expression? acknowledgedAtUtc, + Expression? revision, + Expression? createdAtUtc, + Expression? updatedAtUtc, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (ownerId != null) 'owner_id': ownerId, + if (noticeId != null) 'notice_id': noticeId, + if (acknowledgedAtUtc != null) 'acknowledged_at_utc': acknowledgedAtUtc, + if (revision != null) 'revision': revision, + if (createdAtUtc != null) 'created_at_utc': createdAtUtc, + if (updatedAtUtc != null) 'updated_at_utc': updatedAtUtc, + if (rowid != null) 'rowid': rowid, + }); + } + + NoticeAcknowledgementsCompanion copyWith( + {Value? ownerId, + Value? noticeId, + Value? acknowledgedAtUtc, + Value? revision, + Value? createdAtUtc, + Value? updatedAtUtc, + Value? rowid}) { + return NoticeAcknowledgementsCompanion( + ownerId: ownerId ?? this.ownerId, + noticeId: noticeId ?? this.noticeId, + acknowledgedAtUtc: acknowledgedAtUtc ?? this.acknowledgedAtUtc, + revision: revision ?? this.revision, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (noticeId.present) { + map['notice_id'] = Variable(noticeId.value); + } + if (acknowledgedAtUtc.present) { + map['acknowledged_at_utc'] = Variable(acknowledgedAtUtc.value); + } + if (revision.present) { + map['revision'] = Variable(revision.value); + } + if (createdAtUtc.present) { + map['created_at_utc'] = Variable(createdAtUtc.value); + } + if (updatedAtUtc.present) { + map['updated_at_utc'] = Variable(updatedAtUtc.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('NoticeAcknowledgementsCompanion(') + ..write('ownerId: $ownerId, ') + ..write('noticeId: $noticeId, ') + ..write('acknowledgedAtUtc: $acknowledgedAtUtc, ') + ..write('revision: $revision, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$SchedulerDb extends GeneratedDatabase { _$SchedulerDb(QueryExecutor e) : super(e); $SchedulerDbManager get managers => $SchedulerDbManager(this); late final $TasksTable tasks = $TasksTable(this); + late final $TaskActivitiesTable taskActivities = $TaskActivitiesTable(this); late final $ProjectsTable projects = $ProjectsTable(this); + late final $ProjectStatisticsTableTable projectStatisticsTable = + $ProjectStatisticsTableTable(this); late final $LockedBlocksTable lockedBlocks = $LockedBlocksTable(this); late final $LockedOverridesTable lockedOverrides = $LockedOverridesTable(this); late final $SettingsTableTable settingsTable = $SettingsTableTable(this); late final $SnapshotsTable snapshots = $SnapshotsTable(this); + late final $ApplicationOperationsTable applicationOperations = + $ApplicationOperationsTable(this); + late final $NoticeAcknowledgementsTable noticeAcknowledgements = + $NoticeAcknowledgementsTable(this); late final TaskDao taskDao = TaskDao(this as SchedulerDb); late final ProjectDao projectDao = ProjectDao(this as SchedulerDb); late final LockedBlockDao lockedBlockDao = @@ -4632,11 +6827,15 @@ abstract class _$SchedulerDb extends GeneratedDatabase { @override List get allSchemaEntities => [ tasks, + taskActivities, projects, + projectStatisticsTable, lockedBlocks, lockedOverrides, settingsTable, - snapshots + snapshots, + applicationOperations, + noticeAcknowledgements ]; } @@ -5110,6 +7309,227 @@ typedef $$TasksTableProcessedTableManager = ProcessedTableManager< (TaskRow, BaseReferences<_$SchedulerDb, $TasksTable, TaskRow>), TaskRow, PrefetchHooks Function()>; +typedef $$TaskActivitiesTableCreateCompanionBuilder = TaskActivitiesCompanion + Function({ + required String id, + required String ownerId, + required String taskId, + required String projectId, + required String operationId, + required String code, + required DateTime occurredAtUtc, + required String metadataJson, + Value rowid, +}); +typedef $$TaskActivitiesTableUpdateCompanionBuilder = TaskActivitiesCompanion + Function({ + Value id, + Value ownerId, + Value taskId, + Value projectId, + Value operationId, + Value code, + Value occurredAtUtc, + Value metadataJson, + Value rowid, +}); + +class $$TaskActivitiesTableFilterComposer + extends Composer<_$SchedulerDb, $TaskActivitiesTable> { + $$TaskActivitiesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get taskId => $composableBuilder( + column: $table.taskId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get projectId => $composableBuilder( + column: $table.projectId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get code => $composableBuilder( + column: $table.code, builder: (column) => ColumnFilters(column)); + + ColumnFilters get occurredAtUtc => $composableBuilder( + column: $table.occurredAtUtc, builder: (column) => ColumnFilters(column)); + + ColumnFilters get metadataJson => $composableBuilder( + column: $table.metadataJson, builder: (column) => ColumnFilters(column)); +} + +class $$TaskActivitiesTableOrderingComposer + extends Composer<_$SchedulerDb, $TaskActivitiesTable> { + $$TaskActivitiesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get taskId => $composableBuilder( + column: $table.taskId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get projectId => $composableBuilder( + column: $table.projectId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get code => $composableBuilder( + column: $table.code, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get occurredAtUtc => $composableBuilder( + column: $table.occurredAtUtc, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get metadataJson => $composableBuilder( + column: $table.metadataJson, + builder: (column) => ColumnOrderings(column)); +} + +class $$TaskActivitiesTableAnnotationComposer + extends Composer<_$SchedulerDb, $TaskActivitiesTable> { + $$TaskActivitiesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + GeneratedColumn get taskId => + $composableBuilder(column: $table.taskId, builder: (column) => column); + + GeneratedColumn get projectId => + $composableBuilder(column: $table.projectId, builder: (column) => column); + + GeneratedColumn get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => column); + + GeneratedColumn get code => + $composableBuilder(column: $table.code, builder: (column) => column); + + GeneratedColumn get occurredAtUtc => $composableBuilder( + column: $table.occurredAtUtc, builder: (column) => column); + + GeneratedColumn get metadataJson => $composableBuilder( + column: $table.metadataJson, builder: (column) => column); +} + +class $$TaskActivitiesTableTableManager extends RootTableManager< + _$SchedulerDb, + $TaskActivitiesTable, + TaskActivityRow, + $$TaskActivitiesTableFilterComposer, + $$TaskActivitiesTableOrderingComposer, + $$TaskActivitiesTableAnnotationComposer, + $$TaskActivitiesTableCreateCompanionBuilder, + $$TaskActivitiesTableUpdateCompanionBuilder, + ( + TaskActivityRow, + BaseReferences<_$SchedulerDb, $TaskActivitiesTable, TaskActivityRow> + ), + TaskActivityRow, + PrefetchHooks Function()> { + $$TaskActivitiesTableTableManager( + _$SchedulerDb db, $TaskActivitiesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TaskActivitiesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TaskActivitiesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TaskActivitiesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value ownerId = const Value.absent(), + Value taskId = const Value.absent(), + Value projectId = const Value.absent(), + Value operationId = const Value.absent(), + Value code = const Value.absent(), + Value occurredAtUtc = const Value.absent(), + Value metadataJson = const Value.absent(), + Value rowid = const Value.absent(), + }) => + TaskActivitiesCompanion( + id: id, + ownerId: ownerId, + taskId: taskId, + projectId: projectId, + operationId: operationId, + code: code, + occurredAtUtc: occurredAtUtc, + metadataJson: metadataJson, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + required String ownerId, + required String taskId, + required String projectId, + required String operationId, + required String code, + required DateTime occurredAtUtc, + required String metadataJson, + Value rowid = const Value.absent(), + }) => + TaskActivitiesCompanion.insert( + id: id, + ownerId: ownerId, + taskId: taskId, + projectId: projectId, + operationId: operationId, + code: code, + occurredAtUtc: occurredAtUtc, + metadataJson: metadataJson, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$TaskActivitiesTableProcessedTableManager = ProcessedTableManager< + _$SchedulerDb, + $TaskActivitiesTable, + TaskActivityRow, + $$TaskActivitiesTableFilterComposer, + $$TaskActivitiesTableOrderingComposer, + $$TaskActivitiesTableAnnotationComposer, + $$TaskActivitiesTableCreateCompanionBuilder, + $$TaskActivitiesTableUpdateCompanionBuilder, + ( + TaskActivityRow, + BaseReferences<_$SchedulerDb, $TaskActivitiesTable, TaskActivityRow> + ), + TaskActivityRow, + PrefetchHooks Function()>; typedef $$ProjectsTableCreateCompanionBuilder = ProjectsCompanion Function({ required String id, required String ownerId, @@ -5407,6 +7827,328 @@ typedef $$ProjectsTableProcessedTableManager = ProcessedTableManager< (ProjectRow, BaseReferences<_$SchedulerDb, $ProjectsTable, ProjectRow>), ProjectRow, PrefetchHooks Function()>; +typedef $$ProjectStatisticsTableTableCreateCompanionBuilder + = ProjectStatisticsTableCompanion Function({ + required String projectId, + required String ownerId, + required int completedTaskCount, + required String durationMinuteCountsJson, + required String completionTimeBucketCountsJson, + required int totalPushesBeforeCompletion, + required int completedAfterPushCount, + required String rewardCountsJson, + required String difficultyCountsJson, + required String reminderProfileCountsJson, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + Value rowid, +}); +typedef $$ProjectStatisticsTableTableUpdateCompanionBuilder + = ProjectStatisticsTableCompanion Function({ + Value projectId, + Value ownerId, + Value completedTaskCount, + Value durationMinuteCountsJson, + Value completionTimeBucketCountsJson, + Value totalPushesBeforeCompletion, + Value completedAfterPushCount, + Value rewardCountsJson, + Value difficultyCountsJson, + Value reminderProfileCountsJson, + Value revision, + Value createdAtUtc, + Value updatedAtUtc, + Value rowid, +}); + +class $$ProjectStatisticsTableTableFilterComposer + extends Composer<_$SchedulerDb, $ProjectStatisticsTableTable> { + $$ProjectStatisticsTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get projectId => $composableBuilder( + column: $table.projectId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get completedTaskCount => $composableBuilder( + column: $table.completedTaskCount, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get durationMinuteCountsJson => $composableBuilder( + column: $table.durationMinuteCountsJson, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get completionTimeBucketCountsJson => + $composableBuilder( + column: $table.completionTimeBucketCountsJson, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get totalPushesBeforeCompletion => $composableBuilder( + column: $table.totalPushesBeforeCompletion, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get completedAfterPushCount => $composableBuilder( + column: $table.completedAfterPushCount, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get rewardCountsJson => $composableBuilder( + column: $table.rewardCountsJson, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get difficultyCountsJson => $composableBuilder( + column: $table.difficultyCountsJson, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get reminderProfileCountsJson => $composableBuilder( + column: $table.reminderProfileCountsJson, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get revision => $composableBuilder( + column: $table.revision, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, builder: (column) => ColumnFilters(column)); +} + +class $$ProjectStatisticsTableTableOrderingComposer + extends Composer<_$SchedulerDb, $ProjectStatisticsTableTable> { + $$ProjectStatisticsTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get projectId => $composableBuilder( + column: $table.projectId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completedTaskCount => $composableBuilder( + column: $table.completedTaskCount, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get durationMinuteCountsJson => $composableBuilder( + column: $table.durationMinuteCountsJson, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completionTimeBucketCountsJson => + $composableBuilder( + column: $table.completionTimeBucketCountsJson, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get totalPushesBeforeCompletion => $composableBuilder( + column: $table.totalPushesBeforeCompletion, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get completedAfterPushCount => $composableBuilder( + column: $table.completedAfterPushCount, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get rewardCountsJson => $composableBuilder( + column: $table.rewardCountsJson, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get difficultyCountsJson => $composableBuilder( + column: $table.difficultyCountsJson, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get reminderProfileCountsJson => $composableBuilder( + column: $table.reminderProfileCountsJson, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get revision => $composableBuilder( + column: $table.revision, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnOrderings(column)); +} + +class $$ProjectStatisticsTableTableAnnotationComposer + extends Composer<_$SchedulerDb, $ProjectStatisticsTableTable> { + $$ProjectStatisticsTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get projectId => + $composableBuilder(column: $table.projectId, builder: (column) => column); + + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + GeneratedColumn get completedTaskCount => $composableBuilder( + column: $table.completedTaskCount, builder: (column) => column); + + GeneratedColumn get durationMinuteCountsJson => $composableBuilder( + column: $table.durationMinuteCountsJson, builder: (column) => column); + + GeneratedColumn get completionTimeBucketCountsJson => + $composableBuilder( + column: $table.completionTimeBucketCountsJson, + builder: (column) => column); + + GeneratedColumn get totalPushesBeforeCompletion => $composableBuilder( + column: $table.totalPushesBeforeCompletion, builder: (column) => column); + + GeneratedColumn get completedAfterPushCount => $composableBuilder( + column: $table.completedAfterPushCount, builder: (column) => column); + + GeneratedColumn get rewardCountsJson => $composableBuilder( + column: $table.rewardCountsJson, builder: (column) => column); + + GeneratedColumn get difficultyCountsJson => $composableBuilder( + column: $table.difficultyCountsJson, builder: (column) => column); + + GeneratedColumn get reminderProfileCountsJson => $composableBuilder( + column: $table.reminderProfileCountsJson, builder: (column) => column); + + GeneratedColumn get revision => + $composableBuilder(column: $table.revision, builder: (column) => column); + + GeneratedColumn get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, builder: (column) => column); + + GeneratedColumn get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, builder: (column) => column); +} + +class $$ProjectStatisticsTableTableTableManager extends RootTableManager< + _$SchedulerDb, + $ProjectStatisticsTableTable, + ProjectStatisticsRow, + $$ProjectStatisticsTableTableFilterComposer, + $$ProjectStatisticsTableTableOrderingComposer, + $$ProjectStatisticsTableTableAnnotationComposer, + $$ProjectStatisticsTableTableCreateCompanionBuilder, + $$ProjectStatisticsTableTableUpdateCompanionBuilder, + ( + ProjectStatisticsRow, + BaseReferences<_$SchedulerDb, $ProjectStatisticsTableTable, + ProjectStatisticsRow> + ), + ProjectStatisticsRow, + PrefetchHooks Function()> { + $$ProjectStatisticsTableTableTableManager( + _$SchedulerDb db, $ProjectStatisticsTableTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ProjectStatisticsTableTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$ProjectStatisticsTableTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$ProjectStatisticsTableTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value projectId = const Value.absent(), + Value ownerId = const Value.absent(), + Value completedTaskCount = const Value.absent(), + Value durationMinuteCountsJson = const Value.absent(), + Value completionTimeBucketCountsJson = const Value.absent(), + Value totalPushesBeforeCompletion = const Value.absent(), + Value completedAfterPushCount = const Value.absent(), + Value rewardCountsJson = const Value.absent(), + Value difficultyCountsJson = const Value.absent(), + Value reminderProfileCountsJson = const Value.absent(), + Value revision = const Value.absent(), + Value createdAtUtc = const Value.absent(), + Value updatedAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ProjectStatisticsTableCompanion( + projectId: projectId, + ownerId: ownerId, + completedTaskCount: completedTaskCount, + durationMinuteCountsJson: durationMinuteCountsJson, + completionTimeBucketCountsJson: completionTimeBucketCountsJson, + totalPushesBeforeCompletion: totalPushesBeforeCompletion, + completedAfterPushCount: completedAfterPushCount, + rewardCountsJson: rewardCountsJson, + difficultyCountsJson: difficultyCountsJson, + reminderProfileCountsJson: reminderProfileCountsJson, + revision: revision, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + createCompanionCallback: ({ + required String projectId, + required String ownerId, + required int completedTaskCount, + required String durationMinuteCountsJson, + required String completionTimeBucketCountsJson, + required int totalPushesBeforeCompletion, + required int completedAfterPushCount, + required String rewardCountsJson, + required String difficultyCountsJson, + required String reminderProfileCountsJson, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + Value rowid = const Value.absent(), + }) => + ProjectStatisticsTableCompanion.insert( + projectId: projectId, + ownerId: ownerId, + completedTaskCount: completedTaskCount, + durationMinuteCountsJson: durationMinuteCountsJson, + completionTimeBucketCountsJson: completionTimeBucketCountsJson, + totalPushesBeforeCompletion: totalPushesBeforeCompletion, + completedAfterPushCount: completedAfterPushCount, + rewardCountsJson: rewardCountsJson, + difficultyCountsJson: difficultyCountsJson, + reminderProfileCountsJson: reminderProfileCountsJson, + revision: revision, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ProjectStatisticsTableTableProcessedTableManager + = ProcessedTableManager< + _$SchedulerDb, + $ProjectStatisticsTableTable, + ProjectStatisticsRow, + $$ProjectStatisticsTableTableFilterComposer, + $$ProjectStatisticsTableTableOrderingComposer, + $$ProjectStatisticsTableTableAnnotationComposer, + $$ProjectStatisticsTableTableCreateCompanionBuilder, + $$ProjectStatisticsTableTableUpdateCompanionBuilder, + ( + ProjectStatisticsRow, + BaseReferences<_$SchedulerDb, $ProjectStatisticsTableTable, + ProjectStatisticsRow> + ), + ProjectStatisticsRow, + PrefetchHooks Function()>; typedef $$LockedBlocksTableCreateCompanionBuilder = LockedBlocksCompanion Function({ required String id, @@ -6620,14 +9362,386 @@ typedef $$SnapshotsTableProcessedTableManager = ProcessedTableManager< (SnapshotRow, BaseReferences<_$SchedulerDb, $SnapshotsTable, SnapshotRow>), SnapshotRow, PrefetchHooks Function()>; +typedef $$ApplicationOperationsTableCreateCompanionBuilder + = ApplicationOperationsCompanion Function({ + required String operationId, + required String ownerId, + required String operationName, + required DateTime committedAtUtc, + Value rowid, +}); +typedef $$ApplicationOperationsTableUpdateCompanionBuilder + = ApplicationOperationsCompanion Function({ + Value operationId, + Value ownerId, + Value operationName, + Value committedAtUtc, + Value rowid, +}); + +class $$ApplicationOperationsTableFilterComposer + extends Composer<_$SchedulerDb, $ApplicationOperationsTable> { + $$ApplicationOperationsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get operationName => $composableBuilder( + column: $table.operationName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get committedAtUtc => $composableBuilder( + column: $table.committedAtUtc, + builder: (column) => ColumnFilters(column)); +} + +class $$ApplicationOperationsTableOrderingComposer + extends Composer<_$SchedulerDb, $ApplicationOperationsTable> { + $$ApplicationOperationsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get operationName => $composableBuilder( + column: $table.operationName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get committedAtUtc => $composableBuilder( + column: $table.committedAtUtc, + builder: (column) => ColumnOrderings(column)); +} + +class $$ApplicationOperationsTableAnnotationComposer + extends Composer<_$SchedulerDb, $ApplicationOperationsTable> { + $$ApplicationOperationsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get operationId => $composableBuilder( + column: $table.operationId, builder: (column) => column); + + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + GeneratedColumn get operationName => $composableBuilder( + column: $table.operationName, builder: (column) => column); + + GeneratedColumn get committedAtUtc => $composableBuilder( + column: $table.committedAtUtc, builder: (column) => column); +} + +class $$ApplicationOperationsTableTableManager extends RootTableManager< + _$SchedulerDb, + $ApplicationOperationsTable, + ApplicationOperationRow, + $$ApplicationOperationsTableFilterComposer, + $$ApplicationOperationsTableOrderingComposer, + $$ApplicationOperationsTableAnnotationComposer, + $$ApplicationOperationsTableCreateCompanionBuilder, + $$ApplicationOperationsTableUpdateCompanionBuilder, + ( + ApplicationOperationRow, + BaseReferences<_$SchedulerDb, $ApplicationOperationsTable, + ApplicationOperationRow> + ), + ApplicationOperationRow, + PrefetchHooks Function()> { + $$ApplicationOperationsTableTableManager( + _$SchedulerDb db, $ApplicationOperationsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ApplicationOperationsTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$ApplicationOperationsTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$ApplicationOperationsTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value operationId = const Value.absent(), + Value ownerId = const Value.absent(), + Value operationName = const Value.absent(), + Value committedAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ApplicationOperationsCompanion( + operationId: operationId, + ownerId: ownerId, + operationName: operationName, + committedAtUtc: committedAtUtc, + rowid: rowid, + ), + createCompanionCallback: ({ + required String operationId, + required String ownerId, + required String operationName, + required DateTime committedAtUtc, + Value rowid = const Value.absent(), + }) => + ApplicationOperationsCompanion.insert( + operationId: operationId, + ownerId: ownerId, + operationName: operationName, + committedAtUtc: committedAtUtc, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ApplicationOperationsTableProcessedTableManager + = ProcessedTableManager< + _$SchedulerDb, + $ApplicationOperationsTable, + ApplicationOperationRow, + $$ApplicationOperationsTableFilterComposer, + $$ApplicationOperationsTableOrderingComposer, + $$ApplicationOperationsTableAnnotationComposer, + $$ApplicationOperationsTableCreateCompanionBuilder, + $$ApplicationOperationsTableUpdateCompanionBuilder, + ( + ApplicationOperationRow, + BaseReferences<_$SchedulerDb, $ApplicationOperationsTable, + ApplicationOperationRow> + ), + ApplicationOperationRow, + PrefetchHooks Function()>; +typedef $$NoticeAcknowledgementsTableCreateCompanionBuilder + = NoticeAcknowledgementsCompanion Function({ + required String ownerId, + required String noticeId, + required DateTime acknowledgedAtUtc, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + Value rowid, +}); +typedef $$NoticeAcknowledgementsTableUpdateCompanionBuilder + = NoticeAcknowledgementsCompanion Function({ + Value ownerId, + Value noticeId, + Value acknowledgedAtUtc, + Value revision, + Value createdAtUtc, + Value updatedAtUtc, + Value rowid, +}); + +class $$NoticeAcknowledgementsTableFilterComposer + extends Composer<_$SchedulerDb, $NoticeAcknowledgementsTable> { + $$NoticeAcknowledgementsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get noticeId => $composableBuilder( + column: $table.noticeId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get acknowledgedAtUtc => $composableBuilder( + column: $table.acknowledgedAtUtc, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get revision => $composableBuilder( + column: $table.revision, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, builder: (column) => ColumnFilters(column)); +} + +class $$NoticeAcknowledgementsTableOrderingComposer + extends Composer<_$SchedulerDb, $NoticeAcknowledgementsTable> { + $$NoticeAcknowledgementsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get ownerId => $composableBuilder( + column: $table.ownerId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get noticeId => $composableBuilder( + column: $table.noticeId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get acknowledgedAtUtc => $composableBuilder( + column: $table.acknowledgedAtUtc, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get revision => $composableBuilder( + column: $table.revision, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnOrderings(column)); +} + +class $$NoticeAcknowledgementsTableAnnotationComposer + extends Composer<_$SchedulerDb, $NoticeAcknowledgementsTable> { + $$NoticeAcknowledgementsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get ownerId => + $composableBuilder(column: $table.ownerId, builder: (column) => column); + + GeneratedColumn get noticeId => + $composableBuilder(column: $table.noticeId, builder: (column) => column); + + GeneratedColumn get acknowledgedAtUtc => $composableBuilder( + column: $table.acknowledgedAtUtc, builder: (column) => column); + + GeneratedColumn get revision => + $composableBuilder(column: $table.revision, builder: (column) => column); + + GeneratedColumn get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, builder: (column) => column); + + GeneratedColumn get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, builder: (column) => column); +} + +class $$NoticeAcknowledgementsTableTableManager extends RootTableManager< + _$SchedulerDb, + $NoticeAcknowledgementsTable, + NoticeAcknowledgementRow, + $$NoticeAcknowledgementsTableFilterComposer, + $$NoticeAcknowledgementsTableOrderingComposer, + $$NoticeAcknowledgementsTableAnnotationComposer, + $$NoticeAcknowledgementsTableCreateCompanionBuilder, + $$NoticeAcknowledgementsTableUpdateCompanionBuilder, + ( + NoticeAcknowledgementRow, + BaseReferences<_$SchedulerDb, $NoticeAcknowledgementsTable, + NoticeAcknowledgementRow> + ), + NoticeAcknowledgementRow, + PrefetchHooks Function()> { + $$NoticeAcknowledgementsTableTableManager( + _$SchedulerDb db, $NoticeAcknowledgementsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$NoticeAcknowledgementsTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$NoticeAcknowledgementsTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$NoticeAcknowledgementsTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value ownerId = const Value.absent(), + Value noticeId = const Value.absent(), + Value acknowledgedAtUtc = const Value.absent(), + Value revision = const Value.absent(), + Value createdAtUtc = const Value.absent(), + Value updatedAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => + NoticeAcknowledgementsCompanion( + ownerId: ownerId, + noticeId: noticeId, + acknowledgedAtUtc: acknowledgedAtUtc, + revision: revision, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + createCompanionCallback: ({ + required String ownerId, + required String noticeId, + required DateTime acknowledgedAtUtc, + required int revision, + required DateTime createdAtUtc, + required DateTime updatedAtUtc, + Value rowid = const Value.absent(), + }) => + NoticeAcknowledgementsCompanion.insert( + ownerId: ownerId, + noticeId: noticeId, + acknowledgedAtUtc: acknowledgedAtUtc, + revision: revision, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$NoticeAcknowledgementsTableProcessedTableManager + = ProcessedTableManager< + _$SchedulerDb, + $NoticeAcknowledgementsTable, + NoticeAcknowledgementRow, + $$NoticeAcknowledgementsTableFilterComposer, + $$NoticeAcknowledgementsTableOrderingComposer, + $$NoticeAcknowledgementsTableAnnotationComposer, + $$NoticeAcknowledgementsTableCreateCompanionBuilder, + $$NoticeAcknowledgementsTableUpdateCompanionBuilder, + ( + NoticeAcknowledgementRow, + BaseReferences<_$SchedulerDb, $NoticeAcknowledgementsTable, + NoticeAcknowledgementRow> + ), + NoticeAcknowledgementRow, + PrefetchHooks Function()>; class $SchedulerDbManager { final _$SchedulerDb _db; $SchedulerDbManager(this._db); $$TasksTableTableManager get tasks => $$TasksTableTableManager(_db, _db.tasks); + $$TaskActivitiesTableTableManager get taskActivities => + $$TaskActivitiesTableTableManager(_db, _db.taskActivities); $$ProjectsTableTableManager get projects => $$ProjectsTableTableManager(_db, _db.projects); + $$ProjectStatisticsTableTableTableManager get projectStatisticsTable => + $$ProjectStatisticsTableTableTableManager( + _db, _db.projectStatisticsTable); $$LockedBlocksTableTableManager get lockedBlocks => $$LockedBlocksTableTableManager(_db, _db.lockedBlocks); $$LockedOverridesTableTableManager get lockedOverrides => @@ -6636,4 +9750,9 @@ class $SchedulerDbManager { $$SettingsTableTableTableManager(_db, _db.settingsTable); $$SnapshotsTableTableManager get snapshots => $$SnapshotsTableTableManager(_db, _db.snapshots); + $$ApplicationOperationsTableTableManager get applicationOperations => + $$ApplicationOperationsTableTableManager(_db, _db.applicationOperations); + $$NoticeAcknowledgementsTableTableManager get noticeAcknowledgements => + $$NoticeAcknowledgementsTableTableManager( + _db, _db.noticeAcknowledgements); } diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/database/scheduler_db.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/database/scheduler_db.dart index d2115be..f1e114a 100644 --- a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/database/scheduler_db.dart +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/database/scheduler_db.dart @@ -7,11 +7,15 @@ part of '../../scheduler_db.dart'; @DriftDatabase( tables: [ Tasks, + TaskActivities, Projects, + ProjectStatisticsTable, LockedBlocks, LockedOverrides, SettingsTable, Snapshots, + ApplicationOperations, + NoticeAcknowledgements, ], daos: [ TaskDao, @@ -29,7 +33,7 @@ class SchedulerDb extends _$SchedulerDb { /// Returns the derived `schemaVersion` value for this object. /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. @override - int get schemaVersion => 1; + int get schemaVersion => 2; /// Runs the `migration` operation and completes after its asynchronous work finishes. /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order. @@ -37,6 +41,48 @@ class SchedulerDb extends _$SchedulerDb { MigrationStrategy get migration => MigrationStrategy( onCreate: (Migrator migrator) async { await migrator.createAll(); + await _createApplicationIndexes(); + }, + onUpgrade: (Migrator migrator, int from, int to) async { + if (from < 2) { + await migrator.createTable(taskActivities); + await migrator.createTable(projectStatisticsTable); + await migrator.createTable(applicationOperations); + await migrator.createTable(noticeAcknowledgements); + await _createApplicationIndexes(); + } }, ); + + /// Creates secondary indexes for app-layer tables. + Future _createApplicationIndexes() async { + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_application_operations_owner ' + 'ON application_operations(owner_id)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_task_activities_owner ' + 'ON task_activities(owner_id)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_task_activities_task ' + 'ON task_activities(task_id)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_task_activities_project ' + 'ON task_activities(project_id)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_task_activities_operation ' + 'ON task_activities(operation_id)', + ); + await customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_project_statistics_owner_project ' + 'ON project_statistics(owner_id, project_id)', + ); + await customStatement( + 'CREATE INDEX IF NOT EXISTS idx_notice_acknowledgements_owner ' + 'ON notice_acknowledgements(owner_id)', + ); + } } diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/application_operations.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/application_operations.dart new file mode 100644 index 0000000..47e4406 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/application_operations.dart @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../../../scheduler_db.dart'; + +/// Committed application operation rows used for idempotency. +@DataClassName('ApplicationOperationRow') +class ApplicationOperations extends Table { + /// Returns the derived `tableName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + String get tableName => 'application_operations'; + + /// Returns the derived `operationId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get operationId => text().named('operation_id')(); + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get ownerId => text().named('owner_id')(); + + /// Returns the derived `operationName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get operationName => text().named('operation_name')(); + + /// Returns the derived `committedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get committedAtUtc => dateTime().named('committed_at_utc')(); + + /// Returns the derived `primaryKey` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + Set> get primaryKey => {operationId}; +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/notice_acknowledgements.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/notice_acknowledgements.dart new file mode 100644 index 0000000..f21e489 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/application/notice_acknowledgements.dart @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../../../scheduler_db.dart'; + +/// Owner-scoped acknowledged notice rows. +@DataClassName('NoticeAcknowledgementRow') +class NoticeAcknowledgements extends Table { + /// Returns the derived `tableName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + String get tableName => 'notice_acknowledgements'; + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get ownerId => text().named('owner_id')(); + + /// Returns the derived `noticeId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get noticeId => text().named('notice_id')(); + + /// Returns the derived `acknowledgedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get acknowledgedAtUtc => + dateTime().named('acknowledged_at_utc')(); + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + IntColumn get revision => integer()(); + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get createdAtUtc => dateTime().named('created_at_utc')(); + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get updatedAtUtc => dateTime().named('updated_at_utc')(); + + /// Returns the derived `primaryKey` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + Set> get primaryKey => {ownerId, noticeId}; +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/projects/project_statistics.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/projects/project_statistics.dart new file mode 100644 index 0000000..066fe65 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/projects/project_statistics.dart @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../../../scheduler_db.dart'; + +/// Project-level aggregate statistics rows. +@DataClassName('ProjectStatisticsRow') +class ProjectStatisticsTable extends Table { + /// Returns the derived `tableName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + String get tableName => 'project_statistics'; + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get projectId => text().named('project_id')(); + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get ownerId => text().named('owner_id')(); + + /// Returns the derived `completedTaskCount` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + IntColumn get completedTaskCount => integer().named('completed_task_count')(); + + /// Converts scheduler data for `durationMinuteCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get durationMinuteCountsJson => + text().named('duration_minute_counts_json')(); + + /// Converts scheduler data for `completionTimeBucketCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get completionTimeBucketCountsJson => + text().named('completion_time_bucket_counts_json')(); + + /// Returns the derived `totalPushesBeforeCompletion` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + IntColumn get totalPushesBeforeCompletion => + integer().named('total_pushes_before_completion')(); + + /// Returns the derived `completedAfterPushCount` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + IntColumn get completedAfterPushCount => + integer().named('completed_after_push_count')(); + + /// Converts scheduler data for `rewardCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get rewardCountsJson => text().named('reward_counts_json')(); + + /// Converts scheduler data for `difficultyCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get difficultyCountsJson => + text().named('difficulty_counts_json')(); + + /// Converts scheduler data for `reminderProfileCountsJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get reminderProfileCountsJson => + text().named('reminder_profile_counts_json')(); + + /// Returns the derived `revision` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + IntColumn get revision => integer()(); + + /// Returns the derived `createdAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get createdAtUtc => dateTime().named('created_at_utc')(); + + /// Returns the derived `updatedAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get updatedAtUtc => dateTime().named('updated_at_utc')(); + + /// Returns the derived `primaryKey` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + Set> get primaryKey => {projectId}; +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/tasks/task_activities.dart b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/tasks/task_activities.dart new file mode 100644 index 0000000..796e5b8 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/scheduler_db/tables/tasks/task_activities.dart @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../../../scheduler_db.dart'; + +/// Append-only task activity fact rows. +@DataClassName('TaskActivityRow') +class TaskActivities extends Table { + /// Returns the derived `tableName` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + String get tableName => 'task_activities'; + + /// Returns the derived `id` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get id => text()(); + + /// Returns the derived `ownerId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get ownerId => text().named('owner_id')(); + + /// Returns the derived `taskId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get taskId => text().named('task_id')(); + + /// Returns the derived `projectId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get projectId => text().named('project_id')(); + + /// Returns the derived `operationId` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get operationId => text().named('operation_id')(); + + /// Returns the derived `code` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + TextColumn get code => text()(); + + /// Returns the derived `occurredAtUtc` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + DateTimeColumn get occurredAtUtc => dateTime().named('occurred_at_utc')(); + + /// Converts scheduler data for `metadataJson` without changing the source object. + /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details. + TextColumn get metadataJson => text().named('metadata_json')(); + + /// Returns the derived `primaryKey` value for this object. + /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated. + @override + Set> get primaryKey => {id}; +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_activity_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_activity_repository.dart new file mode 100644 index 0000000..e7735f3 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_activity_repository.dart @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer task activity repository. +final class _SqliteApplicationTaskActivityRepository + implements core.TaskActivityRepository { + /// Creates a task activity repository for [db]. + _SqliteApplicationTaskActivityRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns an activity by stable id. + @override + Future findById(String id) async { + final row = await _activityById(id); + return row == null ? null : _taskActivityFromRow(row); + } + + /// Returns activities emitted by [operationId]. + @override + Future> findByOperationId(String operationId) { + return _activitiesWhere( + (table) => table.operationId.equals(operationId), + ); + } + + /// Returns activities for [taskId]. + @override + Future> findByTaskId(String taskId) { + return _activitiesWhere((table) => table.taskId.equals(taskId)); + } + + /// Returns activities for [projectId]. + @override + Future> findByProjectId(String projectId) { + return _activitiesWhere((table) => table.projectId.equals(projectId)); + } + + /// Returns every activity. + @override + Future> findAll() { + return _activitiesWhere((table) => const drift.Constant(true)); + } + + /// Returns owner-scoped activities. + @override + Future> findByOwner({ + required String ownerId, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await (db.select(db.taskActivities) + ..where((table) => table.ownerId.equals(ownerId)) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.occurredAtUtc), + (table) => drift.OrderingTerm.asc(table.id), + ]) + ..limit( + page.limit + 1, + offset: _applicationCursorOffset(page.cursor), + )) + .get(); + return _applicationPageFromRows(rows, page, _taskActivityFromRow); + } + + /// Saves [activity], inserting it when absent. + @override + Future save(core.TaskActivity activity) async { + final row = await _activityById(activity.id); + final ownerId = row?.ownerId ?? defaultOwnerId; + await db.into(db.taskActivities).insertOnConflictUpdate( + _taskActivityCompanion(activity, ownerId: ownerId), + ); + } + + /// Saves all [activities]. + @override + Future saveAll(Iterable activities) async { + for (final activity in activities) { + await save(activity); + } + } + + /// Appends [activity] for [ownerId] when its id is unused. + @override + Future append({ + required core.TaskActivity activity, + required String ownerId, + }) async { + if (await _activityById(activity.id) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: activity.id, + ); + } + await db.into(db.taskActivities).insert( + _taskActivityCompanion(activity, ownerId: ownerId), + ); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Returns activities matching [where]. + Future> _activitiesWhere( + drift.Expression Function($TaskActivitiesTable table) where, + ) async { + final rows = await (db.select(db.taskActivities) + ..where(where) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.occurredAtUtc), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + return List.unmodifiable( + rows.map(_taskActivityFromRow), + ); + } + + /// Returns a raw activity row by id. + Future _activityById(String id) { + return (db.select(db.taskActivities)..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } +} + +/// Builds a Drift companion for [activity]. +TaskActivitiesCompanion _taskActivityCompanion( + core.TaskActivity activity, { + required String ownerId, +}) { + return TaskActivitiesCompanion.insert( + id: activity.id, + ownerId: ownerId, + taskId: activity.taskId, + projectId: activity.projectId, + operationId: activity.operationId, + code: activity.code.name, + occurredAtUtc: activity.occurredAt.toUtc(), + metadataJson: _metadataJson(activity.metadata), + ); +} + +/// Converts a task activity row to a core activity. +core.TaskActivity _taskActivityFromRow(TaskActivityRow row) { + return core.TaskActivity( + id: row.id, + operationId: row.operationId, + code: _enumByName(core.TaskActivityCode.values, row.code), + taskId: row.taskId, + projectId: row.projectId, + occurredAt: row.occurredAtUtc.toUtc(), + metadata: _metadataFromJson(row.metadataJson), + ); +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_locked_block_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_locked_block_repository.dart new file mode 100644 index 0000000..090bccf --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_locked_block_repository.dart @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer locked-time repository. +final class _SqliteApplicationLockedBlockRepository + implements core.LockedBlockRepository { + /// Creates a locked-time repository for [db]. + _SqliteApplicationLockedBlockRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns a locked block by stable id. + @override + Future findBlockById(String id) async { + final row = await _blockById(id); + return row == null ? null : _lockedBlockFromRow(row); + } + + /// Returns a locked block plus owner/revision metadata. + @override + Future?> findBlockRecordById( + String id, + ) async { + final row = await _blockById(id); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _lockedBlockFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + archivedAt: _storedDateTime(row.archivedAtUtc), + ); + } + + /// Returns every locked block. + @override + Future> findAllBlocks() async { + final rows = await (db.select(db.lockedBlocks) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.name), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + return List.unmodifiable(rows.map(_lockedBlockFromRow)); + } + + /// Returns owner-scoped locked blocks. + @override + Future> findBlocksByOwner({ + required String ownerId, + bool includeArchived = false, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await (db.select(db.lockedBlocks) + ..where((table) { + final ownerClause = table.ownerId.equals(ownerId); + return includeArchived + ? ownerClause + : ownerClause & table.archivedAtUtc.isNull(); + }) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.name), + (table) => drift.OrderingTerm.asc(table.id), + ]) + ..limit( + page.limit + 1, + offset: _applicationCursorOffset(page.cursor), + )) + .get(); + return _applicationPageFromRows(rows, page, _lockedBlockFromRow); + } + + /// Returns a locked override by stable id. + @override + Future findOverrideById(String id) async { + final row = await _overrideById(id); + return row == null ? null : _lockedOverrideFromRow(row); + } + + /// Returns a locked override plus owner/revision metadata. + @override + Future?> + findOverrideRecordById(String id) async { + final row = await _overrideById(id); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _lockedOverrideFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + ); + } + + /// Returns every one-day override. + @override + Future> findAllOverrides() async { + final rows = await (db.select(db.lockedOverrides) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.date), + (table) => drift.OrderingTerm.asc(table.createdAtUtc), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + return List.unmodifiable( + rows.map(_lockedOverrideFromRow), + ); + } + + /// Returns owner-scoped overrides for [date]. + @override + Future> findOverridesForDate({ + required String ownerId, + required core.CivilDate date, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await (db.select(db.lockedOverrides) + ..where( + (table) => + table.ownerId.equals(ownerId) & + table.date.equals(date.toIsoString()), + ) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.createdAtUtc), + (table) => drift.OrderingTerm.asc(table.id), + ]) + ..limit( + page.limit + 1, + offset: _applicationCursorOffset(page.cursor), + )) + .get(); + return _applicationPageFromRows(rows, page, _lockedOverrideFromRow); + } + + /// Saves [block], inserting it when absent. + @override + Future saveBlock(core.LockedBlock block) async { + final row = await _blockById(block.id); + if (row == null) { + await db.into(db.lockedBlocks).insert( + _lockedBlockCompanion( + block, + ownerId: core.OwnerId(defaultOwnerId), + revision: core.Revision.initial, + ), + ); + return; + } + await (db.update(db.lockedBlocks) + ..where((table) => table.id.equals(block.id))) + .write( + _lockedBlockCompanion( + block, + ownerId: core.OwnerId(row.ownerId), + revision: core.Revision(row.revision).next(), + ), + ); + } + + /// Saves [override], inserting it when absent. + @override + Future saveOverride(core.LockedBlockOverride override) async { + final row = await _overrideById(override.id); + if (row == null) { + await db.into(db.lockedOverrides).insert( + _lockedOverrideCompanion( + override, + ownerId: core.OwnerId(defaultOwnerId), + revision: core.Revision.initial, + ), + ); + return; + } + await (db.update(db.lockedOverrides) + ..where((table) => table.id.equals(override.id))) + .write( + _lockedOverrideCompanion( + override, + ownerId: core.OwnerId(row.ownerId), + revision: core.Revision(row.revision).next(), + ), + ); + } + + /// Inserts [block] for [ownerId] when its id is unused. + @override + Future insertBlock({ + required core.LockedBlock block, + required String ownerId, + }) async { + if (await _blockById(block.id) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: block.id, + ); + } + await db.into(db.lockedBlocks).insert( + _lockedBlockCompanion( + block, + ownerId: core.OwnerId(ownerId), + revision: core.Revision.initial, + ), + ); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Saves [block] only when the repository revision matches [expectedRevision]. + @override + Future saveBlockIfRevision({ + required core.LockedBlock block, + required String ownerId, + required int expectedRevision, + }) async { + final row = await _blockById(block.id); + final failure = _compareForWrite( + row: row, + ownerId: ownerId, + expectedRevision: expectedRevision, + entityId: block.id, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next(); + await (db.update(db.lockedBlocks) + ..where( + (table) => + table.id.equals(block.id) & + table.ownerId.equals(ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _lockedBlockCompanion( + block, + ownerId: core.OwnerId(ownerId), + revision: nextRevision, + ), + ); + return core.RepositoryMutationResult.success(revision: nextRevision.value); + } + + /// Archives [blockId] without deleting history. + @override + Future archiveBlock({ + required String blockId, + required String ownerId, + required int expectedRevision, + required DateTime archivedAt, + }) async { + final block = await findBlockById(blockId); + if (block == null) { + return _mutationFailure( + core.RepositoryFailureCode.notFound, + entityId: blockId, + ); + } + return saveBlockIfRevision( + block: block.copyWith(updatedAt: archivedAt, archivedAt: archivedAt), + ownerId: ownerId, + expectedRevision: expectedRevision, + ); + } + + /// Inserts [override] for [ownerId] when its id is unused. + @override + Future insertOverride({ + required core.LockedBlockOverride override, + required String ownerId, + }) async { + if (await _overrideById(override.id) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: override.id, + ); + } + await db.into(db.lockedOverrides).insert( + _lockedOverrideCompanion( + override, + ownerId: core.OwnerId(ownerId), + revision: core.Revision.initial, + ), + ); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Saves [override] only when the repository revision matches [expectedRevision]. + @override + Future saveOverrideIfRevision({ + required core.LockedBlockOverride override, + required String ownerId, + required int expectedRevision, + }) async { + final row = await _overrideById(override.id); + final failure = _compareForWrite( + row: row, + ownerId: ownerId, + expectedRevision: expectedRevision, + entityId: override.id, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next(); + await (db.update(db.lockedOverrides) + ..where( + (table) => + table.id.equals(override.id) & + table.ownerId.equals(ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _lockedOverrideCompanion( + override, + ownerId: core.OwnerId(ownerId), + revision: nextRevision, + ), + ); + return core.RepositoryMutationResult.success(revision: nextRevision.value); + } + + /// Returns a raw block row by id. + Future _blockById(String id) { + return (db.select(db.lockedBlocks)..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } + + /// Returns a raw override row by id. + Future _overrideById(String id) { + return (db.select(db.lockedOverrides) + ..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_mapping.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_mapping.dart new file mode 100644 index 0000000..992855b --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_mapping.dart @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// Returns a typed mutation failure. +core.RepositoryMutationResult _mutationFailure( + core.RepositoryFailureCode code, { + String? entityId, + int? expectedRevision, + int? actualRevision, +}) { + return core.RepositoryMutationResult.failure( + core.RepositoryFailure( + code: code, + entityId: entityId, + expectedRevision: expectedRevision, + actualRevision: actualRevision, + ), + ); +} + +/// Compares row ownership and revision before a write. +core.RepositoryMutationResult? _compareForWrite({ + required Object? row, + required String ownerId, + required int expectedRevision, + required String entityId, + required String? rowOwnerId, + required int? rowRevision, +}) { + if (expectedRevision <= 0) { + return _mutationFailure( + core.RepositoryFailureCode.invalidRevision, + entityId: entityId, + expectedRevision: expectedRevision, + ); + } + if (row == null) { + return _mutationFailure( + core.RepositoryFailureCode.notFound, + entityId: entityId, + ); + } + if (rowOwnerId != ownerId) { + return _mutationFailure( + core.RepositoryFailureCode.ownerMismatch, + entityId: entityId, + ); + } + if (rowRevision != expectedRevision) { + return _mutationFailure( + core.RepositoryFailureCode.staleRevision, + entityId: entityId, + expectedRevision: expectedRevision, + actualRevision: rowRevision, + ); + } + return null; +} + +/// Converts a cursor string to a bounded integer offset. +int _applicationCursorOffset(String? cursor) { + if (cursor == null) { + return 0; + } + final parsed = int.tryParse(cursor); + if (parsed == null || parsed < 0) { + return 0; + } + return parsed; +} + +/// Builds a repository page from already sorted [items]. +core.RepositoryPage _applicationPage( + List items, + core.RepositoryPageRequest request, +) { + final offset = _applicationCursorOffset(request.cursor); + final safeOffset = offset.clamp(0, items.length); + final end = (safeOffset + request.limit).clamp(0, items.length); + final pageItems = items.sublist(safeOffset, end); + return core.RepositoryPage( + items: List.unmodifiable(pageItems), + nextCursor: end < items.length ? '$end' : null, + ); +} + +/// Builds a repository page from Drift [rows] fetched with one extra row. +core.RepositoryPage _applicationPageFromRows( + List rows, + core.RepositoryPageRequest request, + T Function(R row) convert, +) { + final pageRows = rows.take(request.limit).toList(growable: false); + return core.RepositoryPage( + items: List.unmodifiable(pageRows.map(convert)), + nextCursor: rows.length > request.limit + ? '${_applicationCursorOffset(request.cursor) + request.limit}' + : null, + ); +} + +/// Returns a stored UTC instant as a [DateTime], if present. +DateTime? _storedDateTime(DateTime? value) { + return value?.toUtc(); +} + +/// Encodes metadata values to JSON-compatible values. +String _metadataJson(Map metadata) { + return jsonEncode(_jsonSafe(metadata)); +} + +/// Decodes JSON metadata from SQLite. +Map _metadataFromJson(String value) { + return Map.from( + jsonDecode(value) as Map, + ); +} + +/// Converts [value] to values supported by `jsonEncode`. +Object? _jsonSafe(Object? value) { + if (value == null || value is String || value is num || value is bool) { + return value; + } + if (value is DateTime) { + return core.PersistenceDateTimeConvention.toStoredString(value); + } + if (value is Enum) { + return value.name; + } + if (value is Map) { + return { + for (final entry in value.entries) + entry.key.toString(): _jsonSafe(entry.value as Object?), + }; + } + if (value is Iterable) { + return [for (final item in value) _jsonSafe(item as Object?)]; + } + return value.toString(); +} + +/// Encodes integer-keyed counts. +String _intCountMapJson(Map counts) { + return jsonEncode({ + for (final entry in counts.entries) '${entry.key}': entry.value, + }); +} + +/// Decodes integer-keyed counts. +Map _intCountMap(String value) { + final decoded = jsonDecode(value) as Map; + return { + for (final entry in decoded.entries) + int.parse(entry.key as String): entry.value as int, + }; +} + +/// Encodes enum-keyed counts. +String _enumCountMapJson(Map counts) { + return jsonEncode({ + for (final entry in counts.entries) entry.key.name: entry.value, + }); +} + +/// Decodes enum-keyed counts. +Map _enumCountMap( + String value, + List values, +) { + final decoded = jsonDecode(value) as Map; + return { + for (final entry in decoded.entries) + _enumByName(values, entry.key as String): entry.value as int, + }; +} + +/// Returns an enum value by stable [name]. +T _enumByName(List values, String name) { + return values.firstWhere((value) => value.name == name); +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_notice_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_notice_repository.dart new file mode 100644 index 0000000..6f9b58c --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_notice_repository.dart @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer notice acknowledgement repository. +final class _SqliteApplicationNoticeAcknowledgementRepository + implements core.NoticeAcknowledgementRepository { + /// Creates a notice acknowledgement repository for [db]. + _SqliteApplicationNoticeAcknowledgementRepository(this.db); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Returns a notice acknowledgement by owner and notice id. + @override + Future findById({ + required String ownerId, + required String noticeId, + }) async { + final row = await _noticeById(ownerId: ownerId, noticeId: noticeId); + return row == null ? null : _noticeFromRow(row); + } + + /// Returns acknowledgements for [ownerId]. + @override + Future> findByOwnerId( + String ownerId, + ) async { + final rows = await (db.select(db.noticeAcknowledgements) + ..where((table) => table.ownerId.equals(ownerId)) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.acknowledgedAtUtc), + (table) => drift.OrderingTerm.asc(table.noticeId), + ])) + .get(); + return List.unmodifiable( + rows.map(_noticeFromRow), + ); + } + + /// Returns owner-scoped acknowledgements. + @override + Future> findByOwner({ + required String ownerId, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await (db.select(db.noticeAcknowledgements) + ..where((table) => table.ownerId.equals(ownerId)) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.acknowledgedAtUtc), + (table) => drift.OrderingTerm.asc(table.noticeId), + ]) + ..limit( + page.limit + 1, + offset: _applicationCursorOffset(page.cursor), + )) + .get(); + return _applicationPageFromRows(rows, page, _noticeFromRow); + } + + /// Saves [record], inserting it when absent. + @override + Future save(core.NoticeAcknowledgementRecord record) async { + final row = await _noticeById( + ownerId: record.ownerId, + noticeId: record.noticeId, + ); + if (row == null) { + await db.into(db.noticeAcknowledgements).insert( + _noticeCompanion( + record, + revision: 1, + createdAt: record.acknowledgedAt, + updatedAt: record.acknowledgedAt, + ), + ); + return; + } + await (db.update(db.noticeAcknowledgements) + ..where( + (table) => + table.ownerId.equals(record.ownerId) & + table.noticeId.equals(record.noticeId), + )) + .write( + _noticeCompanion( + record, + revision: row.revision + 1, + createdAt: row.createdAtUtc, + updatedAt: record.acknowledgedAt, + ), + ); + } + + /// Inserts [record] when absent. + @override + Future insert( + core.NoticeAcknowledgementRecord record, + ) async { + if (await _noticeById( + ownerId: record.ownerId, + noticeId: record.noticeId, + ) != + null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: record.noticeId, + ); + } + await db.into(db.noticeAcknowledgements).insert( + _noticeCompanion( + record, + revision: 1, + createdAt: record.acknowledgedAt, + updatedAt: record.acknowledgedAt, + ), + ); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Returns a raw notice row by owner and notice id. + Future _noticeById({ + required String ownerId, + required String noticeId, + }) { + return (db.select(db.noticeAcknowledgements) + ..where( + (table) => + table.ownerId.equals(ownerId) & table.noticeId.equals(noticeId), + )) + .getSingleOrNull(); + } +} + +/// Builds a Drift companion for [record]. +NoticeAcknowledgementsCompanion _noticeCompanion( + core.NoticeAcknowledgementRecord record, { + required int revision, + required DateTime createdAt, + required DateTime updatedAt, +}) { + return NoticeAcknowledgementsCompanion.insert( + ownerId: record.ownerId, + noticeId: record.noticeId, + acknowledgedAtUtc: record.acknowledgedAt.toUtc(), + revision: revision, + createdAtUtc: createdAt.toUtc(), + updatedAtUtc: updatedAt.toUtc(), + ); +} + +/// Converts a notice acknowledgement row to a core record. +core.NoticeAcknowledgementRecord _noticeFromRow(NoticeAcknowledgementRow row) { + return core.NoticeAcknowledgementRecord( + noticeId: row.noticeId, + ownerId: row.ownerId, + acknowledgedAt: row.acknowledgedAtUtc.toUtc(), + ); +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_operation_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_operation_repository.dart new file mode 100644 index 0000000..9f28b25 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_operation_repository.dart @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer operation repository. +final class _SqliteApplicationOperationRepository + implements core.ApplicationOperationRepository { + /// Creates an operation repository for [db]. + _SqliteApplicationOperationRepository(this.db); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Returns an operation by stable operation id. + @override + Future findById(String operationId) async { + final row = await _operationById(operationId); + return row == null ? null : _operationFromRow(row); + } + + /// Returns an owner-scoped operation by operation id. + @override + Future findByIdempotencyKey({ + required String ownerId, + required String operationId, + }) async { + final row = await _operationById(operationId); + if (row == null || row.ownerId != ownerId) { + return null; + } + return _operationFromRow(row); + } + + /// Saves [operation], replacing an existing row with the same id. + @override + Future save(core.ApplicationOperationRecord operation) async { + await db.applicationOperations.insertOnConflictUpdate( + _operationCompanion(operation), + ); + } + + /// Inserts [operation] when absent. + @override + Future insertIfAbsent( + core.ApplicationOperationRecord operation, + ) async { + if (await _operationById(operation.operationId) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateOperation, + entityId: operation.operationId, + ); + } + await db + .into(db.applicationOperations) + .insert(_operationCompanion(operation)); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Returns a raw operation row by operation id. + Future _operationById(String operationId) { + return (db.select(db.applicationOperations) + ..where((table) => table.operationId.equals(operationId))) + .getSingleOrNull(); + } +} + +/// Builds a Drift companion for [operation]. +ApplicationOperationsCompanion _operationCompanion( + core.ApplicationOperationRecord operation, +) { + return ApplicationOperationsCompanion.insert( + operationId: operation.operationId, + ownerId: operation.ownerId, + operationName: operation.operationName, + committedAtUtc: operation.committedAt.toUtc(), + ); +} + +/// Converts an operation row to a core record. +core.ApplicationOperationRecord _operationFromRow(ApplicationOperationRow row) { + return core.ApplicationOperationRecord( + operationId: row.operationId, + ownerId: row.ownerId, + operationName: row.operationName, + committedAt: row.committedAtUtc.toUtc(), + ); +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_owner_settings_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_owner_settings_repository.dart new file mode 100644 index 0000000..2fd5319 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_owner_settings_repository.dart @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer owner settings repository. +final class _SqliteApplicationOwnerSettingsRepository + implements core.OwnerSettingsRepository { + /// Creates an owner settings repository for [db]. + _SqliteApplicationOwnerSettingsRepository(this.db); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Returns settings for [ownerId]. + @override + Future findByOwnerId(String ownerId) async { + final row = await _settingsByOwnerId(ownerId); + return row == null ? null : _settingsFromRow(row); + } + + /// Returns settings plus owner/revision metadata. + @override + Future?> findRecordByOwnerId( + String ownerId, + ) async { + final row = await _settingsByOwnerId(ownerId); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _settingsFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + ); + } + + /// Saves [settings], inserting it when absent. + @override + Future save(core.OwnerSettings settings) async { + final row = await _settingsByOwnerId(settings.ownerId); + if (row == null) { + await db.into(db.settingsTable).insert( + _settingsCompanion( + settings, + revision: core.Revision.initial, + createdAt: _epoch, + updatedAt: _epoch, + ), + ); + return; + } + await (db.update(db.settingsTable) + ..where((table) => table.ownerId.equals(settings.ownerId))) + .write( + _settingsCompanion( + settings, + revision: core.Revision(row.revision).next(), + createdAt: row.createdAtUtc, + updatedAt: _epoch, + ), + ); + } + + /// Saves [settings] only when the repository revision matches [expectedRevision]. + @override + Future saveIfRevision({ + required core.OwnerSettings settings, + required int expectedRevision, + }) async { + final row = await _settingsByOwnerId(settings.ownerId); + final failure = _compareForWrite( + row: row, + ownerId: settings.ownerId, + expectedRevision: expectedRevision, + entityId: settings.ownerId, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next(); + await (db.update(db.settingsTable) + ..where( + (table) => + table.ownerId.equals(settings.ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _settingsCompanion( + settings, + revision: nextRevision, + createdAt: row!.createdAtUtc, + updatedAt: _epoch, + ), + ); + return core.RepositoryMutationResult.success(revision: nextRevision.value); + } + + /// Returns a raw settings row by owner id. + Future _settingsByOwnerId(String ownerId) { + return (db.select(db.settingsTable) + ..where((table) => table.ownerId.equals(ownerId))) + .getSingleOrNull(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_repository.dart new file mode 100644 index 0000000..8b8fcab --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_repository.dart @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer project repository. +final class _SqliteApplicationProjectRepository + implements core.ProjectRepository { + /// Creates a project repository for [db]. + _SqliteApplicationProjectRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns a project by stable id. + @override + Future findById(String id) async { + final row = await _projectById(id); + return row == null ? null : _projectFromRow(row); + } + + /// Returns a project plus owner/revision metadata. + @override + Future?> findRecordById( + String id, + ) async { + final row = await _projectById(id); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _projectFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + archivedAt: _storedDateTime(row.archivedAtUtc), + ); + } + + /// Returns every project in deterministic name/id order. + @override + Future> findAll() async { + final rows = await (db.select(db.projects) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.name), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + return List.unmodifiable(rows.map(_projectFromRow)); + } + + /// Returns owner-scoped projects. + @override + Future> findByOwner({ + required String ownerId, + bool includeArchived = false, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await (db.select(db.projects) + ..where((table) { + final ownerClause = table.ownerId.equals(ownerId); + return includeArchived + ? ownerClause + : ownerClause & table.archivedAtUtc.isNull(); + }) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.name), + (table) => drift.OrderingTerm.asc(table.id), + ]) + ..limit( + page.limit + 1, + offset: _applicationCursorOffset(page.cursor), + )) + .get(); + return _applicationPageFromRows(rows, page, _projectFromRow); + } + + /// Saves [project], inserting it when absent. + @override + Future save(core.ProjectProfile project) async { + final row = await _projectById(project.id); + if (row == null) { + await db.into(db.projects).insert( + _projectCompanion( + project, + ownerId: core.OwnerId(defaultOwnerId), + revision: core.Revision.initial, + createdAt: _epoch, + updatedAt: _epoch, + ), + ); + return; + } + final nextRevision = core.Revision(row.revision).next(); + await (db.update(db.projects) + ..where((table) => table.id.equals(project.id))) + .write( + _projectCompanion( + project, + ownerId: core.OwnerId(row.ownerId), + revision: nextRevision, + createdAt: row.createdAtUtc, + updatedAt: _epoch, + ), + ); + } + + /// Inserts [project] for [ownerId] when its id is unused. + @override + Future insert({ + required core.ProjectProfile project, + required String ownerId, + }) async { + if (await _projectById(project.id) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: project.id, + ); + } + await db.into(db.projects).insert( + _projectCompanion( + project, + ownerId: core.OwnerId(ownerId), + revision: core.Revision.initial, + createdAt: _epoch, + updatedAt: _epoch, + ), + ); + return core.RepositoryMutationResult.success(revision: 1); + } + + /// Saves [project] only when the repository revision matches [expectedRevision]. + @override + Future saveIfRevision({ + required core.ProjectProfile project, + required String ownerId, + required int expectedRevision, + }) async { + final row = await _projectById(project.id); + final failure = _compareForWrite( + row: row, + ownerId: ownerId, + expectedRevision: expectedRevision, + entityId: project.id, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next(); + final updated = await (db.update(db.projects) + ..where( + (table) => + table.id.equals(project.id) & + table.ownerId.equals(ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _projectCompanion( + project, + ownerId: core.OwnerId(ownerId), + revision: nextRevision, + createdAt: row!.createdAtUtc, + updatedAt: _epoch, + ), + ); + if (updated != 1) { + return _mutationFailure( + core.RepositoryFailureCode.staleRevision, + entityId: project.id, + expectedRevision: expectedRevision, + actualRevision: row.revision, + ); + } + return core.RepositoryMutationResult.success(revision: nextRevision.value); + } + + /// Archives [projectId] without deleting task history. + @override + Future archive({ + required String projectId, + required String ownerId, + required int expectedRevision, + required DateTime archivedAt, + }) async { + final project = await findById(projectId); + if (project == null) { + return _mutationFailure( + core.RepositoryFailureCode.notFound, + entityId: projectId, + ); + } + return saveIfRevision( + project: project.copyWith(archivedAt: archivedAt), + ownerId: ownerId, + expectedRevision: expectedRevision, + ); + } + + /// Returns a raw project row by id. + Future _projectById(String id) { + return (db.select(db.projects)..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_statistics_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_statistics_repository.dart new file mode 100644 index 0000000..d2f43ed --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_project_statistics_repository.dart @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer project statistics repository. +final class _SqliteApplicationProjectStatisticsRepository + implements core.ProjectStatisticsRepository { + /// Creates a project statistics repository for [db]. + _SqliteApplicationProjectStatisticsRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns statistics by project id. + @override + Future findByProjectId(String projectId) async { + final row = await _statisticsByProjectId(projectId); + return row == null ? null : _projectStatisticsFromRow(row); + } + + /// Returns statistics plus owner/revision metadata. + @override + Future?> findRecordByProjectId( + String projectId) async { + final row = await _statisticsByProjectId(projectId); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _projectStatisticsFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + ); + } + + /// Returns every project statistics aggregate. + @override + Future> findAll() async { + final rows = await (db.select(db.projectStatisticsTable) + ..orderBy([(table) => drift.OrderingTerm.asc(table.projectId)])) + .get(); + return List.unmodifiable( + rows.map(_projectStatisticsFromRow), + ); + } + + /// Saves [statistics], inserting it when absent. + @override + Future save(core.ProjectStatistics statistics) async { + final row = await _statisticsByProjectId(statistics.projectId); + if (row == null) { + await db.into(db.projectStatisticsTable).insert( + _projectStatisticsCompanion( + statistics, + ownerId: defaultOwnerId, + revision: core.Revision.initial.value, + createdAt: _epoch, + updatedAt: _epoch, + ), + ); + return; + } + await (db.update(db.projectStatisticsTable) + ..where((table) => table.projectId.equals(statistics.projectId))) + .write( + _projectStatisticsCompanion( + statistics, + ownerId: row.ownerId, + revision: core.Revision(row.revision).next().value, + createdAt: row.createdAtUtc, + updatedAt: _epoch, + ), + ); + } + + /// Saves [statistics] only when the repository revision matches [expectedRevision]. + @override + Future saveIfRevision({ + required core.ProjectStatistics statistics, + required String ownerId, + required int expectedRevision, + }) async { + final row = await _statisticsByProjectId(statistics.projectId); + final failure = _compareForWrite( + row: row, + ownerId: ownerId, + expectedRevision: expectedRevision, + entityId: statistics.projectId, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next().value; + await (db.update(db.projectStatisticsTable) + ..where( + (table) => + table.projectId.equals(statistics.projectId) & + table.ownerId.equals(ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _projectStatisticsCompanion( + statistics, + ownerId: ownerId, + revision: nextRevision, + createdAt: row!.createdAtUtc, + updatedAt: _epoch, + ), + ); + return core.RepositoryMutationResult.success(revision: nextRevision); + } + + /// Returns a raw project statistics row by project id. + Future _statisticsByProjectId(String projectId) { + return (db.select(db.projectStatisticsTable) + ..where((table) => table.projectId.equals(projectId))) + .getSingleOrNull(); + } +} + +/// Builds a Drift companion for [statistics]. +ProjectStatisticsTableCompanion _projectStatisticsCompanion( + core.ProjectStatistics statistics, { + required String ownerId, + required int revision, + required DateTime createdAt, + required DateTime updatedAt, +}) { + return ProjectStatisticsTableCompanion.insert( + projectId: statistics.projectId, + ownerId: ownerId, + completedTaskCount: statistics.completedTaskCount, + durationMinuteCountsJson: _intCountMapJson( + statistics.durationMinuteCounts, + ), + completionTimeBucketCountsJson: _enumCountMapJson( + statistics.completionTimeBucketCounts, + ), + totalPushesBeforeCompletion: statistics.totalPushesBeforeCompletion, + completedAfterPushCount: statistics.completedAfterPushCount, + rewardCountsJson: _enumCountMapJson(statistics.rewardCounts), + difficultyCountsJson: _enumCountMapJson(statistics.difficultyCounts), + reminderProfileCountsJson: _enumCountMapJson( + statistics.reminderProfileCounts, + ), + revision: revision, + createdAtUtc: createdAt.toUtc(), + updatedAtUtc: updatedAt.toUtc(), + ); +} + +/// Converts a project statistics row to a core aggregate. +core.ProjectStatistics _projectStatisticsFromRow(ProjectStatisticsRow row) { + return core.ProjectStatistics( + projectId: row.projectId, + completedTaskCount: row.completedTaskCount, + durationMinuteCounts: _intCountMap(row.durationMinuteCountsJson), + completionTimeBucketCounts: _enumCountMap( + row.completionTimeBucketCountsJson, + core.ProjectCompletionTimeBucket.values, + ), + totalPushesBeforeCompletion: row.totalPushesBeforeCompletion, + completedAfterPushCount: row.completedAfterPushCount, + rewardCounts: _enumCountMap(row.rewardCountsJson, core.RewardLevel.values), + difficultyCounts: _enumCountMap( + row.difficultyCountsJson, + core.DifficultyLevel.values, + ), + reminderProfileCounts: _enumCountMap( + row.reminderProfileCountsJson, + core.ReminderProfile.values, + ), + ); +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_repositories.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_repositories.dart new file mode 100644 index 0000000..4478d9f --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_repositories.dart @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite repository set exposed inside one application unit of work. +final class _SqliteApplicationRepositories + implements core.ApplicationUnitOfWorkRepositories { + /// Creates a repository set for [db]. + _SqliteApplicationRepositories( + SchedulerDb db, { + required String defaultOwnerId, + }) : tasks = _SqliteApplicationTaskRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + projects = _SqliteApplicationProjectRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + lockedBlocks = _SqliteApplicationLockedBlockRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + schedulingSnapshots = _SqliteApplicationSnapshotRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + taskActivities = _SqliteApplicationTaskActivityRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + projectStatistics = _SqliteApplicationProjectStatisticsRepository( + db, + defaultOwnerId: defaultOwnerId, + ), + ownerSettings = _SqliteApplicationOwnerSettingsRepository(db), + noticeAcknowledgements = + _SqliteApplicationNoticeAcknowledgementRepository(db), + operations = _SqliteApplicationOperationRepository(db); + + /// Repository for task documents. + @override + final core.TaskRepository tasks; + + /// Repository for project profiles. + @override + final core.ProjectRepository projects; + + /// Repository for locked blocks and overrides. + @override + final core.LockedBlockRepository lockedBlocks; + + /// Repository for scheduling snapshots. + @override + final core.SchedulingSnapshotRepository schedulingSnapshots; + + /// Repository for task activity facts. + @override + final core.TaskActivityRepository taskActivities; + + /// Repository for project statistics aggregates. + @override + final core.ProjectStatisticsRepository projectStatistics; + + /// Repository for owner settings. + @override + final core.OwnerSettingsRepository ownerSettings; + + /// Repository for notice acknowledgements. + @override + final core.NoticeAcknowledgementRepository noticeAcknowledgements; + + /// Repository for committed application operations. + @override + final core.ApplicationOperationRepository operations; +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_runtime.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_runtime.dart new file mode 100644 index 0000000..b4a3707 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_runtime.dart @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// Owns a SQLite database handle and its application unit-of-work adapter. +final class SqliteApplicationRuntime { + /// Creates a runtime from an already-open [db]. + SqliteApplicationRuntime({ + required this.db, + String defaultOwnerId = defaultV1OwnerId, + }) : applicationStore = SqliteApplicationUnitOfWork( + db, + defaultOwnerId: defaultOwnerId, + ); + + /// Default fixed owner used by the V1 local runtime. + static const defaultV1OwnerId = 'owner-1'; + + /// Default fixed owner time zone used by the V1 local runtime. + static const defaultV1TimeZoneId = 'UTC'; + + /// Default project id used for V1 quick capture. + static const defaultV1ProjectId = 'home'; + + /// Open scheduler runtime state from the SQLite file at [path]. + static Future open({ + required String path, + String defaultOwnerId = defaultV1OwnerId, + }) async { + final file = File(path); + await file.parent.create(recursive: true); + return SqliteApplicationRuntime( + db: SchedulerDb(NativeDatabase(file)), + defaultOwnerId: defaultOwnerId, + ); + } + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Application transaction boundary backed by [db]. + final SqliteApplicationUnitOfWork applicationStore; + + /// Inserts required owner settings and default project rows when absent. + Future> bootstrap({ + String ownerId = defaultV1OwnerId, + String timeZoneId = defaultV1TimeZoneId, + String defaultProjectId = defaultV1ProjectId, + }) { + return _guardApplicationErrors(() { + return db.transaction(() async { + final repositories = _SqliteApplicationRepositories( + db, + defaultOwnerId: ownerId, + ); + final settings = await repositories.ownerSettings.findByOwnerId( + ownerId, + ); + if (settings == null) { + await repositories.ownerSettings.save( + core.OwnerSettings( + ownerId: ownerId, + timeZoneId: timeZoneId, + compactModeEnabled: true, + ), + ); + } + + final project = await repositories.projects.findById(defaultProjectId); + if (project == null) { + await repositories.projects.save( + core.ProjectProfile( + id: defaultProjectId, + name: 'Home', + colorKey: 'project-home', + ), + ); + } + + return core.ApplicationResult.success(null); + }); + }); + } + + /// Closes the underlying SQLite database. + Future close() { + return db.close(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_snapshot_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_snapshot_repository.dart new file mode 100644 index 0000000..b76f0a2 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_snapshot_repository.dart @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer scheduling snapshot repository. +final class _SqliteApplicationSnapshotRepository + implements core.SchedulingSnapshotRepository { + /// Creates a snapshot repository for [db]. + _SqliteApplicationSnapshotRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns a snapshot by stable id. + @override + Future findById(String id) async { + final row = await _snapshotById(id); + return row == null ? null : _snapshotFromRow(row); + } + + /// Returns snapshots overlapping [window]. + @override + Future> findInWindow( + core.SchedulingWindow window, + ) async { + final rows = await (db.select(db.snapshots) + ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])) + .get(); + final snapshots = rows + .map(_snapshotFromRow) + .where((snapshot) => _windowsOverlap(snapshot.window, window)) + .toList(growable: false); + return List.unmodifiable(snapshots); + } + + /// Saves [snapshot], inserting it when absent. + @override + Future save(core.SchedulingStateSnapshot snapshot) async { + final row = await _snapshotById(snapshot.id); + if (row == null) { + await db.into(db.snapshots).insert( + _snapshotCompanion( + snapshot, + ownerId: core.OwnerId(defaultOwnerId), + revision: core.Revision.initial, + ), + ); + return; + } + await (db.update(db.snapshots) + ..where((table) => table.id.equals(snapshot.id))) + .write( + _snapshotCompanion( + snapshot, + ownerId: core.OwnerId(row.ownerId), + revision: core.Revision(row.revision).next(), + ), + ); + } + + /// Returns a raw snapshot row by id. + Future _snapshotById(String id) { + return (db.select(db.snapshots)..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_task_repository.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_task_repository.dart new file mode 100644 index 0000000..9df51e7 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_task_repository.dart @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite implementation of the app-layer task repository. +final class _SqliteApplicationTaskRepository implements core.TaskRepository { + /// Creates a task repository for [db]. + _SqliteApplicationTaskRepository( + this.db, { + required this.defaultOwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used for save methods without an owner parameter. + final String defaultOwnerId; + + /// Returns a task by stable id. + @override + Future findById(String id) async { + final row = await _taskById(id); + return row == null ? null : _taskFromRow(row); + } + + /// Returns a task plus owner/revision metadata. + @override + Future?> findRecordById(String id) async { + final row = await _taskById(id); + if (row == null) { + return null; + } + return core.RepositoryRecord( + value: _taskFromRow(row), + ownerId: row.ownerId, + revision: row.revision, + ); + } + + /// Returns every task in deterministic id order. + @override + Future> findAll() async { + final rows = await (db.select(db.tasks) + ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])) + .get(); + return List.unmodifiable(rows.map(_taskFromRow)); + } + + /// Returns tasks with [status]. + @override + Future> findByStatus(core.TaskStatus status) async { + final rows = await (db.select(db.tasks) + ..where( + (table) => table.status.equals( + core.PersistenceEnumMapping.encodeTaskStatus(status), + ), + ) + ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])) + .get(); + return List.unmodifiable(rows.map(_taskFromRow)); + } + + /// Returns owner-scoped tasks in deterministic id order. + @override + Future> findByOwner({ + required String ownerId, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await _pagedTaskRows( + page: page, + where: (table) => table.ownerId.equals(ownerId), + orderBy: [(table) => drift.OrderingTerm.asc(table.id)], + ); + return _applicationPageFromRows(rows, page, _taskFromRow); + } + + /// Returns owner-scoped tasks assigned to [projectId]. + @override + Future> findByProjectId({ + required String ownerId, + required String projectId, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await _pagedTaskRows( + page: page, + where: (table) => + table.ownerId.equals(ownerId) & table.projectId.equals(projectId), + orderBy: [(table) => drift.OrderingTerm.asc(table.id)], + ); + return _applicationPageFromRows(rows, page, _taskFromRow); + } + + /// Returns owner-scoped children of [parentTaskId]. + @override + Future> findByParentTaskId({ + required String ownerId, + required String parentTaskId, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await _pagedTaskRows( + page: page, + where: (table) => + table.ownerId.equals(ownerId) & table.parentId.equals(parentTaskId), + orderBy: [(table) => drift.OrderingTerm.asc(table.id)], + ); + return _applicationPageFromRows(rows, page, _taskFromRow); + } + + /// Returns backlog candidates matching [query]. + @override + Future> findBacklogCandidates( + core.BacklogCandidateQuery query, + ) async { + final status = core.PersistenceEnumMapping.encodeTaskStatus( + core.TaskStatus.backlog, + ); + final rows = await (db.select(db.tasks) + ..where((table) { + final projectClause = query.projectId == null + ? const drift.Constant(true) + : table.projectId.equals(query.projectId!); + return table.ownerId.equals(query.ownerId) & + table.status.equals(status) & + projectClause; + }) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.createdAtUtc), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + final tasks = rows + .map(_taskFromRow) + .where((task) => query.tags.every(task.backlogTags.contains)) + .toList(growable: false); + return _applicationPage(tasks, query.page); + } + + /// Returns tasks scheduled in [window]. + @override + Future> findScheduledInWindow( + core.SchedulingWindow window, + ) async { + final rows = await _scheduledRowsInWindow(window); + return List.unmodifiable(rows.map(_taskFromRow)); + } + + /// Returns owner-scoped tasks scheduled in [window]. + @override + Future> findScheduledInWindowForOwner({ + required String ownerId, + required core.SchedulingWindow window, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) async { + final rows = await _scheduledRowsInWindow(window, ownerId: ownerId); + final tasks = rows.map(_taskFromRow).toList(growable: false); + return _applicationPage(tasks, page); + } + + /// Returns owner-scoped tasks for [localDate] in [window]. + @override + Future> findForLocalDay({ + required String ownerId, + required core.CivilDate localDate, + required core.SchedulingWindow window, + core.RepositoryPageRequest page = const core.RepositoryPageRequest(), + }) { + return findScheduledInWindowForOwner( + ownerId: ownerId, + window: window, + page: page, + ); + } + + /// Saves [task], inserting it when absent. + @override + Future save(core.Task task) async { + final row = await _taskById(task.id); + if (row == null) { + await db.into(db.tasks).insert( + _taskCompanion( + task, + ownerId: core.OwnerId(defaultOwnerId), + revision: core.Revision.initial, + ), + ); + return; + } + final nextRevision = core.Revision(row.revision).next(); + await (db.update(db.tasks)..where((table) => table.id.equals(task.id))) + .write( + _taskCompanion( + task, + ownerId: core.OwnerId(row.ownerId), + revision: nextRevision, + ), + ); + } + + /// Saves every task in [tasks]. + @override + Future saveAll(Iterable tasks) async { + for (final task in tasks) { + await save(task); + } + } + + /// Inserts [task] for [ownerId] when its id is unused. + @override + Future insert({ + required core.Task task, + required String ownerId, + }) async { + if (await _taskById(task.id) != null) { + return _mutationFailure( + core.RepositoryFailureCode.duplicateId, + entityId: task.id, + ); + } + await db.into(db.tasks).insert( + _taskCompanion( + task, + ownerId: core.OwnerId(ownerId), + revision: core.Revision.initial, + ), + ); + return core.RepositoryMutationResult.success( + revision: core.Revision.initial.value, + ); + } + + /// Saves [task] only when the repository revision matches [expectedRevision]. + @override + Future saveIfRevision({ + required core.Task task, + required String ownerId, + required int expectedRevision, + }) async { + final row = await _taskById(task.id); + final failure = _compareForWrite( + row: row, + ownerId: ownerId, + expectedRevision: expectedRevision, + entityId: task.id, + rowOwnerId: row?.ownerId, + rowRevision: row?.revision, + ); + if (failure != null) { + return failure; + } + final nextRevision = core.Revision(expectedRevision).next(); + final updated = await (db.update(db.tasks) + ..where( + (table) => + table.id.equals(task.id) & + table.ownerId.equals(ownerId) & + table.revision.equals(expectedRevision), + )) + .write( + _taskCompanion( + task, + ownerId: core.OwnerId(ownerId), + revision: nextRevision, + ), + ); + if (updated != 1) { + return _mutationFailure( + core.RepositoryFailureCode.staleRevision, + entityId: task.id, + expectedRevision: expectedRevision, + actualRevision: row?.revision, + ); + } + return core.RepositoryMutationResult.success(revision: nextRevision.value); + } + + /// Returns a raw task row by id. + Future _taskById(String id) { + return (db.select(db.tasks)..where((table) => table.id.equals(id))) + .getSingleOrNull(); + } + + /// Returns paged task rows for the supplied [where] clause. + Future> _pagedTaskRows({ + required core.RepositoryPageRequest page, + required drift.Expression Function($TasksTable table) where, + required List orderBy, + }) { + final offset = _applicationCursorOffset(page.cursor); + return (db.select(db.tasks) + ..where(where) + ..orderBy(orderBy) + ..limit(page.limit + 1, offset: offset)) + .get(); + } + + /// Returns raw scheduled task rows overlapping [window]. + Future> _scheduledRowsInWindow( + core.SchedulingWindow window, { + String? ownerId, + }) { + final start = window.start.toUtc(); + final end = window.end.toUtc(); + return (db.select(db.tasks) + ..where((table) { + final ownerClause = ownerId == null + ? const drift.Constant(true) + : table.ownerId.equals(ownerId); + return ownerClause & + table.scheduledStartUtc.isNotNull() & + table.scheduledEndUtc.isNotNull() & + table.scheduledStartUtc.isSmallerThanValue(end) & + table.scheduledEndUtc.isBiggerThanValue(start); + }) + ..orderBy([ + (table) => drift.OrderingTerm.asc(table.scheduledStartUtc), + (table) => drift.OrderingTerm.asc(table.id), + ])) + .get(); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_unit_of_work.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_unit_of_work.dart new file mode 100644 index 0000000..36bcb2e --- /dev/null +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_application/sqlite_application_unit_of_work.dart @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +part of '../sqlite_repositories.dart'; + +/// SQLite-backed application transaction boundary. +final class SqliteApplicationUnitOfWork implements core.ApplicationUnitOfWork { + /// Creates an application unit of work backed by [db]. + SqliteApplicationUnitOfWork( + this.db, { + this.defaultOwnerId = SqliteApplicationRuntime.defaultV1OwnerId, + }); + + /// Shared SQLite database handle. + final SchedulerDb db; + + /// Owner used by app-layer save methods that do not carry owner explicitly. + final String defaultOwnerId; + + /// Runs an application command atomically in a Drift transaction. + @override + Future> run({ + required core.ApplicationOperationContext context, + required String operationName, + required Future> Function( + core.ApplicationUnitOfWorkRepositories repositories, + ) action, + }) { + return _guardApplicationErrors(() { + return db.transaction(() async { + final repositories = _SqliteApplicationRepositories( + db, + defaultOwnerId: context.ownerTimeZone.ownerId, + ); + final existing = await repositories.operations.findById( + context.operationId, + ); + if (existing != null) { + return core.ApplicationResult.failure( + core.ApplicationFailure( + code: core.ApplicationFailureCode.duplicateOperation, + entityId: context.operationId, + ), + ); + } + + final result = await action(repositories); + if (result.isFailure) { + return result; + } + + final operationResult = await repositories.operations.insertIfAbsent( + core.ApplicationOperationRecord( + operationId: context.operationId, + ownerId: context.ownerTimeZone.ownerId, + operationName: operationName, + committedAt: context.now, + ), + ); + if (!operationResult.isSuccess) { + return core.ApplicationResult.failure( + core.ApplicationFailure( + code: core.ApplicationFailureCode.duplicateOperation, + entityId: context.operationId, + ), + ); + } + + return result; + }); + }); + } + + /// Runs an application read against SQLite repositories. + @override + Future> read({ + required Future> Function( + core.ApplicationUnitOfWorkRepositories repositories, + ) action, + }) { + return _guardApplicationErrors(() { + return action( + _SqliteApplicationRepositories( + db, + defaultOwnerId: defaultOwnerId, + ), + ); + }); + } +} + +/// Converts expected application exceptions into typed application failures. +Future> _guardApplicationErrors( + Future> Function() action, +) async { + try { + return await action(); + } on core.ApplicationFailureException catch (error) { + return core.ApplicationResult.failure(error.failure); + } on core.DomainValidationException catch (error) { + return core.ApplicationResult.failure( + core.ApplicationFailure( + code: core.ApplicationFailureCode.validation, + detailCode: error.code.name, + ), + ); + } on ArgumentError catch (error) { + return core.ApplicationResult.failure( + core.ApplicationFailure( + code: core.ApplicationFailureCode.validation, + detailCode: error.name, + ), + ); + } on core.ApplicationPersistenceException { + return core.ApplicationResult.failure( + const core.ApplicationFailure( + code: core.ApplicationFailureCode.persistenceFailure, + ), + ); + } on sqlite.SqliteException catch (_) { + return core.ApplicationResult.failure( + const core.ApplicationFailure( + code: core.ApplicationFailureCode.persistenceFailure, + ), + ); + } catch (_) { + return core.ApplicationResult.failure( + const core.ApplicationFailure( + code: core.ApplicationFailureCode.unexpected), + ); + } +} diff --git a/packages/scheduler_persistence_sqlite/lib/src/sqlite_repositories.dart b/packages/scheduler_persistence_sqlite/lib/src/sqlite_repositories.dart index e734091..f5ca9d7 100644 --- a/packages/scheduler_persistence_sqlite/lib/src/sqlite_repositories.dart +++ b/packages/scheduler_persistence_sqlite/lib/src/sqlite_repositories.dart @@ -5,10 +5,13 @@ library; import 'dart:convert'; +import 'dart:io'; import 'package:drift/drift.dart' as drift; +import 'package:drift/native.dart'; import 'package:scheduler_core/scheduler_core.dart' as core; import 'package:scheduler_persistence/persistence.dart'; +import 'package:sqlite3/sqlite3.dart' as sqlite; import 'scheduler_db.dart'; part 'sqlite_repositories/sqlite_task_repository.dart'; @@ -16,6 +19,19 @@ part 'sqlite_repositories/sqlite_project_repository.dart'; part 'sqlite_repositories/sqlite_locked_block_repository.dart'; part 'sqlite_repositories/sqlite_settings_repository.dart'; part 'sqlite_repositories/sqlite_schedule_snapshot_repository.dart'; +part 'sqlite_application/sqlite_application_runtime.dart'; +part 'sqlite_application/sqlite_application_unit_of_work.dart'; +part 'sqlite_application/sqlite_application_repositories.dart'; +part 'sqlite_application/sqlite_application_task_repository.dart'; +part 'sqlite_application/sqlite_application_project_repository.dart'; +part 'sqlite_application/sqlite_application_locked_block_repository.dart'; +part 'sqlite_application/sqlite_application_snapshot_repository.dart'; +part 'sqlite_application/sqlite_application_activity_repository.dart'; +part 'sqlite_application/sqlite_application_project_statistics_repository.dart'; +part 'sqlite_application/sqlite_application_owner_settings_repository.dart'; +part 'sqlite_application/sqlite_application_notice_repository.dart'; +part 'sqlite_application/sqlite_application_operation_repository.dart'; +part 'sqlite_application/sqlite_application_mapping.dart'; /// Private file-level value for `_snapshotRetention`. /// Keeping this value near the helpers that use it avoids hidden global configuration while still preventing duplicate literals. diff --git a/packages/scheduler_persistence_sqlite/pubspec.yaml b/packages/scheduler_persistence_sqlite/pubspec.yaml index 195202e..9b34113 100644 --- a/packages/scheduler_persistence_sqlite/pubspec.yaml +++ b/packages/scheduler_persistence_sqlite/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: drift: ^2.20.0 scheduler_core: any scheduler_persistence: any + sqlite3: ^3.3.3 dev_dependencies: build_runner: ^2.4.13 diff --git a/packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart b/packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart index d1c02f6..cdb00aa 100644 --- a/packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart +++ b/packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart @@ -4,18 +4,21 @@ /// Tests Scheduler Db behavior for the Scheduler Persistence Sqlite package. library; +import 'dart:io'; + import 'package:drift/native.dart'; import 'package:scheduler_persistence_sqlite/sqlite.dart'; +import 'package:sqlite3/sqlite3.dart' as sqlite; import 'package:test/test.dart'; /// Entry point for this executable Dart file. /// It wires the local command, test, or application behavior needed by this repository without exposing additional public API. void main() { - test('opens schema version 1 with expected tables', () async { + test('opens schema version 2 with expected tables', () async { final db = SchedulerDb(NativeDatabase.memory()); addTearDown(db.close); - expect(db.schemaVersion, 1); + expect(db.schemaVersion, 2); final rows = await db .customSelect( @@ -29,13 +32,207 @@ void main() { expect( tableNames, containsAll([ + 'application_operations', 'locked_blocks', 'locked_overrides', + 'notice_acknowledgements', 'projects', + 'project_statistics', 'settings', 'snapshots', + 'task_activities', 'tasks', ]), ); }); + + test('migrates schema version 1 file and preserves existing rows', () async { + final directory = await Directory.systemTemp.createTemp( + 'focus-flow-sqlite-migration-', + ); + addTearDown(() => directory.delete(recursive: true)); + final file = File('${directory.path}/scheduler.sqlite'); + _createVersionOneDatabase(file); + + final db = SchedulerDb(NativeDatabase(file)); + addTearDown(db.close); + + final rows = await db + .customSelect( + "SELECT name FROM sqlite_master " + "WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " + 'ORDER BY name', + ) + .get(); + final tableNames = rows.map((row) => row.data['name']).toList(); + expect(tableNames, contains('application_operations')); + expect(tableNames, contains('task_activities')); + expect(tableNames, contains('project_statistics')); + expect(tableNames, contains('notice_acknowledgements')); + + final taskRows = await db + .customSelect("SELECT title FROM tasks WHERE id = 'v1-task'") + .get(); + final projectRows = await db + .customSelect("SELECT name FROM projects WHERE id = 'home'") + .get(); + final settingsRows = await db + .customSelect( + "SELECT timezone_id FROM settings WHERE owner_id = 'owner-1'") + .get(); + + expect(taskRows.single.data['title'], 'Existing V1 task'); + expect(projectRows.single.data['name'], 'Home'); + expect(settingsRows.single.data['timezone_id'], 'UTC'); + }); +} + +/// Creates a minimal version-1 SQLite database file. +void _createVersionOneDatabase(File file) { + final database = sqlite.sqlite3.open(file.path); + try { + database + ..execute(''' + CREATE TABLE tasks ( + id TEXT PRIMARY KEY NOT NULL, + owner_id TEXT NOT NULL, + title TEXT NOT NULL, + project_id TEXT NOT NULL, + parent_id TEXT NULL, + type TEXT NOT NULL, + status TEXT NOT NULL, + priority TEXT NULL, + reward TEXT NOT NULL, + difficulty TEXT NOT NULL, + duration_minutes INTEGER NULL, + scheduled_start_utc INTEGER NULL, + scheduled_end_utc INTEGER NULL, + actual_start_utc INTEGER NULL, + actual_end_utc INTEGER NULL, + completed_at_utc INTEGER NULL, + backlog_tags_json TEXT NOT NULL, + reminder_override TEXT NULL, + stats_json TEXT NOT NULL, + backlog_entered_at_utc INTEGER NULL, + backlog_entered_provenance TEXT NULL, + revision INTEGER NOT NULL, + created_at_utc INTEGER NOT NULL, + updated_at_utc INTEGER NOT NULL + ) + ''') + ..execute(''' + CREATE TABLE projects ( + id TEXT PRIMARY KEY NOT NULL, + owner_id TEXT NOT NULL, + name TEXT NOT NULL, + color_key TEXT NOT NULL, + default_priority TEXT NOT NULL, + default_reward TEXT NOT NULL, + default_difficulty TEXT NOT NULL, + default_reminder_profile TEXT NOT NULL, + default_duration_minutes INTEGER NULL, + archived_at_utc INTEGER NULL, + revision INTEGER NOT NULL, + created_at_utc INTEGER NOT NULL, + updated_at_utc INTEGER NOT NULL + ) + ''') + ..execute(''' + CREATE TABLE settings ( + owner_id TEXT PRIMARY KEY NOT NULL, + timezone_id TEXT NOT NULL, + day_start_minutes INTEGER NOT NULL, + day_end_minutes INTEGER NOT NULL, + compact_mode INTEGER NOT NULL, + backlog_staleness_json TEXT NOT NULL, + revision INTEGER NOT NULL, + created_at_utc INTEGER NOT NULL, + updated_at_utc INTEGER NOT NULL + ) + ''') + ..execute(''' + INSERT INTO tasks ( + id, + owner_id, + title, + project_id, + type, + status, + reward, + difficulty, + backlog_tags_json, + stats_json, + revision, + created_at_utc, + updated_at_utc + ) VALUES ( + 'v1-task', + 'owner-1', + 'Existing V1 task', + 'home', + 'flexible', + 'backlog', + 'notSet', + 'notSet', + '[]', + '{}', + 1, + 0, + 0 + ) + ''') + ..execute(''' + INSERT INTO projects ( + id, + owner_id, + name, + color_key, + default_priority, + default_reward, + default_difficulty, + default_reminder_profile, + revision, + created_at_utc, + updated_at_utc + ) VALUES ( + 'home', + 'owner-1', + 'Home', + 'project-home', + 'medium', + 'notSet', + 'notSet', + 'none', + 1, + 0, + 0 + ) + ''') + ..execute(''' + INSERT INTO settings ( + owner_id, + timezone_id, + day_start_minutes, + day_end_minutes, + compact_mode, + backlog_staleness_json, + revision, + created_at_utc, + updated_at_utc + ) VALUES ( + 'owner-1', + 'UTC', + 0, + 1440, + 1, + '{}', + 1, + 0, + 0 + ) + ''') + ..execute('PRAGMA user_version = 1'); + } finally { + database.close(); + } } diff --git a/packages/scheduler_persistence_sqlite/test/sqlite_application_unit_of_work_test.dart b/packages/scheduler_persistence_sqlite/test/sqlite_application_unit_of_work_test.dart new file mode 100644 index 0000000..8239206 --- /dev/null +++ b/packages/scheduler_persistence_sqlite/test/sqlite_application_unit_of_work_test.dart @@ -0,0 +1,828 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + +/// Tests SQLite application unit-of-work behavior. +library; + +import 'dart:io'; + +import 'package:scheduler_core/scheduler_core.dart'; +import 'package:scheduler_persistence_sqlite/sqlite.dart'; +import 'package:test/test.dart'; + +/// Runs SQLite application unit-of-work tests. +void main() { + test('bootstrap is idempotent across close and reopen', () async { + final path = await _tempDatabasePath(); + var runtime = await _openBootstrappedRuntime(path); + + var settingsRows = await runtime.db.select(runtime.db.settingsTable).get(); + var projectRows = await runtime.db.select(runtime.db.projects).get(); + expect(settingsRows, hasLength(1)); + expect(projectRows, hasLength(1)); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + addTearDown(runtime.close); + settingsRows = await runtime.db.select(runtime.db.settingsTable).get(); + projectRows = await runtime.db.select(runtime.db.projects).get(); + expect(settingsRows, hasLength(1)); + expect(projectRows, hasLength(1)); + }); + + test('quick capture schedule complete and uncomplete survive reopen', + () async { + final path = await _tempDatabasePath(); + var runtime = await _openBootstrappedRuntime(path); + var commands = _commands(runtime); + var management = _management(runtime); + var todayQuery = _todayQuery(runtime); + + final capture = await commands.quickCaptureToBacklog( + context: _context('capture-task', _instant(8)), + title: 'Persist close reopen task', + projectId: SqliteApplicationRuntime.defaultV1ProjectId, + ); + expect(capture.isSuccess, isTrue); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + commands = _commands(runtime); + management = _management(runtime); + todayQuery = _todayQuery(runtime); + final backlog = await management.getBacklog( + GetBacklogRequest( + context: _context('read-backlog-after-capture', _instant(8, 5)), + sortKey: BacklogSortKey.age, + ), + ); + expect(backlog.requireValue.items, hasLength(1)); + final task = backlog.requireValue.items.single.task; + expect(task.title, 'Persist close reopen task'); + + final scheduled = await commands.scheduleBacklogItemToNextAvailableSlot( + context: _context('schedule-task', _instant(8, 10)), + localDate: _date, + taskId: task.id, + durationMinutes: 30, + expectedUpdatedAt: task.updatedAt, + ); + expect(scheduled.isSuccess, isTrue); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + commands = _commands(runtime); + todayQuery = _todayQuery(runtime); + var today = await todayQuery.execute( + GetTodayStateRequest( + context: _context('read-today-after-schedule', _instant(8, 15)), + date: _date, + ), + ); + var scheduledTask = today.requireValue.timelineItems + .singleWhere((item) => item.taskId == task.id); + expect(scheduledTask.taskStatus, TaskStatus.planned); + + final completed = await commands.completeFlexibleTask( + context: _context('complete-task', _instant(8, 20)), + localDate: _date, + taskId: task.id, + ); + expect(completed.isSuccess, isTrue); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + commands = _commands(runtime); + todayQuery = _todayQuery(runtime); + today = await todayQuery.execute( + GetTodayStateRequest( + context: _context('read-today-after-complete', _instant(8, 25)), + date: _date, + ), + ); + scheduledTask = today.requireValue.timelineItems + .singleWhere((item) => item.taskId == task.id); + expect(scheduledTask.taskStatus, TaskStatus.completed); + + final reopened = await commands.uncompleteTask( + context: _context('uncomplete-task', _instant(8, 30)), + localDate: _date, + taskId: task.id, + ); + expect(reopened.isSuccess, isTrue); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + addTearDown(runtime.close); + todayQuery = _todayQuery(runtime); + today = await todayQuery.execute( + GetTodayStateRequest( + context: _context('read-today-after-uncomplete', _instant(8, 35)), + date: _date, + ), + ); + scheduledTask = today.requireValue.timelineItems + .singleWhere((item) => item.taskId == task.id); + expect(scheduledTask.taskStatus, TaskStatus.planned); + + final activityRows = + await runtime.db.select(runtime.db.taskActivities).get(); + final operationRows = + await runtime.db.select(runtime.db.applicationOperations).get(); + expect(activityRows, isNotEmpty); + expect( + operationRows.map((row) => row.operationId), contains('capture-task')); + }); + + test('failed command rolls back task and operation writes', () async { + final path = await _tempDatabasePath(); + final runtime = await _openBootstrappedRuntime(path); + addTearDown(runtime.close); + final commands = _commands(runtime); + + final result = await commands.quickCaptureToBacklog( + context: _context('invalid-capture', _instant(9)), + title: '', + projectId: SqliteApplicationRuntime.defaultV1ProjectId, + ); + + expect(result.isFailure, isTrue); + final taskRows = await runtime.db.select(runtime.db.tasks).get(); + final operationRows = + await runtime.db.select(runtime.db.applicationOperations).get(); + expect(taskRows, isEmpty); + expect(operationRows, isEmpty); + }); + + test('duplicate operation is rejected after reopen', () async { + final path = await _tempDatabasePath(); + var runtime = await _openBootstrappedRuntime(path); + var commands = _commands(runtime); + + final first = await commands.quickCaptureToBacklog( + context: _context('duplicate-capture', _instant(10)), + title: 'Only once', + projectId: SqliteApplicationRuntime.defaultV1ProjectId, + ); + expect(first.isSuccess, isTrue); + await runtime.close(); + + runtime = await _openBootstrappedRuntime(path); + addTearDown(runtime.close); + commands = _commands(runtime); + final second = await commands.quickCaptureToBacklog( + context: _context('duplicate-capture', _instant(10, 5)), + title: 'Should not persist', + projectId: SqliteApplicationRuntime.defaultV1ProjectId, + ); + + expect(second.failure?.code, ApplicationFailureCode.duplicateOperation); + final taskRows = await runtime.db.select(runtime.db.tasks).get(); + expect(taskRows, hasLength(1)); + expect(taskRows.single.title, 'Only once'); + }); + + test('application repositories persist owner scoped records and conflicts', + () async { + final path = await _tempDatabasePath(); + final runtime = await SqliteApplicationRuntime.open(path: path); + addTearDown(runtime.close); + const ownerId = SqliteApplicationRuntime.defaultV1OwnerId; + final now = _instant(11); + final project = _project('project-alpha', 'Alpha'); + final scheduledTask = _task( + id: 'task-scheduled', + title: 'Scheduled task', + status: TaskStatus.planned, + scheduledStart: _instant(8), + scheduledEnd: _instant(8, 30), + ); + final backlogTask = _task( + id: 'task-backlog', + title: 'Backlog task', + status: TaskStatus.backlog, + backlogTags: {BacklogTag.wishlist}, + ); + final childTask = _task( + id: 'task-child', + title: 'Child task', + status: TaskStatus.backlog, + parentTaskId: backlogTask.id, + ); + final block = LockedBlock( + id: 'block-work', + name: 'Work block', + date: _date, + startTime: WallTime(hour: 9, minute: 0), + endTime: WallTime(hour: 10, minute: 0), + hiddenByDefault: false, + projectId: project.id, + createdAt: now, + updatedAt: now, + ); + final override = LockedBlockOverride.add( + id: 'override-errand', + date: _date, + name: 'Errand', + startTime: WallTime(hour: 13, minute: 0), + endTime: WallTime(hour: 13, minute: 30), + projectId: project.id, + createdAt: now, + ); + final snapshot = SchedulingStateSnapshot( + id: 'snapshot-1', + capturedAt: now, + window: _window(8, 12), + tasks: [scheduledTask], + lockedIntervals: [ + TimeInterval( + start: _instant(9), + end: _instant(10), + label: block.name, + ), + ], + operationName: 'coverage-snapshot', + ); + final activity = TaskActivity( + id: 'activity-1', + operationId: 'repository-surface', + code: TaskActivityCode.completed, + taskId: scheduledTask.id, + projectId: project.id, + occurredAt: now, + metadata: { + 'completedAt': now, + 'reward': RewardLevel.high, + 'labels': ['done', 1], + 'nested': {'ok': true}, + }, + ); + final statistics = ProjectStatistics( + projectId: project.id, + completedTaskCount: 2, + durationMinuteCounts: {30: 2}, + completionTimeBucketCounts: { + ProjectCompletionTimeBucket.morning: 2, + }, + totalPushesBeforeCompletion: 3, + completedAfterPushCount: 1, + rewardCounts: {RewardLevel.high: 2}, + difficultyCounts: {DifficultyLevel.easy: 1}, + reminderProfileCounts: {ReminderProfile.gentle: 2}, + ); + final settings = OwnerSettings( + ownerId: ownerId, + timeZoneId: 'UTC', + dayStart: WallTime(hour: 7, minute: 30), + dayEnd: WallTime(hour: 22, minute: 0), + compactModeEnabled: true, + backlogStalenessSettings: const BacklogStalenessSettings( + greenMaxAge: Duration(days: 3), + blueMaxAge: Duration(days: 10), + ), + ); + final notice = NoticeAcknowledgementRecord( + ownerId: ownerId, + noticeId: 'notice-rollover', + acknowledgedAt: now, + ); + + final write = await runtime.applicationStore.run>( + context: _context('repository-surface', now), + operationName: 'repository-surface', + action: (repositories) async { + await repositories.ownerSettings.save(settings); + final settingsUpdate = await repositories.ownerSettings.saveIfRevision( + settings: settings.copyWith(compactModeEnabled: false), + expectedRevision: 1, + ); + final settingsStale = await repositories.ownerSettings.saveIfRevision( + settings: settings, + expectedRevision: 1, + ); + + final projectInsert = await repositories.projects.insert( + project: project, + ownerId: ownerId, + ); + final projectDuplicate = await repositories.projects.insert( + project: project, + ownerId: ownerId, + ); + final projectUpdate = await repositories.projects.saveIfRevision( + project: project.copyWith(name: 'Alpha Renamed'), + ownerId: ownerId, + expectedRevision: projectInsert.revision!, + ); + final projectArchive = await repositories.projects.archive( + projectId: project.id, + ownerId: ownerId, + expectedRevision: projectUpdate.revision!, + archivedAt: _instant(11, 5), + ); + final projectMissingArchive = await repositories.projects.archive( + projectId: 'missing-project', + ownerId: ownerId, + expectedRevision: 1, + archivedAt: _instant(11, 5), + ); + + final taskInsert = await repositories.tasks.insert( + task: scheduledTask, + ownerId: ownerId, + ); + final taskDuplicate = await repositories.tasks.insert( + task: scheduledTask, + ownerId: ownerId, + ); + final taskUpdate = await repositories.tasks.saveIfRevision( + task: scheduledTask.copyWith(title: 'Scheduled task updated'), + ownerId: ownerId, + expectedRevision: taskInsert.revision!, + ); + await repositories.tasks.saveAll([backlogTask, childTask]); + await repositories.tasks.save( + backlogTask.copyWith(title: 'Backlog task updated'), + ); + + final blockInsert = await repositories.lockedBlocks.insertBlock( + block: block, + ownerId: ownerId, + ); + final blockDuplicate = await repositories.lockedBlocks.insertBlock( + block: block, + ownerId: ownerId, + ); + final blockUpdate = await repositories.lockedBlocks.saveBlockIfRevision( + block: block.copyWith(name: 'Work block renamed'), + ownerId: ownerId, + expectedRevision: blockInsert.revision!, + ); + final blockArchive = await repositories.lockedBlocks.archiveBlock( + blockId: block.id, + ownerId: ownerId, + expectedRevision: blockUpdate.revision!, + archivedAt: _instant(11, 10), + ); + + final overrideInsert = await repositories.lockedBlocks.insertOverride( + override: override, + ownerId: ownerId, + ); + final overrideDuplicate = + await repositories.lockedBlocks.insertOverride( + override: override, + ownerId: ownerId, + ); + final overrideUpdate = + await repositories.lockedBlocks.saveOverrideIfRevision( + override: LockedBlockOverride.add( + id: override.id, + date: _date, + name: 'Errand moved', + startTime: WallTime(hour: 14, minute: 0), + endTime: WallTime(hour: 14, minute: 30), + projectId: project.id, + createdAt: now, + updatedAt: _instant(11, 15), + ), + ownerId: ownerId, + expectedRevision: overrideInsert.revision!, + ); + + await repositories.schedulingSnapshots.save(snapshot); + await repositories.schedulingSnapshots.save( + SchedulingStateSnapshot( + id: snapshot.id, + capturedAt: _instant(11, 20), + window: snapshot.window, + tasks: [scheduledTask, backlogTask], + operationName: 'coverage-snapshot-updated', + ), + ); + + final activityAppend = await repositories.taskActivities.append( + activity: activity, + ownerId: ownerId, + ); + final activityDuplicate = await repositories.taskActivities.append( + activity: activity, + ownerId: ownerId, + ); + await repositories.taskActivities.save( + TaskActivity( + id: activity.id, + operationId: activity.operationId, + code: activity.code, + taskId: activity.taskId, + projectId: activity.projectId, + occurredAt: _instant(11, 30), + metadata: {'updated': true}, + ), + ); + + await repositories.projectStatistics.save(statistics); + final statisticsUpdate = + await repositories.projectStatistics.saveIfRevision( + statistics: statistics.copyWith(completedTaskCount: 3), + ownerId: ownerId, + expectedRevision: 1, + ); + final statisticsOwnerMismatch = + await repositories.projectStatistics.saveIfRevision( + statistics: statistics, + ownerId: 'other-owner', + expectedRevision: statisticsUpdate.revision!, + ); + + final noticeInsert = + await repositories.noticeAcknowledgements.insert(notice); + final noticeDuplicate = + await repositories.noticeAcknowledgements.insert(notice); + await repositories.noticeAcknowledgements.save( + NoticeAcknowledgementRecord( + ownerId: ownerId, + noticeId: notice.noticeId, + acknowledgedAt: _instant(11, 40), + ), + ); + + return ApplicationResult.success({ + 'settingsRevision': settingsUpdate.revision, + 'settingsStale': settingsStale.failure?.code, + 'projectRevision': projectArchive.revision, + 'projectDuplicate': projectDuplicate.failure?.code, + 'projectMissingArchive': projectMissingArchive.failure?.code, + 'taskRevision': taskUpdate.revision, + 'taskDuplicate': taskDuplicate.failure?.code, + 'blockRevision': blockArchive.revision, + 'blockDuplicate': blockDuplicate.failure?.code, + 'overrideRevision': overrideUpdate.revision, + 'overrideDuplicate': overrideDuplicate.failure?.code, + 'activityRevision': activityAppend.revision, + 'activityDuplicate': activityDuplicate.failure?.code, + 'statisticsRevision': statisticsUpdate.revision, + 'statisticsOwnerMismatch': statisticsOwnerMismatch.failure?.code, + 'noticeRevision': noticeInsert.revision, + 'noticeDuplicate': noticeDuplicate.failure?.code, + }); + }, + ); + + final writeSummary = write.requireValue; + expect(writeSummary['settingsRevision'], 2); + expect(writeSummary['settingsStale'], RepositoryFailureCode.staleRevision); + expect(writeSummary['projectRevision'], 3); + expect( + writeSummary['projectDuplicate'], + RepositoryFailureCode.duplicateId, + ); + expect( + writeSummary['projectMissingArchive'], + RepositoryFailureCode.notFound, + ); + expect(writeSummary['taskRevision'], 2); + expect(writeSummary['taskDuplicate'], RepositoryFailureCode.duplicateId); + expect(writeSummary['blockRevision'], 3); + expect(writeSummary['blockDuplicate'], RepositoryFailureCode.duplicateId); + expect(writeSummary['overrideRevision'], 2); + expect( + writeSummary['overrideDuplicate'], + RepositoryFailureCode.duplicateId, + ); + expect(writeSummary['activityRevision'], 1); + expect( + writeSummary['activityDuplicate'], + RepositoryFailureCode.duplicateId, + ); + expect(writeSummary['statisticsRevision'], 2); + expect( + writeSummary['statisticsOwnerMismatch'], + RepositoryFailureCode.ownerMismatch, + ); + expect(writeSummary['noticeRevision'], 1); + expect(writeSummary['noticeDuplicate'], RepositoryFailureCode.duplicateId); + + final read = await runtime.applicationStore.read>( + action: (repositories) async { + final taskPage = await repositories.tasks.findByOwner( + ownerId: ownerId, + page: const RepositoryPageRequest(limit: 2), + ); + final taskPage2 = await repositories.tasks.findByOwner( + ownerId: ownerId, + page: RepositoryPageRequest(cursor: taskPage.nextCursor, limit: 2), + ); + final backlogCandidates = + await repositories.tasks.findBacklogCandidates( + const BacklogCandidateQuery( + ownerId: ownerId, + projectId: 'project-alpha', + tags: {BacklogTag.wishlist}, + ), + ); + final projectActive = await repositories.projects.findByOwner( + ownerId: ownerId, + ); + final projectArchived = await repositories.projects.findByOwner( + ownerId: ownerId, + includeArchived: true, + ); + final blocksActive = await repositories.lockedBlocks.findBlocksByOwner( + ownerId: ownerId, + ); + final blocksArchived = + await repositories.lockedBlocks.findBlocksByOwner( + ownerId: ownerId, + includeArchived: true, + ); + final overridesForDate = + await repositories.lockedBlocks.findOverridesForDate( + ownerId: ownerId, + date: _date, + ); + final scheduled = await repositories.tasks.findScheduledInWindow( + _window(7, 9), + ); + final scheduledForOwner = + await repositories.tasks.findScheduledInWindowForOwner( + ownerId: ownerId, + window: _window(7, 9), + page: const RepositoryPageRequest(cursor: 'bad-cursor', limit: 1), + ); + final snapshotsInWindow = + await repositories.schedulingSnapshots.findInWindow(_window(7, 9)); + final snapshotsOutside = await repositories.schedulingSnapshots + .findInWindow(_window(16, 17)); + final activitiesByOwner = await repositories.taskActivities.findByOwner( + ownerId: ownerId, + ); + final noticePage = + await repositories.noticeAcknowledgements.findByOwner( + ownerId: ownerId, + page: const RepositoryPageRequest(limit: 1), + ); + final operation = await repositories.operations.findById( + 'repository-surface', + ); + final idempotency = await repositories.operations.findByIdempotencyKey( + ownerId: ownerId, + operationId: 'repository-surface', + ); + + return ApplicationResult.success({ + 'taskPageCount': taskPage.items.length, + 'taskPage2Count': taskPage2.items.length, + 'taskNextCursor': taskPage.nextCursor, + 'taskByIdTitle': + (await repositories.tasks.findById(scheduledTask.id))?.title, + 'taskRecordRevision': (await repositories.tasks.findRecordById( + scheduledTask.id, + )) + ?.revision, + 'plannedCount': + (await repositories.tasks.findByStatus(TaskStatus.planned)) + .length, + 'projectTaskCount': (await repositories.tasks.findByProjectId( + ownerId: ownerId, + projectId: project.id, + )) + .items + .length, + 'childTaskCount': (await repositories.tasks.findByParentTaskId( + ownerId: ownerId, + parentTaskId: backlogTask.id, + )) + .items + .length, + 'backlogCandidateTitle': backlogCandidates.items.single.title, + 'scheduledCount': scheduled.length, + 'scheduledForOwnerCount': scheduledForOwner.items.length, + 'activeProjectCount': projectActive.items.length, + 'archivedProjectCount': projectArchived.items.length, + 'projectRecordRevision': (await repositories.projects.findRecordById( + project.id, + )) + ?.revision, + 'allProjectCount': (await repositories.projects.findAll()).length, + 'activeBlockCount': blocksActive.items.length, + 'archivedBlockCount': blocksArchived.items.length, + 'blockRecordRevision': + (await repositories.lockedBlocks.findBlockRecordById(block.id)) + ?.revision, + 'allBlockCount': + (await repositories.lockedBlocks.findAllBlocks()).length, + 'overrideRecordRevision': (await repositories.lockedBlocks + .findOverrideRecordById(override.id)) + ?.revision, + 'overrideCount': overridesForDate.items.length, + 'allOverrideCount': + (await repositories.lockedBlocks.findAllOverrides()).length, + 'snapshotTaskCount': + (await repositories.schedulingSnapshots.findById(snapshot.id)) + ?.tasks + .length, + 'snapshotInWindowCount': snapshotsInWindow.length, + 'snapshotOutsideCount': snapshotsOutside.length, + 'activityMetadataUpdated': + (await repositories.taskActivities.findById(activity.id)) + ?.metadata['updated'], + 'activityByOperationCount': (await repositories.taskActivities + .findByOperationId(activity.operationId)) + .length, + 'activityByTaskCount': + (await repositories.taskActivities.findByTaskId(activity.taskId)) + .length, + 'activityByProjectCount': (await repositories.taskActivities + .findByProjectId(activity.projectId)) + .length, + 'activityByOwnerCount': activitiesByOwner.items.length, + 'activityAllCount': + (await repositories.taskActivities.findAll()).length, + 'statisticsCompleted': + (await repositories.projectStatistics.findByProjectId(project.id)) + ?.completedTaskCount, + 'statisticsRecordRevision': (await repositories.projectStatistics + .findRecordByProjectId(project.id)) + ?.revision, + 'statisticsAllCount': + (await repositories.projectStatistics.findAll()).length, + 'settingsCompact': + (await repositories.ownerSettings.findByOwnerId(ownerId)) + ?.compactModeEnabled, + 'settingsRevision': + (await repositories.ownerSettings.findRecordByOwnerId(ownerId)) + ?.revision, + 'noticeAcknowledgedAt': + (await repositories.noticeAcknowledgements.findById( + ownerId: ownerId, + noticeId: notice.noticeId, + )) + ?.acknowledgedAt, + 'noticeOwnerCount': + (await repositories.noticeAcknowledgements.findByOwnerId(ownerId)) + .length, + 'noticePageCount': noticePage.items.length, + 'operationName': operation?.operationName, + 'idempotencyName': idempotency?.operationName, + }); + }, + ); + + final readSummary = read.requireValue; + expect(readSummary['taskPageCount'], 2); + expect(readSummary['taskPage2Count'], 1); + expect(readSummary['taskNextCursor'], '2'); + expect(readSummary['taskByIdTitle'], 'Scheduled task updated'); + expect(readSummary['taskRecordRevision'], 2); + expect(readSummary['plannedCount'], 1); + expect(readSummary['projectTaskCount'], 3); + expect(readSummary['childTaskCount'], 1); + expect(readSummary['backlogCandidateTitle'], 'Backlog task updated'); + expect(readSummary['scheduledCount'], 1); + expect(readSummary['scheduledForOwnerCount'], 1); + expect(readSummary['activeProjectCount'], 0); + expect(readSummary['archivedProjectCount'], 1); + expect(readSummary['projectRecordRevision'], 3); + expect(readSummary['allProjectCount'], 1); + expect(readSummary['activeBlockCount'], 0); + expect(readSummary['archivedBlockCount'], 1); + expect(readSummary['blockRecordRevision'], 3); + expect(readSummary['allBlockCount'], 1); + expect(readSummary['overrideRecordRevision'], 2); + expect(readSummary['overrideCount'], 1); + expect(readSummary['allOverrideCount'], 1); + expect(readSummary['snapshotTaskCount'], 2); + expect(readSummary['snapshotInWindowCount'], 1); + expect(readSummary['snapshotOutsideCount'], 0); + expect(readSummary['activityMetadataUpdated'], isTrue); + expect(readSummary['activityByOperationCount'], 1); + expect(readSummary['activityByTaskCount'], 1); + expect(readSummary['activityByProjectCount'], 1); + expect(readSummary['activityByOwnerCount'], 1); + expect(readSummary['activityAllCount'], 1); + expect(readSummary['statisticsCompleted'], 3); + expect(readSummary['statisticsRecordRevision'], 2); + expect(readSummary['statisticsAllCount'], 1); + expect(readSummary['settingsCompact'], isFalse); + expect(readSummary['settingsRevision'], 2); + expect(readSummary['noticeAcknowledgedAt'], _instant(11, 40)); + expect(readSummary['noticeOwnerCount'], 1); + expect(readSummary['noticePageCount'], 1); + expect(readSummary['operationName'], 'repository-surface'); + expect(readSummary['idempotencyName'], 'repository-surface'); + }); +} + +/// Fixed date used by lifecycle tests. +final _date = CivilDate(2026, 1, 2); + +/// Returns an instant on the fixed lifecycle date. +DateTime _instant(int hour, [int minute = 0]) { + return DateTime.utc(_date.year, _date.month, _date.day, hour, minute); +} + +/// Opens and bootstraps a runtime at [path]. +Future _openBootstrappedRuntime(String path) async { + final runtime = await SqliteApplicationRuntime.open(path: path); + final bootstrap = await runtime.bootstrap(); + expect(bootstrap.isSuccess, isTrue); + return runtime; +} + +/// Creates command use cases for [runtime]. +V1ApplicationCommandUseCases _commands(SqliteApplicationRuntime runtime) { + return V1ApplicationCommandUseCases( + applicationStore: runtime.applicationStore, + timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), + ); +} + +/// Creates management use cases for [runtime]. +V1ApplicationManagementUseCases _management(SqliteApplicationRuntime runtime) { + return V1ApplicationManagementUseCases( + applicationStore: runtime.applicationStore, + ); +} + +/// Creates the Today read query for [runtime]. +GetTodayStateQuery _todayQuery(SqliteApplicationRuntime runtime) { + return GetTodayStateQuery( + applicationStore: runtime.applicationStore, + timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), + ); +} + +/// Creates an operation context for [operationId]. +ApplicationOperationContext _context(String operationId, DateTime now) { + return ApplicationOperationContext.start( + operationId: operationId, + ownerTimeZone: OwnerTimeZoneContext( + ownerId: SqliteApplicationRuntime.defaultV1OwnerId, + timeZoneId: SqliteApplicationRuntime.defaultV1TimeZoneId, + ), + clock: FixedClock(now), + idGenerator: SequentialIdGenerator(prefix: operationId), + ); +} + +/// Returns a fresh temporary SQLite path. +Future _tempDatabasePath() async { + final directory = await Directory.systemTemp.createTemp( + 'focus-flow-sqlite-app-', + ); + addTearDown(() => directory.delete(recursive: true)); + return '${directory.path}/scheduler.sqlite'; +} + +/// Creates a project profile fixture. +ProjectProfile _project(String id, String name) { + return ProjectProfile( + id: id, + name: name, + colorKey: 'project-$id', + defaultPriority: PriorityLevel.high, + defaultReward: RewardLevel.high, + defaultDifficulty: DifficultyLevel.easy, + defaultReminderProfile: ReminderProfile.gentle, + defaultDurationMinutes: 30, + ); +} + +/// Creates a task fixture. +Task _task({ + required String id, + required String title, + required TaskStatus status, + DateTime? scheduledStart, + DateTime? scheduledEnd, + String? parentTaskId, + Set backlogTags = const {}, +}) { + return Task( + id: id, + title: title, + projectId: 'project-alpha', + type: TaskType.flexible, + status: status, + priority: PriorityLevel.medium, + reward: RewardLevel.high, + difficulty: DifficultyLevel.easy, + durationMinutes: 30, + scheduledStart: scheduledStart, + scheduledEnd: scheduledEnd, + parentTaskId: parentTaskId, + backlogTags: backlogTags, + reminderOverride: ReminderProfile.gentle, + createdAt: _instant(7), + updatedAt: _instant(7), + ); +} + +/// Creates a scheduling window on the fixed test date. +SchedulingWindow _window(int startHour, int endHour) { + return SchedulingWindow( + start: _instant(startHour), + end: _instant(endHour), + ); +} diff --git a/test/scheduler_core_test.dart b/test/scheduler_core_test.dart index bba78f8..316e710 100644 --- a/test/scheduler_core_test.dart +++ b/test/scheduler_core_test.dart @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 FocusFlow contributors +// SPDX-License-Identifier: AGPL-3.0-only + /// Aggregates Scheduler Core workspace tests. library; @@ -69,6 +72,10 @@ import '../packages/scheduler_persistence_memory/test/memory_repository_conforma as memory_repository_conformance_test; import '../packages/scheduler_persistence_sqlite/test/sqlite_repository_conformance_test.dart' as sqlite_repository_conformance_test; +import '../packages/scheduler_persistence_sqlite/test/scheduler_db_test.dart' + as sqlite_scheduler_db_test; +import '../packages/scheduler_persistence_sqlite/test/sqlite_application_unit_of_work_test.dart' + as sqlite_application_unit_of_work_test; /// Entry point for this executable Dart file. /// It wires the local command, test, or application behavior needed by this repository without exposing additional public API. @@ -107,4 +114,6 @@ void main() { repo_contract_smoke_test.main(); memory_repository_conformance_test.main(); sqlite_repository_conformance_test.main(); + sqlite_scheduler_db_test.main(); + sqlite_application_unit_of_work_test.main(); }