tina4-nodejs 3.13.138 → 3.13.140

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.
Files changed (51) hide show
  1. package/CLAUDE.md +2 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +683 -329
  4. package/packages/cli/src/commands/generate.ts +7 -13
  5. package/packages/core/dist/index.js +688 -329
  6. package/packages/core/src/api.ts +33 -4
  7. package/packages/core/src/auth.ts +73 -6
  8. package/packages/core/src/authGate.ts +27 -20
  9. package/packages/core/src/graphql.ts +92 -0
  10. package/packages/core/src/index.ts +1 -0
  11. package/packages/core/src/job.ts +8 -1
  12. package/packages/core/src/mcp.ts +3 -1
  13. package/packages/core/src/middleware.ts +4 -2
  14. package/packages/core/src/push.ts +16 -0
  15. package/packages/core/src/queue.ts +15 -2
  16. package/packages/core/src/queueBackends/liteBackend.ts +15 -0
  17. package/packages/core/src/queueBackends/mongoBackend.ts +40 -6
  18. package/packages/core/src/server.ts +6 -1
  19. package/packages/core/src/sessionHandlers/databaseHandler.ts +60 -3
  20. package/packages/core/src/sso.ts +12 -4
  21. package/packages/core/src/ssoIdentity.ts +19 -0
  22. package/packages/core/src/ssrf.ts +221 -0
  23. package/packages/core/src/testClient.ts +3 -0
  24. package/packages/core/src/websocket.ts +4 -2
  25. package/packages/frond/dist/index.js +6 -0
  26. package/packages/frond/src/engine.ts +9 -0
  27. package/packages/orm/dist/index.js +675 -320
  28. package/packages/orm/src/adapters/firebird.ts +14 -45
  29. package/packages/orm/src/adapters/mssql.ts +21 -58
  30. package/packages/orm/src/adapters/mysql.ts +10 -49
  31. package/packages/orm/src/adapters/odbc.ts +11 -42
  32. package/packages/orm/src/adapters/postgres.ts +20 -60
  33. package/packages/orm/src/adapters/sqlCrud.ts +178 -0
  34. package/packages/orm/src/adapters/sqlite.ts +46 -66
  35. package/packages/orm/src/index.ts +1 -1
  36. package/types/core/src/api.d.ts +6 -0
  37. package/types/core/src/auth.d.ts +30 -0
  38. package/types/core/src/graphql.d.ts +25 -0
  39. package/types/core/src/index.d.ts +1 -0
  40. package/types/core/src/job.d.ts +1 -0
  41. package/types/core/src/push.d.ts +5 -0
  42. package/types/core/src/queue.d.ts +7 -1
  43. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  44. package/types/core/src/queueBackends/mongoBackend.d.ts +8 -1
  45. package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
  46. package/types/core/src/sso.d.ts +1 -0
  47. package/types/core/src/ssoIdentity.d.ts +10 -0
  48. package/types/core/src/ssrf.d.ts +19 -0
  49. package/types/orm/src/adapters/sqlCrud.d.ts +90 -0
  50. package/types/orm/src/adapters/sqlite.d.ts +17 -0
  51. package/types/orm/src/index.d.ts +1 -1
@@ -12,7 +12,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  * Install: npm install node-firebird
13
13
  * URL format: firebird://user:pass@host:port/path/to/database.fdb
14
14
  */
15
- import { firebirdDialect, buildInsert, buildSetClause, buildWhereClause, quoteIdentifierWith } from "./sqlDialect.js";
15
+ import { firebirdDialect, quoteIdentifierWith } from "./sqlDialect.js";
16
+ import { buildDelete, buildInsertRow, buildInsertRows, buildUpdate } from "./sqlCrud.js";
16
17
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
17
18
  import { SQLTranslator } from "../sqlTranslator.js";
