@strapi/database 5.54.0 → 5.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entity-manager/index.d.ts.map +1 -1
- package/dist/entity-manager/index.js +9 -2
- package/dist/entity-manager/index.js.map +1 -1
- package/dist/entity-manager/index.mjs +9 -2
- package/dist/entity-manager/index.mjs.map +1 -1
- package/dist/migrations/internal-migrations/5.0.0-02-document-id.d.ts.map +1 -1
- package/dist/migrations/internal-migrations/5.0.0-02-document-id.js +139 -64
- package/dist/migrations/internal-migrations/5.0.0-02-document-id.js.map +1 -1
- package/dist/migrations/internal-migrations/5.0.0-02-document-id.mjs +139 -64
- package/dist/migrations/internal-migrations/5.0.0-02-document-id.mjs.map +1 -1
- package/dist/query/helpers/where.d.ts +8 -1
- package/dist/query/helpers/where.d.ts.map +1 -1
- package/dist/query/helpers/where.js +25 -0
- package/dist/query/helpers/where.js.map +1 -1
- package/dist/query/helpers/where.mjs +25 -1
- package/dist/query/helpers/where.mjs.map +1 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +18 -4
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-builder.mjs +19 -5
- package/dist/query/query-builder.mjs.map +1 -1
- package/package.json +5 -5
|
@@ -13,80 +13,155 @@ function getBatchSize(trx, defaultSize = 1000) {
|
|
|
13
13
|
].includes(client);
|
|
14
14
|
return isSQLite ? Math.min(defaultSize, 250) : defaultSize;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
GROUP BY :tableName:.id, :joinTableName:.:joinColumn:
|
|
24
|
-
LIMIT 1;
|
|
25
|
-
`, params);
|
|
26
|
-
return res.rows;
|
|
27
|
-
},
|
|
28
|
-
async mysql (knex, params) {
|
|
29
|
-
const [res] = await knex.raw(`
|
|
30
|
-
SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
|
|
31
|
-
FROM :tableName:
|
|
32
|
-
LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
|
|
33
|
-
WHERE :tableName:.document_id IS NULL
|
|
34
|
-
GROUP BY :tableName:.id, :joinTableName:.:joinColumn:
|
|
35
|
-
LIMIT 1;
|
|
36
|
-
`, params);
|
|
37
|
-
return res;
|
|
38
|
-
},
|
|
39
|
-
async sqlite (knex, params) {
|
|
40
|
-
return knex.raw(`
|
|
41
|
-
SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
|
|
42
|
-
FROM :tableName:
|
|
43
|
-
LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
|
|
44
|
-
WHERE :tableName:.document_id IS NULL
|
|
45
|
-
GROUP BY :joinTableName:.:joinColumn:
|
|
46
|
-
LIMIT 1;
|
|
47
|
-
`, params);
|
|
16
|
+
// Union-find (disjoint-set) helpers used to cluster localization-linked rows in memory.
|
|
17
|
+
// Path-compressing `find` keeps repeated lookups close to O(1) amortized.
|
|
18
|
+
const find = (parent, start)=>{
|
|
19
|
+
let current = start;
|
|
20
|
+
while(parent.get(current) !== current){
|
|
21
|
+
parent.set(current, parent.get(parent.get(current)));
|
|
22
|
+
current = parent.get(current);
|
|
48
23
|
}
|
|
24
|
+
return current;
|
|
49
25
|
};
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
joinTableName
|
|
56
|
-
});
|
|
57
|
-
if (res.length > 0) {
|
|
58
|
-
const row = res[0];
|
|
59
|
-
const otherIds = row.other_ids ? row.other_ids.split(',').map((v)=>parseInt(v, 10)) : [];
|
|
60
|
-
return [
|
|
61
|
-
row.id,
|
|
62
|
-
...otherIds
|
|
63
|
-
];
|
|
26
|
+
const union = (parent, a, b)=>{
|
|
27
|
+
const rootA = find(parent, a);
|
|
28
|
+
const rootB = find(parent, b);
|
|
29
|
+
if (rootA !== rootB) {
|
|
30
|
+
parent.set(rootA, rootB);
|
|
64
31
|
}
|
|
65
|
-
return [];
|
|
66
32
|
};
|
|
67
33
|
// Migrate document ids for tables that have localizations
|
|
34
|
+
//
|
|
35
|
+
// This clusters rows in memory (via union-find) instead of discovering one cluster at a
|
|
36
|
+
// time through a `LIMIT 1` query. The previous implementation re-ran a query that scans
|
|
37
|
+
// the whole table (filtered by `document_id IS NULL`, a column with no index at this point
|
|
38
|
+
// in the migration) once per cluster, which is roughly O(n) discovery queries each doing
|
|
39
|
+
// an O(n) scan — effectively O(n^2) for a table with n rows. For tables with many rows
|
|
40
|
+
// and/or many locales this could take hours or longer without making visible progress.
|
|
41
|
+
// See https://github.com/strapi/strapi/issues/23812, 23517, 25231.
|
|
42
|
+
//
|
|
43
|
+
// This also fixes a correctness gap: the original query only follows *direct* links
|
|
44
|
+
// (one hop), so a chain of locale links (A<->B, B<->C, but no direct A<->C row) could be
|
|
45
|
+
// split across two document ids instead of clustered together. Union-find naturally
|
|
46
|
+
// follows transitive links. See https://github.com/strapi/strapi/issues/20948, 24544.
|
|
47
|
+
//
|
|
48
|
+
// Retry-safety: a previous run can crash between clusters, leaving some rows in a
|
|
49
|
+
// locale group already assigned a document_id while their siblings are still NULL. On
|
|
50
|
+
// retry those NULL rows must adopt the sibling's existing document_id instead of minting
|
|
51
|
+
// a fresh one, or a document that used to be one splits into two. To support this, the
|
|
52
|
+
// link fetch and the clustering below include already-migrated neighbor rows, not just
|
|
53
|
+
// pending ones — but only pending (NULL) rows are ever written.
|
|
68
54
|
const migrateDocumentIdsWithLocalizations = async (db, knex, meta, heartbeat)=>{
|
|
69
55
|
const singularName = meta.singularName.toLowerCase();
|
|
70
56
|
const joinColumn = _.snakeCase(`${singularName}_id`);
|
|
71
57
|
const inverseJoinColumn = _.snakeCase(`inv_${singularName}_id`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
58
|
+
const joinTableName = _.snakeCase(`${meta.tableName}_localizations_links`);
|
|
59
|
+
const batchSize = getBatchSize(knex);
|
|
60
|
+
const pendingIds = (await knex(meta.tableName).select('id').whereNull('document_id')).map((row)=>row.id);
|
|
61
|
+
if (pendingIds.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const pendingIdSet = new Set(pendingIds);
|
|
65
|
+
// Fetch every link touching a pending id, on *either* side of the join — a pending
|
|
66
|
+
// row's sibling may already be migrated, in which case it appears as the other column.
|
|
67
|
+
// Batched because passing all ids in a single `whereIn` can exceed the database
|
|
68
|
+
// driver's bound-parameter limit on large tables.
|
|
69
|
+
const links = [];
|
|
70
|
+
for(let i = 0; i < pendingIds.length; i += batchSize){
|
|
71
|
+
const idChunk = pendingIds.slice(i, i + batchSize);
|
|
72
|
+
const chunkLinks = await knex(joinTableName).select(joinColumn, inverseJoinColumn).whereIn(joinColumn, idChunk).orWhereIn(inverseJoinColumn, idChunk);
|
|
73
|
+
links.push(...chunkLinks);
|
|
74
|
+
}
|
|
75
|
+
// Ids referenced by a link that aren't pending are already-migrated neighbors — look up
|
|
76
|
+
// their document_id so a retry can adopt it instead of minting a new one.
|
|
77
|
+
const neighborIds = new Set();
|
|
78
|
+
for (const link of links){
|
|
79
|
+
const a = link[joinColumn];
|
|
80
|
+
const b = link[inverseJoinColumn];
|
|
81
|
+
if (!pendingIdSet.has(a)) neighborIds.add(a);
|
|
82
|
+
if (!pendingIdSet.has(b)) neighborIds.add(b);
|
|
83
|
+
}
|
|
84
|
+
const existingDocumentIds = new Map();
|
|
85
|
+
const neighborIdList = [
|
|
86
|
+
...neighborIds
|
|
87
|
+
];
|
|
88
|
+
for(let i = 0; i < neighborIdList.length; i += batchSize){
|
|
89
|
+
const idChunk = neighborIdList.slice(i, i + batchSize);
|
|
90
|
+
const rows = await knex(meta.tableName).select('id', 'document_id').whereIn('id', idChunk);
|
|
91
|
+
for (const row of rows){
|
|
92
|
+
if (row.document_id) {
|
|
93
|
+
existingDocumentIds.set(row.id, row.document_id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const parent = new Map();
|
|
98
|
+
for (const id of pendingIds){
|
|
99
|
+
parent.set(id, id);
|
|
100
|
+
}
|
|
101
|
+
for (const id of neighborIds){
|
|
102
|
+
parent.set(id, id);
|
|
103
|
+
}
|
|
104
|
+
for (const link of links){
|
|
105
|
+
const a = link[joinColumn];
|
|
106
|
+
const b = link[inverseJoinColumn];
|
|
107
|
+
if (parent.has(a) && parent.has(b)) {
|
|
108
|
+
union(parent, a, b);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const clusters = new Map();
|
|
112
|
+
for (const id of parent.keys()){
|
|
113
|
+
const root = find(parent, id);
|
|
114
|
+
const cluster = clusters.get(root);
|
|
115
|
+
if (cluster) {
|
|
116
|
+
cluster.push(id);
|
|
117
|
+
} else {
|
|
118
|
+
clusters.set(root, [
|
|
119
|
+
id
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Group pending writes by target document_id — one write per cluster instead of one
|
|
124
|
+
// per row shrinks the window for a crash to leave a cluster half-written.
|
|
125
|
+
const writeGroups = new Map();
|
|
126
|
+
for (const members of clusters.values()){
|
|
127
|
+
const existingIdsInCluster = new Set();
|
|
128
|
+
for (const id of members){
|
|
129
|
+
const existing = existingDocumentIds.get(id);
|
|
130
|
+
if (existing) {
|
|
131
|
+
existingIdsInCluster.add(existing);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// A cluster normally carries at most one existing document_id. More than one means an
|
|
135
|
+
// earlier inconsistent partial write — unify onto a single deterministic id rather
|
|
136
|
+
// than minting a third.
|
|
137
|
+
const documentId = existingIdsInCluster.size > 0 ? [
|
|
138
|
+
...existingIdsInCluster
|
|
139
|
+
].sort()[0] : cuid2.createId();
|
|
140
|
+
const pendingMembers = members.filter((id)=>pendingIdSet.has(id));
|
|
141
|
+
if (pendingMembers.length > 0) {
|
|
142
|
+
// Two distinct clusters resolving to the same document_id is only possible via a
|
|
143
|
+
// cuid collision, but merge rather than overwrite so neither group's writes vanish.
|
|
144
|
+
const existingGroup = writeGroups.get(documentId);
|
|
145
|
+
if (existingGroup) {
|
|
146
|
+
existingGroup.push(...pendingMembers);
|
|
147
|
+
} else {
|
|
148
|
+
writeGroups.set(documentId, pendingMembers);
|
|
149
|
+
}
|
|
88
150
|
}
|
|
89
|
-
}
|
|
151
|
+
}
|
|
152
|
+
const groupEntries = [
|
|
153
|
+
...writeGroups.entries()
|
|
154
|
+
];
|
|
155
|
+
let processed = 0;
|
|
156
|
+
for(let i = 0; i < groupEntries.length; i += batchSize){
|
|
157
|
+
const slice = groupEntries.slice(i, i + batchSize);
|
|
158
|
+
await Promise.all(slice.map(([documentId, ids])=>knex(meta.tableName).whereIn('id', ids).update({
|
|
159
|
+
document_id: documentId
|
|
160
|
+
})));
|
|
161
|
+
processed += slice.reduce((sum, [, ids])=>sum + ids.length, 0);
|
|
162
|
+
const rowsProcessed = processed;
|
|
163
|
+
heartbeat.tick((elapsedSeconds)=>`[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed}/${pendingIds.length} rows processed`);
|
|
164
|
+
}
|
|
90
165
|
};
|
|
91
166
|
// Migrate document ids for tables that don't have localizations
|
|
92
167
|
const migrationDocumentIds = async (db, knex, meta, heartbeat)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"5.0.0-02-document-id.js","sources":["../../../src/migrations/internal-migrations/5.0.0-02-document-id.ts"],"sourcesContent":["/**\n * NOTE: This migration avoids using the `identifiers` utility.\n * As the `5.0.0-01-convert-identifiers-long-than-max-length`\n * migration does not convert the `localizations` join tables, as they are not\n * tables that exist anymore in v5 and are not in the db metadata.\n *\n * This migration therefore relies on the fact that those tables still exist, and\n * references them directly.\n *\n * Database join table name: `categories_localizations_links`\n * Actual `identifiers` returned join table name: `categories_localizations_lnk`\n *\n */\nimport { createId } from '@paralleldrive/cuid2';\nimport { snakeCase } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport type { Migration } from '../common';\nimport { createHeartbeatLogger, type HeartbeatLogger } from '../heartbeat';\nimport type { Database } from '../..';\nimport type { Meta } from '../../metadata';\n\nfunction getBatchSize(trx: Knex, defaultSize: number = 1000): number {\n const client = trx.client.config.client;\n const isSQLite =\n typeof client === 'string' && ['sqlite', 'sqlite3', 'better-sqlite3'].includes(client);\n return isSQLite ? Math.min(defaultSize, 250) : defaultSize;\n}\n\ninterface Params {\n joinColumn: string;\n inverseJoinColumn: string;\n tableName: string;\n joinTableName: string;\n}\n\nconst QUERIES = {\n async postgres(knex: Knex, params: Params) {\n const res = await knex.raw(\n `\n SELECT :tableName:.id as id, string_agg(DISTINCT :inverseJoinColumn:::character varying, ',') as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :tableName:.id, :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n\n return res.rows;\n },\n async mysql(knex: Knex, params: Params) {\n const [res] = await knex.raw(\n `\n SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :tableName:.id, :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n\n return res;\n },\n async sqlite(knex: Knex, params: Params) {\n return knex.raw(\n `\n SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n },\n};\n\nconst getNextIdsToCreateDocumentId = async (\n db: Database,\n knex: Knex,\n {\n joinColumn,\n inverseJoinColumn,\n tableName,\n joinTableName,\n }: {\n joinColumn: string;\n inverseJoinColumn: string;\n tableName: string;\n joinTableName: string;\n }\n): Promise<number[]> => {\n const res = await QUERIES[db.dialect.client as keyof typeof QUERIES](knex, {\n joinColumn,\n inverseJoinColumn,\n tableName,\n joinTableName,\n });\n\n if (res.length > 0) {\n const row = res[0];\n const otherIds = row.other_ids\n ? row.other_ids.split(',').map((v: string) => parseInt(v, 10))\n : [];\n\n return [row.id, ...otherIds];\n }\n\n return [];\n};\n\n// Migrate document ids for tables that have localizations\nconst migrateDocumentIdsWithLocalizations = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const singularName = meta.singularName.toLowerCase();\n const joinColumn = snakeCase(`${singularName}_id`);\n const inverseJoinColumn = snakeCase(`inv_${singularName}_id`);\n let ids: number[];\n let processed = 0;\n\n do {\n ids = await getNextIdsToCreateDocumentId(db, knex, {\n joinColumn,\n inverseJoinColumn,\n tableName: meta.tableName,\n joinTableName: snakeCase(`${meta.tableName}_localizations_links`),\n });\n\n if (ids.length > 0) {\n await knex(meta.tableName).update({ document_id: createId() }).whereIn('id', ids);\n processed += ids.length;\n const rowsProcessed = processed;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed} rows processed`\n );\n }\n } while (ids.length > 0);\n};\n\n// Migrate document ids for tables that don't have localizations\nconst migrationDocumentIds = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const batchSize = getBatchSize(knex);\n const total = +(await knex(meta.tableName).count('* as recordsLeft').whereNull('document_id'))[0]\n .recordsLeft;\n let recordsLeft = total;\n while (recordsLeft > 0) {\n const currentBatchSize = recordsLeft < batchSize ? recordsLeft : batchSize;\n const updateRecords = (\n await knex(meta.tableName).select('id').whereNull('document_id').limit(currentBatchSize)\n ).map((item) => ({ id: item.id, document_id: createId() }));\n await knex(meta.tableName).insert(updateRecords).onConflict('id').merge();\n recordsLeft -= updateRecords.length;\n const processed = total - recordsLeft;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${processed}/${total}`\n );\n }\n};\n\nconst isDuplicateColumnError = (error: unknown): boolean => {\n if (!error || typeof error !== 'object') return false;\n const e = error as { code?: string; errno?: number; message?: string };\n if (e.code === '42701') return true;\n if (e.errno === 1060) return true;\n if (typeof e.message === 'string' && /duplicate column/i.test(e.message)) return true;\n return false;\n};\n\nconst createDocumentIdColumn = async (knex: Knex, tableName: string) => {\n try {\n await knex.schema.alterTable(tableName, (table) => {\n table.string('document_id');\n });\n } catch (error) {\n if (!isDuplicateColumnError(error)) {\n throw error;\n }\n }\n};\n\nconst hasLocalizationsJoinTable = async (knex: Knex, tableName: string) => {\n const joinTableName = snakeCase(`${tableName}_localizations_links`);\n return knex.schema.hasTable(joinTableName);\n};\n\nexport const createdDocumentId: Migration = {\n name: '5.0.0-02-created-document-id',\n async up(knex, db) {\n const heartbeat = createHeartbeatLogger((message) => {\n db.logger.info(message);\n });\n\n for (const meta of db.metadata.values()) {\n const hasTable = await knex.schema.hasTable(meta.tableName);\n\n if (!hasTable) {\n continue;\n }\n\n if ('documentId' in meta.attributes) {\n const hasDocumentIdColumn = await knex.schema.hasColumn(meta.tableName, 'document_id');\n\n if (!hasDocumentIdColumn) {\n await createDocumentIdColumn(knex, meta.tableName);\n }\n\n if (await hasLocalizationsJoinTable(knex, meta.tableName)) {\n await migrateDocumentIdsWithLocalizations(db, knex, meta, heartbeat);\n } else {\n await migrationDocumentIds(db, knex, meta, heartbeat);\n }\n }\n }\n },\n async down() {\n throw new Error('not implemented');\n },\n};\n"],"names":["getBatchSize","trx","defaultSize","client","config","isSQLite","includes","Math","min","QUERIES","postgres","knex","params","res","raw","rows","mysql","sqlite","getNextIdsToCreateDocumentId","db","joinColumn","inverseJoinColumn","tableName","joinTableName","dialect","length","row","otherIds","other_ids","split","map","v","parseInt","id","migrateDocumentIdsWithLocalizations","meta","heartbeat","singularName","toLowerCase","snakeCase","ids","processed","update","document_id","createId","whereIn","rowsProcessed","tick","elapsedSeconds","migrationDocumentIds","batchSize","total","count","whereNull","recordsLeft","currentBatchSize","updateRecords","select","limit","item","insert","onConflict","merge","isDuplicateColumnError","error","e","code","errno","message","test","createDocumentIdColumn","schema","alterTable","table","string","hasLocalizationsJoinTable","hasTable","createdDocumentId","name","up","createHeartbeatLogger","logger","info","metadata","values","attributes","hasDocumentIdColumn","hasColumn","down","Error"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"5.0.0-02-document-id.js","sources":["../../../src/migrations/internal-migrations/5.0.0-02-document-id.ts"],"sourcesContent":["/**\n * NOTE: This migration avoids using the `identifiers` utility.\n * As the `5.0.0-01-convert-identifiers-long-than-max-length`\n * migration does not convert the `localizations` join tables, as they are not\n * tables that exist anymore in v5 and are not in the db metadata.\n *\n * This migration therefore relies on the fact that those tables still exist, and\n * references them directly.\n *\n * Database join table name: `categories_localizations_links`\n * Actual `identifiers` returned join table name: `categories_localizations_lnk`\n *\n */\nimport { createId } from '@paralleldrive/cuid2';\nimport { snakeCase } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport type { Migration } from '../common';\nimport { createHeartbeatLogger, type HeartbeatLogger } from '../heartbeat';\nimport type { Database } from '../..';\nimport type { Meta } from '../../metadata';\n\nfunction getBatchSize(trx: Knex, defaultSize: number = 1000): number {\n const client = trx.client.config.client;\n const isSQLite =\n typeof client === 'string' && ['sqlite', 'sqlite3', 'better-sqlite3'].includes(client);\n return isSQLite ? Math.min(defaultSize, 250) : defaultSize;\n}\n\n// Union-find (disjoint-set) helpers used to cluster localization-linked rows in memory.\n// Path-compressing `find` keeps repeated lookups close to O(1) amortized.\nconst find = (parent: Map<number, number>, start: number): number => {\n let current = start;\n while (parent.get(current) !== current) {\n parent.set(current, parent.get(parent.get(current) as number) as number);\n current = parent.get(current) as number;\n }\n return current;\n};\n\nconst union = (parent: Map<number, number>, a: number, b: number): void => {\n const rootA = find(parent, a);\n const rootB = find(parent, b);\n if (rootA !== rootB) {\n parent.set(rootA, rootB);\n }\n};\n\n// Migrate document ids for tables that have localizations\n//\n// This clusters rows in memory (via union-find) instead of discovering one cluster at a\n// time through a `LIMIT 1` query. The previous implementation re-ran a query that scans\n// the whole table (filtered by `document_id IS NULL`, a column with no index at this point\n// in the migration) once per cluster, which is roughly O(n) discovery queries each doing\n// an O(n) scan — effectively O(n^2) for a table with n rows. For tables with many rows\n// and/or many locales this could take hours or longer without making visible progress.\n// See https://github.com/strapi/strapi/issues/23812, 23517, 25231.\n//\n// This also fixes a correctness gap: the original query only follows *direct* links\n// (one hop), so a chain of locale links (A<->B, B<->C, but no direct A<->C row) could be\n// split across two document ids instead of clustered together. Union-find naturally\n// follows transitive links. See https://github.com/strapi/strapi/issues/20948, 24544.\n//\n// Retry-safety: a previous run can crash between clusters, leaving some rows in a\n// locale group already assigned a document_id while their siblings are still NULL. On\n// retry those NULL rows must adopt the sibling's existing document_id instead of minting\n// a fresh one, or a document that used to be one splits into two. To support this, the\n// link fetch and the clustering below include already-migrated neighbor rows, not just\n// pending ones — but only pending (NULL) rows are ever written.\nconst migrateDocumentIdsWithLocalizations = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const singularName = meta.singularName.toLowerCase();\n const joinColumn = snakeCase(`${singularName}_id`);\n const inverseJoinColumn = snakeCase(`inv_${singularName}_id`);\n const joinTableName = snakeCase(`${meta.tableName}_localizations_links`);\n const batchSize = getBatchSize(knex);\n\n const pendingIds: number[] = (\n await knex(meta.tableName).select('id').whereNull('document_id')\n ).map((row: { id: number }) => row.id);\n\n if (pendingIds.length === 0) {\n return;\n }\n\n const pendingIdSet = new Set(pendingIds);\n\n // Fetch every link touching a pending id, on *either* side of the join — a pending\n // row's sibling may already be migrated, in which case it appears as the other column.\n // Batched because passing all ids in a single `whereIn` can exceed the database\n // driver's bound-parameter limit on large tables.\n const links: Array<Record<string, number>> = [];\n for (let i = 0; i < pendingIds.length; i += batchSize) {\n const idChunk = pendingIds.slice(i, i + batchSize);\n const chunkLinks = await knex(joinTableName)\n .select(joinColumn, inverseJoinColumn)\n .whereIn(joinColumn, idChunk)\n .orWhereIn(inverseJoinColumn, idChunk);\n links.push(...chunkLinks);\n }\n\n // Ids referenced by a link that aren't pending are already-migrated neighbors — look up\n // their document_id so a retry can adopt it instead of minting a new one.\n const neighborIds = new Set<number>();\n for (const link of links) {\n const a = link[joinColumn];\n const b = link[inverseJoinColumn];\n if (!pendingIdSet.has(a)) neighborIds.add(a);\n if (!pendingIdSet.has(b)) neighborIds.add(b);\n }\n\n const existingDocumentIds = new Map<number, string>();\n const neighborIdList = [...neighborIds];\n for (let i = 0; i < neighborIdList.length; i += batchSize) {\n const idChunk = neighborIdList.slice(i, i + batchSize);\n const rows: Array<{ id: number; document_id: string | null }> = await knex(meta.tableName)\n .select('id', 'document_id')\n .whereIn('id', idChunk);\n for (const row of rows) {\n if (row.document_id) {\n existingDocumentIds.set(row.id, row.document_id);\n }\n }\n }\n\n const parent = new Map<number, number>();\n for (const id of pendingIds) {\n parent.set(id, id);\n }\n for (const id of neighborIds) {\n parent.set(id, id);\n }\n for (const link of links) {\n const a = link[joinColumn];\n const b = link[inverseJoinColumn];\n if (parent.has(a) && parent.has(b)) {\n union(parent, a, b);\n }\n }\n\n const clusters = new Map<number, number[]>();\n for (const id of parent.keys()) {\n const root = find(parent, id);\n const cluster = clusters.get(root);\n if (cluster) {\n cluster.push(id);\n } else {\n clusters.set(root, [id]);\n }\n }\n\n // Group pending writes by target document_id — one write per cluster instead of one\n // per row shrinks the window for a crash to leave a cluster half-written.\n const writeGroups = new Map<string, number[]>();\n for (const members of clusters.values()) {\n const existingIdsInCluster = new Set<string>();\n for (const id of members) {\n const existing = existingDocumentIds.get(id);\n if (existing) {\n existingIdsInCluster.add(existing);\n }\n }\n\n // A cluster normally carries at most one existing document_id. More than one means an\n // earlier inconsistent partial write — unify onto a single deterministic id rather\n // than minting a third.\n const documentId =\n existingIdsInCluster.size > 0 ? [...existingIdsInCluster].sort()[0] : createId();\n\n const pendingMembers = members.filter((id) => pendingIdSet.has(id));\n if (pendingMembers.length > 0) {\n // Two distinct clusters resolving to the same document_id is only possible via a\n // cuid collision, but merge rather than overwrite so neither group's writes vanish.\n const existingGroup = writeGroups.get(documentId);\n if (existingGroup) {\n existingGroup.push(...pendingMembers);\n } else {\n writeGroups.set(documentId, pendingMembers);\n }\n }\n }\n\n const groupEntries = [...writeGroups.entries()];\n let processed = 0;\n for (let i = 0; i < groupEntries.length; i += batchSize) {\n const slice = groupEntries.slice(i, i + batchSize);\n await Promise.all(\n slice.map(([documentId, ids]) =>\n knex(meta.tableName).whereIn('id', ids).update({ document_id: documentId })\n )\n );\n processed += slice.reduce((sum, [, ids]) => sum + ids.length, 0);\n const rowsProcessed = processed;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed}/${pendingIds.length} rows processed`\n );\n }\n};\n\n// Migrate document ids for tables that don't have localizations\nconst migrationDocumentIds = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const batchSize = getBatchSize(knex);\n const total = +(await knex(meta.tableName).count('* as recordsLeft').whereNull('document_id'))[0]\n .recordsLeft;\n let recordsLeft = total;\n while (recordsLeft > 0) {\n const currentBatchSize = recordsLeft < batchSize ? recordsLeft : batchSize;\n const updateRecords = (\n await knex(meta.tableName).select('id').whereNull('document_id').limit(currentBatchSize)\n ).map((item) => ({ id: item.id, document_id: createId() }));\n await knex(meta.tableName).insert(updateRecords).onConflict('id').merge();\n recordsLeft -= updateRecords.length;\n const processed = total - recordsLeft;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${processed}/${total}`\n );\n }\n};\n\nconst isDuplicateColumnError = (error: unknown): boolean => {\n if (!error || typeof error !== 'object') return false;\n const e = error as { code?: string; errno?: number; message?: string };\n if (e.code === '42701') return true;\n if (e.errno === 1060) return true;\n if (typeof e.message === 'string' && /duplicate column/i.test(e.message)) return true;\n return false;\n};\n\nconst createDocumentIdColumn = async (knex: Knex, tableName: string) => {\n try {\n await knex.schema.alterTable(tableName, (table) => {\n table.string('document_id');\n });\n } catch (error) {\n if (!isDuplicateColumnError(error)) {\n throw error;\n }\n }\n};\n\nconst hasLocalizationsJoinTable = async (knex: Knex, tableName: string) => {\n const joinTableName = snakeCase(`${tableName}_localizations_links`);\n return knex.schema.hasTable(joinTableName);\n};\n\nexport const createdDocumentId: Migration = {\n name: '5.0.0-02-created-document-id',\n async up(knex, db) {\n const heartbeat = createHeartbeatLogger((message) => {\n db.logger.info(message);\n });\n\n for (const meta of db.metadata.values()) {\n const hasTable = await knex.schema.hasTable(meta.tableName);\n\n if (!hasTable) {\n continue;\n }\n\n if ('documentId' in meta.attributes) {\n const hasDocumentIdColumn = await knex.schema.hasColumn(meta.tableName, 'document_id');\n\n if (!hasDocumentIdColumn) {\n await createDocumentIdColumn(knex, meta.tableName);\n }\n\n if (await hasLocalizationsJoinTable(knex, meta.tableName)) {\n await migrateDocumentIdsWithLocalizations(db, knex, meta, heartbeat);\n } else {\n await migrationDocumentIds(db, knex, meta, heartbeat);\n }\n }\n }\n },\n async down() {\n throw new Error('not implemented');\n },\n};\n"],"names":["getBatchSize","trx","defaultSize","client","config","isSQLite","includes","Math","min","find","parent","start","current","get","set","union","a","b","rootA","rootB","migrateDocumentIdsWithLocalizations","db","knex","meta","heartbeat","singularName","toLowerCase","joinColumn","snakeCase","inverseJoinColumn","joinTableName","tableName","batchSize","pendingIds","select","whereNull","map","row","id","length","pendingIdSet","Set","links","i","idChunk","slice","chunkLinks","whereIn","orWhereIn","push","neighborIds","link","has","add","existingDocumentIds","Map","neighborIdList","rows","document_id","clusters","keys","root","cluster","writeGroups","members","values","existingIdsInCluster","existing","documentId","size","sort","createId","pendingMembers","filter","existingGroup","groupEntries","entries","processed","Promise","all","ids","update","reduce","sum","rowsProcessed","tick","elapsedSeconds","migrationDocumentIds","total","count","recordsLeft","currentBatchSize","updateRecords","limit","item","insert","onConflict","merge","isDuplicateColumnError","error","e","code","errno","message","test","createDocumentIdColumn","schema","alterTable","table","string","hasLocalizationsJoinTable","hasTable","createdDocumentId","name","up","createHeartbeatLogger","logger","info","metadata","attributes","hasDocumentIdColumn","hasColumn","down","Error"],"mappings":"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"}
|
|
@@ -11,80 +11,155 @@ function getBatchSize(trx, defaultSize = 1000) {
|
|
|
11
11
|
].includes(client);
|
|
12
12
|
return isSQLite ? Math.min(defaultSize, 250) : defaultSize;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
GROUP BY :tableName:.id, :joinTableName:.:joinColumn:
|
|
22
|
-
LIMIT 1;
|
|
23
|
-
`, params);
|
|
24
|
-
return res.rows;
|
|
25
|
-
},
|
|
26
|
-
async mysql (knex, params) {
|
|
27
|
-
const [res] = await knex.raw(`
|
|
28
|
-
SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
|
|
29
|
-
FROM :tableName:
|
|
30
|
-
LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
|
|
31
|
-
WHERE :tableName:.document_id IS NULL
|
|
32
|
-
GROUP BY :tableName:.id, :joinTableName:.:joinColumn:
|
|
33
|
-
LIMIT 1;
|
|
34
|
-
`, params);
|
|
35
|
-
return res;
|
|
36
|
-
},
|
|
37
|
-
async sqlite (knex, params) {
|
|
38
|
-
return knex.raw(`
|
|
39
|
-
SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids
|
|
40
|
-
FROM :tableName:
|
|
41
|
-
LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:
|
|
42
|
-
WHERE :tableName:.document_id IS NULL
|
|
43
|
-
GROUP BY :joinTableName:.:joinColumn:
|
|
44
|
-
LIMIT 1;
|
|
45
|
-
`, params);
|
|
14
|
+
// Union-find (disjoint-set) helpers used to cluster localization-linked rows in memory.
|
|
15
|
+
// Path-compressing `find` keeps repeated lookups close to O(1) amortized.
|
|
16
|
+
const find = (parent, start)=>{
|
|
17
|
+
let current = start;
|
|
18
|
+
while(parent.get(current) !== current){
|
|
19
|
+
parent.set(current, parent.get(parent.get(current)));
|
|
20
|
+
current = parent.get(current);
|
|
46
21
|
}
|
|
22
|
+
return current;
|
|
47
23
|
};
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
joinTableName
|
|
54
|
-
});
|
|
55
|
-
if (res.length > 0) {
|
|
56
|
-
const row = res[0];
|
|
57
|
-
const otherIds = row.other_ids ? row.other_ids.split(',').map((v)=>parseInt(v, 10)) : [];
|
|
58
|
-
return [
|
|
59
|
-
row.id,
|
|
60
|
-
...otherIds
|
|
61
|
-
];
|
|
24
|
+
const union = (parent, a, b)=>{
|
|
25
|
+
const rootA = find(parent, a);
|
|
26
|
+
const rootB = find(parent, b);
|
|
27
|
+
if (rootA !== rootB) {
|
|
28
|
+
parent.set(rootA, rootB);
|
|
62
29
|
}
|
|
63
|
-
return [];
|
|
64
30
|
};
|
|
65
31
|
// Migrate document ids for tables that have localizations
|
|
32
|
+
//
|
|
33
|
+
// This clusters rows in memory (via union-find) instead of discovering one cluster at a
|
|
34
|
+
// time through a `LIMIT 1` query. The previous implementation re-ran a query that scans
|
|
35
|
+
// the whole table (filtered by `document_id IS NULL`, a column with no index at this point
|
|
36
|
+
// in the migration) once per cluster, which is roughly O(n) discovery queries each doing
|
|
37
|
+
// an O(n) scan — effectively O(n^2) for a table with n rows. For tables with many rows
|
|
38
|
+
// and/or many locales this could take hours or longer without making visible progress.
|
|
39
|
+
// See https://github.com/strapi/strapi/issues/23812, 23517, 25231.
|
|
40
|
+
//
|
|
41
|
+
// This also fixes a correctness gap: the original query only follows *direct* links
|
|
42
|
+
// (one hop), so a chain of locale links (A<->B, B<->C, but no direct A<->C row) could be
|
|
43
|
+
// split across two document ids instead of clustered together. Union-find naturally
|
|
44
|
+
// follows transitive links. See https://github.com/strapi/strapi/issues/20948, 24544.
|
|
45
|
+
//
|
|
46
|
+
// Retry-safety: a previous run can crash between clusters, leaving some rows in a
|
|
47
|
+
// locale group already assigned a document_id while their siblings are still NULL. On
|
|
48
|
+
// retry those NULL rows must adopt the sibling's existing document_id instead of minting
|
|
49
|
+
// a fresh one, or a document that used to be one splits into two. To support this, the
|
|
50
|
+
// link fetch and the clustering below include already-migrated neighbor rows, not just
|
|
51
|
+
// pending ones — but only pending (NULL) rows are ever written.
|
|
66
52
|
const migrateDocumentIdsWithLocalizations = async (db, knex, meta, heartbeat)=>{
|
|
67
53
|
const singularName = meta.singularName.toLowerCase();
|
|
68
54
|
const joinColumn = snakeCase(`${singularName}_id`);
|
|
69
55
|
const inverseJoinColumn = snakeCase(`inv_${singularName}_id`);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
56
|
+
const joinTableName = snakeCase(`${meta.tableName}_localizations_links`);
|
|
57
|
+
const batchSize = getBatchSize(knex);
|
|
58
|
+
const pendingIds = (await knex(meta.tableName).select('id').whereNull('document_id')).map((row)=>row.id);
|
|
59
|
+
if (pendingIds.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const pendingIdSet = new Set(pendingIds);
|
|
63
|
+
// Fetch every link touching a pending id, on *either* side of the join — a pending
|
|
64
|
+
// row's sibling may already be migrated, in which case it appears as the other column.
|
|
65
|
+
// Batched because passing all ids in a single `whereIn` can exceed the database
|
|
66
|
+
// driver's bound-parameter limit on large tables.
|
|
67
|
+
const links = [];
|
|
68
|
+
for(let i = 0; i < pendingIds.length; i += batchSize){
|
|
69
|
+
const idChunk = pendingIds.slice(i, i + batchSize);
|
|
70
|
+
const chunkLinks = await knex(joinTableName).select(joinColumn, inverseJoinColumn).whereIn(joinColumn, idChunk).orWhereIn(inverseJoinColumn, idChunk);
|
|
71
|
+
links.push(...chunkLinks);
|
|
72
|
+
}
|
|
73
|
+
// Ids referenced by a link that aren't pending are already-migrated neighbors — look up
|
|
74
|
+
// their document_id so a retry can adopt it instead of minting a new one.
|
|
75
|
+
const neighborIds = new Set();
|
|
76
|
+
for (const link of links){
|
|
77
|
+
const a = link[joinColumn];
|
|
78
|
+
const b = link[inverseJoinColumn];
|
|
79
|
+
if (!pendingIdSet.has(a)) neighborIds.add(a);
|
|
80
|
+
if (!pendingIdSet.has(b)) neighborIds.add(b);
|
|
81
|
+
}
|
|
82
|
+
const existingDocumentIds = new Map();
|
|
83
|
+
const neighborIdList = [
|
|
84
|
+
...neighborIds
|
|
85
|
+
];
|
|
86
|
+
for(let i = 0; i < neighborIdList.length; i += batchSize){
|
|
87
|
+
const idChunk = neighborIdList.slice(i, i + batchSize);
|
|
88
|
+
const rows = await knex(meta.tableName).select('id', 'document_id').whereIn('id', idChunk);
|
|
89
|
+
for (const row of rows){
|
|
90
|
+
if (row.document_id) {
|
|
91
|
+
existingDocumentIds.set(row.id, row.document_id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const parent = new Map();
|
|
96
|
+
for (const id of pendingIds){
|
|
97
|
+
parent.set(id, id);
|
|
98
|
+
}
|
|
99
|
+
for (const id of neighborIds){
|
|
100
|
+
parent.set(id, id);
|
|
101
|
+
}
|
|
102
|
+
for (const link of links){
|
|
103
|
+
const a = link[joinColumn];
|
|
104
|
+
const b = link[inverseJoinColumn];
|
|
105
|
+
if (parent.has(a) && parent.has(b)) {
|
|
106
|
+
union(parent, a, b);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const clusters = new Map();
|
|
110
|
+
for (const id of parent.keys()){
|
|
111
|
+
const root = find(parent, id);
|
|
112
|
+
const cluster = clusters.get(root);
|
|
113
|
+
if (cluster) {
|
|
114
|
+
cluster.push(id);
|
|
115
|
+
} else {
|
|
116
|
+
clusters.set(root, [
|
|
117
|
+
id
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Group pending writes by target document_id — one write per cluster instead of one
|
|
122
|
+
// per row shrinks the window for a crash to leave a cluster half-written.
|
|
123
|
+
const writeGroups = new Map();
|
|
124
|
+
for (const members of clusters.values()){
|
|
125
|
+
const existingIdsInCluster = new Set();
|
|
126
|
+
for (const id of members){
|
|
127
|
+
const existing = existingDocumentIds.get(id);
|
|
128
|
+
if (existing) {
|
|
129
|
+
existingIdsInCluster.add(existing);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// A cluster normally carries at most one existing document_id. More than one means an
|
|
133
|
+
// earlier inconsistent partial write — unify onto a single deterministic id rather
|
|
134
|
+
// than minting a third.
|
|
135
|
+
const documentId = existingIdsInCluster.size > 0 ? [
|
|
136
|
+
...existingIdsInCluster
|
|
137
|
+
].sort()[0] : createId();
|
|
138
|
+
const pendingMembers = members.filter((id)=>pendingIdSet.has(id));
|
|
139
|
+
if (pendingMembers.length > 0) {
|
|
140
|
+
// Two distinct clusters resolving to the same document_id is only possible via a
|
|
141
|
+
// cuid collision, but merge rather than overwrite so neither group's writes vanish.
|
|
142
|
+
const existingGroup = writeGroups.get(documentId);
|
|
143
|
+
if (existingGroup) {
|
|
144
|
+
existingGroup.push(...pendingMembers);
|
|
145
|
+
} else {
|
|
146
|
+
writeGroups.set(documentId, pendingMembers);
|
|
147
|
+
}
|
|
86
148
|
}
|
|
87
|
-
}
|
|
149
|
+
}
|
|
150
|
+
const groupEntries = [
|
|
151
|
+
...writeGroups.entries()
|
|
152
|
+
];
|
|
153
|
+
let processed = 0;
|
|
154
|
+
for(let i = 0; i < groupEntries.length; i += batchSize){
|
|
155
|
+
const slice = groupEntries.slice(i, i + batchSize);
|
|
156
|
+
await Promise.all(slice.map(([documentId, ids])=>knex(meta.tableName).whereIn('id', ids).update({
|
|
157
|
+
document_id: documentId
|
|
158
|
+
})));
|
|
159
|
+
processed += slice.reduce((sum, [, ids])=>sum + ids.length, 0);
|
|
160
|
+
const rowsProcessed = processed;
|
|
161
|
+
heartbeat.tick((elapsedSeconds)=>`[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed}/${pendingIds.length} rows processed`);
|
|
162
|
+
}
|
|
88
163
|
};
|
|
89
164
|
// Migrate document ids for tables that don't have localizations
|
|
90
165
|
const migrationDocumentIds = async (db, knex, meta, heartbeat)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"5.0.0-02-document-id.mjs","sources":["../../../src/migrations/internal-migrations/5.0.0-02-document-id.ts"],"sourcesContent":["/**\n * NOTE: This migration avoids using the `identifiers` utility.\n * As the `5.0.0-01-convert-identifiers-long-than-max-length`\n * migration does not convert the `localizations` join tables, as they are not\n * tables that exist anymore in v5 and are not in the db metadata.\n *\n * This migration therefore relies on the fact that those tables still exist, and\n * references them directly.\n *\n * Database join table name: `categories_localizations_links`\n * Actual `identifiers` returned join table name: `categories_localizations_lnk`\n *\n */\nimport { createId } from '@paralleldrive/cuid2';\nimport { snakeCase } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport type { Migration } from '../common';\nimport { createHeartbeatLogger, type HeartbeatLogger } from '../heartbeat';\nimport type { Database } from '../..';\nimport type { Meta } from '../../metadata';\n\nfunction getBatchSize(trx: Knex, defaultSize: number = 1000): number {\n const client = trx.client.config.client;\n const isSQLite =\n typeof client === 'string' && ['sqlite', 'sqlite3', 'better-sqlite3'].includes(client);\n return isSQLite ? Math.min(defaultSize, 250) : defaultSize;\n}\n\ninterface Params {\n joinColumn: string;\n inverseJoinColumn: string;\n tableName: string;\n joinTableName: string;\n}\n\nconst QUERIES = {\n async postgres(knex: Knex, params: Params) {\n const res = await knex.raw(\n `\n SELECT :tableName:.id as id, string_agg(DISTINCT :inverseJoinColumn:::character varying, ',') as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :tableName:.id, :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n\n return res.rows;\n },\n async mysql(knex: Knex, params: Params) {\n const [res] = await knex.raw(\n `\n SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :tableName:.id, :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n\n return res;\n },\n async sqlite(knex: Knex, params: Params) {\n return knex.raw(\n `\n SELECT :tableName:.id as id, group_concat(DISTINCT :inverseJoinColumn:) as other_ids\n FROM :tableName:\n LEFT JOIN :joinTableName: ON :tableName:.id = :joinTableName:.:joinColumn:\n WHERE :tableName:.document_id IS NULL\n GROUP BY :joinTableName:.:joinColumn:\n LIMIT 1;\n `,\n params\n );\n },\n};\n\nconst getNextIdsToCreateDocumentId = async (\n db: Database,\n knex: Knex,\n {\n joinColumn,\n inverseJoinColumn,\n tableName,\n joinTableName,\n }: {\n joinColumn: string;\n inverseJoinColumn: string;\n tableName: string;\n joinTableName: string;\n }\n): Promise<number[]> => {\n const res = await QUERIES[db.dialect.client as keyof typeof QUERIES](knex, {\n joinColumn,\n inverseJoinColumn,\n tableName,\n joinTableName,\n });\n\n if (res.length > 0) {\n const row = res[0];\n const otherIds = row.other_ids\n ? row.other_ids.split(',').map((v: string) => parseInt(v, 10))\n : [];\n\n return [row.id, ...otherIds];\n }\n\n return [];\n};\n\n// Migrate document ids for tables that have localizations\nconst migrateDocumentIdsWithLocalizations = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const singularName = meta.singularName.toLowerCase();\n const joinColumn = snakeCase(`${singularName}_id`);\n const inverseJoinColumn = snakeCase(`inv_${singularName}_id`);\n let ids: number[];\n let processed = 0;\n\n do {\n ids = await getNextIdsToCreateDocumentId(db, knex, {\n joinColumn,\n inverseJoinColumn,\n tableName: meta.tableName,\n joinTableName: snakeCase(`${meta.tableName}_localizations_links`),\n });\n\n if (ids.length > 0) {\n await knex(meta.tableName).update({ document_id: createId() }).whereIn('id', ids);\n processed += ids.length;\n const rowsProcessed = processed;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed} rows processed`\n );\n }\n } while (ids.length > 0);\n};\n\n// Migrate document ids for tables that don't have localizations\nconst migrationDocumentIds = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const batchSize = getBatchSize(knex);\n const total = +(await knex(meta.tableName).count('* as recordsLeft').whereNull('document_id'))[0]\n .recordsLeft;\n let recordsLeft = total;\n while (recordsLeft > 0) {\n const currentBatchSize = recordsLeft < batchSize ? recordsLeft : batchSize;\n const updateRecords = (\n await knex(meta.tableName).select('id').whereNull('document_id').limit(currentBatchSize)\n ).map((item) => ({ id: item.id, document_id: createId() }));\n await knex(meta.tableName).insert(updateRecords).onConflict('id').merge();\n recordsLeft -= updateRecords.length;\n const processed = total - recordsLeft;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${processed}/${total}`\n );\n }\n};\n\nconst isDuplicateColumnError = (error: unknown): boolean => {\n if (!error || typeof error !== 'object') return false;\n const e = error as { code?: string; errno?: number; message?: string };\n if (e.code === '42701') return true;\n if (e.errno === 1060) return true;\n if (typeof e.message === 'string' && /duplicate column/i.test(e.message)) return true;\n return false;\n};\n\nconst createDocumentIdColumn = async (knex: Knex, tableName: string) => {\n try {\n await knex.schema.alterTable(tableName, (table) => {\n table.string('document_id');\n });\n } catch (error) {\n if (!isDuplicateColumnError(error)) {\n throw error;\n }\n }\n};\n\nconst hasLocalizationsJoinTable = async (knex: Knex, tableName: string) => {\n const joinTableName = snakeCase(`${tableName}_localizations_links`);\n return knex.schema.hasTable(joinTableName);\n};\n\nexport const createdDocumentId: Migration = {\n name: '5.0.0-02-created-document-id',\n async up(knex, db) {\n const heartbeat = createHeartbeatLogger((message) => {\n db.logger.info(message);\n });\n\n for (const meta of db.metadata.values()) {\n const hasTable = await knex.schema.hasTable(meta.tableName);\n\n if (!hasTable) {\n continue;\n }\n\n if ('documentId' in meta.attributes) {\n const hasDocumentIdColumn = await knex.schema.hasColumn(meta.tableName, 'document_id');\n\n if (!hasDocumentIdColumn) {\n await createDocumentIdColumn(knex, meta.tableName);\n }\n\n if (await hasLocalizationsJoinTable(knex, meta.tableName)) {\n await migrateDocumentIdsWithLocalizations(db, knex, meta, heartbeat);\n } else {\n await migrationDocumentIds(db, knex, meta, heartbeat);\n }\n }\n }\n },\n async down() {\n throw new Error('not implemented');\n },\n};\n"],"names":["getBatchSize","trx","defaultSize","client","config","isSQLite","includes","Math","min","QUERIES","postgres","knex","params","res","raw","rows","mysql","sqlite","getNextIdsToCreateDocumentId","db","joinColumn","inverseJoinColumn","tableName","joinTableName","dialect","length","row","otherIds","other_ids","split","map","v","parseInt","id","migrateDocumentIdsWithLocalizations","meta","heartbeat","singularName","toLowerCase","snakeCase","ids","processed","update","document_id","createId","whereIn","rowsProcessed","tick","elapsedSeconds","migrationDocumentIds","batchSize","total","count","whereNull","recordsLeft","currentBatchSize","updateRecords","select","limit","item","insert","onConflict","merge","isDuplicateColumnError","error","e","code","errno","message","test","createDocumentIdColumn","schema","alterTable","table","string","hasLocalizationsJoinTable","hasTable","createdDocumentId","name","up","createHeartbeatLogger","logger","info","metadata","values","attributes","hasDocumentIdColumn","hasColumn","down","Error"],"mappings":";;;;AAsBA,SAASA,YAAAA,CAAaC,GAAS,EAAEC,WAAAA,GAAsB,IAAI,EAAA;AACzD,IAAA,MAAMC,SAASF,GAAAA,CAAIE,MAAM,CAACC,MAAM,CAACD,MAAM;IACvC,MAAME,QAAAA,GACJ,OAAOF,MAAAA,KAAW,QAAA,IAAY;AAAC,QAAA,QAAA;AAAU,QAAA,SAAA;AAAW,QAAA;AAAiB,KAAA,CAACG,QAAQ,CAACH,MAAAA,CAAAA;AACjF,IAAA,OAAOE,QAAAA,GAAWE,IAAAA,CAAKC,GAAG,CAACN,aAAa,GAAA,CAAA,GAAOA,WAAAA;AACjD;AASA,MAAMO,OAAAA,GAAU;IACd,MAAMC,QAAAA,CAAAA,CAASC,IAAU,EAAEC,MAAc,EAAA;AACvC,QAAA,MAAMC,GAAAA,GAAM,MAAMF,IAAAA,CAAKG,GAAG,CACxB;;;;;;;AAOJ,EAAA,CAAC,EACGF,MAAAA,CAAAA;AAGF,QAAA,OAAOC,IAAIE,IAAI;AACjB,IAAA,CAAA;IACA,MAAMC,KAAAA,CAAAA,CAAML,IAAU,EAAEC,MAAc,EAAA;AACpC,QAAA,MAAM,CAACC,GAAAA,CAAI,GAAG,MAAMF,IAAAA,CAAKG,GAAG,CAC1B;;;;;;;AAOJ,EAAA,CAAC,EACGF,MAAAA,CAAAA;QAGF,OAAOC,GAAAA;AACT,IAAA,CAAA;IACA,MAAMI,MAAAA,CAAAA,CAAON,IAAU,EAAEC,MAAc,EAAA;QACrC,OAAOD,IAAAA,CAAKG,GAAG,CACb;;;;;;;AAOF,IAAA,CAAC,EACCF,MAAAA,CAAAA;AAEJ,IAAA;AACF,CAAA;AAEA,MAAMM,4BAAAA,GAA+B,OACnCC,EAAAA,EACAR,IAAAA,EACA,EACES,UAAU,EACVC,iBAAiB,EACjBC,SAAS,EACTC,aAAa,EAMd,GAAA;IAED,MAAMV,GAAAA,GAAM,MAAMJ,OAAO,CAACU,EAAAA,CAAGK,OAAO,CAACrB,MAAM,CAAyB,CAACQ,IAAAA,EAAM;AACzES,QAAAA,UAAAA;AACAC,QAAAA,iBAAAA;AACAC,QAAAA,SAAAA;AACAC,QAAAA;AACF,KAAA,CAAA;IAEA,IAAIV,GAAAA,CAAIY,MAAM,GAAG,CAAA,EAAG;QAClB,MAAMC,GAAAA,GAAMb,GAAG,CAAC,CAAA,CAAE;AAClB,QAAA,MAAMc,WAAWD,GAAAA,CAAIE,SAAS,GAC1BF,GAAAA,CAAIE,SAAS,CAACC,KAAK,CAAC,GAAA,CAAA,CAAKC,GAAG,CAAC,CAACC,IAAcC,QAAAA,CAASD,CAAAA,EAAG,OACxD,EAAE;QAEN,OAAO;AAACL,YAAAA,GAAAA,CAAIO,EAAE;AAAKN,YAAAA,GAAAA;AAAS,SAAA;AAC9B,IAAA;AAEA,IAAA,OAAO,EAAE;AACX,CAAA;AAEA;AACA,MAAMO,mCAAAA,GAAsC,OAC1Cf,EAAAA,EACAR,IAAAA,EACAwB,IAAAA,EACAC,SAAAA,GAAAA;AAEA,IAAA,MAAMC,YAAAA,GAAeF,IAAAA,CAAKE,YAAY,CAACC,WAAW,EAAA;AAClD,IAAA,MAAMlB,UAAAA,GAAamB,SAAAA,CAAU,CAAA,EAAGF,YAAAA,CAAa,GAAG,CAAC,CAAA;AACjD,IAAA,MAAMhB,oBAAoBkB,SAAAA,CAAU,CAAC,IAAI,EAAEF,YAAAA,CAAa,GAAG,CAAC,CAAA;IAC5D,IAAIG,GAAAA;AACJ,IAAA,IAAIC,SAAAA,GAAY,CAAA;IAEhB,GAAG;QACDD,GAAAA,GAAM,MAAMtB,4BAAAA,CAA6BC,EAAAA,EAAIR,IAAAA,EAAM;AACjDS,YAAAA,UAAAA;AACAC,YAAAA,iBAAAA;AACAC,YAAAA,SAAAA,EAAWa,KAAKb,SAAS;AACzBC,YAAAA,aAAAA,EAAegB,UAAU,CAAA,EAAGJ,IAAAA,CAAKb,SAAS,CAAC,oBAAoB,CAAC;AAClE,SAAA,CAAA;QAEA,IAAIkB,GAAAA,CAAIf,MAAM,GAAG,CAAA,EAAG;AAClB,YAAA,MAAMd,IAAAA,CAAKwB,IAAAA,CAAKb,SAAS,CAAA,CAAEoB,MAAM,CAAC;gBAAEC,WAAAA,EAAaC,QAAAA;aAAW,CAAA,CAAGC,OAAO,CAAC,IAAA,EAAML,GAAAA,CAAAA;AAC7EC,YAAAA,SAAAA,IAAaD,IAAIf,MAAM;AACvB,YAAA,MAAMqB,aAAAA,GAAgBL,SAAAA;AACtBL,YAAAA,SAAAA,CAAUW,IAAI,CACZ,CAACC,cAAAA,GACC,CAAC,6BAA6B,EAAEA,cAAAA,CAAe,KAAK,EAAEb,KAAKb,SAAS,CAAC,CAAC,EAAEwB,aAAAA,CAAc,eAAe,CAAC,CAAA;AAE5G,QAAA;IACF,CAAA,OAASN,GAAAA,CAAIf,MAAM,GAAG,CAAA;AACxB,CAAA;AAEA;AACA,MAAMwB,oBAAAA,GAAuB,OAC3B9B,EAAAA,EACAR,IAAAA,EACAwB,IAAAA,EACAC,SAAAA,GAAAA;AAEA,IAAA,MAAMc,YAAYlD,YAAAA,CAAaW,IAAAA,CAAAA;AAC/B,IAAA,MAAMwC,QAAQ,CAAE,CAAA,MAAMxC,IAAAA,CAAKwB,KAAKb,SAAS,CAAA,CAAE8B,KAAK,CAAC,kBAAA,CAAA,CAAoBC,SAAS,CAAC,aAAA,CAAa,EAAG,CAAA,CAAE,CAC9FC,WAAW;AACd,IAAA,IAAIA,WAAAA,GAAcH,KAAAA;AAClB,IAAA,MAAOG,cAAc,CAAA,CAAG;QACtB,MAAMC,gBAAAA,GAAmBD,WAAAA,GAAcJ,SAAAA,GAAYI,WAAAA,GAAcJ,SAAAA;QACjE,MAAMM,aAAAA,GAAgB,CACpB,MAAM7C,KAAKwB,IAAAA,CAAKb,SAAS,CAAA,CAAEmC,MAAM,CAAC,IAAA,CAAA,CAAMJ,SAAS,CAAC,aAAA,CAAA,CAAeK,KAAK,CAACH,gBAAAA,CAAgB,EACvFzB,GAAG,CAAC,CAAC6B,IAAAA,IAAU;AAAE1B,gBAAAA,EAAAA,EAAI0B,KAAK1B,EAAE;gBAAEU,WAAAA,EAAaC,QAAAA;aAAW,CAAA,CAAA;QACxD,MAAMjC,IAAAA,CAAKwB,IAAAA,CAAKb,SAAS,CAAA,CAAEsC,MAAM,CAACJ,aAAAA,CAAAA,CAAeK,UAAU,CAAC,IAAA,CAAA,CAAMC,KAAK,EAAA;AACvER,QAAAA,WAAAA,IAAeE,cAAc/B,MAAM;AACnC,QAAA,MAAMgB,YAAYU,KAAAA,GAAQG,WAAAA;AAC1BlB,QAAAA,SAAAA,CAAUW,IAAI,CACZ,CAACC,iBACC,CAAC,6BAA6B,EAAEA,cAAAA,CAAe,KAAK,EAAEb,IAAAA,CAAKb,SAAS,CAAC,CAAC,EAAEmB,SAAAA,CAAU,CAAC,EAAEU,KAAAA,CAAAA,CAAO,CAAA;AAElG,IAAA;AACF,CAAA;AAEA,MAAMY,yBAAyB,CAACC,KAAAA,GAAAA;AAC9B,IAAA,IAAI,CAACA,KAAAA,IAAS,OAAOA,KAAAA,KAAU,UAAU,OAAO,KAAA;AAChD,IAAA,MAAMC,CAAAA,GAAID,KAAAA;AACV,IAAA,IAAIC,CAAAA,CAAEC,IAAI,KAAK,OAAA,EAAS,OAAO,IAAA;AAC/B,IAAA,IAAID,CAAAA,CAAEE,KAAK,KAAK,IAAA,EAAM,OAAO,IAAA;IAC7B,IAAI,OAAOF,CAAAA,CAAEG,OAAO,KAAK,QAAA,IAAY,mBAAA,CAAoBC,IAAI,CAACJ,CAAAA,CAAEG,OAAO,CAAA,EAAG,OAAO,IAAA;IACjF,OAAO,KAAA;AACT,CAAA;AAEA,MAAME,sBAAAA,GAAyB,OAAO3D,IAAAA,EAAYW,SAAAA,GAAAA;IAChD,IAAI;AACF,QAAA,MAAMX,KAAK4D,MAAM,CAACC,UAAU,CAAClD,WAAW,CAACmD,KAAAA,GAAAA;AACvCA,YAAAA,KAAAA,CAAMC,MAAM,CAAC,aAAA,CAAA;AACf,QAAA,CAAA,CAAA;AACF,IAAA,CAAA,CAAE,OAAOV,KAAAA,EAAO;QACd,IAAI,CAACD,uBAAuBC,KAAAA,CAAAA,EAAQ;YAClC,MAAMA,KAAAA;AACR,QAAA;AACF,IAAA;AACF,CAAA;AAEA,MAAMW,yBAAAA,GAA4B,OAAOhE,IAAAA,EAAYW,SAAAA,GAAAA;AACnD,IAAA,MAAMC,aAAAA,GAAgBgB,SAAAA,CAAU,CAAA,EAAGjB,SAAAA,CAAU,oBAAoB,CAAC,CAAA;AAClE,IAAA,OAAOX,IAAAA,CAAK4D,MAAM,CAACK,QAAQ,CAACrD,aAAAA,CAAAA;AAC9B,CAAA;MAEasD,iBAAAA,GAA+B;IAC1CC,IAAAA,EAAM,8BAAA;IACN,MAAMC,EAAAA,CAAAA,CAAGpE,IAAI,EAAEQ,EAAE,EAAA;QACf,MAAMiB,SAAAA,GAAY4C,sBAAsB,CAACZ,OAAAA,GAAAA;YACvCjD,EAAAA,CAAG8D,MAAM,CAACC,IAAI,CAACd,OAAAA,CAAAA;AACjB,QAAA,CAAA,CAAA;AAEA,QAAA,KAAK,MAAMjC,IAAAA,IAAQhB,EAAAA,CAAGgE,QAAQ,CAACC,MAAM,EAAA,CAAI;YACvC,MAAMR,QAAAA,GAAW,MAAMjE,IAAAA,CAAK4D,MAAM,CAACK,QAAQ,CAACzC,KAAKb,SAAS,CAAA;AAE1D,YAAA,IAAI,CAACsD,QAAAA,EAAU;AACb,gBAAA;AACF,YAAA;YAEA,IAAI,YAAA,IAAgBzC,IAAAA,CAAKkD,UAAU,EAAE;gBACnC,MAAMC,mBAAAA,GAAsB,MAAM3E,IAAAA,CAAK4D,MAAM,CAACgB,SAAS,CAACpD,IAAAA,CAAKb,SAAS,EAAE,aAAA,CAAA;AAExE,gBAAA,IAAI,CAACgE,mBAAAA,EAAqB;oBACxB,MAAMhB,sBAAAA,CAAuB3D,IAAAA,EAAMwB,IAAAA,CAAKb,SAAS,CAAA;AACnD,gBAAA;AAEA,gBAAA,IAAI,MAAMqD,yBAAAA,CAA0BhE,IAAAA,EAAMwB,IAAAA,CAAKb,SAAS,CAAA,EAAG;oBACzD,MAAMY,mCAAAA,CAAoCf,EAAAA,EAAIR,IAAAA,EAAMwB,IAAAA,EAAMC,SAAAA,CAAAA;gBAC5D,CAAA,MAAO;oBACL,MAAMa,oBAAAA,CAAqB9B,EAAAA,EAAIR,IAAAA,EAAMwB,IAAAA,EAAMC,SAAAA,CAAAA;AAC7C,gBAAA;AACF,YAAA;AACF,QAAA;AACF,IAAA,CAAA;IACA,MAAMoD,IAAAA,CAAAA,GAAAA;AACJ,QAAA,MAAM,IAAIC,KAAAA,CAAM,iBAAA,CAAA;AAClB,IAAA;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"5.0.0-02-document-id.mjs","sources":["../../../src/migrations/internal-migrations/5.0.0-02-document-id.ts"],"sourcesContent":["/**\n * NOTE: This migration avoids using the `identifiers` utility.\n * As the `5.0.0-01-convert-identifiers-long-than-max-length`\n * migration does not convert the `localizations` join tables, as they are not\n * tables that exist anymore in v5 and are not in the db metadata.\n *\n * This migration therefore relies on the fact that those tables still exist, and\n * references them directly.\n *\n * Database join table name: `categories_localizations_links`\n * Actual `identifiers` returned join table name: `categories_localizations_lnk`\n *\n */\nimport { createId } from '@paralleldrive/cuid2';\nimport { snakeCase } from 'lodash/fp';\nimport type { Knex } from 'knex';\n\nimport type { Migration } from '../common';\nimport { createHeartbeatLogger, type HeartbeatLogger } from '../heartbeat';\nimport type { Database } from '../..';\nimport type { Meta } from '../../metadata';\n\nfunction getBatchSize(trx: Knex, defaultSize: number = 1000): number {\n const client = trx.client.config.client;\n const isSQLite =\n typeof client === 'string' && ['sqlite', 'sqlite3', 'better-sqlite3'].includes(client);\n return isSQLite ? Math.min(defaultSize, 250) : defaultSize;\n}\n\n// Union-find (disjoint-set) helpers used to cluster localization-linked rows in memory.\n// Path-compressing `find` keeps repeated lookups close to O(1) amortized.\nconst find = (parent: Map<number, number>, start: number): number => {\n let current = start;\n while (parent.get(current) !== current) {\n parent.set(current, parent.get(parent.get(current) as number) as number);\n current = parent.get(current) as number;\n }\n return current;\n};\n\nconst union = (parent: Map<number, number>, a: number, b: number): void => {\n const rootA = find(parent, a);\n const rootB = find(parent, b);\n if (rootA !== rootB) {\n parent.set(rootA, rootB);\n }\n};\n\n// Migrate document ids for tables that have localizations\n//\n// This clusters rows in memory (via union-find) instead of discovering one cluster at a\n// time through a `LIMIT 1` query. The previous implementation re-ran a query that scans\n// the whole table (filtered by `document_id IS NULL`, a column with no index at this point\n// in the migration) once per cluster, which is roughly O(n) discovery queries each doing\n// an O(n) scan — effectively O(n^2) for a table with n rows. For tables with many rows\n// and/or many locales this could take hours or longer without making visible progress.\n// See https://github.com/strapi/strapi/issues/23812, 23517, 25231.\n//\n// This also fixes a correctness gap: the original query only follows *direct* links\n// (one hop), so a chain of locale links (A<->B, B<->C, but no direct A<->C row) could be\n// split across two document ids instead of clustered together. Union-find naturally\n// follows transitive links. See https://github.com/strapi/strapi/issues/20948, 24544.\n//\n// Retry-safety: a previous run can crash between clusters, leaving some rows in a\n// locale group already assigned a document_id while their siblings are still NULL. On\n// retry those NULL rows must adopt the sibling's existing document_id instead of minting\n// a fresh one, or a document that used to be one splits into two. To support this, the\n// link fetch and the clustering below include already-migrated neighbor rows, not just\n// pending ones — but only pending (NULL) rows are ever written.\nconst migrateDocumentIdsWithLocalizations = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const singularName = meta.singularName.toLowerCase();\n const joinColumn = snakeCase(`${singularName}_id`);\n const inverseJoinColumn = snakeCase(`inv_${singularName}_id`);\n const joinTableName = snakeCase(`${meta.tableName}_localizations_links`);\n const batchSize = getBatchSize(knex);\n\n const pendingIds: number[] = (\n await knex(meta.tableName).select('id').whereNull('document_id')\n ).map((row: { id: number }) => row.id);\n\n if (pendingIds.length === 0) {\n return;\n }\n\n const pendingIdSet = new Set(pendingIds);\n\n // Fetch every link touching a pending id, on *either* side of the join — a pending\n // row's sibling may already be migrated, in which case it appears as the other column.\n // Batched because passing all ids in a single `whereIn` can exceed the database\n // driver's bound-parameter limit on large tables.\n const links: Array<Record<string, number>> = [];\n for (let i = 0; i < pendingIds.length; i += batchSize) {\n const idChunk = pendingIds.slice(i, i + batchSize);\n const chunkLinks = await knex(joinTableName)\n .select(joinColumn, inverseJoinColumn)\n .whereIn(joinColumn, idChunk)\n .orWhereIn(inverseJoinColumn, idChunk);\n links.push(...chunkLinks);\n }\n\n // Ids referenced by a link that aren't pending are already-migrated neighbors — look up\n // their document_id so a retry can adopt it instead of minting a new one.\n const neighborIds = new Set<number>();\n for (const link of links) {\n const a = link[joinColumn];\n const b = link[inverseJoinColumn];\n if (!pendingIdSet.has(a)) neighborIds.add(a);\n if (!pendingIdSet.has(b)) neighborIds.add(b);\n }\n\n const existingDocumentIds = new Map<number, string>();\n const neighborIdList = [...neighborIds];\n for (let i = 0; i < neighborIdList.length; i += batchSize) {\n const idChunk = neighborIdList.slice(i, i + batchSize);\n const rows: Array<{ id: number; document_id: string | null }> = await knex(meta.tableName)\n .select('id', 'document_id')\n .whereIn('id', idChunk);\n for (const row of rows) {\n if (row.document_id) {\n existingDocumentIds.set(row.id, row.document_id);\n }\n }\n }\n\n const parent = new Map<number, number>();\n for (const id of pendingIds) {\n parent.set(id, id);\n }\n for (const id of neighborIds) {\n parent.set(id, id);\n }\n for (const link of links) {\n const a = link[joinColumn];\n const b = link[inverseJoinColumn];\n if (parent.has(a) && parent.has(b)) {\n union(parent, a, b);\n }\n }\n\n const clusters = new Map<number, number[]>();\n for (const id of parent.keys()) {\n const root = find(parent, id);\n const cluster = clusters.get(root);\n if (cluster) {\n cluster.push(id);\n } else {\n clusters.set(root, [id]);\n }\n }\n\n // Group pending writes by target document_id — one write per cluster instead of one\n // per row shrinks the window for a crash to leave a cluster half-written.\n const writeGroups = new Map<string, number[]>();\n for (const members of clusters.values()) {\n const existingIdsInCluster = new Set<string>();\n for (const id of members) {\n const existing = existingDocumentIds.get(id);\n if (existing) {\n existingIdsInCluster.add(existing);\n }\n }\n\n // A cluster normally carries at most one existing document_id. More than one means an\n // earlier inconsistent partial write — unify onto a single deterministic id rather\n // than minting a third.\n const documentId =\n existingIdsInCluster.size > 0 ? [...existingIdsInCluster].sort()[0] : createId();\n\n const pendingMembers = members.filter((id) => pendingIdSet.has(id));\n if (pendingMembers.length > 0) {\n // Two distinct clusters resolving to the same document_id is only possible via a\n // cuid collision, but merge rather than overwrite so neither group's writes vanish.\n const existingGroup = writeGroups.get(documentId);\n if (existingGroup) {\n existingGroup.push(...pendingMembers);\n } else {\n writeGroups.set(documentId, pendingMembers);\n }\n }\n }\n\n const groupEntries = [...writeGroups.entries()];\n let processed = 0;\n for (let i = 0; i < groupEntries.length; i += batchSize) {\n const slice = groupEntries.slice(i, i + batchSize);\n await Promise.all(\n slice.map(([documentId, ids]) =>\n knex(meta.tableName).whereIn('id', ids).update({ document_id: documentId })\n )\n );\n processed += slice.reduce((sum, [, ids]) => sum + ids.length, 0);\n const rowsProcessed = processed;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${rowsProcessed}/${pendingIds.length} rows processed`\n );\n }\n};\n\n// Migrate document ids for tables that don't have localizations\nconst migrationDocumentIds = async (\n db: Database,\n knex: Knex,\n meta: Meta,\n heartbeat: HeartbeatLogger\n) => {\n const batchSize = getBatchSize(knex);\n const total = +(await knex(meta.tableName).count('* as recordsLeft').whereNull('document_id'))[0]\n .recordsLeft;\n let recordsLeft = total;\n while (recordsLeft > 0) {\n const currentBatchSize = recordsLeft < batchSize ? recordsLeft : batchSize;\n const updateRecords = (\n await knex(meta.tableName).select('id').whereNull('document_id').limit(currentBatchSize)\n ).map((item) => ({ id: item.id, document_id: createId() }));\n await knex(meta.tableName).insert(updateRecords).onConflict('id').merge();\n recordsLeft -= updateRecords.length;\n const processed = total - recordsLeft;\n heartbeat.tick(\n (elapsedSeconds) =>\n `[document-id] still running (${elapsedSeconds}s) · ${meta.tableName} ${processed}/${total}`\n );\n }\n};\n\nconst isDuplicateColumnError = (error: unknown): boolean => {\n if (!error || typeof error !== 'object') return false;\n const e = error as { code?: string; errno?: number; message?: string };\n if (e.code === '42701') return true;\n if (e.errno === 1060) return true;\n if (typeof e.message === 'string' && /duplicate column/i.test(e.message)) return true;\n return false;\n};\n\nconst createDocumentIdColumn = async (knex: Knex, tableName: string) => {\n try {\n await knex.schema.alterTable(tableName, (table) => {\n table.string('document_id');\n });\n } catch (error) {\n if (!isDuplicateColumnError(error)) {\n throw error;\n }\n }\n};\n\nconst hasLocalizationsJoinTable = async (knex: Knex, tableName: string) => {\n const joinTableName = snakeCase(`${tableName}_localizations_links`);\n return knex.schema.hasTable(joinTableName);\n};\n\nexport const createdDocumentId: Migration = {\n name: '5.0.0-02-created-document-id',\n async up(knex, db) {\n const heartbeat = createHeartbeatLogger((message) => {\n db.logger.info(message);\n });\n\n for (const meta of db.metadata.values()) {\n const hasTable = await knex.schema.hasTable(meta.tableName);\n\n if (!hasTable) {\n continue;\n }\n\n if ('documentId' in meta.attributes) {\n const hasDocumentIdColumn = await knex.schema.hasColumn(meta.tableName, 'document_id');\n\n if (!hasDocumentIdColumn) {\n await createDocumentIdColumn(knex, meta.tableName);\n }\n\n if (await hasLocalizationsJoinTable(knex, meta.tableName)) {\n await migrateDocumentIdsWithLocalizations(db, knex, meta, heartbeat);\n } else {\n await migrationDocumentIds(db, knex, meta, heartbeat);\n }\n }\n }\n },\n async down() {\n throw new Error('not implemented');\n },\n};\n"],"names":["getBatchSize","trx","defaultSize","client","config","isSQLite","includes","Math","min","find","parent","start","current","get","set","union","a","b","rootA","rootB","migrateDocumentIdsWithLocalizations","db","knex","meta","heartbeat","singularName","toLowerCase","joinColumn","snakeCase","inverseJoinColumn","joinTableName","tableName","batchSize","pendingIds","select","whereNull","map","row","id","length","pendingIdSet","Set","links","i","idChunk","slice","chunkLinks","whereIn","orWhereIn","push","neighborIds","link","has","add","existingDocumentIds","Map","neighborIdList","rows","document_id","clusters","keys","root","cluster","writeGroups","members","values","existingIdsInCluster","existing","documentId","size","sort","createId","pendingMembers","filter","existingGroup","groupEntries","entries","processed","Promise","all","ids","update","reduce","sum","rowsProcessed","tick","elapsedSeconds","migrationDocumentIds","total","count","recordsLeft","currentBatchSize","updateRecords","limit","item","insert","onConflict","merge","isDuplicateColumnError","error","e","code","errno","message","test","createDocumentIdColumn","schema","alterTable","table","string","hasLocalizationsJoinTable","hasTable","createdDocumentId","name","up","createHeartbeatLogger","logger","info","metadata","attributes","hasDocumentIdColumn","hasColumn","down","Error"],"mappings":"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"}
|