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/query/writes.d.ts
CHANGED
|
@@ -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/query/writes.js
CHANGED
|
@@ -76,11 +76,11 @@ function utcDateTimeWrites(qi) {
|
|
|
76
76
|
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
77
77
|
* builder as reads, so no user value is interpolated.
|
|
78
78
|
*/
|
|
79
|
-
export function buildReselectByWhere(qi, whereObj) {
|
|
79
|
+
export function buildReselectByWhere(qi, whereObj, projection) {
|
|
80
80
|
const params = [];
|
|
81
81
|
const clause = whereMod.buildWhereClause(qi, whereObj, params);
|
|
82
82
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
83
|
-
return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
83
|
+
return { sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
@@ -100,7 +100,7 @@ export function buildReselectByWhere(qi, whereObj) {
|
|
|
100
100
|
* A dialect predating the hook raises E017 rather than emitting SQL its engine
|
|
101
101
|
* will reject.
|
|
102
102
|
*/
|
|
103
|
-
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
103
|
+
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates, projection) {
|
|
104
104
|
const build = qi.dialect.buildDefaultValuesInsertStatement;
|
|
105
105
|
if (!build) {
|
|
106
106
|
throw new 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`.');
|
|
@@ -109,10 +109,10 @@ function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
|
109
109
|
table: qi.q(qi.table),
|
|
110
110
|
rowCount,
|
|
111
111
|
skipDuplicates,
|
|
112
|
-
returning: writeReturningColumns(qi),
|
|
112
|
+
returning: writeReturningColumns(qi, projection),
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
export function buildCreate(qi, args) {
|
|
115
|
+
export function buildCreate(qi, args, projection) {
|
|
116
116
|
assertWritable(qi, 'create');
|
|
117
117
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
118
118
|
const entries = writeEntries(qi, args.data);
|
|
@@ -122,12 +122,12 @@ export function buildCreate(qi, args) {
|
|
|
122
122
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
123
123
|
// `data: {}` (or all-undefined) names no column: insert a row of defaults.
|
|
124
124
|
const sql = entries.length === 0
|
|
125
|
-
? buildDefaultValuesInsert(qi, 1)
|
|
125
|
+
? buildDefaultValuesInsert(qi, 1, undefined, projection)
|
|
126
126
|
: qi.dialect.buildInsertStatement({
|
|
127
127
|
table: qi.q(qi.table),
|
|
128
128
|
columns,
|
|
129
129
|
valuePlaceholders: placeholders,
|
|
130
|
-
returning: writeReturningColumns(qi),
|
|
130
|
+
returning: writeReturningColumns(qi, projection),
|
|
131
131
|
});
|
|
132
132
|
return {
|
|
133
133
|
sql,
|
|
@@ -141,12 +141,12 @@ export function buildCreate(qi, args) {
|
|
|
141
141
|
message: `create on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
-
return parseWriteRow(qi, row);
|
|
144
|
+
return parseWriteRow(qi, row, projection);
|
|
145
145
|
},
|
|
146
146
|
tag: `${qi.table}.create`,
|
|
147
147
|
// Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
|
|
148
148
|
// (provided value, else the driver's generated insert id).
|
|
149
|
-
reselect: makeCreateReselect(qi, sql, params, args.data),
|
|
149
|
+
reselect: makeCreateReselect(qi, sql, params, args.data, projection),
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
@@ -155,7 +155,7 @@ export function buildCreate(qi, args) {
|
|
|
155
155
|
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
156
156
|
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
157
157
|
*/
|
|
158
|
-
export function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
158
|
+
export function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
|
|
159
159
|
if (qi.dialect.resultStrategy !== 'reselect')
|
|
160
160
|
return undefined;
|
|
161
161
|
return async (exec) => {
|
|
@@ -172,7 +172,7 @@ export function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
172
172
|
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
173
173
|
}
|
|
174
174
|
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
175
|
-
return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
175
|
+
return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
@@ -331,7 +331,7 @@ export function buildCreateMany(qi, args) {
|
|
|
331
331
|
tag: `${qi.table}.createMany`,
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
|
-
export function buildUpdate(qi, args) {
|
|
334
|
+
export function buildUpdate(qi, args, projection) {
|
|
335
335
|
assertWritable(qi, 'update');
|
|
336
336
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
337
337
|
// `updatedAt`-tagged columns are filled in before anything reads `data`, so
|
|
@@ -374,7 +374,7 @@ export function buildUpdate(qi, args) {
|
|
|
374
374
|
// version check that must still run.
|
|
375
375
|
const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
|
|
376
376
|
if (!hasSetData && !lock) {
|
|
377
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
377
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
378
378
|
return {
|
|
379
379
|
sql: sel.sql,
|
|
380
380
|
params: sel.params,
|
|
@@ -382,14 +382,16 @@ export function buildUpdate(qi, args) {
|
|
|
382
382
|
const row = result.rows[0];
|
|
383
383
|
if (!row)
|
|
384
384
|
throw new NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
|
|
385
|
-
return parseWriteRow(qi, row);
|
|
385
|
+
return parseWriteRow(qi, row, projection);
|
|
386
386
|
},
|
|
387
387
|
tag: `${qi.table}.update`,
|
|
388
388
|
};
|
|
389
389
|
}
|
|
390
390
|
const setFp = fingerprintSet(qi, dataObj);
|
|
391
391
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
392
|
-
const ck = lock
|
|
392
|
+
const ck = lock
|
|
393
|
+
? null
|
|
394
|
+
: `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
393
395
|
const params = [];
|
|
394
396
|
const buildSql = (freshParams) => {
|
|
395
397
|
const setEntries = writeEntries(qi, dataObj);
|
|
@@ -410,7 +412,7 @@ export function buildUpdate(qi, args) {
|
|
|
410
412
|
// `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
|
|
411
413
|
// absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
412
414
|
// `returning` excludes PII columns on tagged tables (else '*').
|
|
413
|
-
const returning = writeReturningColumns(qi);
|
|
415
|
+
const returning = writeReturningColumns(qi, projection);
|
|
414
416
|
return qi.dialect.buildUpdateStatement
|
|
415
417
|
? qi.dialect.buildUpdateStatement({ table: qi.q(qi.table), setClauses, whereSql, returning })
|
|
416
418
|
: `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
|
|
@@ -455,7 +457,7 @@ export function buildUpdate(qi, args) {
|
|
|
455
457
|
operation: 'update',
|
|
456
458
|
});
|
|
457
459
|
}
|
|
458
|
-
return parseWriteRow(qi, row);
|
|
460
|
+
return parseWriteRow(qi, row, projection);
|
|
459
461
|
},
|
|
460
462
|
tag: `${qi.table}.update`,
|
|
461
463
|
preparedName,
|
|
@@ -475,13 +477,13 @@ export function buildUpdate(qi, args) {
|
|
|
475
477
|
expectedVersion: lock.expected,
|
|
476
478
|
});
|
|
477
479
|
}
|
|
478
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
480
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
479
481
|
return exec(sel.sql, sel.params);
|
|
480
482
|
}
|
|
481
483
|
: undefined,
|
|
482
484
|
};
|
|
483
485
|
}
|
|
484
|
-
export function buildDelete(qi, args) {
|
|
486
|
+
export function buildDelete(qi, args, projection) {
|
|
485
487
|
assertWritable(qi, 'delete');
|
|
486
488
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
487
489
|
// Prisma compound-unique selector → the column conjunction (before the guard).
|
|
@@ -495,7 +497,7 @@ export function buildDelete(qi, args) {
|
|
|
495
497
|
assertMutationWhereIdentifiesOneRow(qi.tableMeta, qi.table, userWhere, 'delete');
|
|
496
498
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
497
499
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
498
|
-
const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
500
|
+
const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
|
|
499
501
|
const params = [];
|
|
500
502
|
const buildSql = (freshParams) => {
|
|
501
503
|
const clause = whereMod.buildWhereClause(qi, whereObj, freshParams);
|
|
@@ -503,7 +505,7 @@ export function buildDelete(qi, args) {
|
|
|
503
505
|
// SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
|
|
504
506
|
// absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
505
507
|
// `returning` excludes PII columns on tagged tables (else '*').
|
|
506
|
-
const returning = writeReturningColumns(qi);
|
|
508
|
+
const returning = writeReturningColumns(qi, projection);
|
|
507
509
|
return qi.dialect.buildDeleteStatement
|
|
508
510
|
? qi.dialect.buildDeleteStatement({ table: qi.q(qi.table), whereSql, returning })
|
|
509
511
|
: `DELETE FROM ${qi.q(qi.table)}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
|
|
@@ -523,7 +525,7 @@ export function buildDelete(qi, args) {
|
|
|
523
525
|
operation: 'delete',
|
|
524
526
|
});
|
|
525
527
|
}
|
|
526
|
-
return parseWriteRow(qi, row);
|
|
528
|
+
return parseWriteRow(qi, row, projection);
|
|
527
529
|
},
|
|
528
530
|
tag: `${qi.table}.delete`,
|
|
529
531
|
preparedName: entry.name,
|
|
@@ -531,7 +533,7 @@ export function buildDelete(qi, args) {
|
|
|
531
533
|
// by the same where, then run the DELETE, returning the captured row.
|
|
532
534
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
533
535
|
? async (exec) => {
|
|
534
|
-
const sel = buildReselectByWhere(qi, whereObj);
|
|
536
|
+
const sel = buildReselectByWhere(qi, whereObj, projection);
|
|
535
537
|
const pre = await exec(sel.sql, sel.params);
|
|
536
538
|
await exec(entry.sql, params, entry.name);
|
|
537
539
|
return pre;
|
|
@@ -539,7 +541,7 @@ export function buildDelete(qi, args) {
|
|
|
539
541
|
: undefined,
|
|
540
542
|
};
|
|
541
543
|
}
|
|
542
|
-
export function buildUpsert(qi, args) {
|
|
544
|
+
export function buildUpsert(qi, args, projection) {
|
|
543
545
|
assertWritable(qi, 'upsert');
|
|
544
546
|
assertNoGeneratedColumns(qi, args.create, 'upsert');
|
|
545
547
|
assertNoGeneratedColumns(qi, args.update, 'upsert');
|
|
@@ -595,8 +597,23 @@ export function buildUpsert(qi, args) {
|
|
|
595
597
|
// unqualified column is ambiguous and PostgreSQL rejected EVERY upsert on a
|
|
596
598
|
// globally filtered table at parse time (42702), insert path included.
|
|
597
599
|
let updateWhere;
|
|
600
|
+
const upsertFilter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
601
|
+
if (upsertFilter && !qi.dialect.supportsUpsertUpdateWhere) {
|
|
602
|
+
// MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot and SQL Server's
|
|
603
|
+
// MERGE cannot take the builder's column references there, so on those
|
|
604
|
+
// engines the filter used to be DROPPED from the conflict update, silently.
|
|
605
|
+
// A tenant-scoped upsert whose key matched another tenant's row updated
|
|
606
|
+
// that row. Refused instead, when the filter would compile to anything:
|
|
607
|
+
// the global-filter contract is that it scopes every update, and an
|
|
608
|
+
// upsert that cannot honour it must not run as if it did.
|
|
609
|
+
if (whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), upsertFilter, [])) {
|
|
610
|
+
throw new 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 " +
|
|
611
|
+
'the filter hides. Use findUnique, then update or create, inside $transaction; or pass ' +
|
|
612
|
+
'`skipGlobalFilters: UNSAFE` if the upsert is meant to reach every row.');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
598
615
|
if (qi.dialect.supportsUpsertUpdateWhere) {
|
|
599
|
-
const gf =
|
|
616
|
+
const gf = upsertFilter;
|
|
600
617
|
if (gf) {
|
|
601
618
|
// Compiled through a scope whose FROM-item reference is ALREADY RENDERED
|
|
602
619
|
// (`"users"`), which is what the target table is inside `ON CONFLICT ...
|
|
@@ -621,7 +638,7 @@ export function buildUpsert(qi, args) {
|
|
|
621
638
|
conflictColumns,
|
|
622
639
|
updateSetClauses: setClauses,
|
|
623
640
|
updateWhere,
|
|
624
|
-
returning: writeReturningColumns(qi),
|
|
641
|
+
returning: writeReturningColumns(qi, projection),
|
|
625
642
|
});
|
|
626
643
|
return {
|
|
627
644
|
sql,
|
|
@@ -646,14 +663,14 @@ export function buildUpsert(qi, args) {
|
|
|
646
663
|
: `upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
647
664
|
});
|
|
648
665
|
}
|
|
649
|
-
return parseWriteRow(qi, row);
|
|
666
|
+
return parseWriteRow(qi, row, projection);
|
|
650
667
|
},
|
|
651
668
|
tag: `${qi.table}.upsert`,
|
|
652
669
|
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
653
670
|
reselect: qi.dialect.resultStrategy === 'reselect'
|
|
654
671
|
? async (exec) => {
|
|
655
672
|
await exec(sql, params);
|
|
656
|
-
const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}));
|
|
673
|
+
const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}), projection);
|
|
657
674
|
return exec(sel.sql, sel.params);
|
|
658
675
|
}
|
|
659
676
|
: undefined,
|
|
@@ -802,6 +819,17 @@ export function piiFields(_qi, meta) {
|
|
|
802
819
|
}
|
|
803
820
|
return out;
|
|
804
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* The write-SQL cache segment for a projection. EMPTY for the default shape, so
|
|
824
|
+
* every existing key is byte-identical; a projected statement differs only in
|
|
825
|
+
* its RETURNING/OUTPUT text, and without this segment two calls with the same
|
|
826
|
+
* SET and WHERE but different `select`s would share one cached statement, i.e.
|
|
827
|
+
* the second caller would get the first caller's columns. NUL is the delimiter
|
|
828
|
+
* because it is the one byte a PostgreSQL identifier cannot contain.
|
|
829
|
+
*/
|
|
830
|
+
export function writeProjectionCacheSegment(projection) {
|
|
831
|
+
return projection ? `|rt=${projection.columns.join('\u0000')}` : '';
|
|
832
|
+
}
|
|
805
833
|
/**
|
|
806
834
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
807
835
|
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
@@ -868,7 +896,9 @@ function namedColumns(meta, data) {
|
|
|
868
896
|
}
|
|
869
897
|
return out;
|
|
870
898
|
}
|
|
871
|
-
export function writeReturningColumns(qi) {
|
|
899
|
+
export function writeReturningColumns(qi, projection) {
|
|
900
|
+
if (projection)
|
|
901
|
+
return projection.columns.map((col) => qi.q(col));
|
|
872
902
|
// The PK exemption used to be re-stated here as `|| pk.has(col)`. It now
|
|
873
903
|
// lives in `piiColumns` so every projection inherits it and none can drift.
|
|
874
904
|
const piiCols = piiColumns(qi, qi.tableMeta);
|
|
@@ -881,8 +911,8 @@ export function writeReturningColumns(qi) {
|
|
|
881
911
|
* `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
|
|
882
912
|
* `'*'` when there is no PII column; otherwise the comma-joined quoted list.
|
|
883
913
|
*/
|
|
884
|
-
export function writeReselectSelection(qi) {
|
|
885
|
-
const cols = writeReturningColumns(qi);
|
|
914
|
+
export function writeReselectSelection(qi, projection) {
|
|
915
|
+
const cols = writeReturningColumns(qi, projection);
|
|
886
916
|
return cols === '*' ? '*' : cols.join(', ');
|
|
887
917
|
}
|
|
888
918
|
/**
|
|
@@ -893,10 +923,17 @@ export function writeReselectSelection(qi) {
|
|
|
893
923
|
* no-op. Untagged tables incur only one `for` over a zero-length field list,
|
|
894
924
|
* so behavior is unchanged.
|
|
895
925
|
*/
|
|
896
|
-
export function parseWriteRow(qi, row) {
|
|
926
|
+
export function parseWriteRow(qi, row, projection) {
|
|
897
927
|
const parsed = qi.parseRow(row, qi.table);
|
|
898
|
-
|
|
899
|
-
|
|
928
|
+
// A column the caller named in `select` is the opt-in (see WriteProjection),
|
|
929
|
+
// so the strip spares exactly the projected columns and nothing else.
|
|
930
|
+
const pii = piiColumns(qi, qi.tableMeta);
|
|
931
|
+
if (pii.size === 0)
|
|
932
|
+
return parsed;
|
|
933
|
+
const requested = projection ? new Set(projection.columns) : undefined;
|
|
934
|
+
for (const col of qi.tableMeta.columns) {
|
|
935
|
+
if (pii.has(col.name) && !requested?.has(col.name))
|
|
936
|
+
delete parsed[col.field];
|
|
900
937
|
}
|
|
901
938
|
return parsed;
|
|
902
939
|
}
|
package/dist/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>;
|