From 5a2d84da90b39c03a515789f31c6b45bddd6c179 Mon Sep 17 00:00:00 2001 From: Ignat Remizov Date: Mon, 25 May 2026 23:36:28 +0300 Subject: [PATCH] fix(chunter): skip legacy comment migration when domain is absent 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 --- .../core/packages/model/src/migration.ts | 2 ++ .../server/packages/postgres/src/utils.ts | 33 +++++++++++++++++-- models/chunter/src/migration.ts | 18 ++++++---- server/tool/src/upgrade.ts | 6 ++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/foundations/core/packages/model/src/migration.ts b/foundations/core/packages/model/src/migration.ts index c82b8c2eaf5..9c19a97a60b 100644 --- a/foundations/core/packages/model/src/migration.ts +++ b/foundations/core/packages/model/src/migration.ts @@ -108,6 +108,8 @@ export interface MigrationClient { create: (domain: Domain, doc: T | T[]) => Promise delete: (domain: Domain, _id: Ref) => Promise deleteMany: (domain: Domain, query: DocumentQuery) => Promise + // Check the physical storage domain, including legacy domains no longer present in the active model. + domainExists: (domain: Domain) => Promise hierarchy: Hierarchy model: ModelDb diff --git a/foundations/server/packages/postgres/src/utils.ts b/foundations/server/packages/postgres/src/utils.ts index 469904e3a93..3c0df2258a9 100644 --- a/foundations/server/packages/postgres/src/utils.ts +++ b/foundations/server/packages/postgres/src/utils.ts @@ -325,12 +325,39 @@ export class DBCollectionHelper implements DomainHelperOperations { async create (domain: Domain): Promise {} async exists (domain: Domain): Promise { - // 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> { - 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, options?: { name: string }): Promise {} diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index abfa2b41395..87ed38f030c 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -156,16 +156,20 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat } async function convertCommentsToChatMessages (client: MigrationClient): Promise { - await client.update( - DOMAIN_COMMENT, - { _class: 'chunter:class:Comment' as Ref> }, - { _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: chunter.class.ChatMessage } + ) + await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY) + } } async function removeBacklinks (client: MigrationClient): Promise { - await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref> }) + if (await client.domainExists(DOMAIN_COMMENT)) { + await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref> }) + } await client.deleteMany(DOMAIN_ACTIVITY, { _class: activity.class.DocUpdateMessage, objectClass: 'chunter:class:Backlink' as Ref> diff --git a/server/tool/src/upgrade.ts b/server/tool/src/upgrade.ts index 4792ef47cb0..21a8942e820 100644 --- a/server/tool/src/upgrade.ts +++ b/server/tool/src/upgrade.ts @@ -127,6 +127,12 @@ export class MigrateClientImpl implements MigrationClient { await this.lowLevel.rawDeleteMany(domain, query) } + async domainExists (domain: Domain): Promise { + const adapter = this.pipeline.context.adapterManager?.getAdapter(domain, false) + const helper = adapter?.helper?.() + return helper !== undefined ? await helper.exists(domain) : true + } + async fullReindex (): Promise { await this.queue.send(this.ctx, this.wsIds.uuid, [workspaceEvents.fullReindex()]) }