Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the use of database #55

Closed
monsieurtanuki opened this issue Dec 28, 2020 · 22 comments · Fixed by #62
Closed

Refactor the use of database #55

monsieurtanuki opened this issue Dec 28, 2020 · 22 comments · Fixed by #62

Comments

@monsieurtanuki
Copy link
Contributor

For the moment, user preferences are stored in SharedPreferences, which is appropriate, and products are stored in a Sembast database in full_products_database.dart.

I'm not 100% convinced by Sembast, my personal taste is rather SQLite (with a bit of JSON in it), but this can be challenged, of course.

My concern is rather what we do with the database.
For the moment, not that much: we populate the database when we download products, but we never query the database.

I think it would be interesting to actually use the database:

  • being able, every time we store a product in it, to know when it happened and in which context (barcode, keywords, category search)
  • with that, we could implement the use of cached data - I downloaded category "breakfast cereals" yesterday, I don't need to download it again today - with the user being able to say "I want a systematic refresh, a refresh when the data is considered stale, let me decide each time" (with parameters in the preferences) (possibly with an AlertDialog added AlertDialog following the "smooth" design guidelines #51 "we already have this data, do you want to reuse it or refresh it?")
@stephanegigandet
Copy link
Contributor

There are 2 distinct uses:

  • using the database ase a cache for the product information (that we get from the API on a per product basis, or that we could get from search queries as well, or that we (or the user) could prepopulate). For this storing JSON works very well, we can cache the product data + attributes values.

  • storing lists of products (the most basic one being a scan history, or a "favorites" list). Here SQLite is perfect, as we just store barcodes + context. Then we can query the Sembast cache for the actual product data. We can have tables for the result (we have 5 instances of product barcode 12345678 with different best before dates), as well as table for transactions (I have eaten 12 times this product in the last month). We have a new project for that we will be starting in the next few weeks. It won't be limited to the app though, we'll need to see how we can sync things between the app, the server, different users etc.)

@stephanegigandet
Copy link
Contributor

Regarding the database, the technical document that @PrimaelQuemerais has written for the app has a large section on why we need one and what we want to do with it: https://docs.google.com/document/d/1dOryz657AaAC7feaVMWvzdxB8RQ1Ze8O7PEk1QiJUww/edit#heading=h.u53wgh648h3k

@monsieurtanuki
Copy link
Contributor Author

Disclaimer: I'm a former dev Oracle DBA, and I spent years creating, loading and querying relational databases with optimization in mind. It doesn't mean I know everything and I'm always right, it means I can be trusted regarding relational database (e.g. SQLite). I don't know that much about NoSQL.

A typical use of the full product database is to get the product attached to a barcode (or products attached to barcodes).

It's a normal use of a relational database, whose core is the "primary key" (a unique and non null value), and which is optimized for that (through the transparent use of "unique index").
A similar behavior can be found in Sembast, with a syntax like await store.record(barcode).put(db, product.toJson()); and final Map<String, dynamic> jsonProduct = await store.record(barcode).get(database) as Map<String, dynamic>;.

The good thing about Sembast is that it supports Map<String, dynamic> as a variable type, and we are already comfortable with JSON (as we get JSON data from internet, and we will store the same data in the database). In SQLite, basically we would have to encode/decode the JSON to/from String.

An additional good thing about Sembast is that, as it stores JSON, we may be able to run queries like that:

await store.find(database,
    finder: Finder(
        filter: Filter.and([
      Filter.matches('product_name', 'Wasa'),
      Filter.greaterThan('ecoscore', 'b'),
    ])));

We couldn't do that easily in SQLite, as we would have to scan the whole product database, decode Strings into JSON and so on...

Let's talk about the second use case: history, favorites and custom lists of products.

Both Sembast and SQLite (and even SharedPreferences) can handle that.
I would even say that SQLite is not appropriate for a custom list of products that the end user can reorder: if we want to change the order, we need to change all the records' custom order value. In Sembast, we can just store a List of barcodes, let the end user reorder it, then save in one step the List (List<dynamic> is supported by Sembast).
Anyway, I don't think it would make sense to onboard a second database system just for that list management.

It was not my initial choice, but from what I read about Sembast I think it would be perfect for both use cases.

Later, we may find issues with the performances (too much memory involved, too slow queries, too slow app startup), but for the moment I think we can stay with one database system, named Sembast.

@PrimaelQuemerais
Copy link
Member

Thank you for the detailed review.
Performances was indeed one of the concerns we had while evaluating the databases. I did write a benchmark a while ago but sadly can't find the results back. I recall Sembast was a little bit slow for writing big chunks of data in one shot, nevertheless writing can be done asynchronously without impacting the user experience so we decided to stick with it.

The JSON format and the query system were the reasons why we preferred Sembast over something like Hive which performances were superior but without any custom query possible. The main advantage of storing the data in a JSON format is the ability to use the classes and functions from openfoodfacts-dart, querying products from the local database is done in the exact same way as querying products from the API.

@stephanegigandet
Copy link
Contributor

Regarding the order of lists: I don't really see the use case for letting users re-order manually the lists in a custom order (as in dragging and dropping a list item at a specific place). But there is a big use case to let them display their lists in different orders (e.g. by date of adding the product to the list, by number of days before the products will perish, by number of instances of the product they have consumed / they currently own, by their food preferences, or something else etc.).

One use case to think of: for a given product, we need to know in which lists it belongs to (if any). e.g. you scan a product, and the app tells you it's already in the list "Products in my fridge", or that you scanned it one month ago.

Those use cases can be implemented in many different ways of course.

@monsieurtanuki
Copy link
Contributor Author

@stephanegigandet OK, we won't reorder the lists. I agree with you: those use cases can be implemented with different way of thinking or database system.

@PrimaelQuemerais Performances are already flagged as problematic with Sembast, then.

My main concern with Sembast is this: "The whole document based database resides in a single file and is loaded in memory when opened." (found here). That means the bigger the database, the slower the database opening and the higher the level of memory used. It stinks.

With SQLite, the startup is "almost" immediate : "Whenever a database is opened, the entire schema is scanned and parsed and a parse tree for the schema is held in memory. That means that database connection startup time and initial memory usage is proportional to the size of the schema." (the schema meaning the metadata about the tables) (found here). That means the database size has no impact on the duration of the database opening and the amount of memory used.

From what I read so far, smoothie's only concern is "I give you a barcode, you give me the JSON". Not something more elaborated like Filter.and([Filter.matches('product_name', 'Wasa'), Filter.greaterThan('ecoscore', 'b')]).
In that case we can use SQLite; the only difference is that we need an extra step with the standard conversion json.decode / json.encode. How much more does it cost?

I'm ready to run a test between Sembast and SQLite:

  • I download into a file the results of something like this (1 Mb)
  • I create an empty database (test1 : Sembast, test2 : SQLite)
  • and I loop several times, checking the duration of each step and the memory usage
    • open the database
    • size of the database file
    • count the number of records
    • populate the database with the file products (appending a timestamp to the barcode) (for Sembast, key: barcode, value: myjson) (for SQLite, primary key: barcode, field: json.encode(myjson))
    • count the number of records
    • close the database

Do you agree with that test, guys?

@stephanegigandet
Copy link
Contributor

@monsieurtanuki Just curious, with SQLite, do we have to pre-declare the maximum size of the JSON when the table is created (e.g. something like VARCHAR(4096), or is that handled automagically?

One thing that would be very useful to test as well: what if we have 10k products, 100k products, or even 1M products. e.g. for offline use, we could do something similar to OsmAnd where you can download maps for offline use for the countries you want. We could for instance have downlable archives of the top 10k or 100k most popular products. Or as you suggested, more specific datasets like all vegetarian products or all products sold in a specific store.

@monsieurtanuki
Copy link
Contributor Author

@stephanegigandet SQLite does not care about variable types, actually. Could be a number, could be a string (but no specified size). And there's no limit - actually there's one limit: 1 billion bytes. We'll have a problem with VERY fat foods ;)

There shouldn't be a problem with the number of products in SQLite (our main concern will be with the primary key index's size).
My first test will be Sembast vs. SQLite, until the first of them crashes. Then, another test until the winner crashes.

PS: funny you mention OsmAnd as I contribute to osmdroid.

@monsieurtanuki
Copy link
Contributor Author

Guys, we're lucky. I've got a 6-year-old canary-in-the-coalmine smartphone.

My first test crashed with Sembast: I couldn't reopen my 8K product database. While the SQLite was pretty cool.
As initially thought, Sembast asks for too much memory when opening the database because it loads all the data.

I'll give you more details tomorrow, but to me it's a NO GO for Sembast.

@PrimaelQuemerais
Copy link
Member

Once the functions to store and retrieve a Product from the database are written, Sembast offers little to no advantage over SQLite, except that each time we update the Product class the functions need to be updated aswell, which wasn't the case with Sembast.

The benchmark seems a little bit unfair to me as Sembast would allow to query elements by looking at nested fields in the product, which would not be possible if the data is stored in SQLite in a single field. That being said the chose comes down to which features we actually need. Waiting for the benchmark details :)

@monsieurtanuki
Copy link
Contributor Author

More details about the benchmark:

  • I've got this json file with 100 products, file size around 1Mb (IMHO products are much too verbose, but that's another story)
  • From scratch, I create a database
  • In a 100 iteration loop
    • I open the database
    • I add 1000 unique products (the 100 products of the file with barcodes appended with timestamp)
    • I retrieve two items
    • I close the database
  • and I do that with Sembast (sembast: ^2.4.8+1), and with SQLite (sqflite: ^1.3.2+1), on my humble smartphone, in flutter run "profiler" mode

Results:

  • Sembast quickly crashed the memory. Each time the database is opened, the whole database is loaded into memory. At one point, we reach the limit. In my case, it was about 400Mb memory usage, with 7K products
  • SQLite always behaved with regularity, no matter how big the database got. It eventually crashed because the database file was getting too big for my old smartphone. In my case, with 650Mb database file, with 57K products, but it's just a matter of disk space
  • Database opening (duration)
    • Sembast: 19ms (empty), then 3s (1K products), 7s (2K), 11s, 14s, 17s, 21s, 25s (7K products), then it crashed
    • SQLite: always around 10ms
  • Saving 1000 new products in the database (duration)
    • Sembast: always around 7s
    • SQLite: always around 12s (there's an extra json.decode step to store as a String btw)
  • Getting a product from its barcode (duration)
    • Sembast: around 5 ms
    • SQLite: around 5 ms
  • database file size
    • Sembast: 73.9 Mb with 7K products
    • SQLite: 82.7 Mb with 7K products

Memory usage with Sembast (I don't have similar screenshot for SQLite, the memory usage is so low it doesn't show)
image

Logs with Sembast
I/flutter (13533): 1609316762464:sembast version
I/flutter (13533): 1609316762465:iteration 0 : begin
I/flutter (13533): 1609316762484:opened // 19 ms
I/flutter (13533): 1609316762484:store
I/flutter (13533): 1609316762485:size1=0
I/flutter (13533): 1609316769307:size2=1000
I/flutter (13533): 1609316769311:first item(string length): 10641
I/flutter (13533): 1609316769313:last item(key count): 16
I/flutter (13533): 1609316769314:closed
I/flutter (13533): 1609316769316:dbFileSize=10557636
I/flutter (13533): 1609316769317:iteration 0 : end
I/flutter (13533): 1609316769318:iteration 1 : begin
I/flutter (13533): 1609316772733:opened // 3 seconds
I/flutter (13533): 1609316772734:store
I/flutter (13533): 1609316772735:size1=1000
I/flutter (13533): 1609316779567:size2=2000
I/flutter (13533): 1609316779571:first item(string length): 10641
I/flutter (13533): 1609316779572:last item(key count): 16
I/flutter (13533): 1609316779574:closed
I/flutter (13533): 1609316779575:dbFileSize=21115246
I/flutter (13533): 1609316779577:iteration 1 : end
I/flutter (13533): 1609316779578:iteration 2 : begin
I/flutter (13533): 1609316786957:opened // 7 seconds
I/flutter (13533): 1609316786958:store
I/flutter (13533): 1609316786958:size1=2000
I/flutter (13533): 1609316793928:size2=3000
I/flutter (13533): 1609316793933:first item(string length): 10641
I/flutter (13533): 1609316793934:last item(key count): 16
I/flutter (13533): 1609316793936:closed
I/flutter (13533): 1609316793938:dbFileSize=31672856
I/flutter (13533): 1609316793940:iteration 2 : end
I/flutter (13533): 1609316793942:iteration 3 : begin
I/flutter (13533): 1609316804481:opened // 11 seconds
I/flutter (13533): 1609316804482:store
I/flutter (13533): 1609316804482:size1=3000
I/flutter (13533): 1609316811239:size2=4000
I/flutter (13533): 1609316811243:first item(string length): 10641
I/flutter (13533): 1609316811244:last item(key count): 16
I/flutter (13533): 1609316811246:closed
I/flutter (13533): 1609316811248:dbFileSize=42230466
I/flutter (13533): 1609316811249:iteration 3 : end
I/flutter (13533): 1609316811250:iteration 4 : begin
I/flutter (13533): 1609316825184:opened // 14 seconds
I/flutter (13533): 1609316825185:store
I/flutter (13533): 1609316825185:size1=4000
I/flutter (13533): 1609316832017:size2=5000
I/flutter (13533): 1609316832021:first item(string length): 10641
I/flutter (13533): 1609316832023:last item(key count): 16
I/flutter (13533): 1609316832024:closed
I/flutter (13533): 1609316832025:dbFileSize=52788076
I/flutter (13533): 1609316832027:iteration 4 : end
I/flutter (13533): 1609316832028:iteration 5 : begin
I/flutter (13533): 1609316849436:opened // 17 seconds
I/flutter (13533): 1609316849437:store
I/flutter (13533): 1609316849437:size1=5000
I/flutter (13533): 1609316856134:size2=6000
I/flutter (13533): 1609316856137:first item(string length): 10641
I/flutter (13533): 1609316856138:last item(key count): 16
I/flutter (13533): 1609316856138:closed
I/flutter (13533): 1609316856140:dbFileSize=63345686
I/flutter (13533): 1609316856141:iteration 5 : end
I/flutter (13533): 1609316856141:iteration 6 : begin
I/flutter (13533): 1609316876930:opened // 21 seconds
I/flutter (13533): 1609316876931:store
I/flutter (13533): 1609316876931:size1=6000
I/flutter (13533): 1609316883713:size2=7000
I/flutter (13533): 1609316883717:first item(string length): 10641
I/flutter (13533): 1609316883718:last item(key count): 16
I/flutter (13533): 1609316883718:closed
I/flutter (13533): 1609316883719:dbFileSize=73903296
I/flutter (13533): 1609316883720:iteration 6 : end
I/flutter (13533): 1609316883720:iteration 7 : begin
I/flutter (13533): 1609316908542:opened // 25 seconds
I/flutter (13533): 1609316908544:store
I/flutter (13533): 1609316908545:size1=7000
Lost connection to device.
Logs with SQLite
Launching lib/main.dart on SM G360F in profile mode...
Running Gradle task 'assembleProfile'...
✓ Built build/app/outputs/flutter-apk/app-profile.apk (10.4MB).
Installing build/app/outputs/flutter-apk/app.apk...
Waiting for SM G360F to report its views...
Debug service listening on ws://127.0.0.1:51287/DZ2dA52Ledg=/ws
I/flutter (20063): 1609317780001:sqlite version (bulk : true)
I/flutter (20063): 1609317780001:iteration 0 : begin
I/flutter (20063): 1609317780073:opened
I/flutter (20063): 1609317780074:store (nothing)
I/flutter (20063): 1609317780077:size1=0
I/flutter (20063): 1609317787692:size2=1000
I/flutter (20063): 1609317787697:first item(string length): 10641
I/flutter (20063): 1609317787704:last item(key count): 16
I/flutter (20063): 1609317787712:closed
I/flutter (20063): 1609317787714:dbFileSize=11837440
I/flutter (20063): 1609317787715:iteration 0 : end
I/flutter (20063): 1609317787715:iteration 1 : begin
I/flutter (20063): 1609317787723:opened
I/flutter (20063): 1609317787724:store (nothing)
I/flutter (20063): 1609317787727:size1=1000
I/flutter (20063): 1609317794838:size2=2000
I/flutter (20063): 1609317794845:first item(string length): 10641
I/flutter (20063): 1609317794851:last item(key count): 16
I/flutter (20063): 1609317794857:closed
I/flutter (20063): 1609317794858:dbFileSize=23650304
I/flutter (20063): 1609317794861:iteration 1 : end
I/flutter (20063): 1609317794861:iteration 2 : begin
I/flutter (20063): 1609317794870:opened
I/flutter (20063): 1609317794870:store (nothing)
I/flutter (20063): 1609317794874:size1=2000
I/flutter (20063): 1609317802511:size2=3000
I/flutter (20063): 1609317802517:first item(string length): 10641
I/flutter (20063): 1609317802524:last item(key count): 16
I/flutter (20063): 1609317802529:closed
I/flutter (20063): 1609317802531:dbFileSize=35459072
I/flutter (20063): 1609317802532:iteration 2 : end
I/flutter (20063): 1609317802532:iteration 3 : begin
I/flutter (20063): 1609317802541:opened
I/flutter (20063): 1609317802542:store (nothing)
I/flutter (20063): 1609317802545:size1=3000
I/flutter (20063): 1609317810467:size2=4000
I/flutter (20063): 1609317810474:first item(string length): 10641
I/flutter (20063): 1609317810482:last item(key count): 16
I/flutter (20063): 1609317810488:closed
I/flutter (20063): 1609317810490:dbFileSize=47280128
I/flutter (20063): 1609317810490:iteration 3 : end
I/flutter (20063): 1609317810491:iteration 4 : begin
I/flutter (20063): 1609317810505:opened
I/flutter (20063): 1609317810506:store (nothing)
I/flutter (20063): 1609317810515:size1=4000
I/flutter (20063): 1609317818688:size2=5000
I/flutter (20063): 1609317818694:first item(string length): 10641
I/flutter (20063): 1609317818702:last item(key count): 16
I/flutter (20063): 1609317818708:closed
I/flutter (20063): 1609317818709:dbFileSize=59101184
I/flutter (20063): 1609317818710:iteration 4 : end
I/flutter (20063): 1609317818711:iteration 5 : begin
I/flutter (20063): 1609317818721:opened
I/flutter (20063): 1609317818722:store (nothing)
I/flutter (20063): 1609317818726:size1=5000
I/flutter (20063): 1609317826943:size2=6000
I/flutter (20063): 1609317826949:first item(string length): 10641
I/flutter (20063): 1609317826955:last item(key count): 16
I/flutter (20063): 1609317826961:closed
I/flutter (20063): 1609317826964:dbFileSize=70909952
I/flutter (20063): 1609317826964:iteration 5 : end
I/flutter (20063): 1609317826966:iteration 6 : begin
I/flutter (20063): 1609317826975:opened
I/flutter (20063): 1609317826976:store (nothing)
I/flutter (20063): 1609317826980:size1=6000
I/flutter (20063): 1609317835238:size2=7000
I/flutter (20063): 1609317835244:first item(string length): 10641
I/flutter (20063): 1609317835252:last item(key count): 16
I/flutter (20063): 1609317835258:closed
I/flutter (20063): 1609317835260:dbFileSize=82714624
I/flutter (20063): 1609317835260:iteration 6 : end
I/flutter (20063): 1609317835261:iteration 7 : begin
I/flutter (20063): 1609317835270:opened
I/flutter (20063): 1609317835271:store (nothing)
I/flutter (20063): 1609317835276:size1=7000
I/flutter (20063): 1609317843367:size2=8000
I/flutter (20063): 1609317843373:first item(string length): 10641
I/flutter (20063): 1609317843379:last item(key count): 16
I/flutter (20063): 1609317843383:closed
I/flutter (20063): 1609317843384:dbFileSize=94547968
I/flutter (20063): 1609317843385:iteration 7 : end
I/flutter (20063): 1609317843385:iteration 8 : begin
I/flutter (20063): 1609317843393:opened
I/flutter (20063): 1609317843394:store (nothing)
I/flutter (20063): 1609317843398:size1=8000
I/flutter (20063): 1609317850983:size2=9000
I/flutter (20063): 1609317850989:first item(string length): 10641
I/flutter (20063): 1609317850996:last item(key count): 16
I/flutter (20063): 1609317851002:closed
I/flutter (20063): 1609317851004:dbFileSize=106340352
I/flutter (20063): 1609317851006:iteration 8 : end
I/flutter (20063): 1609317851007:iteration 9 : begin
I/flutter (20063): 1609317851017:opened
I/flutter (20063): 1609317851018:store (nothing)
I/flutter (20063): 1609317851022:size1=9000
I/flutter (20063): 1609317859352:size2=10000
I/flutter (20063): 1609317859357:first item(string length): 10641
I/flutter (20063): 1609317859365:last item(key count): 16
I/flutter (20063): 1609317859371:closed
I/flutter (20063): 1609317859373:dbFileSize=118173696
I/flutter (20063): 1609317859374:iteration 9 : end
I/flutter (20063): 1609317859374:iteration 10 : begin
I/flutter (20063): 1609317859384:opened
I/flutter (20063): 1609317859385:store (nothing)
I/flutter (20063): 1609317859399:size1=10000
I/art     (20063): Background partial concurrent mark sweep GC freed 3058(842KB) AllocSpace objects, 61(1263KB) LOS objects, 29% free, 4MB/6MB, paused 6.488ms total 19.990ms
I/flutter (20063): 1609317867597:size2=11000
I/flutter (20063): 1609317867602:first item(string length): 10641
I/flutter (20063): 1609317867608:last item(key count): 16
I/flutter (20063): 1609317867614:closed
I/flutter (20063): 1609317867615:dbFileSize=129986560
I/flutter (20063): 1609317867617:iteration 10 : end
I/flutter (20063): 1609317867617:iteration 11 : begin
I/flutter (20063): 1609317867626:opened
I/flutter (20063): 1609317867627:store (nothing)
I/flutter (20063): 1609317867631:size1=11000
I/flutter (20063): 1609317876263:size2=12000
I/flutter (20063): 1609317876269:first item(string length): 10641
I/flutter (20063): 1609317876276:last item(key count): 16
I/flutter (20063): 1609317876281:closed
I/flutter (20063): 1609317876282:dbFileSize=141791232
I/flutter (20063): 1609317876283:iteration 11 : end
I/flutter (20063): 1609317876283:iteration 12 : begin
I/flutter (20063): 1609317876292:opened
I/flutter (20063): 1609317876292:store (nothing)
I/flutter (20063): 1609317876296:size1=12000
I/flutter (20063): 1609317884617:size2=13000
I/flutter (20063): 1609317884622:first item(string length): 10641
I/flutter (20063): 1609317884629:last item(key count): 16
I/flutter (20063): 1609317884635:closed
I/flutter (20063): 1609317884636:dbFileSize=153628672
I/flutter (20063): 1609317884637:iteration 12 : end
I/flutter (20063): 1609317884637:iteration 13 : begin
I/flutter (20063): 1609317884647:opened
I/flutter (20063): 1609317884647:store (nothing)
I/flutter (20063): 1609317884651:size1=13000
I/flutter (20063): 1609317893552:size2=14000
I/flutter (20063): 1609317893557:first item(string length): 10641
I/flutter (20063): 1609317893564:last item(key count): 16
I/flutter (20063): 1609317893569:closed
I/flutter (20063): 1609317893570:dbFileSize=165457920
I/flutter (20063): 1609317893570:iteration 13 : end
I/flutter (20063): 1609317893571:iteration 14 : begin
I/flutter (20063): 1609317893580:opened
I/flutter (20063): 1609317893581:store (nothing)
I/flutter (20063): 1609317893586:size1=14000
I/flutter (20063): 1609317901638:size2=15000
I/flutter (20063): 1609317901644:first item(string length): 10641
I/flutter (20063): 1609317901650:last item(key count): 16
I/flutter (20063): 1609317901655:closed
I/flutter (20063): 1609317901656:dbFileSize=177262592
I/flutter (20063): 1609317901657:iteration 14 : end
I/flutter (20063): 1609317901657:iteration 15 : begin
I/flutter (20063): 1609317901665:opened
I/flutter (20063): 1609317901666:store (nothing)
I/flutter (20063): 1609317901671:size1=15000
I/art     (20063): Background sticky concurrent mark sweep GC freed 2106(769KB) AllocSpace objects, 52(1055KB) LOS objects, 26% free, 4MB/6MB, paused 5.141ms total 13.645ms
I/flutter (20063): 1609317910807:size2=16000
I/flutter (20063): 1609317910814:first item(string length): 10641
I/flutter (20063): 1609317910821:last item(key count): 16
I/flutter (20063): 1609317910828:closed
I/flutter (20063): 1609317910829:dbFileSize=189087744
I/flutter (20063): 1609317910830:iteration 15 : end
I/flutter (20063): 1609317910831:iteration 16 : begin
I/flutter (20063): 1609317910839:opened
I/flutter (20063): 1609317910839:store (nothing)
I/flutter (20063): 1609317910844:size1=16000
I/flutter (20063): 1609317919153:size2=17000
I/flutter (20063): 1609317919158:first item(string length): 10641
I/flutter (20063): 1609317919165:last item(key count): 16
I/flutter (20063): 1609317919170:closed
I/flutter (20063): 1609317919171:dbFileSize=200937472
I/flutter (20063): 1609317919172:iteration 16 : end
I/flutter (20063): 1609317919172:iteration 17 : begin
I/flutter (20063): 1609317919180:opened
I/flutter (20063): 1609317919181:store (nothing)
I/flutter (20063): 1609317919185:size1=17000
I/flutter (20063): 1609317927984:size2=18000
I/flutter (20063): 1609317927990:first item(string length): 10641
I/flutter (20063): 1609317927996:last item(key count): 16
I/flutter (20063): 1609317928001:closed
I/flutter (20063): 1609317928003:dbFileSize=212799488
I/flutter (20063): 1609317928004:iteration 17 : end
I/flutter (20063): 1609317928004:iteration 18 : begin
I/flutter (20063): 1609317928012:opened
I/flutter (20063): 1609317928013:store (nothing)
I/flutter (20063): 1609317928018:size1=18000
I/flutter (20063): 1609317936854:size2=19000
I/flutter (20063): 1609317936860:first item(string length): 10641
I/flutter (20063): 1609317936866:last item(key count): 16
I/flutter (20063): 1609317936871:closed
I/flutter (20063): 1609317936872:dbFileSize=224673792
I/flutter (20063): 1609317936873:iteration 18 : end
I/flutter (20063): 1609317936873:iteration 19 : begin
I/flutter (20063): 1609317936882:opened
I/flutter (20063): 1609317936884:store (nothing)
I/flutter (20063): 1609317936889:size1=19000
I/flutter (20063): 1609317945250:size2=20000
I/flutter (20063): 1609317945255:first item(string length): 10641
I/flutter (20063): 1609317945262:last item(key count): 16
I/flutter (20063): 1609317945267:closed
I/flutter (20063): 1609317945268:dbFileSize=236523520
I/flutter (20063): 1609317945269:iteration 19 : end
I/flutter (20063): 1609317945270:iteration 20 : begin
I/flutter (20063): 1609317945279:opened
I/flutter (20063): 1609317945279:store (nothing)
I/flutter (20063): 1609317945284:size1=20000
I/flutter (20063): 1609317954995:size2=21000
I/flutter (20063): 1609317955000:first item(string length): 10641
I/flutter (20063): 1609317955007:last item(key count): 16
I/flutter (20063): 1609317955012:closed
I/flutter (20063): 1609317955013:dbFileSize=248360960
I/flutter (20063): 1609317955014:iteration 20 : end
I/flutter (20063): 1609317955014:iteration 21 : begin
I/flutter (20063): 1609317955022:opened
I/flutter (20063): 1609317955023:store (nothing)
I/flutter (20063): 1609317955028:size1=21000
I/flutter (20063): 1609317963971:size2=22000
I/flutter (20063): 1609317963977:first item(string length): 10641
I/flutter (20063): 1609317963984:last item(key count): 16
I/flutter (20063): 1609317963989:closed
I/flutter (20063): 1609317963990:dbFileSize=260194304
I/flutter (20063): 1609317963991:iteration 21 : end
I/flutter (20063): 1609317963991:iteration 22 : begin
I/flutter (20063): 1609317963999:opened
I/flutter (20063): 1609317964001:store (nothing)
I/flutter (20063): 1609317964007:size1=22000
I/flutter (20063): 1609317972429:size2=23000
I/flutter (20063): 1609317972434:first item(string length): 10641
I/flutter (20063): 1609317972440:last item(key count): 16
I/flutter (20063): 1609317972446:closed
I/flutter (20063): 1609317972448:dbFileSize=272044032
I/flutter (20063): 1609317972449:iteration 22 : end
I/flutter (20063): 1609317972450:iteration 23 : begin
I/flutter (20063): 1609317972459:opened
I/flutter (20063): 1609317972461:store (nothing)
I/flutter (20063): 1609317972466:size1=23000
I/flutter (20063): 1609317981060:size2=24000
I/flutter (20063): 1609317981067:first item(string length): 10641
I/flutter (20063): 1609317981075:last item(key count): 16
I/flutter (20063): 1609317981080:closed
I/flutter (20063): 1609317981082:dbFileSize=283901952
I/flutter (20063): 1609317981082:iteration 23 : end
I/flutter (20063): 1609317981082:iteration 24 : begin
I/flutter (20063): 1609317981092:opened
I/flutter (20063): 1609317981092:store (nothing)
I/flutter (20063): 1609317981099:size1=24000
I/flutter (20063): 1609317989998:size2=25000
I/flutter (20063): 1609317990004:first item(string length): 10641
I/flutter (20063): 1609317990010:last item(key count): 16
I/flutter (20063): 1609317990016:closed
I/flutter (20063): 1609317990016:dbFileSize=295768064
I/flutter (20063): 1609317990017:iteration 24 : end
I/flutter (20063): 1609317990017:iteration 25 : begin
I/flutter (20063): 1609317990026:opened
I/flutter (20063): 1609317990027:store (nothing)
I/flutter (20063): 1609317990033:size1=25000
I/flutter (20063): 1609317999014:size2=26000
I/flutter (20063): 1609317999020:first item(string length): 10641
I/flutter (20063): 1609317999026:last item(key count): 16
I/flutter (20063): 1609317999032:closed
I/flutter (20063): 1609317999033:dbFileSize=307625984
I/flutter (20063): 1609317999034:iteration 25 : end
I/flutter (20063): 1609317999035:iteration 26 : begin
I/flutter (20063): 1609317999044:opened
I/flutter (20063): 1609317999045:store (nothing)
I/flutter (20063): 1609317999050:size1=26000
I/flutter (20063): 1609318008384:size2=27000
I/flutter (20063): 1609318008389:first item(string length): 10641
I/flutter (20063): 1609318008395:last item(key count): 16
I/flutter (20063): 1609318008400:closed
I/flutter (20063): 1609318008401:dbFileSize=319483904
I/flutter (20063): 1609318008402:iteration 26 : end
I/flutter (20063): 1609318008402:iteration 27 : begin
I/flutter (20063): 1609318008411:opened
I/flutter (20063): 1609318008412:store (nothing)
I/flutter (20063): 1609318008417:size1=27000
I/flutter (20063): 1609318019836:size2=28000
I/flutter (20063): 1609318019843:first item(string length): 10641
I/flutter (20063): 1609318019852:last item(key count): 16
I/flutter (20063): 1609318019858:closed
I/flutter (20063): 1609318019860:dbFileSize=331350016
I/flutter (20063): 1609318019861:iteration 27 : end
I/flutter (20063): 1609318019861:iteration 28 : begin
I/flutter (20063): 1609318019871:opened
I/flutter (20063): 1609318019871:store (nothing)
I/flutter (20063): 1609318019877:size1=28000
I/flutter (20063): 1609318028950:size2=29000
I/flutter (20063): 1609318028956:first item(string length): 10641
I/flutter (20063): 1609318028963:last item(key count): 16
I/flutter (20063): 1609318028969:closed
I/flutter (20063): 1609318028971:dbFileSize=343199744
I/flutter (20063): 1609318028972:iteration 28 : end
I/flutter (20063): 1609318028972:iteration 29 : begin
I/flutter (20063): 1609318028982:opened
I/flutter (20063): 1609318028983:store (nothing)
I/flutter (20063): 1609318028989:size1=29000
I/flutter (20063): 1609318039114:size2=30000
I/flutter (20063): 1609318039119:first item(string length): 10641
I/flutter (20063): 1609318039125:last item(key count): 16
I/flutter (20063): 1609318039130:closed
I/flutter (20063): 1609318039133:dbFileSize=355065856
I/flutter (20063): 1609318039134:iteration 29 : end
I/flutter (20063): 1609318039135:iteration 30 : begin
I/flutter (20063): 1609318039147:opened
I/flutter (20063): 1609318039147:store (nothing)
I/flutter (20063): 1609318039153:size1=30000
I/flutter (20063): 1609318049842:size2=31000
I/flutter (20063): 1609318049848:first item(string length): 10641
I/flutter (20063): 1609318049855:last item(key count): 16
I/flutter (20063): 1609318049861:closed
I/flutter (20063): 1609318049862:dbFileSize=366940160
I/flutter (20063): 1609318049862:iteration 30 : end
I/flutter (20063): 1609318049863:iteration 31 : begin
I/flutter (20063): 1609318049871:opened
I/flutter (20063): 1609318049872:store (nothing)
I/flutter (20063): 1609318049878:size1=31000
I/flutter (20063): 1609318060015:size2=32000
I/flutter (20063): 1609318060020:first item(string length): 10641
I/flutter (20063): 1609318060026:last item(key count): 16
I/flutter (20063): 1609318060032:closed
I/flutter (20063): 1609318060033:dbFileSize=378793984
I/flutter (20063): 1609318060034:iteration 31 : end
I/flutter (20063): 1609318060034:iteration 32 : begin
I/flutter (20063): 1609318060042:opened
I/flutter (20063): 1609318060043:store (nothing)
I/flutter (20063): 1609318060049:size1=32000
I/flutter (20063): 1609318069874:size2=33000
I/flutter (20063): 1609318069882:first item(string length): 10641
I/flutter (20063): 1609318069889:last item(key count): 16
I/flutter (20063): 1609318069896:closed
I/flutter (20063): 1609318069897:dbFileSize=390660096
I/flutter (20063): 1609318069898:iteration 32 : end
I/flutter (20063): 1609318069899:iteration 33 : begin
I/flutter (20063): 1609318069908:opened
I/flutter (20063): 1609318069910:store (nothing)
I/flutter (20063): 1609318069918:size1=33000
I/flutter (20063): 1609318079694:size2=34000
I/flutter (20063): 1609318079699:first item(string length): 10641
I/flutter (20063): 1609318079705:last item(key count): 16
I/flutter (20063): 1609318079710:closed
I/flutter (20063): 1609318079712:dbFileSize=402530304
I/flutter (20063): 1609318079713:iteration 33 : end
I/flutter (20063): 1609318079714:iteration 34 : begin
I/flutter (20063): 1609318079722:opened
I/flutter (20063): 1609318079723:store (nothing)
I/flutter (20063): 1609318079730:size1=34000
I/flutter (20063): 1609318090633:size2=35000
I/flutter (20063): 1609318090638:first item(string length): 10641
I/flutter (20063): 1609318090645:last item(key count): 16
I/flutter (20063): 1609318090652:closed
I/flutter (20063): 1609318090655:dbFileSize=414396416
I/flutter (20063): 1609318090655:iteration 34 : end
I/flutter (20063): 1609318090656:iteration 35 : begin
I/flutter (20063): 1609318090666:opened
I/flutter (20063): 1609318090667:store (nothing)
I/flutter (20063): 1609318090674:size1=35000
I/flutter (20063): 1609318100402:size2=36000
I/flutter (20063): 1609318100408:first item(string length): 10641
I/flutter (20063): 1609318100416:last item(key count): 16
I/flutter (20063): 1609318100421:closed
I/flutter (20063): 1609318100422:dbFileSize=426303488
I/flutter (20063): 1609318100423:iteration 35 : end
I/flutter (20063): 1609318100423:iteration 36 : begin
I/flutter (20063): 1609318100432:opened
I/flutter (20063): 1609318100433:store (nothing)
I/flutter (20063): 1609318100439:size1=36000
I/art     (20063): Background partial concurrent mark sweep GC freed 3094(771KB) AllocSpace objects, 62(1249KB) LOS objects, 29% free, 4MB/6MB, paused 5.091ms total 24.268ms
I/flutter (20063): 1609318111134:size2=37000
I/flutter (20063): 1609318111139:first item(string length): 10641
I/flutter (20063): 1609318111145:last item(key count): 16
I/flutter (20063): 1609318111152:closed
I/flutter (20063): 1609318111154:dbFileSize=438210560
I/flutter (20063): 1609318111154:iteration 36 : end
I/flutter (20063): 1609318111154:iteration 37 : begin
I/flutter (20063): 1609318111163:opened
I/flutter (20063): 1609318111164:store (nothing)
I/flutter (20063): 1609318111171:size1=37000
I/flutter (20063): 1609318122732:size2=38000
I/flutter (20063): 1609318122738:first item(string length): 10641
I/flutter (20063): 1609318122745:last item(key count): 16
I/flutter (20063): 1609318122751:closed
I/flutter (20063): 1609318122752:dbFileSize=450060288
I/flutter (20063): 1609318122753:iteration 37 : end
I/flutter (20063): 1609318122753:iteration 38 : begin
I/flutter (20063): 1609318122762:opened
I/flutter (20063): 1609318122762:store (nothing)
I/flutter (20063): 1609318122770:size1=38000
I/flutter (20063): 1609318133485:size2=39000
I/flutter (20063): 1609318133491:first item(string length): 10641
I/flutter (20063): 1609318133498:last item(key count): 16
I/flutter (20063): 1609318133503:closed
I/flutter (20063): 1609318133505:dbFileSize=461914112
I/flutter (20063): 1609318133506:iteration 38 : end
I/flutter (20063): 1609318133506:iteration 39 : begin
I/flutter (20063): 1609318133514:opened
I/flutter (20063): 1609318133515:store (nothing)
I/flutter (20063): 1609318133521:size1=39000
I/flutter (20063): 1609318143695:size2=40000
I/flutter (20063): 1609318143703:first item(string length): 10641
I/flutter (20063): 1609318143710:last item(key count): 16
I/flutter (20063): 1609318143717:closed
I/flutter (20063): 1609318143718:dbFileSize=473747456
I/flutter (20063): 1609318143719:iteration 39 : end
I/flutter (20063): 1609318143720:iteration 40 : begin
I/flutter (20063): 1609318143731:opened
I/flutter (20063): 1609318143732:store (nothing)
I/flutter (20063): 1609318143740:size1=40000
I/flutter (20063): 1609318155096:size2=41000
I/flutter (20063): 1609318155102:first item(string length): 10641
I/flutter (20063): 1609318155109:last item(key count): 16
I/flutter (20063): 1609318155114:closed
I/flutter (20063): 1609318155115:dbFileSize=485580800
I/flutter (20063): 1609318155115:iteration 40 : end
I/flutter (20063): 1609318155116:iteration 41 : begin
I/flutter (20063): 1609318155124:opened
I/flutter (20063): 1609318155125:store (nothing)
I/flutter (20063): 1609318155132:size1=41000
I/flutter (20063): 1609318165135:size2=42000
I/flutter (20063): 1609318165140:first item(string length): 10641
I/flutter (20063): 1609318165147:last item(key count): 16
I/flutter (20063): 1609318165153:closed
I/flutter (20063): 1609318165154:dbFileSize=497414144
I/flutter (20063): 1609318165155:iteration 41 : end
I/flutter (20063): 1609318165155:iteration 42 : begin
I/flutter (20063): 1609318165164:opened
I/flutter (20063): 1609318165165:store (nothing)
I/flutter (20063): 1609318165172:size1=42000
I/flutter (20063): 1609318176934:size2=43000
I/flutter (20063): 1609318176939:first item(string length): 10641
I/flutter (20063): 1609318176946:last item(key count): 16
I/flutter (20063): 1609318176951:closed
I/flutter (20063): 1609318176953:dbFileSize=509288448
I/flutter (20063): 1609318176953:iteration 42 : end
I/flutter (20063): 1609318176953:iteration 43 : begin
I/flutter (20063): 1609318176961:opened
I/flutter (20063): 1609318176963:store (nothing)
I/flutter (20063): 1609318176970:size1=43000
I/flutter (20063): 1609318188849:size2=44000
I/flutter (20063): 1609318188857:first item(string length): 10641
I/flutter (20063): 1609318188864:last item(key count): 16
I/flutter (20063): 1609318188869:closed
I/flutter (20063): 1609318188871:dbFileSize=521166848
I/flutter (20063): 1609318188872:iteration 43 : end
I/flutter (20063): 1609318188872:iteration 44 : begin
I/flutter (20063): 1609318188880:opened
I/flutter (20063): 1609318188880:store (nothing)
I/flutter (20063): 1609318188888:size1=44000
I/flutter (20063): 1609318200050:size2=45000
I/flutter (20063): 1609318200056:first item(string length): 10641
I/flutter (20063): 1609318200065:last item(key count): 16
I/flutter (20063): 1609318200070:closed
I/flutter (20063): 1609318200072:dbFileSize=533024768
I/flutter (20063): 1609318200073:iteration 44 : end
I/flutter (20063): 1609318200073:iteration 45 : begin
I/flutter (20063): 1609318200082:opened
I/flutter (20063): 1609318200083:store (nothing)
I/flutter (20063): 1609318200091:size1=45000
I/flutter (20063): 1609318213460:size2=46000
I/flutter (20063): 1609318213466:first item(string length): 10641
I/flutter (20063): 1609318213472:last item(key count): 16
I/flutter (20063): 1609318213478:closed
I/flutter (20063): 1609318213479:dbFileSize=544940032
I/flutter (20063): 1609318213479:iteration 45 : end
I/flutter (20063): 1609318213479:iteration 46 : begin
I/flutter (20063): 1609318213488:opened
I/flutter (20063): 1609318213488:store (nothing)
I/flutter (20063): 1609318213496:size1=46000
I/flutter (20063): 1609318225616:size2=47000
I/flutter (20063): 1609318225623:first item(string length): 10641
I/flutter (20063): 1609318225631:last item(key count): 16
I/flutter (20063): 1609318225636:closed
I/flutter (20063): 1609318225637:dbFileSize=556830720
I/flutter (20063): 1609318225638:iteration 46 : end
I/flutter (20063): 1609318225638:iteration 47 : begin
I/flutter (20063): 1609318225648:opened
I/flutter (20063): 1609318225648:store (nothing)
I/flutter (20063): 1609318225660:size1=47000
I/flutter (20063): 1609318236743:size2=48000
I/flutter (20063): 1609318236749:first item(string length): 10641
I/flutter (20063): 1609318236755:last item(key count): 16
I/flutter (20063): 1609318236761:closed
I/flutter (20063): 1609318236763:dbFileSize=568688640
I/flutter (20063): 1609318236764:iteration 47 : end
I/flutter (20063): 1609318236764:iteration 48 : begin
I/flutter (20063): 1609318236773:opened
I/flutter (20063): 1609318236774:store (nothing)
I/flutter (20063): 1609318236782:size1=48000
I/flutter (20063): 1609318247102:size2=49000
I/flutter (20063): 1609318247107:first item(string length): 10641
I/flutter (20063): 1609318247113:last item(key count): 16
I/flutter (20063): 1609318247118:closed
I/flutter (20063): 1609318247120:dbFileSize=580530176
I/flutter (20063): 1609318247120:iteration 48 : end
I/flutter (20063): 1609318247120:iteration 49 : begin
I/flutter (20063): 1609318247131:opened
I/flutter (20063): 1609318247131:store (nothing)
I/flutter (20063): 1609318247139:size1=49000
I/flutter (20063): 1609318258442:size2=50000
I/flutter (20063): 1609318258456:first item(string length): 10641
I/flutter (20063): 1609318258463:last item(key count): 16
I/flutter (20063): 1609318258469:closed
I/flutter (20063): 1609318258470:dbFileSize=592375808
I/flutter (20063): 1609318258471:iteration 49 : end
I/flutter (20063): 1609318258472:iteration 50 : begin
I/flutter (20063): 1609318258480:opened
I/flutter (20063): 1609318258481:store (nothing)
I/flutter (20063): 1609318258489:size1=50000
I/flutter (20063): 1609318269685:size2=51000
I/flutter (20063): 1609318269690:first item(string length): 10641
I/flutter (20063): 1609318269696:last item(key count): 16
I/flutter (20063): 1609318269702:closed
I/flutter (20063): 1609318269703:dbFileSize=604196864
I/flutter (20063): 1609318269704:iteration 50 : end
I/flutter (20063): 1609318269705:iteration 51 : begin
I/flutter (20063): 1609318269714:opened
I/flutter (20063): 1609318269715:store (nothing)
I/flutter (20063): 1609318269722:size1=51000
I/art     (20063): Background partial concurrent mark sweep GC freed 2929(742KB) AllocSpace objects, 59(1199KB) LOS objects, 29% free, 4MB/6MB, paused 5.226ms total 18.277ms
I/flutter (20063): 1609318279572:size2=52000
I/flutter (20063): 1609318279577:first item(string length): 10641
I/flutter (20063): 1609318279585:last item(key count): 16
I/flutter (20063): 1609318279592:closed
I/flutter (20063): 1609318279594:dbFileSize=616038400
I/flutter (20063): 1609318279594:iteration 51 : end
I/flutter (20063): 1609318279594:iteration 52 : begin
I/flutter (20063): 1609318279605:opened
I/flutter (20063): 1609318279606:store (nothing)
I/flutter (20063): 1609318279616:size1=52000
I/flutter (20063): 1609318291348:size2=53000 // 12 seconds
I/flutter (20063): 1609318291354:first item(string length): 10641
I/flutter (20063): 1609318291361:last item(key count): 16
I/flutter (20063): 1609318291366:closed
I/flutter (20063): 1609318291367:dbFileSize=627884032
I/flutter (20063): 1609318291368:iteration 52 : end
I/flutter (20063): 1609318291368:iteration 53 : begin
I/flutter (20063): 1609318291377:opened
I/flutter (20063): 1609318291377:store (nothing)
I/flutter (20063): 1609318291385:size1=53000
I/flutter (20063): 1609318307319:size2=54000 // 16 seconds
I/flutter (20063): 1609318307325:first item(string length): 10641
I/flutter (20063): 1609318307331:last item(key count): 16
I/flutter (20063): 1609318307336:closed
I/flutter (20063): 1609318307337:dbFileSize=639758336
I/flutter (20063): 1609318307338:iteration 53 : end
I/flutter (20063): 1609318307339:iteration 54 : begin
I/flutter (20063): 1609318307347:opened
I/flutter (20063): 1609318307348:store (nothing)
I/flutter (20063): 1609318307356:size1=54000
I/flutter (20063): 1609318317495:size2=55000 // 10 seconds
I/flutter (20063): 1609318317501:first item(string length): 10641
I/flutter (20063): 1609318317507:last item(key count): 16
I/flutter (20063): 1609318317513:closed
I/flutter (20063): 1609318317514:dbFileSize=651628544
I/flutter (20063): 1609318317515:iteration 54 : end
I/flutter (20063): 1609318317515:iteration 55 : begin
I/flutter (20063): 1609318317524:opened
I/flutter (20063): 1609318317524:store (nothing)
I/flutter (20063): 1609318317533:size1=55000
I/flutter (20063): 1609318328724:size2=56000 // 11 seconds
I/flutter (20063): 1609318328729:first item(string length): 10641
I/flutter (20063): 1609318328736:last item(key count): 16
I/flutter (20063): 1609318328741:closed
I/flutter (20063): 1609318328742:dbFileSize=663543808
I/flutter (20063): 1609318328743:iteration 55 : end
I/flutter (20063): 1609318328743:iteration 56 : begin
I/flutter (20063): 1609318328752:opened
I/flutter (20063): 1609318328752:store (nothing)
I/flutter (20063): 1609318328761:size1=56000
I/flutter (20063): 1609318340411:size2=57000 // 12 seconds
I/flutter (20063): 1609318340416:first item(string length): 10641
I/flutter (20063): 1609318340422:last item(key count): 16
I/flutter (20063): 1609318340428:closed
I/flutter (20063): 1609318340429:dbFileSize=675430400
I/flutter (20063): 1609318340429:iteration 56 : end
I/flutter (20063): 1609318340430:iteration 57 : begin
I/flutter (20063): 1609318340438:opened
I/flutter (20063): 1609318340438:store (nothing)
I/flutter (20063): 1609318340447:size1=57000
E/flutter (20063): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: DatabaseException(database or disk is full (code 13)) sql 'COMMIT' args []}
E/flutter (20063): #0      wrapDatabaseException (package:sqflite/src/exception_impl.dart:11)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #1      SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:312)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #2      SqfliteDatabaseMixin.endTransaction (package:sqflite_common/src/database_mixin.dart:459)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #3      SqfliteDatabaseMixin._runTransaction (package:sqflite_common/src/database_mixin.dart:481)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #4      BasicLock.synchronized (package:synchronized/src/basic_lock.dart:34)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #5      SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:337)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): #6      _MyHomePageState._sqliteVersion (package:smooth_profiler/main.dart:68)
E/flutter (20063): <asynchronous suspension>
E/flutter (20063): 
I/Timeline(20063): Timeline: Activity_idle id: android.os.BinderProxy@9a3b585 time:87359870
flutter code
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

import 'package:path/path.dart';
import 'package:flutter/material.dart';
import 'package:sembast/sembast.dart' as sembast;
import 'package:sembast/sembast_io.dart' as sembast;
import 'package:sqflite/sqflite.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter App',
      home: MyHomePage(title: 'Counter App Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _sqliteVersion(final dynamic inputProducts, final String dbPath) async {
    const bool bulkOr1by1 = true;

    const String TABLE_NAME = 'Test';
    const String COLUMN_KEY = 'barcode';
    const String COLUMN_VALUE = 'myjson';
    String firstKey;
    String lastKey;

    // cf. https://pub.dev/packages/sqflite
    myprint('sqlite version (bulk : $bulkOr1by1)');
    for (int ite = 0; ite < 100; ite++) {
      // 100 instead of 10!
      myprint('iteration $ite : begin');
      final Database database = await openDatabase(dbPath, version: 1,
          onCreate: (Database db, int version) async {
        // When creating the db, create the table
        await db.execute(
            'CREATE TABLE $TABLE_NAME ($COLUMN_KEY string PRIMARY KEY, $COLUMN_VALUE string)');
      });
      myprint('opened');

      myprint('store (nothing)');

      int count = Sqflite.firstIntValue(
          await database.rawQuery('SELECT COUNT(*) FROM $TABLE_NAME'));
      myprint('size1=$count');

      for (int i = 0; i < 10; i++) {
        final int timestamp = nowInMillis();

        if (bulkOr1by1) {
          // transaction version
          await database.transaction((txn) async {
            for (final dynamic inputProduct in inputProducts) {
              final String fakeKey = '${inputProduct['code']}/$timestamp';
              if (firstKey == null) {
                firstKey = fakeKey;
              }
              lastKey = fakeKey;
              await txn.insert(
                TABLE_NAME,
                {COLUMN_KEY: fakeKey, COLUMN_VALUE: json.encode(inputProduct)},
              );
            }
          });
        } else {
          // one by one version
          for (final dynamic inputProduct in inputProducts) {
            final String fakeKey = '${inputProduct['code']}/$timestamp';
            if (firstKey == null) {
              firstKey = fakeKey;
            }
            lastKey = fakeKey;
            await database.insert(TABLE_NAME,
                {COLUMN_KEY: fakeKey, COLUMN_VALUE: json.encode(inputProduct)});
          }
        }
      }
      count = Sqflite.firstIntValue(
          await database.rawQuery('SELECT COUNT(*) FROM $TABLE_NAME'));
      myprint('size2=$count');

      List<Map<String, dynamic>> result;
      result = await database.query(
        TABLE_NAME,
        columns: [COLUMN_VALUE],
        where: '$COLUMN_KEY = ?',
        whereArgs: [firstKey],
      );
      myprint('first item(string length): ${result[0][COLUMN_VALUE].length}');
      result = await database.query(
        TABLE_NAME,
        columns: [COLUMN_VALUE],
        where: '$COLUMN_KEY = ?',
        whereArgs: [lastKey],
      );
      myprint(
          'last item(key count): ${json.decode(result[0][COLUMN_VALUE]).length}');
      await database.close();
      myprint('closed');
      final File dbFile = File(dbPath);
      final int dbFileSize = await dbFile.length();
      myprint('dbFileSize=$dbFileSize');
      myprint('iteration $ite : end');
    }
  }

  void _sembastVersion(final dynamic inputProducts, final String dbPath) async {
    String firstKey;
    String lastKey;

    final sembast.DatabaseFactory dbFactory = sembast.databaseFactoryIo;

    myprint('sembast version');
    for (int ite = 0; ite < 10; ite++) {
      myprint('iteration $ite : begin');
      final sembast.Database db = await dbFactory.openDatabase(dbPath);
      myprint('opened');
      final sembast.StoreRef store = sembast.StoreRef.main();
      myprint('store');

      int size = await store.count(db);
      myprint('size1=$size');

      for (int i = 0; i < 10; i++) {
        final int timestamp = nowInMillis();
        for (final dynamic inputProduct in inputProducts) {
          final String fakeKey = '${inputProduct['code']}/$timestamp';
          if (firstKey == null) {
            firstKey = fakeKey;
          }
          lastKey = fakeKey;
          await store.record(fakeKey).put(db, inputProduct);
        }
      }
      size = await store.count(db);
      myprint('size2=$size');

      dynamic result;
      result = await store.record(firstKey).get(db);
      myprint('first item(string length): ${json.encode(result).length}');
      result = await store.record(lastKey).get(db);
      myprint('last item(key count): ${result.length}');

      await db.close();
      myprint('closed');
      final File dbFile = File(dbPath);
      final int dbFileSize = await dbFile.length();
      myprint('dbFileSize=$dbFileSize');
      myprint('iteration $ite : end');
    }
  }

  void myprint(final String message) {
    const int COLSIZE = 1000;
    final String result = '${nowInMillis()}:$message';
    int i = 0;
    final int max = result.length;
    while (i < max) {
      int j = i + COLSIZE;
      if (j >= max) {
        j = max;
      }
      print(result.substring(i, j));
      i += COLSIZE;
    }
  }

  int nowInMillis() => DateTime.now().millisecondsSinceEpoch;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              // Provide a Key to this specific Text widget. This allows
              // identifying the widget from inside the test suite,
              // and reading the text.
              key: Key('counter'),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Provide a Key to this button. This allows finding this
        // specific button inside the test suite, and tapping it.
        key: Key('increment'),
        onPressed: () async {
          final String inputString = await DefaultAssetBundle.of(context)
              .loadString('assets/smoothTest.json');
          final dynamic inputJson = json.decode(inputString);
          final dynamic inputProducts = inputJson['products'];

          String directory = await getDatabasesPath();
          directory = (await getExternalStorageDirectory()).path;
          final String dbPath = join(directory, 'sample.db');

          // from scratch
          try {
            final File file = File(dbPath);
            print('file:$file');
            await file.delete();
          } catch (e) {
            // we don't care
          }

          //_sembastVersion(inputProducts, dbPath);
          _sqliteVersion(inputProducts, dbPath);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

@stephanegigandet
Copy link
Contributor

Thanks for all the tests!

  • Saving 1000 new products in the database (duration)

    • Sembast: always around 7s
    • SQLite: always around 12s (there's an extra json.decode step to store as a String btw)

I wonder if those 12 seconds are mostly due to the json.encode. What if we want to insert 10k products (from a downloadable file with the most popular products for instance), where each product data is available as a json string. Could you check how long it would take?

@monsieurtanuki
Copy link
Contributor Author

@stephanegigandet I relaunched the same SQLite test: this time loading 1K product took around 8s. And when I used json.encode only once every 100 products (instead of for every product), loading 1K product took around 5.5 s.
For the details: when I load 1K products I run a 10-time loop of running a transaction of 100 insert statements.

I think the database insert performances are not as important as the database opening:

  • if we need to load an initial database, the end-user can understand that and wait, once, for the init to finish
  • it's not as bad as waiting 25 s every time you re-start the app
  • a big performance factor is the size of the data - what we load is currently much too fat for each product. The leaner the products, the faster the performances. Especially when you download several products whose values will often be similar.
  • with SQLite, we may even be able to provide the SQLite database file as is (no init)
  • there might be smarter ways to deal with bulk inserts in SQLite, maybe with Batch, maybe with a better management of async transactions and inserts
For the record, what a product look like, data-wise (10Kb when you remove the spaces)
{
	"product_name": "Salmón ahumado",
	"lang": "en",
	"brands": "Ocean Sea",
	"serving_size": "50g",
	"nutrition_grade_fr": "d",
	"code": "20034658",
	"selected_images": {
		"front": {
			"thumb": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.100.jpg"
			},
			"display": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.400.jpg"
			},
			"small": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/front_fr.98.200.jpg"
			}
		},
		"ingredients": {
			"small": {
				"es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.200.jpg",
				"fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.200.jpg"
			},
			"thumb": {
				"es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.100.jpg",
				"fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.100.jpg"
			},
			"display": {
				"es": "https://static.openfoodfacts.org/images/products/20034658/ingredients_es.78.400.jpg",
				"fr": "https://static.openfoodfacts.org/images/products/20034658/ingredients_fr.83.400.jpg"
			}
		},
		"nutrition": {
			"display": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.400.jpg"
			},
			"thumb": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.100.jpg"
			},
			"small": {
				"fr": "https://static.openfoodfacts.org/images/products/20034658/nutrition_fr.93.200.jpg"
			}
		}
	},
	"nutriments": {
		"fat_100g": 10,
		"sodium": 1.28,
		"energy_100g": 728,
		"saturated-fat_100g": 1.5,
		"nova-group_100g": 3,
		"sugars_unit": "g",
		"sugars": 0,
		"fiber_serving": 0,
		"fat_unit": "g",
		"fat": 10,
		"proteins": 21,
		"sodium_unit": "g",
		"proteins_value": 21,
		"salt_serving": 1.6,
		"saturated-fat_value": 1.5,
		"energy_unit": "kcal",
		"nutrition-score-fr_100g": 13,
		"nutrition-score-fr": 13,
		"energy-kcal": 174,
		"energy_serving": 364,
		"energy": 728,
		"nova-group": 3,
		"energy-kcal_value": 174,
		"fiber_unit": "g",
		"sodium_100g": 1.28,
		"carbohydrates_value": 0,
		"saturated-fat_serving": 0.75,
		"sodium_value": 1.28,
		"energy-kcal_100g": 174,
		"salt_100g": 3.2,
		"sugars_100g": 0,
		"nova-group_serving": 3,
		"proteins_serving": 10.5,
		"salt_unit": "g",
		"fat_serving": 5,
		"energy_value": 174,
		"fiber": 0,
		"fiber_value": 0,
		"fruits-vegetables-nuts-estimate-from-ingredients_100g": 0,
		"salt": 3.2,
		"carbohydrates_100g": 0,
		"saturated-fat_unit": "g",
		"salt_value": 3.2,
		"carbon-footprint-from-known-ingredients_serving": 321,
		"saturated-fat": 1.5,
		"proteins_unit": "g",
		"carbon-footprint-from-known-ingredients_product": 1280,
		"energy-kcal_serving": 87,
		"carbohydrates_serving": 0,
		"fiber_100g": 0,
		"carbohydrates_unit": "g",
		"energy-kcal_unit": "kcal",
		"proteins_100g": 21,
		"fat_value": 10,
		"carbohydrates": 0,
		"sugars_serving": 0,
		"sugars_value": 0,
		"sodium_serving": 0.64
	},
	"quantity": "200gr",
	"attribute_groups": [{
		"attributes": [{
			"id": "nutriscore",
			"title": "Nutri-Score D",
			"name": "Nutri-Score",
			"description_short": "Poor nutritional quality",
			"match": 32.5,
			"icon_url": "https://static.openfoodfacts.org/images/misc/nutriscore-d.svg",
			"description": "",
			"status": "known"
		}, {
			"icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-salt-high.svg",
			"match": 0,
			"description_short": "3.2 g / 100 g",
			"status": "known",
			"id": "low_salt",
			"name": "Salt",
			"title": "Salt in high quantity"
		}, {
			"name": "Fat",
			"title": "Fat in moderate quantity",
			"id": "low_fat",
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-fat-medium.svg",
			"description_short": "10 g / 100 g",
			"match": 55.2941176470588
		}, {
			"title": "Sugars in low quantity",
			"name": "Sugars",
			"id": "low_sugars",
			"status": "known",
			"match": 100,
			"description_short": "0 g / 100 g",
			"icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-sugars-low.svg"
		}, {
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/nutrient-level-saturated-fat-medium.svg",
			"match": 80,
			"description_short": "1.5 g / 100 g",
			"title": "Saturated fat in moderate quantity",
			"name": "Saturated fat",
			"id": "low_saturated_fat"
		}],
		"id": "nutritional_quality",
		"name": "Nutritional quality"
	}, {
		"id": "allergens",
		"attributes": [{
			"title": "Does not contain: Milk",
			"name": "Milk",
			"id": "allergens_no_milk",
			"debug": "3 ingredients (0 unknown)",
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-milk.svg",
			"match": 100
		}, {
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-gluten.svg",
			"match": 100,
			"name": "Gluten",
			"title": "Does not contain: Gluten",
			"id": "allergens_no_gluten",
			"debug": "3 ingredients (0 unknown)"
		}, {
			"status": "known",
			"match": 100,
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-celery.svg",
			"title": "Does not contain: Celery",
			"name": "Celery",
			"id": "allergens_no_celery",
			"debug": "3 ingredients (0 unknown)"
		}, {
			"title": "Does not contain: Soybeans",
			"name": "Soybeans",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_soybeans",
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-soybeans.svg",
			"match": 100
		}, {
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-mustard.svg",
			"match": 100,
			"name": "Mustard",
			"title": "Does not contain: Mustard",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_mustard"
		}, {
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-none.svg",
			"match": 100,
			"status": "known",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_none",
			"title": "Does not contain: None",
			"name": "None"
		}, {
			"name": "Fish",
			"title": "Does not contain: Fish",
			"id": "allergens_no_fish",
			"debug": "3 ingredients (0 unknown)",
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-fish.svg",
			"match": 100
		}, {
			"name": "Molluscs",
			"title": "Does not contain: Molluscs",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_molluscs",
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-molluscs.svg",
			"match": 100
		}, {
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_crustaceans",
			"title": "Does not contain: Crustaceans",
			"name": "Crustaceans",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-crustaceans.svg",
			"match": 100,
			"status": "known"
		}, {
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_sesame_seeds",
			"title": "Does not contain: Sesame seeds",
			"name": "Sesame seeds",
			"match": 100,
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-sesame-seeds.svg",
			"status": "known"
		}, {
			"match": 100,
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-peanuts.svg",
			"status": "known",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_peanuts",
			"title": "Does not contain: Peanuts",
			"name": "Peanuts"
		}, {
			"status": "known",
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-lupin.svg",
			"match": 100,
			"name": "Lupin",
			"title": "Does not contain: Lupin",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_lupin"
		}, {
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-sulphur-dioxide-and-sulphites.svg",
			"match": 100,
			"status": "known",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_sulphur_dioxide_and_sulphites",
			"name": "Sulphur dioxide and sulphites",
			"title": "Does not contain: Sulphur dioxide and sulphites"
		}, {
			"title": "Does not contain: Nuts",
			"name": "Nuts",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_nuts",
			"status": "known",
			"match": 100,
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-nuts.svg"
		}, {
			"icon_url": "https://static.openfoodfacts.org/images/icons/no-eggs.svg",
			"match": 100,
			"status": "known",
			"debug": "3 ingredients (0 unknown)",
			"id": "allergens_no_eggs",
			"title": "Does not contain: Eggs",
			"name": "Eggs"
		}],
		"warning": "There is always a possibility that data about allergens may be missing, incomplete, incorrect or that the product's composition has changed. If you are allergic, always check the information on the actual product packaging.",
		"name": "Allergens"
	}, {
		"name": "Ingredients",
		"id": "ingredients_analysis",
		"attributes": [{
			"title": "Non-vegan",
			"status": "known",
			"name": "Vegan",
			"match": 0,
			"id": "vegan",
			"icon_url": "https://static.openfoodfacts.org/images/icons/non-vegan.svg"
		}, {
			"match": 0,
			"icon_url": "https://static.openfoodfacts.org/images/icons/non-vegetarian.svg",
			"id": "vegetarian",
			"title": "Non-vegetarian",
			"status": "known",
			"name": "Vegetarian"
		}, {
			"status": "known",
			"title": "Palm oil free",
			"id": "palm-oil-free",
			"icon_url": "https://static.openfoodfacts.org/images/icons/palm-oil-free.svg",
			"match": 100
		}]
	}, {
		"name": "Food processing",
		"id": "processing",
		"attributes": [{
			"match": 50,
			"description_short": "Processed foods",
			"icon_url": "https://static.openfoodfacts.org/images/misc/nova-group-3.svg",
			"description": "",
			"status": "known",
			"id": "nova",
			"title": "NOVA 3",
			"name": "NOVA group"
		}]
	}, {
		"name": "",
		"id": "ingredients",
		"attributes": [{
			"title": "Without additives",
			"status": "known",
			"name": "Additives",
			"match": 100,
			"id": "additives",
			"icon_url": "https://static.openfoodfacts.org/images/icons/0-additives.svg"
		}]
	}, {
		"id": "environment",
		"attributes": [{
			"status": "known",
			"match": 28.6396590441916,
			"description_short": "High environmental impact",
			"icon_url": "https://static.openfoodfacts.org/images/icons/ecoscore-d.svg",
			"description": "",
			"title": "Eco-Score D",
			"name": "Eco-Score",
			"id": "ecoscore"
		}, {
			"icon_url": "https://static.openfoodfacts.org/images/icons/forest-footprint-not-computed.svg",
			"description": "",
			"description_short": "Currently only for products with chicken or eggs",
			"match": 0,
			"status": "known",
			"id": "forest_footprint",
			"title": "Forest footprint not computed",
			"name": "Forest footprint"
		}],
		"name": "Environment"
	}, {
		"name": "Labels",
		"id": "labels",
		"attributes": [{
			"name": "Organic farming",
			"title": "Not an organic product",
			"id": "labels_organic",
			"status": "known",
			"description": "Organic farming aims to protect the environment and to conserve biodiversity by prohibiting or limiting the use of synthetic fertilizers, pesticides and food additives.",
			"icon_url": "https://static.openfoodfacts.org/images/icons/not-organic.svg",
			"description_short": "Organic products promote ecological sustainability and biodiversity.",
			"match": 0
		}, {
			"match": 0,
			"description_short": "Fair trade products help producers in developping countries.",
			"description": "When you buy fair trade products, producers in developing countries are paid an higher and fairer price, which helps them improve and sustain higher social and often environmental standards.",
			"icon_url": "https://static.openfoodfacts.org/images/icons/not-fair-trade.svg",
			"status": "known",
			"id": "labels_fair_trade",
			"name": "Fair trade",
			"title": "Not a fair trade product"
		}]
	}],
	"categories_tags": ["en:seafood", "en:fishes", "en:salmons", "en:smoked-fishes", "en:smoked-salmons", "en:smoked-salmons-from-farming"],
	"labels_tags": [],
	"additives_tags": [],
	"environment_impact_level_tags": [],
	"product_quantity": "200",
	"nutrient_levels": {
		"fat": "moderate",
		"salt": "high",
		"sugars": "low",
		"saturated-fat": "moderate"
	},
	"ingredients_analysis_tags": ["en:palm-oil-free", "en:non-vegan", "en:non-vegetarian"],
	"image_small_url": "https://static.openfoodfacts.org/images/products/20034658/front.4.200.jpg"
}

