tina4-nodejs 3.13.136 → 3.13.138

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 (243) hide show
  1. package/CLAUDE.md +33 -3
  2. package/COMMERCIAL-LICENSE.md +8 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE +11 -0
  5. package/README.md +24 -2
  6. package/package.json +44 -6
  7. package/packages/cli/dist/bin.js +19440 -18017
  8. package/packages/cli/src/bin.ts +8 -0
  9. package/packages/cli/src/commands/build.ts +8 -0
  10. package/packages/cli/src/commands/generate.ts +8 -0
  11. package/packages/cli/src/commands/init.ts +8 -0
  12. package/packages/cli/src/commands/lint.ts +8 -0
  13. package/packages/cli/src/commands/migrate.ts +8 -0
  14. package/packages/cli/src/commands/migrateCreate.ts +8 -0
  15. package/packages/cli/src/commands/migrateRollback.ts +8 -0
  16. package/packages/cli/src/commands/migrateStatus.ts +8 -0
  17. package/packages/cli/src/commands/queue.ts +8 -0
  18. package/packages/cli/src/commands/routes.ts +8 -0
  19. package/packages/cli/src/commands/seed.ts +8 -0
  20. package/packages/cli/src/commands/serve.ts +11 -0
  21. package/packages/cli/src/commands/test.ts +8 -0
  22. package/packages/core/dist/index.js +19432 -18012
  23. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
  24. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
  25. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
  26. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
  27. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
  28. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
  29. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
  30. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
  31. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
  32. package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
  33. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
  34. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
  35. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
  36. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
  37. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
  38. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
  39. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
  40. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
  41. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
  42. package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
  43. package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
  44. package/packages/core/public/bundled-component-provenance.json +48 -0
  45. package/packages/core/public/js/tina4js.min.js +13 -3
  46. package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
  47. package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
  48. package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
  49. package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
  50. package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
  51. package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
  52. package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
  53. package/packages/core/src/__feedback/widget.js +8 -0
  54. package/packages/core/src/_missing.ts +8 -0
  55. package/packages/core/src/ai.ts +8 -0
  56. package/packages/core/src/aiClient.ts +22 -7
  57. package/packages/core/src/api.ts +15 -3
  58. package/packages/core/src/auth.ts +21 -8
  59. package/packages/core/src/authGate.ts +8 -0
  60. package/packages/core/src/background.ts +8 -0
  61. package/packages/core/src/cache.ts +43 -4
  62. package/packages/core/src/constants.ts +8 -0
  63. package/packages/core/src/container.ts +8 -0
  64. package/packages/core/src/context/chunker.ts +8 -0
  65. package/packages/core/src/context/index.ts +8 -0
  66. package/packages/core/src/devAdmin.ts +185 -59
  67. package/packages/core/src/devMailbox.ts +8 -0
  68. package/packages/core/src/dispatchPipeline.ts +8 -0
  69. package/packages/core/src/docs.ts +8 -0
  70. package/packages/core/src/docsAutoDiscovery.ts +8 -0
  71. package/packages/core/src/docsParser.ts +8 -0
  72. package/packages/core/src/docsScanner.ts +44 -0
  73. package/packages/core/src/docsSignatures.ts +8 -0
  74. package/packages/core/src/dotenv.ts +8 -0
  75. package/packages/core/src/env.ts +8 -0
  76. package/packages/core/src/errorOverlay.ts +8 -0
  77. package/packages/core/src/events.ts +8 -0
  78. package/packages/core/src/fakeData.ts +8 -0
  79. package/packages/core/src/feedback.ts +8 -0
  80. package/packages/core/src/graphql.ts +16 -5
  81. package/packages/core/src/health.ts +8 -0
  82. package/packages/core/src/htmlElement.ts +8 -0
  83. package/packages/core/src/i18n.ts +8 -0
  84. package/packages/core/src/index.ts +8 -0
  85. package/packages/core/src/job.ts +8 -0
  86. package/packages/core/src/logger.ts +8 -0
  87. package/packages/core/src/mcp.test.ts +8 -0
  88. package/packages/core/src/mcp.ts +8 -0
  89. package/packages/core/src/messenger.ts +143 -86
  90. package/packages/core/src/metrics.ts +13 -0
  91. package/packages/core/src/middleware.ts +1178 -1096
  92. package/packages/core/src/mqtt.ts +8 -0
  93. package/packages/core/src/mqttMessage.ts +8 -0
  94. package/packages/core/src/optionalPackage.ts +48 -0
  95. package/packages/core/src/plan.ts +8 -0
  96. package/packages/core/src/portTakeover.ts +8 -0
  97. package/packages/core/src/projectIndex.ts +8 -0
  98. package/packages/core/src/projectIndexExtractors.ts +8 -0
  99. package/packages/core/src/projectIndexStorage.ts +8 -0
  100. package/packages/core/src/push.ts +8 -0
  101. package/packages/core/src/queue.ts +8 -0
  102. package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
  103. package/packages/core/src/queueBackends/liteBackend.ts +8 -0
  104. package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
  105. package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
  106. package/packages/core/src/rateLimiter.ts +8 -0
  107. package/packages/core/src/request.ts +59 -10
  108. package/packages/core/src/response.ts +79 -5
  109. package/packages/core/src/routeDiscovery.ts +8 -0
  110. package/packages/core/src/router.ts +8 -0
  111. package/packages/core/src/server.ts +73 -24
  112. package/packages/core/src/service.ts +8 -0
  113. package/packages/core/src/session.ts +8 -0
  114. package/packages/core/src/sessionHandlers/childError.ts +8 -0
  115. package/packages/core/src/sessionHandlers/databaseHandler.ts +8 -0
  116. package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
  117. package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
  118. package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
  119. package/packages/core/src/sessionHandlers/respClient.ts +8 -0
  120. package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
  121. package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
  122. package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
  123. package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
  124. package/packages/core/src/sso.ts +8 -0
  125. package/packages/core/src/static.ts +8 -0
  126. package/packages/core/src/test.ts +8 -0
  127. package/packages/core/src/testClient.ts +8 -0
  128. package/packages/core/src/testing.ts +8 -0
  129. package/packages/core/src/transport.ts +369 -0
  130. package/packages/core/src/trustedProxy.ts +8 -0
  131. package/packages/core/src/types.ts +13 -0
  132. package/packages/core/src/validator.ts +8 -0
  133. package/packages/core/src/version.ts +8 -0
  134. package/packages/core/src/websocket.ts +8 -0
  135. package/packages/core/src/websocketBackplane.ts +49 -32
  136. package/packages/core/src/websocketConnection.ts +8 -0
  137. package/packages/core/src/wsdl.ts +197 -115
  138. package/packages/core/templates/errors/302.twig +8 -0
  139. package/packages/core/templates/errors/401.twig +8 -0
  140. package/packages/core/templates/errors/403.twig +8 -0
  141. package/packages/core/templates/errors/404.twig +8 -0
  142. package/packages/core/templates/errors/500.twig +8 -0
  143. package/packages/core/templates/errors/502.twig +8 -0
  144. package/packages/core/templates/errors/503.twig +8 -0
  145. package/packages/core/templates/errors/base.twig +8 -0
  146. package/packages/frond/dist/index.js +151 -17
  147. package/packages/frond/src/engine.ts +177 -17
  148. package/packages/frond/src/index.ts +8 -0
  149. package/packages/orm/dist/index.js +8135 -6713
  150. package/packages/orm/src/adapters/firebird.ts +50 -3
  151. package/packages/orm/src/adapters/mongodb.ts +8 -0
  152. package/packages/orm/src/adapters/mssql.ts +29 -7
  153. package/packages/orm/src/adapters/mysql.ts +17 -1
  154. package/packages/orm/src/adapters/odbc.ts +11 -0
  155. package/packages/orm/src/adapters/pg-types.d.ts +8 -0
  156. package/packages/orm/src/adapters/postgres.ts +34 -8
  157. package/packages/orm/src/adapters/sqlDialect.ts +57 -0
  158. package/packages/orm/src/adapters/sqlite.ts +26 -0
  159. package/packages/orm/src/autoCrud.ts +78 -36
  160. package/packages/orm/src/baseModel.ts +61 -29
  161. package/packages/orm/src/cachedDatabase.ts +67 -1
  162. package/packages/orm/src/connectTimeout.ts +8 -0
  163. package/packages/orm/src/database.ts +649 -401
  164. package/packages/orm/src/databaseResult.ts +8 -0
  165. package/packages/orm/src/databaseUrl.ts +8 -0
  166. package/packages/orm/src/docstore.ts +23 -1
  167. package/packages/orm/src/fakeData.ts +8 -0
  168. package/packages/orm/src/graph/adapters/arango.ts +8 -0
  169. package/packages/orm/src/graph/adapters/bolt.ts +8 -0
  170. package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
  171. package/packages/orm/src/graph/connectTimeout.ts +8 -0
  172. package/packages/orm/src/graph/errors.ts +8 -0
  173. package/packages/orm/src/graph/graphAdapter.ts +8 -0
  174. package/packages/orm/src/graph/graphDatabase.ts +8 -0
  175. package/packages/orm/src/graph/graphUrl.ts +10 -1
  176. package/packages/orm/src/graph/shapes.ts +8 -0
  177. package/packages/orm/src/index.ts +147 -139
  178. package/packages/orm/src/migration.ts +21 -0
  179. package/packages/orm/src/model.ts +8 -0
  180. package/packages/orm/src/modelCollection.ts +8 -0
  181. package/packages/orm/src/point.ts +8 -0
  182. package/packages/orm/src/query.ts +149 -33
  183. package/packages/orm/src/queryBuilder.ts +8 -0
  184. package/packages/orm/src/realtime/index.ts +8 -0
  185. package/packages/orm/src/realtime/models/attachment.ts +8 -0
  186. package/packages/orm/src/realtime/models/channel.ts +8 -0
  187. package/packages/orm/src/realtime/models/channelMember.ts +8 -0
  188. package/packages/orm/src/realtime/models/message.ts +8 -0
  189. package/packages/orm/src/realtime/models/workspace.ts +8 -0
  190. package/packages/orm/src/realtime/realtime.ts +8 -0
  191. package/packages/orm/src/realtime/storage.ts +43 -12
  192. package/packages/orm/src/seeder.ts +8 -0
  193. package/packages/orm/src/seederTable.ts +8 -0
  194. package/packages/orm/src/seederTypes.ts +8 -0
  195. package/packages/orm/src/sqlTranslator.ts +185 -8
  196. package/packages/orm/src/types.ts +15 -0
  197. package/packages/orm/src/validation.ts +8 -0
  198. package/packages/swagger/src/generator.ts +8 -0
  199. package/packages/swagger/src/index.ts +8 -0
  200. package/packages/swagger/src/ui.ts +8 -0
  201. package/types/core/src/aiClient.d.ts +2 -2
  202. package/types/core/src/background.d.ts +0 -16
  203. package/types/core/src/cache.d.ts +6 -0
  204. package/types/core/src/devAdmin.d.ts +18 -12
  205. package/types/core/src/docs.d.ts +0 -17
  206. package/types/core/src/errorOverlay.d.ts +0 -24
  207. package/types/core/src/events.d.ts +0 -12
  208. package/types/core/src/feedback.d.ts +0 -23
  209. package/types/core/src/graphql.d.ts +0 -21
  210. package/types/core/src/htmlElement.d.ts +0 -15
  211. package/types/core/src/messenger.d.ts +12 -1
  212. package/types/core/src/metrics.d.ts +2 -0
  213. package/types/core/src/middleware.d.ts +45 -1
  214. package/types/core/src/mqtt.d.ts +0 -33
  215. package/types/core/src/optionalPackage.d.ts +25 -0
  216. package/types/core/src/plan.d.ts +0 -10
  217. package/types/core/src/projectIndex.d.ts +0 -10
  218. package/types/core/src/queueBackends/mongoBackend.d.ts +2 -0
  219. package/types/core/src/request.d.ts +12 -0
  220. package/types/core/src/response.d.ts +13 -0
  221. package/types/core/src/server.d.ts +19 -0
  222. package/types/core/src/sessionHandlers/childError.d.ts +0 -15
  223. package/types/core/src/transport.d.ts +28 -0
  224. package/types/core/src/trustedProxy.d.ts +0 -16
  225. package/types/core/src/types.d.ts +5 -0
  226. package/types/core/src/websocketBackplane.d.ts +1 -0
  227. package/types/core/src/wsdl.d.ts +8 -22
  228. package/types/frond/src/engine.d.ts +7 -1
  229. package/types/orm/src/adapters/firebird.d.ts +12 -0
  230. package/types/orm/src/adapters/mssql.d.ts +11 -0
  231. package/types/orm/src/adapters/mysql.d.ts +2 -0
  232. package/types/orm/src/adapters/postgres.d.ts +11 -0
  233. package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
  234. package/types/orm/src/adapters/sqlite.d.ts +7 -0
  235. package/types/orm/src/baseModel.d.ts +5 -0
  236. package/types/orm/src/cachedDatabase.d.ts +27 -0
  237. package/types/orm/src/database.d.ts +46 -7
  238. package/types/orm/src/graph/graphUrl.d.ts +0 -8
  239. package/types/orm/src/index.d.ts +1 -1
  240. package/types/orm/src/query.d.ts +48 -2
  241. package/types/orm/src/realtime/storage.d.ts +2 -1
  242. package/types/orm/src/sqlTranslator.d.ts +70 -18
  243. package/types/orm/src/types.d.ts +6 -0
