Skip to content

Commit

Permalink
feat: openfoodfacts#2280 - DaoProduct's just migrated to sqflite (ope…
Browse files Browse the repository at this point in the history
…nfoodfacts#2289)

New files:
* `abstract_sql_dao.dart`: DAO abstraction for SQL.
* `bulk_deletable.dart`: Interface for bulk database deletes.
* `bulk_insertable.dart`: Interface for bulk database inserts.
* `bulk_manager.dart`: Manager for bulk database inserts and deletes.
* `dao_hive_product.dart`: moved code from the previous `hive` version of `DaoProduct`, renamed here `DaoHiveProduct`

Impacted files:
* `dao_product.dart`: `sqflite` version of `DaoProduct`, that is from now on the only one to be used
* `local_database.dart`: added `sqflite` and its one table so far, also named `DaoProduct` in order to minimize the changes everywhere else (the old `DaoProduct` being renamed `DaoHiveProduct`); migrated the product data from `hive` to `sqflite`
* `Podfile.lock`: wtf
* `pubspec.lock`: wtf
* `data_importer/pubspec.yaml`: updated the `sqflite` dependency to the same as in `smooth_app`
* `smooth_app/pubspec.yaml`: added a dependency to `sqflite`
  • Loading branch information
monsieurtanuki authored Jun 16, 2022
1 parent 347c39c commit 1662685
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 53 deletions.
6 changes: 1 addition & 5 deletions packages/data_importer/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ dependencies:
data_importer_shared:
path: ../data_importer_shared

# Custom SQLite plugin only with the Android impl
sqflite:
git:
url: 'https://github.com/g123k/sqflite_android_only.git'
path: 'sqflite'
sqflite: ^2.0.2+1

flutter_secure_storage: ^5.0.2
path: ^1.8.0
Expand Down
12 changes: 12 additions & 0 deletions packages/smooth_app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ PODS:
- Flutter
- flutter_secure_storage (3.3.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- google_mlkit_barcode_scanning (0.3.0):
- Flutter
- google_mlkit_commons
Expand Down Expand Up @@ -110,6 +113,9 @@ PODS:
- Sentry (~> 7.11.0)
- shared_preferences_ios (0.0.1):
- Flutter
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
- TOCropViewController (2.6.1)
- url_launcher_ios (0.0.1):
- Flutter
Expand All @@ -134,10 +140,12 @@ DEPENDENCIES:
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)

SPEC REPOS:
trunk:
- FMDB
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
Expand Down Expand Up @@ -196,6 +204,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sentry_flutter/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"

Expand All @@ -208,6 +218,7 @@ SPEC CHECKSUMS:
flutter_isolate: 0edf5081826d071adf21759d1eb10ff5c24503b5
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
google_mlkit_barcode_scanning: 56e88993b6c915ce7134f9d77cb5b2de2fca8cfa
google_mlkit_commons: e9070f57232c3a3e4bd42fdfa621bb1f4bb3e709
GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940
Expand Down Expand Up @@ -236,6 +247,7 @@ SPEC CHECKSUMS:
Sentry: 0c5cd63d714187b4a39c331c1f0eb04ba7868341
sentry_flutter: efb3df2c203cd03aad255892a8d628a458656d14
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de

Expand Down
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/database/abstract_sql_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:smooth_app/database/local_database.dart';

/// DAO abstraction for SQL.
abstract class AbstractSqlDao {
AbstractSqlDao(this.localDatabase);

final LocalDatabase localDatabase;
}
9 changes: 9 additions & 0 deletions packages/smooth_app/lib/database/bulk_deletable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:smooth_app/database/bulk_insertable.dart';

/// Interface for bulk database deletes.
///
/// cf. [BulkManager], [BulkInsertable]
abstract class BulkDeletable implements BulkInsertable {
/// "where" clause for delete in bulk mode
String getDeleteWhere(final List<dynamic> deleteWhereArgs);
}
12 changes: 12 additions & 0 deletions packages/smooth_app/lib/database/bulk_insertable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:smooth_app/database/bulk_manager.dart';

/// Interface for bulk database inserts.
///
/// cf. [BulkManager]
abstract class BulkInsertable {
/// Insert columns for bulk mode
List<String> getInsertColumns();

/// Table name
String getTableName();
}
87 changes: 87 additions & 0 deletions packages/smooth_app/lib/database/bulk_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'dart:async';
import 'dart:math';

import 'package:smooth_app/database/bulk_deletable.dart';
import 'package:smooth_app/database/bulk_insertable.dart';
import 'package:sqflite/sqflite.dart';