18
19
  import { connectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -643,19 +644,15 @@ export class FirebirdAdapter implements DatabaseAdapter {
643
644
  // the batch reports affectedRows == row count and no lastInsertId (same as the
644
645
  // single-row path). See PostgresAdapter for the array-crash rationale.
645
646
  if (Array.isArray(data)) {
646
- if (data.length === 0) return { success: true, affectedRows: 0 };
647
- const keys = Object.keys(data[0]);
648
- const sql = buildInsert(FB_DIALECT, table, keys);
649
- const paramsList = data.map((row) => keys.map((k) => row[k]));
650
- const result = await this.executeManyAsync(sql, paramsList);
647
+ const batch = buildInsertRows(FB_DIALECT, table, data);
648
+ if (!batch) return { success: true, affectedRows: 0 };
649
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
651
650
  // The generator holds the LAST inserted id after the batch (FB-LASTID-GAP).
652
651
  const lastId = (await this.readGeneratorId(table)) ?? result.lastId;
653
652
  return { success: true, affectedRows: result.totalAffected, lastId: lastId ?? undefined };
654
653
  }
655
654
 
656
- const keys = Object.keys(data);
657
- const sql = buildInsert(FB_DIALECT, table, keys);
658
- const values = Object.values(data);
655
+ const { sql, values } = buildInsertRow(FB_DIALECT, table, data);
659
656
 
660
657
  // FAIL LOUD, like fetch/execute and the other three frameworks: a bad statement
661
658
  // RAISES and never returns a falsy result. Swallowing it into {success:false}
@@ -674,29 +671,12 @@ export class FirebirdAdapter implements DatabaseAdapter {
674
671
 
675
672
  async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
676
673
  this.ensureConnected();
677
- // Identifiers go through fbQuote, exactly like insertAsync. Firebird folds an
678
- // UNQUOTED identifier to uppercase, so a conventional `CREATE TABLE probe_t`
679
- // stores PROBE_T/ID — and a hand-rolled `"${k}"` emits lowercase-quoted "id",
680
- // which is a DIFFERENT, non-existent column. update and delete were therefore
681
- // broken on every conventionally-created Firebird table while insert worked.
682
- const setClauses = buildSetClause(FB_DIALECT, Object.keys(data));
683
-
684
- // A raw WHERE fragment + params is half the write_path contract's filter
685
- // form. Without this branch Object.keys("id = ?") yields the STRING INDICES
686
- // ["0","1",...] and the statement addresses columns that do not exist.
687
- // Firebird already uses `?`, so the fragment needs no rewriting.
688
- if (typeof filter === "string") {
689
- const where = filter ? ` WHERE ${filter}` : "";
690
- const affected = await this.executeReturningCount(
691
- `UPDATE ${fbQuote(table)} SET ${setClauses}${where}`,
692
- [...Object.values(data), ...(params ?? [])],
693
- );
694
- return { success: true, affectedRows: affected };
695
- }
696
-
697
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
698
- const sql = `UPDATE ${FB_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
699
- const values = [...Object.values(data), ...Object.values(filter)];
674
+ // FB_DIALECT.quote is fbQuote: Firebird folds an UNQUOTED identifier to
675
+ // uppercase, so a conventional `CREATE TABLE probe_t` stores PROBE_T/ID and a
676
+ // lowercase-quoted "id" would be a different, non-existent column. The
677
+ // composer routes every identifier through it. Firebird uses `?` natively, so
678
+ // a raw string filter needs no rewrite.
679
+ const { sql, values } = buildUpdate(FB_DIALECT, table, data, filter, params);
700
680
 
701
681
  const affected = await this.executeReturningCount(sql, values);
702
682
  return { success: true, affectedRows: affected };
@@ -708,19 +688,8 @@ export class FirebirdAdapter implements DatabaseAdapter {
708
688
 
709
689
  async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
710
690
  this.ensureConnected();
711
-
712
- // See updateAsync: truncate() calls this with "1 = 1", which walked the
713
- // string as an object — db.truncate() was broken outright.
714
- if (typeof filter === "string") {
715
- const where = filter ? ` WHERE ${filter}` : "";
716
- const affected = await this.executeReturningCount(`DELETE FROM ${fbQuote(table)}${where}`, params ?? []);
717
- return { success: true, affectedRows: affected };
718
- }
719
-
720
- // Same fbQuote policy as insert/update — see updateAsync.
721
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
722
- const sql = `DELETE FROM ${FB_DIALECT.quote(table)} WHERE ${whereClauses}`;
723
- const values = Object.values(filter);
691
+ // Same fbQuote (FB_DIALECT.quote) policy as insert/update; `?` markers native.
692
+ const { sql, values } = buildDelete(FB_DIALECT, table, filter, params);
724
693
 
725
694
  const affected = await this.executeReturningCount(sql, values);
726
695
  return { success: true, affectedRows: affected };
@@ -12,7 +12,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  * Install: npm install tedious
13
13
  * URL format: mssql://user:pass@host:port/database
14
14
  */
15
- import { ANSI_DIALECT, MSSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
15
+ import { ANSI_DIALECT, MSSQL_DIALECT } from "./sqlDialect.js";
16
+ import { buildDelete, buildInsertRow, buildInsertRows, buildUpdate } from "./sqlCrud.js";
16
17
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
17
18
  import { SQLTranslator } from "../sqlTranslator.js";
18
19
  import { connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -360,15 +361,13 @@ export class MssqlAdapter implements DatabaseAdapter {
360
361
  // tracked for a batch — affectedRows == row count is what callers rely on).
361
362
  // See PostgresAdapter for the array-crash rationale this branch fixes.
362
363
  if (Array.isArray(data)) {
363
- if (data.length === 0) return { success: true, affectedRows: 0 };
364
- const keys = Object.keys(data[0]);
365
364
  // `?` placeholders — executeManyAsync -> executeAsync runs convertPlaceholders,
366
- // which rewrites them to @p0, @p1, ... for tedious.
367
- // The batch path binds through executeMany, which converts "?" itself.
368
- const sql = buildInsert({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table, keys);
369
- const paramsList = data.map((row) => keys.map((k) => row[k]));
365
+ // which rewrites them to @p0, @p1, ... for tedious. So the batch INSERT is
366
+ // built with MSSQL's bracket quoting but the ANSI `?` marker.
367
+ const batch = buildInsertRows({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table, data);
368
+ if (!batch) return { success: true, affectedRows: 0 };
370
369
  try {
371
- const result = await this.executeManyAsync(sql, paramsList);
370
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
372
371
  if (result.lastId !== undefined) this._lastInsertId = result.lastId;
373
372
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
374
373
  } catch (e) {
@@ -376,11 +375,9 @@ export class MssqlAdapter implements DatabaseAdapter {
376
375
  }
377
376
  }
378
377
 
379
- const keys = Object.keys(data);
380
378
  // startAt 0: MSSQL BINDS by the marker name, so @p must start where the
381
379
  // binding loop starts. Shifting to 1 would name parameters that do not exist.
382
- const sql = buildInsert(MSSQL_DIALECT, table, keys, "; SELECT SCOPE_IDENTITY() AS id", 0);
383
- const values = Object.values(data);
380
+ const { sql, values } = buildInsertRow(MSSQL_DIALECT, table, data, "; SELECT SCOPE_IDENTITY() AS id", { startAt: 0 });
384
381
 
385
382
  try {
386
383
  const result = await this.execSqlPromise(sql, values);
@@ -406,32 +403,13 @@ export class MssqlAdapter implements DatabaseAdapter {
406
403
 
407
404
  async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
408
405
  this.ensureConnected();
409
- const dataKeys = Object.keys(data);
410
- let paramIndex = 0;
411
- const setClauses = buildSetClause(MSSQL_DIALECT, dataKeys, paramIndex);
412
- paramIndex += dataKeys.length;
413
-
414
- // A raw WHERE fragment + params is half the write_path contract's filter
415
- // form. Without this branch Object.keys("id = ?") yields the STRING INDICES
416
- // ["0","1",...], producing `WHERE [0] = @p1 AND [1] = @p2` — SQL Server then
417
- // reports an invalid column name '0'.
418
- if (typeof filter === "string") {
419
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
420
- const sql = `UPDATE ${MSSQL_DIALECT.quote(table)} SET ${setClauses}${where}`;
421
- const values = [...Object.values(data), ...(params ?? [])];
422
- try {
423
- const result = await this.execSqlPromise(sql, values);
424
- return { success: true, affectedRows: result.rowCount };
425
- } catch (e) {
426
- return { success: false, affectedRows: 0, error: (e as Error).message };
427
- }
428
- }
429
-
430
- const filterKeys = Object.keys(filter);
431
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
432
- paramIndex += filterKeys.length;
433
- const sql = `UPDATE ${MSSQL_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
434
- const values = [...Object.values(data), ...Object.values(filter)];
406
+ // @pN markers name from 0 (MSSQL binds by name); the WHERE markers continue
407
+ // past the SET values. A raw string filter's `?` are rewritten to @pN via
408
+ // convertPlaceholders at the correct start position.
409
+ const { sql, values } = buildUpdate(MSSQL_DIALECT, table, data, filter, params, {
410
+ startAt: 0,
411
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt),
412
+ });
435
413
 
436
414
  try {
437
415
  const result = await this.execSqlPromise(sql, values);
@@ -447,27 +425,12 @@ export class MssqlAdapter implements DatabaseAdapter {
447
425
 
448
426
  async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
449
427
  this.ensureConnected();
450
-
451
- // See updateAsync: truncate() calls this with "1 = 1", which became
452
- // `WHERE [0] = @p0 AND [1] = @p1 ...` — db.truncate() was broken outright.
453
- if (typeof filter === "string") {
454
- const sql = filter
455
- ? `DELETE FROM [${table}] WHERE ${this.bindable(filter, params)}`
456
- : `DELETE FROM [${table}]`;
457
- try {
458
- const result = await this.execSqlPromise(sql, params ?? []);
459
- return { success: true, affectedRows: result.rowCount };
460
- } catch (e) {
461
- return { success: false, affectedRows: 0, error: (e as Error).message };
462
- }
463
- }
464
-
465
- const filterKeys = Object.keys(filter);
466
- let paramIndex = 0;
467
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
468
- paramIndex += filterKeys.length;
469
- const sql = `DELETE FROM ${MSSQL_DIALECT.quote(table)} WHERE ${whereClauses}`;
470
- const values = Object.values(filter);
428
+ // bindable() rewrites a raw fragment's `?` to @pN only when params are bound
429
+ // (truncate() passes "1 = 1" with none). A hash filter numbers @pN from 0.
430
+ const { sql, values } = buildDelete(MSSQL_DIALECT, table, filter, params, {
431
+ startAt: 0,
432
+ convertFragment: (fragment) => this.bindable(fragment, params),
433
+ });
471
434
 
472
435
  try {
473
436
  const result = await this.execSqlPromise(sql, values);
@@ -12,7 +12,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  * Install: npm install mysql2
13
13
  * URL format: mysql://user:pass@host:port/database
14
14
  */
15
- import { MYSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause, quoteIdentifierWith } from "./sqlDialect.js";
15
+ import { MYSQL_DIALECT, quoteIdentifierWith } from "./sqlDialect.js";
16
+ import { buildDelete, buildInsertRow, buildInsertRows, buildUpdate } from "./sqlCrud.js";
16
17
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
17
18
  import { SQLTranslator } from "../sqlTranslator.js";
18
19
  import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -263,12 +264,10 @@ export class MysqlAdapter implements DatabaseAdapter {
263
264
  // executeManyAsync (ONE connection). See PostgresAdapter for the rationale;
264
265
  // without this branch a list crashed/mis-built SQL via Object.keys() on the array.
265
266
  if (Array.isArray(data)) {
266
- if (data.length === 0) return { success: true, affectedRows: 0 };
267
- const keys = Object.keys(data[0]);
268
- const sql = buildInsert(MYSQL_DIALECT, table, keys);
269
- const paramsList = data.map((row) => keys.map((k) => row[k]));
267
+ const batch = buildInsertRows(MYSQL_DIALECT, table, data);
268
+ if (!batch) return { success: true, affectedRows: 0 };
270
269
  try {
271
- const result = await this.executeManyAsync(sql, paramsList);
270
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
272
271
  if (result.lastId !== undefined) this._lastInsertId = result.lastId;
273
272
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
274
273
  } catch (e) {
@@ -276,9 +275,7 @@ export class MysqlAdapter implements DatabaseAdapter {
276
275
  }
277
276
  }
278
277
 
279
- const keys = Object.keys(data);
280
- const sql = buildInsert(MYSQL_DIALECT, table, keys);
281
- const values = Object.values(data);
278
+ const { sql, values } = buildInsertRow(MYSQL_DIALECT, table, data);
282
279
 
283
280
  try {
284
281
  const result = await this.queryPromise(sql, values);
@@ -299,28 +296,9 @@ export class MysqlAdapter implements DatabaseAdapter {
299
296
 
300
297
  async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
301
298
  this.ensureConnected();
302
- const setClauses = buildSetClause(MYSQL_DIALECT, Object.keys(data));
303
-
304
- // A raw WHERE fragment + params is half the write_path contract's filter
305
- // form. Without this branch Object.keys("id = ?") yields the STRING INDICES
306
- // ["0","1",...], producing `WHERE \`0\` = ? AND \`1\` = ?` — MySQL then
307
- // reports an unknown column '0'. MySQL already uses `?`, so the fragment
308
- // needs no placeholder rewriting.
309
- if (typeof filter === "string") {
310
- const where = filter ? ` WHERE ${filter}` : "";
311
- const sql = `UPDATE ${MYSQL_DIALECT.quote(table)} SET ${setClauses}${where}`;
312
- const values = [...Object.values(data), ...(params ?? [])];
313
- try {
314
- const result = await this.queryPromise(sql, values);
315
- return { success: true, affectedRows: result.affectedRows ?? 0 };
316
- } catch (e) {
317
- return { success: false, affectedRows: 0, error: (e as Error).message };
318
- }
319
- }
320
-
321
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
322
- const sql = `UPDATE ${MYSQL_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
323
- const values = [...Object.values(data), ...Object.values(filter)];
299
+ // MySQL uses `?` markers natively, so a raw string filter needs no rewrite;
300
+ // the string vs hash branch and value assembly are the shared composer's.
301
+ const { sql, values } = buildUpdate(MYSQL_DIALECT, table, data, filter, params);
324
302
 
325
303
  try {
326
304
  const result = await this.queryPromise(sql, values);
@@ -336,24 +314,7 @@ export class MysqlAdapter implements DatabaseAdapter {
336
314
 
337
315
  async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
338
316
  this.ensureConnected();
339
-
340
- // See updateAsync: truncate() calls this with "1 = 1", which became
341
- // `WHERE \`0\` = ? AND \`1\` = ? ...` — db.truncate() was broken outright.
342
- if (typeof filter === "string") {
343
- const sql = filter
344
- ? `DELETE FROM \`${table}\` WHERE ${filter}`
345
- : `DELETE FROM \`${table}\``;
346
- try {
347
- const result = await this.queryPromise(sql, params ?? []);
348
- return { success: true, affectedRows: result.affectedRows ?? 0 };
349
- } catch (e) {
350
- return { success: false, affectedRows: 0, error: (e as Error).message };
351
- }
352
- }
353
-
354
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
355
- const sql = `DELETE FROM ${MYSQL_DIALECT.quote(table)} WHERE ${whereClauses}`;
356
- const values = Object.values(filter);
317
+ const { sql, values } = buildDelete(MYSQL_DIALECT, table, filter, params);
357
318
 
358
319
  try {
359
320
  const result = await this.queryPromise(sql, values);
@@ -16,7 +16,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
16
16
  * The connection string after stripping the "odbc:///" prefix is passed
17
17
  * directly to odbc.connect(), so any valid ODBC connection string works.
18
18
  */
19
- import { ANSI_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
19
+ import { ANSI_DIALECT } from "./sqlDialect.js";
20
+ import { buildDelete, buildInsertRow, buildInsertRows, buildUpdate } from "./sqlCrud.js";
20
21
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
21
22
  import { createRequire } from "node:module";
22
23
  import { SQLTranslator } from "../sqlTranslator.js";
@@ -303,17 +304,13 @@ export class OdbcAdapter implements DatabaseAdapter {
303
304
  // Python master. The single-object path used to run Object.keys() over the
304
305
  // array here -> ["0","1",...], a broken INSERT, so batch insert never worked.
305
306
  if (Array.isArray(data)) {
306
- if (data.length === 0) return { success: true, affectedRows: 0 };
307
- const keys = Object.keys(data[0]);
308
- const sql = buildInsert(ANSI_DIALECT, table, keys);
309
- const paramsList = data.map((row) => keys.map((k) => (row as Record<string, unknown>)[k]));
310
- const { totalAffected } = await this.executeManyAsync(sql, paramsList);
307
+ const batch = buildInsertRows(ANSI_DIALECT, table, data);
308
+ if (!batch) return { success: true, affectedRows: 0 };
309
+ const { totalAffected } = await this.executeManyAsync(batch.sql, batch.paramsList);
311
310
  return { success: true, affectedRows: totalAffected };
312
311
  }
313
312
 
314
- const keys = Object.keys(data);
315
- const sql = buildInsert(ANSI_DIALECT, table, keys);
316
- const values = Object.values(data);
313
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table, data);
317
314
 
318
315
  try {
319
316
  const result = await this.connection.query(sql, values);
@@ -331,23 +328,10 @@ export class OdbcAdapter implements DatabaseAdapter {
331
328
  params?: unknown[],
332
329
  ): Promise<DatabaseResult> {
333
330
  this.ensureConnected();
334
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
335
-
336
- let whereSql: string;
337
- let values: unknown[];
338
- if (typeof filter === "string") {
339
- // The string form ("id = ?" + params). Without this branch Object.keys()
340
- // walked the STRING -> ["0","1",...], building a nonsense WHERE clause -
341
- // the exact bug the pg/mysql/mssql adapters guard against. params was also
342
- // dropped entirely (the method never took it), so a parameterised string
343
- // filter could not bind at all.
344
- whereSql = filter;
345
- values = [...Object.values(data), ...(params ?? [])];
346
- } else {
347
- whereSql = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
348
- values = [...Object.values(data), ...Object.values(filter)];
349
- }
350
- const sql = `UPDATE ${ANSI_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereSql}`;
331
+ // ODBC (the lab DSN is PostgreSQL) still passes `?` markers through — the
332
+ // driver binds them positionally — so no fragment rewrite. The string vs
333
+ // hash branch and value assembly are the shared composer's.
334
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table, data, filter, params);
351
335
 
352
336
  try {
353
337
  const result = await this.connection.query(sql, values);
@@ -374,22 +358,7 @@ export class OdbcAdapter implements DatabaseAdapter {
374
358
  return { success: true, affectedRows: totalAffected };
375
359
  }
376
360
 
377
- if (typeof filter === "string") {
378
- // The string form binds its own params (was dropped: query ran with []).
379
- const sql = filter
380
- ? `DELETE FROM "${table}" WHERE ${filter}`
381
- : `DELETE FROM "${table}"`;
382
- try {
383
- const result = await this.connection.query(sql, params ?? []);
384
- return { success: true, affectedRows: this.affectedCount(result) };
385
- } catch (e) {
386
- return { success: false, affectedRows: 0, error: (e as Error).message };
387
- }
388
- }
389
-
390
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
391
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table)} WHERE ${whereClauses}`;
392
- const values = Object.values(filter);
361
+ const { sql, values } = buildDelete(ANSI_DIALECT, table, filter, params);
393
362
 
394
363
  try {
395
364
  const result = await this.connection.query(sql, values);
@@ -12,7 +12,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
12
12
  * Install: npm install pg @types/pg
13
13
  * URL format: postgresql://user:pass@host:port/database
14
14
  */
15
- import { ANSI_DIALECT, POSTGRES_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
15
+ import { ANSI_DIALECT, POSTGRES_DIALECT } from "./sqlDialect.js";
16
+ import { buildDelete, buildInsertRow, buildInsertRows, buildUpdate } from "./sqlCrud.js";
16
17
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
17
18
  import { SQLTranslator } from "../sqlTranslator.js";
18
19
  import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -315,13 +316,11 @@ export class PostgresAdapter implements DatabaseAdapter {
315
316
  // — producing garbage SQL (mirrors the Python `'list' has no attribute keys`
316
317
  // crash this fix addresses).
317
318
  if (Array.isArray(data)) {
318
- if (data.length === 0) return { success: true, affectedRows: 0 };
319
- const keys = Object.keys(data[0]);
320
319
  // The batch path binds through executeManyAsync, which converts "?" itself.
321
- const sql = buildInsert(ANSI_DIALECT, table, keys);
322
- const paramsList = data.map((row) => keys.map((k) => row[k]));
320
+ const batch = buildInsertRows(ANSI_DIALECT, table, data);
321
+ if (!batch) return { success: true, affectedRows: 0 };
323
322
  try {
324
- const result = await this.executeManyAsync(sql, paramsList);
323
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
325
324
  if (result.lastId !== undefined) this._lastInsertId = result.lastId;
326
325
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
327
326
  } catch (e) {
@@ -329,9 +328,7 @@ export class PostgresAdapter implements DatabaseAdapter {
329
328
  }
330
329
  }
331
330
 
332
- const keys = Object.keys(data);
333
- const sql = buildInsert(POSTGRES_DIALECT, table, keys, " RETURNING *");
334
- const values = Object.values(data);
331
+ const { sql, values } = buildInsertRow(POSTGRES_DIALECT, table, data, " RETURNING *");
335
332
 
336
333
  try {
337
334
  const result = await this.client!.query(sql, values);
@@ -354,34 +351,13 @@ export class PostgresAdapter implements DatabaseAdapter {
354
351
 
355
352
  async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
356
353
  this.ensureConnected();
357
- const dataKeys = Object.keys(data);
358
- let paramIndex = 1;
359
- const setClauses = buildSetClause(POSTGRES_DIALECT, dataKeys, paramIndex);
360
- paramIndex += dataKeys.length;
361
-
362
- // A raw WHERE fragment + params is half the write_path contract's filter
363
- // form ("a string filter with params works the same as a hash filter").
364
- // Without this branch Object.keys("id = ?") yields the STRING INDICES
365
- // ["0","1",...], producing `WHERE "0" = $2 AND "1" = $3` — the engine then
366
- // reports `column "0" does not exist`. sqlite/mongodb/odbc already carried
367
- // this branch; postgres/mysql/mssql/firebird did not.
368
- if (typeof filter === "string") {
369
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
370
- const sql = `UPDATE ${POSTGRES_DIALECT.quote(table)} SET ${setClauses}${where}`;
371
- const values = [...Object.values(data), ...(params ?? [])];
372
- try {
373
- const result = await this.client!.query(sql, values);
374
- return { success: true, affectedRows: result.rowCount ?? 0 };
375
- } catch (e) {
376
- return { success: false, affectedRows: 0, error: (e as Error).message };
377
- }
378
- }
379
-
380
- const filterKeys = Object.keys(filter);
381
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
382
- paramIndex += filterKeys.length;
383
- const sql = `UPDATE ${POSTGRES_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
384
- const values = [...Object.values(data), ...Object.values(filter)];
354
+ // The composer numbers the SET markers from $1 and continues the WHERE
355
+ // markers past them; a raw string filter's `?` are rewritten to $N via
356
+ // convertPlaceholders (identity would leave `?`, which pg rejects).
357
+ const { sql, values } = buildUpdate(POSTGRES_DIALECT, table, data, filter, params, {
358
+ startAt: 1,
359
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt),
360
+ });
385
361
 
386
362
  try {
387
363
  const result = await this.client!.query(sql, values);
@@ -397,29 +373,13 @@ export class PostgresAdapter implements DatabaseAdapter {
397
373
 
398
374
  async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
399
375
  this.ensureConnected();
400
-
401
- // See updateAsync: a raw WHERE fragment must not be walked as an object.
402
- // truncate() calls this with "1 = 1", which became `WHERE "0" = $1 AND
403
- // "1" = $2 ...` and failed with `column "0" does not exist` — db.truncate()
404
- // was broken outright on PostgreSQL.
405
- if (typeof filter === "string") {
406
- const sql = filter
407
- ? `DELETE FROM "${table}" WHERE ${this.bindable(filter, params)}`
408
- : `DELETE FROM "${table}"`;
409
- try {
410
- const result = await this.client!.query(sql, params ?? []);
411
- return { success: true, affectedRows: result.rowCount ?? 0 };
412
- } catch (e) {
413
- return { success: false, affectedRows: 0, error: (e as Error).message };
414
- }
415
- }
416
-
417
- const filterKeys = Object.keys(filter);
418
- let paramIndex = 1;
419
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
420
- paramIndex += filterKeys.length;
421
- const sql = `DELETE FROM ${POSTGRES_DIALECT.quote(table)} WHERE ${whereClauses}`;
422
- const values = Object.values(filter);
376
+ // bindable() rewrites a raw fragment's `?` to $N only when params are bound
377
+ // (truncate() passes "1 = 1" with none — no rewrite, no error). A hash filter
378
+ // numbers its `$N` from 1.
379
+ const { sql, values } = buildDelete(POSTGRES_DIALECT, table, filter, params, {
380
+ startAt: 1,
381
+ convertFragment: (fragment) => this.bindable(fragment, params),
382
+ });
423
383
 
424
384
  try {
425
385
  const result = await this.client!.query(sql, values);