@@ -1,10 +1,18 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 Firebird Adapter — uses the `node-firebird` package (optional peer dependency).
3
11
  *
4
12
  * Install: npm install node-firebird
5
13
  * URL format: firebird://user:pass@host:port/path/to/database.fdb
6
14
  */
7
- import { firebirdDialect, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
15
+ import { firebirdDialect, buildInsert, buildSetClause, buildWhereClause, quoteIdentifierWith } from "./sqlDialect.js";
8
16
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
9
17
  import { SQLTranslator } from "../sqlTranslator.js";
10
18
  import { connectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -235,6 +243,15 @@ export class FirebirdAdapter implements DatabaseAdapter {
235
243
  return 'firebird';
236
244
  }
237
245
 
246
+ /**
247
+ * Quote a name the ORM emits the way Firebird stores it: UPPER CASE (see
248
+ * fbQuote). An already-quoted name passes through - the escape hatch for a
249
+ * genuinely case-sensitive table. Python master: FirebirdAdapter.quote_identifier.
250
+ */
251
+ quoteIdentifier(name: string): string {
252
+ return quoteIdentifierWith(name, '"', '"', true);
253
+ }
254
+
238
255
  /** ADR-0044: readable/writable native boolean. */
239
256
  autocommit = true;
240
257
 
@@ -523,17 +540,39 @@ export class FirebirdAdapter implements DatabaseAdapter {
523
540
 
524
541
  async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
525
542
  this.ensureConnected();
543
+ if (/\bRETURNING\b/i.test(sql)) {
544
+ // A RETURNING statement hands back rows (the ORM reads the generated key
545
+ // from them, as it does on PostgreSQL). node-firebird answers a single
546
+ // returned row as a bare object, so normalise it to a row list.
547
+ const returned = await this.queryPromise(sql, params);
548
+ const rows = Array.isArray(returned) ? returned : returned ? [returned] : [];
549
+ return { rows: rows.map((row) => foldColumnNames(row)) };
550
+ }
526
551
  await this.executePromise(sql, params);
527
552
  return undefined;
528
553
  }
529
554
 
555
+ /**
556
+ * Run a row-producing statement ONCE and return its rows, for
557
+ * Database.execute(). node-firebird's execute() returns no rows, so a SELECT
558
+ * or an `INSERT ... RETURNING` through execute() used to lose them.
559
+ */
560
+ async executeRowsAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
561
+ this.ensureConnected();
562
+ return this.queryAsync<T>(sql, params);
563
+ }
564
+
530
565
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
531
566
  throw new Error("Use queryAsync() for Firebird.");
532
567
  }
