Derive cloudPath from parentID chain instead of denormalized column#459
Derive cloudPath from parentID chain instead of denormalized column#459tobihagemann wants to merge 3 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
WalkthroughThis PR migrates cloud path storage from the Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift (1)
48-68: 💤 Low valueConsider whether collision handling is applicable to all task types.
The collision handler creates updated tasks for all CloudTask types, including
DownloadTaskandItemEnumerationTask. However,CloudProviderError.itemAlreadyExiststypically occurs during write operations (upload, folder creation, reparent), not read operations. If a download or enumeration task reaches this handler, it may indicate an unexpected code path.The current implementation is defensive and won't break, but you may want to add logging or an assertion for these unexpected cases.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift` around lines 48 - 68, cloudPathCollisionUpdate currently treats all CloudTask subclasses the same, but collision errors are expected only for write operations; update cloudPathCollisionUpdate(for:) to log or assert when it receives unexpected read-only types (DownloadTask, ItemEnumerationTask) instead of silently treating them as normal: inside cloudPathCollisionUpdate(for:) after computing collisionFreeCloudPath and before returning the updated task, add a warning log via your logger or a precondition/assert for the cases matching DownloadTask and ItemEnumerationTask (referencing the DownloadTask and ItemEnumerationTask type checks in the switch) so unexpected paths are visible during debugging while still returning or throwing as appropriate.CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift (1)
23-31: ⚡ Quick winAdd an explicit unsaved-metadata guard test for
createNewTaskRecord(for:).Given the new contract, this suite should lock in that passing metadata without a persisted
idthrowsDBManagerError.nonSavedItemMetadata(instead of silently creating invalid task rows).Proposed test addition
+ func testCreateNewTaskRecordForUnsavedMetadataThrowsNonSavedItemMetadata() throws { + let unsavedMetadata = ItemMetadata( + name: "Unsaved", + type: .file, + size: nil, + parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, + lastModifiedDate: nil, + statusCode: .isUploaded, + isPlaceholderItem: false + ) + XCTAssertThrowsError(try manager.createNewTaskRecord(for: unsavedMetadata)) { error in + guard case DBManagerError.nonSavedItemMetadata = error else { + XCTFail("Throws the wrong error: \(error)") + return + } + } + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift` around lines 23 - 31, Add a unit test that verifies UploadTaskDBManager.createNewTaskRecord(for:) throws DBManagerError.nonSavedItemMetadata when given ItemMetadata that has not been persisted; specifically, instantiate an ItemMetadata (do not call ItemMetadataDBManager.cacheMetadata or otherwise persist it), call manager.createNewTaskRecord(for: unsavedItem) and assert it throws DBManagerError.nonSavedItemMetadata, referencing UploadTaskDBManager.createNewTaskRecord(for:) and DBManagerError.nonSavedItemMetadata so the new contract is locked in.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CryptomatorFileProvider/DB/DatabaseHelper.swift`:
- Around line 212-237: Before rebuilding itemMetadata in
migrator.registerMigration("v5"), detect and handle case-only sibling collisions
by querying itemMetadata grouped by parentID and lower(name) (e.g., SELECT
parentID, lower(name) AS lname, GROUP_CONCAT(id) ids, COUNT(*) cnt FROM
itemMetadata GROUP BY parentID, lname HAVING cnt > 1); if any rows are returned
either abort the migration with a clear MigrationError including the offending
ids (so upgrade can be fixed manually) or deterministically resolve them (choose
one row to keep, e.g., MIN(id) or newest lastEnumeratedAt, and delete the other
ids) before proceeding to CREATE/INSERT/DROP/RENAME; implement this check/repair
inside the same migrator.registerMigration("v5") block referencing itemMetadata
and cloudPath so childOfFolder/cacheMetadata will not later pick the wrong row.
In `@CryptomatorFileProvider/DB/ItemMetadataDBManager.swift`:
- Around line 143-160: The method getAllCachedMetadata currently force-unwraps
parent.id when building the recursive SQL arguments; replace that with a guarded
check: validate parent.id with guard let id = parent.id else { throw ... } and
throw the same non-saved-item error pattern used by the task DB managers (i.e.
the existing non-saved-item error used elsewhere) instead of crashing, then use
the guarded id variable in the SQL arguments and keep the rest of the logic
unchanged.
- Around line 225-240: The ancestor CTE currently hardcodes the root row id as
`1` which breaks if the root database value differs; in
`resolveCloudPath(for:database:)` update the SQL to use the `rootID` variable
instead of `1` (e.g. replace `WHERE a.id != 1` with a parameterized check
against `rootID`) and add `rootID` to the `arguments` array passed to
`Row.fetchAll(...)` so the query uses the derived
`NSFileProviderItemIdentifier.rootContainerDatabaseValue` consistently.
In
`@CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift`:
- Around line 57-58: The test currently hides failures by defaulting to
CloudPath("/") when metadataManagerMock.getCloudPath(for: id) fails; change the
fixture to fail fast instead: make the constructor for the UploadTask test
fixture require a valid cloud path and propagate/throw the error from
metadataManagerMock.getCloudPath(for: id) (or assert/force-unwrap in the test)
rather than creating CloudPath("/"), so any broken metadata chain is surfaced;
update the call site that constructs UploadTask(taskRecord:..., itemMetadata:
metadata, cloudPath: ..., onURLSessionTaskCreation: ...) to use the real
CloudPath from metadataManagerMock.getCloudPath(for: id) and fail the test if
that lookup does not succeed.
---
Nitpick comments:
In `@CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift`:
- Around line 48-68: cloudPathCollisionUpdate currently treats all CloudTask
subclasses the same, but collision errors are expected only for write
operations; update cloudPathCollisionUpdate(for:) to log or assert when it
receives unexpected read-only types (DownloadTask, ItemEnumerationTask) instead
of silently treating them as normal: inside cloudPathCollisionUpdate(for:) after
computing collisionFreeCloudPath and before returning the updated task, add a
warning log via your logger or a precondition/assert for the cases matching
DownloadTask and ItemEnumerationTask (referencing the DownloadTask and
ItemEnumerationTask type checks in the switch) so unexpected paths are visible
during debugging while still returning or throwing as appropriate.
In `@CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift`:
- Around line 23-31: Add a unit test that verifies
UploadTaskDBManager.createNewTaskRecord(for:) throws
DBManagerError.nonSavedItemMetadata when given ItemMetadata that has not been
persisted; specifically, instantiate an ItemMetadata (do not call
ItemMetadataDBManager.cacheMetadata or otherwise persist it), call
manager.createNewTaskRecord(for: unsavedItem) and assert it throws
DBManagerError.nonSavedItemMetadata, referencing
UploadTaskDBManager.createNewTaskRecord(for:) and
DBManagerError.nonSavedItemMetadata so the new contract is locked in.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de082e5f-9fd5-48ee-8797-656631676a65
📒 Files selected for processing (61)
CryptomatorFileProvider/CloudTask/CloudTask.swiftCryptomatorFileProvider/CloudTask/DeletionTask.swiftCryptomatorFileProvider/CloudTask/DownloadTask.swiftCryptomatorFileProvider/CloudTask/FolderCreationTask.swiftCryptomatorFileProvider/CloudTask/ItemEnumerationTask.swiftCryptomatorFileProvider/CloudTask/ReparentTask.swiftCryptomatorFileProvider/CloudTask/UploadTask.swiftCryptomatorFileProvider/DB/DatabaseHelper.swiftCryptomatorFileProvider/DB/DeletionTaskDBManager.swiftCryptomatorFileProvider/DB/DownloadTaskDBManager.swiftCryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swiftCryptomatorFileProvider/DB/ItemMetadata.swiftCryptomatorFileProvider/DB/ItemMetadataDBManager.swiftCryptomatorFileProvider/DB/ReparentTaskDBManager.swiftCryptomatorFileProvider/DB/UploadTaskDBManager.swiftCryptomatorFileProvider/FileProviderAdapter.swiftCryptomatorFileProvider/FileProviderAdapterError.swiftCryptomatorFileProvider/FileProviderAdapterManager.swiftCryptomatorFileProvider/FileProviderItem.swiftCryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swiftCryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swiftCryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swiftCryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swiftCryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swiftCryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swiftCryptomatorFileProviderTests/DB/CachedFileManagerTests.swiftCryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swiftCryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swiftCryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swiftCryptomatorFileProviderTests/DB/MaintenanceManagerTests.swiftCryptomatorFileProviderTests/DB/MetadataManagerTests.swiftCryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swiftCryptomatorFileProviderTests/DB/UploadTaskManagerTests.swiftCryptomatorFileProviderTests/FileImportingServiceSourceTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swiftCryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swiftCryptomatorFileProviderTests/FileProviderEnumeratorTests.swiftCryptomatorFileProviderTests/FileProviderItemTests.swiftCryptomatorFileProviderTests/FileProviderNotificatorTests.swiftCryptomatorFileProviderTests/Middleware/ErrorMapperTests.swiftCryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swiftCryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swiftCryptomatorFileProviderTests/PermissionProviderImplTests.swiftCryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swiftCryptomatorFileProviderTests/WorkingSetObserverTests.swift
phil1995
left a comment
There was a problem hiding this comment.
Had a quick look at the code. I personally like the idea more of deriving the cloudPath from the relationship graph.
I wonder if we can still run into wrong / stale paths if we have a move task queued before another task for the same item as we store the snapshot at task construction. Need to disclaim that I have not tested this in the Files app yet if it's even possible to do this or the item is in a state where it's not possible to interact with it anyway (I guess if we rely on that assumption than it needs to be tested with every supported major iOS version).
| private let itemMetadataManager: ItemMetadataManager | ||
|
|
||
| init(database: DatabaseWriter) { | ||
| init(database: DatabaseWriter, itemMetadataManager: ItemMetadataManager) { |
There was a problem hiding this comment.
Maybe it makes sense to provide ItemMetadataManager via swift-dependencies?
Closes #450. Alternative to #457.
The stale-descendant bug exists because
cloudPathis denormalized per row:moveItemLocallyupdates the moved folder but leaves descendants pointing at the old path. #457 fixes this by rewriting descendants on move and adding a repair migration. This PR removes the source of the inconsistency instead: drop the column, derive on demand by walking theparentIDchain.What changed
itemMetadatawithout thecloudPathcolumn; addparentIDindex.ItemMetadataManager.getCloudPath(for id:)walks theparentIDchain via recursive CTE.getCachedMetadata(for: CloudPath)does a case-insensitive per-component descent (Swift.lowercased(), full Unicode rather than SQLite's ASCII-onlyLOWER()).getAllCachedMetadata(inside:)uses a recursive descendant CTE. Per-component descent orders siblings byid, so two siblings differing only by case resolve to a stable row.CloudTasknow carries acloudPathresolved when the task is created. Executors readtask.cloudPathinstead ofitemMetadata.cloudPath. Survives concurrent local renames of the same row.ItemMetadataManagerto resolve the path at construction. Four task-DB-manager functions touched by this refactor now guarditem.idwiththrow DBManagerError.nonSavedItemMetadata(matching the existingReparentTaskDBManager.createTaskRecordprecedent). Three of those functions had pre-existingitem.id!that the guard now covers too; mild scope creep in the name of consistency within the changed surface.UploadTaskDBManager.createNewTaskRecordandItemMetadataDBManager.getAllCachedMetadata(inside:)drop theirid!force-unwraps for the same guard.getCloudPath(for:)/getCachedMetadata(for:)since the derived path lives off the row now. Direct DB-layer tests covergetCloudPathresolution plus itsitemNotFoundandunresolvableParentChainpaths, the unsaved-folder guard, and deterministic case-only-sibling resolution (forced viaPRAGMA reverse_unordered_selects).Behavior change worth flagging
cacheMetadata's upsert key is now case-insensitive too, closing the long-standing asymmetry #457's description called out (lookup was case-insensitive, upsert was byte-exact). Practical effect: enumeratingFoo.txtthenfoo.txtfrom the cloud now updates one row in-place rather than creating two with conflicting cases.Trade-offs vs #457
cloudPath:fromItemMetadata(...)calls.getItemIdentifierenumeration fallback structure stays the same, just changes the final comparison fromcloudPath ==to a re-lookup against the populated cache.Out of scope (worth a separate look)
DownloadTaskDBManager.init(deleted fromItemEnumerationTaskRecordinstead ofDownloadTaskRecord) is fixed inline, with a regression test. Wasn't related to Investigation: Hardcoded CloudPath #450 but the one-line fix was hard to leave in place once seen.ItemMetadata.idis still optional; only the four new DB-manager sites adopt theguard letpattern. Project-wide cleanup is a separate refactor.