0

So, I've got a database collection thing I am working on. The main view is just a list of all the stuff in the database, and I'm currently trying to add optional filtering by certain parameters. I have a get all items function that I had been using to run the app to test the filtered items query in the database inspector because for a while, I kept getting SQL syntax errors while trying to make the function and that would cause the app to crash upon startup (the start view has this list). Well I finally got the list to generate using the filtered items function, but now selecting filters doesn't change the list (I even forced recompositions). I've got logging to check some things, so I know selecting a filter does properly update the filter state flow (though the view model that controls the list isn't grabbing those emitted values, but that's another problem for another time). I'm not sure where the breakdown is.

The desired behavior is that by default, no filters are selected and the entire list is returned if all filter options are set to null. There are 5 different values by which it can be filtered, 3 of them are booleans (favorites, dislikes, out of stock) and two are string lists (brand, type). The desired filtering behavior also includes the ability to select 1 or more of brand and/or type, and any kind of combination of these filters (and ignoring any other non-selected filters). For example, filter by Brand A and Brand B in favorites, or all Type A in dislikes, or all Brand A, types B and C. And taking the last example further, selecting Brand A as a filter and types B and C should return all items that fit Brand A AND Type B AND Type C, regardless of whether they are or are not in favorites, dislikes or out of stock.

The filtering is controled in a shared view model (I plan to use filtering on 2 different screens) which is scoped such that it's persistent through screen changes. I have confirmed this filtering view model does initialize first and persists and updates correctly, I've even confirmed that the selected filters get passed to the home view model where the list is and passed into the function (though again, have to force a reinitializing of the home viewmodel and recomp home screen), but the filters aren't working. The entire list is returned regardless of options.

I've got a Dao set up with an ItemsRepository and an OfflineItemsRepository (overrides ItemsRepository), and I'm not sure if the values are getting passed to the query properly. Again though, I know the query works as intended through database inspection (and many different filtering scenarios combining various values and leaving various fields blank).

Also, side note, I just realized now (as I am posting this and then going to bed), that I should probably change the outOfStock part, the filter selection is a boolean but I'm not sure if the query is right for checking that quantity = 0 is the true state and any other quantity is false... I have to think about that tomorrow, I don't remember now if I properly tested that part in the database inspector... it's been a long week of fighting this (and fighting a janky modal bottom sheet).

I can provide other code if needed, but this is the setup:

