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.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. 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 / 2100 params
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
- /** Max bound parameters per SQL Server statement. */
103
- const MSSQL_MAX_PARAMS = 2100;
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`/`Uint8Array` pass through; any remaining object/array → JSON text
118
- * (matches how Turbine already pre-serializes JSON filter / `IN`-list params).
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
- if (value instanceof Uint8Array)
136
- return value; // Buffer is a Uint8Array subclass
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
- /** Bind positional `params[]` to `@p1`, `@p2`, … inputs and run the statement. */
254
- async function runRequest(request, text, params) {
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.input(`p${i + 1}`, toMssqlParam(params[i]));
356
+ bindParam(request, `p${i + 1}`, params[i], types);
257
357
  }
258
358
  try {
259
- const result = await request.query(text);
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
- if (t === 'bigint') {
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. This
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, so this works
633
- // for numbers AND strings. OPENJSON requires SQL Server 2016+.
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 JSON.stringify(values ?? []);
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 / 2100-param statement limits.
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
- const orderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
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 apply a LIMIT.
950
- return limitPlaceholder !== undefined
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
- return '';
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
- const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable);
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
- // Probe the server version (fail fast on SQL Server < 2016).
1667
+ let client;
1394
1668
  try {
1395
- const rows = (await pool.query("SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS v")).rows;
1396
- assertSupportedVersion(Number(rows[0]?.v ?? 0));
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
- if (err instanceof ConnectionError)
1400
- throw err;
1401
- // A non-version probe failure (permissions, etc.) should not block startup.
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;