Skip to content

Commit e2b0536

Browse files
committed
existing users get seeded into DBSelfProfile
1 parent c094c86 commit e2b0536

3 files changed

Lines changed: 82 additions & 33 deletions

File tree

ConvosCore/Sources/ConvosCore/Profiles/Repository/ProfileBackfill.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,26 @@ struct ProfileBackfill {
4545
try DBMemberProfile.fetchAll(db)
4646
}
4747
try await mirror(rows)
48+
try await seedSelfFromGlobalProfile()
49+
}
50+
51+
/// Seeds the canonical self profile from the legacy global profile
52+
/// (`DBMyProfile`), where an existing user's name/metadata lives when they
53+
/// never had a self `member_profile` row. Fill-only: it never overwrites a
54+
/// value already authored through the canonical path (`publishMyProfile`).
55+
private func seedSelfFromGlobalProfile() async throws {
56+
let global = try await databaseReader.read { db in
57+
try DBMyProfile.filter(DBMyProfile.Columns.inboxId == selfInboxId).fetchOne(db)
58+
}
59+
guard let global else { return }
60+
let existing = try await selfProfileStore.load()
61+
let mergedName = ProfileMerge.nonBlank(existing?.name) ?? ProfileMerge.nonBlank(global.name)
62+
let mergedMetadata = existing?.metadata ?? global.metadata
63+
guard mergedName != nil || mergedMetadata != nil else { return }
64+
guard existing == nil || mergedName != existing?.name || mergedMetadata != existing?.metadata else { return }
65+
try await selfProfileStore.save(
66+
DBSelfProfile(inboxId: selfInboxId, name: mergedName, metadata: mergedMetadata, updatedAt: existing?.updatedAt ?? floor)
67+
)
4868
}
4969

5070
/// Mirrors the given legacy rows into the canonical stores. Idempotent: a

ConvosCore/Sources/ConvosCore/Storage/Repositories/MyProfileRepository.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,20 +144,20 @@ class MyProfileRepository: MyProfileRepositoryProtocol {
144144
}
145145
}
146146

147-
/// Reads the current user's identity from the inbox-wide `DBMyProfile`.
147+
/// Reads the current user's identity from the canonical `DBSelfProfile`.
148148
/// After the unified-profile cutover self identity is global (no longer
149-
/// per-conversation), so the legacy `member_profile` row is not consulted -
150-
/// it is kept only as vestigial storage and may be empty.
149+
/// per-conversation), and every self-write path (`publishMyProfile`, the
150+
/// settings editor) updates `DBSelfProfile`, so reading it here keeps the
151+
/// editor's own view current. The legacy per-conversation `member_profile`
152+
/// row is not consulted.
151153
private static func observedProfile(_ db: Database, inboxId: String, conversationId: String) throws -> Profile {
152-
if let global = try DBMyProfile
153-
.filter(DBMyProfile.Columns.inboxId == inboxId)
154-
.fetchOne(db) {
154+
if let selfRow = try DBSelfProfile.fetchOne(db, inboxId: inboxId) {
155155
return Profile(
156156
inboxId: inboxId,
157157
conversationId: conversationId,
158-
name: (global.name?.isEmpty ?? true) ? nil : global.name,
158+
name: (selfRow.name?.isEmpty ?? true) ? nil : selfRow.name,
159159
avatar: nil,
160-
metadata: global.metadata
160+
metadata: selfRow.metadata
161161
)
162162
}
163163
return .empty(inboxId: inboxId, conversationId: conversationId)

docs/plans/2026-06-29-profile-table-implementation.md

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -729,33 +729,62 @@ What shipped:
729729
`MyGlobalProfileWriter` / `DBMyProfile` as the global edit-side model.
730730
- [done] `member_profile` is left fully populated. The `deleteAll`-after-backfill
731731
clear was removed after PR review: several live subsystems still read
732-
`DBMemberProfile` (see "remaining migration work"), so clearing (or dropping)
733-
the table is not yet safe. `member_profile` is simply no longer the *rendering*
732+
`DBMemberProfile` (see the cleanup checklist), so clearing (or dropping) the
733+
table is not yet safe. `member_profile` is simply no longer the *rendering*
734734
identity source; the un-migrated subsystems keep working off it unchanged.
735-
736-
### Remaining migration work (before any member_profile clear/drop)
737-
738-
The rendering choke point was migrated to the canonical tables, but these live
739-
paths still read/write `DBMemberProfile` and must move to `DBProfile` /
740-
`DBProfileAvatar` before `member_profile` can be cleared or dropped:
741-
742-
Readers:
743-
- `ContactSyncCoordinator.syncConversation` - builds contact snapshots per member.
744-
- `AssetRenewalURLCollector.collectRenewableAssets` - enumerates avatar assets.
745-
- `AgentTimezonePublisher.republishTimezoneForAgentConversations` - finds the
735+
- [done] Self-source consistency (PR review): `ProfileBackfill` now also seeds
736+
`DBSelfProfile` from the legacy global `DBMyProfile` (fill-only), and
737+
`MyProfileRepository` reads self identity from `DBSelfProfile` (the table every
738+
self-write path updates) so the editor's own view stays current. This is a
739+
stopgap - `DBMyProfile` and `DBSelfProfile` still both exist and overlap; see
740+
the cleanup checklist for the consolidation.
741+
742+
### Cleanup checklist for a subsequent release
743+
744+
The rendering choke point is on the canonical tables, but the cutover is not
745+
complete. None of the following block *this* PR (it is additive and safe), but
746+
they must land before `member_profile` can ever be cleared or dropped, and they
747+
resolve known rough edges.
748+
749+
Migrate the remaining `DBMemberProfile` readers to `DBProfile`/`DBProfileAvatar`:
750+
- [ ] `ContactSyncCoordinator.syncConversation` - builds contact snapshots per
751+
member.
752+
- [ ] `AssetRenewalURLCollector.collectRenewableAssets` - enumerates avatar
753+
assets for renewal.
754+
- [ ] `AgentTimezonePublisher.republishTimezoneForAgentConversations` - finds the
746755
user's conversations.
747-
- `AgentBuilderConnectionGrantReplayer.verifiedAgentInboxIds` - filters verified
748-
agents (an agent reverified only in `DBProfile` is invisible here).
749-
750-
Write paths (populate `DBMemberProfile` but not canonical, so identity from these
751-
alone renders as "Somebody"):
752-
- `InviteJoinRequestsManager` - joiner profile via `ContactsWriter` mirror.
753-
- `ConversationWriter.persist` appData gap-fill.
754-
755-
Ordering: `IncomingMessageWriter.bootstrapSenderProfile` reads `DBProfile`, but in
756-
`BatchCatchUp` message persist runs before `ProfileInboundApplier` (detached), so
757-
a verified agent's grant request can be dropped on cold-launch backlog replay.
758-
Needs the canonical write to land before the gate is checked.
756+
- [ ] `AgentBuilderConnectionGrantReplayer.verifiedAgentInboxIds` - filters
757+
verified agents (an agent reverified only in `DBProfile` is invisible here).
758+
759+
Migrate the remaining `DBMemberProfile` write paths to canonical (otherwise
760+
identity introduced only through them renders as "Somebody"):
761+
- [ ] `InviteJoinRequestsManager` - joiner profile (currently via the
762+
`ContactsWriter` mirror).
763+
- [ ] `ConversationWriter.persist` appData gap-fill, and the appData-only member
764+
case generally (member identity that lives only in group appData, never in a
765+
ProfileUpdate/Snapshot).
766+
767+
Ordering:
768+
- [ ] `IncomingMessageWriter.bootstrapSenderProfile` reads `DBProfile`, but in
769+
`BatchCatchUp` message persist runs before `ProfileInboundApplier` (detached),
770+
so a verified agent's grant request can be dropped on cold-launch backlog
771+
replay. Land the canonical write before the trusted-sender gate is checked.
772+
773+
Self-profile consolidation:
774+
- [ ] Collapse `DBMyProfile` and `DBSelfProfile` into one authoritative self
775+
source. Options: fold the editor's raw image bytes into the canonical path and
776+
retire `DBMyProfile`, or keep `DBMyProfile` strictly as a local image-bytes
777+
cache and make `DBSelfProfile` derived from it on every write. Remove the
778+
backfill stopgap once done.
779+
780+
Rendering path:
781+
- [ ] Wire ViewModels/Views to the repository's reactive reads
782+
(`profilePublisher` / `selfProfilePublisher`); today rendering flows through the
783+
choke-point GRDB reroute and those publishers are unused.
784+
785+
Then, and only then:
786+
- [ ] Clear + drop `member_profile` (Android cleared but never dropped; a
787+
`deleteAll` after backfill plus a `DROP TABLE` migration).
759788

760789
---
761790

0 commit comments

Comments
 (0)