tina4-nodejs 3.13.98 → 3.13.99

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 (96) hide show
  1. package/CLAUDE.md +24 -25
  2. package/package.json +1 -2
  3. package/packages/cli/dist/bin.js +20620 -18995
  4. package/packages/cli/src/bin.ts +28 -71
  5. package/packages/cli/src/commands/migrate.ts +36 -75
  6. package/packages/cli/src/commands/migrateRollback.ts +10 -1
  7. package/packages/cli/src/commands/test.ts +92 -21
  8. package/packages/core/dist/index.js +20459 -18815
  9. package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
  10. package/packages/core/src/ai.ts +28 -12
  11. package/packages/core/src/api.ts +13 -5
  12. package/packages/core/src/background.ts +9 -3
  13. package/packages/core/src/devAdmin.ts +135 -20
  14. package/packages/core/src/dispatchPipeline.ts +185 -1
  15. package/packages/core/src/docs.ts +33 -5
  16. package/packages/core/src/env.ts +1 -1
  17. package/packages/core/src/errorOverlay.ts +39 -48
  18. package/packages/core/src/fakeData.ts +15 -0
  19. package/packages/core/src/index.ts +17 -6
  20. package/packages/core/src/logger.ts +892 -572
  21. package/packages/core/src/mcp.ts +9 -1
  22. package/packages/core/src/messenger.ts +31 -4
  23. package/packages/core/src/middleware.ts +169 -43
  24. package/packages/core/src/portTakeover.ts +232 -0
  25. package/packages/core/src/request.ts +57 -8
  26. package/packages/core/src/response.ts +67 -0
  27. package/packages/core/src/router.ts +35 -7
  28. package/packages/core/src/server.ts +450 -190
  29. package/packages/core/src/static.ts +81 -12
  30. package/packages/core/src/testClient.ts +126 -137
  31. package/packages/core/src/testing.ts +16 -12
  32. package/packages/core/src/types.ts +21 -9
  33. package/packages/core/src/version.ts +66 -0
  34. package/packages/core/src/websocket.ts +2 -2
  35. package/packages/core/src/websocketBackplane.ts +2 -2
  36. package/packages/frond/dist/index.js +31 -13
  37. package/packages/frond/src/engine.ts +39 -7
  38. package/packages/orm/dist/index.js +10879 -9258
  39. package/packages/orm/src/adapters/firebird.ts +200 -27
  40. package/packages/orm/src/adapters/mongodb.ts +160 -10
  41. package/packages/orm/src/adapters/mssql.ts +38 -11
  42. package/packages/orm/src/adapters/mysql.ts +24 -1
  43. package/packages/orm/src/adapters/odbc.ts +127 -29
  44. package/packages/orm/src/adapters/postgres.ts +18 -0
  45. package/packages/orm/src/adapters/sqlite.ts +93 -14
  46. package/packages/orm/src/autoCrud.ts +72 -8
  47. package/packages/orm/src/baseModel.ts +323 -71
  48. package/packages/orm/src/cachedDatabase.ts +48 -1
  49. package/packages/orm/src/database.ts +162 -59
  50. package/packages/orm/src/fakeData.ts +6 -2
  51. package/packages/orm/src/index.ts +4 -1
  52. package/packages/orm/src/migration.ts +95 -52
  53. package/packages/orm/src/query.ts +16 -4
  54. package/packages/orm/src/seeder.ts +43 -25
  55. package/packages/orm/src/sqlTranslator.ts +104 -19
  56. package/packages/orm/src/types.ts +97 -21
  57. package/packages/orm/src/validation.ts +5 -1
  58. package/packages/swagger/dist/index.js +3 -2
  59. package/packages/swagger/src/generator.ts +19 -4
  60. package/packages/swagger/src/ui.ts +6 -4
  61. package/types/cli/src/bin.d.ts +0 -22
  62. package/types/core/src/api.d.ts +11 -4
  63. package/types/core/src/background.d.ts +5 -2
  64. package/types/core/src/devAdmin.d.ts +35 -0
  65. package/types/core/src/dispatchPipeline.d.ts +41 -1
  66. package/types/core/src/errorOverlay.d.ts +13 -13
  67. package/types/core/src/index.d.ts +9 -6
  68. package/types/core/src/logger.d.ts +111 -185
  69. package/types/core/src/middleware.d.ts +40 -5
  70. package/types/core/src/portTakeover.d.ts +50 -0
  71. package/types/core/src/request.d.ts +15 -0
  72. package/types/core/src/response.d.ts +29 -0
  73. package/types/core/src/server.d.ts +92 -0
  74. package/types/core/src/testClient.d.ts +29 -3
  75. package/types/core/src/testing.d.ts +16 -12
  76. package/types/core/src/types.d.ts +21 -9
  77. package/types/core/src/version.d.ts +11 -0
  78. package/types/core/src/websocketBackplane.d.ts +1 -1
  79. package/types/frond/src/engine.d.ts +10 -0
  80. package/types/orm/src/adapters/firebird.d.ts +61 -2
  81. package/types/orm/src/adapters/mongodb.d.ts +20 -0
  82. package/types/orm/src/adapters/mssql.d.ts +11 -0
  83. package/types/orm/src/adapters/mysql.d.ts +11 -0
  84. package/types/orm/src/adapters/odbc.d.ts +35 -4
  85. package/types/orm/src/adapters/postgres.d.ts +11 -0
  86. package/types/orm/src/adapters/sqlite.d.ts +23 -4
  87. package/types/orm/src/baseModel.d.ts +45 -25
  88. package/types/orm/src/cachedDatabase.d.ts +27 -1
  89. package/types/orm/src/database.d.ts +56 -6
  90. package/types/orm/src/index.d.ts +3 -2
  91. package/types/orm/src/migration.d.ts +23 -5
  92. package/types/orm/src/query.d.ts +3 -0
  93. package/types/orm/src/seeder.d.ts +15 -2
  94. package/types/orm/src/sqlTranslator.d.ts +17 -4
  95. package/types/orm/src/types.d.ts +75 -16
  96. package/packages/core/src/errorOverlay.test.ts +0 -122