@stephanegigandet
Copy link
Contributor

Thanks for all the tests. I agree SQLite seems to be the best option for our needs.

@monsieurtanuki
Copy link
Contributor Author

@stephanegigandet Great! Let's start with SQLite then. I'll send a PR within a week.

We won't be stuck with SQLite forever anyway: as far as most developers are concerned, they will call a black-box that returns a product stored locally, from its barcode. Regardless of the type of database behind. If for some very good reason we feel appropriate in the future to switch to another type of database, nothing should stop us and it will be transparent for the developers.

More specifically about the new SQLite database, we'll create a "table" in it (that's the relational database terminology), that will contain all the (e.g. downloaded) products.
For obvious reasons, the "primary key" (unique, not null) will be a "column" name barcode, of type TEXT.
Another obvious column is the JSON version of the product, maybe called encoded_json, of type TEXT (maybe BLOB, I have to double-check that).
Another interesting column would be the last database insert/upsert timestamp, maybe called last_upsert, of type INT.

As we already saw, the extraction of the JSON part is more or less painful.

  • It's easy to say: give me those products from this barcode list, SQLite is built for that.
  • It's not easy to say: give me the products from the brand "Ferrero", because the brand is inside the JSON: we'll have to parse ALL products, decode the JSON and check if the brand matches.
  • => Are there obvious fields that are in the product class that we will definitely use for a query filter on the whole database? Like perhaps nutriscore or lastModified? It will be much easier to know in advance which fields are interesting, whenever we're filtering the whole database. When we're filtering a small subset it's not important: we'll select all the products anyway, and then apply the filtering/ordering in dart, not in SQLite. Anyway, it will still be possible to say "hey wait a minute, we definitely need to filter on brands, can we add it in the new version?", but it's better if we can avoid the corresponding refresh phase.

