@atscript/db-postgres 0.1.153 → 0.1.155
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/dist/index.cjs +89 -12
- package/dist/index.d.cts +24 -5
- package/dist/index.d.mts +24 -5
- package/dist/index.mjs +90 -13
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -138,6 +138,12 @@ function toPgValue(value) {
|
|
|
138
138
|
if (typeof value === "object") return JSON.stringify(value);
|
|
139
139
|
return value;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Text a JSON column can take as it is (`SqlDialect.jsonFromText`): an
|
|
143
|
+
* object / array (by its first characters), or a whole JSON string, number,
|
|
144
|
+
* `true`, `false` or `null` — so `42 Main St` is no number.
|
|
145
|
+
*/
|
|
146
|
+
const PG_JSON_LIKE = String.raw`^\s*(\{\s*["}]|\[\s*(["{[\]tfn0-9-])|("([^"\\]|\\.)*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?|true|false|null)\s*$)`;
|
|
141
147
|
const pgDialect = {
|
|
142
148
|
quoteIdentifier(name) {
|
|
143
149
|
return qi(name);
|
|
@@ -171,6 +177,9 @@ const pgDialect = {
|
|
|
171
177
|
},
|
|
172
178
|
calendarBucket: pgCalendarBucket,
|
|
173
179
|
jsonExtract: pgJsonExtract,
|
|
180
|
+
jsonExtractText: (quotedCol, path) => `((${quotedCol})::jsonb #>> ${pgJsonPath(path)})`,
|
|
181
|
+
jsonExtractValue: (quotedCol, path) => `NULLIF((${quotedCol})::jsonb #> ${pgJsonPath(path)}, 'null'::jsonb)`,
|
|
182
|
+
jsonFromText: (quotedCol) => `CASE WHEN ${quotedCol} ~ '${PG_JSON_LIKE}' THEN ${quotedCol} ELSE to_jsonb(${quotedCol})::text END`,
|
|
174
183
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
|
|
175
184
|
anyValue: pgAnyValue,
|
|
176
185
|
booleanAggregates: {
|
|
@@ -204,6 +213,10 @@ const PG_JSON_LEAF_CAST = {
|
|
|
204
213
|
number: "::double precision",
|
|
205
214
|
boolean: "::boolean"
|
|
206
215
|
};
|
|
216
|
+
/** A `#>` / `#>>` path literal (`'{"a","b"}'`). */
|
|
217
|
+
function pgJsonPath(path) {
|
|
218
|
+
return (0, _atscript_db_sql_tools.sqlStringLiteral)(`{${(0, _atscript_db_sql_tools.quotedJsonPathSegments)(path).join(",")}}`);
|
|
219
|
+
}
|
|
207
220
|
/**
|
|
208
221
|
* Typed read of a JSON leaf (`SqlDialect.jsonExtract`): `jsonb_typeof()`
|
|
209
222
|
* guards the declared type, so a missing path, JSON `null` or another JSON
|
|
@@ -211,7 +224,7 @@ const PG_JSON_LEAF_CAST = {
|
|
|
211
224
|
* native. The column is cast `::jsonb`, so JSON and JSONB storage both work.
|
|
212
225
|
*/
|
|
213
226
|
function pgJsonExtract(quotedCol, path, type) {
|
|
214
|
-
const p = (
|
|
227
|
+
const p = pgJsonPath(path);
|
|
215
228
|
const j = `(${quotedCol})::jsonb`;
|
|
216
229
|
const cast = PG_JSON_LEAF_CAST[type];
|
|
217
230
|
return `CASE jsonb_typeof(${j} #> ${p}) WHEN '${type}' THEN ${cast ? `(${j} #>> ${p})${cast}` : `${j} #>> ${p}`} END`;
|
|
@@ -513,6 +526,13 @@ const GEO_PROBE_SQL = `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname =
|
|
|
513
526
|
LEFT JOIN pg_type b ON b.oid = NULLIF(t.typbasetype, 0)
|
|
514
527
|
WHERE a.attrelid = to_regclass($1::text) AND a.attnum > 0 AND NOT a.attisdropped
|
|
515
528
|
AND a.attname = ANY($2::text[])) AS "columns"`;
|
|
529
|
+
const identity = (value) => value;
|
|
530
|
+
/** A column type node-postgres parses on read. */
|
|
531
|
+
const PG_JSON_TYPE = /^\s*jsonb?\s*$/i;
|
|
532
|
+
/** A JSON string value as JSON text — see `formatValue`. */
|
|
533
|
+
function reencodeJsonString(value) {
|
|
534
|
+
return typeof value === "string" ? JSON.stringify(value) : value;
|
|
535
|
+
}
|
|
516
536
|
/** A PostGIS column type — a geo value binds as EWKT. Anything else (JSONB, TEXT) takes the JSON form. */
|
|
517
537
|
const POSTGIS_TYPES = new Set(["geography", "geometry"]);
|
|
518
538
|
/** The SQL `$geoWithin` renders (`pgDialect.geoWithin`) — PostGIS only. */
|
|
@@ -868,6 +888,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
868
888
|
}
|
|
869
889
|
};
|
|
870
890
|
}
|
|
891
|
+
if (field.storage === "json" && !field.encrypted && PG_JSON_TYPE.test(this.typeMapper(field))) return {
|
|
892
|
+
toStorage: identity,
|
|
893
|
+
fromStorage: reencodeJsonString
|
|
894
|
+
};
|
|
871
895
|
}
|
|
872
896
|
/**
|
|
873
897
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -1305,6 +1329,12 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1305
1329
|
this._log(sql);
|
|
1306
1330
|
return (await this._exec().get(sql, []))?.present ?? false;
|
|
1307
1331
|
}
|
|
1332
|
+
async countKeyViolations(columns, tableName) {
|
|
1333
|
+
const sql = (0, _atscript_db_sql_tools.buildKeyViolationCount)(pgDialect, tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName()), columns);
|
|
1334
|
+
this._log(sql);
|
|
1335
|
+
const row = await this._exec().get(sql, []);
|
|
1336
|
+
return Number(row?.violations ?? 0);
|
|
1337
|
+
}
|
|
1308
1338
|
/**
|
|
1309
1339
|
* Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
|
|
1310
1340
|
* composite keys (`conkey`/`confkey` are positionally aligned, unlike the
|
|
@@ -1350,8 +1380,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1350
1380
|
}
|
|
1351
1381
|
/**
|
|
1352
1382
|
* `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
|
|
1353
|
-
* (atomic
|
|
1354
|
-
* key
|
|
1383
|
+
* (atomic: rows a concurrent write made violate the new key fail it with
|
|
1384
|
+
* the old key in place). Called on an empty table or one whose rows satisfy
|
|
1385
|
+
* the new key; `ADD PRIMARY KEY` makes the new key columns NOT NULL. A demoted identity column is handled by the
|
|
1355
1386
|
* `defaultChanged` diff (`DROP IDENTITY`).
|
|
1356
1387
|
*/
|
|
1357
1388
|
async rebuildPrimaryKey(change) {
|
|
@@ -1445,10 +1476,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1445
1476
|
this._log(ddl);
|
|
1446
1477
|
await this._exec().exec(ddl);
|
|
1447
1478
|
}
|
|
1448
|
-
for (const { field } of diff.typeChanged ?? []) {
|
|
1479
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1449
1480
|
const sqlType = this.typeMapper(field);
|
|
1450
1481
|
const col = qi(field.physicalName);
|
|
1451
|
-
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${convertColumnExpr(col, field, sqlType)}`;
|
|
1482
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${convertColumnExpr(col, field, sqlType, existingType)}`;
|
|
1452
1483
|
this._log(ddl);
|
|
1453
1484
|
await this._exec().exec(ddl);
|
|
1454
1485
|
}
|
|
@@ -1556,8 +1587,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1556
1587
|
const selectExprs = commonFields.map((field) => {
|
|
1557
1588
|
const c = qi(field.physicalName);
|
|
1558
1589
|
const sqlType = this.typeMapper(field);
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1590
|
+
const oldType = oldTypes.get(field.physicalName);
|
|
1591
|
+
const changed = (0, _atscript_db.isColumnTypeChanged)(oldType, sqlType);
|
|
1592
|
+
const value = changed ? convertColumnExpr(c, field, sqlType, oldType) : c;
|
|
1561
1593
|
if (!field.optional && !field.isPrimaryKey) return `COALESCE(${value}, ${field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType)}) AS ${c}`;
|
|
1562
1594
|
return changed ? `${value} AS ${c}` : c;
|
|
1563
1595
|
}).join(", ");
|
|
@@ -1674,6 +1706,20 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1674
1706
|
this._log(ddl);
|
|
1675
1707
|
await this._exec().exec(ddl);
|
|
1676
1708
|
}
|
|
1709
|
+
async copyFromJsonColumn(source, targets) {
|
|
1710
|
+
const sql = (0, _atscript_db_sql_tools.buildJsonColumnCopy)(pgDialect, this.resolveTableName(), source, targets, (expr, target) => {
|
|
1711
|
+
if (target.kind !== "text") return expr;
|
|
1712
|
+
const type = this.typeMapper(target.field);
|
|
1713
|
+
return /^(VARCHAR|CHAR)\b/i.test(type) ? expr : `(${expr})::${type}`;
|
|
1714
|
+
});
|
|
1715
|
+
this._log(sql);
|
|
1716
|
+
await this._exec().exec(sql);
|
|
1717
|
+
}
|
|
1718
|
+
async jsonifyTextColumn(column) {
|
|
1719
|
+
const sql = (0, _atscript_db_sql_tools.buildJsonifyText)(pgDialect, this.resolveTableName(), column);
|
|
1720
|
+
this._log(sql);
|
|
1721
|
+
await this._exec().exec(sql);
|
|
1722
|
+
}
|
|
1677
1723
|
async dropColumns(columns) {
|
|
1678
1724
|
const tableName = this.resolveTableName();
|
|
1679
1725
|
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
@@ -2190,6 +2236,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2190
2236
|
};
|
|
2191
2237
|
/** A text / character type: the value is converted by the assignment into the column. */
|
|
2192
2238
|
const CHARACTER_TYPE = /^\s*(?:text|varchar|character varying|char|character|bpchar)\b/i;
|
|
2239
|
+
/** A floating-point / decimal column type (live or mapped). */
|
|
2240
|
+
const FRACTIONAL_TYPE = /^\s*(?:double precision|float[48]?|real|numeric|decimal)\b/i;
|
|
2193
2241
|
/**
|
|
2194
2242
|
* The expression converting column `col` (quoted) to `sqlType`, the field's
|
|
2195
2243
|
* new type — one rule for `ALTER COLUMN … TYPE … USING` and the recreate copy.
|
|
@@ -2197,11 +2245,19 @@ const CHARACTER_TYPE = /^\s*(?:text|varchar|character varying|char|character|bpc
|
|
|
2197
2245
|
* fails the statement instead of being coerced. A TEXT / VARCHAR(n) / CHAR(n)
|
|
2198
2246
|
* target stops at `::text`: an explicit cast to VARCHAR(n) / CHAR(n) would
|
|
2199
2247
|
* truncate an over-long value, the assignment into the column rejects it
|
|
2200
|
-
* (since 0.1.137).
|
|
2248
|
+
* (since 0.1.137). An epoch-ms fractional column (`existingType`) becoming
|
|
2249
|
+
* the `BIGINT` of `@db.default.now` (`number.timestamp` →
|
|
2250
|
+
* `number.timestamp.created`) is rounded (since 0.1.155): its text
|
|
2251
|
+
* (`1700000000123.5`) does not parse as an integer. Any other fractional
|
|
2252
|
+
* value still fails a change to an integer type.
|
|
2201
2253
|
*/
|
|
2202
|
-
function convertColumnExpr(col, field, sqlType) {
|
|
2203
|
-
if (field.isGeoPoint && sqlType.startsWith("geography"))
|
|
2254
|
+
function convertColumnExpr(col, field, sqlType, existingType) {
|
|
2255
|
+
if (field.isGeoPoint && sqlType.startsWith("geography")) {
|
|
2256
|
+
const json = `${col}::jsonb`;
|
|
2257
|
+
return `CASE WHEN ${col} IS NULL THEN NULL ELSE ST_SetSRID(ST_MakePoint((${json}->>0)::float8, (${json}->>1)::float8), 4326)::geography END`;
|
|
2258
|
+
}
|
|
2204
2259
|
if (CHARACTER_TYPE.test(sqlType)) return `${col}::text`;
|
|
2260
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now" && /^BIGINT$/i.test(sqlType.trim()) && existingType && FRACTIONAL_TYPE.test(existingType)) return `round(${col})::bigint`;
|
|
2205
2261
|
return `${col}::text::${sqlType}`;
|
|
2206
2262
|
}
|
|
2207
2263
|
/**
|
|
@@ -2361,6 +2417,10 @@ function createCustomTypes(pgTypes) {
|
|
|
2361
2417
|
return pgTypes.getTypeParser(oid, format);
|
|
2362
2418
|
} };
|
|
2363
2419
|
}
|
|
2420
|
+
/** The warning logged for a pool client whose connection broke (`57P01`, `ECONNRESET`, …). */
|
|
2421
|
+
function lostConnectionMessage(err, where) {
|
|
2422
|
+
return `[atscript/db-postgres] ${where} pool connection lost (${err.code ?? err.name}): ${err.message}`;
|
|
2423
|
+
}
|
|
2364
2424
|
/**
|
|
2365
2425
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
2366
2426
|
*
|
|
@@ -2387,6 +2447,12 @@ function createCustomTypes(pgTypes) {
|
|
|
2387
2447
|
* const driver = new PgDriver(pool)
|
|
2388
2448
|
* ```
|
|
2389
2449
|
*
|
|
2450
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
2451
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
2452
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
2453
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
2454
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
2455
|
+
*
|
|
2390
2456
|
* Requires `pg` to be installed:
|
|
2391
2457
|
* ```bash
|
|
2392
2458
|
* pnpm add pg
|
|
@@ -2395,7 +2461,9 @@ function createCustomTypes(pgTypes) {
|
|
|
2395
2461
|
var PgDriver = class {
|
|
2396
2462
|
pool;
|
|
2397
2463
|
poolInit;
|
|
2398
|
-
|
|
2464
|
+
logger;
|
|
2465
|
+
constructor(poolOrConfig, options = {}) {
|
|
2466
|
+
this.logger = options.logger ?? console;
|
|
2399
2467
|
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
2400
2468
|
else this.poolInit = import("pg").then((pg) => {
|
|
2401
2469
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
@@ -2409,6 +2477,7 @@ var PgDriver = class {
|
|
|
2409
2477
|
...poolOrConfig,
|
|
2410
2478
|
types: customTypes
|
|
2411
2479
|
});
|
|
2480
|
+
this.pool.on("error", (err) => this.logger.warn(lostConnectionMessage(err, "idle")));
|
|
2412
2481
|
return this.pool;
|
|
2413
2482
|
});
|
|
2414
2483
|
}
|
|
@@ -2433,6 +2502,13 @@ var PgDriver = class {
|
|
|
2433
2502
|
}
|
|
2434
2503
|
async getConnection() {
|
|
2435
2504
|
const client = await (await this.getPool()).connect();
|
|
2505
|
+
let lost;
|
|
2506
|
+
const onError = (err) => {
|
|
2507
|
+
if (lost) return;
|
|
2508
|
+
lost = err;
|
|
2509
|
+
this.logger.warn(lostConnectionMessage(err, "checked-out"));
|
|
2510
|
+
};
|
|
2511
|
+
client.on("error", onError);
|
|
2436
2512
|
return {
|
|
2437
2513
|
async run(sql, params) {
|
|
2438
2514
|
const result = await client.query(sql, sanitizeParams(params));
|
|
@@ -2451,7 +2527,8 @@ var PgDriver = class {
|
|
|
2451
2527
|
await client.query(sql);
|
|
2452
2528
|
},
|
|
2453
2529
|
release() {
|
|
2454
|
-
client.
|
|
2530
|
+
client.removeListener("error", onError);
|
|
2531
|
+
client.release(lost);
|
|
2455
2532
|
}
|
|
2456
2533
|
};
|
|
2457
2534
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
3
3
|
import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
|
|
4
4
|
|
|
@@ -293,6 +293,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
293
293
|
/** `schema.name` when the adapter targets a named schema. */
|
|
294
294
|
private _qualify;
|
|
295
295
|
hasRows(tableName?: string): Promise<boolean>;
|
|
296
|
+
countKeyViolations(columns: readonly string[], tableName?: string): Promise<number>;
|
|
296
297
|
/**
|
|
297
298
|
* Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
|
|
298
299
|
* composite keys (`conkey`/`confkey` are positionally aligned, unlike the
|
|
@@ -304,8 +305,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
304
305
|
dropTablesByName(tableNames: string[]): Promise<void>;
|
|
305
306
|
/**
|
|
306
307
|
* `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
|
|
307
|
-
* (atomic
|
|
308
|
-
* key
|
|
308
|
+
* (atomic: rows a concurrent write made violate the new key fail it with
|
|
309
|
+
* the old key in place). Called on an empty table or one whose rows satisfy
|
|
310
|
+
* the new key; `ADD PRIMARY KEY` makes the new key columns NOT NULL. A demoted identity column is handled by the
|
|
309
311
|
* `defaultChanged` diff (`DROP IDENTITY`).
|
|
310
312
|
*/
|
|
311
313
|
rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
|
|
@@ -346,6 +348,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
346
348
|
* dependents.
|
|
347
349
|
*/
|
|
348
350
|
dropTable(): Promise<void>;
|
|
351
|
+
copyFromJsonColumn(source: string, targets: readonly TJsonCopyTarget[]): Promise<void>;
|
|
352
|
+
jsonifyTextColumn(column: string): Promise<void>;
|
|
349
353
|
dropColumns(columns: string[]): Promise<void>;
|
|
350
354
|
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
351
355
|
dropTableByName(tableName: string): Promise<void>;
|
|
@@ -442,6 +446,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
442
446
|
}
|
|
443
447
|
//#endregion
|
|
444
448
|
//#region src/pg-driver.d.ts
|
|
449
|
+
/** Options of {@link PgDriver}. */
|
|
450
|
+
interface TPgDriverOptions {
|
|
451
|
+
/**
|
|
452
|
+
* Receives a warning when a pool connection breaks — a terminated backend,
|
|
453
|
+
* failover or network reset (default `console`). See {@link PgDriver}.
|
|
454
|
+
*/
|
|
455
|
+
logger?: Pick<TGenericLogger, "warn">;
|
|
456
|
+
}
|
|
445
457
|
/**
|
|
446
458
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
447
459
|
*
|
|
@@ -468,6 +480,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
468
480
|
* const driver = new PgDriver(pool)
|
|
469
481
|
* ```
|
|
470
482
|
*
|
|
483
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
484
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
485
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
486
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
487
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
488
|
+
*
|
|
471
489
|
* Requires `pg` to be installed:
|
|
472
490
|
* ```bash
|
|
473
491
|
* pnpm add pg
|
|
@@ -476,7 +494,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
476
494
|
declare class PgDriver implements TPgDriver {
|
|
477
495
|
private pool;
|
|
478
496
|
private poolInit;
|
|
479
|
-
|
|
497
|
+
private readonly logger;
|
|
498
|
+
constructor(poolOrConfig: string | import("pg").Pool | import("pg").PoolConfig, options?: TPgDriverOptions);
|
|
480
499
|
private getPool;
|
|
481
500
|
run(sql: string, params?: unknown[]): Promise<TPgRunResult>;
|
|
482
501
|
all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
@@ -517,4 +536,4 @@ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): T
|
|
|
517
536
|
*/
|
|
518
537
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
519
538
|
//#endregion
|
|
520
|
-
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
539
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgDriverOptions, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
|
|
3
3
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
@@ -293,6 +293,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
293
293
|
/** `schema.name` when the adapter targets a named schema. */
|
|
294
294
|
private _qualify;
|
|
295
295
|
hasRows(tableName?: string): Promise<boolean>;
|
|
296
|
+
countKeyViolations(columns: readonly string[], tableName?: string): Promise<number>;
|
|
296
297
|
/**
|
|
297
298
|
* Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
|
|
298
299
|
* composite keys (`conkey`/`confkey` are positionally aligned, unlike the
|
|
@@ -304,8 +305,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
304
305
|
dropTablesByName(tableNames: string[]): Promise<void>;
|
|
305
306
|
/**
|
|
306
307
|
* `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
|
|
307
|
-
* (atomic
|
|
308
|
-
* key
|
|
308
|
+
* (atomic: rows a concurrent write made violate the new key fail it with
|
|
309
|
+
* the old key in place). Called on an empty table or one whose rows satisfy
|
|
310
|
+
* the new key; `ADD PRIMARY KEY` makes the new key columns NOT NULL. A demoted identity column is handled by the
|
|
309
311
|
* `defaultChanged` diff (`DROP IDENTITY`).
|
|
310
312
|
*/
|
|
311
313
|
rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
|
|
@@ -346,6 +348,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
346
348
|
* dependents.
|
|
347
349
|
*/
|
|
348
350
|
dropTable(): Promise<void>;
|
|
351
|
+
copyFromJsonColumn(source: string, targets: readonly TJsonCopyTarget[]): Promise<void>;
|
|
352
|
+
jsonifyTextColumn(column: string): Promise<void>;
|
|
349
353
|
dropColumns(columns: string[]): Promise<void>;
|
|
350
354
|
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
351
355
|
dropTableByName(tableName: string): Promise<void>;
|
|
@@ -442,6 +446,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
442
446
|
}
|
|
443
447
|
//#endregion
|
|
444
448
|
//#region src/pg-driver.d.ts
|
|
449
|
+
/** Options of {@link PgDriver}. */
|
|
450
|
+
interface TPgDriverOptions {
|
|
451
|
+
/**
|
|
452
|
+
* Receives a warning when a pool connection breaks — a terminated backend,
|
|
453
|
+
* failover or network reset (default `console`). See {@link PgDriver}.
|
|
454
|
+
*/
|
|
455
|
+
logger?: Pick<TGenericLogger, "warn">;
|
|
456
|
+
}
|
|
445
457
|
/**
|
|
446
458
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
447
459
|
*
|
|
@@ -468,6 +480,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
468
480
|
* const driver = new PgDriver(pool)
|
|
469
481
|
* ```
|
|
470
482
|
*
|
|
483
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
484
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
485
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
486
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
487
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
488
|
+
*
|
|
471
489
|
* Requires `pg` to be installed:
|
|
472
490
|
* ```bash
|
|
473
491
|
* pnpm add pg
|
|
@@ -476,7 +494,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
476
494
|
declare class PgDriver implements TPgDriver {
|
|
477
495
|
private pool;
|
|
478
496
|
private poolInit;
|
|
479
|
-
|
|
497
|
+
private readonly logger;
|
|
498
|
+
constructor(poolOrConfig: string | import("pg").Pool | import("pg").PoolConfig, options?: TPgDriverOptions);
|
|
480
499
|
private getPool;
|
|
481
500
|
run(sql: string, params?: unknown[]): Promise<TPgRunResult>;
|
|
482
501
|
all<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
@@ -517,4 +536,4 @@ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): T
|
|
|
517
536
|
*/
|
|
518
537
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
519
538
|
//#endregion
|
|
520
|
-
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
539
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgDriverOptions, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, defaultFulltextIndex, describeFulltext, fkColumns, isColumnTypeChanged, searchTermInteger, splitFulltextFields, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
2
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
|
-
import { SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, vectorDistanceSource } from "@atscript/db-sql-tools";
|
|
3
|
+
import { SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, vectorDistanceSource } from "@atscript/db-sql-tools";
|
|
4
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
5
5
|
//#region src/insert-ignore.ts
|
|
6
6
|
/**
|
|
@@ -137,6 +137,12 @@ function toPgValue(value) {
|
|
|
137
137
|
if (typeof value === "object") return JSON.stringify(value);
|
|
138
138
|
return value;
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Text a JSON column can take as it is (`SqlDialect.jsonFromText`): an
|
|
142
|
+
* object / array (by its first characters), or a whole JSON string, number,
|
|
143
|
+
* `true`, `false` or `null` — so `42 Main St` is no number.
|
|
144
|
+
*/
|
|
145
|
+
const PG_JSON_LIKE = String.raw`^\s*(\{\s*["}]|\[\s*(["{[\]tfn0-9-])|("([^"\\]|\\.)*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?|true|false|null)\s*$)`;
|
|
140
146
|
const pgDialect = {
|
|
141
147
|
quoteIdentifier(name) {
|
|
142
148
|
return qi(name);
|
|
@@ -170,6 +176,9 @@ const pgDialect = {
|
|
|
170
176
|
},
|
|
171
177
|
calendarBucket: pgCalendarBucket,
|
|
172
178
|
jsonExtract: pgJsonExtract,
|
|
179
|
+
jsonExtractText: (quotedCol, path) => `((${quotedCol})::jsonb #>> ${pgJsonPath(path)})`,
|
|
180
|
+
jsonExtractValue: (quotedCol, path) => `NULLIF((${quotedCol})::jsonb #> ${pgJsonPath(path)}, 'null'::jsonb)`,
|
|
181
|
+
jsonFromText: (quotedCol) => `CASE WHEN ${quotedCol} ~ '${PG_JSON_LIKE}' THEN ${quotedCol} ELSE to_jsonb(${quotedCol})::text END`,
|
|
173
182
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
|
|
174
183
|
anyValue: pgAnyValue,
|
|
175
184
|
booleanAggregates: {
|
|
@@ -203,6 +212,10 @@ const PG_JSON_LEAF_CAST = {
|
|
|
203
212
|
number: "::double precision",
|
|
204
213
|
boolean: "::boolean"
|
|
205
214
|
};
|
|
215
|
+
/** A `#>` / `#>>` path literal (`'{"a","b"}'`). */
|
|
216
|
+
function pgJsonPath(path) {
|
|
217
|
+
return sqlStringLiteral(`{${quotedJsonPathSegments(path).join(",")}}`);
|
|
218
|
+
}
|
|
206
219
|
/**
|
|
207
220
|
* Typed read of a JSON leaf (`SqlDialect.jsonExtract`): `jsonb_typeof()`
|
|
208
221
|
* guards the declared type, so a missing path, JSON `null` or another JSON
|
|
@@ -210,7 +223,7 @@ const PG_JSON_LEAF_CAST = {
|
|
|
210
223
|
* native. The column is cast `::jsonb`, so JSON and JSONB storage both work.
|
|
211
224
|
*/
|
|
212
225
|
function pgJsonExtract(quotedCol, path, type) {
|
|
213
|
-
const p =
|
|
226
|
+
const p = pgJsonPath(path);
|
|
214
227
|
const j = `(${quotedCol})::jsonb`;
|
|
215
228
|
const cast = PG_JSON_LEAF_CAST[type];
|
|
216
229
|
return `CASE jsonb_typeof(${j} #> ${p}) WHEN '${type}' THEN ${cast ? `(${j} #>> ${p})${cast}` : `${j} #>> ${p}`} END`;
|
|
@@ -512,6 +525,13 @@ const GEO_PROBE_SQL = `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname =
|
|
|
512
525
|
LEFT JOIN pg_type b ON b.oid = NULLIF(t.typbasetype, 0)
|
|
513
526
|
WHERE a.attrelid = to_regclass($1::text) AND a.attnum > 0 AND NOT a.attisdropped
|
|
514
527
|
AND a.attname = ANY($2::text[])) AS "columns"`;
|
|
528
|
+
const identity = (value) => value;
|
|
529
|
+
/** A column type node-postgres parses on read. */
|
|
530
|
+
const PG_JSON_TYPE = /^\s*jsonb?\s*$/i;
|
|
531
|
+
/** A JSON string value as JSON text — see `formatValue`. */
|
|
532
|
+
function reencodeJsonString(value) {
|
|
533
|
+
return typeof value === "string" ? JSON.stringify(value) : value;
|
|
534
|
+
}
|
|
515
535
|
/** A PostGIS column type — a geo value binds as EWKT. Anything else (JSONB, TEXT) takes the JSON form. */
|
|
516
536
|
const POSTGIS_TYPES = new Set(["geography", "geometry"]);
|
|
517
537
|
/** The SQL `$geoWithin` renders (`pgDialect.geoWithin`) — PostGIS only. */
|
|
@@ -867,6 +887,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
867
887
|
}
|
|
868
888
|
};
|
|
869
889
|
}
|
|
890
|
+
if (field.storage === "json" && !field.encrypted && PG_JSON_TYPE.test(this.typeMapper(field))) return {
|
|
891
|
+
toStorage: identity,
|
|
892
|
+
fromStorage: reencodeJsonString
|
|
893
|
+
};
|
|
870
894
|
}
|
|
871
895
|
/**
|
|
872
896
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -1304,6 +1328,12 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1304
1328
|
this._log(sql);
|
|
1305
1329
|
return (await this._exec().get(sql, []))?.present ?? false;
|
|
1306
1330
|
}
|
|
1331
|
+
async countKeyViolations(columns, tableName) {
|
|
1332
|
+
const sql = buildKeyViolationCount(pgDialect, tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName()), columns);
|
|
1333
|
+
this._log(sql);
|
|
1334
|
+
const row = await this._exec().get(sql, []);
|
|
1335
|
+
return Number(row?.violations ?? 0);
|
|
1336
|
+
}
|
|
1307
1337
|
/**
|
|
1308
1338
|
* Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
|
|
1309
1339
|
* composite keys (`conkey`/`confkey` are positionally aligned, unlike the
|
|
@@ -1349,8 +1379,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1349
1379
|
}
|
|
1350
1380
|
/**
|
|
1351
1381
|
* `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
|
|
1352
|
-
* (atomic
|
|
1353
|
-
* key
|
|
1382
|
+
* (atomic: rows a concurrent write made violate the new key fail it with
|
|
1383
|
+
* the old key in place). Called on an empty table or one whose rows satisfy
|
|
1384
|
+
* the new key; `ADD PRIMARY KEY` makes the new key columns NOT NULL. A demoted identity column is handled by the
|
|
1354
1385
|
* `defaultChanged` diff (`DROP IDENTITY`).
|
|
1355
1386
|
*/
|
|
1356
1387
|
async rebuildPrimaryKey(change) {
|
|
@@ -1444,10 +1475,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1444
1475
|
this._log(ddl);
|
|
1445
1476
|
await this._exec().exec(ddl);
|
|
1446
1477
|
}
|
|
1447
|
-
for (const { field } of diff.typeChanged ?? []) {
|
|
1478
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1448
1479
|
const sqlType = this.typeMapper(field);
|
|
1449
1480
|
const col = qi(field.physicalName);
|
|
1450
|
-
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${convertColumnExpr(col, field, sqlType)}`;
|
|
1481
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${convertColumnExpr(col, field, sqlType, existingType)}`;
|
|
1451
1482
|
this._log(ddl);
|
|
1452
1483
|
await this._exec().exec(ddl);
|
|
1453
1484
|
}
|
|
@@ -1555,8 +1586,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1555
1586
|
const selectExprs = commonFields.map((field) => {
|
|
1556
1587
|
const c = qi(field.physicalName);
|
|
1557
1588
|
const sqlType = this.typeMapper(field);
|
|
1558
|
-
const
|
|
1559
|
-
const
|
|
1589
|
+
const oldType = oldTypes.get(field.physicalName);
|
|
1590
|
+
const changed = isColumnTypeChanged(oldType, sqlType);
|
|
1591
|
+
const value = changed ? convertColumnExpr(c, field, sqlType, oldType) : c;
|
|
1560
1592
|
if (!field.optional && !field.isPrimaryKey) return `COALESCE(${value}, ${field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value) : defaultValueForType$1(field.designType)}) AS ${c}`;
|
|
1561
1593
|
return changed ? `${value} AS ${c}` : c;
|
|
1562
1594
|
}).join(", ");
|
|
@@ -1673,6 +1705,20 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1673
1705
|
this._log(ddl);
|
|
1674
1706
|
await this._exec().exec(ddl);
|
|
1675
1707
|
}
|
|
1708
|
+
async copyFromJsonColumn(source, targets) {
|
|
1709
|
+
const sql = buildJsonColumnCopy(pgDialect, this.resolveTableName(), source, targets, (expr, target) => {
|
|
1710
|
+
if (target.kind !== "text") return expr;
|
|
1711
|
+
const type = this.typeMapper(target.field);
|
|
1712
|
+
return /^(VARCHAR|CHAR)\b/i.test(type) ? expr : `(${expr})::${type}`;
|
|
1713
|
+
});
|
|
1714
|
+
this._log(sql);
|
|
1715
|
+
await this._exec().exec(sql);
|
|
1716
|
+
}
|
|
1717
|
+
async jsonifyTextColumn(column) {
|
|
1718
|
+
const sql = buildJsonifyText(pgDialect, this.resolveTableName(), column);
|
|
1719
|
+
this._log(sql);
|
|
1720
|
+
await this._exec().exec(sql);
|
|
1721
|
+
}
|
|
1676
1722
|
async dropColumns(columns) {
|
|
1677
1723
|
const tableName = this.resolveTableName();
|
|
1678
1724
|
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
@@ -2189,6 +2235,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
2189
2235
|
};
|
|
2190
2236
|
/** A text / character type: the value is converted by the assignment into the column. */
|
|
2191
2237
|
const CHARACTER_TYPE = /^\s*(?:text|varchar|character varying|char|character|bpchar)\b/i;
|
|
2238
|
+
/** A floating-point / decimal column type (live or mapped). */
|
|
2239
|
+
const FRACTIONAL_TYPE = /^\s*(?:double precision|float[48]?|real|numeric|decimal)\b/i;
|
|
2192
2240
|
/**
|
|
2193
2241
|
* The expression converting column `col` (quoted) to `sqlType`, the field's
|
|
2194
2242
|
* new type — one rule for `ALTER COLUMN … TYPE … USING` and the recreate copy.
|
|
@@ -2196,11 +2244,19 @@ const CHARACTER_TYPE = /^\s*(?:text|varchar|character varying|char|character|bpc
|
|
|
2196
2244
|
* fails the statement instead of being coerced. A TEXT / VARCHAR(n) / CHAR(n)
|
|
2197
2245
|
* target stops at `::text`: an explicit cast to VARCHAR(n) / CHAR(n) would
|
|
2198
2246
|
* truncate an over-long value, the assignment into the column rejects it
|
|
2199
|
-
* (since 0.1.137).
|
|
2247
|
+
* (since 0.1.137). An epoch-ms fractional column (`existingType`) becoming
|
|
2248
|
+
* the `BIGINT` of `@db.default.now` (`number.timestamp` →
|
|
2249
|
+
* `number.timestamp.created`) is rounded (since 0.1.155): its text
|
|
2250
|
+
* (`1700000000123.5`) does not parse as an integer. Any other fractional
|
|
2251
|
+
* value still fails a change to an integer type.
|
|
2200
2252
|
*/
|
|
2201
|
-
function convertColumnExpr(col, field, sqlType) {
|
|
2202
|
-
if (field.isGeoPoint && sqlType.startsWith("geography"))
|
|
2253
|
+
function convertColumnExpr(col, field, sqlType, existingType) {
|
|
2254
|
+
if (field.isGeoPoint && sqlType.startsWith("geography")) {
|
|
2255
|
+
const json = `${col}::jsonb`;
|
|
2256
|
+
return `CASE WHEN ${col} IS NULL THEN NULL ELSE ST_SetSRID(ST_MakePoint((${json}->>0)::float8, (${json}->>1)::float8), 4326)::geography END`;
|
|
2257
|
+
}
|
|
2203
2258
|
if (CHARACTER_TYPE.test(sqlType)) return `${col}::text`;
|
|
2259
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now" && /^BIGINT$/i.test(sqlType.trim()) && existingType && FRACTIONAL_TYPE.test(existingType)) return `round(${col})::bigint`;
|
|
2204
2260
|
return `${col}::text::${sqlType}`;
|
|
2205
2261
|
}
|
|
2206
2262
|
/**
|
|
@@ -2360,6 +2416,10 @@ function createCustomTypes(pgTypes) {
|
|
|
2360
2416
|
return pgTypes.getTypeParser(oid, format);
|
|
2361
2417
|
} };
|
|
2362
2418
|
}
|
|
2419
|
+
/** The warning logged for a pool client whose connection broke (`57P01`, `ECONNRESET`, …). */
|
|
2420
|
+
function lostConnectionMessage(err, where) {
|
|
2421
|
+
return `[atscript/db-postgres] ${where} pool connection lost (${err.code ?? err.name}): ${err.message}`;
|
|
2422
|
+
}
|
|
2363
2423
|
/**
|
|
2364
2424
|
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
2365
2425
|
*
|
|
@@ -2386,6 +2446,12 @@ function createCustomTypes(pgTypes) {
|
|
|
2386
2446
|
* const driver = new PgDriver(pool)
|
|
2387
2447
|
* ```
|
|
2388
2448
|
*
|
|
2449
|
+
* A broken pool connection (a terminated backend, failover, network reset) is
|
|
2450
|
+
* logged through `options.logger` instead of crashing the process (since
|
|
2451
|
+
* 0.1.154): idle clients of a pool the driver creates, and clients checked out
|
|
2452
|
+
* for a transaction. A pre-created `pg.Pool` is the caller's — attach your own
|
|
2453
|
+
* `pool.on('error', …)` listener, or an idle client's error crashes the process.
|
|
2454
|
+
*
|
|
2389
2455
|
* Requires `pg` to be installed:
|
|
2390
2456
|
* ```bash
|
|
2391
2457
|
* pnpm add pg
|
|
@@ -2394,7 +2460,9 @@ function createCustomTypes(pgTypes) {
|
|
|
2394
2460
|
var PgDriver = class {
|
|
2395
2461
|
pool;
|
|
2396
2462
|
poolInit;
|
|
2397
|
-
|
|
2463
|
+
logger;
|
|
2464
|
+
constructor(poolOrConfig, options = {}) {
|
|
2465
|
+
this.logger = options.logger ?? console;
|
|
2398
2466
|
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
2399
2467
|
else this.poolInit = import("pg").then((pg) => {
|
|
2400
2468
|
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
@@ -2408,6 +2476,7 @@ var PgDriver = class {
|
|
|
2408
2476
|
...poolOrConfig,
|
|
2409
2477
|
types: customTypes
|
|
2410
2478
|
});
|
|
2479
|
+
this.pool.on("error", (err) => this.logger.warn(lostConnectionMessage(err, "idle")));
|
|
2411
2480
|
return this.pool;
|
|
2412
2481
|
});
|
|
2413
2482
|
}
|
|
@@ -2432,6 +2501,13 @@ var PgDriver = class {
|
|
|
2432
2501
|
}
|
|
2433
2502
|
async getConnection() {
|
|
2434
2503
|
const client = await (await this.getPool()).connect();
|
|
2504
|
+
let lost;
|
|
2505
|
+
const onError = (err) => {
|
|
2506
|
+
if (lost) return;
|
|
2507
|
+
lost = err;
|
|
2508
|
+
this.logger.warn(lostConnectionMessage(err, "checked-out"));
|
|
2509
|
+
};
|
|
2510
|
+
client.on("error", onError);
|
|
2435
2511
|
return {
|
|
2436
2512
|
async run(sql, params) {
|
|
2437
2513
|
const result = await client.query(sql, sanitizeParams(params));
|
|
@@ -2450,7 +2526,8 @@ var PgDriver = class {
|
|
|
2450
2526
|
await client.query(sql);
|
|
2451
2527
|
},
|
|
2452
2528
|
release() {
|
|
2453
|
-
client.
|
|
2529
|
+
client.removeListener("error", onError);
|
|
2530
|
+
client.release(lost);
|
|
2454
2531
|
}
|
|
2455
2532
|
};
|
|
2456
2533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.155",
|
|
4
4
|
"description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -43,20 +43,20 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@atscript/core": "^0.1.
|
|
47
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
+
"@atscript/core": "^0.1.105",
|
|
47
|
+
"@atscript/typescript": "^0.1.105",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
49
|
"@uniqu/core": "^0.1.15",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.105"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
54
|
+
"@atscript/core": "^0.1.105",
|
|
55
|
+
"@atscript/typescript": "^0.1.105",
|
|
56
56
|
"@uniqu/core": "^0.1.15",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.155",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.155"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|