diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6470f007..672db1dd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1112: Packaging a module with a globals resource now respects SourcesRoot, placing the exported file at the correct path in the tarball
- #1057: Fix IPM not cleaning up after itself on self-uninstall
- #1122: Packaging should recognize resources in dependency modules set to deploy
+- #1119: The update command should check version requirements using post-update values instead of what's currently installed
## [0.10.6] - 2026-02-24
diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls
index 493621677..05748a458 100644
--- a/src/cls/IPM/Storage/Module.cls
+++ b/src/cls/IPM/Storage/Module.cls
@@ -1109,7 +1109,7 @@ Method ProcessSingleDependencyIterative(
}
set sc = ##class(%IPM.Utils.Module).GetRequiredVersionExpression(
- pDep.Name,otherDepsList,.installedReqExpr,.installedConstraintList
+ pDep.Name,otherDepsList,,.installedReqExpr,.installedConstraintList
)
$$$ThrowOnError(sc)
set searchExpr = searchExpr.And(installedReqExpr)
diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls
index b2903e36d..d41928965 100644
--- a/src/cls/IPM/Utils/Module.cls
+++ b/src/cls/IPM/Utils/Module.cls
@@ -114,7 +114,7 @@ ClassMethod LoadModuleReference(
// Ensure requested versions match those required by other modules in the namespace, excluding versions currently being installed
// (the requirements of such modules are already known to be satisfied)
- set tSC = ..GetRequiredVersionExpression(pModuleName,,.tExpression,.tSourceList)
+ set tSC = ..GetRequiredVersionExpression(pModuleName,,.pDependencyGraph,.tExpression,.tSourceList)
if $$$ISERR(tSC) {
quit
}
@@ -349,36 +349,79 @@ ClassMethod GetModuleObjectFromString(
/// Returns a semantic version expression capturing all version requirements for a given module name in the current namespace.
/// A list of modules to exclude may be provided (for example, if these modules would be updated at the same time).
+///
+/// If provided a dependency graph, will use versions defined there instead of what the SQL call returns.
+/// This is especially important for the update command, where we want to check requirements with post-update versions as opposed to what's currently installed.
ClassMethod GetRequiredVersionExpression(
- pModuleName As %String,
- pExcludeModules As %List = "",
- Output pExpression As %IPM.General.SemanticVersionExpression,
- Output pSourceList As %List) As %Status
+ moduleName As %String,
+ excludeModules As %List = "",
+ ByRef dependencyGraph,
+ Output expression As %IPM.General.SemanticVersionExpression,
+ Output sourceList As %List) As %Status
{
- set tSC = $$$OK
+ set sc = $$$OK
try {
- set pExpression = ##class(%IPM.General.SemanticVersionExpression).%New()
- set pSourceList = ""
+ // Add modules from the dependency graph to the excludeModules list
+ if ($data(dependencyGraph)) {
+ do ..ConstructInvertedDependencyGraph(.dependencyGraph, .invertedDependencyGraph)
+ set flatDepList = ..GetFlatDependencyListFromInvertedDependencyGraph(.invertedDependencyGraph)
+ for i = 1:1:flatDepList.Count() {
+ set excludeModules = excludeModules _ $listbuild(flatDepList.GetAt(i).Name)
+ }
+ }
- set tResult = ##class(%IPM.Storage.Module).VersionRequirementsFunc(pModuleName,pExcludeModules)
- if (tResult.%SQLCODE < 0) {
- $$$ThrowStatus($$$ERROR($$$SQLCode,tResult.%SQLCODE,tResult.%Message))
+ set expression = ##class(%IPM.General.SemanticVersionExpression).%New()
+ set sourceList = ""
+
+ set result = ##class(%IPM.Storage.Module).VersionRequirementsFunc(moduleName,excludeModules)
+ if (result.%SQLCODE < 0) {
+ $$$ThrowStatus($$$ERROR($$$SQLCode,result.%SQLCODE,result.%Message))
}
- while tResult.%Next(.tSC) {
- $$$ThrowOnError(tSC)
- set tVersion = tResult.%Get("Version")
- $$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(tVersion,.tVersionExpr))
- set pExpression = pExpression.And(tVersionExpr)
- set pSourceList = pSourceList_$listbuild($listtostring(tResult.%Get("ModuleNames"),", ")_": "_tVersion)
+ while result.%Next(.sc) {
+ $$$ThrowOnError(sc)
+ set version = result.%Get("Version")
+ $$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(version,.versionExpr))
+ set expression = expression.And(versionExpr)
+ set sourceList = sourceList_$listbuild($listtostring(result.%Get("ModuleNames"),", ")_": "_version)
+ }
+ $$$ThrowOnError(sc)
+
+ // Add modules from the dependency graph to the expression and sourceList objects
+ if ($data(dependencyGraph)) {
+ set key = ""
+ for {
+ set key = $order(dependencyGraph(moduleName, key))
+ if (key = "") {
+ quit
+ }
+ // Name of module which requires this one + the version expression string it requires
+ set requiringModuleName = $piece(key, " ")
+ set version = dependencyGraph(moduleName, key)
+
+ // Iterate over sourceList; if the required version expression is equivalent to this one, add this module name to that version
+ set newVersion = 1
+ for i=1:1:$listlength(sourceList) {
+ if $find($listget(sourceList, i), version) {
+ set $list(sourceList, i) = requiringModuleName _ ", " _ $list(sourceList, i)
+ set newVersion = 0
+ quit
+ }
+ }
+ // If this is a new required version expression, add a new item to the list
+ if (newVersion) {
+ $$$ThrowOnError(##class(%IPM.General.SemanticVersionExpression).FromString(version, .versionExpr))
+ set expression = expression.And(versionExpr)
+ set sourceList = sourceList _ $listbuild(requiringModuleName _ ": " _ version)
+ }
+ }
}
- $$$ThrowOnError(tSC)
} catch e {
- set pExpression = $$$NULLOREF
- set pSourceList = ""
- set tSC = e.AsStatus()
+ set expression = $$$NULLOREF
+ set sourceList = ""
+ set sc = e.AsStatus()
}
- quit tSC
+ quit sc
}
/// Returns a flat list of dependents for a given module name (and optional version)
@@ -1278,7 +1321,7 @@ ClassMethod LoadDependencies(
// Ignore modules already installed that do not need to be installed again
continue
}
- set sc = ..LoadModuleReference(moduleReference.ServerName, moduleReference.Name, moduleReference.VersionString, moduleReference.Deployed, moduleReference.PlatformVersion, .pParams)
+ set sc = ..LoadModuleReference(moduleReference.ServerName, moduleReference.Name, moduleReference.VersionString, moduleReference.Deployed, moduleReference.PlatformVersion, .pParams, .dependencyGraph)
$$$ThrowOnError(sc)
}
diff --git a/tests/integration_tests/Test/PM/Integration/Update.cls b/tests/integration_tests/Test/PM/Integration/Update.cls
index 9fe4a9a6f..b00907917 100644
--- a/tests/integration_tests/Test/PM/Integration/Update.cls
+++ b/tests/integration_tests/Test/PM/Integration/Update.cls
@@ -662,4 +662,65 @@ Method TestDevModePropagation()
do $$$AssertEquals(dep.DeveloperMode, 0)
}
+/// Test case to make sure that update doesn't fail due to version errors that get resolved as a part of update
+/// i.e. - Module A 2.0.0 requires Module B ^1.0.0
+/// - Module A 2.1.0 requires Module B ^2.0.0
+/// - Pre-Update: Module A 2.0.0, Module B 1.0.0
+/// - Post-Update: Module A 2.1.0, Module B 2.0.0
+/// - Don't want a version error to be thrown in flight when Module A is 2.1.0 and Module B is still 1.0.0
+Method TestDependencyVersionsUpdated()
+{
+ // Define variables for this test
+ set modA = "module-a"
+ set modB = "module-b"
+ set modC = "module-c"
+ set modD = "module-d"
+
+ set modAPreVersion = "2.0.0"
+ set modBPreVersion = "1.0.0"
+ set modCPreVersion = "5.6.45+snapshot"
+
+ set modAPostVersion = "2.1.0+snapshot"
+ set modBPostVersion = "2.0.0+snapshot"
+ set modCPostVersion = "6.2.0+snapshot"
+
+ // Install base module module-a
+ set sc = ##class(%IPM.Main).Shell("install -v " _ modA _ " " _ modAPreVersion)
+ do $$$AssertStatusOK(sc, "Successfully installed " _ modA _ " " _ modAPreVersion)
+
+ // Confirm that module-a and dependencies were installed with the correct versions
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modA)
+ do $$$AssertEquals(mod.Version.ToString(), modAPreVersion, "Module " _ modA _ " correctly installed as " _ modAPreVersion)
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modB)
+ do $$$AssertEquals(mod.Version.ToString(), modBPreVersion, "Module " _ modB _ " correctly installed as " _ modBPreVersion)
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modC)
+ do $$$AssertEquals(mod.Version.ToString(), modCPreVersion, "Module " _ modC _ " correctly installed as " _ modCPreVersion)
+
+ // Install module-d prior to update
+ set sc = ##class(%IPM.Main).Shell("install -v " _ modD)
+ do $$$AssertStatusOK(sc, "Successfully installed " _ modD)
+
+ // Update base module module-a. Update should error due to version incompatabilities caused by module-d
+ set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion)
+ do $$$AssertStatusNotOK(sc, "Updating " _ modA _ " fails due to version errors with previously installed " _ modD)
+
+ // Uninstall module-d then try the update again
+ set sc = ##class(%IPM.Main).Shell("uninstall -v " _ modD)
+ do $$$AssertStatusOK(sc, "Successfully uninstalled " _ modD)
+ set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion)
+ do $$$AssertStatusOK(sc, "Successfully updated " _ modA _ " and its dependencies")
+
+ // Confirm that module-a and dependencies were updated to the correct versions
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modA)
+ do $$$AssertEquals(mod.Version.ToString(), modAPostVersion, "Module " _ modA _ " correctly installed as " _ modAPostVersion)
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modB)
+ do $$$AssertEquals(mod.Version.ToString(), modBPostVersion, "Module " _ modB _ " correctly installed as " _ modBPostVersion)
+ set mod = ##class(%IPM.Storage.Module).NameOpen(modC)
+ do $$$AssertEquals(mod.Version.ToString(), modCPostVersion, "Module " _ modC _ " correctly installed as " _ modCPostVersion)
+
+ // Uninstall module-a and dependencies
+ set sc = ##class(%IPM.Main).Shell("uninstall -r " _ modA)
+ do $$$AssertStatusOK(sc, "Successfully uninstalled " _ modA _ " and dependencies")
+}
+
}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/ModuleA/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/ModuleA/Class1.cls
new file mode 100644
index 000000000..20027a563
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/ModuleA/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleA.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleA.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml
new file mode 100644
index 000000000..d5f02ab07
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ module-a
+ 2.1.0+snapshot
+
+
+
+ module-b
+ ^2.0.0
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/cls/ModuleB/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/cls/ModuleB/Class1.cls
new file mode 100644
index 000000000..6b96fe0bd
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/cls/ModuleB/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleB.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleB.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/module.xml
new file mode 100644
index 000000000..d85acd61f
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-b/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ module-b
+ 2.0.0+snapshot
+
+
+
+ module-c
+ ^6.1.15
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/ModuleC/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/ModuleC/Class1.cls
new file mode 100644
index 000000000..3d8712188
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/ModuleC/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleC.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleC.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml
new file mode 100644
index 000000000..7c0928fff
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ module-c
+ 6.2.0+snapshot
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/cls/ModuleA/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/cls/ModuleA/Class1.cls
new file mode 100644
index 000000000..20027a563
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/cls/ModuleA/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleA.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleA.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/module.xml
new file mode 100644
index 000000000..3d7525cca
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-a/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ module-a
+ 2.0.0
+
+
+
+ module-b
+ 1.0.0
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/ModuleB/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/ModuleB/Class1.cls
new file mode 100644
index 000000000..6b96fe0bd
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/ModuleB/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleB.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleB.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml
new file mode 100644
index 000000000..c5376a6f5
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ module-b
+ 1.0.0
+
+
+
+ module-c
+ ^5.4.1
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/ModuleC/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/ModuleC/Class1.cls
new file mode 100644
index 000000000..3d8712188
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/ModuleC/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleC.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleC.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml
new file mode 100644
index 000000000..293705f51
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ module-c
+ 5.6.45+snapshot
+
+
+
+
\ No newline at end of file
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/cls/ModuleD/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/cls/ModuleD/Class1.cls
new file mode 100644
index 000000000..47801f80f
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/cls/ModuleD/Class1.cls
@@ -0,0 +1,9 @@
+Class ModuleD.Class1
+{
+
+ClassMethod MethodA()
+{
+ write !, "This is ##class(ModuleD.Class1).MethodA()"
+}
+
+}
diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/module.xml
new file mode 100644
index 000000000..ac813f17f
--- /dev/null
+++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-d/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ module-d
+ 1.0.0
+
+
+
+ module-c
+ ^5.3.0
+
+
+
+
+
\ No newline at end of file