From fe3f7ee529765fb9426d07b433332b635193279c Mon Sep 17 00:00:00 2001 From: gabriel20xx Date: Sat, 25 Oct 2025 15:54:31 +0200 Subject: [PATCH] feat: Add methods to retrieve all field and item IDs in DAO interfaces --- .../com/collabtable/app/data/dao/FieldDao.kt | 3 + .../com/collabtable/app/data/dao/ItemDao.kt | 3 + .../app/data/repository/SyncRepository.kt | 79 +++++++++--------- CollabTableAndroid/build-output.txt | Bin 6662 -> 4866 bytes 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt index 02b57cd..42fe727 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt @@ -12,6 +12,9 @@ interface FieldDao { @Query("SELECT * FROM fields WHERE id = :fieldId") suspend fun getFieldById(fieldId: String): Field? + @Query("SELECT id FROM fields") + suspend fun getAllFieldIds(): List + @Upsert suspend fun insertField(field: Field) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt index 18408d2..e074c59 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt @@ -17,6 +17,9 @@ interface ItemDao { @Query("SELECT * FROM items WHERE id = :itemId") suspend fun getItemById(itemId: String): Item? + @Query("SELECT id FROM items") + suspend fun getAllItemIds(): List + @Upsert suspend fun insertItem(item: Item) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt index e35e457..e83edc3 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt @@ -7,6 +7,7 @@ import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.utils.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import androidx.room.withTransaction class SyncRepository(context: Context) { private val database = CollabTableDatabase.getDatabase(context) @@ -25,9 +26,8 @@ class SyncRepository(context: Context) { try { val lastSync = getLastSyncTimestamp() val isInitialSync = lastSync == 0L - if (isInitialSync) { - Logger.i("Sync", "🔄 Starting initial sync with server") + Logger.i("Sync", "[SYNC] Starting initial sync with server") } // Gather local changes since last sync @@ -36,19 +36,13 @@ class SyncRepository(context: Context) { val localItems = database.itemDao().getItemsUpdatedSince(lastSync) val localValues = database.itemValueDao().getValuesUpdatedSince(lastSync) - // Only log when sending data - if (localLists.isNotEmpty() || localFields.isNotEmpty() || localItems.isNotEmpty()) { - Logger.i("Sync", "⬆️ Sending to server:") - if (localLists.isNotEmpty()) { - Logger.i("Sync", " 📋 ${localLists.size} list(s)") - localLists.forEach { list -> - val action = if (list.isDeleted) "Deleted" else "Updated" - Logger.i("Sync", " $action: ${list.name}") - } - } - if (localFields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${localFields.size} field(s)") - if (localItems.isNotEmpty()) Logger.i("Sync", " 📝 ${localItems.size} item(s)") - if (localValues.isNotEmpty()) Logger.i("Sync", " 💾 ${localValues.size} value(s)") + // Only log send when there are actual local changes + val localTotal = localLists.size + localFields.size + localItems.size + localValues.size + if (localTotal > 0) { + Logger.i( + "Sync", + "[OUT] Sending changes (lists=${localLists.size}, fields=${localFields.size}, items=${localItems.size}, values=${localValues.size})" + ) } // Send to server and get updates @@ -65,43 +59,52 @@ class SyncRepository(context: Context) { if (response.isSuccessful) { val syncResponse = response.body()!! - // Only log when receiving data - if (syncResponse.lists.isNotEmpty() || syncResponse.fields.isNotEmpty() || - syncResponse.items.isNotEmpty() || syncResponse.itemValues.isNotEmpty()) { - Logger.i("Sync", "⬇️ Received from server:") - if (syncResponse.lists.isNotEmpty()) { - Logger.i("Sync", " 📋 ${syncResponse.lists.size} list(s)") - syncResponse.lists.forEach { list -> - if (!list.isDeleted) { - Logger.i("Sync", " ${list.name}") - } - } - } - if (syncResponse.fields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${syncResponse.fields.size} field(s)") - if (syncResponse.items.isNotEmpty()) Logger.i("Sync", " 📝 ${syncResponse.items.size} item(s)") - if (syncResponse.itemValues.isNotEmpty()) Logger.i("Sync", " 💾 ${syncResponse.itemValues.size} value(s)") + // Only log receive when there are actual server changes + val inTotal = syncResponse.lists.size + syncResponse.fields.size + syncResponse.items.size + syncResponse.itemValues.size + if (inTotal > 0) { + Logger.i( + "Sync", + "[IN] Received changes (lists=${syncResponse.lists.size}, fields=${syncResponse.fields.size}, items=${syncResponse.items.size}, values=${syncResponse.itemValues.size})" + ) } - // Apply server changes to local database - database.listDao().insertLists(syncResponse.lists) - database.fieldDao().insertFields(syncResponse.fields) - database.itemDao().insertItems(syncResponse.items) - database.itemValueDao().insertValues(syncResponse.itemValues) + // Apply server changes to local database atomically in correct order + database.withTransaction { + database.listDao().insertLists(syncResponse.lists) + database.fieldDao().insertFields(syncResponse.fields) + database.itemDao().insertItems(syncResponse.items) + + // Filter item values to only those whose parents exist locally to avoid FK violations + val existingItemIds = database.itemDao().getAllItemIds().toSet() + val existingFieldIds = database.fieldDao().getAllFieldIds().toSet() + val filteredValues = syncResponse.itemValues.filter { v -> + v.itemId in existingItemIds && v.fieldId in existingFieldIds + } + + if (filteredValues.size != syncResponse.itemValues.size) { + val dropped = syncResponse.itemValues.size - filteredValues.size + Logger.w("Sync", "Dropping $dropped item value(s) with missing parents to avoid FK errors") + } + + if (filteredValues.isNotEmpty()) { + database.itemValueDao().insertValues(filteredValues) + } + } // Update last sync timestamp setLastSyncTimestamp(syncResponse.serverTimestamp) if (isInitialSync) { - Logger.i("Sync", "✅ Initial sync completed") + Logger.i("Sync", "[SYNC] Initial sync completed") } return@withContext Result.success(Unit) } else { - Logger.e("Sync", "❌ Sync failed: HTTP ${response.code()}") + Logger.e("Sync", "[ERROR] Sync failed: HTTP ${response.code()}") return@withContext Result.failure(Exception("Sync failed: ${response.code()}")) } } catch (e: Exception) { - Logger.e("Sync", "❌ Sync error: ${e.message}") + Logger.e("Sync", "[ERROR] Sync error: ${e.message}") return@withContext Result.failure(e) } } diff --git a/CollabTableAndroid/build-output.txt b/CollabTableAndroid/build-output.txt index 8a11f4bf78313620969b32fb23b4e7bb59861d04..ebc0ca33f678bb9eca0ad2eea851addf7d57155a 100644 GIT binary patch delta 30 jcmZoOX;Rx@!!!9Cv()4$9yJiXd6qB>8>8vu`I6}XrN9ap literal 6662 zcmeI1TW=ai6vxkVrGAH%s>-cYAa0r}@k3SOOXM^cE5xq)RG9^A#~35P#{Tk6{=Yd) zXTdTRTvRv0~%-0yfFvnA(uA(uxnB3ibKy{i90mA8avXR{m{RwIX&yBeWpIUo3+?juz0&G_!5$6m79HfG*U&YR zrqIXvO4Pq5d*Zd4><|6Pb+R7azIKf7oX+^_rfB0$MLWKc&U10=%CoWJdeY~zUOU*V z$O9k0lBM0Zn;`}o;>uk7>#at5^7>AYV6>98+jb{U!B2BNiG;Q|z6mjQDT*mJ%KIsq z3@xFbA|>P|hG*s9EQg|I{y)kG+TkF?Tk^imaodD%eUi3sbsYu2i9SVbDf(J@ndeo8 zhH?(sniyXtJLD6OpOtQwZn?^`IHT2rXOb~jk#?R2)}t8rPlQj$%zAXO$S~AS>RXEG z1I1zVgTChUg6(>W*^98EyjRSts(a$x7oQ8!C6~2Ty}N3$L*y#YC24*?A& z6v=(fKxGOG1f^V3;3D%-?3Ww?S|HIh)XhfO;$OTwb5-&sI?JrcS|e2 zx1a4vs43U>N$;Qs=A(&ji0)=M-8gDl)L)m@TB3d}`Yo;YGf+*GK(jS@nj?jpcz6}7 zsh=JCGjihG5jjN~oS&eS@DlP76?iya!YcH1qI=uM@|5ckk6o(@Nsgq8Uu;|VZF??V zeN=5@Z5HO_VUNXgM>ZM7morh$WJB`!?Sy(3_r8E@gg;l+ zd#Ew?e51Q#YxZ%jRX5TAJ8;Rd)IjpN+==6s{jT10p1TuOWX9$N}$gO$sp0e^SI=9C?`>C}2 zzUN6lO*4H(S%*3mB%u;CTSW59^tWfHa9Ns;LARD>|U$nFnRyd+7YRj=Z$O%*vIgyDYZX<>{$qoW;0>eZE;n8=mcgJRo zbTkjt8u0~q2CM^0aD>Wv%aN9&9nQ<~TQ}$^YR11ep?Bs%+p$Kp4>n;fmUW#m3roY( zwQ{cKNVA5juHgQbXpkwY6a432+pB@YnZ}=MWLx&BNg`+K-lD%u+1xk6qVQ6OF`iI(wVnSonKIFT&5hcE3fbHWy0!e!>m8%aG zm3yMnP