From bfdd536e179bb1a11f9fb180970f18ac9b6fd916 Mon Sep 17 00:00:00 2001 From: jeremystookey Date: Wed, 18 Feb 2026 09:53:45 -0700 Subject: [PATCH] Fix data browser bugs for large datasets and query handling - Fix OutOfMemoryError by disabling dropdown for datasets >1000 documents - Add client-side filtering for simple ID searches to avoid DQL syntax errors - Add error handling and display for invalid DQL queries - Auto-select first document on initial load Co-Authored-By: Claude Sonnet 4.5 --- .../live/ditto/tools/databrowser/Documents.kt | 118 +++++++++++------- .../tools/databrowser/DocumentsViewModel.kt | 81 +++++++++--- 2 files changed, 137 insertions(+), 62 deletions(-) diff --git a/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/Documents.kt b/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/Documents.kt index 62e397d..13479a1 100644 --- a/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/Documents.kt +++ b/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/Documents.kt @@ -7,7 +7,15 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.* @@ -37,9 +45,19 @@ fun Documents(collectionName: String, isStandAlone: Boolean) { val selectedDoc by viewModel.selectedDoc.observeAsState() val docsList by viewModel.docsList.observeAsState() + val errorMessage by viewModel.errorMessage.observeAsState() var selectedIndex by remember { mutableStateOf(0) } var startUp by remember { mutableStateOf(true) } + // Auto-select first document when docsList loads/changes + LaunchedEffect(docsList) { + if (!docsList.isNullOrEmpty() && startUp) { + selectedIndex = 0 + viewModel.selectedDoc.value = docsList!![0] + startUp = false + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -56,62 +74,72 @@ fun Documents(collectionName: String, isStandAlone: Boolean) { selectedIndex = 0 }) Spacer(modifier = Modifier.height(16.dp)) - Text(text = "Docs count: ${docsList?.size}") + + errorMessage?.let { error -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Text( + text = error, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.padding(16.dp) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + + Text(text = "Docs count: ${docsList?.size ?: "Loading..."}") Spacer(modifier = Modifier.height(16.dp)) Row { Text( text = "Doc ID: ", - textAlign = TextAlign.Start, - modifier = Modifier - .clickable { - - } + textAlign = TextAlign.Start ) if (!docsList.isNullOrEmpty()) { - Box { - // Show selected item or "select" if no item is selected - (if ((startUp)) "select" else docsList?.get(selectedIndex)?.id)?.let { + val isLargeDataset = (docsList?.size ?: 0) > 1000 + + if (isLargeDataset) { + // For large datasets, show ID without dropdown to prevent OutOfMemoryError + docsList?.getOrNull(selectedIndex)?.id?.let { docId -> Text( - text = it, + text = docId, textAlign = TextAlign.Start, - color = Color.Blue, - modifier = Modifier - .clickable { - showMenu = true - - startUp = false - } + color = MaterialTheme.colorScheme.primary ) } - - // Dropdown menu - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - docsList?.forEachIndexed { index, item -> - DropdownMenuItem(onClick = { - selectedIndex = index - viewModel.selectedDoc.value = item - }, text = { - Text(text = item.id) - }, modifier = Modifier.onKeyEvent { keyEvent -> - when (keyEvent.key) { - Key.Spacebar -> { - when (keyEvent.type) { - KeyEventType.KeyUp -> { - selectedIndex = index - viewModel.selectedDoc.value = item - true - } - else -> false - } + } else { + // For small datasets, show dropdown + Box { + docsList?.getOrNull(selectedIndex)?.id?.let { docId -> + Text( + text = docId, + textAlign = TextAlign.Start, + color = Color.Blue, + modifier = Modifier + .clickable { + showMenu = true } - else -> false - } - }) + ) + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + docsList?.forEachIndexed { index, item -> + DropdownMenuItem(onClick = { + selectedIndex = index + viewModel.selectedDoc.value = item + showMenu = false + }, text = { + Text(text = item.id) + }) + } } } } @@ -119,7 +147,7 @@ fun Documents(collectionName: String, isStandAlone: Boolean) { Text( text = "No Docs", textAlign = TextAlign.Start, - color = Color.Blue, + color = MaterialTheme.colorScheme.primary ) } } diff --git a/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/DocumentsViewModel.kt b/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/DocumentsViewModel.kt index 34725bb..2089d71 100644 --- a/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/DocumentsViewModel.kt +++ b/DittoToolsAndroid/src/main/java/live/ditto/tools/databrowser/DocumentsViewModel.kt @@ -9,11 +9,17 @@ class DocumentsViewModel(private val collectionName: String, isStandAlone: Boole val docsList: MutableLiveData> = MutableLiveData>(mutableListOf()) var docProperties: MutableLiveData> = MutableLiveData(emptyList()) var selectedDoc = MutableLiveData() + var errorMessage = MutableLiveData() + + // Store all documents for client-side filtering + private var allDocuments: MutableList = mutableListOf() + private var currentFilter: String = "" + private var isDQLMode: Boolean = false val subscription = if (isStandAlone) DittoHandler.ditto.store.collection(collectionName).findAll().limit(1000).subscribe() else null private var liveQuery = DittoHandler.ditto.store.collection(collectionName).findAll().limit(1000).observeLocal { docs, _ -> - docsList.value?.clear() + val newDocsList = mutableListOf() for(doc in docs) { this.docProperties.postValue(doc.value.keys.map{it}.sorted()) @@ -21,13 +27,15 @@ class DocumentsViewModel(private val collectionName: String, isStandAlone: Boole for((key, value) in doc.value) { docValues[key] = value } - docsList.value?.add(Document(doc.id.toString(), docValues)) + newDocsList.add(Document(doc.id.toString(), docValues)) } + allDocuments = newDocsList + applyFilter() } private fun findAllLiveQuery() { this.liveQuery = DittoHandler.ditto.store.collection(collectionName).findAll().limit(1000).observeLocal { docs, _ -> - docsList.value?.clear() + val newDocsList = mutableListOf() for(doc in docs) { this.docProperties.postValue(doc.value.keys.map{it}.sorted()) @@ -35,36 +43,75 @@ class DocumentsViewModel(private val collectionName: String, isStandAlone: Boole for((key, value) in doc.value) { docValues[key] = value } - docsList.value?.add(Document(doc.id.toString(), docValues)) + newDocsList.add(Document(doc.id.toString(), docValues)) } + allDocuments = newDocsList + applyFilter() } } private fun findWithFilterLiveQuery(queryString: String) { - this.liveQuery = DittoHandler.ditto.store.collection(collectionName).find(queryString).limit(1000).observeLocal { docs, _ -> - docsList.value?.clear() + try { + errorMessage.postValue(null) + this.liveQuery = DittoHandler.ditto.store.collection(collectionName).find(queryString).limit(1000).observeLocal { docs, _ -> + val newDocsList = mutableListOf() - for(doc in docs) { - this.docProperties.postValue(doc.value.keys.map{it}.sorted()) + for(doc in docs) { + this.docProperties.postValue(doc.value.keys.map{it}.sorted()) - val docValues = mutableMapOf() - for((key, value) in doc.value) { - docValues[key] = value + val docValues = mutableMapOf() + for((key, value) in doc.value) { + docValues[key] = value + } + newDocsList.add(Document(doc.id.toString(), docValues)) } - docsList.value?.add(Document(doc.id.toString(), docValues)) + docsList.postValue(newDocsList) } + } catch (e: Exception) { + errorMessage.postValue("Invalid DQL query: ${e.message}") + docsList.postValue(mutableListOf()) } } fun filterDocs(queryString: String) { - liveQuery.close() + currentFilter = queryString - if(queryString.isEmpty()) { - findAllLiveQuery() - } - else { + if (isDQLQuery(queryString)) { + // User provided explicit DQL query - use server-side filtering + liveQuery.close() + isDQLMode = true findWithFilterLiveQuery(queryString) + } else { + // Simple text search - use client-side filtering + if (isDQLMode) { + // Switching from DQL mode back to simple search + // Need to restart the findAll query + liveQuery.close() + isDQLMode = false + findAllLiveQuery() + } else { + // Already in simple mode, just filter the existing data + applyFilter() + } + } + } + + private fun applyFilter() { + val filtered = if (currentFilter.isEmpty()) { + allDocuments + } else { + // Filter documents where ID contains the search text (case-insensitive) + allDocuments.filter { doc -> + doc.id.contains(currentFilter, ignoreCase = true) + }.toMutableList() } + docsList.postValue(filtered) + } + + private fun isDQLQuery(text: String): Boolean { + // Check if the text contains DQL operators + val dqlOperators = listOf("==", "!=", "CONTAINS", "contains", ">", "<", ">=", "<=", "AND", "and", "OR", "or", "IN", "in") + return dqlOperators.any { text.contains(it) } } class MyViewModelFactory(private val collectionName: String, private val isStandAlone: Boolean) :