feat(storage,app): PostgreSQL backend (D3) + project manager window (D4)

D3: oakdb+pg:// fully wired (shared sea-orm entities, BIGSERIAL DDL,
connect-probe instead of pool retry on dead servers); Storage/Backend=pg
+ Storage/PgUrl config; 13 OAK_TEST_PG_URL-gated PG tests (verified
against a Docker postgres:16), always-on clean-error tests otherwise.

D4: DaVinci-style project manager — list with derived stats, create/
rename/duplicate/delete (confirm)/import/export (native dialogs,
ove/otio/fcpxml), shown at startup and from the file menu; facade
oakengine_library_* exports (list/create/delete/rename/duplicate/
import/export + project_load_library that binds write-through);
save/save-as menu becomes 'export project file', open splits into
from-library/from-file; status bar shows library write state; storage
activates on app start and flushes on exit; spawn_modal reentrancy
fixed (window-callback path) with a doc note.

Also: the P1 audio test's environment probe was lost in the ffi purge;
restored on cpal (the output device is cpal now).
This commit is contained in:
2026-08-16 10:39:31 +08:00
parent 5fabad8efd
commit 025dc88c25
32 changed files with 4711 additions and 657 deletions
+29 -2
View File
@@ -91,12 +91,39 @@ removed clips.
or otio backend; nothing is read from or written to the database
beyond the current state.
## Configuration
Library selection and behavior are driven by the `Storage` config group
(read by `crates/oakengine/src/storage.rs`):
- `Storage/Backend``"sqlite"` (the documented default), `"database"`
or `"pg"` enables write-through; any other value (e.g. `"off"`)
disables it. When the key is absent no library is configured and
projects stay unbound (headless consumers and the test suite never
touch the user's real library).
- `Storage/SqlitePath` — the SQLite library file; default
`<system data dir>/library.db` (honoring `OAK_CONFIG_DIR`).
- `Storage/PgUrl` — the PostgreSQL connection string, used when
`Backend = "pg"`: `user:pass@host:5432/dbname` (libpq URL form; an
optional `postgres://`/`postgresql://` scheme is stripped). The
resolved library URI is `oakdb+pg://<PgUrl>`.
- `Storage/SnapshotIntervalSec` (default 600) and
`Storage/JournalRetentionDays` (default 0 = keep forever) — see above.
## Multi-writer and platforms
v1 assumes a single writer per database (SQLite `busy_timeout`, PG row
locks). Multi-writer collaboration is future work (M14). The default
database is a single user-level SQLite file; PostgreSQL is selected
with an `oakdb+pg://` URI.
database is a single user-level SQLite file; PostgreSQL is selected with
`Storage/Backend = "pg"` + `Storage/PgUrl`, or directly with an
`oakdb+pg://` URI.
Database tests: `cargo test -p oakstorage` is green without PostgreSQL —
the SQLite suite always runs; the PG suite (`tests/database_pg_test.rs`)
connects to a real server when `OAK_TEST_PG_URL` is set (e.g.
`postgres://user:pass@host:5432/db`) and skips with a note otherwise.
The URL should point at a dedicated test database: each test resets the
four tables.
See also: [M10 oakstorage manual](plans/riir/M10-oakstorage.md),
[M13 write-through plan](plans/riir/M13-storage-live.md),
Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 438 KiB

+42
View File
@@ -109,6 +109,24 @@ facade 的 undo 推送路径挂钩(`oakengine_undo_push` / `undo_group_end`
> 键缺失 = "无库配置"(工程不绑定、写穿不触发)——这是 §2"无库配置优雅
> 降级"的默认形态,保证 headless 消费者(oak-cli)与测试进程永远不写
> 用户的真实库。app 侧(D4/D5)在启动时显式设置该配置即可启用。
>
> D3 落地记录(2026-08):`oakdb+pg://` 全量走通(load/save/load_at/
> snapshot/list/delete/duplicate/rename/export/import)。方言差异收敛在
> 连接与 migration 两层:`crates/oakstorage/src/backends/database/
> migration.rs` 按 `DatabaseBackend` 选 SQLite/PG DDLPG 仅 `BIGSERIAL`
> PK + `BIGINT` FK,其余同构;`CREATE TABLE IF NOT EXISTS` 幂等,每次
> 连接时执行;payload 维持 TEXT,不做 BYTEA);sea-orm 实体与 save/replay
> 逻辑两库共用零分支。`connect_pg` 先单次直连探测(pool 对被拒连接会
> 退避重试到 acquire 超时,死库会挂起数秒),失败映射干净 E_IO。
> **配置**`Storage/Backend = "pg"` 启用,`Storage/PgUrl` 给连接串
> `user:pass@host:5432/dbname`,容忍 `postgres://` 前缀,oakdb uri
> 剥掉);URI 的 `?project=` 选择器只在 query 含 `project=` 键时生效,
> 否则整段(含 `?sslmode=…`)视为连接串。**测试矩阵**SQLite 18 个
> 常驻;PG 13 个变体(round-trip/journal 语义/快照重放/撤销/清理/管理
> API/导入导出/选择)门控 `OAK_TEST_PG_URL`,未设置则早退打印说明
> (CI 无 PG 也绿);非法连接串(E_INVALIDparse 期)+ 连不上(E_IO
> 单次探测)为常驻错误路径测试,无需真实 PG。共享 fixture 抽到
> `tests/common/mod.rs`(建 fixture/保存加载/行检查 sqlite+pg 双探针)。
## 4. 项目管理器窗口(app
@@ -119,6 +137,30 @@ facade 的 undo 推送路径挂钩(`oakengine_undo_push` / `undo_group_end`
- 导入 .ove/.otio/.fcpxml 为新库行;选中工程导出为 .ove/.otio/.fcpxml。
- 数据源走 oakstorage 会话 APIRust 直调;需要 C ABI 时 facade 只增)。
> D4 落地记录(2026-08):app 只链接 `liboakengine` dylib,故数据源走
> 新增 C ABI`crates/oakengine/src/library.rs`,只增):
> `oakengine_library_list`JSON 行:uuid/name/created/modified + 派生
> 统计)/ `_create` / `_delete` / `_rename` / `_duplicate` / `_import`
> / `_export`(按扩展名走 oakstorage registry 分发 ove-xml 或 otio 后端)
> / `oakengine_project_load_library`(载入并**绑定**写穿会话,与
> `project_load` 同契约)。有副作用的 create/duplicate/import 返回 uuid
> 用单次定长缓冲调用(不能两段式 measure-then-read——会在 C 侧执行两次)。
> app 侧:`src/manager.rs``ProjectManager` 内容视图 + `ManagerEvent`
> 请求枚举 + `NamePrompt`/`ConfirmContent` 子对话框;时间/时长格式化为
> 无依赖纯函数)。`src/app.rs`:无 `--project` 启动时开管理器(驱动根
> entity,不能走 `WindowHandle::update`——`spawn_modal` 的
> `update_window` 会重入失败);`run_with` 启动时
> `real::configure_storage()`(仅在 `Storage/Backend` 未配置时设
> `sqlite`,路径用 facade 默认 `<数据目录>/library.db`),退出前
> `storage_flush()`。菜单语义(提前并入 D5):保存/另存为 →
> 导出工程文件…(⌘S,扩展名分发 ove/otio/fcpxml);打开 → 从库中打开…
> (= 管理器)+ 打开工程文件…;新增 项目管理器…。状态栏自动保存段 →
> 库写入状态(`storage_bound` / `last_error`,失败红色)。MockEngine 带
> 内存假库(3 行种子 + 全操作),app gpui 测试覆盖建/删/复制/重命名/
> 导入/导出/打开;facade 侧 `it_library.rs` 5 测试(建/列/打开绑定写穿/
> 重命名复制删除/导出导入 round-trip/禁用降级)。截图
> docs/screenshot-manager.png + -en.png。
## 5. 分期与判据
| 期 | 内容 | 完成判据 |
+26 -2
View File
@@ -74,11 +74,35 @@ clip 行自带时间线区间(`<range in out/>`)、媒体偏移(`<media_in
- 导出:内存序列化经 ove-xml 或 otio 后端写出;除当前状态外不读写
数据库。
## 配置
库的选择与行为全部由 `Storage` 配置组驱动(写穿由
`crates/oakengine/src/storage.rs` 读取):
- `Storage/Backend``"sqlite"`(默认值)、`"database"``"pg"`
启用写穿;其它值(如 `"off"`)禁用。键缺失 = 无库配置,工程不
绑定库(headless 消费者与测试进程永不写用户真实库)。
- `Storage/SqlitePath` — SQLite 库文件;默认
`<系统数据目录>/library.db`(尊重 `OAK_CONFIG_DIR`)。
- `Storage/PgUrl` — PostgreSQL 连接串,`Backend = "pg"` 时使用:
`user:pass@host:5432/dbname`libpq URL 形式,带
`postgres://`/`postgresql://` 前缀也会被剥掉)。解析出的库 URI 为
`oakdb+pg://<PgUrl>`
- `Storage/SnapshotIntervalSec`(默认 600)、`Storage/JournalRetentionDays`
(默认 0 = 全保留)见上。
## 多写者与平台
v1 假设单写者(SQLite `busy_timeout`PG 行锁);多写者协作是后续
工作(M14)。默认数据库是用户级单一 SQLite 文件;PostgreSQL
`oakdb+pg://` 连接串选择。
工作(M14)。默认数据库是用户级单一 SQLite 文件;PostgreSQL 通过
`Storage/Backend = "pg"` + `Storage/PgUrl` 选择,或直接用
`oakdb+pg://` 连接串 URI。
数据库测试:`cargo test -p oakstorage` 全绿无需 PostgreSQL——SQLite
套件常驻运行;PG 套件(`tests/database_pg_test.rs`)在设置了
`OAK_TEST_PG_URL`(如 `postgres://user:pass@host:5432/db`)时连接真实
PG 全量运行,未设置则跳过并打印说明。该 URL 应指向专用测试库:每个
测试会重置四张表。
另见:[M10 oakstorage 手册](plans/riir/M10-oakstorage.md)、
[M13 写穿计划](plans/riir/M13-storage-live.md)、