diff --git a/qbit-core/src/commonMain/kotlin/qbit/Conn.kt b/qbit-core/src/commonMain/kotlin/qbit/Conn.kt index 55547c27..013697b8 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/Conn.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/Conn.kt @@ -1,5 +1,8 @@ package qbit +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.toSet import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer @@ -23,6 +26,7 @@ import qbit.factoring.serializatoin.KSFactorizer import qbit.index.Indexer import qbit.index.InternalDb import qbit.index.RawEntity +import qbit.index.entities import qbit.ns.Namespace import qbit.resolving.lastWriterWinsResolve import qbit.resolving.logsDiff @@ -32,7 +36,7 @@ import qbit.storage.SerializedStorage import qbit.trx.* import kotlin.reflect.KClass -suspend fun qbit(storage: Storage, appSerialModule: SerializersModule): Conn { +suspend fun qbit(storage: Storage, appSerialModule: SerializersModule, registerFolders: Map Any>): Conn { val iid = Iid(1, 4) // TODO: fix dbUuid retrieving val dbUuid = DbUuid(iid) @@ -42,7 +46,14 @@ suspend fun qbit(storage: Storage, appSerialModule: SerializersModule): Conn { val systemSerialModule = createSystemSerialModule(appSerialModule) val factor = KSFactorizer(systemSerialModule)::factor val head = loadOrInitHead(storage, nodesStorage, serializedStorage, dbUuid, factor) - val db = Indexer(systemSerialModule, null, null, nodesResolver(nodesStorage)).index(head) + val db = Indexer( + systemSerialModule, + null, + null, + nodesResolver(nodesStorage), + causalHashesResolver(nodesStorage), + registerFolders + ).index(head) return QConn(dbUuid, serializedStorage, head, factor, nodesStorage, db) } @@ -82,6 +93,8 @@ class QConn( private val resolveNode = nodesResolver(nodesStorage) + private val resolveCausality = causalHashesResolver(nodesStorage) + private val gidSequence: GidSequence = with(db.pull(Gid(dbUuid.iid, theInstanceEid))) { if (this == null) { throw QBitException("Corrupted DB - the instance entity not found") @@ -99,7 +112,7 @@ class QConn( } override fun trx(): Trx { - return QTrx(db.pull(Gid(dbUuid.iid, theInstanceEid))!!, trxLog, db, this, factor, gidSequence) + return QTrx(db.pull(Gid(dbUuid.iid, theInstanceEid))!!, trxLog, db, this, factor, gidSequence, resolveCausality) } override suspend fun trx(body: Trx.() -> T): T { @@ -144,18 +157,17 @@ class QConn( newDb: InternalDb ): Pair { val logsDifference = logsDiff(baseLog, committedLog, committingLog, resolveNode) - - val committedEavs = logsDifference - .logAEntities() - .toEavsList() val reconciliationEavs = logsDifference .reconciliationEntities(lastWriterWinsResolve { db.attr(it) }) .toEavsList() - val mergedDb = newDb - .with(committedEavs) - .with(reconciliationEavs) val mergedLog = committingLog.mergeWith(committedLog, baseLog.hash, reconciliationEavs) + val allNodes = nodesBetween(null, mergedLog.head, resolveNode).toList() + val indexedNodes = nodesBetween(null, committingLog.head, resolveNode).toSet() + val notIndexedNodes = allNodes.filter { node -> indexedNodes.none { it.hash == node.hash } } + val mergedDb = notIndexedNodes.fold(newDb) {db, n -> + db.with(n.entities().flatMap { it.second }, n.hash, resolveCausality(n.hash)) + } return mergedLog to mergedDb } @@ -172,6 +184,13 @@ private fun nodesResolver(nodeStorage: NodesStorage): (Node) -> NodeVal List = { hash -> + val node = nodeStorage.load(NodeRef(hash)) ?: throw QBitException("Error: could not resolve node for hash $hash") + val resolveNode = nodesResolver(nodeStorage) + val causalNodes = nodesBetween(null, node, resolveNode) + causalNodes.map { it.hash }.toList() +} + @Suppress("EXPERIMENTAL_API_USAGE") class SchemaValidator : SerializersModuleCollector { diff --git a/qbit-core/src/commonMain/kotlin/qbit/api/QbitSelfSchema.kt b/qbit-core/src/commonMain/kotlin/qbit/api/QbitSelfSchema.kt index db1b1203..f8962390 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/api/QbitSelfSchema.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/api/QbitSelfSchema.kt @@ -51,7 +51,7 @@ object Instances { val nextEid = Attr( Gid(1, 5), "Instance/nextEid", - QInt.code, + QInt.counter().code, unique = false, list = false ) diff --git a/qbit-core/src/commonMain/kotlin/qbit/api/model/DataTypes.kt b/qbit-core/src/commonMain/kotlin/qbit/api/model/DataTypes.kt index ce35fdea..143c1b36 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/api/model/DataTypes.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/api/model/DataTypes.kt @@ -21,6 +21,11 @@ import kotlin.reflect.KClass * - List */ +val scalarRange = 0..31 +val listRange = 32..63 +val counterRange = 64..95 +val registerRange = 96..127 + @Suppress("UNCHECKED_CAST") sealed class DataType { @@ -31,12 +36,13 @@ sealed class DataType { private val values: Array> get() = arrayOf(QBoolean, QByte, QInt, QLong, QString, QBytes, QGid, QRef) - fun ofCode(code: Byte): DataType<*>? = - if (code <= 19) { - values.firstOrNull { it.code == code } - } else { - values.map { it.list() }.firstOrNull { it.code == code } - } + fun ofCode(code: Byte): DataType<*>? = when(code) { + in scalarRange -> values.firstOrNull { it.code == code } + in listRange -> ofCode((code - listRange.first).toByte())?.list() + in counterRange -> ofCode((code - counterRange.first).toByte())?.counter() + in registerRange -> ofCode((code - registerRange.first).toByte())?.register() + else -> null + } fun ofValue(value: T?): DataType? = when (value) { is Boolean -> QBoolean as DataType @@ -46,7 +52,7 @@ sealed class DataType { is String -> QString as DataType is ByteArray -> QBytes as DataType is Gid -> QGid as DataType - is List<*> -> value.firstOrNull()?.let { ofValue(it)?.list() } as DataType + is List<*> -> value.firstOrNull()?.let { ofValue(it)?.list() } as DataType? else -> QRef as DataType } } @@ -57,9 +63,25 @@ sealed class DataType { return QList(this) } - fun isList(): Boolean = (code.toInt().and(32)) > 0 + fun isList(): Boolean = code in listRange - fun ref(): Boolean = this == QRef || this is QList<*> && this.itemsType == QRef + fun counter(): QCounter { + require(this is QByte || this is QInt || this is QLong) { "Only primitive number values are allowed in counters" } + return QCounter(this) + } + + fun isCounter(): Boolean = code in counterRange + + fun register(): QRegister { + require(!(this is QList<*> || this is QCounter || this is QRegister)) { "Nested wrappers is not allowed" } + return QRegister(this) + } + + fun isRegister(): Boolean = code in registerRange + + fun ref(): Boolean = this == QRef || + this is QList<*> && this.itemsType == QRef || + this is QRegister<*> && this.itemsType == QRef fun value(): Boolean = !ref() @@ -73,15 +95,28 @@ sealed class DataType { is QBytes -> ByteArray::class is QGid -> Gid::class is QList<*> -> this.itemsType.typeClass() + is QCounter<*> -> this.primitiveType.typeClass() + is QRegister<*> -> this.itemsType.typeClass() QRef -> Any::class } } - } data class QList(val itemsType: DataType) : DataType>() { - override val code = (32 + itemsType.code).toByte() + override val code = (listRange.first + itemsType.code).toByte() + +} + +data class QCounter(val primitiveType: DataType) : DataType() { + + override val code = (counterRange.first + primitiveType.code).toByte() + +} + +data class QRegister(val itemsType: DataType) : DataType() { + + override val code = (registerRange.first + itemsType.code).toByte() } diff --git a/qbit-core/src/commonMain/kotlin/qbit/index/Index.kt b/qbit-core/src/commonMain/kotlin/qbit/index/Index.kt index ea9a5ea0..2e2b2a5b 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/index/Index.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/index/Index.kt @@ -2,7 +2,10 @@ package qbit.index import qbit.api.db.QueryPred import qbit.api.gid.Gid +import qbit.api.model.Attr +import qbit.api.model.DataType import qbit.api.model.Eav +import qbit.api.model.Hash import qbit.api.tombstone import qbit.platform.assert import qbit.platform.collections.firstMatchIdx @@ -13,7 +16,7 @@ import qbit.platform.collections.subList typealias RawEntity = Pair> fun Index(entities: List): Index = - Index().add(entities) + Index().add(entities, null, emptyList()) fun eidPattern(eid: Gid) = { other: Eav -> other.gid.compareTo(eid) } @@ -61,17 +64,17 @@ class Index( } } - fun addFacts(facts: List): Index = - addFacts(facts as Iterable) + fun addFacts(facts: List, hash: Hash? = null, causalHashes: List = emptyList(), resolveAttr: (String) -> Attr<*>? = { null }): Index = + addFacts(facts as Iterable, hash, causalHashes, resolveAttr) - fun addFacts(facts: Iterable): Index { + fun addFacts(facts: Iterable, hash: Hash?, causalHashes: List, resolveAttr: (String) -> Attr<*>? = { null }): Index { val entities = facts .groupBy { it.gid } .map { it.key to it.value } - return add(entities) + return add(entities, hash, causalHashes, resolveAttr) } - fun add(entities: List): Index { + fun add(entities: List, hash: Hash?, causalHashes: List, resolveAttr: (String) -> Attr<*>? = { null }): Index { val newEntities = HashMap(this.entities) // eavs of removed or updated entities @@ -82,17 +85,42 @@ class Index( val (gid, eavs) = e val isUpdate = eavs[0].attr != tombstone.name - val obsoleteEntity = - if (isUpdate) { - newEntities.put(gid, e) - } else { - newEntities.remove(gid) + val obsoleteEntity = newEntities.get(gid) + + if (isUpdate) { + val effectiveEavs = ArrayList() + eavs.mapTo(effectiveEavs) { eav -> + val attr = resolveAttr(eav.attr) + if (attr != null && DataType.ofCode(attr.type)!!.isRegister()) { + val persistedEav = obsoleteEntity?.second?.firstOrNull { it.attr == eav.attr } + if (persistedEav != null) { + persistedEav.copy(value = (persistedEav.value as IndexedRegister).indexValue(hash, eav.value, causalHashes)) + } else { + eav.copy(value = IndexedRegister(listOf(Pair(hash, eav.value)))) + } + } else { + eav + } } + if(obsoleteEntity != null) { + effectiveEavs.addAll(obsoleteEntity.second.filter { eav -> + val attr = resolveAttr(eav.attr) + attr != null && + DataType.ofCode(attr.type)!!.let { it.isCounter() || it.isRegister() } && + eavs.none { it.attr == eav.attr } + }) + } + + newEntities.put(gid, RawEntity(gid, effectiveEavs)) + newEavs.addAll(effectiveEavs.filter { it.value is Comparable<*> && it.attr != tombstone.name }) + } else { + newEntities.remove(gid) + } + if (obsoleteEntity != null) { obsoleteEavs.addAll(obsoleteEntity.second) } - newEavs.addAll(eavs.filter { it.value is Comparable<*> && it.attr != tombstone.name }) } obsoleteEavs.sortWith(aveCmp) @@ -103,8 +131,8 @@ class Index( return Index(newEntities, newAveIndex) } - fun add(e: RawEntity): Index { - return add(listOf(e)) + fun add(e: RawEntity, hash: Hash?, causalHashes: List, resolveAttr: (String) -> Attr<*>? = { null }): Index { + return add(listOf(e), hash, causalHashes, resolveAttr) } fun entityById(eid: Gid): Map>? = diff --git a/qbit-core/src/commonMain/kotlin/qbit/index/IndexDb.kt b/qbit-core/src/commonMain/kotlin/qbit/index/IndexDb.kt index 96e79231..2023f0e4 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/index/IndexDb.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/index/IndexDb.kt @@ -14,12 +14,14 @@ import qbit.api.gid.Gid import qbit.api.model.* import qbit.api.model.impl.AttachedEntity import qbit.collections.LimitedPersistentMap +import qbit.trx.deoperationalize import qbit.typing.typify import kotlin.reflect.KClass class IndexDb( internal val index: Index, - private val serialModule: SerializersModule + private val serialModule: SerializersModule, + private val registerFolders: Map Any> ) : InternalDb() { private val schema = loadAttrs(index) @@ -30,8 +32,8 @@ class IndexDb( private val dataClassesCache = atomic>(LimitedPersistentMap(1024)) - override fun with(facts: Iterable): InternalDb { - return IndexDb(index.addFacts(facts), serialModule) + override fun with(facts: Iterable, commitHash: Hash?, causalHashes: List): IndexDb { + return IndexDb(index.addFacts(deoperationalize(this, facts.toList()), commitHash, causalHashes, this::attr), serialModule, registerFolders) } override fun pullEntity(gid: Gid): StoredEntity? { @@ -64,8 +66,8 @@ class IndexDb( // see https://github.com/d-r-q/qbit/issues/114, https://github.com/d-r-q/qbit/issues/132 private fun fixNumberType(attr: Attr, value: Any) = when (attr.type) { - QByte.code -> (value as Number).toByte() - QInt.code -> (value as Number).toInt() + QByte.code, QByte.counter().code -> (value as Number).toByte() + QInt.code, QInt.counter().code -> (value as Number).toInt() else -> value } @@ -79,7 +81,7 @@ class IndexDb( return cached as R } - val dc = typify(schema::get, entity, type, serialModule) + val dc = typify(schema::get, entity, type, serialModule, registerFolders) dataClassesCache.update { it.put(entity, dc) } return dc } diff --git a/qbit-core/src/commonMain/kotlin/qbit/index/IndexedRegister.kt b/qbit-core/src/commonMain/kotlin/qbit/index/IndexedRegister.kt new file mode 100644 index 00000000..b1b28eec --- /dev/null +++ b/qbit-core/src/commonMain/kotlin/qbit/index/IndexedRegister.kt @@ -0,0 +1,14 @@ +package qbit.index + +import qbit.api.model.Hash + +class IndexedRegister( + val cells: List> +) { + fun indexValue(hash: Hash?, value: Any, causalHashes: List): IndexedRegister { + val concurrentCells = cells.filter { !causalHashes.contains(it.first) } + return IndexedRegister(concurrentCells + Pair(hash, value)) + } + + fun values() = cells.map { it.second } +} \ No newline at end of file diff --git a/qbit-core/src/commonMain/kotlin/qbit/index/Indexer.kt b/qbit-core/src/commonMain/kotlin/qbit/index/Indexer.kt index 280e9033..e278de98 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/index/Indexer.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/index/Indexer.kt @@ -10,20 +10,21 @@ class Indexer( private val base: IndexDb?, private val baseNode: Node?, private val resolveNode: (Node) -> NodeVal?, + private val causalHashesResolver: suspend (Hash) -> List, + private val registerFolders: Map Any> ) { suspend fun index(from: Node): IndexDb { return nodesBetween(baseNode, from, resolveNode) .toList() - .map { it.entities() } - .fold(base ?: IndexDb(Index(), serialModule)) { db, n -> - IndexDb(db.index.add(n), serialModule) + .fold(base ?: IndexDb(Index(), serialModule, registerFolders)) { db, n -> + db.with(n.entities().flatMap { it.second }, n.hash, causalHashesResolver(n.hash)) } } } -private fun NodeVal.entities(): List = +fun NodeVal.entities(): List = data.trxes.toList() .groupBy { it.gid } .map { it.key to it.value } diff --git a/qbit-core/src/commonMain/kotlin/qbit/index/InternalDb.kt b/qbit-core/src/commonMain/kotlin/qbit/index/InternalDb.kt index 4f000867..f21793b3 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/index/InternalDb.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/index/InternalDb.kt @@ -3,10 +3,7 @@ package qbit.index import qbit.api.db.Db import qbit.api.db.QueryPred import qbit.api.gid.Gid -import qbit.api.model.Attr -import qbit.api.model.Eav -import qbit.api.model.Entity -import qbit.api.model.StoredEntity +import qbit.api.model.* abstract class InternalDb : Db() { @@ -16,7 +13,7 @@ abstract class InternalDb : Db() { // Todo: add check that attrs are presented in schema abstract fun query(vararg preds: QueryPred): Sequence - abstract fun with(facts: Iterable): InternalDb + abstract fun with(facts: Iterable, commitHash: Hash? = null, causalHashes: List = emptyList()): InternalDb abstract fun attr(attr: String): Attr? diff --git a/qbit-core/src/commonMain/kotlin/qbit/resolving/ConflictResolving.kt b/qbit-core/src/commonMain/kotlin/qbit/resolving/ConflictResolving.kt index 4d3bdbc8..addd9f2e 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/resolving/ConflictResolving.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/resolving/ConflictResolving.kt @@ -1,9 +1,9 @@ package qbit.resolving import kotlinx.coroutines.flow.toList -import qbit.api.Instances import qbit.api.gid.Gid import qbit.api.model.Attr +import qbit.api.model.DataType import qbit.api.model.Eav import qbit.api.model.Hash import qbit.index.RawEntity @@ -41,7 +41,12 @@ data class LogsDiff( resolve(writesFromA[it]!!, writesFromB[it]!!) } } - return resolvingEavsByGid.values.map { RawEntity(it.first().gid, it) } + return resolvingEavsByGid + .filter { it.value.isNotEmpty() } // CRDT values in eavs are operations. + // Operations should not be created during merge + // So, it is possible to have "empty" entities there + // They should be filtered out + .values.map { RawEntity(it.first().gid, it) } } fun logAEntities(): List { @@ -65,9 +70,9 @@ internal fun lastWriterWinsResolve(resolveAttrName: (String) -> Attr?): (Li ?: throw IllegalArgumentException("Cannot resolve ${eavsFromA[0].eav.attr}") when { - // temporary dirty hack until crdt counter or custom resolution strategy support is implemented - attr == Instances.nextEid -> listOf((eavsFromA + eavsFromB).maxByOrNull { it.eav.value as Int }!!.eav) attr.list -> (eavsFromA + eavsFromB).map { it.eav }.distinct() + DataType.ofCode(attr.type)!!.isCounter() -> ArrayList() + DataType.ofCode(attr.type)!!.isRegister() -> ArrayList() else -> listOf((eavsFromA + eavsFromB).maxByOrNull { it.timestamp }!!.eav) } } diff --git a/qbit-core/src/commonMain/kotlin/qbit/schema/SchemaDsl.kt b/qbit-core/src/commonMain/kotlin/qbit/schema/SchemaDsl.kt index 384abb6e..29f45106 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/schema/SchemaDsl.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/schema/SchemaDsl.kt @@ -8,23 +8,26 @@ import qbit.factoring.serializatoin.AttrName import kotlin.reflect.KClass import kotlin.reflect.KProperty1 -fun schema(serialModule: SerializersModule, body: SchemaBuilder.() -> Unit): List> { +fun schema(serialModule: SerializersModule, body: SchemaBuilder.() -> Unit): Pair>, HashMap Any>> { val scb = SchemaBuilder(serialModule) scb.body() - return scb.attrs + return scb.attrs to scb.folders } class SchemaBuilder(private val serialModule: SerializersModule) { internal val attrs: MutableList> = ArrayList() + internal val folders: HashMap Any> = HashMap() + fun entity(type: KClass, body: EntityBuilder.() -> Unit = {}) { val descr = serialModule.getContextual(type)?.descriptor ?: throw QBitException("Cannot find descriptor for $type") val eb = EntityBuilder(descr) eb.body() - attrs.addAll(schemaFor(descr, eb.uniqueProps)) + folders += eb.registerFolders + attrs.addAll(schemaFor(descr, eb.uniqueProps, eb.counters, eb.registerFolders.keys)) } } @@ -33,6 +36,10 @@ class EntityBuilder(private val descr: SerialDescriptor) { internal val uniqueProps = HashSet() + internal val counters = HashSet() + + internal val registerFolders = HashMap Any>() + fun uniqueInt(prop: KProperty1) { uniqueAttr(prop) } @@ -42,21 +49,49 @@ class EntityBuilder(private val descr: SerialDescriptor) { } private fun uniqueAttr(prop: KProperty1) { + uniqueProps.add(getAttrName(prop)) + } + + fun byteCounter(prop: KProperty1) { + counter(prop) + } + + fun intCounter(prop: KProperty1) { + counter(prop) + } + + fun longCounter(prop: KProperty1) { + counter(prop) + } + + private fun counter(prop: KProperty1) { + counters.add(getAttrName(prop)) + } + + fun register(prop: KProperty1, fold: (V, V) -> V) { + registerFolders.put(getAttrName(prop), fold as (Any, Any) -> Any) + } + + private fun getAttrName(prop: KProperty1): String { val (idx, _) = descr.elementNames .withIndex().firstOrNull { (_, name) -> name == prop.name } ?: throw QBitException("Cannot find attr for ${prop.name} in $descr") - uniqueProps.add(AttrName(descr, idx).asString()) + return AttrName(descr, idx).asString() } } -fun schemaFor(rootDesc: SerialDescriptor, unique: Set = emptySet()): List> { +fun schemaFor(rootDesc: SerialDescriptor, unique: Set = emptySet(), counters: Set = emptySet(), registers: Set = emptySet()): List> { return rootDesc.elementDescriptors .withIndex() .filter { rootDesc.getElementName(it.index) !in setOf("id", "gid") } .map { (idx, desc) -> - val dataType = DataType.of(desc) val attr = AttrName(rootDesc, idx).asString() + val dataType = when { + attr in counters -> DataType.of(desc).counter() + attr in registers -> DataType.of(desc).register() + else -> DataType.of(desc) + } Attr(null, attr, dataType.code, attr in unique, dataType.isList()) } } diff --git a/qbit-core/src/commonMain/kotlin/qbit/serialization/Simple.kt b/qbit-core/src/commonMain/kotlin/qbit/serialization/Simple.kt index f198ee6f..ed3fc6d8 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/serialization/Simple.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/serialization/Simple.kt @@ -176,7 +176,7 @@ internal fun deserialize(ins: Input): Any { private fun readMark(ins: Input, expectedMark: DataType): Any { return when (expectedMark) { QBoolean -> (ins.readByte() == 1.toByte()) as T - QByte, QInt, QLong -> readLong(ins) as T + QByte, QInt, QLong, is QCounter<*> -> readLong(ins) as T QBytes -> readLong(ins).let { count -> readBytes(ins, count.toInt()) as T @@ -186,6 +186,7 @@ private fun readMark(ins: Input, expectedMark: DataType): Any { readBytes(ins, count.toInt()).decodeUtf8() as T } QGid -> Gid(readLong(ins)) as T + is QRegister<*> -> readMark(ins, expectedMark.itemsType) as T QRef -> throw AssertionError("Should never happen") is QList<*> -> throw AssertionError("Should never happen") } diff --git a/qbit-core/src/commonMain/kotlin/qbit/trx/Operationalization.kt b/qbit-core/src/commonMain/kotlin/qbit/trx/Operationalization.kt new file mode 100644 index 00000000..fcfe9e30 --- /dev/null +++ b/qbit-core/src/commonMain/kotlin/qbit/trx/Operationalization.kt @@ -0,0 +1,61 @@ +package qbit.trx + +import qbit.api.QBitException +import qbit.api.model.Attr +import qbit.api.model.DataType +import qbit.api.model.Eav +import qbit.index.InternalDb + +fun operationalize(db: InternalDb, facts: List): List { + return facts.mapNotNull { + val attr = db.attr(it.attr)!! + val dataType = DataType.ofCode(attr.type)!! + when { + dataType.isCounter() -> operationalizeCounter(db, it, attr) + dataType.isRegister() -> operationalizeRegister(db, it, attr) + else -> it + } + } +} + +private fun operationalizeCounter(db: InternalDb, fact: Eav, attr: Attr<*>): Eav? { + val previous = db.pullEntity(fact.gid)?.tryGet(attr) + return if (previous != null) { + if(previous != fact.value) { + Eav( + fact.gid, + fact.attr, + if (fact.value is Byte) fact.value - (previous as Number).toByte() + else if (fact.value is Int) fact.value - (previous as Number).toInt() + else if (fact.value is Long) fact.value - (previous as Number).toLong() + else throw QBitException("Unexpected counter value type for $fact") + ) + } else null + } else fact +} + +private fun operationalizeRegister(db: InternalDb, fact: Eav, attr: Attr<*>): Eav? { + val previous = db.pullEntity(fact.gid)?.tryGet(attr) + return if (fact.value != previous) fact else null +} + +fun deoperationalize(db: InternalDb, facts: List): List { + return facts.map { deoperationalizeCounter(db, it) } +} + +private fun deoperationalizeCounter(db: InternalDb, fact: Eav): Eav { + val attr = db.attr(fact.attr) + return if (attr != null && DataType.ofCode(attr.type)!!.isCounter()) { + val previous = db.pullEntity(fact.gid)?.tryGet(attr) + if (previous != null) { + Eav( + fact.gid, + fact.attr, + if (fact.value is Byte) (previous as Number).toByte() + fact.value + else if (fact.value is Int) (previous as Number).toInt() + fact.value + else if (fact.value is Long) (previous as Number).toLong() + fact.value + else throw QBitException("Unexpected counter value type for $fact") + ) + } else fact + } else fact +} \ No newline at end of file diff --git a/qbit-core/src/commonMain/kotlin/qbit/trx/Trx.kt b/qbit-core/src/commonMain/kotlin/qbit/trx/Trx.kt index f598b549..8c60b1e5 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/trx/Trx.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/trx/Trx.kt @@ -14,7 +14,8 @@ import qbit.platform.collections.EmptyIterator internal class QTrx( private val inst: Instance, private val trxLog: TrxLog, private var base: InternalDb, private val commitHandler: CommitHandler, private val factor: Factor, - private val gids: GidSequence + private val gids: GidSequence, + private val causalHashesResolver: suspend (Hash) -> List ) : Trx() { private var curDb: InternalDb = base @@ -44,8 +45,9 @@ internal class QTrx( return QbitWriteResult(entityGraphRoot, curDb) } validate(curDb, updatedFacts) - factsBuffer.addAll(updatedFacts) - curDb = curDb.with(updatedFacts) + val operationalizedFacts = operationalize(base, updatedFacts) + factsBuffer.addAll(operationalizedFacts) + curDb = curDb.with(operationalizedFacts) //? val res = if (facts.entityFacts[entityGraphRoot]!!.firstOrNull()?.gid in entities) { entityGraphRoot @@ -62,9 +64,11 @@ internal class QTrx( } val instance = factor(inst.copy(nextEid = gids.next().eid), curDb::attr, EmptyIterator) - val newLog = trxLog.append(factsBuffer + instance) + val operationalizedInstance = operationalize(curDb, instance.toList()) + val newFacts = factsBuffer + operationalizedInstance + val newLog = trxLog.append(newFacts) try { - base = curDb.with(instance) + base = base.with(newFacts, newLog.hash, causalHashesResolver(newLog.hash)) commitHandler.update(trxLog, newLog, base) factsBuffer.clear() } catch (e: Throwable) { @@ -92,7 +96,7 @@ fun Entity.toFacts(): Collection = val type = DataType.ofCode(attr.type)!! @Suppress("UNCHECKED_CAST") when { - type.value() && !attr.list -> listOf(valToFacts(gid, attr, value)) + type.isRegister() || type.value() && !attr.list -> listOf(valToFacts(gid, attr, value)) type.value() && attr.list -> listToFacts(gid, attr, value as List) type.ref() && !attr.list -> listOf(refToFacts(gid, attr, value)) type.ref() && attr.list -> refListToFacts(gid, attr, value as List) diff --git a/qbit-core/src/commonMain/kotlin/qbit/typing/ListDecoder.kt b/qbit-core/src/commonMain/kotlin/qbit/typing/ListDecoder.kt new file mode 100644 index 00000000..2dccbe69 --- /dev/null +++ b/qbit-core/src/commonMain/kotlin/qbit/typing/ListDecoder.kt @@ -0,0 +1,133 @@ +package qbit.typing + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.SerializersModule +import qbit.api.QBitException +import qbit.api.gid.Gid +import qbit.api.model.Attr +import qbit.api.model.StoredEntity +import qbit.factoring.serializatoin.AttrName + +@Suppress("UNCHECKED_CAST") +class ListDecoder( + val schema: (String) -> Attr<*>?, + val entity: StoredEntity, + private val elements: List, + override val serializersModule: SerializersModule, + private val cache: HashMap, + val registerFolders: Map Any> +) : StubDecoder() { + + private var isRefList = false + + private var indexCounter = 0 + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + val elementDescriptor = descriptor.getElementDescriptor(0) + val elementKind = elementDescriptor.kind + + isRefList = when { + isValueAttr(elementDescriptor) -> false + isRefAttr(elementDescriptor) -> true + else -> throw QBitException("$elementKind not yet supported") + } + + return this + } + + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + val element = elements[index] + return if(!isRefList) { + element as T? + } else { + decodeReferred(element as Gid, deserializer) as T? + } + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T?, + ): T { + return decodeNullableSerializableElement(descriptor, index, deserializer as DeserializationStrategy) as T + } + + private fun isValueAttr(elementDescriptor: SerialDescriptor): Boolean { + val listElementsKind = elementDescriptor.kind + return listElementsKind is PrimitiveKind || + listElementsKind is StructureKind.LIST // ByteArrays + } + + private fun isRefAttr(elementDescriptor: SerialDescriptor): Boolean { + return elementDescriptor.kind is StructureKind.CLASS + } + + private fun decodeReferred(gid: Gid, deserializer: DeserializationStrategy<*>): Any? { + val referee = entity.pull(gid) ?: throw QBitException("Dangling ref: $gid") + val decoder = EntityDecoder(schema, referee, serializersModule, registerFolders) + return cache.getOrPut(gid) { deserializer.deserialize(decoder) } + } + + private fun decodeElement(descriptor: SerialDescriptor, index: Int): T { + val attrName = AttrName(descriptor, index).asString() + return entity[schema(attrName) as Attr] + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + return if (indexCounter < elements.size) indexCounter++ else CompositeDecoder.DECODE_DONE + } + + @ExperimentalSerializationApi + override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder { + return this + } + + @ExperimentalSerializationApi + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { + return this + } + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean { + return decodeElement(descriptor, index) + } + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { + return decodeElement(descriptor, index) + } + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { + return decodeElement(descriptor, index) + } + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double { + return decodeElement(descriptor, index) + } + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { + return decodeElement(descriptor, index) + } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int { + return decodeElement(descriptor, index) + } + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { + return decodeElement(descriptor, index) + } + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String { + return decodeElement(descriptor, index) + } +} \ No newline at end of file diff --git a/qbit-core/src/commonMain/kotlin/qbit/typing/SerializationTyping.kt b/qbit-core/src/commonMain/kotlin/qbit/typing/SerializationTyping.kt index a7d22cbc..5e347bc1 100644 --- a/qbit-core/src/commonMain/kotlin/qbit/typing/SerializationTyping.kt +++ b/qbit-core/src/commonMain/kotlin/qbit/typing/SerializationTyping.kt @@ -2,9 +2,7 @@ package qbit.typing import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE import kotlinx.serialization.encoding.Decoder @@ -12,8 +10,10 @@ import kotlinx.serialization.modules.SerializersModule import qbit.api.QBitException import qbit.api.gid.Gid import qbit.api.model.Attr +import qbit.api.model.DataType import qbit.api.model.StoredEntity import qbit.factoring.serializatoin.AttrName +import qbit.index.IndexedRegister import kotlin.reflect.KClass fun typify( @@ -21,9 +21,10 @@ fun typify( entity: StoredEntity, type: KClass, serialModule: SerializersModule, + registerFolders: Map Any> ): T { val contextual = serialModule.getContextual(type) ?: throw QBitException("Cannot find serializer for $type") - return contextual.deserialize(EntityDecoder(schema, entity, serialModule)) + return contextual.deserialize(EntityDecoder(schema, entity, serialModule, registerFolders)) } @Suppress("UNCHECKED_CAST") @@ -31,6 +32,7 @@ class EntityDecoder( val schema: (String) -> Attr<*>?, val entity: StoredEntity, override val serializersModule: SerializersModule, + val registerFolders: Map Any> ) : StubDecoder() { private var fields = 0 @@ -71,14 +73,34 @@ class EntityDecoder( } } - if (descriptor.kind == StructureKind.LIST && elementKind == StructureKind.CLASS) { - val decoder = EntityDecoder(schema, entity, serializersModule) - return deserializer.deserialize(decoder) - } - val attrName = AttrName(descriptor, index).asString() val attr: Attr = schema(attrName) as Attr? ?: throw QBitException("Corrupted entity $entity, there is no attr $attrName in schema") + val dataType = DataType.ofCode(attr.type)!! + + if(dataType.isList()) { + val elements = entity.tryGet(attr) ?: return null // TODO CHECK NULLABILITY + val decoder = ListDecoder(schema, entity, elements as List, serializersModule, cache, registerFolders) + return deserializer.deserialize(decoder) + } + + if(dataType.isRegister()) { + val register = entity.tryGet(attr) as IndexedRegister? ?: return null + val folder = registerFolders[attrName] ?: throw QBitException("There is no folder for attr $attrName") + val values = when { + isValueAttr(elementDescriptor) -> register.values() + isRefAttr(elementDescriptor) -> register.values().map { + decodeReferred( + elementDescriptor, + attrName, + it as Gid, + deserializer + ) as Any // TODO NULLABILITY + } + else -> throw QBitException("$elementKind not yet supported") + } + return values.reduce(folder) as T? + } return when { isValueAttr(elementDescriptor) -> entity.tryGet(attr) @@ -95,50 +117,28 @@ class EntityDecoder( private fun decodeReferred( elementDescriptor: SerialDescriptor, attrName: String, - gids: Any?, + gid: Gid?, deserializer: DeserializationStrategy, ): Any? { when { - gids == null && elementDescriptor.isNullable -> return null - gids == null && !elementDescriptor.isNullable -> throw QBitException("Corrupted entity: $entity, no value for $attrName") + gid == null && elementDescriptor.isNullable -> return null + gid == null && !elementDescriptor.isNullable -> throw QBitException("Corrupted entity: $entity, no value for $attrName") } - check(gids != null) + check(gid != null) - val sureGids = when (gids) { - is Gid -> listOf(gids) - is List<*> -> gids as List - else -> throw AssertionError("Unexpected gids: $gids") - } - - val referreds = sureGids.map { - val referee = entity.pull(it) ?: throw QBitException("Dangling ref: $it") - val decoder = EntityDecoder(schema, referee, serializersModule) - val res = cache.getOrPut(it, { deserializer.deserialize(decoder) }) - if (res is List<*>) { - res[0] as T - } else { - res as T - } - } - - return when (elementDescriptor.kind) { - is StructureKind.CLASS -> referreds[0] - is StructureKind.LIST -> referreds - else -> throw AssertionError("Unexpected kind: ${elementDescriptor.kind}") - } + val referee = entity.pull(gid) ?: throw QBitException("Dangling ref: $gid") + val decoder = EntityDecoder(schema, referee, serializersModule, registerFolders) + return cache.getOrPut(gid) { deserializer.deserialize(decoder) } } private fun isValueAttr(elementDescriptor: SerialDescriptor): Boolean { val elementKind = elementDescriptor.kind val listElementsKind = elementDescriptor.takeIf { it.kind is StructureKind.LIST }?.getElementDescriptor(0)?.kind - return elementKind is PrimitiveKind || listElementsKind is PrimitiveKind || - listElementsKind is StructureKind.LIST // List of ByteArrays + return elementKind is PrimitiveKind || listElementsKind is PrimitiveKind } private fun isRefAttr(elementDescriptor: SerialDescriptor): Boolean { - val elementKind = elementDescriptor.kind - val listElementsKind = elementDescriptor.takeIf { it.kind is StructureKind.LIST }?.getElementDescriptor(0)?.kind - return elementKind is StructureKind.CLASS || listElementsKind is StructureKind.CLASS + return elementDescriptor.kind is StructureKind.CLASS } override fun decodeSerializableElement( @@ -188,7 +188,13 @@ class EntityDecoder( private fun decodeElement(descriptor: SerialDescriptor, index: Int): T { val attrName = AttrName(descriptor, index).asString() - return entity[schema(attrName) as Attr] + val element = entity[schema(attrName) as Attr] + return if (element is IndexedRegister) { + val folder = registerFolders[attrName] ?: throw QBitException("There is no folder for attr $attrName") + element.values().reduce(folder) as T + } else { + element + } } } \ No newline at end of file diff --git a/qbit-core/src/commonTest/kotlin/qbit/BootstrapTest.kt b/qbit-core/src/commonTest/kotlin/qbit/BootstrapTest.kt index aefad429..86d5e7f4 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/BootstrapTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/BootstrapTest.kt @@ -42,13 +42,13 @@ class BootstrapTest { head, KSFactorizer(qbitSerialModule + testsSerialModule)::factor, nodesStorage, - Indexer(qbitSerialModule + testsSerialModule, null, null, testNodesResolver(nodesStorage)).index(head) + Indexer(qbitSerialModule + testsSerialModule, null, null, testNodesResolver(nodesStorage), causalHashesResolver(nodesStorage), testSchema.second).index(head), ) } @Test fun testInit() { runBlocking { - val db = qbit(storage, testsSerialModule) + val db = qbit(storage, testsSerialModule, testSchema.second) assertNotNull(db) assertTrue(storage.keys(Namespace("nodes")).isNotEmpty()) } diff --git a/qbit-core/src/commonTest/kotlin/qbit/ConnTest.kt b/qbit-core/src/commonTest/kotlin/qbit/ConnTest.kt index 29fb3db2..dd210e48 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/ConnTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/ConnTest.kt @@ -59,7 +59,7 @@ class ConnTest { storedRoot, testSchemaFactorizer::factor, nodesStorage, - Indexer(qbitSerialModule + testsSerialModule, null, null, testNodesResolver(nodesStorage)).index(storedRoot) + Indexer(qbitSerialModule + testsSerialModule, null, null, testNodesResolver(nodesStorage), causalHashesResolver(nodesStorage), testSchema.second).index(storedRoot) ) val newLog = FakeTrxLog(storedLeaf.hash) diff --git a/qbit-core/src/commonTest/kotlin/qbit/FunTest.kt b/qbit-core/src/commonTest/kotlin/qbit/FunTest.kt index c53945e2..028cd3c4 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/FunTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/FunTest.kt @@ -313,11 +313,11 @@ class FunTest { val storage = MemStorage() setupTestSchema(storage) - val conn1 = qbit(storage, testsSerialModule) + val conn1 = qbit(storage, testsSerialModule, testSchema.second) assertNotNull((conn1.db() as InternalDb).attr(Scientists.name.name)) conn1.persist(IntEntity(null, 2)) - val conn2 = qbit(storage, testsSerialModule) + val conn2 = qbit(storage, testsSerialModule, testSchema.second) assertNotNull(conn2.db().query(attrIs(IntEntities.int, 2)).firstOrNull()) } } @@ -400,7 +400,7 @@ class FunTest { assertEquals(bomb.country, storedBomb.country) assertEquals(bomb.optCountry, storedBomb.optCountry) assertEquals( - listOf(Country(12884901889, "Country1", 0), Country(4294967383, "Country3", 2)), + listOf(Country(12884901889, "Country1", 0), Country(4294967386, "Country3", 2)), storedBomb.countiesList ) // todo: assertEquals(bomb.countriesListOpt, storedBomb.countriesListOpt) @@ -459,9 +459,9 @@ class FunTest { trx1.persist(eBrewer.copy(name = "Im different change")) val trx2 = conn.trx() trx2.persist(eCodd.copy(name = "Im change 2")) - delay(100) trx2.persist(pChen.copy(name = "Im different change")) trx1.commit() + delay(1) trx2.commit() conn.db { assertEquals("Im change 2", it.pull(eCodd.id!!)!!.name) @@ -489,7 +489,7 @@ class FunTest { val trx3 = conn1.trx() trx3.persist(mStonebreaker.copy(name = "Im change 3")) trx3.commit() - val conn2 = qbit(storage, testsSerialModule) + val conn2 = qbit(storage, testsSerialModule, testSchema.second) conn2.db { assertEquals("Im change 2", it.pull(eCodd.id!!)!!.name) assertEquals("Im different change", it.pull(pChen.id!!)!!.name) @@ -510,7 +510,7 @@ class FunTest { // When the entity is persisted val stored = conn1.persist(entity) // And storage is reopened - val conn2 = qbit(storage, testsSerialModule) + val conn2 = qbit(storage, testsSerialModule, testSchema.second) // And the entity is pulled val loaded = conn2.db().pull(Gid(stored.persisted!!.id!!))!! @@ -540,6 +540,7 @@ class FunTest { ) ) trx1.commit() + delay(1) trx2.commit() conn.db { assertEquals("Im change 2", it.pull(eCodd.id!!)!!.name) @@ -574,4 +575,86 @@ class FunTest { assertEquals(Gid(nsk.id!!), trx2EntityAttrValues.first { it.attr.name == "City/region" }.value) } } + + @JsName("qbit_should_accumulate_concurrent_increments_of_counter") + @Test + fun `qbit should accumulate concurrent increments of counter`() { + runBlocking { + val conn = setupTestSchema() + val counter = IntCounterEntity(1, 10) + val trx = conn.trx() + trx.persist(counter) + trx.commit() + + val trx1 = conn.trx() + val trx2 = conn.trx() + trx1.persist(counter.copy(counter = 40)) + trx2.persist(counter.copy(counter = 70)) + trx1.commit() + trx2.commit() + + assertEquals(conn.db().pull(1)?.counter, 100) + } + } + + @JsName("qbit_should_keep_both_concurrent_writes_to_a_value_register") + @Test + fun `qbit should keep both concurrent writes to a value register`() { + runBlocking { + val conn = setupTestSchema() + conn.trx { + persist(StringRegisterEntity(1, "ABC")) + } + assertEquals(conn.db().pull(1)?.register, "ABC") + + val trx1 = conn.trx() + val trx2 = conn.trx() + trx1.persist(StringRegisterEntity(1, "DEF")) + trx2.persist(StringRegisterEntity(1, "XYZ")) + trx1.commit() + trx2.commit() + assertContains(listOf("DEF, XYZ", "XYZ, DEF"), conn.db().pull(1)?.register) + + conn.trx { + persist(StringRegisterEntity(1, "GHI")) + } + assertEquals(conn.db().pull(1)?.register, "GHI") + } + } + + @JsName("qbit_should_keep_both_concurrent_writes_to_a_ref_register") + @Test + fun `qbit should keep both concurrent writes to a ref register`() { + runBlocking { + val conn = setupTestSchema() + val sweden = Country(null, "Sweden", 10350000) + val norway = Country(null, "Norway", 5379000) + val denmark = Country(null, "Denmark", 5831000) + val finland = Country(null, "Finland", 5531000) + + conn.trx { + persist(CountryRegisterEntity(1, sweden)) + } + assertEquals(conn.db().pull(1)?.register?.copy(id = null), sweden) + + val trx1 = conn.trx() + val trx2 = conn.trx() + trx1.persist(CountryRegisterEntity(1, norway)) + trx2.persist(CountryRegisterEntity(1, denmark)) + trx1.commit() + trx2.commit() + assertContains( + listOf( + Country(null, "${norway.name}-${denmark.name}", norway.population!! + denmark.population!!), + Country(null, "${denmark.name}-${norway.name}", norway.population!! + denmark.population!!) + ), + conn.db().pull(1)?.register?.copy(id = null) + ) + + conn.trx { + persist(CountryRegisterEntity(1, finland)) + } + assertEquals(conn.db().pull(1)?.register?.copy(id = null), finland) + } + } } \ No newline at end of file diff --git a/qbit-core/src/commonTest/kotlin/qbit/OperationalizationTest.kt b/qbit-core/src/commonTest/kotlin/qbit/OperationalizationTest.kt new file mode 100644 index 00000000..0d8ac717 --- /dev/null +++ b/qbit-core/src/commonTest/kotlin/qbit/OperationalizationTest.kt @@ -0,0 +1,41 @@ +package qbit + +import qbit.api.gid.nextGids +import qbit.factoring.serializatoin.KSFactorizer +import qbit.test.model.IntCounterEntity +import qbit.trx.operationalize +import qbit.typing.qbitCoreTestsSerialModule +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals + +class OperationalizationTest { + + private val gids = qbit.api.gid.Gid(0, 0).nextGids() + + val factor = KSFactorizer(qbitCoreTestsSerialModule)::factor + + val emptyDb = dbOf(gids, *(bootstrapSchema.values + testSchema.first).toTypedArray()) + + @JsName("Counter_not_persisted_in_db_should_pass_as_is") + @Test + fun `Counter not persisted in db should pass as-is`() { + val counterEntity = IntCounterEntity(null, 10) + val facts = operationalize(emptyDb, factor(counterEntity, emptyDb::attr, gids).entityFacts.values.first()) + assertEquals(1, facts.size, "Factoring of single entity with single attr should produce single fact") + assertEquals("IntCounterEntity/counter", facts[0].attr) + assertEquals(10, facts[0].value) + } + + @JsName("Persisted_counter_should_turn_into_difference") + @Test + fun `Persisted counter should turn into difference`() { + val counterEntity = IntCounterEntity(1, 10) + val updatedDb = emptyDb.with(factor(IntCounterEntity(1, 10), emptyDb::attr, gids)) + + val facts = operationalize(updatedDb, factor(counterEntity.copy(counter = 100), updatedDb::attr, gids).entityFacts.values.first()) + assertEquals(1, facts.size, "Factoring of single entity with single attr should produce single fact") + assertEquals("IntCounterEntity/counter", facts[0].attr) + assertEquals(90, facts[0].value) + } +} \ No newline at end of file diff --git a/qbit-core/src/commonTest/kotlin/qbit/TestSchema.kt b/qbit-core/src/commonTest/kotlin/qbit/TestSchema.kt index fc7d77a9..142733da 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/TestSchema.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/TestSchema.kt @@ -36,6 +36,21 @@ val testSchema = schema(internalTestsSerialModule) { entity(NullableList::class) entity(NullableRef::class) entity(IntEntity::class) + entity(IntCounterEntity::class) { + intCounter(IntCounterEntity::counter) + } + entity(StringRegisterEntity::class) { + register(StringRegisterEntity::register) { l, r -> "$l, $r" } + } + entity(CountryRegisterEntity::class) { + register(CountryRegisterEntity::register) { l, r -> + Country( + null, + "${l.name}-${r.name}", + if (l.population == null || r.population == null) null else l.population!! + r.population!! + ) + } + } entity(ResearchGroup::class) entity(EntityWithByteArray::class) entity(EntityWithListOfBytes::class) @@ -49,7 +64,7 @@ val testSchema = schema(internalTestsSerialModule) { } private val gids = Gid(2, 0).nextGids() -val schemaMap: Map> = testSchema +val schemaMap: Map> = testSchema.first .map { it.name to it.id(gids.next()) } .toMap() diff --git a/qbit-core/src/commonTest/kotlin/qbit/TestUtilsCore.kt b/qbit-core/src/commonTest/kotlin/qbit/TestUtilsCore.kt index 7df25f1b..977384cb 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/TestUtilsCore.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/TestUtilsCore.kt @@ -54,8 +54,8 @@ internal object EmptyDb : InternalDb() { override fun attr(attr: String): Attr? = bootstrapSchema[attr] - override fun with(facts: Iterable): InternalDb { - return IndexDb(Index().addFacts(facts), testsSerialModule) + override fun with(facts: Iterable, commitHash: Hash?, causalHashes: List): InternalDb { + return IndexDb(Index().addFacts(facts, commitHash, causalHashes, this::attr), testsSerialModule, testSchema.second) } } @@ -68,9 +68,11 @@ internal fun TestIndexer( serialModule: SerializersModule = testsSerialModule, baseDb: IndexDb? = null, baseHash: Hash? = null, - nodeResolver: (Node) -> NodeVal? = identityNodeResolver + nodeResolver: (Node) -> NodeVal? = identityNodeResolver, + causalHashesResolver: suspend (Hash) -> List = { emptyList() }, + registerFolders: Map Any> = testSchema.second ) = - Indexer(serialModule, baseDb, baseHash?.let { NodeRef(it) }, nodeResolver) + Indexer(serialModule, baseDb, baseHash?.let { NodeRef(it) }, nodeResolver, causalHashesResolver, registerFolders) inline fun assertThrows(body: () -> Unit) { try { @@ -128,7 +130,7 @@ internal fun dbOf(eids: Iterator = Gid(0, 0).nextGids(), vararg entities: A .map { it.name to it } .toMap() val facts = entities.flatMap { testSchemaFactorizer.factor(it, (bootstrapSchema + addedAttrs)::get, eids) } - return IndexDb(Index(facts.groupBy { it.gid }.map { it.key to it.value }), testsSerialModule) + return IndexDb(Index(facts.groupBy { it.gid }.map { it.key to it.value }), testsSerialModule, testSchema.second) } inline fun > ListAttr(id: Gid?, name: String, unique: Boolean = true): Attr { @@ -142,9 +144,9 @@ inline fun > ListAttr(id: Gid?, name: Strin } suspend fun setupTestSchema(storage: Storage = MemStorage()): Conn { - val conn = qbit(storage, testsSerialModule) + val conn = qbit(storage, testsSerialModule, testSchema.second) conn.trx { - testSchema.forEach { + testSchema.first.forEach { persist(it) } } diff --git a/qbit-core/src/commonTest/kotlin/qbit/TrxTest.kt b/qbit-core/src/commonTest/kotlin/qbit/TrxTest.kt index 840ccd9e..16ac9c5c 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/TrxTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/TrxTest.kt @@ -6,18 +6,16 @@ import qbit.api.Attrs import qbit.api.Instances import qbit.api.QBitException import qbit.api.db.Conn -import qbit.api.db.attrIs import qbit.api.db.pull -import qbit.api.db.query import qbit.api.gid.Gid import qbit.api.gid.nextGids -import qbit.api.model.Attr import qbit.api.system.Instance import qbit.ns.Key import qbit.ns.ns import qbit.platform.runBlocking import qbit.spi.Storage import qbit.storage.MemStorage +import qbit.test.model.IntCounterEntity import qbit.test.model.Region import qbit.test.model.Scientist import qbit.test.model.testsSerialModule @@ -112,7 +110,8 @@ class TrxTest { Gid(0, 0).nextGids(), *entities ), conn, testSchemaFactorizer::factor, - GidSequence(0, 1) + GidSequence(0, 1), + { emptyList() } // TODO THINK ) @Ignore @@ -176,9 +175,28 @@ class TrxTest { } } + @JsName("Counter_test") + @Test + fun `Counter test`() { // TODO: find an appropriate place for this test + runBlocking { + val conn = setupTestData() + val counterEntity = IntCounterEntity(1, 10) + + conn.trx { + persist(counterEntity) + } + assertEquals(conn.db().pull(1)?.counter, 10) + + conn.trx { + persist(counterEntity.copy(counter = 90)) + } + assertEquals(conn.db().pull(1)?.counter, 90) + } + } + private suspend fun openEmptyConn(): Pair { val storage = MemStorage() - val conn = qbit(storage, testsSerialModule) + val conn = qbit(storage, testsSerialModule, testSchema.second) return conn to storage } diff --git a/qbit-core/src/commonTest/kotlin/qbit/ValidationTest.kt b/qbit-core/src/commonTest/kotlin/qbit/ValidationTest.kt index ebe22611..be788d28 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/ValidationTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/ValidationTest.kt @@ -30,7 +30,7 @@ class ValidationTest { fun `Subsequent storing of unique value for the same entity should not be treated as uniqueness violation`() { val db = dbOf( Gid(0, firstInstanceEid).nextGids(), - *(bootstrapSchema.values + testSchema).toTypedArray() + *(bootstrapSchema.values + testSchema.first).toTypedArray() ).with(eCodd.toFacts()) validate(db, listOf(Eav(Gid(eCodd.id!!), extId, eCodd.externalId))) } diff --git a/qbit-core/src/commonTest/kotlin/qbit/index/IndexDbTest.kt b/qbit-core/src/commonTest/kotlin/qbit/index/IndexDbTest.kt index e7147a92..9ee2ba73 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/index/IndexDbTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/index/IndexDbTest.kt @@ -77,7 +77,7 @@ class DbTest { dbUuid, currentTimeMillis(), NodeData((bootstrapSchema.values.flatMap { it.toFacts() } + - testSchema.flatMap { testSchemaFactorizer.factor(it, bootstrapSchema::get, gids) } + + testSchema.first.flatMap { testSchemaFactorizer.factor(it, bootstrapSchema::get, gids) } + extId.toFacts() + name.toFacts() + nicks.toFacts() + eCodd.toFacts()).toTypedArray())) val nodes = hashMapOf>(root.hash to root) val nodeResolver = mapNodeResolver(nodes) diff --git a/qbit-core/src/commonTest/kotlin/qbit/index/IndexTest.kt b/qbit-core/src/commonTest/kotlin/qbit/index/IndexTest.kt index 57814475..e2392952 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/index/IndexTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/index/IndexTest.kt @@ -62,7 +62,9 @@ class IndexTest { f(0, userName, "baz"), f(1, userName, "bar"), f(2, userName, "bar") - ) + ), + null, + emptyList() ) var lst = idx.eidsByPred(AttrValuePred(extId.name, 1)) @@ -91,7 +93,9 @@ class IndexTest { f(0, userName, "bar"), f(1, userName, "bar"), f(2, userName, "baz") - ) + ), + null, + emptyList() ) assertEquals(2, idx.eidsByPred(AttrPred(extId.name)).count()) @@ -219,7 +223,7 @@ class IndexTest { Gid(0, 1) to listOf(Eav(Gid(0, 1), "to-keep", "any")) ) ) - val filtered = idx.addFacts(listOf(Eav(deletedEntityGid, qbit.api.tombstone.name, true))) + val filtered = idx.addFacts(listOf(Eav(deletedEntityGid, qbit.api.tombstone.name, true)), null, emptyList()) assertEquals(1, filtered.entities.size) assertEquals(1, filtered.aveIndex.size) assertNotNull(filtered.eidsByPred(AttrValuePred("to-keep", "any")).firstOrNull(), "Cannot find entity by to-keep=any") diff --git a/qbit-core/src/commonTest/kotlin/qbit/resolving/ResolveConflictsTest.kt b/qbit-core/src/commonTest/kotlin/qbit/resolving/ResolveConflictsTest.kt index c6a66ed9..6aec372b 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/resolving/ResolveConflictsTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/resolving/ResolveConflictsTest.kt @@ -1,11 +1,7 @@ package qbit.resolving import qbit.Attr -import qbit.api.Instances -import qbit.api.gid.Gid -import qbit.api.model.Eav import qbit.api.model.Hash -import qbit.api.model.nullHash import qbit.platform.runBlocking import qbit.serialization.NodeVal import kotlin.js.JsName @@ -67,19 +63,4 @@ class ResolveConflictsTest { assertEquals(eavA.value, result[0].second[0].value) } } - - @JsName("Test_last_writer_wins_resolving_for_nextEid_attribute") - @Test - fun `Test last writer wins resolving for nextEid attribute`(){ - runBlocking { - val resolveConflictForNextEidAttr = lastWriterWinsResolve { Instances.nextEid } - val eav1 = Eav(Gid(1,8), Instances.nextEid.name, 10) - val eav2 = Eav(Gid(1,8), Instances.nextEid.name, 11) - val result = resolveConflictForNextEidAttr( - listOf(PersistedEav(eav1, 11, nullHash)), - listOf(PersistedEav(eav2, 10, nullHash)) - ) - assertEquals(listOf(eav2), result) - } - } } \ No newline at end of file diff --git a/qbit-core/src/commonTest/kotlin/qbit/typing/MappingTest.kt b/qbit-core/src/commonTest/kotlin/qbit/typing/MappingTest.kt index 9c59adf6..54ebb673 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/typing/MappingTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/typing/MappingTest.kt @@ -25,7 +25,7 @@ class MappingTest { private val gids = Gid(0, 0).nextGids() private fun createTestDb(): IndexDb = - IndexDb(Index().addFacts(testSchema.flatMap { factor(it, EmptyDb::attr, gids) }), testsSerialModule) + IndexDb(Index().addFacts(testSchema.first.flatMap { factor(it, EmptyDb::attr, gids) }, null, ), testsSerialModule, testSchema.second) @JsName("Test_simple_entity_mapping") @@ -41,10 +41,10 @@ class MappingTest { val db = createTestDb() val facts = factor(user, db::attr, gids) - val db2 = IndexDb(db.index.addFacts(facts), testsSerialModule) + val db2 = IndexDb(db.index.addFacts(facts, null, emptyList(), db::attr), testsSerialModule, testSchema.second) val se = db2.pullEntity(facts.entityFacts[user]!!.first().gid)!! - val fullUser = typify(db::attr, se, MUser::class, testsSerialModule) + val fullUser = typify(db::attr, se, MUser::class, testsSerialModule, testSchema.second) assertEquals("optAddr", fullUser.optTheSimplestEntity!!.scalar) assertEquals("login", fullUser.login) assertEquals(listOf("str1", "str2"), fullUser.strs) @@ -65,10 +65,10 @@ class MappingTest { ) val db = createTestDb() val facts = factor(user, db::attr, gids) - val db2 = IndexDb(db.index.addFacts(facts), testsSerialModule) + val db2 = IndexDb(db.index.addFacts(facts, null, emptyList(), db::attr), testsSerialModule, testSchema.second) val se = db2.pullEntity(facts.entityFacts[user]!!.first().gid)!! - val fullUser = typify(db::attr, se, MUser::class, testsSerialModule) + val fullUser = typify(db::attr, se, MUser::class, testsSerialModule, testSchema.second) assertEquals(fullUser.theSimplestEntity, fullUser.optTheSimplestEntity) assertEquals(fullUser.optTheSimplestEntity, fullUser.theSimplestEntities[0]) @@ -116,6 +116,7 @@ class MappingTest { val attrs = schema(testsSerialModule) { entity(Bomb::class) } + .first .associateBy { it.name } assertEquals( QBoolean.code, @@ -331,9 +332,9 @@ class MappingTest { // When it's factorized, stored, pulled and typed val testDb = createTestDb() val facts = factor(bomb, testDb::attr, gids) - val db2 = IndexDb(testDb.index.addFacts(facts), testsSerialModule) + val db2 = IndexDb(testDb.index.addFacts(facts, null, emptyList(), testDb::attr), testsSerialModule, testSchema.second) val se = db2.pullEntity(facts.entityFacts[bomb]!!.first().gid)!! - val typedBomb = typify(testDb::attr, se, Bomb::class, testsSerialModule) + val typedBomb = typify(testDb::attr, se, Bomb::class, testsSerialModule, testSchema.second) // then result is equal to original assertEquals(bomb, typedBomb) @@ -356,9 +357,9 @@ class MappingTest { // When it's factorized, stored, pulled and typed val testDb = createTestDb() val facts = factor(bomb, testDb::attr, gids) - val db2 = IndexDb(testDb.index.addFacts(facts), testsSerialModule) + val db2 = IndexDb(testDb.index.addFacts(facts, null, emptyList(), testDb::attr), testsSerialModule, testSchema.second) val se = db2.pullEntity(facts.entityFacts[bomb]!!.first().gid)!! - var typedBomb = typify(testDb::attr, se, Bomb::class, testsSerialModule) + var typedBomb = typify(testDb::attr, se, Bomb::class, testsSerialModule, testSchema.second) // Fix empty list handling typedBomb = typedBomb.copy( diff --git a/qbit-core/src/commonTest/kotlin/qbit/typing/SerializationTypingTest.kt b/qbit-core/src/commonTest/kotlin/qbit/typing/SerializationTypingTest.kt index 773a8dae..96d219d9 100644 --- a/qbit-core/src/commonTest/kotlin/qbit/typing/SerializationTypingTest.kt +++ b/qbit-core/src/commonTest/kotlin/qbit/typing/SerializationTypingTest.kt @@ -25,7 +25,7 @@ class SerializationTypingTest { val map = HashMap() val theSimplestEntity = AttachedEntity(gids.next(), listOf(TheSimplestEntities.scalar to "Aleksey Lyapunov"), map::get) - val typedEntity = typify(schemaMap::get, theSimplestEntity, TheSimplestEntity::class, testsSerialModule) + val typedEntity = typify(schemaMap::get, theSimplestEntity, TheSimplestEntity::class, testsSerialModule, testSchema.second) assertEquals(typedEntity.scalar, "Aleksey Lyapunov") } @@ -37,7 +37,7 @@ class SerializationTypingTest { listOf(Countries.name to "Russia", Countries.population to 146_000_000), nullGidResolver ) - val typedRu = typify(schemaMap::get, ru, Country::class, testsSerialModule) + val typedRu = typify(schemaMap::get, ru, Country::class, testsSerialModule, testSchema.second) assertEquals("Russia", typedRu.name) assertEquals(146_000_000, typedRu.population) } @@ -58,7 +58,7 @@ class SerializationTypingTest { map::get ) map[ru.gid] = ru - val typedNsk = typify(schemaMap::get, nsk, Region::class, testsSerialModule) + val typedNsk = typify(schemaMap::get, nsk, Region::class, testsSerialModule, testSchema.second) assertEquals("Novosibirskaya obl.", typedNsk.name) assertEquals("Russia", typedNsk.country.name) } @@ -72,7 +72,7 @@ class SerializationTypingTest { listOf(Countries.name to "Russia", Countries.population to 146_000_000), nullGidResolver ) - val typedRu = typify(schemaMap::get, ru, Country::class, testsSerialModule) + val typedRu = typify(schemaMap::get, ru, Country::class, testsSerialModule, testSchema.second) assertEquals(146_000_000, typedRu.population) } @@ -85,7 +85,7 @@ class SerializationTypingTest { listOf(Papers.name to "ER-Model"), nullGidResolver ) - val typedEr = typify(schemaMap::get, er, Paper::class, testsSerialModule) + val typedEr = typify(schemaMap::get, er, Paper::class, testsSerialModule, testSchema.second) assertEquals("ER-Model", typedEr.name) assertNull(typedEr.editor) } @@ -123,7 +123,7 @@ class SerializationTypingTest { map[aLaypunov.gid] = aLaypunov map[aErshov.gid] = aErshov map[ru.gid] = ru - val typedErshov = typify(schemaMap::get, aErshov, Scientist::class, testsSerialModule) + val typedErshov = typify(schemaMap::get, aErshov, Scientist::class, testsSerialModule, testSchema.second) @Suppress("UNCHECKED_CAST") assertEquals(aLaypunov[Scientists.name as Attr], typedErshov.reviewer?.name) } @@ -137,7 +137,7 @@ class SerializationTypingTest { val e = AttachedEntity(gids.next(), listOf(NullableScalars.placeholder to 0), map::get) map[e.gid] = e - val ns = typify(schemaMap::get, e, NullableScalar::class, testsSerialModule) + val ns = typify(schemaMap::get, e, NullableScalar::class, testsSerialModule, testSchema.second) // Workaround for strange failure in case of longs comparison on js platform: // qbit.typing // SerializationTypingTest @@ -159,7 +159,7 @@ class SerializationTypingTest { ) map[e.gid] = e - val ns = typify(schemaMap::get, e, NullableScalar::class, testsSerialModule) + val ns = typify(schemaMap::get, e, NullableScalar::class, testsSerialModule, testSchema.second) assertEquals(1L, ns.placeholder) assertEquals(1.toByte(), ns.scalar) } @@ -177,7 +177,7 @@ class SerializationTypingTest { ) map[e.gid] = e - val ns = typify(schemaMap::get, e, NullableList::class, testsSerialModule) + val ns = typify(schemaMap::get, e, NullableList::class, testsSerialModule, testSchema.second) assertEquals(listOf(1.toByte()), ns.lst) } @@ -193,7 +193,7 @@ class SerializationTypingTest { map[r.gid] = r map[e.gid] = e - val ns = typify(schemaMap::get, e, NullableRef::class, testsSerialModule) + val ns = typify(schemaMap::get, e, NullableRef::class, testsSerialModule, testSchema.second) assertEquals(1, ns.ref?.int) } @@ -221,7 +221,7 @@ class SerializationTypingTest { map[aLaypunov.gid] = aLaypunov map[aErshov.gid] = aErshov map[ru.gid] = ru - val typedErshov = typify(schemaMap::get, aErshov, Scientist::class, testsSerialModule) + val typedErshov = typify(schemaMap::get, aErshov, Scientist::class, testsSerialModule, testSchema.second) assertEquals("Andrey Ershov", typedErshov.name) assertEquals("Aleksey Lyapunov", typedErshov.reviewer?.name) } @@ -254,7 +254,7 @@ class SerializationTypingTest { map[researchGroup.gid] = researchGroup map[ru.gid] = ru - val typedGroup = typify(schemaMap::get, researchGroup, ResearchGroup::class, testsSerialModule) + val typedGroup = typify(schemaMap::get, researchGroup, ResearchGroup::class, testsSerialModule, testSchema.second) assertEquals(2, typedGroup.members.size) assertEquals("Aleksey Lyapunov", typedGroup.members[0].name) assertEquals("Andrey Ershov", typedGroup.members[1].name) @@ -273,7 +273,7 @@ class SerializationTypingTest { map[nsk.gid] = nsk map[nskCity.gid] = nskCity - val typedNsk = typify(schemaMap::get, nskCity, City::class, testsSerialModule) + val typedNsk = typify(schemaMap::get, nskCity, City::class, testsSerialModule, testSchema.second) assertEquals("Russia", typedNsk.region.country.name) } @@ -285,7 +285,7 @@ class SerializationTypingTest { AttachedEntity(gids.next(), listOf(EntityWithByteArrays.byteArray to byteArrayOf(1, 2, 3)), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule, testSchema.second) // Then it contains assertArrayEquals(byteArrayOf(1, 2, 3), typed.byteArray) @@ -299,7 +299,7 @@ class SerializationTypingTest { AttachedEntity(gids.next(), listOf(EntityWithByteArrays.byteArray to byteArrayOf()), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule, testSchema.second) // Then it contains assertArrayEquals(byteArrayOf(), typed.byteArray) @@ -312,7 +312,7 @@ class SerializationTypingTest { val entity = AttachedEntity(gids.next(), listOf(), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithByteArray::class, testsSerialModule, testSchema.second) // Then it contains assertNull(typed.byteArray) @@ -326,7 +326,7 @@ class SerializationTypingTest { AttachedEntity(gids.next(), listOf(EntityWithListOfBytess.bytes to listOf(1, 2, 3)), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithListOfBytes::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithListOfBytes::class, testsSerialModule, testSchema.second) // Then it contains assertEquals(listOf(1, 2, 3), typed.bytes) @@ -343,7 +343,7 @@ class SerializationTypingTest { ) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithListOfByteArray::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithListOfByteArray::class, testsSerialModule, testSchema.second) // Then it contains assertArrayEquals(byteArrayOf(1), typed.byteArrays[0]) @@ -358,7 +358,7 @@ class SerializationTypingTest { AttachedEntity(gids.next(), listOf(EntityWithListOfStringss.strings to listOf("1")), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule, testSchema.second) // Then it contains assertEquals(listOf("1"), typed.strings) @@ -372,7 +372,7 @@ class SerializationTypingTest { AttachedEntity(gids.next(), listOf(EntityWithListOfStringss.strings to listOf()), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule, testSchema.second) // Then it contains assertEquals(listOf(), typed.strings) @@ -385,7 +385,7 @@ class SerializationTypingTest { val entity = AttachedEntity(gids.next(), listOf(), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule) + val typed = typify(schemaMap::get, entity, EntityWithListOfString::class, testsSerialModule, testSchema.second) // Then it contains assertNull(typed.strings) @@ -399,7 +399,7 @@ class SerializationTypingTest { val entity = AttachedEntity(gid, listOf(GidEntities.bool to true), nullGidResolver) // When it typed - val typed = typify(schemaMap::get, entity, GidEntity::class, internalTestsSerialModule) + val typed = typify(schemaMap::get, entity, GidEntity::class, internalTestsSerialModule, testSchema.second) // Then it has correct gid assertEquals(gid, typed.id) @@ -522,7 +522,7 @@ class SerializationTypingTest { ) // When it typed - val typed = typify(schemaMap::get, root, ParentToChildrenTreeEntity::class, internalTestsSerialModule) + val typed = typify(schemaMap::get, root, ParentToChildrenTreeEntity::class, internalTestsSerialModule, testSchema.second) // Then root has correct name and children count assertEquals("root", typed.name) diff --git a/qbit-core/src/jvmTest/kotlin/q5/Q5Test.kt b/qbit-core/src/jvmTest/kotlin/q5/Q5Test.kt index 855e43e2..96f8d91d 100644 --- a/qbit-core/src/jvmTest/kotlin/q5/Q5Test.kt +++ b/qbit-core/src/jvmTest/kotlin/q5/Q5Test.kt @@ -70,7 +70,7 @@ val q5Schema = schema(q5SerialModule) { entity(Trx::class) } -val schemaMap = q5Schema.map { it.name to it }.toMap() +val schemaMap = q5Schema.first.map { it.name to it }.toMap() class Q5Test { @@ -84,9 +84,9 @@ class Q5Test { } val dataFiles = dataDir.listFiles() - val conn = qbit(MemStorage(), q5SerialModule) + val conn = qbit(MemStorage(), q5SerialModule, q5Schema.second) - q5Schema.forEach { conn.persist(it) } + q5Schema.first.forEach { conn.persist(it) } val categories = HashMap() dataFiles!! diff --git a/qbit-http-storages/src/commonTest/kotlin/qbit/storage/StorageTest.kt b/qbit-http-storages/src/commonTest/kotlin/qbit/storage/StorageTest.kt index 70c602a5..332200e2 100644 --- a/qbit-http-storages/src/commonTest/kotlin/qbit/storage/StorageTest.kt +++ b/qbit-http-storages/src/commonTest/kotlin/qbit/storage/StorageTest.kt @@ -43,7 +43,7 @@ abstract class StorageTest { val origin = MemStorage() // initialize storage - qbit(origin, testsSerialModule) + qbit(origin, testsSerialModule, emptyMap()) // actually it compiles val storage = storage() diff --git a/qbit-test-fixtures/src/commonMain/kotlin/qbit/test/model/TestModels.kt b/qbit-test-fixtures/src/commonMain/kotlin/qbit/test/model/TestModels.kt index d88500fc..d53fab50 100644 --- a/qbit-test-fixtures/src/commonMain/kotlin/qbit/test/model/TestModels.kt +++ b/qbit-test-fixtures/src/commonMain/kotlin/qbit/test/model/TestModels.kt @@ -9,6 +9,15 @@ data class TheSimplestEntity(val id: Long?, val scalar: String) @Serializable data class IntEntity(val id: Long?, val int: Int) +@Serializable +data class IntCounterEntity(val id: Long?, val counter: Int) + +@Serializable +data class StringRegisterEntity(val id: Long?, val register: String) + +@Serializable +data class CountryRegisterEntity(val id: Long?, val register: Country) + @Serializable data class NullableIntEntity(val id: Long?, val int: Int?) @@ -307,6 +316,9 @@ val testsSerialModule = SerializersModule { contextual(ByteArrayEntity::class, ByteArrayEntity.serializer()) contextual(ListOfByteArraysEntity::class, ListOfByteArraysEntity.serializer()) contextual(IntEntity::class, IntEntity.serializer()) + contextual(IntCounterEntity::class, IntCounterEntity.serializer()) + contextual(StringRegisterEntity::class, StringRegisterEntity.serializer()) + contextual(CountryRegisterEntity::class, CountryRegisterEntity.serializer()) contextual(Region::class, Region.serializer()) contextual(ParentToChildrenTreeEntity::class, ParentToChildrenTreeEntity.serializer()) contextual(EntityWithRefsToSameType::class, EntityWithRefsToSameType.serializer())