From 7594ac1223499abf5125e2f996aaaa74c4b08823 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Sun, 2 Aug 2026 11:43:51 +0600 Subject: [PATCH 1/3] fix(mobile): clamp out of range datetimes so timeline queries cannot crash --- .../drift_schemas/main/drift_schema_v32.json | 3754 +++++++++++++++++ .../entities/asset_face.entity.dart | 3 +- .../entities/auth_user.entity.dart | 4 +- .../infrastructure/entities/exif.entity.dart | 3 +- .../entities/local_album.entity.dart | 3 +- .../entities/local_asset.entity.dart | 3 +- .../infrastructure/entities/log.entity.dart | 3 +- .../entities/memory.entity.dart | 15 +- .../entities/person.entity.dart | 7 +- .../entities/remote_album.entity.dart | 5 +- .../entities/remote_asset.entity.dart | 7 +- .../remote_asset_cloud_id.entity.dart | 5 +- .../entities/settings.entity.dart | 3 +- .../infrastructure/entities/stack.entity.dart | 5 +- .../infrastructure/entities/user.entity.dart | 4 +- .../repositories/db.repository.dart | 39 +- .../repositories/db.repository.steps.dart | 593 +++ .../lib/infrastructure/utils/asset.mixin.dart | 5 +- .../utils/datetime.converter.dart | 32 + .../utils/datetime.converter_test.dart | 91 + .../medium/datetime_clamp_converter_test.dart | 242 ++ mobile/test/medium/migration_test.dart | 137 + .../timeline_repository_test.dart | 50 + 23 files changed, 4983 insertions(+), 30 deletions(-) create mode 100644 mobile/drift_schemas/main/drift_schema_v32.json create mode 100644 mobile/lib/infrastructure/utils/datetime.converter.dart create mode 100644 mobile/test/infrastructure/utils/datetime.converter_test.dart create mode 100644 mobile/test/medium/datetime_clamp_converter_test.dart create mode 100644 mobile/test/medium/migration_test.dart diff --git a/mobile/drift_schemas/main/drift_schema_v32.json b/mobile/drift_schemas/main/drift_schema_v32.json new file mode 100644 index 0000000000..e1f0642459 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v32.json @@ -0,0 +1,3754 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": true + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "remote_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "local_date_time", + "getter_name": "localDateTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "thumb_hash", + "getter_name": "thumbHash", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "uploaded_at", + "getter_name": "uploadedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "live_photo_video_id", + "getter_name": "livePhotoVideoId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "visibility", + "getter_name": "visibility", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetVisibility.values)", + "dart_type_name": "AssetVisibility" + } + }, + { + "name": "stack_id", + "getter_name": "stackId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "library_id", + "getter_name": "libraryId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_edited", + "getter_name": "isEdited", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_edited\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 2, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "stack_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "primary_asset_id", + "getter_name": "primaryAssetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "i_cloud_id", + "getter_name": "iCloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 4, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'\\'')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "thumbnail_asset_id", + "getter_name": "thumbnailAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "is_activity_enabled", + "getter_name": "isActivityEnabled", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "order", + "getter_name": "order", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)", + "dart_type_name": "AlbumAssetOrder" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 5, + "references": [ + 4 + ], + "type": "table", + "data": { + "name": "local_album_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "backup_selection", + "getter_name": "backupSelection", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(BackupSelection.values)", + "dart_type_name": "BackupSelection" + } + }, + { + "name": "is_ios_shared_album", + "getter_name": "isIosSharedAlbum", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "linked_remote_album_id", + "getter_name": "linkedRemoteAlbumId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 6, + "references": [ + 3, + 5 + ], + "type": "table", + "data": { + "name": "local_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES local_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES local_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "local_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "marker", + "getter_name": "marker_", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"marker\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"marker\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 7, + "references": [ + 6 + ], + "type": "index", + "data": { + "on": 6, + "name": "idx_local_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 8, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 9, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 10, + "references": [ + 3 + ], + "type": "index", + "data": { + "on": 3, + "name": "idx_local_asset_created_at", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)", + "unique": false, + "columns": [] + } + }, + { + "id": 11, + "references": [ + 2 + ], + "type": "index", + "data": { + "on": 2, + "name": "idx_stack_primary_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 12, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 13, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "UQ_remote_assets_owner_library_checksum", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n", + "unique": true, + "columns": [] + } + }, + { + "id": 14, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 15, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_stack_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 16, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n", + "unique": false, + "columns": [] + } + }, + { + "id": 17, + "references": [ + 1 + ], + "type": "index", + "data": { + "on": 1, + "name": "idx_remote_asset_uploaded", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)", + "unique": false, + "columns": [] + } + }, + { + "id": 18, + "references": [], + "type": "table", + "data": { + "name": "auth_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "email", + "getter_name": "email", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_admin", + "getter_name": "isAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_profile_image", + "getter_name": "hasProfileImage", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "profile_changed_at", + "getter_name": "profileChangedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "avatar_color", + "getter_name": "avatarColor", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AvatarColor.values)", + "dart_type_name": "AvatarColor" + } + }, + { + "name": "quota_size_in_bytes", + "getter_name": "quotaSizeInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quota_usage_in_bytes", + "getter_name": "quotaUsageInBytes", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pin_code", + "getter_name": "pinCode", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 19, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_metadata_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "key", + "getter_name": "key", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)", + "dart_type_name": "UserMetadataKey" + } + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "userMetadataConverter", + "dart_type_name": "Map" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "user_id", + "key" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "partner_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shared_by_id", + "getter_name": "sharedById", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "shared_with_id", + "getter_name": "sharedWithId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "in_timeline", + "getter_name": "inTimeline", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"in_timeline\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"in_timeline\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "shared_by_id", + "shared_with_id" + ] + } + }, + { + "id": 21, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_exif_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "city", + "getter_name": "city", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state", + "getter_name": "state", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "country", + "getter_name": "country", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "date_time_original", + "getter_name": "dateTimeOriginal", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "exposure_time", + "getter_name": "exposureTime", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "f_number", + "getter_name": "fNumber", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "file_size", + "getter_name": "fileSize", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "focal_length", + "getter_name": "focalLength", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "iso", + "getter_name": "iso", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "make", + "getter_name": "make", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "model", + "getter_name": "model", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "lens", + "getter_name": "lens", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "time_zone", + "getter_name": "timeZone", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "rating", + "getter_name": "rating", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "projection_type", + "getter_name": "projectionType", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 22, + "references": [ + 1, + 4 + ], + "type": "table", + "data": { + "name": "remote_album_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "album_id" + ] + } + }, + { + "id": 23, + "references": [ + 4, + 0 + ], + "type": "table", + "data": { + "name": "remote_album_user_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_album_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "role", + "getter_name": "role", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)", + "dart_type_name": "AlbumUserRole" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "album_id", + "user_id" + ] + } + }, + { + "id": 24, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "remote_asset_cloud_id_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "cloud_id", + "getter_name": "cloudId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "adjustment_time", + "getter_name": "adjustmentTime", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "latitude", + "getter_name": "latitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "longitude", + "getter_name": "longitude", + "moor_type": "double", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id" + ] + } + }, + { + "id": 25, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "memory_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(MemoryTypeEnum.values)", + "dart_type_name": "MemoryTypeEnum" + } + }, + { + "name": "data", + "getter_name": "data", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_saved", + "getter_name": "isSaved", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_saved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "memory_at", + "getter_name": "memoryAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "seen_at", + "getter_name": "seenAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "show_at", + "getter_name": "showAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "hide_at", + "getter_name": "hideAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 26, + "references": [ + 1, + 25 + ], + "type": "table", + "data": { + "name": "memory_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "memory_id", + "getter_name": "memoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "memory_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "asset_id", + "memory_id" + ] + } + }, + { + "id": 27, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "person_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "owner_id", + "getter_name": "ownerId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "face_asset_id", + "getter_name": "faceAssetId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "color", + "getter_name": "color", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "birth_date", + "getter_name": "birthDate", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 28, + "references": [ + 1, + 27 + ], + "type": "table", + "data": { + "name": "asset_face_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "person_id", + "getter_name": "personId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "person_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "image_width", + "getter_name": "imageWidth", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "image_height", + "getter_name": "imageHeight", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x1", + "getter_name": "boundingBoxX1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y1", + "getter_name": "boundingBoxY1", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_x2", + "getter_name": "boundingBoxX2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "bounding_box_y2", + "getter_name": "boundingBoxY2", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source_type", + "getter_name": "sourceType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_at", + "getter_name": "deletedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 29, + "references": [], + "type": "table", + "data": { + "name": "store_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "string_value", + "getter_name": "stringValue", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "int_value", + "getter_name": "intValue", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 30, + "references": [], + "type": "table", + "data": { + "name": "trashed_local_asset_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetType.values)", + "dart_type_name": "AssetType" + } + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + }, + { + "name": "width", + "getter_name": "width", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "height", + "getter_name": "height", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "duration_ms", + "getter_name": "durationMs", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "album_id", + "getter_name": "albumId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "checksum", + "getter_name": "checksum", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "orientation", + "getter_name": "orientation", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "source", + "getter_name": "source", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(TrashOrigin.values)", + "dart_type_name": "TrashOrigin" + } + }, + { + "name": "playback_style", + "getter_name": "playbackStyle", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)", + "dart_type_name": "AssetPlaybackStyle" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id", + "album_id" + ] + } + }, + { + "id": 31, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_edit_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "action", + "getter_name": "action", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(AssetEditAction.values)", + "dart_type_name": "AssetEditAction" + } + }, + { + "name": "parameters", + "getter_name": "parameters", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "editParameterConverter", + "dart_type_name": "Map" + } + }, + { + "name": "sequence", + "getter_name": "sequence", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 32, + "references": [], + "type": "table", + "data": { + "name": "settings", + "was_declared_in_moor": false, + "columns": [ + { + "name": "key", + "getter_name": "key", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "value", + "getter_name": "value", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "updated_at", + "getter_name": "updatedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DateTimeClampConverter()", + "dart_type_name": "DateTime" + } + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "key" + ] + } + }, + { + "id": 33, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "asset_ocr_entity", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "asset_id", + "getter_name": "assetId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "remote_asset_entity", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "x1", + "getter_name": "x1", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y1", + "getter_name": "y1", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x2", + "getter_name": "x2", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y2", + "getter_name": "y2", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x3", + "getter_name": "x3", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y3", + "getter_name": "y3", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "x4", + "getter_name": "x4", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "y4", + "getter_name": "y4", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "box_score", + "getter_name": "boxScore", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "text_score", + "getter_name": "textScore", + "moor_type": "double", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recognized_text", + "getter_name": "recognizedText", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_visible", + "getter_name": "isVisible", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_visible\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": true, + "constraints": [], + "strict": true, + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 34, + "references": [ + 20 + ], + "type": "index", + "data": { + "on": 20, + "name": "idx_partner_shared_with_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 35, + "references": [ + 21 + ], + "type": "index", + "data": { + "on": 21, + "name": "idx_lat_lng", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)", + "unique": false, + "columns": [] + } + }, + { + "id": 36, + "references": [ + 21 + ], + "type": "index", + "data": { + "on": 21, + "name": "idx_remote_exif_city", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 37, + "references": [ + 22 + ], + "type": "index", + "data": { + "on": 22, + "name": "idx_remote_album_asset_album_asset", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 38, + "references": [ + 24 + ], + "type": "index", + "data": { + "on": 24, + "name": "idx_remote_asset_cloud_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 39, + "references": [ + 27 + ], + "type": "index", + "data": { + "on": 27, + "name": "idx_person_owner_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 40, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_person_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 41, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 42, + "references": [ + 28 + ], + "type": "index", + "data": { + "on": 28, + "name": "idx_asset_face_visible_person", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n", + "unique": false, + "columns": [] + } + }, + { + "id": 43, + "references": [ + 30 + ], + "type": "index", + "data": { + "on": 30, + "name": "idx_trashed_local_asset_checksum", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)", + "unique": false, + "columns": [] + } + }, + { + "id": 44, + "references": [ + 30 + ], + "type": "index", + "data": { + "on": 30, + "name": "idx_trashed_local_asset_album", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 45, + "references": [ + 31 + ], + "type": "index", + "data": { + "on": 31, + "name": "idx_asset_edit_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)", + "unique": false, + "columns": [] + } + }, + { + "id": 46, + "references": [ + 33 + ], + "type": "index", + "data": { + "on": 33, + "name": "idx_asset_ocr_asset_id", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)", + "unique": false, + "columns": [] + } + } + ], + "fixed_sql": [ + { + "name": "user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"uploaded_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "stack_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "local_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_local_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_local_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)" + } + ] + }, + { + "name": "idx_local_asset_created_at", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)" + } + ] + }, + { + "name": "idx_stack_primary_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)" + } + ] + }, + { + "name": "UQ_remote_assets_owner_library_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "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)" + } + ] + }, + { + "name": "idx_remote_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_remote_asset_stack_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)" + } + ] + }, + { + "name": "idx_remote_asset_owner_visibility_deleted_created", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)" + } + ] + }, + { + "name": "idx_remote_asset_uploaded", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)" + } + ] + }, + { + "name": "auth_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "user_metadata_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "partner_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_exif_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_album_user_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "remote_asset_cloud_id_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "memory_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "person_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_face_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "store_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "trashed_local_asset_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_edit_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "settings", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"settings\" (\"key\" TEXT NOT NULL, \"value\" TEXT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "asset_ocr_entity", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"asset_ocr_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"x1\" REAL NOT NULL, \"y1\" REAL NOT NULL, \"x2\" REAL NOT NULL, \"y2\" REAL NOT NULL, \"x3\" REAL NOT NULL, \"y3\" REAL NOT NULL, \"x4\" REAL NOT NULL, \"y4\" REAL NOT NULL, \"box_score\" REAL NOT NULL, \"text_score\" REAL NOT NULL, \"recognized_text\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;" + } + ] + }, + { + "name": "idx_partner_shared_with_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)" + } + ] + }, + { + "name": "idx_lat_lng", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)" + } + ] + }, + { + "name": "idx_remote_exif_city", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL" + } + ] + }, + { + "name": "idx_remote_album_asset_album_asset", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)" + } + ] + }, + { + "name": "idx_remote_asset_cloud_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)" + } + ] + }, + { + "name": "idx_person_owner_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)" + } + ] + }, + { + "name": "idx_asset_face_person_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)" + } + ] + }, + { + "name": "idx_asset_face_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_face_visible_person", + "sql": [ + { + "dialect": "sqlite", + "sql": "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" + } + ] + }, + { + "name": "idx_trashed_local_asset_checksum", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)" + } + ] + }, + { + "name": "idx_trashed_local_asset_album", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)" + } + ] + }, + { + "name": "idx_asset_edit_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)" + } + ] + }, + { + "name": "idx_asset_ocr_asset_id", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)" + } + ] + } + ] +} \ No newline at end of file diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index b94a0cf094..c011db77f5 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -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.converter.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 => dateTime().nullable().map(const DateTimeClampConverter())(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/auth_user.entity.dart b/mobile/lib/infrastructure/entities/auth_user.entity.dart index cbbeaf536f..ee58b12ed4 100644 --- a/mobile/lib/infrastructure/entities/auth_user.entity.dart +++ b/mobile/lib/infrastructure/entities/auth_user.entity.dart @@ -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.converter.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class AuthUserEntity extends Table with DriftDefaultsMixin { @@ -12,7 +13,8 @@ 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 => + dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); IntColumn get avatarColor => intEnum()(); // Quota diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 3328b38f31..d598d48f35 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -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.converter.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 => dateTime().nullable().map(const DateTimeClampConverter())(); TextColumn get description => text().nullable()(); diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index 641a5359f6..e7c300d04b 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -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.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); IntColumn get backupSelection => intEnum()(); BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index a19c1aa540..9ae7e112bf 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -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.converter.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 => dateTime().nullable().map(const DateTimeClampConverter())(); RealColumn get latitude => real().nullable()(); diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index 264bd3331f..97d3f01520 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -1,6 +1,7 @@ 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.converter.dart'; class LogMessageEntity extends Table { const LogMessageEntity(); @@ -12,7 +13,7 @@ class LogMessageEntity extends Table { TextColumn get message => text()(); TextColumn get details => text().nullable()(); IntColumn get level => intEnum()(); - DateTimeColumn get createdAt => dateTime()(); + DateTimeColumn get createdAt => dateTime().map(const DateTimeClampConverter())(); TextColumn get logger => text().nullable()(); TextColumn get stack => text().nullable()(); } diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart index 63dcfef5cc..5b3ecbd890 100644 --- a/mobile/lib/infrastructure/entities/memory.entity.dart +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -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.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get deletedAt => dateTime().nullable()(); + DateTimeColumn get deletedAt => dateTime().nullable().map(const DateTimeClampConverter())(); 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 => dateTime().map(const DateTimeClampConverter())(); - DateTimeColumn get seenAt => dateTime().nullable()(); + DateTimeColumn get seenAt => dateTime().nullable().map(const DateTimeClampConverter())(); - DateTimeColumn get showAt => dateTime().nullable()(); + DateTimeColumn get showAt => dateTime().nullable().map(const DateTimeClampConverter())(); - DateTimeColumn get hideAt => dateTime().nullable()(); + DateTimeColumn get hideAt => dateTime().nullable().map(const DateTimeClampConverter())(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index 6e014590ab..dfc81d2c55 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -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.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); 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 => dateTime().nullable().map(const DateTimeClampConverter())(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart index d12d511148..1640cab689 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -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.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); TextColumn get thumbnailAssetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()(); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 3fe56ae431..067c399a67 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -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.converter.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 => dateTime().nullable().map(const DateTimeClampConverter())(); TextColumn get thumbHash => text().nullable()(); - DateTimeColumn get deletedAt => dateTime().nullable()(); + DateTimeColumn get deletedAt => dateTime().nullable().map(const DateTimeClampConverter())(); - DateTimeColumn get uploadedAt => dateTime().nullable()(); + DateTimeColumn get uploadedAt => dateTime().nullable().map(const DateTimeClampConverter())(); TextColumn get livePhotoVideoId => text().nullable()(); diff --git a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart index 593931f986..afa0fa3348 100644 --- a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart @@ -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.converter.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 => dateTime().nullable().map(const DateTimeClampConverter())(); - DateTimeColumn get adjustmentTime => dateTime().nullable()(); + DateTimeColumn get adjustmentTime => dateTime().nullable().map(const DateTimeClampConverter())(); RealColumn get latitude => real().nullable()(); diff --git a/mobile/lib/infrastructure/entities/settings.entity.dart b/mobile/lib/infrastructure/entities/settings.entity.dart index a246c03f5d..bbbe094ffd 100644 --- a/mobile/lib/infrastructure/entities/settings.entity.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/datetime.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); @override Set get primaryKey => {key}; diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index 4f90845a45..23605a0e7b 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -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.converter.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 => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 8d4371672c..5f676609c1 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -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.converter.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserEntity extends Table with DriftDefaultsMixin { @@ -11,7 +12,8 @@ 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 => + dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); IntColumn get avatarColor => intEnum().withDefault(const Constant(0))(); @override diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 69e45685de..687d7b42cb 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -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,40 @@ class Drift extends $Drift { ); } +// hardcoded on purpose: datetimes are stored as text, so versioned schema tables type +// these columns String and the set cannot be derived — migration_test pins it to drift_schema_v32.json +@visibleForTesting +const healDateTimeColumns = >{ + '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-converter syncs), plus anything later than the safe +// midnight ceiling, which re-overflows sqlite under 'localtime' east of UTC +@visibleForTesting +Future healOutOfRangeDateTimes(GeneratedDatabase db) async { + for (final MapEntry(key: table, value: columns) in healDateTimeColumns.entries) { + for (final column in columns) { + await db.customStatement( + "UPDATE $table SET $column = CASE WHEN substr($column, 1, 1) = '-' OR substr($column, 1, 4) = '0000' THEN '0001-01-01T00:00:00.000Z' ELSE '9999-12-31T00:00:00.000Z' END WHERE substr($column, 1, 1) IN ('+', '-') OR substr($column, 1, 4) = '0000' OR $column > '9999-12-31T00:00:00.000Z'", + ); + } + } +} + class DriftDatabaseRepository { final Drift _db; const DriftDatabaseRepository(this._db); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 39f9c2ea04..a4cbb924ef 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -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 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 Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -16537,6 +17122,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema29 schema) from28To29, required Future Function(i1.Migrator m, Schema30 schema) from29To30, required Future Function(i1.Migrator m, Schema31 schema) from30To31, + required Future 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 Function(i1.Migrator m, Schema29 schema) from28To29, required Future Function(i1.Migrator m, Schema30 schema) from29To30, required Future Function(i1.Migrator m, Schema31 schema) from30To31, + required Future 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, ), ); diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart index 17e7e5d707..093e327a3d 100644 --- a/mobile/lib/infrastructure/utils/asset.mixin.dart +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -1,11 +1,12 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/utils/datetime.converter.dart'; mixin AssetEntityMixin on Table { TextColumn get name => text()(); IntColumn get type => intEnum()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); IntColumn get width => integer().nullable()(); IntColumn get height => integer().nullable()(); IntColumn get durationMs => integer().nullable()(); diff --git a/mobile/lib/infrastructure/utils/datetime.converter.dart b/mobile/lib/infrastructure/utils/datetime.converter.dart new file mode 100644 index 0000000000..89d41e3b11 --- /dev/null +++ b/mobile/lib/infrastructure/utils/datetime.converter.dart @@ -0,0 +1,32 @@ +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 every stored datetime into the representable range; anything +// in range passes through untouched. +final class DateTimeClampConverter extends TypeConverter { + const DateTimeClampConverter(); + + // 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 + static final DateTime _floor = DateTime.utc(1); + static final DateTime _ceiling = DateTime.utc(9999, 12, 31); + + @override + DateTime fromSql(DateTime fromDb) => _clamp(fromDb); + + @override + DateTime toSql(DateTime value) => _clamp(value); + + static DateTime _clamp(DateTime value) { + if (value.isBefore(_floor)) { + return _floor; + } + if (value.isAfter(_ceiling)) { + return _ceiling; + } + return value; + } +} diff --git a/mobile/test/infrastructure/utils/datetime.converter_test.dart b/mobile/test/infrastructure/utils/datetime.converter_test.dart new file mode 100644 index 0000000000..71dd715ef8 --- /dev/null +++ b/mobile/test/infrastructure/utils/datetime.converter_test.dart @@ -0,0 +1,91 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; +import 'package:immich_mobile/infrastructure/utils/datetime.converter.dart'; + +void main() { + const converter = DateTimeClampConverter(); + + test('clamps far-future years to the ceiling in both directions', () { + // the reporter's date from #28524 + final poison = DateTime.utc(144769, 11, 18, 12, 38, 32); + final ceiling = DateTime.utc(9999, 12, 31); + expect(converter.toSql(poison), ceiling); + expect(converter.fromSql(poison), ceiling); + expect(converter.toSql(DateTime(144769, 1, 1)), ceiling); // local poison normalizes to utc + }); + + test('clamps BCE and year zero to the floor in both directions', () { + final floor = DateTime.utc(1, 1, 1); + expect(converter.toSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); + expect(converter.fromSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); + expect(converter.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(converter.toSql(low), low); + expect(converter.fromSql(low), low); + expect(converter.toSql(justBefore), justBefore); + expect(converter.toSql(normal), normal); + expect(converter.fromSql(normal), normal); + // local datetimes keep their zone flag + final local = DateTime(2024, 6, 15, 10, 30, 25); + expect(converter.toSql(local), local); + expect(converter.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(converter.toSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); + expect(converter.fromSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); + expect(converter.toSql(DateTime(9999, 12, 31, 23, 59, 59)), ceiling); // local normalizes to utc + }); + + group('every datetime column carries the clamp converter', () { + int expectClampConverter(GeneratedDatabase db) { + final poison = DateTime.utc(144769, 11, 18, 12, 38, 32); + var count = 0; + for (final table in db.allTables) { + for (final column in table.$columns) { + if (column.type != DriftSqlType.dateTime) { + continue; + } + count++; + // nullable columns wrap the same converter in drift's NullAwareTypeConverter + expect( + column, + isA>(), + reason: '${table.entityName}.${column.$name} is missing the clamp converter', + ); + final converter = (column as GeneratedColumnWithTypeConverter).converter; + expect( + converter.toSql(poison), + DateTime.utc(9999, 12, 31), + reason: '${table.entityName}.${column.$name} is missing the clamp converter', + ); + } + } + return count; + } + + test('main database: all 32 columns across all tables', () { + final db = Drift(NativeDatabase.memory()); + addTearDown(db.close); + expect(expectClampConverter(db), 32); + }); + + test('logs database: the log timestamp column', () { + final logDb = DriftLogger.fromExecutor(NativeDatabase.memory()); + addTearDown(logDb.close); + expect(expectClampConverter(logDb), 1); + }); + }); +} diff --git a/mobile/test/medium/datetime_clamp_converter_test.dart b/mobile/test/medium/datetime_clamp_converter_test.dart new file mode 100644 index 0000000000..10b41db7da --- /dev/null +++ b/mobile/test/medium/datetime_clamp_converter_test.dart @@ -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 = + Function(String suffix, DateTime d), List 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.type == DriftSqlType.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('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('v'), ceiling); + }); +} diff --git a/mobile/test/medium/migration_test.dart b/mobile/test/medium/migration_test.dart new file mode 100644 index 0000000000..838103ece2 --- /dev/null +++ b/mobile/test/medium/migration_test.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'dart:io'; + +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 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('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())); + + 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); + }); + + test('the heal column map matches the datetime columns of the v32 schema json', () async { + final schemaJson = jsonDecode(await File('drift_schemas/main/drift_schema_v32.json').readAsString()); + final expected = { + for (final entity in schemaJson['entities'] as List) + if (entity['type'] == 'table') + for (final column in entity['data']['columns'] as List) + if (column['moor_type'] == 'dateTime') '${entity['data']['name']}.${column['name']}', + }; + + final mapped = { + for (final MapEntry(key: table, value: columns) in healDateTimeColumns.entries) + for (final column in columns) '$table.$column', + }; + + final missing = expected.difference(mapped); + final extra = mapped.difference(expected); + expect(missing, isEmpty, reason: 'heal map misses v32 datetime columns: ${missing.join(', ')}'); + expect(extra, isEmpty, reason: 'heal map has columns that are not v32 datetimes: ${extra.join(', ')}'); + final mappedCount = healDateTimeColumns.values.fold(0, (sum, columns) => sum + columns.length); + expect(mappedCount, expected.length, reason: 'heal map count differs from the v32 schema json'); + }); + }); +} diff --git a/mobile/test/medium/repositories/timeline_repository_test.dart b/mobile/test/medium/repositories/timeline_repository_test.dart index 502a65d45b..df25c8c313 100644 --- a/mobile/test/medium/repositories/timeline_repository_test.dart +++ b/mobile/test/medium/repositories/timeline_repository_test.dart @@ -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())); + + 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)]); + }); + }); } From 3f26f1d99969147035dbb7c569732540273e56c2 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Tue, 4 Aug 2026 12:58:16 +0600 Subject: [PATCH 2/3] replace the per column converters with a clamping custom type --- mobile/analysis_options.yaml | 2 + .../drift_schemas/main/drift_schema_v32.json | 256 +++++------------- .../entities/asset_face.entity.dart | 2 +- .../entities/auth_user.entity.dart | 3 +- .../infrastructure/entities/exif.entity.dart | 2 +- .../entities/local_album.entity.dart | 2 +- .../entities/local_asset.entity.dart | 2 +- .../infrastructure/entities/log.entity.dart | 11 +- .../entities/memory.entity.dart | 14 +- .../entities/person.entity.dart | 6 +- .../entities/remote_album.entity.dart | 4 +- .../entities/remote_asset.entity.dart | 6 +- .../remote_asset_cloud_id.entity.dart | 4 +- .../entities/settings.entity.dart | 2 +- .../infrastructure/entities/stack.entity.dart | 4 +- .../infrastructure/entities/user.entity.dart | 3 +- .../repositories/db.repository.dart | 2 +- .../lib/infrastructure/utils/asset.mixin.dart | 7 +- .../utils/datetime.converter.dart | 48 ++-- .../utils/drift_default.mixin.dart | 4 + .../utils/datetime.converter_test.dart | 73 ++--- .../medium/datetime_clamp_converter_test.dart | 2 +- mobile/test/medium/migration_test.dart | 10 +- 23 files changed, 174 insertions(+), 295 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index e828760789..de10c415ff 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -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: diff --git a/mobile/drift_schemas/main/drift_schema_v32.json b/mobile/drift_schemas/main/drift_schema_v32.json index e1f0642459..8147efca1a 100644 --- a/mobile/drift_schemas/main/drift_schema_v32.json +++ b/mobile/drift_schemas/main/drift_schema_v32.json @@ -62,16 +62,12 @@ { "name": "profile_changed_at", "getter_name": "profileChangedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "avatar_color", @@ -134,30 +130,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "width", @@ -252,16 +240,12 @@ { "name": "local_date_time", "getter_name": "localDateTime", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "thumb_hash", @@ -276,30 +260,22 @@ { "name": "deleted_at", "getter_name": "deletedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "uploaded_at", "getter_name": "uploadedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "live_photo_video_id", @@ -392,30 +368,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "owner_id", @@ -498,30 +466,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "width", @@ -610,16 +570,12 @@ { "name": "adjustment_time", "getter_name": "adjustmentTime", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "latitude", @@ -708,30 +664,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "thumbnail_asset_id", @@ -830,16 +778,12 @@ { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "backup_selection", @@ -1230,16 +1174,12 @@ { "name": "profile_changed_at", "getter_name": "profileChangedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "avatar_color", @@ -1526,16 +1466,12 @@ { "name": "date_time_original", "getter_name": "dateTimeOriginal", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "description", @@ -1928,30 +1864,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "adjustment_time", "getter_name": "adjustmentTime", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "latitude", @@ -2006,44 +1934,32 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "deleted_at", "getter_name": "deletedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "owner_id", @@ -2112,58 +2028,42 @@ { "name": "memory_at", "getter_name": "memoryAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "seen_at", "getter_name": "seenAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "show_at", "getter_name": "showAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "hide_at", "getter_name": "hideAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] } ], "is_virtual": false, @@ -2272,30 +2172,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "owner_id", @@ -2384,16 +2276,12 @@ { "name": "birth_date", "getter_name": "birthDate", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] } ], "is_virtual": false, @@ -2565,16 +2453,12 @@ { "name": "deleted_at", "getter_name": "deletedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] } ], "is_virtual": false, @@ -2669,30 +2553,22 @@ { "name": "created_at", "getter_name": "createdAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] }, { "name": "width", @@ -2942,16 +2818,12 @@ { "name": "updated_at", "getter_name": "updatedAt", - "moor_type": "dateTime", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')", "default_client_dart": null, - "dsl_features": [], - "type_converter": { - "dart_expr": "const DateTimeClampConverter()", - "dart_type_name": "DateTime" - } + "dsl_features": [] } ], "is_virtual": false, diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index c011db77f5..49fcd9202a 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -36,7 +36,7 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin { BoolColumn get isVisible => boolean().withDefault(const Constant(true))(); - DateTimeColumn get deletedAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/auth_user.entity.dart b/mobile/lib/infrastructure/entities/auth_user.entity.dart index ee58b12ed4..18d3eeb76e 100644 --- a/mobile/lib/infrastructure/entities/auth_user.entity.dart +++ b/mobile/lib/infrastructure/entities/auth_user.entity.dart @@ -13,8 +13,7 @@ class AuthUserEntity extends Table with DriftDefaultsMixin { // Profile image BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))(); - DateTimeColumn get profileChangedAt => - dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get profileChangedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); IntColumn get avatarColor => intEnum()(); // Quota diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index d598d48f35..43ac53d377 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -22,7 +22,7 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin { TextColumn get country => text().nullable()(); - DateTimeColumn get dateTimeOriginal => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get dateTimeOriginal => customType(clampedDateTime).nullable()(); TextColumn get description => text().nullable()(); diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index e7c300d04b..c3ced83be1 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -10,7 +10,7 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin { TextColumn get id => text()(); TextColumn get name => text()(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); IntColumn get backupSelection => intEnum()(); BoolColumn get isIosSharedAlbum => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 9ae7e112bf..ea336fcb02 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -21,7 +21,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { TextColumn get iCloudId => text().nullable()(); - DateTimeColumn get adjustmentTime => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get adjustmentTime => customType(clampedDateTime).nullable()(); RealColumn get latitude => real().nullable()(); diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index 97d3f01520..c45ec842a0 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -2,18 +2,25 @@ 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.converter.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()(); - DateTimeColumn get createdAt => dateTime().map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime)(); TextColumn get logger => text().nullable()(); TextColumn get stack => text().nullable()(); } diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart index 5b3ecbd890..bd7d44216b 100644 --- a/mobile/lib/infrastructure/entities/memory.entity.dart +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -9,11 +9,11 @@ class MemoryEntity extends Table with DriftDefaultsMixin { TextColumn get id => text()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); - DateTimeColumn get deletedAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()(); TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); @@ -23,13 +23,13 @@ class MemoryEntity extends Table with DriftDefaultsMixin { BoolColumn get isSaved => boolean().withDefault(const Constant(false))(); - DateTimeColumn get memoryAt => dateTime().map(const DateTimeClampConverter())(); + DateTimeColumn get memoryAt => customType(clampedDateTime)(); - DateTimeColumn get seenAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get seenAt => customType(clampedDateTime).nullable()(); - DateTimeColumn get showAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get showAt => customType(clampedDateTime).nullable()(); - DateTimeColumn get hideAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get hideAt => customType(clampedDateTime).nullable()(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index dfc81d2c55..6d633093f7 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -9,9 +9,9 @@ class PersonEntity extends Table with DriftDefaultsMixin { TextColumn get id => text()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); @@ -25,7 +25,7 @@ class PersonEntity extends Table with DriftDefaultsMixin { TextColumn get color => text().nullable()(); - DateTimeColumn get birthDate => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get birthDate => customType(clampedDateTime).nullable()(); @override Set get primaryKey => {id}; diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart index 1640cab689..2104967f2d 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -13,9 +13,9 @@ class RemoteAlbumEntity extends Table with DriftDefaultsMixin { TextColumn get description => text().withDefault(const Constant(''))(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); TextColumn get thumbnailAssetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull).nullable()(); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 067c399a67..68dfed11a5 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -34,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().map(const DateTimeClampConverter())(); + DateTimeColumn get localDateTime => customType(clampedDateTime).nullable()(); TextColumn get thumbHash => text().nullable()(); - DateTimeColumn get deletedAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get deletedAt => customType(clampedDateTime).nullable()(); - DateTimeColumn get uploadedAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get uploadedAt => customType(clampedDateTime).nullable()(); TextColumn get livePhotoVideoId => text().nullable()(); diff --git a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart index afa0fa3348..a548676df6 100644 --- a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart @@ -9,9 +9,9 @@ class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin { TextColumn get cloudId => text().nullable()(); - DateTimeColumn get createdAt => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime).nullable()(); - DateTimeColumn get adjustmentTime => dateTime().nullable().map(const DateTimeClampConverter())(); + DateTimeColumn get adjustmentTime => customType(clampedDateTime).nullable()(); RealColumn get latitude => real().nullable()(); diff --git a/mobile/lib/infrastructure/entities/settings.entity.dart b/mobile/lib/infrastructure/entities/settings.entity.dart index bbbe094ffd..31bed6980d 100644 --- a/mobile/lib/infrastructure/entities/settings.entity.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.dart @@ -9,7 +9,7 @@ class SettingsEntity extends Table with DriftDefaultsMixin { TextColumn get value => text().nullable()(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); @override Set get primaryKey => {key}; diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index 23605a0e7b..db3800be43 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -9,9 +9,9 @@ class StackEntity extends Table with DriftDefaultsMixin { TextColumn get id => text()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get createdAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get updatedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); TextColumn get ownerId => text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 5f676609c1..8df3ae1930 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -12,8 +12,7 @@ class UserEntity extends Table with DriftDefaultsMixin { // Profile image BoolColumn get hasProfileImage => boolean().withDefault(const Constant(false))(); - DateTimeColumn get profileChangedAt => - dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + DateTimeColumn get profileChangedAt => customType(clampedDateTime).withDefault(currentDateAndTime)(); IntColumn get avatarColor => intEnum().withDefault(const Constant(0))(); @override diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 687d7b42cb..a333d1af21 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -348,7 +348,7 @@ class Drift extends $Drift { } // hardcoded on purpose: datetimes are stored as text, so versioned schema tables type -// these columns String and the set cannot be derived — migration_test pins it to drift_schema_v32.json +// these columns String and the set cannot be derived — migration_test pins it to drift_schema_v31.json @visibleForTesting const healDateTimeColumns = >{ 'auth_user_entity': ['profile_changed_at'], diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart index 093e327a3d..ee4b11bf4f 100644 --- a/mobile/lib/infrastructure/utils/asset.mixin.dart +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -1,12 +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.converter.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()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); - DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime).map(const DateTimeClampConverter())(); + 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()(); diff --git a/mobile/lib/infrastructure/utils/datetime.converter.dart b/mobile/lib/infrastructure/utils/datetime.converter.dart index 89d41e3b11..580a52ee63 100644 --- a/mobile/lib/infrastructure/utils/datetime.converter.dart +++ b/mobile/lib/infrastructure/utils/datetime.converter.dart @@ -4,29 +4,39 @@ import 'package:drift/drift.dart'; // (1..9999), which turns bucket queries into NULLs and crashes the timeline // (#28524). Clamps every stored datetime into the representable range; anything // in range passes through untouched. -final class DateTimeClampConverter extends TypeConverter { - const DateTimeClampConverter(); +const clampedDateTime = DateTimeClampType(); - // 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 - static final DateTime _floor = DateTime.utc(1); - static final DateTime _ceiling = DateTime.utc(9999, 12, 31); +final class DateTimeClampType implements DialectAwareSqlType { + const DateTimeClampType(); @override - DateTime fromSql(DateTime fromDb) => _clamp(fromDb); + DateTime read(SqlTypes types, Object fromSql) => _clampDateTime(types.read(DriftSqlType.dateTime, fromSql)!); @override - DateTime toSql(DateTime value) => _clamp(value); + Object mapToSqlParameter(GenerationContext context, DateTime value) => + context.typeMapping.mapToSqlVariable(_clampDateTime(value))!; - static DateTime _clamp(DateTime value) { - if (value.isBefore(_floor)) { - return _floor; - } - if (value.isAfter(_ceiling)) { - return _ceiling; - } - return 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; } diff --git a/mobile/lib/infrastructure/utils/drift_default.mixin.dart b/mobile/lib/infrastructure/utils/drift_default.mixin.dart index 1ba4589ed5..9ecf99d03e 100644 --- a/mobile/lib/infrastructure/utils/drift_default.mixin.dart +++ b/mobile/lib/infrastructure/utils/drift_default.mixin.dart @@ -1,6 +1,10 @@ import 'package:drift/drift.dart'; mixin DriftDefaultsMixin on Table { + @Deprecated('Use customType(clampedDateTime)') + @override + ColumnBuilder dateTime() => super.dateTime(); + @override bool get isStrict => true; diff --git a/mobile/test/infrastructure/utils/datetime.converter_test.dart b/mobile/test/infrastructure/utils/datetime.converter_test.dart index 71dd715ef8..3f47cf8d9a 100644 --- a/mobile/test/infrastructure/utils/datetime.converter_test.dart +++ b/mobile/test/infrastructure/utils/datetime.converter_test.dart @@ -4,24 +4,29 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/utils/datetime.converter.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; void main() { - const converter = DateTimeClampConverter(); + 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))!; + DateTime fromSql(DateTime value) => clampedDateTime.read(types, types.mapToSqlVariable(value)!); test('clamps far-future years to the ceiling in both directions', () { // the reporter's date from #28524 final poison = DateTime.utc(144769, 11, 18, 12, 38, 32); final ceiling = DateTime.utc(9999, 12, 31); - expect(converter.toSql(poison), ceiling); - expect(converter.fromSql(poison), ceiling); - expect(converter.toSql(DateTime(144769, 1, 1)), ceiling); // local poison normalizes to utc + expect(toSql(poison), ceiling); + expect(fromSql(poison), ceiling); + expect(toSql(DateTime(144769, 1, 1)), ceiling); // local poison normalizes to utc }); test('clamps BCE and year zero to the floor in both directions', () { final floor = DateTime.utc(1, 1, 1); - expect(converter.toSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); - expect(converter.fromSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); - expect(converter.toSql(DateTime.utc(0, 12, 31)), floor); + expect(toSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); + expect(fromSql(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', () { @@ -29,63 +34,43 @@ void main() { 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(converter.toSql(low), low); - expect(converter.fromSql(low), low); - expect(converter.toSql(justBefore), justBefore); - expect(converter.toSql(normal), normal); - expect(converter.fromSql(normal), normal); + expect(toSql(low), low); + expect(fromSql(low), low); + expect(toSql(justBefore), justBefore); + expect(toSql(normal), normal); + expect(fromSql(normal), normal); // local datetimes keep their zone flag final local = DateTime(2024, 6, 15, 10, 30, 25); - expect(converter.toSql(local), local); - expect(converter.toSql(local).isUtc, isFalse); + 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(converter.toSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); - expect(converter.fromSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); - expect(converter.toSql(DateTime(9999, 12, 31, 23, 59, 59)), ceiling); // local normalizes to utc + expect(toSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); + expect(fromSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); + expect(toSql(DateTime(9999, 12, 31, 23, 59, 59)), ceiling); // local normalizes to utc }); - group('every datetime column carries the clamp converter', () { - int expectClampConverter(GeneratedDatabase db) { - final poison = DateTime.utc(144769, 11, 18, 12, 38, 32); - var count = 0; + group('every table uses the datetime guard', () { + void expectGuard(GeneratedDatabase db) { for (final table in db.allTables) { - for (final column in table.$columns) { - if (column.type != DriftSqlType.dateTime) { - continue; - } - count++; - // nullable columns wrap the same converter in drift's NullAwareTypeConverter - expect( - column, - isA>(), - reason: '${table.entityName}.${column.$name} is missing the clamp converter', - ); - final converter = (column as GeneratedColumnWithTypeConverter).converter; - expect( - converter.toSql(poison), - DateTime.utc(9999, 12, 31), - reason: '${table.entityName}.${column.$name} is missing the clamp converter', - ); - } + expect(table, isA(), reason: '${table.entityName} bypasses the datetime guard'); } - return count; } - test('main database: all 32 columns across all tables', () { + test('main database tables', () { final db = Drift(NativeDatabase.memory()); addTearDown(db.close); - expect(expectClampConverter(db), 32); + expectGuard(db); }); - test('logs database: the log timestamp column', () { + test('logs database table', () { final logDb = DriftLogger.fromExecutor(NativeDatabase.memory()); addTearDown(logDb.close); - expect(expectClampConverter(logDb), 1); + expectGuard(logDb); }); }); } diff --git a/mobile/test/medium/datetime_clamp_converter_test.dart b/mobile/test/medium/datetime_clamp_converter_test.dart index 10b41db7da..abbe4a9cd6 100644 --- a/mobile/test/medium/datetime_clamp_converter_test.dart +++ b/mobile/test/medium/datetime_clamp_converter_test.dart @@ -200,7 +200,7 @@ void main() { for (final table in ctx.db.allTables) { final dateColumns = [ for (final column in table.$columns) - if (column.type == DriftSqlType.dateTime) column.$name, + if (column is GeneratedColumn) column.$name, ]; if (dateColumns.isEmpty) { continue; diff --git a/mobile/test/medium/migration_test.dart b/mobile/test/medium/migration_test.dart index 838103ece2..aff2ce7cd5 100644 --- a/mobile/test/medium/migration_test.dart +++ b/mobile/test/medium/migration_test.dart @@ -112,8 +112,8 @@ void main() { expect(row.createdAt, local); }); - test('the heal column map matches the datetime columns of the v32 schema json', () async { - final schemaJson = jsonDecode(await File('drift_schemas/main/drift_schema_v32.json').readAsString()); + test('the heal column map matches the datetime columns of the v31 schema json', () async { + final schemaJson = jsonDecode(await File('drift_schemas/main/drift_schema_v31.json').readAsString()); final expected = { for (final entity in schemaJson['entities'] as List) if (entity['type'] == 'table') @@ -128,10 +128,10 @@ void main() { final missing = expected.difference(mapped); final extra = mapped.difference(expected); - expect(missing, isEmpty, reason: 'heal map misses v32 datetime columns: ${missing.join(', ')}'); - expect(extra, isEmpty, reason: 'heal map has columns that are not v32 datetimes: ${extra.join(', ')}'); + expect(missing, isEmpty, reason: 'heal map misses v31 datetime columns: ${missing.join(', ')}'); + expect(extra, isEmpty, reason: 'heal map has columns that are not v31 datetimes: ${extra.join(', ')}'); final mappedCount = healDateTimeColumns.values.fold(0, (sum, columns) => sum + columns.length); - expect(mappedCount, expected.length, reason: 'heal map count differs from the v32 schema json'); + expect(mappedCount, expected.length, reason: 'heal map count differs from the v31 schema json'); }); }); } From c51cb9ad639bf92c2fc16f7af29182ab5fe75997 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Wed, 5 Aug 2026 18:22:52 +0600 Subject: [PATCH 3/3] heal each table in one statement, rename the clamp type, read dates as stored --- .../entities/asset_face.entity.dart | 2 +- .../entities/auth_user.entity.dart | 2 +- .../infrastructure/entities/exif.entity.dart | 2 +- .../entities/local_album.entity.dart | 2 +- .../entities/local_asset.entity.dart | 2 +- .../infrastructure/entities/log.entity.dart | 2 +- .../entities/memory.entity.dart | 2 +- .../entities/person.entity.dart | 2 +- .../entities/remote_album.entity.dart | 2 +- .../entities/remote_asset.entity.dart | 2 +- .../remote_asset_cloud_id.entity.dart | 2 +- .../entities/settings.entity.dart | 2 +- .../infrastructure/entities/stack.entity.dart | 2 +- .../infrastructure/entities/user.entity.dart | 2 +- .../repositories/db.repository.dart | 23 ++++++----- .../lib/infrastructure/utils/asset.mixin.dart | 2 +- ...onverter.dart => datetime_clamp.type.dart} | 5 +-- ...est.dart => datetime_clamp.type_test.dart} | 38 ++++--------------- ...est.dart => datetime_clamp_type_test.dart} | 0 mobile/test/medium/migration_test.dart | 25 ------------ 20 files changed, 38 insertions(+), 83 deletions(-) rename mobile/lib/infrastructure/utils/{datetime.converter.dart => datetime_clamp.type.dart} (85%) rename mobile/test/infrastructure/utils/{datetime.converter_test.dart => datetime_clamp.type_test.dart} (57%) rename mobile/test/medium/{datetime_clamp_converter_test.dart => datetime_clamp_type_test.dart} (100%) diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index 49fcd9202a..96f6848b4d 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -1,7 +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.converter.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)') diff --git a/mobile/lib/infrastructure/entities/auth_user.entity.dart b/mobile/lib/infrastructure/entities/auth_user.entity.dart index 18d3eeb76e..580724c687 100644 --- a/mobile/lib/infrastructure/entities/auth_user.entity.dart +++ b/mobile/lib/infrastructure/entities/auth_user.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.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 { diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 43ac53d377..33c6abe943 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -2,7 +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.converter.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'; diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index c3ced83be1..a9819eb882 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -2,7 +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.converter.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 { diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index ea336fcb02..bc65c3c69d 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -2,7 +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.converter.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)') diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index c45ec842a0..8057085007 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -1,7 +1,7 @@ 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.converter.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 with DriftDefaultsMixin { diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart index bd7d44216b..8f9fce9c7d 100644 --- a/mobile/lib/infrastructure/entities/memory.entity.dart +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -1,7 +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.converter.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 { diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index 6d633093f7..28bdba44ab 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.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)') diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart index 2104967f2d..d969154988 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -1,7 +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.converter.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 { diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 68dfed11a5..6df42eeda1 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -3,7 +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.converter.dart'; +import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @TableIndex.sql(''' diff --git a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart index a548676df6..ae619d726e 100644 --- a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.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)') diff --git a/mobile/lib/infrastructure/entities/settings.entity.dart b/mobile/lib/infrastructure/entities/settings.entity.dart index 31bed6980d..725ea3d876 100644 --- a/mobile/lib/infrastructure/entities/settings.entity.dart +++ b/mobile/lib/infrastructure/entities/settings.entity.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.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 { diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index db3800be43..be4b90205e 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.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)') diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 8df3ae1930..71aaf78e4e 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -1,6 +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.converter.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 { diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index a333d1af21..c8a6f73fde 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -347,8 +347,8 @@ class Drift extends $Drift { ); } -// hardcoded on purpose: datetimes are stored as text, so versioned schema tables type -// these columns String and the set cannot be derived — migration_test pins it to drift_schema_v31.json +// 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 = >{ 'auth_user_entity': ['profile_changed_at'], @@ -368,16 +368,21 @@ const healDateTimeColumns = >{ }; // Rewrites datetime text sqlite date functions cannot handle: signed extended -// years and year 0000 (pre-converter syncs), plus anything later than the safe -// midnight ceiling, which re-overflows sqlite under 'localtime' east of UTC +// 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 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) { - for (final column in columns) { - await db.customStatement( - "UPDATE $table SET $column = CASE WHEN substr($column, 1, 1) = '-' OR substr($column, 1, 4) = '0000' THEN '0001-01-01T00:00:00.000Z' ELSE '9999-12-31T00:00:00.000Z' END WHERE substr($column, 1, 1) IN ('+', '-') OR substr($column, 1, 4) = '0000' OR $column > '9999-12-31T00:00:00.000Z'", - ); - } + 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 ')}'); } } diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart index ee4b11bf4f..4478eadaa2 100644 --- a/mobile/lib/infrastructure/utils/asset.mixin.dart +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.dart'; +import 'package:immich_mobile/infrastructure/utils/datetime_clamp.type.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; mixin AssetEntityMixin on DriftDefaultsMixin { diff --git a/mobile/lib/infrastructure/utils/datetime.converter.dart b/mobile/lib/infrastructure/utils/datetime_clamp.type.dart similarity index 85% rename from mobile/lib/infrastructure/utils/datetime.converter.dart rename to mobile/lib/infrastructure/utils/datetime_clamp.type.dart index 580a52ee63..faecfbf959 100644 --- a/mobile/lib/infrastructure/utils/datetime.converter.dart +++ b/mobile/lib/infrastructure/utils/datetime_clamp.type.dart @@ -2,15 +2,14 @@ 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 every stored datetime into the representable range; anything -// in range passes through untouched. +// (#28524). Clamps on write; the v32 heal migration rewrites the backlog. const clampedDateTime = DateTimeClampType(); final class DateTimeClampType implements DialectAwareSqlType { const DateTimeClampType(); @override - DateTime read(SqlTypes types, Object fromSql) => _clampDateTime(types.read(DriftSqlType.dateTime, fromSql)!); + DateTime read(SqlTypes types, Object fromSql) => types.read(DriftSqlType.dateTime, fromSql)!; @override Object mapToSqlParameter(GenerationContext context, DateTime value) => diff --git a/mobile/test/infrastructure/utils/datetime.converter_test.dart b/mobile/test/infrastructure/utils/datetime_clamp.type_test.dart similarity index 57% rename from mobile/test/infrastructure/utils/datetime.converter_test.dart rename to mobile/test/infrastructure/utils/datetime_clamp.type_test.dart index 3f47cf8d9a..e3fb9c8c74 100644 --- a/mobile/test/infrastructure/utils/datetime.converter_test.dart +++ b/mobile/test/infrastructure/utils/datetime_clamp.type_test.dart @@ -1,31 +1,24 @@ import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/datetime.converter.dart'; -import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.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))!; - DateTime fromSql(DateTime value) => clampedDateTime.read(types, types.mapToSqlVariable(value)!); - test('clamps far-future years to the ceiling in both directions', () { + 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(fromSql(poison), ceiling); expect(toSql(DateTime(144769, 1, 1)), ceiling); // local poison normalizes to utc }); - test('clamps BCE and year zero to the floor in both directions', () { + 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(fromSql(DateTime.utc(-4712, 3, 4, 5, 6, 7)), floor); expect(toSql(DateTime.utc(0, 12, 31)), floor); }); @@ -35,10 +28,8 @@ void main() { 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(fromSql(low), low); expect(toSql(justBefore), justBefore); expect(toSql(normal), normal); - expect(fromSql(normal), normal); // local datetimes keep their zone flag final local = DateTime(2024, 6, 15, 10, 30, 25); expect(toSql(local), local); @@ -50,27 +41,12 @@ void main() { // 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(fromSql(DateTime.utc(9999, 12, 31, 23, 59, 59)), ceiling); expect(toSql(DateTime(9999, 12, 31, 23, 59, 59)), ceiling); // local normalizes to utc }); - group('every table uses the datetime guard', () { - void expectGuard(GeneratedDatabase db) { - for (final table in db.allTables) { - expect(table, isA(), reason: '${table.entityName} bypasses the datetime guard'); - } - } - - test('main database tables', () { - final db = Drift(NativeDatabase.memory()); - addTearDown(db.close); - expectGuard(db); - }); - - test('logs database table', () { - final logDb = DriftLogger.fromExecutor(NativeDatabase.memory()); - addTearDown(logDb.close); - expectGuard(logDb); - }); + 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)); }); } diff --git a/mobile/test/medium/datetime_clamp_converter_test.dart b/mobile/test/medium/datetime_clamp_type_test.dart similarity index 100% rename from mobile/test/medium/datetime_clamp_converter_test.dart rename to mobile/test/medium/datetime_clamp_type_test.dart diff --git a/mobile/test/medium/migration_test.dart b/mobile/test/medium/migration_test.dart index aff2ce7cd5..e9f86a904c 100644 --- a/mobile/test/medium/migration_test.dart +++ b/mobile/test/medium/migration_test.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:drift/drift.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -111,27 +108,5 @@ void main() { final row = await (ctx.db.select(ctx.db.localAssetEntity)..where((t) => t.id.equals(asset.id))).getSingle(); expect(row.createdAt, local); }); - - test('the heal column map matches the datetime columns of the v31 schema json', () async { - final schemaJson = jsonDecode(await File('drift_schemas/main/drift_schema_v31.json').readAsString()); - final expected = { - for (final entity in schemaJson['entities'] as List) - if (entity['type'] == 'table') - for (final column in entity['data']['columns'] as List) - if (column['moor_type'] == 'dateTime') '${entity['data']['name']}.${column['name']}', - }; - - final mapped = { - for (final MapEntry(key: table, value: columns) in healDateTimeColumns.entries) - for (final column in columns) '$table.$column', - }; - - final missing = expected.difference(mapped); - final extra = mapped.difference(expected); - expect(missing, isEmpty, reason: 'heal map misses v31 datetime columns: ${missing.join(', ')}'); - expect(extra, isEmpty, reason: 'heal map has columns that are not v31 datetimes: ${extra.join(', ')}'); - final mappedCount = healDateTimeColumns.values.fold(0, (sum, columns) => sum + columns.length); - expect(mappedCount, expected.length, reason: 'heal map count differs from the v31 schema json'); - }); }); }