@stephanegigandet
Copy link
Contributor

At this point, I'm more thinking of the SQLite database as a big cache of product data, that we access only as a key (code) / value (json) cache. Whenever we need data for a specific product (that we scan, or that we have in a list like the history of scanned products), we look for the corresponding codes through the database (and through the API if we have some network).

Those lists will probably be reasonably small (less than 1000 products), so if we want to filter them, we can do it outside the database.

If we wanted to support a full offline search / exploration (like the current "category browser" that is in the app), then we could have separate fields for categories etc. but at this point this feature is very experimental and very likely to change. Let's do it through the OFF API first, and if there's really a need to make it available offline (which is less and less of a need as connectivity gets more ubiquitous), we can see that later.

We could consider adding a last access time (so that we can expire entries that are never accessed), but then that would mean updating the entries whenever we access them. Not sure if it's worth it.

A "source" field would be useful, so that we know if the entry was added through a scan, a search, a downloaded / initial database etc. Maybe also a field to indicate whether the product belongs to at least one list.

@monsieurtanuki
Copy link
Contributor Author

Ok, I'll stay with my 3 columns then for SQLite table product. Something like that:

create table product(
    barcode TEXT PRIMARY KEY,
    encoded_json TEXT NOT NULL,
    last_upsert INT NOT NULL
)

