aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-sat-resolver/src/transaction.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
committernsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
commit8cc1ba8a02c0318b65658f1634de378c780392b9 (patch)
treefdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-sat-resolver/src/transaction.rs
parent72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff)
downloadphp-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz
php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst
php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry, mozart-sat-resolver, and mozart-vcs into mozart-core to align the source layout with Composer's structure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-sat-resolver/src/transaction.rs')
-rw-r--r--crates/mozart-sat-resolver/src/transaction.rs568
1 files changed, 0 insertions, 568 deletions
diff --git a/crates/mozart-sat-resolver/src/transaction.rs b/crates/mozart-sat-resolver/src/transaction.rs
deleted file mode 100644
index bf7befc..0000000
--- a/crates/mozart-sat-resolver/src/transaction.rs
+++ /dev/null
@@ -1,568 +0,0 @@
-use crate::decisions::Decisions;
-use crate::pool::{PackageId, Pool, literal_to_package_id};
-use indexmap::{IndexMap, IndexSet};
-
-/// An operation to perform on a package.
-///
-/// Port of Composer's SolverOperation hierarchy.
-#[derive(Debug, Clone)]
-pub enum Operation {
- /// Install a new package.
- Install { package_id: PackageId },
- /// Update a package from one version to another.
- Update {
- initial_id: PackageId,
- target_id: PackageId,
- },
- /// Remove a package.
- Uninstall { package_id: PackageId },
-}
-
-impl Operation {
- /// Get the operation type as a string.
- pub fn operation_type(&self) -> &'static str {
- match self {
- Operation::Install { .. } => "install",
- Operation::Update { .. } => "update",
- Operation::Uninstall { .. } => "uninstall",
- }
- }
-
- /// Format the operation as a human-readable string using pool data.
- pub fn pretty_string(&self, pool: &Pool) -> String {
- match self {
- Operation::Install { package_id } => {
- let pkg = pool.package_by_id(*package_id);
- format!("Installing {} ({})", pkg.name, pkg.pretty_version)
- }
- Operation::Update {
- initial_id,
- target_id,
- } => {
- let initial = pool.package_by_id(*initial_id);
- let target = pool.package_by_id(*target_id);
- format!(
- "Updating {} ({} => {})",
- target.name, initial.pretty_version, target.pretty_version
- )
- }
- Operation::Uninstall { package_id } => {
- let pkg = pool.package_by_id(*package_id);
- format!("Removing {} ({})", pkg.name, pkg.pretty_version)
- }
- }
- }
-}
-
-/// Computes install/update/remove operations from solver results.
-///
-/// Port of Composer's Transaction.php.
-pub struct Transaction<'a> {
- pool: &'a Pool,
- /// Currently installed package IDs.
- present_ids: Vec<PackageId>,
- /// Result package IDs from the solver.
- result_ids: Vec<PackageId>,
- /// Computed operations.
- operations: Vec<Operation>,
-}
-
-impl<'a> Transaction<'a> {
- /// Create a new transaction from present and result package sets.
- pub fn new(pool: &'a Pool, present_ids: Vec<PackageId>, result_ids: Vec<PackageId>) -> Self {
- let mut tx = Transaction {
- pool,
- present_ids,
- result_ids,
- operations: Vec::new(),
- };
- tx.calculate_operations();
- tx
- }
-
- /// Create a transaction from solver decisions.
- pub fn from_decisions(
- pool: &'a Pool,
- present_ids: Vec<PackageId>,
- decisions: &Decisions,
- ) -> Self {
- let mut result_ids = Vec::new();
- for i in 0..decisions.len() {
- let decision = decisions.at_offset(i);
- if decision.literal > 0 {
- result_ids.push(literal_to_package_id(decision.literal));
- }
- }
- Self::new(pool, present_ids, result_ids)
- }
-
- /// Get the computed operations.
- pub fn operations(&self) -> &[Operation] {
- &self.operations
- }
-
- /// Calculate the delta between present and result packages.
- fn calculate_operations(&mut self) {
- // Build maps: name -> package_id for present packages
- let mut present_by_name: IndexMap<&str, PackageId> = IndexMap::new();
- for &id in &self.present_ids {
- let pkg = self.pool.package_by_id(id);
- present_by_name.insert(&pkg.name, id);
- }
-
- // Track which present packages have been matched
- let mut matched_present: IndexSet<PackageId> = IndexSet::new();
-
- // Build topologically sorted result packages via DFS
- let sorted_results = self.topological_sort();
-
- // Process result packages in topological order
- for &result_id in &sorted_results {
- let result_pkg = self.pool.package_by_id(result_id);
-
- if let Some(&present_id) = present_by_name.get(result_pkg.name.as_str()) {
- matched_present.insert(present_id);
- let present_pkg = self.pool.package_by_id(present_id);
-
- // Check if update is needed (version changed)
- if present_pkg.version != result_pkg.version || present_id != result_id {
- self.operations.push(Operation::Update {
- initial_id: present_id,
- target_id: result_id,
- });
- }
- // Otherwise: no change needed, skip
- } else {
- // New package: install
- self.operations.push(Operation::Install {
- package_id: result_id,
- });
- }
- }
-
- // Remove packages that are present but not in result
- let mut uninstalls = Vec::new();
- for &present_id in &self.present_ids {
- if !matched_present.contains(&present_id) {
- uninstalls.push(Operation::Uninstall {
- package_id: present_id,
- });
- }
- }
-
- // Prepend uninstalls (remove before install/update)
- uninstalls.append(&mut self.operations);
- self.operations = uninstalls;
- }
-
- /// Topologically sort result packages by their dependency order.
- /// Uses DFS: dependencies are processed before dependents.
- fn topological_sort(&self) -> Vec<PackageId> {
- // Index every result package by every name it answers to (own name +
- // `replaces` targets + `provides` targets). Mirrors Composer's
- // `resultPackagesByName` map, which `getProvidersInResult` queries
- // when walking a package's requires — so a replace/provide target
- // resolves to the package that satisfies it. Without this expansion
- // the DFS treats replace/provide-only requires as unsatisfied and
- // misses the transitive ordering edge.
- let mut result_by_target: IndexMap<&str, Vec<PackageId>> = IndexMap::new();
- for &id in &self.result_ids {
- let pkg = self.pool.package_by_id(id);
- result_by_target.entry(&pkg.name).or_default().push(id);
- for link in &pkg.replaces {
- result_by_target.entry(&link.target).or_default().push(id);
- }
- for link in &pkg.provides {
- result_by_target.entry(&link.target).or_default().push(id);
- }
- }
-
- let mut visited: IndexSet<PackageId> = IndexSet::new();
- let mut order: Vec<PackageId> = Vec::new();
-
- // Find root packages (not required by any other result package)
- let roots = self.get_root_packages(&result_by_target);
-
- // DFS from roots
- let mut stack: Vec<(PackageId, bool)> = Vec::new();
- for &root_id in roots.iter().rev() {
- stack.push((root_id, false));
- }
-
- while let Some((pkg_id, processed)) = stack.pop() {
- if processed {
- if visited.insert(pkg_id) {
- order.push(pkg_id);
- }
- continue;
- }
-
- if visited.contains(&pkg_id) {
- continue;
- }
-
- // Push self as "processed" marker
- stack.push((pkg_id, true));
-
- // Push dependencies
- let pkg = self.pool.package_by_id(pkg_id);
- for req in &pkg.requires {
- if let Some(provider_ids) = result_by_target.get(req.target.as_str()) {
- for &dep_id in provider_ids {
- if !visited.contains(&dep_id) {
- stack.push((dep_id, false));
- }
- }
- }
- }
- }
-
- // Add any remaining unvisited packages
- for &id in &self.result_ids {
- if !visited.contains(&id) {
- order.push(id);
- }
- }
-
- order
- }
-
- /// Find root packages: result packages not required by any other result
- /// package. A package whose own name (or any `replaces`/`provides`
- /// target) appears in another result package's `requires` is excluded.
- /// Mirrors Composer's `Transaction::getRootPackages`, which uses
- /// `getProvidersInResult` to do the same expansion.
- fn get_root_packages(
- &self,
- result_by_target: &IndexMap<&str, Vec<PackageId>>,
- ) -> Vec<PackageId> {
- let mut required: IndexSet<PackageId> = IndexSet::new();
- for &id in &self.result_ids {
- let pkg = self.pool.package_by_id(id);
- for req in &pkg.requires {
- if let Some(provider_ids) = result_by_target.get(req.target.as_str()) {
- for &dep_id in provider_ids {
- if dep_id != id {
- required.insert(dep_id);
- }
- }
- }
- }
- }
-
- let mut roots: Vec<PackageId> = Vec::new();
- for &id in &self.result_ids {
- if !required.contains(&id) {
- roots.push(id);
- }
- }
-
- // If no roots found (circular), use all
- if roots.is_empty() {
- return self.result_ids.clone();
- }
-
- roots
- }
-}
-
-/// Lock transaction: specialization for computing lock file operations.
-///
-/// Port of Composer's LockTransaction.php.
-pub struct LockTransaction<'a> {
- /// The base transaction.
- transaction: Transaction<'a>,
- /// All result package IDs.
- all_result_ids: Vec<PackageId>,
- /// Non-dev result package IDs.
- non_dev_ids: Vec<PackageId>,
- /// Dev result package IDs.
- dev_ids: Vec<PackageId>,
-}
-
-impl<'a> LockTransaction<'a> {
- /// Create a lock transaction from solver decisions.
- pub fn new(
- pool: &'a Pool,
- present_ids: Vec<PackageId>,
- unlockable_ids: IndexSet<PackageId>,
- decisions: &Decisions,
- ) -> Self {
- // Extract result packages from decisions
- let mut all_result_ids = Vec::new();
- let mut non_dev_ids = Vec::new();
- for i in 0..decisions.len() {
- let decision = decisions.at_offset(i);
- if decision.literal > 0 {
- let pkg_id = literal_to_package_id(decision.literal);
- all_result_ids.push(pkg_id);
- if !unlockable_ids.contains(&pkg_id) {
- non_dev_ids.push(pkg_id);
- }
- }
- }
-
- let transaction = Transaction::new(pool, present_ids, all_result_ids.clone());
-
- LockTransaction {
- transaction,
- all_result_ids,
- non_dev_ids,
- dev_ids: Vec::new(),
- }
- }
-
- /// Set the non-dev packages from an extraction-only solve result.
- /// `extraction_ids` are the package IDs that were resolved without dev deps.
- pub fn set_non_dev_packages(&mut self, extraction_ids: &[PackageId]) {
- let extraction_names: IndexSet<String> = extraction_ids
- .iter()
- .map(|&id| self.transaction.pool.package_by_id(id).name.clone())
- .collect();
-
- self.non_dev_ids.clear();
- self.dev_ids.clear();
-
- for &id in &self.all_result_ids {
- let pkg = self.transaction.pool.package_by_id(id);
- if extraction_names.contains(&pkg.name) {
- self.non_dev_ids.push(id);
- } else {
- self.dev_ids.push(id);
- }
- }
- }
-
- /// Get the computed operations.
- pub fn operations(&self) -> &[Operation] {
- self.transaction.operations()
- }
-
- /// Get all result package IDs.
- pub fn all_result_ids(&self) -> &[PackageId] {
- &self.all_result_ids
- }
-
- /// Get non-dev result package IDs.
- pub fn non_dev_ids(&self) -> &[PackageId] {
- &self.non_dev_ids
- }
-
- /// Get dev result package IDs.
- pub fn dev_ids(&self) -> &[PackageId] {
- &self.dev_ids
- }
-
- /// Get new lock packages for writing to the lock file.
- /// If `dev_mode` is true, returns dev packages; otherwise non-dev.
- pub fn new_lock_package_ids(&self, dev_mode: bool) -> &[PackageId] {
- if dev_mode {
- &self.dev_ids
- } else {
- &self.non_dev_ids
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::{PoolLink, PoolPackageInput};
-
- fn make_input(name: &str, version: &str, pretty: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: pretty.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- fn make_input_with_deps(
- name: &str,
- version: &str,
- pretty: &str,
- deps: Vec<(&str, &str)>,
- ) -> PoolPackageInput {
- let requires = deps
- .into_iter()
- .map(|(target, constraint)| PoolLink {
- target: target.to_string(),
- constraint: constraint.to_string(),
- source: name.to_string(),
- })
- .collect();
-
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: pretty.to_string(),
- requires,
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_fresh_install() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let tx = Transaction::new(&pool, vec![], vec![1, 2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- assert!(matches!(ops[0], Operation::Install { package_id: _ }));
- assert!(matches!(ops[1], Operation::Install { package_id: _ }));
- }
-
- #[test]
- fn test_update_package() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("a/a", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a 1.0.0 (id=1), Result: a/a 2.0.0 (id=2)
- let tx = Transaction::new(&pool, vec![1], vec![2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 1);
- match &ops[0] {
- Operation::Update {
- initial_id,
- target_id,
- } => {
- assert_eq!(*initial_id, 1);
- assert_eq!(*target_id, 2);
- }
- _ => panic!("Expected update operation"),
- }
- }
-
- #[test]
- fn test_uninstall_package() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a and b/b, Result: only a/a
- let tx = Transaction::new(&pool, vec![1, 2], vec![1]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 1);
- match &ops[0] {
- Operation::Uninstall { package_id } => {
- assert_eq!(*package_id, 2);
- }
- _ => panic!("Expected uninstall operation"),
- }
- }
-
- #[test]
- fn test_uninstalls_before_installs() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a, Result: b/b (uninstall a, install b)
- let tx = Transaction::new(&pool, vec![1], vec![2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- assert!(
- matches!(ops[0], Operation::Uninstall { .. }),
- "Uninstalls should come first"
- );
- assert!(
- matches!(ops[1], Operation::Install { .. }),
- "Installs should come after"
- );
- }
-
- #[test]
- fn test_dependency_ordering() {
- // a/a requires b/b — b/b should be installed before a/a
- let pool = Pool::new(
- vec![
- make_input_with_deps("a/a", "1.0.0.0", "1.0.0", vec![("b/b", "^1.0")]),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- let tx = Transaction::new(&pool, vec![], vec![1, 2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- // b/b (dependency) should be installed before a/a
- match (&ops[0], &ops[1]) {
- (
- Operation::Install { package_id: first },
- Operation::Install { package_id: second },
- ) => {
- assert_eq!(*first, 2, "b/b should be installed first");
- assert_eq!(*second, 1, "a/a should be installed second");
- }
- _ => panic!("Expected two install operations"),
- }
- }
-
- #[test]
- fn test_no_change() {
- let pool = Pool::new(vec![make_input("a/a", "1.0.0.0", "1.0.0")], vec![]);
-
- // Same package present and in result
- let tx = Transaction::new(&pool, vec![1], vec![1]);
- let ops = tx.operations();
-
- assert!(ops.is_empty(), "No operations when nothing changed");
- }
-
- #[test]
- fn test_operation_pretty_string() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("a/a", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let install = Operation::Install { package_id: 1 };
- assert_eq!(install.pretty_string(&pool), "Installing a/a (1.0.0)");
-
- let update = Operation::Update {
- initial_id: 1,
- target_id: 2,
- };
- assert_eq!(update.pretty_string(&pool), "Updating a/a (1.0.0 => 2.0.0)");
-
- let uninstall = Operation::Uninstall { package_id: 1 };
- assert_eq!(uninstall.pretty_string(&pool), "Removing a/a (1.0.0)");
- }
-}