tina4-nodejs 3.13.94 → 3.13.95
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 +157 -28
- package/README.md +1 -1
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +32418 -29638
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +32364 -29501
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +5 -4
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +9 -13
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +6 -9
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +751 -414
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -147
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/orm/dist/index.js +22367 -19504
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +60 -24
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +237 -197
- package/packages/orm/src/databaseResult.ts +65 -13
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -3
- package/packages/orm/src/migration.ts +18 -3
- package/packages/orm/src/queryBuilder.ts +38 -4
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +15 -4
- package/types/core/src/ai.d.ts +1 -1
- package/types/core/src/auth.d.ts +28 -5
- package/types/core/src/background.d.ts +3 -3
- package/types/core/src/cache.d.ts +15 -12
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/dotenv.d.ts +38 -16
- package/types/core/src/index.d.ts +5 -6
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +2 -2
- package/types/core/src/metrics.d.ts +25 -61
- package/types/core/src/middleware.d.ts +134 -11
- package/types/core/src/queue.d.ts +54 -5
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
- package/types/core/src/router.d.ts +14 -3
- package/types/core/src/server.d.ts +15 -0
- package/types/core/src/session.d.ts +87 -2
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
- package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +28 -5
- package/types/core/src/websocket.d.ts +26 -0
- package/types/orm/src/adapters/firebird.d.ts +55 -10
- package/types/orm/src/adapters/mongodb.d.ts +2 -2
- package/types/orm/src/adapters/mssql.d.ts +18 -11
- package/types/orm/src/adapters/mysql.d.ts +11 -10
- package/types/orm/src/adapters/odbc.d.ts +9 -12
- package/types/orm/src/adapters/postgres.d.ts +11 -10
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +15 -3
- package/types/orm/src/baseModel.d.ts +45 -9
- package/types/orm/src/cachedDatabase.d.ts +18 -5
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +72 -26
- package/types/orm/src/databaseResult.d.ts +24 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +102 -43
- package/types/orm/src/index.d.ts +5 -2
- package/types/orm/src/queryBuilder.d.ts +23 -3
- package/types/orm/src/sqlTranslator.d.ts +126 -2
- package/types/orm/src/types.d.ts +14 -4
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Install: npm install node-firebird
|
|
5
5
|
* URL format: firebird://user:pass@host:port/path/to/database.fdb
|
|
6
6
|
*/
|
|
7
|
+
import { firebirdDialect, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
7
8
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
9
|
import { SQLTranslator } from "../sqlTranslator.js";
|
|
10
|
+
import { connectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
9
11
|
import { createRequire } from "node:module";
|
|
10
12
|
|
|
11
13
|
let firebird: any = null;
|
|
@@ -135,6 +137,66 @@ export interface FirebirdConfig {
|
|
|
135
137
|
charset?: string;
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Quote an identifier the way Firebird actually stores it: UPPERCASE.
|
|
142
|
+
*
|
|
143
|
+
* Firebird folds an UNQUOTED identifier to upper case and treats a QUOTED one as
|
|
144
|
+
* case-sensitive. So after the ordinary `CREATE TABLE probe_t (...)` the table is
|
|
145
|
+
* PROBE_T, and `INSERT INTO "probe_t"` matches nothing:
|
|
146
|
+
*
|
|
147
|
+
* Dynamic SQL Error / Table unknown / probe_t
|
|
148
|
+
*
|
|
149
|
+
* That broke the insert path against every conventionally-created table, columns
|
|
150
|
+
* included. A name the caller has ALREADY quoted is passed through untouched,
|
|
151
|
+
* which is the escape hatch for a genuinely case-sensitive `CREATE TABLE "orders"`.
|
|
152
|
+
*/
|
|
153
|
+
export function fbQuote(name: string): string {
|
|
154
|
+
if (!name) return name;
|
|
155
|
+
const trimmed = name.trim();
|
|
156
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed;
|
|
157
|
+
return `"${trimmed.toUpperCase().replace(/"/g, '""')}"`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Firebird's dialect for the shared CRUD builder: its own upper-casing quoter,
|
|
162
|
+
* plain "?" markers. Defined here rather than in sqlDialect.ts because the
|
|
163
|
+
* quoting rule is a Firebird fact, not a generic one.
|
|
164
|
+
*/
|
|
165
|
+
const FB_DIALECT = firebirdDialect(fbQuote);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Firebird's stored column name, folded back only when it was folded.
|
|
169
|
+
*
|
|
170
|
+
* Firebird's identifier folding is ASYMMETRIC. An unquoted `AS x` is stored
|
|
171
|
+
* UPPERCASE, so the driver hands back "X" where every other engine Tina4
|
|
172
|
+
* supports gives "x" — PostgreSQL folds to lower, and MySQL, SQLite and MSSQL
|
|
173
|
+
* preserve what you wrote. Portable code reading row.x broke on Firebird alone.
|
|
174
|
+
*
|
|
175
|
+
* A QUOTED `AS "MyCol"` is stored exactly as written, and that case is
|
|
176
|
+
* deliberate — the caller asked for it — so it is left alone. Folding
|
|
177
|
+
* unconditionally makes a mixed-case key unreachable, the same asymmetric trap
|
|
178
|
+
* that made tableExists miss quoted tables.
|
|
179
|
+
*
|
|
180
|
+
* So: fold back only a name carrying no lowercase letter, the only thing
|
|
181
|
+
* unquoted folding can produce. A quoted ALL-CAPS name is genuinely
|
|
182
|
+
* indistinguishable from a folded one and is lowercased too; that ambiguity is
|
|
183
|
+
* Firebird's, and it is the one spelling this cannot round-trip.
|
|
184
|
+
*/
|
|
185
|
+
export function firebirdColumnName(raw: string): string {
|
|
186
|
+
const name = raw.trim();
|
|
187
|
+
return name === name.toUpperCase() ? name.toLowerCase() : name;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Apply {@link firebirdColumnName} across one row's keys. */
|
|
191
|
+
function foldColumnNames<T>(row: T): T {
|
|
192
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) return row;
|
|
193
|
+
const out: Record<string, unknown> = {};
|
|
194
|
+
for (const [k, v] of Object.entries(row as Record<string, unknown>)) {
|
|
195
|
+
out[firebirdColumnName(k)] = v;
|
|
196
|
+
}
|
|
197
|
+
return out as T;
|
|
198
|
+
}
|
|
199
|
+
|
|
138
200
|
export class FirebirdAdapter implements DatabaseAdapter {
|
|
139
201
|
private db: any = null;
|
|
140
202
|
private transaction: any = null;
|
|
@@ -191,15 +253,20 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
191
253
|
fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
|
|
192
254
|
}
|
|
193
255
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
})
|
|
202
|
-
|
|
256
|
+
// node-firebird has NO connect-timeout option of its own, so there is no
|
|
257
|
+
// driver timer to translate and the outer bound is the ONLY thing standing
|
|
258
|
+
// between a silent driver and a permanently hung boot. This is the adapter
|
|
259
|
+
// the 16-minute probe measured.
|
|
260
|
+
this.db = await withConnectTimeout(
|
|
261
|
+
() => new Promise<any>((resolve, reject) => {
|
|
262
|
+
fb.attach(fbConfig, (err: Error | null, db: any) => (err ? reject(err) : resolve(db)));
|
|
263
|
+
}),
|
|
264
|
+
connectTimeoutMillis(),
|
|
265
|
+
fbConfig.host,
|
|
266
|
+
fbConfig.port,
|
|
267
|
+
// Answered after we gave up: detach so the socket does not outlive the boot.
|
|
268
|
+
(db: any) => { try { db?.detach?.(() => {}); } catch { /* already gone */ } },
|
|
269
|
+
);
|
|
203
270
|
}
|
|
204
271
|
|
|
205
272
|
private parseUrl(url: string): { host?: string; port?: number; user?: string; password?: string; database?: string } {
|
|
@@ -312,7 +379,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
312
379
|
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
313
380
|
this.ensureConnected();
|
|
314
381
|
const rows = await this.queryPromise(sql, params);
|
|
315
|
-
return (rows as T[]).map(row => this.decodeBlobs(row));
|
|
382
|
+
return (rows as T[]).map(row => this.decodeBlobs(foldColumnNames(row)));
|
|
316
383
|
}
|
|
317
384
|
|
|
318
385
|
/** Ensure BLOB columns are readable — node-firebird may return callback-based
|
|
@@ -361,69 +428,81 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
361
428
|
if (Array.isArray(data)) {
|
|
362
429
|
if (data.length === 0) return { success: true, affectedRows: 0 };
|
|
363
430
|
const keys = Object.keys(data[0]);
|
|
364
|
-
const
|
|
365
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
431
|
+
const sql = buildInsert(FB_DIALECT, table, keys);
|
|
366
432
|
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
|
|
370
|
-
} catch (e) {
|
|
371
|
-
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
372
|
-
}
|
|
433
|
+
const result = await this.executeManyAsync(sql, paramsList);
|
|
434
|
+
return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
|
|
373
435
|
}
|
|
374
436
|
|
|
375
437
|
const keys = Object.keys(data);
|
|
376
|
-
const
|
|
377
|
-
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
438
|
+
const sql = buildInsert(FB_DIALECT, table, keys);
|
|
378
439
|
const values = Object.values(data);
|
|
379
440
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
} catch (e) {
|
|
388
|
-
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
389
|
-
}
|
|
441
|
+
// FAIL LOUD, like fetch/execute and the other three frameworks: a bad statement
|
|
442
|
+
// RAISES and never returns a falsy result. Swallowing it into {success:false}
|
|
443
|
+
// is what hid a wholly broken write path — the caller awaited a resolved
|
|
444
|
+
// promise, read back zero rows, and no error surfaced anywhere.
|
|
445
|
+
await this.executePromise(sql, values);
|
|
446
|
+
// Firebird doesn't have a generic last_insert_id — return success without id
|
|
447
|
+
return { success: true, affectedRows: 1 };
|
|
390
448
|
}
|
|
391
449
|
|
|
392
450
|
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
393
451
|
throw new Error("Use updateAsync() for Firebird.");
|
|
394
452
|
}
|
|
395
453
|
|
|
396
|
-
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
454
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
397
455
|
this.ensureConnected();
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
456
|
+
// Identifiers go through fbQuote, exactly like insertAsync. Firebird folds an
|
|
457
|
+
// UNQUOTED identifier to uppercase, so a conventional `CREATE TABLE probe_t`
|
|
458
|
+
// stores PROBE_T/ID — and a hand-rolled `"${k}"` emits lowercase-quoted "id",
|
|
459
|
+
// which is a DIFFERENT, non-existent column. update and delete were therefore
|
|
460
|
+
// broken on every conventionally-created Firebird table while insert worked.
|
|
461
|
+
const setClauses = buildSetClause(FB_DIALECT, Object.keys(data));
|
|
462
|
+
|
|
463
|
+
// A raw WHERE fragment + params is half the write_path contract's filter
|
|
464
|
+
// form. Without this branch Object.keys("id = ?") yields the STRING INDICES
|
|
465
|
+
// ["0","1",...] and the statement addresses columns that do not exist.
|
|
466
|
+
// Firebird already uses `?`, so the fragment needs no rewriting.
|
|
467
|
+
if (typeof filter === "string") {
|
|
468
|
+
const where = filter ? ` WHERE ${filter}` : "";
|
|
469
|
+
await this.executePromise(
|
|
470
|
+
`UPDATE ${fbQuote(table)} SET ${setClauses}${where}`,
|
|
471
|
+
[...Object.values(data), ...(params ?? [])],
|
|
472
|
+
);
|
|
405
473
|
return { success: true, affectedRows: 1 };
|
|
406
|
-
} catch (e) {
|
|
407
|
-
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
408
474
|
}
|
|
475
|
+
|
|
476
|
+
const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
|
|
477
|
+
const sql = `UPDATE ${FB_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
478
|
+
const values = [...Object.values(data), ...Object.values(filter)];
|
|
479
|
+
|
|
480
|
+
await this.executePromise(sql, values);
|
|
481
|
+
return { success: true, affectedRows: 1 };
|
|
409
482
|
}
|
|
410
483
|
|
|
411
484
|
delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
412
485
|
throw new Error("Use deleteAsync() for Firebird.");
|
|
413
486
|
}
|
|
414
487
|
|
|
415
|
-
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
488
|
+
async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
416
489
|
this.ensureConnected();
|
|
417
|
-
const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
|
|
418
|
-
const sql = `DELETE FROM "${table}" WHERE ${whereClauses}`;
|
|
419
|
-
const values = Object.values(filter);
|
|
420
490
|
|
|
421
|
-
|
|
422
|
-
|
|
491
|
+
// See updateAsync: truncate() calls this with "1 = 1", which walked the
|
|
492
|
+
// string as an object — db.truncate() was broken outright.
|
|
493
|
+
if (typeof filter === "string") {
|
|
494
|
+
const where = filter ? ` WHERE ${filter}` : "";
|
|
495
|
+
await this.executePromise(`DELETE FROM ${fbQuote(table)}${where}`, params ?? []);
|
|
423
496
|
return { success: true, affectedRows: 1 };
|
|
424
|
-
} catch (e) {
|
|
425
|
-
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
426
497
|
}
|
|
498
|
+
|
|
499
|
+
// Same fbQuote policy as insert/update — see updateAsync.
|
|
500
|
+
const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
|
|
501
|
+
const sql = `DELETE FROM ${FB_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
502
|
+
const values = Object.values(filter);
|
|
503
|
+
|
|
504
|
+
await this.executePromise(sql, values);
|
|
505
|
+
return { success: true, affectedRows: 1 };
|
|
427
506
|
}
|
|
428
507
|
|
|
429
508
|
startTransaction(): void {
|
|
@@ -477,7 +556,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
477
556
|
});
|
|
478
557
|
}
|
|
479
558
|
|
|
480
|
-
|
|
559
|
+
getTables(): string[] {
|
|
481
560
|
throw new Error("Use tablesAsync() for Firebird.");
|
|
482
561
|
}
|
|
483
562
|
|
|
@@ -491,7 +570,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
491
570
|
});
|
|
492
571
|
}
|
|
493
572
|
|
|
494
|
-
|
|
573
|
+
getColumns(table: string): ColumnInfo[] {
|
|
495
574
|
throw new Error("Use columnsAsync() for Firebird.");
|
|
496
575
|
}
|
|
497
576
|
|
|
@@ -503,6 +582,29 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
503
582
|
WHERE RF.RDB$RELATION_NAME = ?`,
|
|
504
583
|
[table.toUpperCase()],
|
|
505
584
|
);
|
|
585
|
+
|
|
586
|
+
// The primary key comes from the constraint catalogue. This used to be
|
|
587
|
+
// hardcoded false for every column, so the key was invisible to anything
|
|
588
|
+
// that introspects it -- including the filterless-write guard that lifts the
|
|
589
|
+
// PK out of `data`. Same bug the Python master and the Ruby driver carried.
|
|
590
|
+
const pkNames = new Set<string>();
|
|
591
|
+
try {
|
|
592
|
+
const pkRows = await this.queryAsync<Record<string, unknown>>(
|
|
593
|
+
`SELECT SG.RDB$FIELD_NAME FROM RDB$INDEX_SEGMENTS SG
|
|
594
|
+
JOIN RDB$RELATION_CONSTRAINTS RC ON SG.RDB$INDEX_NAME = RC.RDB$INDEX_NAME
|
|
595
|
+
WHERE RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY' AND RC.RDB$RELATION_NAME = ?
|
|
596
|
+
ORDER BY SG.RDB$FIELD_POSITION`,
|
|
597
|
+
[table.toUpperCase()],
|
|
598
|
+
);
|
|
599
|
+
for (const row of pkRows) {
|
|
600
|
+
const raw = (row["RDB$FIELD_NAME"] ?? row["rdb$field_name"] ?? "") as string;
|
|
601
|
+
const trimmed = String(raw).trim().toUpperCase();
|
|
602
|
+
if (trimmed) pkNames.add(trimmed);
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
// A table with no primary key is not an error.
|
|
606
|
+
}
|
|
607
|
+
|
|
506
608
|
return rows.map((r) => {
|
|
507
609
|
const name = (r["RDB$FIELD_NAME"] ?? r["rdb$field_name"] ?? "") as string;
|
|
508
610
|
return {
|
|
@@ -510,7 +612,9 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
510
612
|
type: firebirdFieldTypeToString(r["RDB$FIELD_TYPE"] ?? r["rdb$field_type"]),
|
|
511
613
|
nullable: (r["RDB$NULL_FLAG"] ?? r["rdb$null_flag"]) === null,
|
|
512
614
|
default: r["RDB$DEFAULT_SOURCE"] ?? r["rdb$default_source"],
|
|
513
|
-
primaryKey:
|
|
615
|
+
primaryKey: pkNames.has(
|
|
616
|
+
(typeof name === "string" ? name.trim() : String(name).trim()).toUpperCase(),
|
|
617
|
+
),
|
|
514
618
|
};
|
|
515
619
|
});
|
|
516
620
|
}
|
|
@@ -531,10 +635,28 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
531
635
|
throw new Error("Use tableExistsAsync() for Firebird.");
|
|
532
636
|
}
|
|
533
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Is this table present, under either spelling Firebird could have stored?
|
|
640
|
+
*
|
|
641
|
+
* Firebird's folding rule is ASYMMETRIC:
|
|
642
|
+
* CREATE TABLE foo -> stored as FOO (unquoted folds to UPPER)
|
|
643
|
+
* CREATE TABLE "Foo" -> stored as Foo (quoted keeps its case)
|
|
644
|
+
*
|
|
645
|
+
* So upper-casing is CORRECT for the unquoted case - the common one - and
|
|
646
|
+
* WRONG for a quoted mixed-case table, which is a real thing on Firebird.
|
|
647
|
+
* Dropping the upper-case would not fix that, it would invert which half is
|
|
648
|
+
* broken.
|
|
649
|
+
*
|
|
650
|
+
* tableExistsAsync("Foo") is genuinely AMBIGUOUS: the caller could mean the
|
|
651
|
+
* quoted `Foo` or the unquoted `FOO`. Match EITHER. Do not "simplify" this
|
|
652
|
+
* back to one comparison - that is the bug it replaces, where a quoted
|
|
653
|
+
* mixed-case table read as absent and createTableAsync's idempotency guard
|
|
654
|
+
* (below) never fired.
|
|
655
|
+
*/
|
|
534
656
|
async tableExistsAsync(name: string): Promise<boolean> {
|
|
535
657
|
const rows = await this.queryAsync<Record<string, unknown>>(
|
|
536
|
-
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?",
|
|
537
|
-
[name.toUpperCase()],
|
|
658
|
+
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ? OR RDB$RELATION_NAME = ?",
|
|
659
|
+
[name, name.toUpperCase()],
|
|
538
660
|
);
|
|
539
661
|
return rows.length > 0;
|
|
540
662
|
}
|
|
@@ -552,7 +674,12 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
552
674
|
|
|
553
675
|
for (const [colName, def] of Object.entries(columns)) {
|
|
554
676
|
const sqlType = fieldTypeToFirebird(def);
|
|
555
|
-
|
|
677
|
+
// fbQuote, not a hand-rolled `"${colName}"` — DDL must agree with the write
|
|
678
|
+
// path or the ORM creates a table it cannot write to. Lowercase-quoted "id"
|
|
679
|
+
// is a case-sensitive column; every insert/update/delete addresses ID.
|
|
680
|
+
// tableExistsAsync looks up name.toUpperCase() and would not have found it
|
|
681
|
+
// either, so createTableAsync would re-run and fail already-exists.
|
|
682
|
+
const parts = [`${fbQuote(colName)} ${sqlType}`];
|
|
556
683
|
|
|
557
684
|
if (def.primaryKey && !def.autoIncrement) parts.push("PRIMARY KEY");
|
|
558
685
|
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
@@ -568,7 +695,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
568
695
|
colDefs.push(parts.join(" "));
|
|
569
696
|
}
|
|
570
697
|
|
|
571
|
-
const sql = `CREATE TABLE
|
|
698
|
+
const sql = `CREATE TABLE ${fbQuote(name)} (${colDefs.join(", ")})`;
|
|
572
699
|
await this.executeAsync(sql);
|
|
573
700
|
|
|
574
701
|
// Create sequences and triggers for auto-increment columns
|
|
@@ -579,7 +706,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
579
706
|
|
|
580
707
|
await this.executeAsync(`CREATE SEQUENCE "${seqName}"`);
|
|
581
708
|
await this.executeAsync(
|
|
582
|
-
`CREATE TRIGGER "${trigName}" FOR
|
|
709
|
+
`CREATE TRIGGER "${trigName}" FOR ${fbQuote(name)} ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.${fbQuote(colName)} IS NULL) THEN NEW.${fbQuote(colName)} = NEXT VALUE FOR "${seqName}"; END`,
|
|
583
710
|
);
|
|
584
711
|
}
|
|
585
712
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* URL format: mongodb://host:port/dbname or mongodb+srv://user:pass@host/dbname
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
+
import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
8
9
|
|
|
9
10
|
export interface MongoConfig {
|
|
10
11
|
host?: string;
|
|
@@ -309,8 +310,28 @@ export class MongodbAdapter implements DatabaseAdapter {
|
|
|
309
310
|
);
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
// The driver's own budget is 30s (serverSelectionTimeoutMS and
|
|
314
|
+
// connectTimeoutMS both). It is set from the Tina4 budget so ONE variable
|
|
315
|
+
// governs, and omitted when the bound is disabled so the driver keeps its
|
|
316
|
+
// own 30s exactly as before.
|
|
317
|
+
const budgetMs = connectTimeoutMillis();
|
|
318
|
+
const driverMs = driverConnectTimeoutMillis(budgetMs);
|
|
319
|
+
const timeoutOptions = driverMs === null
|
|
320
|
+
? {}
|
|
321
|
+
: { serverSelectionTimeoutMS: driverMs, connectTimeoutMS: driverMs };
|
|
322
|
+
|
|
323
|
+
const { host, port } = connectTarget(this._connectionString, 27017);
|
|
324
|
+
await withConnectTimeout(
|
|
325
|
+
() => {
|
|
326
|
+
this.client = new MongoClient(this._connectionString, timeoutOptions);
|
|
327
|
+
return this.client.connect();
|
|
328
|
+
},
|
|
329
|
+
budgetMs,
|
|
330
|
+
host,
|
|
331
|
+
port,
|
|
332
|
+
// Answered after we gave up: close it so the pool does not outlive the boot.
|
|
333
|
+
() => { void Promise.resolve(this.client?.close()).catch(() => { /* already gone */ }); },
|
|
334
|
+
);
|
|
314
335
|
this.db = this.client.db(this._dbName);
|
|
315
336
|
}
|
|
316
337
|
|
|
@@ -556,7 +577,7 @@ export class MongodbAdapter implements DatabaseAdapter {
|
|
|
556
577
|
this._inTransaction = false;
|
|
557
578
|
}
|
|
558
579
|
|
|
559
|
-
|
|
580
|
+
getTables(): string[] {
|
|
560
581
|
throw new Error("Use tablesAsync() for MongoDB — async adapter requires async methods.");
|
|
561
582
|
}
|
|
562
583
|
|
|
@@ -566,7 +587,7 @@ export class MongodbAdapter implements DatabaseAdapter {
|
|
|
566
587
|
return collections.map((c: any) => c.name as string);
|
|
567
588
|
}
|
|
568
589
|
|
|
569
|
-
|
|
590
|
+
getColumns(table: string): ColumnInfo[] {
|
|
570
591
|
throw new Error("Use columnsAsync() for MongoDB — async adapter requires async methods.");
|
|
571
592
|
}
|
|
572
593
|
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Install: npm install tedious
|
|
5
5
|
* URL format: mssql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
|
+
import { ANSI_DIALECT, MSSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
|
|
7
8
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
9
|
import { SQLTranslator } from "../sqlTranslator.js";
|
|
10
|
+
import { connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
|
|
9
11
|
import { createRequire } from "node:module";
|
|
10
12
|
|
|
11
13
|
let tedious: any = null;
|
|
@@ -38,6 +40,14 @@ export interface MssqlConfig {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export class MssqlAdapter implements DatabaseAdapter {
|
|
43
|
+
/**
|
|
44
|
+
* Postgres, MySQL and MSSQL all REQUIRE a name for a derived table, so
|
|
45
|
+
* the COUNT probe in Database.countProbe wraps as
|
|
46
|
+
* `FROM (sql) AS _count_query`. SQLite and Firebird leave this unset and
|
|
47
|
+
* get no alias - Firebird rejects `AS` in that position.
|
|
48
|
+
*/
|
|
49
|
+
readonly countSubqueryAlias = "_count_query";
|
|
50
|
+
|
|
41
51
|
private connection: any = null;
|
|
42
52
|
private _lastInsertId: number | bigint | null = null;
|
|
43
53
|
// True between startTransactionAsync() and commit/rollback. executeManyAsync
|
|
@@ -52,6 +62,14 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
52
62
|
const tediousModule = requireTedious();
|
|
53
63
|
const Connection = tediousModule.Connection;
|
|
54
64
|
|
|
65
|
+
// tedious has its own connectTimeout (15s by default). It is set from the
|
|
66
|
+
// Tina4 budget so ONE variable governs - otherwise a configured 60s would
|
|
67
|
+
// still fail at tedious's 15s, with tedious's message, and the variable
|
|
68
|
+
// would be a lie. Omitted when the bound is disabled, restoring the old 15s.
|
|
69
|
+
const budgetMs = connectTimeoutMillis();
|
|
70
|
+
const driverMs = driverConnectTimeoutMillis(budgetMs);
|
|
71
|
+
const timeoutOption = driverMs === null ? {} : { connectTimeout: driverMs };
|
|
72
|
+
|
|
55
73
|
let tediousConfig: any;
|
|
56
74
|
|
|
57
75
|
if (typeof this.config === "string") {
|
|
@@ -70,6 +88,7 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
70
88
|
port: parsed.port ?? 1433,
|
|
71
89
|
trustServerCertificate: true,
|
|
72
90
|
encrypt: false,
|
|
91
|
+
...timeoutOption,
|
|
73
92
|
},
|
|
74
93
|
};
|
|
75
94
|
} else {
|
|
@@ -87,19 +106,29 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
87
106
|
port: this.config.port ?? 1433,
|
|
88
107
|
trustServerCertificate: true,
|
|
89
108
|
encrypt: false,
|
|
109
|
+
...timeoutOption,
|
|
90
110
|
...this.config.options,
|
|
91
111
|
},
|
|
92
112
|
};
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
// Inside the thunk so the Tina4 clock starts before tedious arms its own
|
|
116
|
+
// connectTimer (connection.js, at the top of its connect flow).
|
|
117
|
+
await withConnectTimeout(
|
|
118
|
+
() => new Promise<void>((resolve, reject) => {
|
|
119
|
+
this.connection = new Connection(tediousConfig);
|
|
120
|
+
this.connection.on("connect", (err: Error | null) => {
|
|
121
|
+
if (err) reject(err);
|
|
122
|
+
else resolve();
|
|
123
|
+
});
|
|
124
|
+
this.connection.connect();
|
|
125
|
+
}),
|
|
126
|
+
budgetMs,
|
|
127
|
+
tediousConfig.server ?? "localhost",
|
|
128
|
+
tediousConfig.options?.port ?? 1433,
|
|
129
|
+
// Answered after we gave up: close it so the socket does not outlive the boot.
|
|
130
|
+
() => { try { this.connection?.close?.(); } catch { /* already gone */ } },
|
|
131
|
+
);
|
|
103
132
|
}
|
|
104
133
|
|
|
105
134
|
private parseUrl(url: string): { host: string; port?: number; user?: string; password?: string; database?: string } {
|
|
@@ -166,9 +195,15 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
166
195
|
});
|
|
167
196
|
}
|
|
168
197
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Convert ? placeholders to @p0, @p1, ... for tedious.
|
|
200
|
+
*
|
|
201
|
+
* `startAt` lets a caller that has already consumed N placeholders (an UPDATE
|
|
202
|
+
* whose SET values are @p0..@p{N-1}) continue the numbering into a raw WHERE
|
|
203
|
+
* fragment instead of restarting at @p0.
|
|
204
|
+
*/
|
|
205
|
+
private convertPlaceholders(sql: string, startAt = 0): string {
|
|
206
|
+
let count = startAt;
|
|
172
207
|
return sql.replace(/\?/g, () => {
|
|
173
208
|
return `@p${count++}`;
|
|
174
209
|
});
|
|
@@ -272,8 +307,8 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
272
307
|
const keys = Object.keys(data[0]);
|
|
273
308
|
// `?` placeholders — executeManyAsync -> executeAsync runs convertPlaceholders,
|
|
274
309
|
// which rewrites them to @p0, @p1, ... for tedious.
|
|
275
|
-
|
|
276
|
-
const sql =
|
|
310
|
+
// The batch path binds through executeMany, which converts "?" itself.
|
|
311
|
+
const sql = buildInsert({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table, keys);
|
|
277
312
|
const paramsList = data.map((row) => keys.map((k) => row[k]));
|
|
278
313
|
try {
|
|
279
314
|
const result = await this.executeManyAsync(sql, paramsList);
|
|
@@ -285,8 +320,9 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
285
320
|
}
|
|
286
321
|
|
|
287
322
|
const keys = Object.keys(data);
|
|
288
|
-
|
|
289
|
-
|
|
323
|
+
// startAt 0: MSSQL BINDS by the marker name, so @p must start where the
|
|
324
|
+
// binding loop starts. Shifting to 1 would name parameters that do not exist.
|
|
325
|
+
const sql = buildInsert(MSSQL_DIALECT, table, keys, "; SELECT SCOPE_IDENTITY() AS id", 0);
|
|
290
326
|
const values = Object.values(data);
|
|
291
327
|
|
|
292
328
|
try {
|
|
@@ -311,15 +347,33 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
311
347
|
throw new Error("Use updateAsync() for MSSQL.");
|
|
312
348
|
}
|
|
313
349
|
|
|
314
|
-
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
350
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
315
351
|
this.ensureConnected();
|
|
316
352
|
const dataKeys = Object.keys(data);
|
|
317
|
-
const filterKeys = Object.keys(filter);
|
|
318
353
|
let paramIndex = 0;
|
|
354
|
+
const setClauses = buildSetClause(MSSQL_DIALECT, dataKeys, paramIndex);
|
|
355
|
+
paramIndex += dataKeys.length;
|
|
356
|
+
|
|
357
|
+
// A raw WHERE fragment + params is half the write_path contract's filter
|
|
358
|
+
// form. Without this branch Object.keys("id = ?") yields the STRING INDICES
|
|
359
|
+
// ["0","1",...], producing `WHERE [0] = @p1 AND [1] = @p2` — SQL Server then
|
|
360
|
+
// reports an invalid column name '0'.
|
|
361
|
+
if (typeof filter === "string") {
|
|
362
|
+
const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
|
|
363
|
+
const sql = `UPDATE ${MSSQL_DIALECT.quote(table)} SET ${setClauses}${where}`;
|
|
364
|
+
const values = [...Object.values(data), ...(params ?? [])];
|
|
365
|
+
try {
|
|
366
|
+
const result = await this.execSqlPromise(sql, values);
|
|
367
|
+
return { success: true, affectedRows: result.rowCount };
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
319
372
|
|
|
320
|
-
const
|
|
321
|
-
const whereClauses =
|
|
322
|
-
|
|
373
|
+
const filterKeys = Object.keys(filter);
|
|
374
|
+
const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
|
|
375
|
+
paramIndex += filterKeys.length;
|
|
376
|
+
const sql = `UPDATE ${MSSQL_DIALECT.quote(table)} SET ${setClauses} WHERE ${whereClauses}`;
|
|
323
377
|
const values = [...Object.values(data), ...Object.values(filter)];
|
|
324
378
|
|
|
325
379
|
try {
|
|
@@ -334,12 +388,28 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
334
388
|
throw new Error("Use deleteAsync() for MSSQL.");
|
|
335
389
|
}
|
|
336
390
|
|
|
337
|
-
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
391
|
+
async deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult> {
|
|
338
392
|
this.ensureConnected();
|
|
393
|
+
|
|
394
|
+
// See updateAsync: truncate() calls this with "1 = 1", which became
|
|
395
|
+
// `WHERE [0] = @p0 AND [1] = @p1 ...` — db.truncate() was broken outright.
|
|
396
|
+
if (typeof filter === "string") {
|
|
397
|
+
const sql = filter
|
|
398
|
+
? `DELETE FROM [${table}] WHERE ${this.convertPlaceholders(filter)}`
|
|
399
|
+
: `DELETE FROM [${table}]`;
|
|
400
|
+
try {
|
|
401
|
+
const result = await this.execSqlPromise(sql, params ?? []);
|
|
402
|
+
return { success: true, affectedRows: result.rowCount };
|
|
403
|
+
} catch (e) {
|
|
404
|
+
return { success: false, affectedRows: 0, error: (e as Error).message };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
339
408
|
const filterKeys = Object.keys(filter);
|
|
340
409
|
let paramIndex = 0;
|
|
341
|
-
const whereClauses =
|
|
342
|
-
|
|
410
|
+
const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
|
|
411
|
+
paramIndex += filterKeys.length;
|
|
412
|
+
const sql = `DELETE FROM ${MSSQL_DIALECT.quote(table)} WHERE ${whereClauses}`;
|
|
343
413
|
const values = Object.values(filter);
|
|
344
414
|
|
|
345
415
|
try {
|
|
@@ -389,7 +459,7 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
389
459
|
this._inTransaction = false;
|
|
390
460
|
}
|
|
391
461
|
|
|
392
|
-
|
|
462
|
+
getTables(): string[] {
|
|
393
463
|
throw new Error("Use tablesAsync() for MSSQL.");
|
|
394
464
|
}
|
|
395
465
|
|
|
@@ -400,7 +470,7 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
400
470
|
return rows.map((r) => r.TABLE_NAME);
|
|
401
471
|
}
|
|
402
472
|
|
|
403
|
-
|
|
473
|
+
getColumns(table: string): ColumnInfo[] {
|
|
404
474
|
throw new Error("Use columnsAsync() for MSSQL.");
|
|
405
475
|
}
|
|
406
476
|
|
|
@@ -413,17 +483,32 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
413
483
|
DATA_TYPE: string;
|
|
414
484
|
IS_NULLABLE: string;
|
|
415
485
|
COLUMN_DEFAULT: string | null;
|
|
486
|
+
is_primary: number;
|
|
416
487
|
}>(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
488
|
+
`SELECT c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, c.COLUMN_DEFAULT,
|
|
489
|
+
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS is_primary
|
|
490
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
491
|
+
LEFT JOIN (
|
|
492
|
+
SELECT ku.COLUMN_NAME
|
|
493
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
494
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
|
|
495
|
+
ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
|
496
|
+
WHERE tc.TABLE_NAME = ? AND (? IS NULL OR tc.TABLE_SCHEMA = ?)
|
|
497
|
+
AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
498
|
+
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
|
499
|
+
WHERE c.TABLE_NAME = ? AND (? IS NULL OR c.TABLE_SCHEMA = ?)
|
|
500
|
+
ORDER BY c.ORDINAL_POSITION`,
|
|
501
|
+
[tbl, schema, schema, tbl, schema, schema],
|
|
420
502
|
);
|
|
421
503
|
return rows.map((r) => ({
|
|
422
504
|
name: r.COLUMN_NAME,
|
|
423
505
|
type: r.DATA_TYPE,
|
|
424
506
|
nullable: r.IS_NULLABLE === "YES",
|
|
425
507
|
default: r.COLUMN_DEFAULT,
|
|
426
|
-
|
|
508
|
+
// Same hole PostgreSQL had: hardcoded false meant primaryKey(table)
|
|
509
|
+
// introspected NOTHING on SQL Server, so the feature-4 filterless-write
|
|
510
|
+
// guard rejected every PK-keyed update. Ported from the Python master.
|
|
511
|
+
primaryKey: Number(r.is_primary) === 1,
|
|
427
512
|
}));
|
|
428
513
|
}
|
|
429
514
|
|