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/powql.js
CHANGED
|
@@ -71,6 +71,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
71
71
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72
72
|
exports.PowqlInterface = exports.MAX_POWQL_DATETIME_TERMS = void 0;
|
|
73
73
|
const node_crypto_1 = require("node:crypto");
|
|
74
|
+
const connection_guard_js_1 = require("./connection-guard.js");
|
|
74
75
|
const errors_js_1 = require("./errors.js");
|
|
75
76
|
const nested_write_js_1 = require("./nested-write.js");
|
|
76
77
|
const powdb_shared_js_1 = require("./powdb-shared.js");
|
|
@@ -1272,6 +1273,34 @@ class PowqlInterface {
|
|
|
1272
1273
|
}
|
|
1273
1274
|
return entity;
|
|
1274
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* A single-row write's `select` / `omit`, resolved by {@link projectionPlan}
|
|
1278
|
+
* so the rules and messages are the read path's, and the SQL engines' (an
|
|
1279
|
+
* unknown name is E003, `select` must name something, the pair is refused).
|
|
1280
|
+
* `undefined` for the default return shape. Resolved BEFORE the write is sent,
|
|
1281
|
+
* so a bad projection writes nothing.
|
|
1282
|
+
*
|
|
1283
|
+
* The narrowing itself happens on the returned row ({@link shapeWriteRow}):
|
|
1284
|
+
* PowQL's `returning` takes no column list (see {@link stripWritePii}), so
|
|
1285
|
+
* unlike the SQL engines the unselected columns still cross the wire here.
|
|
1286
|
+
* The RESULT is identical across engines; the byte saving is SQL-only.
|
|
1287
|
+
*/
|
|
1288
|
+
writeReturnPlan(args) {
|
|
1289
|
+
if (args.select === undefined && args.omit === undefined)
|
|
1290
|
+
return undefined;
|
|
1291
|
+
return this.projectionPlan(args.select, args.omit, false);
|
|
1292
|
+
}
|
|
1293
|
+
/** Apply a write's return plan to its row, or the default PII strip without one. */
|
|
1294
|
+
shapeWriteRow(entity, plan) {
|
|
1295
|
+
if (!entity || !plan)
|
|
1296
|
+
return this.stripWritePii(entity);
|
|
1297
|
+
const keep = new Set(plan.cols.filter((c) => !plan.forcedPk.includes(c)).map((c) => this.meta.reverseColumnMap[c] ?? c));
|
|
1298
|
+
for (const key of Object.keys(entity)) {
|
|
1299
|
+
if (!keep.has(key))
|
|
1300
|
+
delete entity[key];
|
|
1301
|
+
}
|
|
1302
|
+
return entity;
|
|
1303
|
+
}
|
|
1275
1304
|
/** `{ .c1, .c2, … }` projection clause. */
|
|
1276
1305
|
projection(cols) {
|
|
1277
1306
|
return `{ ${cols.map((c) => this.colRefName(c)).join(', ')} }`;
|
|
@@ -2712,6 +2741,7 @@ class PowqlInterface {
|
|
|
2712
2741
|
}
|
|
2713
2742
|
async create(args) {
|
|
2714
2743
|
return this.withMiddleware('create', args, async () => {
|
|
2744
|
+
const plan = this.writeReturnPlan(args);
|
|
2715
2745
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
|
|
2716
2746
|
return this.nestedCreate(args);
|
|
2717
2747
|
}
|
|
@@ -2723,7 +2753,7 @@ class PowqlInterface {
|
|
|
2723
2753
|
.join(', ');
|
|
2724
2754
|
// `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
|
|
2725
2755
|
const { rows, native } = await this.exec(`insert ${this.qt} { ${body} } returning`, params, args.timeout, 'create');
|
|
2726
|
-
const row = rows.length ? this.
|
|
2756
|
+
const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
|
|
2727
2757
|
if (!row)
|
|
2728
2758
|
throw new errors_js_1.NotFoundError({ table: this.table, where: data });
|
|
2729
2759
|
return row;
|
|
@@ -2798,6 +2828,7 @@ class PowqlInterface {
|
|
|
2798
2828
|
}
|
|
2799
2829
|
async update(args) {
|
|
2800
2830
|
return this.withMiddleware('update', args, async () => {
|
|
2831
|
+
const plan = this.writeReturnPlan(args);
|
|
2801
2832
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
|
|
2802
2833
|
// The nested engine re-enters `update` / `findUnique` for the parent
|
|
2803
2834
|
// row before it writes anything, so the identity rule below still runs
|
|
@@ -2835,7 +2866,7 @@ class PowqlInterface {
|
|
|
2835
2866
|
}
|
|
2836
2867
|
// `returning` hands back the post-update row(s); take the first (single-row contract).
|
|
2837
2868
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
|
|
2838
|
-
const row = rows.length ? this.
|
|
2869
|
+
const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
|
|
2839
2870
|
if (!row) {
|
|
2840
2871
|
if (lock) {
|
|
2841
2872
|
throw new errors_js_1.OptimisticLockError({
|
|
@@ -2912,18 +2943,21 @@ class PowqlInterface {
|
|
|
2912
2943
|
}
|
|
2913
2944
|
async nestedCreate(args) {
|
|
2914
2945
|
const data = args.data;
|
|
2946
|
+
// Applied by the nested engine's final read-back, as on the SQL engines.
|
|
2947
|
+
const shape = { select: args.select, omit: args.omit };
|
|
2915
2948
|
if (this.isTxScoped()) {
|
|
2916
|
-
return (0, nested_write_js_1.executeNestedCreate)(this.buildNestedCtx(), this.table, data);
|
|
2949
|
+
return (0, nested_write_js_1.executeNestedCreate)(this.buildNestedCtx(), this.table, data, 0, [], shape);
|
|
2917
2950
|
}
|
|
2918
|
-
return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedCreate)(ctx, this.table, data));
|
|
2951
|
+
return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedCreate)(ctx, this.table, data, 0, [], shape));
|
|
2919
2952
|
}
|
|
2920
2953
|
async nestedUpdate(args) {
|
|
2921
2954
|
const data = args.data;
|
|
2922
2955
|
const where = args.where;
|
|
2956
|
+
const shape = { select: args.select, omit: args.omit };
|
|
2923
2957
|
if (this.isTxScoped()) {
|
|
2924
|
-
return (0, nested_write_js_1.executeNestedUpdate)(this.buildNestedCtx(), this.table, where, data);
|
|
2958
|
+
return (0, nested_write_js_1.executeNestedUpdate)(this.buildNestedCtx(), this.table, where, data, 0, [], shape);
|
|
2925
2959
|
}
|
|
2926
|
-
return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedUpdate)(ctx, this.table, where, data));
|
|
2960
|
+
return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedUpdate)(ctx, this.table, where, data, 0, [], shape));
|
|
2927
2961
|
}
|
|
2928
2962
|
/** Open a flat PowDB transaction on a pinned connection and run `fn` inside it. */
|
|
2929
2963
|
async runInImplicitTx(fn) {
|
|
@@ -2936,6 +2970,9 @@ class PowqlInterface {
|
|
|
2936
2970
|
// drifts from `powdbDialect`; falls back to the literal lowercase keywords.
|
|
2937
2971
|
const d = this.options.dialect;
|
|
2938
2972
|
const client = await this.pool.connect();
|
|
2973
|
+
// A no-op today (PowDB checkouts have no event surface), kept so this path
|
|
2974
|
+
// cannot become the one unguarded checkout if a transport ever emits 'error'.
|
|
2975
|
+
const checkout = (0, connection_guard_js_1.guardCheckout)(client);
|
|
2939
2976
|
let began = false;
|
|
2940
2977
|
try {
|
|
2941
2978
|
await client.query(d?.beginStatement?.() ?? 'begin');
|
|
@@ -2975,7 +3012,7 @@ class PowqlInterface {
|
|
|
2975
3012
|
throw err;
|
|
2976
3013
|
}
|
|
2977
3014
|
finally {
|
|
2978
|
-
|
|
3015
|
+
checkout.release();
|
|
2979
3016
|
}
|
|
2980
3017
|
}
|
|
2981
3018
|
/** Already inside a transaction: build a context whose table accessors reuse the pinned pool. */
|
|
@@ -2988,6 +3025,7 @@ class PowqlInterface {
|
|
|
2988
3025
|
}
|
|
2989
3026
|
async delete(args) {
|
|
2990
3027
|
return this.withMiddleware('delete', args, async () => {
|
|
3028
|
+
const plan = this.writeReturnPlan(args);
|
|
2991
3029
|
const allowFullTableScan = (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan');
|
|
2992
3030
|
const userWhere = this.expandedWhere(args.where);
|
|
2993
3031
|
const params = [];
|
|
@@ -2998,7 +3036,7 @@ class PowqlInterface {
|
|
|
2998
3036
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2999
3037
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
3000
3038
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
3001
|
-
const row = rows.length ? this.
|
|
3039
|
+
const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
|
|
3002
3040
|
if (!row)
|
|
3003
3041
|
throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
|
|
3004
3042
|
return row;
|
|
@@ -3024,13 +3062,45 @@ class PowqlInterface {
|
|
|
3024
3062
|
// disagreeing about whether a query is VALID, which is the failure class
|
|
3025
3063
|
// 0.64.0 and 0.72.0 were both spent on. Before `applyPkDefault`, so a
|
|
3026
3064
|
// refused upsert mints no client-side UUID and sends nothing.
|
|
3027
|
-
|
|
3065
|
+
const upsertWhere = (this.expandedWhere(args.where) ?? {});
|
|
3066
|
+
this.assertMutationIdentifiesOneRow(upsertWhere, false, 'upsert');
|
|
3067
|
+
const plan = this.writeReturnPlan(args);
|
|
3028
3068
|
const createData = this.applyPkDefault(args.create);
|
|
3029
3069
|
const pkCol = this.meta.primaryKey[0];
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3070
|
+
// The conflict target is the columns `where` names, as on the SQL
|
|
3071
|
+
// engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
|
|
3072
|
+
// taken from `create`. This path used to conflict on the primary key
|
|
3073
|
+
// whatever `where` named, so an upsert keyed on another unique column
|
|
3074
|
+
// (`where: { email }`) never found the existing row.
|
|
3075
|
+
const conflictColumns = Object.keys(upsertWhere)
|
|
3076
|
+
.filter((k) => upsertWhere[k] !== undefined)
|
|
3077
|
+
.map((k) => this.column(k).name);
|
|
3078
|
+
// PowQL's native `upsert … on .col` expresses exactly one shape: a single
|
|
3079
|
+
// conflict column that is the primary key, with no predicate on its
|
|
3080
|
+
// conflict branch. Everything else is an atomic reselect-or-write
|
|
3081
|
+
// transaction.
|
|
3082
|
+
//
|
|
3083
|
+
// That includes a table under a global filter. The native statement's
|
|
3084
|
+
// `on conflict` branch carries no predicate, so a tenant-scoped client
|
|
3085
|
+
// whose `create` key matched ANOTHER tenant's row updated that row, and
|
|
3086
|
+
// the tenant-filtered reselect then reported the write it had just made
|
|
3087
|
+
// as a NotFoundError. The SQL engines guard the same branch with the
|
|
3088
|
+
// filter (`DO UPDATE … WHERE tenant_id = $n`). The lookup-first path runs
|
|
3089
|
+
// its find and its update through the filtered interface, so another
|
|
3090
|
+
// tenant's row is invisible to it: the insert then collides on the key
|
|
3091
|
+
// and fails, as it does on the SQL engines, and nothing is overwritten.
|
|
3092
|
+
const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
|
|
3093
|
+
const native = this.meta.primaryKey.length === 1 &&
|
|
3094
|
+
pkCol !== undefined &&
|
|
3095
|
+
conflictColumns.length === 1 &&
|
|
3096
|
+
conflictColumns[0] === pkCol &&
|
|
3097
|
+
!filtered;
|
|
3098
|
+
if (!native) {
|
|
3099
|
+
return this.upsertLookupFirst(createData, args.update, conflictColumns, {
|
|
3100
|
+
select: args.select,
|
|
3101
|
+
omit: args.omit,
|
|
3102
|
+
skipGlobalFilters: args.skipGlobalFilters,
|
|
3103
|
+
});
|
|
3034
3104
|
}
|
|
3035
3105
|
const params = [];
|
|
3036
3106
|
const createBody = this.scalarData(createData)
|
|
@@ -3043,40 +3113,61 @@ class PowqlInterface {
|
|
|
3043
3113
|
// reselect-by-PK fetch; create/update/delete all use `returning`.
|
|
3044
3114
|
await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
|
|
3045
3115
|
const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
|
|
3046
|
-
// Either spelling, as `
|
|
3116
|
+
// Either spelling, as `upsertLookupFirst` already does below: reading the
|
|
3047
3117
|
// camelCase field alone reselected `undefined` for a snake-spelled PK and
|
|
3048
3118
|
// reported a write that had SUCCEEDED as a NotFoundError.
|
|
3049
|
-
|
|
3119
|
+
// `args` carries skipGlobalFilters too: an upsert that opted out of the
|
|
3120
|
+
// filter must not have its own write filtered out of the reselect.
|
|
3121
|
+
const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout, args);
|
|
3050
3122
|
if (!row)
|
|
3051
3123
|
throw new errors_js_1.NotFoundError({ table: this.table, where: createData });
|
|
3052
|
-
|
|
3124
|
+
// Without a plan the reselect's default projection already left PII out.
|
|
3125
|
+
return plan ? this.shapeWriteRow(row, plan) : row;
|
|
3053
3126
|
});
|
|
3054
3127
|
}
|
|
3055
3128
|
/**
|
|
3056
|
-
*
|
|
3057
|
-
*
|
|
3058
|
-
*
|
|
3059
|
-
*
|
|
3060
|
-
|
|
3061
|
-
|
|
3129
|
+
* Upsert as a reselect-or-write inside one flat transaction, for every shape
|
|
3130
|
+
* the native `upsert … on .col` statement cannot express: a conflict target
|
|
3131
|
+
* other than a single-column primary key (it takes one column and PowDB has
|
|
3132
|
+
* no composite unique), and a table under a global filter (its conflict
|
|
3133
|
+
* branch takes no predicate). PowDB's single writer makes the read-then-write
|
|
3134
|
+
* safe from concurrent writers; the transaction makes it atomic with the
|
|
3135
|
+
* write.
|
|
3136
|
+
*
|
|
3137
|
+
* The row is looked up by the conflict columns with `create`'s values for
|
|
3138
|
+
* them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
|
|
3139
|
+
* find and the update run through the transaction's table interface, so a
|
|
3140
|
+
* configured global filter applies to both exactly as it does to any other
|
|
3141
|
+
* read or write, and `skipGlobalFilters` is forwarded to both.
|
|
3142
|
+
*/
|
|
3143
|
+
async upsertLookupFirst(createData, updateData, conflictColumns, shape) {
|
|
3144
|
+
const { skipGlobalFilters, ...projection } = shape;
|
|
3145
|
+
const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
|
|
3062
3146
|
// Accept either the camelCase field or the snake_case column in `create`
|
|
3063
3147
|
// (create() resolves both), and key the where by field name.
|
|
3064
|
-
const
|
|
3065
|
-
const field = this.meta.reverseColumnMap[
|
|
3066
|
-
return { field, value: createData[field] ?? createData[
|
|
3148
|
+
const keyPairs = conflictColumns.map((col) => {
|
|
3149
|
+
const field = this.meta.reverseColumnMap[col] ?? col;
|
|
3150
|
+
return { field, value: createData[field] ?? createData[col] };
|
|
3067
3151
|
});
|
|
3068
|
-
|
|
3069
|
-
|
|
3152
|
+
const keyless = keyPairs.some((p) => p.value == null);
|
|
3153
|
+
const isCompositePk = keyPairs.length > 1 &&
|
|
3154
|
+
keyPairs.length === this.meta.primaryKey.length &&
|
|
3155
|
+
conflictColumns.every((c) => this.meta.primaryKey.includes(c));
|
|
3156
|
+
if (keyless && isCompositePk) {
|
|
3157
|
+
throw new errors_js_1.ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
|
|
3070
3158
|
.map((p) => p.field)
|
|
3071
3159
|
.join(', ')}).`);
|
|
3072
3160
|
}
|
|
3073
|
-
const
|
|
3161
|
+
const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
|
|
3074
3162
|
const run = async (ctx) => {
|
|
3075
3163
|
const tbl = ctx.tx.table(this.table);
|
|
3076
|
-
|
|
3164
|
+
// A row whose `create` leaves a conflict column unset cannot collide on
|
|
3165
|
+
// it (NULL never equals NULL in a unique index, and a server-assigned key
|
|
3166
|
+
// is new), which is what `ON CONFLICT` concludes on the SQL engines too.
|
|
3167
|
+
const existing = keyless ? null : await tbl.findUnique({ where: keyWhere, ...skip });
|
|
3077
3168
|
return existing
|
|
3078
|
-
? (await tbl.update({ where:
|
|
3079
|
-
: (await tbl.create({ data: createData }));
|
|
3169
|
+
? (await tbl.update({ where: keyWhere, data: updateData, ...projection, ...skip }))
|
|
3170
|
+
: (await tbl.create({ data: createData, ...projection }));
|
|
3080
3171
|
};
|
|
3081
3172
|
return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
|
|
3082
3173
|
}
|
|
@@ -3123,33 +3214,64 @@ class PowqlInterface {
|
|
|
3123
3214
|
'every SQL engine exactly when the column is nullable. `_count: true` (a row count) and a per-field ' +
|
|
3124
3215
|
'`_count` of a NOT NULL column are correct on every version and are never refused.');
|
|
3125
3216
|
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Can `aggregate()` compute several aggregates in ONE statement on this engine?
|
|
3219
|
+
*
|
|
3220
|
+
* PowQL refuses a bare multi-aggregate projection (`T { a: sum(.x), b: sum(.y) }`
|
|
3221
|
+
* is "aggregate function in an unsupported position" on every engine version),
|
|
3222
|
+
* so the only one-statement form is a grouping over a literal key,
|
|
3223
|
+
* `T filter … group 1 { agg_0: sum(.x), agg_1: sum(.y) }`, which yields a
|
|
3224
|
+
* single group holding every row the filter matched. Measured against the
|
|
3225
|
+
* embedded addon at 0.7.1 through 0.28: below 0.13 a literal group key does not
|
|
3226
|
+
* parse, from 0.13 to 0.19.1 the grouped per-field `count` disagrees with the
|
|
3227
|
+
* scalar `count(T { .col })` on a nullable column, and from 0.20 every
|
|
3228
|
+
* aggregate kind (count / sum / avg / min / max over int, float, str, an
|
|
3229
|
+
* all-null column, and several filters) answers identically on both wires.
|
|
3230
|
+
*
|
|
3231
|
+
* Derived from the PROBED engine version rather than from a capability flag,
|
|
3232
|
+
* so an unprobed pool (`engineVersion: null`, e.g. an injected pool carrying
|
|
3233
|
+
* {@link ALL_POWDB_CAPABILITIES}) keeps the per-field statements: this changes
|
|
3234
|
+
* the emitted PowQL, and an older engine would reject it outright, the same
|
|
3235
|
+
* probe-only discipline `nestedProjections` follows.
|
|
3236
|
+
*/
|
|
3237
|
+
get groupsAggregatesInOneStatement() {
|
|
3238
|
+
const sem = (0, powdb_shared_js_1.parsePowdbSemver)(this.capabilities.engineVersion);
|
|
3239
|
+
return sem !== null && (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20);
|
|
3240
|
+
}
|
|
3126
3241
|
async aggregate(args) {
|
|
3127
3242
|
this.assertNoForceCustomPlan(args);
|
|
3128
3243
|
return this.withMiddleware('aggregate', args, async () => {
|
|
3129
|
-
// One scalar query per aggregate, PowDB's bare-projection aggregate is broken.
|
|
3130
3244
|
const result = {};
|
|
3131
3245
|
const filterParams = [];
|
|
3132
3246
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
3133
3247
|
let where = this.buildWhere(resolvedWhere, filterParams);
|
|
3134
3248
|
where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
|
|
3135
3249
|
const filter = where ? ` filter ${where}` : '';
|
|
3136
|
-
const
|
|
3137
|
-
const params = [...filterParams];
|
|
3138
|
-
const { rows } = await this.exec(expr, params, args.timeout, 'aggregate');
|
|
3139
|
-
const v = rows[0]?.value;
|
|
3140
|
-
return v == null || v === 'null' ? null : Number(v);
|
|
3141
|
-
};
|
|
3250
|
+
const plan = [];
|
|
3142
3251
|
if (args._count) {
|
|
3143
3252
|
if (args._count === true) {
|
|
3144
|
-
|
|
3253
|
+
plan.push({
|
|
3254
|
+
scalar: `count(${this.qt}${filter})`,
|
|
3255
|
+
inner: 'count(*)',
|
|
3256
|
+
assign: (v) => {
|
|
3257
|
+
result._count = v ?? 0;
|
|
3258
|
+
},
|
|
3259
|
+
});
|
|
3145
3260
|
}
|
|
3146
3261
|
else {
|
|
3147
3262
|
const counts = {};
|
|
3263
|
+
result._count = counts;
|
|
3148
3264
|
for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
|
|
3149
3265
|
this.assertProjectedCountSupported(field);
|
|
3150
|
-
|
|
3266
|
+
const ref = this.ref(field);
|
|
3267
|
+
plan.push({
|
|
3268
|
+
scalar: `count(${this.qt}${filter} { ${ref} })`,
|
|
3269
|
+
inner: `count(${ref})`,
|
|
3270
|
+
assign: (v) => {
|
|
3271
|
+
counts[field] = v ?? 0;
|
|
3272
|
+
},
|
|
3273
|
+
});
|
|
3151
3274
|
}
|
|
3152
|
-
result._count = counts;
|
|
3153
3275
|
}
|
|
3154
3276
|
}
|
|
3155
3277
|
for (const fn of ['_sum', '_avg', '_min', '_max']) {
|
|
@@ -3157,15 +3279,44 @@ class PowqlInterface {
|
|
|
3157
3279
|
if (!spec)
|
|
3158
3280
|
continue;
|
|
3159
3281
|
const acc = {};
|
|
3282
|
+
result[fn] = acc;
|
|
3283
|
+
const powfn = fn.slice(1); // sum/avg/min/max
|
|
3160
3284
|
for (const field of Object.keys(spec).filter((f) => spec[f])) {
|
|
3161
|
-
const powfn = fn.slice(1); // sum/avg/min/max
|
|
3162
3285
|
// Same PII contract as the SQL engines: _min/_max return a stored cell.
|
|
3163
3286
|
if (fn === '_min' || fn === '_max') {
|
|
3164
3287
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
3165
3288
|
}
|
|
3166
|
-
|
|
3289
|
+
const ref = this.ref(field);
|
|
3290
|
+
plan.push({
|
|
3291
|
+
scalar: `${powfn}(${this.qt}${filter} { ${ref} })`,
|
|
3292
|
+
inner: `${powfn}(${ref})`,
|
|
3293
|
+
assign: (v) => {
|
|
3294
|
+
acc[field] = v;
|
|
3295
|
+
},
|
|
3296
|
+
});
|
|
3167
3297
|
}
|
|
3168
|
-
|
|
3298
|
+
}
|
|
3299
|
+
// A scalar cell and a grouped cell decode through the same wire decoder,
|
|
3300
|
+
// so one coercion serves both forms.
|
|
3301
|
+
const toNumber = (v) => (v == null || v === 'null' ? null : Number(v));
|
|
3302
|
+
if (plan.length > 1 && this.groupsAggregatesInOneStatement) {
|
|
3303
|
+
const proj = plan.map((p, i) => `agg_${i}: ${p.inner}`).join(', ');
|
|
3304
|
+
const { rows } = await this.exec(`${this.qt}${filter} group 1 { ${proj} }`, [...filterParams], args.timeout, 'aggregate');
|
|
3305
|
+
// Zero groups means the filter matched no row. The per-field statements
|
|
3306
|
+
// below answer that case instead of a synthesized one, because what an
|
|
3307
|
+
// aggregate over NO rows returns is engine-versioned (`sum` answered 0
|
|
3308
|
+
// before PowDB 0.28 and null since), and the scalar form is the one
|
|
3309
|
+
// whose answer this method has always returned.
|
|
3310
|
+
const row = rows.length === 1 ? rows[0] : undefined;
|
|
3311
|
+
if (row) {
|
|
3312
|
+
for (const [i, p] of plan.entries())
|
|
3313
|
+
p.assign(toNumber(row[`agg_${i}`]));
|
|
3314
|
+
return result;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
for (const p of plan) {
|
|
3318
|
+
const { rows } = await this.exec(p.scalar, [...filterParams], args.timeout, 'aggregate');
|
|
3319
|
+
p.assign(toNumber(rows[0]?.value));
|
|
3169
3320
|
}
|
|
3170
3321
|
return result;
|
|
3171
3322
|
});
|
|
@@ -3568,12 +3719,15 @@ class PowqlInterface {
|
|
|
3568
3719
|
// Reselect helper (upsert only, PowDB's upsert has no `returning`)
|
|
3569
3720
|
// -------------------------------------------------------------------------
|
|
3570
3721
|
/** Reselect a single row by its single-column primary key value. */
|
|
3571
|
-
async reselectByPk(pkValue, timeout) {
|
|
3722
|
+
async reselectByPk(pkValue, timeout, shape) {
|
|
3572
3723
|
const pkField = this.meta.reverseColumnMap[this.meta.primaryKey[0]] ?? this.meta.primaryKey[0];
|
|
3573
3724
|
const { rows, native } = await this.runFind({
|
|
3574
3725
|
where: { [pkField]: pkValue },
|
|
3575
3726
|
limit: 1,
|
|
3576
3727
|
timeout,
|
|
3728
|
+
select: shape?.select,
|
|
3729
|
+
omit: shape?.omit,
|
|
3730
|
+
...(shape?.skipGlobalFilters === undefined ? {} : { skipGlobalFilters: shape.skipGlobalFilters }),
|
|
3577
3731
|
});
|
|
3578
3732
|
return rows.length ? this.shape(rows, native)[0] : null;
|
|
3579
3733
|
}
|
|
@@ -1005,6 +1005,22 @@ function resolveWriteProjection(ctx, mm, op, args) {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
return NO_WRITE_PROJECTION;
|
|
1007
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* The same projection in core's spelling, for the write itself. Passed DOWN so
|
|
1010
|
+
* the SQL engines narrow the RETURNING list rather than fetching the whole row
|
|
1011
|
+
* for {@link applyWriteProjection} to trim afterwards, which on a table with a
|
|
1012
|
+
* large JSON column was most of the bytes the write moved. The trim stays: it
|
|
1013
|
+
* is what keeps the result Prisma-shaped on every path, including the
|
|
1014
|
+
* lookup-first upsert and PowDB, whose `returning` takes no column list.
|
|
1015
|
+
*/
|
|
1016
|
+
function coreWriteProjection(mm, proj) {
|
|
1017
|
+
const spell = (keys) => Object.fromEntries([...keys].map((k) => [renameField(mm, k), true]));
|
|
1018
|
+
if (proj.pick)
|
|
1019
|
+
return { select: spell(proj.pick) };
|
|
1020
|
+
if (proj.omit)
|
|
1021
|
+
return { omit: spell(proj.omit) };
|
|
1022
|
+
return {};
|
|
1023
|
+
}
|
|
1008
1024
|
/** Apply a {@link WriteProjection} to one already-reshaped write result row. */
|
|
1009
1025
|
function applyWriteProjection(proj, row) {
|
|
1010
1026
|
if (!isPlainObject(row))
|
|
@@ -1799,9 +1815,12 @@ function upsertKeysMatch(t) {
|
|
|
1799
1815
|
/** Prisma upsert semantics: look up by where; update the found row, else insert create. */
|
|
1800
1816
|
async function upsertLookupFirst(qi, t) {
|
|
1801
1817
|
const existing = await qi.findUnique({ where: t.where });
|
|
1818
|
+
// The projection rides along so both branches return what the native upsert
|
|
1819
|
+
// path returns (an explicitly selected PII field included).
|
|
1820
|
+
const shape = { select: t.select, omit: t.omit };
|
|
1802
1821
|
if (existing)
|
|
1803
|
-
return qi.update({ where: t.where, data: t.update });
|
|
1804
|
-
return qi.create({ data: t.create });
|
|
1822
|
+
return qi.update({ where: t.where, data: t.update, ...shape });
|
|
1823
|
+
return qi.create({ data: t.create, ...shape });
|
|
1805
1824
|
}
|
|
1806
1825
|
/**
|
|
1807
1826
|
* The row shapes a translated `createMany` has to insert, as contiguous runs
|
|
@@ -1909,7 +1928,10 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1909
1928
|
const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
|
|
1910
1929
|
return defer('create', args, () => {
|
|
1911
1930
|
proj = resolveWriteProjection(ctx, mm, 'create', args);
|
|
1912
|
-
const t = {
|
|
1931
|
+
const t = {
|
|
1932
|
+
data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)),
|
|
1933
|
+
...coreWriteProjection(mm, proj),
|
|
1934
|
+
};
|
|
1913
1935
|
(0, index_js_1.applyNativeOptions)(index_js_1.CREATE_OPTIONS, args, t);
|
|
1914
1936
|
return t;
|
|
1915
1937
|
}, (qi, t) => qi.create(t).then(shape), {
|
|
@@ -1954,6 +1976,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1954
1976
|
const t = {
|
|
1955
1977
|
where: translateWhere(ctx, mm, a.where),
|
|
1956
1978
|
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1979
|
+
...coreWriteProjection(mm, proj),
|
|
1957
1980
|
};
|
|
1958
1981
|
(0, index_js_1.applyNativeOptions)(index_js_1.UPDATE_OPTIONS, a, t);
|
|
1959
1982
|
// `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
|
|
@@ -1993,7 +2016,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1993
2016
|
return defer('delete', args, () => {
|
|
1994
2017
|
const a = requireWhere(args, 'delete');
|
|
1995
2018
|
proj = resolveWriteProjection(ctx, mm, 'delete', a);
|
|
1996
|
-
const t = { where: translateWhere(ctx, mm, a.where) };
|
|
2019
|
+
const t = { where: translateWhere(ctx, mm, a.where), ...coreWriteProjection(mm, proj) };
|
|
1997
2020
|
(0, index_js_1.applyNativeOptions)(index_js_1.DELETE_OPTIONS, a, t);
|
|
1998
2021
|
return t;
|
|
1999
2022
|
}, (qi, t) => qi.delete(t).then(shape), { build: (qi, t) => qi.buildDelete(t), reshape: shape });
|
|
@@ -2018,6 +2041,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
2018
2041
|
where: translateWhere(ctx, mm, a.where),
|
|
2019
2042
|
create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
|
|
2020
2043
|
update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
|
|
2044
|
+
...coreWriteProjection(mm, proj),
|
|
2021
2045
|
};
|
|
2022
2046
|
(0, index_js_1.applyNativeOptions)(index_js_1.UPSERT_OPTIONS, a, t);
|
|
2023
2047
|
return t;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { PgCompatPool } from '../pg-types.js';
|
|
14
14
|
import type { SchemaMetadata } from '../schema.js';
|
|
15
|
-
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
15
|
+
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
16
16
|
/**
|
|
17
17
|
* Discard the memoized cross-check environment so the next cache hit re-reads
|
|
18
18
|
* `process.env`.
|
|
@@ -993,10 +993,34 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
993
993
|
* here and is not refused.
|
|
994
994
|
*/
|
|
995
995
|
private preparedNameFor;
|
|
996
|
+
/**
|
|
997
|
+
* {@link queryWithTimeout} for a statement that only READS, which makes it
|
|
998
|
+
* safe to send twice. Outside a transaction, a read that fails because the
|
|
999
|
+
* connection it went out on had already been closed by the server
|
|
1000
|
+
* ({@link isStaleConnectionError}) is sent once more, after one event-loop
|
|
1001
|
+
* turn, on whatever connection the pool hands out next.
|
|
1002
|
+
*
|
|
1003
|
+
* Reads only, and the distinction is the whole design. For a write, "the
|
|
1004
|
+
* connection died" does not say whether the statement committed first: a
|
|
1005
|
+
* connection can drop after the server commits and before the reply
|
|
1006
|
+
* arrives, so resending an INSERT can insert it twice. A read has no such
|
|
1007
|
+
* outcome. Nor inside a transaction, where the connection that died WAS the
|
|
1008
|
+
* transaction and a retry on another one would run outside it.
|
|
1009
|
+
*
|
|
1010
|
+
* The loop turn is what makes a second attempt worth making: pg-pool evicts
|
|
1011
|
+
* an idle connection whose close it has read, so after one poll phase every
|
|
1012
|
+
* other connection that died alongside this one is gone from the idle list
|
|
1013
|
+
* rather than lent out to the retry.
|
|
1014
|
+
*
|
|
1015
|
+
* Every read call site uses this and every write uses `queryWithTimeout`
|
|
1016
|
+
* directly; `src/test/stale-connection-retry.test.ts` pins which is which.
|
|
1017
|
+
*/
|
|
1018
|
+
private readWithTimeout;
|
|
996
1019
|
/**
|
|
997
1020
|
* Execute a pool.query with an optional timeout.
|
|
998
1021
|
* If timeout is set, races the query against a timer and rejects on expiry.
|
|
999
1022
|
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
1023
|
+
* `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
|
|
1000
1024
|
*/
|
|
1001
1025
|
private queryWithTimeout;
|
|
1002
1026
|
/**
|
|
@@ -1013,11 +1037,18 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1013
1037
|
* write and a follow-up SELECT; the SELECT's rows feed the transform.
|
|
1014
1038
|
*/
|
|
1015
1039
|
private executeMutation;
|
|
1016
|
-
buildCreate(args: CreateArgs<T>): DeferredQuery<T
|
|
1040
|
+
buildCreate<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
|
|
1041
|
+
/**
|
|
1042
|
+
* A single-row write's `select` / `omit`, resolved through the SAME
|
|
1043
|
+
* `resolveProjection` reads use (see WriteProjection in writes.ts), or
|
|
1044
|
+
* `undefined` for the default return shape. Resolved here rather than in
|
|
1045
|
+
* writes.ts because relations.ts imports writes.ts.
|
|
1046
|
+
*/
|
|
1047
|
+
private resolveWriteProjection;
|
|
1017
1048
|
buildCreateMany(args: CreateManyArgs<T>): DeferredQuery<T[]>;
|
|
1018
|
-
buildUpdate(args: UpdateArgs<T>): DeferredQuery<T
|
|
1019
|
-
buildDelete(args: DeleteArgs<T>): DeferredQuery<T
|
|
1020
|
-
buildUpsert(args: UpsertArgs<T>): DeferredQuery<T
|
|
1049
|
+
buildUpdate<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
|
|
1050
|
+
buildDelete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
|
|
1051
|
+
buildUpsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
|
|
1021
1052
|
buildUpdateMany(args: UpdateManyArgs<T>): DeferredQuery<{
|
|
1022
1053
|
count: number;
|
|
1023
1054
|
}>;
|
|
@@ -1235,34 +1266,23 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1235
1266
|
buildFindFirstOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args?: FindManyArgs<T, R, W, S, O>): DeferredQuery<QueryResult<T, R, W, S, O>>;
|
|
1236
1267
|
findUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): Promise<QueryResult<T, R, W, S, O>>;
|
|
1237
1268
|
buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): DeferredQuery<QueryResult<T, R, W, S, O>>;
|
|
1238
|
-
create(args: CreateArgs<T, R>): Promise<T
|
|
1269
|
+
create<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1239
1270
|
createMany(args: CreateManyArgs<T>): Promise<T[]>;
|
|
1240
|
-
update(args: UpdateArgs<T, R>): Promise<T
|
|
1271
|
+
update<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1241
1272
|
private nestedCreate;
|
|
1242
1273
|
private nestedUpdate;
|
|
1243
1274
|
/**
|
|
1244
|
-
* Check out a pooled connection,
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
* (SQLSTATE 28P01), no such database (3D000), nothing listening
|
|
1249
|
-
* (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
|
|
1250
|
-
* those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
|
|
1251
|
-
* as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
|
|
1252
|
-
* property Turbine puts `TURBINE_E0NN` in.
|
|
1253
|
-
*
|
|
1254
|
-
* client.ts has its own copy for `$transaction` / `connect()`; this one
|
|
1255
|
-
* exists because `query/` must not import client.ts (circular dependency).
|
|
1256
|
-
* The query paths need no equivalent: `pool.query()` opens the connection
|
|
1257
|
-
* itself and rejects with the connect error, which the query boundary
|
|
1258
|
-
* already wraps.
|
|
1275
|
+
* Check out a guarded pooled connection, as a typed error when that fails.
|
|
1276
|
+
* Shared with client.ts through checkout.ts (see there for why each part
|
|
1277
|
+
* matters); the cursor stream holds it, and a nested write opens its
|
|
1278
|
+
* implicit transaction on one through `openCheckout`.
|
|
1259
1279
|
*/
|
|
1260
1280
|
private acquireConnection;
|
|
1261
1281
|
private runInImplicitTx;
|
|
1262
1282
|
private buildNestedCtx;
|
|
1263
1283
|
private makeTxProxy;
|
|
1264
|
-
delete(args: DeleteArgs<T, R>): Promise<T
|
|
1265
|
-
upsert(args: UpsertArgs<T, R>): Promise<T
|
|
1284
|
+
delete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1285
|
+
upsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1266
1286
|
updateMany(args: UpdateManyArgs<T, R>): Promise<{
|
|
1267
1287
|
count: number;
|
|
1268
1288
|
}>;
|