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.
- package/CLAUDE.md +24 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20620 -18995
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20459 -18815
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +28 -12
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +31 -13
- package/packages/frond/src/engine.ts +39 -7
- package/packages/orm/dist/index.js +10879 -9258
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +10 -0
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- 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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
1058
|
-
* Wraps
|
|
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
|
|
1062
|
-
* @returns
|
|
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<
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
|
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
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
)
|
|
1356
|
-
|
|
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
|
-
"
|
|
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
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
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
|
|
1492
|
+
// Sequence missing — create it idempotently below.
|
|
1409
1493
|
}
|
|
1410
1494
|
|
|
1411
|
-
//
|
|
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
|
-
//
|
|
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({
|
|
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({
|
|
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
|
|
43
|
-
|
|
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";
|