@compforge/harness-toolbox 0.3.1 → 0.3.2

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.
@@ -50,9 +50,20 @@ try:
50
50
  )
51
51
  try:
52
52
  with connection.cursor(pymysql.cursors.DictCursor) as cursor:
53
- cursor.execute(request["sql"], request["values"])
54
- rows = cursor.fetchall()
55
- print(json.dumps({"rows": rows}, default=encode, ensure_ascii=False))
53
+ statements = request.get("statements")
54
+ if statements is None:
55
+ cursor.execute(request["sql"], request["values"])
56
+ rows = cursor.fetchall()
57
+ print(json.dumps({"rows": rows}, default=encode, ensure_ascii=False))
58
+ else:
59
+ # Batch: one process and one connection for independent statements;
60
+ # the per-statement deadline is re-armed before each execute.
61
+ results = []
62
+ for statement in statements:
63
+ signal.alarm(math.ceil(request["queryTimeoutMs"] / 1000))
64
+ cursor.execute(statement["sql"], statement["values"])
65
+ results.append({"rows": cursor.fetchall()})
66
+ print(json.dumps({"results": results}, default=encode, ensure_ascii=False))
56
67
  finally:
57
68
  connection.close()
58
69
  except Exception as error:
@@ -62,6 +73,9 @@ except Exception as error:
62
73
  finally:
63
74
  signal.alarm(0)