And about additional fields like "source", it's not related to a product, it's related to a product list:

  • scan product list: barcode 0123 was scanned by the user ten days ago
  • category product list: barcode 0123 (same value) is part of the "breakfast cereals" list downloaded yesterday
  • history product list: barcode 0123 (same value again)'s details were viewed today

@M123-dev
Copy link
Member

M123-dev commented Dec 31, 2020

I think it is more logical to make a list out of the last one.
This leads to a better history for and statistics for example how often a user scanned a certain produkt.

monsieurtanuki referenced this issue in monsieurtanuki/smooth-app Jan 1, 2021
…get's

New files:
* `barcode_product_query.dart`: API query / product by barcode (used to be in `full_products_database.dart`)
* `local_database.dart`: local SQLite database with one table (`product`)

Deleted files:
* `choose_page_model.dart`: moved the code to `StatefulWidget` `ChoosePage`
* `full_product_database.dart`: database code was deprecated; API fields is now in `ProductQuery`

Impacted files:
* `alternative_continuous_scan_page.dart`: now a `StatefulWidget`; refactored with `ContinuousScanPage`
* `build.gradle`: unrelated - lowered the Android `minSdkVersion` from `24` to `19`, in order to run the app on my old smartphone ;)
* `choose_page.dart`: refactoring as changed to `StatefulWidget`
* `continuous_scan_model.dart`: not a `ChangeNotifier` anymore - should be improved when we use product list
* `continuous_scan_page.dart`: now a `StatefulWidget`
* `group_product_query.dart`: minor refactoring due to the changes in `product_query.dart`
* `keywords_product_query.dart`: minor refactoring due to the changes in `product_query.dart`
* `main.dart`: added `LocalDatabase` to the providers; simplified the code; removed mentions to `SharedPreferences`
* `product_query.dart`: refactored a bit; moved here some fields from `full_product_database.dart`
* `product_query_model.dart`: now using `LocalDatabase`
* `product_query_page.dart`: now using `LocalDatabase`; refactoring
* `pubspec.yaml`: replaced `sembast with `sqflite`
monsieurtanuki referenced this issue in monsieurtanuki/smooth-app Jan 2, 2021
…el without Widgets

Impacted files:
* `alternative_continuous_scan_page.dart`: refactoring mainly due to changes in `ContinuousScanModel`
* `continuous_scan_model.dart`: now we don't deal with `Widget`s, only metadata, and we use a NotifyListeners method
* `continuous_scan_page.dart`: refactoring mainly due to changes in `ContinuousScanModel`
* `contribution_page.dart`: unrelated refactoring about the use of `UserPreferences`
* `local_database.dart`: added a temporary "dummy"`NotifyListeners` method, for the sake of demonstration - to be removed
* `smooth_product_carousel.dart`: now we build the `Widget`s from metadata, and we control here the carousel
@M123-dev
Copy link
Member

M123-dev commented Jan 4, 2021

I don't know if this has already been said somewhere, but I wanted to mention that scanning no longer works for me in the current master.

E/flutter (15852): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: FormatException: Invalid double
E/flutter (15852): null
E/flutter (15852): #0      double.parse (dart:core-patch/double_patch.dart:111:28)
E/flutter (15852): #1      AttributeGroups.fromJson (package:openfoodfacts/model/AttributeGroups.dart:67:27)
E/flutter (15852): #2      _$ProductFromJson (package:openfoodfacts/model/Product.g.dart:71:25)
E/flutter (15852): #3      new Product.fromJson (package:openfoodfacts/model/Product.dart:180:7)
E/flutter (15852): #4      _$ProductResultFromJson (package:openfoodfacts/model/ProductResult.g.dart:16:19)
E/flutter (15852): #5      new ProductResult.fromJson (package:openfoodfacts/model/ProductResult.dart:20:7)
E/flutter (15852): #6      OpenFoodAPIClient.getProduct (package:openfoodfacts/openfoodfacts.dart:143:42)

@PrimaelQuemerais
Copy link
Member

I don't know if this has already been said somewhere, but I wanted to mention that scanning no longer works for me in the current master.

E/flutter (15852): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: FormatException: Invalid double
E/flutter (15852): null
E/flutter (15852): #0      double.parse (dart:core-patch/double_patch.dart:111:28)
E/flutter (15852): #1      AttributeGroups.fromJson (package:openfoodfacts/model/AttributeGroups.dart:67:27)
E/flutter (15852): #2      _$ProductFromJson (package:openfoodfacts/model/Product.g.dart:71:25)
E/flutter (15852): #3      new Product.fromJson (package:openfoodfacts/model/Product.dart:180:7)
E/flutter (15852): #4      _$ProductResultFromJson (package:openfoodfacts/model/ProductResult.g.dart:16:19)
E/flutter (15852): #5      new ProductResult.fromJson (package:openfoodfacts/model/ProductResult.dart:20:7)
E/flutter (15852): #6      OpenFoodAPIClient.getProduct (package:openfoodfacts/openfoodfacts.dart:143:42)

This error comes from the openfoodfacts-dart plugin, will investigate

@monsieurtanuki
Copy link
Contributor Author

@PrimaelQuemerais I think it's fixed in openfoodfacts/openfoodfacts-dart#65, where the double match field is more safely loaded with JsonObject.parseDouble(json[_JSON_TAG_MATCH]) rather that double.parse(attribute['match'].toString()).
Is that normal that we get null for match: that's another story...
Hey, work on your exams instead ;)

@PrimaelQuemerais
Copy link
Member

Great! I will take a look at the PR.
Haha thanks I was just taking a break 😄

monsieurtanuki referenced this issue in monsieurtanuki/smooth-app Jan 7, 2021
monsieurtanuki referenced this issue in monsieurtanuki/smooth-app Jan 8, 2021
PrimaelQuemerais added a commit that referenced this issue Jan 8, 2021
feature/#55 - SQLite local database, and refactoring with StatefulWidget's
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants