pg-interleave 0.5.0 → 0.6.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/dist/demo.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/demo.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const pg_1 = require("pg");
4
+ const index_1 = require("./index");
5
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6
+ async function runDemo() {
7
+ const chaos = new index_1.PgInterleaveClient("http://127.0.0.1:9090");
8
+ const connStr = "postgres://postgres:postgres@127.0.0.1:5433/postgres";
9
+ console.log("\x1b[36m[pg-interleave]\x1b[0m Resetting proxy control plane...");
10
+ await chaos.reset();
11
+ const observer = new pg_1.Client({ connectionString: connStr });
12
+ const workerA = new pg_1.Client({ connectionString: connStr });
13
+ await observer.connect();
14
+ await workerA.connect();
15
+ console.log("\x1b[36m[setup]\x1b[0m Initializing accounts (id=1, balance=100)...");
16
+ await observer.query(`
17
+ DROP TABLE IF EXISTS accounts CASCADE;
18
+ CREATE TABLE accounts (id int PRIMARY KEY, balance int NOT NULL);
19
+ INSERT INTO accounts VALUES (1, 100);
20
+ `);
21
+ console.log("\x1b[33m[barrier]\x1b[0m Arming barrier on: \x1b[1mUPDATE accounts\x1b[0m");
22
+ const barrier = await chaos.intercept({
23
+ client: workerA,
24
+ onQueryMatch: /UPDATE accounts/,
25
+ });
26
+ console.log("\x1b[32m[workerA]\x1b[0m Dispatching: UPDATE accounts SET balance = balance - 50 WHERE id = 1");
27
+ const pendingUpdate = workerA.query("UPDATE accounts SET balance = balance - 50 WHERE id = 1");
28
+ console.log("\x1b[33m[barrier]\x1b[0m Waiting for query to freeze in proxy pipeline...");
29
+ await barrier.wait();
30
+ console.log("\x1b[31m[proxy]\x1b[0m \x1b[1mPAUSED!\x1b[0m PostgreSQL has NOT seen the query yet.");
31
+ await sleep(1500);
32
+ const beforeResume = await observer.query("SELECT balance FROM accounts WHERE id = 1");
33
+ console.log(`\x1b[35m[observer]\x1b[0m Reading balance during freeze: \x1b[1m$${beforeResume.rows[0].balance}\x1b[0m`);
34
+ await sleep(1500);
35
+ console.log("\x1b[33m[barrier]\x1b[0m Releasing barrier: barrier.resume()...");
36
+ await barrier.resume();
37
+ await pendingUpdate;
38
+ console.log("\x1b[32m[workerA]\x1b[0m UPDATE finished successfully.");
39
+ const afterResume = await observer.query("SELECT balance FROM accounts WHERE id = 1");
40
+ console.log(`\x1b[35m[observer]\x1b[0m Final balance: \x1b[1m$${afterResume.rows[0].balance}\x1b[0m`);
41
+ await observer.end();
42
+ await workerA.end();
43
+ }
44
+ runDemo().catch(console.error);
package/dist/index.d.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import type { Client } from "pg";
2
+ export type SqlOperation = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "select" | "insert" | "update" | "delete";
3
+ export interface AstSelector {
4
+ operation?: SqlOperation;
5
+ table?: string;
6
+ }
7
+ export type BarrierAction = "PAUSE_BEFORE_QUERY" | "PAUSE_AFTER_RESPONSE" | "pause_before_query" | "pause_after_response";
8
+ export type ParamValue = string | number | boolean | null;
2
9
  export interface InterceptOptions {
3
10
  client?: Client;
4
11
  clientPid?: number;
5
12
  sessionTag?: string;
6
- onQueryMatch: string | RegExp;
7
- withParams?: Array<string | number | boolean | null>;
8
- action?: "PAUSE_BEFORE_QUERY" | "PAUSE_AFTER_RESPONSE" | "pause_before_query" | "pause_after_response";
13
+ onQueryMatch?: RegExp | string;
14
+ ast?: AstSelector;
15
+ withParams?: (ParamValue | "*")[];
16
+ action?: BarrierAction;
9
17
  times?: number;
10
18
  }
11
19
  export declare class BarrierHandle {
@@ -18,9 +26,6 @@ export declare class BarrierHandle {
18
26
  export declare class PgInterleaveClient {
19
27
  private readonly controlUrl;
20
28
  constructor(controlUrl?: string);
21
- /**
22
- * Appends session routing comment to an SQL query for connection pool compatibility
23
- */
24
29
  static tag(sql: string, sessionTag: string): string;
25
30
  reset(): Promise<void>;
26
31
  resumeNext(): Promise<{
package/dist/index.js CHANGED
@@ -42,9 +42,6 @@ class PgInterleaveClient {
42
42
  constructor(controlUrl = "http://127.0.0.1:9090") {
43
43
  this.controlUrl = controlUrl;
44
44
  }
45
- /**
46
- * Appends session routing comment to an SQL query for connection pool compatibility
47
- */
48
45
  static tag(sql, sessionTag) {
49
46
  return `/* interleave: session=${sessionTag} */ ${sql}`;
50
47
  }
@@ -83,34 +80,35 @@ class PgInterleaveClient {
83
80
  return res.rows[0].pid;
84
81
  }
85
82
  async intercept(options) {
83
+ if (!options.onQueryMatch && !options.ast) {
84
+ throw new Error("Either 'onQueryMatch' or 'ast' selector must be provided");
85
+ }
86
86
  let pid = options.clientPid;
87
87
  if (!pid && options.client) {
88
88
  pid = await this.getPid(options.client);
89
89
  }
90
- const pattern = typeof options.onQueryMatch === "string"
91
- ? options.onQueryMatch
92
- : options.onQueryMatch.source;
93
- const action = options.action
94
- ? options.action.toLowerCase()
95
- : "pause_before_query";
96
- const serializedParams = options.withParams !== undefined
97
- ? options.withParams.map((p) => (p === null || p === undefined ? null : String(p)))
98
- : undefined;
99
- const body = {
100
- query_pattern: pattern,
101
- action,
90
+ const payload = {
91
+ targetPid: pid,
92
+ sessionTag: options.sessionTag,
93
+ action: options.action ?? "pause_before_query",
102
94
  times: options.times ?? 1,
103
95
  };
104
- if (pid !== undefined)
105
- body.target_pid = pid;
106
- if (options.sessionTag !== undefined)
107
- body.session_tag = options.sessionTag;
108
- if (serializedParams !== undefined)
109
- body.with_params = serializedParams;
96
+ if (options.onQueryMatch) {
97
+ payload.queryPattern =
98
+ typeof options.onQueryMatch === "string"
99
+ ? options.onQueryMatch
100
+ : options.onQueryMatch.source;
101
+ }
102
+ if (options.ast) {
103
+ payload.ast = options.ast;
104
+ }
105
+ if (options.withParams) {
106
+ payload.withParams = options.withParams.map((p) => typeof p === "number" ? String(p) : p);
107
+ }
110
108
  const res = await fetch(`${this.controlUrl}/intercept`, {
111
109
  method: "POST",
112
110
  headers: { "Content-Type": "application/json" },
113
- body: JSON.stringify(body),
111
+ body: JSON.stringify(payload),
114
112
  });
115
113
  if (!res.ok) {
116
114
  const errBody = await res.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-interleave",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Deterministic PostgreSQL wire-protocol proxy client for concurrency and chaos testing",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",