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/cjs/mysql.js CHANGED
@@ -78,6 +78,7 @@ const errors_js_1 = require("./errors.js");
78
78
  const introspect_js_1 = require("./introspect.js");
79
79
  const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
80
80
  const schema_js_1 = require("./schema.js");
81
+ const sql_placeholders_js_1 = require("./sql-placeholders.js");
81
82
  // ---------------------------------------------------------------------------
82
83
  // Value coercion (params in)
83
84
  // ---------------------------------------------------------------------------
@@ -86,7 +87,7 @@ const schema_js_1 = require("./schema.js");
86
87
  * 1/0 (MySQL has no boolean type; `TINYINT(1)` stores 1/0), `undefined`/`null`
87
88
  * → NULL, `Date`/`Uint8Array` pass through (mysql2 formats them), and any
88
89
  * remaining object/array → JSON text (matches how Turbine already pre-serializes
89
- * JSON filter / `IN`-list params).
90
+ * JSON filter params). An `in` list element binds through here too, one by one.
90
91
  */
91
92
  function toMysqlParam(value) {
92
93
  if (value === undefined || value === null)
@@ -95,31 +96,136 @@ function toMysqlParam(value) {
95
96
  case 'boolean':
96
97
  return value ? 1 : 0;
97
98
  case 'number':
99
+ // MySQL has no NaN or Infinity: the server stored NaN as 0 without a
100
+ // word, and refused Infinity with a raw driver error.
101
+ if (!Number.isFinite(value)) {
102
+ throw new errors_js_1.ValidationError(`MySQL cannot store or compare ${value}: a bound number must be finite. Pass a finite number, or null.`);
103
+ }
104
+ return value;
98
105
  case 'bigint':
99
106
  case 'string':
100
107
  return value;
101
108
  }
102
- if (value instanceof Date)
109
+ if (value instanceof Date) {
110
+ // mysql2 renders an Invalid Date as '0000-00-00 00:00:00.000', which the
111
+ // server then reports as that value instead of the caller's mistake.
112
+ if (Number.isNaN(value.getTime())) {
113
+ throw new errors_js_1.ValidationError('MySQL cannot bind an Invalid Date. Pass a valid Date, or null.');
114
+ }
103
115
  return value;
116
+ }
104
117
  if (value instanceof Uint8Array)
105
118
  return value; // also covers Buffer
106
119
  return JSON.stringify(value);
107
120
  }
108
121
  /**
109
- * Bind the positional `params[]` (in 1-indexed generation order) to the named
110
- * `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
111
- * independent of where each placeholder lands in the SQL text, the same
112
- * guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
113
- * statements (BEGIN/COMMIT/DDL) so they bind nothing.
122
+ * An `in` / `notIn` list, as the builder hands it to the shim: ONE param, so
123
+ * the SQL text and its cache entry stay independent of the list's length (see
124
+ * {@link mysqlDialect.buildInClause}). The shim expands it into one placeholder
125
+ * per element, each bound exactly as an equality operand is, which is what
126
+ * makes `{ in: [v] }` match the rows `{ equals: v }` matches on every column
127
+ * type: a `JSON_TABLE` list compared JSON values, so a boolean, a date or time,
128
+ * a binary value, a decimal or bigint written as a string, and every
129
+ * case-folded string missed, and `notIn` then matched every row.
130
+ */
131
+ class MysqlInList {
132
+ values;
133
+ constructor(values) {
134
+ this.values = values;
135
+ }
136
+ }
137
+ /** MySQL's limit on placeholders in one prepared statement. */
138
+ const MYSQL_MAX_PLACEHOLDERS = 65_535;
139
+ /** A list of `n` elements is sent padded to the next power of two (by repeating its last element). */
140
+ function paddedListLength(n) {
141
+ return n <= 1 ? n : 2 ** Math.ceil(Math.log2(n));
142
+ }
143
+ /** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
144
+ function refusePlaceholder(message) {
145
+ throw new errors_js_1.ValidationError(message);
146
+ }
147
+ /**
148
+ * The statement text and named binding for one execution: `$n` placeholders
149
+ * (the spelling `rawUnsafe` documents) rewritten to `:pN`, every
150
+ * {@link MysqlInList} expanded, and every other value bound by name. Mapping by
151
+ * NAME makes binding independent of where each placeholder lands in the SQL
152
+ * text, the same guarantee Postgres' numbered `$N` gives. `binding` is
153
+ * `undefined` when nothing is bound, so parameter-less statements (BEGIN,
154
+ * COMMIT, DDL) run through `.query()`.
114
155
  */
115
- function toNamedBinding(values) {
156
+ function bindStatement(sql, values) {
116
157
  if (values.length === 0)
117
- return undefined;
158
+ return { sql, binding: undefined };
159
+ const text = (0, sql_placeholders_js_1.rewritePositionalPlaceholders)(sql, 'mysql', values.length, (i) => `:p${i}`, refusePlaceholder);
118
160
  const named = {};
161
+ let lists;
119
162
  for (let i = 0; i < values.length; i++) {
120
- named[`p${i + 1}`] = toMysqlParam(values[i]);
163
+ const value = values[i];
164
+ if (value instanceof MysqlInList) {
165
+ lists ??= new Map();
166
+ lists.set(`p${i + 1}`, value);
167
+ }
168
+ else {
169
+ named[`p${i + 1}`] = toMysqlParam(value);
170
+ }
171
+ }
172
+ if (lists === undefined)
173
+ return { sql: text, binding: named };
174
+ const expanded = expandInLists(text, lists, named);
175
+ return { sql: expanded, binding: Object.keys(named).length > 0 ? named : undefined };
176
+ }
177
+ /**
178
+ * Replace each list placeholder (`:p3`) with one placeholder per element
179
+ * (`:p3_0, :p3_1, ...`), binding each element into `named`. An empty list
180
+ * becomes a subquery with no rows, so `IN` is false and `NOT IN` true, as
181
+ * Postgres answers `= ANY('{}')` and `!= ALL('{}')`.
182
+ *
183
+ * Padding to a power of two bounds the statement texts a caller can produce
184
+ * (mysql2 prepares and caches one server-side statement per distinct text and
185
+ * connection, and the server caps the total across every client at
186
+ * `max_prepared_stmt_count`): about seventeen lengths per list instead of one
187
+ * per length. A repeated element changes neither `IN` nor `NOT IN`.
188
+ */
189
+ function expandInLists(sql, lists, named) {
190
+ const tokens = (0, sql_placeholders_js_1.scanPlaceholders)(sql, 'mysql');
191
+ const listTokens = tokens.filter((t) => t.kind === 'named' && lists.has(t.name));
192
+ const others = tokens.length - listTokens.length;
193
+ let padded = 0;
194
+ let exact = 0;
195
+ for (const t of listTokens) {
196
+ const n = lists.get(t.name).values.length;
197
+ padded += paddedListLength(n);
198
+ exact += n;
199
+ }
200
+ if (others + exact > MYSQL_MAX_PLACEHOLDERS) {
201
+ throw new errors_js_1.ValidationError(`An \`in\` / \`notIn\` list on MySQL binds one placeholder per element, and this statement needs ` +
202
+ `${others + exact} where MySQL allows ${MYSQL_MAX_PLACEHOLDERS}. Split the list across several queries.`);
203
+ }
204
+ const pad = others + padded <= MYSQL_MAX_PLACEHOLDERS;
205
+ for (const [name, list] of lists) {
206
+ list.values.forEach((v, j) => {
207
+ named[`${name}_${j}`] = toMysqlParam(v);
208
+ });
121
209
  }
122
- return named;
210
+ let out = '';
211
+ let last = 0;
212
+ for (const t of listTokens) {
213
+ const n = lists.get(t.name).values.length;
214
+ let replacement;
215
+ if (n === 0) {
216
+ replacement = 'SELECT NULL FROM DUAL WHERE FALSE';
217
+ }
218
+ else {
219
+ const parts = [];
220
+ const length = pad ? paddedListLength(n) : n;
221
+ for (let j = 0; j < length; j++)
222
+ parts.push(`:${t.name}_${Math.min(j, n - 1)}`);
223
+ replacement = parts.join(', ');
224
+ }
225
+ out += sql.slice(last, t.start) + replacement;
226
+ last = t.end;
227
+ }
228
+ return out + sql.slice(last);
123
229
  }
124
230
  // ---------------------------------------------------------------------------
125
231
  // Result shaping + error translation
@@ -254,10 +360,50 @@ function augmentMysqlError(err) {
254
360
  case 1205:
255
361
  target.code = '40001';
256
362
  return err;
257
- default:
363
+ default: {
364
+ // Data exceptions (a bound value that does not fit the column) take the
365
+ // SQLSTATE class 22 path in `wrapPgError`, which builds a ValidationError
366
+ // naming the column and redacts the value-bearing message in 'safe' mode.
367
+ const sqlstate = mysqlDataExceptionState(e.errno, e.sqlState, msg);
368
+ if (sqlstate === undefined)
369
+ return err;
370
+ target.code = sqlstate;
371
+ // "... for column 'name' at row 1", or "... in value for column 't.meta'."
372
+ const column = /for column '([^']+)'/i.exec(msg)?.[1];
373
+ if (column)
374
+ target.column = column.slice(column.lastIndexOf('.') + 1);
258
375
  return err;
376
+ }
259
377
  }
260
378
  }
379
+ /**
380
+ * The SQLSTATE a MySQL data exception maps to, or `undefined` when `errno` is
381
+ * not one. MySQL reports several of these under the generic `HY000`, so the
382
+ * common ones are mapped by errno to the code PostgreSQL uses for the same
383
+ * failure; anything else MySQL itself files under class 22 keeps its own code.
384
+ */
385
+ function mysqlDataExceptionState(errno, sqlState, message) {
386
+ switch (errno) {
387
+ case 1406: // ER_DATA_TOO_LONG
388
+ return '22001';
389
+ case 1264: // ER_WARN_DATA_OUT_OF_RANGE
390
+ case 1690: // ER_DATA_OUT_OF_RANGE
391
+ return '22003';
392
+ case 1366: // ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: "Incorrect integer value: 'abc' for column ..."
393
+ case 1265: // WARN_DATA_TRUNCATED: a value outside an ENUM, a malformed DECIMAL
394
+ return '22P02';
395
+ case 1292: // ER_TRUNCATED_WRONG_VALUE: an invalid datetime, or a non-numeric string compared as a number
396
+ case 1411: // ER_WRONG_VALUE_FOR_TYPE
397
+ return /\b(?:date|time|datetime|timestamp)\b/i.test(message) ? '22007' : '22P02';
398
+ case 1365: // ER_DIVISION_BY_ZERO
399
+ return '22012';
400
+ case 3140: // ER_INVALID_JSON_TEXT
401
+ case 3141: // ER_INVALID_JSON_TEXT_IN_PARAM
402
+ case 3144: // ER_INVALID_JSON_CHARSET
403
+ return '22032';
404
+ }
405
+ return typeof sqlState === 'string' && /^22[0-9A-Z]{3}$/.test(sqlState) ? sqlState : undefined;
406
+ }
261
407
  function normalizeQueryArgs(arg, values) {
262
408
  if (typeof arg === 'string')
263
409
  return { text: arg, params: values ?? [] };
@@ -271,9 +417,10 @@ function normalizeQueryArgs(arg, values) {
271
417
  * the `{ p1, p2, … }` object built from the positional params.
272
418
  */
273
419
  async function execOne(runner, sql, params) {
274
- const binding = toNamedBinding(params);
420
+ const bound = bindStatement(sql, params);
421
+ const binding = bound.binding;
275
422
  try {
276
- const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
423
+ const [result, fields] = binding ? await runner.execute(bound.sql, binding) : await runner.query(bound.sql);
277
424
  return shapeResult(result, fields);
278
425
  }
279
426
  catch (err) {
@@ -387,6 +534,18 @@ function mysqlTypeToTs(dialectType, nullable, columnType) {
387
534
  * matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
388
535
  */
389
536
  const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
537
+ /** Integer column types narrower than 64 bits, in MySQL and code-first (PostgreSQL) spellings. */
538
+ const SMALL_INTEGER_TYPES = new Set([
539
+ 'tinyint',
540
+ 'smallint',
541
+ 'mediumint',
542
+ 'int',
543
+ 'integer',
544
+ 'int2',
545
+ 'int4',
546
+ 'serial',
547
+ 'smallserial',
548
+ ]);
390
549
  /**
391
550
  * Rewrite every boolean column's recorded dialect type to
392
551
  * {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
@@ -439,6 +598,11 @@ function isMysqlDateType(dialectType) {
439
598
  }
440
599
  /** Map a schema-builder (Postgres-flavored) column type to MySQL DDL. */
441
600
  function mysqlColumnType(type, maxLength) {
601
+ // An enum column's type from `buildEnumColumn`, already spelled for MySQL. It
602
+ // returns first: its labels are free text, and one named `int` or `date`
603
+ // would otherwise match a pattern below.
604
+ if (/^ENUM\(/.test(type))
605
+ return type;
442
606
  const t = type.toUpperCase();
443
607
  if (/BIGSERIAL/.test(t))
444
608
  return 'BIGINT AUTO_INCREMENT';
@@ -462,14 +626,19 @@ function mysqlColumnType(type, maxLength) {
462
626
  return 'JSON';
463
627
  if (/UUID/.test(t))
464
628
  return 'CHAR(36)';
629
+ // DATETIME(3), not bare DATETIME: the bare type has zero fractional digits and
630
+ // rounds a JS Date's milliseconds away on write.
465
631
  if (/TIMESTAMPTZ|TIMESTAMP|DATETIME/.test(t))
466
- return 'DATETIME';
632
+ return 'DATETIME(3)';
467
633
  if (/\bDATE\b/.test(t))
468
634
  return 'DATE';
469
635
  if (/\bTIME\b/.test(t))
470
636
  return 'TIME';
637
+ // A bounded binary column is VARBINARY(n): a BLOB cannot be a key.
638
+ if (/VARBINARY/.test(t))
639
+ return `VARBINARY(${maxLength ?? 255})`;
471
640
  if (/BYTEA|BLOB/.test(t))
472
- return 'BLOB';
641
+ return maxLength != null ? `VARBINARY(${maxLength})` : 'BLOB';
473
642
  if (/VARCHAR/.test(t))
474
643
  return maxLength != null ? `VARCHAR(${maxLength})` : 'VARCHAR(255)';
475
644
  if (/CHAR/.test(t))
@@ -483,6 +652,32 @@ function mysqlColumnType(type, maxLength) {
483
652
  // ---------------------------------------------------------------------------
484
653
  // mysqlDialect, the full Dialect contract for MySQL 8
485
654
  // ---------------------------------------------------------------------------
655
+ /** The row alias a multi-row upsert gives its incoming rows (`AS ... ON DUPLICATE KEY UPDATE`). */
656
+ const MYSQL_INCOMING_ALIAS = '`turbine_excluded`';
657
+ /**
658
+ * The LIKE escape character on MySQL (see `mysqlDialect.likeEscapeCharacter`).
659
+ * One character, nothing special inside a single-quoted literal in either
660
+ * `sql_mode`, and not a LIKE metacharacter.
661
+ */
662
+ const MYSQL_LIKE_ESCAPE = '!';
663
+ /** The LIKE metacharacters under {@link MYSQL_LIKE_ESCAPE}: itself, `%` and `_`. */
664
+ const MYSQL_LIKE_META_G = /[!%_]/g;
665
+ /**
666
+ * A `JSON_OBJECT` key as SQL text that reads the same in every `sql_mode`.
667
+ * A key without a backslash is a plain quoted literal. A backslash has no
668
+ * literal spelling that both modes agree on (`'a\\b'` is one backslash by
669
+ * default and two under `NO_BACKSLASH_ESCAPES`, which Turbine's own
670
+ * connections set and a caller-supplied pool may not), so such a key is
671
+ * written as its UTF-8 bytes in hex, which no mode re-reads.
672
+ */
673
+ function mysqlKeyLiteral(key, dialect) {
674
+ if (!key.includes('\\'))
675
+ return `'${dialect.escapeStringLiteral(key)}'`;
676
+ let hex = '';
677
+ for (const byte of new TextEncoder().encode(key))
678
+ hex += byte.toString(16).padStart(2, '0');
679
+ return `CONVERT(X'${hex}' USING utf8mb4)`;
680
+ }
486
681
  /**
487
682
  * MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
488
683
  * quoting, named `:pN` placeholders (NOT positional `?`, see the module
@@ -494,6 +689,47 @@ function mysqlColumnType(type, maxLength) {
494
689
  exports.mysqlDialect = {
495
690
  ...dialect_js_1.postgresDialect,
496
691
  name: 'mysql',
692
+ capabilities: Object.freeze({
693
+ distinctOn: false,
694
+ positionalJson: false,
695
+ nullsOrderingClause: false,
696
+ nullsSortHigh: false,
697
+ insensitiveInList: false,
698
+ jsonDocumentEquality: 'exact',
699
+ jsonExtractTyped: false,
700
+ jsonExactNumericCast: false,
701
+ numericTextComparison: true,
702
+ enforcesForeignKeys: true,
703
+ partitionLimitPushdown: false,
704
+ // A zone-less literal reads in the session time zone; only `date` columns rewrite.
705
+ utcTimestampRewrite: false,
706
+ paramTypeCasts: false,
707
+ namedEnumTypes: false,
708
+ structuredValueColumns: 'scalars-and-text',
709
+ // Counted in a 16-bit field.
710
+ maxBindParameters: 65535,
711
+ maxInsertRows: Number.POSITIVE_INFINITY,
712
+ upsertConflictTarget: 'any-unique-key',
713
+ setClauseOrder: 'left-to-right',
714
+ reselectLocksRows: true,
715
+ integerDivision: 'DIV',
716
+ updateExpressions: true,
717
+ // Caught by the onConflict re-read, which runs in the reselect transaction.
718
+ bulkUpsertAppliesRepeatedKeys: true,
719
+ readOnlyTransactions: true,
720
+ connectionParameters: false,
721
+ backendCancel: false,
722
+ idleTransactionTimeout: false,
723
+ abortsTransactionOnError: false,
724
+ connectionDriver: 'engine-driver',
725
+ // mysql2 execute() prepares each statement text; max_prepared_stmt_count caps them server-wide.
726
+ preparesEveryStatement: true,
727
+ statementSyntax: 'mysql',
728
+ // An inline REFERENCES is parsed and ignored.
729
+ inlineForeignKeys: false,
730
+ // TEXT / BLOB need a prefix length in a key (ER_BLOB_KEY_WITHOUT_LENGTH).
731
+ unboundedTextKeys: false,
732
+ }),
497
733
  // No RETURNING → run the write, then re-SELECT by PK/where (Phase-0 reselect).
498
734
  resultStrategy: 'reselect',
499
735
  supportsReturning: false,
@@ -514,6 +750,13 @@ exports.mysqlDialect = {
514
750
  // Postgres-only in this release (flipping it on is a one-line change + tests).
515
751
  supportsLateralJoin: false,
516
752
  supportsPlanCacheMode: false,
753
+ // FOR UPDATE / FOR SHARE (8.0); there is no NO KEY UPDATE or KEY SHARE.
754
+ rowLockModes: ['update', 'share'],
755
+ // No percentile_cont / percentile_disc.
756
+ supportsPercentileAggregates: false,
757
+ // INSERT ... AS alias ON DUPLICATE KEY UPDATE, with the refusals that
758
+ // keyless conflict resolution needs.
759
+ supportsBulkUpsert: true,
517
760
  // GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
518
761
  supportsAdvisoryLock: true,
519
762
  // Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
@@ -546,15 +789,18 @@ exports.mysqlDialect = {
546
789
  * `NO_BACKSLASH_ESCAPES`, `\` is an escape character inside a string literal,
547
790
  * so doubling only `'` (the inherited Postgres rule) leaves a value ending in
548
791
  * a backslash able to escape its own closing quote. Escape the backslash
549
- * first, then the quote; every other byte is safe raw. The only caller is
550
- * `buildJsonObject` (relation / column names from schema metadata), so this
551
- * is defence in depth rather than a user-value path.
792
+ * first, then the quote; every other byte is safe raw.
793
+ *
794
+ * That is right for the server's default `sql_mode` only: under
795
+ * `NO_BACKSLASH_ESCAPES` (which Turbine sets on the connections it opens) the
796
+ * doubled backslash reads as two. So `buildJsonObject`, the one caller, never
797
+ * hands it a key holding a backslash (see {@link mysqlKeyLiteral}).
552
798
  */
553
799
  escapeStringLiteral(value) {
554
800
  return value.replace(/\\/g, '\\\\').replace(/'/g, "''");
555
801
  },
556
802
  buildJsonObject(pairs) {
557
- const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
803
+ const args = pairs.map(([key, expr]) => `${mysqlKeyLiteral(key, this)}, ${expr}`);
558
804
  return `JSON_OBJECT(${args.join(', ')})`;
559
805
  },
560
806
  // JSON_ARRAYAGG over an empty group returns NULL → COALESCE to JSON_ARRAY().
@@ -584,7 +830,9 @@ exports.mysqlDialect = {
584
830
  // came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
585
831
  // mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
586
832
  // DECIMAL is always a string), so carry the text and keep it.
587
- if (t === 'bigint') {
833
+ // `int8` is the spelling a code-first (`defineSchema`) schema records for
834
+ // the same BIGINT column, so it needs the same rule.
835
+ if (t === 'bigint' || t === 'int8') {
588
836
  return {
589
837
  sql: (ref) => `CAST(${ref} AS CHAR)`,
590
838
  decode: (value) => {
@@ -642,9 +890,9 @@ exports.mysqlDialect = {
642
890
  // parsed value. It is also what `pg` returns for json/jsonb, so this is
643
891
  // Postgres parity rather than a MySQL convention.
644
892
  //
645
- // Contrast SQLite and SQL Server, which return TEXT for the same schema:
646
- // neither has a JSON column type, so the value there really is a string in
647
- // a text column and there is nothing to parse it back from.
893
+ // Contrast SQLite, which has no JSON column type and stores the document as
894
+ // text: its driver shim parses the text back for a column the schema
895
+ // declares json/jsonb, and its own rule does the same through a join.
648
896
  // Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
649
897
  // `base64:type15:…` marker string, so the caller got that text instead of
650
898
  // bytes. Carry hex and rebuild the buffer.
@@ -659,6 +907,14 @@ exports.mysqlDialect = {
659
907
  castAggregate(expr, target) {
660
908
  return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
661
909
  },
910
+ // AVG over an integer column is a DECIMAL with only four more digits of
911
+ // scale (div_precision_increment), so 4/3 came back 1.3333 where PostgreSQL
912
+ // returns 1.3333333333333333. Adding a DOUBLE zero averages in DOUBLE (the
913
+ // `CAST(... AS DOUBLE)` spelling needs 8.0.17; the engine floor is 8.0.0).
914
+ // BIGINT keeps its exact DECIMAL average, like PostgreSQL's numeric one.
915
+ averageArgument(columnRef, columnType) {
916
+ return SMALL_INTEGER_TYPES.has(columnType.toLowerCase()) ? `(${columnRef} + 0E0)` : undefined;
917
+ },
662
918
  // MySQL has no bare OFFSET: the grammar requires a LIMIT for OFFSET to
663
919
  // attach to, so `offset` without `limit` (valid on Postgres, which the
664
920
  // default path is written for) is a syntax error here. The MySQL manual's
@@ -678,17 +934,16 @@ exports.mysqlDialect = {
678
934
  s += ` OFFSET ${offsetPlaceholder}`;
679
935
  return s;
680
936
  },
681
- // No array params in MySQL. JSON_TABLE expands a single JSON-array param into a
682
- // row set, keeping ONE placeholder (so the SQL cache stays valid regardless of
683
- // list length) and handling the empty-list case (zero rows). MySQL coerces the
684
- // non-JSON `expr` to JSON for the comparison, so this works for numbers AND
685
- // strings. JSON_TABLE requires MySQL 8.0+.
937
+ // No array params in MySQL. The list is ONE builder param (a MysqlInList),
938
+ // so the SQL text and its cache entry do not depend on the list's length; the
939
+ // shim expands the placeholder into one per element when the statement runs
940
+ // (see expandInLists). Each element binds as an equality operand does, so the
941
+ // list compares under the column's own type and collation rules.
686
942
  buildInClause(expr, paramRef, negated) {
687
- const inner = `SELECT \`v\` FROM JSON_TABLE(${paramRef}, '$[*]' COLUMNS (\`v\` JSON PATH '$')) AS \`jt\``;
688
- return `${expr} ${negated ? 'NOT IN' : 'IN'} (${inner})`;
943
+ return `${expr} ${negated ? 'NOT IN' : 'IN'} (${paramRef})`;
689
944
  },
690
945
  inClauseParam(values) {
691
- return JSON.stringify(values ?? []);
946
+ return new MysqlInList(Array.isArray(values) ? values : []);
692
947
  },
693
948
  // No RETURNING, resultStrategy 'reselect' re-fetches the row.
694
949
  buildReturningClause() {
@@ -712,6 +967,28 @@ exports.mysqlDialect = {
712
967
  params: input.rowValues.flat(),
713
968
  };
714
969
  },
970
+ buildBulkUpsertStatement(input) {
971
+ // 8.0.19's row alias (`VALUES(col)` is deprecated since 8.0.20). There is
972
+ // no conflict target: ON DUPLICATE KEY UPDATE fires on whichever unique key
973
+ // a row violates, which is why the builder refuses a table where another
974
+ // key's columns are in the data, and why it refuses `ignore` and a
975
+ // conflict predicate (this grammar has no slot for one).
976
+ if (!input.update) {
977
+ throw new errors_js_1.UnsupportedFeatureError('createMany onConflict { ignore: true }', 'mysql', 'MySQL cannot tell the inserted rows from the skipped ones afterwards; use onConflict: { target, update }.');
978
+ }
979
+ let n = 0;
980
+ const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
981
+ const params = input.rowValues.flat();
982
+ const update = input.update(params, { target: input.table, incoming: MYSQL_INCOMING_ALIAS });
983
+ if (update.where) {
984
+ throw new errors_js_1.UnsupportedFeatureError('A predicate on createMany onConflict', 'mysql', 'ON DUPLICATE KEY UPDATE has no WHERE slot.');
985
+ }
986
+ return {
987
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} AS ${MYSQL_INCOMING_ALIAS} ` +
988
+ `ON DUPLICATE KEY UPDATE ${update.setClauses.join(', ')}`,
989
+ params,
990
+ };
991
+ },
715
992
  buildDefaultValuesInsertStatement(input) {
716
993
  // MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
717
994
  // repeats for any row count, so this is the same statement the column-driven
@@ -739,12 +1016,39 @@ exports.mysqlDialect = {
739
1016
  // unless a functional/generated index exists.
740
1017
  return `LOWER(${column}) LIKE LOWER(${paramRef})`;
741
1018
  },
1019
+ /**
1020
+ * `!`, not `\`: with the server's default `sql_mode` a backslash escapes
1021
+ * inside a string literal, so the `ESCAPE '\'` every other engine emits is an
1022
+ * unterminated string here, and only a session running with
1023
+ * `NO_BACKSLASH_ESCAPES` reads it as one backslash. Turbine sets that mode on
1024
+ * the connections it opens, never on a pool the caller supplies (it would
1025
+ * change how the caller's own SQL parses), so the clause has to mean the same
1026
+ * thing in both modes, and `'!'` does. The operand is bound, so a backslash in
1027
+ * the VALUE is a plain character under this escape.
1028
+ */
1029
+ likeEscapeCharacter: MYSQL_LIKE_ESCAPE,
1030
+ /** Prefix `%`, `_` and the escape character itself with {@link MYSQL_LIKE_ESCAPE}. */
1031
+ escapeLikePattern(value) {
1032
+ return value.replace(MYSQL_LIKE_META_G, `${MYSQL_LIKE_ESCAPE}$&`);
1033
+ },
742
1034
  buildJsonContains(column, paramRef) {
743
1035
  return `JSON_CONTAINS(${column}, ${paramRef})`;
744
1036
  },
745
1037
  buildJsonPathExtract(column, pathParamRef) {
746
1038
  return `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${pathParamRef}))`;
747
1039
  },
1040
+ // MySQL compares two JSON values by JSON type and value, so binding the
1041
+ // operand as JSON text makes `{ path, equals }` exact for every shape:
1042
+ // 5 vs "5" and true vs 1 differ, 5 equals 5.0, null matches JSON null, and
1043
+ // arrays and objects compare structurally (object key order does not matter).
1044
+ // A missing path extracts SQL NULL, which matches nothing. With no path the
1045
+ // whole document is compared.
1046
+ buildJsonPathEquals(input) {
1047
+ const { value } = input;
1048
+ const text = typeof value === 'bigint' ? value.toString() : JSON.stringify(value);
1049
+ const lhs = input.pathRef === undefined ? input.column : `JSON_EXTRACT(${input.column}, ${input.pathRef})`;
1050
+ return `${lhs} = CAST(${input.bind(text)} AS JSON)`;
1051
+ },
748
1052
  // ---- Type mapping -------------------------------------------------------
749
1053
  typeToTypeScript(dialectType, nullable) {
750
1054
  return mysqlTypeToTs(dialectType, nullable);
@@ -756,6 +1060,24 @@ exports.mysqlDialect = {
756
1060
  buildColumnType(input) {
757
1061
  return mysqlColumnType(input.type, input.maxLength);
758
1062
  },
1063
+ /**
1064
+ * MySQL declares an enum's labels on the column, `ENUM('a', 'b')`, which
1065
+ * refuses any other value in strict mode. A label's quotes are doubled. A
1066
+ * label holding a backslash is refused: whether `\` escapes in a string
1067
+ * literal depends on the session's NO_BACKSLASH_ESCAPES (Turbine's own
1068
+ * connections set it, other clients do not), so the same DDL would declare
1069
+ * a different label depending on what runs it.
1070
+ */
1071
+ buildEnumColumn(column, labels) {
1072
+ const literals = labels.map((label) => {
1073
+ if (label.includes('\\')) {
1074
+ throw new errors_js_1.ValidationError(`The enum label ${JSON.stringify(label)} on column ${column} holds a backslash, which MySQL reads ` +
1075
+ 'differently with and without NO_BACKSLASH_ESCAPES. Use a label without one.');
1076
+ }
1077
+ return `'${label.replace(/'/g, "''")}'`;
1078
+ });
1079
+ return { type: `ENUM(${literals.join(', ')})` };
1080
+ },
759
1081
  buildColumnDefinition(input) {
760
1082
  const isSerial = /serial/i.test(input.type);
761
1083
  const parts = [input.name, this.buildColumnType(input)];
@@ -839,8 +1161,8 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
839
1161
  * same logical schema, legacy-first naming, per-column disambiguation, and
840
1162
  * collision resolution against scalar column fields included.
841
1163
  */
842
- function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
843
- return (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable);
1164
+ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, logger) {
1165
+ return (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable, logger);
844
1166
  }
845
1167
  /**
846
1168
  * Introspect a MySQL database into the same {@link SchemaMetadata} shape the
@@ -849,7 +1171,8 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
849
1171
  *
850
1172
  * @param exec Runs a parameterized (`:p1`, `:p2`, …) query and returns rows.
851
1173
  * @param schema The MySQL database (schema) name to introspect.
852
- * @param options Optional include / exclude table-name filters.
1174
+ * @param options Optional include / exclude table-name filters, and the logger
1175
+ * that receives relation-naming notes (console when absent).
853
1176
  */
854
1177
  async function introspectMysqlWith(exec, schema, options = {}) {
855
1178
  // ----- Tables -----
@@ -862,7 +1185,7 @@ async function introspectMysqlWith(exec, schema, options = {}) {
862
1185
  COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH
863
1186
  FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
864
1187
  const columnsByTable = new Map();
865
- const enums = {};
1188
+ const enums = (0, introspect_js_1.nullRecord)();
866
1189
  for (const c of columnRows) {
867
1190
  if (!tableSet.has(c.TABLE_NAME))
868
1191
  continue;
@@ -890,6 +1213,8 @@ async function introspectMysqlWith(exec, schema, options = {}) {
890
1213
  tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
891
1214
  nullable,
892
1215
  hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
1216
+ // The server numbers it, so a create's re-read can use the insert id.
1217
+ isGenerated: /auto_increment/i.test(c.EXTRA),
893
1218
  isArray: false,
894
1219
  arrayType: undefined,
895
1220
  pgArrayType: 'text[]',
@@ -971,16 +1296,19 @@ async function introspectMysqlWith(exec, schema, options = {}) {
971
1296
  }
972
1297
  }
973
1298
  // ----- Relations -----
974
- const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable);
1299
+ // One field per column, as the PostgreSQL introspector requires.
1300
+ for (const [tableName, cols] of columnsByTable)
1301
+ (0, introspect_js_1.assertDistinctColumnFields)(tableName, cols);
1302
+ const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
975
1303
  // ----- Assemble TableMetadata -----
976
- const tables = {};
1304
+ const tables = (0, introspect_js_1.nullRecord)();
977
1305
  for (const tableName of tableNames) {
978
1306
  const columns = columnsByTable.get(tableName) ?? [];
979
- const columnMap = {};
980
- const reverseColumnMap = {};
1307
+ const columnMap = (0, introspect_js_1.nullRecord)();
1308
+ const reverseColumnMap = (0, introspect_js_1.nullRecord)();
981
1309
  const dateColumns = new Set();
982
- const dialectTypes = {};
983
- const pgTypes = {};
1310
+ const dialectTypes = (0, introspect_js_1.nullRecord)();
1311
+ const pgTypes = (0, introspect_js_1.nullRecord)();
984
1312
  const allColumns = [];
985
1313
  for (const col of columns) {
986
1314
  columnMap[col.field] = col.name;
@@ -1002,7 +1330,7 @@ async function introspectMysqlWith(exec, schema, options = {}) {
1002
1330
  allColumns,
1003
1331
  primaryKey: pkByTable.get(tableName) ?? [],
1004
1332
  uniqueColumns: uniqueByTable.get(tableName) ?? [],
1005
- relations: relationsByTable.get(tableName) ?? {},
1333
+ relations: relationsByTable.get(tableName) ?? (0, introspect_js_1.nullRecord)(),
1006
1334
  indexes: indexesByTable.get(tableName) ?? [],
1007
1335
  };
1008
1336
  }
@@ -1032,7 +1360,11 @@ async function introspectMysql(options) {
1032
1360
  // try/finally runs the finally BEFORE the promise settles, so `pool.end()`
1033
1361
  // closed the pool out from under every introspection query and the whole
1034
1362
  // call failed with mysql2's "Pool is closed".
1035
- return await introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
1363
+ return await introspectMysqlWith(exec, schemaName, {
1364
+ include: options.include,
1365
+ exclude: options.exclude,
1366
+ logger: options.logger,
1367
+ });
1036
1368
  }
1037
1369
  finally {
1038
1370
  await pool.end();
@@ -1043,7 +1375,7 @@ async function introspectMysql(options) {
1043
1375
  // ---------------------------------------------------------------------------
1044
1376
  /** mysql2 connection flags Turbine pins for correct behavior. */
1045
1377
  const MYSQL_DRIVER_FLAGS = {
1046
- // Named placeholders (`:pN`), see paramPlaceholder + toNamedBinding.
1378
+ // Named placeholders (`:pN`), see paramPlaceholder + bindStatement.
1047
1379
  namedPlaceholders: true,
1048
1380
  // Safe-int policy matching Postgres int8: number when it fits, decimal string
1049
1381
  // otherwise. DECIMAL always comes back as a string. No global state mutated.
@@ -1169,11 +1501,15 @@ async function turbineMysql(target, schema, options = {}) {
1169
1501
  connectionLimit: options.connectionLimit ?? 10,
1170
1502
  ...MYSQL_DRIVER_FLAGS,
1171
1503
  });
1172
- // Every NEW physical connection: disable backslash string escaping so the
1173
- // builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
1174
- // MySQL, by default MySQL would parse `'\'` as an escaped quote (syntax
1175
- // error). This also makes string-literal semantics match Postgres. Turbine
1176
- // parameterizes every value, so no behavior depends on backslash escaping.
1504
+ // Every NEW physical connection: disable backslash string escaping, so a
1505
+ // string literal in raw SQL on a Turbine-owned pool reads the way it does
1506
+ // on PostgreSQL. Nothing Turbine GENERATES depends on it any more: the LIKE
1507
+ // clause names `!` (see mysqlDialect.likeEscapeCharacter), a JSON_OBJECT
1508
+ // key holding a backslash is written in hex (mysqlKeyLiteral), and every
1509
+ // value is a bound parameter, so the SQL means the same thing on a pool
1510
+ // the caller supplies, which keeps the caller's own mode. This stays
1511
+ // because removing it would change how raw SQL already written against an
1512
+ // owned pool parses.
1177
1513
  //
1178
1514
  // NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
1179
1515
  // *raw* (callback-style) connection, its `.query()` returns a non-thenable
@@ -1186,32 +1522,46 @@ async function turbineMysql(target, schema, options = {}) {
1186
1522
  pool = new MysqlPool(mysql2Pool);
1187
1523
  owns = true;
1188
1524
  }
1189
- // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
1190
- const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
1191
- assertSupportedVersion(String(versionRows[0]?.v ?? ''));
1192
- // Everything that is not a pool-construction option is client config and
1193
- // rides through untouched, so a new TurbineConfig option works here the day
1194
- // it lands. The engine-owned keys come last, so no caller can unbind the
1195
- // dialect.
1196
- const { connectionLimit: _connectionLimit, ...clientConfig } = options;
1197
- const client = new client_js_1.TurbineClient({
1198
- ...clientConfig,
1199
- pool,
1200
- dialect: exports.mysqlDialect,
1201
- preparedStatements: false,
1202
- },
1203
- // The join strategy is the only read path that consults a recorded dialect
1204
- // type, so a schema that spells a boolean any other way would read 1/0
1205
- // there and `true`/`false` everywhere else.
1206
- normalizeBooleanDialectTypes(schema));
1525
+ let client;
1526
+ try {
1527
+ // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
1528
+ const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
1529
+ assertSupportedVersion(String(versionRows[0]?.v ?? ''));
1530
+ // Everything that is not a pool-construction option is client config and
1531
+ // rides through untouched, so a new TurbineConfig option works here the day
1532
+ // it lands. The engine-owned keys come last, so no caller can unbind the
1533
+ // dialect.
1534
+ const { connectionLimit: _connectionLimit, ...clientConfig } = options;
1535
+ client = new client_js_1.TurbineClient({
1536
+ ...clientConfig,
1537
+ pool,
1538
+ dialect: exports.mysqlDialect,
1539
+ preparedStatements: false,
1540
+ },
1541
+ // The join strategy is the only read path that consults a recorded dialect
1542
+ // type, so a schema that spells a boolean any other way would read 1/0
1543
+ // there and `true`/`false` everywhere else.
1544
+ normalizeBooleanDialectTypes(schema));
1545
+ }
1546
+ catch (err) {
1547
+ // A failed factory hands the caller nothing to close, so a pool it BUILT
1548
+ // is closed here: the version probe's connection would otherwise keep the
1549
+ // process alive (the client constructor refuses a bad config, a static
1550
+ // global filter naming no table or column for one). A pool the caller
1551
+ // supplied is the caller's and stays open.
1552
+ if (owns)
1553
+ await pool.end().catch(() => { });
1554
+ throw err;
1555
+ }
1207
1556
  if (owns) {
1208
1557
  // Turbine built this pool, so disconnect()/end() must close it. External
1209
1558
  // pools (injection) stay the caller's responsibility (disconnect() no-op),
1210
1559
  // consistent with turbineHttp / turbineSqlite.
1211
1560
  const baseDisconnect = client.disconnect.bind(client);
1212
- const close = async () => {
1213
- await baseDisconnect();
1561
+ const close = async (options) => {
1562
+ const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
1214
1563
  await pool.end();
1564
+ return result ?? undefined;
1215
1565
  };
1216
1566
  const patch = client;
1217
1567
  patch.disconnect = close;