tina4-nodejs 3.13.45 → 3.13.46

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/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.45)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.46)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.13.45",
6
+ "version": "3.13.46",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript \u2014 54 built-in features, zero dependencies",
@@ -40,6 +40,10 @@ export interface MssqlConfig {
40
40
  export class MssqlAdapter implements DatabaseAdapter {
41
41
  private connection: any = null;
42
42
  private _lastInsertId: number | bigint | null = null;
43
+ // True between startTransactionAsync() and commit/rollback. executeManyAsync
44
+ // uses it to decide whether IT owns the batch transaction (mirrors the Python
45
+ // master's owns_txn guard) so it never double-BEGINs inside an explicit one.
46
+ private _inTransaction = false;
43
47
 
44
48
  constructor(private config: MssqlConfig | string) {}
45
49
 
@@ -179,10 +183,26 @@ export class MssqlAdapter implements DatabaseAdapter {
179
183
  }
180
184
 
181
185
  async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
186
+ // Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
187
+ // a bad row mid-batch rolls back the rows already inserted instead of
188
+ // leaving a partial write. Mirrors the documented "wrapped in a transaction"
189
+ // contract, the SQLite adapter, and the Python master's execute_many. Only
190
+ // own the transaction when not already inside an explicit one (owns guard),
191
+ // so a batch insert nested in a caller's startTransaction() just joins it.
192
+ const owns = !this._inTransaction;
193
+ if (owns) await this.startTransactionAsync();
182
194
  let totalAffected = 0;
183
- for (const params of paramsList) {
184
- await this.executeAsync(sql, params);
185
- totalAffected++;
195
+ try {
196
+ for (const params of paramsList) {
197
+ await this.executeAsync(sql, params);
198
+ totalAffected++;
199
+ }
200
+ if (owns) await this.commitAsync();
201
+ } catch (e) {
202
+ if (owns) {
203
+ try { await this.rollbackAsync(); } catch { /* surface the original error */ }
204
+ }
205
+ throw e;
186
206
  }
187
207
  return { totalAffected };
188
208
  }
@@ -275,7 +295,11 @@ export class MssqlAdapter implements DatabaseAdapter {
275
295
  if (id !== null) this._lastInsertId = id;
276
296
  return {
277
297
  success: true,
278
- rowsAffected: result.rowCount,
298
+ // A single-object insert affects exactly one row. Do NOT use
299
+ // result.rowCount here: the statement is "INSERT ...; SELECT
300
+ // SCOPE_IDENTITY()", and tedious sums the row counts of BOTH statements
301
+ // (1 for the INSERT + 1 for the SELECT), which reported rowsAffected=2.
302
+ rowsAffected: 1,
279
303
  lastInsertId: id ?? undefined,
280
304
  };
281
305
  } catch (e) {
@@ -331,7 +355,16 @@ export class MssqlAdapter implements DatabaseAdapter {
331
355
  }
332
356
 
333
357
  async startTransactionAsync(): Promise<void> {
334
- await this.executeAsync("BEGIN TRANSACTION");
358
+ // Use tedious's NATIVE transaction API, NOT a raw "BEGIN TRANSACTION" via
359
+ // execSql: every adapter statement runs through sp_executesql (an RPC), and
360
+ // SQL Server forbids changing @@TRANCOUNT inside an sp_executesql call, so a
361
+ // raw BEGIN raised "Transaction count ... mismatching BEGIN and COMMIT".
362
+ // beginTransaction manages the transaction at the TDS protocol level, so the
363
+ // INSERTs inside it commit/rollback atomically.
364
+ await new Promise<void>((resolve, reject) => {
365
+ this.connection.beginTransaction((err: Error | null) => (err ? reject(err) : resolve()));
366
+ });
367
+ this._inTransaction = true;
335
368
  }
336
369
 
337
370
  commit(): void {
@@ -339,7 +372,10 @@ export class MssqlAdapter implements DatabaseAdapter {
339
372
  }
340
373
 
341
374
  async commitAsync(): Promise<void> {
342
- await this.executeAsync("COMMIT");
375
+ await new Promise<void>((resolve, reject) => {
376
+ this.connection.commitTransaction((err: Error | null) => (err ? reject(err) : resolve()));
377
+ });
378
+ this._inTransaction = false;
343
379
  }
344
380
 
345
381
  rollback(): void {
@@ -347,7 +383,10 @@ export class MssqlAdapter implements DatabaseAdapter {
347
383
  }
348
384
 
349
385
  async rollbackAsync(): Promise<void> {
350
- await this.executeAsync("ROLLBACK");
386
+ await new Promise<void>((resolve, reject) => {
387
+ this.connection.rollbackTransaction((err: Error | null) => (err ? reject(err) : resolve()));
388
+ });
389
+ this._inTransaction = false;
351
390
  }
352
391
 
353
392
  tables(): string[] {
@@ -39,6 +39,10 @@ export interface MysqlConfig {
39
39
  export class MysqlAdapter implements DatabaseAdapter {
40
40
  private connection: any = null;
41
41
  private _lastInsertId: number | bigint | null = null;
42
+ // True between startTransactionAsync() and commit/rollback. executeManyAsync
43
+ // uses it to decide whether IT owns the batch transaction (mirrors the Python
44
+ // master's owns_txn guard) so it never double-BEGINs inside an explicit one.
45
+ private _inTransaction = false;
42
46
 
43
47
  constructor(private config: MysqlConfig | string) {}
44
48
 
@@ -108,12 +112,28 @@ export class MysqlAdapter implements DatabaseAdapter {
108
112
  }
109
113
 
110
114
  async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
115
+ // Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
116
+ // a bad row mid-batch rolls back the rows already inserted instead of
117
+ // leaving a partial write. Mirrors the documented "wrapped in a transaction"
118
+ // contract, the SQLite adapter, and the Python master's execute_many. Only
119
+ // own the transaction when not already inside an explicit one (owns guard),
120
+ // so a batch insert nested in a caller's startTransaction() just joins it.
121
+ const owns = !this._inTransaction;
122
+ if (owns) await this.startTransactionAsync();
111
123
  let totalAffected = 0;
112
124
  let lastId: number | bigint | undefined;
113
- for (const params of paramsList) {
114
- const result = await this.executeAsync(sql, params) as any;
115
- totalAffected += result?.affectedRows ?? 1;
116
- if (result?.insertId) lastId = result.insertId;
125
+ try {
126
+ for (const params of paramsList) {
127
+ const result = await this.executeAsync(sql, params) as any;
128
+ totalAffected += result?.affectedRows ?? 1;
129
+ if (result?.insertId) lastId = result.insertId;
130
+ }
131
+ if (owns) await this.commitAsync();
132
+ } catch (e) {
133
+ if (owns) {
134
+ try { await this.rollbackAsync(); } catch { /* surface the original error */ }
135
+ }
136
+ throw e;
117
137
  }
118
138
  return { totalAffected, lastInsertId: lastId };
119
139
  }
@@ -248,6 +268,7 @@ export class MysqlAdapter implements DatabaseAdapter {
248
268
 
249
269
  async startTransactionAsync(): Promise<void> {
250
270
  await this.executeAsync("START TRANSACTION");
271
+ this._inTransaction = true;
251
272
  }
252
273
 
253
274
  commit(): void {
@@ -256,6 +277,7 @@ export class MysqlAdapter implements DatabaseAdapter {
256
277
 
257
278
  async commitAsync(): Promise<void> {
258
279
  await this.executeAsync("COMMIT");
280
+ this._inTransaction = false;
259
281
  }
260
282
 
261
283
  rollback(): void {
@@ -264,6 +286,7 @@ export class MysqlAdapter implements DatabaseAdapter {
264
286
 
265
287
  async rollbackAsync(): Promise<void> {
266
288
  await this.executeAsync("ROLLBACK");
289
+ this._inTransaction = false;
267
290
  }
268
291
 
269
292
  tables(): string[] {
@@ -72,6 +72,10 @@ export class PostgresAdapter implements DatabaseAdapter {
72
72
  // `id uuid PRIMARY KEY DEFAULT gen_random_uuid()` shape) returns its id as a
73
73
  // 36-char string via RETURNING, not a SERIAL integer (#256).
74
74
  private _lastInsertId: number | bigint | string | null = null;
75
+ // True between startTransactionAsync() and commit/rollback. executeManyAsync
76
+ // uses it to decide whether IT owns the batch transaction (mirrors the Python
77
+ // master's owns_txn guard) so it never double-BEGINs inside an explicit one.
78
+ private _inTransaction = false;
75
79
 
76
80
  constructor(private config: PostgresConfig | string) {}
77
81
 
@@ -150,14 +154,30 @@ export class PostgresAdapter implements DatabaseAdapter {
150
154
 
151
155
  /** Async executeMany for real usage. */
152
156
  async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
157
+ // Run the whole batch in ONE transaction so it is atomic (all-or-nothing) —
158
+ // a bad row mid-batch rolls back the rows already inserted instead of
159
+ // leaving a partial write. Mirrors the documented "wrapped in a transaction"
160
+ // contract, the SQLite adapter, and the Python master's execute_many. Only
161
+ // own the transaction when not already inside an explicit one (owns guard),
162
+ // so a batch insert nested in a caller's startTransaction() just joins it.
163
+ const owns = !this._inTransaction;
164
+ if (owns) await this.startTransactionAsync();
153
165
  let totalAffected = 0;
154
166
  let lastId: number | bigint | undefined;
155
- for (const params of paramsList) {
156
- const result = await this.executeAsync(sql, params);
157
- totalAffected++;
158
- if (result && typeof result === "object" && "lastInsertId" in (result as any)) {
159
- lastId = (result as any).lastInsertId;
167
+ try {
168
+ for (const params of paramsList) {
169
+ const result = await this.executeAsync(sql, params);
170
+ totalAffected++;
171
+ if (result && typeof result === "object" && "lastInsertId" in (result as any)) {
172
+ lastId = (result as any).lastInsertId;
173
+ }
174
+ }
175
+ if (owns) await this.commitAsync();
176
+ } catch (e) {
177
+ if (owns) {
178
+ try { await this.rollbackAsync(); } catch { /* surface the original error */ }
160
179
  }
180
+ throw e;
161
181
  }
162
182
  return { totalAffected, lastInsertId: lastId };
163
183
  }