Skip to content

Derive cloudPath from parentID chain instead of denormalized column - #459

Merged
tobihagemann merged 7 commits into
developfrom
feature/derive-cloudpath-from-parent-chain
Jul 31, 2026
Merged

Derive cloudPath from parentID chain instead of denormalized column#459
tobihagemann merged 7 commits into
developfrom
feature/derive-cloudpath-from-parent-chain

Conversation

@tobihagemann

@tobihagemann tobihagemann commented May 17, 2026

Copy link
Copy Markdown
Member

Closes #450. Alternative to #457.

The stale-descendant bug exists because cloudPath is denormalized per row: moveItemLocally updates 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 the parentID chain.

What changed

  • Schema (v5): rebuild itemMetadata without the cloudPath column; add parentID index.
  • Path resolution: ItemMetadataManager.getCloudPath(for id:) walks the parentID chain via recursive CTE. getCachedMetadata(for: CloudPath) does a per-component descent, preferring an exactly matching child and falling back to case-insensitive matching (Swift .lowercased(), full Unicode rather than SQLite's ASCII-only LOWER()). getAllCachedMetadata(inside:) uses a recursive descendant CTE. Siblings are ordered by id, so the fallback resolves to a stable row.
  • Snapshot at task construction: each CloudTask now carries a cloudPath resolved when the task is created. Executors read task.cloudPath instead of itemMetadata.cloudPath. Survives concurrent local renames of the same row.
  • Batch reconciliation: cacheMetadata([ItemMetadata]) groups by parent and reconciles each folder in two passes against a single sibling fetch. Every exact name match claims its row first; only leftovers may claim a row case-insensitively, and only one still flagged isMaybeOutdated and unclaimed. Placeholders never claim by case, so a newly created item cannot adopt the identity of an existing row.
  • DB managers: task creators inject ItemMetadataManager to resolve the path at construction. Four task-DB-manager functions touched by this refactor now guard item.id with throw DBManagerError.nonSavedItemMetadata (matching the existing ReparentTaskDBManager.createTaskRecord precedent). Three of those functions had pre-existing item.id! that the guard now covers too; mild scope creep in the name of consistency within the changed surface. UploadTaskDBManager.createNewTaskRecord and ItemMetadataDBManager.getAllCachedMetadata(inside:) drop their id! force-unwraps for the same guard.
  • Regression tests: borrowed both folder-move tests from Rewrite descendant cloudPath on folder move and repair stale rows #457 (@phil1995's hypothesis test from Investigation: Hardcoded CloudPath #450 and a 3-level subtree variant), adapted to assert via getCloudPath(for:) / getCachedMetadata(for:) since the derived path lives off the row now. Direct DB-layer tests cover getCloudPath resolution plus its itemNotFound and unresolvableParentChain paths, the unsaved-folder guard, deterministic case-only-sibling resolution (forced via PRAGMA reverse_unordered_selects), and the reconciliation rules above.

Name matching

Sibling identity is case-sensitive, because Cryptor.encryptFileName(_:dirId:encoding:) derives a different ciphertext name per case: Foo.txt and foo.txt are two distinct cloud items and get two rows. It precomposes to NFC before encrypting, so canonically equivalent NFC/NFD spellings share one ciphertext name and resolve to a single row. Path lookup stays case-insensitive, since Shortcuts and the local-collision pre-flight both pass user-supplied casing, but an exact match wins over a case-folded one.

One known limit: reconciliation is per batch, and the File Provider extension pages every listing at 500 items, so any folder above that is reconciled in several batches. If a case-variant pair straddles a page boundary, the two rows can swap identities; both files stay visible either way. Which page an entry lands on is effectively unpredictable from its cleartext name, since paging and ordering happen on the ciphertext name.

Trade-offs vs #457

  • Larger diff: 63 files, +1141 / -516. Most of the test churn is dropping cloudPath: from ItemMetadata(...) calls.
  • No repair migration needed: stale-descendant rows simply stop being read; the v5 rebuild drops the column they lived in.
  • The getItemIdentifier enumeration fallback structure stays the same, just changes the final comparison from cloudPath == to a re-lookup against the populated cache.

Out of scope (worth a separate look)

  • A 4-year-old typo in DownloadTaskDBManager.init (deleted from ItemEnumerationTaskRecord instead of DownloadTaskRecord) 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.
  • Cycle rejection at the move-folder API level (move-into-own-descendant) isn't included; can land as its own PR.
  • Project-wide ItemMetadata.id is still optional; only the four new DB-manager sites adopt the guard let pattern. Project-wide cleanup is a separate refactor.
  • childrenOfFolder materializes a folder's rows to match names in Swift, which stock SQLite can't do for canonical equivalence. Fine at realistic folder sizes; persisted NFC name keys plus a (parentID, name) index would restore indexed lookups.

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR removes cloudPath from persisted ItemMetadata and reconstructs paths from parent relationships. Cloud tasks now capture explicit paths. Database managers, adapters, middleware, and executors pass or use those paths. A migration removes the redundant column. Tests cover path reconstruction, invalid parent chains, sibling lookup, moved descendants, and task behavior.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes an unrelated DownloadTaskDBManager purge typo fix and regression test that are outside issue #450. Remove the unrelated DownloadTaskDBManager typo fix and its regression test, or move them to a separate pull request.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses issue #450 by preventing stale descendant paths through parentID-based derivation and adds regression tests for moved folder descendants.
Title check ✅ Passed The title clearly summarizes the primary change: deriving cloud paths from the parentID chain instead of storing a denormalized column.
Description check ✅ Passed The description directly explains the schema, path-resolution, task, reconciliation, and testing changes in the pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/derive-cloudpath-from-parent-chain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift (1)

48-68: 💤 Low value

Consider whether collision handling is applicable to all task types.

The collision handler creates updated tasks for all CloudTask types, including DownloadTask and ItemEnumerationTask. However, CloudProviderError.itemAlreadyExists typically 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 win

Add an explicit unsaved-metadata guard test for createNewTaskRecord(for:).

Given the new contract, this suite should lock in that passing metadata without a persisted id throws DBManagerError.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

📥 Commits

Reviewing files that changed from the base of the PR and between ed59b6e and 2660fb9.

📒 Files selected for processing (61)
  • CryptomatorFileProvider/CloudTask/CloudTask.swift
  • CryptomatorFileProvider/CloudTask/DeletionTask.swift
  • CryptomatorFileProvider/CloudTask/DownloadTask.swift
  • CryptomatorFileProvider/CloudTask/FolderCreationTask.swift
  • CryptomatorFileProvider/CloudTask/ItemEnumerationTask.swift
  • CryptomatorFileProvider/CloudTask/ReparentTask.swift
  • CryptomatorFileProvider/CloudTask/UploadTask.swift
  • CryptomatorFileProvider/DB/DatabaseHelper.swift
  • CryptomatorFileProvider/DB/DeletionTaskDBManager.swift
  • CryptomatorFileProvider/DB/DownloadTaskDBManager.swift
  • CryptomatorFileProvider/DB/ItemEnumerationTaskDBManager.swift
  • CryptomatorFileProvider/DB/ItemMetadata.swift
  • CryptomatorFileProvider/DB/ItemMetadataDBManager.swift
  • CryptomatorFileProvider/DB/ReparentTaskDBManager.swift
  • CryptomatorFileProvider/DB/UploadTaskDBManager.swift
  • CryptomatorFileProvider/FileProviderAdapter.swift
  • CryptomatorFileProvider/FileProviderAdapterError.swift
  • CryptomatorFileProvider/FileProviderAdapterManager.swift
  • CryptomatorFileProvider/FileProviderItem.swift
  • CryptomatorFileProvider/Middleware/OnlineItemNameCollisionHandler.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/DeletionTaskExecutor.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift
  • CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift
  • CryptomatorFileProviderTests/DB/CachedFileManagerTests.swift
  • CryptomatorFileProviderTests/DB/DeletionTaskManagerTests.swift
  • CryptomatorFileProviderTests/DB/DownloadTaskManagerTests.swift
  • CryptomatorFileProviderTests/DB/ItemEnumerationTaskManagerTests.swift
  • CryptomatorFileProviderTests/DB/MaintenanceManagerTests.swift
  • CryptomatorFileProviderTests/DB/MetadataManagerTests.swift
  • CryptomatorFileProviderTests/DB/ReparentTaskManagerTests.swift
  • CryptomatorFileProviderTests/DB/UploadTaskManagerTests.swift
  • CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterCreateDirectoryTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterDeleteItemTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemIdentifierTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterGetItemTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterMoveItemTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterRecoverUploadsTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetFavoriteRankTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterSetTagDataTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterStartProvidingItemTests.swift
  • CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift
  • CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift
  • CryptomatorFileProviderTests/FileProviderItemTests.swift
  • CryptomatorFileProviderTests/FileProviderNotificatorTests.swift
  • CryptomatorFileProviderTests/Middleware/ErrorMapperTests.swift
  • CryptomatorFileProviderTests/Middleware/OnlineItemNameCollisionHandlerTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/DeletionTaskExecutorTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/DownloadTaskExecutorTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/FolderCreationTaskExecutorTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/ReparentTaskExecutorTests.swift
  • CryptomatorFileProviderTests/Middleware/TaskExecutor/UploadTaskExecutorTests.swift
  • CryptomatorFileProviderTests/PermissionProviderImplTests.swift
  • CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift
  • CryptomatorFileProviderTests/WorkingSetObserverTests.swift

Comment thread CryptomatorFileProvider/DB/DatabaseHelper.swift
Comment thread CryptomatorFileProvider/DB/ItemMetadataDBManager.swift Outdated
Comment thread CryptomatorFileProvider/DB/ItemMetadataDBManager.swift Outdated
@tobihagemann tobihagemann added this to the 3.1.2 milestone May 28, 2026

@phil1995 phil1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to provide ItemMetadataManager via swift-dependencies?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this and I'm not sure it's the right fit. Unlike our stateless services like permissionProvider, ItemMetadataManager is built per-vault from that vault's database in createAdapterCacheItem and the same instance is shared across the task managers, the adapter, and the working set observer, so there's no real global liveValue to register. Modeling that with swift-dependencies would mean an unimplemented live value plus a withDependencies scope around the whole per-vault graph construction (something we don't do anywhere yet), and switching only UploadTaskDBManager while DownloadTaskDBManager and ItemEnumerationTaskDBManager keep constructor injection would make it less consistent rather than more. Did you have a specific scoping approach in mind, or were you thinking of migrating all the DB managers?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
CryptomatorFileProvider/DB/ItemMetadataDBManager.swift (1)

300-319: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider index maps to avoid the O(n*m) sibling scans.

Both passes scan cachedChildren linearly for every reported item, and firstClaimableByCase scans it again. For a folder with many children the cost grows as reported items times cached rows, with canonical-equivalence string comparison on each step.

You can keep the exact semantics by keying dictionaries on precomposedStringWithCanonicalMapping, because Swift String equality already treats canonically equivalent spellings as equal. The case-claim map can key on the lowercased canonical form and hold the lowest-id eligible row, which preserves the current "first by id" choice from childrenOfFolder. Both maps must be updated inside overwrite and after each insert so the in-pass merge behavior stays intact.

This is optional. It does not change behavior, so defer it if folder sizes stay small in practice.

🤖 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/DB/ItemMetadataDBManager.swift` around lines 300 -
319, Optionally optimize the sibling matching in the metadata update flow by
replacing repeated cachedChildren scans with canonical-name and
lowercased-canonical-name maps. Preserve exact matching semantics and
first-by-id case claiming, and update both maps whenever overwrite is called or
a new metadata row is inserted so later items in the same pass still merge
correctly.
🤖 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.

Nitpick comments:
In `@CryptomatorFileProvider/DB/ItemMetadataDBManager.swift`:
- Around line 300-319: Optionally optimize the sibling matching in the metadata
update flow by replacing repeated cachedChildren scans with canonical-name and
lowercased-canonical-name maps. Preserve exact matching semantics and
first-by-id case claiming, and update both maps whenever overwrite is called or
a new metadata row is inserted so later items in the same pass still merge
correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fff49756-2b7f-4775-bec7-14b57514c078

📥 Commits

Reviewing files that changed from the base of the PR and between a258d2c and 3cd9789.

📒 Files selected for processing (2)
  • CryptomatorFileProvider/DB/ItemMetadataDBManager.swift
  • CryptomatorFileProviderTests/DB/MetadataManagerTests.swift

@tobihagemann
tobihagemann merged commit 5ed1ab7 into develop Jul 31, 2026
6 checks passed
@tobihagemann
tobihagemann deleted the feature/derive-cloudpath-from-parent-chain branch July 31, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants