diff --git a/entity-store/src/main/kotlin/jetbrains/exodus/entitystore/orientdb/OVertexEntity.kt b/entity-store/src/main/kotlin/jetbrains/exodus/entitystore/orientdb/OVertexEntity.kt index 0c1db8005..be040170b 100644 --- a/entity-store/src/main/kotlin/jetbrains/exodus/entitystore/orientdb/OVertexEntity.kt +++ b/entity-store/src/main/kotlin/jetbrains/exodus/entitystore/orientdb/OVertexEntity.kt @@ -41,6 +41,14 @@ class OVertexEntity(private var vertex: OVertex, private val store: PersistentEn fun blobHashProperty(propertyName: String) = "\$$propertyName$STRING_BLOB_HASH_PROPERTY_NAME_SUFFIX" const val STRING_BLOB_CLASS_NAME: String = "StringBlob" + + // Backward compatible EntityId + + const val CLASS_ID_CUSTOM_PROPERTY_NAME = "classId" + const val CLASS_ID_SEQUENCE_NAME = "sequence_classId" + + const val BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME = "backwardCompatibleLocalEntityId" + fun localEntityIdSequenceName(className: String): String = "${className}_sequence_localEntityId" } private val activeSession get() = ODatabaseSession.getActiveSession() diff --git a/query/src/main/kotlin/jetbrains/exodus/query/metadata/IndicesCreator.kt b/query/src/main/kotlin/jetbrains/exodus/query/metadata/IndicesCreator.kt index 9c92678a9..f402abeb5 100644 --- a/query/src/main/kotlin/jetbrains/exodus/query/metadata/IndicesCreator.kt +++ b/query/src/main/kotlin/jetbrains/exodus/query/metadata/IndicesCreator.kt @@ -73,7 +73,7 @@ data class DeferredIndex( ) { constructor(ownerVertexName: String, properties: List, unique: Boolean): this( ownerVertexName, - indexName = "${ownerVertexName}_${properties.joinToString("_") { it.name }}", + indexName = "${ownerVertexName}_${properties.joinToString("_") { it.name }}${if (unique) "_unique" else ""}", properties, unique = unique ) diff --git a/query/src/main/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializer.kt b/query/src/main/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializer.kt index 0fe6c90f5..414cb6aef 100644 --- a/query/src/main/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializer.kt +++ b/query/src/main/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializer.kt @@ -20,9 +20,14 @@ import com.orientechnologies.orient.core.db.ODatabaseSession import com.orientechnologies.orient.core.metadata.schema.OClass import com.orientechnologies.orient.core.metadata.schema.OProperty import com.orientechnologies.orient.core.metadata.schema.OType +import com.orientechnologies.orient.core.metadata.sequence.OSequence import com.orientechnologies.orient.core.record.ODirection import com.orientechnologies.orient.core.record.OVertex import jetbrains.exodus.entitystore.orientdb.OVertexEntity +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_CUSTOM_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_SEQUENCE_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.localEntityIdSequenceName import mu.KotlinLogging private val log = KotlinLogging.logger {} @@ -30,9 +35,10 @@ private val log = KotlinLogging.logger {} fun ODatabaseSession.applySchema( model: ModelMetaDataImpl, indexForEverySimpleProperty: Boolean = false, - applyLinkCardinality: Boolean = true + applyLinkCardinality: Boolean = true, + backwardCompatibleEntityId: Boolean = false, ): Map> { - val initializer = OrientDbSchemaInitializer(model, this, indexForEverySimpleProperty, applyLinkCardinality) + val initializer = OrientDbSchemaInitializer(model, this, indexForEverySimpleProperty, applyLinkCardinality, backwardCompatibleEntityId) initializer.apply() return initializer.getIndices() } @@ -41,7 +47,8 @@ internal class OrientDbSchemaInitializer( private val dnqModel: ModelMetaDataImpl, private val oSession: ODatabaseSession, private val indexForEverySimpleProperty: Boolean, - private val applyLinkCardinality: Boolean + private val applyLinkCardinality: Boolean, + private val backwardCompatibleEntityId: Boolean ) { private val paddedLogger = PaddedLogger(log) @@ -52,10 +59,10 @@ internal class OrientDbSchemaInitializer( private fun appendLine(s: String = "") = paddedLogger.appendLine(s) - private val indices = HashMap>() + private val indices = HashMap>() private fun addIndex(index: DeferredIndex) { - indices.getOrPut(index.ownerVertexName) { HashSet() }.add(index) + indices.getOrPut(index.ownerVertexName) { HashMap() }[index.indexName] = index } private fun simplePropertyIndex(entityName: String, propertyName: String): DeferredIndex { @@ -65,11 +72,14 @@ internal class OrientDbSchemaInitializer( return DeferredIndex(entityName, listOf(indexField), unique = false) } - fun getIndices(): Map> = indices - + fun getIndices(): Map> = indices.map { it.key to it.value.values.toSet() }.toMap() fun apply() { try { + if (backwardCompatibleEntityId) { + createClassIdSequenceIfAbsent() + } + appendLine("applying the DNQ schema to OrientDB") val sortedEntities = dnqModel.entitiesMetaData.sortedTopologically() @@ -120,8 +130,8 @@ internal class OrientDbSchemaInitializer( for ((indexOwner, indices) in indices) { appendLine("$indexOwner:") withPadding { - for (index in indices) { - appendLine(index.indexName) + for ((indexName, _) in indices) { + appendLine(indexName) } } } @@ -131,6 +141,20 @@ internal class OrientDbSchemaInitializer( } } + // ClassId + + private fun createClassIdSequenceIfAbsent() { + createSequenceIfAbsent(CLASS_ID_SEQUENCE_NAME) + } + + private fun OClass.setClassIdIfAbsent() { + if (getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME) == null) { + val sequences = oSession.metadata.sequenceLibrary + val sequence: OSequence = sequences.getSequence(CLASS_ID_SEQUENCE_NAME) ?: throw IllegalStateException("$CLASS_ID_SEQUENCE_NAME not found") + + setCustom(CLASS_ID_CUSTOM_PROPERTY_NAME, sequence.next().toString()) + } + } // Vertices and Edges @@ -140,6 +164,16 @@ internal class OrientDbSchemaInitializer( oClass.applySuperClass(dnqEntity.superType) appendLine() + if (backwardCompatibleEntityId) { + oClass.setClassIdIfAbsent() + createSequenceIfAbsent(localEntityIdSequenceName(dnqEntity.type)) + /* + * We do not apply a unique index to the localEntityId property because indices in OrientDB are polymorphic. + * So, you can not have the same value in a property in an instance of a superclass and in an instance of its subclass. + * But it exactly what happens in the original Xodus. + * */ + } + /* * It is more efficient to create indices after the data migration. * So, we only remember indices here and let the user create them later. @@ -302,6 +336,12 @@ internal class OrientDbSchemaInitializer( oClass.applySimpleProperty(propertyMetaData, required || requiredBecauseOfIndex) } } + if (backwardCompatibleEntityId) { + val prop = SimplePropertyMetaDataImpl(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME, "long") + oClass.applySimpleProperty(prop, true) + // we need this index regardless what we have in indexForEverySimpleProperty + addIndex(simplePropertyIndex(dnqEntity.type, BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME)) + } } } @@ -515,6 +555,15 @@ internal class OrientDbSchemaInitializer( return oProperty } + private fun createSequenceIfAbsent(sequenceName: String) { + val sequences = oSession.metadata.sequenceLibrary + if (sequences.getSequence(sequenceName) == null) { + val params = OSequence.CreateParams() + params.start = 0L + sequences.createSequence(sequenceName, OSequence.SEQUENCE_TYPE.ORDERED, params) + } + } + private fun getOType(jvmTypeName: String): OType { return when (jvmTypeName.lowercase()) { "boolean" -> OType.BOOLEAN diff --git a/query/src/main/kotlin/jetbrains/exodus/query/metadata/XodusToOrientDataMigrator.kt b/query/src/main/kotlin/jetbrains/exodus/query/metadata/XodusToOrientDataMigrator.kt index 7edd1e6df..e7035a03d 100644 --- a/query/src/main/kotlin/jetbrains/exodus/query/metadata/XodusToOrientDataMigrator.kt +++ b/query/src/main/kotlin/jetbrains/exodus/query/metadata/XodusToOrientDataMigrator.kt @@ -1,13 +1,31 @@ package jetbrains.exodus.query.metadata import com.orientechnologies.orient.core.db.ODatabaseSession +import com.orientechnologies.orient.core.metadata.schema.OType +import com.orientechnologies.orient.core.metadata.sequence.OSequence import jetbrains.exodus.entitystore.EntityId import jetbrains.exodus.entitystore.PersistentEntityStore import jetbrains.exodus.entitystore.orientdb.OPersistentEntityStore import jetbrains.exodus.entitystore.orientdb.OVertexEntity +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.BINARY_BLOB_CLASS_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_CUSTOM_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_SEQUENCE_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.localEntityIdSequenceName import jetbrains.exodus.entitystore.orientdb.withSession +fun migrateDataFromXodusToOrientDb( + xodus: PersistentEntityStore, + orient: OPersistentEntityStore, + /* + * How many entities should be copied in a single transaction + * */ + entitiesPerTransaction: Int = 10, + backwardCompatibleEntityId: Boolean = false +) { + val migrator = XodusToOrientDataMigrator(xodus, orient, entitiesPerTransaction, backwardCompatibleEntityId) + return migrator.migrate() +} /** * This class is responsible for migrating data from Xodus to OrientDB. @@ -16,13 +34,14 @@ import jetbrains.exodus.entitystore.orientdb.withSession * @param orient The OrientDB OPersistentEntityStore instance. * @param entitiesPerTransaction The number of entities to be copied in a single transaction. */ -class XodusToOrientDataMigrator( +internal class XodusToOrientDataMigrator( private val xodus: PersistentEntityStore, private val orient: OPersistentEntityStore, /* * How many entities should be copied in a single transaction * */ - private val entitiesPerTransaction: Int = 10 + private val entitiesPerTransaction: Int = 10, + private val backwardCompatibleEntityId: Boolean = false ) { private val xEntityIdToOEntityId = HashMap() @@ -38,12 +57,33 @@ class XodusToOrientDataMigrator( private fun createVertexClassesIfAbsent(oSession: ODatabaseSession) { // make sure all the vertex classes are created in OrientDB // classes can not be created in a transaction, so we have to create them before copying the data + var maxClassId = 0 xodus.withReadonlyTx { xTx -> for (type in xTx.entityTypes) { - oSession.getClass(type) ?: oSession.createVertexClass(type) + val oClass = oSession.getClass(type) ?: oSession.createVertexClass(type) + val classId = xodus.getEntityTypeId(type) + + if (backwardCompatibleEntityId) { + oClass.setCustom(CLASS_ID_CUSTOM_PROPERTY_NAME, classId.toString()) + maxClassId = maxOf(maxClassId, classId) + + // create localEntityId property if absent + if (oClass.getProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME) == null) { + oClass.createProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME, OType.LONG) + } + } } } + if (backwardCompatibleEntityId) { + // create a sequence to generate classIds + val sequences = oSession.metadata.sequenceLibrary + + require(sequences.getSequence(CLASS_ID_SEQUENCE_NAME) == null) { "$CLASS_ID_SEQUENCE_NAME is already created. It means that some data migration has happened to the target database before. Such a scenario is not supported." } + + oSession.createSequenceIfAbsent(CLASS_ID_SEQUENCE_NAME, maxClassId.toLong()) + } + oSession.getClass(BINARY_BLOB_CLASS_NAME) ?: oSession.createClass(BINARY_BLOB_CLASS_NAME) } @@ -58,6 +98,7 @@ class XodusToOrientDataMigrator( xodus.withReadonlyTx { xTx -> oSession.withCountingTx(entitiesPerTransaction) { countingTx -> for (type in xTx.entityTypes) { + var largestEntityId = 0L for (xEntity in xTx.getAll(type)) { val vertex = oSession.newVertex(type) val oEntity = OVertexEntity(vertex, orient) @@ -73,6 +114,18 @@ class XodusToOrientDataMigrator( countingTx.increment() edgeClassesToCreate.addAll(xEntity.linkNames) + + if (backwardCompatibleEntityId) { + // copy localEntityId + val localEntityId = xEntity.id.localId + oEntity.setProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME, localEntityId) + + largestEntityId = maxOf(largestEntityId, localEntityId) + } + } + if (backwardCompatibleEntityId) { + // create a sequence to generate localEntityIds for the class + oSession.createSequenceIfAbsent(localEntityIdSequenceName(type), largestEntityId) } } } @@ -80,6 +133,15 @@ class XodusToOrientDataMigrator( return edgeClassesToCreate } + private fun ODatabaseSession.createSequenceIfAbsent(sequenceName: String, startingFrom: Long) { + val sequences = metadata.sequenceLibrary + if (sequences.getSequence(sequenceName) == null) { + val params = OSequence.CreateParams() + params.start = startingFrom + sequences.createSequence(sequenceName, OSequence.SEQUENCE_TYPE.ORDERED, params) + } + } + private fun copyLinks(oSession: ODatabaseSession) { xodus.withReadonlyTx { xTx -> oSession.withCountingTx(entitiesPerTransaction) { countingTx -> @@ -87,6 +149,7 @@ class XodusToOrientDataMigrator( for (xEntity in xTx.getAll(type)) { val oEntityId = xEntityIdToOEntityId.getValue(xEntity.id) val oEntity = orient.getEntity(oEntityId) + var copiedSomeLinks = false for (linkName in xEntity.linkNames) { for (xTargetEntity in xEntity.getLinks(linkName)) { diff --git a/query/src/test/kotlin/jetbrains/exodus/query/metadata/MigrateDataTest.kt b/query/src/test/kotlin/jetbrains/exodus/query/metadata/MigrateDataTest.kt index 54529b7de..503562497 100644 --- a/query/src/test/kotlin/jetbrains/exodus/query/metadata/MigrateDataTest.kt +++ b/query/src/test/kotlin/jetbrains/exodus/query/metadata/MigrateDataTest.kt @@ -6,11 +6,18 @@ import com.orientechnologies.orient.core.record.impl.OVertexDocument import jetbrains.exodus.entitystore.StoreTransaction import jetbrains.exodus.entitystore.XodusTestDB import jetbrains.exodus.entitystore.orientdb.OVertexEntity +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_CUSTOM_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_SEQUENCE_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.localEntityIdSequenceName import jetbrains.exodus.entitystore.orientdb.testutil.InMemoryOrientDB +import junit.framework.TestCase.assertNull import org.junit.Assert import org.junit.Rule import org.junit.Test import java.io.ByteArrayInputStream +import kotlin.test.assertEquals +import kotlin.test.assertTrue class MigrateDataTest { @@ -37,7 +44,7 @@ class MigrateDataTest { tx.createEntities(entities) } - XodusToOrientDataMigrator(xodus.store, orientDb.store).migrate() + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store) orientDb.withSession { oSession -> oSession.assertOrientContainsAllTheEntities(entities) @@ -60,7 +67,7 @@ class MigrateDataTest { tx.createEntities(entities) } - XodusToOrientDataMigrator(xodus.store, orientDb.store).migrate() + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store) orientDb.withSession { oSession -> oSession.assertOrientContainsAllTheEntities(entities) @@ -95,26 +102,161 @@ class MigrateDataTest { tx.createEntities(entities) } - XodusToOrientDataMigrator(xodus.store, orientDb.store).migrate() + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store) orientDb.withSession { oSession -> oSession.assertOrientContainsAllTheEntities(entities) } } + @Test + fun `if backward compatible EntityId enabled, copy existing class IDs and create the sequence to generate new class IDs`() { + val entities = pileOfEntities( + eProps("type1", 1), + eProps("type1", 2), + eProps("type1", 3), + + eProps("type2", 2), + eProps("type2", 4), + eProps("type2", 5), + ) + xodus.withTx { tx -> + tx.createEntities(entities) + } + + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store, backwardCompatibleEntityId = true) + + var maxClassId = 0 + xodus.withTx { xTx -> + orientDb.withSession { oSession -> + for (type in xTx.entityTypes) { + val typeId = xodus.store.getEntityTypeId(type) + Assert.assertEquals(typeId, oSession.getClass(type).getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME).toInt()) + maxClassId = maxOf(maxClassId, typeId) + } + assertTrue(maxClassId > 0) + + val nextGeneratedClassId = oSession.metadata.sequenceLibrary.getSequence(CLASS_ID_SEQUENCE_NAME).next() + assertEquals(maxClassId.toLong() + 1, nextGeneratedClassId) + } + } + } + + @Test + fun `if backward compatible EntityId disabled, ignore class IDs`() { + val entities = pileOfEntities( + eProps("type1", 1), + eProps("type1", 2), + eProps("type1", 3), + + eProps("type2", 2), + eProps("type2", 4), + eProps("type2", 5), + ) + xodus.withTx { tx -> + tx.createEntities(entities) + } + + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store) + + xodus.withTx { xTx -> + orientDb.withSession { oSession -> + for (type in xTx.entityTypes) { + Assert.assertNull(oSession.getClass(type).getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME)) + } + Assert.assertNull(oSession.metadata.sequenceLibrary.getSequence(CLASS_ID_SEQUENCE_NAME)) + } + } + } + + @Test + fun `if backward compatible EntityId enabled, copy localEntityId for every entity and create a sequence for every class to generate new localEntityIds`() { + val entities = pileOfEntities( + eProps("type1", 1), + eProps("type1", 2), + eProps("type1", 3), + + eProps("type2", 2), + eProps("type2", 4), + eProps("type2", 5), + ) + xodus.withTx { tx -> + tx.createEntities(entities) + } + + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store, backwardCompatibleEntityId = true) + + xodus.withTx { xTx -> + orientDb.withSession { oSession -> + for (type in xTx.entityTypes) { + val xTestIdToLocalEntityId = HashMap() + val oTestIdToLocalEntityId = HashMap() + var maxLocalEntityId = 0L + for (xEntity in xTx.getAll(type)) { + val testId = xEntity.getProperty("id") as Int + val localEntityId = xEntity.id.localId + xTestIdToLocalEntityId[testId] = localEntityId + maxLocalEntityId = maxOf(maxLocalEntityId, localEntityId) + } + + for (oEntity in oSession.browseClass(type)) { + val testId = oEntity.getTestId() + val localEntityId = oEntity.getProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME) + oTestIdToLocalEntityId[testId] = localEntityId + } + + assertTrue(maxLocalEntityId > 0) + val nextGeneratedLocalEntityId = oSession.metadata.sequenceLibrary.getSequence(localEntityIdSequenceName(type)).next() + assertEquals(maxLocalEntityId + 1, nextGeneratedLocalEntityId) + + assertEquals(xTestIdToLocalEntityId, oTestIdToLocalEntityId) + } + } + } + } + + @Test + fun `if backward compatible EntityId disabled, ignore localEntityId`() { + val entities = pileOfEntities( + eProps("type1", 1), + eProps("type1", 2), + eProps("type1", 3), + + eProps("type2", 2), + eProps("type2", 4), + eProps("type2", 5), + ) + xodus.withTx { tx -> + tx.createEntities(entities) + } + + migrateDataFromXodusToOrientDb(xodus.store, orientDb.store) + + xodus.withTx { xTx -> + orientDb.withSession { oSession -> + for (type in xTx.entityTypes) { + val oClass = oSession.getClass(type) + assertNull(oClass.getProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME)) + assertNull(oSession.metadata.sequenceLibrary.getSequence(localEntityIdSequenceName(type))) + } + } + } + } + private fun ODatabaseSession.assertOrientContainsAllTheEntities(pile: PileOfEntities) { for (type in pile.types) { for (record in this.browseClass(type)) { - val entity = pile.getEntity(type, record.getId()) - assertEquals(entity, record) + val entity = pile.getEntity(type, record.getTestId()) + record.assertEquals(entity) } } } - private fun assertEquals(expected: Entity, actualDocument: ODocument) { + private fun ODocument.assertEquals(expected: Entity) { + val actualDocument = this val actual = OVertexEntity(actualDocument as OVertexDocument, orientDb.store) - Assert.assertEquals(expected.id, actualDocument.getId()) + Assert.assertEquals(expected.id, actualDocument.getTestId()) for ((propName, propValue) in expected.props) { Assert.assertEquals(propValue, actual.getProperty(propName)) } @@ -130,7 +272,7 @@ class MigrateDataTest { } } - private fun ODocument.getId(): Int = getProperty("id") + private fun ODocument.getTestId(): Int = getProperty("id") private fun StoreTransaction.createEntities(pile: PileOfEntities) { for (type in pile.types) { diff --git a/query/src/test/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializerTest.kt b/query/src/test/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializerTest.kt index 6e1cc510b..da64cfdff 100644 --- a/query/src/test/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializerTest.kt +++ b/query/src/test/kotlin/jetbrains/exodus/query/metadata/OrientDbSchemaInitializerTest.kt @@ -22,12 +22,17 @@ import com.orientechnologies.orient.core.metadata.schema.OType import com.orientechnologies.orient.core.record.ODirection import com.orientechnologies.orient.core.record.OVertex import jetbrains.exodus.entitystore.orientdb.OVertexEntity +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.CLASS_ID_CUSTOM_PROPERTY_NAME +import jetbrains.exodus.entitystore.orientdb.OVertexEntity.Companion.localEntityIdSequenceName import jetbrains.exodus.entitystore.orientdb.testutil.InMemoryOrientDB import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull class OrientDbSchemaInitializerTest { @Rule @@ -328,8 +333,125 @@ class OrientDbSchemaInitializerTest { assertTrue(indices.isEmpty()) } + + // Backward compatible EntityId + + @Test + fun `backward compatible EntityId disabled, classId is null`(): Unit = orientDb.withSession { oSession -> + val types = listOf("type1", "type2", "type3") + val model = model { + for (type in types) { + entity(type) + } + } + + oSession.applySchema(model) + + for (type in types) { + assertNull(oSession.getClass(type).getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME)) + } + } + + @Test + fun `backward compatible EntityId enabled, classId is a monotonically increasing long`(): Unit = orientDb.withSession { oSession -> + val types = mutableListOf("type1", "type2", "type3") + val model = model { + for (type in types) { + entity(type) + } + } + + oSession.applySchema(model, backwardCompatibleEntityId = true) + + val classIds = mutableSetOf() + val classIdToClassName = mutableMapOf() + for (type in types) { + val classId = oSession.getClass(type).getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME).toLong() + classIdToClassName[classId] = type + classIds.add(classId) + } + assertEquals(setOf(1, 2, 3), classIds) + + + // emulate the next run of the application with new classes in the codebase + types.add("type4") + types.add("type5") + val anotherModel = model { + for (type in types) { + entity(type) + } + } + + oSession.applySchema(anotherModel, backwardCompatibleEntityId = true) + + classIds.clear() + for (type in types) { + val classId = oSession.getClass(type).getCustom(CLASS_ID_CUSTOM_PROPERTY_NAME).toLong() + // classId is not changed if it has been already assigned + if (classId in classIdToClassName) { + assertEquals(classIdToClassName.getValue(classId), type) + } + classIds.add(classId) + } + assertEquals(setOf(1, 2, 3, 4, 5), classIds) + } + + @Test + fun `backward compatible EntityId enabled, every class gets localEntityId property`(): Unit = orientDb.withSession { oSession -> + val types = mutableListOf("type1", "type2", "type3") + val model = model { + for (type in types) { + entity(type) + } + } + + val indices = oSession.applySchema(model, backwardCompatibleEntityId = true) + + val sequences = oSession.metadata.sequenceLibrary + for (type in types) { + assertNotNull(oSession.getClass(type).getProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME)) + // index for the localEntityId must be created regardless the indexForEverySimpleProperty param + indices.checkIndex(type, false, BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME) + // the index for localEntityId must not be unique, otherwise it will not let the same localEntityId + // for subtypes of a supertype + assertTrue(indices.getValue(type).none { it.unique }) + + val sequence = sequences.getSequence(localEntityIdSequenceName(type)) + assertNotNull(sequence) + assertEquals(1, sequence.next()) + } + + // emulate the next run of the application + oSession.applySchema(model, backwardCompatibleEntityId = true) + + for (type in types) { + val sequence = sequences.getSequence(localEntityIdSequenceName(type)) + // sequences are the same + assertEquals(2, sequence.next()) + } + } + + @Test + fun `backward compatible EntityId disable, localEntityId is ignored`(): Unit = orientDb.withSession { oSession -> + val types = mutableListOf("type1", "type2", "type3") + val model = model { + for (type in types) { + entity(type) + } + } + + val indices = oSession.applySchema(model) + + val sequences = oSession.metadata.sequenceLibrary + for (type in types) { + assertNull(oSession.getClass(type).getProperty(BACKWARD_COMPATIBLE_LOCAL_ENTITY_ID_PROPERTY_NAME)) + assertTrue(indices.isEmpty()) + assertNull(sequences.getSequence(localEntityIdSequenceName(type))) + } + } + private fun OClass.checkIndex(unique: Boolean, vararg fieldNames: String) { - val indexName = "${name}_${fieldNames.joinToString("_")}" + val indexName = indexName(name, unique, *fieldNames) val index = indexes.first { it.name == indexName } assertEquals(unique, index.isUnique) @@ -345,7 +467,7 @@ class OrientDbSchemaInitializerTest { } private fun Map>.checkIndex(entityName: String, unique: Boolean, vararg fieldNames: String) { - val indexName = "${entityName}_${fieldNames.joinToString("_")}" + val indexName = indexName(entityName, unique, *fieldNames) val indices = getValue(entityName) val index = indices.first { it.indexName == indexName } @@ -359,6 +481,8 @@ class OrientDbSchemaInitializerTest { } } + private fun indexName(entityName: String, unique: Boolean, vararg fieldNames: String): String = "${entityName}_${fieldNames.joinToString("_")}${if (unique) "_unique" else ""}" + private fun ODatabaseSession.checkAssociation(edgeName: String, outClass: OClass, inClass: OClass, cardinality: AssociationEndCardinality?) { val edge = requireEdgeClass(edgeName)