64
75
  `;
76
+ function parsePodResponse(raw) {
77
+ return JSON.parse(raw, (_key, value) => value?.type === "Buffer" && Array.isArray(value.data) ? Buffer.from(value.data) : value);
78
+ }
65
79
  async function queryMysqlViaPod(transport, target, sql, values, options) {
66
80
  const raw = await transport.run(POD_QUERY, {
67
81
  target,
@@ -69,11 +83,28 @@ async function queryMysqlViaPod(transport, target, sql, values, options) {
69
83
  values,
70
84
  ...options
71
85
  }, options.queryTimeoutMs + 3000);
72
- const response = JSON.parse(raw, (_key, value) => value?.type === "Buffer" && Array.isArray(value.data) ? Buffer.from(value.data) : value);
86
+ const response = parsePodResponse(raw);
73
87
  if (response.error || !Array.isArray(response.rows)) {
74
88
  throw new Error(`MySQL via Pod failed: ${response.error ?? "invalid response"}${response.code ? ` (${response.code})` : ""}`);
75
89
  }
76
90
  return response.rows;
77
91
  }
92
+ async function queryMysqlViaPodBatch(transport, target, statements, options) {
93
+ const raw = await transport.run(POD_QUERY, {
94
+ target,
95
+ statements: statements.map(({ sql, values }) => ({ sql: preparePodSql(sql, values), values })),
96
+ ...options
97
+ }, options.connectTimeoutMs + options.queryTimeoutMs * statements.length + 3000);
98
+ const response = parsePodResponse(raw);
99
+ if (response.error || !Array.isArray(response.results)) {
100
+ throw new Error(`MySQL via Pod batch failed: ${response.error ?? "invalid response"}${response.code ? ` (${response.code})` : ""}`);
101
+ }
102
+ return response.results.map((result, index) => {
103
+ if (!Array.isArray(result.rows)) {
104
+ throw new Error(`MySQL via Pod batch statement ${index} returned no rows`);
105
+ }
106
+ return result.rows;
107
+ });
108
+ }
78
109
 
79
- export { preparePodSql, queryMysqlViaPod };
110
+ export { preparePodSql, queryMysqlViaPod, queryMysqlViaPodBatch };
@@ -3,8 +3,9 @@ import {
3
3
  validateQueryLimits
4
4
  } from "./chunk-kbewz29a.js";
5
5
  import {
6
- queryMysqlViaPod
7
- } from "./chunk-s06hs7a8.js";
6
+ queryMysqlViaPod,
7
+ queryMysqlViaPodBatch
8
+ } from "./chunk-m91t44jh.js";
8
9
  import {
9
10
  ConcurrencyPool
10
11
  } from "./chunk-v3hxvnrc.js";
@@ -28,6 +29,22 @@ class MysqlDatabase {
28
29
  query(target, sql, values) {
29
30
  return this.#queries.run(() => this.#query(target, sql, values), this.options.signal);
30
31
  }
32
+ queryBatch(target, statements) {
33
+ return this.#queries.run(async () => {
34
+ if (!statements.length)
35
+ return [];
36
+ return this.#withSession(target, async (session) => {
37
+ if (session.kind === "python")
38
+ return queryMysqlViaPodBatch(session.transport, target, statements, this.options);
39
+ const results = [];
40
+ for (const { sql, values } of statements) {
41
+ const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
42
+ results.push(rows);
43
+ }
44
+ return results;
45
+ });
46
+ }, this.options.signal);
47
+ }
31
48
  queryReadonly(target, sql, values, limits) {
32
49
  validateQueryLimits(limits);
33
50
  return this.#queries.run(async () => {
@@ -60,7 +77,7 @@ class MysqlDatabase {
60
77
  }
61
78
  return pending;
62
79
  }
63
- async#query(target, sql, values) {
80
+ async#withSession(target, run) {
64
81
  this.options.signal?.throwIfAborted();
65
82
  const key = [target.host, target.port, target.database, target.user, target.password].join("\x00");
66
83
  const pending = this.#session(target);
@@ -68,10 +85,7 @@ class MysqlDatabase {
68
85
  try {
69
86
  session = await pending;
70
87
  this.options.signal?.throwIfAborted();
71
- if (session.kind === "python")
72
- return await queryMysqlViaPod(session.transport, target, sql, values, this.options);
73
- const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
74
- return rows;
88
+ return await run(session);
75
89
  } catch (error) {
76
90
  if (this.#connections.get(key) === pending)
77
91
  this.#connections.delete(key);
@@ -80,6 +94,14 @@ class MysqlDatabase {
80
94
  throw error;
81
95
  }
82
96
  }
97
+ async#query(target, sql, values) {
98
+ return this.#withSession(target, async (session) => {
99
+ if (session.kind === "python")
100
+ return queryMysqlViaPod(session.transport, target, sql, values, this.options);
101
+ const [rows] = await session.connection.execute({ sql, values: [...values], timeout: this.options.queryTimeoutMs });
102
+ return rows;
103
+ });
104
+ }
83
105
  async#open(target) {
84
106
  let reason;
85
107
  for (let i = 0;i < this.transports.length; i++) {
@@ -1,6 +1,6 @@
1
1
  import type { Client } from "../client";
2
2
  import { type Connection, type ConnectionOptions } from "mysql2/promise";
3
- import type { Database, DatabaseTarget, DatabaseRow, DatabaseQueryLimits, DatabaseQueryResult } from "./types";
3
+ import type { Database, DatabaseTarget, DatabaseRow, DatabaseQueryLimits, DatabaseQueryResult, SqlStatement } from "./types";
4
4
  import type { ConnectionSource, ClientLifecycle } from "../datasource";
5
5
  import { type Transport } from "../transport";
6
6
  export interface MysqlDatabaseOptions extends ClientLifecycle {
@@ -16,6 +16,12 @@ export declare class MysqlDatabase implements Database {
16
16
  private readonly connect;
17
17
  constructor(transports: readonly Transport[], options: MysqlDatabaseOptions, connect?: ConnectionFactory);
18
18
  query(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow[]>;
19
+ /**
20
+ * Independent statements in submission order, one session: a Pod transport pays the exec
21
+ * and connection cost once instead of per statement. Fail-fast like query(); use query()
22
+ * when a statement depends on an earlier result.
23
+ */
24
+ queryBatch(target: DatabaseTarget, statements: readonly SqlStatement[]): Promise<DatabaseRow[][]>;
19
25
  /** Bounded, single-statement prepared execution on an isolated READ ONLY session (native only). */
20
26
  queryReadonly(target: DatabaseTarget, sql: string, values: readonly unknown[], limits: DatabaseQueryLimits): Promise<DatabaseQueryResult>;
21
27
  initialize(target: DatabaseTarget): Promise<void>;
@@ -2,9 +2,9 @@ import {
2
2
  MysqlClient,
3
3
  MysqlDatabase,
4
4
  openMysql
5
- } from "../chunk-jkpwa8tb.js";
5
+ } from "../chunk-ssdxknsk.js";
6
6
  import"../chunk-kbewz29a.js";
7
- import"../chunk-s06hs7a8.js";
7
+ import"../chunk-m91t44jh.js";
8
8
  import"../chunk-v3hxvnrc.js";
9
9
  import"../chunk-xy4g56g4.js";
10
10
  export {
@@ -6,9 +6,9 @@ import {
6
6
  MysqlClient,
7
7
  MysqlDatabase,
8
8
  openMysql
9
- } from "../chunk-jkpwa8tb.js";
9
+ } from "../chunk-ssdxknsk.js";
10
10
  import"../chunk-kbewz29a.js";
11
- import"../chunk-s06hs7a8.js";
11
+ import"../chunk-m91t44jh.js";
12
12
  import"../chunk-v3hxvnrc.js";
13
13
  import"../chunk-xy4g56g4.js";
14
14
  export {
@@ -1,7 +1,16 @@
1
- import type { DatabaseRow, DatabaseTarget } from "./types";
1
+ import type { DatabaseRow, DatabaseTarget, SqlStatement } from "./types";
2
2
  import type { PodPythonTransport } from "../transport";
3
3
  export declare function preparePodSql(sql: string, values: readonly unknown[]): string;
4
4
  export declare function queryMysqlViaPod(transport: PodPythonTransport, target: DatabaseTarget, sql: string, values: readonly unknown[], options: {
5
5
  connectTimeoutMs: number;
6
6
  queryTimeoutMs: number;
7
7
  }): Promise<DatabaseRow[]>;
8
+ /**
9
+ * Independent statements over one Pod process and one MySQL connection: the per-exec cost
10
+ * (kubectl handshake, interpreter start, auth) is paid once. Fail-fast: a statement error
11
+ * aborts the batch, later statements never execute, and the whole batch throws.
12
+ */
13
+ export declare function queryMysqlViaPodBatch(transport: PodPythonTransport, target: DatabaseTarget, statements: readonly SqlStatement[], options: {
14
+ connectTimeoutMs: number;
15
+ queryTimeoutMs: number;
16
+ }): Promise<DatabaseRow[][]>;
package/dist/mysql/pod.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  preparePodSql,
3
- queryMysqlViaPod
4
- } from "../chunk-s06hs7a8.js";
3
+ queryMysqlViaPod,
4
+ queryMysqlViaPodBatch
5
+ } from "../chunk-m91t44jh.js";
5
6
  export {
7
+ queryMysqlViaPodBatch,
6
8
  queryMysqlViaPod,
7
9
  preparePodSql
8
10
  };
@@ -8,6 +8,10 @@ export interface DatabaseTarget extends DatabaseIdentity {
8
8
  database: string;
9
9
  }
10
10
  export type DatabaseRow = Record<string, unknown>;
11
+ export interface SqlStatement {
12
+ sql: string;
13
+ values: readonly unknown[];
14
+ }
11
15
  export interface DatabaseQueryLimits {
12
16
  timeoutMs: number;
13
17
  maxRows: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {