Skip to content

Commit 5ee165c

Browse files
authored
Merge pull request #2173 from modelix/fix/change-config
- Fixed return value of getRepositoryConfig for legacyNameBasedRoles (was inverted) - Implemented migration for changing legacyGlobalStorage (both directions were unimplemented) - Corrected the comment about allowed and disallowed migrations - Throw an exception for unsupported (unimplemented) migrations
2 parents 1f17926 + eb7a786 commit 5ee165c

File tree

3 files changed

+269
-56
lines changed

3 files changed

+269
-56
lines changed

model-server/src/main/kotlin/org/modelix/model/server/handlers/HttpException.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,18 @@ class ObjectValueNotFoundException(objectHash: String, cause: Throwable? = null)
175175
type = "/problems/object-value-not-found",
176176
cause = cause,
177177
)
178+
179+
/**
180+
* An [HttpException] indicating that a requested repository migration is not supported.
181+
*
182+
* @param reason description of why the migration is not supported
183+
* @param cause The causing exception for the bad request or null if none.
184+
*/
185+
class UnsupportedMigrationException(reason: String, cause: Throwable? = null) :
186+
HttpException(
187+
HttpStatusCode.NotImplemented,
188+
title = "Unsupported repository migration",
189+
details = reason,
190+
type = "/problems/unsupported-migration",
191+
cause = cause,
192+
)

model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt

