feat: add export/import to the postgres backend; import auto-detects backend
CI / format (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 23s
CI / install-and-build (pull_request) Successful in 45s
CI / typecheck-source (pull_request) Successful in 25s
CI / typecheck-tests (pull_request) Successful in 30s
CI / test (pull_request) Successful in 40s

The export/import plan predated the postgres backend. Bring it up to parity:

- PostgresInterface implements Exportable + Importable, mirroring the sqlite
  backend. Export is a signal-checked async generator (one query per table);
  import drives a transaction via `WrappedPool.multi`, which rolls back on any
  error/abort and commits only after the whole stream is consumed cleanly.
- The `note` kind is skipped on postgres (its note CRUD is still unimplemented,
  so a pg database holds none) — a full dump from sqlite imports its
  user/abode/resident/apikey records and drops notes.
- Unlike sqlite (PRAGMA foreign_keys=off), postgres keeps FK enforcement; the
  FK-safe insertion order keeps a full dump valid, and truly-dangling partial
  dumps will (correctly) fail.
- abode-import now resolves the backend via getDbInterface instead of
  constructing SqliteInterface directly; isImportable keeps it from ever
  running over the remote (api) interface, which has no import.
- Add isImportable(); make postgres selectClientApikeys' where optional.
- Tests: exercise the real PostgresInterface export/import paths against an
  in-memory fake WrappedPgClient (no pg service in CI) — NDJSON shape,
  meta.source, filtering, note-skip, insert dispatch, and error/abort rollback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:36:21 +00:00
co-authored by Claude
parent aadc950e24
commit 3a56dcd9e5
5 changed files with 524 additions and 13 deletions
+15 -10
View File
@@ -1,9 +1,7 @@
import { createReadStream } from "node:fs";
import { getWrappedDb } from "../db/sqlite/impl/index.js";
import { SqliteInterface } from "../db/sqlite/SqliteInterface.js";
import { parseSqliteUrl } from "../db/sqlite/url.js";
import { getDbInterface } from "../db/index.js";
import { isExportKind } from "../db/export/filter.js";
import type { ExportFilter } from "../db/types/ExportImport.js";
import { isImportable, type ExportFilter } from "../db/types/ExportImport.js";
const args = process.argv.slice(2);
@@ -15,14 +13,15 @@ function printUsage(err: boolean | string = false): never {
}
log("Usage:");
log("\tabode-import --help");
log("\tabode-import <sqlite-database-url> <input-file|-> [--kinds=...] \\");
log("\tabode-import <database-url> <input-file|-> [--kinds=...] \\");
log(
"\t [--exclude-kinds=...] [--abodes=aid,...] [--users=uid,...]",
);
log("");
log(
"The target database must already be migrated (run abode-migrate first).",
"The target must be a local backend (sqlite or postgres), already migrated",
);
log("(run abode-migrate first). Remote (api) targets are not importable.");
process.exit(err ? 1 : 0);
}
@@ -42,7 +41,7 @@ for (const arg of args) {
const url = positional[0];
const input = positional[1];
if (!url) printUsage("missing <sqlite-database-url>");
if (!url) printUsage("missing <database-url>");
if (!input) printUsage("missing <input-file|->");
if (positional.length > 2) printUsage("too many arguments");
@@ -73,9 +72,15 @@ if (abodes) filter.abodes = abodes;
const users = parseList(flags.get("users"));
if (users) filter.users = users;
// Import is sqlite-only: construct the backend directly rather than resolving
// generically, so it can never be pointed at a remote (api) target.
const db = new SqliteInterface(getWrappedDb(...parseSqliteUrl(url)));
// Resolve the backend generically. Import lives on the local backends (sqlite,
// postgres); the remote (api) interface has no `import`, so `isImportable`
// keeps it from ever running over HTTP.
const db = await getDbInterface(url);
if (!isImportable(db)) {
console.error(`Error: backend '${db.name}' does not support import`);
await db.close().catch(() => {});
process.exit(1);
}
const source = input === "-" ? process.stdin : createReadStream(input);