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
|
@@ -44,22 +44,22 @@ export declare function coerceWriteValue(qi: BuilderCtx, key: string, value: unk
|
|
|
44
44
|
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
45
45
|
* builder as reads, so no user value is interpolated.
|
|
46
46
|
*/
|
|
47
|
-
export declare function buildReselectByWhere(qi: BuilderCtx, whereObj: Record<string, unknown
|
|
47
|
+
export declare function buildReselectByWhere(qi: BuilderCtx, whereObj: Record<string, unknown>, projection?: WriteProjection): {
|
|
48
48
|
sql: string;
|
|
49
49
|
params: unknown[];
|
|
50
50
|
};
|
|
51
|
-
export declare function buildCreate<T extends object>(qi: BuilderCtx, args: CreateArgs<T
|
|
51
|
+
export declare function buildCreate<T extends object>(qi: BuilderCtx, args: CreateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
52
52
|
/**
|
|
53
53
|
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
|
|
54
54
|
* `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
|
|
55
55
|
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
56
56
|
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
57
57
|
*/
|
|
58
|
-
export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, insertSql: string, insertParams: unknown[], data: Record<string, unknown
|
|
58
|
+
export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, insertSql: string, insertParams: unknown[], data: Record<string, unknown>, projection?: WriteProjection): DeferredQuery<T>['reselect'];
|
|
59
59
|
export declare function buildCreateMany<T extends object>(qi: BuilderCtx, args: CreateManyArgs<T>): DeferredQuery<T[]>;
|
|
60
|
-
export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T
|
|
61
|
-
export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T
|
|
62
|
-
export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T
|
|
60
|
+
export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
61
|
+
export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
62
|
+
export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
63
63
|
export declare function buildUpdateMany<T extends object>(qi: BuilderCtx, args: UpdateManyArgs<T>): DeferredQuery<{
|
|
64
64
|
count: number;
|
|
65
65
|
}>;
|
|
@@ -83,6 +83,36 @@ export declare function piiColumns(_qi: BuilderCtx, meta: TableMetadata): Set<st
|
|
|
83
83
|
* exclusion; you may still write PII fields freely).
|
|
84
84
|
*/
|
|
85
85
|
export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[];
|
|
86
|
+
/**
|
|
87
|
+
* A single-row write's caller-chosen return shape, from its `select` / `omit`.
|
|
88
|
+
*
|
|
89
|
+
* Resolved ONCE per call by builder.ts through `resolveProjection`, the same
|
|
90
|
+
* authority reads use, so a write and a read agree on every rule: a name must
|
|
91
|
+
* resolve to a column or the call throws E003, a relation name gets its own
|
|
92
|
+
* message, `select` must name something, `select` and `omit` are exclusive, the
|
|
93
|
+
* list is in TABLE order (a caller's key order must not mint distinct
|
|
94
|
+
* statements), and an explicit `select` of a PII column is the opt-in that
|
|
95
|
+
* returns it while `omit` leaves PII excluded. (builder.ts rather than this
|
|
96
|
+
* module because relations.ts already imports this one.)
|
|
97
|
+
*
|
|
98
|
+
* The point is bytes: `RETURNING *` sends back every column, a large JSON
|
|
99
|
+
* payload included, on every insert. Measured with a 3.1 KB jsonb column at
|
|
100
|
+
* concurrency 50, the same INSERT ran at 60,900/s with no RETURNING and at
|
|
101
|
+
* 34,500/s with `RETURNING *`.
|
|
102
|
+
*/
|
|
103
|
+
export interface WriteProjection {
|
|
104
|
+
/** Unquoted column names, in table order. Never empty. */
|
|
105
|
+
readonly columns: readonly string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The write-SQL cache segment for a projection. EMPTY for the default shape, so
|
|
109
|
+
* every existing key is byte-identical; a projected statement differs only in
|
|
110
|
+
* its RETURNING/OUTPUT text, and without this segment two calls with the same
|
|
111
|
+
* SET and WHERE but different `select`s would share one cached statement, i.e.
|
|
112
|
+
* the second caller would get the first caller's columns. NUL is the delimiter
|
|
113
|
+
* because it is the one byte a PostgreSQL identifier cannot contain.
|
|
114
|
+
*/
|
|
115
|
+
export declare function writeProjectionCacheSegment(projection: WriteProjection | undefined): string;
|
|
86
116
|
/**
|
|
87
117
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
88
118
|
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
@@ -119,13 +149,13 @@ export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[
|
|
|
119
149
|
* time (`SET "updated_at" = $1, "updated_at" = $2`, PostgreSQL 42701).
|
|
120
150
|
*/
|
|
121
151
|
export declare function applyUpdatedAtColumns(qi: BuilderCtx, data: Record<string, unknown>): Record<string, unknown>;
|
|
122
|
-
export declare function writeReturningColumns(qi: BuilderCtx): ReturningSelection;
|
|
152
|
+
export declare function writeReturningColumns(qi: BuilderCtx, projection?: WriteProjection): ReturningSelection;
|
|
123
153
|
/**
|
|
124
154
|
* String form of {@link writeReturningColumns} for a `SELECT` list (the
|
|
125
155
|
* `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
|
|
126
156
|
* `'*'` when there is no PII column; otherwise the comma-joined quoted list.
|
|
127
157
|
*/
|
|
128
|
-
export declare function writeReselectSelection(qi: BuilderCtx): string;
|
|
158
|
+
export declare function writeReselectSelection(qi: BuilderCtx, projection?: WriteProjection): string;
|
|
129
159
|
/**
|
|
130
160
|
* Parse a write's returned row (create/update/upsert/delete), then strip the
|
|
131
161
|
* table's PII fields: the write-side read policy. On PII-tagged tables the
|
|
@@ -134,7 +164,7 @@ export declare function writeReselectSelection(qi: BuilderCtx): string;
|
|
|
134
164
|
* no-op. Untagged tables incur only one `for` over a zero-length field list,
|
|
135
165
|
* so behavior is unchanged.
|
|
136
166
|
*/
|
|
137
|
-
export declare function parseWriteRow(qi: BuilderCtx, row: Record<string, unknown
|
|
167
|
+
export declare function parseWriteRow(qi: BuilderCtx, row: Record<string, unknown>, projection?: WriteProjection): Record<string, unknown>;
|
|
138
168
|
/**
|
|
139
169
|
* Reject any write against a view (H4). Views are introspected with
|
|
140
170
|
* `isView: true` and are read-only in every engine; a write raises a
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -57,6 +57,7 @@ exports.buildUpdateMany = buildUpdateMany;
|
|
|
57
57
|
exports.buildDeleteMany = buildDeleteMany;
|
|
58
58
|
exports.piiColumns = piiColumns;
|
|
59
59
|
exports.piiFields = piiFields;
|
|
60
|
+
exports.writeProjectionCacheSegment = writeProjectionCacheSegment;
|
|
60
61
|
exports.applyUpdatedAtColumns = applyUpdatedAtColumns;
|
|
61
62
|
exports.writeReturningColumns = writeReturningColumns;
|
|
62
63
|
exports.writeReselectSelection = writeReselectSelection;
|
|
@@ -133,11 +134,11 @@ function utcDateTimeWrites(qi) {
|
|
|
133
134
|
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
134
135
|
* builder as reads, so no user value is interpolated.
|
|
135
136
|
*/
|
|
136
|
-
function buildReselectByWhere(qi, whereObj) {
|
|
137
|
+
function buildReselectByWhere(qi, whereObj, projection) {
|
|
137
138
|
const params = [];
|
|
138
139
|
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
139
140
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
140
|
-
return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
141
|
+
return { sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
143
144
|
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
@@ -157,7 +158,7 @@ function buildReselectByWhere(qi, whereObj) {
|
|
|
157
158
|
* A dialect predating the hook raises E017 rather than emitting SQL its engine
|
|
158
159
|
* will reject.
|
|
159
160
|
*/
|
|
160
|
-
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
161
|
+
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates, projection) {
|
|
161
162
|
const build = qi.dialect.buildDefaultValuesInsertStatement;
|
|
162
163
|
if (!build) {
|
|
163
164
|
throw new errors_js_1.UnsupportedFeatureError('create/createMany with an empty data object', qi.dialect.name, 'This dialect has no all-defaults INSERT form; name at least one column in `data`.');
|
|
@@ -166,10 +167,10 @@ function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
|
166
167
|
table: qi.q(qi.table),
|
|
167
168
|
rowCount,
|
|
168
169
|
skipDuplicates,
|
|
169
|
-
returning: writeReturningColumns(qi),
|
|
170
|
+
returning: writeReturningColumns(qi, projection),
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
|
-
function buildCreate(qi, args) {
|
|
173
|
+
function buildCreate(qi, args, projection) {
|
|
173
174
|
assertWritable(qi, 'create');
|
|
174
175
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
175
176
|
const entries = writeEntries(qi, args.data);
|
|
@@ -179,12 +180,12 @@ function buildCreate(qi, args) {
|
|
|
179
180
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
180
181
|
// `data: {}` (or all-undefined) names no column: insert a row of defaults.
|
|
181
182
|
const sql = entries.length === 0
|
|
182
|
-
? buildDefaultValuesInsert(qi, 1)
|
|
183
|
+
? buildDefaultValuesInsert(qi, 1, undefined, projection)
|
|
183
184
|
: qi.dialect.buildInsertStatement({
|
|
184
185
|
table: qi.q(qi.table),
|
|
185
186
|
columns,
|
|
186
187
|
valuePlaceholders: placeholders,
|
|
187
|
-
returning: writeReturningColumns(qi),
|
|
188
|
+
returning: writeReturningColumns(qi, projection),
|
|
188
189
|
});
|
|
189
190
|
return {
|
|
190
191
|
sql,
|
|
@@ -198,12 +199,12 @@ function buildCreate(qi, args) {
|
|
|
198
199
|
message: `create on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
|
-
return parseWriteRow(qi, row);
|
|
202
|
+
return parseWriteRow(qi, row, projection);
|
|
202
203
|
},
|
|
203
204
|
tag: `${qi.table}.create`,
|
|
204
205
|
// Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
|
|
205
206
|
// (provided value, else the driver's generated insert id).
|
|
206
|
-
reselect: makeCreateReselect(qi, sql, params, args.data),
|
|
207
|
+
reselect: makeCreateReselect(qi, sql, params, args.data, projection),
|
|
207
208
|
};
|
|
208
209
|
}
|
|
209
210
|
/**
|
|
@@ -212,7 +213,7 @@ function buildCreate(qi, args) {
|
|
|
212
213
|
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
213
214
|
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
214
215
|
*/
|
|
215
|
-
function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
216
|
+
function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
|
|
216
217
|
if (qi.dialect.resultStrategy !== 'reselect')
|
|
217
218
|
return undefined;
|
|
218
219
|
return async (exec) => {
|
|
@@ -229,7 +230,7 @@ function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
229
230
|
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
230
231
|
}
|
|
231
232
|
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
232
|
-
return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
233
|
+
return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
/**
|
|
@@ -388,7 +389,7 @@ function buildCreateMany(qi, args) {
|
|
|
388
389
|
tag: `${qi.table}.createMany`,
|
|
389
390
|
};
|
|
390
391
|
}
|
|
391
|
-
function buildUpdate(qi, args) {
|
|
392
|
+
function buildUpdate(qi, args, projection) {
|
|
392
393
|
assertWritable(qi, 'update');
|
|
393
394
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
394
395
|
// `updatedAt`-tagged columns are filled in before anything reads `data`, so
|
|
@@ -431,7 +432,7 @@ function buildUpdate(qi, args) {
|
|
|
431
432
|
// version check that must still run.
|
|
432
433
|
const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
|
|
433
434
|
if (!hasSetData && !lock) {
|
|
434
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
435
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
435
436
|
return {
|
|
436
437
|
sql: sel.sql,
|
|
437
438
|
params: sel.params,
|
|
@@ -439,14 +440,16 @@ function buildUpdate(qi, args) {
|
|
|
439
440
|
const row = result.rows[0];
|
|
440
441
|
if (!row)
|
|
441
442
|
throw new errors_js_1.NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
|
|
442
|
-
return parseWriteRow(qi, row);
|
|
443
|
+
return parseWriteRow(qi, row, projection);
|
|
443
444
|
},
|
|
444
445
|
tag: `${qi.table}.update`,
|
|
445
446
|
};
|
|
446
447
|
}
|
|
447
448
|
const setFp = fingerprintSet(qi, dataObj);
|
|
448
449
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
449
|
-
const ck = lock
|
|
450
|
+
const ck = lock
|
|
451
|
+
? null
|
|
452
|
+
: `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
450
453
|
const params = [];
|
|
451
454
|
const buildSql = (freshParams) => {
|
|
452
455
|
const setEntries = writeEntries(qi, dataObj);
|
|
@@ -467,7 +470,7 @@ function buildUpdate(qi, args) {
|
|
|
467
470
|
// `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
|
|
468
471
|
// absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
469
472
|
// `returning` excludes PII columns on tagged tables (else '*').
|
|
470
|
-
const returning = writeReturningColumns(qi);
|
|
473
|
+
const returning = writeReturningColumns(qi, projection);
|
|
471
474
|
return qi.dialect.buildUpdateStatement
|
|
472
475
|
? qi.dialect.buildUpdateStatement({ table: qi.q(qi.table), setClauses, whereSql, returning })
|
|
473
476
|
: `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
|
|
@@ -512,7 +515,7 @@ function buildUpdate(qi, args) {
|
|
|
512
515
|
operation: 'update',
|
|
513
516
|
});
|
|
514
517
|
}
|
|
515
|
-
return parseWriteRow(qi, row);
|
|
518
|
+
return parseWriteRow(qi, row, projection);
|
|
516
519
|
},
|
|
517
520
|
tag: `${qi.table}.update`,
|
|
518
521
|
preparedName,
|
|
@@ -532,13 +535,13 @@ function buildUpdate(qi, args) {
|
|
|
532
535
|
expectedVersion: lock.expected,
|
|
533
536
|
});
|
|
534
537
|
}
|
|
535
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
538
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
536
539
|
return exec(sel.sql, sel.params);
|
|
537
540
|
}
|
|
538
541
|
: undefined,
|
|
539
542
|
};
|
|
540
543
|
}
|
|
541
|
-
function buildDelete(qi, args) {
|
|
544
|
+
function buildDelete(qi, args, projection) {
|
|
542
545
|
assertWritable(qi, 'delete');
|
|
543
546
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
544
547
|
// Prisma compound-unique selector → the column conjunction (before the guard).
|
|
@@ -552,7 +555,7 @@ function buildDelete(qi, args) {
|
|
|
552
555
|
(0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(qi.tableMeta, qi.table, userWhere, 'delete');
|
|
553
556
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
554
557
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
555
|
-
const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
558
|
+
const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
556
559
|
const params = [];
|
|
557
560
|
const buildSql = (freshParams) => {
|
|
558
561
|
const clause = whereMod.buildWhereClause(qi, whereObj, freshParams);
|
|
@@ -560,7 +563,7 @@ function buildDelete(qi, args) {
|
|
|
560
563
|
// SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
|
|
561
564
|
// absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
562
565
|
// `returning` excludes PII columns on tagged tables (else '*').
|
|
563
|
-
const returning = writeReturningColumns(qi);
|
|
566
|
+
const returning = writeReturningColumns(qi, projection);
|
|
564
567
|
return qi.dialect.buildDeleteStatement
|
|
565
568
|
? qi.dialect.buildDeleteStatement({ table: qi.q(qi.table), whereSql, returning })
|
|
566
569
|
: `DELETE FROM ${qi.q(qi.table)}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
|
|
@@ -580,7 +583,7 @@ function buildDelete(qi, args) {
|
|
|
580
583
|
operation: 'delete',
|
|
581
584
|
});
|
|
582
585
|
}
|
|
583
|
-
return parseWriteRow(qi, row);
|
|
586
|
+
return parseWriteRow(qi, row, projection);
|
|
584
587
|
},
|
|
585
588
|
tag: `${qi.table}.delete`,
|
|
586
589
|
preparedName: entry.name,
|
|
@@ -588,7 +591,7 @@ function buildDelete(qi, args) {
|
|
|
588
591
|
// by the same where, then run the DELETE, returning the captured row.
|
|
589
592
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
590
593
|
? async (exec) => {
|
|
591
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
594
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
592
595
|
const pre = await exec(sel.sql, sel.params);
|
|
593
596
|
await exec(entry.sql, params, entry.name);
|
|
594
597
|
return pre;
|
|
@@ -596,7 +599,7 @@ function buildDelete(qi, args) {
|
|
|
596
599
|
: undefined,
|
|
597
600
|
};
|
|
598
601
|
}
|
|
599
|
-
function buildUpsert(qi, args) {
|
|
602
|
+
function buildUpsert(qi, args, projection) {
|
|
600
603
|
assertWritable(qi, 'upsert');
|
|
601
604
|
assertNoGeneratedColumns(qi, args.create, 'upsert');
|
|
602
605
|
assertNoGeneratedColumns(qi, args.update, 'upsert');
|
|
@@ -652,8 +655,23 @@ function buildUpsert(qi, args) {
|
|
|
652
655
|
// unqualified column is ambiguous and PostgreSQL rejected EVERY upsert on a
|
|
653
656
|
// globally filtered table at parse time (42702), insert path included.
|
|
654
657
|
let updateWhere;
|
|
658
|
+
const upsertFilter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
659
|
+
if (upsertFilter && !qi.dialect.supportsUpsertUpdateWhere) {
|
|
660
|
+
// MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot and SQL Server's
|
|
661
|
+
// MERGE cannot take the builder's column references there, so on those
|
|
662
|
+
// engines the filter used to be DROPPED from the conflict update, silently.
|
|
663
|
+
// A tenant-scoped upsert whose key matched another tenant's row updated
|
|
664
|
+
// that row. Refused instead, when the filter would compile to anything:
|
|
665
|
+
// the global-filter contract is that it scopes every update, and an
|
|
666
|
+
// upsert that cannot honour it must not run as if it did.
|
|
667
|
+
if (whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), upsertFilter, [])) {
|
|
668
|
+
throw new errors_js_1.UnsupportedFeatureError(`upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, "This engine's upsert statement cannot carry the filter on its conflict update, so it could update a row " +
|
|
669
|
+
'the filter hides. Use findUnique, then update or create, inside $transaction; or pass ' +
|
|
670
|
+
'`skipGlobalFilters: UNSAFE` if the upsert is meant to reach every row.');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
655
673
|
if (qi.dialect.supportsUpsertUpdateWhere) {
|
|
656
|
-
const gf =
|
|
674
|
+
const gf = upsertFilter;
|
|
657
675
|
if (gf) {
|
|
658
676
|
// Compiled through a scope whose FROM-item reference is ALREADY RENDERED
|
|
659
677
|
// (`"users"`), which is what the target table is inside `ON CONFLICT ...
|
|
@@ -678,7 +696,7 @@ function buildUpsert(qi, args) {
|
|
|
678
696
|
conflictColumns,
|
|
679
697
|
updateSetClauses: setClauses,
|
|
680
698
|
updateWhere,
|
|
681
|
-
returning: writeReturningColumns(qi),
|
|
699
|
+
returning: writeReturningColumns(qi, projection),
|
|
682
700
|
});
|
|
683
701
|
return {
|
|
684
702
|
sql,
|
|
@@ -703,14 +721,14 @@ function buildUpsert(qi, args) {
|
|
|
703
721
|
: `upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
704
722
|
});
|
|
705
723
|
}
|
|
706
|
-
return parseWriteRow(qi, row);
|
|
724
|
+
return parseWriteRow(qi, row, projection);
|
|
707
725
|
},
|
|
708
726
|
tag: `${qi.table}.upsert`,
|
|
709
727
|
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
710
728
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
711
729
|
? async (exec) => {
|
|
712
730
|
await exec(sql, params);
|
|
713
|
-
const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}));
|
|
731
|
+
const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}), projection);
|
|
714
732
|
return exec(sel.sql, sel.params);
|
|
715
733
|
}
|
|
716
734
|
: undefined,
|
|
@@ -859,6 +877,17 @@ function piiFields(_qi, meta) {
|
|
|
859
877
|
}
|
|
860
878
|
return out;
|
|
861
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* The write-SQL cache segment for a projection. EMPTY for the default shape, so
|
|
882
|
+
* every existing key is byte-identical; a projected statement differs only in
|
|
883
|
+
* its RETURNING/OUTPUT text, and without this segment two calls with the same
|
|
884
|
+
* SET and WHERE but different `select`s would share one cached statement, i.e.
|
|
885
|
+
* the second caller would get the first caller's columns. NUL is the delimiter
|
|
886
|
+
* because it is the one byte a PostgreSQL identifier cannot contain.
|
|
887
|
+
*/
|
|
888
|
+
function writeProjectionCacheSegment(projection) {
|
|
889
|
+
return projection ? `|rt=${projection.columns.join('\u0000')}` : '';
|
|
890
|
+
}
|
|
862
891
|
/**
|
|
863
892
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
864
893
|
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
@@ -925,7 +954,9 @@ function namedColumns(meta, data) {
|
|
|
925
954
|
}
|
|
926
955
|
return out;
|
|
927
956
|
}
|
|
928
|
-
function writeReturningColumns(qi) {
|
|
957
|
+
function writeReturningColumns(qi, projection) {
|
|
958
|
+
if (projection)
|
|
959
|
+
return projection.columns.map((col) => qi.q(col));
|
|
929
960
|
// The PK exemption used to be re-stated here as `|| pk.has(col)`. It now
|
|
930
961
|
// lives in `piiColumns` so every projection inherits it and none can drift.
|
|
931
962
|
const piiCols = piiColumns(qi, qi.tableMeta);
|
|
@@ -938,8 +969,8 @@ function writeReturningColumns(qi) {
|
|
|
938
969
|
* `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
|
|
939
970
|
* `'*'` when there is no PII column; otherwise the comma-joined quoted list.
|
|
940
971
|
*/
|
|
941
|
-
function writeReselectSelection(qi) {
|
|
942
|
-
const cols = writeReturningColumns(qi);
|
|
972
|
+
function writeReselectSelection(qi, projection) {
|
|
973
|
+
const cols = writeReturningColumns(qi, projection);
|
|
943
974
|
return cols === '*' ? '*' : cols.join(', ');
|
|
944
975
|
}
|
|
945
976
|
/**
|
|
@@ -950,10 +981,17 @@ function writeReselectSelection(qi) {
|
|
|
950
981
|
* no-op. Untagged tables incur only one `for` over a zero-length field list,
|
|
951
982
|
* so behavior is unchanged.
|
|
952
983
|
*/
|
|
953
|
-
function parseWriteRow(qi, row) {
|
|
984
|
+
function parseWriteRow(qi, row, projection) {
|
|
954
985
|
const parsed = qi.parseRow(row, qi.table);
|
|
955
|
-
|
|
956
|
-
|
|
986
|
+
// A column the caller named in `select` is the opt-in (see WriteProjection),
|
|
987
|
+
// so the strip spares exactly the projected columns and nothing else.
|
|
988
|
+
const pii = piiColumns(qi, qi.tableMeta);
|
|
989
|
+
if (pii.size === 0)
|
|
990
|
+
return parsed;
|
|
991
|
+
const requested = projection ? new Set(projection.columns) : undefined;
|
|
992
|
+
for (const col of qi.tableMeta.columns) {
|
|
993
|
+
if (pii.has(col.name) && !requested?.has(col.name))
|
|
994
|
+
delete parsed[col.field];
|
|
957
995
|
}
|
|
958
996
|
return parsed;
|
|
959
997
|
}
|
package/dist/cjs/realtime.d.ts
CHANGED
|
@@ -24,6 +24,18 @@
|
|
|
24
24
|
* (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
|
|
25
25
|
* `$listen` will surface a clear error rather than hang. `$notify` works
|
|
26
26
|
* everywhere, it's a single round-trip `SELECT pg_notify(...)`.
|
|
27
|
+
*
|
|
28
|
+
* Connection loss:
|
|
29
|
+
*
|
|
30
|
+
* A subscription's connection is held indefinitely with no query in flight,
|
|
31
|
+
* so a database restart, failover or `pg_terminate_backend` reaches it only
|
|
32
|
+
* as an `'error'` event. That event used to have no listener, which exits the
|
|
33
|
+
* process. It is now guarded (connection-guard.ts), and the subscription
|
|
34
|
+
* RECONNECTS by default: the dead connection is destroyed, a fresh one is
|
|
35
|
+
* checked out with exponential backoff, and `LISTEN` is re-issued. Postgres
|
|
36
|
+
* does not queue notifications for a listener that is not connected, so
|
|
37
|
+
* anything NOTIFYed during the gap is gone for good; `onReconnect` is the
|
|
38
|
+
* caller's cue to resynchronise from the source of truth.
|
|
27
39
|
*/
|
|
28
40
|
import type { PgCompatPool } from './client.js';
|
|
29
41
|
/**
|
|
@@ -36,6 +48,34 @@ import type { PgCompatPool } from './client.js';
|
|
|
36
48
|
export declare function validateChannel(channel: string): void;
|
|
37
49
|
/** Handler invoked with the raw NOTIFY payload string (empty string if none). */
|
|
38
50
|
export type NotificationHandler = (payload: string) => void;
|
|
51
|
+
/** Backoff for re-establishing a subscription whose connection was lost. */
|
|
52
|
+
export interface ListenReconnectOptions {
|
|
53
|
+
/** Delay before the first reconnect attempt. Default 100 ms. */
|
|
54
|
+
initialDelayMs?: number;
|
|
55
|
+
/** Ceiling for the doubling delay between attempts. Default 30,000 ms. */
|
|
56
|
+
maxDelayMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Options for `$listen`. */
|
|
59
|
+
export interface ListenOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Re-establish the subscription after its connection is lost (a restart,
|
|
62
|
+
* failover, compute suspend, `pg_terminate_backend`). Default `true`. With
|
|
63
|
+
* `false` the subscription ends at the first loss, after `onError`.
|
|
64
|
+
*/
|
|
65
|
+
reconnect?: boolean | ListenReconnectOptions;
|
|
66
|
+
/**
|
|
67
|
+
* Called when the connection is lost and after each failed reconnect
|
|
68
|
+
* attempt. Defaults to one `console.error` line per event. The subscription
|
|
69
|
+
* keeps retrying unless `reconnect` is `false`; call `unsubscribe()` to stop.
|
|
70
|
+
*/
|
|
71
|
+
onError?: (err: Error) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Called once `LISTEN` is active again on a fresh connection. Notifications
|
|
74
|
+
* sent while the subscription was disconnected were NOT delivered and never
|
|
75
|
+
* will be, so this is where to resynchronise from the source of truth.
|
|
76
|
+
*/
|
|
77
|
+
onReconnect?: () => void;
|
|
78
|
+
}
|
|
39
79
|
/**
|
|
40
80
|
* A live LISTEN subscription. Call `unsubscribe()` to UNLISTEN, detach the
|
|
41
81
|
* handler, and release the dedicated connection back to the pool.
|
|
@@ -46,6 +86,7 @@ export interface Subscription {
|
|
|
46
86
|
/**
|
|
47
87
|
* Stop listening: runs `UNLISTEN "chan"`, removes the notification listener,
|
|
48
88
|
* and releases the dedicated connection. Idempotent, safe to call twice.
|
|
89
|
+
* Also cancels a pending reconnect.
|
|
49
90
|
*/
|
|
50
91
|
unsubscribe(): Promise<void>;
|
|
51
92
|
}
|
|
@@ -59,12 +100,15 @@ export interface ActiveSubscription extends Subscription {
|
|
|
59
100
|
}
|
|
60
101
|
/**
|
|
61
102
|
* Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
|
|
103
|
+
* A failure here is thrown to the `$listen` caller; only a connection lost
|
|
104
|
+
* AFTER the subscription is established is retried.
|
|
62
105
|
*
|
|
63
106
|
* @param pool the pg-compatible pool to check a long-lived client out of
|
|
64
107
|
* @param channel channel name, MUST already be validated by the caller
|
|
65
108
|
* @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
|
|
66
109
|
* @param handler called with each notification's payload
|
|
67
|
-
* @param onClosed invoked when the subscription
|
|
110
|
+
* @param onClosed invoked when the subscription ends, so the client can
|
|
68
111
|
* drop it from its active-subscription registry
|
|
112
|
+
* @param options reconnect policy and loss/reconnect callbacks
|
|
69
113
|
*/
|
|
70
|
-
export declare function createSubscription(pool: PgCompatPool, channel: string, quotedChannel: string, handler: NotificationHandler, onClosed: (sub: ActiveSubscription) => void): Promise<ActiveSubscription>;
|
|
114
|
+
export declare function createSubscription(pool: PgCompatPool, channel: string, quotedChannel: string, handler: NotificationHandler, onClosed: (sub: ActiveSubscription) => void, options?: ListenOptions): Promise<ActiveSubscription>;
|