diff --git a/mobile/lib/db/device_files_db.dart b/mobile/lib/db/device_files_db.dart index 25c88daca1..fbd1649e26 100644 --- a/mobile/lib/db/device_files_db.dart +++ b/mobile/lib/db/device_files_db.dart @@ -22,61 +22,55 @@ extension DeviceFiles on FilesDB { ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore, }) async { debugPrint("Inserting missing PathIDToLocalIDMapping"); - final db = await database; - var batch = db.batch(); + final parameterSets = >[]; int batchCounter = 0; for (MapEntry e in mappingToAdd.entries) { final String pathID = e.key; for (String localID in e.value) { + parameterSets.add([localID, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await _insertBatch(parameterSets, conflictAlgorithm); + parameterSets.clear(); batchCounter = 0; } - batch.insert( - "device_files", - { - "id": localID, - "path_id": pathID, - }, - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } } - await batch.commit(noResult: true); + await _insertBatch(parameterSets, conflictAlgorithm); + parameterSets.clear(); + batchCounter = 0; } Future deletePathIDToLocalIDMapping( Map> mappingsToRemove, ) async { debugPrint("removing PathIDToLocalIDMapping"); - final db = await database; - var batch = db.batch(); + final parameterSets = >[]; int batchCounter = 0; for (MapEntry e in mappingsToRemove.entries) { final String pathID = e.key; + for (String localID in e.value) { + parameterSets.add([localID, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await _deleteBatch(parameterSets); + parameterSets.clear(); batchCounter = 0; } - batch.delete( - "device_files", - where: 'id = ? AND path_id = ?', - whereArgs: [localID, pathID], - ); - batchCounter++; } } - await batch.commit(noResult: true); + await _deleteBatch(parameterSets); + parameterSets.clear(); + batchCounter = 0; } Future> getDevicePathIDToImportedFileCount() async { try { - final db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT count(*) as count, path_id FROM device_files @@ -96,8 +90,8 @@ extension DeviceFiles on FilesDB { Future>> getDevicePathIDToLocalIDMap() async { try { - final db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT id, path_id FROM device_files; ''', ); final result = >{}; @@ -116,8 +110,8 @@ extension DeviceFiles on FilesDB { } Future> getDevicePathIDs() async { - final Database db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT id FROM device_collections ''', @@ -133,34 +127,42 @@ extension DeviceFiles on FilesDB { List localPathAssets, { bool shouldAutoBackup = false, }) async { - final Database db = await database; + final db = await sqliteAsyncDB; final Map> pathIDToLocalIDsMap = {}; try { - final batch = db.batch(); final Set existingPathIds = await getDevicePathIDs(); + final parameterSetsForUpdate = >[]; + final parameterSetsForInsert = >[]; for (LocalPathAsset localPathAsset in localPathAssets) { if (localPathAsset.localIDs.isNotEmpty) { pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs; } if (existingPathIds.contains(localPathAsset.pathID)) { - batch.rawUpdate( - "UPDATE device_collections SET name = ? where id = " - "?", - [localPathAsset.pathName, localPathAsset.pathID], - ); + parameterSetsForUpdate + .add([localPathAsset.pathName, localPathAsset.pathID]); } else if (localPathAsset.localIDs.isNotEmpty) { - batch.insert( - "device_collections", - { - "id": localPathAsset.pathID, - "name": localPathAsset.pathName, - "should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse, - }, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); + parameterSetsForInsert.add([ + localPathAsset.pathID, + localPathAsset.pathName, + shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse, + ]); } } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + INSERT OR IGNORE INTO device_collections (id, name, should_backup) VALUES (?, ?, ?); + ''', + parameterSetsForInsert, + ); + + await db.executeBatch( + ''' + UPDATE device_collections SET name = ? WHERE id = ?; + ''', + parameterSetsForUpdate, + ); + // add the mappings for localIDs if (pathIDToLocalIDsMap.isNotEmpty) { await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap); @@ -177,7 +179,7 @@ extension DeviceFiles on FilesDB { }) async { bool hasUpdated = false; try { - final Database db = await database; + final db = await sqliteAsyncDB; final Set existingPathIds = await getDevicePathIDs(); for (Tuple2 tup in devicePathInfo) { final AssetPathEntity pathEntity = tup.item1; @@ -185,35 +187,42 @@ extension DeviceFiles on FilesDB { final String localID = tup.item2; final bool shouldUpdate = existingPathIds.contains(pathEntity.id); if (shouldUpdate) { - final rowUpdated = await db.rawUpdate( - "UPDATE device_collections SET name = ?, cover_id = ?, count" - " = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)", - [ - pathEntity.name, - localID, - assetCount, - pathEntity.id, - pathEntity.name, - localID, - assetCount, - ], - ); + final rowUpdated = await db.writeTransaction((tx) async { + await tx.execute( + "UPDATE device_collections SET name = ?, cover_id = ?, count" + " = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)", + [ + pathEntity.name, + localID, + assetCount, + pathEntity.id, + pathEntity.name, + localID, + assetCount, + ], + ); + final result = await tx.get("SELECT changes();"); + return result["changes()"] as int; + }); + if (rowUpdated > 0) { _logger.fine("Updated $rowUpdated rows for ${pathEntity.name}"); hasUpdated = true; } } else { hasUpdated = true; - await db.insert( - "device_collections", - { - "id": pathEntity.id, - "name": pathEntity.name, - "count": assetCount, - "cover_id": localID, - "should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse, - }, - conflictAlgorithm: ConflictAlgorithm.ignore, + await db.execute( + ''' + INSERT INTO device_collections (id, name, count, cover_id, should_backup) + VALUES (?, ?, ?, ?, ?); + ''', + [ + pathEntity.id, + pathEntity.name, + assetCount, + localID, + shouldBackup ? _sqlBoolTrue : _sqlBoolFalse, + ], ); } } @@ -231,15 +240,17 @@ extension DeviceFiles on FilesDB { // feature, where we delete files which are backed up. Deleting such // entries here result in us losing out on the information that // those folders were marked for automatic backup. - await db.delete( - "device_collections", - where: 'id = ? and should_backup = $_sqlBoolFalse ', - whereArgs: [pathID], + await db.execute( + ''' + DELETE FROM device_collections WHERE id = ? AND should_backup = $_sqlBoolFalse; + ''', + [pathID], ); - await db.delete( - "device_files", - where: 'path_id = ?', - whereArgs: [pathID], + await db.execute( + ''' + DELETE FROM device_files WHERE path_id = ?; + ''', + [pathID], ); } } @@ -253,8 +264,8 @@ extension DeviceFiles on FilesDB { // getDeviceSyncCollectionIDs returns the collectionIDs for the // deviceCollections which are marked for auto-backup Future> getDeviceSyncCollectionIDs() async { - final Database db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT collection_id FROM device_collections where should_backup = $_sqlBoolTrue @@ -268,40 +279,47 @@ extension DeviceFiles on FilesDB { return result; } - Future updateDevicePathSyncStatus(Map syncStatus) async { - final db = await database; - var batch = db.batch(); + Future updateDevicePathSyncStatus( + Map syncStatus, + ) async { + final db = await sqliteAsyncDB; int batchCounter = 0; + final parameterSets = >[]; for (MapEntry e in syncStatus.entries) { final String pathID = e.key; + parameterSets.add([e.value ? _sqlBoolTrue : _sqlBoolFalse, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await db.executeBatch( + ''' + UPDATE device_collections SET should_backup = ? WHERE id = ?; + ''', + parameterSets, + ); + parameterSets.clear(); batchCounter = 0; } - batch.update( - "device_collections", - { - "should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse, - }, - where: 'id = ?', - whereArgs: [pathID], - ); - batchCounter++; } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + UPDATE device_collections SET should_backup = ? WHERE id = ?; + ''', + parameterSets, + ); } Future updateDeviceCollection( String pathID, int collectionID, ) async { - final db = await database; - await db.update( - "device_collections", - {"collection_id": collectionID}, - where: 'id = ?', - whereArgs: [pathID], + final db = await sqliteAsyncDB; + await db.execute( + ''' + UPDATE device_collections SET collection_id = ? WHERE id = ?; + ''', + [collectionID, pathID], ); return; } @@ -314,7 +332,7 @@ extension DeviceFiles on FilesDB { int? limit, bool? asc, }) async { - final db = await database; + final db = await sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); final String rawQuery = ''' SELECT * @@ -329,7 +347,7 @@ extension DeviceFiles on FilesDB { ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order ''' + (limit != null ? ' limit $limit;' : ';'); - final results = await db.rawQuery(rawQuery); + final results = await db.getAll(rawQuery); final files = convertToFiles(results); final dedupe = deduplicateByLocalID(files); return FileLoadResult(dedupe, files.length == limit); @@ -339,7 +357,7 @@ extension DeviceFiles on FilesDB { String pathID, int ownerID, ) async { - final db = await database; + final db = await sqliteAsyncDB; const String rawQuery = ''' SELECT ${FilesDB.columnLocalID}, ${FilesDB.columnUploadedFileID}, ${FilesDB.columnFileSize} @@ -351,7 +369,7 @@ extension DeviceFiles on FilesDB { ${FilesDB.columnLocalID} IN (SELECT id FROM device_files where path_id = ?) '''; - final results = await db.rawQuery(rawQuery, [ownerID, pathID]); + final results = await db.getAll(rawQuery, [ownerID, pathID]); final localIDs = {}; final uploadedIDs = {}; int localSize = 0; @@ -375,17 +393,17 @@ extension DeviceFiles on FilesDB { "$includeCoverThumbnail", ); try { - final db = await database; + final db = await sqliteAsyncDB; final coverFiles = []; if (includeCoverThumbnail) { - final fileRows = await db.rawQuery( + final fileRows = await db.getAll( '''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id; ''', ); final files = convertToFiles(fileRows); coverFiles.addAll(files); } - final deviceCollectionRows = await db.rawQuery( + final deviceCollectionRows = await db.getAll( '''SELECT * from device_collections''', ); final List deviceCollections = []; @@ -433,8 +451,8 @@ extension DeviceFiles on FilesDB { Future getDeviceCollectionThumbnail(String pathID) async { debugPrint("Call fallback method to get potential thumbnail"); - final db = await database; - final fileRows = await db.rawQuery( + final db = await sqliteAsyncDB; + final fileRows = await db.getAll( '''SELECT * FROM FILES f JOIN device_files df on f.local_id = df.id and df.path_id= ? order by f.creation_time DESC limit 1; ''', @@ -447,4 +465,28 @@ extension DeviceFiles on FilesDB { return null; } } + + Future _insertBatch( + List> parameterSets, + ConflictAlgorithm conflictAlgorithm, + ) async { + final db = await sqliteAsyncDB; + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO device_files (id, path_id) VALUES (?, ?); + ''', + parameterSets, + ); + } + + Future _deleteBatch(List> parameterSets) async { + final db = await sqliteAsyncDB; + await db.executeBatch( + ''' + DELETE FROM device_files WHERE id = ? AND path_id = ?; + ''', + parameterSets, + ); + } } diff --git a/mobile/lib/db/entities_db.dart b/mobile/lib/db/entities_db.dart index cee32641a0..3cd9d47639 100644 --- a/mobile/lib/db/entities_db.dart +++ b/mobile/lib/db/entities_db.dart @@ -10,53 +10,78 @@ extension EntitiesDB on FilesDB { ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, }) async { debugPrint("entitiesDB: upsertEntities ${data.length} entities"); - final db = await database; - var batch = db.batch(); + final db = await sqliteAsyncDB; + final parameterSets = >[]; int batchCounter = 0; for (LocalEntityData e in data) { + parameterSets.add([ + e.id, + e.type.name, + e.ownerID, + e.data, + e.updatedAt, + ]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO entities (id, type, ownerID, data, updatedAt) + VALUES (?, ?, ?, ?, ?) +''', + parameterSets, + ); + parameterSets.clear(); batchCounter = 0; } - batch.insert( - "entities", - e.toJson(), - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } - await batch.commit(noResult: true); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO entities (id, type, ownerID, data, updatedAt) + VALUES (?, ?, ?, ?, ?) +''', + parameterSets, + ); } Future deleteEntities( List ids, ) async { - final db = await database; - var batch = db.batch(); + final db = await sqliteAsyncDB; + final parameterSets = >[]; int batchCounter = 0; for (String id in ids) { - if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); - batchCounter = 0; - } - batch.delete( - "entities", - where: "id = ?", - whereArgs: [id], + parameterSets.add( + [id], ); batchCounter++; + + if (batchCounter == 400) { + await db.executeBatch( + ''' + DELETE FROM entities WHERE id = ? + ''', + parameterSets, + ); + parameterSets.clear(); + batchCounter = 0; + } } - await batch.commit(noResult: true); + await db.executeBatch( + ''' + DELETE FROM entities WHERE id = ? + ''', + parameterSets, + ); } Future> getEntities(EntityType type) async { - final db = await database; - final List> maps = await db.query( - "entities", - where: "type = ?", - whereArgs: [type.typeToString()], + final db = await sqliteAsyncDB; + final List> maps = await db.getAll( + 'SELECT * FROM entities WHERE type = ?', + [type.name], ); return List.generate(maps.length, (i) { return LocalEntityData.fromJson(maps[i]); @@ -64,11 +89,10 @@ extension EntitiesDB on FilesDB { } Future getEntity(EntityType type, String id) async { - final db = await database; - final List> maps = await db.query( - "entities", - where: "type = ? AND id = ?", - whereArgs: [type.typeToString(), id], + final db = await sqliteAsyncDB; + final List> maps = await db.getAll( + 'SELECT * FROM entities WHERE type = ? AND id = ?', + [type.name, id], ); if (maps.isEmpty) { return null; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index f72ecb32a4..c6ba617e0e 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1,3 +1,4 @@ +import "dart:async"; import "dart:io"; import "package:computer/computer.dart"; @@ -14,9 +15,9 @@ import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/common_keys.dart"; import "package:photos/services/filter/db_filters.dart"; import 'package:photos/utils/file_uploader_util.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:sqflite_migration/sqflite_migration.dart'; -import 'package:sqlite_async/sqlite_async.dart' as sqlite_async; +import "package:photos/utils/primitive_wrapper.dart"; +import "package:photos/utils/sqlite_util.dart"; +import 'package:sqlite_async/sqlite_async.dart'; class FilesDB { /* @@ -72,10 +73,11 @@ class FilesDB { // we need to write query based on that field static const columnMMdVisibility = 'mmd_visibility'; - static final initializationScript = [ - ...createTable(filesTable), - ]; +//If adding or removing a new column, make sure to update the `_columnNames` list +//and update `_generateColumnsAndPlaceholdersForInsert` and +//`_generateUpdateAssignmentsWithPlaceholders` static final migrationScripts = [ + ...createTable(filesTable), ...alterDeviceFolderToAllowNULL(), ...alterTimestampColumnTypes(), ...addIndices(), @@ -90,10 +92,38 @@ class FilesDB { ...addAddedTime(), ]; - final dbConfig = MigrationConfig( - initializationScript: initializationScript, - migrationScripts: migrationScripts, - ); + static const List _columnNames = [ + columnGeneratedID, + columnLocalID, + columnUploadedFileID, + columnOwnerID, + columnCollectionID, + columnTitle, + columnDeviceFolder, + columnLatitude, + columnLongitude, + columnFileType, + columnModificationTime, + columnEncryptedKey, + columnKeyDecryptionNonce, + columnFileDecryptionHeader, + columnThumbnailDecryptionHeader, + columnMetadataDecryptionHeader, + columnCreationTime, + columnUpdationTime, + columnFileSubType, + columnDuration, + columnExif, + columnHash, + columnMetadataVersion, + columnMMdEncodedJson, + columnMMdVersion, + columnMMdVisibility, + columnPubMMdEncodedJson, + columnPubMMdVersion, + columnFileSize, + columnAddedTime, + ]; // make this a singleton class FilesDB._privateConstructor(); @@ -101,36 +131,46 @@ class FilesDB { static final FilesDB instance = FilesDB._privateConstructor(); // only have a single app-wide reference to the database - static Future? _dbFuture; - static Future? _sqliteAsyncDBFuture; + static Future? _sqliteAsyncDBFuture; - @Deprecated("Use sqliteAsyncDB instead (sqlite_async)") - Future get database async { + Future get sqliteAsyncDB async { // lazily instantiate the db the first time it is accessed - _dbFuture ??= _initDatabase(); - return _dbFuture!; - } - - Future get sqliteAsyncDB async { _sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase(); return _sqliteAsyncDBFuture!; } // this opens the database (and creates it if it doesn't exist) - Future _initDatabase() async { + Future _initSqliteAsyncDatabase() async { final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); _logger.info("DB path " + path); - return await openDatabaseWithMigration(path, dbConfig); + final database = SqliteDatabase(path: path); + await _migrate(database); + + return database; } - Future _initSqliteAsyncDatabase() async { - final Directory documentsDirectory = - await getApplicationDocumentsDirectory(); - final String path = join(documentsDirectory.path, _databaseName); - _logger.info("DB path " + path); - return sqlite_async.SqliteDatabase(path: path); + Future _migrate( + SqliteDatabase database, + ) async { + final result = await database.execute('PRAGMA user_version'); + final currentVersion = result[0]['user_version'] as int; + final toVersion = migrationScripts.length; + + if (currentVersion < toVersion) { + _logger.info("Migrating database from $currentVersion to $toVersion"); + await database.writeTransaction((tx) async { + for (int i = currentVersion + 1; i <= toVersion; i++) { + await tx.execute(migrationScripts[i - 1]); + } + await tx.execute('PRAGMA user_version = $toVersion'); + }); + } else if (currentVersion > toVersion) { + throw AssertionError( + "currentVersion($currentVersion) cannot be greater than toVersion($toVersion)", + ); + } } // SQL code to create the database table @@ -400,11 +440,11 @@ class FilesDB { } Future clearTable() async { - final db = await instance.database; - await db.delete(filesTable); - await db.delete("device_files"); - await db.delete("device_collections"); - await db.delete("entities"); + final db = await instance.sqliteAsyncDB; + await db.execute('DELETE FROM $filesTable'); + await db.execute('DELETE FROM device_files'); + await db.execute('DELETE FROM device_collections'); + await db.execute('DELETE FROM entities'); } Future deleteDB() async { @@ -414,32 +454,75 @@ class FilesDB { await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); File(path).deleteSync(recursive: true); - _dbFuture = null; + _sqliteAsyncDBFuture = null; } } Future insertMultiple( List files, { - ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, + SqliteAsyncConflictAlgorithm conflictAlgorithm = + SqliteAsyncConflictAlgorithm.replace, }) async { + if (files.isEmpty) return; + final startTime = DateTime.now(); - final db = await database; - var batch = db.batch(); - int batchCounter = 0; + final db = await sqliteAsyncDB; + + ///Strong batch counter in an object so that it gets passed by reference + ///Primitives are passed by value + final genIdNotNullbatchCounter = PrimitiveWrapper(0); + final genIdNullbatchCounter = PrimitiveWrapper(0); + final genIdNullParameterSets = >[]; + final genIdNotNullParameterSets = >[]; + + final genIdNullcolumnNames = + _columnNames.where((element) => element != columnGeneratedID); + for (EnteFile file in files) { - if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); - batchCounter = 0; + final fileGenIdIsNull = file.generatedID == null; + + if (!fileGenIdIsNull) { + await _batchAndInsertFile( + file, + conflictAlgorithm, + db, + genIdNotNullParameterSets, + genIdNotNullbatchCounter, + isGenIdNull: fileGenIdIsNull, + ); + } else { + await _batchAndInsertFile( + file, + conflictAlgorithm, + db, + genIdNullParameterSets, + genIdNullbatchCounter, + isGenIdNull: fileGenIdIsNull, + ); } - batch.insert( - filesTable, - _getRowForFile(file), - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } - await batch.commit(noResult: true); + + if (genIdNotNullbatchCounter.value > 0) { + await _insertBatch( + conflictAlgorithm, + _columnNames, + db, + genIdNotNullParameterSets, + ); + genIdNotNullbatchCounter.value = 0; + genIdNotNullParameterSets.clear(); + } + if (genIdNullbatchCounter.value > 0) { + await _insertBatch( + conflictAlgorithm, + genIdNullcolumnNames, + db, + genIdNullParameterSets, + ); + genIdNullbatchCounter.value = 0; + genIdNullParameterSets.clear(); + } + final endTime = DateTime.now(); final duration = Duration( microseconds: @@ -454,16 +537,35 @@ class FilesDB { ); } - Future insert(EnteFile file) async { + Future insert(EnteFile file) async { _logger.info("Inserting $file"); - final db = await instance.database; - return db.insert( - filesTable, - _getRowForFile(file), - conflictAlgorithm: ConflictAlgorithm.replace, + final db = await instance.sqliteAsyncDB; + final columnsAndPlaceholders = + _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); + final values = _getParameterSetForFile(file); + + await db.execute( + 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', + values, ); } + Future insertAndGetId(EnteFile file) async { + _logger.info("Inserting $file"); + final db = await instance.sqliteAsyncDB; + final columnsAndPlaceholders = + _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); + final values = _getParameterSetForFile(file); + return await db.writeTransaction((tx) async { + await tx.execute( + 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', + values, + ); + final result = await tx.get('SELECT last_insert_rowid()'); + return result["last_insert_rowid()"] as int; + }); + } + Future getFile(int generatedID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( @@ -522,13 +624,11 @@ class FilesDB { Future<(Set, Map)> getUploadAndHash( int collectionID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - columns: [columnUploadedFileID, columnHash], - where: - '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', - whereArgs: [ + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT $columnUploadedFileID, $columnHash FROM $filesTable' + ' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', + [ collectionID, ], ); @@ -801,15 +901,13 @@ class FilesDB { // Files which user added to a collection manually but they are not // uploaded yet or files belonging to a collection which is marked for backup Future> getFilesPendingForUpload() async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: - '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND ' - '$columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND ' - '$columnLocalID IS NOT NULL AND $columnLocalID IS NOT -1', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' + '$columnUploadedFileID IS -1) AND $columnCollectionID IS NOT NULL AND ' + '$columnCollectionID IS NOT -1 AND $columnLocalID IS NOT NULL AND ' + '$columnLocalID IS NOT -1 GROUP BY $columnLocalID ' + 'ORDER BY $columnCreationTime DESC', ); final files = convertToFiles(results); // future-safe filter just to ensure that the query doesn't end up returning files @@ -824,29 +922,23 @@ class FilesDB { } Future> getUnUploadedLocalFiles() async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: - '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' + '$columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL ' + 'GROUP BY $columnLocalID ORDER BY $columnCreationTime DESC', ); return convertToFiles(results); } Future> getUploadedFileIDsToBeUpdated(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnUploadedFileID], - where: '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' - '($columnUploadedFileID ' - 'IS NOT ' - 'NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)', - whereArgs: [ownerID], - orderBy: '$columnCreationTime DESC', - distinct: true, + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnUploadedFileID FROM $filesTable WHERE ' + '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' + '($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' + 'AND $columnUpdationTime IS NULL) ORDER BY $columnCreationTime DESC ', + [ownerID], ); final uploadedFileIDs = []; for (final row in rows) { @@ -859,15 +951,11 @@ class FilesDB { int uploadedFileID, int userID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' - '$columnUploadedFileID = ?', - whereArgs: [ - userID, - uploadedFileID, - ], + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnLocalID IS NOT NULL AND ' + '$columnOwnerID = ? AND $columnUploadedFileID = ?', + [userID, uploadedFileID], ); if (results.isEmpty) { return []; @@ -876,14 +964,12 @@ class FilesDB { } Future> getExistingLocalFileIDs(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' - '$columnOwnerID = ?)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' + '$columnOwnerID = ?)', + [ownerID], ); final result = {}; for (final row in rows) { @@ -893,16 +979,13 @@ class FilesDB { } Future> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' - 'AND ' - '$columnCollectionID != -1) AND ($columnOwnerID = ? OR ' - '$columnOwnerID IS NULL)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' + 'AND $columnCollectionID != -1) AND ($columnOwnerID = ? OR ' + '$columnOwnerID IS NULL)', + [ownerID], ); final result = {}; for (final row in rows) { @@ -912,12 +995,11 @@ class FilesDB { } Future> getLocalFileIDsForCollection(int collectionID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - where: '$columnLocalID IS NOT NULL AND $columnCollectionID = ?', - whereArgs: [collectionID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND $columnCollectionID = ?', + [collectionID], ); final result = {}; for (final row in rows) { @@ -928,17 +1010,17 @@ class FilesDB { // Sets the collectionID for the files with given LocalIDs if the // corresponding file entries are not already mapped to some other collection - Future setCollectionIDForUnMappedLocalFiles( + Future setCollectionIDForUnMappedLocalFiles( int collectionID, Set localIDs, ) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; String inParam = ""; for (final localID in localIDs) { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - return await db.rawUpdate( + await db.execute( ''' UPDATE $filesTable SET $columnCollectionID = $collectionID @@ -948,7 +1030,7 @@ class FilesDB { ); } - Future markFilesForReUpload( + Future markFilesForReUpload( int ownerID, String localID, String? title, @@ -957,22 +1039,30 @@ class FilesDB { int modificationTime, FileType fileType, ) async { - final db = await instance.database; - return await db.update( - filesTable, - { - columnTitle: title, - columnLatitude: location?.latitude, - columnLongitude: location?.longitude, - columnCreationTime: creationTime, - columnModificationTime: modificationTime, - // #hack reset updation time to null for re-upload - columnUpdationTime: null, - columnFileType: getInt(fileType), - }, - where: - '$columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL)', - whereArgs: [localID, ownerID], + final db = await instance.sqliteAsyncDB; + + await db.execute( + ''' + UPDATE $filesTable + SET $columnTitle = ?, + $columnLatitude = ?, + $columnLongitude = ?, + $columnCreationTime = ?, + $columnModificationTime = ?, + $columnUpdationTime = NULL, + $columnFileType = ? + WHERE $columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL); + ''', + [ + title, + location?.latitude, + location?.longitude, + creationTime, + modificationTime, + getInt(fileType), + localID, + ownerID, + ], ); } @@ -987,12 +1077,12 @@ class FilesDB { required String title, required String deviceFolder, }) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; // on iOS, match using localID and fileType. title can either match or // might be null based on how the file was imported - String whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND - $columnLocalID = ? AND $columnFileType = ? AND - ($columnTitle=? OR $columnTitle IS NULL) '''; + String query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? + OR $columnOwnerID IS NULL) AND $columnLocalID = ? + AND $columnFileType = ? AND ($columnTitle=? OR $columnTitle IS NULL) '''; List whereArgs = [ ownerID, localID, @@ -1000,9 +1090,9 @@ class FilesDB { title, ]; if (Platform.isAndroid) { - whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND - $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ? - '''; + query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? OR + $columnOwnerID IS NULL) AND $columnLocalID = ? AND $columnFileType = ? + AND $columnTitle=? AND $columnDeviceFolder= ? '''; whereArgs = [ ownerID, localID, @@ -1012,10 +1102,9 @@ class FilesDB { ]; } - final rows = await db.query( - filesTable, - where: whereClause, - whereArgs: whereArgs, + final rows = await db.getAll( + query, + whereArgs, ); return convertToFiles(rows); @@ -1037,7 +1126,7 @@ class FilesDB { return {}; } final inParam = hashes.map((e) => "'$e'").join(','); - final rows = await db.execute(''' + final rows = await db.getAll(''' SELECT * FROM $filesTable WHERE $columnHash IN ($inParam) AND $columnOwnerID = $userID; '''); final matchedFiles = convertToFiles(rows); @@ -1053,14 +1142,12 @@ class FilesDB { if (fileType == FileType.livePhoto && hashData.zipHash != null) { inParam += ",'${hashData.zipHash}'"; } - final db = await instance.database; - final rows = await db.query( - filesTable, - where: '($columnUploadedFileID != NULL OR $columnUploadedFileID != -1) ' - 'AND $columnOwnerID = ? AND $columnFileType =' - ' ? ' - 'AND $columnHash IN ($inParam)', - whereArgs: [ + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID != NULL OR ' + '$columnUploadedFileID != -1) AND $columnOwnerID = ? AND ' + '$columnFileType = ? AND $columnHash IN ($inParam)', + [ ownerID, getInt(fileType), ], @@ -1068,87 +1155,90 @@ class FilesDB { return convertToFiles(rows); } - Future update(EnteFile file) async { - final db = await instance.database; - return await db.update( - filesTable, - _getRowForFile(file), - where: '$columnGeneratedID = ?', - whereArgs: [file.generatedID], + Future update(EnteFile file) async { + final db = await instance.sqliteAsyncDB; + final parameterSet = _getParameterSetForFile(file)..add(file.generatedID); + final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( + fileGenId: file.generatedID, + ); + await db.execute( + 'UPDATE $filesTable ' + 'SET $updateAssignments WHERE $columnGeneratedID = ?', + parameterSet, ); } - Future updateUploadedFileAcrossCollections(EnteFile file) async { - final db = await instance.database; - return await db.update( - filesTable, - _getRowForFileWithoutCollection(file), - where: '$columnUploadedFileID = ?', - whereArgs: [file.uploadedFileID], + Future updateUploadedFileAcrossCollections(EnteFile file) async { + final db = await instance.sqliteAsyncDB; + final parameterSet = _getParameterSetForFile(file, omitCollectionId: true) + ..add(file.uploadedFileID); + final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( + fileGenId: file.generatedID, + omitCollectionId: true, + ); + await db.execute( + 'UPDATE $filesTable' + 'SET $updateAssignments WHERE $columnUploadedFileID = ?', + parameterSet, ); } - Future updateLocalIDForUploaded(int uploadedID, String localID) async { - final db = await instance.database; - return await db.update( - filesTable, - {columnLocalID: localID}, - where: '$columnUploadedFileID = ? AND $columnLocalID IS NULL', - whereArgs: [uploadedID], + Future updateLocalIDForUploaded(int uploadedID, String localID) async { + final db = await instance.sqliteAsyncDB; + await db.execute( + 'UPDATE $filesTable SET $columnLocalID = ? WHERE $columnUploadedFileID = ?' + ' AND $columnLocalID IS NULL', + [localID, uploadedID], ); } - Future delete(int uploadedFileID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnUploadedFileID =?', - whereArgs: [uploadedFileID], + Future deleteByGeneratedID(int genID) async { + final db = await instance.sqliteAsyncDB; + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID = ?', + [genID], ); } - Future deleteByGeneratedID(int genID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnGeneratedID =?', - whereArgs: [genID], + Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { + final db = await instance.sqliteAsyncDB; + final inParam = uploadedFileIDs.join(','); + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); } - Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { - final db = await instance.database; - return await db.delete( - filesTable, - where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', - ); - } - - Future deleteMultipleByGeneratedIDs(List generatedIDs) async { + Future deleteMultipleByGeneratedIDs(List generatedIDs) async { if (generatedIDs.isEmpty) { - return 0; + return; } - final db = await instance.database; - return await db.delete( - filesTable, - where: '$columnGeneratedID IN (${generatedIDs.join(', ')})', + + final db = await instance.sqliteAsyncDB; + final inParam = generatedIDs.join(','); + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID IN ($inParam)', ); } - Future deleteLocalFile(EnteFile file) async { - final db = await instance.database; + Future deleteLocalFile(EnteFile file) async { + final db = await instance.sqliteAsyncDB; if (file.localID != null) { // delete all files with same local ID - return db.delete( - filesTable, - where: '$columnLocalID =?', - whereArgs: [file.localID], + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnLocalID = ?', + [file.localID], + ), ); } else { - return db.delete( - filesTable, - where: '$columnGeneratedID =?', - whereArgs: [file.generatedID], + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID = ?', + [file.generatedID], + ), ); } } @@ -1159,8 +1249,8 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - await db.rawQuery( + final db = await instance.sqliteAsyncDB; + await db.execute( ''' UPDATE $filesTable SET $columnLocalID = NULL @@ -1175,34 +1265,30 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnLocalID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + ''' + SELECT * FROM $filesTable + WHERE $columnLocalID IN ($inParam); + ''', ); return convertToFiles(results); } - Future deleteUnSyncedLocalFiles(List localIDs) async { + Future deleteUnSyncedLocalFiles(List localIDs) async { String inParam = ""; for (final localID in localIDs) { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - return db.delete( - filesTable, - where: - '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)', - ); - } - - Future deleteFromCollection(int uploadedFileID, int collectionID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', - whereArgs: [uploadedFileID, collectionID], + final db = await instance.sqliteAsyncDB; + unawaited( + db.execute( + ''' + DELETE FROM $filesTable + WHERE ($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam) + ''', + ), ); } @@ -1210,24 +1296,27 @@ class FilesDB { int collectionID, List uploadedFileIDs, ) async { - final db = await instance.database; - return db.delete( - filesTable, - where: - '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', - whereArgs: [collectionID], - ); + final db = await instance.sqliteAsyncDB; + return db.writeTransaction((tx) async { + await tx.execute( + ''' + DELETE FROM $filesTable + WHERE $columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')}); + ''', + [collectionID], + ); + final res = await tx.get('SELECT changes()'); + return res['changes()'] as int; + }); } Future collectionFileCount(int collectionID) async { - final db = await instance.database; - final count = Sqflite.firstIntValue( - await db.rawQuery( - 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = ' - '$collectionID AND $columnUploadedFileID IS NOT -1', - ), + final db = await instance.sqliteAsyncDB; + final row = await db.get( + 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = ' + '$collectionID AND $columnUploadedFileID IS NOT -1', ); - return count ?? 0; + return row['COUNT(*)'] as int; } Future archivedFilesCount( @@ -1235,43 +1324,45 @@ class FilesDB { int ownerID, Set hiddenCollections, ) async { - final db = await instance.database; - final count = Sqflite.firstIntValue( - await db.rawQuery( - 'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where ' - '$columnMMdVisibility' - ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})', + final db = await instance.sqliteAsyncDB; + final count = await db.getAll( + 'SELECT COUNT(distinct($columnUploadedFileID)) as COUNT FROM $filesTable where ' + '$columnMMdVisibility' + ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})', + ); + return count.first['COUNT'] as int; + } + + Future deleteCollection(int collectionID) async { + final db = await instance.sqliteAsyncDB; + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnCollectionID = ?', + [collectionID], ), ); - return count ?? 0; } - Future deleteCollection(int collectionID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnCollectionID = ?', - whereArgs: [collectionID], - ); - } - - Future removeFromCollection(int collectionID, List fileIDs) async { - final db = await instance.database; - return db.delete( - filesTable, - where: - '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})', - whereArgs: [collectionID], + Future removeFromCollection(int collectionID, List fileIDs) async { + final db = await instance.sqliteAsyncDB; + final inParam = fileIDs.join(','); + unawaited( + db.execute( + ''' + DELETE FROM $filesTable + WHERE $columnCollectionID = ? AND $columnUploadedFileID IN ($inParam); + ''', + [collectionID], + ), ); } Future> getPendingUploadForCollection(int collectionID) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NULL OR ' - '$columnUploadedFileID = -1)', - whereArgs: [collectionID], + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnCollectionID = ? AND ' + '($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)', + [collectionID], ); return convertToFiles(results); } @@ -1287,8 +1378,8 @@ class FilesDB { } } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final rows = await db.rawQuery( + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT $columnLocalID FROM $filesTable @@ -1307,8 +1398,8 @@ class FilesDB { // creationTime of the files in the collection. Future> getCollectionIDToMaxCreationTime() async { final enteWatch = EnteWatch("getCollectionIDToMaxCreationTime")..start(); - final db = await instance.database; - final rows = await db.rawQuery( + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time FROM $filesTable @@ -1350,16 +1441,17 @@ class FilesDB { int collectionID, bool sortAsc, ) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; final order = sortAsc ? 'ASC' : 'DESC'; - final rows = await db.query( - filesTable, - where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL ' - 'AND $columnUploadedFileID IS NOT -1)', - whereArgs: [collectionID], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: 1, + final rows = await db.getAll( + ''' + SELECT * FROM $filesTable + WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL + AND $columnUploadedFileID IS NOT -1) + ORDER BY $columnCreationTime $order, $columnModificationTime $order + LIMIT 1; + ''', + [collectionID], ); if (rows.isEmpty) { return null; @@ -1376,8 +1468,8 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - await db.rawUpdate( + final db = await instance.sqliteAsyncDB; + await db.execute( ''' UPDATE $filesTable SET $columnUpdationTime = NULL @@ -1391,12 +1483,11 @@ class FilesDB { int uploadedFileID, int collectionID, ) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', - whereArgs: [uploadedFileID, collectionID], - limit: 1, + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND ' + '$columnCollectionID = ? LIMIT 1', + [uploadedFileID, collectionID], ); return rows.isNotEmpty; } @@ -1411,10 +1502,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (final file in files) { @@ -1433,10 +1523,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnGeneratedID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnGeneratedID IN ($inParam)', ); final files = convertToFiles(results); for (final file in files) { @@ -1457,10 +1546,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (EnteFile eachFile in files) { @@ -1475,13 +1563,13 @@ class FilesDB { Future> getAllCollectionIDsOfFile( int uploadedFileID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID != -1', - columns: [columnCollectionID], - whereArgs: [uploadedFileID], - distinct: true, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + ''' + SELECT DISTINCT $columnCollectionID FROM $filesTable + WHERE $columnUploadedFileID = ? AND $columnCollectionID != -1 + ''', + [uploadedFileID], ); final collectionIDsOfFile = {}; for (var result in results) { @@ -1510,14 +1598,13 @@ class FilesDB { int cutOffTime, int ownerID, ) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnGeneratedID], - distinct: true, - where: - '$columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?)', - whereArgs: [cutOffTime, ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnGeneratedID FROM $filesTable + WHERE $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) + ''', + [cutOffTime, ownerID], ); final result = []; for (final row in rows) { @@ -1529,15 +1616,14 @@ class FilesDB { // For givenUserID, get List of unique LocalIDs for files which are // uploaded by the given user and location is missing Future> getLocalIDsForFilesWithoutLocation(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ' - '($columnLatitude IS NULL OR ' - '$columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND + ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLatitude = 0.0 or $columnLongitude = 0.0) + ''', + [ownerID], ); final result = []; for (final row in rows) { @@ -1548,13 +1634,13 @@ class FilesDB { // For a given userID, return unique uploadedFileId for the given userID Future> getUploadIDsWithMissingSize(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnUploadedFileID], - distinct: true, - where: '$columnOwnerID = ? AND $columnFileSize IS NULL', - whereArgs: [userId], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnUploadedFileID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnFileSize IS NULL + ''', + [userId], ); final result = []; for (final row in rows) { @@ -1565,14 +1651,13 @@ class FilesDB { // For a given userID, return unique localID for all uploaded live photos Future> getLivePhotosForUser(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnOwnerID = ? AND ' - '$columnFileType = ? AND $columnLocalID IS NOT NULL', - whereArgs: [userId, getInt(FileType.livePhoto)], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnFileType = ? AND $columnLocalID IS NOT NULL + ''', + [userId, getInt(FileType.livePhoto)], ); final result = []; for (final row in rows) { @@ -1582,15 +1667,16 @@ class FilesDB { } Future> getLocalFilesBackedUpWithoutLocation(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: - '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' - 'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', - whereArgs: [userId], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND + ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) + AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR + $columnLatitude = 0.0 or $columnLongitude = 0.0) + ''', + [userId], ); final result = []; for (final row in rows) { @@ -1607,17 +1693,24 @@ class FilesDB { if (uploadedFileIDToSize.isEmpty) { return; } - final db = await instance.database; - final batch = db.batch(); + final db = await instance.sqliteAsyncDB; + final parameterSets = >[]; + for (final uploadedFileID in uploadedFileIDToSize.keys) { - batch.update( - filesTable, - {columnFileSize: uploadedFileIDToSize[uploadedFileID]}, - where: '$columnUploadedFileID = ?', - whereArgs: [uploadedFileID], - ); + parameterSets.add([ + uploadedFileIDToSize[uploadedFileID], + uploadedFileID, + ]); } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + UPDATE $filesTable + SET $columnFileSize = ? + WHERE $columnUploadedFileID = ?; + ''', + parameterSets, + ); } Future> getAllFilesFromDB( @@ -1642,9 +1735,13 @@ class FilesDB { } Future> fetchFilesCountbyType(int userID) async { - final db = await instance.database; - final result = await db.rawQuery( - "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType", + final db = await instance.sqliteAsyncDB; + final result = await db.getAll( + ''' + SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) + FROM $filesTable WHERE $columnUploadedFileID != -1 AND + $columnOwnerID IS $userID GROUP BY $columnFileType + ''', ); final filesCount = {}; @@ -1663,18 +1760,28 @@ class FilesDB { bool? asc, required DBFilterOptions? filterOptions, }) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); - final results = await db.query( - filesTable, - where: - '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)' - ' AND $columnCreationTime >= ? AND $columnCreationTime <= ?' - ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))', - whereArgs: [startTime, endTime], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: limit, + String query = ''' + SELECT * FROM $filesTable + WHERE $columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND + ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0) AND + $columnCreationTime >= ? AND $columnCreationTime <= ? AND + ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND + $columnCollectionID IS NOT -1)) + ORDER BY $columnCreationTime $order, $columnModificationTime $order + '''; + + final args = [startTime, endTime]; + + if (limit != null) { + query += ' LIMIT ?'; + args.add(limit); + } + + final results = await db.getAll( + query, + args, ); final files = convertToFiles(results); final List filteredFiles = @@ -1718,100 +1825,144 @@ class FilesDB { return convertToFiles(results); } - Map _getRowForFile(EnteFile file) { - final row = {}; - if (file.generatedID != null) { - row[columnGeneratedID] = file.generatedID; + ///Returns "columnName1 = ?, columnName2 = ?, ..." + String _generateUpdateAssignmentsWithPlaceholders({ + required int? fileGenId, + bool omitCollectionId = false, + }) { + final assignments = []; + + for (String columnName in _columnNames) { + if (columnName == columnGeneratedID && fileGenId == null) { + continue; + } + if (columnName == columnCollectionID && omitCollectionId) { + continue; + } + assignments.add("$columnName = ?"); } - row[columnLocalID] = file.localID; - row[columnUploadedFileID] = file.uploadedFileID ?? -1; - row[columnOwnerID] = file.ownerID; - row[columnCollectionID] = file.collectionID ?? -1; - row[columnTitle] = file.title; - row[columnDeviceFolder] = file.deviceFolder; - // if (file.location == null || - // (file.location!.latitude == null && file.location!.longitude == null)) { - // file.location = Location.randomLocation(); - // } - if (file.location != null) { - row[columnLatitude] = file.location!.latitude; - row[columnLongitude] = file.location!.longitude; + + return assignments.join(","); + } + + Map _generateColumnsAndPlaceholdersForInsert({ + required int? fileGenId, + }) { + final columnNames = []; + + for (String columnName in _columnNames) { + if (columnName == columnGeneratedID && fileGenId == null) { + continue; + } + + columnNames.add(columnName); } - row[columnFileType] = getInt(file.fileType); - row[columnCreationTime] = file.creationTime; - row[columnModificationTime] = file.modificationTime; - row[columnUpdationTime] = file.updationTime; - row[columnAddedTime] = - file.addedTime ?? DateTime.now().microsecondsSinceEpoch; - row[columnEncryptedKey] = file.encryptedKey; - row[columnKeyDecryptionNonce] = file.keyDecryptionNonce; - row[columnFileDecryptionHeader] = file.fileDecryptionHeader; - row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader; - row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader; - row[columnFileSubType] = file.fileSubType ?? -1; - row[columnDuration] = file.duration ?? 0; - row[columnExif] = file.exif; - row[columnHash] = file.hash; - row[columnMetadataVersion] = file.metadataVersion; - row[columnFileSize] = file.fileSize; - row[columnMMdVersion] = file.mMdVersion; - row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; - row[columnMMdVisibility] = file.magicMetadata.visibility; - row[columnPubMMdVersion] = file.pubMmdVersion; - row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; - // override existing fields to avoid re-writing all queries and logic + + return { + "columns": columnNames.join(","), + "placeholders": List.filled(columnNames.length, "?").join(","), + }; + } + + List _getParameterSetForFile( + EnteFile file, { + bool omitCollectionId = false, + }) { + final values = []; + + double? latitude = file.location?.latitude; + double? longitude = file.location?.longitude; + + int? creationTime = file.creationTime; if (file.pubMagicMetadata != null) { if (file.pubMagicMetadata!.editedTime != null) { - row[columnCreationTime] = file.pubMagicMetadata!.editedTime; + creationTime = file.pubMagicMetadata!.editedTime; } if (file.pubMagicMetadata!.lat != null && file.pubMagicMetadata!.long != null) { - row[columnLatitude] = file.pubMagicMetadata!.lat; - row[columnLongitude] = file.pubMagicMetadata!.long; + latitude = file.pubMagicMetadata!.lat; + longitude = file.pubMagicMetadata!.long; } } - return row; + + if (file.generatedID != null) { + values.add(file.generatedID); + } + values.addAll([ + file.localID, + file.uploadedFileID ?? -1, + file.ownerID, + file.collectionID ?? -1, + file.title, + file.deviceFolder, + latitude, + longitude, + getInt(file.fileType), + file.modificationTime, + file.encryptedKey, + file.keyDecryptionNonce, + file.fileDecryptionHeader, + file.thumbnailDecryptionHeader, + file.metadataDecryptionHeader, + creationTime, + file.updationTime, + file.fileSubType ?? -1, + file.duration ?? 0, + file.exif, + file.hash, + file.metadataVersion, + file.mMdEncodedJson ?? '{}', + file.mMdVersion, + file.magicMetadata.visibility, + file.pubMmdEncodedJson ?? '{}', + file.pubMmdVersion, + file.fileSize, + file.addedTime ?? DateTime.now().microsecondsSinceEpoch, + ]); + + if (omitCollectionId) { + values.removeAt(3); + } + + return values; } - Map _getRowForFileWithoutCollection(EnteFile file) { - final row = {}; - row[columnLocalID] = file.localID; - row[columnUploadedFileID] = file.uploadedFileID ?? -1; - row[columnOwnerID] = file.ownerID; - row[columnTitle] = file.title; - row[columnDeviceFolder] = file.deviceFolder; - if (file.location != null) { - row[columnLatitude] = file.location!.latitude; - row[columnLongitude] = file.location!.longitude; - } - row[columnFileType] = getInt(file.fileType); - row[columnCreationTime] = file.creationTime; - row[columnModificationTime] = file.modificationTime; - row[columnUpdationTime] = file.updationTime; - row[columnAddedTime] = - file.addedTime ?? DateTime.now().microsecondsSinceEpoch; - row[columnFileDecryptionHeader] = file.fileDecryptionHeader; - row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader; - row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader; - row[columnFileSubType] = file.fileSubType ?? -1; - row[columnDuration] = file.duration ?? 0; - row[columnExif] = file.exif; - row[columnHash] = file.hash; - row[columnMetadataVersion] = file.metadataVersion; + Future _batchAndInsertFile( + EnteFile file, + SqliteAsyncConflictAlgorithm conflictAlgorithm, + SqliteDatabase db, + List> parameterSets, + PrimitiveWrapper batchCounter, { + required bool isGenIdNull, + }) async { + parameterSets.add(_getParameterSetForFile(file)); + batchCounter.value++; - row[columnMMdVersion] = file.mMdVersion; - row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; - row[columnMMdVisibility] = file.magicMetadata.visibility; - - row[columnPubMMdVersion] = file.pubMmdVersion; - row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; - if (file.pubMagicMetadata != null && - file.pubMagicMetadata!.editedTime != null) { - // override existing creationTime to avoid re-writing all queries related - // to loading the gallery - row[columnCreationTime] = file.pubMagicMetadata!.editedTime!; + final columnNames = isGenIdNull + ? _columnNames.where((column) => column != columnGeneratedID) + : _columnNames; + if (batchCounter.value == 400) { + _logger.info("Inserting batch with genIdNull: $isGenIdNull"); + await _insertBatch(conflictAlgorithm, columnNames, db, parameterSets); + batchCounter.value = 0; + parameterSets.clear(); } - return row; + } + + Future _insertBatch( + SqliteAsyncConflictAlgorithm conflictAlgorithm, + Iterable columnNames, + SqliteDatabase db, + List> parameterSets, + ) async { + final valuesPlaceholders = List.filled(columnNames.length, "?").join(","); + final columnNamesJoined = columnNames.join(","); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} INTO $filesTable($columnNamesJoined) VALUES($valuesPlaceholders) + ''', + parameterSets, + ); } EnteFile _getFileFromRow(Map row) { diff --git a/mobile/lib/services/favorites_service.dart b/mobile/lib/services/favorites_service.dart index fef4a323a8..2f64e63d41 100644 --- a/mobile/lib/services/favorites_service.dart +++ b/mobile/lib/services/favorites_service.dart @@ -151,9 +151,7 @@ class FavoritesService { final collectionID = await _getOrCreateFavoriteCollectionID(); final List files = [file]; if (file.uploadedFileID == null) { - file.collectionID = collectionID; - await _filesDB.insert(file); - Bus.instance.fire(CollectionUpdatedEvent(collectionID, files, "addTFav")); + throw AssertionError("Can only favorite uploaded items"); } else { await _collectionsService.addOrCopyToCollection(collectionID, files); } diff --git a/mobile/lib/services/local_file_update_service.dart b/mobile/lib/services/local_file_update_service.dart index e00ac6c459..ce5a9080af 100644 --- a/mobile/lib/services/local_file_update_service.dart +++ b/mobile/lib/services/local_file_update_service.dart @@ -193,7 +193,7 @@ class LocalFileUpdateService { } else if (e.reason == InvalidReason.imageToLivePhotoTypeChanged) { fileType = FileType.livePhoto; } - final int count = await FilesDB.instance.markFilesForReUpload( + await FilesDB.instance.markFilesForReUpload( userID, file.localID!, file.title, @@ -202,8 +202,7 @@ class LocalFileUpdateService { file.modificationTime!, fileType, ); - _logger.fine('fileType changed for ${file.tag} to ${e.reason} for ' - '$count files'); + _logger.fine('fileType changed for ${file.tag} to ${e.reason} for '); } else { _logger.severe("failed to check hash: invalid file ${file.tag}", e); } diff --git a/mobile/lib/services/local_sync_service.dart b/mobile/lib/services/local_sync_service.dart index 93b3c94373..1915ac30c2 100644 --- a/mobile/lib/services/local_sync_service.dart +++ b/mobile/lib/services/local_sync_service.dart @@ -21,8 +21,8 @@ import "package:photos/services/ignored_files_service.dart"; import 'package:photos/services/local/local_sync_util.dart'; import "package:photos/utils/debouncer.dart"; import "package:photos/utils/photo_manager_util.dart"; +import "package:photos/utils/sqlite_util.dart"; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; import 'package:tuple/tuple.dart'; class LocalSyncService { @@ -184,7 +184,7 @@ class LocalSyncService { if (hasUnsyncedFiles) { await _db.insertMultiple( localDiffResult.uniqueLocalFiles!, - conflictAlgorithm: ConflictAlgorithm.ignore, + conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore, ); _logger.info( "Inserted ${localDiffResult.uniqueLocalFiles?.length} " @@ -321,7 +321,7 @@ class LocalSyncService { files.removeWhere((file) => existingLocalDs.contains(file.localID)); await _db.insertMultiple( files, - conflictAlgorithm: ConflictAlgorithm.ignore, + conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore, ); _logger.info('Inserted ${files.length} files'); Bus.instance.fire( diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 9f153ffa89..222fd50b8f 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -580,6 +580,9 @@ class FaceMlService { _isIndexingOrClusteringRunning = true; final clusterAllImagesTime = DateTime.now(); + _logger.info('Pulling remote feedback before actually clustering'); + await PersonService.instance.fetchRemoteClusterFeedback(); + try { // Get a sense of the total number of faces in the database final int totalFaces = await FaceMLDataDB.instance diff --git a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart index 7517d057d5..682deaff0c 100644 --- a/mobile/lib/services/machine_learning/face_ml/person/person_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/person/person_service.dart @@ -73,7 +73,7 @@ class PersonService { Future reconcileClusters() async { final EnteWatch? w = kDebugMode ? EnteWatch("reconcileClusters") : null; w?.start(); - await storeRemoteFeedback(); + await fetchRemoteClusterFeedback(); w?.log("Stored remote feedback"); final dbPersonClusterInfo = await faceMLDataDB.getPersonToClusterIdToFaceIds(); @@ -225,7 +225,7 @@ class PersonService { Bus.instance.fire(PeopleChangedEvent()); } - Future storeRemoteFeedback() async { + Future fetchRemoteClusterFeedback() async { await entityService.syncEntities(); final entities = await entityService.getEntities(EntityType.person); entities.sort((a, b) => a.updatedAt.compareTo(b.updatedAt)); diff --git a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart index 376793769f..844f71c01b 100644 --- a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart @@ -193,7 +193,7 @@ class _FaceDebugSectionWidgetState extends State { trailingIconIsMuted: true, onTap: () async { try { - await PersonService.instance.storeRemoteFeedback(); + await PersonService.instance.fetchRemoteClusterFeedback(); FaceMlService.instance.debugIndexingDisabled = false; await FaceMlService.instance .clusterAllImages(clusterInBuckets: true); diff --git a/mobile/lib/ui/tools/editor/image_editor_page.dart b/mobile/lib/ui/tools/editor/image_editor_page.dart index 4830df9523..5314d9ca86 100644 --- a/mobile/lib/ui/tools/editor/image_editor_page.dart +++ b/mobile/lib/ui/tools/editor/image_editor_page.dart @@ -371,7 +371,7 @@ class _ImageEditorPageState extends State { ); } } - newFile.generatedID = await FilesDB.instance.insert(newFile); + newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile); Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave")); unawaited(SyncService.instance.sync()); showShortToast(context, S.of(context).editsSaved); diff --git a/mobile/lib/ui/viewer/location/location_screen.dart b/mobile/lib/ui/viewer/location/location_screen.dart index 374d70cb9f..55975dd3fa 100644 --- a/mobile/lib/ui/viewer/location/location_screen.dart +++ b/mobile/lib/ui/viewer/location/location_screen.dart @@ -146,6 +146,8 @@ class _LocationGalleryWidgetState extends State { late final StreamSubscription _filesUpdateEvent; @override void initState() { + super.initState(); + final collectionsToHide = CollectionsService.instance.archivedOrHiddenCollectionIds(); fileLoadResult = FilesDB.instance @@ -179,8 +181,6 @@ class _LocationGalleryWidgetState extends State { }); galleryHeaderWidget = const GalleryHeaderWidget(); - - super.initState(); } @override diff --git a/mobile/lib/utils/primitive_wrapper.dart b/mobile/lib/utils/primitive_wrapper.dart new file mode 100644 index 0000000000..20ea9bbb6e --- /dev/null +++ b/mobile/lib/utils/primitive_wrapper.dart @@ -0,0 +1,6 @@ +///This is useful when you want to pass a primitive by reference. + +class PrimitiveWrapper { + var value; + PrimitiveWrapper(this.value); +} diff --git a/mobile/lib/utils/sqlite_util.dart b/mobile/lib/utils/sqlite_util.dart new file mode 100644 index 0000000000..b83bd58e15 --- /dev/null +++ b/mobile/lib/utils/sqlite_util.dart @@ -0,0 +1,39 @@ +enum SqliteAsyncConflictAlgorithm { + /// When a constraint violation occurs, an immediate ROLLBACK occurs, + /// thus ending the current transaction, and the command aborts with a + /// return code of SQLITE_CONSTRAINT. If no transaction is active + /// (other than the implied transaction that is created on every command) + /// then this algorithm works the same as ABORT. + rollback, + + /// When a constraint violation occurs,no ROLLBACK is executed + /// so changes from prior commands within the same transaction + /// are preserved. This is the default behavior. + abort, + + /// When a constraint violation occurs, the command aborts with a return + /// code SQLITE_CONSTRAINT. But any changes to the database that + /// the command made prior to encountering the constraint violation + /// are preserved and are not backed out. + fail, + + /// When a constraint violation occurs, the one row that contains + /// the constraint violation is not inserted or changed. + /// But the command continues executing normally. Other rows before and + /// after the row that contained the constraint violation continue to be + /// inserted or updated normally. No error is returned. + ignore, + + /// When a UNIQUE constraint violation occurs, the pre-existing rows that + /// are causing the constraint violation are removed prior to inserting + /// or updating the current row. Thus the insert or update always occurs. + /// The command continues executing normally. No error is returned. + /// If a NOT NULL constraint violation occurs, the NULL value is replaced + /// by the default value for that column. If the column has no default + /// value, then the ABORT algorithm is used. If a CHECK constraint + /// violation occurs then the IGNORE algorithm is used. When this conflict + /// resolution strategy deletes rows in order to satisfy a constraint, + /// it does not invoke delete triggers on those rows. + /// This behavior might change in a future release. + replace, +} diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index d3f49380f9..1311b4e793 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.112+636 +version: 0.8.113+637 publish_to: none environment: