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/mysql.js CHANGED
@@ -62,12 +62,13 @@
62
62
  * await db.disconnect();
63
63
  * ```
64
64
  */
65
- import { TurbineClient } from './client.js';
65
+ import { TurbineClient, } from './client.js';
66
66
  import { postgresDialect, } from './dialect.js';
67
- import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, UnsupportedFeatureError, } from './errors.js';
68
- import { applyTableFilters, deriveEngineRelations } from './introspect.js';
67
+ import { ConnectionError, malformedConnectionStringMessage, markValueBearingMessage, UnsupportedFeatureError, ValidationError, } from './errors.js';
68
+ import { applyTableFilters, assertDistinctColumnFields, deriveEngineRelations, nullRecord } from './introspect.js';
69
69
  import importOptionalPeer from './optional-peer-import.cjs';
70
70
  import { isDateType, snakeToCamel, } from './schema.js';
71
+ import { rewritePositionalPlaceholders, scanPlaceholders } from './sql-placeholders.js';
71
72
  // ---------------------------------------------------------------------------
72
73
  // Value coercion (params in)
73
74
  // ---------------------------------------------------------------------------
@@ -76,7 +77,7 @@ import { isDateType, snakeToCamel, } from './schema.js';
76
77
  * 1/0 (MySQL has no boolean type; `TINYINT(1)` stores 1/0), `undefined`/`null`
77
78
  * → NULL, `Date`/`Uint8Array` pass through (mysql2 formats them), and any
78
79
  * remaining object/array → JSON text (matches how Turbine already pre-serializes
79
- * JSON filter / `IN`-list params).
80
+ * JSON filter params). An `in` list element binds through here too, one by one.
80
81
  */
81
82
  function toMysqlParam(value) {
82
83
  if (value === undefined || value === null)
@@ -85,31 +86,136 @@ function toMysqlParam(value) {
85
86
  case 'boolean':
86
87
  return value ? 1 : 0;
87
88
  case 'number':
89
+ // MySQL has no NaN or Infinity: the server stored NaN as 0 without a
90
+ // word, and refused Infinity with a raw driver error.
91
+ if (!Number.isFinite(value)) {
92
+ throw new ValidationError(`MySQL cannot store or compare ${value}: a bound number must be finite. Pass a finite number, or null.`);
93
+ }
94
+ return value;
88
95
  case 'bigint':
89
96
  case 'string':
90
97
  return value;
91
98
  }
92
- if (value instanceof Date)
99
+ if (value instanceof Date) {
100
+ // mysql2 renders an Invalid Date as '0000-00-00 00:00:00.000', which the
101
+ // server then reports as that value instead of the caller's mistake.
102
+ if (Number.isNaN(value.getTime())) {
103
+ throw new ValidationError('MySQL cannot bind an Invalid Date. Pass a valid Date, or null.');
104
+ }
93
105
  return value;
106
+ }
94
107
  if (value instanceof Uint8Array)
95
108
  return value; // also covers Buffer
96
109
  return JSON.stringify(value);
97
110
  }
98
111
  /**
99
- * Bind the positional `params[]` (in 1-indexed generation order) to the named
100
- * `:p1`, `:p2`, … placeholders the dialect emits. Mapping by NAME makes binding
101
- * independent of where each placeholder lands in the SQL text, the same
102
- * guarantee Postgres' numbered `$N` gives. Returns `undefined` for parameter-less
103
- * statements (BEGIN/COMMIT/DDL) so they bind nothing.
112
+ * An `in` / `notIn` list, as the builder hands it to the shim: ONE param, so
113
+ * the SQL text and its cache entry stay independent of the list's length (see
114
+ * {@link mysqlDialect.buildInClause}). The shim expands it into one placeholder
115
+ * per element, each bound exactly as an equality operand is, which is what
116
+ * makes `{ in: [v] }` match the rows `{ equals: v }` matches on every column
117
+ * type: a `JSON_TABLE` list compared JSON values, so a boolean, a date or time,
118
+ * a binary value, a decimal or bigint written as a string, and every
119
+ * case-folded string missed, and `notIn` then matched every row.
120
+ */
121
+ class MysqlInList {
122
+ values;
123
+ constructor(values) {
124
+ this.values = values;
125
+ }
126
+ }
127
+ /** MySQL's limit on placeholders in one prepared statement. */
128
+ const MYSQL_MAX_PLACEHOLDERS = 65_535;
129
+ /** A list of `n` elements is sent padded to the next power of two (by repeating its last element). */
130
+ function paddedListLength(n) {
131
+ return n <= 1 ? n : 2 ** Math.ceil(Math.log2(n));
132
+ }
133
+ /** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
134
+ function refusePlaceholder(message) {
135
+ throw new ValidationError(message);
136
+ }
137
+ /**
138
+ * The statement text and named binding for one execution: `$n` placeholders
139
+ * (the spelling `rawUnsafe` documents) rewritten to `:pN`, every
140
+ * {@link MysqlInList} expanded, and every other value bound by name. Mapping by
141
+ * NAME makes binding independent of where each placeholder lands in the SQL
142
+ * text, the same guarantee Postgres' numbered `$N` gives. `binding` is
143
+ * `undefined` when nothing is bound, so parameter-less statements (BEGIN,
144
+ * COMMIT, DDL) run through `.query()`.
104
145
  */
105
- function toNamedBinding(values) {
146
+ function bindStatement(sql, values) {
106
147
  if (values.length === 0)
107
- return undefined;
148
+ return { sql, binding: undefined };
149
+ const text = rewritePositionalPlaceholders(sql, 'mysql', values.length, (i) => `:p${i}`, refusePlaceholder);
108
150
  const named = {};
151
+ let lists;
109
152
  for (let i = 0; i < values.length; i++) {
110
- named[`p${i + 1}`] = toMysqlParam(values[i]);
153
+ const value = values[i];
154
+ if (value instanceof MysqlInList) {
155
+ lists ??= new Map();
156
+ lists.set(`p${i + 1}`, value);
157
+ }
158
+ else {
159
+ named[`p${i + 1}`] = toMysqlParam(value);
160
+ }
161
+ }
162
+ if (lists === undefined)
163
+ return { sql: text, binding: named };
164
+ const expanded = expandInLists(text, lists, named);
165
+ return { sql: expanded, binding: Object.keys(named).length > 0 ? named : undefined };
166
+ }
167
+ /**
168
+ * Replace each list placeholder (`:p3`) with one placeholder per element
169
+ * (`:p3_0, :p3_1, ...`), binding each element into `named`. An empty list
170
+ * becomes a subquery with no rows, so `IN` is false and `NOT IN` true, as
171
+ * Postgres answers `= ANY('{}')` and `!= ALL('{}')`.
172
+ *
173
+ * Padding to a power of two bounds the statement texts a caller can produce
174
+ * (mysql2 prepares and caches one server-side statement per distinct text and
175
+ * connection, and the server caps the total across every client at
176
+ * `max_prepared_stmt_count`): about seventeen lengths per list instead of one
177
+ * per length. A repeated element changes neither `IN` nor `NOT IN`.
178
+ */
179
+ function expandInLists(sql, lists, named) {
180
+ const tokens = scanPlaceholders(sql, 'mysql');
181
+ const listTokens = tokens.filter((t) => t.kind === 'named' && lists.has(t.name));
182
+ const others = tokens.length - listTokens.length;
183
+ let padded = 0;
184
+ let exact = 0;
185
+ for (const t of listTokens) {
186
+ const n = lists.get(t.name).values.length;
187
+ padded += paddedListLength(n);
188
+ exact += n;
189
+ }
190
+ if (others + exact > MYSQL_MAX_PLACEHOLDERS) {
191
+ throw new ValidationError(`An \`in\` / \`notIn\` list on MySQL binds one placeholder per element, and this statement needs ` +
192
+ `${others + exact} where MySQL allows ${MYSQL_MAX_PLACEHOLDERS}. Split the list across several queries.`);
193
+ }
194
+ const pad = others + padded <= MYSQL_MAX_PLACEHOLDERS;
195
+ for (const [name, list] of lists) {
196
+ list.values.forEach((v, j) => {
197
+ named[`${name}_${j}`] = toMysqlParam(v);
198
+ });
111
199
  }
112
- return named;
200
+ let out = '';
201
+ let last = 0;
202
+ for (const t of listTokens) {
203
+ const n = lists.get(t.name).values.length;
204
+ let replacement;
205
+ if (n === 0) {
206
+ replacement = 'SELECT NULL FROM DUAL WHERE FALSE';
207
+ }
208
+ else {
209
+ const parts = [];
210
+ const length = pad ? paddedListLength(n) : n;
211
+ for (let j = 0; j < length; j++)
212
+ parts.push(`:${t.name}_${Math.min(j, n - 1)}`);
213
+ replacement = parts.join(', ');
214
+ }
215
+ out += sql.slice(last, t.start) + replacement;
216
+ last = t.end;
217
+ }
218
+ return out + sql.slice(last);
113
219
  }
114
220
  // ---------------------------------------------------------------------------
115
221
  // Result shaping + error translation
@@ -244,10 +350,50 @@ function augmentMysqlError(err) {
244
350
  case 1205:
245
351
  target.code = '40001';
246
352
  return err;
247
- default:
353
+ default: {
354
+ // Data exceptions (a bound value that does not fit the column) take the
355
+ // SQLSTATE class 22 path in `wrapPgError`, which builds a ValidationError
356
+ // naming the column and redacts the value-bearing message in 'safe' mode.
357
+ const sqlstate = mysqlDataExceptionState(e.errno, e.sqlState, msg);
358
+ if (sqlstate === undefined)
359
+ return err;
360
+ target.code = sqlstate;
361
+ // "... for column 'name' at row 1", or "... in value for column 't.meta'."
362
+ const column = /for column '([^']+)'/i.exec(msg)?.[1];
363
+ if (column)
364
+ target.column = column.slice(column.lastIndexOf('.') + 1);
248
365
  return err;
366
+ }
249
367
  }
250
368
  }
369
+ /**
370
+ * The SQLSTATE a MySQL data exception maps to, or `undefined` when `errno` is
371
+ * not one. MySQL reports several of these under the generic `HY000`, so the
372
+ * common ones are mapped by errno to the code PostgreSQL uses for the same
373
+ * failure; anything else MySQL itself files under class 22 keeps its own code.
374
+ */
375
+ function mysqlDataExceptionState(errno, sqlState, message) {
376
+ switch (errno) {
377
+ case 1406: // ER_DATA_TOO_LONG
378
+ return '22001';
379
+ case 1264: // ER_WARN_DATA_OUT_OF_RANGE
380
+ case 1690: // ER_DATA_OUT_OF_RANGE
381
+ return '22003';
382
+ case 1366: // ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: "Incorrect integer value: 'abc' for column ..."
383
+ case 1265: // WARN_DATA_TRUNCATED: a value outside an ENUM, a malformed DECIMAL
384
+ return '22P02';
385
+ case 1292: // ER_TRUNCATED_WRONG_VALUE: an invalid datetime, or a non-numeric string compared as a number
386
+ case 1411: // ER_WRONG_VALUE_FOR_TYPE
387
+ return /\b(?:date|time|datetime|timestamp)\b/i.test(message) ? '22007' : '22P02';
388
+ case 1365: // ER_DIVISION_BY_ZERO
389
+ return '22012';
390
+ case 3140: // ER_INVALID_JSON_TEXT
391
+ case 3141: // ER_INVALID_JSON_TEXT_IN_PARAM
392
+ case 3144: // ER_INVALID_JSON_CHARSET
393
+ return '22032';
394
+ }
395
+ return typeof sqlState === 'string' && /^22[0-9A-Z]{3}$/.test(sqlState) ? sqlState : undefined;
396
+ }
251
397
  function normalizeQueryArgs(arg, values) {
252
398
  if (typeof arg === 'string')
253
399
  return { text: arg, params: values ?? [] };
@@ -261,9 +407,10 @@ function normalizeQueryArgs(arg, values) {
261
407
  * the `{ p1, p2, … }` object built from the positional params.
262
408
  */
263
409
  async function execOne(runner, sql, params) {
264
- const binding = toNamedBinding(params);
410
+ const bound = bindStatement(sql, params);
411
+ const binding = bound.binding;
265
412
  try {
266
- const [result, fields] = binding ? await runner.execute(sql, binding) : await runner.query(sql);
413
+ const [result, fields] = binding ? await runner.execute(bound.sql, binding) : await runner.query(bound.sql);
267
414
  return shapeResult(result, fields);
268
415
  }
269
416
  catch (err) {
@@ -376,6 +523,18 @@ export function mysqlTypeToTs(dialectType, nullable, columnType) {
376
523
  * matter are {@link mysqlTypeToTs} and {@link mysqlDialect.jsonWireRule}.
377
524
  */
378
525
  const MYSQL_BOOLEAN_TYPE = 'tinyint(1)';
526
+ /** Integer column types narrower than 64 bits, in MySQL and code-first (PostgreSQL) spellings. */
527
+ const SMALL_INTEGER_TYPES = new Set([
528
+ 'tinyint',
529
+ 'smallint',
530
+ 'mediumint',
531
+ 'int',
532
+ 'integer',
533
+ 'int2',
534
+ 'int4',
535
+ 'serial',
536
+ 'smallserial',
537
+ ]);
379
538
  /**
380
539
  * Rewrite every boolean column's recorded dialect type to
381
540
  * {@link MYSQL_BOOLEAN_TYPE}, returning a schema that is safe to key
@@ -428,6 +587,11 @@ function isMysqlDateType(dialectType) {
428
587
  }
429
588
  /** Map a schema-builder (Postgres-flavored) column type to MySQL DDL. */
430
589
  function mysqlColumnType(type, maxLength) {
590
+ // An enum column's type from `buildEnumColumn`, already spelled for MySQL. It
591
+ // returns first: its labels are free text, and one named `int` or `date`
592
+ // would otherwise match a pattern below.
593
+ if (/^ENUM\(/.test(type))
594
+ return type;
431
595
  const t = type.toUpperCase();
432
596
  if (/BIGSERIAL/.test(t))
433
597
  return 'BIGINT AUTO_INCREMENT';
@@ -451,14 +615,19 @@ function mysqlColumnType(type, maxLength) {
451
615
  return 'JSON';
452
616
  if (/UUID/.test(t))
453
617
  return 'CHAR(36)';
618
+ // DATETIME(3), not bare DATETIME: the bare type has zero fractional digits and
619
+ // rounds a JS Date's milliseconds away on write.
454
620
  if (/TIMESTAMPTZ|TIMESTAMP|DATETIME/.test(t))
455
- return 'DATETIME';
621
+ return 'DATETIME(3)';
456
622
  if (/\bDATE\b/.test(t))
457
623
  return 'DATE';
458
624
  if (/\bTIME\b/.test(t))
459
625
  return 'TIME';
626
+ // A bounded binary column is VARBINARY(n): a BLOB cannot be a key.
627
+ if (/VARBINARY/.test(t))
628
+ return `VARBINARY(${maxLength ?? 255})`;
460
629
  if (/BYTEA|BLOB/.test(t))
461
- return 'BLOB';
630
+ return maxLength != null ? `VARBINARY(${maxLength})` : 'BLOB';
462
631
  if (/VARCHAR/.test(t))
463
632
  return maxLength != null ? `VARCHAR(${maxLength})` : 'VARCHAR(255)';
464
633
  if (/CHAR/.test(t))
@@ -472,6 +641,32 @@ function mysqlColumnType(type, maxLength) {
472
641
  // ---------------------------------------------------------------------------
473
642
  // mysqlDialect, the full Dialect contract for MySQL 8
474
643
  // ---------------------------------------------------------------------------
644
+ /** The row alias a multi-row upsert gives its incoming rows (`AS ... ON DUPLICATE KEY UPDATE`). */
645
+ const MYSQL_INCOMING_ALIAS = '`turbine_excluded`';
646
+ /**
647
+ * The LIKE escape character on MySQL (see `mysqlDialect.likeEscapeCharacter`).
648
+ * One character, nothing special inside a single-quoted literal in either
649
+ * `sql_mode`, and not a LIKE metacharacter.
650
+ */
651
+ const MYSQL_LIKE_ESCAPE = '!';
652
+ /** The LIKE metacharacters under {@link MYSQL_LIKE_ESCAPE}: itself, `%` and `_`. */
653
+ const MYSQL_LIKE_META_G = /[!%_]/g;
654
+ /**
655
+ * A `JSON_OBJECT` key as SQL text that reads the same in every `sql_mode`.
656
+ * A key without a backslash is a plain quoted literal. A backslash has no
657
+ * literal spelling that both modes agree on (`'a\\b'` is one backslash by
658
+ * default and two under `NO_BACKSLASH_ESCAPES`, which Turbine's own
659
+ * connections set and a caller-supplied pool may not), so such a key is
660
+ * written as its UTF-8 bytes in hex, which no mode re-reads.
661
+ */
662
+ function mysqlKeyLiteral(key, dialect) {
663
+ if (!key.includes('\\'))
664
+ return `'${dialect.escapeStringLiteral(key)}'`;
665
+ let hex = '';
666
+ for (const byte of new TextEncoder().encode(key))
667
+ hex += byte.toString(16).padStart(2, '0');
668
+ return `CONVERT(X'${hex}' USING utf8mb4)`;
669
+ }
475
670
  /**
476
671
  * MySQL 8 implementation of the {@link Dialect} contract. Backtick identifier
477
672
  * quoting, named `:pN` placeholders (NOT positional `?`, see the module
@@ -483,6 +678,47 @@ function mysqlColumnType(type, maxLength) {
483
678
  export const mysqlDialect = {
484
679
  ...postgresDialect,
485
680
  name: 'mysql',
681
+ capabilities: Object.freeze({
682
+ distinctOn: false,
683
+ positionalJson: false,
684
+ nullsOrderingClause: false,
685
+ nullsSortHigh: false,
686
+ insensitiveInList: false,
687
+ jsonDocumentEquality: 'exact',
688
+ jsonExtractTyped: false,
689
+ jsonExactNumericCast: false,
690
+ numericTextComparison: true,
691
+ enforcesForeignKeys: true,
692
+ partitionLimitPushdown: false,
693
+ // A zone-less literal reads in the session time zone; only `date` columns rewrite.
694
+ utcTimestampRewrite: false,
695
+ paramTypeCasts: false,
696
+ namedEnumTypes: false,
697
+ structuredValueColumns: 'scalars-and-text',
698
+ // Counted in a 16-bit field.
699
+ maxBindParameters: 65535,
700
+ maxInsertRows: Number.POSITIVE_INFINITY,
701
+ upsertConflictTarget: 'any-unique-key',
702
+ setClauseOrder: 'left-to-right',
703
+ reselectLocksRows: true,
704
+ integerDivision: 'DIV',
705
+ updateExpressions: true,
706
+ // Caught by the onConflict re-read, which runs in the reselect transaction.
707
+ bulkUpsertAppliesRepeatedKeys: true,
708
+ readOnlyTransactions: true,
709
+ connectionParameters: false,
710
+ backendCancel: false,
711
+ idleTransactionTimeout: false,
712
+ abortsTransactionOnError: false,
713
+ connectionDriver: 'engine-driver',
714
+ // mysql2 execute() prepares each statement text; max_prepared_stmt_count caps them server-wide.
715
+ preparesEveryStatement: true,
716
+ statementSyntax: 'mysql',
717
+ // An inline REFERENCES is parsed and ignored.
718
+ inlineForeignKeys: false,
719
+ // TEXT / BLOB need a prefix length in a key (ER_BLOB_KEY_WITHOUT_LENGTH).
720
+ unboundedTextKeys: false,
721
+ }),
486
722
  // No RETURNING → run the write, then re-SELECT by PK/where (Phase-0 reselect).
487
723
  resultStrategy: 'reselect',
488
724
  supportsReturning: false,
@@ -503,6 +739,13 @@ export const mysqlDialect = {
503
739
  // Postgres-only in this release (flipping it on is a one-line change + tests).
504
740
  supportsLateralJoin: false,
505
741
  supportsPlanCacheMode: false,
742
+ // FOR UPDATE / FOR SHARE (8.0); there is no NO KEY UPDATE or KEY SHARE.
743
+ rowLockModes: ['update', 'share'],
744
+ // No percentile_cont / percentile_disc.
745
+ supportsPercentileAggregates: false,
746
+ // INSERT ... AS alias ON DUPLICATE KEY UPDATE, with the refusals that
747
+ // keyless conflict resolution needs.
748
+ supportsBulkUpsert: true,
506
749
  // GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
507
750
  supportsAdvisoryLock: true,
508
751
  // Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
@@ -535,15 +778,18 @@ export const mysqlDialect = {
535
778
  * `NO_BACKSLASH_ESCAPES`, `\` is an escape character inside a string literal,
536
779
  * so doubling only `'` (the inherited Postgres rule) leaves a value ending in
537
780
  * a backslash able to escape its own closing quote. Escape the backslash
538
- * first, then the quote; every other byte is safe raw. The only caller is
539
- * `buildJsonObject` (relation / column names from schema metadata), so this
540
- * is defence in depth rather than a user-value path.
781
+ * first, then the quote; every other byte is safe raw.
782
+ *
783
+ * That is right for the server's default `sql_mode` only: under
784
+ * `NO_BACKSLASH_ESCAPES` (which Turbine sets on the connections it opens) the
785
+ * doubled backslash reads as two. So `buildJsonObject`, the one caller, never
786
+ * hands it a key holding a backslash (see {@link mysqlKeyLiteral}).
541
787
  */
542
788
  escapeStringLiteral(value) {
543
789
  return value.replace(/\\/g, '\\\\').replace(/'/g, "''");
544
790
  },
545
791
  buildJsonObject(pairs) {
546
- const args = pairs.map(([key, expr]) => `'${this.escapeStringLiteral(key)}', ${expr}`);
792
+ const args = pairs.map(([key, expr]) => `${mysqlKeyLiteral(key, this)}, ${expr}`);
547
793
  return `JSON_OBJECT(${args.join(', ')})`;
548
794
  },
549
795
  // JSON_ARRAYAGG over an empty group returns NULL → COALESCE to JSON_ARRAY().
@@ -573,7 +819,9 @@ export const mysqlDialect = {
573
819
  // came back 9007199254740992, and DECIMAL '1000.50' came back 1000.5.
574
820
  // mysql2 hands both back as STRINGS at top level (supportBigNumbers, and
575
821
  // DECIMAL is always a string), so carry the text and keep it.
576
- if (t === 'bigint') {
822
+ // `int8` is the spelling a code-first (`defineSchema`) schema records for
823
+ // the same BIGINT column, so it needs the same rule.
824
+ if (t === 'bigint' || t === 'int8') {
577
825
  return {
578
826
  sql: (ref) => `CAST(${ref} AS CHAR)`,
579
827
  decode: (value) => {
@@ -631,9 +879,9 @@ export const mysqlDialect = {
631
879
  // parsed value. It is also what `pg` returns for json/jsonb, so this is
632
880
  // Postgres parity rather than a MySQL convention.
633
881
  //
634
- // Contrast SQLite and SQL Server, which return TEXT for the same schema:
635
- // neither has a JSON column type, so the value there really is a string in
636
- // a text column and there is nothing to parse it back from.
882
+ // Contrast SQLite, which has no JSON column type and stores the document as
883
+ // text: its driver shim parses the text back for a column the schema
884
+ // declares json/jsonb, and its own rule does the same through a join.
637
885
  // Binary columns are worse than lossy: JSON_OBJECT emits MySQL's internal
638
886
  // `base64:type15:…` marker string, so the caller got that text instead of
639
887
  // bytes. Carry hex and rebuild the buffer.
@@ -648,6 +896,14 @@ export const mysqlDialect = {
648
896
  castAggregate(expr, target) {
649
897
  return `CAST(${expr} AS ${target === 'int' ? 'SIGNED' : 'DECIMAL(65,30)'})`;
650
898
  },
899
+ // AVG over an integer column is a DECIMAL with only four more digits of
900
+ // scale (div_precision_increment), so 4/3 came back 1.3333 where PostgreSQL
901
+ // returns 1.3333333333333333. Adding a DOUBLE zero averages in DOUBLE (the
902
+ // `CAST(... AS DOUBLE)` spelling needs 8.0.17; the engine floor is 8.0.0).
903
+ // BIGINT keeps its exact DECIMAL average, like PostgreSQL's numeric one.
904
+ averageArgument(columnRef, columnType) {
905
+ return SMALL_INTEGER_TYPES.has(columnType.toLowerCase()) ? `(${columnRef} + 0E0)` : undefined;
906
+ },
651
907
  // MySQL has no bare OFFSET: the grammar requires a LIMIT for OFFSET to
652
908
  // attach to, so `offset` without `limit` (valid on Postgres, which the
653
909
  // default path is written for) is a syntax error here. The MySQL manual's
@@ -667,17 +923,16 @@ export const mysqlDialect = {
667
923
  s += ` OFFSET ${offsetPlaceholder}`;
668
924
  return s;
669
925
  },
670
- // No array params in MySQL. JSON_TABLE expands a single JSON-array param into a
671
- // row set, keeping ONE placeholder (so the SQL cache stays valid regardless of
672
- // list length) and handling the empty-list case (zero rows). MySQL coerces the
673
- // non-JSON `expr` to JSON for the comparison, so this works for numbers AND
674
- // strings. JSON_TABLE requires MySQL 8.0+.
926
+ // No array params in MySQL. The list is ONE builder param (a MysqlInList),
927
+ // so the SQL text and its cache entry do not depend on the list's length; the
928
+ // shim expands the placeholder into one per element when the statement runs
929
+ // (see expandInLists). Each element binds as an equality operand does, so the
930
+ // list compares under the column's own type and collation rules.
675
931
  buildInClause(expr, paramRef, negated) {
676
- const inner = `SELECT \`v\` FROM JSON_TABLE(${paramRef}, '$[*]' COLUMNS (\`v\` JSON PATH '$')) AS \`jt\``;
677
- return `${expr} ${negated ? 'NOT IN' : 'IN'} (${inner})`;
932
+ return `${expr} ${negated ? 'NOT IN' : 'IN'} (${paramRef})`;
678
933
  },
679
934
  inClauseParam(values) {
680
- return JSON.stringify(values ?? []);
935
+ return new MysqlInList(Array.isArray(values) ? values : []);
681
936
  },
682
937
  // No RETURNING, resultStrategy 'reselect' re-fetches the row.
683
938
  buildReturningClause() {
@@ -701,6 +956,28 @@ export const mysqlDialect = {
701
956
  params: input.rowValues.flat(),
702
957
  };
703
958
  },
959
+ buildBulkUpsertStatement(input) {
960
+ // 8.0.19's row alias (`VALUES(col)` is deprecated since 8.0.20). There is
961
+ // no conflict target: ON DUPLICATE KEY UPDATE fires on whichever unique key
962
+ // a row violates, which is why the builder refuses a table where another
963
+ // key's columns are in the data, and why it refuses `ignore` and a
964
+ // conflict predicate (this grammar has no slot for one).
965
+ if (!input.update) {
966
+ throw new UnsupportedFeatureError('createMany onConflict { ignore: true }', 'mysql', 'MySQL cannot tell the inserted rows from the skipped ones afterwards; use onConflict: { target, update }.');
967
+ }
968
+ let n = 0;
969
+ const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
970
+ const params = input.rowValues.flat();
971
+ const update = input.update(params, { target: input.table, incoming: MYSQL_INCOMING_ALIAS });
972
+ if (update.where) {
973
+ throw new UnsupportedFeatureError('A predicate on createMany onConflict', 'mysql', 'ON DUPLICATE KEY UPDATE has no WHERE slot.');
974
+ }
975
+ return {
976
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} AS ${MYSQL_INCOMING_ALIAS} ` +
977
+ `ON DUPLICATE KEY UPDATE ${update.setClauses.join(', ')}`,
978
+ params,
979
+ };
980
+ },
704
981
  buildDefaultValuesInsertStatement(input) {
705
982
  // MySQL is the one engine whose empty `VALUES ()` tuple is legal, and it
706
983
  // repeats for any row count, so this is the same statement the column-driven
@@ -728,12 +1005,39 @@ export const mysqlDialect = {
728
1005
  // unless a functional/generated index exists.
729
1006
  return `LOWER(${column}) LIKE LOWER(${paramRef})`;
730
1007
  },
1008
+ /**
1009
+ * `!`, not `\`: with the server's default `sql_mode` a backslash escapes
1010
+ * inside a string literal, so the `ESCAPE '\'` every other engine emits is an
1011
+ * unterminated string here, and only a session running with
1012
+ * `NO_BACKSLASH_ESCAPES` reads it as one backslash. Turbine sets that mode on
1013
+ * the connections it opens, never on a pool the caller supplies (it would
1014
+ * change how the caller's own SQL parses), so the clause has to mean the same
1015
+ * thing in both modes, and `'!'` does. The operand is bound, so a backslash in
1016
+ * the VALUE is a plain character under this escape.
1017
+ */
1018
+ likeEscapeCharacter: MYSQL_LIKE_ESCAPE,
1019
+ /** Prefix `%`, `_` and the escape character itself with {@link MYSQL_LIKE_ESCAPE}. */
1020
+ escapeLikePattern(value) {
1021
+ return value.replace(MYSQL_LIKE_META_G, `${MYSQL_LIKE_ESCAPE}$&`);
1022
+ },
731
1023
  buildJsonContains(column, paramRef) {
732
1024
  return `JSON_CONTAINS(${column}, ${paramRef})`;
733
1025
  },
734
1026
  buildJsonPathExtract(column, pathParamRef) {
735
1027
  return `JSON_UNQUOTE(JSON_EXTRACT(${column}, ${pathParamRef}))`;
736
1028
  },
1029
+ // MySQL compares two JSON values by JSON type and value, so binding the
1030
+ // operand as JSON text makes `{ path, equals }` exact for every shape:
1031
+ // 5 vs "5" and true vs 1 differ, 5 equals 5.0, null matches JSON null, and
1032
+ // arrays and objects compare structurally (object key order does not matter).
1033
+ // A missing path extracts SQL NULL, which matches nothing. With no path the
1034
+ // whole document is compared.
1035
+ buildJsonPathEquals(input) {
1036
+ const { value } = input;
1037
+ const text = typeof value === 'bigint' ? value.toString() : JSON.stringify(value);
1038
+ const lhs = input.pathRef === undefined ? input.column : `JSON_EXTRACT(${input.column}, ${input.pathRef})`;
1039
+ return `${lhs} = CAST(${input.bind(text)} AS JSON)`;
1040
+ },
737
1041
  // ---- Type mapping -------------------------------------------------------
738
1042
  typeToTypeScript(dialectType, nullable) {
739
1043
  return mysqlTypeToTs(dialectType, nullable);
@@ -745,6 +1049,24 @@ export const mysqlDialect = {
745
1049
  buildColumnType(input) {
746
1050
  return mysqlColumnType(input.type, input.maxLength);
747
1051
  },
1052
+ /**
1053
+ * MySQL declares an enum's labels on the column, `ENUM('a', 'b')`, which
1054
+ * refuses any other value in strict mode. A label's quotes are doubled. A
1055
+ * label holding a backslash is refused: whether `\` escapes in a string
1056
+ * literal depends on the session's NO_BACKSLASH_ESCAPES (Turbine's own
1057
+ * connections set it, other clients do not), so the same DDL would declare
1058
+ * a different label depending on what runs it.
1059
+ */
1060
+ buildEnumColumn(column, labels) {
1061
+ const literals = labels.map((label) => {
1062
+ if (label.includes('\\')) {
1063
+ throw new ValidationError(`The enum label ${JSON.stringify(label)} on column ${column} holds a backslash, which MySQL reads ` +
1064
+ 'differently with and without NO_BACKSLASH_ESCAPES. Use a label without one.');
1065
+ }
1066
+ return `'${label.replace(/'/g, "''")}'`;
1067
+ });
1068
+ return { type: `ENUM(${literals.join(', ')})` };
1069
+ },
748
1070
  buildColumnDefinition(input) {
749
1071
  const isSerial = /serial/i.test(input.type);
750
1072
  const parts = [input.name, this.buildColumnType(input)];
@@ -828,8 +1150,8 @@ const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
828
1150
  * same logical schema, legacy-first naming, per-column disambiguation, and
829
1151
  * collision resolution against scalar column fields included.
830
1152
  */
831
- function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
832
- return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
1153
+ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, logger) {
1154
+ return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable, logger);
833
1155
  }
834
1156
  /**
835
1157
  * Introspect a MySQL database into the same {@link SchemaMetadata} shape the
@@ -838,7 +1160,8 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
838
1160
  *
839
1161
  * @param exec Runs a parameterized (`:p1`, `:p2`, …) query and returns rows.
840
1162
  * @param schema The MySQL database (schema) name to introspect.
841
- * @param options Optional include / exclude table-name filters.
1163
+ * @param options Optional include / exclude table-name filters, and the logger
1164
+ * that receives relation-naming notes (console when absent).
842
1165
  */
843
1166
  export async function introspectMysqlWith(exec, schema, options = {}) {
844
1167
  // ----- Tables -----
@@ -851,7 +1174,7 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
851
1174
  COLUMN_DEFAULT, EXTRA, CHARACTER_MAXIMUM_LENGTH
852
1175
  FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :p1 ORDER BY TABLE_NAME, ORDINAL_POSITION`, [schema]));
853
1176
  const columnsByTable = new Map();
854
- const enums = {};
1177
+ const enums = nullRecord();
855
1178
  for (const c of columnRows) {
856
1179
  if (!tableSet.has(c.TABLE_NAME))
857
1180
  continue;
@@ -879,6 +1202,8 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
879
1202
  tsType: mysqlTypeToTs(dataType, nullable, c.COLUMN_TYPE),
880
1203
  nullable,
881
1204
  hasDefault: c.COLUMN_DEFAULT !== null || /auto_increment/i.test(c.EXTRA),
1205
+ // The server numbers it, so a create's re-read can use the insert id.
1206
+ isGenerated: /auto_increment/i.test(c.EXTRA),
882
1207
  isArray: false,
883
1208
  arrayType: undefined,
884
1209
  pgArrayType: 'text[]',
@@ -960,16 +1285,19 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
960
1285
  }
961
1286
  }
962
1287
  // ----- Relations -----
963
- const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable);
1288
+ // One field per column, as the PostgreSQL introspector requires.
1289
+ for (const [tableName, cols] of columnsByTable)
1290
+ assertDistinctColumnFields(tableName, cols);
1291
+ const relationsByTable = buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
964
1292
  // ----- Assemble TableMetadata -----
965
- const tables = {};
1293
+ const tables = nullRecord();
966
1294
  for (const tableName of tableNames) {
967
1295
  const columns = columnsByTable.get(tableName) ?? [];
968
- const columnMap = {};
969
- const reverseColumnMap = {};
1296
+ const columnMap = nullRecord();
1297
+ const reverseColumnMap = nullRecord();
970
1298
  const dateColumns = new Set();
971
- const dialectTypes = {};
972
- const pgTypes = {};
1299
+ const dialectTypes = nullRecord();
1300
+ const pgTypes = nullRecord();
973
1301
  const allColumns = [];
974
1302
  for (const col of columns) {
975
1303
  columnMap[col.field] = col.name;
@@ -991,7 +1319,7 @@ export async function introspectMysqlWith(exec, schema, options = {}) {
991
1319
  allColumns,
992
1320
  primaryKey: pkByTable.get(tableName) ?? [],
993
1321
  uniqueColumns: uniqueByTable.get(tableName) ?? [],
994
- relations: relationsByTable.get(tableName) ?? {},
1322
+ relations: relationsByTable.get(tableName) ?? nullRecord(),
995
1323
  indexes: indexesByTable.get(tableName) ?? [],
996
1324
  };
997
1325
  }
@@ -1021,7 +1349,11 @@ export async function introspectMysql(options) {
1021
1349
  // try/finally runs the finally BEFORE the promise settles, so `pool.end()`
1022
1350
  // closed the pool out from under every introspection query and the whole
1023
1351
  // call failed with mysql2's "Pool is closed".
1024
- return await introspectMysqlWith(exec, schemaName, { include: options.include, exclude: options.exclude });
1352
+ return await introspectMysqlWith(exec, schemaName, {
1353
+ include: options.include,
1354
+ exclude: options.exclude,
1355
+ logger: options.logger,
1356
+ });
1025
1357
  }
1026
1358
  finally {
1027
1359
  await pool.end();
@@ -1032,7 +1364,7 @@ export async function introspectMysql(options) {
1032
1364
  // ---------------------------------------------------------------------------
1033
1365
  /** mysql2 connection flags Turbine pins for correct behavior. */
1034
1366
  const MYSQL_DRIVER_FLAGS = {
1035
- // Named placeholders (`:pN`), see paramPlaceholder + toNamedBinding.
1367
+ // Named placeholders (`:pN`), see paramPlaceholder + bindStatement.
1036
1368
  namedPlaceholders: true,
1037
1369
  // Safe-int policy matching Postgres int8: number when it fits, decimal string
1038
1370
  // otherwise. DECIMAL always comes back as a string. No global state mutated.
@@ -1158,11 +1490,15 @@ export async function turbineMysql(target, schema, options = {}) {
1158
1490
  connectionLimit: options.connectionLimit ?? 10,
1159
1491
  ...MYSQL_DRIVER_FLAGS,
1160
1492
  });
1161
- // Every NEW physical connection: disable backslash string escaping so the
1162
- // builder's `LIKE … ESCAPE '\'` clause (a single literal backslash) is valid
1163
- // MySQL, by default MySQL would parse `'\'` as an escaped quote (syntax
1164
- // error). This also makes string-literal semantics match Postgres. Turbine
1165
- // parameterizes every value, so no behavior depends on backslash escaping.
1493
+ // Every NEW physical connection: disable backslash string escaping, so a
1494
+ // string literal in raw SQL on a Turbine-owned pool reads the way it does
1495
+ // on PostgreSQL. Nothing Turbine GENERATES depends on it any more: the LIKE
1496
+ // clause names `!` (see mysqlDialect.likeEscapeCharacter), a JSON_OBJECT
1497
+ // key holding a backslash is written in hex (mysqlKeyLiteral), and every
1498
+ // value is a bound parameter, so the SQL means the same thing on a pool
1499
+ // the caller supplies, which keeps the caller's own mode. This stays
1500
+ // because removing it would change how raw SQL already written against an
1501
+ // owned pool parses.
1166
1502
  //
1167
1503
  // NOTE: even on a `mysql2/promise` pool, the 'connection' event yields the
1168
1504
  // *raw* (callback-style) connection, its `.query()` returns a non-thenable
@@ -1175,32 +1511,46 @@ export async function turbineMysql(target, schema, options = {}) {
1175
1511
  pool = new MysqlPool(mysql2Pool);
1176
1512
  owns = true;
1177
1513
  }
1178
- // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
1179
- const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
1180
- assertSupportedVersion(String(versionRows[0]?.v ?? ''));
1181
- // Everything that is not a pool-construction option is client config and
1182
- // rides through untouched, so a new TurbineConfig option works here the day
1183
- // it lands. The engine-owned keys come last, so no caller can unbind the
1184
- // dialect.
1185
- const { connectionLimit: _connectionLimit, ...clientConfig } = options;
1186
- const client = new TurbineClient({
1187
- ...clientConfig,
1188
- pool,
1189
- dialect: mysqlDialect,
1190
- preparedStatements: false,
1191
- },
1192
- // The join strategy is the only read path that consults a recorded dialect
1193
- // type, so a schema that spells a boolean any other way would read 1/0
1194
- // there and `true`/`false` everywhere else.
1195
- normalizeBooleanDialectTypes(schema));
1514
+ let client;
1515
+ try {
1516
+ // Probe the server version (fail fast on MySQL < 8.0 / MariaDB).
1517
+ const versionRows = (await pool.query('SELECT VERSION() AS v')).rows;
1518
+ assertSupportedVersion(String(versionRows[0]?.v ?? ''));
1519
+ // Everything that is not a pool-construction option is client config and
1520
+ // rides through untouched, so a new TurbineConfig option works here the day
1521
+ // it lands. The engine-owned keys come last, so no caller can unbind the
1522
+ // dialect.
1523
+ const { connectionLimit: _connectionLimit, ...clientConfig } = options;
1524
+ client = new TurbineClient({
1525
+ ...clientConfig,
1526
+ pool,
1527
+ dialect: mysqlDialect,
1528
+ preparedStatements: false,
1529
+ },
1530
+ // The join strategy is the only read path that consults a recorded dialect
1531
+ // type, so a schema that spells a boolean any other way would read 1/0
1532
+ // there and `true`/`false` everywhere else.
1533
+ normalizeBooleanDialectTypes(schema));
1534
+ }
1535
+ catch (err) {
1536
+ // A failed factory hands the caller nothing to close, so a pool it BUILT
1537
+ // is closed here: the version probe's connection would otherwise keep the
1538
+ // process alive (the client constructor refuses a bad config, a static
1539
+ // global filter naming no table or column for one). A pool the caller
1540
+ // supplied is the caller's and stays open.
1541
+ if (owns)
1542
+ await pool.end().catch(() => { });
1543
+ throw err;
1544
+ }
1196
1545
  if (owns) {
1197
1546
  // Turbine built this pool, so disconnect()/end() must close it. External
1198
1547
  // pools (injection) stay the caller's responsibility (disconnect() no-op),
1199
1548
  // consistent with turbineHttp / turbineSqlite.
1200
1549
  const baseDisconnect = client.disconnect.bind(client);
1201
- const close = async () => {
1202
- await baseDisconnect();
1550
+ const close = async (options) => {
1551
+ const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
1203
1552
  await pool.end();
1553
+ return result ?? undefined;
1204
1554
  };
1205
1555
  const patch = client;
1206
1556
  patch.disconnect = close;