/// Manager for bulk database inserts and deletes.
///
/// In tests it looked 33% faster to use delete/insert rather than upsert
/// And of course it's much faster to perform bulk actions
/// rather than numerous single actions
/// cf. [BulkInsertable], [BulkDeletable]
class BulkManager {
/// Max number of parameters in a SQFlite query
///
/// cf. SQLITE_MAX_VARIABLE_NUMBER, "which defaults to 999"
// TODO(monsieurtanuki): find a way to retrieve this number from SQFlite system tables, cf. https://github.com/tekartik/sqflite/issues/663
static const int SQLITE_MAX_VARIABLE_NUMBER = 999;

/// Returns the number of inserted rows by optimized bulk insert
Future<int> insert({
required final BulkInsertable bulkInsertable,
required final List<dynamic> parameters,
required final DatabaseExecutor databaseExecutor,
}) async {
int result = 0;
final String tableName = bulkInsertable.getTableName();
final List<String> columnNames = bulkInsertable.getInsertColumns();
final int numCols = columnNames.length;
if (parameters.isEmpty) {
return result;
}
if (columnNames.isEmpty) {
throw Exception('There must be at least one column!');
}
if (parameters.length % numCols != 0) {
throw Exception(
'Parameter list size (${parameters.length}) cannot be divided by $numCols');
}
final String variables = '?${',?' * (columnNames.length - 1)}';
final int maxSlice = (SQLITE_MAX_VARIABLE_NUMBER ~/ numCols) * numCols;
for (int start = 0; start < parameters.length; start += maxSlice) {
final int size = min(parameters.length - start, maxSlice);
final int insertedRows = size ~/ numCols;
final int additionalRecordsNumber = -1 + insertedRows;
await databaseExecutor.rawInsert(
'insert into $tableName(${columnNames.join(',')}) '
'values($variables)${',($variables)' * additionalRecordsNumber}',
parameters.sublist(start, start + size),
);
result += insertedRows;
}
return result;
}

/// Returns the number of deleted rows by optimized bulk delete
Future<int> delete({
required final BulkDeletable bulkDeletable,
required final List<dynamic> parameters,
required final DatabaseExecutor databaseExecutor,
final List<dynamic>? additionalParameters,
}) async {
int result = 0;
final String tableName = bulkDeletable.getTableName();
if (parameters.isEmpty) {
return result;
}
final int maxSlice =
SQLITE_MAX_VARIABLE_NUMBER - (additionalParameters?.length ?? 0);
for (int start = 0; start < parameters.length; start += maxSlice) {
final int size = min(parameters.length - start, maxSlice);
final List<dynamic> currentParameters = <dynamic>[];
if (additionalParameters != null && additionalParameters.isNotEmpty) {
currentParameters.addAll(additionalParameters);
}
currentParameters.addAll(parameters.sublist(start, start + size));
final int deleted = await databaseExecutor.delete(
tableName,
where: bulkDeletable.getDeleteWhere(currentParameters),
whereArgs: currentParameters,
);
result += deleted;
}
return result;
}
}
76 changes: 76 additions & 0 deletions packages/smooth_app/lib/database/dao_hive_product.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:async';
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:smooth_app/database/abstract_dao.dart';
import 'package:smooth_app/database/local_database.dart';

/// Hive type adapter for [Product]
class _ProductAdapter extends TypeAdapter<Product> {
@override
final int typeId = 1;

@override
Product read(BinaryReader reader) =>
Product.fromJson(jsonDecode(reader.readString()) as Map<String, dynamic>);

@override
void write(BinaryWriter writer, Product obj) =>
writer.writeString(jsonEncode(obj.toJson()));
}

/// Where we store the products as "barcode => product".
@Deprecated('use [DaoProduct] instead')
class DaoHiveProduct extends AbstractDao {
@Deprecated('use [DaoProduct] instead')
DaoHiveProduct(final LocalDatabase localDatabase) : super(localDatabase);

static const String _hiveBoxName = 'products';

@override
Future<void> init() async => Hive.openLazyBox<Product>(_hiveBoxName);

@override
void registerAdapter() => Hive.registerAdapter(_ProductAdapter());

LazyBox<Product> _getBox() => Hive.lazyBox<Product>(_hiveBoxName);

Future<Product?> get(final String barcode) async => _getBox().get(barcode);

Future<Map<String, Product>> getAll(final List<String> barcodes) async {
final LazyBox<Product> box = _getBox();
final Map<String, Product> result = <String, Product>{};
for (final String barcode in barcodes) {
final Product? product = await box.get(barcode);
if (product != null) {
result[barcode] = product;
}
}
return result;
}

Future<void> put(final Product product) async => putAll(<Product>[product]);

Future<void> putAll(final Iterable<Product> products) async {
final Map<String, Product> upserts = <String, Product>{};
for (final Product product in products) {
upserts[product.barcode!] = product;
}
await _getBox().putAll(upserts);
}

Future<List<String>> getAllKeys() async {
final LazyBox<Product> box = _getBox();
final List<String> result = <String>[];
for (final dynamic key in box.keys) {
result.add(key.toString());
}
return result;
}

// Just for the migration
Future<void> deleteAll(final List<String> barcodes) async {
final LazyBox<Product> box = _getBox();
await box.deleteAll(barcodes);
}
}
Loading

0 comments on commit 1662685

Please sign in to comment.