ItemsDao (in database inspector testing, this didn't work at first until I cast the booleans as integers; brand, type, favorite, disliked, quantity are the database entity values):

    @Query("""
        SELECT * FROM items WHERE
            (:brands IS NULL OR :brands = '' OR brand IN (:brands))
            AND (:types IS NULL OR :types = '' OR type IN (:types))
            AND (:favorites IS NULL OR favorite = CAST(:favorites AS INTEGER))
            AND (:dislikeds IS NULL OR disliked = CAST(:dislikeds AS INTEGER))
            AND (:outOfStock IS NULL OR quantity = 0)
        """)
    fun getFilteredItems(
        brands: List<String>?,
        types: List<String>?,
        favorites: Boolean?,
        dislikeds: Boolean?,
        outOfStock: Boolean?
    ): Flow<List<Items>>

ItemsRepository:

    fun getFilteredItems(
        brands: List<String>?,
        types: List<String>?,
        favorites: Boolean?,
        dislikeds: Boolean?,
        outOfStock: Boolean?,
    ): Flow<List<Items>>

OfflineItemsRepository (I had several attempts at the itemsDao.getFilteredItems parameters here, it was only by setting them all to null that I quit getting the SQL syntax error, not sure if this is right):

    override fun getFilteredItems(
        brands: List<String>?,
        types: List<String>?,
        favorites: Boolean?,
        dislikeds: Boolean?,
        outOfStock: Boolean?,
    ): Flow<List<Items>> =
        itemsDao.getFilteredItems(
            null,
            null,
            null,
            null,
            null,
        )

HomeViewModel (homeUiState controls the list):

    val homeUiState: StateFlow<HomeUiState> =
        combine(
            itemsRepository.getFilteredItems(
                brands = filterViewModel.selectedBrands.value,
                types = filterViewModel.selectedTypes.value,
                favorites = filterViewModel.selectedFavorites.value,
                dislikeds = filterViewModel.selectedDislikeds.value,
                outOfStock = filterViewModel.selectedOutOfStock.value
            ),
//            itemsRepository.getAllItemsStream(),
            preferencesRepo.isTableView,
        ) { items, isTableView ->
            Log.d("HomeViewModel", "disliked items: ${filterViewModel.selectedDislikeds.value}")
            HomeUiState(items, isTableView) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
            initialValue = HomeUiState(isLoading = true)
        )

FilterViewModel stuff, in case it's relevant:

    private val _selectedBrands = MutableStateFlow<List<String>>(emptyList())
    val selectedBrands: StateFlow<List<String>> = _selectedBrands

    private val _selectedTypes = MutableStateFlow<List<String>>(emptyList())
    val selectedTypes: StateFlow<List<String>> = _selectedTypes

    private val _selectedFavorites = MutableStateFlow(false)
    val selectedFavorites: StateFlow<Boolean> = _selectedFavorites

    private val _selectedDislikeds = MutableStateFlow(false)
    val selectedDislikeds: StateFlow<Boolean> = _selectedDislikeds

    private val _selectedOutOfStock = MutableStateFlow(false)
    val selectedOutOfStock: StateFlow<Boolean> = _selectedOutOfStock

    init {
        Log.d("FilterViewModel", "Initial selectedBrands: ${_selectedBrands.value}")
        Log.d("FilterViewModel", "Initial selectedTypes: ${_selectedTypes.value}")
        Log.d("FilterViewModel", "Initial selectedFavorites: ${_selectedFavorites.value}")
        Log.d("FilterViewModel", "Initial selectedDislikeds: ${_selectedDislikeds.value}")
        Log.d("FilterViewModel", "Initial selectedOutOfStock: ${_selectedOutOfStock.value}")
    }

    // Filtering update functions //
    fun updateSelectedBrands(brand: String, isSelected: Boolean) {
        if (isSelected) { _selectedBrands.value = listOf(brand) }
        else { _selectedBrands.value -= brand }
        Log.d("FilterViewModel", "Selected brands: ${_selectedBrands.value}")
    }

    fun updateSelectedTypes(type: String, isSelected: Boolean) {
        if (isSelected) { _selectedTypes.value += type }
        else { _selectedTypes.value -= type }
        Log.d("FilterViewModel", "Selected types: ${_selectedTypes.value}")
    }

    fun updateSelectedFavorites(isSelected: Boolean) {
        _selectedFavorites.value = isSelected
        Log.d("FilterViewModel", "Selected favorites: ${_selectedFavorites.value}")
    }

    fun updateSelectedDislikeds(isSelected: Boolean) {
        _selectedDislikeds.value = isSelected
        Log.d("FilterViewModel", "Selected dislikeds: ${_selectedDislikeds.value}")
    }

    fun updateSelectedOutOfStock(isSelected: Boolean) {
        _selectedOutOfStock.value = isSelected
        Log.d("FilterViewModel", "Selected out of stock: ${_selectedOutOfStock.value}")
    }

To me, it seems like having "null" in the OfflineItemsRepository function is hardcoding "null" rather than taking the values from the FilterViewModel, and that's why it's not working. It makes sense to me that the function should be like all my other override functions:

    override fun getFilteredItems(
        brands: List<String>?,
        types: List<String>?,
        favorites: Boolean?,
        dislikeds: Boolean?,
        outOfStock: Boolean?,
    ): Flow<List<Items>> =
        itemsDao.getFilteredItems(
            brands,
            types,
            favorites,
            dislikeds,
            outOfStock,
        )

But if I put anything other than "null" as a hardcoded value there, I get an SQL syntax error:

android.database.sqlite.SQLiteException: near "IS": syntax error (code 1 SQLITE_ERROR): , while compiling: SELECT * FROM items WHERE
( IS NULL OR  = '' OR brand IN ())
AND ( IS NULL OR  = '' OR type IN ())
AND (? IS NULL OR favorite = CAST(? AS INTEGER))
AND (? IS NULL OR disliked = CAST(? AS INTEGER))
AND (? IS NULL OR quantity = 0)

I noticed in the above error, the values for the two parameters that are lists are just blank spaces (whereas the booleans are "?"), and I'm not sure if that's how that should be. So maybe the Query or function is not setup correctly to work together, though again, the Query works fine on it's own in the database inspector.


EDIT:

I was digging around and looking at the build implementation and it looks like, while the query worked in inspector, it wasn't handling list parameters in the build code implementation (from what I could tell). I've narrowed the issue down to specifically being caused by the two list inputs/parameters.

Looking around, I've been leaning towards a raw query with query builder, but I can't for the life of me figure that out at all. But I do know the problem specifically is the lists and it's breaking the query and not being compiled correctly when the app is built.

0

2 Answers 2

0

A problem you have is how Room handles a list when passed to query. A List will be converted to a ? per list item (with a comma separating each ?). This is suitable for an IN(....).

e.g. it could be OR brand IN (?,?,?,?,?,?). However, the full SQL for the brand part of the WHERE clause would be WHERE (?,?,?,?,?,? IS NULL OR ?,?,?,?,?,? = '' OR brand IN (?,?,?,?,?,?))

i.e. the left of each comparison is comprised of multiple values separated by commas which would result in an SQL syntax error. That is IN is comparing/testing multiple values whilst IS NULL/= compares a single value.

What you could do is to inspect the list and then decide what query. Consider the simple (just handling the brand list) with the additional DAO annotated functions:-

@Query("SELECT  * FROM items")
fun getAllItems(): List<Items>
@Query("SELECT * FROM items WHERE brand IN (:brands)")
fun getFilteredItemsIfBrandInList(brands: List<String>): List<Items>

@Query("")
fun getFilteredItemsV2(
    brands: List<String>?
): List<Items> {
    if (brands==null || brands.isEmpty()) {
        return getAllItems()
    } else
        return getFilteredItemsIfBrandInList(brands)
}
  • the first used when there are no brands (null or empty list)
  • the second used when there are brands and thus uses the IN clause.
  • the third is the function that determines which query is to be invoked.
  • Note this is just one way and could get quite complex in that the final solution could invoke one of many queries

Demonstration note main thread used for brevity

First the Items class used (based upon what can be gleaned from the question):-

@Entity
data class Items(
    @PrimaryKey
    val brand: String,
    val type: String,
    val favorite: Boolean,
    val disliked: Boolean,
    val quantity: Int
)

A single @Dao annotated interface (with functions sufficient for the demo):-

@Dao
interface AllDAOs {
    @Insert
    fun insert(item: Items): Long
    @Update
    fun update(item: Items): Int

    @Query("""
        SELECT * FROM items WHERE
            (:brands IS NULL OR :brands = '' OR brand IN (:brands))
            AND (:types IS NULL OR :types = '' OR type IN (:types))
            AND (:favorites IS NULL OR favorite = CAST(:favorites AS INTEGER))
            AND (:dislikeds IS NULL OR disliked = CAST(:dislikeds AS INTEGER))
            AND (:outOfStock IS NULL OR quantity = 0)
        """)
    fun getFilteredItems(
        brands: List<String>?,
        types: List<String>?,
        favorites: Boolean?,
        dislikeds: Boolean?,
        outOfStock: Boolean?
    ): List<Items>

    @Query("SELECT  * FROM items")
    fun getAllItems(): List<Items>
    @Query("SELECT * FROM items WHERE brand IN (:brands)")
    fun getFilteredItemsIfBrandInList(brands: List<String>): List<Items>

    @Query("")
    fun getFilteredItemsV2(
        brands: List<String>?
    ): List<Items> {
        if (brands==null || brands.isEmpty()) {
            return getAllItems()
        } else
            return getFilteredItemsIfBrandInList(brands)
    }
}

an @Database annotated abstract class so the demo can run:-

@Database(entities = [Items::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDAOs(): AllDAOs
    companion object {

        private var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context, TheDatabase::class.java, "the_db.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

Finally a basic activity that will add some data and then attempt to extract data via the 2 filter functions (wrapped in try/catch so the activity can continue after exceptions):-

class MainActivity : AppCompatActivity() {
    val TAG = "DBINFO"
    lateinit var db: TheDatabase
    lateinit var dao: AllDAOs
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        db = TheDatabase.getInstance(this)
        dao = db.getAllDAOs()

        for(i in 1..10) {
            dao.insert(Items("BRAND${i}","TYPE${i*100}", i/2>1,i/2<1,i))
        }
        doVariousFilters()

    }

    fun doVariousFilters() {
        val brandList = listOf<String>("X","Y","Z","BRAND1","BRAND01","BRAND3")
        val typeList = listOf<String>("X","Y","Z","TYPE100","TYPE111","TYPE0100","TYPE300")
        try {
            logItems(dao.getFilteredItems(null, null, null, null, null), "STAGE1")
            logItems(dao.getFilteredItems(brandList, null, null, null, null), "STAGE2")
            logItems(dao.getFilteredItems(brandList, typeList, null, null, null), "STAGE3")
            logItems(dao.getFilteredItems(brandList, typeList, true, true, true), "STAGE4")
        } catch (e: SQLiteException) {
            Log.d("${TAG}_OOOP","Capture Exception\n\t${e.message}")
        }

        try {
            logItems(dao.getFilteredItemsV2(null),"STAGE10")
            logItems(dao.getFilteredItemsV2(brandList),"STAGE11")
        } catch (e: SQLiteException) {
            Log.d("${TAG}_OUCH","Captured Exception\n\t${e.message}")
        }
    }

    fun logItems(l: List<Items>, tagSuffix: String) {
        for (i in l) {
            Log.d("${TAG}_${tagSuffix}","Item is ${i.brand} Type is ${i.type} Fav is ${i.favorite} Dis is ${i.disliked} QTY is ${i.quantity}")
        }
    }
}

So first some data is added which AppInspection shows as:-

Items table data

Then attempts are made to access the data via the query in the question and then the 2nd version of the query resulting in the log showing:-

2024-08-31 12:57:20.993 D/DBINFO_STAGE1: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 12:57:20.993 D/DBINFO_STAGE1: Item is BRAND2 Type is TYPE200 Fav is false Dis is false QTY is 2
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND4 Type is TYPE400 Fav is true Dis is false QTY is 4
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND5 Type is TYPE500 Fav is true Dis is false QTY is 5
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND6 Type is TYPE600 Fav is true Dis is false QTY is 6
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND7 Type is TYPE700 Fav is true Dis is false QTY is 7
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND8 Type is TYPE800 Fav is true Dis is false QTY is 8
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND9 Type is TYPE900 Fav is true Dis is false QTY is 9
2024-08-31 12:57:20.994 D/DBINFO_STAGE1: Item is BRAND10 Type is TYPE1000 Fav is true Dis is false QTY is 10
2024-08-31 12:57:20.996 D/DBINFO_OOOP: Capture Exception
        row value misused (code 1 SQLITE_ERROR): , while compiling: SELECT * FROM items WHERE
                (?,?,?,?,?,? IS NULL OR ?,?,?,?,?,? = '' OR brand IN (?,?,?,?,?,?))
                AND (? IS NULL OR ? = '' OR type IN (?))
                AND (? IS NULL OR favorite = CAST(? AS INTEGER))
                AND (? IS NULL OR disliked = CAST(? AS INTEGER))
                AND (? IS NULL OR quantity = 0)




2024-08-31 12:57:20.997 D/DBINFO_STAGE10: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND2 Type is TYPE200 Fav is false Dis is false QTY is 2
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND4 Type is TYPE400 Fav is true Dis is false QTY is 4
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND5 Type is TYPE500 Fav is true Dis is false QTY is 5
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND6 Type is TYPE600 Fav is true Dis is false QTY is 6
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND7 Type is TYPE700 Fav is true Dis is false QTY is 7
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND8 Type is TYPE800 Fav is true Dis is false QTY is 8
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND9 Type is TYPE900 Fav is true Dis is false QTY is 9
2024-08-31 12:57:20.998 D/DBINFO_STAGE10: Item is BRAND10 Type is TYPE1000 Fav is true Dis is false QTY is 10
2024-08-31 12:57:21.000 D/DBINFO_STAGE11: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 12:57:21.000 D/DBINFO_STAGE11: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3

As can be seen the original query fails at the second usage as explained. The v2 query however copes with the list as the getFilteredItemsIfBrandInList is invoked (i.e. STAGE11 shows the expected 2 rows i.e BRAND1 and BRAND3 matched the provided list of X, Y , Z, BRAND1, BRAND01 and BRAND3)

Alternative approach

An alternative approach, could be to pass a list as 2 values per list. The actual list and hence the question marks and additionally the list as a single value to determine if there are list items or not. For example (as one of many ways) consider:-

@Query("SELECT * FROM items WHERE length(coalesce(:brandlistassinglestring,'')) < 1 OR brand IN(:brands)")
fun getFilteredItemsV3(
    brandlistassinglestring: String,
    brands: List<String>?,
): List<Items>
@Query("")
fun getFilteredV3(
    brands: List<String>?,
    types: List<String>?,
    favorites: Boolean?,
    dislikeds: Boolean?,
    outOfStock: Boolean?
): List<Items> {
    var sb = StringBuilder()
    if (!brands.isNullOrEmpty()) {
        sb.append("somebrandswerefound_so_letthisstringcontaonsomething")
    }
    return getFilteredItemsV3(sb.toString(),brands)
}
  • again, only considering brands for brevity.

  • so if the list of brands is empty then and empty string along with the empty list is passed to the query. However, if the list is not empty, then a non empty string AND the actual list is passed to the query thus the comparison (as opposed to the IN) is a single value (which makes the comparison false i.e. length is 1 or more).

  • you may wish to refer to https://www.sqlite.org/lang_corefunc.html#coalesce and/or https://www.sqlite.org/lang_corefunc.html#length

Applying the above to the demo, as per:-

logItems(dao.getFilteredV3(null,null,null,null,null),"STAGE20")
            logItems(dao.getFilteredV3(brandList,null,null,null,null),"STAGE21")

Results in the log then also containing:-

2024-08-31 13:29:18.519 D/DBINFO_STAGE20: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 13:29:18.519 D/DBINFO_STAGE20: Item is BRAND2 Type is TYPE200 Fav is false Dis is false QTY is 2
2024-08-31 13:29:18.519 D/DBINFO_STAGE20: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 13:29:18.519 D/DBINFO_STAGE20: Item is BRAND4 Type is TYPE400 Fav is true Dis is false QTY is 4
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND5 Type is TYPE500 Fav is true Dis is false QTY is 5
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND6 Type is TYPE600 Fav is true Dis is false QTY is 6
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND7 Type is TYPE700 Fav is true Dis is false QTY is 7
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND8 Type is TYPE800 Fav is true Dis is false QTY is 8
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND9 Type is TYPE900 Fav is true Dis is false QTY is 9
2024-08-31 13:29:18.520 D/DBINFO_STAGE20: Item is BRAND10 Type is TYPE1000 Fav is true Dis is false QTY is 10
2024-08-31 13:29:18.521 D/DBINFO_STAGE21: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 13:29:18.521 D/DBINFO_STAGE21: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
  • i.e. same results all and then filtered as per expectations

EDIT Re edited question and comments

Perhaps consider the following: which with limited testing appears to work:-

@Query(
    "SELECT * FROM items " +
        "WHERE (NOT length(coalesce(:brandSV,'')) OR brand IN(:brands)) "  +

            " AND  (NOT length(coalesce(:typeSV,'')) OR type IN(:types)) " +
            " AND (:favorites IS NULL OR favorite = CAST(:favorites AS INTEGER)) " +
            " AND (:dislikeds IS NULL OR disliked = CAST(:dislikeds AS INTEGER)) " +
            " AND (:outOfStock IS NULL OR quantity = 0) "

)
fun baseQuery(brandSV: String, brands: List<String>?, typeSV: String, types: List<String>?, favorites: Boolean?, dislikeds: Boolean?, outOfStock: Boolean?): List<Items>
fun baseFeeder(brands: List<String>?, types: List<String>?, favorites: Boolean?, dislikeds: Boolean?, outOfStock: Boolean?): List<Items> {
    val brandSV = StringBuilder()
    if (!brands.isNullOrEmpty()) brandSV.append("x")
    val typeSV = StringBuilder()
    if (!types.isNullOrEmpty()) typeSV.append("x")
    return baseQuery(brandSV.toString(),brands,typeSV.toString(),types,favorites,dislikeds,outOfStock)
}
  • of course change function names to suit.

The limited testing undertaken based upon the added activity code function :-

fun doFullFiltering() {
    val brandList = listOf<String>("X","Y","Z","BRAND1","BRAND01","BRAND3")
    val typeList = listOf<String>("X","Y","Z","TYPE100","TYPE111","TYPE0100","TYPE300")
    logItems(dao.baseFeeder(null,null,null,null,null),"STAGE30")
    logItems(dao.baseFeeder(brandList,null,null,null,null),"STAGE31")
    logItems(dao.baseFeeder(null,typeList,null,null,null),"STAGE32")
    logItems(dao.baseFeeder(brandList,typeList,null,null,null),"STAGE33")
    logItems(dao.baseFeeder(null,null,true,null,null),"STAGE34")
    logItems(dao.baseFeeder(null,null,null,true,null),"STAGE35")
    logItems(dao.baseFeeder(null,null,null,null,true),"STAGE36")
    logItems(dao.baseFeeder(brandList,typeList,true,true,true),"STAGE37")

}

Calling it using:-

    //doVariousFilters()
    doFullFiltering()

Results in:-

2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND2 Type is TYPE200 Fav is false Dis is false QTY is 2
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND4 Type is TYPE400 Fav is true Dis is false QTY is 4
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND5 Type is TYPE500 Fav is true Dis is false QTY is 5
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND6 Type is TYPE600 Fav is true Dis is false QTY is 6
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND7 Type is TYPE700 Fav is true Dis is false QTY is 7
2024-08-31 17:58:10.736 D/DBINFO_STAGE30: Item is BRAND8 Type is TYPE800 Fav is true Dis is false QTY is 8
2024-08-31 17:58:10.737 D/DBINFO_STAGE30: Item is BRAND9 Type is TYPE900 Fav is true Dis is false QTY is 9
2024-08-31 17:58:10.737 D/DBINFO_STAGE30: Item is BRAND10 Type is TYPE1000 Fav is true Dis is false QTY is 10
2024-08-31 17:58:10.739 D/DBINFO_STAGE31: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 17:58:10.739 D/DBINFO_STAGE31: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 17:58:10.742 D/DBINFO_STAGE32: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 17:58:10.743 D/DBINFO_STAGE32: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 17:58:10.746 D/DBINFO_STAGE33: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
2024-08-31 17:58:10.746 D/DBINFO_STAGE33: Item is BRAND3 Type is TYPE300 Fav is false Dis is false QTY is 3
2024-08-31 17:58:10.749 D/DBINFO_STAGE34: Item is BRAND4 Type is TYPE400 Fav is true Dis is false QTY is 4
2024-08-31 17:58:10.749 D/DBINFO_STAGE34: Item is BRAND5 Type is TYPE500 Fav is true Dis is false QTY is 5
2024-08-31 17:58:10.749 D/DBINFO_STAGE34: Item is BRAND6 Type is TYPE600 Fav is true Dis is false QTY is 6
2024-08-31 17:58:10.749 D/DBINFO_STAGE34: Item is BRAND7 Type is TYPE700 Fav is true Dis is false QTY is 7
2024-08-31 17:58:10.749 D/DBINFO_STAGE34: Item is BRAND8 Type is TYPE800 Fav is true Dis is false QTY is 8
2024-08-31 17:58:10.750 D/DBINFO_STAGE34: Item is BRAND9 Type is TYPE900 Fav is true Dis is false QTY is 9
2024-08-31 17:58:10.750 D/DBINFO_STAGE34: Item is BRAND10 Type is TYPE1000 Fav is true Dis is false QTY is 10
2024-08-31 17:58:10.752 D/DBINFO_STAGE35: Item is BRAND1 Type is TYPE100 Fav is false Dis is true QTY is 1
  • note the output has only really been glanced at BUT none of the various permutations of parameters result in an exception and as can be seen the first 6 permutations returned results
    • the 7th (and I believe also the 8th) returned nothing as the quantity in all rows is greater than 0.
Sign up to request clarification or add additional context in comments.

4 Comments

That seems like, to get all the possible filtering combinations of values, like a massive boatload of code if I am following this correctly? Also, does this rely on known set valuea for brands? Because there's hundreds, and not everyone has the same ones. Anyway, as I said, my query worked just fine in Database Inspector, I am leaning more towards a raw query and string builder, as the problem seems to be the compiled build code isn't handling list inputs right .
Side-note, I'm no developer/programmer, rudimentary understanding only, I'm just mostly making this app for myself, but if other people that collect the same thing want it, I'll give it to them too (for free). This is partly for fun (or it was until I found out how broken Compose was, but that's the free course that was there so... ugh). I've largely relied on adapting free code samples and manually digging through the code libary hunting for things that I don't know what they're called and just thowing it at the wall until something sticks. Most of what you said is way over my head.
@sardonicus87 you say * I am leaning more towards a raw query and string builder, as the problem seems to be the compiled build code isn't handling list inputs right*. Raw query could be the way but the SQL isn't checked at compile time. Issue with DB Inspector, is that passed values would need to be provided which may lead to the values not being what would be passed. Lists would/should only really be used in IN clause (some other potential obscure uses) so Room handles them conveniently for typical usage.
@sardonicus87 not really massive and really same number of permutations if using raw query (but perhaps easier to do with raw). You will still be testing, at some stage, if list then IN otherwise ALL.
0

Well, I dug around and found a solution where someone fixed a list parameter issue using a string builder, so I went with RawQuery and included a string builder within the query builder and it seems to work in all testing so far.

The string builder just builds a string that appends a "?" for each brand I select to the where clause, so by the end the query builder builds the query by adding the string builder "brands" string which results in ?, ?, ? IS null OR ?, ?, ? IS '' ... IN brand (assuming I selected 3 options to put in the list) instead of IS null OR IS ''..., as was happening before, causing the syntax error when running in-app. In other words, the string builder builds the string used for the where clause for brands, and another for types.

A summary of what happens:

  • The query runs like a SELECT * FROM items if no filters are selected (filter options in query builder are only added at the end by if (whereClauses.isNotEmpty()) {).
  • Whenever any filter is selected, the query builder appends the query with a WHERE clause, but only if a filter is selected (if ([filterVal] != null) {).
  • Each filter just builds it's own line as if it were the only WHERE clause which gets put into a list of claues (val whereClauses = mutableListOf<String>()) so that if more than one filter is selected, the query builder automatically writes the list (at the end) with the necessary " AND " separator between clauses, but only if the "whereClauses" is not empty.
  • Arguments for the where clauses are also stored in a list which would be each brand, type, and a true value for booleans IF they are selected (no arguments passed and no generation done for those if boolean filter is not selected, to prevent displaying all items except those that are true for that value when the filter value is not selected), and the arguments are generated with the particular sections in which they fall, so they automatically map to the correct built "?" in the query.
  • The first two are lists and a string builder builds the string of that part of the where clause by what is selected, adding a "?" for each selection and appending ", " as long as it's not the last item in the list (if (i < types.size - 1) { typeClause.append(", ") }).
  • The query builder then generates the query with the correct format as if it were a specific query (example SELECT * FROM items WHERE ?, ?, ? IN brand) and then passes in the arguments from the list.

The solution I came up with (ItemsRepository stays the same):

ItemsDao:

@RawQuery(observedEntities = [Items::class])
    fun getFilteredItems(query: SupportSQLiteQuery): Flow<List<Items>>

OfflineItemsRepository:

override fun getFilteredItems(
    brands: List<String>?,
    types: List<String>?,
    favorites: Boolean?,
    dislikeds: Boolean?,
    outOfStock: Boolean?,
): Flow<List<Items>> {
    val queryBuilder = SupportSQLiteQueryBuilder.builder("items")
    val args = mutableListOf<Any>()
    val whereClauses = mutableListOf<String>()

    if (!brands.isNullOrEmpty()) {
        val brandClause = StringBuilder()
        brandClause.append("brand IN (")
        for (i in brands.indices) {
            brandClause.append("?")
            if (i < brands.size - 1) {
                brandClause.append(", ")
            }
            args.add(brands[i])
        }
        brandClause.append(")")
        whereClauses.add(brandClause.toString())
    }

    if (!types.isNullOrEmpty()) {
        val typeClause = StringBuilder()
        typeClause.append("type IN (")
        for (i in types.indices) {
            typeClause.append("?")
            if (i < types.size - 1) {
                typeClause.append(", ")
            }
            args.add(types[i])
        }
        typeClause.append(")")
        whereClauses.add(typeClause.toString())
    }

    if (favorites != null) {
        if (favorites) {
            whereClauses.add("favorite = ?")
            args.add(1)
        }
    }

    if (dislikeds != null) {
        if (dislikeds) {
            whereClauses.add("disliked = ?")
            args.add(1)
        }
    }

    if (outOfStock != null) {
        if (outOfStock) {
            whereClauses.add("quantity = 0")
        }
    }

    if (whereClauses.isNotEmpty()) {
        val whereClause = whereClauses.joinToString(" AND ")
        queryBuilder.selection(whereClause, args.toTypedArray())
    }

    val query = queryBuilder.create()

    // Log the query details
    Log.d("OfflineItemsRepository", "Query: ${query.sql}")
    for (i in 0 until query.argCount) {
        Log.d("OfflineItemsRepository", "Arg $i: ${args[i]}")
    }

    return itemsDao.getFilteredItems(query)
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.