turbine-orm 0.79.1 → 0.80.0
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/README.md +4 -4
- package/dist/checkout.d.ts +53 -0
- package/dist/checkout.js +78 -0
- package/dist/cjs/checkout.d.ts +53 -0
- package/dist/cjs/checkout.js +82 -0
- package/dist/cjs/cli/index.js +4 -0
- package/dist/cjs/cli/mcp.js +4 -0
- package/dist/cjs/cli/migrate.js +6 -0
- package/dist/cjs/cli/observe.js +8 -0
- package/dist/cjs/cli/studio.js +13 -1
- package/dist/cjs/client.d.ts +12 -2
- package/dist/cjs/client.js +61 -75
- package/dist/cjs/connection-guard.d.ts +120 -0
- package/dist/cjs/connection-guard.js +191 -0
- package/dist/cjs/errors.d.ts +26 -0
- package/dist/cjs/errors.js +85 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/nested-write.d.ts +12 -2
- package/dist/cjs/nested-write.js +4 -10
- package/dist/cjs/pipeline.js +12 -7
- package/dist/cjs/plan-flip-probe.js +4 -0
- package/dist/cjs/powdb-shared.d.ts +22 -2
- package/dist/cjs/powdb-shared.js +27 -2
- package/dist/cjs/powdb.js +36 -37
- package/dist/cjs/powql.d.ts +51 -6
- package/dist/cjs/powql.js +199 -45
- package/dist/cjs/prisma-compat.js +28 -4
- package/dist/cjs/query/builder.d.ts +44 -24
- package/dist/cjs/query/builder.js +125 -66
- package/dist/cjs/query/deferred.d.ts +9 -0
- package/dist/cjs/query/option-surface.js +12 -0
- package/dist/cjs/query/types.d.ts +68 -4
- package/dist/cjs/query/writes.d.ts +39 -9
- package/dist/cjs/query/writes.js +72 -34
- package/dist/cjs/realtime.d.ts +46 -2
- package/dist/cjs/realtime.js +125 -20
- package/dist/cjs/schema-sql.js +6 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/mcp.js +4 -0
- package/dist/cli/migrate.js +6 -0
- package/dist/cli/observe.js +8 -0
- package/dist/cli/studio.js +13 -1
- package/dist/client.d.ts +12 -2
- package/dist/client.js +62 -76
- package/dist/connection-guard.d.ts +120 -0
- package/dist/connection-guard.js +183 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +83 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/nested-write.d.ts +12 -2
- package/dist/nested-write.js +4 -10
- package/dist/pipeline.js +13 -8
- package/dist/plan-flip-probe.js +4 -0
- package/dist/powdb-shared.d.ts +22 -2
- package/dist/powdb-shared.js +25 -2
- package/dist/powdb.js +23 -24
- package/dist/powql.d.ts +51 -6
- package/dist/powql.js +200 -46
- package/dist/prisma-compat.js +28 -4
- package/dist/query/builder.d.ts +44 -24
- package/dist/query/builder.js +126 -67
- package/dist/query/deferred.d.ts +9 -0
- package/dist/query/option-surface.js +12 -0
- package/dist/query/types.d.ts +68 -4
- package/dist/query/writes.d.ts +39 -9
- package/dist/query/writes.js +71 -34
- package/dist/realtime.d.ts +46 -2
- package/dist/realtime.js +125 -20
- package/dist/schema-sql.js +6 -0
- package/package.json +5 -3
package/dist/cjs/errors.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.getErrorMessageMode = getErrorMessageMode;
|
|
|
9
9
|
exports.markValueBearingMessage = markValueBearingMessage;
|
|
10
10
|
exports.describeTargetForMessage = describeTargetForMessage;
|
|
11
11
|
exports.malformedConnectionStringMessage = malformedConnectionStringMessage;
|
|
12
|
+
exports.explainConnectionLoss = explainConnectionLoss;
|
|
13
|
+
exports.isStaleConnectionError = isStaleConnectionError;
|
|
12
14
|
exports.wrapPgError = wrapPgError;
|
|
13
15
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
14
16
|
/**
|
|
@@ -1231,6 +1233,84 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
1231
1233
|
'CERT_HAS_EXPIRED',
|
|
1232
1234
|
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
1233
1235
|
]);
|
|
1236
|
+
/**
|
|
1237
|
+
* The driver's own words for "this connection is gone", which carry NO code.
|
|
1238
|
+
*
|
|
1239
|
+
* node-postgres raises these as plain `Error`s with no SQLSTATE and no socket
|
|
1240
|
+
* code: the first when the socket closes under a live client, the next two for
|
|
1241
|
+
* any query sent to a client after that, the last two when a connect or an
|
|
1242
|
+
* orderly `end()` cuts a query off. Keyed on `.code` alone, wrapPgError handed
|
|
1243
|
+
* every one of them back untyped, so the commonest symptom of a database
|
|
1244
|
+
* restart was the one connection failure a `catch (e) { if (e instanceof
|
|
1245
|
+
* ConnectionError) ... }` did not see. Exact matches on the full message, and
|
|
1246
|
+
* only on code-less errors: these strings are pg's and have not changed across
|
|
1247
|
+
* the 8.x line, and nothing else produces them.
|
|
1248
|
+
*/
|
|
1249
|
+
const PG_CONNECTION_LOSS_MESSAGES = new Set([
|
|
1250
|
+
'Connection terminated unexpectedly',
|
|
1251
|
+
'Client has encountered a connection error and is not queryable',
|
|
1252
|
+
'Client was closed and is not queryable',
|
|
1253
|
+
'Connection terminated',
|
|
1254
|
+
'Connection terminated due to connection timeout',
|
|
1255
|
+
]);
|
|
1256
|
+
/** The follow-on errors pg raises for a query sent to a client already known to be dead. */
|
|
1257
|
+
const PG_NOT_QUERYABLE_MESSAGES = new Set([
|
|
1258
|
+
'Client has encountered a connection error and is not queryable',
|
|
1259
|
+
'Client was closed and is not queryable',
|
|
1260
|
+
]);
|
|
1261
|
+
/**
|
|
1262
|
+
* Report the error that KILLED a held connection instead of its follow-on.
|
|
1263
|
+
*
|
|
1264
|
+
* When a connection dies while a transaction callback is between queries, the
|
|
1265
|
+
* next query fails with "Client has encountered a connection error and is not
|
|
1266
|
+
* queryable", which says nothing about why. The connection guard recorded the
|
|
1267
|
+
* real cause (`lostWith`, e.g. 57P01 `terminating connection due to
|
|
1268
|
+
* administrator command`), so a not-queryable ConnectionError is swapped for
|
|
1269
|
+
* that one. Anything else passes through: an error the caller threw, or a
|
|
1270
|
+
* query that was IN FLIGHT when the connection died, which already carries the
|
|
1271
|
+
* server's own message.
|
|
1272
|
+
*/
|
|
1273
|
+
function explainConnectionLoss(err, lostWith) {
|
|
1274
|
+
if (!lostWith || !(err instanceof ConnectionError))
|
|
1275
|
+
return err;
|
|
1276
|
+
const cause = err.cause;
|
|
1277
|
+
if (!(cause instanceof Error) || !PG_NOT_QUERYABLE_MESSAGES.has(cause.message))
|
|
1278
|
+
return err;
|
|
1279
|
+
const original = wrapPgError(lostWith);
|
|
1280
|
+
return original instanceof ConnectionError ? original : err;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Codes that mean an ESTABLISHED connection went away: the server ended the
|
|
1284
|
+
* backend (57P01, which is also what a dead idle connection hands the next
|
|
1285
|
+
* query, since pg attributes the FATAL it buffered to whatever is sent next),
|
|
1286
|
+
* or the peer reset the socket.
|
|
1287
|
+
*/
|
|
1288
|
+
const STALE_CONNECTION_CODES = new Set(['57P01', 'ECONNRESET', 'EPIPE']);
|
|
1289
|
+
/** pg's code-less messages for the same thing. */
|
|
1290
|
+
const STALE_CONNECTION_MESSAGES = new Set(['Connection terminated unexpectedly', ...PG_NOT_QUERYABLE_MESSAGES]);
|
|
1291
|
+
/**
|
|
1292
|
+
* Whether `err` says a connection that was already open is gone, so the same
|
|
1293
|
+
* statement on a fresh connection can succeed. Accepts the raw driver error or
|
|
1294
|
+
* the {@link ConnectionError} wrapPgError made of it.
|
|
1295
|
+
*
|
|
1296
|
+
* Narrower than "is a ConnectionError" on purpose. A connection that could not
|
|
1297
|
+
* be OPENED (refused, DNS, auth, a connect timeout, 57P03 while the server
|
|
1298
|
+
* starts) will not open on an immediate second try either, and retrying it
|
|
1299
|
+
* only doubles the wait before the caller hears about it. Nor does it cover
|
|
1300
|
+
* `Connection terminated` without "unexpectedly", which is the pool itself
|
|
1301
|
+
* shutting down.
|
|
1302
|
+
*/
|
|
1303
|
+
function isStaleConnectionError(err) {
|
|
1304
|
+
const raw = err instanceof ConnectionError && err.cause instanceof Error ? err.cause : err;
|
|
1305
|
+
if (!raw || typeof raw !== 'object')
|
|
1306
|
+
return false;
|
|
1307
|
+
const code = raw.code;
|
|
1308
|
+
if (code)
|
|
1309
|
+
return STALE_CONNECTION_CODES.has(code);
|
|
1310
|
+
return raw instanceof Error && STALE_CONNECTION_MESSAGES.has(raw.message);
|
|
1311
|
+
}
|
|
1312
|
+
const CONNECTION_LOSS_HINT = 'The server or the network closed it (a restart, failover, idle timeout, compute suspend, or ' +
|
|
1313
|
+
'pg_terminate_backend). The pool discards the connection, so retrying the operation opens a fresh one.';
|
|
1234
1314
|
/**
|
|
1235
1315
|
* Actionable next step per connection-class code, appended to the driver's own
|
|
1236
1316
|
* message. The driver message states WHAT happened ("password authentication
|
|
@@ -1503,8 +1583,12 @@ function wrapPgError(err) {
|
|
|
1503
1583
|
if (!err || typeof err !== 'object')
|
|
1504
1584
|
return err;
|
|
1505
1585
|
const e = err;
|
|
1506
|
-
if (!e.code)
|
|
1586
|
+
if (!e.code) {
|
|
1587
|
+
if (err instanceof Error && PG_CONNECTION_LOSS_MESSAGES.has(err.message)) {
|
|
1588
|
+
return new ConnectionError(`Database connection lost: ${err.message}. ${CONNECTION_LOSS_HINT}`, { cause: err });
|
|
1589
|
+
}
|
|
1507
1590
|
return err;
|
|
1591
|
+
}
|
|
1508
1592
|
switch (e.code) {
|
|
1509
1593
|
case '23505': {
|
|
1510
1594
|
const cols = e.detail ? parseColumnsFromDetail(e.detail) : undefined;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSu
|
|
|
46
46
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
47
47
|
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderBySpec, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationOrderBy, type RelationOrderByChain, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
48
48
|
export { MAX_QUERY_TAG_LENGTH, RAW_QUERY_MODEL } from './query-events.js';
|
|
49
|
-
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
49
|
+
export { type ActiveSubscription, type ListenOptions, type ListenReconnectOptions, type NotificationHandler, type Subscription, validateChannel, } from './realtime.js';
|
|
50
50
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
51
51
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
|
52
52
|
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
|
|
@@ -137,10 +137,20 @@ export declare function createManyShapeRuns<T extends Record<string, unknown>>(r
|
|
|
137
137
|
* operation (create, connect, connectOrCreate), and finally reads back the
|
|
138
138
|
* full tree using `findUnique` with an auto-built `with` clause.
|
|
139
139
|
*/
|
|
140
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The caller's `select` / `omit` for the TOP-LEVEL row of a nested write. Only
|
|
142
|
+
* the final read-back applies it (see executeNestedCreate), so it narrows the
|
|
143
|
+
* returned scalars exactly as it does on a read, while every intermediate
|
|
144
|
+
* statement keeps the full row it needs for keys.
|
|
145
|
+
*/
|
|
146
|
+
export interface NestedReturnShape {
|
|
147
|
+
select?: Record<string, boolean>;
|
|
148
|
+
omit?: Record<string, boolean>;
|
|
149
|
+
}
|
|
150
|
+
export declare function executeNestedCreate(ctx: NestedWriteContext, tableName: string, data: Record<string, unknown>, depth?: number, path?: string[], returnShape?: NestedReturnShape): Promise<Record<string, unknown>>;
|
|
141
151
|
/**
|
|
142
152
|
* Tree-walking update: updates the parent row with scalar data, then
|
|
143
153
|
* processes each relation operation (create, connect, connectOrCreate,
|
|
144
154
|
* disconnect, set, delete), and reads back the full tree.
|
|
145
155
|
*/
|
|
146
|
-
export declare function executeNestedUpdate(ctx: NestedWriteContext, tableName: string, where: Record<string, unknown>, data: Record<string, unknown>, depth?: number, path?: string[]): Promise<Record<string, unknown>>;
|
|
156
|
+
export declare function executeNestedUpdate(ctx: NestedWriteContext, tableName: string, where: Record<string, unknown>, data: Record<string, unknown>, depth?: number, path?: string[], returnShape?: NestedReturnShape): Promise<Record<string, unknown>>;
|
package/dist/cjs/nested-write.js
CHANGED
|
@@ -836,15 +836,7 @@ function rethrowAsNotRelated(err, op, relName, rel, target) {
|
|
|
836
836
|
}
|
|
837
837
|
throw err;
|
|
838
838
|
}
|
|
839
|
-
|
|
840
|
-
// executeNestedCreate
|
|
841
|
-
// ---------------------------------------------------------------------------
|
|
842
|
-
/**
|
|
843
|
-
* Tree-walking create: inserts the parent row, then processes each relation
|
|
844
|
-
* operation (create, connect, connectOrCreate), and finally reads back the
|
|
845
|
-
* full tree using `findUnique` with an auto-built `with` clause.
|
|
846
|
-
*/
|
|
847
|
-
async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
|
|
839
|
+
async function executeNestedCreate(ctx, tableName, data, depth = 0, path = [], returnShape) {
|
|
848
840
|
if (depth > MAX_DEPTH) {
|
|
849
841
|
throw new errors_js_1.CircularRelationError(path);
|
|
850
842
|
}
|
|
@@ -910,6 +902,7 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
|
|
|
910
902
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
911
903
|
where: pkWhere(tableMeta, parentRow),
|
|
912
904
|
with: withClause,
|
|
905
|
+
...returnShape,
|
|
913
906
|
});
|
|
914
907
|
return (fullRow ?? parentRow);
|
|
915
908
|
}
|
|
@@ -921,7 +914,7 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
|
|
|
921
914
|
* processes each relation operation (create, connect, connectOrCreate,
|
|
922
915
|
* disconnect, set, delete), and reads back the full tree.
|
|
923
916
|
*/
|
|
924
|
-
async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path = []) {
|
|
917
|
+
async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path = [], returnShape) {
|
|
925
918
|
if (depth > MAX_DEPTH) {
|
|
926
919
|
throw new errors_js_1.CircularRelationError(path);
|
|
927
920
|
}
|
|
@@ -1016,6 +1009,7 @@ async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path
|
|
|
1016
1009
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
1017
1010
|
where: pkWhere(tableMeta, parentRow),
|
|
1018
1011
|
with: readBackWith(ctx.schema, tableName, relations),
|
|
1012
|
+
...returnShape,
|
|
1019
1013
|
});
|
|
1020
1014
|
return (fullRow ?? parentRow);
|
|
1021
1015
|
}
|
package/dist/cjs/pipeline.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.executePipeline = executePipeline;
|
|
24
24
|
exports.pipelineSupported = pipelineSupported;
|
|
25
|
+
const connection_guard_js_1 = require("./connection-guard.js");
|
|
25
26
|
const dialect_js_1 = require("./dialect.js");
|
|
26
27
|
const errors_js_1 = require("./errors.js");
|
|
27
28
|
const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
|
|
@@ -206,6 +207,9 @@ async function executePipeline(pool, queries, options) {
|
|
|
206
207
|
catch (err) {
|
|
207
208
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
208
209
|
}
|
|
210
|
+
// Guarded for the checkout window, see connection-guard.ts: a connection that
|
|
211
|
+
// dies mid-batch emits 'error', which with no listener exits the process.
|
|
212
|
+
const checkout = (0, connection_guard_js_1.guardCheckout)(client);
|
|
209
213
|
try {
|
|
210
214
|
if ((0, pipeline_submittable_js_1.supportsExtendedPipeline)(client)) {
|
|
211
215
|
// Real pipeline path, uses extended-query protocol wire methods
|
|
@@ -227,8 +231,8 @@ async function executePipeline(pool, queries, options) {
|
|
|
227
231
|
// driver error that must not escape with a SQLSTATE sitting in the same
|
|
228
232
|
// `.code` slot Turbine puts TURBINE_E0NN in.
|
|
229
233
|
if (err instanceof errors_js_1.TurbineError)
|
|
230
|
-
throw err;
|
|
231
|
-
throw (0, errors_js_1.wrapPgError)(err);
|
|
234
|
+
throw (0, errors_js_1.explainConnectionLoss)(err, checkout.lostWith);
|
|
235
|
+
throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), checkout.lostWith);
|
|
232
236
|
}
|
|
233
237
|
finally {
|
|
234
238
|
// A client the pipeline could not return to a clean state is released WITH
|
|
@@ -239,10 +243,10 @@ async function executePipeline(pool, queries, options) {
|
|
|
239
243
|
// normally is what turned one failed batch into `25P02` on somebody else's
|
|
240
244
|
// query.
|
|
241
245
|
if ((0, pipeline_submittable_js_1.pipelineClientNeedsDiscard)(client)) {
|
|
242
|
-
|
|
246
|
+
checkout.release(new Error('turbine: pipeline connection left an open transaction and was discarded'));
|
|
243
247
|
}
|
|
244
248
|
else {
|
|
245
|
-
|
|
249
|
+
checkout.release();
|
|
246
250
|
}
|
|
247
251
|
}
|
|
248
252
|
}
|
|
@@ -254,15 +258,16 @@ async function executePipeline(pool, queries, options) {
|
|
|
254
258
|
* Note: This acquires and immediately releases a connection to inspect it.
|
|
255
259
|
*/
|
|
256
260
|
async function pipelineSupported(pool) {
|
|
257
|
-
let
|
|
261
|
+
let checkout;
|
|
258
262
|
try {
|
|
259
|
-
client = await pool.connect();
|
|
263
|
+
const client = await pool.connect();
|
|
264
|
+
checkout = (0, connection_guard_js_1.guardCheckout)(client);
|
|
260
265
|
return (0, pipeline_submittable_js_1.supportsExtendedPipeline)(client);
|
|
261
266
|
}
|
|
262
267
|
catch {
|
|
263
268
|
return false;
|
|
264
269
|
}
|
|
265
270
|
finally {
|
|
266
|
-
|
|
271
|
+
checkout?.release();
|
|
267
272
|
}
|
|
268
273
|
}
|
|
@@ -118,6 +118,7 @@ exports.buildFlipProbeSql = buildFlipProbeSql;
|
|
|
118
118
|
exports.verdictFromPlanJson = verdictFromPlanJson;
|
|
119
119
|
exports.probePlanFlips = probePlanFlips;
|
|
120
120
|
exports.applyFlipVerdicts = applyFlipVerdicts;
|
|
121
|
+
const connection_guard_js_1 = require("./connection-guard.js");
|
|
121
122
|
const utils_js_1 = require("./query/utils.js");
|
|
122
123
|
/** An empty result, which keeps every finding. Used when probing is off. */
|
|
123
124
|
function emptyFlipProbeResult() {
|
|
@@ -262,6 +263,9 @@ async function probePlanFlips(options) {
|
|
|
262
263
|
}
|
|
263
264
|
const { Client } = (await Promise.resolve().then(() => __importStar(require('pg')))).default;
|
|
264
265
|
const client = new Client({ connectionString: options.connectionString });
|
|
266
|
+
// A dropped connection must fail the probe (which then reports 'unknown'),
|
|
267
|
+
// not exit the process through an unheard 'error' event.
|
|
268
|
+
(0, connection_guard_js_1.guardConnection)(client);
|
|
265
269
|
try {
|
|
266
270
|
await client.connect();
|
|
267
271
|
// READ ONLY is belt-and-braces: EXPLAIN without ANALYZE cannot write, and the
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Everything here is re-exported by powdb.ts under its original name, so the
|
|
21
21
|
* public `turbine-orm/powdb` surface is byte-identical to before the split.
|
|
22
|
-
* The few helpers powdb.ts consumes but never published (`isDateColumn
|
|
23
|
-
* exported from this module and NOT
|
|
22
|
+
* The few helpers powdb.ts consumes but never published (`isDateColumn`,
|
|
23
|
+
* `parsePowdbSemver`, `atLeastVersion`) are exported from this module and NOT
|
|
24
|
+
* re-exported from powdb.ts.
|
|
24
25
|
*
|
|
25
26
|
* @module
|
|
26
27
|
*/
|
|
@@ -140,6 +141,25 @@ export interface PowdbCapabilities {
|
|
|
140
141
|
}
|
|
141
142
|
/** The feature-gate capability keys (everything except the version/nativeRaw metadata). */
|
|
142
143
|
type PowdbFeatureKey = 'jsonDocs' | 'docFieldIndexes' | 'introspection' | 'serverJoins' | 'nestedProjections' | 'entityLinks' | 'linkIntrospection' | 'linkPaths' | 'datetimeCompare' | 'projectedCountNonNull';
|
|
144
|
+
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
145
|
+
export declare function parsePowdbSemver(version: string | undefined | null): {
|
|
146
|
+
major: number;
|
|
147
|
+
minor: number;
|
|
148
|
+
patch: number;
|
|
149
|
+
} | null;
|
|
150
|
+
/**
|
|
151
|
+
* Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
|
|
152
|
+
* so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
|
|
153
|
+
* old major/minor comparison did (matches every patch of 0.19), while a
|
|
154
|
+
* three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
|
|
155
|
+
* the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
|
|
156
|
+
* existing two-argument call keeps its prior semantics unchanged.
|
|
157
|
+
*/
|
|
158
|
+
export declare function atLeastVersion(sem: {
|
|
159
|
+
major: number;
|
|
160
|
+
minor: number;
|
|
161
|
+
patch: number;
|
|
162
|
+
}, major: number, minor: number, patch?: number): boolean;
|
|
143
163
|
/**
|
|
144
164
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
145
165
|
* for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
|
package/dist/cjs/powdb-shared.js
CHANGED
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Everything here is re-exported by powdb.ts under its original name, so the
|
|
22
22
|
* public `turbine-orm/powdb` surface is byte-identical to before the split.
|
|
23
|
-
* The few helpers powdb.ts consumes but never published (`isDateColumn
|
|
24
|
-
* exported from this module and NOT
|
|
23
|
+
* The few helpers powdb.ts consumes but never published (`isDateColumn`,
|
|
24
|
+
* `parsePowdbSemver`, `atLeastVersion`) are exported from this module and NOT
|
|
25
|
+
* re-exported from powdb.ts.
|
|
25
26
|
*
|
|
26
27
|
* @module
|
|
27
28
|
*/
|
|
28
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
30
|
exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.PowdbJsonParam = exports.PowdbFloatParam = void 0;
|
|
31
|
+
exports.parsePowdbSemver = parsePowdbSemver;
|
|
32
|
+
exports.atLeastVersion = atLeastVersion;
|
|
30
33
|
exports.requireCapability = requireCapability;
|
|
31
34
|
exports.baseTsType = baseTsType;
|
|
32
35
|
exports.isJsonColumn = isJsonColumn;
|
|
@@ -77,6 +80,28 @@ class PowdbJsonParam {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
exports.PowdbJsonParam = PowdbJsonParam;
|
|
83
|
+
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
84
|
+
function parsePowdbSemver(version) {
|
|
85
|
+
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
|
|
86
|
+
if (!m)
|
|
87
|
+
return null;
|
|
88
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
|
|
92
|
+
* so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
|
|
93
|
+
* old major/minor comparison did (matches every patch of 0.19), while a
|
|
94
|
+
* three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
|
|
95
|
+
* the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
|
|
96
|
+
* existing two-argument call keeps its prior semantics unchanged.
|
|
97
|
+
*/
|
|
98
|
+
function atLeastVersion(sem, major, minor, patch = 0) {
|
|
99
|
+
if (sem.major !== major)
|
|
100
|
+
return sem.major > major;
|
|
101
|
+
if (sem.minor !== minor)
|
|
102
|
+
return sem.minor > minor;
|
|
103
|
+
return sem.patch >= patch;
|
|
104
|
+
}
|
|
80
105
|
/**
|
|
81
106
|
* Minimum engine version each gated feature needs, for the E017 hint text.
|
|
82
107
|
* Most gates carry a `major.minor` floor (patch-insensitive); the two link
|
package/dist/cjs/powdb.js
CHANGED
|
@@ -270,28 +270,6 @@ function assertSupportedPowdbVersion(version) {
|
|
|
270
270
|
* powql.ts.
|
|
271
271
|
*/
|
|
272
272
|
exports.POWQL_MAX_NESTING_DEPTH = 64;
|
|
273
|
-
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
274
|
-
function parsePowdbSemver(version) {
|
|
275
|
-
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
|
|
276
|
-
if (!m)
|
|
277
|
-
return null;
|
|
278
|
-
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
|
|
282
|
-
* so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
|
|
283
|
-
* old major/minor comparison did (matches every patch of 0.19), while a
|
|
284
|
-
* three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
|
|
285
|
-
* the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
|
|
286
|
-
* existing two-argument call keeps its prior semantics unchanged.
|
|
287
|
-
*/
|
|
288
|
-
function atLeastVersion(sem, major, minor, patch = 0) {
|
|
289
|
-
if (sem.major !== major)
|
|
290
|
-
return sem.major > major;
|
|
291
|
-
if (sem.minor !== minor)
|
|
292
|
-
return sem.minor > minor;
|
|
293
|
-
return sem.patch >= patch;
|
|
294
|
-
}
|
|
295
273
|
/**
|
|
296
274
|
* Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
|
|
297
275
|
* unknown version turns every gate OFF (the E017 hint then tells the caller to
|
|
@@ -299,7 +277,7 @@ function atLeastVersion(sem, major, minor, patch = 0) {
|
|
|
299
277
|
* to expose `queryNativeRaw` (passed in) AND server ≥ 0.13.
|
|
300
278
|
*/
|
|
301
279
|
function capabilitiesFromVersion(version, opts = {}) {
|
|
302
|
-
const sem = parsePowdbSemver(version);
|
|
280
|
+
const sem = (0, powdb_shared_js_1.parsePowdbSemver)(version);
|
|
303
281
|
if (!sem) {
|
|
304
282
|
return {
|
|
305
283
|
engineVersion: version ?? null,
|
|
@@ -318,20 +296,20 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
318
296
|
}
|
|
319
297
|
return {
|
|
320
298
|
engineVersion: `${sem.major}.${sem.minor}.${sem.patch}`,
|
|
321
|
-
introspection: atLeastVersion(sem, 0, 10),
|
|
322
|
-
jsonDocs: atLeastVersion(sem, 0, 12),
|
|
323
|
-
docFieldIndexes: atLeastVersion(sem, 0, 13),
|
|
324
|
-
serverJoins: atLeastVersion(sem, 0, 13),
|
|
325
|
-
nestedProjections: atLeastVersion(sem, 0, 18),
|
|
326
|
-
entityLinks: atLeastVersion(sem, 0, 19),
|
|
299
|
+
introspection: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 10),
|
|
300
|
+
jsonDocs: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 12),
|
|
301
|
+
docFieldIndexes: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
|
|
302
|
+
serverJoins: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
|
|
303
|
+
nestedProjections: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 18),
|
|
304
|
+
entityLinks: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19),
|
|
327
305
|
// PATCH-floored at 0.19.1: the `schema links` listing + `describe` link rows
|
|
328
306
|
// and the safe (hard-erroring) scalar-path traversal both landed in 0.19.1,
|
|
329
307
|
// never 0.19.0.
|
|
330
|
-
linkIntrospection: atLeastVersion(sem, 0, 19, 1),
|
|
331
|
-
linkPaths: atLeastVersion(sem, 0, 19, 1),
|
|
332
|
-
datetimeCompare: atLeastVersion(sem, 0, 20),
|
|
333
|
-
projectedCountNonNull: atLeastVersion(sem, 0, 20),
|
|
334
|
-
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
308
|
+
linkIntrospection: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19, 1),
|
|
309
|
+
linkPaths: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19, 1),
|
|
310
|
+
datetimeCompare: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20),
|
|
311
|
+
projectedCountNonNull: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20),
|
|
312
|
+
nativeRaw: Boolean(opts.hasNativeRaw) && (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
|
|
335
313
|
};
|
|
336
314
|
}
|
|
337
315
|
/**
|
|
@@ -396,6 +374,19 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
396
374
|
// m2m junction's `(source_id, target_id)`) marks its columns `required` but
|
|
397
375
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
398
376
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
377
|
+
// The primary key's OWN index: introspection lists it in `indexes` (the
|
|
378
|
+
// `<table>_pkey` a PRIMARY KEY constraint creates), so a composite key used
|
|
379
|
+
// to reach the composite-index refusal below even though the type body
|
|
380
|
+
// already handles it. Identified by what it guarantees rather than by name:
|
|
381
|
+
// a full (non-partial) unique index over exactly the PK's column SET states
|
|
382
|
+
// the PK constraint and nothing else, whatever order it lists the columns in.
|
|
383
|
+
// Anything short of that (non-unique, partial, a subset or superset) is a
|
|
384
|
+
// genuine composite index and is still refused.
|
|
385
|
+
const isPrimaryKeyIndex = (idx) => idx.unique &&
|
|
386
|
+
!idx.partial &&
|
|
387
|
+
!idx.docPath &&
|
|
388
|
+
new Set(idx.columns).size === pkSet.size &&
|
|
389
|
+
idx.columns.every((c) => pkSet.has(c));
|
|
399
390
|
const fields = meta.columns.map((col) => {
|
|
400
391
|
const powqlType = (0, powdb_shared_js_1.powqlColumnType)(col);
|
|
401
392
|
// Gate `json` columns behind the engine's jsonDocs capability when a
|
|
@@ -449,8 +440,16 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
449
440
|
stmts.push(`alter ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} add ${kind} (.${(0, powdb_shared_js_1.quotePowqlIdent)(column)}${segs})`);
|
|
450
441
|
}
|
|
451
442
|
else {
|
|
443
|
+
// The composite PK's index is already the type body's `required` columns
|
|
444
|
+
// (see isPrimaryKeyIndex); a single-column PK's index is skipped by the
|
|
445
|
+
// emittedUnique check below.
|
|
446
|
+
if (idx.columns.length > 1 && isPrimaryKeyIndex(idx))
|
|
447
|
+
continue;
|
|
452
448
|
// Plain column index. PowDB has no composite index (`add index` takes a
|
|
453
|
-
// single `.column`), so a multi-column entry is a typed E017.
|
|
449
|
+
// single `.column`), so a multi-column entry is a typed E017. That
|
|
450
|
+
// includes a composite UNIQUE index other than the PK: it is an
|
|
451
|
+
// integrity rule the engine cannot enforce, so it is refused rather than
|
|
452
|
+
// dropped without a word.
|
|
454
453
|
if (idx.columns.length !== 1) {
|
|
455
454
|
throw new errors_js_1.UnsupportedFeatureError('composite indexes', 'PowDB', `PowDB has no composite index. Index "${idx.name}" on ${meta.name} lists ` +
|
|
456
455
|
`${idx.columns.length} columns; declare a single-column index (or a doc-field index) instead.`);
|
|
@@ -1634,8 +1633,8 @@ class PowdbEmbeddedPool {
|
|
|
1634
1633
|
// reach this branch with a newer-than-ceiling engine is the anomalous
|
|
1635
1634
|
// newer-addon-without-native case (feature-detect failed). Refuse it rather
|
|
1636
1635
|
// than inline-encode against an unverified lexer.
|
|
1637
|
-
const engineSem = parsePowdbSemver(this.capabilities.engineVersion);
|
|
1638
|
-
const ceiling = parsePowdbSemver(exports.POWQL_LEXER_TESTED_CEILING);
|
|
1636
|
+
const engineSem = (0, powdb_shared_js_1.parsePowdbSemver)(this.capabilities.engineVersion);
|
|
1637
|
+
const ceiling = (0, powdb_shared_js_1.parsePowdbSemver)(exports.POWQL_LEXER_TESTED_CEILING);
|
|
1639
1638
|
if (engineSem &&
|
|
1640
1639
|
ceiling &&
|
|
1641
1640
|
(engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
|
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -499,6 +499,21 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
499
499
|
* becomes a no-op like {@link parseWriteRow} on the SQL engines.
|
|
500
500
|
*/
|
|
501
501
|
private stripWritePii;
|
|
502
|
+
/**
|
|
503
|
+
* A single-row write's `select` / `omit`, resolved by {@link projectionPlan}
|
|
504
|
+
* so the rules and messages are the read path's, and the SQL engines' (an
|
|
505
|
+
* unknown name is E003, `select` must name something, the pair is refused).
|
|
506
|
+
* `undefined` for the default return shape. Resolved BEFORE the write is sent,
|
|
507
|
+
* so a bad projection writes nothing.
|
|
508
|
+
*
|
|
509
|
+
* The narrowing itself happens on the returned row ({@link shapeWriteRow}):
|
|
510
|
+
* PowQL's `returning` takes no column list (see {@link stripWritePii}), so
|
|
511
|
+
* unlike the SQL engines the unselected columns still cross the wire here.
|
|
512
|
+
* The RESULT is identical across engines; the byte saving is SQL-only.
|
|
513
|
+
*/
|
|
514
|
+
private writeReturnPlan;
|
|
515
|
+
/** Apply a write's return plan to its row, or the default PII strip without one. */
|
|
516
|
+
private shapeWriteRow;
|
|
502
517
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
503
518
|
private projection;
|
|
504
519
|
/**
|
|
@@ -874,12 +889,21 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
874
889
|
}>;
|
|
875
890
|
upsert(args: UpsertArgs<T>): Promise<T>;
|
|
876
891
|
/**
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
|
|
882
|
-
|
|
892
|
+
* Upsert as a reselect-or-write inside one flat transaction, for every shape
|
|
893
|
+
* the native `upsert … on .col` statement cannot express: a conflict target
|
|
894
|
+
* other than a single-column primary key (it takes one column and PowDB has
|
|
895
|
+
* no composite unique), and a table under a global filter (its conflict
|
|
896
|
+
* branch takes no predicate). PowDB's single writer makes the read-then-write
|
|
897
|
+
* safe from concurrent writers; the transaction makes it atomic with the
|
|
898
|
+
* write.
|
|
899
|
+
*
|
|
900
|
+
* The row is looked up by the conflict columns with `create`'s values for
|
|
901
|
+
* them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
|
|
902
|
+
* find and the update run through the transaction's table interface, so a
|
|
903
|
+
* configured global filter applies to both exactly as it does to any other
|
|
904
|
+
* read or write, and `skipGlobalFilters` is forwarded to both.
|
|
905
|
+
*/
|
|
906
|
+
private upsertLookupFirst;
|
|
883
907
|
count(args?: CountArgs<T>): Promise<number>;
|
|
884
908
|
/**
|
|
885
909
|
* Gate ONE field of a per-field `_count`.
|
|
@@ -902,6 +926,27 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
902
926
|
* instead of values below 0.20, the same drift any stale-metadata query has.
|
|
903
927
|
*/
|
|
904
928
|
private assertProjectedCountSupported;
|
|
929
|
+
/**
|
|
930
|
+
* Can `aggregate()` compute several aggregates in ONE statement on this engine?
|
|
931
|
+
*
|
|
932
|
+
* PowQL refuses a bare multi-aggregate projection (`T { a: sum(.x), b: sum(.y) }`
|
|
933
|
+
* is "aggregate function in an unsupported position" on every engine version),
|
|
934
|
+
* so the only one-statement form is a grouping over a literal key,
|
|
935
|
+
* `T filter … group 1 { agg_0: sum(.x), agg_1: sum(.y) }`, which yields a
|
|
936
|
+
* single group holding every row the filter matched. Measured against the
|
|
937
|
+
* embedded addon at 0.7.1 through 0.28: below 0.13 a literal group key does not
|
|
938
|
+
* parse, from 0.13 to 0.19.1 the grouped per-field `count` disagrees with the
|
|
939
|
+
* scalar `count(T { .col })` on a nullable column, and from 0.20 every
|
|
940
|
+
* aggregate kind (count / sum / avg / min / max over int, float, str, an
|
|
941
|
+
* all-null column, and several filters) answers identically on both wires.
|
|
942
|
+
*
|
|
943
|
+
* Derived from the PROBED engine version rather than from a capability flag,
|
|
944
|
+
* so an unprobed pool (`engineVersion: null`, e.g. an injected pool carrying
|
|
945
|
+
* {@link ALL_POWDB_CAPABILITIES}) keeps the per-field statements: this changes
|
|
946
|
+
* the emitted PowQL, and an older engine would reject it outright, the same
|
|
947
|
+
* probe-only discipline `nestedProjections` follows.
|
|
948
|
+
*/
|
|
949
|
+
private get groupsAggregatesInOneStatement();
|
|
905
950
|
aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
|
|
906
951
|
groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
|
|
907
952
|
/** Validate a JSON-path target (group key / aggregate target): non-empty array of keys/indexes. */
|