mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge c51cb9ad63 into f9c05af45f
This commit is contained in:
commit
0a123d762b
25 changed files with 4819 additions and 32 deletions
|
|
@ -48,6 +48,7 @@ linter:
|
|||
combinators_ordering: true
|
||||
avoid_multiple_declarations_per_line: true
|
||||
unnecessary_breaks: true
|
||||
deprecated_member_use_from_same_package: true
|
||||
|
||||
# Correctness
|
||||
no_adjacent_strings_in_list: true
|
||||
|
|
@ -87,6 +88,7 @@ analyzer:
|
|||
errors:
|
||||
unawaited_futures: warning
|
||||
always_put_control_body_on_new_line: warning
|
||||
deprecated_member_use_from_same_package: error
|
||||
|
||||
dart_code_metrics:
|
||||
rules:
|
||||
|
|
|
|||
3626
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
3626
mobile/drift_schemas/main/drift_schema_v32.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
|
||||
|
|
@ -35,7 +36,7 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
BoolColumn get isVisible => boolean().withDefault(const Constant(true))();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class AuthUserEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -12,7 +13,7 @@ class AuthUserEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
// Profile image
|
||||
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get profileChangedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
IntColumn get avatarColor => intEnum<AvatarColor>()();
|
||||
|
||||
// Quota
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:drift/drift.dart' hide Query;
|
|||
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get country => text().nullable()();
|
||||
|
||||
DateTimeColumn get dateTimeOriginal => dateTime().nullable()();
|
||||
DateTimeColumn get dateTimeOriginal => customType(clampedDateTime).nullable()();
|
||||
|
||||
TextColumn get description => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -9,7 +10,7 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
||||
BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
||||
|
|
@ -20,7 +21,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||
|
||||
TextColumn get iCloudId => text().nullable()();
|
||||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
DateTimeColumn get adjustmentTime => customType(clampedDateTime).nullable()();
|
||||
|
||||
RealColumn get latitude => real().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart' as domain;
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class LogMessageEntity extends Table {
|
||||
class LogMessageEntity extends Table with DriftDefaultsMixin {
|
||||
const LogMessageEntity();
|
||||
|
||||
@override
|
||||
String get tableName => 'logger_messages';
|
||||
|
||||
@override
|
||||
bool get isStrict => false;
|
||||
|
||||
@override
|
||||
bool get withoutRowId => false;
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get message => text()();
|
||||
TextColumn get details => text().nullable()();
|
||||
IntColumn get level => intEnum<domain.LogLevel>()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime)();
|
||||
TextColumn get logger => text().nullable()();
|
||||
TextColumn get stack => text().nullable()();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class MemoryEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -8,11 +9,11 @@ class MemoryEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get id => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
|
|
@ -22,13 +23,13 @@ class MemoryEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
BoolColumn get isSaved => boolean().withDefault(const Constant(false))();
|
||||
|
||||
DateTimeColumn get memoryAt => dateTime()();
|
||||
DateTimeColumn get memoryAt => customType(clampedDateTime)();
|
||||
|
||||
DateTimeColumn get seenAt => dateTime().nullable()();
|
||||
DateTimeColumn get seenAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
DateTimeColumn get showAt => dateTime().nullable()();
|
||||
DateTimeColumn get showAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
DateTimeColumn get hideAt => dateTime().nullable()();
|
||||
DateTimeColumn get hideAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)')
|
||||
|
|
@ -8,9 +9,9 @@ class PersonEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get id => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ class PersonEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get color => text().nullable()();
|
||||
|
||||
DateTimeColumn get birthDate => dateTime().nullable()();
|
||||
DateTimeColumn get birthDate => customType(clampedDateTime).nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -12,9 +13,9 @@ class RemoteAlbumEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get description => text().withDefault(const Constant(''))();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
TextColumn get thumbnailAssetId =>
|
||||
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('''
|
||||
|
|
@ -33,13 +34,13 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
|
|||
|
||||
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
DateTimeColumn get localDateTime => dateTime().nullable()();
|
||||
DateTimeColumn get localDateTime => customType(clampedDateTime).nullable()();
|
||||
|
||||
TextColumn get thumbHash => text().nullable()();
|
||||
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
DateTimeColumn get uploadedAt => dateTime().nullable()();
|
||||
DateTimeColumn get uploadedAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
TextColumn get livePhotoVideoId => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)')
|
||||
|
|
@ -8,9 +9,9 @@ class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get cloudId => text().nullable()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).nullable()();
|
||||
|
||||
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||
DateTimeColumn get adjustmentTime => customType(clampedDateTime).nullable()();
|
||||
|
||||
RealColumn get latitude => real().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class SettingsEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -8,7 +9,7 @@ class SettingsEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get value => text().nullable()();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {key};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)')
|
||||
|
|
@ -8,9 +9,9 @@ class StackEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get id => text()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
|
||||
TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart' hide Index;
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class UserEntity extends Table with DriftDefaultsMixin {
|
||||
|
|
@ -11,7 +12,7 @@ class UserEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
// Profile image
|
||||
BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get profileChangedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get profileChangedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
IntColumn get avatarColor => intEnum<AvatarColor>().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class Drift extends $Drift {
|
|||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 31;
|
||||
int get schemaVersion => 32;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
|
|
@ -318,6 +318,9 @@ class Drift extends $Drift {
|
|||
from30To31: (m, v31) async {
|
||||
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||
},
|
||||
from31To32: (m, v32) async {
|
||||
await healOutOfRangeDateTimes(this);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -344,6 +347,45 @@ class Drift extends $Drift {
|
|||
);
|
||||
}
|
||||
|
||||
// every datetime column of the v31 schema, hardcoded: the heal runs once at v31->v32,
|
||||
// so the set must not follow later schema changes
|
||||
@visibleForTesting
|
||||
const healDateTimeColumns = <String, List<String>>{
|
||||
'auth_user_entity': ['profile_changed_at'],
|
||||
'user_entity': ['profile_changed_at'],
|
||||
'local_album_entity': ['updated_at'],
|
||||
'local_asset_entity': ['created_at', 'updated_at', 'adjustment_time'],
|
||||
'remote_asset_entity': ['created_at', 'updated_at', 'local_date_time', 'deleted_at', 'uploaded_at'],
|
||||
'trashed_local_asset_entity': ['created_at', 'updated_at'],
|
||||
'remote_exif_entity': ['date_time_original'],
|
||||
'remote_album_entity': ['created_at', 'updated_at'],
|
||||
'remote_asset_cloud_id_entity': ['created_at', 'adjustment_time'],
|
||||
'memory_entity': ['created_at', 'updated_at', 'deleted_at', 'memory_at', 'seen_at', 'show_at', 'hide_at'],
|
||||
'stack_entity': ['created_at', 'updated_at'],
|
||||
'person_entity': ['created_at', 'updated_at', 'birth_date'],
|
||||
'asset_face_entity': ['deleted_at'],
|
||||
'settings': ['updated_at'],
|
||||
};
|
||||
|
||||
// Rewrites datetime text sqlite date functions cannot handle: signed extended
|
||||
// years and year 0000 (pre-clamp syncs), plus anything later than the safe
|
||||
// midnight ceiling, which re-overflows sqlite under 'localtime' east of UTC.
|
||||
// One statement per table: each column heals only when its own value is out of range
|
||||
@visibleForTesting
|
||||
Future<void> healOutOfRangeDateTimes(GeneratedDatabase db) async {
|
||||
const floor = '0001-01-01T00:00:00.000Z';
|
||||
const ceiling = '9999-12-31T00:00:00.000Z';
|
||||
for (final MapEntry(key: table, value: columns) in healDateTimeColumns.entries) {
|
||||
String low(String c) => "substr($c, 1, 1) = '-' OR substr($c, 1, 4) = '0000'";
|
||||
String high(String c) => "substr($c, 1, 1) = '+' OR $c > '$ceiling'";
|
||||
final assignments = columns.map(
|
||||
(c) => "$c = CASE WHEN ${low(c)} THEN '$floor' WHEN ${high(c)} THEN '$ceiling' ELSE $c END",
|
||||
);
|
||||
final outOfRange = columns.map((c) => '${low(c)} OR ${high(c)}');
|
||||
await db.customStatement('UPDATE $table SET ${assignments.join(', ')} WHERE ${outOfRange.join(' OR ')}');
|
||||
}
|
||||
}
|
||||
|
||||
class DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftDatabaseRepository(this._db);
|
||||
|
|
|
|||
|
|
@ -16506,6 +16506,591 @@ final class Schema31 extends i0.VersionedSchema {
|
|||
);
|
||||
}
|
||||
|
||||
final class Schema32 extends i0.VersionedSchema {
|
||||
Schema32({required super.database}) : super(version: 32);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxLocalAssetCreatedAt,
|
||||
idxStackPrimaryAssetId,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||
idxRemoteAssetUploaded,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
assetEditEntity,
|
||||
settings,
|
||||
assetOcrEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteExifCity,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxAssetFaceVisiblePerson,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
idxAssetEditAssetId,
|
||||
idxAssetOcrAssetId,
|
||||
];
|
||||
late final Shape33 userEntity = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape50 remoteAssetEntity = Shape50(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_119,
|
||||
_column_120,
|
||||
_column_121,
|
||||
_column_122,
|
||||
_column_123,
|
||||
_column_124,
|
||||
_column_212,
|
||||
_column_125,
|
||||
_column_126,
|
||||
_column_127,
|
||||
_column_128,
|
||||
_column_129,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 stackEntity = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_130,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape36 localAssetEntity = Shape36(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape48 remoteAlbumEntity = Shape48(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_138,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_139,
|
||||
_column_140,
|
||||
_column_141,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 localAlbumEntity = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_115,
|
||||
_column_142,
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_146, _column_147, _column_145],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxLocalAssetCreatedAt = i1.Index(
|
||||
'idx_local_asset_created_at',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||
'idx_remote_asset_owner_visibility_deleted_created',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetUploaded = i1.Index(
|
||||
'idx_remote_asset_uploaded',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||
);
|
||||
late final Shape40 authUserEntity = Shape40(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_148,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_149,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_153, _column_154, _column_155],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape41 partnerEntity = Shape41(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_156, _column_157, _column_158],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape42 remoteExifEntity = Shape42(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_117,
|
||||
_column_116,
|
||||
_column_165,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_169,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_176,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_159, _column_177],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_177, _column_153, _column_178],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_159,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_134,
|
||||
_column_135,
|
||||
_column_136,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape44 memoryEntity = Shape44(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_124,
|
||||
_column_121,
|
||||
_column_113,
|
||||
_column_181,
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_159, _column_187],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape45 personEntity = Shape45(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_121,
|
||||
_column_108,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_192,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape46 assetFaceEntity = Shape46(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_193,
|
||||
_column_194,
|
||||
_column_195,
|
||||
_column_196,
|
||||
_column_197,
|
||||
_column_198,
|
||||
_column_199,
|
||||
_column_200,
|
||||
_column_201,
|
||||
_column_124,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_202, _column_203, _column_204],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
_column_107,
|
||||
_column_205,
|
||||
_column_131,
|
||||
_column_120,
|
||||
_column_132,
|
||||
_column_206,
|
||||
_column_137,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 assetEditEntity = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_edit_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_207,
|
||||
_column_208,
|
||||
_column_209,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape49 settings = Shape49(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'settings',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY("key")'],
|
||||
columns: [_column_210, _column_224, _column_115],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape51 assetOcrEntity = Shape51(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_ocr_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_107,
|
||||
_column_159,
|
||||
_column_213,
|
||||
_column_214,
|
||||
_column_215,
|
||||
_column_216,
|
||||
_column_217,
|
||||
_column_218,
|
||||
_column_219,
|
||||
_column_220,
|
||||
_column_221,
|
||||
_column_222,
|
||||
_column_223,
|
||||
_column_201,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxRemoteExifCity = i1.Index(
|
||||
'idx_remote_exif_city',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||
'idx_asset_face_visible_person',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||
'idx_asset_edit_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||
);
|
||||
final i1.Index idxAssetOcrAssetId = i1.Index(
|
||||
'idx_asset_ocr_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
|
||||
);
|
||||
}
|
||||
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
|
|
@ -16537,6 +17122,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -16690,6 +17276,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from30To31(migrator, schema);
|
||||
return 31;
|
||||
case 31:
|
||||
final schema = Schema32(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from31To32(migrator, schema);
|
||||
return 32;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -16727,6 +17318,7 @@ i1.OnUpgrade stepByStep({
|
|||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||
required Future<void> Function(i1.Migrator m, Schema32 schema) from31To32,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
|
|
@ -16759,5 +17351,6 @@ i1.OnUpgrade stepByStep({
|
|||
from28To29: from28To29,
|
||||
from29To30: from29To30,
|
||||
from30To31: from30To31,
|
||||
from31To32: from31To32,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
mixin AssetEntityMixin on Table {
|
||||
mixin AssetEntityMixin on DriftDefaultsMixin {
|
||||
TextColumn get name => text()();
|
||||
IntColumn get type => intEnum<AssetType>()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)();
|
||||
IntColumn get width => integer().nullable()();
|
||||
IntColumn get height => integer().nullable()();
|
||||
IntColumn get durationMs => integer().nullable()();
|
||||
|
|
|
|||
41
mobile/lib/infrastructure/utils/datetime_clamp.type.dart
Normal file
41
mobile/lib/infrastructure/utils/datetime_clamp.type.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
// Synced dates can fall outside the year range sqlite date functions handle
|
||||
// (1..9999), which turns bucket queries into NULLs and crashes the timeline
|
||||
// (#28524). Clamps on write; the v32 heal migration rewrites the backlog.
|
||||
const clampedDateTime = DateTimeClampType();
|
||||
|
||||
final class DateTimeClampType implements DialectAwareSqlType<DateTime> {
|
||||
const DateTimeClampType();
|
||||
|
||||
@override
|
||||
DateTime read(SqlTypes types, Object fromSql) => types.read(DriftSqlType.dateTime, fromSql)!;
|
||||
|
||||
@override
|
||||
Object mapToSqlParameter(GenerationContext context, DateTime value) =>
|
||||
context.typeMapping.mapToSqlVariable(_clampDateTime(value))!;
|
||||
|
||||
@override
|
||||
String mapToSqlLiteral(GenerationContext context, DateTime value) =>
|
||||
context.typeMapping.mapToSqlLiteral(_clampDateTime(value));
|
||||
|
||||
@override
|
||||
String sqlTypeName(GenerationContext context) => DriftSqlType.dateTime.sqlTypeName(context);
|
||||
}
|
||||
|
||||
// utc so drift's text mapping never formats these with a historical local
|
||||
// offset (those carry seconds, which drift refuses to store). the ceiling
|
||||
// stays at midnight: a later time would overflow sqlite's year range again
|
||||
// when a query applies 'localtime' east of UTC
|
||||
final DateTime _floor = DateTime.utc(1);
|
||||
final DateTime _ceiling = DateTime.utc(9999, 12, 31);
|
||||
|
||||
DateTime _clampDateTime(DateTime value) {
|
||||
if (value.isBefore(_floor)) {
|
||||
return _floor;
|
||||
}
|
||||
if (value.isAfter(_ceiling)) {
|
||||
return _ceiling;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
mixin DriftDefaultsMixin on Table {
|
||||
@Deprecated('Use customType(clampedDateTime)')
|
||||
@override
|
||||
ColumnBuilder<DateTime> dateTime() => super.dateTime();
|
||||
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart';
|
||||
|
||||
void main() {
|
||||
const options = DriftDatabaseOptions(storeDateTimeAsText: true);
|
||||
final types = options.createTypeMapping(SqlDialect.sqlite);
|
||||
final ctx = GenerationContext(options, null);
|
||||
DateTime toSql(DateTime value) => types.read(DriftSqlType.dateTime, clampedDateTime.mapToSqlParameter(ctx, value))!;
|
||||
|
||||
test('clamps far-future years to the ceiling on write', () {
|
||||
// the reporter's date from #28524
|
||||
final poison = DateTime.utc(144769, 11, 18, 12, 38, 32);
|
||||
final ceiling = DateTime.utc(9999, 12, 31);
|
||||
expect(toSql(poison), ceiling);
|
||||
expect(toSql(DateTime(144769, 1, 1)), ceiling); // local poison normalizes to utc
|
||||
});
|
||||
|
||||
test('clamps BCE and year zero to the floor on write', () {
|
||||
final floor = DateTime.utc(1, 1, 1);
|
||||
expect(toSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor);
|
||||
expect(toSql(DateTime.utc(0, 12, 31)), floor);
|
||||
});
|
||||
|
||||
test('keeps in-range years untouched, but clamps the last hours of 9999-12-31', () {
|
||||
// the range-exact lesson: in-range values never change, even near the edges
|
||||
final low = DateTime.utc(1, 1, 1);
|
||||
final justBefore = DateTime.utc(9999, 12, 30, 23, 59, 59);
|
||||
final normal = DateTime.utc(2024, 1, 2, 3, 4, 5, 123);
|
||||
expect(toSql(low), low);
|
||||
expect(toSql(justBefore), justBefore);
|
||||
expect(toSql(normal), normal);
|
||||
// local datetimes keep their zone flag
|
||||
final local = DateTime(2024, 6, 15, 10, 30, 25);
|
||||
expect(toSql(local), local);
|
||||
expect(toSql(local).isUtc, isFalse);
|
||||
});
|
||||
|
||||
test('clamps the late hours of 9999-12-31 to the midnight ceiling', () {
|
||||
// legal upstream (year 9999 passes the server's rule), but east of UTC the
|
||||
// bucket query's 'localtime' overflows sqlite's year range on it -> NULL
|
||||
final ceiling = DateTime.utc(9999, 12, 31);
|
||||
expect(toSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling);
|
||||
expect(toSql(DateTime(9999, 12, 31, 23, 59, 59)), ceiling); // local normalizes to utc
|
||||
});
|
||||
|
||||
test('reads pass stored values through untouched', () {
|
||||
// writes clamp and the v32 migration heals the backlog, so reads never clamp
|
||||
final stored = types.mapToSqlVariable(DateTime.utc(2024, 1, 2, 3, 4, 5, 123))!;
|
||||
expect(clampedDateTime.read(types, stored), DateTime.utc(2024, 1, 2, 3, 4, 5, 123));
|
||||
});
|
||||
}
|
||||
242
mobile/test/medium/datetime_clamp_type_test.dart
Normal file
242
mobile/test/medium/datetime_clamp_type_test.dart
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
|
||||
import 'repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
final farFuture = DateTime.utc(144769, 11, 18, 12, 38, 32); // the reporter's year from #28524
|
||||
final bce = DateTime.utc(-4712, 3, 4, 5, 6, 7);
|
||||
const ceiling = '9999-12-31T00:00:00.000Z';
|
||||
const floor = '0001-01-01T00:00:00.000Z';
|
||||
|
||||
test('clamps poison on every datetime column of every table (auto-covers future columns)', () async {
|
||||
// one row builder + pk values per table with datetime columns, in
|
||||
// allTables order (foreign keys resolve in this order). a date table
|
||||
// missing here fails the walk, and a date column left unpoisoned fails
|
||||
// the read-back, so schema growth forces this test to be extended
|
||||
final builders =
|
||||
<String, (Insertable<dynamic> Function(String suffix, DateTime d), List<String> Function(String suffix))>{
|
||||
'auth_user_entity': (
|
||||
(s, d) => AuthUserEntityCompanion(
|
||||
id: .new('au1$s'),
|
||||
name: const .new('n'),
|
||||
email: const .new('e'),
|
||||
avatarColor: const .new(AvatarColor.primary),
|
||||
profileChangedAt: .new(d),
|
||||
),
|
||||
(s) => ['au1$s'],
|
||||
),
|
||||
'user_entity': (
|
||||
(s, d) => UserEntityCompanion(
|
||||
id: .new('u1$s'),
|
||||
name: const .new('n'),
|
||||
email: const .new('e'),
|
||||
profileChangedAt: .new(d),
|
||||
),
|
||||
(s) => ['u1$s'],
|
||||
),
|
||||
'local_album_entity': (
|
||||
(s, d) => LocalAlbumEntityCompanion(
|
||||
id: .new('la1$s'),
|
||||
name: const .new('n'),
|
||||
updatedAt: .new(d),
|
||||
backupSelection: const .new(BackupSelection.selected),
|
||||
),
|
||||
(s) => ['la1$s'],
|
||||
),
|
||||
'local_asset_entity': (
|
||||
(s, d) => LocalAssetEntityCompanion(
|
||||
id: .new('l1$s'),
|
||||
name: const .new('n'),
|
||||
type: const .new(AssetType.image),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
adjustmentTime: .new(d),
|
||||
),
|
||||
(s) => ['l1$s'],
|
||||
),
|
||||
'remote_asset_entity': (
|
||||
(s, d) => RemoteAssetEntityCompanion(
|
||||
id: .new('a1$s'),
|
||||
name: const .new('n'),
|
||||
type: const .new(AssetType.image),
|
||||
checksum: .new('c$s'),
|
||||
ownerId: .new('u1$s'),
|
||||
visibility: const .new(AssetVisibility.timeline),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
localDateTime: .new(d),
|
||||
deletedAt: .new(d),
|
||||
uploadedAt: .new(d),
|
||||
),
|
||||
(s) => ['a1$s'],
|
||||
),
|
||||
'remote_exif_entity': (
|
||||
(s, d) => RemoteExifEntityCompanion(assetId: .new('a1$s'), dateTimeOriginal: .new(d)),
|
||||
(s) => ['a1$s'],
|
||||
),
|
||||
'remote_album_entity': (
|
||||
(s, d) => RemoteAlbumEntityCompanion(
|
||||
id: .new('ra1$s'),
|
||||
name: const .new('n'),
|
||||
order: const .new(AlbumAssetOrder.asc),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
),
|
||||
(s) => ['ra1$s'],
|
||||
),
|
||||
'remote_asset_cloud_id_entity': (
|
||||
(s, d) =>
|
||||
RemoteAssetCloudIdEntityCompanion(assetId: .new('a1$s'), createdAt: .new(d), adjustmentTime: .new(d)),
|
||||
(s) => ['a1$s'],
|
||||
),
|
||||
'memory_entity': (
|
||||
(s, d) => MemoryEntityCompanion(
|
||||
id: .new('m1$s'),
|
||||
ownerId: .new('u1$s'),
|
||||
type: const .new(MemoryTypeEnum.onThisDay),
|
||||
data: const .new('{}'),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
deletedAt: .new(d),
|
||||
memoryAt: .new(d),
|
||||
seenAt: .new(d),
|
||||
showAt: .new(d),
|
||||
hideAt: .new(d),
|
||||
),
|
||||
(s) => ['m1$s'],
|
||||
),
|
||||
'stack_entity': (
|
||||
(s, d) => StackEntityCompanion(
|
||||
id: .new('s1$s'),
|
||||
ownerId: .new('u1$s'),
|
||||
primaryAssetId: .new('a1$s'),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
),
|
||||
(s) => ['s1$s'],
|
||||
),
|
||||
'person_entity': (
|
||||
(s, d) => PersonEntityCompanion(
|
||||
id: .new('p1$s'),
|
||||
ownerId: .new('u1$s'),
|
||||
name: const .new('n'),
|
||||
isFavorite: const .new(false),
|
||||
isHidden: const .new(false),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
birthDate: .new(d),
|
||||
),
|
||||
(s) => ['p1$s'],
|
||||
),
|
||||
'asset_face_entity': (
|
||||
(s, d) => AssetFaceEntityCompanion(
|
||||
id: .new('f1$s'),
|
||||
assetId: .new('a1$s'),
|
||||
imageWidth: const .new(1),
|
||||
imageHeight: const .new(1),
|
||||
boundingBoxX1: const .new(0),
|
||||
boundingBoxY1: const .new(0),
|
||||
boundingBoxX2: const .new(1),
|
||||
boundingBoxY2: const .new(1),
|
||||
sourceType: const .new('machine-learning'),
|
||||
deletedAt: .new(d),
|
||||
),
|
||||
(s) => ['f1$s'],
|
||||
),
|
||||
'trashed_local_asset_entity': (
|
||||
(s, d) => TrashedLocalAssetEntityCompanion(
|
||||
id: .new('t1$s'),
|
||||
albumId: .new('al1$s'),
|
||||
name: const .new('n'),
|
||||
type: const .new(AssetType.image),
|
||||
source: const .new(TrashOrigin.localSync),
|
||||
createdAt: .new(d),
|
||||
updatedAt: .new(d),
|
||||
),
|
||||
(s) => ['t1$s', 'al1$s'],
|
||||
),
|
||||
'settings': ((s, d) => SettingsEntityCompanion(key: .new('k1$s'), updatedAt: .new(d)), (s) => ['k1$s']),
|
||||
};
|
||||
|
||||
String pkWhere(String table) => switch (table) {
|
||||
'settings' => '"key" = ?',
|
||||
'remote_exif_entity' || 'remote_asset_cloud_id_entity' => '"asset_id" = ?',
|
||||
'trashed_local_asset_entity' => '"id" = ? AND "album_id" = ?',
|
||||
_ => '"id" = ?',
|
||||
};
|
||||
|
||||
var walked = 0;
|
||||
for (final table in ctx.db.allTables) {
|
||||
final dateColumns = [
|
||||
for (final column in table.$columns)
|
||||
if (column is GeneratedColumn<DateTime>) column.$name,
|
||||
];
|
||||
if (dateColumns.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
walked++;
|
||||
|
||||
final builder = builders[table.entityName];
|
||||
expect(builder, isNotNull, reason: 'no poison row defined for ${table.entityName}');
|
||||
|
||||
for (final (poison, expected, suffix) in [(farFuture, ceiling, '_f'), (bce, floor, '_b')]) {
|
||||
await ctx.db.into(table).insert(builder!.$1(suffix, poison));
|
||||
|
||||
final pkVars = [for (final value in builder.$2(suffix)) Variable(value)];
|
||||
for (final column in dateColumns) {
|
||||
final row = await ctx.db
|
||||
.customSelect(
|
||||
'SELECT "$column" AS v FROM "${table.entityName}" WHERE ${pkWhere(table.entityName)}',
|
||||
variables: pkVars,
|
||||
)
|
||||
.getSingle();
|
||||
expect(row.read<String>('v'), expected, reason: '${table.entityName}.$column');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(walked, 14, reason: 'every table with datetime columns must be walked');
|
||||
});
|
||||
|
||||
test('a late hour on the last day of 9999 is stored at the midnight ceiling', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id, createdAt: DateTime.utc(9999, 12, 31, 23, 59, 59));
|
||||
|
||||
final row = await ctx.db
|
||||
.customSelect('SELECT created_at AS v FROM remote_asset_entity WHERE id = ?', variables: [Variable(asset.id)])
|
||||
.getSingle();
|
||||
|
||||
expect(row.read<String>('v'), ceiling);
|
||||
});
|
||||
}
|
||||
112
mobile/test/medium/migration_test.dart
Normal file
112
mobile/test/medium/migration_test.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
import 'repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
Future<String?> rawStored(String column, String id) async {
|
||||
final row = await ctx.db
|
||||
.customSelect('SELECT $column AS v FROM remote_asset_entity WHERE id = ?', variables: [Variable(id)])
|
||||
.getSingle();
|
||||
return row.readNullable<String>('v');
|
||||
}
|
||||
|
||||
group('healOutOfRangeDateTimes', () {
|
||||
test('rewrites out-of-range stored text and leaves valid rows untouched', () async {
|
||||
final user = await ctx.newUser();
|
||||
final farFuture = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final bce = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final yearZero = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
final valid = await ctx.newRemoteAsset(ownerId: user.id, createdAt: DateTime.utc(2024, 1, 2, 3, 4, 5, 123));
|
||||
|
||||
// raw sql is the only way poison can exist now: the drift api clamps on write
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE remote_asset_entity SET created_at = '+144769-11-18T12:38:32.000Z', local_date_time = '+144769-11-18T18:38:32.000 +06:00' WHERE id = ?",
|
||||
[farFuture.id],
|
||||
);
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE remote_asset_entity SET created_at = '-004712-03-04T05:06:07.000Z' WHERE id = ?",
|
||||
[bce.id],
|
||||
);
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE remote_asset_entity SET created_at = '0000-06-15T10:30:00.000Z' WHERE id = ?",
|
||||
[yearZero.id],
|
||||
);
|
||||
|
||||
await healOutOfRangeDateTimes(ctx.db);
|
||||
|
||||
expect(await rawStored('created_at', farFuture.id), '9999-12-31T00:00:00.000Z');
|
||||
expect(await rawStored('local_date_time', farFuture.id), '9999-12-31T00:00:00.000Z');
|
||||
expect(await rawStored('created_at', bce.id), '0001-01-01T00:00:00.000Z');
|
||||
expect(await rawStored('created_at', yearZero.id), '0001-01-01T00:00:00.000Z');
|
||||
expect(await rawStored('created_at', valid.id), '2024-01-02T03:04:05.123Z');
|
||||
});
|
||||
|
||||
test('heals a stored late-9999 value the year predicate missed', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id);
|
||||
// legal by the server's rule, but re-overflows sqlite under 'localtime' east of UTC
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE remote_asset_entity SET created_at = '9999-12-31T23:59:59.000Z' WHERE id = ?",
|
||||
[asset.id],
|
||||
);
|
||||
|
||||
await healOutOfRangeDateTimes(ctx.db);
|
||||
expect(await rawStored('created_at', asset.id), '9999-12-31T00:00:00.000Z');
|
||||
|
||||
// the healed value itself is not rewritten again
|
||||
await healOutOfRangeDateTimes(ctx.db);
|
||||
expect(await rawStored('created_at', asset.id), '9999-12-31T00:00:00.000Z');
|
||||
});
|
||||
|
||||
test('the merged bucket query crashes before and succeeds after the heal', () async {
|
||||
final user = await ctx.newUser();
|
||||
final album = await ctx.newLocalAlbum(backupSelection: .selected);
|
||||
final asset = await ctx.newLocalAsset(createdAt: DateTime.utc(2024, 1, 2));
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id);
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE local_asset_entity SET created_at = '+144769-11-18T12:38:32.000Z' WHERE id = ?",
|
||||
[asset.id],
|
||||
);
|
||||
|
||||
// the #28559 class: the native merged bucket reads the NULL bucket date
|
||||
final query = ctx.db.mergedAssetDrift.mergedBucket(userIds: [user.id], groupBy: 0);
|
||||
await expectLater(query.get(), throwsA(isA<TypeError>()));
|
||||
|
||||
await healOutOfRangeDateTimes(ctx.db);
|
||||
|
||||
final buckets = await query.get();
|
||||
expect(buckets, hasLength(1));
|
||||
expect(buckets.first.assetCount, 1);
|
||||
expect(buckets.first.bucketDate, '9999-12-31');
|
||||
});
|
||||
|
||||
test('stores datetimes byte-identical to drift text mapping', () async {
|
||||
final user = await ctx.newUser();
|
||||
final remote = await ctx.newRemoteAsset(
|
||||
ownerId: user.id,
|
||||
createdAt: DateTime.utc(2024, 1, 2, 3, 4, 5, 123),
|
||||
updatedAt: DateTime.utc(2024, 2, 3, 4, 5, 6, 456),
|
||||
);
|
||||
|
||||
expect(await rawStored('created_at', remote.id), '2024-01-02T03:04:05.123Z');
|
||||
expect(await rawStored('updated_at', remote.id), '2024-02-03T04:05:06.456Z');
|
||||
|
||||
final local = DateTime(2024, 6, 15, 10, 30, 25);
|
||||
final asset = await ctx.newLocalAsset(createdAt: local);
|
||||
final row = await (ctx.db.select(ctx.db.localAssetEntity)..where((t) => t.id.equals(asset.id))).getSingle();
|
||||
expect(row.createdAt, local);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
|
|
@ -146,4 +150,50 @@ void main() {
|
|||
expect(buckets.single.assetCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('dates sqlite cannot format', () {
|
||||
// Regression check for #28524: out-of-range stored dates are healed by the
|
||||
// migration, so bucket queries never read a NULL bucket date
|
||||
test('archive returns a sane bucket once the poisoned date is healed', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id, visibility: .archive);
|
||||
await ctx.db.customStatement(
|
||||
"UPDATE remote_asset_entity SET created_at = '+144769-11-18T12:38:32.000Z', local_date_time = '+144769-11-18T18:38:32.000 +06:00' WHERE id = ?",
|
||||
[asset.id],
|
||||
);
|
||||
|
||||
final query = sut.archived(user.id, .day);
|
||||
await expectLater(query.bucketSource().first, throwsA(isA<TypeError>()));
|
||||
|
||||
await healOutOfRangeDateTimes(ctx.db);
|
||||
|
||||
expect(await query.bucketSource().first, [TimeBucket(date: DateTime(9999, 12, 31), assetCount: 1)]);
|
||||
|
||||
final assets = await query.assetSource(0, 10);
|
||||
expect(assets, hasLength(1));
|
||||
expect((assets.single as RemoteAsset).id, asset.id);
|
||||
});
|
||||
|
||||
test('a late-9999 date is clamped on write, so the bucket query stays safe', () async {
|
||||
// local_date_time null -> the bucket coalesce falls to created_at with
|
||||
// 'localtime', which overflows sqlite on the unclamped value east of UTC.
|
||||
// the converter writes the midnight ceiling instead (sqlite probe receipt)
|
||||
final user = await ctx.newUser();
|
||||
await ctx.db.remoteAssetEntity.insertOne(
|
||||
RemoteAssetEntityCompanion.insert(
|
||||
id: 'late1',
|
||||
name: 'late1.jpg',
|
||||
type: AssetType.image,
|
||||
checksum: 'ck_late1',
|
||||
ownerId: user.id,
|
||||
visibility: AssetVisibility.archive,
|
||||
createdAt: Value(DateTime.utc(9999, 12, 31, 23, 59, 59)),
|
||||
localDateTime: const Value(null),
|
||||
),
|
||||
);
|
||||
|
||||
final query = sut.archived(user.id, .day);
|
||||
expect(await query.bucketSource().first, [TimeBucket(date: DateTime(9999, 12, 31), assetCount: 1)]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue