docs: codify the FK-safe stream ordering as a wire-format invariant
CI / lint (pull_request) Successful in 31s
CI / format (pull_request) Successful in 31s
CI / install-and-build (pull_request) Successful in 55s
CI / typecheck-tests (pull_request) Successful in 31s
CI / typecheck-source (pull_request) Successful in 31s
CI / test (pull_request) Successful in 42s

The postgres importer inserts records sequentially with FK enforcement live,
so it depends on records arriving in dependency order. That requirement was
implicit in each backend's export table list; make it explicit and enforced.

- Add EXPORT_KIND_ORDER (user, abode, resident, apikey, note) as a documented
  single source of truth, with the FK dependency chain spelled out on its doc
  comment, and note the ordering guarantee on the ExportEnvelope wire-format
  doc. Derive the isExportKind set from it.
- Both backends' export() now iterate EXPORT_KIND_ORDER via a loader map
  (postgres omits note by leaving it out of the map), so emission order is
  tied to the constant and can't drift.
- Tests: a change-detector on EXPORT_KIND_ORDER, plus assertions that both the
  sqlite and postgres (fake) exports emit record kinds grouped in FK-safe
  order (kind rank non-decreasing down the stream, after the leading meta).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #13.
This commit is contained in:
2026-07-23 00:14:15 +00:00
co-authored by Claude
parent 3a56dcd9e5
commit d7e31dfce9
6 changed files with 159 additions and 47 deletions
+20 -17
View File
@@ -48,13 +48,14 @@ import type {
import type { WrappedDb } from "./impl/types.js";
import { Readable } from "node:stream";
import readline from "node:readline";
import type {
Exportable,
ExportKind,
ExportOptions,
Importable,
ImportOptions,
ImportResult,
import {
EXPORT_KIND_ORDER,
type Exportable,
type ExportKind,
type ExportOptions,
type Importable,
type ImportOptions,
type ImportResult,
} from "../types/ExportImport.js";
import { isExportKind, kindAllowed, recordAllowed } from "../export/filter.js";
@@ -558,14 +559,16 @@ export class SqliteInterface
// `load()` is exactly one `WrappedDb.all()`). Kept lazy so the first query
// only fires once the destination starts pulling, and skipped entirely
// once the signal is aborted — no further reads after the destination
// goes away.
const tables: [ExportKind, () => { uid?: string; aid?: string }[]][] = [
["user", () => selectClientUsers(db)],
["abode", () => selectAbodes(db)],
["resident", () => selectResidents(db)],
["apikey", () => selectClientApikeys(db)],
["note", () => selectNotes(db)],
];
// goes away. Emission follows the FK-safe EXPORT_KIND_ORDER (part of the
// wire contract; see ExportImport.ts).
const loaders: Record<ExportKind, () => { uid?: string; aid?: string }[]> =
{
user: () => selectClientUsers(db),
abode: () => selectAbodes(db),
resident: () => selectResidents(db),
apikey: () => selectClientApikeys(db),
note: () => selectNotes(db),
};
async function* generate(): AsyncGenerator<string> {
if (signal?.aborted) return;
@@ -579,10 +582,10 @@ export class SqliteInterface
},
}) + "\n";
try {
for (const [kind, load] of tables) {
for (const kind of EXPORT_KIND_ORDER) {
if (signal?.aborted) return;
if (!kindAllowed(filter, kind)) continue;
for (const row of load()) {
for (const row of loaders[kind]()) {
if (signal?.aborted) return;
if (recordAllowed(filter, kind, row)) {
yield JSON.stringify({ kind, data: row }) + "\n";