import assert from "node:assert/strict"; import test from "node:test"; import { nextSequenceIdAfterRestore, restoreSqlText, sequenceSetvalSql, sqlLiteral, type QueryExecutor } from "../src/databaseExport.ts"; import { exportTableSql } from "../src/databaseExport.ts"; const jsonbColumn = { dataType: "jsonb", udtName: "jsonb" }; test("sqlLiteral serializes primitive values", () => { assert.equal(sqlLiteral(null), "NULL"); assert.equal(sqlLiteral(undefined), "NULL"); assert.equal(sqlLiteral("normaler Text"), "'normaler Text'"); assert.equal(sqlLiteral("O'Brien"), "'O''Brien'"); assert.equal(sqlLiteral("erste Zeile\nzweite Zeile"), "'erste Zeile\nzweite Zeile'"); assert.equal(sqlLiteral("Grüße"), "'Grüße'"); assert.equal(sqlLiteral(true), "TRUE"); assert.equal(sqlLiteral(false), "FALSE"); assert.equal(sqlLiteral(0), "0"); assert.equal(sqlLiteral(42), "42"); assert.equal(sqlLiteral(-10), "-10"); assert.equal(sqlLiteral(1.25), "1.25"); }); test("sqlLiteral serializes dates, buffers, objects and arrays", () => { assert.equal(sqlLiteral(new Date("2026-09-11T10:15:30.000Z")), "'2026-09-11T10:15:30.000Z'"); assert.equal(sqlLiteral(Buffer.from("abc")), "decode('616263', 'hex')"); assert.equal(sqlLiteral({}), "'{}'"); assert.equal(sqlLiteral([]), "'[]'"); assert.equal(sqlLiteral({ foo: "bar" }), `'{"foo":"bar"}'`); assert.equal(sqlLiteral({ foo: "O'Brien" }), `'{"foo":"O''Brien"}'`); assert.equal(sqlLiteral({ nested: { enabled: true }, items: [{ id: 1 }] }), `'{"nested":{"enabled":true},"items":[{"id":1}]}'`); }); test("sqlLiteral serializes JSON columns as valid JSON values", () => { assert.equal(sqlLiteral({ foo: "bar", enabled: true }, jsonbColumn), `'{"foo":"bar","enabled":true}'`); assert.equal(sqlLiteral("plain json string", jsonbColumn), `'"plain json string"'`); assert.equal(sqlLiteral(true, jsonbColumn), "'true'"); }); test("generated table export never contains implicit object stringification", async () => { const client: QueryExecutor = { async query>(text: string): Promise<{ rows: T[]; rowCount: number | null }> { if (text.includes("information_schema.columns")) { return { rowCount: 3, rows: [ { columnName: "id", dataType: "bigint", udtName: "int8", ordinalPosition: 1, isNullable: "NO" }, { columnName: "value", dataType: "jsonb", udtName: "jsonb", ordinalPosition: 2, isNullable: "NO" }, { columnName: "label", dataType: "text", udtName: "text", ordinalPosition: 3, isNullable: "YES" } ] as T[] }; } if (text.includes("pg_get_serial_sequence")) { return { rowCount: 1, rows: [{ sequenceName: "public.example_id_seq" }] as T[] }; } return { rowCount: 1, rows: [{ id: 4, value: { foo: "bar" }, label: "O'Brien" }] as T[] }; } }; const sql = (await exportTableSql(client, "example")).join("\n"); assert.doesNotMatch(sql, /\[object Object\]/); assert.match(sql, /'\{"foo":"bar"\}'/); }); test("sequence helper advances after restored max id", () => { assert.equal(nextSequenceIdAfterRestore(4), 5); assert.equal(nextSequenceIdAfterRestore(null), 1); assert.match(sequenceSetvalSql("users", "public.users_id_seq"), /SELECT setval\('public\.users_id_seq'/); assert.match(sequenceSetvalSql("users", "public.users_id_seq"), /MAX\("id"\) FROM "users"/); }); test("restoreSqlText removes dump transaction wrapper after export header", () => { const body = restoreSqlText(` -- TicketTracker Datenbankexport -- Erstellt: 2026-09-14T10:00:00.000Z BEGIN; SET LOCAL lock_timeout = '10s'; SET LOCAL statement_timeout = '15min'; TRUNCATE TABLE "users", "auth_sessions" RESTART IDENTITY CASCADE; INSERT INTO "users" ("id", "username") VALUES (1, 'admin'); COMMIT; `); assert.doesNotMatch(body, /^\s*BEGIN\s*;/im); assert.doesNotMatch(body, /^\s*COMMIT\s*;/im); assert.doesNotMatch(body, /^\s*SET\s+LOCAL/im); assert.doesNotMatch(body, /TRUNCATE\s+TABLE/i); assert.match(body, /INSERT INTO "users"/); });