@nocobase/plugin-backups 2.2.0-beta.5 → 2.2.0-beta.7

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.
@@ -10,6 +10,9 @@ import { DatabaseOptions } from '@nocobase/database';
10
10
  export type DBBackupOptions = {
11
11
  dir: string;
12
12
  skipFdw?: boolean;
13
+ /**
14
+ * @deprecated Prefer excludeTables. includeTables may miss dependent database objects.
15
+ */
13
16
  includeTables?: string[];
14
17
  excludeTables?: string[];
15
18
  };
@@ -17,9 +20,13 @@ export type DBRestoreOptions = {
17
20
  filePath: string;
18
21
  schema?: string;
19
22
  skipDropAllTables?: boolean;
23
+ restoreMode?: 'preserveTables';
24
+ toolchain?: DBBackupToolchain;
20
25
  };
26
+ export type DBBackupToolchain = 'postgres' | 'kingbase';
21
27
  export interface DBAdapter {
22
28
  dbOpts: DatabaseOptions;
29
+ backupToolchain?: DBBackupToolchain;
23
30
  backup(options: DBBackupOptions): Promise<void>;
24
31
  restore(options: DBRestoreOptions): Promise<void>;
25
32
  check(op: 'backup' | 'restore'): Promise<void>;
@@ -52,8 +52,15 @@ const D$$ = import_os.default.platform() === "win32" ? "$$" : "\\$\\$";
52
52
  const STREAM_BUFFER_SIZE = 2 * 1024 * 1024;
53
53
  const run = async (command, envVars = {}) => {
54
54
  try {
55
- const { stdout } = await exec(command, { env: { ...process.env, ...envVars } });
56
- return stdout;
55
+ const result = await exec(command, { env: { ...process.env, ...envVars } });
56
+ if (typeof result === "string") {
57
+ return result;
58
+ }
59
+ if (result && typeof result === "object" && "stdout" in result) {
60
+ const { stdout } = result;
61
+ return typeof stdout === "string" ? stdout : String(stdout ?? "");
62
+ }
63
+ return "";
57
64
  } catch (error) {
58
65
  throw new Error(`${error.message}`);
59
66
  }
@@ -68,6 +75,20 @@ const escapeStringLiteral = (value) => String(value).replace(/'/g, "''");
68
75
  const quotePgIdentifier = (value) => `"${String(value).replace(/"/g, '""')}"`;
69
76
  const quoteShellArg = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
70
77
  const quotePgTablePattern = (table) => quoteShellArg(String(table).split(".").map(quotePgIdentifier).join("."));
78
+ const isPgRestoreSchemaTocEntry = (line) => /^\d+;\s+\d+\s+\d+\s+SCHEMA\s+-\s+/.test(line);
79
+ const parsePgTableReference = (table, defaultSchema) => {
80
+ const parts = String(table).split(".");
81
+ if (parts.length > 1) {
82
+ return {
83
+ schema: parts[0],
84
+ table: parts.slice(1).join(".")
85
+ };
86
+ }
87
+ return {
88
+ schema: defaultSchema,
89
+ table: parts[0]
90
+ };
91
+ };
71
92
  const qualifyPgTablePattern = (table, schema) => {
72
93
  const tablePattern = String(table);
73
94
  if (!schema || tablePattern.includes(".")) {
@@ -97,6 +118,14 @@ class BaseDBAdapter {
97
118
  );
98
119
  }
99
120
  };
121
+ hasCommand(command) {
122
+ try {
123
+ (0, import_child_process.execSync)(`${command} --version`);
124
+ return true;
125
+ } catch (_error) {
126
+ return false;
127
+ }
128
+ }
100
129
  }
101
130
  class MySQLAdapter extends BaseDBAdapter {
102
131
  #backupCmd = "mysqldump";
@@ -145,6 +174,7 @@ class MySQLAdapter extends BaseDBAdapter {
145
174
  "--set-gtid-purged=OFF",
146
175
  "--routines",
147
176
  "--triggers",
177
+ "--events",
148
178
  ...version && version > 7 ? ["--column-statistics=0"] : [],
149
179
  database
150
180
  ];
@@ -237,9 +267,47 @@ class MySQLAdapter extends BaseDBAdapter {
237
267
  return "";
238
268
  }
239
269
  }
240
- async restore({ filePath, skipDropAllTables = false }) {
270
+ async restore({ filePath, skipDropAllTables = false, restoreMode }) {
241
271
  const { username, host, port, database, password } = this.dbOpts;
242
- if (!skipDropAllTables) {
272
+ if (restoreMode === "preserveTables") {
273
+ const dropViewsCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
274
+ DELIMITER $$
275
+ DROP PROCEDURE IF EXISTS drop_all_views$$
276
+ CREATE PROCEDURE drop_all_views()
277
+ BEGIN
278
+ DECLARE _done INT DEFAULT FALSE;
279
+ DECLARE _viewName VARCHAR(255);
280
+
281
+ DECLARE _cursor CURSOR FOR
282
+ SELECT table_name
283
+ FROM information_schema.VIEWS
284
+ WHERE table_schema = SCHEMA();
285
+
286
+ DECLARE CONTINUE HANDLER FOR NOT FOUND SET _done = TRUE;
287
+
288
+ OPEN _cursor;
289
+
290
+ REPEAT
291
+ FETCH _cursor INTO _viewName;
292
+
293
+ IF NOT _done THEN
294
+ SET @stmt_sql = CONCAT('DROP VIEW IF EXISTS ', _viewName);
295
+ PREPARE stmt FROM @stmt_sql;
296
+ EXECUTE stmt;
297
+ DEALLOCATE PREPARE stmt;
298
+ END IF;
299
+
300
+ UNTIL _done END REPEAT;
301
+
302
+ CLOSE _cursor;
303
+ END$$
304
+
305
+ CALL drop_all_views()$$
306
+ DROP PROCEDURE drop_all_views$$
307
+ DELIMITER ;
308
+ "`;
309
+ await run(dropViewsCommand, { MYSQL_PWD: password });
310
+ } else if (!skipDropAllTables) {
243
311
  const dropDataCommand = `mysql -u ${username} -h ${host} ${port ? `-P ${port}` : ""} --protocol=tcp -D ${database} -e "
244
312
  DELIMITER $$
245
313
  DROP PROCEDURE IF EXISTS drop_all_tables_and_triggers$$
@@ -327,24 +395,36 @@ class MySQLAdapter extends BaseDBAdapter {
327
395
  }
328
396
  }
329
397
  class PostgresAdapter extends BaseDBAdapter {
330
- get #backupCmd() {
398
+ get backupToolchain() {
399
+ return "postgres";
400
+ }
401
+ getBackupCommandName(_toolchain = this.backupToolchain) {
331
402
  return formatPathInEnv(process.env.PG_DUMP_PATH) || "pg_dump";
332
403
  }
333
- get #restoreCmd() {
404
+ getRestoreCommandName(_toolchain = this.backupToolchain) {
334
405
  return formatPathInEnv(process.env.PG_RESTORE_PATH) || "pg_restore";
335
406
  }
407
+ getSqlCommandName(_toolchain = this.backupToolchain) {
408
+ return formatPathInEnv(process.env.PSQL_PATH) || "psql";
409
+ }
410
+ getPasswordEnvVars(password, _toolchain = this.backupToolchain) {
411
+ return { PGPASSWORD: password };
412
+ }
413
+ getBackupSchema() {
414
+ return this.dbOpts.schema;
415
+ }
336
416
  async check(op) {
337
417
  switch (op) {
338
418
  case "backup":
339
- this.assertCommand(this.#backupCmd);
419
+ this.assertCommand(this.getBackupCommandName());
340
420
  break;
341
421
  case "restore":
342
- this.assertCommand(this.#restoreCmd);
422
+ this.assertCommand(this.getRestoreCommandName());
343
423
  break;
344
424
  }
345
425
  }
346
426
  async clientVersion(op) {
347
- const cmd = op === "backup" ? this.#backupCmd : this.#restoreCmd;
427
+ const cmd = op === "backup" ? this.getBackupCommandName() : this.getRestoreCommandName();
348
428
  try {
349
429
  return (0, import_child_process.execSync)(`${cmd} --version`).toString();
350
430
  } catch (_error) {
@@ -352,15 +432,53 @@ class PostgresAdapter extends BaseDBAdapter {
352
432
  }
353
433
  }
354
434
  async backup({ dir, includeTables, excludeTables }) {
355
- const { username, host, port, database, password, schema: backupSchema } = this.dbOpts;
435
+ const { username, host, port, database, password } = this.dbOpts;
356
436
  const filePath = `${dir}/data`;
437
+ const backupSchema = this.getBackupSchema();
357
438
  const schemaOption = backupSchema ? `--schema=${backupSchema}` : "";
439
+ const expandedExcludeTables = Array.isArray(excludeTables) ? [.../* @__PURE__ */ new Set([...excludeTables, ...await this.getOwnedSequenceTables(excludeTables, backupSchema)])] : [];
358
440
  const includeOption = Array.isArray(includeTables) && includeTables.length ? includeTables.map((table) => `-t ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
359
- const excludeOption = Array.isArray(excludeTables) && excludeTables.length ? excludeTables.map((table) => `-T ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
360
- const command = `${this.#backupCmd} ${includeOption} ${excludeOption} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -F c -b --quote-all-identifiers ${schemaOption} -f ${filePath} ${database}`;
361
- await run(command, { PGPASSWORD: password });
441
+ const excludeOption = expandedExcludeTables.length ? expandedExcludeTables.map((table) => `-T ${quotePgTablePattern(qualifyPgTablePattern(table, backupSchema))}`).join(" ") : "";
442
+ const command = `${this.getBackupCommandName()} ${includeOption} ${excludeOption} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -F c -b --quote-all-identifiers ${schemaOption} -f ${filePath} ${database}`;
443
+ await run(command, this.getPasswordEnvVars(password));
444
+ }
445
+ async getOwnedSequenceTables(excludeTables, backupSchema) {
446
+ if (!Array.isArray(excludeTables) || !excludeTables.length) {
447
+ return [];
448
+ }
449
+ const { username, host, port, database, password } = this.dbOpts;
450
+ const tableRefs = excludeTables.map((table) => parsePgTableReference(table, backupSchema)).filter((ref) => ref.table);
451
+ if (!tableRefs.length) {
452
+ return [];
453
+ }
454
+ const values = tableRefs.map((ref) => {
455
+ const schemaValue = ref.schema == null ? "NULL" : `'${escapeStringLiteral(ref.schema)}'`;
456
+ return `(${schemaValue}, '${escapeStringLiteral(ref.table)}')`;
457
+ }).join(",");
458
+ const query = `
459
+ WITH excluded(schema_name, table_name) AS (VALUES ${values})
460
+ SELECT seq_ns.nspname || '.' || seq.relname
461
+ FROM excluded
462
+ JOIN pg_class tbl ON tbl.relname = excluded.table_name
463
+ JOIN pg_namespace tbl_ns ON tbl_ns.oid = tbl.relnamespace
464
+ JOIN pg_depend dep ON dep.refobjid = tbl.oid
465
+ JOIN pg_class seq ON seq.oid = dep.objid AND seq.relkind = 'S'
466
+ JOIN pg_namespace seq_ns ON seq_ns.oid = seq.relnamespace
467
+ WHERE tbl.relkind IN ('r', 'p')
468
+ AND dep.deptype IN ('a', 'i')
469
+ AND (excluded.schema_name IS NULL OR tbl_ns.nspname = excluded.schema_name)
470
+ `;
471
+ const command = `${this.getSqlCommandName()} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -At -c ${quoteShellArg(query)}`;
472
+ const output = String(await run(command, this.getPasswordEnvVars(password)));
473
+ return output.split("\n").map((line) => line.trim()).filter(Boolean);
362
474
  }
363
- async restore({ filePath, schema, skipDropAllTables = false }) {
475
+ async restore({
476
+ filePath,
477
+ schema,
478
+ skipDropAllTables = false,
479
+ restoreMode,
480
+ toolchain = this.backupToolchain
481
+ }) {
364
482
  const { username, host, port, database, password } = this.dbOpts;
365
483
  let schemaOption = this.dbOpts.schema;
366
484
  if (schema && !schemaOption) {
@@ -376,13 +494,34 @@ class PostgresAdapter extends BaseDBAdapter {
376
494
  const j = Math.max(1, Math.floor(cpuCores / 2));
377
495
  const schemaNameCondition = schemaOption ? `WHERE schemaname = '${schemaOption}'` : `WHERE schemaname NOT IN ('pg_catalog', 'information_schema')`;
378
496
  const relnamespaceCondition = schemaOption ? `WHERE relnamespace = '${schemaOption}'::regnamespace` : `WHERE tgrelid IN (SELECT oid FROM pg_class WHERE relnamespace NOT IN (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname IN ('pg_catalog', 'information_schema')))`;
379
- if (!skipDropAllTables) {
380
- const dropDataCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
497
+ if (restoreMode === "preserveTables") {
498
+ const dropViewsCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
499
+ DO ${D$$} DECLARE r RECORD;
500
+ BEGIN
501
+ FOR r IN (
502
+ SELECT schemaname, viewname, false AS materialized FROM pg_views ${schemaNameCondition}
503
+ UNION ALL
504
+ SELECT schemaname, matviewname AS viewname, true AS materialized FROM pg_matviews ${schemaNameCondition}
505
+ ) LOOP
506
+ BEGIN
507
+ IF r.materialized THEN
508
+ EXECUTE 'DROP MATERIALIZED VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
509
+ ELSE
510
+ EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
511
+ END IF;
512
+ EXCEPTION
513
+ WHEN OTHERS THEN
514
+ END;
515
+ END LOOP;
516
+ END ${D$$};"`.replace(/\n/g, " ");
517
+ await run(dropViewsCommand, this.getPasswordEnvVars(password, toolchain));
518
+ } else if (!skipDropAllTables) {
519
+ const dropDataCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
381
520
  DO ${D$$} DECLARE r RECORD;
382
521
  BEGIN
383
522
  FOR r IN (SELECT viewname,schemaname FROM pg_views ${schemaNameCondition}) LOOP
384
523
  BEGIN
385
- EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.tablename) || ' CASCADE';
524
+ EXECUTE 'DROP VIEW IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.viewname) || ' CASCADE';
386
525
  EXCEPTION
387
526
  WHEN OTHERS THEN
388
527
  END;
@@ -398,7 +537,7 @@ class PostgresAdapter extends BaseDBAdapter {
398
537
 
399
538
  FOR r IN (SELECT sequencename,schemaname FROM pg_sequences ${schemaNameCondition}) LOOP
400
539
  BEGIN
401
- EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.tablename) || ' CASCADE';
540
+ EXECUTE 'DROP SEQUENCE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.sequencename) || ' CASCADE';
402
541
  EXCEPTION
403
542
  WHEN OTHERS THEN
404
543
  END;
@@ -417,21 +556,47 @@ class PostgresAdapter extends BaseDBAdapter {
417
556
  END LOOP;
418
557
 
419
558
  END ${D$$};"`.replace(/\n/g, " ");
420
- await run(dropDataCommand, { PGPASSWORD: password });
559
+ await run(dropDataCommand, this.getPasswordEnvVars(password, toolchain));
421
560
  }
422
561
  if (schema === schemaOption || !schemaOption) {
423
- const pgRestoreCommand = `${this.#restoreCmd} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} --clean --if-exists --no-owner -j ${j} ${filePath}`;
424
- await run(pgRestoreCommand, { PGPASSWORD: password });
562
+ const restoreList = restoreMode === "preserveTables" ? await this.createRestoreListWithoutSchema(filePath, toolchain) : void 0;
563
+ const pgRestoreCommand = `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} --clean --if-exists --no-owner -j ${j} ${(restoreList == null ? void 0 : restoreList.option) ?? ""} ${filePath}`;
564
+ try {
565
+ await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
566
+ } finally {
567
+ if (restoreList) {
568
+ await fsPromises.unlink(restoreList.filePath).catch(() => {
569
+ });
570
+ }
571
+ }
425
572
  } else {
426
573
  const srcSchema = schema || "public";
427
- const pgRestoreCommand = `${this.#restoreCmd} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -n ${srcSchema} -d ${database} --clean --if-exists --no-owner -j ${j} ${filePath}`;
428
- await this.#restoreSchema(srcSchema, schemaOption, pgRestoreCommand);
574
+ const pgRestoreCommand = this.buildSchemaRestoreCommand(srcSchema, schemaOption, filePath, j, toolchain);
575
+ await this.restoreSchema(srcSchema, schemaOption, pgRestoreCommand, toolchain);
429
576
  }
430
577
  }
431
- async #restoreSchema(srcSchema, targetSchema, pgRestoreCommand) {
578
+ async createRestoreListWithoutSchema(filePath, toolchain = this.backupToolchain) {
579
+ const listOutput = String(await run(`${this.getRestoreCommandName(toolchain)} --list ${quoteShellArg(filePath)}`));
580
+ const filteredList = listOutput.split("\n").filter((line) => !isPgRestoreSchemaTocEntry(line)).join("\n");
581
+ const listFilePath = import_path.default.join(
582
+ import_os.default.tmpdir(),
583
+ `nocobase-pg-restore-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.list`
584
+ );
585
+ await fsPromises.writeFile(listFilePath, filteredList);
586
+ return {
587
+ option: `-L ${quoteShellArg(listFilePath)}`,
588
+ filePath: listFilePath
589
+ };
590
+ }
591
+ buildSchemaRestoreCommand(srcSchema, _targetSchema, filePath, jobs, toolchain = this.backupToolchain) {
592
+ const { username, host, port, database } = this.dbOpts;
593
+ return `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -n ${srcSchema} -d ${database} --clean --if-exists --no-owner -j ${jobs} ${filePath}`;
594
+ }
595
+ async restoreSchema(srcSchema, targetSchema, pgRestoreCommand, toolchain = this.backupToolchain) {
432
596
  const { username, host, port, database, password } = this.dbOpts;
433
597
  const ts = Date.now();
434
- const preCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
598
+ const sqlCommandName = this.getSqlCommandName(toolchain);
599
+ const preCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
435
600
  DO ${D$$}
436
601
  BEGIN
437
602
  IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = '${srcSchema}') THEN
@@ -439,7 +604,7 @@ class PostgresAdapter extends BaseDBAdapter {
439
604
  END IF;
440
605
  EXECUTE 'CREATE SCHEMA ${srcSchema}';
441
606
  END ${D$$};"`.replace(/\n/g, " ");
442
- const postCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
607
+ const postCommand = `${sqlCommandName} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
443
608
  DO ${D$$}
444
609
  BEGIN
445
610
  EXECUTE 'DROP SCHEMA ${targetSchema} CASCADE';
@@ -448,21 +613,21 @@ class PostgresAdapter extends BaseDBAdapter {
448
613
  EXECUTE 'ALTER SCHEMA ${srcSchema}_${ts} RENAME TO ${srcSchema}';
449
614
  END IF;
450
615
  END ${D$$};"`.replace(/\n/g, " ");
451
- await run(preCommand, { PGPASSWORD: password });
616
+ await run(preCommand, this.getPasswordEnvVars(password, toolchain));
452
617
  try {
453
- await run(pgRestoreCommand, { PGPASSWORD: password });
618
+ await run(pgRestoreCommand, this.getPasswordEnvVars(password, toolchain));
454
619
  } finally {
455
- await run(postCommand, { PGPASSWORD: password });
620
+ await run(postCommand, this.getPasswordEnvVars(password, toolchain));
456
621
  }
457
- await this.#syncCollectionSchemaMetadata(srcSchema, targetSchema);
622
+ await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
458
623
  }
459
- async #syncCollectionSchemaMetadata(srcSchema, targetSchema) {
624
+ async syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain = this.backupToolchain) {
460
625
  const { username, host, port, database, password, tablePrefix } = this.dbOpts;
461
626
  const collectionsTable = `${tablePrefix || ""}collections`;
462
627
  const targetSchemaLiteral = escapeStringLiteral(targetSchema);
463
628
  const srcSchemaLiteral = escapeStringLiteral(srcSchema);
464
629
  const collectionsTableLiteral = escapeStringLiteral(collectionsTable);
465
- const updateCollectionSchemaCommand = `psql -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
630
+ const updateCollectionSchemaCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "
466
631
  DO ${D$$}
467
632
  DECLARE
468
633
  collections_table text := '${collectionsTableLiteral}';
@@ -482,7 +647,61 @@ class PostgresAdapter extends BaseDBAdapter {
482
647
  );
483
648
  END IF;
484
649
  END ${D$$};"`.replace(/\n/g, " ");
485
- await run(updateCollectionSchemaCommand, { PGPASSWORD: password });
650
+ await run(updateCollectionSchemaCommand, this.getPasswordEnvVars(password, toolchain));
651
+ }
652
+ }
653
+ class KingbaseAdapter extends PostgresAdapter {
654
+ get backupToolchain() {
655
+ return this.hasKingbaseToolchain() ? "kingbase" : "postgres";
656
+ }
657
+ getKingbaseBackupCommandName() {
658
+ return formatPathInEnv(process.env.KINGBASE_DUMP_PATH) || "sys_dump";
659
+ }
660
+ getKingbaseRestoreCommandName() {
661
+ return formatPathInEnv(process.env.KINGBASE_RESTORE_PATH) || "sys_restore";
662
+ }
663
+ getKingbaseSqlCommandName() {
664
+ return formatPathInEnv(process.env.KINGBASE_PSQL_PATH) || "ksql";
665
+ }
666
+ hasKingbaseToolchain() {
667
+ return this.hasCommand(this.getKingbaseBackupCommandName()) && this.hasCommand(this.getKingbaseRestoreCommandName()) && this.hasCommand(this.getKingbaseSqlCommandName());
668
+ }
669
+ getBackupCommandName(toolchain = this.backupToolchain) {
670
+ return toolchain === "kingbase" ? this.getKingbaseBackupCommandName() : super.getBackupCommandName(toolchain);
671
+ }
672
+ getRestoreCommandName(toolchain = this.backupToolchain) {
673
+ return toolchain === "kingbase" ? this.getKingbaseRestoreCommandName() : super.getRestoreCommandName(toolchain);
674
+ }
675
+ getSqlCommandName(toolchain = this.backupToolchain) {
676
+ return toolchain === "kingbase" ? this.getKingbaseSqlCommandName() : super.getSqlCommandName(toolchain);
677
+ }
678
+ getPasswordEnvVars(password, toolchain = this.backupToolchain) {
679
+ if (toolchain === "postgres") {
680
+ return super.getPasswordEnvVars(password, toolchain);
681
+ }
682
+ return { KINGBASE_PASSWORD: password };
683
+ }
684
+ getBackupSchema() {
685
+ return this.dbOpts.schema || "public";
686
+ }
687
+ buildSchemaRestoreCommand(srcSchema, targetSchema, filePath, jobs, toolchain = this.backupToolchain) {
688
+ if (toolchain === "postgres") {
689
+ return super.buildSchemaRestoreCommand(srcSchema, targetSchema, filePath, jobs, toolchain);
690
+ }
691
+ const { username, host, port, database } = this.dbOpts;
692
+ return `${this.getRestoreCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -g ${srcSchema} -G ${targetSchema} -d ${database} --clean --if-exists --no-owner -j ${jobs} ${filePath}`;
693
+ }
694
+ async restoreSchema(srcSchema, targetSchema, restoreCommand, toolchain = this.backupToolchain) {
695
+ if (toolchain === "postgres") {
696
+ await super.restoreSchema(srcSchema, targetSchema, restoreCommand, toolchain);
697
+ return;
698
+ }
699
+ const { username, host, port, database, password } = this.dbOpts;
700
+ const targetSchemaIdentifier = quotePgIdentifier(targetSchema).replace(/"/g, '\\"');
701
+ const createSchemaCommand = `${this.getSqlCommandName(toolchain)} -U ${username} -h ${host} ${port ? `-p ${port}` : ""} -d ${database} -c "CREATE SCHEMA IF NOT EXISTS ${targetSchemaIdentifier};"`;
702
+ await run(createSchemaCommand, this.getPasswordEnvVars(password, toolchain));
703
+ await run(restoreCommand, this.getPasswordEnvVars(password, toolchain));
704
+ await this.syncCollectionSchemaMetadata(srcSchema, targetSchema, toolchain);
486
705
  }
487
706
  }
488
707
  class SQLiteAdapter extends BaseDBAdapter {
@@ -503,6 +722,7 @@ class MariaDBAdapter extends MySQLAdapter {
503
722
  const adapterMap = {
504
723
  mysql: MySQLAdapter,
505
724
  postgres: PostgresAdapter,
725
+ kingbase: KingbaseAdapter,
506
726
  sqlite: SQLiteAdapter,
507
727
  mariadb: MariaDBAdapter
508
728
  };
@@ -9,5 +9,5 @@
9
9
  export { BackupManager } from './managers/backup';
10
10
  export type { BackupSettings, BackupCreator, BackupTaskResult } from './managers/backup';
11
11
  export { RestoreManager, RestoreOptions } from './managers/restore';
12
- export { BACKUP_EXTENSION } from './utils';
12
+ export { BACKUP_EXTENSION, METADATA_EXTENSION, Extractor, compressDirectoryToBackupArchive } from './utils';
13
13
  export { default } from './plugin';
@@ -38,8 +38,11 @@ var server_exports = {};
38
38
  __export(server_exports, {
39
39
  BACKUP_EXTENSION: () => import_utils.BACKUP_EXTENSION,
40
40
  BackupManager: () => import_backup.BackupManager,
41
+ Extractor: () => import_utils.Extractor,
42
+ METADATA_EXTENSION: () => import_utils.METADATA_EXTENSION,
41
43
  RestoreManager: () => import_restore.RestoreManager,
42
44
  RestoreOptions: () => import_restore.RestoreOptions,
45
+ compressDirectoryToBackupArchive: () => import_utils.compressDirectoryToBackupArchive,
43
46
  default: () => import_plugin.default
44
47
  });
45
48
  module.exports = __toCommonJS(server_exports);
@@ -51,6 +54,9 @@ var import_plugin = __toESM(require("./plugin"));
51
54
  0 && (module.exports = {
52
55
  BACKUP_EXTENSION,
53
56
  BackupManager,
57
+ Extractor,
58
+ METADATA_EXTENSION,
54
59
  RestoreManager,
55
- RestoreOptions
60
+ RestoreOptions,
61
+ compressDirectoryToBackupArchive
56
62
  });
@@ -17,10 +17,14 @@ export interface BackupSettings {
17
17
  keep?: number;
18
18
  scheduled: boolean;
19
19
  cron: string;
20
+ /**
21
+ * @deprecated Prefer excludeTables. includeTables may miss dependent database objects.
22
+ */
20
23
  includeTables?: string[];
21
24
  excludeTables?: string[];
22
25
  description?: string;
23
26
  createdBy?: BackupCreator;
27
+ metadata?: Record<string, unknown>;
24
28
  }
25
29
  export interface BackupFile {
26
30
  name: string;
@@ -50,7 +50,7 @@ var import_utils = require("@nocobase/utils");
50
50
  var import_util = require("util");
51
51
  var import_database = require("../adapters/database");
52
52
  var import_utils2 = require("../utils");
53
- const BACKUP_METADATA_VERSION = 1;
53
+ const BACKUP_METADATA_VERSION = 2;
54
54
  class BackupManager {
55
55
  app;
56
56
  ctx;
@@ -99,7 +99,10 @@ class BackupManager {
99
99
  }
100
100
  async backup(fileBaseName, opts) {
101
101
  const contentPath = import_path.default.join(this.#tempDir, fileBaseName);
102
- return this.#runBackupTask({ ...this.#settings, ...opts ?? {} }, fileBaseName, contentPath);
102
+ const runtimeTables = [...this.app.db.collections.values()].filter((collection) => collection.dataCategory === "runtime").map((collection) => collection.getTableNameWithSchemaAsString());
103
+ const backupOptions = { ...this.#settings, ...opts ?? {} };
104
+ backupOptions.excludeTables = [.../* @__PURE__ */ new Set([...backupOptions.excludeTables ?? [], ...runtimeTables])];
105
+ return this.#runBackupTask(backupOptions, fileBaseName, contentPath);
103
106
  }
104
107
  async destroy(fileName) {
105
108
  const filePath = this.#getValidatedFilePath(fileName);
@@ -187,6 +190,7 @@ class BackupManager {
187
190
  };
188
191
  });
189
192
  const metadata = {
193
+ ...opts.metadata ?? {},
190
194
  metadataVersion: BACKUP_METADATA_VERSION,
191
195
  enableFilesBackup: opts.enableFilesBackup,
192
196
  version: await this.app.version.get(),
@@ -194,6 +198,7 @@ class BackupManager {
194
198
  createdBy: opts.createdBy,
195
199
  database: {
196
200
  dialect,
201
+ toolchain: this.#dbAdapter.backupToolchain,
197
202
  underscored,
198
203
  tablePrefix,
199
204
  schema,
@@ -11,6 +11,7 @@ import { ResourcerContext } from '@nocobase/resourcer';
11
11
  export interface RestoreOptions {
12
12
  forceSchemaRestore?: boolean;
13
13
  skipDropAllTables?: boolean;
14
+ restoreMode?: 'preserveTables';
14
15
  }
15
16
  export declare class RestoreManager {
16
17
  #private;
@@ -142,7 +142,9 @@ class RestoreManager {
142
142
  await this.#dbAdapter.restore({
143
143
  filePath: import_path.default.join(extractedDir, dbFile),
144
144
  schema: metadata.database.schema,
145
- skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true
145
+ skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true,
146
+ restoreMode: options == null ? void 0 : options.restoreMode,
147
+ toolchain: this.#resolveRestoreToolchain(metadata)
146
148
  });
147
149
  this.ctx.logger.info("Database restored successfully", { module: import_utils2.BACKUPS });
148
150
  await this.#restoreFilesAndCleanup(restoreUploads, extractedDir);
@@ -195,7 +197,7 @@ class RestoreManager {
195
197
  await this.#restoreFilesAndCleanup(uploadsExist, extractedDir);
196
198
  await this.ctx.app.runCommand("upgrade");
197
199
  } else {
198
- if (!tolerentMode && this.#dbAdapter.dbOpts.dialect === "postgres") {
200
+ if (!tolerentMode && this.#isPostgresLikeDialect(this.#dbAdapter.dbOpts.dialect)) {
199
201
  const backupClientVersion = Number((0, import_utils2.toMajorVersion)(metadata.database.backupClientVersion));
200
202
  const dbServerVersion = Number((0, import_utils2.toMajorVersion)(dbVersion));
201
203
  if (backupClientVersion > 16 && dbServerVersion <= 16) {
@@ -248,7 +250,7 @@ class RestoreManager {
248
250
  if ((metadata.database.tablePrefix || "") !== (tablePrefix || "")) {
249
251
  throw new Error(this.#t("Database table prefix mismatch"));
250
252
  }
251
- const forceSchemaRestore = (options == null ? void 0 : options.forceSchemaRestore) === true && dialect === "postgres";
253
+ const forceSchemaRestore = (options == null ? void 0 : options.forceSchemaRestore) === true && this.#isPostgresLikeDialect(dialect);
252
254
  if (!forceSchemaRestore) {
253
255
  if (((_b = (_a = this.ctx.request) == null ? void 0 : _a.body) == null ? void 0 : _b.dbSchema) && ((_d = (_c = this.ctx.request) == null ? void 0 : _c.body) == null ? void 0 : _d.dbSchema) !== (schema || "public")) {
254
256
  throw new Error(this.#t("Database schema mismatch"));
@@ -289,6 +291,25 @@ class RestoreManager {
289
291
  }
290
292
  return true;
291
293
  }
294
+ #isPostgresLikeDialect(dialect) {
295
+ return dialect === "postgres" || dialect === "kingbase";
296
+ }
297
+ #resolveRestoreToolchain(metadata) {
298
+ if (metadata.database.dialect !== "kingbase") {
299
+ return void 0;
300
+ }
301
+ if (metadata.database.toolchain === "kingbase" || metadata.database.toolchain === "postgres") {
302
+ return metadata.database.toolchain;
303
+ }
304
+ const backupClientVersion = metadata.database.backupClientVersion || "";
305
+ if (/sys_dump/i.test(backupClientVersion)) {
306
+ return "kingbase";
307
+ }
308
+ if (/pg_dump|postgresql/i.test(backupClientVersion)) {
309
+ return "postgres";
310
+ }
311
+ return this.#dbAdapter.backupToolchain;
312
+ }
292
313
  async #decompressFiles(filePath, password) {
293
314
  const fileBaseName = import_path.default.basename(filePath, `.${import_utils2.BACKUP_EXTENSION}`);
294
315
  const outputDir = import_path.default.join(this.#tempDir, fileBaseName);
@@ -372,7 +393,9 @@ class RestoreManager {
372
393
  await this.#dbAdapter.restore({
373
394
  filePath: import_path.default.join(extractedDir, dbFile),
374
395
  schema: metadata.database.schema,
375
- skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true
396
+ skipDropAllTables: (options == null ? void 0 : options.skipDropAllTables) === true,
397
+ restoreMode: options == null ? void 0 : options.restoreMode,
398
+ toolchain: this.#resolveRestoreToolchain(metadata)
376
399
  });
377
400
  this.ctx.logger.info("Database restored successfully", { module: import_utils2.BACKUPS });
378
401
  if (restoreUploads) {
@@ -33,6 +33,7 @@ export declare class Extractor extends Writable {
33
33
  private extract;
34
34
  promise(): Promise<void>;
35
35
  }
36
+ export declare function compressDirectoryToBackupArchive(sourceDir: string, backupFilePath: string): Promise<void>;
36
37
  export declare function humanFileSize(bytes: number, si?: boolean, dp?: number): string;
37
38
  export declare function getDBVersion(db: Database): Promise<string>;
38
39
  export declare function isQsTruly(value?: string | boolean): any;