@crvouga/mockingbird-service-postgres 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog — @crvouga/mockingbird-service-postgres
2
2
 
3
+ ## 1.7.0 (2026-10-02)
4
+
5
+ ### Features
6
+
7
+ - coordinate HTTP and protocol mocks ([3168653](https://github.com/crvouga/mockingbird/commit/31686534c55a9823fdaa92afc9cc456cf741b809))
8
+
9
+ ### Fixes and improvements
10
+
11
+ - decouple smoke types from built packages ([a4b31c6](https://github.com/crvouga/mockingbird/commit/a4b31c6205d92aa0c6f7c9de3518d5ae44d38829))
12
+ - protect child admin endpoints ([5410b94](https://github.com/crvouga/mockingbird/commit/5410b948a30dcedc21e9a70e44fd415c1ea25394))
13
+ - explain discovery and controls ([dd0fed6](https://github.com/crvouga/mockingbird/commit/dd0fed6f5f94cac51ae71f2d2e7d58a6b01ccc63))
14
+ - harden protocol startup and state ([6f5b557](https://github.com/crvouga/mockingbird/commit/6f5b557e87b48b31985949e04f3b5ef58f51d295))
15
+ - preserve checkpoints and lifecycle ([50e8c32](https://github.com/crvouga/mockingbird/commit/50e8c322feded2f7f45afce9d8cdef43c37ab9ab))
16
+ - clean branch state on reset ([3b714d4](https://github.com/crvouga/mockingbird/commit/3b714d4a5df8e75fee0bb31e98d7c7566e164381))
17
+ - type signal listener cleanup ([5279852](https://github.com/crvouga/mockingbird/commit/5279852e56ea60262ccf17aa6e46b3c31091a34a))
18
+
3
19
  ## 1.6.0 (2026-10-01)
4
20
 
5
21
  ### Features
@@ -36,6 +52,7 @@
36
52
  - allow unpopulated materialized views to reference each other ([3ce1d8f](https://github.com/crvouga/mockingbird/commit/3ce1d8f555ed4008dd894aed20ebf64ab0253774))
37
53
  - support CREATE EXTENSION pg_trgm ([2296ba2](https://github.com/crvouga/mockingbird/commit/2296ba2f3d1a9ea7ce8ec9f528e979034692ebf1))
38
54
  - install pgcrypto digest() ([b5f3ccf](https://github.com/crvouga/mockingbird/commit/b5f3ccf79c6fedcabab157f1ee07af18a7871057))
55
+ - boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
39
56
 
40
57
  ## 1.3.1 (2026-09-29)
41
58
 
package/COMPATIBILITY.md CHANGED
@@ -10,6 +10,10 @@ See [COMPATIBILITY-AUDIT.md](COMPATIBILITY-AUDIT.md) for the latest evidence-bas
10
10
 
11
11
  Reference oracle: **PostgreSQL 18.3** via PGlite (`@electric-sql/pglite`, real Postgres compiled to WASM, in-process). Inventory: `bun run inventory`. Construct catalog: `bun run scenarios` → [`compat/scenarios.ts`](compat/scenarios.ts). Divergences: [`compat/divergences.json`](compat/divergences.json). Requirements matrix: `bun run requirements` → `compat/requirements.json` + `compat/coverage.json`.
12
12
 
13
+ The Node `/server` entry participates in protocol fleets. Acceptance tests use `pg@8.23.0`
14
+ for authenticated readiness, shutdown, namespace isolation, full-catalog reset/snapshot/restore and
15
+ clock controls. This supervisor surface does not change the SQL dialect compatibility contract.
16
+
13
17
  ## Proof surface
14
18
 
15
19
  Differential tests compare a **tuple** per statement: rows (normalized to canonical PostgreSQL text where typed), column names and type names where requested, error SQLSTATE class + normalized message, command tag / `rowCount`, and transaction status, plus a **logical state dump** (catalog names, column definitions, row payloads, sequence values) after write sequences.
package/README.md CHANGED
@@ -193,6 +193,13 @@ server from one frozen template, so a seeded stack starts from the same bytes ea
193
193
  surface as every other mock, including the table explorer (`GET /sql/tables`, `POST /sql/query`).
194
194
  The wire server does not speak that HTTP API.
195
195
 
196
+ `mockingbird-postgres serve --config mockingbird.json` also supervises a mixed HTTP/Postgres/Redis
197
+ fleet through the Node `/server` entry. See [fleet configuration and controls](../../../docs/FLEETS.md)
198
+ for ephemeral discovery, namespace endpoints, aggregate health, snapshots and clocks. Each declared
199
+ namespace has an independent cluster. Fleet checkpoints restore its entire catalog, including created
200
+ and dropped databases, records, deterministic random state, faults and time; live client sessions
201
+ return a conflict until closed.
202
+
196
203
  `server.snapshot()` freezes the live state; `server.fault({ dropConnection | delayStatementMs |
197
204
  failCommit })` arms the next statement or connection for a drop, a delay, or a `40001` commit
198
205
  failure.
package/dist/admin.js CHANGED
@@ -1561,29 +1561,30 @@ var createControlPlane = (context) => {
1561
1561
  return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
1562
1562
  },
1563
1563
  "GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
1564
- "GET /clock": () => json(200, context.clock.state()),
1565
- "POST /clock": ({ body }) => {
1564
+ "GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
1565
+ "POST /clock": ({ body, namespace }) => {
1566
+ const clock = context.namespaceClock?.(namespace) ?? context.clock;
1566
1567
  if (!isRecord2(body))
1567
1568
  return adminError(400, "expected a JSON object");
1568
1569
  if (body.reset === true)
1569
- context.clock.reset();
1570
+ clock.reset();
1570
1571
  if (body.freeze === true)
1571
- context.clock.freeze();
1572
+ clock.freeze();
1572
1573
  if (body.set !== void 0) {
1573
1574
  const instant = parseInstant(body.set);
1574
1575
  if (instant === void 0)
1575
1576
  return adminError(400, "set: expected epoch ms or ISO-8601");
1576
- context.clock.set(instant);
1577
+ clock.set(instant);
1577
1578
  }
1578
1579
  if (body.advance !== void 0) {
1579
1580
  const delta = parseDuration(body.advance);
1580
1581
  if (delta === void 0)
1581
1582
  return adminError(400, 'advance: expected ms or "15m"-style');
1582
- context.clock.advance(delta);
1583
+ clock.advance(delta);
1583
1584
  }
1584
1585
  if (body.freeze === false)
1585
- context.clock.unfreeze();
1586
- return json(200, context.clock.state());
1586
+ clock.unfreeze();
1587
+ return json(200, clock.state());
1587
1588
  },
1588
1589
  "GET /faults": () => json(200, { faults: context.faults.list() }),
1589
1590
  "POST /faults": ({ body, namespace }) => {
@@ -2425,7 +2426,33 @@ var faultResponse = (rule) => {
2425
2426
  };
2426
2427
  var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
2427
2428
  const entries = [];
2429
+ let isolated = false;
2430
+ const scopedRngs = /* @__PURE__ */ new Map();
2431
+ const random = (namespace) => {
2432
+ if (!isolated || namespace === "default")
2433
+ return rng;
2434
+ let scoped = scopedRngs.get(namespace);
2435
+ if (!scoped) {
2436
+ scoped = createRng(rng.seed);
2437
+ scopedRngs.set(namespace, scoped);
2438
+ }
2439
+ return scoped;
2440
+ };
2428
2441
  return {
2442
+ isolateNamespaces() {
2443
+ isolated = true;
2444
+ },
2445
+ snapshot(namespace) {
2446
+ return {
2447
+ rules: structuredClone(entries.map((entry, position) => ({
2448
+ ...entry.rule,
2449
+ remaining: entry.remaining,
2450
+ hits: entry.hits,
2451
+ position
2452
+ })).filter((entry) => entry.namespace === namespace)),
2453
+ rngState: random(namespace).state()
2454
+ };
2455
+ },
2429
2456
  add(rule) {
2430
2457
  const existing = entries.findIndex((e) => e.rule.id === rule.id);
2431
2458
  const entry = { rule, remaining: rule.count ?? null, hits: 0 };
@@ -2445,6 +2472,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
2445
2472
  },
2446
2473
  clear() {
2447
2474
  entries.length = 0;
2475
+ rng.reset();
2476
+ scopedRngs.clear();
2477
+ },
2478
+ restore(namespace, state) {
2479
+ for (let i = entries.length - 1; i >= 0; i--)
2480
+ if (entries[i]?.rule.namespace === namespace)
2481
+ entries.splice(i, 1);
2482
+ for (const { remaining, hits, position, ...rule } of state.rules)
2483
+ entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
2484
+ random(namespace).setState(state.rngState);
2448
2485
  },
2449
2486
  async take(candidate) {
2450
2487
  const hits = [];
@@ -2454,7 +2491,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
2454
2491
  if (!matches(entry.rule, candidate))
2455
2492
  continue;
2456
2493
  const rate = entry.rule.rate ?? 1;
2457
- if (rng.next() >= rate)
2494
+ if (random(candidate.namespace).next() >= rate)
2458
2495
  continue;
2459
2496
  entry.hits++;
2460
2497
  if (entry.remaining !== null)
@@ -2521,7 +2558,8 @@ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
2521
2558
  };
2522
2559
 
2523
2560
  // ../core/dist/metrics.js
2524
- var createMetrics = () => {
2561
+ var createMetrics = (partition = true) => {
2562
+ const namespaces = /* @__PURE__ */ new Map();
2525
2563
  let requests = 0;
2526
2564
  let faults = 0;
2527
2565
  let totalDurationMs = 0;
@@ -2529,6 +2567,14 @@ var createMetrics = () => {
2529
2567
  const unmatched = /* @__PURE__ */ new Map();
2530
2568
  return {
2531
2569
  record(entry) {
2570
+ if (partition) {
2571
+ let scoped = namespaces.get(entry.namespace);
2572
+ if (!scoped) {
2573
+ scoped = createMetrics(false);
2574
+ namespaces.set(entry.namespace, scoped);
2575
+ }
2576
+ scoped.record(entry);
2577
+ }
2532
2578
  requests++;
2533
2579
  totalDurationMs += entry.durationMs;
2534
2580
  if (entry.faultId !== void 0)
@@ -2540,7 +2586,7 @@ var createMetrics = () => {
2540
2586
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
2541
2587
  }
2542
2588
  },
2543
- report: () => ({
2589
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
2544
2590
  requests,
2545
2591
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
2546
2592
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -2549,8 +2595,13 @@ var createMetrics = () => {
2549
2595
  }),
2550
2596
  faults,
2551
2597
  totalDurationMs
2552
- }),
2553
- reset() {
2598
+ },
2599
+ reset(namespace) {
2600
+ if (namespace !== void 0) {
2601
+ namespaces.delete(namespace);
2602
+ return;
2603
+ }
2604
+ namespaces.clear();
2554
2605
  requests = 0;
2555
2606
  faults = 0;
2556
2607
  totalDurationMs = 0;