@atscript/db-postgres 0.1.154 → 0.1.156
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 +66 -10
- package/dist/index.d.cts +7 -3
- package/dist/index.d.mts +7 -3
- package/dist/index.mjs +67 -11
- 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
|
/**
|
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, TGenericLogger, 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>;
|
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, TGenericLogger, 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>;
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.156",
|
|
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.106",
|
|
47
|
+
"@atscript/typescript": "^0.1.106",
|
|
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.106"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
54
|
+
"@atscript/core": "^0.1.106",
|
|
55
|
+
"@atscript/typescript": "^0.1.106",
|
|
56
56
|
"@uniqu/core": "^0.1.15",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.156",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.156"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|