Skip to content

Commit a466a71

Browse files
authored
Merge pull request #2364 from teableio/fix/link-integrity-backfill
fix: backfill link fks during integrity repair (T1503)
2 parents 459b406 + 207a7dd commit a466a71

File tree

2 files changed

+568
-1
lines changed

2 files changed

+568
-1
lines changed

apps/nestjs-backend/src/features/integrity/link-integrity.service.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CellValueType,
77
DbFieldType,
88
Relationship,
9+
DriverClient,
910
} from '@teable/core';
1011
import type { Field } from '@teable/db-main-prisma';
1112
import { Prisma, PrismaService } from '@teable/db-main-prisma';
@@ -382,6 +383,11 @@ export class LinkIntegrityService {
382383
return;
383384
}
384385

386+
if (options.relationship === Relationship.OneOne && options.foreignKeyName === '__id') {
387+
// Symmetric OneOne fields do not own the FK column.
388+
return;
389+
}
390+
385391
const tableDomain = await this.tableDomainQueryService.getTableDomainById(fieldRaw.tableId);
386392
const tableNameMap = await this.linkFieldQueryService.getTableNameMapForLinkFields(
387393
fieldRaw.tableId,
@@ -532,13 +538,258 @@ export class LinkIntegrityService {
532538
}
533539
}
534540

541+
await this.backfillForeignKeysFromLinkColumn({
542+
dbTableName: tableMeta.dbTableName,
543+
linkDbFieldName: linkField.dbFieldName,
544+
fkHostTableName: options.fkHostTableName,
545+
selfKeyName: options.selfKeyName,
546+
foreignKeyName: options.foreignKeyName,
547+
relationship: options.relationship,
548+
isOneWay: options.isOneWay,
549+
});
550+
535551
return {
536552
type: issueType ?? IntegrityIssueType.ForeignKeyNotFound,
537553
fieldId,
538554
message: `Restored missing foreign key columns for link field (Field Name: ${fieldRaw.name}, Field ID: ${fieldId})`,
539555
};
540556
}
541557

558+
private async backfillForeignKeysFromLinkColumn(params: {
559+
dbTableName: string;
560+
linkDbFieldName: string;
561+
fkHostTableName: string;
562+
selfKeyName: string;
563+
foreignKeyName: string;
564+
relationship: Relationship;
565+
isOneWay?: boolean;
566+
}) {
567+
const {
568+
dbTableName,
569+
linkDbFieldName,
570+
fkHostTableName,
571+
selfKeyName,
572+
foreignKeyName,
573+
relationship,
574+
isOneWay,
575+
} = params;
576+
const prisma = this.prismaService.txClient();
577+
578+
const linkColumnExists = await this.dbProvider.checkColumnExist(
579+
dbTableName,
580+
linkDbFieldName,
581+
prisma
582+
);
583+
if (!linkColumnExists) {
584+
return;
585+
}
586+
587+
const usesJunction =
588+
relationship === Relationship.ManyMany ||
589+
(relationship === Relationship.OneMany && Boolean(isOneWay));
590+
591+
if (relationship === Relationship.ManyOne || relationship === Relationship.OneOne) {
592+
const foreignKeyExists = await this.dbProvider.checkColumnExist(
593+
fkHostTableName,
594+
foreignKeyName,
595+
prisma
596+
);
597+
if (!foreignKeyExists) {
598+
return;
599+
}
600+
601+
const query =
602+
this.dbProvider.driver === DriverClient.Pg
603+
? this.knex(fkHostTableName)
604+
.update({
605+
[foreignKeyName]: this.knex.raw(`NULLIF(??->>'id','')`, [linkDbFieldName]),
606+
})
607+
.whereNotNull(linkDbFieldName)
608+
.whereNull(foreignKeyName)
609+
.toQuery()
610+
: this.knex(fkHostTableName)
611+
.update({
612+
[foreignKeyName]: this.knex.raw(`json_extract(??, '$.id')`, [linkDbFieldName]),
613+
})
614+
.whereNotNull(linkDbFieldName)
615+
.whereNull(foreignKeyName)
616+
.toQuery();
617+
618+
await prisma.$executeRawUnsafe(query);
619+
return;
620+
}
621+
622+
if (relationship === Relationship.OneMany && !usesJunction) {
623+
const selfKeyExists = await this.dbProvider.checkColumnExist(
624+
fkHostTableName,
625+
selfKeyName,
626+
prisma
627+
);
628+
if (!selfKeyExists) {
629+
return;
630+
}
631+
632+
const query =
633+
this.dbProvider.driver === DriverClient.Pg
634+
? this.knex
635+
.raw(
636+
`
637+
WITH pairs AS (
638+
SELECT s.__id AS self_id,
639+
(elem->>'id') AS foreign_id
640+
FROM ?? AS s
641+
JOIN LATERAL jsonb_array_elements(??.??) elem ON true
642+
WHERE ??.?? IS NOT NULL
643+
),
644+
dedup AS (
645+
SELECT foreign_id, MIN(self_id) AS self_id
646+
FROM pairs
647+
WHERE foreign_id IS NOT NULL
648+
GROUP BY foreign_id
649+
)
650+
UPDATE ?? AS f
651+
SET ?? = d.self_id
652+
FROM dedup d
653+
WHERE f.__id = d.foreign_id
654+
AND f.?? IS NULL
655+
`,
656+
[
657+
dbTableName,
658+
's',
659+
linkDbFieldName,
660+
's',
661+
linkDbFieldName,
662+
fkHostTableName,
663+
selfKeyName,
664+
selfKeyName,
665+
]
666+
)
667+
.toQuery()
668+
: this.knex
669+
.raw(
670+
`
671+
WITH pairs AS (
672+
SELECT s.__id AS self_id,
673+
json_extract(j.value, '$.id') AS foreign_id
674+
FROM ?? AS s
675+
JOIN json_each(??.??) j
676+
WHERE ??.?? IS NOT NULL
677+
),
678+
dedup AS (
679+
SELECT foreign_id, MIN(self_id) AS self_id
680+
FROM pairs
681+
WHERE foreign_id IS NOT NULL
682+
GROUP BY foreign_id
683+
)
684+
UPDATE ??
685+
SET ?? = (SELECT d.self_id FROM dedup d WHERE d.foreign_id = ??.__id)
686+
WHERE __id IN (SELECT foreign_id FROM dedup)
687+
AND ?? IS NULL
688+
`,
689+
[
690+
dbTableName,
691+
's',
692+
linkDbFieldName,
693+
's',
694+
linkDbFieldName,
695+
fkHostTableName,
696+
selfKeyName,
697+
fkHostTableName,
698+
selfKeyName,
699+
]
700+
)
701+
.toQuery();
702+
703+
await prisma.$executeRawUnsafe(query);
704+
return;
705+
}
706+
707+
if (!usesJunction) {
708+
return;
709+
}
710+
711+
const [selfKeyExists, foreignKeyExists] = await Promise.all([
712+
this.dbProvider.checkColumnExist(fkHostTableName, selfKeyName, prisma),
713+
this.dbProvider.checkColumnExist(fkHostTableName, foreignKeyName, prisma),
714+
]);
715+
if (!selfKeyExists || !foreignKeyExists) {
716+
return;
717+
}
718+
719+
const query =
720+
this.dbProvider.driver === DriverClient.Pg
721+
? this.knex
722+
.raw(
723+
`
724+
WITH pairs AS (
725+
SELECT s.__id AS self_id,
726+
(elem->>'id') AS foreign_id
727+
FROM ?? AS s
728+
JOIN LATERAL jsonb_array_elements(??.??) elem ON true
729+
WHERE ??.?? IS NOT NULL
730+
)
731+
INSERT INTO ?? (??, ??)
732+
SELECT DISTINCT p.self_id, p.foreign_id
733+
FROM pairs p
734+
WHERE p.foreign_id IS NOT NULL
735+
AND NOT EXISTS (
736+
SELECT 1 FROM ?? j
737+
WHERE j.?? = p.self_id AND j.?? = p.foreign_id
738+
)
739+
`,
740+
[
741+
dbTableName,
742+
's',
743+
linkDbFieldName,
744+
's',
745+
linkDbFieldName,
746+
fkHostTableName,
747+
selfKeyName,
748+
foreignKeyName,
749+
fkHostTableName,
750+
selfKeyName,
751+
foreignKeyName,
752+
]
753+
)
754+
.toQuery()
755+
: this.knex
756+
.raw(
757+
`
758+
WITH pairs AS (
759+
SELECT s.__id AS self_id,
760+
json_extract(j.value, '$.id') AS foreign_id
761+
FROM ?? AS s
762+
JOIN json_each(??.??) j
763+
WHERE ??.?? IS NOT NULL
764+
)
765+
INSERT INTO ?? (??, ??)
766+
SELECT DISTINCT p.self_id, p.foreign_id
767+
FROM pairs p
768+
WHERE p.foreign_id IS NOT NULL
769+
AND NOT EXISTS (
770+
SELECT 1 FROM ?? j
771+
WHERE j.?? = p.self_id AND j.?? = p.foreign_id
772+
)
773+
`,
774+
[
775+
dbTableName,
776+
's',
777+
linkDbFieldName,
778+
's',
779+
linkDbFieldName,
780+
fkHostTableName,
781+
selfKeyName,
782+
foreignKeyName,
783+
fkHostTableName,
784+
selfKeyName,
785+
foreignKeyName,
786+
]
787+
)
788+
.toQuery();
789+
790+
await prisma.$executeRawUnsafe(query);
791+
}
792+
542793
async linkIntegrityFix(baseId: string, tableId?: string): Promise<IIntegrityIssue[]> {
543794
const checkResult = await this.linkIntegrityCheck(baseId, tableId || '');
544795
const fixResults: IIntegrityIssue[] = [];

0 commit comments

Comments
 (0)