part of 'test_util.dart'; extension DiContainerExtension on DiContainer { // ignore: deprecated_member_use compat.SqliteDb get sqliteDb => (npDb as NpDbSqlite).compatDb; } class _ByAccount { const _ByAccount.sql(compat.Account account) : this._(sqlAccount: account); // const _ByAccount.app(Account account) : this._(appAccount: account); const _ByAccount._({ this.sqlAccount, this.appAccount, }) : assert((sqlAccount != null) != (appAccount != null)); final compat.Account? sqlAccount; final Account? appAccount; } class _AccountFileRowIds { const _AccountFileRowIds( this.accountFileRowId, this.accountRowId, this.fileRowId); final int accountFileRowId; final int accountRowId; final int fileRowId; } class _AccountFileRowIdsWithFileId { const _AccountFileRowIdsWithFileId( this.accountFileRowId, this.accountRowId, this.fileRowId, this.fileId); final int accountFileRowId; final int accountRowId; final int fileRowId; final int fileId; } extension on compat.SqliteDb { /// Query AccountFiles, Accounts and Files row ID by app File /// /// Only one of [sqlAccount] and [appAccount] must be passed Future<_AccountFileRowIds?> accountFileRowIdsOfOrNull( FileDescriptor file, { compat.Account? sqlAccount, Account? appAccount, }) { assert((sqlAccount != null) != (appAccount != null)); final query = queryFiles().let((q) { q.setQueryMode(_FilesQueryMode.expression, expressions: [ accountFiles.rowId, accountFiles.account, accountFiles.file, ]); if (sqlAccount != null) { q.setSqlAccount(sqlAccount); } else { q.setAppAccount(appAccount!); } try { q.byFileId(file.fdId); } catch (_) { q.byRelativePath(file.strippedPathWithEmpty); } return q.build()..limit(1); }); return query .map((r) => _AccountFileRowIds( r.read(accountFiles.rowId)!, r.read(accountFiles.account)!, r.read(accountFiles.file)!, )) .getSingleOrNull(); } /// See [accountFileRowIdsOfOrNull] Future<_AccountFileRowIds> accountFileRowIdsOf( FileDescriptor file, { compat.Account? sqlAccount, Account? appAccount, }) => accountFileRowIdsOfOrNull(file, sqlAccount: sqlAccount, appAccount: appAccount) .notNull(); /// Query AccountFiles, Accounts and Files row ID by fileIds /// /// Returned files are NOT guaranteed to be sorted as [fileIds] Future> accountFileRowIdsByFileIds( _ByAccount account, Iterable fileIds) { final query = queryFiles().let((q) { q.setQueryMode(_FilesQueryMode.expression, expressions: [ accountFiles.rowId, accountFiles.account, accountFiles.file, files.fileId, ]); if (account.sqlAccount != null) { q.setSqlAccount(account.sqlAccount!); } else { q.setAppAccount(account.appAccount!); } q.byFileIds(fileIds); return q.build(); }); return query .map((r) => _AccountFileRowIdsWithFileId( r.read(accountFiles.rowId)!, r.read(accountFiles.account)!, r.read(accountFiles.file)!, r.read(files.fileId)!, )) .get(); } _FilesQueryBuilder queryFiles() => _FilesQueryBuilder(this); } class _SqliteAlbumConverter { static Album fromSql( compat.Album album, File albumFile, List shares) { return Album( lastUpdated: album.lastUpdated, name: album.name, provider: AlbumProvider.fromJson({ "type": album.providerType, "content": jsonDecode(album.providerContent), }), coverProvider: AlbumCoverProvider.fromJson({ "type": album.coverProviderType, "content": jsonDecode(album.coverProviderContent), }), sortProvider: AlbumSortProvider.fromJson({ "type": album.sortProviderType, "content": jsonDecode(album.sortProviderContent), }), shares: shares.isEmpty ? null : shares .map((e) => AlbumShare( userId: e.userId.toCi(), displayName: e.displayName, sharedAt: e.sharedAt.toUtc(), )) .toList(), // replace with the original etag when this album was cached albumFile: albumFile.copyWith(etag: OrNull(album.fileEtag)), savedVersion: album.version, ); } static compat.CompleteAlbumCompanion toSql( Album album, int albumFileRowId, String albumFileEtag) { final providerJson = album.provider.toJson(); final coverProviderJson = album.coverProvider.toJson(); final sortProviderJson = album.sortProvider.toJson(); final dbAlbum = compat.AlbumsCompanion.insert( file: albumFileRowId, fileEtag: sql.Value(albumFileEtag), version: Album.version, lastUpdated: album.lastUpdated, name: album.name, providerType: providerJson["type"], providerContent: jsonEncode(providerJson["content"]), coverProviderType: coverProviderJson["type"], coverProviderContent: jsonEncode(coverProviderJson["content"]), sortProviderType: sortProviderJson["type"], sortProviderContent: jsonEncode(sortProviderJson["content"]), ); final dbAlbumShares = album.shares ?.map((s) => compat.AlbumSharesCompanion( userId: sql.Value(s.userId.toCaseInsensitiveString()), displayName: sql.Value(s.displayName), sharedAt: sql.Value(s.sharedAt), )) .toList(); return compat.CompleteAlbumCompanion(dbAlbum, 1, dbAlbumShares ?? []); } } class _SqliteFileConverter { static File fromSql(String userId, compat.CompleteFile f) { final metadata = f.image?.let((obj) => Metadata( lastUpdated: obj.lastUpdated, fileEtag: obj.fileEtag, imageWidth: obj.width, imageHeight: obj.height, exif: obj.exifRaw?.let((e) => Exif.fromJson(jsonDecode(e))), )); final location = f.imageLocation?.let((obj) => ImageLocation( version: obj.version, name: obj.name, latitude: obj.latitude, longitude: obj.longitude, countryCode: obj.countryCode, admin1: obj.admin1, admin2: obj.admin2, )); return File( path: "remote.php/dav/files/$userId/${f.accountFile.relativePath}", contentLength: f.file.contentLength, contentType: f.file.contentType, etag: f.file.etag, lastModified: f.file.lastModified, isCollection: f.file.isCollection, usedBytes: f.file.usedBytes, hasPreview: f.file.hasPreview, fileId: f.file.fileId, isFavorite: f.accountFile.isFavorite, ownerId: f.file.ownerId?.toCi(), ownerDisplayName: f.file.ownerDisplayName, trashbinFilename: f.trash?.filename, trashbinOriginalLocation: f.trash?.originalLocation, trashbinDeletionTime: f.trash?.deletionTime, metadata: metadata, isArchived: f.accountFile.isArchived, overrideDateTime: f.accountFile.overrideDateTime, location: location, ); } static compat.CompleteFileCompanion toSql( compat.Account? account, File file) { final dbFile = compat.FilesCompanion( server: account == null ? const sql.Value.absent() : sql.Value(account.server), fileId: sql.Value(file.fileId!), contentLength: sql.Value(file.contentLength), contentType: sql.Value(file.contentType), etag: sql.Value(file.etag), lastModified: sql.Value(file.lastModified), isCollection: sql.Value(file.isCollection), usedBytes: sql.Value(file.usedBytes), hasPreview: sql.Value(file.hasPreview), ownerId: sql.Value(file.ownerId!.toCaseInsensitiveString()), ownerDisplayName: sql.Value(file.ownerDisplayName), ); final dbAccountFile = compat.AccountFilesCompanion( account: account == null ? const sql.Value.absent() : sql.Value(account.rowId), relativePath: sql.Value(file.strippedPathWithEmpty), isFavorite: sql.Value(file.isFavorite), isArchived: sql.Value(file.isArchived), overrideDateTime: sql.Value(file.overrideDateTime), bestDateTime: sql.Value(file.bestDateTime), ); final dbImage = file.metadata?.let((m) => compat.ImagesCompanion.insert( lastUpdated: m.lastUpdated, fileEtag: sql.Value(m.fileEtag), width: sql.Value(m.imageWidth), height: sql.Value(m.imageHeight), exifRaw: sql.Value(m.exif?.toJson().let((j) => jsonEncode(j))), dateTimeOriginal: sql.Value(m.exif?.dateTimeOriginal), )); final dbImageLocation = file.location?.let((l) => compat.ImageLocationsCompanion.insert( version: l.version, name: sql.Value(l.name), latitude: sql.Value(l.latitude), longitude: sql.Value(l.longitude), countryCode: sql.Value(l.countryCode), admin1: sql.Value(l.admin1), admin2: sql.Value(l.admin2), )); final dbTrash = file.trashbinDeletionTime == null ? null : compat.TrashesCompanion.insert( filename: file.trashbinFilename!, originalLocation: file.trashbinOriginalLocation!, deletionTime: file.trashbinDeletionTime!, ); return compat.CompleteFileCompanion( dbFile, dbAccountFile, dbImage, dbImageLocation, dbTrash); } } enum _FilesQueryMode { file, completeFile, expression, } typedef _FilesQueryRelativePathBuilder = sql.Expression Function( sql.GeneratedColumn relativePath); /// Build a Files table query /// /// If you call more than one by* methods, the condition will be added up /// instead of replaced. No validations will be made to make sure the resulting /// conditions make sense class _FilesQueryBuilder { _FilesQueryBuilder(this.db); /// Set the query mode /// /// If [mode] == FilesQueryMode.expression, [expressions] must be defined and /// not empty void setQueryMode( _FilesQueryMode mode, { Iterable? expressions, }) { assert((mode == _FilesQueryMode.expression) != (expressions?.isEmpty != false)); _queryMode = mode; _selectExpressions = expressions; } void setSqlAccount(compat.Account account) { assert(_appAccount == null); _sqlAccount = account; } void setAppAccount(Account account) { assert(_sqlAccount == null); _appAccount = account; } void setAccountless() { assert(_sqlAccount == null && _appAccount == null); _isAccountless = true; } void byRowId(int rowId) { _byRowId = rowId; } void byFileId(int fileId) { _byFileId = fileId; } void byFileIds(Iterable fileIds) { _byFileIds = fileIds; } void byRelativePath(String path) { _byRelativePath = path; } void byOrRelativePath(String path) { _byOrRelativePathBuilder((relativePath) => relativePath.equals(path)); } void byOrRelativePathPattern(String pattern) { _byOrRelativePathBuilder((relativePath) => relativePath.like(pattern)); } void byMimePattern(String pattern) { (_byMimePatterns ??= []).add(pattern); } void byFavorite(bool favorite) { _byFavorite = favorite; } void byDirRowId(int dirRowId) { _byDirRowId = dirRowId; } void byServerRowId(int serverRowId) { _byServerRowId = serverRowId; } void byLocation(String location) { _byLocation = location; } sql.JoinedSelectStatement build() { if (_sqlAccount == null && _appAccount == null && !_isAccountless) { throw StateError("Invalid query: missing account"); } final dynamic select = _queryMode == _FilesQueryMode.expression ? db.selectOnly(db.files) : db.select(db.files); final query = select.join([ sql.innerJoin( db.accountFiles, db.accountFiles.file.equalsExp(db.files.rowId), useColumns: _queryMode == _FilesQueryMode.completeFile), if (_appAccount != null) ...[ sql.innerJoin( db.accounts, db.accounts.rowId.equalsExp(db.accountFiles.account), useColumns: false), sql.innerJoin( db.servers, db.servers.rowId.equalsExp(db.accounts.server), useColumns: false), ], if (_byDirRowId != null) sql.innerJoin(db.dirFiles, db.dirFiles.child.equalsExp(db.files.rowId), useColumns: false), if (_queryMode == _FilesQueryMode.completeFile) ...[ sql.leftOuterJoin( db.images, db.images.accountFile.equalsExp(db.accountFiles.rowId)), sql.leftOuterJoin(db.imageLocations, db.imageLocations.accountFile.equalsExp(db.accountFiles.rowId)), sql.leftOuterJoin( db.trashes, db.trashes.file.equalsExp(db.files.rowId)), ], ]) as sql.JoinedSelectStatement; if (_queryMode == _FilesQueryMode.expression) { query.addColumns(_selectExpressions!); } if (_sqlAccount != null) { query.where(db.accountFiles.account.equals(_sqlAccount!.rowId)); } else if (_appAccount != null) { query ..where(db.servers.address.equals(_appAccount!.url)) ..where(db.accounts.userId .equals(_appAccount!.userId.toCaseInsensitiveString())); } if (_byRowId != null) { query.where(db.files.rowId.equals(_byRowId!)); } if (_byFileId != null) { query.where(db.files.fileId.equals(_byFileId!)); } if (_byFileIds != null) { query.where(db.files.fileId.isIn(_byFileIds!)); } if (_byRelativePath != null) { query.where(db.accountFiles.relativePath.equals(_byRelativePath!)); } if (_byOrRelativePathBuilders?.isNotEmpty == true) { final expression = _byOrRelativePathBuilders! .sublist(1) .fold>( _byOrRelativePathBuilders![0](db.accountFiles.relativePath), (previousValue, builder) => previousValue | builder(db.accountFiles.relativePath)); query.where(expression); } if (_byMimePatterns?.isNotEmpty == true) { final expression = _byMimePatterns!.sublist(1).fold>( db.files.contentType.like(_byMimePatterns![0]), (previousValue, element) => previousValue | db.files.contentType.like(element)); query.where(expression); } if (_byFavorite != null) { if (_byFavorite!) { query.where(db.accountFiles.isFavorite.equals(true)); } else { // null are treated as false query.where(db.accountFiles.isFavorite.equals(true).not()); } } if (_byDirRowId != null) { query.where(db.dirFiles.dir.equals(_byDirRowId!)); } if (_byServerRowId != null) { query.where(db.files.server.equals(_byServerRowId!)); } if (_byLocation != null) { var clause = db.imageLocations.name.like(_byLocation!) | db.imageLocations.admin1.like(_byLocation!) | db.imageLocations.admin2.like(_byLocation!); final countryCode = nameToAlpha2Code(_byLocation!.toCi()); if (countryCode != null) { clause = clause | db.imageLocations.countryCode.equals(countryCode); } else if (_byLocation!.length == 2 && alpha2CodeToName(_byLocation!.toUpperCase()) != null) { clause = clause | db.imageLocations.countryCode.equals(_byLocation!.toUpperCase()); } query.where(clause); } return query; } void _byOrRelativePathBuilder(_FilesQueryRelativePathBuilder builder) { (_byOrRelativePathBuilders ??= []).add(builder); } final compat.SqliteDb db; _FilesQueryMode _queryMode = _FilesQueryMode.file; Iterable? _selectExpressions; compat.Account? _sqlAccount; Account? _appAccount; bool _isAccountless = false; int? _byRowId; int? _byFileId; Iterable? _byFileIds; String? _byRelativePath; List<_FilesQueryRelativePathBuilder>? _byOrRelativePathBuilders; List? _byMimePatterns; bool? _byFavorite; int? _byDirRowId; int? _byServerRowId; String? _byLocation; }