Repository navigation
Expand file tree
/
Copy pathtables.js
More file actions
196 lines (178 loc) · 7.06 KB
/
Copy pathtables.js
File metadata and controls
196 lines (178 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
const DatabaseInfo = require('@tryghost/database-info');
const config = require('../../../../shared/config');
const { commands } = require('../../schema');
const { createIrreversibleMigration, createNonTransactionalMigration } = require('./migrations');
function isDevelopmentOrTesting() {
const env = config.get('env');
return env === 'development' || env.startsWith('testing');
}
/**
* @param {import('knex').Knex} connection
* @param {string} table
* @returns {Promise<string[]>} the other tables with a foreign key to `table`
*/
async function getReferencingTables(connection, table) {
if (DatabaseInfo.isMySQL(connection)) {
const [rows] = await connection.raw(
`SELECT DISTINCT TABLE_NAME AS name
FROM information_schema.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
AND REFERENCED_TABLE_NAME = ?
AND TABLE_NAME <> ?`,
[table, table],
);
return rows.map((/** @type {{name: string}} */ row) => row.name);
}
const referencing = [];
for (const other of await commands.getTables(connection)) {
if (other === table) {
continue;
}
const foreignKeys = await connection.raw(`PRAGMA foreign_key_list('${other}');`);
if (
foreignKeys.some((/** @type {{table: string}} */ foreignKey) => foreignKey.table === table)
) {
referencing.push(other);
}
}
return referencing;
}
/**
* Drops a development copy of a table along with the tables that reference it,
* so no rows are left pointing at a table that no longer exists. Boot recreates
* the dropped tables that are still in development.
*
* @param {import('knex').Knex} connection
* @param {string} table
* @param {Set<string>} [dropped]
*/
async function dropDevelopmentCopy(connection, table, dropped = new Set()) {
dropped.add(table);
for (const referencing of await getReferencingTables(connection, table)) {
if (!dropped.has(referencing)) {
await dropDevelopmentCopy(connection, referencing, dropped);
}
}
logging.info(`Dropping development copy of table: ${table}`);
await commands.deleteTable(table, connection);
}
/**
* Creates a migrations which will add a new table from schema.js to the database
* @param {string} name - table name
* @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration, this parameter MUST be present
* @param {Object} [options]
* @param {boolean} [options.replaceDevelopmentCopy] - set when finalising a table that was in development (see
* schema/in-development.ts). Development and testing databases already have a copy built from an earlier
* definition, so there an existing table is replaced, discarding its data and dropping the tables that reference
* it. Other environments skip it as usual.
*
* @returns {Object} migration object returning config/up/down properties
*/
function addTable(name, tableSpec, { replaceDevelopmentCopy = false } = {}) {
return createNonTransactionalMigration(
async function up(connection) {
const tableExists = await connection.schema.hasTable(name);
if (tableExists && replaceDevelopmentCopy && isDevelopmentOrTesting()) {
await dropDevelopmentCopy(connection, name);
} else if (tableExists) {
logging.warn(`Skipping adding table: ${name} - table already exists`);
return;
}
logging.info(`Adding table: ${name}`);
return commands.createTable(name, connection, tableSpec);
},
async function down(connection) {
const tableExists = await connection.schema.hasTable(name);
if (!tableExists) {
logging.warn(`Skipping dropping table: ${name} - table does not exist`);
return;
}
if (replaceDevelopmentCopy && isDevelopmentOrTesting()) {
return dropDevelopmentCopy(connection, name);
}
logging.info(`Dropping table: ${name}`);
return commands.deleteTable(name, connection);
},
);
}
/**
* Creates migration which will drop tables
*
* Without `tableSpecs` the migration is irreversible: a dropped table's rows are gone, so
* rolling back would hand an earlier version an empty table it expects data in. Passing a
* spec for every table makes it reversible, recreating the tables empty and in reverse
* order. Only do that for tables whose rows can be lost, such as ones behind a private
* feature flag.
*
* @param {string[]} names - names of the tables to drop, each before any table it references
* @param {Object<string, Object>} [tableSpecs] - copy of each table's schema definition as defined in schema.js at the moment of writing the migration, keyed by table name
*
* @returns {Object} migration object returning config/up/down properties
*/
function dropTables(names, tableSpecs) {
async function up(connection) {
for (const name of names) {
const exists = await connection.schema.hasTable(name);
if (!exists) {
logging.warn(`Skipping dropping table: ${name} - table does not exist`);
} else {
logging.info(`Dropping table: ${name}`);
await commands.deleteTable(name, connection);
}
}
}
if (!tableSpecs) {
return createIrreversibleMigration(up);
}
const missingSpecs = names.filter((name) => !Object.hasOwn(tableSpecs, name));
if (missingSpecs.length) {
throw new errors.IncorrectUsageError({
message: `Cannot recreate dropped tables without their specs: ${missingSpecs.join(', ')}`,
});
}
return createNonTransactionalMigration(up, async function down(connection) {
for (const name of names.slice().reverse()) {
const exists = await connection.schema.hasTable(name);
if (exists) {
logging.warn(`Skipping adding table: ${name} - table already exists`);
} else {
logging.info(`Adding table: ${name}`);
await commands.createTable(name, connection, tableSpecs[name]);
}
}
});
}
/**
* Creates a migration which will drop an existing table and then re-add a new table based on provided spec
* @param {string} name - table name
* @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration, this parameter MUST be present
*
* @returns {Object} migration object returning config/up/down properties
*/
function recreateTable(name, tableSpec) {
return createNonTransactionalMigration(
async function up(connection) {
const exists = await connection.schema.hasTable(name);
if (!exists) {
logging.warn(`Skipping dropping table: ${name} - table does not exist`);
} else {
logging.info(`Dropping table: ${name}`);
await commands.deleteTable(name, connection);
logging.info(`Re-adding table: ${name}`);
await commands.createTable(name, connection, tableSpec);
}
},
async function down() {
// noop: we cannot go back to old table schema
logging.warn(`Ignoring rollback for table recreate: ${name}`);
return Promise.resolve();
},
);
}
module.exports = {
addTable,
dropTables,
recreateTable,
};