Skip to content

*Don't* Remove All

Compare
Choose a tag to compare
@mergesort mergesort released this 31 Jan 21:32
· 11 commits to main since this release
f48a087

Important

This release contains a crucial fix, please update your library.

This release fixes an bug in Boutique that could lead to data-loss in specific circumstances when chaining .remove() and .insert() using Boutique.

Boutique was exhibiting incorrect behavior when chaining the remove() function with an insert() after, due to an underlying implementation bug. The code below demonstrates how the bug would manifest.

// We start with self.items = [1, 2, 3, 4, 5, 6, 7, 8]

// An API call is made and we receive [1, 2, 3, 8, 9, 10] to be inserted into to self.items.
// We pass that `updatedItems` array into an `update` function that removes any items that need to be removed, and then inserts the newly updated items.

func update(_ updatedItems: [Int]) async throws {
    let items = self.items.filter({ updatedItems.contains($0) })

    try await self.$items
        .remove(items)
        .insert(updatedItems)
        .run()
}

// `self.items` now should be [1, 2, 3, 4, 5, 6, 7, 8] 
// `self.items` is actually [10] 

There was an assumption built into how chained operations work, based on how Boutique was being used in the early days of the library.

Internally Boutique has two ItemRemovalStrategy properties, .removeAll which removes all the items by deleting the underlying table, and removeItems(items) to remove a specific set of items. Unfortunately due to a logic error .removeAll would be called whenever the amount of items to remove matched the amount of items that were being inserted in a chain, which is not always the developer's intention. That would delete the underlying data and insert the last item, leaving users with only one item.

My sincerest apologies for this bug, and since this pattern is not necessarily common I hope that it has not affected many users.