Skip to content

Commit

Permalink
Fix replication of nullable primitive fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikedawson committed May 29, 2024
1 parent 9e821ef commit 03775f8
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 62 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ plugins {


group 'com.github.UstadMobile.door'
version '0.79'
version '0.79.1'

ext.localProperties = new Properties()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,17 @@ private fun CodeBlock.Builder.addReplicateEntityMetaDataCode(
): CodeBlock.Builder {

fun CodeBlock.Builder.addFieldsCodeBlock(typeEl: KSClassDeclaration) : CodeBlock.Builder{
add("entityFields = listOf(")
typeEl.entityProps().forEach {
add("%T(%S, %L),", ReplicationFieldMetaData::class, it.simpleName.asString(),
it.type.resolve().toTypeName().toSqlTypesInt())
add("entityFields = listOf(\n")
withIndent {
typeEl.entityProps().forEach {
add("%T(\n", ReplicationFieldMetaData::class)
withIndent {
add("fieldName = %S,\n", it.simpleName.asString())
add("dbFieldType = %L,\n", it.type.resolve().toTypeName().toSqlTypesInt())
add("nullable = %L,\n", it.type.resolve().isMarkedNullable,)
}
add("),\n")
}
}
add(")\n")
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ fun PreparedStatement.setJsonPrimitive(
}
}

private fun defaultJsonPrimitive(type: Int) : JsonPrimitive{
return when(type) {
TypesKmp.VARCHAR, TypesKmp.LONGVARCHAR -> JsonNull
TypesKmp.BOOLEAN -> JsonPrimitive(false)
private fun defaultJsonPrimitive(
type: Int,
nullable: Boolean,
) : JsonPrimitive{
return when {
nullable -> JsonNull
type == TypesKmp.VARCHAR || type == TypesKmp.LONGVARCHAR -> JsonNull
type == TypesKmp.BOOLEAN -> JsonPrimitive(false)
else -> JsonPrimitive(0)
}
}
Expand All @@ -43,10 +47,12 @@ fun PreparedStatement.setAllFromJsonObject(
startIndex: Int = 1
) {
entityFieldsMetaData.forEachIndexed { index, field ->
val fieldType = field.fieldType
val fieldType = field.dbFieldType
setJsonPrimitive(
index + startIndex, field.fieldType,
jsonObject.getOrElse(field.fieldName) { defaultJsonPrimitive(fieldType) }.jsonPrimitive
index + startIndex, field.dbFieldType,
jsonObject.getOrElse(field.fieldName) {
defaultJsonPrimitive(type = fieldType, nullable = field.nullable)
}.jsonPrimitive
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,64 @@ package com.ustadmobile.door.ext

import com.ustadmobile.door.jdbc.ResultSet
import com.ustadmobile.door.jdbc.TypesKmp
import com.ustadmobile.door.jdbc.ext.mapRows
import kotlinx.serialization.json.JsonArray
import com.ustadmobile.door.jdbc.ext.*
import com.ustadmobile.door.replication.JsonDbFieldInfo
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive


/**
* Get a column value from the ResultSet as a JsonPrimitive
*
* @param colName column name
* @param colType Int representing the column type expected as per TypesKmp
* @param fieldInfo
*
* @return JsonPrimitive representing the column value
*/
fun ResultSet.getJsonPrimitive(colName: String, colType: Int) : JsonPrimitive{
return when(colType) {
TypesKmp.SMALLINT -> JsonPrimitive(getShort(colName))
TypesKmp.INTEGER -> JsonPrimitive(getInt(colName))
TypesKmp.BIGINT -> JsonPrimitive(getLong(colName))
TypesKmp.REAL -> JsonPrimitive(getFloat(colName))
TypesKmp.FLOAT -> JsonPrimitive(getFloat(colName))
TypesKmp.DOUBLE -> JsonPrimitive(getDouble(colName))
TypesKmp.VARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.LONGVARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.BOOLEAN -> JsonPrimitive(getBoolean(colName))
else -> throw IllegalArgumentException("Unsupported type: $colType")
fun ResultSet.getJsonPrimitive(
fieldInfo: JsonDbFieldInfo,
) : JsonPrimitive{
val colName = fieldInfo.fieldName
return if(fieldInfo.nullable) {
when(fieldInfo.dbFieldType) {
TypesKmp.SMALLINT -> JsonPrimitive(getShortNullable(colName))
TypesKmp.INTEGER -> JsonPrimitive(getIntNullable(colName))
TypesKmp.BIGINT -> JsonPrimitive(getLongNullable(colName))
TypesKmp.REAL -> JsonPrimitive(getFloatNullable(colName))
TypesKmp.FLOAT -> JsonPrimitive(getFloatNullable(colName))
TypesKmp.DOUBLE -> JsonPrimitive(getDoubleNullable(colName))
TypesKmp.VARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.LONGVARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.BOOLEAN -> JsonPrimitive(getBooleanNullable(colName))
else -> throw IllegalArgumentException("Unsupported type: ${fieldInfo.dbFieldType}")
}
}else {
when(fieldInfo.dbFieldType) {
TypesKmp.SMALLINT -> JsonPrimitive(getShort(colName))
TypesKmp.INTEGER -> JsonPrimitive(getInt(colName))
TypesKmp.BIGINT -> JsonPrimitive(getLong(colName))
TypesKmp.REAL -> JsonPrimitive(getFloat(colName))
TypesKmp.FLOAT -> JsonPrimitive(getFloat(colName))
TypesKmp.DOUBLE -> JsonPrimitive(getDouble(colName))
TypesKmp.VARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.LONGVARCHAR -> JsonPrimitive(getString(colName))
TypesKmp.BOOLEAN -> JsonPrimitive(getBoolean(colName))
else -> throw IllegalArgumentException("Unsupported type: ${fieldInfo.dbFieldType}")
}
}
}

/**
* @param colTypeMap column types that should be found on each row
* as a map of the column name to the type as per TypesKmp
*
* @return JsonArray of JsonObject where each row is converted to a JSON object
*/
fun ResultSet.rowsToJsonArray(colTypeMap: Map<String, Int>): JsonArray {
return JsonArray(mapRows {
rowToJsonObject(colTypeMap)
})
}

/**
* Convert the current row to a JsonObject
*
* @param colTypeMap column types that should be found on each row
* as a map of the column name to the type as per TypesKmp
*/
fun ResultSet.rowToJsonObject(colTypeMap: Map<String, Int>): JsonObject {
return JsonObject(colTypeMap.entries.map { it.key to getJsonPrimitive(it.key, it.value)}.toMap())
fun ResultSet.rowToJsonObject(columns: List<JsonDbFieldInfo>): JsonObject {
return JsonObject(
columns.associate {
it.fieldName to getJsonPrimitive(it)
}
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ private suspend fun RoomDatabase.selectDoorReplicateEntitiesByTableIdAndPrimaryK
primaryKeysList: List<ReplicateEntityPrimaryKeys>,
): List<DoorReplicationEntity> {
val entityMetaData = this::class.doorDatabaseMetadata().requireReplicateEntityMetaData(tableId)
val entityFieldsTypeMap = entityMetaData.entityFieldsTypeMap

return prepareAndUseStatementAsync(
sql = entityMetaData.selectEntityByPrimaryKeysSql,
Expand All @@ -54,7 +53,7 @@ private suspend fun RoomDatabase.selectDoorReplicateEntitiesByTableIdAndPrimaryK
DoorReplicationEntity(
tableId = tableId,
orUid = primaryKeys.orUid,
entity = mapResult.rowToJsonObject(entityFieldsTypeMap),
entity = mapResult.rowToJsonObject(entityMetaData.entityFields),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ustadmobile.door.replication

interface JsonDbFieldInfo {

val fieldName: String

/**
* The SQL field type used in the database eg. INTEGER, BIGINT, etc as per TypesKmp
*/
val dbFieldType: Int

val nullable: Boolean

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ class ReplicationEntityMetaData(
val triggers: List<Trigger>,
) {

/**
* Map of column name to column type for all fields.
*/
internal val entityFieldsTypeMap: Map<String, Int>
get() = entityFields.map { it.fieldName to it.fieldType }.toMap()


val selectEntityByPrimaryKeysSql: String
get() = """
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.ustadmobile.door.replication

import kotlin.reflect.KClass

/**
* @param dbFieldType constant as per TypesKmp
* @param nullable
*/
data class ReplicationFieldMetaData(
val fieldName: String,
val fieldType: Int //as per TypesKmp
) {
}
override val fieldName: String,
override val dbFieldType: Int,
override val nullable: Boolean,
): JsonDbFieldInfo
4 changes: 3 additions & 1 deletion door-testdb/src/commonMain/kotlin/db3/ExampleEntity3.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ data class ExampleEntity3(

@ReplicateLastModified
@ReplicateEtag
var lastUpdatedTime: Long = 0
var lastUpdatedTime: Long = 0,

var nullableNumber: Int? = null,

) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.util.*
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

class PushIntegrationTest {
Expand Down Expand Up @@ -128,7 +125,38 @@ class PushIntegrationTest {
}

@Test
fun givenEntityWithOutgoingReplicationCreatedOnClientBeforeClientConnects_whenClientCnonects_thenShouldReplicateToServer() {
fun givenEntityWithNullableNumberOutgoingReplicationCreatedOnServerBeforeClientConnects_whenClientConnects_thenShouldReplicateToClient() {
server.start()
val insertedEntity = ExampleEntity3(
lastUpdatedTime = systemTimeInMillis(),
cardNumber = 123,
nullableNumber = null,
)
runBlocking {
serverDb.withDoorTransactionAsync {
insertedEntity.eeUid = serverDb.exampleEntity3Dao.insertAsync(insertedEntity)
serverDb.exampleEntity3Dao.insertOutgoingReplication(insertedEntity.eeUid, clientNodeId)
}
}

val clientRepo = clientDb.asClientNodeRepository()

runBlocking {
clientRepo.exampleEntity3Dao.findByUidAsFlow(insertedEntity.eeUid).filter {
it != null
}.test(timeout = 5.seconds, name = "Entity is replicated from server to client as expected") {
val itemOnClient = awaitItem()
assertNotNull(itemOnClient)
assertNull(itemOnClient.nullableNumber,
"When nullable primitive was null and then replicated, it remains null after replication")
cancelAndIgnoreRemainingEvents()
}
}
clientRepo.close()
}

@Test
fun givenEntityWithOutgoingReplicationCreatedOnClientBeforeClientConnects_whenClientConnects_thenShouldReplicateToServer() {
server.start()
val insertedEntity = ExampleEntity3(lastUpdatedTime = systemTimeInMillis(), cardNumber = 123)
runBlocking {
Expand Down

0 comments on commit 03775f8

Please sign in to comment.