fix(chunter): skip legacy comment migration when domain is absent (#10891)

Allow old Chunter migrations to complete cleanly for workspaces that no longer have the legacy `comment` storage domain. The migration still runs normally when the domain exists, but workspaces created or restored after the comment-to-activity migration can now skip the obsolete table operations and record the migration state.

Changes:
- Add a migration-client `domainExists` helper backed by the active adapter.
- Make the Postgres domain helper check actual public tables instead of assuming every domain exists.
- Keep Postgres domain listing compatible with model domain names while including translated table names.
- Guard legacy `comment` update/move/delete operations in the Chunter migration.

Validation:
- git diff --cached --check
- node common/scripts/install-run-rush.js build -t @hcengineering/model -t @hcengineering/server-tool -t @hcengineering/postgres -t @hcengineering/model-chunter

Signed-off-by: Ignat Remizov <ignat@ignatremizov.com>
This commit is contained in:
Ignat Remizov
2026-09-29 07:36:56 +02:00
committed by GitHub
parent ce3c4a1dc3
commit 7d01eb2cd3
4 changed files with 49 additions and 10 deletions
@@ -108,6 +108,8 @@ export interface MigrationClient {
create: <T extends Doc>(domain: Domain, doc: T | T[]) => Promise<void>
delete: <T extends Doc>(domain: Domain, _id: Ref<T>) => Promise<void>
deleteMany: <T extends Doc>(domain: Domain, query: DocumentQuery<T>) => Promise<void>
// Check the physical storage domain, including legacy domains no longer present in the active model.
domainExists: (domain: Domain) => Promise<boolean>
hierarchy: Hierarchy
model: ModelDb
@@ -325,12 +325,39 @@ export class DBCollectionHelper implements DomainHelperOperations {
async create (domain: Domain): Promise<void> {}
async exists (domain: Domain): Promise<boolean> {
// Always exists. We don't need to check for index existence
return true
// Migrations can reference legacy domains that were removed from the current model,
// so this must check the physical table instead of the initialized model-domain set.
const tableName = translateDomain(domain)
const res = await this.client.execute(
`
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
LIMIT 1
`,
[tableName]
)
return res.length > 0
}
async listDomains (): Promise<Set<Domain>> {
return this.domains
const rows = await this.client.execute(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name NOT LIKE 'pg_%'
AND table_name NOT LIKE 'cluster_%'
AND table_name NOT LIKE 'kv_%'
AND table_name NOT LIKE 'node_%'
`)
const tableNames = new Set(rows.map((it) => it.table_name as Domain))
for (const domain of this.domains) {
if (tableNames.has(translateDomain(domain) as Domain)) {
tableNames.add(domain)
}
}
return tableNames
}
async createIndex (domain: Domain, value: string | FieldIndexConfig<Doc>, options?: { name: string }): Promise<void> {}
+11 -7
View File
@@ -156,16 +156,20 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat
}
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_COMMENT,
{ _class: 'chunter:class:Comment' as Ref<Class<Doc>> },
{ _class: chunter.class.ChatMessage }
)
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
if (await client.domainExists(DOMAIN_COMMENT)) {
await client.update(
DOMAIN_COMMENT,
{ _class: 'chunter:class:Comment' as Ref<Class<Doc>> },
{ _class: chunter.class.ChatMessage }
)
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
}
}
async function removeBacklinks (client: MigrationClient): Promise<void> {
await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref<Class<Doc>> })
if (await client.domainExists(DOMAIN_COMMENT)) {
await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref<Class<Doc>> })
}
await client.deleteMany(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
objectClass: 'chunter:class:Backlink' as Ref<Class<Doc>>
+6
View File
@@ -127,6 +127,12 @@ export class MigrateClientImpl implements MigrationClient {
await this.lowLevel.rawDeleteMany(domain, query)
}
async domainExists (domain: Domain): Promise<boolean> {
const adapter = this.pipeline.context.adapterManager?.getAdapter(domain, false)
const helper = adapter?.helper?.()
return helper !== undefined ? await helper.exists(domain) : true
}
async fullReindex (): Promise<void> {
await this.queue.send(this.ctx, this.wsIds.uuid, [workspaceEvents.fullReindex()])
}