Lines changed: 147 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -474,42 +474,161 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager {
474474

475475
/**
476476
* Possible migrations:
477-
* - name<-->id based roles: not possible without the metamodel
478-
* - global-->isolated storage: copy all reachable objects
479-
* - isolated storage --> global storage: possible, but no use cases for it
480-
* - int64-->string node IDs: create new version with different tree implementation and copy all content
481-
* - string-->int64 node IDs: bulk sync into new version with reassigned IDs
482-
* - changed model ID: bulk sync into new version with reassigned IDs
477+
* - name<-->id based roles: not supported (requires metamodel to map role names to role IDs)
478+
* - global<-->isolated storage: works (copies all reachable objects and updates repository lists)
479+
* - int64-->string node IDs: works (references preserved as strings)
480+
* - string-->int64 node IDs: works only if string IDs are valid hex-encoded int64 values
481+
* - changed model ID: not supported (NodeAssociationForIdMigration cannot resolve nodes across different model IDs)
483482
*/
484483
@RequiresTransaction
485484
override fun migrateRepository(
486485
newConfig: RepositoryConfig,
487486
author: String?,
488487
) {
489488
val repositoryId = RepositoryId(newConfig.repositoryId)
490-
val oldConfig = getConfig(repositoryId, repositoryId.getBranchReference(getBranchNames(repositoryId).first()))
491-
492-
if (oldConfig.nodeIdType == RepositoryConfig.NodeIdType.INT64 && newConfig.nodeIdType == RepositoryConfig.NodeIdType.STRING) {
493-
val branches = getBranches(repositoryId)
494-
for (branch in branches) {
495-
val oldVersion = getVersion(branch) ?: continue
496-
val sourceModel = oldVersion.getModelTree().asModelSingleThreaded()
497-
val targetTree = createEmptyTree(newConfig).asMutableSingleThreaded()
498-
val targetModel = targetTree.asModel()
499-
ModelSynchronizer(
500-
sourceRoot = sourceModel.getRootNode(),
501-
targetRoot = targetModel.getRootNode(),
502-
nodeAssociation = NodeAssociationForIdMigration(targetModel),
503-
).synchronize()
504-
val newVersion = IVersion.builder()
505-
.tree(targetTree.getTransaction().tree)
506-
.baseVersion(oldVersion)
507-
.author(author)
508-
.currentTime()
509-
.build()
510-
putVersionHash(branch, newVersion.getContentHash())
489+
val currentConfig = getConfig(repositoryId, repositoryId.getBranchReference())
490+
491+
// Validate that the migration is supported
492+
validateMigration(currentConfig, newConfig)
493+
494+
val currentIsolated = isIsolated(repositoryId)!!
495+
val targetIsolated = !newConfig.legacyGlobalStorage
496+
497+
// Handle storage migration first
498+
if (currentIsolated != targetIsolated) {
499+
migrateStorage(repositoryId, currentIsolated, targetIsolated)
500+
}
501+
502+
// Skip tree migration if nothing relevant has changed
503+
if (currentConfig.legacyNameBasedRoles == newConfig.legacyNameBasedRoles &&
504+
currentConfig.nodeIdType == newConfig.nodeIdType &&
505+
currentConfig.modelId == newConfig.modelId
506+
) {
507+
return
508+
}
509+
510+
// Perform tree migration for other config changes
511+
val branches = getBranches(repositoryId)
512+
for (branch in branches) {
513+
val oldVersion = getVersion(branch) ?: continue
514+
val sourceModel = oldVersion.getModelTree().asModelSingleThreaded()
515+
val targetTree = createEmptyTree(newConfig).asMutableSingleThreaded()
516+
val targetModel = targetTree.asModel()
517+
ModelSynchronizer(
518+
sourceRoot = sourceModel.getRootNode(),
519+
targetRoot = targetModel.getRootNode(),
520+
nodeAssociation = NodeAssociationForIdMigration(targetModel),
521+
).synchronize()
522+
val newVersion = IVersion.builder()
523+
.tree(targetTree.getTransaction().tree)
524+
.baseVersion(oldVersion)
525+
.author(author)
526+
.currentTime()
527+
.build()
528+
putVersionHash(branch, newVersion.getContentHash())
529+
}
530+
}
531+
532+
private fun validateMigration(currentConfig: RepositoryConfig, newConfig: RepositoryConfig) {
533+
// Check for role storage migration (name-based <-> id-based)
534+
if (currentConfig.legacyNameBasedRoles != newConfig.legacyNameBasedRoles) {
535+
throw UnsupportedMigrationException(
536+
"Migration between name-based and id-based role storage is not supported. " +
537+
"This requires a metamodel to map role names to role IDs.",
538+
)
539+
}
540+
541+
// Check for model ID changes
542+
if (currentConfig.modelId != newConfig.modelId) {
543+
throw UnsupportedMigrationException(
544+
"Changing the model ID is not supported. " +
545+
"Node references include the model ID and cannot be resolved across different model IDs.",
546+
)
547+
}
548+
549+
// Note: Storage migration (global <-> isolated) is supported and handled separately
550+
// Note: int64 -> string node IDs is supported
551+
// Note: string -> int64 node IDs is supported if strings are valid hex-encoded int64 values
552+
// (validation happens at runtime during the migration)
553+
}
554+
555+
@RequiresTransaction
556+
private fun migrateStorage(repositoryId: RepositoryId, fromIsolated: Boolean, toIsolated: Boolean) {
557+
// Collect branches, version hashes, and branch names BEFORE updating repository lists
558+
val branches = getBranches(repositoryId)
559+
val versionHashes = branches.mapNotNull { getVersionHash(it) }.toSet()
560+
val branchNames = getBranchNames(repositoryId)
561+
562+
if (fromIsolated && !toIsolated) {
563+
// isolated → global: copy all objects from this repository to global storage
564+
stores.genericStore.copyRepositoryObjects(repositoryId, RepositoryId(""))
565+
}
566+
567+
// Update repository lists
568+
val fromRepositories = getRepositories(fromIsolated)
569+
val toRepositories = getRepositories(toIsolated)
570+
stores.getGlobalStoreClient().put(
571+
repositoriesListKey(fromIsolated),
572+
(fromRepositories - repositoryId).joinToString("\n") { it.id },
573+
)
574+
stores.getGlobalStoreClient().put(
575+
repositoriesListKey(toIsolated),
576+
(toRepositories + repositoryId).joinToString("\n") { it.id },
577+
)
578+
579+
// Update branch list
580+
stores.genericStore.put(branchListKey(repositoryId, fromIsolated), null)
581+
stores.genericStore.put(branchListKey(repositoryId, toIsolated), branchNames.joinToString("\n"))
582+
583+
// Migrate version hashes to new storage location
584+
for (branch in branches) {
585+
val versionHash = stores.genericStore[branchKey(branch, fromIsolated)]
586+
if (versionHash != null) {
587+
stores.genericStore.put(branchKey(branch, toIsolated), versionHash, false)
588+
stores.genericStore.put(branchKey(branch, fromIsolated), null, false)
511589
}
512590
}
591+
592+
if (!fromIsolated && toIsolated) {
593+
// global → isolated: Copy only objects reachable from this repository's versions
594+
copyReachableObjectsToIsolatedStorage(repositoryId, versionHashes)
595+
}
596+
}
597+
598+
@RequiresTransaction
599+
private fun copyReachableObjectsToIsolatedStorage(repositoryId: RepositoryId, versionHashes: Set<String>) {
600+
if (versionHashes.isEmpty()) return
601+
602+
val sourceStore = stores.getAsyncStore(null) // global storage
603+
val objectsToCopy = mutableMapOf<ObjectInRepository, String>()
604+
605+
// Collect all reachable object hashes from the repository's versions
606+
val reachableHashes = mutableSetOf<String>()
607+
608+
for (versionHash in versionHashes) {
609+
// Add the version hash itself
610+
reachableHashes.add(versionHash)
611+
612+
// Load the version and collect all objects it references
613+
val version = CLVersion.loadFromHash(versionHash, sourceStore)
614+
615+
// Use diff with empty list to get all objects reachable from this version
616+
version.diff(emptyList()).iterateBlocking(sourceStore) { obj ->
617+
reachableHashes.add(obj.getHashString())
618+
}
619+
}
620+
621+
// Copy all reachable objects to isolated storage
622+
for (hash in reachableHashes) {
623+
val globalKey = ObjectInRepository.global(hash)
624+
val value = stores.genericStore[globalKey]
625+
if (value != null) {
626+
val isolatedKey = ObjectInRepository(repositoryId.id, hash)
627+
objectsToCopy[isolatedKey] = value
628+
}
629+
}
630+
631+
stores.genericStore.putAll(objectsToCopy, silent = true)
513632
}
514633

515634
@RequiresTransaction
@@ -519,7 +638,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager {
519638
}
520639
val treeData = version.obj.data.getTree(TreeType.MAIN).resolveNow().data
521640
return RepositoryConfig(
522-
legacyNameBasedRoles = treeData.usesRoleIds,
641+
legacyNameBasedRoles = !treeData.usesRoleIds,
523642
legacyGlobalStorage = isIsolated(repositoryId) == false,
524643
nodeIdType = if (treeData.trieWithNodeRefIds != null) RepositoryConfig.NodeIdType.STRING else RepositoryConfig.NodeIdType.INT64,
525644
primaryTreeType = if (treeData.trieWithNodeRefIds != null) RepositoryConfig.TreeType.PATRICIA_TRIE else RepositoryConfig.TreeType.HASH_ARRAY_MAPPED_TRIE,

0 commit comments

Comments
 (0)