feat(storage): database backend D1 — SQLite, node-granular journal, snapshots, persistent undo
- sea-orm schema: projects/settings/snapshots/journal (journal rows are per-node before/after images produced by diffing the serializer output, never full-project copies) - replay = latest snapshot + journal; load_at() rewinds to any command seq (cross-session undo history); snapshot interval and journal retention configurable; snapshots pruned to the newest 3 - project manager API: list/delete/duplicate/rename, export/import (.ove/.otio/.fcpxml), stats derivation; PG surfaces as E_NO_BACKEND until D3 - docs(riir): M13 finalized (aggregate-granular persistence)
This commit is contained in:
Generated
+2
@@ -4877,6 +4877,8 @@ dependencies = [
|
||||
name = "oakstorage"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"oakcommon",
|
||||
"oakcore-rs",
|
||||
"oaknode",
|
||||
"oakotio",
|
||||
|
||||
@@ -10,8 +10,13 @@ crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oakcore-rs = { path = "../oakcore" }
|
||||
oakcommon = { path = "../oakcommon" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
# Timestamp columns in the database schema (plan M13 §1: TIMESTAMP NOT
|
||||
# NULL). sea-orm maps chrono NaiveDateTime via its default with-chrono
|
||||
# feature; the direct dependency is needed for the entity field types.
|
||||
chrono = "0.4"
|
||||
# Error derive (Display + std::error::Error) for the crate error enum
|
||||
# (src/error.rs). Same major version the other modules use (oakotio, ...).
|
||||
thiserror = "2"
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The database backend (PostgreSQL + SQLite via SeaORM).
|
||||
//!
|
||||
//! URI forms:
|
||||
//! - `oakdb+sqlite:///absolute/path.db` — local file database
|
||||
//! - `oakdb+pg://user:pass@host:5432/dbname` — PostgreSQL
|
||||
//!
|
||||
//! The async SeaORM API is driven by a private current-thread tokio
|
||||
//! runtime so the public surface stays synchronous (M10: no callbacks,
|
||||
//! sync commands only). The graph payload is the same serialized form
|
||||
//! the ove-xml backend produces — one serialization truth.
|
||||
|
||||
/// The database backend (schemes `oakdb+sqlite` / `oakdb+pg`).
|
||||
pub struct DatabaseBackend {
|
||||
// Private current-thread runtime + SeaORM connection cache.
|
||||
}
|
||||
|
||||
impl DatabaseBackend {
|
||||
/// Construct (no connection is opened until first use).
|
||||
pub fn new() -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Parse the URI into a SeaORM connection URL + project key
|
||||
/// (query param `?project=` or row id; default: singleton "default"
|
||||
/// project row).
|
||||
///
|
||||
/// Dead until the database proxy lands (the whole backend is a
|
||||
/// declared stub); kept for the schema contract.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn parse_target(
|
||||
_uri: &crate::uri::StorageUri,
|
||||
) -> crate::error::Result<(String, String)> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::backend::StorageBackend for DatabaseBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn uri_scheme(&self) -> &'static str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn can_handle(&self, uri: &crate::uri::StorageUri) -> bool {
|
||||
uri.scheme.starts_with("oakdb")
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
_uri: &crate::uri::StorageUri,
|
||||
) -> crate::error::Result<crate::backend::LoadResult> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_project: crate::handle::CHandle,
|
||||
_uri: &crate::uri::StorageUri,
|
||||
_options: u32,
|
||||
) -> crate::error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `journal` table: the command log (plan §1). One row per affected
|
||||
//! node per command — the node's before image (`old_xml`) and after
|
||||
//! image (`new_xml`), whole-node XML produced by the diff. `node_identity`
|
||||
//! 0 is the settings pseudo-node; real node identities are stored
|
||||
//! offset by +1 so the 0 sentinel never collides with a graph slot.
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
/// The `journal` entity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "journal")]
|
||||
pub struct Model {
|
||||
/// Owning project (ON DELETE CASCADE).
|
||||
pub project_id: i64,
|
||||
/// Monotonic per-project command sequence.
|
||||
#[sea_orm(primary_key)]
|
||||
pub seq: i64,
|
||||
/// The affected node's identity (+1; 0 = settings pseudo-node).
|
||||
#[sea_orm(primary_key)]
|
||||
pub node_identity: i64,
|
||||
/// Command kind: 'redo' | 'undo' | 'jump' | 'group' | 'import'.
|
||||
pub kind: String,
|
||||
/// Before image (whole node XML; NULL for newly added nodes).
|
||||
pub old_xml: Option<String>,
|
||||
/// After image (whole node XML; NULL for deleted nodes).
|
||||
pub new_xml: Option<String>,
|
||||
/// Journal write time.
|
||||
pub at: DateTime,
|
||||
}
|
||||
|
||||
/// `journal` has no relations yet.
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! SeaORM entity models for the four-table schema (plan M13 §1).
|
||||
//!
|
||||
//! One module per entity: the `DeriveEntityModel` macro generates
|
||||
//! `Entity`/`Column`/`Model`/`ActiveModel` in the defining module, so
|
||||
//! each table gets its own scope. The same definitions serve SQLite
|
||||
//! today and PostgreSQL (D3) — the column types (`i64`, `String`,
|
||||
//! `Option<String>`, `NaiveDateTime`) map to BIGINT/TEXT/TIMESTAMP on
|
||||
//! both.
|
||||
|
||||
pub mod journal;
|
||||
pub mod project;
|
||||
pub mod settings;
|
||||
pub mod snapshot;
|
||||
@@ -0,0 +1,47 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `projects` table: one row per project library entry (plan §1).
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
/// The `projects` entity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "projects")]
|
||||
pub struct Model {
|
||||
/// Surrogate row id (BIGSERIAL/INTEGER PK).
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// Project uuid (same value the serializer persists; unique).
|
||||
#[sea_orm(unique)]
|
||||
pub uuid: String,
|
||||
/// Display name (the project manager lists this).
|
||||
pub name: String,
|
||||
/// Payload format version (the serializer's CURRENT_VERSION).
|
||||
pub schema_ver: i32,
|
||||
/// Row creation timestamp.
|
||||
pub created_at: DateTime,
|
||||
/// Last-write timestamp (manager sort key).
|
||||
pub modified_at: DateTime,
|
||||
/// Current head command seq (the newest journal seq).
|
||||
pub command_seq: i64,
|
||||
}
|
||||
|
||||
/// `projects` has no relations yet (queries join explicitly).
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `settings` table: the project's KV settings (plan §1). The
|
||||
//! current values are mirrored here on every save; the history lives in
|
||||
//! the journal (node_identity = 0 pseudo-node).
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
/// The `settings` entity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "settings")]
|
||||
pub struct Model {
|
||||
/// Owning project (ON DELETE CASCADE).
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: i64,
|
||||
/// Setting key.
|
||||
#[sea_orm(primary_key)]
|
||||
pub key: String,
|
||||
/// Setting value.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// `settings` has no relations yet.
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `snapshots` table: periodic full payloads that only accelerate
|
||||
//! loading (plan §0/§1). The newest snapshot at or before a target seq
|
||||
//! is the replay base; older ones are pruned to keep the newest
|
||||
//! [`crate::backends::database::SNAPSHOT_KEEP`].
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
/// The `snapshots` entity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "snapshots")]
|
||||
pub struct Model {
|
||||
/// Owning project (ON DELETE CASCADE).
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: i64,
|
||||
/// The journal command seq this snapshot captures.
|
||||
#[sea_orm(primary_key)]
|
||||
pub command_seq: i64,
|
||||
/// Full-feature project XML (identical to a `.ove` payload).
|
||||
pub payload: String,
|
||||
/// Snapshot write time.
|
||||
pub written_at: DateTime,
|
||||
}
|
||||
|
||||
/// `snapshots` has no relations yet.
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Schema bootstrap (plan M13 §1).
|
||||
//!
|
||||
//! Chosen over the sea-orm-migration crate on purpose: the schema is
|
||||
//! four tables, static for the whole D1 surface, and `CREATE TABLE IF
|
||||
//! NOT EXISTS` is idempotent — no versioned migration machinery, no
|
||||
//! extra crate in the tree. The DDL runs once at first connection open.
|
||||
|
||||
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement};
|
||||
|
||||
/// SQLite DDL for the four tables (plan §1, SQLite dialect:
|
||||
/// `INTEGER PRIMARY KEY AUTOINCREMENT` + `TIMESTAMP`).
|
||||
const SQLITE_DDL: &[&str] = &[
|
||||
"CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
schema_ver INTEGER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
modified_at TIMESTAMP NOT NULL,
|
||||
command_seq BIGINT NOT NULL DEFAULT 0
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS settings (
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (project_id, key)
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS snapshots (
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
command_seq BIGINT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
written_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (project_id, command_seq)
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS journal (
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
seq BIGINT NOT NULL,
|
||||
node_identity BIGINT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
old_xml TEXT,
|
||||
new_xml TEXT,
|
||||
at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (project_id, seq, node_identity)
|
||||
)",
|
||||
];
|
||||
|
||||
/// Apply the schema (idempotent; safe to run on every connection open).
|
||||
///
|
||||
/// PostgreSQL (D3) gets its own `BIGSERIAL` DDL — same shape, currently
|
||||
/// unreachable because the backend rejects `oakdb+pg://` before any
|
||||
/// connection is opened.
|
||||
pub async fn migrate(db: &impl ConnectionTrait) -> crate::error::Result<()> {
|
||||
let backend = db.get_database_backend();
|
||||
let statements: &[&str] = match backend {
|
||||
DatabaseBackend::Sqlite => SQLITE_DDL,
|
||||
// D3: `BIGSERIAL PRIMARY KEY` for projects.id, `BIGINT` for the
|
||||
// FKs — the rest of the column set is identical.
|
||||
DatabaseBackend::Postgres => {
|
||||
return Err(crate::error::Error::Failed(
|
||||
"oakdb+pg is a D3 milestone; the PostgreSQL schema is not wired yet".to_string(),
|
||||
));
|
||||
}
|
||||
other => {
|
||||
return Err(crate::error::Error::Failed(format!(
|
||||
"unsupported database backend {other:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
for sql in statements {
|
||||
db.execute_raw(Statement::from_string(backend, sql.to_string()))
|
||||
.await
|
||||
.map_err(|e| crate::error::Error::Io(format!("schema migration: {e}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,9 @@
|
||||
|
||||
//! Built-in backends.
|
||||
//!
|
||||
//! The database backend (`database`) is *not* registered: it is the
|
||||
//! future replacement (M10 §3 — "数据库替换路径"), provided by a later
|
||||
//! proxy. The file backends cover today's surface.
|
||||
//! Arbitration order inside `file://`: ove-xml before otio so a `.ove`
|
||||
//! path (which the otio backend would also claim as JSON) stays on the
|
||||
//! ove backend. The database backend claims its own `oakdb+…` schemes.
|
||||
|
||||
pub mod database;
|
||||
pub mod otio;
|
||||
@@ -32,5 +32,6 @@ pub fn builtins() -> Vec<Arc<dyn crate::backend::StorageBackend>> {
|
||||
vec![
|
||||
Arc::new(ove_xml::OveXmlBackend::new()),
|
||||
Arc::new(otio::OtioBackend::new()),
|
||||
Arc::new(database::DatabaseBackend::new()),
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user