Migrate frozen data contracts and authored source to schema 4

This commit is contained in:
2026-09-06 03:39:02 +03:00
parent c5276b1943
commit 1230ab8a3e
21 changed files with 2006 additions and 126 deletions
+52 -20
View File
@@ -1,9 +1,11 @@
//! Pure, bounded source3 projects. All includes resolve against supplied UTF-8
//! Pure, bounded source projects. All includes resolve against supplied UTF-8
//! documents; this module never reads a filesystem, URL, or process directory.
use crate::{Id, Kind, Project, language, persistence::DecodeResult, validation};
use language::{Diagnostic, ParseLimits, Span};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[path = "source_project/source_migration.rs"]
mod source_migration;
#[path = "source_project/source_structure.rs"]
mod source_structure;
@@ -278,21 +280,24 @@ pub fn lint_bundle(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDi
}
}
/// Compile all reachable files as one source3 model and stabilize omitted UUIDs.
/// Compile reachable source3/4 files, explicitly upgrade data contracts, and stabilize UUIDs.
/// Missing/unreachable files and every admission or engineering error leave the
/// input untouched. V1/V2 single-file migration remains in `language::parse`.
pub fn compile_bundle(
bundle: &SourceBundle,
limits: ProjectLimits,
) -> Result<CompiledSourceProject, Vec<SourceDiagnostic>> {
let (compiled, insertions) = compile_once(bundle, limits)?;
if insertions.is_empty() {
return Ok(compiled);
let (mut compiled, insertions) = compile_once(bundle, limits)?;
if !insertions.is_empty() {
let mut stable = bundle.clone();
apply_edits(&mut stable, insertions)?;
// Re-index actual returned bytes before performing the schema upgrade.
compiled = compile_once(&stable, limits)?.0;
}
let mut stable = bundle.clone();
apply_edits(&mut stable, insertions)?;
// Re-index the actual returned bytes. Never expose offsets into pre-ID text.
compile_once(&stable, limits).map(|(compiled, _)| compiled)
if compiled.decoded.migration.is_some() {
return source_migration::upgrade(&compiled, limits);
}
Ok(compiled)
}
fn compile_once(
@@ -568,7 +573,7 @@ fn compile_once(
span,
}),
};
let project =
let decoded =
language::resolve_declarations(header.project, &declarations, &aliases, limits.model)
.map_err(|(index, error)| {
let path = declaration_paths.get(index).unwrap_or(&bundle.entry);
@@ -578,7 +583,7 @@ fn compile_once(
related: Vec::new(),
}]
})?;
let diagnostics = model_diagnostics(bundle, &index, &project);
let diagnostics = model_diagnostics(bundle, &index, &decoded.project);
let errors: Vec<_> = diagnostics
.iter()
.filter(|issue| issue.diagnostic.severity == validation::Severity::Error)
@@ -590,10 +595,7 @@ fn compile_once(
Ok((
CompiledSourceProject {
bundle: bundle.clone(),
decoded: DecodeResult {
project,
migration: None,
},
decoded,
index,
diagnostics,
},
@@ -718,7 +720,7 @@ fn apply_edits(bundle: &mut SourceBundle, edits: Edits) -> Result<(), Vec<Source
Ok(())
}
/// Canonical, readable source3 export. Like `language::format`, the caller is
/// Canonical, readable source4 export. Like `language::format`, the caller is
/// responsible for supplying a valid model; compilation checks the result.
/// Existing source layout is preserved by `update_bundle`, not this formatter.
pub fn format_bundle(project: &Project) -> SourceBundle {
@@ -734,7 +736,7 @@ pub fn format_bundle(project: &Project) -> SourceBundle {
.push_str(&unindent(&declaration));
}
let mut entry = format!(
"arcadia 3;\n{}{{\n description {};\n",
"arcadia 4;\n{}{{\n description {};\n",
project_header(project),
quote(&project.description)
);
@@ -923,6 +925,7 @@ pub fn update_bundle_with_identity_map(
after: new_text,
old_aliases: &old_aliases,
new_aliases: &new_aliases,
legacy_value_paths: false,
};
let local = edits.entry(path.clone()).or_default();
update.header(&actual.header, &previous.header, &next.header, local);
@@ -951,6 +954,7 @@ pub fn update_bundle_with_identity_map(
after: &after,
old_aliases: &old_aliases,
new_aliases: &new_aliases,
legacy_value_paths: false,
};
let mut local = Vec::new();
update.header(
@@ -1119,6 +1123,9 @@ struct SyntaxUpdate<'a> {
after: &'a str,
old_aliases: &'a BTreeMap<String, String>,
new_aliases: &'a BTreeMap<String, String>,
// Source3 migration only adds path qualification; the existing field identity
// must keep its authored argument trivia during that one-time transformation.
legacy_value_paths: bool,
}
impl SyntaxUpdate<'_> {
fn header(
@@ -1338,11 +1345,11 @@ impl SyntaxUpdate<'_> {
let previous = before.unwrap_or(&[]);
let old_keys: Option<Vec<_>> = previous
.iter()
.map(|item| item_key(item, self.before, self.old_aliases))
.map(|item| item_key(item, self.before, self.old_aliases, self.legacy_value_paths))
.collect();
let new_keys: Option<Vec<_>> = after
.iter()
.map(|item| item_key(item, self.after, self.new_aliases))
.map(|item| item_key(item, self.after, self.new_aliases, self.legacy_value_paths))
.collect();
let keyed = old_keys.zip(new_keys).filter(|(old, new)| {
old.iter().collect::<BTreeSet<_>>().len() == old.len()
@@ -1449,6 +1456,7 @@ fn item_key(
value: &language::EditValue,
source: &str,
aliases: &BTreeMap<String, String>,
legacy_value_paths: bool,
) -> Option<String> {
use language::EditKind;
match &value.kind {
@@ -1462,7 +1470,31 @@ fn item_key(
.find_map(|key| {
fields.get(key).and_then(|field| {
if let EditKind::Reference(alias) = &field.value.kind {
aliases.get(alias).map(|id| format!("{key}:{id}"))
aliases.get(alias).map(|id| {
if key == "field" && !legacy_value_paths {
let path = fields
.get("path")
.and_then(|field| {
if let EditKind::List(path) = &field.value.kind {
path.iter()
.map(|hop| {
if let EditKind::Reference(alias) = &hop.kind {
aliases.get(alias).cloned()
} else {
None
}
})
.collect::<Option<Vec<_>>>()
} else {
None
}
})
.unwrap_or_default();
format!("field:{}:{id}", path.join("/"))
} else {
format!("{key}:{id}")
}
})
} else {
None
}