turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/mssql.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* the row selection).
|
|
53
53
|
* - **Result strategy `'output'`:** create/update/delete/upsert return their rows
|
|
54
54
|
* from the same statement. `createMany` returns the inserted rows via
|
|
55
|
-
* `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows /
|
|
55
|
+
* `OUTPUT INSERTED.*` on the multi-row VALUES insert (≤ 1000 rows / 2098 params
|
|
56
56
|
* per statement, exceeding either throws a clear `ValidationError`; chunk
|
|
57
57
|
* yourself or use single `create`s).
|
|
58
58
|
* - **MERGE concurrency caveat:** `MERGE` is the upsert primitive; under high
|
|
@@ -89,20 +89,38 @@
|
|
|
89
89
|
* await db.disconnect();
|
|
90
90
|
* ```
|
|
91
91
|
*/
|
|
92
|
-
import { TurbineClient } from './client.js';
|
|
92
|
+
import { TurbineClient, } from './client.js';
|
|
93
93
|
import { postgresDialect, } from './dialect.js';
|
|
94
94
|
import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, RelationError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
95
|
-
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
95
|
+
import { applyTableFilters, assertDistinctColumnFields, deriveEngineRelations, nullRecord } from './introspect.js';
|
|
96
96
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
97
|
+
import { orderByEntries } from './query/filters.js';
|
|
97
98
|
import { availableClause } from './query/utils.js';
|
|
98
99
|
import { camelToSnake, isDateType, normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
100
|
+
import { rewritePositionalPlaceholders } from './sql-placeholders.js';
|
|
99
101
|
// ---------------------------------------------------------------------------
|
|
100
102
|
// SQL Server / connection limits
|
|
101
103
|
// ---------------------------------------------------------------------------
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Max bound parameters per SQL Server statement, as the driver sends them.
|
|
106
|
+
* The server's limit is 2100 per RPC call, and `mssql` runs a parameterized
|
|
107
|
+
* statement through `sp_executesql`, whose own `@stmt` and `@params` take two
|
|
108
|
+
* of them: measured, 2098 runs and 2099 is refused with error 8003.
|
|
109
|
+
*/
|
|
110
|
+
const MSSQL_MAX_PARAMS = 2098;
|
|
104
111
|
/** Max rows per multi-row INSERT … VALUES statement. */
|
|
105
112
|
const MSSQL_MAX_INSERT_ROWS = 1000;
|
|
113
|
+
/** Integer column types narrower than 64 bits, in SQL Server and code-first (PostgreSQL) spellings. */
|
|
114
|
+
const SMALL_INTEGER_TYPES = new Set([
|
|
115
|
+
'tinyint',
|
|
116
|
+
'smallint',
|
|
117
|
+
'int',
|
|
118
|
+
'integer',
|
|
119
|
+
'int2',
|
|
120
|
+
'int4',
|
|
121
|
+
'serial',
|
|
122
|
+
'smallserial',
|
|
123
|
+
]);
|
|
106
124
|
/** Canonical key sort matching the query builder's `sortedEntries`/collect order. */
|
|
107
125
|
function sortedRelEntries(obj) {
|
|
108
126
|
return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
@@ -114,8 +132,9 @@ function sortedRelEntries(obj) {
|
|
|
114
132
|
* Coerce an arbitrary JS value into something `mssql` can bind. `BIT` accepts JS
|
|
115
133
|
* booleans directly; `undefined`/`null` → NULL; `bigint` follows the safe-int
|
|
116
134
|
* policy (number when it fits in 2^53, else a decimal string, no precision
|
|
117
|
-
* loss); `Date
|
|
118
|
-
*
|
|
135
|
+
* loss); a `Date` passes through, a `Uint8Array` binds as a Buffer view (VARBINARY);
|
|
136
|
+
* any remaining object/array → JSON text
|
|
137
|
+
* (matches how Turbine already pre-serializes JSON filter params).
|
|
119
138
|
*/
|
|
120
139
|
function toMssqlParam(value) {
|
|
121
140
|
if (value === undefined || value === null)
|
|
@@ -132,10 +151,45 @@ function toMssqlParam(value) {
|
|
|
132
151
|
}
|
|
133
152
|
if (value instanceof Date)
|
|
134
153
|
return value;
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
// The driver infers VARBINARY only for a Buffer; any other Uint8Array was
|
|
155
|
+
// bound as NVARCHAR and refused ("Invalid string"). A Buffer view shares the
|
|
156
|
+
// bytes, so nothing is copied.
|
|
157
|
+
if (value instanceof Uint8Array) {
|
|
158
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
159
|
+
}
|
|
137
160
|
return JSON.stringify(value);
|
|
138
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The JSON text an `in` / `notIn` list binds as. OPENJSON hands each element
|
|
164
|
+
* back as NVARCHAR text, and SQL Server converts that text to the compared
|
|
165
|
+
* column's type, so each element is written as the text that converts to the
|
|
166
|
+
* value `equals` would bind: a bigint as its digits (`JSON.stringify` refuses
|
|
167
|
+
* one) and a Date as its UTC date and time with no zone (the driver binds a
|
|
168
|
+
* Date as DATETIME2 in UTC; `JSON.stringify` adds a `Z`). No text converts to
|
|
169
|
+
* bytes, so a binary element is refused with the spelling that works.
|
|
170
|
+
*/
|
|
171
|
+
function mssqlInListJson(values) {
|
|
172
|
+
const parts = values.map((value) => {
|
|
173
|
+
if (typeof value === 'bigint')
|
|
174
|
+
return value.toString();
|
|
175
|
+
if (value instanceof Date) {
|
|
176
|
+
if (Number.isNaN(value.getTime())) {
|
|
177
|
+
throw new ValidationError('SQL Server cannot bind an Invalid Date in an `in` list. Pass a valid Date, or null.');
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify(value.toISOString().slice(0, 23));
|
|
180
|
+
}
|
|
181
|
+
if (value instanceof Uint8Array) {
|
|
182
|
+
throw new UnsupportedFeatureError('a binary value in an `in` / `notIn` list', 'mssql', 'SQL Server receives the list as JSON text, which cannot carry bytes. Write the list as branches: ' +
|
|
183
|
+
'`{ OR: [{ field: value1 }, { field: value2 }] }`, or use the default `join` relation strategy for ' +
|
|
184
|
+
'a relation keyed on a binary column.');
|
|
185
|
+
}
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
return 'null';
|
|
188
|
+
const bound = toMssqlParam(value);
|
|
189
|
+
return JSON.stringify(bound);
|
|
190
|
+
});
|
|
191
|
+
return `[${parts.join(',')}]`;
|
|
192
|
+
}
|
|
139
193
|
// ---------------------------------------------------------------------------
|
|
140
194
|
// Result shaping + error translation
|
|
141
195
|
// ---------------------------------------------------------------------------
|
|
@@ -184,6 +238,17 @@ function coerceBigIntColumns(rows, columns) {
|
|
|
184
238
|
}
|
|
185
239
|
}
|
|
186
240
|
const EMPTY_RESULT = { rows: [], rowCount: 0 };
|
|
241
|
+
/** A JSON column's stored text as its value; text that is not JSON is returned as stored. */
|
|
242
|
+
function parseJsonText(value) {
|
|
243
|
+
if (typeof value !== 'string')
|
|
244
|
+
return value;
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(value);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
187
252
|
/**
|
|
188
253
|
* Augment a `mssql` driver error with the Postgres-shaped `.code` (SQLSTATE) and
|
|
189
254
|
* detail fields that `wrapPgError` understands, so SQL Server constraint failures
|
|
@@ -250,13 +315,48 @@ function normalizeQueryArgs(arg, values) {
|
|
|
250
315
|
return { text: arg, params: values ?? [] };
|
|
251
316
|
return { text: arg.text, params: arg.values ?? values ?? [] };
|
|
252
317
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Bind one parameter. Two value kinds get an explicit SQL type, because the
|
|
320
|
+
* driver's inference picks the wrong one for them:
|
|
321
|
+
*
|
|
322
|
+
* - A JS `bigint` is declared BIGINT. Inferred, a small one became INT and one
|
|
323
|
+
* past 2^53 an NVARCHAR string, and a multi-row `VALUES` list types each
|
|
324
|
+
* column by the highest-precedence type across its rows, so a `createMany`
|
|
325
|
+
* mixing the two converted the string to INT and failed ("overflowed an int
|
|
326
|
+
* column").
|
|
327
|
+
* - A `Date` is declared DATETIME2(7). Inferred, it is the legacy DATETIME,
|
|
328
|
+
* whose 1/300 s ticks round milliseconds on the way in, so 23:59:59.999 was
|
|
329
|
+
* stored as the next day's 00:00:00.000. DATETIME2 holds the exact instant
|
|
330
|
+
* and converts to every temporal column type.
|
|
331
|
+
*/
|
|
332
|
+
function bindParam(request, name, value, types) {
|
|
333
|
+
if (typeof value === 'bigint' && types?.BigInt !== undefined) {
|
|
334
|
+
request.input(name, types.BigInt, value);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (value instanceof Date && !Number.isNaN(value.getTime()) && typeof types?.DateTime2 === 'function') {
|
|
338
|
+
request.input(name, types.DateTime2(7), value);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
request.input(name, toMssqlParam(value));
|
|
342
|
+
}
|
|
343
|
+
/** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
|
|
344
|
+
function refusePlaceholder(message) {
|
|
345
|
+
throw new ValidationError(message);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Bind positional `params[]` to `@p1`, `@p2`, … inputs and run the statement.
|
|
349
|
+
* A `$n` (the spelling `rawUnsafe` documents for every engine) is rewritten to
|
|
350
|
+
* `@pN` first: T-SQL reads `$1` as a MONEY literal, so it would run, compare
|
|
351
|
+
* against the number 1, and bind nothing.
|
|
352
|
+
*/
|
|
353
|
+
async function runRequest(request, text, params, types) {
|
|
354
|
+
const sql = rewritePositionalPlaceholders(text, 'mssql', params.length, (i) => `@p${i}`, refusePlaceholder);
|
|
255
355
|
for (let i = 0; i < params.length; i++) {
|
|
256
|
-
request
|
|
356
|
+
bindParam(request, `p${i + 1}`, params[i], types);
|
|
257
357
|
}
|
|
258
358
|
try {
|
|
259
|
-
const result = await request.query(
|
|
359
|
+
const result = await request.query(sql);
|
|
260
360
|
return shapeResult(result);
|
|
261
361
|
}
|
|
262
362
|
catch (err) {
|
|
@@ -342,7 +442,7 @@ class MssqlTxClient {
|
|
|
342
442
|
return EMPTY_RESULT;
|
|
343
443
|
}
|
|
344
444
|
const request = new this.sqlNS.Request(this.tx ?? this.pool);
|
|
345
|
-
return runRequest(request, rawSql, params);
|
|
445
|
+
return runRequest(request, rawSql, params, this.sqlNS);
|
|
346
446
|
}
|
|
347
447
|
release() {
|
|
348
448
|
// The transaction's connection is owned by the mssql Transaction object and is
|
|
@@ -386,7 +486,7 @@ export class MssqlPool {
|
|
|
386
486
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
387
487
|
async query(text, values) {
|
|
388
488
|
const { text: sql, params } = normalizeQueryArgs(text, values);
|
|
389
|
-
return runRequest(this.pool.request(), sql, params);
|
|
489
|
+
return runRequest(this.pool.request(), sql, params, this.sqlNS);
|
|
390
490
|
}
|
|
391
491
|
async connect() {
|
|
392
492
|
return new MssqlTxClient(this.pool, this.sqlNS);
|
|
@@ -467,8 +567,11 @@ function mssqlColumnType(type, maxLength) {
|
|
|
467
567
|
return 'DATE';
|
|
468
568
|
if (/\bTIME\b/.test(t))
|
|
469
569
|
return 'TIME';
|
|
570
|
+
// A bounded binary column is VARBINARY(n): VARBINARY(MAX) cannot be a key.
|
|
571
|
+
if (/VARBINARY/.test(t))
|
|
572
|
+
return `VARBINARY(${maxLength ?? 255})`;
|
|
470
573
|
if (/BYTEA|BLOB/.test(t))
|
|
471
|
-
return 'VARBINARY(MAX)';
|
|
574
|
+
return maxLength != null ? `VARBINARY(${maxLength})` : 'VARBINARY(MAX)';
|
|
472
575
|
if (/VARCHAR/.test(t))
|
|
473
576
|
return maxLength != null ? `NVARCHAR(${maxLength})` : 'NVARCHAR(255)';
|
|
474
577
|
if (/CHAR/.test(t))
|
|
@@ -510,6 +613,45 @@ function mssqlOutput(returning, alias) {
|
|
|
510
613
|
export const mssqlDialect = {
|
|
511
614
|
...postgresDialect,
|
|
512
615
|
name: 'mssql',
|
|
616
|
+
capabilities: Object.freeze({
|
|
617
|
+
distinctOn: false,
|
|
618
|
+
positionalJson: false,
|
|
619
|
+
nullsOrderingClause: false,
|
|
620
|
+
nullsSortHigh: false,
|
|
621
|
+
insensitiveInList: false,
|
|
622
|
+
jsonDocumentEquality: 'text',
|
|
623
|
+
jsonExtractTyped: false,
|
|
624
|
+
jsonExactNumericCast: false,
|
|
625
|
+
numericTextComparison: false,
|
|
626
|
+
enforcesForeignKeys: true,
|
|
627
|
+
partitionLimitPushdown: false,
|
|
628
|
+
utcTimestampRewrite: false,
|
|
629
|
+
paramTypeCasts: false,
|
|
630
|
+
namedEnumTypes: false,
|
|
631
|
+
// No JSON column type: json is NVARCHAR(MAX), so a text column holds JSON.
|
|
632
|
+
structuredValueColumns: 'scalars',
|
|
633
|
+
// The limit the driver can actually send (see MSSQL_MAX_PARAMS), not 2100.
|
|
634
|
+
maxBindParameters: MSSQL_MAX_PARAMS,
|
|
635
|
+
maxInsertRows: MSSQL_MAX_INSERT_ROWS,
|
|
636
|
+
upsertConflictTarget: 'explicit',
|
|
637
|
+
setClauseOrder: 'snapshot',
|
|
638
|
+
reselectLocksRows: false,
|
|
639
|
+
integerDivision: '/',
|
|
640
|
+
updateExpressions: true,
|
|
641
|
+
// MERGE refuses a target row matched twice (error 8672).
|
|
642
|
+
bulkUpsertAppliesRepeatedKeys: false,
|
|
643
|
+
readOnlyTransactions: false,
|
|
644
|
+
connectionParameters: false,
|
|
645
|
+
backendCancel: false,
|
|
646
|
+
idleTransactionTimeout: false,
|
|
647
|
+
abortsTransactionOnError: false,
|
|
648
|
+
connectionDriver: 'engine-driver',
|
|
649
|
+
preparesEveryStatement: false,
|
|
650
|
+
statementSyntax: 'mssql',
|
|
651
|
+
inlineForeignKeys: true,
|
|
652
|
+
// NVARCHAR(MAX) / VARBINARY(MAX) cannot be an index key column.
|
|
653
|
+
unboundedTextKeys: false,
|
|
654
|
+
}),
|
|
513
655
|
// No RETURNING → the statement emits its rows via OUTPUT INSERTED.* in ONE
|
|
514
656
|
// round-trip (Phase-0 'output' strategy, executed like 'returning').
|
|
515
657
|
resultStrategy: 'output',
|
|
@@ -533,6 +675,17 @@ export const mssqlDialect = {
|
|
|
533
675
|
// is Postgres-only (out of scope here).
|
|
534
676
|
supportsLateralJoin: false,
|
|
535
677
|
supportsPlanCacheMode: false,
|
|
678
|
+
// SQL Server locks through table hints, not a trailing clause: not in this
|
|
679
|
+
// release.
|
|
680
|
+
rowLockModes: [],
|
|
681
|
+
// PERCENTILE_CONT is a window function here (OVER required), so it does not
|
|
682
|
+
// combine with other aggregates in one statement.
|
|
683
|
+
supportsPercentileAggregates: false,
|
|
684
|
+
// MERGE ... WITH (HOLDLOCK).
|
|
685
|
+
supportsBulkUpsert: true,
|
|
686
|
+
// Both MERGE forms alias the target `T` (and the incoming rows `S`), so the
|
|
687
|
+
// conflict-UPDATE reads the existing row through `T.`, never the table name.
|
|
688
|
+
upsertTargetAlias: 'T',
|
|
536
689
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
537
690
|
supportsAdvisoryLock: true,
|
|
538
691
|
// No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
|
|
@@ -591,7 +744,9 @@ export const mssqlDialect = {
|
|
|
591
744
|
// returned `1`, i.e. the strategy silently changed the caller's value
|
|
592
745
|
// types. Above 2^53 all three keep the string, which is the point of
|
|
593
746
|
// carrying text at all.
|
|
594
|
-
|
|
747
|
+
// `int8` is the spelling a code-first (`defineSchema`) schema records for
|
|
748
|
+
// the same BIGINT column, so it needs the same rule.
|
|
749
|
+
if (t === 'bigint' || t === 'int8') {
|
|
595
750
|
return {
|
|
596
751
|
sql: (ref) => `CAST(${ref} AS NVARCHAR(50))`,
|
|
597
752
|
decode: (value) => {
|
|
@@ -609,7 +764,8 @@ export const mssqlDialect = {
|
|
|
609
764
|
// nvarchar cell as a JSON *string* (only `buildRelationSubquery`'s nested
|
|
610
765
|
// relations are JSON_QUERY-wrapped, never a scalar column), so parsing the
|
|
611
766
|
// row hands back exactly the string the driver returns for a top-level
|
|
612
|
-
// read. Both routes already agree, and casting would only add noise
|
|
767
|
+
// read. Both routes already agree, and casting would only add noise; the
|
|
768
|
+
// text is parsed for both by `decodeColumnValue` in the row parser. This
|
|
613
769
|
// is the divergence MySQL DOES have, where JSON_OBJECT embeds a real JSON
|
|
614
770
|
// column as a nested value while the driver hands back text.
|
|
615
771
|
//
|
|
@@ -623,19 +779,41 @@ export const mssqlDialect = {
|
|
|
623
779
|
}
|
|
624
780
|
return undefined;
|
|
625
781
|
},
|
|
782
|
+
// A json/jsonb column is NVARCHAR(MAX) here, so the driver hands back the
|
|
783
|
+
// document's text, and FOR JSON PATH embeds that text as a JSON string, which
|
|
784
|
+
// the relation parser turns back into the same text. Parsing it once in the
|
|
785
|
+
// row parser gives both paths the value PostgreSQL returns.
|
|
786
|
+
decodeColumnValue(columnType) {
|
|
787
|
+
const t = columnType.toLowerCase();
|
|
788
|
+
return t === 'json' || t === 'jsonb' ? parseJsonText : undefined;
|
|
789
|
+
},
|
|
626
790
|
castAggregate(expr, target) {
|
|
627
791
|
return `CAST(${expr} AS ${target === 'int' ? 'INT' : 'FLOAT'})`;
|
|
628
792
|
},
|
|
793
|
+
// AVG over an integer column is an integer here (56.4 came back 56), and
|
|
794
|
+
// casting the RESULT cannot recover the fraction. Widen the input instead:
|
|
795
|
+
// FLOAT for the narrow integers, the PostgreSQL `avg` value in double
|
|
796
|
+
// precision; DECIMAL for BIGINT, which keeps an exact average (AVG of
|
|
797
|
+
// DECIMAL(38,0) is DECIMAL(38,6)) where FLOAT would round past 2^53.
|
|
798
|
+
averageArgument(columnRef, columnType) {
|
|
799
|
+
const t = columnType.toLowerCase();
|
|
800
|
+
if (t === 'bigint' || t === 'int8' || t === 'bigserial')
|
|
801
|
+
return `CAST(${columnRef} AS DECIMAL(38, 0))`;
|
|
802
|
+
if (SMALL_INTEGER_TYPES.has(t))
|
|
803
|
+
return `CAST(${columnRef} AS FLOAT)`;
|
|
804
|
+
return undefined;
|
|
805
|
+
},
|
|
629
806
|
// No array params. OPENJSON expands a single JSON-array param into a row set,
|
|
630
807
|
// keeping ONE placeholder (so the SQL cache stays valid regardless of list
|
|
631
808
|
// length) and handling the empty list (OPENJSON('[]') → zero rows). SQL Server
|
|
632
|
-
// implicitly converts the nvarchar `value` to the column's type,
|
|
633
|
-
//
|
|
809
|
+
// implicitly converts the nvarchar `value` to the column's type, and
|
|
810
|
+
// mssqlInListJson writes each element as the text that converts to the value
|
|
811
|
+
// `equals` binds. OPENJSON requires SQL Server 2016+.
|
|
634
812
|
buildInClause(expr, paramRef, negated) {
|
|
635
813
|
return `${expr} ${negated ? 'NOT IN' : 'IN'} (SELECT [value] FROM OPENJSON(${paramRef}))`;
|
|
636
814
|
},
|
|
637
815
|
inClauseParam(values) {
|
|
638
|
-
return
|
|
816
|
+
return mssqlInListJson(Array.isArray(values) ? values : []);
|
|
639
817
|
},
|
|
640
818
|
// OUTPUT replaces RETURNING, injected mid-statement by the statement builders,
|
|
641
819
|
// never as a trailing clause.
|
|
@@ -648,7 +826,7 @@ export const mssqlDialect = {
|
|
|
648
826
|
},
|
|
649
827
|
buildBulkInsertStatement(input) {
|
|
650
828
|
// No UNNEST in SQL Server, emit multi-row VALUES with flattened, named `@pN`
|
|
651
|
-
// placeholders. Enforce the engine's 1000-row /
|
|
829
|
+
// placeholders. Enforce the engine's 1000-row / 2098-param statement limits.
|
|
652
830
|
const rowCount = input.rowValues.length;
|
|
653
831
|
const paramCount = input.rowValues.reduce((n, row) => n + row.length, 0);
|
|
654
832
|
if (rowCount > MSSQL_MAX_INSERT_ROWS) {
|
|
@@ -707,6 +885,35 @@ export const mssqlDialect = {
|
|
|
707
885
|
`WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${sourceVals})` +
|
|
708
886
|
`${out};`);
|
|
709
887
|
},
|
|
888
|
+
buildBulkUpsertStatement(input) {
|
|
889
|
+
// MERGE, with HOLDLOCK: without it two concurrent MERGEs can both see "not
|
|
890
|
+
// matched" for one key and the second insert fails on the unique key. A
|
|
891
|
+
// NULL in a key column never matches (T.c = S.c is not true), so the row
|
|
892
|
+
// is inserted, as PostgreSQL's ON CONFLICT treats it. `ignore` drops the
|
|
893
|
+
// WHEN MATCHED arm, so only the inserted rows come back through OUTPUT.
|
|
894
|
+
// The bind-parameter limit (MSSQL_MAX_PARAMS) is enforced by the caller's chunking.
|
|
895
|
+
let n = 0;
|
|
896
|
+
const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
|
|
897
|
+
const params = input.rowValues.flat();
|
|
898
|
+
const alias = this.upsertTargetAlias ?? 'T';
|
|
899
|
+
const on = input.conflictColumns.map((c) => `${alias}.${c} = S.${c}`).join(' AND ');
|
|
900
|
+
const cols = input.columns.join(', ');
|
|
901
|
+
let matched = '';
|
|
902
|
+
if (input.update) {
|
|
903
|
+
const update = input.update(params, { target: alias, incoming: 'S' });
|
|
904
|
+
matched =
|
|
905
|
+
`WHEN MATCHED${update.where ? ` AND (${update.where})` : ''} ` +
|
|
906
|
+
`THEN UPDATE SET ${update.setClauses.join(', ')} `;
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
sql: `MERGE INTO ${input.table} WITH (HOLDLOCK) AS ${alias} ` +
|
|
910
|
+
`USING (VALUES ${tuples}) AS S (${cols}) ON (${on}) ` +
|
|
911
|
+
matched +
|
|
912
|
+
`WHEN NOT MATCHED THEN INSERT (${cols}) VALUES (${input.columns.map((c) => `S.${c}`).join(', ')})` +
|
|
913
|
+
`${mssqlOutput(input.returning, 'INSERTED')};`,
|
|
914
|
+
params,
|
|
915
|
+
};
|
|
916
|
+
},
|
|
710
917
|
// UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
|
|
711
918
|
// WHERE), a trailing clause would be invalid T-SQL.
|
|
712
919
|
buildUpdateStatement(input) {
|
|
@@ -750,6 +957,35 @@ export const mssqlDialect = {
|
|
|
750
957
|
buildJsonPathExtract(column, pathParamRef) {
|
|
751
958
|
return `JSON_VALUE(${column}, ${pathParamRef})`;
|
|
752
959
|
},
|
|
960
|
+
// `JSON_VALUE` returns the same text for 5 and "5", so `{ path, equals }`
|
|
961
|
+
// reads the value's JSON TYPE from OPENJSON over the parent of the path
|
|
962
|
+
// (type 0 null, 1 string, 2 number, 3 boolean) and compares within it.
|
|
963
|
+
// Every part is bound, so the SQL text is the same for every operand type.
|
|
964
|
+
// Numbers compare as float (5 equals 5.0; beyond 2^53 precision is lost);
|
|
965
|
+
// strings compare with a binary collation, i.e. case-sensitively, whatever
|
|
966
|
+
// the column's collation. Arrays and objects are refused: OPENJSON returns
|
|
967
|
+
// their stored text, so whitespace and key order would decide the match.
|
|
968
|
+
buildJsonPathEquals(input) {
|
|
969
|
+
const { value, path } = input;
|
|
970
|
+
if (path.length === 0) {
|
|
971
|
+
throw new UnsupportedFeatureError(`JSON \`equals\` on the whole document (${input.label})`, 'mssql', 'SQL Server stores the document as text, so whitespace and key order would decide the match. Compare a ' +
|
|
972
|
+
'scalar with `{ path: [...], equals }`, or run the query on PostgreSQL or MySQL.');
|
|
973
|
+
}
|
|
974
|
+
if (value !== null && typeof value === 'object') {
|
|
975
|
+
throw new UnsupportedFeatureError(`JSON path \`equals\` with ${Array.isArray(value) ? 'an array' : 'an object'} (${input.label})`, 'mssql', 'SQL Server returns an array or object at a path as its stored text, so whitespace and key order would ' +
|
|
976
|
+
'decide the match. Compare a scalar at a deeper path, or run the query on PostgreSQL or MySQL.');
|
|
977
|
+
}
|
|
978
|
+
const type = value === null ? 0 : typeof value === 'string' ? 1 : typeof value === 'boolean' ? 3 : 2;
|
|
979
|
+
const text = value === null ? null : String(value);
|
|
980
|
+
const parent = input.bind(input.encodePath(path.slice(0, -1)));
|
|
981
|
+
const key = input.bind(String(path[path.length - 1]));
|
|
982
|
+
const typeRef = input.bind(type);
|
|
983
|
+
const v = input.bind(text);
|
|
984
|
+
return (`EXISTS (SELECT 1 FROM OPENJSON(${input.column}, ${parent}) AS [jpe] ` +
|
|
985
|
+
`WHERE [jpe].[key] = ${key} AND [jpe].[type] = ${typeRef} AND ([jpe].[type] = 0 ` +
|
|
986
|
+
`OR ([jpe].[type] = 2 AND TRY_CONVERT(float, [jpe].[value]) = TRY_CONVERT(float, ${v})) ` +
|
|
987
|
+
`OR ([jpe].[type] <> 2 AND [jpe].[value] = ${v} COLLATE Latin1_General_BIN2)))`);
|
|
988
|
+
},
|
|
753
989
|
// ---- Type mapping -------------------------------------------------------
|
|
754
990
|
typeToTypeScript(dialectType, nullable) {
|
|
755
991
|
return mssqlTypeToTs(dialectType, nullable);
|
|
@@ -760,6 +996,18 @@ export const mssqlDialect = {
|
|
|
760
996
|
buildColumnType(input) {
|
|
761
997
|
return mssqlColumnType(input.type, input.maxLength);
|
|
762
998
|
},
|
|
999
|
+
/**
|
|
1000
|
+
* SQL Server has no enum type: an NVARCHAR column long enough for the
|
|
1001
|
+
* longest label (at least 255), and a CHECK that admits only the labels.
|
|
1002
|
+
* The CHECK compares under the column's collation, so on a case-insensitive
|
|
1003
|
+
* default collation `'ACTIVE'` passes for a label `'active'`, as it does in
|
|
1004
|
+
* every other text comparison there.
|
|
1005
|
+
*/
|
|
1006
|
+
buildEnumColumn(column, labels) {
|
|
1007
|
+
const literals = labels.map((label) => `N'${label.replace(/'/g, "''")}'`);
|
|
1008
|
+
const longest = Math.max(255, ...labels.map((label) => label.length));
|
|
1009
|
+
return { type: 'VARCHAR', maxLength: longest, check: `${column} IN (${literals.join(', ')})` };
|
|
1010
|
+
},
|
|
763
1011
|
buildColumnDefinition(input) {
|
|
764
1012
|
const isSerial = /serial/i.test(input.type);
|
|
765
1013
|
const parts = [input.name, this.buildColumnType(input)];
|
|
@@ -869,7 +1117,7 @@ export const mssqlDialect = {
|
|
|
869
1117
|
* as a string.
|
|
870
1118
|
*
|
|
871
1119
|
* **Param-push order** strictly mirrors `collectRelationSubqueryParams`:
|
|
872
|
-
* - manyToMany / to-many with limit|orderBy: `where` → `limit` → nested;
|
|
1120
|
+
* - manyToMany / to-many with limit|offset|orderBy: `where` → `limit` → `offset` → nested;
|
|
873
1121
|
* - to-one / unordered-unlimited to-many: nested → `where` (no limit).
|
|
874
1122
|
*/
|
|
875
1123
|
function buildForJsonSubquery(dialect, ctx) {
|
|
@@ -877,9 +1125,27 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
877
1125
|
const q = (name) => dialect.quoteIdentifier(name);
|
|
878
1126
|
const qTarget = q(targetTable);
|
|
879
1127
|
const qParent = q(parentRef);
|
|
880
|
-
|
|
1128
|
+
// `orderByEntries` flattens the array form (`[{ views: 'desc' }, { id: 'asc' }]`)
|
|
1129
|
+
// into the same entry list as the object form, as the core relation builder
|
|
1130
|
+
// does; `Object.entries` read an array's indexes as field names.
|
|
1131
|
+
const orderEntries = spec !== true && spec.orderBy ? orderByEntries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
881
1132
|
const hasOrder = orderEntries.length > 0;
|
|
882
1133
|
const hasLimit = spec !== true && spec.limit !== undefined;
|
|
1134
|
+
// `skip` arrives folded into `offset` (the args seam does that, and refuses
|
|
1135
|
+
// it on a to-one relation).
|
|
1136
|
+
const offset = spec === true ? undefined : spec.offset;
|
|
1137
|
+
const hasOffset = offset !== undefined;
|
|
1138
|
+
/** Bind the relation offset after the limit, the order the param-collect mirror pushes them. */
|
|
1139
|
+
const bindOffset = () => {
|
|
1140
|
+
if (!hasOffset)
|
|
1141
|
+
return undefined;
|
|
1142
|
+
const n = Number(offset);
|
|
1143
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
1144
|
+
throw new ValidationError(`relation offset on "${targetTable}" must be a non-negative integer, received: ${String(offset)}`);
|
|
1145
|
+
}
|
|
1146
|
+
params.push(n);
|
|
1147
|
+
return dialect.paramPlaceholder(params.length);
|
|
1148
|
+
};
|
|
883
1149
|
/**
|
|
884
1150
|
* `<alias>.<col> AS [<field>]` selection for the FOR JSON object keys.
|
|
885
1151
|
*
|
|
@@ -913,7 +1179,10 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
913
1179
|
return cols;
|
|
914
1180
|
};
|
|
915
1181
|
/** ORDER BY + OFFSET/FETCH paging clause for a to-many FOR JSON subquery. */
|
|
916
|
-
const buildPaging = (a, limitPlaceholder) => {
|
|
1182
|
+
const buildPaging = (a, limitPlaceholder, offsetPlaceholder) => {
|
|
1183
|
+
const offsetRows = `OFFSET ${offsetPlaceholder ?? '0'} ROWS`;
|
|
1184
|
+
const fetch = limitPlaceholder !== undefined ? ` FETCH NEXT ${limitPlaceholder} ROWS ONLY` : '';
|
|
1185
|
+
const paged = limitPlaceholder !== undefined || offsetPlaceholder !== undefined;
|
|
917
1186
|
if (hasOrder) {
|
|
918
1187
|
const orderBy = orderEntries
|
|
919
1188
|
.map(([k, dir]) => {
|
|
@@ -946,19 +1215,15 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
946
1215
|
return `${a}.${q(col)} ${safeDir}`;
|
|
947
1216
|
})
|
|
948
1217
|
.join(', ');
|
|
949
|
-
// FOR JSON permits ORDER BY; OFFSET/FETCH is only needed to
|
|
950
|
-
return
|
|
951
|
-
? ` ORDER BY ${orderBy} OFFSET 0 ROWS FETCH NEXT ${limitPlaceholder} ROWS ONLY`
|
|
952
|
-
: ` ORDER BY ${orderBy}`;
|
|
953
|
-
}
|
|
954
|
-
if (limitPlaceholder !== undefined) {
|
|
955
|
-
return ` ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT ${limitPlaceholder} ROWS ONLY`;
|
|
1218
|
+
// FOR JSON permits ORDER BY; OFFSET/FETCH is only needed to page.
|
|
1219
|
+
return paged ? ` ORDER BY ${orderBy} ${offsetRows}${fetch}` : ` ORDER BY ${orderBy}`;
|
|
956
1220
|
}
|
|
957
|
-
|
|
1221
|
+
// OFFSET/FETCH requires an ORDER BY.
|
|
1222
|
+
return paged ? ` ORDER BY (SELECT NULL) ${offsetRows}${fetch}` : '';
|
|
958
1223
|
};
|
|
959
1224
|
// ----- manyToMany: JOIN target through the junction table -----------------
|
|
960
1225
|
if (relDef.type === 'manyToMany') {
|
|
961
|
-
return buildForJsonManyToMany(dialect, ctx, { colSelect, buildNested, buildPaging, hasLimit });
|
|
1226
|
+
return buildForJsonManyToMany(dialect, ctx, { colSelect, buildNested, buildPaging, bindOffset, hasLimit });
|
|
962
1227
|
}
|
|
963
1228
|
const isToOne = relDef.type === 'belongsTo' || relDef.type === 'hasOne';
|
|
964
1229
|
// Correlation direction is about WHERE THE FK LIVES, not cardinality:
|
|
@@ -977,17 +1242,19 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
977
1242
|
return `SELECT TOP 1 ${cols} FROM ${qTarget} ${alias} WHERE ${where} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES`;
|
|
978
1243
|
}
|
|
979
1244
|
// ----- hasMany ------------------------------------------------------------
|
|
980
|
-
const willWrap = hasLimit || hasOrder;
|
|
1245
|
+
const willWrap = hasLimit || hasOrder || hasOffset;
|
|
981
1246
|
let nestedCols;
|
|
982
1247
|
let extra;
|
|
983
1248
|
let limitPlaceholder;
|
|
1249
|
+
let offsetPlaceholder;
|
|
984
1250
|
if (willWrap) {
|
|
985
|
-
// Wrap order: where → limit → nested.
|
|
1251
|
+
// Wrap order: where → limit → offset → nested.
|
|
986
1252
|
extra = ctx.buildWhere(alias);
|
|
987
1253
|
if (hasLimit) {
|
|
988
1254
|
params.push(Number(spec.limit));
|
|
989
1255
|
limitPlaceholder = dialect.paramPlaceholder(params.length);
|
|
990
1256
|
}
|
|
1257
|
+
offsetPlaceholder = bindOffset();
|
|
991
1258
|
nestedCols = buildNested(alias);
|
|
992
1259
|
}
|
|
993
1260
|
else {
|
|
@@ -997,7 +1264,7 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
997
1264
|
}
|
|
998
1265
|
const where = extra ? `${correlation} AND ${extra}` : correlation;
|
|
999
1266
|
const cols = [...colSelect(alias), ...nestedCols].join(', ');
|
|
1000
|
-
const paging = buildPaging(alias, limitPlaceholder);
|
|
1267
|
+
const paging = buildPaging(alias, limitPlaceholder, offsetPlaceholder);
|
|
1001
1268
|
return `SELECT ISNULL((SELECT ${cols} FROM ${qTarget} ${alias} WHERE ${where}${paging} FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`;
|
|
1002
1269
|
}
|
|
1003
1270
|
/** `FOR JSON PATH` subquery for a manyToMany relation (JOIN target through junction). */
|
|
@@ -1031,17 +1298,18 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1031
1298
|
`but referenceKey has ${refKeys.length}.`);
|
|
1032
1299
|
}
|
|
1033
1300
|
const correlation = sourceKeys.map((jcol, i) => `${jalias}.${q(jcol)} = ${qParent}.${q(refKeys[i])}`).join(' AND ');
|
|
1034
|
-
// Param order mirrors collectRelationSubqueryParams: where → limit → nested.
|
|
1301
|
+
// Param order mirrors collectRelationSubqueryParams: where → limit → offset → nested.
|
|
1035
1302
|
const extra = ctx.buildWhere(alias);
|
|
1036
1303
|
let limitPlaceholder;
|
|
1037
1304
|
if (h.hasLimit) {
|
|
1038
1305
|
params.push(Number(spec.limit));
|
|
1039
1306
|
limitPlaceholder = dialect.paramPlaceholder(params.length);
|
|
1040
1307
|
}
|
|
1308
|
+
const offsetPlaceholder = h.bindOffset();
|
|
1041
1309
|
const nestedCols = h.buildNested(alias);
|
|
1042
1310
|
const where = extra ? `${correlation} AND ${extra}` : correlation;
|
|
1043
1311
|
const cols = [...h.colSelect(alias), ...nestedCols].join(', ');
|
|
1044
|
-
const paging = h.buildPaging(alias, limitPlaceholder);
|
|
1312
|
+
const paging = h.buildPaging(alias, limitPlaceholder, offsetPlaceholder);
|
|
1045
1313
|
return (`SELECT ISNULL((SELECT ${cols} FROM ${qTarget} ${alias} ` +
|
|
1046
1314
|
`JOIN ${qJunction} ${jalias} ON ${joinOn} WHERE ${where}${paging} ` +
|
|
1047
1315
|
`FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`);
|
|
@@ -1075,8 +1343,8 @@ const bitIsTrue = (v) => v === true || v === 1 || v === '1' || v === 'true';
|
|
|
1075
1343
|
* same logical schema, legacy-first naming, per-column disambiguation, and
|
|
1076
1344
|
* collision resolution against scalar column fields included.
|
|
1077
1345
|
*/
|
|
1078
|
-
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
1079
|
-
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
1346
|
+
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, logger) {
|
|
1347
|
+
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable, logger);
|
|
1080
1348
|
}
|
|
1081
1349
|
/**
|
|
1082
1350
|
* Introspect a SQL Server database into the same {@link SchemaMetadata} shape the
|
|
@@ -1087,6 +1355,8 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
|
|
|
1087
1355
|
*
|
|
1088
1356
|
* @param exec Runs a parameterized (`@p1`, `@p2`, …) query and returns rows.
|
|
1089
1357
|
* @param schema The SQL Server schema to introspect (default `dbo`).
|
|
1358
|
+
* @param options Optional include / exclude table-name filters, and the logger
|
|
1359
|
+
* that receives relation-naming notes (console when absent).
|
|
1090
1360
|
*/
|
|
1091
1361
|
export async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
1092
1362
|
// ----- Tables -----
|
|
@@ -1225,16 +1495,19 @@ export async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1225
1495
|
}
|
|
1226
1496
|
}
|
|
1227
1497
|
// ----- Relations -----
|
|
1228
|
-
|
|
1498
|
+
// One field per column, as the PostgreSQL introspector requires.
|
|
1499
|
+
for (const [tableName, cols] of columnsByTable)
|
|
1500
|
+
assertDistinctColumnFields(tableName, cols);
|
|
1501
|
+
const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
|
|
1229
1502
|
// ----- Assemble TableMetadata -----
|
|
1230
|
-
const tables =
|
|
1503
|
+
const tables = nullRecord();
|
|
1231
1504
|
for (const tableName of tableNames) {
|
|
1232
1505
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
1233
|
-
const columnMap =
|
|
1234
|
-
const reverseColumnMap =
|
|
1506
|
+
const columnMap = nullRecord();
|
|
1507
|
+
const reverseColumnMap = nullRecord();
|
|
1235
1508
|
const dateColumns = new Set();
|
|
1236
|
-
const dialectTypes =
|
|
1237
|
-
const pgTypes =
|
|
1509
|
+
const dialectTypes = nullRecord();
|
|
1510
|
+
const pgTypes = nullRecord();
|
|
1238
1511
|
const allColumns = [];
|
|
1239
1512
|
for (const col of columns) {
|
|
1240
1513
|
columnMap[col.field] = col.name;
|
|
@@ -1256,7 +1529,7 @@ export async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1256
1529
|
allColumns,
|
|
1257
1530
|
primaryKey: pkByTable.get(tableName) ?? [],
|
|
1258
1531
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
1259
|
-
relations: relationsByTable.get(tableName) ??
|
|
1532
|
+
relations: relationsByTable.get(tableName) ?? nullRecord(),
|
|
1260
1533
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
1261
1534
|
};
|
|
1262
1535
|
}
|
|
@@ -1282,6 +1555,7 @@ export async function introspectMssql(options) {
|
|
|
1282
1555
|
return await introspectMssqlWith(exec, options.schema ?? 'dbo', {
|
|
1283
1556
|
include: options.include,
|
|
1284
1557
|
exclude: options.exclude,
|
|
1558
|
+
logger: options.logger,
|
|
1285
1559
|
});
|
|
1286
1560
|
}
|
|
1287
1561
|
finally {
|
|
@@ -1390,35 +1664,48 @@ export async function turbineMssql(target, schema, options = {}) {
|
|
|
1390
1664
|
owns = true;
|
|
1391
1665
|
}
|
|
1392
1666
|
}
|
|
1393
|
-
|
|
1667
|
+
let client;
|
|
1394
1668
|
try {
|
|
1395
|
-
|
|
1396
|
-
|
|
1669
|
+
// Probe the server version (fail fast on SQL Server < 2016).
|
|
1670
|
+
try {
|
|
1671
|
+
const rows = (await pool.query("SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS v")).rows;
|
|
1672
|
+
assertSupportedVersion(Number(rows[0]?.v ?? 0));
|
|
1673
|
+
}
|
|
1674
|
+
catch (err) {
|
|
1675
|
+
if (err instanceof ConnectionError)
|
|
1676
|
+
throw err;
|
|
1677
|
+
// A non-version probe failure (permissions, etc.) should not block startup.
|
|
1678
|
+
}
|
|
1679
|
+
// Everything that is not a SQL Server introspection option is client config
|
|
1680
|
+
// and rides through untouched, so a new TurbineConfig option works here the
|
|
1681
|
+
// day it lands. The engine-owned keys come last, so no caller can unbind the
|
|
1682
|
+
// dialect.
|
|
1683
|
+
const { schema: _dbSchema, ...clientConfig } = options;
|
|
1684
|
+
client = new TurbineClient({
|
|
1685
|
+
...clientConfig,
|
|
1686
|
+
pool,
|
|
1687
|
+
dialect: mssqlDialect,
|
|
1688
|
+
preparedStatements: false,
|
|
1689
|
+
}, schema);
|
|
1397
1690
|
}
|
|
1398
1691
|
catch (err) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
//
|
|
1692
|
+
// A failed factory hands the caller nothing to close, so a pool it BUILT
|
|
1693
|
+
// (already connected) is closed here rather than left holding the process
|
|
1694
|
+
// open; the client constructor refuses a bad config, a static global filter
|
|
1695
|
+
// naming no table or column for one. A pool the caller supplied stays open.
|
|
1696
|
+
if (owns)
|
|
1697
|
+
await pool.end().catch(() => { });
|
|
1698
|
+
throw err;
|
|
1402
1699
|
}
|
|
1403
|
-
// Everything that is not a SQL Server introspection option is client config
|
|
1404
|
-
// and rides through untouched, so a new TurbineConfig option works here the
|
|
1405
|
-
// day it lands. The engine-owned keys come last, so no caller can unbind the
|
|
1406
|
-
// dialect.
|
|
1407
|
-
const { schema: _dbSchema, ...clientConfig } = options;
|
|
1408
|
-
const client = new TurbineClient({
|
|
1409
|
-
...clientConfig,
|
|
1410
|
-
pool,
|
|
1411
|
-
dialect: mssqlDialect,
|
|
1412
|
-
preparedStatements: false,
|
|
1413
|
-
}, schema);
|
|
1414
1700
|
if (owns) {
|
|
1415
1701
|
// Turbine built this pool, so disconnect()/end() must close it. External pools
|
|
1416
1702
|
// (injection) stay the caller's responsibility (disconnect() no-op), consistent
|
|
1417
1703
|
// with turbineHttp / turbineMysql / turbineSqlite.
|
|
1418
1704
|
const baseDisconnect = client.disconnect.bind(client);
|
|
1419
|
-
const close = async () => {
|
|
1420
|
-
await baseDisconnect();
|
|
1705
|
+
const close = async (options) => {
|
|
1706
|
+
const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
|
|
1421
1707
|
await pool.end();
|
|
1708
|
+
return result ?? undefined;
|
|
1422
1709
|
};
|
|
1423
1710
|
const patch = client;
|
|
1424
1711
|
patch.disconnect = close;
|