@@ -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