feat: disable trigger when import collection (#1417)

* feat: dump with pg user defined functions

* chore: restore db in restorer

* chore: restore log

* feat: disable trigger when import collection
This commit is contained in:
ChengLei Shao 2023-02-07 11:55:11 +08:00 committed by GitHub
parent 8657710997
commit debd95894f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 2 deletions

View File

@ -15,7 +15,7 @@ describe('dump', () => {
let testDir: string;
beforeEach(async () => {
testDir = path.resolve(os.tmpdir(), `nocobase-dump-${Date.now()}`);
await fsPromises.mkdir(testDir, { recursive: true });
app = mockServer();
db = app.db;
@ -52,6 +52,7 @@ describe('dump', () => {
fields: [],
});
await app.cleanDb();
await db.sync();
});
@ -116,4 +117,52 @@ describe('dump', () => {
await db.sequelize.query(sql, { type: 'INSERT' });
});
it('should dump user defined functions', async () => {
if (db.sequelize.getDialect() !== 'postgres') {
return;
}
await db.sequelize.query(`
CREATE OR REPLACE FUNCTION add(integer, integer) RETURNS integer
AS 'select $1 + $2;'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
`);
await db.sequelize.query(`
CREATE OR REPLACE FUNCTION trigger_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
-- trigger logic
END;
$$`);
await db.sequelize.query(`
CREATE TRIGGER last_name_changes
BEFORE UPDATE
ON ${app.db.getCollection('users').model.tableName}
FOR EACH ROW
EXECUTE PROCEDURE trigger_function();
`);
await db.sequelize.query(`
CREATE OR REPLACE VIEW vistaView AS SELECT 'Hello World' as hello;
`);
const dumper = new Dumper(app, {
workDir: testDir,
});
await dumper.dumpDb();
const restorer = new Restorer(app, {
workDir: testDir,
});
await restorer.importDb();
});
});

View File

@ -72,10 +72,70 @@ export class Dumper extends AppMigrator {
}
await this.dumpMeta();
await this.dumpDb();
await this.packDumpedDir();
await this.clearWorkDir();
}
async dumpDb() {
const db = this.app.db;
const dialect = db.sequelize.getDialect();
let sqlContent = [];
if (dialect === 'postgres') {
// get user defined functions in postgres
const functions = await db.sequelize.query(
`SELECT
n.nspname AS function_schema,
p.proname AS function_name,
pg_get_functiondef(p.oid) AS def
FROM
pg_proc p
LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE
n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY
function_schema,
function_name;`,
{
type: 'SELECT',
},
);
for (const f of functions) {
sqlContent.push(f['def']);
}
// get user defined triggers in postgres
const triggers = await db.sequelize.query(`select pg_get_triggerdef(oid) from pg_trigger`, {
type: 'SELECT',
});
for (const t of triggers) {
sqlContent.push(t['pg_get_triggerdef']);
}
// get user defined views in postgres
const views = await db.sequelize.query(
`SELECT table_schema, table_name, pg_get_viewdef("table_name", true) as def
FROM information_schema.views
WHERE table_schema NOT IN ('information_schema', 'pg_catalog')
ORDER BY table_schema, table_name`,
{
type: 'SELECT',
},
);
for (const v of views) {
sqlContent.push(`CREATE OR REPLACE VIEW ${v['table_name']} AS ${v['def']}`);
}
}
if (sqlContent.length > 0) {
const dbDumpPath = path.resolve(this.workDir, 'db.sql');
await fsPromises.writeFile(dbDumpPath, JSON.stringify(sqlContent), 'utf8');
}
}
async dumpMeta() {
const metaPath = path.resolve(this.workDir, 'meta');

View File

@ -6,7 +6,7 @@ import { AppMigrator } from './app-migrator';
import { CollectionGroupManager } from './collection-group-manager';
import { FieldValueWriter } from './field-value-writer';
import { readLines, sqlAdapter } from './utils';
import fs from 'fs';
export class Restorer extends AppMigrator {
direction = 'restore' as const;
@ -31,6 +31,7 @@ export class Restorer extends AppMigrator {
await this.decompressBackup(filePath);
await this.importCollections();
await this.importDb();
await this.clearWorkDir();
}
@ -122,7 +123,18 @@ export class Restorer extends AppMigrator {
const results = await inquirer.prompt(questions);
const importCollection = async (collectionName: string) => {
const collectionMetaPath = path.resolve(this.workDir, 'collections', collectionName, 'meta');
const metaContent = await fsPromises.readFile(collectionMetaPath, 'utf8');
const meta = JSON.parse(metaContent);
const tableName = meta.tableName;
try {
// disable trigger
if (this.app.db.inDialect('postgres')) {
await this.app.db.sequelize.query(`ALTER TABLE IF EXISTS "${tableName}" DISABLE TRIGGER ALL`);
}
await this.importCollection({
name: collectionName,
});
@ -130,6 +142,10 @@ export class Restorer extends AppMigrator {
this.app.log.warn(`import collection ${collectionName} failed`, {
err,
});
} finally {
if (this.app.db.inDialect('postgres')) {
await this.app.db.sequelize.query(`ALTER TABLE IF EXISTS "${tableName}" ENABLE TRIGGER ALL`);
}
}
};
@ -199,6 +215,7 @@ export class Restorer extends AppMigrator {
const metaContent = await fsPromises.readFile(collectionMetaPath, 'utf8');
const meta = JSON.parse(metaContent);
app.log.info(`collection meta ${metaContent}`);
const tableName = meta.tableName;
if (options.clear !== false) {
@ -321,4 +338,30 @@ export class Restorer extends AppMigrator {
this.importedCollections.push(collection.name);
}
async importDb() {
const sqlFilePath = path.resolve(this.workDir, 'db.sql');
// if db.sql file not exists, skip import
if (!fs.existsSync(sqlFilePath)) {
return;
}
// read file content from db.sql
const queriesContent = await fsPromises.readFile(sqlFilePath, 'utf8');
const queries = JSON.parse(queriesContent);
for (const sql of queries) {
try {
this.app.log.info(`import sql: ${sql}`);
await this.app.db.sequelize.query(sql);
} catch (e) {
if (e.name === 'SequelizeDatabaseError') {
this.app.logger.error(e.message);
} else {
throw e;
}
}
}
}
}