@@ -311,12 +311,43 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
311
311
 
312
312
  // ── DatabaseAdapter interface — writes flush, reads cache ──
313
313
 
314
+ /** ADR-0044 required capability — delegates to the wrapped adapter. */
315
+ connect(): void | Promise<void> {
316
+ return this.adapter.connect?.();
317
+ }
318
+
319
+ /** ADR-0044 required capability — delegates to the wrapped adapter. */
320
+ getDatabaseType(): string {
321
+ return this.adapter.getDatabaseType();
322
+ }
323
+
324
+ /**
325
+ * ADR-0044 required capability — a native boolean, readable and writable.
326
+ * A getter/setter pair (not a plain field) so it genuinely delegates to the
327
+ * wrapped adapter rather than drifting out of sync with its real setting.
328
+ */
329
+ get autocommit(): boolean {
330
+ return this.adapter.autocommit;
331
+ }
332
+
333
+ set autocommit(value: boolean) {
334
+ this.adapter.autocommit = value;
335
+ }
336
+
337
+ get supportsAtomicBatch(): boolean {
338
+ return this.adapter.supportsAtomicBatch ?? true;
339
+ }
340
+
341
+ set supportsAtomicBatch(value: boolean) {
342
+ this.adapter.supportsAtomicBatch = value;
343
+ }
344
+
314
345
  execute(sql: string, params?: unknown[]): unknown {
315
346
  if (this.enabled) this.invalidate();
316
347
  return this.adapter.execute(sql, params);
317
348
  }
318
349
 
319
- executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastId?: number | bigint } {
350
+ executeMany(sql: string, paramsList: unknown[][]): import("./types.js").DatabaseResult | { totalAffected: number; lastId?: number | bigint } {
320
351
  if (this.enabled) this.invalidate();
321
352
  return this.adapter.executeMany(sql, paramsList);
322
353
  }
@@ -546,6 +577,22 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
546
577
  : this.adapter.execute(sql, params);
547
578
  }
548
579
 
580
+ /**
581
+ * ADR-0044: the async passthrough executeMany() itself was missing (unlike
582
+ * its executeAsync/insertAsync siblings above), so adapterExecuteMany()'s
583
+ * `(adapter as any).executeManyAsync` check found nothing on THIS wrapper
584
+ * and fell through to the synchronous executeMany() below — which forwards
585
+ * to the wrapped adapter's OWN sync executeMany(), the throwing "Use
586
+ * executeManyAsync()" stub on every async-native adapter (Postgres/MySQL/
587
+ * MSSQL/Firebird/Mongo). Real bug, caught by executeManyFacadeTxn.test.ts.
588
+ */
589
+ async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<import("./types.js").DatabaseResult | { totalAffected: number; lastId?: number | bigint }> {
590
+ if (this.enabled) await this.invalidateAsync();
591
+ return (this.adapter as any).executeManyAsync
592
+ ? await (this.adapter as any).executeManyAsync(sql, paramsList)
593
+ : this.adapter.executeMany(sql, paramsList);
594
+ }
595
+
549
596
  async insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult> {
550
597
  if (this.enabled) await this.invalidateAsync();
551
598
  return (this.adapter as any).insertAsync
@@ -3,7 +3,7 @@ import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo
3
3
  import { DatabaseResult } from "./databaseResult.js";
4
4
  import { DatabaseUrl } from "./databaseUrl.js";
5
5
  import { CachedDatabaseAdapter, type CachedAdapterOptions } from "./cachedDatabase.js";
6
- import { QueryCache, SQLTranslator } from "./sqlTranslator.js";
6
+ import { QueryCache } from "./sqlTranslator.js";
7
7
 
8
8
  /**
9
9
  * v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL
@@ -73,6 +73,62 @@ export async function adapterExecute(
73
73
  : adapter.execute(sql, params);
74
74
  }
75
75
 
76
+ /**
77
+ * ADR-0044: the adapter-level batch primitive, called exactly once by
78
+ * Database#executeMany (never looped) — one aggregate DatabaseResult for the
79
+ * whole batch. Normalises whichever native shape an adapter returns: SQLite's
80
+ * `{success, affectedRows, lastId}` (the shared write shape already used by
81
+ * insert/update/delete) or an async-native adapter's `{totalAffected, lastId}`
82
+ * (pre-ADR-0044 shape, not yet unified per-adapter — normalised HERE at the
83
+ * one chokepoint every public write flows through, so the facade's contract
84
+ * is uniform without touching each of the five adapter files' internals).
85
+ */
86
+ export async function adapterExecuteMany(
87
+ adapter: DatabaseAdapter, sql: string, paramsList: unknown[][],
88
+ ): Promise<import("./types.js").DatabaseResult> {
89
+ const raw = (adapter as any).executeManyAsync
90
+ ? await (adapter as any).executeManyAsync(sql, paramsList)
91
+ : adapter.executeMany(sql, paramsList);
92
+ if (raw && typeof raw === "object" && "success" in raw) {
93
+ return raw as import("./types.js").DatabaseResult;
94
+ }
95
+ const legacy = raw as { totalAffected?: number; lastId?: number | bigint };
96
+ return { success: true, affectedRows: legacy?.totalAffected ?? 0, lastId: legacy?.lastId };
97
+ }
98
+
99
+ /**
100
+ * Insert one row (or a batch) through the adapter's OWN native insert path
101
+ * (each adapter's `buildInsert()`/`Dialect`, feature 3's SQL builder
102
+ * consolidation) instead of hand-built SQL. This is the ONLY correct way to
103
+ * insert into a caller-named table/columns: Firebird's Dialect quotes only
104
+ * when it has to (an unquoted identifier folds to UPPERCASE, so quoting a
105
+ * lower-case name makes it unfindable — SQL error -204 "Table unknown"),
106
+ * while PostgreSQL/MSSQL/SQLite quote unconditionally. A caller that hand-
107
+ * quotes with one fixed style (e.g. always `"col"`) works on three engines
108
+ * and silently breaks on the fourth. seedTable()/seedOrm() route through
109
+ * this so their engine portability matches insert()/insertAsync()'s, which
110
+ * the write-path + provider contract suites already prove on all four real
111
+ * engines (features 9/10/11/12).
112
+ */
113
+ export async function adapterInsert(
114
+ adapter: DatabaseAdapter, table: string, data: Record<string, unknown>,
115
+ ): Promise<unknown> {
116
+ const result: any = (adapter as any).insertAsync
117
+ ? await (adapter as any).insertAsync(table, data)
118
+ : adapter.insert(table, data);
119
+ // FAIL LOUD, matching adapterExecute(): the async adapters (Postgres/MSSQL/
120
+ // Firebird) already throw directly on a bad statement, but SQLiteAdapter's
121
+ // synchronous insert() CATCHES the driver error and returns
122
+ // `{ success: false, error }` instead (its own documented contract, unlike
123
+ // execute()'s always-throw). Without this check a constraint violation on
124
+ // SQLite silently reported success=seeded to seedTable()/seedOrm(), which
125
+ // count failures via a catch block that a non-throwing result never enters.
126
+ if (result && result.success === false) {
127
+ throw new Error(result.error ?? `insert into '${table}' failed`);
128
+ }
129
+ return result;
130
+ }
131
+
76
132
  export async function adapterStartTransaction(adapter: DatabaseAdapter): Promise<void> {
77
133
  if ((adapter as any).startTransactionAsync) await (adapter as any).startTransactionAsync();
78
134
  else adapter.startTransaction();
@@ -732,7 +788,19 @@ export class Database {
732
788
  }
733
789
  }
734
790
 
735
- /** Insert one row (object) or a batch of rows (array of objects) into a table. */
791
+ /**
792
+ * Insert one row (object) or a batch of rows (array of objects) into a table.
793
+ *
794
+ * FAIL LOUD, matching update()/delete()/truncate(): a real driver failure
795
+ * (e.g. a NOT NULL / UNIQUE constraint violation) throws rather than
796
+ * resolving to `{ success: false, affectedRows: 0 }`. The async adapters
797
+ * (Postgres/MySQL/MSSQL/Firebird) already throw directly from insertAsync();
798
+ * SQLiteAdapter.insert() is the one adapter that CATCHES the driver error
799
+ * and returns a `{ success: false, error }` result instead (its own
800
+ * documented contract for the synchronous path) — assertWrote is what
801
+ * converts that into the same thrown DatabaseException every other engine
802
+ * already produces, exactly as it already does for update/delete/truncate.
803
+ */
736
804
  async insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseWriteResult> {
737
805
  const adapter = this.getNextAdapter();
738
806
  const result = (adapter as any).insertAsync
@@ -741,7 +809,7 @@ export class Database {
741
809
  if (this.autoCommit && !this.inExplicitTransaction()) {
742
810
  try { await adapterCommit(adapter); } catch { /* no active transaction */ }
743
811
  }
744
- return result;
812
+ return Database.assertWrote(result, "insert", table);
745
813
  }
746
814
 
747
815
  /**
@@ -756,7 +824,19 @@ export class Database {
756
824
  let pk: string[] = [];
757
825
  try {
758
826
  const columns = await this.getColumns(table);
759
- pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
827
+ // ADR-0044 amendment: sort by primaryKeyPosition so a composite
828
+ // PRIMARY KEY (b, a) returns ["b", "a"] (declared key order), not
829
+ // table-column order. A column with no reported position sorts last.
830
+ const pkColumns = columns.filter((c) => c.primaryKey);
831
+ pkColumns.sort((a, b) => {
832
+ const posA = a.primaryKeyPosition ?? null;
833
+ const posB = b.primaryKeyPosition ?? null;
834
+ if (posA === posB) return 0;
835
+ if (posA === null) return 1;
836
+ if (posB === null) return -1;
837
+ return posA - posB;
838
+ });
839
+ pk = pkColumns.map((c) => c.name);
760
840
  } catch {
761
841
  pk = [];
762
842
  }
@@ -1049,21 +1129,35 @@ export class Database {
1049
1129
  * @param tableName - Name of the table to inspect.
1050
1130
  * @returns Array of column info objects: { name, type, nullable, default, primaryKey }.
1051
1131
  */
1052
- async getColumns(tableName: string): Promise<{ name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[]> {
1132
+ async getColumns(tableName: string): Promise<{ name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean; primaryKeyPosition?: number | null }[]> {
1053
1133
  return adapterColumns(this.getNextAdapter(), tableName);
1054
1134
  }
1055
1135
 
1056
1136
  /**
1057
- * Execute a SQL statement with multiple parameter sets (batch insert/update).
1058
- * Wraps all executions in a single transaction for atomicity and performance.
1137
+ * Execute a SQL statement with multiple parameter sets as ONE aggregate
1138
+ * batch (ADR-0044). Wraps the single delegated call in a transaction for
1139
+ * atomicity — never loops #execute itself.
1140
+ *
1141
+ * BREAKING (ADR-0044, pre-3.14.0): used to return one result PER ROW
1142
+ * (`unknown[]`, callers indexed into it) built by the FACADE looping
1143
+ * execute()/adapterExecute() per chunk or per row. It now delegates to the
1144
+ * adapter's OWN executeMany/executeManyAsync exactly once (DBA-D02: facade
1145
+ * delegates once, never a facade row loop) and returns the SAME shared
1146
+ * DatabaseResult shape insert()/update()/delete() already return
1147
+ * ({success, affectedRows, lastId}) — affectedRows is the total ROW count,
1148
+ * never the number of chunks/statements. A caller that indexed into the old
1149
+ * per-row array must switch to inspecting the aggregate result.
1059
1150
  *
1060
1151
  * @param sql - The SQL statement with parameter placeholders.
1061
- * @param paramSets - Array of parameter arrays, one per execution.
1062
- * @returns Array of results from each execution.
1152
+ * @param paramSets - Array of parameter arrays, one per row.
1153
+ * @returns The aggregate DatabaseResult for the whole batch.
1063
1154
  */
1064
- async executeMany(sql: string, paramSets: unknown[][] = []): Promise<unknown[]> {
1065
- const adapter = this.getNextAdapter();
1066
- const results: unknown[] = [];
1155
+ async executeMany(sql: string, paramSets: unknown[][] = []): Promise<DatabaseWriteResult> {
1156
+ // ADR-0044 (DBA-B01): empty input is a successful no-op — it opens no
1157
+ // transaction and calls no adapter.
1158
+ if (paramSets.length === 0) {
1159
+ return { success: true, affectedRows: 0 };
1160
+ }
1067
1161
 
1068
1162
  // Own the batch transaction ONLY when not already inside a caller's explicit
1069
1163
  // transaction. inExplicitTransaction() is true when startTransaction() has
@@ -1073,48 +1167,27 @@ export class Database {
1073
1167
  // rollback() would undo nothing (the batch rows survive). So: standalone
1074
1168
  // batch -> own BEGIN/COMMIT (atomic, all-or-nothing); nested batch -> join
1075
1169
  // the caller's transaction and let their commit/rollback decide. Mirrors the
1076
- // sibling execute()/insert()/update()/delete() owns-guard, the PostgreSQL
1077
- // adapter's executeManyAsync owns-guard, and the Python master
1078
- // (Database.execute_many delegating to adapter.execute_many's owns_txn guard).
1170
+ // sibling execute()/insert()/update()/delete() owns-guard and the Python
1171
+ // master (Database.execute_many delegating to adapter.execute_many's
1172
+ // owns_txn guard).
1079
1173
  const owns = !this.inExplicitTransaction();
1174
+ const adapter = this.getNextAdapter();
1080
1175
  if (owns) await adapterStartTransaction(adapter);
1081
1176
 
1082
- // ONE round-trip per CHUNK instead of one per ROW. Looping execute() here
1083
- // pays a full network round-trip for every row: 500 rows took 9848ms on
1084
- // PostgreSQL against 15.8ms as a single multi-row VALUES (625x), MySQL 216x,
1085
- // MSSQL 121x. buildBatchInserts returns an empty array for anything it
1086
- // cannot collapse safely — RETURNING, upserts, non-INSERT statements, ragged
1087
- // rows, Firebird — and the row-at-a-time loop then runs unchanged.
1088
- const batched = SQLTranslator.buildBatchInserts(sql, paramSets, this.dbType ?? "");
1089
-
1177
+ let result: DatabaseWriteResult;
1090
1178
  try {
1091
- if (batched.length > 0) {
1092
- let row = 0;
1093
- for (const [chunkSql, chunkParams] of batched) {
1094
- const result = await adapterExecute(adapter, chunkSql, chunkParams);
1095
- // executeMany's contract is ONE RESULT PER ROW, and callers index into
1096
- // it. Collapsing rows into chunks must not shorten the array, so each
1097
- // row reports the result of the statement that actually wrote it.
1098
- // Node is the only one of the four returning per-row results — Python,
1099
- // PHP and Ruby return a count or a single DatabaseResult — so this is
1100
- // the one place the collapse could have been observable.
1101
- const rowsInChunk = chunkParams.length / (paramSets[0]?.length || 1);
1102
- for (let i = 0; i < rowsInChunk && row < paramSets.length; i++, row++) {
1103
- results.push(result);
1104
- }
1105
- }
1106
- } else {
1107
- for (const params of paramSets) {
1108
- results.push(await adapterExecute(adapter, sql, params));
1109
- }
1110
- }
1179
+ // ONE delegated call — native batching (one multi-row round-trip instead
1180
+ // of one per row: 500 rows measured 9848ms on PostgreSQL row-at-a-time
1181
+ // against 15.8ms batched — 625x, MySQL 216x, MSSQL 121x) is the
1182
+ // ADAPTER's job, not a facade loop.
1183
+ result = await adapterExecuteMany(adapter, sql, paramSets);
1111
1184
  if (owns) await adapterCommit(adapter);
1112
1185
  } catch (e) {
1113
1186
  if (owns) await adapterRollback(adapter);
1114
1187
  throw e;
1115
1188
  }
1116
1189
 
1117
- return results;
1190
+ return result;
1118
1191
  }
1119
1192
 
1120
1193
  /** Return the last execute() error message, or null. */
@@ -1349,15 +1422,17 @@ export class Database {
1349
1422
  // Row likely already exists (PK conflict) — fine, keep going.
1350
1423
  try { await adapterRollback(adapter); } catch { /* nothing to roll back */ }
1351
1424
  }
1352
- await adapterExecute(adapter,
1353
- "UPDATE tina4_sequences SET current_value = current_value + 1 WHERE seq_name = ?",
1354
- [seqName],
1355
- );
1356
- try { await adapterCommit(adapter); } catch { /* no active transaction */ }
1425
+ // Single ATOMIC increment-and-return. The old path did the UPDATE then a
1426
+ // SEPARATE SELECT, with an `await` between them: another concurrent caller
1427
+ // could increment and commit in that window, so both read the same value and
1428
+ // returned a DUPLICATE id (a TOCTOU). PostgreSQL (the engine that reaches
1429
+ // this fallback) supports UPDATE ... RETURNING, so the value read is exactly
1430
+ // the one this statement wrote.
1357
1431
  const row = await adapterFetchOne<Record<string, unknown>>(adapter,
1358
- "SELECT current_value FROM tina4_sequences WHERE seq_name = ?",
1432
+ "UPDATE tina4_sequences SET current_value = current_value + 1 WHERE seq_name = ? RETURNING current_value",
1359
1433
  [seqName],
1360
1434
  );
1435
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
1361
1436
  if (!row || row.current_value == null) {
1362
1437
  throw new Error(`getNextId: sequence row '${seqName}' missing`);
1363
1438
  }
@@ -1377,9 +1452,17 @@ export class Database {
1377
1452
  async getNextId(table: string, pkColumn = "id", generatorName?: string): Promise<number> {
1378
1453
  const adapter = this.getNextAdapter();
1379
1454
 
1380
- // MongoDB uses ObjectId for _id by default; integer sequences fall through
1381
- // to the tina4_sequences table strategy below (which works on Mongo too
1382
- // because the adapter implements the SQL-ish methods over collections).
1455
+ // MongoDB a DEDICATED atomic counter (findOneAndUpdate($inc) keyed by _id),
1456
+ // monotonic and concurrency-safe. NEVER routed through the relational
1457
+ // tina4_sequences path: the Mongo SET-clause parser matches only `col = ?`
1458
+ // and DROPS the arithmetic `current_value + 1`, so the increment vanished
1459
+ // (empty $set) and every call returned the same id — a duplicate generator.
1460
+ if (this.dbType === "mongodb") {
1461
+ const mongo = adapter as unknown as { getNextId?: (t: string, p: string) => Promise<number> };
1462
+ if (typeof mongo.getNextId === "function") {
1463
+ return mongo.getNextId(table, pkColumn);
1464
+ }
1465
+ }
1383
1466
 
1384
1467
  // Firebird — use generators (atomic)
1385
1468
  if (this.dbType === "firebird") {
@@ -1399,29 +1482,41 @@ export class Database {
1399
1482
  // PostgreSQL — try sequence first, auto-create if missing, fall through to sequence table
1400
1483
  if (this.dbType === "postgres") {
1401
1484
  const seqName = generatorName ?? `${table.toLowerCase()}_${pkColumn.toLowerCase()}_seq`;
1485
+ // Fast path: the sequence already exists — nextval() is atomic.
1402
1486
  try {
1403
1487
  const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
1404
1488
  if (row?.next_id != null) {
1405
1489
  return Number(row.next_id);
1406
1490
  }
1407
1491
  } catch {
1408
- // Sequence doesn't exist try to auto-create it
1492
+ // Sequence missingcreate it idempotently below.
1409
1493
  }
1410
1494
 
1411
- // Auto-create sequence seeded from MAX
1495
+ // First use: create the sequence IDEMPOTENTLY (CREATE SEQUENCE IF NOT
1496
+ // EXISTS), seeded from MAX(pk). Two concurrent first-callers therefore
1497
+ // share ONE counter — the loser's create is a no-op, not an error, so it
1498
+ // never falls to the tina4_sequences table and draws a DUPLICATE id from a
1499
+ // second, independent counter (the first-use race).
1412
1500
  try {
1413
1501
  const maxRow = await adapterFetchOne<Record<string, unknown>>(adapter,
1414
1502
  `SELECT COALESCE(MAX(${pkColumn}), 0) AS max_id FROM ${table}`
1415
1503
  );
1416
1504
  const start = maxRow?.max_id != null ? Number(maxRow.max_id) + 1 : 1;
1417
- await adapterExecute(adapter, `CREATE SEQUENCE ${seqName} START WITH ${start}`);
1505
+ await adapterExecute(adapter, `CREATE SEQUENCE IF NOT EXISTS ${seqName} START WITH ${start}`);
1418
1506
  try { await adapterCommit(adapter); } catch { /* no active transaction */ }
1507
+ } catch {
1508
+ // A concurrent creator won the catalog race — the sequence exists now.
1509
+ }
1510
+
1511
+ // ALWAYS draw from the sequence now that it exists. Never fall to the
1512
+ // sequence table just because our own CREATE lost the race.
1513
+ try {
1419
1514
  const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
1420
1515
  if (row?.next_id != null) {
1421
1516
  return Number(row.next_id);
1422
1517
  }
1423
1518
  } catch {
1424
- // Fall through to sequence table
1519
+ // Truly cannot use a sequence — last-resort table below.
1425
1520
  }
1426
1521
  }
1427
1522
 
@@ -1518,7 +1613,11 @@ async function buildAdapterFromUrl(url: string, username?: string, password?: st
1518
1613
  }
1519
1614
  case "odbc": {
1520
1615
  const { OdbcAdapter } = await import("./adapters/odbc.js");
1521
- const adapter = new OdbcAdapter({ connectionString: parsed.connectionString ?? "" });
1616
+ const adapter = new OdbcAdapter({
1617
+ connectionString: parsed.connectionString ?? "",
1618
+ username: parsed.username ?? undefined,
1619
+ password: parsed.password ?? undefined,
1620
+ });
1522
1621
  await adapter.connect();
1523
1622
  return adapter;
1524
1623
  }
@@ -1723,7 +1822,11 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
1723
1822
  case "odbc": {
1724
1823
  const { OdbcAdapter } = await import("./adapters/odbc.js");
1725
1824
  const connStr = config?.connectionString ?? config?.url?.replace(/^odbc:\/\/\//, "") ?? "";
1726
- const adapter = new OdbcAdapter({ connectionString: connStr });
1825
+ const adapter = new OdbcAdapter({
1826
+ connectionString: connStr,
1827
+ username: resolvedUser ?? undefined,
1828
+ password: resolvedPassword ?? undefined,
1829
+ });
1727
1830
  await adapter.connect();
1728
1831
  return finished(adapter);
1729
1832
  }
@@ -39,8 +39,12 @@ export class FakeData extends CoreFakeData {
39
39
  return undefined;
40
40
  }
41
41
 
42
- // If there's a default, use it sometimes (but not always for variety)
43
- if (fieldDef.default !== undefined) {
42
+ // If there's a default, use it SOME of the time (SEED-NODE-DEFAULT fix) so
43
+ // a defaulted field still gets varied fakes across a seeded batch instead
44
+ // of the identical value on every row. This coin-flip reads from the same
45
+ // instance PRNG as every other generator, so it stays reproducible under
46
+ // a seed.
47
+ if (fieldDef.default !== undefined && this.boolean()) {
44
48
  return fieldDef.default;
45
49
  }
46
50
 
@@ -9,11 +9,13 @@ export type {
9
9
  RelationshipDefinition,
10
10
  } from "./types.js";
11
11
 
12
+ export { REQUIRED_ADAPTER_CAPABILITIES, NOT_REQUIRED_ON_ADAPTER } from "./types.js";
13
+
12
14
  export { DatabaseResult } from "./databaseResult.js";
13
15
  export type { ColumnInfoResult } from "./databaseResult.js";
14
16
  export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
15
17
  export {
16
- adapterFetch, adapterQuery, adapterFetchOne, adapterExecute,
18
+ adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterInsert,
17
19
  adapterStartTransaction, adapterCommit, adapterRollback,
18
20
  adapterTableExists, adapterTables, adapterColumns, adapterCreateTable,
19
21
  extractLastInsertId,
@@ -43,6 +45,7 @@ export {
43
45
  normalizeQuotes,
44
46
  sortMigrationFiles,
45
47
  shouldSkipCreateTable,
48
+ shouldSkipForFirebird,
46
49
  } from "./migration.js";
47
50
  export type { MigrationResult, MigrationStatus } from "./migration.js";
48
51
  export { AutoCrud, generateCrudRoutes, crudEligibleModels } from "./autoCrud.js";