mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Reviewed and verified at 69792ccbcf.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
68 lines
3.8 KiB
TypeScript
68 lines
3.8 KiB
TypeScript
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { gunzipSync } from "node:zlib";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { runDatabaseBackup } from "./backup-lib.js";
|
|
|
|
const transport = vi.hoisted(() => ({ rows: [] as unknown[][], projection: "", end: vi.fn() }));
|
|
vi.mock("postgres", () => ({
|
|
default: () => {
|
|
const columns = [
|
|
{ column_name: "bytes", data_type: "bytea", udt_name: "bytea", udt_schema: "pg_catalog" },
|
|
{ column_name: "payloads", data_type: "ARRAY", udt_name: "_bytea", udt_schema: "pg_catalog" },
|
|
{ column_name: "metadata", data_type: "jsonb", udt_name: "jsonb", udt_schema: "pg_catalog" },
|
|
{ column_name: "redacted", data_type: "ARRAY", udt_name: "_bytea", udt_schema: "pg_catalog" },
|
|
];
|
|
const sql = async (parts: TemplateStringsArray) => {
|
|
const query = parts.join("?");
|
|
if (query.includes("FROM information_schema.tables")) return [{ schema_name: "public", tablename: "binary_fixture" }];
|
|
if (query.includes("FROM information_schema.columns")) return columns.map(column => ({ ...column, udt_schema: "pg_catalog", is_nullable: "YES", is_identity: "NO", column_default: null }));
|
|
return [];
|
|
};
|
|
return Object.assign(sql, {
|
|
end: transport.end,
|
|
unsafe: (query: string) => query.startsWith("SELECT count(*)") ? Promise.resolve([{ n: transport.rows.length }])
|
|
: query.startsWith("SELECT ") && query.includes('FROM "public"."binary_fixture"')
|
|
? { values: () => ({ cursor: async function* () { transport.projection = query; yield transport.rows; } }) }
|
|
: query,
|
|
});
|
|
},
|
|
}));
|
|
|
|
describe("JavaScript backup binary SQL", () => {
|
|
it("writes exact binary scalars and nested arrays without changing JSON or redaction", async () => {
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "backup-binary-sql-"));
|
|
const binary = Buffer.from([0, 255, ...Buffer.from("雪")]);
|
|
const metadata = { bytes: binary, values: [null, "雪"] };
|
|
const hex = binary.toString("hex");
|
|
const payloads = `{"\\\\x${hex}","\\\\x",NULL}`;
|
|
const boundedPayloads = `[0:1][3:4]={{"\\\\x${hex}",NULL},{"\\\\x","\\\\x00ff"}}`;
|
|
transport.rows = [
|
|
[binary, payloads, metadata, payloads],
|
|
[new Uint8Array(), "{}", [], payloads],
|
|
[null, null, null, payloads],
|
|
[new Uint8Array([0, 255]), boundedPayloads, {}, boundedPayloads],
|
|
];
|
|
transport.end.mockClear();
|
|
try {
|
|
const archive = await runDatabaseBackup({ connectionString: "postgres://unused/binary_test", backupDir: directory,
|
|
backupEngine: "javascript", retention: { dailyDays: 1, weeklyWeeks: 1, monthlyMonths: 1 },
|
|
nullifyColumns: { binary_fixture: ["redacted"] } });
|
|
const body = gunzipSync(await readFile(archive.backupFile)).toString("utf8");
|
|
const inserts = body.split("\n").filter(line => line.startsWith("INSERT INTO"));
|
|
const literal = (value: string) => `$paperclip$${value}$paperclip$`;
|
|
const array = (value: string) => literal(value);
|
|
const prefix = 'INSERT INTO "public"."binary_fixture" ("bytes", "payloads", "metadata", "redacted") VALUES ';
|
|
expect(inserts).toEqual([
|
|
`${prefix}(decode('${hex}', 'hex'), ${array(`{"\\\\x${hex}","\\\\x",NULL}`)}, ${literal(JSON.stringify(metadata))}, NULL);`,
|
|
`${prefix}(decode('', 'hex'), ${array("{}")}, ${literal("[]")}, NULL);`,
|
|
`${prefix}(NULL, NULL, NULL, NULL);`,
|
|
`${prefix}(decode('00ff', 'hex'), ${array(boundedPayloads)}, ${literal("{}")}, NULL);`,
|
|
]);
|
|
expect(transport.projection).toBe('SELECT "bytes", "payloads"::text AS "payloads", "metadata", "redacted"::text AS "redacted" FROM "public"."binary_fixture"');
|
|
expect(transport.end).toHaveBeenCalledTimes(1);
|
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
});
|
|
});
|