fix(server): gate viewFilter.relationTargetFieldMetadataId behind its 2.6 upgrade command (#21267)

## Problem

Self-hosted upgrades crossing 2.6 (e.g. `2.4 → 2.6/2.9`) can abort with:

```
column ViewFilterEntity.relationTargetFieldMetadataId does not exist
  at WorkspaceFlatViewFilterMapCacheService.computeForCache
  at WorkspaceCacheService.recomputeDataFromProvider
[UpgradeSequenceRunnerService] Workspace steps ended with 1 failure(s). Aborting
```

This is **Failure #1** from #20841 — the counterpart to the
role-permission cache crash fixed in #21257 (Failure #2). Same shape: a
workspace **cache recompute runs mid-upgrade and reads schema that the
target version's migration hasn't applied yet**.

## Root cause

`ViewFilterEntity.relationTargetFieldMetadataId` is added to
`core.viewFilter` only at the **2.6.0** cursor
(`AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand`, ts
`1798000005000`). But the workspace cache recompute SELECTs every column
of the entity, and it runs during *earlier* (2.5) workspace steps.
Unlike `RolePermissionFlagEntity.permissionFlag`, this column has **no
`@WasIntroducedInUpgrade` gate**, so the proxy can't hide it — and the
SELECT fails when the column isn't there yet.

There are three `IF NOT EXISTS` backport commands (2.3/2.4/2.5) meant to
add the column sooner, but they use **low timestamps** that sort to the
front of their version bundles. An instance whose cursor has already
advanced past those positions (e.g. it reached 2.4, or a prior failed
attempt advanced it through 2.5 instance commands) treats them as
already-applied and **skips them** — so the column is never created, yet
the entity keeps selecting it.

## Fix

Gate the column with `@WasIntroducedInUpgrade` pointing at the **2.6.0**
command that adds it:

```ts
@WasIntroducedInUpgrade({
  upgradeCommandName:
    '2.6.0_AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand_1798000005000',
})
@Column({ nullable: true, type: 'uuid', default: null })
relationTargetFieldMetadataId: string | null;
```

`UpgradeAwareRepositoryProxy` then hides the column from reads while the
cursor is < 2.6, so the cache recompute simply omits it — no crash — and
it becomes visible once the 2.6.0 command has run (where it's guaranteed
to exist). Gating to **2.6.0** specifically (not the earlier backports)
is what fixes the cursor-skip case: 2.6.0 is the first point where the
column is reliably present regardless of whether the backports ran.

Validator-safe: the referenced command resolves to a real step
(`computeCommandName` = `${version}_${className}_${timestamp}`), so
`validate-upgrade-aware-entity-decorators` accepts it. The existing
backport commands are left untouched (committed instance commands).

## Recovery for already-stuck instances

This prevents *new* failures. An instance already aborted mid-upgrade
needs the column added manually before retrying:

```sql
ALTER TABLE core."viewFilter" ADD COLUMN IF NOT EXISTS "relationTargetFieldMetadataId" uuid;
DELETE FROM core."upgradeMigration" WHERE status='failed';
```
then re-run the upgrade on a build that includes this fix.

Refs #20841

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles Bochet
2026-06-05 18:56:02 +02:00
committed by GitHub
parent 51e3eddb29
commit 20d9244639
@@ -12,6 +12,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
@@ -64,6 +65,10 @@ export class ViewFilterEntity
@Column({ nullable: true, type: 'text', default: null })
subFieldName: string | null;
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.6.0_AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand_1798000005000',
})
@Column({ nullable: true, type: 'uuid', default: null })
relationTargetFieldMetadataId: string | null;