uql-orm 0.51.0 → 0.52.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 +1 -1
- package/dist/bunSql/bunSql.util.d.ts +33 -10
- package/dist/bunSql/bunSql.util.js +57 -42
- package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
- package/dist/bunSql/bunSqlQuerier.js +17 -8
- package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
- package/dist/bunSql/bunSqlQuerierPool.js +38 -25
- package/dist/bunSql/index.d.ts +1 -3
- package/dist/bunSql/index.js +0 -3
- package/dist/dialect/abstractSqlDialect.d.ts +58 -4
- package/dist/dialect/abstractSqlDialect.js +85 -24
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/mergeSqlDialect.d.ts +45 -0
- package/dist/dialect/mergeSqlDialect.js +89 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
- package/dist/dialect/pgLikeSqlDialect.js +4 -1
- package/dist/migrate/builder/expressions.js +5 -0
- package/dist/migrate/introspection/index.d.ts +2 -0
- package/dist/migrate/introspection/index.js +2 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
- package/dist/migrate/introspection/registry.d.ts +3 -0
- package/dist/migrate/introspection/registry.js +28 -0
- package/dist/migrate/migrator.js +2 -21
- package/dist/mongo/mongoDialect.js +4 -1
- package/dist/mssql/index.d.ts +3 -0
- package/dist/mssql/index.js +3 -0
- package/dist/mssql/mssqlDialect.d.ts +144 -0
- package/dist/mssql/mssqlDialect.js +328 -0
- package/dist/mssql/mssqlQuerier.d.ts +23 -0
- package/dist/mssql/mssqlQuerier.js +137 -0
- package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
- package/dist/mssql/mssqlQuerierPool.js +32 -0
- package/dist/mssql/mssqlWireTypes.d.ts +23 -0
- package/dist/mssql/mssqlWireTypes.js +44 -0
- package/dist/pglite/pgliteQuerier.d.ts +4 -2
- package/dist/pglite/pgliteQuerier.js +7 -2
- package/dist/postgres/pgCursorStream.d.ts +20 -0
- package/dist/postgres/pgCursorStream.js +49 -0
- package/dist/postgres/pgDialect.d.ts +1 -1
- package/dist/postgres/pgDialect.js +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
- package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
- package/dist/schema/canonicalType.js +96 -113
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +31 -4
- package/dist/type/migratorDialect.d.ts +1 -1
- package/dist/type/migratorDialect.js +1 -0
- package/package.json +13 -3
- package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
- package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
- package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
- package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
- package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
- package/dist/bunSql/bunSqliteDialect.js +0 -6
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { COUNT_ALIAS, JSON_ELEM_ALIAS_PREFIX } from '../dialect/aliases.js';
|
|
2
|
+
import { jsonPath } from '../dialect/jsonSql.js';
|
|
3
|
+
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
4
|
+
import { getMeta } from '../entity/index.js';
|
|
5
|
+
import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
|
|
6
|
+
import { QueryRaw } from '../type/index.js';
|
|
7
|
+
import { parseQueryLock } from '../type/index.js';
|
|
8
|
+
import { isAutoIncrement } from '../util/field.util.js';
|
|
9
|
+
import { assertNonNegativeInteger } from '../util/index.js';
|
|
10
|
+
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
11
|
+
/**
|
|
12
|
+
* Microsoft SQL Server 2017 and up - the floor `STRING_AGG` sets, every other construct here being
|
|
13
|
+
* 2016 or older.
|
|
14
|
+
*
|
|
15
|
+
* Identifiers are `"`-quoted rather than bracketed: `escapeIdChar` is one character that doubles to
|
|
16
|
+
* escape itself, `"` is the ANSI spelling, and `tedious` enables `QUOTED_IDENTIFIER` by default.
|
|
17
|
+
* Brackets would buy nothing and cost the shared dialect spec, which reads that one character.
|
|
18
|
+
*/
|
|
19
|
+
export class MsSqlDialect extends MergeSqlDialect {
|
|
20
|
+
featureDefaults = {
|
|
21
|
+
explicitJsonCast: false,
|
|
22
|
+
nativeArrays: false,
|
|
23
|
+
supportsJsonb: false,
|
|
24
|
+
// Neither object takes an `IF NOT EXISTS`; both need a `sys` catalogue lookup around them, which
|
|
25
|
+
// the generator does not emit.
|
|
26
|
+
ifNotExists: false,
|
|
27
|
+
indexIfNotExists: false,
|
|
28
|
+
schemas: true,
|
|
29
|
+
dropTableCascade: false,
|
|
30
|
+
// `sp_rename` is a stored procedure, not DDL, so a rename is refused by name rather than emitted
|
|
31
|
+
// as an `ALTER TABLE` the parser rejects.
|
|
32
|
+
renameColumn: false,
|
|
33
|
+
foreignKeyAlter: true,
|
|
34
|
+
primaryKeyAlter: true,
|
|
35
|
+
generatedColumnAdd: true,
|
|
36
|
+
// Extended properties are out-of-band metadata with their own procedures, not comments.
|
|
37
|
+
commentSyntax: 'none',
|
|
38
|
+
vectorIndexRequiresNotNull: false,
|
|
39
|
+
vectorSupportsLength: false,
|
|
40
|
+
supportsTimestamptz: false,
|
|
41
|
+
stringSizing: 'varchar',
|
|
42
|
+
supportsUnsigned: false,
|
|
43
|
+
// Error 1785: the constraint is refused at create time, so any diamond-shaped schema would fail
|
|
44
|
+
// to build at all rather than misbehave on write.
|
|
45
|
+
multipleCascadePaths: false,
|
|
46
|
+
serverSideCursors: false,
|
|
47
|
+
};
|
|
48
|
+
dialectName = 'mssql';
|
|
49
|
+
/** `OPENJSON`'s own output columns, which every JSON operator below reads through. */
|
|
50
|
+
#elem = {
|
|
51
|
+
value: this.escapeId('value'),
|
|
52
|
+
key: this.escapeId('key'),
|
|
53
|
+
type: this.escapeId('type'),
|
|
54
|
+
};
|
|
55
|
+
autoIncrementSuffix = 'IDENTITY(1,1)';
|
|
56
|
+
tableOptions = '';
|
|
57
|
+
beginTransactionCommand = 'BEGIN TRANSACTION';
|
|
58
|
+
commitTransactionCommand = 'COMMIT TRANSACTION';
|
|
59
|
+
rollbackTransactionCommand = 'ROLLBACK TRANSACTION';
|
|
60
|
+
/**
|
|
61
|
+
* The level rides the `BEGIN` rather than preceding it as its own statement: a `SET TRANSACTION
|
|
62
|
+
* ISOLATION LEVEL` sent on its own would go to whichever pooled connection served it, not the one
|
|
63
|
+
* the transaction opens on, and would then stick to that connection for unrelated later queries.
|
|
64
|
+
* `MsSqlQuerier` reads the level back off this command and hands it to the driver.
|
|
65
|
+
*/
|
|
66
|
+
isolationLevelStrategy = 'inline';
|
|
67
|
+
dropIndexSyntax = 'on-table';
|
|
68
|
+
booleanLiteral = 'integer';
|
|
69
|
+
/** [The hard server limit](https://github.com/yiisoft/yii2/issues/10371), not a driver preference. */
|
|
70
|
+
maxBindValues = 2100;
|
|
71
|
+
/** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
|
|
72
|
+
returningPosition = 'after-target';
|
|
73
|
+
insertIdSource = 'returning';
|
|
74
|
+
/** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
|
|
75
|
+
mergeTargetHint = ' WITH (HOLDLOCK)';
|
|
76
|
+
statementTerminator = ';';
|
|
77
|
+
/**
|
|
78
|
+
* `TOP (0)`, the only way this engine says "no rows": `FETCH NEXT 0 ROWS ONLY` is rejected
|
|
79
|
+
* outright ("the number of rows provided for a FETCH clause must be greater then zero").
|
|
80
|
+
*/
|
|
81
|
+
selectModifier(q) {
|
|
82
|
+
return q.$limit === 0 ? 'TOP (0) ' : '';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `TOP (0)` is the whole page when nothing is wanted: the engine refuses `TOP` alongside an
|
|
86
|
+
* `OFFSET`, and a skip into an empty result changes nothing, so no clause follows it.
|
|
87
|
+
*/
|
|
88
|
+
pager(ctx, opts, sorted = false) {
|
|
89
|
+
if (opts.$limit === 0) {
|
|
90
|
+
assertNonNegativeInteger(opts.$skip ?? 0, '$skip');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
super.pager(ctx, opts, sorted);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `SET IDENTITY_INSERT` around the insert, where the payload states a key the engine would
|
|
97
|
+
* otherwise generate: writing one is refused outright ("cannot insert explicit value for identity
|
|
98
|
+
* column ... when IDENTITY_INSERT is set to OFF") rather than ignored.
|
|
99
|
+
*
|
|
100
|
+
* Emitted only for that case, because the setting is per-session and only one table may hold it at
|
|
101
|
+
* a time, so it is turned back off in the same batch it was turned on.
|
|
102
|
+
*/
|
|
103
|
+
insert(ctx, entity, payload, opts) {
|
|
104
|
+
const table = this.identityInsertTarget(entity, payload);
|
|
105
|
+
if (table) {
|
|
106
|
+
ctx.append(`SET IDENTITY_INSERT ${table} ON; `);
|
|
107
|
+
}
|
|
108
|
+
super.insert(ctx, entity, payload, opts);
|
|
109
|
+
if (table) {
|
|
110
|
+
ctx.append(`; SET IDENTITY_INSERT ${table} OFF`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The table to toggle, or nothing when no record writes a key the engine would have generated. */
|
|
114
|
+
identityInsertTarget(entity, payload) {
|
|
115
|
+
const meta = getMeta(entity);
|
|
116
|
+
const [idKey] = meta.ids;
|
|
117
|
+
const field = meta.ids.length === 1 ? meta.fields[idKey] : undefined;
|
|
118
|
+
if (!field || !isAutoIncrement(field, true)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const records = Array.isArray(payload) ? payload : [payload];
|
|
122
|
+
const stated = records.some((record) => record[idKey] !== undefined);
|
|
123
|
+
return stated ? this.escapedTableName(meta) : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A `DECIMAL` read back as the exact text it was written as, where the entity declared the field a
|
|
127
|
+
* `String`. `tedious` decodes the type to a JS number before anything here can see it, so the
|
|
128
|
+
* digits past 2^53 are gone at the wire unless the column is converted before it crosses - the same
|
|
129
|
+
* reason MariaDB reads a vector column through `VEC_ToText`. 41 characters covers `DECIMAL(38, s)`
|
|
130
|
+
* with room for the sign and the point.
|
|
131
|
+
*/
|
|
132
|
+
selectFieldExpr(escapedColumn, field) {
|
|
133
|
+
const exactDecimal = field.type === String && fieldOptionsToCanonical(field).category === 'decimal';
|
|
134
|
+
return exactDecimal ? `CONVERT(NVARCHAR(41), ${escapedColumn})` : escapedColumn;
|
|
135
|
+
}
|
|
136
|
+
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
137
|
+
placeholder(index) {
|
|
138
|
+
return `@p${index}`;
|
|
139
|
+
}
|
|
140
|
+
/** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
|
|
141
|
+
returningId(meta) {
|
|
142
|
+
const expression = this.returningIdExpression(meta);
|
|
143
|
+
return expression ? `OUTPUT ${expression}` : '';
|
|
144
|
+
}
|
|
145
|
+
returningIdExpression(meta) {
|
|
146
|
+
const [idKey] = meta.ids;
|
|
147
|
+
return meta.ids.length === 1 ? `INSERTED.${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
|
|
148
|
+
}
|
|
149
|
+
mergeReturning(expression) {
|
|
150
|
+
return `OUTPUT ${expression}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A row lock is a hint on the table here, not a clause at the end of the statement, so
|
|
154
|
+
* {@link lockHint} emits it and this only keeps the guard - see the base declaration.
|
|
155
|
+
*/
|
|
156
|
+
appendLock(_ctx, entity, q) {
|
|
157
|
+
this.assertLockSupported(entity, q);
|
|
158
|
+
}
|
|
159
|
+
lockHint(q) {
|
|
160
|
+
const wait = parseQueryLock(q.$lock);
|
|
161
|
+
if (!wait) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
// `READPAST` skips locked rows and `NOWAIT` raises instead of waiting - what `SKIP LOCKED` and
|
|
165
|
+
// `NOWAIT` mean elsewhere. `ROWLOCK` asks the engine not to escalate to a page or the table.
|
|
166
|
+
const extra = wait === 'skip' ? ', READPAST' : wait === 'nowait' ? ', NOWAIT' : '';
|
|
167
|
+
return ` WITH (UPDLOCK, ROWLOCK${extra})`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `N'...'`, always. A bare literal is `VARCHAR`, whose codepage silently destroys anything outside
|
|
171
|
+
* it, and every string column this dialect creates is `NVARCHAR`. Bound parameters need nothing -
|
|
172
|
+
* `tedious` binds a JS string as `NVarChar` already - so this reaches only inlined literals.
|
|
173
|
+
*/
|
|
174
|
+
escape(value) {
|
|
175
|
+
if (typeof value === 'string') {
|
|
176
|
+
return `N'${escapeSingleQuotes(value)}'`;
|
|
177
|
+
}
|
|
178
|
+
if (value instanceof Uint8Array) {
|
|
179
|
+
return `0x${Array.from(value, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
180
|
+
}
|
|
181
|
+
return super.escape(value);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* `REGEXP_LIKE` is SQL Server 2025 at database compatibility level 170; a server below that rejects
|
|
185
|
+
* it itself, neither the version nor the compatibility level being knowable here - the same terms
|
|
186
|
+
* `uuidv7()` is emitted on. It is a predicate rather than a value, so it stands alone.
|
|
187
|
+
*/
|
|
188
|
+
regexCondition(operand, placeholder) {
|
|
189
|
+
return `REGEXP_LIKE(${operand}, ${placeholder})`;
|
|
190
|
+
}
|
|
191
|
+
/** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
|
|
192
|
+
createSchemaSql(schema) {
|
|
193
|
+
const literal = escapeSingleQuotes(schema);
|
|
194
|
+
const quoted = escapeSingleQuotes(this.escapeId(schema, true));
|
|
195
|
+
return `IF SCHEMA_ID(N'${literal}') IS NULL EXEC(N'CREATE SCHEMA ${quoted}')`;
|
|
196
|
+
}
|
|
197
|
+
/** The estimate the engine already keeps per partition, live without a stats refresh. */
|
|
198
|
+
estimatedCount(ctx, entity) {
|
|
199
|
+
const meta = getMeta(entity);
|
|
200
|
+
ctx.append(`SELECT SUM(p.rows) ${this.escapeId(COUNT_ALIAS, true)} FROM sys.partitions p` +
|
|
201
|
+
` JOIN sys.objects o ON o.object_id = p.object_id` +
|
|
202
|
+
` JOIN sys.schemas s ON s.schema_id = o.schema_id` +
|
|
203
|
+
` WHERE p.index_id IN (0, 1) AND o.name = `);
|
|
204
|
+
ctx.addValue(this.resolveTableAlias(meta));
|
|
205
|
+
ctx.append(' AND s.name = ');
|
|
206
|
+
ctx.addValue(this.resolveSchema(meta) ?? 'dbo');
|
|
207
|
+
}
|
|
208
|
+
numericCast(expr) {
|
|
209
|
+
return `TRY_CAST(${expr} AS FLOAT)`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* `JSON_VALUE` returns `NVARCHAR(4000)` and, in the lax mode that is the default, answers NULL
|
|
213
|
+
* rather than erroring for anything longer - so a long string read through it disappears without a
|
|
214
|
+
* word. `OPENJSON` has no such bound, so the path is split and its last segment matched as a key.
|
|
215
|
+
*/
|
|
216
|
+
getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
|
|
217
|
+
const dot = jsonPathStr.lastIndexOf('.');
|
|
218
|
+
const parent = dot === -1 ? '$' : `$.${jsonPathStr.slice(0, dot).split('.').map(escapeSingleQuotes).join('.')}`;
|
|
219
|
+
const leaf = escapeSingleQuotes(jsonPathStr.slice(dot + 1));
|
|
220
|
+
return `(SELECT ${this.#elem.value} FROM OPENJSON(${escapedColumn}, '${parent}') WHERE ${this.#elem.key} = N'${leaf}')`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
|
|
224
|
+
* an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
|
|
225
|
+
* `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
|
|
226
|
+
* text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
|
|
227
|
+
*/
|
|
228
|
+
getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
|
|
229
|
+
return this.getJsonPathScalarExpr(escapedColumn, jsonPathStr);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A value being *compared* against a JSON path, which reads back as the text `JSON_VALUE` yields:
|
|
233
|
+
* `'true'` for a boolean, `'12'` for a number. So only a boolean needs re-spelling; a number or a
|
|
234
|
+
* string already binds as the text it will be compared with.
|
|
235
|
+
*
|
|
236
|
+
* There is no "parse this text as JSON" cast to bind through the way `CAST(? AS JSON)` and
|
|
237
|
+
* `json(?)` serve the other families - `JSON_QUERY` marks text as JSON but answers NULL for a
|
|
238
|
+
* scalar - which is why reading and writing need the two different binders here.
|
|
239
|
+
*/
|
|
240
|
+
jsonScalarParam(ctx, value) {
|
|
241
|
+
return (this.#jsonCompound(ctx, value) ??
|
|
242
|
+
this.addValue(ctx.values, typeof value === 'boolean' ? JSON.stringify(value) : value));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* A value being *written* to a JSON path, which takes the type the driver sent: a `BIT` becomes a
|
|
246
|
+
* JSON boolean, a number a JSON number, a string a JSON string. `normalizeValue` has already
|
|
247
|
+
* flattened a boolean to 1/0 for this engine's columns, so the cast is what restores it.
|
|
248
|
+
*/
|
|
249
|
+
jsonWriteParam(ctx, value) {
|
|
250
|
+
const compound = this.#jsonCompound(ctx, value);
|
|
251
|
+
if (compound) {
|
|
252
|
+
return compound;
|
|
253
|
+
}
|
|
254
|
+
const placeholder = this.addValue(ctx.values, value);
|
|
255
|
+
return typeof value === 'boolean' ? `CAST(${placeholder} AS BIT)` : placeholder;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* An object or array bound as JSON, which is the half both binders spell the same way, or
|
|
259
|
+
* `undefined` for a scalar - where they diverge.
|
|
260
|
+
*/
|
|
261
|
+
#jsonCompound(ctx, value) {
|
|
262
|
+
if (value === null || typeof value !== 'object' || value instanceof QueryRaw) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
ctx.pushValue(JSON.stringify(value));
|
|
266
|
+
return `JSON_QUERY(${this.placeholder(ctx.values.length)})`;
|
|
267
|
+
}
|
|
268
|
+
/** An exploded element compares as text here, so `$elemMatch` always expands per field. */
|
|
269
|
+
jsonContainmentIsPartial = false;
|
|
270
|
+
jsonElemFrom(jsonField, _fields, alias) {
|
|
271
|
+
return `OPENJSON(${jsonField}) ${alias}`;
|
|
272
|
+
}
|
|
273
|
+
/** `JSON_VALUE`'s 4000-character bound applies to an element's field, unlike a whole column. */
|
|
274
|
+
jsonElemRef(alias, field) {
|
|
275
|
+
return field === undefined
|
|
276
|
+
? `${alias}.${this.#elem.value}`
|
|
277
|
+
: `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
|
|
278
|
+
}
|
|
279
|
+
jsonAll(ctx, jsonField, value) {
|
|
280
|
+
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
281
|
+
const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
|
|
282
|
+
return `(${conditions.join(' AND ')})`;
|
|
283
|
+
}
|
|
284
|
+
jsonSize(ctx, jsonField, value) {
|
|
285
|
+
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
286
|
+
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
|
|
287
|
+
}
|
|
288
|
+
/** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
|
|
289
|
+
jsonSet(ctx, expr, set, _field) {
|
|
290
|
+
for (const [key, value] of Object.entries(set)) {
|
|
291
|
+
if (value === null) {
|
|
292
|
+
throw new TypeError(`mssql cannot $set '${key}' to null: JSON_MODIFY deletes the key instead. Use $unset, or store a JSON null through a whole-column write.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return Object.entries(set).reduce((acc, [key, value]) => `JSON_MODIFY(${acc}, ${jsonPath(key)}, ${this.jsonWriteParam(ctx, value)})`, `COALESCE(${expr}, '{}')`);
|
|
296
|
+
}
|
|
297
|
+
/** `'append '` prefixing the path extends the array there, creating it where there is none. */
|
|
298
|
+
jsonPush(ctx, expr, push) {
|
|
299
|
+
return Object.entries(push).reduce((acc, [key, value]) => `JSON_MODIFY(${acc}, 'append ${jsonPath(key).slice(1, -1)}', ${this.jsonWriteParam(ctx, value)})`, `COALESCE(${expr}, '{}')`);
|
|
300
|
+
}
|
|
301
|
+
/** Assigning NULL to a path deletes it in lax mode, which is the default. */
|
|
302
|
+
jsonUnset(_ctx, expr, unset) {
|
|
303
|
+
return unset.reduce((acc, key) => `JSON_MODIFY(${acc}, ${jsonPath(key)}, NULL)`, expr);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The surviving elements are re-aggregated into an array and written back whole - there is no
|
|
307
|
+
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
|
|
308
|
+
*/
|
|
309
|
+
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
310
|
+
const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
|
|
311
|
+
const val = `${alias}.${this.#elem.value}`;
|
|
312
|
+
// `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
|
|
313
|
+
// re-encoded from its reported `type` before the array is put back together - concatenated raw,
|
|
314
|
+
// the result is text the engine then refuses to parse as JSON.
|
|
315
|
+
const encoded = `CASE ${alias}.${this.#elem.type}` +
|
|
316
|
+
` WHEN 0 THEN 'null'` +
|
|
317
|
+
` WHEN 1 THEN '"' + STRING_ESCAPE(${val}, 'json') + '"'` +
|
|
318
|
+
` ELSE ${val} END`;
|
|
319
|
+
// `IS NULL OR` because a JSON null element reads back as SQL NULL, and `<>` against one is
|
|
320
|
+
// unknown rather than true - which silently dropped every null from the array it rebuilt.
|
|
321
|
+
const kept = `SELECT '[' + STRING_AGG(${encoded}, ',') + ']' FROM OPENJSON(${escapedCol}, ${jsonPath(key)}) ${alias}` +
|
|
322
|
+
` WHERE ${val} IS NULL OR ${val} <> ${this.jsonScalarParam(ctx, value)}`;
|
|
323
|
+
// `JSON_MODIFY` creates a path it does not find, so a `$pull` against an absent key would add an
|
|
324
|
+
// empty array where the other engines leave the document alone.
|
|
325
|
+
const path = jsonPath(key);
|
|
326
|
+
return `CASE WHEN JSON_QUERY(${escapedCol}, ${path}) IS NULL THEN ${expr} ELSE JSON_MODIFY(${expr}, ${path}, JSON_QUERY(COALESCE((${kept}), '[]'))) END`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConnectionPool } from 'mssql';
|
|
2
|
+
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
3
|
+
import type { ExtraOptions, QueryUpdateResult } from '../type/index.js';
|
|
4
|
+
import type { MsSqlDialect } from './mssqlDialect.js';
|
|
5
|
+
/**
|
|
6
|
+
* A connection is a `ConnectionPool` handle here rather than a checked-out socket: `mssql` owns its
|
|
7
|
+
* own pool and hands out `Request`s, so what UQL holds is the pool plus, once a transaction opens,
|
|
8
|
+
* the `Transaction` every later request has to be bound to.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MsSqlQuerier extends AbstractPoolQuerier<ConnectionPool> {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(connect: () => Promise<ConnectionPool>, dialect: MsSqlDialect, extra?: ExtraOptions);
|
|
13
|
+
internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
14
|
+
internalRun(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
|
|
15
|
+
/**
|
|
16
|
+
* `tedious` streams by event, not by async iterator, so rows are handed over as they arrive rather
|
|
17
|
+
* than collected first - buffering the whole result set would make this `all()` with extra steps.
|
|
18
|
+
* The `query` promise is awaited at the end so its rejection surfaces rather than going unhandled.
|
|
19
|
+
*/
|
|
20
|
+
internalStream<T>(query: string, values?: unknown[]): AsyncGenerator<Awaited<T>, void, unknown>;
|
|
21
|
+
/** The pool owns the socket; releasing a querier only drops this one's claim on it. */
|
|
22
|
+
protected releaseConn(_conn: ConnectionPool, _discard: boolean): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ISOLATION_LEVEL } from 'mssql';
|
|
2
|
+
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
3
|
+
import { decodeWireTypes } from './mssqlWireTypes.js';
|
|
4
|
+
/**
|
|
5
|
+
* A connection is a `ConnectionPool` handle here rather than a checked-out socket: `mssql` owns its
|
|
6
|
+
* own pool and hands out `Request`s, so what UQL holds is the pool plus, once a transaction opens,
|
|
7
|
+
* the `Transaction` every later request has to be bound to.
|
|
8
|
+
*/
|
|
9
|
+
export class MsSqlQuerier extends AbstractPoolQuerier {
|
|
10
|
+
#transaction;
|
|
11
|
+
constructor(connect, dialect, extra) {
|
|
12
|
+
super(dialect, connect, extra);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `tedious` infers
|
|
16
|
+
* a type from the JS value, which is why a `Date` and a `Uint8Array` reach it unconverted - the
|
|
17
|
+
* inference is right for both, and wrong only for a bare `null`, which it calls `NVarChar`.
|
|
18
|
+
*/
|
|
19
|
+
#request(values) {
|
|
20
|
+
const request = this.#transaction ? this.#transaction.request() : this.getConn().request();
|
|
21
|
+
values?.forEach((value, index) => request.input(`p${index + 1}`, value));
|
|
22
|
+
return request;
|
|
23
|
+
}
|
|
24
|
+
async internalAll(query, values) {
|
|
25
|
+
const res = (await this.#request(values).query(query));
|
|
26
|
+
return decodeWireTypes(res.recordset, res.recordset?.columns);
|
|
27
|
+
}
|
|
28
|
+
async internalRun(query, values) {
|
|
29
|
+
if (await this.#driveTransaction(query)) {
|
|
30
|
+
return { changes: 0 };
|
|
31
|
+
}
|
|
32
|
+
const res = (await this.#request(values).query(query));
|
|
33
|
+
return this.buildUpdateResult({
|
|
34
|
+
// `rowsAffected` carries one entry per statement, and a `MERGE` upsert emits its `OUTPUT`
|
|
35
|
+
// alongside the write, so the counts are summed rather than read at [0].
|
|
36
|
+
changes: res.rowsAffected.reduce((total, count) => total + count, 0),
|
|
37
|
+
rows: decodeWireTypes(res.recordset, res.recordset?.columns),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `tedious` streams by event, not by async iterator, so rows are handed over as they arrive rather
|
|
42
|
+
* than collected first - buffering the whole result set would make this `all()` with extra steps.
|
|
43
|
+
* The `query` promise is awaited at the end so its rejection surfaces rather than going unhandled.
|
|
44
|
+
*/
|
|
45
|
+
async *internalStream(query, values) {
|
|
46
|
+
const request = this.#request(values);
|
|
47
|
+
request.stream = true;
|
|
48
|
+
let pending = [];
|
|
49
|
+
let done = false;
|
|
50
|
+
let failure;
|
|
51
|
+
let wake;
|
|
52
|
+
const arrived = () => {
|
|
53
|
+
wake?.();
|
|
54
|
+
wake = undefined;
|
|
55
|
+
};
|
|
56
|
+
request.on('row', (row) => {
|
|
57
|
+
pending.push(row);
|
|
58
|
+
arrived();
|
|
59
|
+
});
|
|
60
|
+
request.on('error', (err) => {
|
|
61
|
+
failure ??= err;
|
|
62
|
+
arrived();
|
|
63
|
+
});
|
|
64
|
+
request.on('done', () => {
|
|
65
|
+
done = true;
|
|
66
|
+
arrived();
|
|
67
|
+
});
|
|
68
|
+
const completed = request.query(query).catch((err) => {
|
|
69
|
+
failure ??= err instanceof Error ? err : new Error(String(err));
|
|
70
|
+
arrived();
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
if (pending.length) {
|
|
75
|
+
const batch = pending;
|
|
76
|
+
pending = [];
|
|
77
|
+
yield* batch;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (failure)
|
|
81
|
+
throw failure;
|
|
82
|
+
if (done)
|
|
83
|
+
return;
|
|
84
|
+
await new Promise((resolve) => {
|
|
85
|
+
wake = resolve;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
// `completed` is awaited rather than left floating so a late rejection is handled; its own
|
|
91
|
+
// `catch` has already recorded it, and the loop above is what raises one to the caller.
|
|
92
|
+
request.cancel();
|
|
93
|
+
await completed;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `mssql` owns its pool and hands out a `Request` per call, so a `BEGIN TRANSACTION` sent as text
|
|
98
|
+
* would open one on a connection the next call may not be given. Its `Transaction` object is the
|
|
99
|
+
* only thing that pins them together, so the three commands the dialect names are driven through
|
|
100
|
+
* it here instead of being sent. Compared against the dialect's own strings rather than literals,
|
|
101
|
+
* so renaming one cannot silently turn it back into text.
|
|
102
|
+
*/
|
|
103
|
+
async #driveTransaction(query) {
|
|
104
|
+
const { beginTransactionCommand, commitTransactionCommand, rollbackTransactionCommand } = this.dialect;
|
|
105
|
+
if (query.startsWith(beginTransactionCommand)) {
|
|
106
|
+
this.#transaction = this.getConn().transaction();
|
|
107
|
+
await this.#transaction.begin(isolationLevelOf(query.slice(beginTransactionCommand.length)));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (query !== commitTransactionCommand && query !== rollbackTransactionCommand) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const transaction = this.#transaction;
|
|
114
|
+
this.#transaction = undefined;
|
|
115
|
+
await (query === commitTransactionCommand ? transaction?.commit() : transaction?.rollback());
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/** The pool owns the socket; releasing a querier only drops this one's claim on it. */
|
|
119
|
+
async releaseConn(_conn, _discard) {
|
|
120
|
+
const transaction = this.#transaction;
|
|
121
|
+
this.#transaction = undefined;
|
|
122
|
+
// A querier handed back mid-transaction would otherwise leave it open on a pooled connection.
|
|
123
|
+
await transaction?.rollback().catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The driver constant for the level the dialect spelled into its `BEGIN`, so it applies to the
|
|
128
|
+
* connection the transaction actually opens on. Undefined leaves the server's own default.
|
|
129
|
+
*/
|
|
130
|
+
function isolationLevelOf(suffix) {
|
|
131
|
+
const level = suffix
|
|
132
|
+
.replace(/^\s*ISOLATION LEVEL\s*/i, '')
|
|
133
|
+
.trim()
|
|
134
|
+
.toUpperCase()
|
|
135
|
+
.replaceAll(' ', '_');
|
|
136
|
+
return level ? ISOLATION_LEVEL[level] : undefined;
|
|
137
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConnectionPool, type config as MsSqlConfig } from 'mssql';
|
|
2
|
+
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
3
|
+
import type { ExtraOptions } from '../type/index.js';
|
|
4
|
+
import { MsSqlDialect } from './mssqlDialect.js';
|
|
5
|
+
import { MsSqlQuerier } from './mssqlQuerier.js';
|
|
6
|
+
export declare class MsSqlQuerierPool extends AbstractSqlQuerierPool<MsSqlQuerier, MsSqlDialect> {
|
|
7
|
+
#private;
|
|
8
|
+
readonly pool: ConnectionPool;
|
|
9
|
+
constructor(opts: MsSqlConfig, extra?: ExtraOptions);
|
|
10
|
+
/**
|
|
11
|
+
* `mssql` connects the pool as a whole rather than per checkout, so the promise is shared. A
|
|
12
|
+
* failed one is dropped rather than kept: memoized, a single transient failure would be handed to
|
|
13
|
+
* every later caller for the life of the pool.
|
|
14
|
+
*/
|
|
15
|
+
getQuerier(): Promise<MsSqlQuerier>;
|
|
16
|
+
end(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ConnectionPool } from 'mssql';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
3
|
+
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
4
|
+
import { MsSqlDialect } from './mssqlDialect.js';
|
|
5
|
+
import { MsSqlQuerier } from './mssqlQuerier.js';
|
|
6
|
+
export class MsSqlQuerierPool extends AbstractSqlQuerierPool {
|
|
7
|
+
pool;
|
|
8
|
+
#connected;
|
|
9
|
+
constructor(opts, extra) {
|
|
10
|
+
super(new MsSqlDialect(dialectOptionsFrom(extra)), extra);
|
|
11
|
+
this.pool = new ConnectionPool(opts);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `mssql` connects the pool as a whole rather than per checkout, so the promise is shared. A
|
|
15
|
+
* failed one is dropped rather than kept: memoized, a single transient failure would be handed to
|
|
16
|
+
* every later caller for the life of the pool.
|
|
17
|
+
*/
|
|
18
|
+
async getQuerier() {
|
|
19
|
+
return new MsSqlQuerier(() => this.#connect(), this.dialect, this.extra);
|
|
20
|
+
}
|
|
21
|
+
#connect() {
|
|
22
|
+
this.#connected ??= this.pool.connect().catch((err) => {
|
|
23
|
+
this.#connected = undefined;
|
|
24
|
+
throw err;
|
|
25
|
+
});
|
|
26
|
+
return this.#connected;
|
|
27
|
+
}
|
|
28
|
+
async end() {
|
|
29
|
+
this.#connected = undefined;
|
|
30
|
+
await this.pool.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The column metadata `tedious` reports beside a recordset, narrowed to the one field read here. */
|
|
2
|
+
type ColumnTypes = Record<string, {
|
|
3
|
+
readonly type: unknown;
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Decode `BIGINT` as a JS number, leaving every other type to the driver.
|
|
7
|
+
*
|
|
8
|
+
* uql owes this to the caller because uql picks the column: `type: Number` maps to BIGINT (see
|
|
9
|
+
* `schema/canonicalType.ts`), and `tedious` hands that back as a string to protect the digits past
|
|
10
|
+
* 2^53 - so without this a field declared `number` reads back as `'9'`, including every generated
|
|
11
|
+
* primary key on every entity.
|
|
12
|
+
*
|
|
13
|
+
* At the wire, for the reason `pgNumericTypes` gives: everything crosses it exactly once - entity
|
|
14
|
+
* reads, the ids an `OUTPUT` reports, raw SQL, counts, aggregates - where the ORM's own hydration
|
|
15
|
+
* only ever sees entity reads.
|
|
16
|
+
*
|
|
17
|
+
* Exact to 2^53, which covers any generated id. A value past it keeps the string the driver gave,
|
|
18
|
+
* because a number could no longer represent it: that is the one case where handing back the exact
|
|
19
|
+
* text is more useful than handing back the type that was asked for. The lighter escape hatch for a
|
|
20
|
+
* column that big is the declaration - `@Field({ type: String, columnType: 'bigint' })`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function decodeWireTypes<T>(rows: T[] | undefined, columns: ColumnTypes | undefined): T[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode `BIGINT` as a JS number, leaving every other type to the driver.
|
|
3
|
+
*
|
|
4
|
+
* uql owes this to the caller because uql picks the column: `type: Number` maps to BIGINT (see
|
|
5
|
+
* `schema/canonicalType.ts`), and `tedious` hands that back as a string to protect the digits past
|
|
6
|
+
* 2^53 - so without this a field declared `number` reads back as `'9'`, including every generated
|
|
7
|
+
* primary key on every entity.
|
|
8
|
+
*
|
|
9
|
+
* At the wire, for the reason `pgNumericTypes` gives: everything crosses it exactly once - entity
|
|
10
|
+
* reads, the ids an `OUTPUT` reports, raw SQL, counts, aggregates - where the ORM's own hydration
|
|
11
|
+
* only ever sees entity reads.
|
|
12
|
+
*
|
|
13
|
+
* Exact to 2^53, which covers any generated id. A value past it keeps the string the driver gave,
|
|
14
|
+
* because a number could no longer represent it: that is the one case where handing back the exact
|
|
15
|
+
* text is more useful than handing back the type that was asked for. The lighter escape hatch for a
|
|
16
|
+
* column that big is the declaration - `@Field({ type: String, columnType: 'bigint' })`.
|
|
17
|
+
*/
|
|
18
|
+
export function decodeWireTypes(rows, columns) {
|
|
19
|
+
if (!rows?.length || !columns) {
|
|
20
|
+
return rows ?? [];
|
|
21
|
+
}
|
|
22
|
+
const wide = Object.keys(columns).filter((name) => typeName(columns[name]?.type) === 'BigInt');
|
|
23
|
+
if (!wide.length) {
|
|
24
|
+
// The overwhelmingly common case, so an ordinary read copies nothing.
|
|
25
|
+
return rows;
|
|
26
|
+
}
|
|
27
|
+
return rows.map((row) => {
|
|
28
|
+
const decoded = { ...row };
|
|
29
|
+
for (const name of wide) {
|
|
30
|
+
const value = decoded[name];
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
const asNumber = Number(value);
|
|
33
|
+
if (Number.isSafeInteger(asNumber)) {
|
|
34
|
+
decoded[name] = asNumber;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return decoded;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** `tedious` reports a column's type as its factory function, whose `name` is the type's own. */
|
|
42
|
+
function typeName(type) {
|
|
43
|
+
return typeof type === 'function' ? type.name : undefined;
|
|
44
|
+
}
|
|
@@ -21,8 +21,8 @@ export type PgliteDatabase = {
|
|
|
21
21
|
* Querier for PGlite, Postgres compiled to WASM and run in this process.
|
|
22
22
|
*
|
|
23
23
|
* @remarks Extends {@link AbstractSqlQuerier} rather than `AbstractPgQuerier`, whose `internalStream`
|
|
24
|
-
* hands a `pg-query-stream` object to `query()
|
|
25
|
-
*
|
|
24
|
+
* hands a `pg-query-stream` object to `query()`, which PGlite's client has no equivalent of - so
|
|
25
|
+
* streaming pages the rows in SQL instead. `BEGIN`/`COMMIT` are plain statements on the single
|
|
26
26
|
* connection, leaving transactions to the base class.
|
|
27
27
|
*/
|
|
28
28
|
export declare class PgliteQuerier extends AbstractSqlQuerier {
|
|
@@ -31,6 +31,8 @@ export declare class PgliteQuerier extends AbstractSqlQuerier {
|
|
|
31
31
|
constructor(db: PgliteDatabase, dialect: PgliteDialect, extra?: ExtraOptions | undefined);
|
|
32
32
|
internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
33
33
|
internalRun(query: string, values?: unknown[]): Promise<import("../type/query.js").QueryUpdateResult>;
|
|
34
|
+
/** Postgres compiled to WASM is still Postgres: `DECLARE`/`FETCH` streams what the client cannot. */
|
|
35
|
+
protected internalStream<T>(query: string, values?: unknown[]): AsyncGenerator<Awaited<T>, void, any>;
|
|
34
36
|
/** The handle belongs to the pool, which hands out one querier per unit of work over it. */
|
|
35
37
|
internalRelease(): Promise<void>;
|
|
36
38
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { streamViaCursor } from '../postgres/pgCursorStream.js';
|
|
1
2
|
import { AbstractSqlQuerier } from '../querier/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* Querier for PGlite, Postgres compiled to WASM and run in this process.
|
|
4
5
|
*
|
|
5
6
|
* @remarks Extends {@link AbstractSqlQuerier} rather than `AbstractPgQuerier`, whose `internalStream`
|
|
6
|
-
* hands a `pg-query-stream` object to `query()
|
|
7
|
-
*
|
|
7
|
+
* hands a `pg-query-stream` object to `query()`, which PGlite's client has no equivalent of - so
|
|
8
|
+
* streaming pages the rows in SQL instead. `BEGIN`/`COMMIT` are plain statements on the single
|
|
8
9
|
* connection, leaving transactions to the base class.
|
|
9
10
|
*/
|
|
10
11
|
export class PgliteQuerier extends AbstractSqlQuerier {
|
|
@@ -25,6 +26,10 @@ export class PgliteQuerier extends AbstractSqlQuerier {
|
|
|
25
26
|
// where the latter also counts a `SELECT`'s rows and is absent altogether from a DDL tag.
|
|
26
27
|
return this.buildUpdateResult({ rows: res.rows, changes: res.affectedRows ?? 0 });
|
|
27
28
|
}
|
|
29
|
+
/** Postgres compiled to WASM is still Postgres: `DECLARE`/`FETCH` streams what the client cannot. */
|
|
30
|
+
async *internalStream(query, values) {
|
|
31
|
+
yield* streamViaCursor((sql, params) => this.internalAll(sql, params), query, values, this.hasOpenTransaction);
|
|
32
|
+
}
|
|
28
33
|
/** The handle belongs to the pool, which hands out one querier per unit of work over it. */
|
|
29
34
|
async internalRelease() { }
|
|
30
35
|
}
|