Migrate schema 2 ports through frozen readers and expose schema 3 source API
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-09-05 18:23:33 +03:00
parent 0d14438b7b
commit b9f88ce1f3
21 changed files with 7513 additions and 152 deletions
+52 -5
View File
@@ -1,6 +1,7 @@
//! Single-owner on-disk envelopes. All methods run on a blocking worker.
use crate::error::{ApiError, ApiResult, issues};
use axum::http::StatusCode;
use oa_core::persistence::{DecodeResult, MigrationReport};
use oa_core::{Id, Project};
use serde::{Deserialize, Serialize};
use std::{
@@ -29,6 +30,8 @@ pub struct StoredProject {
pub resource_id: Id,
pub revision: u64,
pub model: Project,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub migration: Option<MigrationReport>,
}
impl StoredProject {
@@ -46,6 +49,7 @@ pub(crate) struct ProjectSummary {
pub name: String,
pub elements: usize,
pub relations: usize,
pub migration: Option<MigrationReport>,
}
impl From<&StoredProject> for ProjectSummary {
fn from(stored: &StoredProject) -> Self {
@@ -57,6 +61,7 @@ impl From<&StoredProject> for ProjectSummary {
name: stored.model.name.clone(),
elements: stored.model.elements.len(),
relations: stored.model.relations.len(),
migration: stored.migration.clone(),
}
}
}
@@ -88,11 +93,11 @@ struct StoreInner {
/// Constructed only after complete candidate validation, outside the catalog
/// mutex. Commit methods cannot accidentally accept an unchecked candidate.
struct PreparedModel(Project);
struct PreparedModel(Project, Option<MigrationReport>);
impl PreparedModel {
fn new(model: Project) -> ApiResult<Self> {
validate_model(&model)?;
Ok(Self(model))
Ok(Self(model, None))
}
}
@@ -129,6 +134,22 @@ impl Store {
self.lock()?.create(prepared)
}
pub(crate) fn create_decoded(&self, decoded: DecodeResult) -> ApiResult<StoredProject> {
let mut prepared = PreparedModel::new(decoded.project)?;
prepared.1 = decoded.migration;
self.lock()?.create(prepared)
}
pub(crate) fn replace_decoded(
&self,
id: Id,
expected: &str,
decoded: DecodeResult,
) -> ApiResult<(StoredProject, bool)> {
let mut prepared = PreparedModel::new(decoded.project)?;
prepared.1 = decoded.migration;
self.lock()?.replace(id, expected, prepared)
}
pub(crate) fn replace(
&self,
id: Id,
@@ -201,8 +222,26 @@ impl Store {
"At most 128 models and 256 MiB of serialized envelopes are supported.".into(),
));
}
let stored: StoredProject =
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Envelope {
resource_id: Id,
revision: u64,
model: Box<serde_json::value::RawValue>,
}
let envelope: Envelope =
serde_json::from_slice(&bytes).map_err(|error| corrupt(error.to_string()))?;
let decoded = oa_core::persistence::decode_with_report_limited(
envelope.model.get().as_bytes(),
MODEL_LIMITS,
)
.map_err(|error| corrupt(error.to_string()))?;
let stored = StoredProject {
resource_id: envelope.resource_id,
revision: envelope.revision,
model: decoded.project,
migration: decoded.migration,
};
if stored.resource_id != filename_id || stored.revision == 0 {
return Err(corrupt(
"Resource ID/filename mismatch or zero revision.".into(),
@@ -254,6 +293,7 @@ impl StoreInner {
resource_id: Id::new_v4(),
revision: 1,
model,
migration: prepared.1,
};
let size = self.write(&stored, true)?;
self.adopt(stored.clone(), size);
@@ -270,7 +310,11 @@ impl StoreInner {
let current = self.checked(id, expected)?;
let model = prepared.0;
if current.model == model {
return Ok((current.clone(), false));
let mut unchanged = current.clone();
if prepared.1.is_some() {
unchanged.migration = prepared.1;
}
return Ok((unchanged, false));
}
let revision = current.revision.checked_add(1).ok_or_else(|| {
ApiError::new(
@@ -283,6 +327,7 @@ impl StoreInner {
resource_id: id,
revision,
model,
migration: prepared.1,
};
let size = self.write(&stored, false)?;
self.adopt(stored.clone(), size);
@@ -312,7 +357,9 @@ impl StoreInner {
}
fn write(&self, stored: &StoredProject, create: bool) -> ApiResult<usize> {
let bytes = serde_json::to_vec(stored)
// Migration observations are response metadata, never persisted facts.
let envelope = serde_json::json!({"resource_id": stored.resource_id, "revision": stored.revision, "model": stored.model});
let bytes = serde_json::to_vec(&envelope)
.map_err(|_| ApiError::internal("Could not serialize model."))?;
if bytes.len() > MAX_ENVELOPE_BYTES {
return Err(ApiError::new(