tina4-nodejs 3.13.92 → 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 +170 -28
- package/README.md +2 -2
- package/package.json +13 -9
- package/packages/cli/dist/bin.js +33126 -30055
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +33062 -29908
- 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/devMailbox.ts +20 -44
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +7 -6
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +81 -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 +109 -13
- 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/childError.ts +72 -0
- 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 -202
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -143
- 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/frond/dist/index.js +74 -31
- package/packages/frond/src/engine.ts +99 -33
- package/packages/orm/dist/index.js +26554 -23400
- 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 +64 -25
- 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 +338 -198
- 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/cli/src/bin.d.ts +92 -0
- package/types/cli/src/commands/build.d.ts +2 -0
- package/types/cli/src/commands/generate.d.ts +47 -0
- package/types/cli/src/commands/init.d.ts +1 -0
- package/types/cli/src/commands/metrics.d.ts +6 -0
- package/types/cli/src/commands/migrate.d.ts +1 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -0
- package/types/cli/src/commands/migrateRollback.d.ts +1 -0
- package/types/cli/src/commands/migrateStatus.d.ts +1 -0
- package/types/cli/src/commands/queue.d.ts +20 -0
- package/types/cli/src/commands/routes.d.ts +1 -0
- package/types/cli/src/commands/seed.d.ts +1 -0
- package/types/cli/src/commands/serve.d.ts +6 -0
- package/types/cli/src/commands/test.d.ts +1 -0
- package/types/core/src/ai.d.ts +64 -0
- package/types/core/src/api.d.ts +262 -0
- package/types/core/src/auth.d.ts +177 -0
- package/types/core/src/authGate.d.ts +20 -0
- package/types/core/src/background.d.ts +34 -0
- package/types/core/src/cache.d.ts +163 -0
- package/types/core/src/constants.d.ts +38 -0
- package/types/core/src/container.d.ts +44 -0
- package/types/core/src/context/chunker.d.ts +31 -0
- package/types/core/src/context/index.d.ts +93 -0
- package/types/core/src/devAdmin.d.ts +179 -0
- package/types/core/src/devMailbox.d.ts +54 -0
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/docs.d.ts +141 -0
- package/types/core/src/docsAutoDiscovery.d.ts +6 -0
- package/types/core/src/dotenv.d.ts +87 -0
- package/types/core/src/env.d.ts +28 -0
- package/types/core/src/errorOverlay.d.ts +36 -0
- package/types/core/src/events.d.ts +75 -0
- package/types/core/src/fakeData.d.ts +55 -0
- package/types/core/src/feedback.d.ts +90 -0
- package/types/core/src/graphql.d.ts +207 -0
- package/types/core/src/health.d.ts +22 -0
- package/types/core/src/htmlElement.d.ts +75 -0
- package/types/core/src/i18n.d.ts +37 -0
- package/types/core/src/index.d.ts +92 -0
- package/types/core/src/job.d.ts +39 -0
- package/types/core/src/logger.d.ts +200 -0
- package/types/core/src/mcp.d.ts +248 -0
- package/types/core/src/messenger.d.ts +191 -0
- package/types/core/src/metrics.d.ts +41 -0
- package/types/core/src/middleware.d.ts +330 -0
- package/types/core/src/mqtt.d.ts +257 -0
- package/types/core/src/mqttMessage.d.ts +67 -0
- package/types/core/src/plan.d.ts +96 -0
- package/types/core/src/projectIndex.d.ts +56 -0
- package/types/core/src/queue.d.ts +268 -0
- package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
- package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
- package/types/core/src/rateLimiter.d.ts +49 -0
- package/types/core/src/request.d.ts +25 -0
- package/types/core/src/response.d.ts +28 -0
- package/types/core/src/routeDiscovery.d.ts +12 -0
- package/types/core/src/router.d.ts +366 -0
- package/types/core/src/scss.d.ts +19 -0
- package/types/core/src/server.d.ts +146 -0
- package/types/core/src/service.d.ts +115 -0
- package/types/core/src/session.d.ts +341 -0
- package/types/core/src/sessionHandlers/childError.d.ts +34 -0
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
- package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
- 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/sessionHandlers/valkeyHandler.d.ts +65 -0
- package/types/core/src/static.d.ts +2 -0
- package/types/core/src/test.d.ts +94 -0
- package/types/core/src/testClient.d.ts +36 -0
- package/types/core/src/testing.d.ts +58 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +242 -0
- package/types/core/src/validator.d.ts +52 -0
- package/types/core/src/websocket.d.ts +402 -0
- package/types/core/src/websocketBackplane.d.ts +166 -0
- package/types/core/src/websocketConnection.d.ts +54 -0
- package/types/core/src/wsdl.d.ts +101 -0
- package/types/frond/src/engine.d.ts +263 -0
- package/types/frond/src/index.d.ts +2 -0
- package/types/orm/src/adapters/firebird.d.ts +183 -0
- package/types/orm/src/adapters/mongodb.d.ts +81 -0
- package/types/orm/src/adapters/mssql.d.ts +77 -0
- package/types/orm/src/adapters/mysql.d.ts +67 -0
- package/types/orm/src/adapters/odbc.d.ts +94 -0
- package/types/orm/src/adapters/postgres.d.ts +86 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +68 -0
- package/types/orm/src/autoCrud.d.ts +73 -0
- package/types/orm/src/baseModel.d.ts +427 -0
- package/types/orm/src/cachedDatabase.d.ts +190 -0
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +655 -0
- package/types/orm/src/databaseResult.d.ts +109 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +241 -0
- package/types/orm/src/fakeData.d.ts +22 -0
- package/types/orm/src/index.d.ts +43 -0
- package/types/orm/src/migration.d.ts +275 -0
- package/types/orm/src/model.d.ts +7 -0
- package/types/orm/src/query.d.ts +14 -0
- package/types/orm/src/queryBuilder.d.ts +193 -0
- package/types/orm/src/realtime/index.d.ts +7 -0
- package/types/orm/src/realtime/models/attachment.d.ts +43 -0
- package/types/orm/src/realtime/models/channel.d.ts +32 -0
- package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
- package/types/orm/src/realtime/models/message.d.ts +36 -0
- package/types/orm/src/realtime/models/workspace.d.ts +26 -0
- package/types/orm/src/realtime/realtime.d.ts +24 -0
- package/types/orm/src/realtime/storage.d.ts +61 -0
- package/types/orm/src/seeder.d.ts +118 -0
- package/types/orm/src/sqlTranslator.d.ts +258 -0
- package/types/orm/src/types.d.ts +148 -0
- package/types/orm/src/validation.d.ts +6 -0
- package/types/swagger/src/generator.d.ts +46 -0
- package/types/swagger/src/index.d.ts +2 -0
- package/types/swagger/src/ui.d.ts +11 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
3
3
|
import { DatabaseResult } from "./databaseResult.js";
|
|
4
|
+
import { DatabaseUrl } from "./databaseUrl.js";
|
|
4
5
|
import { CachedDatabaseAdapter, type CachedAdapterOptions } from "./cachedDatabase.js";
|
|
5
|
-
import { QueryCache } from "./sqlTranslator.js";
|
|
6
|
+
import { QueryCache, SQLTranslator } from "./sqlTranslator.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL
|
|
@@ -96,13 +97,13 @@ export async function adapterTableExists(adapter: DatabaseAdapter, name: string)
|
|
|
96
97
|
export async function adapterTables(adapter: DatabaseAdapter): Promise<string[]> {
|
|
97
98
|
return (adapter as any).tablesAsync
|
|
98
99
|
? await (adapter as any).tablesAsync()
|
|
99
|
-
: adapter.
|
|
100
|
+
: adapter.getTables();
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export async function adapterColumns(adapter: DatabaseAdapter, table: string): Promise<ColumnInfo[]> {
|
|
103
104
|
return (adapter as any).columnsAsync
|
|
104
105
|
? await (adapter as any).columnsAsync(table)
|
|
105
|
-
: adapter.
|
|
106
|
+
: adapter.getColumns(table);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
export async function adapterCreateTable(
|
|
@@ -131,6 +132,16 @@ export function extractLastInsertId(result: unknown): number | bigint | null {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
let activeAdapter: DatabaseAdapter | null = null;
|
|
135
|
+
/**
|
|
136
|
+
* The default row cap on every read path that advertises a `limit`.
|
|
137
|
+
*
|
|
138
|
+
* One number for the whole family (Python, PHP, Ruby and Node all default to
|
|
139
|
+
* this). Pagination is a default principle: an un-paginated read of a table
|
|
140
|
+
* that grew to a million rows is a production incident waiting to happen. A
|
|
141
|
+
* caller who wants more passes a bigger limit.
|
|
142
|
+
*/
|
|
143
|
+
export const DEFAULT_ROW_CAP = 100;
|
|
144
|
+
|
|
134
145
|
const namedAdapters: Map<string, DatabaseAdapter> = new Map();
|
|
135
146
|
|
|
136
147
|
/**
|
|
@@ -289,166 +300,20 @@ export interface DatabaseConfig {
|
|
|
289
300
|
/**
|
|
290
301
|
* Parsed result from a TINA4_DATABASE_URL connection string.
|
|
291
302
|
*/
|
|
292
|
-
export interface ParsedDatabaseUrl {
|
|
293
|
-
type: "sqlite" | "postgres" | "mysql" | "mssql" | "firebird" | "mongodb" | "odbc";
|
|
294
|
-
path?: string;
|
|
295
|
-
host?: string;
|
|
296
|
-
port?: number;
|
|
297
|
-
user?: string;
|
|
298
|
-
password?: string;
|
|
299
|
-
database?: string;
|
|
300
|
-
/** ODBC-specific: raw connection string passed to odbc.connect() */
|
|
301
|
-
connectionString?: string;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
303
|
/**
|
|
305
|
-
* Parse a
|
|
304
|
+
* Parse a connection URL into a `DatabaseUrl` value.
|
|
306
305
|
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* mysql://user:pass@host:port/dbname
|
|
306
|
+
* Breaking (feature 5): this returned a `ParsedDatabaseUrl` struct whose fields
|
|
307
|
+
* were `type`, `user` and `path`. It now returns a `DatabaseUrl`, whose fields
|
|
308
|
+
* are `engine`, `username` and `database` - the same names PHP, Python and Ruby
|
|
309
|
+
* use, and the same names as the TINA4_DATABASE_USERNAME env var they come from.
|
|
310
|
+
* `ParsedDatabaseUrl` is gone rather than kept as an alias.
|
|
313
311
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* @param password - Optional password to merge when the URL has no credentials.
|
|
317
|
-
* @returns Parsed database configuration.
|
|
318
|
-
* @throws Error if the URL scheme is not supported.
|
|
312
|
+
* The 43-CC body that used to live here - the worst function measured anywhere
|
|
313
|
+
* in the audit - is now one small parser per engine inside the value type.
|
|
319
314
|
*/
|
|
320
|
-
export function parseDatabaseUrl(url: string, username?: string, password?: string):
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
// Handle sqlite:// separately because URL class mangles the path.
|
|
324
|
-
//
|
|
325
|
-
// Convention (matches tina4-python, tina4-php, and the docs):
|
|
326
|
-
// sqlite::memory: → in-memory
|
|
327
|
-
// sqlite:///:memory: → in-memory (URL form)
|
|
328
|
-
// sqlite:///app.db → ./app.db (relative to cwd)
|
|
329
|
-
// sqlite:///data/app.db → ./data/app.db (relative)
|
|
330
|
-
// sqlite:////absolute/app.db → /absolute/app.db (absolute)
|
|
331
|
-
// sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
|
|
332
|
-
if (url === "sqlite::memory:" || url === "sqlite:///:memory:") {
|
|
333
|
-
result = { type: "sqlite", path: ":memory:" };
|
|
334
|
-
} else if (url.startsWith("sqlite:///")) {
|
|
335
|
-
// Strip the "sqlite://" prefix (leaving one "/" + path)
|
|
336
|
-
let rest = url.slice("sqlite://".length); // e.g. "/data/app.db" or "//abs/app.db" or "/C:/Users/..."
|
|
337
|
-
// Drop exactly one leading "/"
|
|
338
|
-
if (rest.startsWith("/")) rest = rest.slice(1);
|
|
339
|
-
// Windows absolute: C:/Users/app.db or C:\...
|
|
340
|
-
const isWindowsAbs = /^[A-Za-z]:[\/\\]/.test(rest);
|
|
341
|
-
// Unix absolute: still starts with "/" after the strip (four-slash URL form)
|
|
342
|
-
const isUnixAbs = rest.startsWith("/");
|
|
343
|
-
result = { type: "sqlite", path: isWindowsAbs || isUnixAbs ? rest : rest };
|
|
344
|
-
// Relative paths are resolved against cwd by the SQLite adapter at connect time;
|
|
345
|
-
// keep the string as-is here so tests can inspect the raw form.
|
|
346
|
-
} else if (url.startsWith("sqlite://")) {
|
|
347
|
-
// sqlite://./relative or sqlite://relative — legacy two-slash form
|
|
348
|
-
const path = url.slice("sqlite://".length);
|
|
349
|
-
result = { type: "sqlite", path };
|
|
350
|
-
} else if (url.startsWith("sqlite:")) {
|
|
351
|
-
// sqlite:/abs/app.db (one slash = a real absolute path) or sqlite:app.db (relative).
|
|
352
|
-
// Keep the leading slash so resolveSqlitePath's isAbsolute() sees the absolute path —
|
|
353
|
-
// this form used to fall through and throw "unsupported scheme" (the naive-abs footgun).
|
|
354
|
-
const path = url.slice("sqlite:".length);
|
|
355
|
-
result = { type: "sqlite", path };
|
|
356
|
-
} else if (url.startsWith("mssql://") || url.startsWith("sqlserver://")) {
|
|
357
|
-
// Handle mssql:// and sqlserver:// with custom parsing (URL class doesn't know these schemes)
|
|
358
|
-
const match = url.match(/(?:mssql|sqlserver):\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.*)/);
|
|
359
|
-
if (!match) throw new Error(`Invalid MSSQL URL: ${url}`);
|
|
360
|
-
result = {
|
|
361
|
-
type: "mssql",
|
|
362
|
-
user: match[1] ? decodeURIComponent(match[1]) : undefined,
|
|
363
|
-
password: match[2] ? decodeURIComponent(match[2]) : undefined,
|
|
364
|
-
host: match[3],
|
|
365
|
-
port: match[4] ? parseInt(match[4], 10) : undefined,
|
|
366
|
-
database: match[5],
|
|
367
|
-
};
|
|
368
|
-
} else if (url.startsWith("firebird://")) {
|
|
369
|
-
const match = url.match(/firebird:\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.*)/);
|
|
370
|
-
if (!match) throw new Error(`Invalid Firebird URL: ${url}`);
|
|
371
|
-
result = {
|
|
372
|
-
type: "firebird",
|
|
373
|
-
user: match[1] ? decodeURIComponent(match[1]) : undefined,
|
|
374
|
-
password: match[2] ? decodeURIComponent(match[2]) : undefined,
|
|
375
|
-
host: match[3],
|
|
376
|
-
port: match[4] ? parseInt(match[4], 10) : undefined,
|
|
377
|
-
database: "/" + match[5],
|
|
378
|
-
};
|
|
379
|
-
} else if (url.startsWith("odbc:///")) {
|
|
380
|
-
// odbc:///DSN=MyDSN or odbc:///DRIVER={driver};SERVER=host;DATABASE=db
|
|
381
|
-
// Strip the "odbc:///" prefix and pass the rest directly as the connection string
|
|
382
|
-
const connectionString = url.slice("odbc:///".length);
|
|
383
|
-
result = { type: "odbc", connectionString };
|
|
384
|
-
} else if (url.startsWith("mongodb://") || url.startsWith("mongodb+srv://")) {
|
|
385
|
-
// Pass through as-is; MongodbAdapter handles the full connection string
|
|
386
|
-
let parsed: URL;
|
|
387
|
-
try {
|
|
388
|
-
parsed = new URL(url);
|
|
389
|
-
} catch {
|
|
390
|
-
throw new Error(`Invalid MongoDB URL: ${url}`);
|
|
391
|
-
}
|
|
392
|
-
const database = parsed.pathname.replace(/^\//, "") || "tina4";
|
|
393
|
-
result = {
|
|
394
|
-
type: "mongodb",
|
|
395
|
-
host: parsed.hostname || undefined,
|
|
396
|
-
port: parsed.port ? parseInt(parsed.port, 10) : undefined,
|
|
397
|
-
user: parsed.username ? decodeURIComponent(parsed.username) : undefined,
|
|
398
|
-
password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
|
|
399
|
-
database,
|
|
400
|
-
};
|
|
401
|
-
} else {
|
|
402
|
-
// Normalize postgres:// and pgsql:// (the PDO/Laravel/Doctrine scheme
|
|
403
|
-
// name, issue #58) to postgresql:// for URL parsing.
|
|
404
|
-
const normalizedUrl = /^(postgres|pgsql):\/\//.test(url)
|
|
405
|
-
? url.replace(/^(postgres|pgsql):\/\//, "postgresql://")
|
|
406
|
-
: url;
|
|
407
|
-
|
|
408
|
-
let parsed: URL;
|
|
409
|
-
try {
|
|
410
|
-
parsed = new URL(normalizedUrl);
|
|
411
|
-
} catch {
|
|
412
|
-
throw new Error(`Invalid database URL: ${url}`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const scheme = parsed.protocol.replace(/:$/, "");
|
|
416
|
-
let type: "sqlite" | "postgres" | "mysql" | "mssql" | "firebird";
|
|
417
|
-
|
|
418
|
-
switch (scheme) {
|
|
419
|
-
case "postgresql":
|
|
420
|
-
type = "postgres";
|
|
421
|
-
break;
|
|
422
|
-
case "mysql":
|
|
423
|
-
type = "mysql";
|
|
424
|
-
break;
|
|
425
|
-
default:
|
|
426
|
-
throw new Error(`Unsupported database URL scheme: "${scheme}". Supported: sqlite, postgres/postgresql, mysql, mssql/sqlserver, firebird.`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const database = parsed.pathname.startsWith("/")
|
|
430
|
-
? parsed.pathname.slice(1)
|
|
431
|
-
: parsed.pathname;
|
|
432
|
-
|
|
433
|
-
result = {
|
|
434
|
-
type,
|
|
435
|
-
host: parsed.hostname || undefined,
|
|
436
|
-
port: parsed.port ? parseInt(parsed.port, 10) : undefined,
|
|
437
|
-
user: parsed.username ? decodeURIComponent(parsed.username) : undefined,
|
|
438
|
-
password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
|
|
439
|
-
database: database || undefined,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Merge separate username/password when the URL contained no credentials
|
|
444
|
-
if (!result.user && username) {
|
|
445
|
-
result.user = username;
|
|
446
|
-
}
|
|
447
|
-
if (!result.password && password) {
|
|
448
|
-
result.password = password;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return result;
|
|
315
|
+
export function parseDatabaseUrl(url: string, username?: string, password?: string): DatabaseUrl {
|
|
316
|
+
return new DatabaseUrl(url, username, password);
|
|
452
317
|
}
|
|
453
318
|
|
|
454
319
|
/**
|
|
@@ -484,6 +349,9 @@ export class Database {
|
|
|
484
349
|
/** Factory for creating new adapters (used by pool) */
|
|
485
350
|
private adapterFactory: (() => Promise<DatabaseAdapter>) | null = null;
|
|
486
351
|
|
|
352
|
+
/** table -> primary-key column name (or null), introspected once */
|
|
353
|
+
private _pkCache: Map<string, string[]> = new Map();
|
|
354
|
+
|
|
487
355
|
/**
|
|
488
356
|
* Whether a standalone write auto-commits. ON by default — a write made
|
|
489
357
|
* outside an explicit transaction commits on its own connection before
|
|
@@ -571,7 +439,7 @@ export class Database {
|
|
|
571
439
|
db.poolIndex = 0;
|
|
572
440
|
db.adapter = null; // Don't use single-adapter path
|
|
573
441
|
db.adapterFactory = async () => wrapWithCache(await createAdapterFromUrl(url, username, password), { sharedCache });
|
|
574
|
-
db.dbType = parsed.
|
|
442
|
+
db.dbType = parsed.engine;
|
|
575
443
|
return exposeDb(db);
|
|
576
444
|
}
|
|
577
445
|
|
|
@@ -581,7 +449,7 @@ export class Database {
|
|
|
581
449
|
const adapter = await createAdapterFromUrl(url, username, password);
|
|
582
450
|
const wrapped = setAdapter(adapter);
|
|
583
451
|
const db = new Database(wrapped);
|
|
584
|
-
db.dbType = parsed.
|
|
452
|
+
db.dbType = parsed.engine;
|
|
585
453
|
return exposeDb(db);
|
|
586
454
|
}
|
|
587
455
|
|
|
@@ -672,7 +540,31 @@ export class Database {
|
|
|
672
540
|
* the fallback resolves instantly). This is the breaking change that makes
|
|
673
541
|
* the wrapper work uniformly across every engine.
|
|
674
542
|
*/
|
|
543
|
+
/**
|
|
544
|
+
* Fetch rows with pagination, capped at DEFAULT_ROW_CAP (100) when the
|
|
545
|
+
* caller does not pass a limit.
|
|
546
|
+
*
|
|
547
|
+
* The cap is the one row-cap number the whole family shares (Python, PHP and
|
|
548
|
+
* Ruby all default `fetch` to 100). Node was the outlier: `limit` was
|
|
549
|
+
* optional with NO default, so a bare `db.fetch("select * from big_table")`
|
|
550
|
+
* returned every row.
|
|
551
|
+
*
|
|
552
|
+
* `fetchAll` deliberately does NOT inherit the cap — see below.
|
|
553
|
+
*/
|
|
675
554
|
async fetch(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean }): Promise<DatabaseResult> {
|
|
555
|
+
return this._fetchWithLimit(sql, params, limit ?? DEFAULT_ROW_CAP, offset, opts);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* The shared read body. `limit` is passed through VERBATIM: `undefined`
|
|
560
|
+
* means "no LIMIT clause at all", which is how `fetchAll` stays uncapped.
|
|
561
|
+
*
|
|
562
|
+
* This exists because Node's adapters treat `limit: 0` as `LIMIT 0` (zero
|
|
563
|
+
* rows), not as the "no truncation" sentinel Python and PHP use — so the cap
|
|
564
|
+
* cannot live on the parameter default, or `fetchAll()` would silently
|
|
565
|
+
* inherit it and stop returning every row.
|
|
566
|
+
*/
|
|
567
|
+
private async _fetchWithLimit(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean }): Promise<DatabaseResult> {
|
|
676
568
|
// v3.13.12: strip trailing `;` before the adapter wraps with COUNT(*)
|
|
677
569
|
// or appends LIMIT/OFFSET. Without this, `"SELECT * FROM t;"` becomes
|
|
678
570
|
// `"SELECT * FROM t; LIMIT 100 OFFSET 0"` — a syntax error.
|
|
@@ -683,7 +575,8 @@ export class Database {
|
|
|
683
575
|
// no store, run directly (mirrors the Python master's `no_cache`).
|
|
684
576
|
const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
|
|
685
577
|
this.lastError = null;
|
|
686
|
-
|
|
578
|
+
const total = await this.countProbe(adapter, sql, params, limit);
|
|
579
|
+
return new DatabaseResult(rows, undefined, total, limit, offset, adapter, sql);
|
|
687
580
|
} catch (e: any) {
|
|
688
581
|
// v3.13.11 #49.2: fetch() records last_error like execute() does.
|
|
689
582
|
this.lastError = e?.message ?? String(e);
|
|
@@ -691,6 +584,63 @@ export class Database {
|
|
|
691
584
|
}
|
|
692
585
|
}
|
|
693
586
|
|
|
587
|
+
/**
|
|
588
|
+
* The true row count for `sql`, ignoring the pagination we appended.
|
|
589
|
+
*
|
|
590
|
+
* `count` is the TRUE TOTAL for the filter, not the number of rows this page
|
|
591
|
+
* returned. Node and Ruby used to populate it with `records.length` while
|
|
592
|
+
* Python and PHP populated it from a probe, so `db.fetch(sql).count` answered
|
|
593
|
+
* 20 here and 250 there for one query against one table, and every paginated
|
|
594
|
+
* response built on it under-reported. MEASURED 2026-08-05 on a 250-row table
|
|
595
|
+
* read with limit=20: Node reported total 20 over 2 pages against Python's
|
|
596
|
+
* 250 over 13.
|
|
597
|
+
*
|
|
598
|
+
* Only probed when a limit was actually applied. With no limit the rows
|
|
599
|
+
* returned ARE the whole answer for this SQL, so `records.length` is already
|
|
600
|
+
* the true total and a second round-trip would buy nothing — which is also
|
|
601
|
+
* what keeps `fetchAll()` at one query.
|
|
602
|
+
*
|
|
603
|
+
* BEST EFFORT, and it can never mask a real failure: it runs AFTER the main
|
|
604
|
+
* query (which has already thrown on bad SQL) and returns undefined on any
|
|
605
|
+
* error. `undefined` — not 0 — is the miss value, so DatabaseResult falls
|
|
606
|
+
* back to records.length, a true lower bound. Reporting 0 next to 100 real
|
|
607
|
+
* records would be the same "states a wrong number authoritatively" defect
|
|
608
|
+
* this change exists to remove.
|
|
609
|
+
*
|
|
610
|
+
* The closing paren goes on its OWN LINE: appended inline, a trailing
|
|
611
|
+
* `-- comment` in the caller's SQL comments it out and the probe dies with
|
|
612
|
+
* "incomplete input". Postgres, MySQL and MSSQL additionally require a name
|
|
613
|
+
* for the derived table; SQLite and Firebird do not, and Firebird rejects
|
|
614
|
+
* `AS` there — so the alias comes from the adapter, not an assumption.
|
|
615
|
+
*/
|
|
616
|
+
private async countProbe(
|
|
617
|
+
adapter: DatabaseAdapter,
|
|
618
|
+
sql: string,
|
|
619
|
+
params: unknown[] | undefined,
|
|
620
|
+
limit: number | undefined,
|
|
621
|
+
): Promise<number | undefined> {
|
|
622
|
+
if (limit === undefined || limit <= 0) return undefined;
|
|
623
|
+
try {
|
|
624
|
+
const alias = (adapter as any).countSubqueryAlias as string | undefined;
|
|
625
|
+
const suffix = alias ? ` AS ${alias}` : "";
|
|
626
|
+
const rows = await adapterFetch(
|
|
627
|
+
adapter,
|
|
628
|
+
`SELECT COUNT(*) AS tina4_total FROM (${sql}\n)${suffix}`,
|
|
629
|
+
params,
|
|
630
|
+
undefined,
|
|
631
|
+
undefined,
|
|
632
|
+
true,
|
|
633
|
+
);
|
|
634
|
+
const row = Array.isArray(rows) ? (rows[0] as Record<string, unknown> | undefined) : undefined;
|
|
635
|
+
if (!row) return undefined;
|
|
636
|
+
const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
|
|
637
|
+
const n = Number(value);
|
|
638
|
+
return Number.isFinite(n) ? n : undefined;
|
|
639
|
+
} catch {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
694
644
|
/**
|
|
695
645
|
* Fetch a single row or null.
|
|
696
646
|
*
|
|
@@ -738,7 +688,10 @@ export class Database {
|
|
|
738
688
|
* SEPARATE trailing argument, never the params array.
|
|
739
689
|
*/
|
|
740
690
|
async fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean }): Promise<T[]> {
|
|
741
|
-
|
|
691
|
+
// Routes through _fetchWithLimit, NOT fetch(), so `limit` stays verbatim.
|
|
692
|
+
// Going through fetch() would apply the 100-row cap and make a method
|
|
693
|
+
// called "fetchAll" quietly stop returning them all.
|
|
694
|
+
return (await this._fetchWithLimit(sql, params, limit, offset, opts)).records as T[];
|
|
742
695
|
}
|
|
743
696
|
|
|
744
697
|
/**
|
|
@@ -787,28 +740,174 @@ export class Database {
|
|
|
787
740
|
return result;
|
|
788
741
|
}
|
|
789
742
|
|
|
790
|
-
/**
|
|
791
|
-
|
|
743
|
+
/**
|
|
744
|
+
* The table's primary-key column, introspected once and cached.
|
|
745
|
+
*
|
|
746
|
+
* Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
|
|
747
|
+
* primaryKey per column on every adapter. Resolves to null when the table has
|
|
748
|
+
* no primary key or cannot be introspected.
|
|
749
|
+
*/
|
|
750
|
+
async primaryKey(table: string): Promise<string[]> {
|
|
751
|
+
if (!this._pkCache.has(table)) {
|
|
752
|
+
let pk: string[] = [];
|
|
753
|
+
try {
|
|
754
|
+
const columns = await this.getColumns(table);
|
|
755
|
+
pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
756
|
+
} catch {
|
|
757
|
+
pk = [];
|
|
758
|
+
}
|
|
759
|
+
this._pkCache.set(table, pk);
|
|
760
|
+
}
|
|
761
|
+
return this._pkCache.get(table) ?? [];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* A failed write must be loud.
|
|
766
|
+
*
|
|
767
|
+
* The adapters catch a SQL error and return { success: false, affectedRows: 0 },
|
|
768
|
+
* so a filterless update produced invalid SQL ("... WHERE ") and reported
|
|
769
|
+
* nothing rather than raising. A caller who does not inspect the result
|
|
770
|
+
* believes the write landed (audit feature 4, P1).
|
|
771
|
+
*/
|
|
772
|
+
private static assertWrote(result: DatabaseWriteResult, verb: string, table: string): DatabaseWriteResult {
|
|
773
|
+
if (result && (result as any).success === false) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`${verb} failed on ${table}: ${(result as any).error ?? "unknown error"}`,
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Update rows. A write with no filter is an error, not a full-table write.
|
|
783
|
+
*
|
|
784
|
+
* With no explicit filter the primary key is taken out of `data` and used as
|
|
785
|
+
* the WHERE clause. With neither a filter nor a primary key in `data` this
|
|
786
|
+
* throws rather than silently changing nothing (audit feature 4, P1).
|
|
787
|
+
*/
|
|
788
|
+
async update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseWriteResult> {
|
|
789
|
+
let effectiveFilter: Record<string, unknown> | string = filter ?? {};
|
|
790
|
+
let effectiveData = data;
|
|
791
|
+
|
|
792
|
+
// A string filter is the OTHER documented form ("id = ?" + params), so it
|
|
793
|
+
// must be tested as a string: Object.keys("id = ?") is ["0",..,"5"], which
|
|
794
|
+
// is non-empty by accident rather than by meaning — and an EMPTY string
|
|
795
|
+
// filter would then be treated as a real filter instead of falling through
|
|
796
|
+
// to the primary key.
|
|
797
|
+
const filterIsEmpty = typeof effectiveFilter === "string"
|
|
798
|
+
? effectiveFilter.trim() === ""
|
|
799
|
+
: Object.keys(effectiveFilter).length === 0;
|
|
800
|
+
|
|
801
|
+
if (filterIsEmpty) {
|
|
802
|
+
const pkColumns = await this.primaryKey(table);
|
|
803
|
+
// Resolve each key column to the caller's OWN key for it, matched
|
|
804
|
+
// case-insensitively.
|
|
805
|
+
//
|
|
806
|
+
// The engines disagree about identifier case BY DESIGN and always will:
|
|
807
|
+
// Firebird folds an unquoted identifier to UPPER, PostgreSQL folds it to
|
|
808
|
+
// LOWER, MySQL and SQLite preserve what was typed. Introspection returns
|
|
809
|
+
// the ENGINE's spelling while `data` carries the caller's, so `c in data`
|
|
810
|
+
// failed on whichever engine folds the other way. A case-sensitivity bug,
|
|
811
|
+
// not a Firebird quirk - Firebird just made it visible first.
|
|
812
|
+
//
|
|
813
|
+
// Deliberately does NOT lower-case introspection output: that would
|
|
814
|
+
// special-case one engine and break a genuinely quoted mixed-case table.
|
|
815
|
+
// The WHERE is built from the ENGINE's column name and the CALLER's value.
|
|
816
|
+
const resolved: Record<string, string> = {};
|
|
817
|
+
const missing: string[] = [];
|
|
818
|
+
for (const col of pkColumns) {
|
|
819
|
+
const folded = String(col).toLowerCase();
|
|
820
|
+
const matches = Object.keys(data).filter((k) => k.toLowerCase() === folded);
|
|
821
|
+
if (matches.length > 1) {
|
|
822
|
+
// Ambiguity is refused, never guessed - choosing wrong here writes the
|
|
823
|
+
// WHERE clause of an UPDATE.
|
|
824
|
+
throw new Error(
|
|
825
|
+
`update was given more than one key for the primary-key column ${col}: ` +
|
|
826
|
+
`[${matches.slice().sort().join(", ")}] (table=${table}). These differ ` +
|
|
827
|
+
`only by case, so which one identifies the row is ambiguous - pass ` +
|
|
828
|
+
`exactly one, or pass an explicit filter.`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
if (matches.length === 1) resolved[col] = matches[0];
|
|
832
|
+
else missing.push(col);
|
|
833
|
+
}
|
|
834
|
+
if (pkColumns.length === 0 || missing.length > 0) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`update requires a filter or the complete primary key in the data; pass ` +
|
|
837
|
+
`filter explicitly to update multiple rows (table=${table}, ` +
|
|
838
|
+
`primary key=[${pkColumns.join(", ")}], missing from data=[${missing.join(", ")}]). ` +
|
|
839
|
+
`To empty a table use truncate(${table}).`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
// EVERY key column goes into the WHERE. A composite key built from only its
|
|
843
|
+
// first column would match every row sharing that value - the data-loss bug
|
|
844
|
+
// this method exists to prevent, reintroduced.
|
|
845
|
+
effectiveData = { ...data };
|
|
846
|
+
const keyed: Record<string, unknown> = {};
|
|
847
|
+
for (const col of pkColumns) {
|
|
848
|
+
const callerKey = resolved[col];
|
|
849
|
+
keyed[col] = effectiveData[callerKey];
|
|
850
|
+
delete effectiveData[callerKey];
|
|
851
|
+
}
|
|
852
|
+
if (Object.keys(effectiveData).length === 0) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
`update was given only the primary key [${pkColumns.join(", ")}] and no ` +
|
|
855
|
+
`columns to set (table=${table})`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
effectiveFilter = keyed;
|
|
859
|
+
}
|
|
860
|
+
|
|
792
861
|
const adapter = this.getNextAdapter();
|
|
793
862
|
const result = (adapter as any).updateAsync
|
|
794
|
-
? await (adapter as any).updateAsync(table,
|
|
795
|
-
: adapter.update(table,
|
|
863
|
+
? await (adapter as any).updateAsync(table, effectiveData, effectiveFilter, params)
|
|
864
|
+
: adapter.update(table, effectiveData, effectiveFilter, params);
|
|
796
865
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
797
866
|
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
798
867
|
}
|
|
799
|
-
return result;
|
|
868
|
+
return Database.assertWrote(result, "update", table);
|
|
800
869
|
}
|
|
801
870
|
|
|
802
|
-
/** Delete rows
|
|
803
|
-
async delete(table: string, filter?: Record<string, unknown
|
|
871
|
+
/** Delete rows. A filterless delete throws; use truncate() to empty a table. */
|
|
872
|
+
async delete(table: string, filter?: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseWriteResult> {
|
|
873
|
+
const effectiveFilter = filter ?? {};
|
|
874
|
+
// A BLANK string counts as no filter. The old guard skipped the emptiness
|
|
875
|
+
// test for anything typed string, so `delete(t, "")` fell through to the
|
|
876
|
+
// adapter, which renders an empty WHERE as `DELETE FROM "t"` — a silent
|
|
877
|
+
// whole-table delete through the very method that exists to make that
|
|
878
|
+
// impossible. truncate() is the explicit spelling.
|
|
879
|
+
const filterIsEmpty = Array.isArray(effectiveFilter)
|
|
880
|
+
? effectiveFilter.length === 0
|
|
881
|
+
: typeof effectiveFilter === "string"
|
|
882
|
+
? effectiveFilter.trim() === ""
|
|
883
|
+
: Object.keys(effectiveFilter).length === 0;
|
|
884
|
+
if (filterIsEmpty) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
`delete requires a filter (table=${table}). To remove every row use truncate(${table}).`,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
804
890
|
const adapter = this.getNextAdapter();
|
|
805
891
|
const result = (adapter as any).deleteAsync
|
|
806
|
-
? await (adapter as any).deleteAsync(table,
|
|
807
|
-
: adapter.delete(table,
|
|
892
|
+
? await (adapter as any).deleteAsync(table, effectiveFilter, params)
|
|
893
|
+
: adapter.delete(table, effectiveFilter, params);
|
|
808
894
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
809
895
|
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
810
896
|
}
|
|
811
|
-
return result;
|
|
897
|
+
return Database.assertWrote(result, "delete", table);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** Remove every row. The explicit spelling of a whole-table delete. */
|
|
901
|
+
async truncate(table: string): Promise<DatabaseWriteResult> {
|
|
902
|
+
const adapter = this.getNextAdapter();
|
|
903
|
+
// The adapters' delete() already accepts a raw string WHERE clause.
|
|
904
|
+
const result = (adapter as any).deleteAsync
|
|
905
|
+
? await (adapter as any).deleteAsync(table, "1 = 1", [])
|
|
906
|
+
: adapter.delete(table, "1 = 1" as any, []);
|
|
907
|
+
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
908
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
909
|
+
}
|
|
910
|
+
return Database.assertWrote(result, "truncate", table);
|
|
812
911
|
}
|
|
813
912
|
|
|
814
913
|
/** Close all database connections (pool or single). */
|
|
@@ -975,9 +1074,35 @@ export class Database {
|
|
|
975
1074
|
// (Database.execute_many delegating to adapter.execute_many's owns_txn guard).
|
|
976
1075
|
const owns = !this.inExplicitTransaction();
|
|
977
1076
|
if (owns) await adapterStartTransaction(adapter);
|
|
1077
|
+
|
|
1078
|
+
// ONE round-trip per CHUNK instead of one per ROW. Looping execute() here
|
|
1079
|
+
// pays a full network round-trip for every row: 500 rows took 9848ms on
|
|
1080
|
+
// PostgreSQL against 15.8ms as a single multi-row VALUES (625x), MySQL 216x,
|
|
1081
|
+
// MSSQL 121x. buildBatchInserts returns an empty array for anything it
|
|
1082
|
+
// cannot collapse safely — RETURNING, upserts, non-INSERT statements, ragged
|
|
1083
|
+
// rows, Firebird — and the row-at-a-time loop then runs unchanged.
|
|
1084
|
+
const batched = SQLTranslator.buildBatchInserts(sql, paramSets, this.dbType ?? "");
|
|
1085
|
+
|
|
978
1086
|
try {
|
|
979
|
-
|
|
980
|
-
|
|
1087
|
+
if (batched.length > 0) {
|
|
1088
|
+
let row = 0;
|
|
1089
|
+
for (const [chunkSql, chunkParams] of batched) {
|
|
1090
|
+
const result = await adapterExecute(adapter, chunkSql, chunkParams);
|
|
1091
|
+
// executeMany's contract is ONE RESULT PER ROW, and callers index into
|
|
1092
|
+
// it. Collapsing rows into chunks must not shorten the array, so each
|
|
1093
|
+
// row reports the result of the statement that actually wrote it.
|
|
1094
|
+
// Node is the only one of the four returning per-row results — Python,
|
|
1095
|
+
// PHP and Ruby return a count or a single DatabaseResult — so this is
|
|
1096
|
+
// the one place the collapse could have been observable.
|
|
1097
|
+
const rowsInChunk = chunkParams.length / (paramSets[0]?.length || 1);
|
|
1098
|
+
for (let i = 0; i < rowsInChunk && row < paramSets.length; i++, row++) {
|
|
1099
|
+
results.push(result);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
for (const params of paramSets) {
|
|
1104
|
+
results.push(await adapterExecute(adapter, sql, params));
|
|
1105
|
+
}
|
|
981
1106
|
}
|
|
982
1107
|
if (owns) await adapterCommit(adapter);
|
|
983
1108
|
} catch (e) {
|
|
@@ -1316,21 +1441,31 @@ export class Database {
|
|
|
1316
1441
|
* connected; SQLite connects lazily.
|
|
1317
1442
|
*/
|
|
1318
1443
|
export async function createAdapterFromUrl(url: string, username?: string, password?: string): Promise<DatabaseAdapter> {
|
|
1444
|
+
const adapter = await buildAdapterFromUrl(url, username, password);
|
|
1445
|
+
// Tag the adapter with WHICH DATABASE it is connected to. The query cache
|
|
1446
|
+
// folds this into every key, so two databases sharing one cache backend
|
|
1447
|
+
// cannot serve each other's rows. Set here because this is the single funnel
|
|
1448
|
+
// where a URL becomes an adapter.
|
|
1449
|
+
adapter.cacheIdentity = QueryCache.cacheIdentity(url);
|
|
1450
|
+
return adapter;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
async function buildAdapterFromUrl(url: string, username?: string, password?: string): Promise<DatabaseAdapter> {
|
|
1319
1454
|
const parsed = parseDatabaseUrl(url, username, password);
|
|
1320
1455
|
|
|
1321
|
-
switch (parsed.
|
|
1456
|
+
switch (parsed.engine) {
|
|
1322
1457
|
case "sqlite": {
|
|
1323
1458
|
const { SQLiteAdapter } = await import("./adapters/sqlite.js");
|
|
1324
|
-
return new SQLiteAdapter(parsed.
|
|
1459
|
+
return new SQLiteAdapter(parsed.database || "./data/tina4.db");
|
|
1325
1460
|
}
|
|
1326
1461
|
case "postgres": {
|
|
1327
1462
|
const { PostgresAdapter } = await import("./adapters/postgres.js");
|
|
1328
1463
|
const adapter = new PostgresAdapter({
|
|
1329
|
-
host: parsed.host,
|
|
1330
|
-
port: parsed.port,
|
|
1331
|
-
user: parsed.
|
|
1332
|
-
password: parsed.password,
|
|
1333
|
-
database: parsed.database,
|
|
1464
|
+
host: parsed.host ?? undefined,
|
|
1465
|
+
port: parsed.port ?? undefined,
|
|
1466
|
+
user: parsed.username ?? undefined,
|
|
1467
|
+
password: parsed.password ?? undefined,
|
|
1468
|
+
database: parsed.database || undefined,
|
|
1334
1469
|
});
|
|
1335
1470
|
await adapter.connect();
|
|
1336
1471
|
return adapter;
|
|
@@ -1338,11 +1473,11 @@ export async function createAdapterFromUrl(url: string, username?: string, passw
|
|
|
1338
1473
|
case "mysql": {
|
|
1339
1474
|
const { MysqlAdapter } = await import("./adapters/mysql.js");
|
|
1340
1475
|
const adapter = new MysqlAdapter({
|
|
1341
|
-
host: parsed.host,
|
|
1342
|
-
port: parsed.port,
|
|
1343
|
-
user: parsed.
|
|
1344
|
-
password: parsed.password,
|
|
1345
|
-
database: parsed.database,
|
|
1476
|
+
host: parsed.host ?? undefined,
|
|
1477
|
+
port: parsed.port ?? undefined,
|
|
1478
|
+
user: parsed.username ?? undefined,
|
|
1479
|
+
password: parsed.password ?? undefined,
|
|
1480
|
+
database: parsed.database || undefined,
|
|
1346
1481
|
});
|
|
1347
1482
|
await adapter.connect();
|
|
1348
1483
|
return adapter;
|
|
@@ -1350,11 +1485,11 @@ export async function createAdapterFromUrl(url: string, username?: string, passw
|
|
|
1350
1485
|
case "mssql": {
|
|
1351
1486
|
const { MssqlAdapter } = await import("./adapters/mssql.js");
|
|
1352
1487
|
const adapter = new MssqlAdapter({
|
|
1353
|
-
host: parsed.host,
|
|
1354
|
-
port: parsed.port,
|
|
1355
|
-
user: parsed.
|
|
1356
|
-
password: parsed.password,
|
|
1357
|
-
database: parsed.database,
|
|
1488
|
+
host: parsed.host ?? undefined,
|
|
1489
|
+
port: parsed.port ?? undefined,
|
|
1490
|
+
user: parsed.username ?? undefined,
|
|
1491
|
+
password: parsed.password ?? undefined,
|
|
1492
|
+
database: parsed.database || undefined,
|
|
1358
1493
|
});
|
|
1359
1494
|
await adapter.connect();
|
|
1360
1495
|
return adapter;
|
|
@@ -1362,11 +1497,11 @@ export async function createAdapterFromUrl(url: string, username?: string, passw
|
|
|
1362
1497
|
case "firebird": {
|
|
1363
1498
|
const { FirebirdAdapter } = await import("./adapters/firebird.js");
|
|
1364
1499
|
const adapter = new FirebirdAdapter({
|
|
1365
|
-
host: parsed.host,
|
|
1366
|
-
port: parsed.port,
|
|
1367
|
-
user: parsed.
|
|
1368
|
-
password: parsed.password,
|
|
1369
|
-
database: parsed.database,
|
|
1500
|
+
host: parsed.host ?? undefined,
|
|
1501
|
+
port: parsed.port ?? undefined,
|
|
1502
|
+
user: parsed.username ?? undefined,
|
|
1503
|
+
password: parsed.password ?? undefined,
|
|
1504
|
+
database: parsed.database || undefined,
|
|
1370
1505
|
});
|
|
1371
1506
|
await adapter.connect();
|
|
1372
1507
|
return adapter;
|
|
@@ -1477,7 +1612,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1477
1612
|
const parsed = parseDatabaseUrl(url, resolvedUser, resolvedPassword);
|
|
1478
1613
|
const adapter = await createAdapterFromUrl(url, resolvedUser, resolvedPassword);
|
|
1479
1614
|
const db = new Database(setAdapter(adapter));
|
|
1480
|
-
db.setDbType(parsed.
|
|
1615
|
+
db.setDbType(parsed.engine);
|
|
1481
1616
|
return exposeDb(db);
|
|
1482
1617
|
}
|
|
1483
1618
|
|
|
@@ -1504,6 +1639,11 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1504
1639
|
// default and a `{ type: "postgres" }` connection takes the SQLite getNextId
|
|
1505
1640
|
// branch and crashes on the missing tina4_sequences table (#255).
|
|
1506
1641
|
const finished = (adapter: DatabaseAdapter): Database => {
|
|
1642
|
+
// Same identity tag as the URL path above - a config-object connection is
|
|
1643
|
+
// just as capable of sharing a cache backend with another database.
|
|
1644
|
+
adapter.cacheIdentity = QueryCache.cacheIdentity(
|
|
1645
|
+
`${type}://${config?.host ?? ""}:${config?.port ?? ""}/${config?.database ?? config?.path ?? ""}`,
|
|
1646
|
+
);
|
|
1507
1647
|
const db = new Database(setAdapter(adapter));
|
|
1508
1648
|
db.setDbType(type);
|
|
1509
1649
|
return exposeDb(db);
|