mirror of
https://github.com/tiennm99/miti99bot-js.git
synced 2026-05-24 00:24:51 +00:00
3f03521e84
Pre-execution prerequisites for the Phase 07 cutover. Stage 2 of the cutover keeps DUAL_WRITE=0 for ~6 days; if anything regresses during that window the operator MUST be able to roll back to KV/D1 with the last N days of Mongo-only writes recovered. Pre-building these scripts (per code-reviewer #4) eliminates "draft a backfill under outage pressure" — the anti-pattern of writing untested code at 4am. Reverse-backfill - scripts/backfill-mongo-to-kv.js: full-scan Mongo collection per module, PUT each doc back to CF KV via REST. expiresAt → expirationTtl (clamped to 60s minimum per CF KV); already-expired docs are skipped (won't resurrect dead state). 50 ops/sec throttle. --dry-run + --module flags. - scripts/backfill-mongo-to-d1.js: full-scan trading_trades, build INSERT SQL preserving legacy_id where present (round-trips D1 autoincrement IDs preserved by phase-05 forward backfill). Sequential int generation for any docs without legacy_id. Pipes through wrangler d1 execute. - scripts/lib/migration-helpers.js: cfKvPut helper added. Delete guard (debugger #12) - scripts/wrangler-delete-guard.sh: interactive CONFIRM wrapper around wrangler kv namespace delete + wrangler d1 delete. Exits 3 when stdin is not a tty so it cannot run in CI. Documented: never run in CI. package.json: backfill:mongo:kv[:dry] + backfill:mongo:d1[:dry] scripts wired. Tests: 697 → 733 (+36). - 7 cfKvPut tests (REST URL, querystring, body, expiration_ttl param). - 10 reverse-KV TTL math tests (expired sentinel, future seconds, no-TTL, CF 60s minimum clamp). - 9 reverse-D1 SQL construction tests (escaping, legacy_id preservation, sequential generation). Lint clean. No Worker code touched. Stage 1 cutover, 7-day soak, snapshots, and Stage 3 cleanup (delete CFKVStore + simplify factories + edit package.json deploy chain) remain operator-driven and will be committed separately after binding deletion.
123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
/**
|
|
* @file backfill-mongo-to-d1.test.js — unit tests for SQL-statement construction.
|
|
*
|
|
* Tests `buildInsertSql` and `sqlStr` exported from backfill-mongo-to-d1.js.
|
|
* No execSync, no wrangler, no real Mongo connection.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildInsertSql, sqlStr } from "../../scripts/backfill-mongo-to-d1.js";
|
|
|
|
// ─── sqlStr ───────────────────────────────────────────────────────────────────
|
|
|
|
describe("sqlStr", () => {
|
|
it("wraps a plain string in single quotes", () => {
|
|
expect(sqlStr("hello")).toBe("'hello'");
|
|
});
|
|
|
|
it("escapes internal single quotes by doubling them", () => {
|
|
expect(sqlStr("it's")).toBe("'it''s'");
|
|
});
|
|
|
|
it("escapes multiple single quotes", () => {
|
|
expect(sqlStr("a'b'c")).toBe("'a''b''c'");
|
|
});
|
|
|
|
it("returns NULL for null", () => {
|
|
expect(sqlStr(null)).toBe("NULL");
|
|
});
|
|
|
|
it("returns NULL for undefined", () => {
|
|
expect(sqlStr(undefined)).toBe("NULL");
|
|
});
|
|
|
|
it("handles empty string", () => {
|
|
expect(sqlStr("")).toBe("''");
|
|
});
|
|
|
|
it("coerces non-string values via String()", () => {
|
|
// @ts-ignore — testing runtime coercion
|
|
expect(sqlStr(42)).toBe("'42'");
|
|
});
|
|
});
|
|
|
|
// ─── buildInsertSql ───────────────────────────────────────────────────────────
|
|
|
|
describe("buildInsertSql", () => {
|
|
/** @type {Parameters<typeof buildInsertSql>[0]} */
|
|
const BASE_ROW = {
|
|
id: 1,
|
|
user_id: "u123",
|
|
symbol: "BTC",
|
|
side: "buy",
|
|
qty: 0.5,
|
|
price_vnd: 1500000,
|
|
ts: 1700000000,
|
|
};
|
|
|
|
it("produces a valid INSERT statement", () => {
|
|
const sql = buildInsertSql(BASE_ROW);
|
|
expect(sql).toMatch(
|
|
/^INSERT INTO trading_trades \(id, user_id, symbol, side, qty, price_vnd, ts\) VALUES \(/,
|
|
);
|
|
expect(sql).toMatch(/\);$/);
|
|
});
|
|
|
|
it("includes all column values in correct order", () => {
|
|
const sql = buildInsertSql(BASE_ROW);
|
|
expect(sql).toContain("1,"); // id
|
|
expect(sql).toContain("'u123'"); // user_id
|
|
expect(sql).toContain("'BTC'"); // symbol
|
|
expect(sql).toContain("'buy'"); // side
|
|
expect(sql).toContain("0.5,"); // qty
|
|
expect(sql).toContain("1500000,"); // price_vnd
|
|
expect(sql).toContain("1700000000"); // ts
|
|
});
|
|
|
|
it("preserves legacy_id as the INSERT id", () => {
|
|
const sql = buildInsertSql({ ...BASE_ROW, id: 42 });
|
|
// id=42 should be the first value
|
|
expect(sql).toMatch(/VALUES \(42,/);
|
|
});
|
|
|
|
it("escapes single quotes in user_id", () => {
|
|
const sql = buildInsertSql({ ...BASE_ROW, user_id: "o'brien" });
|
|
expect(sql).toContain("'o''brien'");
|
|
});
|
|
|
|
it("escapes single quotes in symbol", () => {
|
|
const sql = buildInsertSql({ ...BASE_ROW, symbol: "it's" });
|
|
expect(sql).toContain("'it''s'");
|
|
});
|
|
|
|
it("handles decimal qty correctly", () => {
|
|
const sql = buildInsertSql({ ...BASE_ROW, qty: 1.23456789 });
|
|
expect(sql).toContain("1.23456789");
|
|
});
|
|
|
|
it("handles zero values without coercion errors", () => {
|
|
const sql = buildInsertSql({
|
|
id: 0,
|
|
user_id: "",
|
|
symbol: "",
|
|
side: "",
|
|
qty: 0,
|
|
price_vnd: 0,
|
|
ts: 0,
|
|
});
|
|
expect(sql).toMatch(/VALUES \(0, '', '', '', 0, 0, 0\);/);
|
|
});
|
|
|
|
it("sequential id generation: each row gets unique ascending id", () => {
|
|
// Simulate the script logic of incrementing nextId for docs without legacy_id
|
|
let nextId = 1;
|
|
const rows = [
|
|
{ user_id: "a", symbol: "BTC", side: "buy", qty: 1, price_vnd: 100, ts: 1 },
|
|
{ user_id: "b", symbol: "ETH", side: "sell", qty: 2, price_vnd: 200, ts: 2 },
|
|
];
|
|
const stmts = rows.map((r) => buildInsertSql({ ...r, id: nextId++ }));
|
|
expect(stmts[0]).toMatch(/VALUES \(1,/);
|
|
expect(stmts[1]).toMatch(/VALUES \(2,/);
|
|
});
|
|
});
|