// 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 .
//! oakstorage database-backend integration tests (plan M13 D1, the
//! aggregation-granularity design).
//!
//! End-to-end against real SQLite library files in temp directories:
//! full-feature round-trip through a fresh session, the node-granularity
//! journal (import + diff), snapshot + journal replay, undo to any
//! point, snapshot pruning and journal retention, the project-manager
//! API surface (list/delete/duplicate/rename/import/export) and the
//! error/URI matrices. Nothing touches a real library.
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use sea_orm::entity::prelude::*;
use sea_orm::QueryOrder;
use oakcore_rs::Rational;
use oaknode::block::ClipBlockBehavior;
use oaknode::footage::FootageBehavior;
use oaknode::id::NodeId;
use oaknode::keyframe::{Interpolation, Keyframe};
use oaknode::node::NodeCore;
use oaknode::project::Project;
use oaknode::sequence::SequenceBehavior;
use oaknode::track::{TrackBehavior, TrackListBehavior, TrackType};
use oaknode::value::NodeValue;
use oakstorage::backend::StorageBackend;
use oakstorage::backends::database::entities::{journal, project, settings, snapshot};
use oakstorage::backends::database::{derive_stats, DatabaseBackend, KIND_IMPORT, KIND_REDO, ProjectStats, SNAPSHOT_KEEP};
use oakstorage::error::{
OAKSTORAGE_E_FORMAT, OAKSTORAGE_E_INVALID, OAKSTORAGE_E_IO, OAKSTORAGE_E_NO_BACKEND,
OAKSTORAGE_E_NOT_FOUND, OAKSTORAGE_OK,
};
use oakstorage::handle::CHandle;
use oakstorage::nodeutil::{make_project_owned, project_arc};
use oakstorage::registry::Registry;
use oakstorage::uri::StorageUri;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// A fresh, unique temp directory for one test.
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("oakstorage_db_{}_{}", std::process::id(), tag));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
/// `oakdb+sqlite:///…` URI for a database file.
fn db_uri(path: &Path) -> String {
format!("oakdb+sqlite://{}", path.display())
}
/// `oakdb+sqlite:///…?project=` URI selecting one library row.
fn project_uri(db: &str, uuid: &str) -> String {
format!("{db}?project={uuid}")
}
/// `file://…` URI for a plain file.
fn file_uri(path: &Path) -> String {
format!("file://{}", path.display())
}
/// Release an owned handle (refcount 1).
fn release(h: CHandle) {
if let Some(release) = h.release {
unsafe { release(h.ctx) };
}
}
/// Save an `Arc>` through the database backend.
fn save_project(
backend: &DatabaseBackend,
project: &Arc>,
uri: &str,
) -> oakstorage::error::Result<()> {
let parsed = StorageUri::parse(uri).unwrap();
let handle = make_project_owned(project.clone());
let result = backend.save(handle, &parsed, 0);
release(handle);
result
}
/// Load a project through a *new* database backend session (a fresh
/// connection pool = a fresh session), returning `(uuid, loaded)`.
fn load_project(backend: &DatabaseBackend, uri: &str) -> (String, Arc>) {
let parsed = StorageUri::parse(uri).unwrap();
let result = backend.load(&parsed).unwrap();
assert_eq!(result.version_info, OAKSTORAGE_OK);
let handle = result.project;
let loaded = unsafe { project_arc(&handle) }.unwrap();
let uuid = loaded.lock().unwrap().uuid.clone();
release(handle);
(uuid, loaded)
}
/// Load the state at `seq` (undo to any point).
fn load_at(backend: &DatabaseBackend, uri: &str, uuid: &str, seq: i64) -> Arc> {
let parsed = StorageUri::parse(uri).unwrap();
let handle = backend.load_at(&parsed, uuid, seq).unwrap();
let loaded = unsafe { project_arc(&handle) }.unwrap();
release(handle);
loaded
}
fn r_to_f(r: Rational) -> f64 {
r.numerator() as f64 / r.denominator() as f64
}
fn assert_close(a: f64, b: f64) {
assert!((a - b).abs() < 1e-6, "expected {a} close to {b}");
}
/// Open a raw sea-orm connection to the database file (for inspecting
/// and injecting rows behind the backend's back) and drive one future
/// against it on a private current-thread runtime. The connection is
/// handed to `f` by value (a clone of the pool).
fn inspect_db(path: &Path, f: impl FnOnce(sea_orm::DatabaseConnection) -> Fut) -> R
where
Fut: std::future::Future