Skip to content

Add custom/shared zone support to the query path (#146) - #426

Open
leogdion wants to merge 4 commits into
v1.0.0-beta.4from
146-custom-zone-queries
Open

Add custom/shared zone support to the query path (#146)#426
leogdion wants to merge 4 commits into
v1.0.0-beta.4from
146-custom-zone-queries

Conversation

@leogdion

Copy link
Copy Markdown
Member

Summary

queryRecords hard-coded zoneID: .init(zoneName: "_defaultZone") into the records/query request body (CloudKitService+Operations.swift:85), making custom and shared zones unqueryable. This threads an optional zone through the whole query path.

Design decisions

ZoneID rather than zoneName: String

The issue sketched zoneName: String = "_defaultZone". I used the existing ZoneID type instead:

  • ZoneID carries ownerName, which a bare string cannot. Shared zones live in the owner's database and are unaddressable without it — so a zoneName-only parameter would have left the issue's own "working with shared zones" use case unsolved.
  • It matches the established precedent in this codebase: modifyRecords(_:atomic:zoneID:desiredKeys:numbersAsStrings:database:) already takes zoneID: ZoneID? = nil, with the same Components.Schemas.ZoneID(from:) conversion. Query and modify now name their zone the same way.
  • ZoneID.defaultZone remains available for callers who want to say "default zone" explicitly.

Defaulted (= nil) rather than required

Per .claude/memory/feedback_no_silent_policy_defaults.md this was a deliberate call, not an oversight:

  • That memory targets parameters carrying credential / attribution policy (PublicAuthPreference), where a silent default caused records to be mis-attributed. Nothing comparable rides on zone selection — a wrong zone yields wrong-or-empty results, visibly, not a silent security/ownership mistake.
  • nil does not mean "quietly substitute _defaultZone". It means omit the zoneID key from the request entirely and let CloudKit resolve the default zone server-side. There is no client-side policy being invented.
  • It matches the sibling request-option convention (zoneID / desiredKeys / numbersAsStrings / zoneWide on modifyRecords and fetchRecordChanges are all optional-defaulted).
  • The public database has only one zone, so requiring the parameter would tax every public-database caller for a choice they don't have.

database: still has no default, as CLAUDE.md requires — untouched.

Surfaces updated

Surface Change
queryRecords(_:limit:desiredKeys:continuationMarker:zoneID:zoneWide:numbersAsStrings:database:) The primitive; builds the request body
queryRecords(recordType:…) × 2 (deprecated) Forward zoneID to the primitive
queryAllRecords(…) Forwards zoneID on every page it fetches
fetchExistingRecordNames(…) So a pre-fetch targets the same zone as the modifyRecords(_:zoneID:) that follows it in the classify workflow

RecordManaging.queryRecords(recordType:) / queryAllRecords(recordType:) are intentionally left zone-unaware — that protocol is a deprecated, database-agnostic abstraction (it hard-codes .public(.prefers(.serverToServer)) too). Callers needing a zone should use CloudKitService directly. Happy to revisit if you'd rather it be zone-aware.

No OpenAPI regeneration needed

Components.Schemas.ZoneID already models both zoneName and ownerName, and the records/query request body already $refs it. openapi.yaml and Sources/MistKitOpenAPI/ are untouched.

Source compatibility

Source-compatible. Every new parameter is defaulted, and each was inserted before the non-defaulted database:, so existing call sites keep compiling unchanged — confirmed by the Examples build below, which needed no edits.

One behavioral change worth flagging: requests previously always carried "zoneID": {"zoneName": "_defaultZone"}; they now omit the key when no zone is given. CloudKit resolves the same default zone either way, so this is wire-level-different but semantically equivalent. The queryOmitsZoneIDByDefault test pins the new behavior.

Tests

Six new @Tests in Tests/MistKitTests/CloudKitService/Query/CloudKitServiceTests.Query+ZoneID.swift, added as a Zone Selection suite under the existing CloudKitServiceTests.Query enum parent (per the enum-vs-struct convention — that parent already spans multiple files). They assert against the serialized request body captured by MockTransport:

  • zoneID omitted when none is supplied (default-zone behavior unchanged)
  • custom zoneName forwarded, with no stray ownerName
  • shared-zone ownerName forwarded
  • ZoneID.defaultZone forwarded explicitly when asked
  • queryAllRecords forwards the zone on both pages of a paginated run
  • fetchExistingRecordNames forwards the zone

Verification

Check Result
swift build Pass
swift test Pass — 556 tests in 174 suites, 0 failures (6 new)
mise exec -- swift-format -i -r Sources/ Tests/ Pass — no changes to make
./Scripts/lint.sh Pass — SwiftLint 0 violations / 391 files, headers OK, periphery: no unused code
Examples/MistDemo swift build Pass — no call-site edits needed

Docs

  • AGENTS.md (CLAUDE.md symlink): new "Zone selection on queries (Feature: Add custom CloudKit zone support for queries #146)" section recording the ZoneID-vs-zoneName and default-vs-required calls.
  • WorkingWithRecords.md: new "Querying a custom or shared zone" subsection with custom-zone and shared-zone examples, plus a note on zoneID vs zoneWide.
  • DocC symbol links to queryRecords / queryAllRecords updated for the new signatures across five .docc articles.

Closes #146

🤖 Generated with Claude Code

leogdion and others added 2 commits August 20, 2026 14:16
Adds two project memory files plus their MEMORY.md index entries:

- project_beta4_worktree_layout: the branch/worktree split for the
  remaining v1.0.0-beta.4 issues, the grouping rule (shared openapi.yaml
  path family => shared branch, to avoid Sources/MistKitOpenAPI/
  regeneration collisions), and why #407 was excluded.
- project_419_fixed_in_beta3: #419 is already fixed by 5a58120 and
  verified building on macOS Swift 6.3.2; do not re-implement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`queryRecords` hard-coded `zoneID: .init(zoneName: "_defaultZone")` into
the `records/query` request body, which made custom and shared zones
unqueryable.

Thread an optional `zoneID: ZoneID? = nil` through the query path:

- `queryRecords(_:limit:desiredKeys:continuationMarker:zoneID:…)` — the
  `Query` primitive that builds the request body
- both deprecated `queryRecords(recordType:…)` overloads
- `queryAllRecords(…)` — forwarded on every page it fetches
- `fetchExistingRecordNames(…)` — so a pre-fetch can target the same
  zone as the `modifyRecords(_:zoneID:)` that follows it

Use `ZoneID` rather than the bare `zoneName: String` the issue sketched,
so shared zones can carry `ownerName`. This also matches the existing
`modifyRecords(_:atomic:zoneID:…)` parameter.

`nil` omits the `zoneID` key entirely and lets CloudKit resolve the
database's default zone. That is not a silent policy default in the
sense of `.claude/memory/feedback_no_silent_policy_defaults.md` — no
credential or attribution semantics ride on it — and it keeps the change
source-compatible. `database:` still has no default.

`RecordManaging`'s query methods stay zone-unaware: that protocol is a
deprecated, database-agnostic abstraction.

No OpenAPI change was needed — `Components.Schemas.ZoneID` already
models `zoneName` + `ownerName`, and `records/query` already references
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0875c33-0448-483c-bf76-68082b30908a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review

Reviewed the diff (gh pr diff 426) and cross-checked the touched files/tests against the current tree. This is a clean, well-scoped fix.

Code quality & design

  • The core fix is minimal and correct: zoneID: .init(zoneName: "_defaultZone")zoneID: zoneID.map { Components.Schemas.ZoneID(from: $0) } (CloudKitService+Operations.swift:87), reusing the existing Components.Schemas.ZoneID(from:) conversion already proven by modifyRecords.
  • Choosing ZoneID over a bare zoneName: String is the right call — a string-only parameter would have left shared zones (which need ownerName) unsolved, which is half the issue title.
  • New zoneID parameters are consistently inserted right before the non-defaulted database: in every affected signature (queryRecords primitive + 2 deprecated overloads, queryAllRecords, fetchExistingRecordNames), which is exactly what keeps this source-compatible for all labeled call sites.
  • Docs (AGENTS.md, 5 DocC articles, WorkingWithRecords.md) were updated in the same PR rather than drifting — including the DocC symbol-link signatures, which is easy to forget and often missed.
  • The .claude/memory/ additions are appropriately scoped (worktree layout, MistDemoApp fails to build: NoteEditView/RecordDetailView have no accessible initializers (Swift 6.3) #419 status) and don't leak unrelated content into this PR's diff footprint.

Potential bugs / edge cases

  • No client-side interaction check between zoneID and zoneWide (docs note they "pull in opposite directions" but nothing throws or warns if both are set). Confirmed this isn't a regression — zoneWide had no such validation before this PR either — so it's a pre-existing gap this PR merely inherits, not introduces. Worth a follow-up issue if you want CloudKit misconfigurations caught client-side rather than server-side.
  • RecordManaging.queryRecords(recordType:) / queryAllRecords(recordType:) intentionally stay zone-unaware. Reasonable given that protocol is already documented as deprecated and database-agnostic, but worth double-checking no current caller actually needs zone selection through that protocol before the next deprecation pass removes it.

Performance

No concerns — this only changes what gets serialized into an existing request body; no new allocations, requests, or pagination behavior beyond forwarding zoneID per page (correctly verified by queryAllRecordsForwardsZoneIDPerPage).

Security

No concerns. ownerName/zoneName are passed through unchanged (no new string interpolation into URLs or logging paths), and the change doesn't touch auth/credential handling.

Test coverage

The new Zone Selection suite (CloudKitServiceTests.Query+ZoneID.swift) is solid and asserts against the actual serialized request body rather than mocking away the interesting part:

  • omission when nil (pins the "no silent default" behavior change called out in the PR description)
  • custom zone name forwarded, no stray ownerName
  • shared-zone ownerName forwarded
  • ZoneID.defaultZone forwarded explicitly
  • queryAllRecords forwards zoneID on every page (verified against both calls, not just the first)
  • fetchExistingRecordNames forwards zoneID

I did not find a test for the zoneWide+zoneID combination, but given there's no code path that treats them specially, that's consistent with the rest of the coverage rather than a gap introduced here.

Nit

The PR description states the wire-level behavior change (dropping the always-present zoneID key when unset) is "semantically equivalent" per CloudKit — reasonable, but since this is the one behavioral (non-additive) change in an otherwise source-compatible PR, it might be worth a one-line mention in a CHANGELOG/release notes entry if this repo tracks one for the beta.4 milestone.

Overall: solid, well-tested, well-documented fix that matches established codebase conventions (ZoneID reuse, optional-defaulted request-option parameter pattern, docs-in-same-PR). No blocking issues found.

@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review: PR #426 — Custom/shared zone support for the query path

Nice, focused change with strong justification for the design calls. Went through the diff plus the surrounding codebase for consistency.

Code quality / correctness

  • zoneID.map { Components.Schemas.ZoneID(from: $0) } in CloudKitService+Operations.swift:90 correctly replaces the hard-coded .init(zoneName: "_defaultZone") and matches the same conversion pattern already used in modifyRecords, uploadAssets, fetchZoneChanges, etc. — good consistency.
  • Parameter placement (zoneID inserted before zoneWide/numbersAsStrings, and before the non-defaulted database:) preserves source compatibility for existing labeled call sites, and threading through queryAllRecords, the deprecated recordType: overloads, and fetchExistingRecordNames is complete — no caller of the query path appears to have been missed.
  • ZoneID vs. a bare zoneName: String is the right call: ownerName is required to address shared zones, and it mirrors modifyRecords(_:zoneID:)'s existing convention rather than inventing a second zone-naming scheme.
  • The nil = "omit the key" semantics (rather than a client-side default of _defaultZone) is a reasonable read of feedback_no_silent_policy_defaults.md — this isn't credential/attribution policy, and it matches the existing optional-field convention on zoneWide/numbersAsStrings/desiredKeys.

Potential issues

  • zoneID + zoneWide: true interaction is undocumented-in-code. The docs note they "pull in opposite directions," but nothing prevents a caller from passing both, and there's no test pinning what CloudKit actually does when both are present in the same request (silently ignore zoneID? error?). Not a blocker, but worth either a doc-only clarification test or a note if this is unverified against the live API.
  • Scope of the diff: the two new .claude/memory/*.md files (project_419_fixed_in_beta3.md, project_beta4_worktree_layout.md) and their MEMORY.md index entries are unrelated to the zone-selection issue — they're process/worktree notes about a different issue and the beta.4 branch layout. Bundling them into this PR adds unrelated diff noise for reviewers; consider splitting agent-memory housekeeping into its own PR/commit going forward.

Test coverage
The new Zone Selection suite (CloudKitServiceTests.Query+ZoneID.swift) is thorough for what it covers: omission when nil, custom zone name (and that no stray ownerName leaks in), shared zone ownerName forwarding, explicit ZoneID.defaultZone, per-page forwarding through queryAllRecords's pagination, and fetchExistingRecordNames forwarding. All assert against the actual serialized request body via MockTransport, which is the right level (catches wire-format regressions, not just that a value was "passed somewhere"). One gap: no test for the zoneID+zoneWide combination noted above, and no negative/error-path test — though that's arguably CloudKit's problem, not MistKit's, since this is pure request-shaping.

Security / performance
No concerns — this only changes what's serialized into an already-authenticated request; no new attack surface, no perf-sensitive path touched (one optional field, no extra allocations beyond the existing .map).

Docs
AGENTS.md/CLAUDE.md and WorkingWithRecords.md updates are clear and include both the custom-zone and shared-zone examples called out in the originating issue. DocC symbol links across the five .docc articles were updated for the new signature — good catch, easy to miss.

Overall: solid, well-scoped implementation matching established codebase conventions. The memory-file scope creep is the only thing worth asking about before merging.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (v1.0.0-beta.4@d295c30). Learn more about missing BASE report.

Additional details and impacted files
@@               Coverage Diff                @@
##             v1.0.0-beta.4     #426   +/-   ##
================================================
  Coverage                 ?   76.25%           
================================================
  Files                    ?      158           
  Lines                    ?     3765           
  Branches                 ?        0           
================================================
  Hits                     ?     2871           
  Misses                   ?      894           
  Partials                 ?        0           
Flag Coverage Δ
mistdemo-spm-macos 11.28% <0.00%> (?)
mistdemo-swift-6.2-jammy 11.29% <0.00%> (?)
mistdemo-swift-6.2-noble 11.29% <0.00%> (?)
mistdemo-swift-6.3-jammy 11.29% <0.00%> (?)
mistdemo-swift-6.3-noble 11.29% <0.00%> (?)
spm 74.26% <100.00%> (?)
swift-6.1-jammy 74.38% <100.00%> (?)
swift-6.1-noble 74.09% <100.00%> (?)
swift-6.2-jammy 74.14% <100.00%> (?)
swift-6.2-noble 74.25% <100.00%> (?)
swift-6.3-jammy 74.25% <100.00%> (?)
swift-6.3-noble 74.28% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Wire zoneID and optional zone owner through QueryCommand, the query
integration phase, and the web query path so #146 can be exercised live.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude

claude Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review: Add custom/shared zone support to the query path (#146)

Reviewed against CLAUDE.md/AGENTS.md conventions. Overall this is a well-scoped, well-documented change that follows the codebase's existing patterns closely.

Strengths

  • Consistent with modifyRecords: queryRecords's new zoneID: ZoneID? = nil (CloudKitService+Operations.swift:90) uses the exact same zoneID.map { Components.Schemas.ZoneID(from: $0) } pattern and nil-omission semantics as the existing modifyRecords(_:atomic:zoneID:...) (CloudKitService+WriteOperations.swift:111). Good API consistency.
  • ZoneID conversion is clean: Components.Schemas.ZoneID.init(from:) (ZoneID.swift:57-64) is a straightforward 1:1 field mapping — no swaps, no force-unwraps, no truncation.
  • Pagination threading verified correct: queryAllRecords (CloudKitService+QueryPagination.swift) re-passes zoneID from the outer parameter on every page (not stale/hardcoded), and the stuck-marker/continuation-marker logic is untouched.
  • Solid new test suite: CloudKitServiceTests.Query+ZoneID.swift covers omit-by-default, custom zone, shared-zone ownerName, explicit ZoneID.defaultZone, per-page forwarding in queryAllRecords, and fetchExistingRecordNames forwarding — a good spread against the serialized request body rather than just the Swift call site.
  • Source-compatible: new params are defaulted and inserted before non-defaulted database:, matching CLAUDE.md's requirement that database: never gets a default.
  • Docs (AGENTS.md, DocC articles) are thoughtfully updated and explain the design decisions (ZoneID vs bare zoneName, why nil isn't a "silent policy default" per the repo's own memory convention).

Issues

  1. Silent drop of zoneOwner in the MistDemo web backend (Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend.swift:45-47):

    let zoneID = zoneName.map { ZoneID(zoneName: $0, ownerName: zoneOwner) }

    If a web API caller sends zoneOwner but omits zoneName, the whole zoneID becomes nil and zoneOwner is silently discarded — the query quietly falls back to the default zone instead of failing loudly. There's no cross-field validation anywhere in WebRequests.swift/WebServer+CRUD.swift to catch this. Given the rest of this codebase's "fail loud" philosophy (see the ConversionError.typeValueMismatch pattern in CLAUDE.md), I'd expect this to at least throw a clear error rather than silently mis-resolve the zone.

  2. CLI/web inconsistency for the same edge case: QueryCommand (Commands/QueryCommand.swift:85) always builds an explicit ZoneID(zoneName: config.zone, ownerName: config.zoneOwner) because QueryConfig.zone defaults to "_defaultZone" (non-optional) rather than nil. Two consequences:

    • The MistDemo CLI can never exercise the "omit the zoneID key" wire behavior this PR introduces at the library level — every CLI-issued query now always sends an explicit zoneID, which is the opposite of what the PR description highlights as the behavioral change.
    • --zone-owner without --zone behaves differently than the same input on the web backend: the CLI applies it against _defaultZone (a zone that doesn't have a meaningful "owner") instead of dropping it. Worth reconciling, or at least documenting the intended semantics for both surfaces.
  3. Test coverage gap on the web HTTP path: the existing queryForwards test in WebServerTests+CRUD.swift (POST /api/records/query forwards to the backend) wasn't updated to include zoneName/zoneOwner in its JSON body or assertions. The new WebRequests.Query.zoneName/.zoneOwner decode wiring is therefore only exercised indirectly (CLI-level QueryConfig tests, and MockBackend's pass-through capture, which bypasses real JSON decoding). A test mirroring the existing recordsChangesForwards test (which does assert zone decode-through-HTTP) would close this gap and would be a natural place to also cover/lock in the zoneOwner-without-zoneName behavior from point 1.

Minor

  • The diff also carries unrelated .claude/memory/* housekeeping (new project_419_fixed_in_beta3.md, project_beta4_worktree_layout.md, MEMORY.md index entries). Not wrong, but orthogonal to the zone-query feature — would read cleaner split out, consistent with the repo's own "unrelated changes stay isolated" convention.

Performance / Security

No concerns on either front. The added parameter is optional and zero-cost when omitted, and zone name/owner are opaque identifiers passed straight through to CloudKit with no new attack surface (same handling as the existing modifyRecords zone parameter).

🤖 Generated with Claude Code

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.

1 participant