533
568
 
534
569
  async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
535
570
  this.ensureConnected();
536
- const rows = await this.queryPromise(sql, params);
571
+ const result = await this.queryPromise(sql, params);
572
+ // A singleton statement (`INSERT ... VALUES ... RETURNING id`) comes back from
573
+ // node-firebird as ONE row object, not an array of rows (MEASURED), so it is
574
+ // wrapped here rather than iterated as if it were a list.
575
+ const rows: unknown[] = Array.isArray(result) ? result : (result && typeof result === "object" ? [result] : []);
537
576
  const decoded: T[] = [];
538
577
  for (const row of rows as T[]) {
539
578
  decoded.push(await this.decodeBlobs(foldColumnNames(row)));
@@ -565,6 +604,9 @@ export class FirebirdAdapter implements DatabaseAdapter {
565
604
  }
566
605
 
567
606
  async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
607
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
608
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
609
+ if (SQLTranslator.isWriteStatement(sql)) return this.queryAsync<T>(sql, params);
568
610
  let effectiveSql = sql;
569
611
  if (limit !== undefined) {
570
612
  const offset = skip ?? 0;
@@ -581,7 +623,12 @@ export class FirebirdAdapter implements DatabaseAdapter {
581
623
  }
582
624
 
583
625
  async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
584
- const rows = await this.fetchAsync<T>(sql, params, 1, 0);
626
+ // A write runs unpaginated: Firebird rejects `INSERT ... RETURNING id ROWS 1
627
+ // TO 1` (ROWS is not valid on INSERT, and on UPDATE/DELETE it must precede
628
+ // RETURNING), so fetchOne() of a RETURNING write failed (#133 contract).
629
+ const rows = SQLTranslator.isWriteStatement(sql)
630
+ ? await this.queryAsync<T>(sql, params)
631
+ : await this.fetchAsync<T>(sql, params, 1, 0);
585
632
  return rows[0] ?? null;
586
633
  }
587
634
 
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 MongoDB Adapter — uses the `mongodb` package (optional peer dependency).
3
11
  *
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 MSSQL Adapter — uses the `tedious` package (optional peer dependency).
3
11
  *
@@ -231,12 +239,23 @@ export class MssqlAdapter implements DatabaseAdapter {
231
239
  * `startAt` lets a caller that has already consumed N placeholders (an UPDATE
232
240
  * whose SET values are @p0..@p{N-1}) continue the numbering into a raw WHERE
233
241
  * fragment instead of restarting at @p0.
242
+ *
243
+ * Only REAL placeholders are rewritten: a `?` inside a string literal, a
244
+ * quoted identifier or a comment is left alone (tina4-python#138).
234
245
  */
235
246
  private convertPlaceholders(sql: string, startAt = 0): string {
236
- let count = startAt;
237
- return sql.replace(/\?/g, () => {
238
- return `@p${count++}`;
239
- });
247
+ return SQLTranslator.replacePlaceholders(sql, (index) => `@p${startAt + index}`);
248
+ }
249
+
250
+ /**
251
+ * The SQL the driver receives. With NO parameters it is sent EXACTLY as
252
+ * written (the cross-framework contract): there is nothing to bind.
253
+ *
254
+ * tina4: with parameters every `?` outside a literal, quoted identifier or
255
+ * comment is a placeholder.
256
+ */
257
+ private bindable(sql: string, params?: unknown[]): string {
258
+ return params && params.length > 0 ? this.convertPlaceholders(sql) : sql;
240
259
  }
241
260
 
242
261
  execute(sql: string, params?: unknown[]): unknown {
@@ -275,7 +294,7 @@ export class MssqlAdapter implements DatabaseAdapter {
275
294
  async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
276
295
  this.ensureConnected();
277
296
  const translated = this.translateSql(sql);
278
- const converted = this.convertPlaceholders(translated);
297
+ const converted = this.bindable(translated, params);
279
298
  return this.execSqlPromise(converted, params);
280
299
  }
281
300
 
@@ -286,7 +305,7 @@ export class MssqlAdapter implements DatabaseAdapter {
286
305
  async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
287
306
  this.ensureConnected();
288
307
  const translated = this.translateSql(sql);
289
- const converted = this.convertPlaceholders(translated);
308
+ const converted = this.bindable(translated, params);
290
309
  const result = await this.execSqlPromise(converted, params);
291
310
  return result.rows as T[];
292
311
  }
@@ -296,6 +315,9 @@ export class MssqlAdapter implements DatabaseAdapter {
296
315
  }
297
316
 
298
317
  async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
318
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
319
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
320
+ if (SQLTranslator.isWriteStatement(sql)) return this.queryAsync<T>(sql, params);
299
321
  let effectiveSql = sql;
300
322
  if (limit !== undefined && limit > 0) {
301
323
  // MSSQL-PAGINATION-DIVERGE: ONE pagination strategy across all four
@@ -430,7 +452,7 @@ export class MssqlAdapter implements DatabaseAdapter {
430
452
  // `WHERE [0] = @p0 AND [1] = @p1 ...` — db.truncate() was broken outright.
431
453
  if (typeof filter === "string") {
432
454
  const sql = filter
433
- ? `DELETE FROM [${table}] WHERE ${this.convertPlaceholders(filter)}`
455
+ ? `DELETE FROM [${table}] WHERE ${this.bindable(filter, params)}`
434
456
  : `DELETE FROM [${table}]`;
435
457
  try {
436
458
  const result = await this.execSqlPromise(sql, params ?? []);
@@ -1,10 +1,18 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 MySQL Adapter — uses the `mysql2` package (optional peer dependency).
3
11
  *
4
12
  * Install: npm install mysql2
5
13
  * URL format: mysql://user:pass@host:port/database
6
14
  */
7
- import { MYSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
15
+ import { MYSQL_DIALECT, buildInsert, buildSetClause, buildWhereClause, quoteIdentifierWith } from "./sqlDialect.js";
8
16
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
9
17
  import { SQLTranslator } from "../sqlTranslator.js";
10
18
  import { connectTarget, connectTimeoutMillis, driverConnectTimeoutMillis, withConnectTimeout } from "../connectTimeout.js";
@@ -62,6 +70,11 @@ export class MysqlAdapter implements DatabaseAdapter {
62
70
  return 'mysql';
63
71
  }
64
72
 
73
+ /** MySQL identifiers are backtick-quoted; a double-quoted token is a string. */
74
+ quoteIdentifier(name: string): string {
75
+ return quoteIdentifierWith(name, "`", "`");
76
+ }
77
+
65
78
  /** ADR-0044: readable/writable native boolean. */
66
79
  autocommit = true;
67
80
 
@@ -218,6 +231,9 @@ export class MysqlAdapter implements DatabaseAdapter {
218
231
  }
219
232
 
220
233
  async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
234
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
235
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
236
+ if (SQLTranslator.isWriteStatement(sql)) return this.queryAsync<T>(sql, params);
221
237
  let effectiveSql = sql;
222
238
  if (limit !== undefined) {
223
239
  effectiveSql += ` LIMIT ${limit}`;
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 ODBC Adapter — uses the `odbc` package (optional peer dependency).
3
11
  *
@@ -270,6 +278,9 @@ export class OdbcAdapter implements DatabaseAdapter {
270
278
  limit?: number,
271
279
  skip?: number,
272
280
  ): Promise<T[]> {
281
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
282
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
283
+ if (SQLTranslator.isWriteStatement(sql)) return this.queryAsync<T>(sql, params);
273
284
  // See SQLTranslator.appendLimit: the old inline check was a substring search,
274
285
  // so a LIMIT in a string literal or a trailing comment silently dropped the
275
286
  // row cap and returned every row.
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Minimal ambient type surface for the optional `pg` (node-postgres) peer
3
11
  * dependency. `@types/pg` is intentionally NOT a dependency (pg is an optional
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 PostgreSQL Adapter — uses the `pg` package (optional peer dependency).
3
11
  *
@@ -159,12 +167,27 @@ export class PostgresAdapter implements DatabaseAdapter {
159
167
  // `startAt` lets a caller that has already consumed N placeholders (an UPDATE
160
168
  // whose SET values are $1..$N) continue the numbering into a raw WHERE
161
169
  // fragment instead of restarting at $1.
170
+ //
171
+ // Only REAL placeholders are rewritten: a `?` inside a string literal (incl.
172
+ // E'...' and $$...$$), a quoted identifier or a comment is left alone. A plain
173
+ // text replace bound `SELECT 'why?' AS v, ? AS n`'s parameter INTO the
174
+ // literal and left the real marker unbound (tina4-python#138).
162
175
  private convertPlaceholders(sql: string, startAt = 1): string {
163
- let count = startAt - 1;
164
- return sql.replace(/\?/g, () => {
165
- count++;
166
- return `$${count}`;
167
- });
176
+ return SQLTranslator.replacePlaceholders(sql, (index) => `$${startAt + index}`);
177
+ }
178
+
179
+ /**
180
+ * The SQL the driver receives. With NO parameters it is sent EXACTLY as
181
+ * written (the cross-framework contract): there is nothing to bind, and the
182
+ * rewrite would turn PostgreSQL's jsonb operators `?`, `?|` and `?&` into
183
+ * unbound markers.
184
+ *
185
+ * tina4: with parameters every `?` outside a literal, quoted identifier or
186
+ * comment is a placeholder - use jsonb_exists(), jsonb_exists_any() or
187
+ * jsonb_exists_all() instead of the `?`, `?|` or `?&` operators.
188
+ */
189
+ private bindable(sql: string, params?: unknown[]): string {
190
+ return params && params.length > 0 ? this.convertPlaceholders(sql) : sql;
168
191
  }
169
192
 
170
193
  /**
@@ -232,7 +255,7 @@ export class PostgresAdapter implements DatabaseAdapter {
232
255
  /** Async execute for real usage. */
233
256
  async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
234
257
  this.ensureConnected();
235
- const convertedSql = this.convertPlaceholders(sql);
258
+ const convertedSql = this.bindable(sql, params);
236
259
  const result = await this.client!.query(convertedSql, params);
237
260
  if (result.rows?.[0]?.id !== undefined) {
238
261
  this._lastInsertId = this.normalizeId(result.rows[0].id);
@@ -247,7 +270,7 @@ export class PostgresAdapter implements DatabaseAdapter {
247
270
  /** Async query for real usage. */
248
271
  async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
249
272
  this.ensureConnected();
250
- const convertedSql = this.convertPlaceholders(sql);
273
+ const convertedSql = this.bindable(sql, params);
251
274
  const result = await this.client!.query(convertedSql, params);
252
275
  return (result.rows as T[]).map(row => this.decodeBlobs(row));
253
276
  }
@@ -257,6 +280,9 @@ export class PostgresAdapter implements DatabaseAdapter {
257
280
  }
258
281
 
259
282
  async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
283
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
284
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
285
+ if (SQLTranslator.isWriteStatement(sql)) return this.queryAsync<T>(sql, params);
260
286
  let effectiveSql = sql;
261
287
  if (limit !== undefined) {
262
288
  effectiveSql += ` LIMIT ${limit}`;
@@ -378,7 +404,7 @@ export class PostgresAdapter implements DatabaseAdapter {
378
404
  // was broken outright on PostgreSQL.
379
405
  if (typeof filter === "string") {
380
406
  const sql = filter
381
- ? `DELETE FROM "${table}" WHERE ${this.convertPlaceholders(filter)}`
407
+ ? `DELETE FROM "${table}" WHERE ${this.bindable(filter, params)}`
382
408
  : `DELETE FROM "${table}"`;
383
409
  try {
384
410
  const result = await this.client!.query(sql, params ?? []);
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * One CRUD SQL builder for every engine, instead of one per adapter.
3
11
  *
@@ -32,6 +40,35 @@ export interface Dialect {
32
40
  }
33
41
 
34
42
  const doubleQuote = (name: string): string => `"${name}"`;
43
+
44
+ /**
45
+ * Quote a table/column name the ORM emits, in one engine's identifier quotes.
46
+ *
47
+ * Port of the Python master's `quote_identifier` (adapter.py; Firebird's
48
+ * override upper-cases): idempotent (an already-quoted name is returned
49
+ * unchanged), dot-aware (`schema.table` quotes each part), and a non-identifier
50
+ * (`*`, `COUNT(*)`, an expression) is passed through untouched. An embedded
51
+ * closing quote is doubled.
52
+ *
53
+ * `upperCase` is Firebird's rule: an unquoted identifier is stored UPPER CASE,
54
+ * so a lower-case name must be quoted upper-case to match it.
55
+ */
56
+ export function quoteIdentifierWith(name: string, open: string, close: string, upperCase = false): string {
57
+ if (!name) return name;
58
+ const trimmed = name.trim();
59
+ if (trimmed.length >= 2 && trimmed.startsWith(open) && trimmed.endsWith(close)) return trimmed;
60
+ if (trimmed.includes(".")) {
61
+ return trimmed.split(".").map((part) => quoteIdentifierWith(part, open, close, upperCase)).join(".");
62
+ }
63
+ if (!/^[\p{L}\p{N}_$]+$/u.test(trimmed)) return trimmed;
64
+ const body = upperCase ? trimmed.toUpperCase() : trimmed;
65
+ return `${open}${body.split(close).join(close + close)}${close}`;
66
+ }
67
+
68
+ /** The ANSI default: `"name"` (SQLite, PostgreSQL, MSSQL, ODBC). */
69
+ export function quoteIdentifierAnsi(name: string): string {
70
+ return quoteIdentifierWith(name, '"', '"');
71
+ }
35
72
  const questionMark = (): string => "?";
36
73
 
37
74
  /** SQLite, and ODBC which follows the SQL standard spelling. */
@@ -64,6 +101,23 @@ export function firebirdDialect(fbQuote: (name: string) => string): Dialect {
64
101
  return { quote: fbQuote, marker: questionMark };
65
102
  }
66
103
 
104
+ const PLAIN_COLUMN_NAME = /^[A-Za-z_][A-Za-z0-9_$]*$/;
105
+
106
+ /**
107
+ * Refuse a data or filter-map key that is not a plain identifier.
108
+ *
109
+ * tina4: ADR-0069 (G3) - the write helpers emit these keys as column names, and
110
+ * the dialect quoters do not escape, so a key must be a plain identifier
111
+ * (letters, digits, underscore, dollar; not starting with a digit) before any
112
+ * SQL is built. Valid names are emitted exactly as before. Table names are
113
+ * developer code and are not checked here.
114
+ */
115
+ export function assertColumnNames(keys: Iterable<string>): void {
116
+ for (const key of keys) {
117
+ if (!PLAIN_COLUMN_NAME.test(key)) throw new Error(`Invalid column name '${key}'`);
118
+ }
119
+ }
120
+
67
121
  /**
68
122
  * `INSERT INTO <table> (<cols>) VALUES (<markers>)`.
69
123
  *
@@ -81,6 +135,7 @@ export function buildInsert(
81
135
  suffix = "",
82
136
  startAt = 1,
83
137
  ): string {
138
+ assertColumnNames(keys);
84
139
  const columns = keys.map((k) => dialect.quote(k)).join(", ");
85
140
  const placeholders = keys.map((_, i) => dialect.marker(startAt + i)).join(", ");
86
141
  return `INSERT INTO ${dialect.quote(table)} (${columns}) VALUES (${placeholders})${suffix}`;
@@ -98,6 +153,7 @@ export function buildSetClause(
98
153
  keys: string[],
99
154
  startAt = 1,
100
155
  ): string {
156
+ assertColumnNames(keys);
101
157
  return keys
102
158
  .map((k, i) => `${dialect.quote(k)} = ${dialect.marker(startAt + i)}`)
103
159
  .join(", ");
@@ -114,6 +170,7 @@ export function buildWhereClause(
114
170
  keys: string[],
115
171
  startAt = 1,
116
172
  ): string {
173
+ assertColumnNames(keys);
117
174
  return keys
118
175
  .map((k, i) => `${dialect.quote(k)} = ${dialect.marker(startAt + i)}`)
119
176
  .join(" AND ");
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  import { DatabaseSync } from "node:sqlite";
2
10
  import { ANSI_DIALECT, buildInsert, buildSetClause, buildWhereClause } from "./sqlDialect.js";
3
11
  import { mkdirSync } from "node:fs";
@@ -219,7 +227,25 @@ export class SQLiteAdapter implements DatabaseAdapter {
219
227
  return (params ? stmt.all(...toSqlParams(params)) : stmt.all()) as T[];
220
228
  }
221
229
 
230
+ /**
231
+ * Run a row-producing statement ONCE and return its rows, for
232
+ * Database.execute(). execute()'s run() discards rows, so a SELECT or an
233
+ * `INSERT ... RETURNING` through execute() used to lose them. A write still
234
+ * records its last insert id, exactly as execute() does.
235
+ */
236
+ executeRows<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
237
+ const rows = this.query<T>(sql, params);
238
+ if (SQLTranslator.isWriteStatement(sql)) {
239
+ const last = this.db.prepare("SELECT last_insert_rowid() AS id").get() as { id?: number | bigint } | undefined;
240
+ if (last?.id !== undefined) this._lastInsertId = last.id;
241
+ }
242
+ return rows;
243
+ }
244
+
222
245
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
246
+ // A write that returns rows (INSERT/UPDATE/DELETE ... RETURNING) runs exactly
247
+ // as written: a LIMIT/OFFSET appended to DML is a syntax error (#133 contract).
248
+ if (SQLTranslator.isWriteStatement(sql)) return this.query<T>(sql, params);
223
249
  // SQLTranslator.appendLimit owns BOTH halves of this decision: it scrubs
224
250
  // literals and comments before asking "does the caller already have a
225
251
  // LIMIT?", and it appends on a new line so the clause can never land inside