@mistriwork/engine 0.0.0-stage → 1.0.0-rc.10

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 (546) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/adapters/adapter.d.ts +693 -0
  4. package/dist/adapters/adapter.js +444 -0
  5. package/dist/adapters/cache/cacheStore.d.ts +75 -0
  6. package/dist/adapters/cache/cacheStore.js +160 -0
  7. package/dist/adapters/cache/resp.d.ts +52 -0
  8. package/dist/adapters/cache/resp.js +237 -0
  9. package/dist/adapters/commands/canonicalJson.d.ts +19 -0
  10. package/dist/adapters/commands/canonicalJson.js +154 -0
  11. package/dist/adapters/connect.d.ts +29 -0
  12. package/dist/adapters/connect.js +40 -0
  13. package/dist/adapters/connectionBudget.d.ts +101 -0
  14. package/dist/adapters/connectionBudget.js +128 -0
  15. package/dist/adapters/dbLatency.d.ts +35 -0
  16. package/dist/adapters/dbLatency.js +79 -0
  17. package/dist/adapters/epochCache.d.ts +50 -0
  18. package/dist/adapters/epochCache.js +85 -0
  19. package/dist/adapters/http/egress.d.ts +48 -0
  20. package/dist/adapters/http/egress.js +175 -0
  21. package/dist/adapters/http/httpAdapter.d.ts +71 -0
  22. package/dist/adapters/http/httpAdapter.js +512 -0
  23. package/dist/adapters/http/httpDescription.d.ts +32 -0
  24. package/dist/adapters/http/httpDescription.js +246 -0
  25. package/dist/adapters/http/openapi.d.ts +98 -0
  26. package/dist/adapters/http/openapi.js +487 -0
  27. package/dist/adapters/layoutRefusal.d.ts +24 -0
  28. package/dist/adapters/layoutRefusal.js +35 -0
  29. package/dist/adapters/net/networkPolicy.d.ts +44 -0
  30. package/dist/adapters/net/networkPolicy.js +123 -0
  31. package/dist/adapters/net/untrustedDatabase.d.ts +18 -0
  32. package/dist/adapters/net/untrustedDatabase.js +120 -0
  33. package/dist/adapters/poolAdmission.d.ts +43 -0
  34. package/dist/adapters/poolAdmission.js +184 -0
  35. package/dist/adapters/poolMetrics.d.ts +121 -0
  36. package/dist/adapters/poolMetrics.js +185 -0
  37. package/dist/adapters/postgres.d.ts +436 -0
  38. package/dist/adapters/postgres.js +1890 -0
  39. package/dist/adapters/postgresCatalog.d.ts +96 -0
  40. package/dist/adapters/postgresCatalog.js +387 -0
  41. package/dist/adapters/postgresCatalogDigest.d.ts +30 -0
  42. package/dist/adapters/postgresCatalogDigest.js +87 -0
  43. package/dist/adapters/postgresCatalogRows.d.ts +62 -0
  44. package/dist/adapters/postgresCatalogRows.js +37 -0
  45. package/dist/adapters/postgresDates.d.ts +1 -0
  46. package/dist/adapters/postgresDates.js +39 -0
  47. package/dist/adapters/postgresMaintenance.d.ts +33 -0
  48. package/dist/adapters/postgresMaintenance.js +179 -0
  49. package/dist/adapters/postgresPool.d.ts +255 -0
  50. package/dist/adapters/postgresPool.js +838 -0
  51. package/dist/adapters/postgresPooler.d.ts +28 -0
  52. package/dist/adapters/postgresPooler.js +267 -0
  53. package/dist/adapters/postgresReadCatalog.d.ts +31 -0
  54. package/dist/adapters/postgresReadCatalog.js +226 -0
  55. package/dist/adapters/postgresReadSnapshot.d.ts +46 -0
  56. package/dist/adapters/postgresReadSnapshot.js +133 -0
  57. package/dist/adapters/postgresSchemaEpoch.d.ts +24 -0
  58. package/dist/adapters/postgresSchemaEpoch.js +149 -0
  59. package/dist/adapters/postgresTransactionHandle.d.ts +173 -0
  60. package/dist/adapters/postgresTransactionHandle.js +524 -0
  61. package/dist/adapters/postgresValues.d.ts +30 -0
  62. package/dist/adapters/postgresValues.js +75 -0
  63. package/dist/adapters/replicas.d.ts +70 -0
  64. package/dist/adapters/replicas.js +208 -0
  65. package/dist/adapters/reservedSql.d.ts +38 -0
  66. package/dist/adapters/reservedSql.js +264 -0
  67. package/dist/adapters/rolePrivileges.d.ts +17 -0
  68. package/dist/adapters/rolePrivileges.js +22 -0
  69. package/dist/adapters/scoped/keyRows.d.ts +65 -0
  70. package/dist/adapters/scoped/keyRows.js +173 -0
  71. package/dist/adapters/scoped/postgresScopeCatalog.d.ts +16 -0
  72. package/dist/adapters/scoped/postgresScopeCatalog.js +117 -0
  73. package/dist/adapters/scoped/scopedCommandOperations.d.ts +5 -0
  74. package/dist/adapters/scoped/scopedCommandOperations.js +48 -0
  75. package/dist/adapters/scoped/scopedReads.d.ts +20 -0
  76. package/dist/adapters/scoped/scopedReads.js +174 -0
  77. package/dist/adapters/scoped/scopedTransaction.d.ts +89 -0
  78. package/dist/adapters/scoped/scopedTransaction.js +303 -0
  79. package/dist/adapters/scoped/sqliteScopeCatalog.d.ts +16 -0
  80. package/dist/adapters/scoped/sqliteScopeCatalog.js +241 -0
  81. package/dist/adapters/scoped/updateGuard.d.ts +3 -0
  82. package/dist/adapters/scoped/updateGuard.js +40 -0
  83. package/dist/adapters/serverSettings.d.ts +45 -0
  84. package/dist/adapters/serverSettings.js +46 -0
  85. package/dist/adapters/sqlite.d.ts +262 -0
  86. package/dist/adapters/sqlite.js +1434 -0
  87. package/dist/adapters/sqliteCostlyReads.d.ts +33 -0
  88. package/dist/adapters/sqliteCostlyReads.js +49 -0
  89. package/dist/adapters/sqliteDecimals.d.ts +22 -0
  90. package/dist/adapters/sqliteDecimals.js +57 -0
  91. package/dist/adapters/sqliteHandles.d.ts +43 -0
  92. package/dist/adapters/sqliteHandles.js +143 -0
  93. package/dist/adapters/sqliteStatementProcess.d.ts +29 -0
  94. package/dist/adapters/sqliteStatementProcess.js +131 -0
  95. package/dist/adapters/statementControl.d.ts +34 -0
  96. package/dist/adapters/statementControl.js +90 -0
  97. package/dist/adapters/storeHost.d.ts +130 -0
  98. package/dist/adapters/storeHost.js +13 -0
  99. package/dist/adapters/telemetry/drivers.d.ts +21 -0
  100. package/dist/adapters/telemetry/drivers.js +321 -0
  101. package/dist/adapters/telemetry/metrics.d.ts +63 -0
  102. package/dist/adapters/telemetry/metrics.js +122 -0
  103. package/dist/adapters/telemetry/otlp.d.ts +12 -0
  104. package/dist/adapters/telemetry/otlp.js +320 -0
  105. package/dist/adapters/telemetry/otlpEncode.d.ts +48 -0
  106. package/dist/adapters/telemetry/otlpEncode.js +366 -0
  107. package/dist/adapters/telemetry/otlpEnv.d.ts +1 -0
  108. package/dist/adapters/telemetry/otlpEnv.js +12 -0
  109. package/dist/adapters/telemetry/serverTiming.d.ts +83 -0
  110. package/dist/adapters/telemetry/serverTiming.js +503 -0
  111. package/dist/adapters/telemetry/start.d.ts +10 -0
  112. package/dist/adapters/telemetry/start.js +25 -0
  113. package/dist/adapters/telemetry/statement.d.ts +16 -0
  114. package/dist/adapters/telemetry/statement.js +134 -0
  115. package/dist/adapters/telemetry/trace.d.ts +127 -0
  116. package/dist/adapters/telemetry/trace.js +295 -0
  117. package/dist/adapters/writeAhead.d.ts +27 -0
  118. package/dist/adapters/writeAhead.js +46 -0
  119. package/dist/adapters/writeAttribution.d.ts +18 -0
  120. package/dist/adapters/writeAttribution.js +23 -0
  121. package/dist/adapters/writeCapture.d.ts +73 -0
  122. package/dist/adapters/writeCapture.js +20 -0
  123. package/dist/adapters/writeLanes.d.ts +94 -0
  124. package/dist/adapters/writeLanes.js +232 -0
  125. package/dist/engine.d.ts +15 -0
  126. package/dist/engine.js +13 -0
  127. package/dist/server/auth/credentials.d.ts +61 -0
  128. package/dist/server/auth/credentials.js +260 -0
  129. package/dist/server/cacheService.d.ts +50 -0
  130. package/dist/server/cacheService.js +110 -0
  131. package/dist/server/cli/args.d.ts +176 -0
  132. package/dist/server/cli/args.js +342 -0
  133. package/dist/server/cli/bin.d.ts +5 -0
  134. package/dist/server/cli/bin.js +66 -0
  135. package/dist/server/cli/browser.d.ts +15 -0
  136. package/dist/server/cli/browser.js +37 -0
  137. package/dist/server/cli/config.d.ts +50 -0
  138. package/dist/server/cli/config.js +206 -0
  139. package/dist/server/cli/startup.d.ts +36 -0
  140. package/dist/server/cli/startup.js +128 -0
  141. package/dist/server/cli/startupOptions.d.ts +63 -0
  142. package/dist/server/cli/startupOptions.js +143 -0
  143. package/dist/server/cli/subcommand.d.ts +28 -0
  144. package/dist/server/cli/subcommand.js +32 -0
  145. package/dist/server/cli/usage.d.ts +13 -0
  146. package/dist/server/cli/usage.js +31 -0
  147. package/dist/server/clientHttp.d.ts +184 -0
  148. package/dist/server/clientHttp.js +700 -0
  149. package/dist/server/clientSurface.d.ts +128 -0
  150. package/dist/server/clientSurface.js +182 -0
  151. package/dist/server/columnGrants.d.ts +103 -0
  152. package/dist/server/columnGrants.js +730 -0
  153. package/dist/server/compileCache.d.ts +1 -0
  154. package/dist/server/compileCache.js +30 -0
  155. package/dist/server/config/locations.d.ts +1 -0
  156. package/dist/server/config/locations.js +15 -0
  157. package/dist/server/configPaths.d.ts +7 -0
  158. package/dist/server/configPaths.js +34 -0
  159. package/dist/server/configSections.d.ts +64 -0
  160. package/dist/server/configSections.js +72 -0
  161. package/dist/server/connectionRedaction.d.ts +30 -0
  162. package/dist/server/connectionRedaction.js +77 -0
  163. package/dist/server/connections.d.ts +353 -0
  164. package/dist/server/connections.js +978 -0
  165. package/dist/server/constraints.d.ts +82 -0
  166. package/dist/server/constraints.js +169 -0
  167. package/dist/server/demo.d.ts +9 -0
  168. package/dist/server/demo.js +554 -0
  169. package/dist/server/devclient.d.ts +108 -0
  170. package/dist/server/devclient.js +361 -0
  171. package/dist/server/evidence.d.ts +55 -0
  172. package/dist/server/evidence.js +323 -0
  173. package/dist/server/hiddenColumns.d.ts +32 -0
  174. package/dist/server/hiddenColumns.js +238 -0
  175. package/dist/server/http/baselineHeaders.d.ts +24 -0
  176. package/dist/server/http/baselineHeaders.js +12 -0
  177. package/dist/server/http/captured.d.ts +35 -0
  178. package/dist/server/http/captured.js +73 -0
  179. package/dist/server/http/clock.d.ts +1 -0
  180. package/dist/server/http/clock.js +14 -0
  181. package/dist/server/http/connectionSource.d.ts +4 -0
  182. package/dist/server/http/connectionSource.js +48 -0
  183. package/dist/server/http/context.d.ts +189 -0
  184. package/dist/server/http/context.js +11 -0
  185. package/dist/server/http/crawlerText.d.ts +15 -0
  186. package/dist/server/http/crawlerText.js +32 -0
  187. package/dist/server/http/dataRoutes.d.ts +88 -0
  188. package/dist/server/http/dataRoutes.js +137 -0
  189. package/dist/server/http/door.d.ts +57 -0
  190. package/dist/server/http/door.js +39 -0
  191. package/dist/server/http/exportRoute.d.ts +15 -0
  192. package/dist/server/http/exportRoute.js +320 -0
  193. package/dist/server/http/firstPaint.d.ts +108 -0
  194. package/dist/server/http/firstPaint.js +262 -0
  195. package/dist/server/http/httpOperationRoutes.d.ts +11 -0
  196. package/dist/server/http/httpOperationRoutes.js +50 -0
  197. package/dist/server/http/pageReads.d.ts +20 -0
  198. package/dist/server/http/pageReads.js +205 -0
  199. package/dist/server/http/pageRoutes.d.ts +39 -0
  200. package/dist/server/http/pageRoutes.js +291 -0
  201. package/dist/server/http/probes.d.ts +22 -0
  202. package/dist/server/http/probes.js +27 -0
  203. package/dist/server/http/rateLimit.d.ts +131 -0
  204. package/dist/server/http/rateLimit.js +351 -0
  205. package/dist/server/http/readRoutes.d.ts +9 -0
  206. package/dist/server/http/readRoutes.js +131 -0
  207. package/dist/server/http/readRouting.d.ts +20 -0
  208. package/dist/server/http/readRouting.js +80 -0
  209. package/dist/server/http/recordCompose.d.ts +67 -0
  210. package/dist/server/http/recordCompose.js +167 -0
  211. package/dist/server/http/recordWrites.d.ts +89 -0
  212. package/dist/server/http/recordWrites.js +298 -0
  213. package/dist/server/http/refusalReport.d.ts +38 -0
  214. package/dist/server/http/refusalReport.js +63 -0
  215. package/dist/server/http/reply.d.ts +130 -0
  216. package/dist/server/http/reply.js +394 -0
  217. package/dist/server/http/requestOrigin.d.ts +11 -0
  218. package/dist/server/http/requestOrigin.js +8 -0
  219. package/dist/server/http/router.d.ts +45 -0
  220. package/dist/server/http/router.js +22 -0
  221. package/dist/server/http/serverCore.d.ts +68 -0
  222. package/dist/server/http/serverCore.js +177 -0
  223. package/dist/server/http/traceRequest.d.ts +30 -0
  224. package/dist/server/http/traceRequest.js +224 -0
  225. package/dist/server/http/trust.d.ts +261 -0
  226. package/dist/server/http/trust.js +472 -0
  227. package/dist/server/http/writeRefusal.d.ts +33 -0
  228. package/dist/server/http/writeRefusal.js +24 -0
  229. package/dist/server/httpConnection.d.ts +49 -0
  230. package/dist/server/httpConnection.js +170 -0
  231. package/dist/server/impact.d.ts +55 -0
  232. package/dist/server/impact.js +47 -0
  233. package/dist/server/journal.d.ts +134 -0
  234. package/dist/server/journal.js +398 -0
  235. package/dist/server/keysetReadHttp.d.ts +52 -0
  236. package/dist/server/keysetReadHttp.js +85 -0
  237. package/dist/server/layouts.d.ts +49 -0
  238. package/dist/server/layouts.js +192 -0
  239. package/dist/server/migrate/cli.d.ts +55 -0
  240. package/dist/server/migrate/cli.js +517 -0
  241. package/dist/server/migrate/database.d.ts +37 -0
  242. package/dist/server/migrate/database.js +247 -0
  243. package/dist/server/migrate/desired.d.ts +76 -0
  244. package/dist/server/migrate/desired.js +292 -0
  245. package/dist/server/migrate/files.d.ts +34 -0
  246. package/dist/server/migrate/files.js +141 -0
  247. package/dist/server/migrate/flyway.d.ts +7 -0
  248. package/dist/server/migrate/flyway.js +69 -0
  249. package/dist/server/migrate/history.d.ts +70 -0
  250. package/dist/server/migrate/history.js +211 -0
  251. package/dist/server/migrate/runner.d.ts +100 -0
  252. package/dist/server/migrate/runner.js +409 -0
  253. package/dist/server/migrate/snapshot.d.ts +30 -0
  254. package/dist/server/migrate/snapshot.js +239 -0
  255. package/dist/server/migrate/target.d.ts +33 -0
  256. package/dist/server/migrate/target.js +82 -0
  257. package/dist/server/openapiDraft.d.ts +25 -0
  258. package/dist/server/openapiDraft.js +171 -0
  259. package/dist/server/operations/diagnostics.d.ts +101 -0
  260. package/dist/server/operations/diagnostics.js +284 -0
  261. package/dist/server/operations/requestMetrics.d.ts +33 -0
  262. package/dist/server/operations/requestMetrics.js +94 -0
  263. package/dist/server/packageVersion.d.ts +1 -0
  264. package/dist/server/packageVersion.js +11 -0
  265. package/dist/server/poolConfig.d.ts +34 -0
  266. package/dist/server/poolConfig.js +114 -0
  267. package/dist/server/product.d.ts +20 -0
  268. package/dist/server/product.js +12 -0
  269. package/dist/server/recordHistoryOperations.d.ts +39 -0
  270. package/dist/server/recordHistoryOperations.js +43 -0
  271. package/dist/server/recordWriteOperations.d.ts +111 -0
  272. package/dist/server/recordWriteOperations.js +329 -0
  273. package/dist/server/resourceReadHttp.d.ts +81 -0
  274. package/dist/server/resourceReadHttp.js +606 -0
  275. package/dist/server/resourceReadOperations.d.ts +147 -0
  276. package/dist/server/resourceReadOperations.js +452 -0
  277. package/dist/server/scaffoldMachine.d.ts +54 -0
  278. package/dist/server/scaffoldMachine.js +184 -0
  279. package/dist/server/seed/cli.d.ts +20 -0
  280. package/dist/server/seed/cli.js +164 -0
  281. package/dist/server/seed/lexicon.d.ts +26 -0
  282. package/dist/server/seed/lexicon.js +585 -0
  283. package/dist/server/seed/seed.d.ts +70 -0
  284. package/dist/server/seed/seed.js +779 -0
  285. package/dist/server/seed/words.d.ts +21 -0
  286. package/dist/server/seed/words.js +164 -0
  287. package/dist/server/serverRequests.d.ts +6 -0
  288. package/dist/server/serverRequests.js +33 -0
  289. package/dist/server/serverTimeouts.d.ts +33 -0
  290. package/dist/server/serverTimeouts.js +32 -0
  291. package/dist/server/shapeonly.d.ts +32 -0
  292. package/dist/server/shapeonly.js +110 -0
  293. package/dist/server/startupWarnings.d.ts +32 -0
  294. package/dist/server/startupWarnings.js +92 -0
  295. package/dist/shared/addressParams.d.ts +28 -0
  296. package/dist/shared/addressParams.js +47 -0
  297. package/dist/shared/appCards.d.ts +123 -0
  298. package/dist/shared/appCards.js +385 -0
  299. package/dist/shared/appComponents.d.ts +177 -0
  300. package/dist/shared/appComponents.js +422 -0
  301. package/dist/shared/appDocuments.d.ts +43 -0
  302. package/dist/shared/appDocuments.js +131 -0
  303. package/dist/shared/appEmbeds.d.ts +21 -0
  304. package/dist/shared/appEmbeds.js +24 -0
  305. package/dist/shared/appIcons.d.ts +28 -0
  306. package/dist/shared/appIcons.js +223 -0
  307. package/dist/shared/appLinks.d.ts +17 -0
  308. package/dist/shared/appLinks.js +124 -0
  309. package/dist/shared/appListColumns.d.ts +3 -0
  310. package/dist/shared/appListColumns.js +66 -0
  311. package/dist/shared/appLocale.d.ts +36 -0
  312. package/dist/shared/appLocale.js +86 -0
  313. package/dist/shared/appMessages.d.ts +44 -0
  314. package/dist/shared/appMessages.js +78 -0
  315. package/dist/shared/appNoun.d.ts +21 -0
  316. package/dist/shared/appNoun.js +32 -0
  317. package/dist/shared/autoBucket.d.ts +98 -0
  318. package/dist/shared/autoBucket.js +305 -0
  319. package/dist/shared/autoValues.d.ts +25 -0
  320. package/dist/shared/autoValues.js +25 -0
  321. package/dist/shared/breakdown.d.ts +228 -0
  322. package/dist/shared/breakdown.js +557 -0
  323. package/dist/shared/breakdowntext.d.ts +41 -0
  324. package/dist/shared/breakdowntext.js +400 -0
  325. package/dist/shared/builtInThemes.d.ts +2 -0
  326. package/dist/shared/builtInThemes.js +771 -0
  327. package/dist/shared/chartSeparation.d.ts +11 -0
  328. package/dist/shared/chartSeparation.js +58 -0
  329. package/dist/shared/clauses.d.ts +22 -0
  330. package/dist/shared/clauses.js +97 -0
  331. package/dist/shared/columnCaption.d.ts +16 -0
  332. package/dist/shared/columnCaption.js +90 -0
  333. package/dist/shared/commandFieldPath.d.ts +2 -0
  334. package/dist/shared/commandFieldPath.js +7 -0
  335. package/dist/shared/commandWire.d.ts +26 -0
  336. package/dist/shared/commandWire.js +41 -0
  337. package/dist/shared/computed.d.ts +161 -0
  338. package/dist/shared/computed.js +378 -0
  339. package/dist/shared/computedParse.d.ts +13 -0
  340. package/dist/shared/computedParse.js +278 -0
  341. package/dist/shared/computedValue.d.ts +47 -0
  342. package/dist/shared/computedValue.js +191 -0
  343. package/dist/shared/connection.d.ts +30 -0
  344. package/dist/shared/connection.js +1 -0
  345. package/dist/shared/csv.d.ts +20 -0
  346. package/dist/shared/csv.js +63 -0
  347. package/dist/shared/dashLayout.d.ts +30 -0
  348. package/dist/shared/dashLayout.js +132 -0
  349. package/dist/shared/dashboardAsks.d.ts +93 -0
  350. package/dist/shared/dashboardAsks.js +129 -0
  351. package/dist/shared/dashboardTrend.d.ts +20 -0
  352. package/dist/shared/dashboardTrend.js +50 -0
  353. package/dist/shared/declaredReferences.d.ts +50 -0
  354. package/dist/shared/declaredReferences.js +202 -0
  355. package/dist/shared/definedFields.d.ts +63 -0
  356. package/dist/shared/definedFields.js +103 -0
  357. package/dist/shared/definedValues.d.ts +63 -0
  358. package/dist/shared/definedValues.js +184 -0
  359. package/dist/shared/displayDefaults.d.ts +135 -0
  360. package/dist/shared/displayDefaults.js +529 -0
  361. package/dist/shared/dueDate.d.ts +6 -0
  362. package/dist/shared/dueDate.js +6 -0
  363. package/dist/shared/duration.d.ts +4 -0
  364. package/dist/shared/duration.js +23 -0
  365. package/dist/shared/fileBounds.d.ts +9 -0
  366. package/dist/shared/fileBounds.js +9 -0
  367. package/dist/shared/filter.d.ts +217 -0
  368. package/dist/shared/filter.js +305 -0
  369. package/dist/shared/firstPaint.d.ts +24 -0
  370. package/dist/shared/firstPaint.js +33 -0
  371. package/dist/shared/form.d.ts +21 -0
  372. package/dist/shared/form.js +25 -0
  373. package/dist/shared/formValues.d.ts +104 -0
  374. package/dist/shared/formValues.js +352 -0
  375. package/dist/shared/funnel.d.ts +45 -0
  376. package/dist/shared/funnel.js +108 -0
  377. package/dist/shared/httpOperations.d.ts +48 -0
  378. package/dist/shared/httpOperations.js +9 -0
  379. package/dist/shared/iconGlyphs.d.ts +4 -0
  380. package/dist/shared/iconGlyphs.js +151 -0
  381. package/dist/shared/iconWeights.d.ts +3 -0
  382. package/dist/shared/iconWeights.js +162 -0
  383. package/dist/shared/imageWidths.d.ts +32 -0
  384. package/dist/shared/imageWidths.js +38 -0
  385. package/dist/shared/keysetPage.d.ts +64 -0
  386. package/dist/shared/keysetPage.js +161 -0
  387. package/dist/shared/like.d.ts +20 -0
  388. package/dist/shared/like.js +29 -0
  389. package/dist/shared/listEmpty.d.ts +12 -0
  390. package/dist/shared/listEmpty.js +36 -0
  391. package/dist/shared/listFilters.d.ts +28 -0
  392. package/dist/shared/listFilters.js +84 -0
  393. package/dist/shared/listPresentations.d.ts +199 -0
  394. package/dist/shared/listPresentations.js +7 -0
  395. package/dist/shared/listQuestion.d.ts +52 -0
  396. package/dist/shared/listQuestion.js +137 -0
  397. package/dist/shared/lookPalettes.d.ts +62 -0
  398. package/dist/shared/lookPalettes.js +306 -0
  399. package/dist/shared/maskQuoted.d.ts +24 -0
  400. package/dist/shared/maskQuoted.js +48 -0
  401. package/dist/shared/mentionLabel.d.ts +19 -0
  402. package/dist/shared/mentionLabel.js +38 -0
  403. package/dist/shared/mentionLookup.d.ts +17 -0
  404. package/dist/shared/mentionLookup.js +64 -0
  405. package/dist/shared/menuCounts.d.ts +16 -0
  406. package/dist/shared/menuCounts.js +74 -0
  407. package/dist/shared/model.d.ts +79 -0
  408. package/dist/shared/model.js +153 -0
  409. package/dist/shared/money.d.ts +44 -0
  410. package/dist/shared/money.js +81 -0
  411. package/dist/shared/moneyEntry.d.ts +7 -0
  412. package/dist/shared/moneyEntry.js +27 -0
  413. package/dist/shared/mount.d.ts +118 -0
  414. package/dist/shared/mount.js +248 -0
  415. package/dist/shared/niceTicks.d.ts +26 -0
  416. package/dist/shared/niceTicks.js +50 -0
  417. package/dist/shared/order.d.ts +59 -0
  418. package/dist/shared/order.js +133 -0
  419. package/dist/shared/page.d.ts +135 -0
  420. package/dist/shared/page.js +1187 -0
  421. package/dist/shared/pageAnatomy.d.ts +99 -0
  422. package/dist/shared/pageAnatomy.js +370 -0
  423. package/dist/shared/pageConfig.d.ts +25 -0
  424. package/dist/shared/pageConfig.js +1308 -0
  425. package/dist/shared/pageEmptyActions.d.ts +12 -0
  426. package/dist/shared/pageEmptyActions.js +20 -0
  427. package/dist/shared/pageLayouts.d.ts +72 -0
  428. package/dist/shared/pageLayouts.js +205 -0
  429. package/dist/shared/pageModel.d.ts +785 -0
  430. package/dist/shared/pageModel.js +121 -0
  431. package/dist/shared/pagePaths.d.ts +180 -0
  432. package/dist/shared/pagePaths.js +869 -0
  433. package/dist/shared/pagePlans.d.ts +52 -0
  434. package/dist/shared/pagePlans.js +108 -0
  435. package/dist/shared/pageRoutes.d.ts +9 -0
  436. package/dist/shared/pageRoutes.js +9 -0
  437. package/dist/shared/query.d.ts +166 -0
  438. package/dist/shared/query.js +1302 -0
  439. package/dist/shared/queryDates.d.ts +12 -0
  440. package/dist/shared/queryDates.js +101 -0
  441. package/dist/shared/queryOperands.d.ts +99 -0
  442. package/dist/shared/queryOperands.js +131 -0
  443. package/dist/shared/reads.d.ts +103 -0
  444. package/dist/shared/reads.js +154 -0
  445. package/dist/shared/recordLabel.d.ts +51 -0
  446. package/dist/shared/recordLabel.js +123 -0
  447. package/dist/shared/recordReadKeys.d.ts +30 -0
  448. package/dist/shared/recordReadKeys.js +51 -0
  449. package/dist/shared/recordReads.d.ts +268 -0
  450. package/dist/shared/recordReads.js +368 -0
  451. package/dist/shared/recordview.d.ts +198 -0
  452. package/dist/shared/recordview.js +356 -0
  453. package/dist/shared/referenceModel.d.ts +58 -0
  454. package/dist/shared/referenceModel.js +194 -0
  455. package/dist/shared/refusal.d.ts +64 -0
  456. package/dist/shared/refusal.js +144 -0
  457. package/dist/shared/relatedCreate.d.ts +30 -0
  458. package/dist/shared/relatedCreate.js +162 -0
  459. package/dist/shared/resourceHttp.d.ts +464 -0
  460. package/dist/shared/resourceHttp.js +1 -0
  461. package/dist/shared/richText.d.ts +85 -0
  462. package/dist/shared/richText.js +305 -0
  463. package/dist/shared/rollups.d.ts +143 -0
  464. package/dist/shared/rollups.js +64 -0
  465. package/dist/shared/rowScope.d.ts +72 -0
  466. package/dist/shared/rowScope.js +376 -0
  467. package/dist/shared/runtimeTables.d.ts +44 -0
  468. package/dist/shared/runtimeTables.js +79 -0
  469. package/dist/shared/schema.d.ts +523 -0
  470. package/dist/shared/schema.js +627 -0
  471. package/dist/shared/schematext.d.ts +38 -0
  472. package/dist/shared/schematext.js +101 -0
  473. package/dist/shared/scopeCatalog.d.ts +29 -0
  474. package/dist/shared/scopeCatalog.js +468 -0
  475. package/dist/shared/servedApp.d.ts +681 -0
  476. package/dist/shared/servedApp.js +45 -0
  477. package/dist/shared/sessionLease.d.ts +34 -0
  478. package/dist/shared/sessionLease.js +3 -0
  479. package/dist/shared/shape.d.ts +55 -0
  480. package/dist/shared/shape.js +168 -0
  481. package/dist/shared/slug.d.ts +1 -0
  482. package/dist/shared/slug.js +12 -0
  483. package/dist/shared/sortText.d.ts +14 -0
  484. package/dist/shared/sortText.js +26 -0
  485. package/dist/shared/sqlRelation.d.ts +4 -0
  486. package/dist/shared/sqlRelation.js +8 -0
  487. package/dist/shared/sqlsaved.d.ts +44 -0
  488. package/dist/shared/sqlsaved.js +63 -0
  489. package/dist/shared/stitchedPaths.d.ts +27 -0
  490. package/dist/shared/stitchedPaths.js +38 -0
  491. package/dist/shared/structuredFilter.d.ts +22 -0
  492. package/dist/shared/structuredFilter.js +1 -0
  493. package/dist/shared/subtitlePath.d.ts +20 -0
  494. package/dist/shared/subtitlePath.js +24 -0
  495. package/dist/shared/suggest.d.ts +18 -0
  496. package/dist/shared/suggest.js +59 -0
  497. package/dist/shared/tableSizing.d.ts +4 -0
  498. package/dist/shared/tableSizing.js +9 -0
  499. package/dist/shared/tagLists.d.ts +57 -0
  500. package/dist/shared/tagLists.js +34 -0
  501. package/dist/shared/textRules.d.ts +54 -0
  502. package/dist/shared/textRules.js +133 -0
  503. package/dist/shared/theme.d.ts +94 -0
  504. package/dist/shared/theme.js +800 -0
  505. package/dist/shared/themeColor.d.ts +34 -0
  506. package/dist/shared/themeColor.js +109 -0
  507. package/dist/shared/themeFaces.d.ts +30 -0
  508. package/dist/shared/themeFaces.js +42 -0
  509. package/dist/shared/themeInk.d.ts +20 -0
  510. package/dist/shared/themeInk.js +52 -0
  511. package/dist/shared/themeLooks.d.ts +9 -0
  512. package/dist/shared/themeLooks.js +572 -0
  513. package/dist/shared/themeParts.d.ts +26 -0
  514. package/dist/shared/themeParts.js +71 -0
  515. package/dist/shared/themePartsCheck.d.ts +21 -0
  516. package/dist/shared/themePartsCheck.js +91 -0
  517. package/dist/shared/themePresets.d.ts +85 -0
  518. package/dist/shared/themePresets.js +139 -0
  519. package/dist/shared/themeRecipeWords.d.ts +156 -0
  520. package/dist/shared/themeRecipeWords.js +156 -0
  521. package/dist/shared/themeRecipes.d.ts +213 -0
  522. package/dist/shared/themeRecipes.js +234 -0
  523. package/dist/shared/themeResolve.d.ts +55 -0
  524. package/dist/shared/themeResolve.js +52 -0
  525. package/dist/shared/themeTokens.d.ts +107 -0
  526. package/dist/shared/themeTokens.js +116 -0
  527. package/dist/shared/themeType.d.ts +38 -0
  528. package/dist/shared/themeType.js +205 -0
  529. package/dist/shared/unitWord.d.ts +4 -0
  530. package/dist/shared/unitWord.js +37 -0
  531. package/dist/shared/view.d.ts +195 -0
  532. package/dist/shared/view.js +987 -0
  533. package/dist/shared/viewLabels.d.ts +47 -0
  534. package/dist/shared/viewLabels.js +459 -0
  535. package/dist/shared/viewPaths.d.ts +248 -0
  536. package/dist/shared/viewPaths.js +294 -0
  537. package/dist/shared/viewtext.d.ts +57 -0
  538. package/dist/shared/viewtext.js +282 -0
  539. package/dist/shared/vocabulary.d.ts +71 -0
  540. package/dist/shared/vocabulary.js +249 -0
  541. package/dist/shared/weekStart.d.ts +7 -0
  542. package/dist/shared/weekStart.js +36 -0
  543. package/mark.svg +1 -0
  544. package/package.json +261 -4
  545. package/schema/model.d.ts +298 -0
  546. package/schema/theme.d.ts +587 -0
@@ -0,0 +1,1890 @@
1
+ /**
2
+ * Postgres adapter, on `pg`: connecting, querying, transactions and writes.
3
+ *
4
+ * Describing a database is the one job this file does not do itself. The
5
+ * catalog queries and their reading live in `postgresCatalog.ts`, which holds
6
+ * the ordering that makes two reads of one database agree; `describe` below is
7
+ * the seam, and it is called on whichever handle is asking — the reader on
8
+ * connect, or the leased writer inside a scoped transaction.
9
+ */
10
+ import { isProxy } from 'node:util/types';
11
+ import { POSTGRES_STYLE, buildWhere, clampLimit, boundStatement, dedupe, looksWrappable, refuseStacked, trimStatement, clampOffset, Refusal, WriteConflict, TransactionOperations, queueRecordLock, recordLockFilter, TransactionOutcomeUnknown, refuseAnalyze, countStatement, countEachStatement, countEachRows, countsTogether, orderClause, } from './adapter.js';
12
+ import { coerceForColumn } from '../shared/schema.js';
13
+ import { buildUpdateGuard } from './scoped/updateGuard.js';
14
+ import { createScopedTransaction, wholeWithin } from './scoped/scopedTransaction.js';
15
+ import { readPostgresScopeCatalog } from './scoped/postgresScopeCatalog.js';
16
+ import { captureScopeCatalog, mapScopeCatalog } from '../shared/scopeCatalog.js';
17
+ import { createPinnedPostgresReadOwner } from './postgresReadCatalog.js';
18
+ import { isRuntimeRelation, qualified, redact, valueSafe, withBytes, toJsonSafe } from './postgresValues.js';
19
+ import { beginWriteCapture, capturesWrites, committedWriteCapture, finishWriteCapture } from './writeCapture.js';
20
+ import { lendStoreHost, } from './storeHost.js';
21
+ import { compileView, compileViewCount } from '../shared/view.js';
22
+ import { compileBreakdown, compileBreakdownCount } from '../shared/breakdown.js';
23
+ import { PgConnectionManager, sharedPgConnections } from './postgresPool.js';
24
+ import { aheadReport, behindWrite, generationOf, holdAhead, redoWithoutAhead, sendAhead, sendsAhead, waitsAhead, RolledBackAtCommit, transactionHandle, attestedOn, keepAttestedOn, forgetAttestations, noteCatalogVersion, readsCatalogVersion, suspectsStores, } from './postgresTransactionHandle.js';
25
+ import { withoutWriteAhead, writeAhead } from './writeAhead.js';
26
+ import { KeyRows } from './scoped/keyRows.js';
27
+ import { postgresRowEstimate } from './postgresCatalogRows.js';
28
+ import { describePostgresCatalog, postgresScopeStyle, SYSTEM_SCHEMAS, postgresColumnsOf, assertPostgresTable, } from './postgresCatalog.js';
29
+ import { postgresCatalogDigestOf, postgresCatalogDigestStatement } from './postgresCatalogDigest.js';
30
+ import { epochlessEpoch, SCHEMA_EPOCH_TABLE, schemaEpochOf, schemaEpochStatement, schemaMoved } from './postgresSchemaEpoch.js';
31
+ import { EpochCache } from './epochCache.js';
32
+ import { keepDatesAsText } from './postgresDates.js';
33
+ import { captureRowScope, snapshotRowScope } from '../shared/rowScope.js';
34
+ import { createScopedReads } from './scoped/scopedReads.js';
35
+ import { scopedCommandInvoker } from './scoped/scopedCommandOperations.js';
36
+ import { postgresPrivileges } from './rolePrivileges.js';
37
+ import { RESERVED_SCHEMAS, refusePostgresReserved, refuseReservedPlan, refuseReservedText } from './reservedSql.js';
38
+ import { statementCap } from './statementControl.js';
39
+ import { WriteLanes, keyedLane, lockTimedOut } from './writeLanes.js';
40
+ import { primaryReads } from './replicas.js';
41
+ import { readSnapshotOn } from './postgresReadSnapshot.js';
42
+ const pinnedReaders = new WeakMap();
43
+ /** Constructor-issued source identity, checked without reading receiver data. */
44
+ export const isPostgresVerifiedCommandSource = (value) => !!value && typeof value === 'object' && !isProxy(value) && pinnedReaders.has(value);
45
+ const SCOPED_CATALOG_DIGEST = postgresCatalogDigestStatement(SYSTEM_SCHEMAS);
46
+ /* Every adapter of one database in this process describes the same catalog, and a tenant App's start made a dozen
47
+ of them, each describing it afresh in a snapshot of its own: startup's two checks, its trial enrollment and the
48
+ host's, each read source, the first scoped transaction, every platform source twice — sixteen descriptions of
49
+ two databases, six round trips each, before the port opened. A description is kept here under the exact version
50
+ it was read at — the catalog digest read in its own snapshot, or the schema epoch — and any adapter of the same
51
+ database and schemas that reads the same version is handed a copy: the same version is the same catalog rows
52
+ (postgresCatalogDigest.ts), so nothing is trusted that a fresh description would not have said. A time-filed
53
+ version (epochlessEpoch) is never shared: it stands only for the adapter that took the risk.
54
+
55
+ A write reads its version, then describes, each statement seeing what had committed when it ran (READ COMMITTED):
56
+ it reads the version again and files the description only if that has not moved, so nothing is kept under a
57
+ version its description never saw. A snapshot describes the catalog its digest was read in, and files at once.
58
+
59
+ Two things a description carries move without the version: comments, and the planner's row estimates
60
+ (`approxRows`, which ANALYZE and autovacuum change, and which decide whether a large table's count is estimated,
61
+ resourceReadOperations.ts). A kept description is therefore served for a few minutes only, after which the next
62
+ reader describes again: a table that grew is counted as large a few minutes later rather than never, and a start,
63
+ whose readers come within seconds, still describes each catalog once. */
64
+ const sharedDescriptions = new EpochCache(32);
65
+ const SHARED_DESCRIPTION_MS = 5 * 60_000;
66
+ let descriptionsRead = 0, descriptionsKept = 0;
67
+ /** For the budget tests: how many times this process described a PostgreSQL catalog, and how many were found kept. */
68
+ export function postgresDescriptionCounts() {
69
+ return { described: descriptionsRead, kept: descriptionsKept };
70
+ }
71
+ /**
72
+ * An adapter on the manager every adapter of `url` with this `shape` shares (sharedPgConnections): its own catalog,
73
+ * lanes and stores, the pools of its neighbors. Closing it lets its share go; the pools close with the last one.
74
+ */
75
+ export function sharedPostgresAdapter(url, label, shape = {}) {
76
+ return new PostgresAdapter(url, label, [], sharedPgConnections(url, shape, keepDatesAsText));
77
+ }
78
+ /**
79
+ * Whether a statement sent ahead behind a write's BEGIN (writeAhead.ts) failed, so the transaction must begin again
80
+ * without them. Not when one waited out its lock (55P03): begun again, the transaction asked for the same lock and
81
+ * waited for it a second time, so a held row was answered `row_locked` after twice the lock timeout, ten seconds
82
+ * where five are documented (a live load test). That wait is the transaction's own answer, and it is thrown.
83
+ */
84
+ async function aheadsFailed(aheads) {
85
+ const settled = await Promise.allSettled(aheads);
86
+ const waited = settled.find((outcome) => outcome.status === 'rejected' && lockTimedOut(outcome.reason));
87
+ if (waited)
88
+ throw waited.reason;
89
+ return settled.some((outcome) => outcome.status === 'rejected');
90
+ }
91
+ export class PostgresAdapter {
92
+ label;
93
+ schemas;
94
+ dialect = 'postgres';
95
+ connections;
96
+ /* The order writes run in (writeLanes.ts): one at a time for the database
97
+ browser, by record for an App, each on a writer of its own. Raw SQL
98
+ waits for everything before it, in either. */
99
+ writes = new WriteLanes();
100
+ /* What a description is shared under (sharedDescriptions): the database, and the schemas this adapter describes. */
101
+ describes;
102
+ /* The database alone: what a store attestation is shared under (noteCatalogVersion), whatever schemas are described. */
103
+ database;
104
+ /* What a write transaction asks the catalog — the journal's layout, the
105
+ scope catalog, a relation's kind, its columns and key — kept under the
106
+ schema epoch the transaction read in the message that began it
107
+ (openWrite, postgresSchemaEpoch.ts). A warm command asks nothing. */
108
+ catalogCache = new EpochCache();
109
+ /** The epoch each open write transaction read, by the handle it runs on. */
110
+ txEpochs = new WeakMap();
111
+ /* Whether the database holds the epoch store: learned from the first
112
+ write transaction; absent, every transaction reads the digest as before. */
113
+ epochStore;
114
+ /** `work`, forgetting the kept catalog if it dies on a table or column that is gone: what was kept is wrong. */
115
+ watched(work) {
116
+ return work.catch((error) => {
117
+ if (schemaMoved(error)) {
118
+ this.catalogCache.clear();
119
+ sharedDescriptions.forget(this.describes);
120
+ }
121
+ throw error;
122
+ });
123
+ }
124
+ constructor(connectionString, label = redact(connectionString),
125
+ /** Restrict to these schemas. Empty means every non-system schema. */
126
+ schemas = [], connections,
127
+ /** The reader count the connection's config names; the process defaults fill the rest. */
128
+ pool) {
129
+ this.label = label;
130
+ this.schemas = schemas;
131
+ this.connections = connections ?? new PgConnectionManager(connectionString, keepDatesAsText, undefined, pool);
132
+ this.describes = `${connectionString}\0${schemas.join('\0')}`;
133
+ this.database = connectionString;
134
+ readsCatalogVersion(this, connectionString, async (db) => `digest:${postgresCatalogDigestOf(await db.query(SCOPED_CATALOG_DIGEST))}`);
135
+ if (this.connections instanceof PgConnectionManager)
136
+ pinnedReaders.set(this, this.connections);
137
+ /* The App stores this database may hold read and write through these (stores/appStores.ts). */
138
+ lendStoreHost(this, {
139
+ dialect: 'postgres',
140
+ withReadClient: (work) => this.connections.withReadClient(work),
141
+ withWriteClient: (work) => this.connections.withWriteClient(work),
142
+ attestedStore: (client, store, read) => this.attestedStore(client, store, read),
143
+ verifiedSource: () => this.verifiedSource(),
144
+ journalTransaction: (journal, run, options) => this.journalTransaction(journal, run, options),
145
+ readSnapshot: (run) => this.readSnapshot(run, true),
146
+ hasReplicas: () => this.connections.hasReplicas(),
147
+ onHealthyReplicas: (work) => this.connections.onHealthyReplicas(work),
148
+ });
149
+ }
150
+ /* Constructor-issued, over the connections it was built with: what a verified transaction is allowed to run on. */
151
+ verifiedSource() {
152
+ return isPostgresVerifiedCommandSource(this) && pinnedReaders.get(this) === this.connections;
153
+ }
154
+ /** The pools' meters, for the operator surface. */
155
+ poolStats() {
156
+ return this.connections.stats();
157
+ }
158
+ /** Each replica's state, when the connection names any (replicas.ts). */
159
+ replicaHealth() {
160
+ return this.connections.replicaHealth();
161
+ }
162
+ conn() {
163
+ return this.connections.read();
164
+ }
165
+ /**
166
+ * Run a statement the user wrote.
167
+ *
168
+ * The session already carries `default_transaction_read_only = on`, and for
169
+ * everything else in this adapter that is enough, because every statement is
170
+ * one this file built. It is **not** enough here: `SET
171
+ * default_transaction_read_only = off` is itself a statement, and a user who
172
+ * typed it followed by a `DELETE` would get both.
173
+ *
174
+ * `BEGIN READ ONLY` is the version that holds, for one statement. Postgres
175
+ * refuses `SET TRANSACTION READ WRITE` once the transaction has taken its
176
+ * first snapshot, and one statement takes it before it runs. It does not hold
177
+ * against several: `COMMIT; BEGIN READ WRITE; DELETE …` ends the read-only
178
+ * transaction and opens a writable one. So the text goes on the extended
179
+ * protocol (`queryMode: 'extended'`), where Postgres itself refuses a second
180
+ * statement; refuseStacked is the earlier, clearer refusal, not the guard.
181
+ * (node-postgres sends a query with an empty `values` on the simple protocol,
182
+ * which runs every statement in the string.) The `ROLLBACK` afterwards also
183
+ * discards any session state the statement set, so one run cannot change what
184
+ * the next one means.
185
+ *
186
+ * The rollback runs in a `finally`: a statement that errors has still opened
187
+ * a transaction, and leaving it open would leave the connection in an aborted
188
+ * state where every later query fails with "current transaction is aborted".
189
+ *
190
+ * `write: true` is the other half, and it is a different request rather than
191
+ * a relaxation of this one: a separate handle, no READ ONLY transaction, and
192
+ * a decision made further up about whether this connection may write at all.
193
+ * The two paths share the statement text and nothing else.
194
+ */
195
+ async runSql(request) {
196
+ const stacked = refuseStacked(request.text, 'postgres');
197
+ if (stacked)
198
+ throw new Error(stacked);
199
+ if (request.refuseReserved)
200
+ refusePostgresReserved(request.text, request.write);
201
+ const limit = clampLimit(request.limit);
202
+ /* A write is never wrapped in a bounding subquery: `UPDATE … RETURNING`
203
+ inside `SELECT * FROM (…)` is a different statement, and one Postgres
204
+ refuses outright. The caller asked for this text; it runs as typed. */
205
+ const bounded = !request.write && looksWrappable(request.text);
206
+ const text = bounded ? boundStatement(request.text, limit) : trimStatement(request.text);
207
+ if (request.write) {
208
+ /* On the write handle, and outside the READ ONLY transaction every other
209
+ statement here gets — which is the whole difference. Whether this is
210
+ allowed at all was decided before the request arrived: the connection
211
+ has to be marked writable in the config, and that check does not live
212
+ in the adapter.
213
+
214
+ No wrapper transaction of our own. One statement is already atomic in
215
+ Postgres, and a BEGIN/COMMIT around it would only add a way to leave a
216
+ transaction open on a connection that outlives the request. */
217
+ return this.writes.run('all', () => this.connections.withWriteClient(async (write) => {
218
+ const started = performance.now();
219
+ // Extended protocol: one statement, as on the read path below.
220
+ const result = await write.query({ text, values: [], rowMode: 'array', queryMode: 'extended' });
221
+ return {
222
+ /* A write that returns rows — `RETURNING` — is worth showing, and one
223
+ that does not returns an empty grid rather than a fabricated one.
224
+ Array rows, so two columns of one name keep both values. */
225
+ columns: dedupe(result.fields?.map((f) => f.name) ?? []),
226
+ rows: (result.rows ?? []).map((row) => row.map(valueSafe)),
227
+ ms: Math.round(performance.now() - started),
228
+ truncated: false,
229
+ bounded: false,
230
+ affected: Number(result.rowCount ?? 0),
231
+ sql: { text, params: [] },
232
+ };
233
+ }, true));
234
+ }
235
+ return this.connections.withReadClient(async (db) => {
236
+ const started = performance.now();
237
+ /* The statement's cap, set where it dies with the transaction, so the
238
+ session's own is untouched — and statementCap's floor stops a caller
239
+ asking for a timeout so low that even the fast path loses races to it. */
240
+ const cap = statementCap(request.timeoutMs);
241
+ const begin = () => db.query('BEGIN READ ONLY').then(() => db.query(`SET LOCAL statement_timeout = ${cap}`));
242
+ await begin();
243
+ try {
244
+ if (request.refuseReserved)
245
+ await refuseReservedPlan(db, request.text, []);
246
+ /* `rowMode: 'array'` so two output columns of the same name keep both
247
+ values; the default object rows would let the second overwrite the
248
+ first. `queryMode: 'extended'` is what carries exactly one statement:
249
+ an empty `values` alone leaves node-postgres on the simple protocol,
250
+ which would run a stacked COMMIT and a write after it. */
251
+ let wrapped = bounded;
252
+ const result = await db.query({ text, values: [], rowMode: 'array', queryMode: 'extended' }).catch(async (err) => {
253
+ /* The wrap is an optimisation that is allowed to fail — not every
254
+ legal statement survives being made a derived table. Run as
255
+ written and reported as unbounded. The transaction has to be
256
+ restarted: a failed statement aborts it, and every later query in
257
+ it would fail with "current transaction is aborted". A statement
258
+ that was stopped — cancelled, or past its cap (57014) — is not a
259
+ wrap that failed, and running it again would spend the cap twice. */
260
+ if (!bounded || err.code === '57014')
261
+ throw err;
262
+ wrapped = false;
263
+ await db.query('ROLLBACK');
264
+ await begin();
265
+ return db.query({ text: trimStatement(request.text), values: [], rowMode: 'array', queryMode: 'extended' });
266
+ });
267
+ const truncated = result.rows.length > limit;
268
+ return {
269
+ columns: dedupe(result.fields.map((f) => f.name)),
270
+ rows: (truncated ? result.rows.slice(0, limit) : result.rows).map((row) => row.map(valueSafe)),
271
+ ms: Math.round(performance.now() - started),
272
+ truncated,
273
+ bounded: wrapped,
274
+ sql: { text: wrapped ? text : trimStatement(request.text), params: [] },
275
+ };
276
+ }
277
+ finally {
278
+ await db.query('ROLLBACK').catch(() => {
279
+ /* The connection is already unusable if this fails; the original error
280
+ is the one worth reporting. */
281
+ });
282
+ }
283
+ }, true);
284
+ }
285
+ /**
286
+ * EXPLAIN, plain — never the analysing variant that executes to time.
287
+ *
288
+ * `runSql`'s bounding wrap is precisely wrong here: a statement made a
289
+ * derived table is a different statement, with a different plan. The text
290
+ * goes to the planner as written, inside the same READ ONLY transaction
291
+ * every user statement gets — so even a smuggled write is only ever
292
+ * planned, and an execution attempt is the transaction's to refuse, not a
293
+ * string check's.
294
+ */
295
+ async explainSql(request) {
296
+ const stacked = refuseStacked(request.text, 'postgres');
297
+ if (stacked)
298
+ throw new Error(stacked);
299
+ const analyze = refuseAnalyze(request.text);
300
+ if (analyze)
301
+ throw new Error(analyze);
302
+ if (request.refuseReserved)
303
+ refuseReservedText(request.text, 'postgres');
304
+ return this.connections.withReadClient(async (db) => {
305
+ const started = performance.now();
306
+ await db.query('BEGIN READ ONLY');
307
+ try {
308
+ /* Planning runs nothing, but planning a wide join still takes time. */
309
+ await db.query(`SET LOCAL statement_timeout = ${statementCap()}`);
310
+ if (request.refuseReserved)
311
+ await refuseReservedPlan(db, request.text, (request.params ?? []));
312
+ const result = await db.query({
313
+ text: `EXPLAIN ${trimStatement(request.text)}`,
314
+ values: (request.params ?? []),
315
+ rowMode: 'array',
316
+ // One statement, whether or not there are parameters: see runSql.
317
+ queryMode: 'extended',
318
+ });
319
+ return {
320
+ plan: result.rows.map((r) => String(r[0])),
321
+ ms: Math.round(performance.now() - started),
322
+ };
323
+ }
324
+ finally {
325
+ await db.query('ROLLBACK').catch(() => { });
326
+ }
327
+ }, true);
328
+ }
329
+ /**
330
+ * The planner's own statistics, read rather than computed.
331
+ *
332
+ * `pg_stats` already holds the null share, the distinct estimate, the most
333
+ * common values with their frequencies, and the histogram — ANALYZE paid
334
+ * for them, reading them touches no user table. The numbers are
335
+ * sample-based and stale until the next ANALYZE, which is what
336
+ * `source: 'statistics'` tells the caller; a column ANALYZE has never
337
+ * visited simply has no numbers, which beats invented ones.
338
+ */
339
+ async profile(table) {
340
+ const { schema, name } = await this.readTable(table);
341
+ const db = await this.conn();
342
+ const started = performance.now();
343
+ const rel = await db.query(`SELECT CASE WHEN c.reltuples < 0 THEN NULL ELSE c.reltuples END AS approx_rows
344
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
345
+ WHERE n.nspname = $1 AND c.relname = $2`, [schema, name]);
346
+ const approx = rel.rows[0]?.approx_rows;
347
+ const rows = postgresRowEstimate(approx);
348
+ /* Every live column, with its stats row where ANALYZE has made one.
349
+ `most_common_vals` and `histogram_bounds` are `anyarray`, which the
350
+ driver has no parser for; the ::text::text[] two-step turns them into
351
+ the text array it can parse. */
352
+ const stats = await db.query(`SELECT a.attname::text AS column,
353
+ format_type(a.atttypid, a.atttypmod) AS type,
354
+ s.null_frac,
355
+ s.n_distinct,
356
+ s.most_common_vals::text::text[] AS common_values,
357
+ s.most_common_freqs AS common_freqs,
358
+ s.histogram_bounds::text::text[] AS bounds
359
+ FROM pg_attribute a
360
+ JOIN pg_class c ON c.oid = a.attrelid
361
+ JOIN pg_namespace n ON n.oid = c.relnamespace
362
+ LEFT JOIN pg_stats s ON s.schemaname = n.nspname AND s.tablename = c.relname AND s.attname = a.attname
363
+ WHERE n.nspname = $1 AND c.relname = $2 AND a.attnum > 0 AND NOT a.attisdropped
364
+ ORDER BY a.attnum`, [schema, name]);
365
+ const columns = stats.rows.map((r) => {
366
+ /* Negative n_distinct is a ratio — "-0.3 of the rows are distinct" —
367
+ which only converts when a row estimate exists to multiply by. */
368
+ const distinct = r.n_distinct == null
369
+ ? undefined
370
+ : Number(r.n_distinct) >= 0
371
+ ? Math.round(Number(r.n_distinct))
372
+ : rows === undefined
373
+ ? undefined
374
+ : Math.round(-Number(r.n_distinct) * rows);
375
+ const common = r.common_values?.length && r.common_freqs?.length
376
+ ? r.common_values.map((v, i) => ({ value: v, frac: Number(r.common_freqs?.[i] ?? 0) }))
377
+ : undefined;
378
+ return {
379
+ column: r.column,
380
+ type: r.type,
381
+ nullFrac: r.null_frac == null ? undefined : Number(r.null_frac),
382
+ distinct,
383
+ /* The histogram's outer edges, as text from the sample. A column too
384
+ uniform to earn a histogram gets no min/max — its common values
385
+ sorted lexically would call 9 larger than 10. */
386
+ min: r.bounds?.length ? r.bounds[0] : undefined,
387
+ max: r.bounds?.length ? r.bounds[r.bounds.length - 1] : undefined,
388
+ common,
389
+ };
390
+ });
391
+ return { table, rows, columns, source: 'statistics', ms: Math.round(performance.now() - started) };
392
+ }
393
+ /* The catalog is the primary's, whatever route asks for it first: a
394
+ replica a migration has not reached yet would describe the old one, and
395
+ every read after compiles against this copy. */
396
+ introspect() {
397
+ return primaryReads(() => this.introspectNow());
398
+ }
399
+ async introspectNow() {
400
+ const db = await this.conn();
401
+ const schema = await this.describe(db);
402
+ /* The server's version, read with the catalog it goes with, so a row
403
+ scope compiles for it (scopeReads) without a statement of its own. */
404
+ this.scopeStyleRead = postgresScopeStyle(db).catch(() => POSTGRES_STYLE);
405
+ await this.scopeStyleRead;
406
+ /* Seeded, not only returned: the registry introspects on connect, and the
407
+ view, breakdown and read paths resolve against this copy. Without it the
408
+ first of them described the whole catalog again. */
409
+ this.cachedSchema = schema;
410
+ return schema;
411
+ }
412
+ scopeStyleRead;
413
+ /**
414
+ * The style a row-scoped read compiles with on this server: its CTEs
415
+ * inlined from Postgres 12 on (POSTGRES_INLINE_STYLE). Asked once per
416
+ * adapter; a failed ask is not remembered, and that read compiles as
417
+ * before — the hint is a plan, never a rule about rows.
418
+ */
419
+ scopeStyle() {
420
+ this.scopeStyleRead ??= primaryReads(() => this.conn().then(postgresScopeStyle)).catch(() => {
421
+ this.scopeStyleRead = undefined;
422
+ return POSTGRES_STYLE;
423
+ });
424
+ return this.scopeStyleRead;
425
+ }
426
+ /** The role's reach, asked of the catalog on the reader (rolePrivileges.ts). */
427
+ async privileges() {
428
+ return primaryReads(async () => postgresPrivileges(await this.conn(), this.schemas, SYSTEM_SCHEMAS));
429
+ }
430
+ /** The catalog as one handle sees it, read by postgresCatalog.ts: the
431
+ * reader on connect, or the leased writer inside a scoped transaction,
432
+ * where the same description is the evidence the deployed permissions are
433
+ * attested against. */
434
+ /* Still `async`: a caller holding this as a value gets a rejected promise
435
+ rather than a synchronous throw, exactly as the method it replaced did. */
436
+ async describe(db) {
437
+ descriptionsRead++;
438
+ return describePostgresCatalog(db, { schemas: this.schemas, label: this.label, dialect: this.dialect });
439
+ }
440
+ /** The catalog as `db` sees it at `version`, read there before: a copy of the one kept under it (sharedDescriptions),
441
+ * or described now and kept. Another adapter's copy carries this one's label. */
442
+ async describedAt(version, db, reread) {
443
+ const shared = !version.startsWith('ttl:');
444
+ const found = shared ? sharedDescriptions.get(version, this.describes) : undefined;
445
+ const kept = found && Date.now() - found.at < SHARED_DESCRIPTION_MS
446
+ ? found.schema
447
+ : shared
448
+ ? undefined
449
+ : this.catalogCache.get(version, 'describe');
450
+ if (kept) {
451
+ if (shared)
452
+ descriptionsKept++;
453
+ return { ...structuredClone(kept), label: this.label };
454
+ }
455
+ const schema = await this.describe(db);
456
+ // A write's version is read again: a description that saw DDL committed after it is not kept under it.
457
+ if (shared) {
458
+ if (!reread || (await reread()) === version)
459
+ sharedDescriptions.set(version, this.describes, Object.freeze({ schema: Object.freeze(structuredClone(schema)), at: Date.now() }));
460
+ }
461
+ else
462
+ this.catalogCache.set(version, 'describe', Object.freeze(structuredClone(schema)));
463
+ return schema;
464
+ }
465
+ /** A snapshot's description: the digest first, in the same snapshot, then the description kept under it or a new one. */
466
+ async describeSnapshot(db, committed = false) {
467
+ const version = `digest:${postgresCatalogDigestOf(await db.query(SCOPED_CATALOG_DIGEST))}`;
468
+ const reread = committed ? this.rereadOf(db, version) : undefined;
469
+ return this.describedAt(this.versionRead(db, version, reread), db, reread);
470
+ }
471
+ /** `version`, the exact one `db`'s transaction just read, said for what it attests next (noteCatalogVersion); a time-filed one is not said. */
472
+ versionRead(db, version, reread) {
473
+ if (!version.startsWith('ttl:'))
474
+ noteCatalogVersion(db, this.database, version, reread);
475
+ return version;
476
+ }
477
+ /** How a write (READ COMMITTED) reads `version` again, the digest or the epoch as it read it; a time-filed one is not read. */
478
+ rereadOf(db, version) {
479
+ if (version.startsWith('ttl:'))
480
+ return undefined;
481
+ if (version.startsWith('digest:'))
482
+ return async () => `digest:${postgresCatalogDigestOf(await db.query(SCOPED_CATALOG_DIGEST))}`;
483
+ return async () => schemaEpochOf(await db.query(schemaEpochStatement()));
484
+ }
485
+ /**
486
+ * The catalog as a scoped transaction's writer sees it, described again
487
+ * only when it has moved. Every transaction reads the catalog digest
488
+ * (postgresCatalogDigest.ts) — one statement where a description is six —
489
+ * and reuses a description read just after the same digest was, by this
490
+ * adapter or another of the same database (sharedDescriptions). Read
491
+ * before, never after: DDL committing
492
+ * between the two then leaves a digest no later read can match, so the
493
+ * description is taken again rather than trusted on a digest it postdates.
494
+ * The digest covers every schema but the system's, a superset of what
495
+ * this adapter describes, and moves with privileges and roles too. What it
496
+ * does not see, comments and the planner's row estimates, is display and
497
+ * never authority.
498
+ */
499
+ async describeScoped(db) {
500
+ // Under an epoch this transaction read, a description kept under it, by any adapter of this database, is exact.
501
+ const epoch = this.txEpochs.get(db);
502
+ return epoch !== undefined ? this.describedAt(epoch, db, this.rereadOf(db, epoch)) : this.describeSnapshot(db, true);
503
+ }
504
+ /**
505
+ * Begin a write transaction and read the schema epoch in the same message:
506
+ * `BEGIN`, the LOCK a scope pinned last time under the epoch it expects,
507
+ * and the epoch statement, in one round trip (the simple protocol carries
508
+ * several statements when none has parameters). The LOCK comes first, so
509
+ * DDL on a pinned table after the epoch was read has to wait for this
510
+ * transaction; whether the lock was the right one is then decided by the
511
+ * epoch alone. A message that fails — the store gone, a pinned table
512
+ * dropped, a privilege revoked — is only a miss: rolled back, begun again
513
+ * plainly, and the transaction describes what it needs as before.
514
+ */
515
+ async openWrite(write, options, began) {
516
+ // Begun again on the same handle (a scope's retry), what the last transaction read is not this one's.
517
+ noteCatalogVersion(write, this.database, undefined);
518
+ if (this.epochStore === false) {
519
+ if (options.digest)
520
+ return this.openWithDigest(write, options, began);
521
+ const opening = write.query('BEGIN');
522
+ options.sent?.();
523
+ await opening;
524
+ began();
525
+ const epoch = epochlessEpoch();
526
+ this.txEpochs.set(write, epoch);
527
+ return { epoch };
528
+ }
529
+ const statement = schemaEpochStatement();
530
+ let answered;
531
+ try {
532
+ const opening = write.query(`BEGIN;${options.lock?.length ? ` LOCK TABLE ${options.lock.join(', ')} IN ROW EXCLUSIVE MODE;` : ''}${options.pin ? ` ${options.pin};` : ''} ${statement}`);
533
+ // Behind it, in the same flight: what the transaction opens with (sendWriteAhead).
534
+ options.sent?.();
535
+ answered = await opening;
536
+ }
537
+ catch (error) {
538
+ const failure = error;
539
+ if ((failure.code === '42P01' || failure.code === '42501') && String(failure.message).includes(SCHEMA_EPOCH_TABLE))
540
+ this.epochStore = false;
541
+ try {
542
+ await write.query('ROLLBACK');
543
+ }
544
+ catch {
545
+ /* The BEGIN below says whether this client can go on. */
546
+ }
547
+ if (this.epochStore === false && options.digest)
548
+ return this.openWithDigest(write, options, began);
549
+ await write.query('BEGIN');
550
+ began();
551
+ return {};
552
+ }
553
+ began();
554
+ this.epochStore = true;
555
+ const results = (Array.isArray(answered) ? answered : [answered]);
556
+ const last = results.at(-1);
557
+ const epoch = schemaEpochOf(last);
558
+ if (epoch !== undefined)
559
+ this.txEpochs.set(write, this.versionRead(write, epoch, this.rereadOf(write, epoch)));
560
+ // The store is there but not whole (its trigger disabled): the digest stands in, read after the LOCK above.
561
+ else if (options.digest) {
562
+ const read = `digest:${postgresCatalogDigestOf(await write.query(SCOPED_CATALOG_DIGEST))}`;
563
+ const digest = this.versionRead(write, read, this.rereadOf(write, read));
564
+ this.txEpochs.set(write, digest);
565
+ return { epoch: digest };
566
+ }
567
+ return { epoch };
568
+ }
569
+ /**
570
+ * Without the epoch store, a scoped transaction reads the catalog digest
571
+ * (postgresCatalogDigest.ts) — it always did, to describe the scope — and
572
+ * the digest is then its epoch: one statement that moves with every catalog
573
+ * row the scope, a relation's kind and columns, and the journal's layout
574
+ * depend on, role membership, and (for a command) the journal's meta row.
575
+ * Read in the message that begins the transaction, after the LOCK the scope
576
+ * pinned last time, as the epoch is. Slower than the epoch store's one row,
577
+ * never less exact; a message that fails begins again the long way.
578
+ */
579
+ async openWithDigest(write, options, began) {
580
+ let answered;
581
+ try {
582
+ const opening = write.query(`BEGIN;${options.lock?.length ? ` LOCK TABLE ${options.lock.join(', ')} IN ROW EXCLUSIVE MODE;` : ''}${options.pin ? ` ${options.pin};` : ''} ${SCOPED_CATALOG_DIGEST}`);
583
+ options.sent?.();
584
+ answered = await opening;
585
+ }
586
+ catch {
587
+ try {
588
+ await write.query('ROLLBACK');
589
+ }
590
+ catch {
591
+ /* The BEGIN below says whether this client can go on. */
592
+ }
593
+ await write.query('BEGIN');
594
+ began();
595
+ return {};
596
+ }
597
+ began();
598
+ const results = (Array.isArray(answered) ? answered : [answered]);
599
+ const read = `digest:${postgresCatalogDigestOf(results.at(-1))}`;
600
+ const epoch = this.versionRead(write, read, this.rereadOf(write, read));
601
+ this.txEpochs.set(write, epoch);
602
+ return { epoch };
603
+ }
604
+ /**
605
+ * A reserved store's layout, attested the first time this adapter reads it
606
+ * and not again (attestOnce, postgresTransactionHandle.ts): told whether it
607
+ * stands attested. A read that fails the way a moved catalog fails one
608
+ * forgets it, and the next read attests in full.
609
+ */
610
+ async attestedStore(client, store, read) {
611
+ const key = `store:${store.name}`, attested = attestedOn(this, key);
612
+ try {
613
+ const result = await read(attested);
614
+ if (!attested)
615
+ keepAttestedOn(this, key);
616
+ return result;
617
+ }
618
+ catch (error) {
619
+ if (suspectsStores(error))
620
+ forgetAttestations(this, client);
621
+ throw error;
622
+ }
623
+ }
624
+ /** A journal's layout and meta row, attested the first time this adapter opens a transaction with it (a journal
625
+ that names no meta row, in every transaction). */
626
+ async attestJournal(write, journal) {
627
+ if (!journal.meta)
628
+ return void (await journal.attest(write));
629
+ const key = `journal:${journal.meta.name}`;
630
+ if (attestedOn(this, key))
631
+ return;
632
+ await journal.attest(write);
633
+ keepAttestedOn(this, key);
634
+ }
635
+ async query(request) {
636
+ return this.queryOn(request, await this.conn(), true);
637
+ }
638
+ async scopeReads(input) {
639
+ // Capture before metadata I/O: callers cannot change an in-flight policy.
640
+ const scope = captureRowScope(input);
641
+ const schema = await this.schema();
642
+ /* Inlined only where the catalog read at connect said the server can;
643
+ otherwise as before. Never a statement here: a scoped read's I/O is
644
+ its own reads (tenant sources ask scopeStyle() once, at construction). */
645
+ const style = this.scopeStyleRead ? await this.scopeStyleRead : POSTGRES_STYLE;
646
+ return createScopedReads(schema, scope, style, async (compiled, hint) => {
647
+ // Resolve the reader per operation so capabilities cannot bypass shutdown.
648
+ const db = await this.conn();
649
+ const started = performance.now();
650
+ const result = await db.query(compiled.text, compiled.params);
651
+ return {
652
+ columns: result.fields.map((field) => field.name),
653
+ rows: result.rows.map(hint?.binary ? withBytes : toJsonSafe),
654
+ ms: Math.round(performance.now() - started),
655
+ sql: { text: compiled.text, params: compiled.params },
656
+ };
657
+ });
658
+ }
659
+ async queryOn(request, db, read = false) {
660
+ const { schema, name } = read ? await this.readTable(request.table) : await this.assertTable(request.table, db);
661
+ const qualified = `${POSTGRES_STYLE.quote(schema)}.${POSTGRES_STYLE.quote(name)}`;
662
+ const where = buildWhere(request.filter, POSTGRES_STYLE);
663
+ const cols = request.columns?.length ? request.columns.map((c) => POSTGRES_STYLE.quote(c)).join(', ') : '*';
664
+ const order = orderClause(request.orderBy, request.orderBy?.length ? (await this.schema()).tables.find((t) => t.id === request.table) : undefined, POSTGRES_STYLE, where.params.length + 1);
665
+ /* Every read is bounded. There is no code path that reaches the database
666
+ without a LIMIT, including one that forgot to ask for one. */
667
+ const limit = clampLimit(request.limit);
668
+ /* LIMIT and OFFSET continue the placeholder numbering the WHERE clause
669
+ started, which is the one thing Postgres needs that SQLite does not. */
670
+ const n = where.params.length + order.params.length;
671
+ const text = `SELECT ${cols} FROM ${qualified}${where.text}${order.text} LIMIT $${n + 1} OFFSET $${n + 2}`;
672
+ const params = [...where.params, ...order.params, limit, clampOffset(request.offset)];
673
+ const started = performance.now();
674
+ const result = await db.query(text, params);
675
+ const ms = Math.round(performance.now() - started);
676
+ return {
677
+ // Taking columns from `fields` rather than the first row means an empty
678
+ // result still renders its headers instead of collapsing to nothing.
679
+ columns: result.fields.map((f) => f.name),
680
+ rows: result.rows.map(request.binary ? withBytes : toJsonSafe),
681
+ ms,
682
+ sql: { text, params },
683
+ };
684
+ }
685
+ /**
686
+ * Run a compiled view.
687
+ *
688
+ * Note what is *not* here: any check that the tables named in the view
689
+ * exist. `compileView` resolves the base table and every hop against the
690
+ * introspected catalog and refuses anything it cannot find, so the only
691
+ * identifiers that reach the quoting step are ones the catalog produced.
692
+ * The user's contribution — the values — never leaves the parameter array.
693
+ *
694
+ * The dialect difference this exercises is placeholder numbering:
695
+ * `POSTGRES_STYLE` counts $1..$n across the whole statement, and the joins
696
+ * sitting between the SELECT list and the WHERE clause are exactly what
697
+ * makes an off-by-one here easy and invisible.
698
+ */
699
+ async runView(view, offset = 0) {
700
+ const db = await this.conn();
701
+ const schema = await this.schema();
702
+ const compiled = compileView(schema, view, POSTGRES_STYLE, offset);
703
+ const started = performance.now();
704
+ const result = await db.query(compiled.text, compiled.params);
705
+ const ms = Math.round(performance.now() - started);
706
+ return {
707
+ columns: compiled.columns.map((c) => c.name),
708
+ rows: result.rows.map(toJsonSafe),
709
+ ms,
710
+ sql: { text: compiled.text, params: compiled.params },
711
+ resolved: compiled.columns,
712
+ joins: compiled.joins,
713
+ };
714
+ }
715
+ async runBreakdown(breakdown, offset = 0) {
716
+ const db = await this.conn();
717
+ const compiled = compileBreakdown(await this.schema(), breakdown, POSTGRES_STYLE, offset);
718
+ const started = performance.now();
719
+ const result = await db.query(compiled.text, compiled.params);
720
+ return {
721
+ columns: compiled.columns.map((c) => c.name),
722
+ rows: result.rows.map(toJsonSafe),
723
+ ms: Math.round(performance.now() - started),
724
+ sql: { text: compiled.text, params: compiled.params },
725
+ resolved: compiled.columns,
726
+ };
727
+ }
728
+ async countBreakdown(breakdown, limit) {
729
+ const db = await this.conn();
730
+ const compiled = compileBreakdownCount(await this.schema(), breakdown, POSTGRES_STYLE, undefined, limit);
731
+ const result = await db.query(compiled.text, compiled.params);
732
+ return Number(result.rows[0]?.n ?? 0);
733
+ }
734
+ async countView(view, limit) {
735
+ const db = await this.conn();
736
+ const compiled = compileViewCount(await this.schema(), view, POSTGRES_STYLE, undefined, limit);
737
+ const result = await db.query(compiled.text, compiled.params);
738
+ return Number(result.rows[0]?.n ?? 0);
739
+ }
740
+ /* Introspection here is three catalog queries; a view preview runs on every
741
+ keystroke. Caching it is the difference between a live preview and a
742
+ stutter. The registry refreshes by discarding the adapter, so a stale
743
+ cache cannot outlive an explicit reload. */
744
+ cachedSchema = null;
745
+ async schema() {
746
+ if (!this.cachedSchema)
747
+ this.cachedSchema = await this.introspect();
748
+ return this.cachedSchema;
749
+ }
750
+ /**
751
+ * The planner's row estimate, straight from `pg_class`.
752
+ *
753
+ * `reltuples` is what the planner itself uses to decide how to run a query,
754
+ * so it is the number Postgres is already trusting. It is -1 on a table that
755
+ * has never been analysed, which is a "do not know" rather than a zero —
756
+ * returning undefined there gets an exact count instead of confidently
757
+ * reporting an empty table that is not empty.
758
+ */
759
+ async estimateCount(table) {
760
+ const db = await this.conn();
761
+ const { schema, name } = await this.readTable(table);
762
+ const result = await db.query(`SELECT c.reltuples AS n
763
+ FROM pg_class c JOIN pg_namespace ns ON ns.oid = c.relnamespace
764
+ WHERE ns.nspname = $1 AND c.relname = $2`, [schema, name]);
765
+ return postgresRowEstimate(result.rows[0]?.n);
766
+ }
767
+ async count(table, filter, limit) {
768
+ return this.countOn(table, filter, await this.conn(), limit, true);
769
+ }
770
+ async countEach(items, limit) {
771
+ const relations = await Promise.all(items.map(async (item) => {
772
+ const { schema, name } = await this.readTable(item.table);
773
+ return { relation: `${POSTGRES_STYLE.quote(schema)}.${POSTGRES_STYLE.quote(name)}`, filter: item.filter };
774
+ }));
775
+ const statement = countEachStatement(relations, POSTGRES_STYLE, { limit, together: countsTogether(this.cachedSchema, items, limit) });
776
+ const result = await (await this.conn()).query(statement.text, statement.params);
777
+ return countEachRows(result.rows, items.length);
778
+ }
779
+ async countOn(table, filter, db, limit, read = false) {
780
+ const { schema, name } = read ? await this.readTable(table) : await this.assertTable(table, db);
781
+ const where = buildWhere(filter, POSTGRES_STYLE);
782
+ const text = countStatement(`${POSTGRES_STYLE.quote(schema)}.${POSTGRES_STYLE.quote(name)}${where.text}`, limit);
783
+ const result = await db.query(text, where.params);
784
+ return Number(result.rows[0]?.n ?? 0);
785
+ }
786
+ async columnsOf(schema, name, db) {
787
+ return postgresColumnsOf(schema, name, db, this.epochOf(db), this.catalogCache, () => this.conn());
788
+ }
789
+ /**
790
+ * Update one row, having proved it is one row.
791
+ *
792
+ * The pre-flight count runs on the *read* handle, so the check that decides
793
+ * whether to write cannot itself write. A key that matches anything other
794
+ * than exactly one row stops here rather than at the UPDATE — the danger in
795
+ * a cell editor was never injection, it is a WHERE broader than its author
796
+ * believed.
797
+ */
798
+ async update(request) {
799
+ // A table that keeps a history is written with its row, or not at all.
800
+ const lane = keyedLane(request);
801
+ if (this.recordsHistory(request))
802
+ return this.transact(lane, (tx) => tx.update(request));
803
+ return this.watched(this.writes.run(lane, () => this.connections.withWriteClient((write) => this.updateNow(request, write))));
804
+ }
805
+ async updateNow(request, write, read) {
806
+ const transactional = read !== undefined;
807
+ read ??= await this.conn();
808
+ const found = await this.assertTable(request.table, read);
809
+ const id = `${found.schema}.${found.name}`;
810
+ /* Postgres updates through a simple view, and the update lands on the
811
+ table underneath. Refused: what the editor showed and what changed are
812
+ then two different objects. */
813
+ if (found.isView)
814
+ throw new Refusal(`${id} is a view, so no single row can be identified.`);
815
+ const meta = await this.columnsOf(found.schema, found.name, read);
816
+ const pk = meta.filter((c) => c.is_pk).map((c) => c.name);
817
+ if (!pk.length)
818
+ throw new Refusal(`${id} has no primary key, so no single row can be identified.`);
819
+ const keyColumns = Object.keys(request.key);
820
+ /* The key must be the whole primary key and nothing else. A partial key
821
+ would let a two-column key match a whole group of rows, and extra
822
+ columns would let the caller widen the match. */
823
+ const same = keyColumns.length === pk.length && pk.every((c) => keyColumns.includes(c));
824
+ if (!same)
825
+ throw new Refusal(`Updating ${id} needs exactly its primary key (${pk.join(', ')}).`);
826
+ const entries = Object.entries(request.values ?? {});
827
+ if (!entries.length)
828
+ throw new Refusal('Nothing to update.');
829
+ const applied = {};
830
+ for (const [column, raw] of entries) {
831
+ const declared = meta.find((c) => c.name === column);
832
+ if (!declared)
833
+ throw new Refusal(`${id} has no column called "${column}".`);
834
+ if (pk.includes(column)) {
835
+ // Editing a key in place silently repoints every row that referenced
836
+ // it, or fails on the constraint. Neither belongs behind an editor.
837
+ throw new Refusal(`"${column}" is part of the primary key and cannot be edited here.`);
838
+ }
839
+ /* Coerced against the declared type, because everything typed into a
840
+ text box arrives as a string — and Postgres, unlike SQLite, rejects
841
+ "17901" for a numeric column rather than quietly taking it. */
842
+ applied[column] = coerceForColumn(declared.type || '', raw);
843
+ if (applied[column] === null && declared.notnull) {
844
+ throw new Refusal(`"${column}" is NOT NULL and cannot be emptied.`);
845
+ }
846
+ }
847
+ const filter = {
848
+ groups: [pk.map((column) => ({ column, op: '=', value: request.key[column] }))],
849
+ };
850
+ /* Outside a transaction the count runs first, on the read handle. Inside
851
+ one the UPDATE's own row count is the check: the key is the whole
852
+ primary key, so it matches one row or none, and a refusal rolls the
853
+ transaction back. */
854
+ const refuseMatches = (matches) => {
855
+ if (request.expected !== undefined && matches === 0)
856
+ throw new WriteConflict();
857
+ throw new Refusal(`Refused: that key matches ${matches} rows, not 1. An update is only allowed when it is certain which row it changes.`);
858
+ };
859
+ const matches = transactional ? 1 : await this.count(id, filter);
860
+ if (matches !== 1)
861
+ refuseMatches(matches);
862
+ /* The WHERE is built after the SET, so its placeholders continue the same
863
+ numbering — `$1, $2` for the assignments, `$3…` for the key. Postgres
864
+ numbers parameters rather than taking them in order, which is the one
865
+ place this differs from the other two adapters. */
866
+ const columns = Object.keys(applied);
867
+ const assignments = columns.map((c, i) => `${POSTGRES_STYLE.quote(c)} = ${POSTGRES_STYLE.placeholder(i + 1)}`).join(', ');
868
+ const where = buildWhere(filter, POSTGRES_STYLE, columns.length + 1);
869
+ const guard = buildUpdateGuard(request, Object.fromEntries(meta.map((column) => [column.name, column.type])), POSTGRES_STYLE, 'postgres', columns.length + where.params.length + 1);
870
+ const text = `UPDATE ${qualified(found)} SET ${assignments}${where.text}${guard.text}`;
871
+ const params = [...Object.values(applied), ...where.params, ...guard.params];
872
+ const result = await write.query(text, params);
873
+ if (request.expected !== undefined && Number(result.rowCount ?? 0) === 0)
874
+ throw new WriteConflict();
875
+ if (transactional && Number(result.rowCount ?? 0) !== 1)
876
+ refuseMatches(Number(result.rowCount ?? 0));
877
+ /* Postgres counts rows matched, not rows whose bytes changed, so saving a
878
+ cell without changing its value still reports 1 — the editor would
879
+ otherwise tell the user a successful write failed. */
880
+ return { affected: Number(result.rowCount ?? 0), applied, sql: { text, params } };
881
+ }
882
+ /**
883
+ * Add one row.
884
+ *
885
+ * `update`'s guards where they apply, with the key rules inverted: a primary
886
+ * key may be supplied (a country code somebody types) or left for the
887
+ * database to assign. What is deliberately *not* pre-checked is
888
+ * completeness — a missing NOT NULL column with no default is the database's
889
+ * refusal to make, and its message names the column better than a paraphrase
890
+ * would.
891
+ *
892
+ * `RETURNING *` rather than a re-read by key: one statement gives back the
893
+ * row as stored, defaults filled and the assigned key included, with no
894
+ * window for another write to land in between.
895
+ */
896
+ async insert(request) {
897
+ // A new row has no lane of its own: nothing earlier can be writing it.
898
+ if (this.recordsHistory(request))
899
+ return this.transact([], (tx) => tx.insert(request));
900
+ return this.watched(this.writes.run([], () => this.connections.withWriteClient((write) => this.insertNow(request, write))));
901
+ }
902
+ async insertNow(request, write, read, scoped = false) {
903
+ read ??= await this.conn();
904
+ const found = await this.assertTable(request.table, read);
905
+ const id = `${found.schema}.${found.name}`;
906
+ if (found.isView)
907
+ throw new Refusal(`${id} is a view. Insert into the table underneath it.`);
908
+ const meta = await this.columnsOf(found.schema, found.name, read);
909
+ const entries = Object.entries(request.values ?? {});
910
+ const applied = {};
911
+ for (const [column, raw] of entries) {
912
+ const declared = meta.find((c) => c.name === column);
913
+ if (!declared)
914
+ throw new Refusal(`${id} has no column called "${column}".`);
915
+ applied[column] = coerceForColumn(declared.type || '', raw);
916
+ if (applied[column] === null && declared.notnull) {
917
+ throw new Refusal(`"${column}" is NOT NULL and cannot be null. Leave it out to use its default, if it has one.`);
918
+ }
919
+ }
920
+ const columns = Object.keys(applied);
921
+ const text = `INSERT INTO ${qualified(found)}` +
922
+ (columns.length
923
+ ? ` (${columns.map((c) => POSTGRES_STYLE.quote(c)).join(', ')}) VALUES (${columns.map((_, i) => POSTGRES_STYLE.placeholder(i + 1)).join(', ')})`
924
+ : ' DEFAULT VALUES') +
925
+ ' RETURNING *';
926
+ const params = Object.values(applied);
927
+ const result = await write.query(text, params);
928
+ const row = result.rows[0] ? toJsonSafe(result.rows[0]) : undefined;
929
+ const pk = meta.filter((c) => c.is_pk).map((c) => c.name);
930
+ /* A scope re-reads the stored row by this key. A bytea key would come
931
+ back as its "<n bytes>" label, which matches nothing and proves
932
+ nothing; refuse rather than let a label stand in for the value. */
933
+ if (scoped &&
934
+ pk.some((column) => {
935
+ const value = result.rows[0]?.[column];
936
+ return !(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value)));
937
+ }))
938
+ throw new Refusal('The stored key cannot be checked in this row scope.');
939
+ /* Read off the returned row rather than off what was sent: a key the
940
+ database assigned is only knowable from what came back, and a key that
941
+ was supplied comes back identical. */
942
+ const key = {};
943
+ if (row)
944
+ for (const c of pk)
945
+ key[c] = row[c];
946
+ return { key, row, applied, sql: { text, params } };
947
+ }
948
+ /**
949
+ * Remove one row, having proved it is one row.
950
+ *
951
+ * What is *not* checked here is whether the row should go: what references
952
+ * it, and what the database will do to those references, is answered above
953
+ * this layer where the schema is in hand. A `RESTRICT` that fires is the
954
+ * correct outcome in the engine's own words.
955
+ */
956
+ async remove(request) {
957
+ const lane = keyedLane(request);
958
+ if (this.recordsHistory(request))
959
+ return this.transact(lane, (tx) => tx.remove(request));
960
+ return this.watched(this.writes.run(lane, () => this.connections.withWriteClient((write) => this.removeNow(request, write))));
961
+ }
962
+ recordsHistory(request) {
963
+ return capturesWrites(this, request?.table);
964
+ }
965
+ /* What rides beneath one transaction's writes on its leased writer, beneath
966
+ every facade (writeCapture.ts): the App's record history and notices. The
967
+ before image is read under the row's lock, so it is the one the write
968
+ replaces. */
969
+ historyWrites(write, base,
970
+ /* The capture's read of the row it keeps a before image of: an unscoped transaction's reads by key, which a row
971
+ sent ahead may answer (plainReads). */
972
+ query = (request) => this.queryOn(request, write)) {
973
+ const history = beginWriteCapture(this, {
974
+ writer: { dialect: 'postgres', client: write },
975
+ read: async (table, key, columns, lock) => {
976
+ if (lock)
977
+ await this.lockRecordNow({ table, key }, write);
978
+ const result = await query({
979
+ table,
980
+ ...(columns ? { columns } : {}),
981
+ limit: 2,
982
+ offset: 0,
983
+ filter: { groups: [Object.entries(key).map(([column, value]) => ({ column, op: '=', value }))] },
984
+ });
985
+ return result.rows.length === 1 ? result.rows[0] : undefined;
986
+ },
987
+ rows: async (table, where, columns, limit) => (await this.queryOn({
988
+ table,
989
+ columns,
990
+ limit,
991
+ offset: 0,
992
+ filter: { groups: [Object.entries(where).map(([column, value]) => ({ column, op: '=', value }))] },
993
+ }, write)).rows,
994
+ behind: (table, ride) => {
995
+ // The table as updateNow names it: assertPostgresTable reads a bare name in the public schema.
996
+ const update = `UPDATE ${qualified(plainTable(table))} SET `;
997
+ return behindWrite(write, (text) => text.startsWith(update), ride);
998
+ },
999
+ });
1000
+ if (!history)
1001
+ return { ...base, finish: async () => { }, committed: () => { } };
1002
+ return {
1003
+ insert: (r) => history.insert(r, base.insert),
1004
+ update: (r) => history.update(r, base.update),
1005
+ remove: (r) => history.remove(r, base.remove),
1006
+ tell: (r) => history.tell(r),
1007
+ finish: () => finishWriteCapture(history),
1008
+ committed: () => committedWriteCapture(history),
1009
+ };
1010
+ }
1011
+ async removeNow(request, write, read) {
1012
+ const transactional = read !== undefined;
1013
+ read ??= await this.conn();
1014
+ const found = await this.assertTable(request.table, read);
1015
+ const id = `${found.schema}.${found.name}`;
1016
+ if (found.isView)
1017
+ throw new Refusal(`${id} is a view, so no single row can be identified.`);
1018
+ const meta = await this.columnsOf(found.schema, found.name, read);
1019
+ const pk = meta.filter((c) => c.is_pk).map((c) => c.name);
1020
+ if (!pk.length)
1021
+ throw new Refusal(`${id} has no primary key, so no single row can be identified.`);
1022
+ const keyColumns = Object.keys(request.key);
1023
+ const same = keyColumns.length === pk.length && pk.every((c) => keyColumns.includes(c));
1024
+ if (!same)
1025
+ throw new Refusal(`Deleting from ${id} needs exactly its primary key (${pk.join(', ')}).`);
1026
+ const filter = {
1027
+ groups: [pk.map((column) => ({ column, op: '=', value: request.key[column] }))],
1028
+ };
1029
+ // As an update's: inside a transaction the DELETE's own count is the check.
1030
+ const refuseMatches = (matches) => {
1031
+ throw new Refusal(`Refused: that key matches ${matches} rows, not 1. A delete is only allowed when it is certain which row it removes.`);
1032
+ };
1033
+ const matches = transactional ? 1 : await this.count(id, filter);
1034
+ if (matches !== 1)
1035
+ refuseMatches(matches);
1036
+ const where = buildWhere(filter, POSTGRES_STYLE);
1037
+ const text = `DELETE FROM ${qualified(found)}${where.text}`;
1038
+ const result = await write.query(text, where.params);
1039
+ if (transactional && Number(result.rowCount ?? 0) !== 1)
1040
+ refuseMatches(Number(result.rowCount ?? 0));
1041
+ return { affected: Number(result.rowCount ?? 0), sql: { text, params: where.params } };
1042
+ }
1043
+ /**
1044
+ * Several writes as one, or none of them.
1045
+ *
1046
+ * Plain `BEGIN` / `COMMIT` on the write connection. The callback is handed
1047
+ * the same write bodies the public methods call, so every guard — the whole
1048
+ * key, the pre-flight count, the type coercion — applies inside a
1049
+ * transaction exactly as it does outside one.
1050
+ *
1051
+ * Reads, metadata checks, and preflight counts use the same leased client.
1052
+ * This preserves read-after-write visibility; it does not promise serializable
1053
+ * isolation. Call lockRecord explicitly before capturing protected originals:
1054
+ * where an App writes, other transactions of this process run beside this
1055
+ * one (writeLanes.ts), and a row read without its lock may change before
1056
+ * the write that depends on it, as a row read in another pod always could.
1057
+ */
1058
+ async transaction(run) {
1059
+ return this.transact([], run);
1060
+ }
1061
+ /* A transaction in the lanes `lane` names: a single record's write that
1062
+ keeps a history names its record, so it keeps its place behind that
1063
+ record's earlier writes. */
1064
+ async transact(lane, run) {
1065
+ const hint = writeAhead();
1066
+ const schema = hint ? await this.schema().catch(() => undefined) : undefined;
1067
+ return this.watched(this.writes.run(lane, () => this.connections.withWriteClient(async (leased) => {
1068
+ // Its own handle, as a journal's: what it was told leaves behind its BEGIN, and writes nothing reads with its COMMIT.
1069
+ // A writer that pipelines sends reads asked together in one flight (TransactionOperations.read).
1070
+ const write = transactionHandle(leased, this), operations = new TransactionOperations(sendsAhead(write));
1071
+ let began = false;
1072
+ let commitAttempted = false;
1073
+ let committed = false;
1074
+ try {
1075
+ const reads = await this.openPlain(write, schema, undefined, hint, () => {
1076
+ began = true;
1077
+ });
1078
+ const writes = this.historyWrites(write, {
1079
+ insert: (r) => this.insertNow(r, write, write),
1080
+ remove: (r) => this.removeNow(r, write, write),
1081
+ update: (r) => this.updateNow(r, write, write),
1082
+ }, reads);
1083
+ // The transaction's own work runs without the hint: a transaction it opens is not this one.
1084
+ const out = await withoutWriteAhead(() => run({
1085
+ lockRecord: (r) => queueRecordLock(operations, r, (request) => this.lockRecordNow(request, write)),
1086
+ query: (r) => operations.read(() => reads(r)),
1087
+ count: (table, filter) => operations.read(() => this.countOn(table, filter, write)),
1088
+ insert: (r) => operations.run(() => writes.insert(r)),
1089
+ remove: (r) => operations.run(() => writes.remove(r)),
1090
+ update: (r) => operations.run(() => writes.update(r)),
1091
+ ...(writes.tell ? { tell: (r) => operations.run(() => writes.tell(r)) } : {}),
1092
+ }));
1093
+ await operations.finish();
1094
+ await writes.finish();
1095
+ commitAttempted = true;
1096
+ await write.query('COMMIT');
1097
+ committed = true;
1098
+ writes.committed();
1099
+ return out;
1100
+ }
1101
+ catch (err) {
1102
+ await operations.finish().catch(() => { });
1103
+ let rolledBack = false;
1104
+ if (began) {
1105
+ try {
1106
+ await write.query('ROLLBACK');
1107
+ rolledBack = true;
1108
+ }
1109
+ catch {
1110
+ /* A failed acknowledgement cannot establish the outcome. */
1111
+ }
1112
+ }
1113
+ // A write sent with the COMMIT failed and the server answered it as a ROLLBACK: nothing committed.
1114
+ if (err instanceof RolledBackAtCommit)
1115
+ throw err.cause;
1116
+ // withWriteClient destroys on any callback error, including a failed
1117
+ // BEGIN and a lost COMMIT acknowledgement followed by successful rollback.
1118
+ if (commitAttempted || (began && !rolledBack))
1119
+ throw new TransactionOutcomeUnknown(err);
1120
+ throw err;
1121
+ }
1122
+ finally {
1123
+ this.txEpochs.delete(write);
1124
+ reportWriteAhead(aheadReport(write), committed);
1125
+ }
1126
+ })));
1127
+ }
1128
+ /**
1129
+ * Begin an unscoped write transaction with what it was told it opens with
1130
+ * (writeAhead.ts) behind its BEGIN, as a scoped one does (sendWriteAhead):
1131
+ * the journal's receipt lookup, and for each target its lock and its whole
1132
+ * row, kept to answer the transaction's reads of it by key (keyRows.ts) —
1133
+ * the command's own read of its target, its handler's, the history
1134
+ * capture's before image. The lock goes first, so the row is read under it
1135
+ * and cannot move until this transaction ends; its own writes move the
1136
+ * statement generation and are read again. One that fails aborts the
1137
+ * opening, which begins again without them. Answers the transaction's
1138
+ * reader: by key from the kept rows where they answer, else in its turn.
1139
+ */
1140
+ async openPlain(write, schema, journal, hint, began) {
1141
+ const keyRows = new KeyRows(() => generationOf(write));
1142
+ let aheads = [];
1143
+ await this.openWrite(write, schema && hint ? { sent: () => void (aheads = this.sendPlainAhead(write, schema, keyRows, journal, hint)) } : {}, began);
1144
+ if (aheads.length && (await aheadsFailed(aheads))) {
1145
+ redoWithoutAhead(write);
1146
+ keyRows.forget();
1147
+ // Its ROLLBACK lets go of every lock it held.
1148
+ heldLocks.delete(write);
1149
+ heldRows.delete(write);
1150
+ await write.query('ROLLBACK');
1151
+ await this.openWrite(write, {}, began);
1152
+ }
1153
+ const plain = (request) => this.queryOn(request, write);
1154
+ if (!schema || !aheads.length)
1155
+ return plain;
1156
+ /* A table is asked for bare (`task`) or by its id (`public.task`), as assertPostgresTable reads either: the kept
1157
+ rows are filed under the id, and a read asked in its turn is sent as it was asked. */
1158
+ const asked = new WeakMap();
1159
+ const kept = keyRows.over({ query: (request) => plain(asked.get(request) ?? request) }, PLAIN_SCOPE, schema, (request) => plainStatement(request), (row, request) => (request.binary ? withBytes : toJsonSafe)(row), false,
1160
+ /* A row the opening locked FOR UPDATE cannot move until this transaction ends: read by key after a write of
1161
+ the transaction's own has dropped what was kept of it, it is read whole once, and the reads of it that follow
1162
+ (the machine's columns, the history capture's before image) are answered from that read, as a scoped
1163
+ transaction's are (keyRows.ts, `widen`). The driver's own rows, formatted as each read asks. */
1164
+ {
1165
+ held: (table, key) => heldRows.get(write)?.has(lockKey(...rowLockStatement(table, key))) === true,
1166
+ reads: {
1167
+ query: async (request) => {
1168
+ const sql = plainStatement(request);
1169
+ const result = await write.query(sql.text, sql.params);
1170
+ return { columns: result.fields.map((field) => field.name), rows: result.rows, ms: 0, sql };
1171
+ },
1172
+ },
1173
+ }).query;
1174
+ return (request) => {
1175
+ const id = typeof request?.table === 'string' ? catalogId(request.table) : request?.table;
1176
+ if (id === request?.table)
1177
+ return kept(request);
1178
+ const named = { ...request, table: id };
1179
+ asked.set(named, request);
1180
+ return kept(named);
1181
+ };
1182
+ }
1183
+ /** The statements openPlain sends behind the BEGIN; each exactly what the transaction sends in its turn (lockRecordNow, queryOn). */
1184
+ sendPlainAhead(write, schema, keyRows, journal, hint) {
1185
+ if (!sendsAhead(write))
1186
+ return [];
1187
+ const opening = [];
1188
+ try {
1189
+ if (hint.receipt !== undefined && journal?.ahead)
1190
+ opening.push(...journal.ahead(hint.receipt));
1191
+ }
1192
+ catch {
1193
+ /* Asked in its turn. */
1194
+ }
1195
+ const answers = sendAhead(write, opening);
1196
+ for (const target of hint.targets) {
1197
+ const table = schema.tables.find((candidate) => candidate.id === catalogId(target.table));
1198
+ if (!table || table.isView || isRuntimeRelation(table.id))
1199
+ continue;
1200
+ const pk = table.columns.filter((column) => column.primaryKey).map((column) => column.name);
1201
+ const key = target.key;
1202
+ if (!pk.length ||
1203
+ Object.keys(key).length !== pk.length ||
1204
+ !pk.every((column) => Object.hasOwn(key, column) && (typeof key[column] === 'string' || typeof key[column] === 'number')))
1205
+ continue;
1206
+ const filter = { groups: [pk.map((column) => ({ column, op: '=', value: key[column] }))] };
1207
+ if (!hint.readOnly && (hint.locks || (hint.writes && capturesWrites(this, table.id)))) {
1208
+ /* lockRecordNow's statement, where the transaction will send it (the save's own lock, the history capture's). Held rather than answered once: the row stays locked until the transaction ends,
1209
+ so each later lock of it in this transaction (the command's, the history capture's) is answered by it, even
1210
+ after a write to another record has moved the statement generation. */
1211
+ const [text, values] = rowLockStatement(table, key);
1212
+ const lock = holdAhead(write, { text, values });
1213
+ if (lock) {
1214
+ answers.push(lock.answer);
1215
+ const locks = heldLocks.get(write) ?? new Map();
1216
+ heldLocks.set(write, locks);
1217
+ const rows = heldRows.get(write) ?? new Set();
1218
+ heldRows.set(write, rows);
1219
+ void lock.answer.then((result) => {
1220
+ if (result.rows.length <= 1)
1221
+ locks.set(lockKey(text, values), lock.take);
1222
+ // Only a row that is there is held: one that is not could be inserted by another transaction meanwhile.
1223
+ if (result.rows.length === 1)
1224
+ rows.add(lockKey(text, values));
1225
+ }, () => { });
1226
+ }
1227
+ }
1228
+ const read = plainStatement({ table: table.id, filter, limit: 2, offset: 0 });
1229
+ const row = read && holdAhead(write, { text: read.text, values: read.params });
1230
+ if (row) {
1231
+ answers.push(row.answer);
1232
+ keyRows.keep(PLAIN_SCOPE, table, filter, row.answer.then((result) => result.rows), row.take);
1233
+ }
1234
+ }
1235
+ return answers;
1236
+ }
1237
+ /**
1238
+ * Same-source records and a durable journal in one transaction, the
1239
+ * journal lent by the App framework (storeHost.ts TransactionJournal): the
1240
+ * command journal, or a public placement's. Lent through the store host,
1241
+ * never on the class, so the database browser has no journal path at all.
1242
+ *
1243
+ * The SQLite version opens `BEGIN IMMEDIATE`, which serialises every writer
1244
+ * in the file; here the transaction is plain `BEGIN` at the server's
1245
+ * default READ COMMITTED, and the journal's per-scope advisory lock does
1246
+ * the one piece of that serialisation a command needs — two deliveries of
1247
+ * the same command cannot both execute. SERIALIZABLE was the alternative
1248
+ * and was refused: it would turn unrelated concurrent commands into 40001
1249
+ * retries the SQLite path never has, and a conditional `expected` update
1250
+ * already refuses a stale row in the write itself.
1251
+ *
1252
+ * The journal is attested inside the transaction and before the callback,
1253
+ * so an unmigrated or altered layout refuses without a write. The outcome
1254
+ * contract is SQLite's: a failure after COMMIT was sent, or a rollback that
1255
+ * was not acknowledged, is `unknown`; everything else is `not_applied`.
1256
+ * `withWriteClient` destroys the client on any failure, so a connection in
1257
+ * an uncertain state is never handed to the next write.
1258
+ */
1259
+ async journalTransaction(journal, run, options = {}) {
1260
+ if (options.scope === undefined) {
1261
+ /* The verifier supplies current authority, never actor/receipt scope or record filtering. */
1262
+ if (options.ordinaryVerify !== undefined && !this.verifiedSource())
1263
+ throw new Refusal('This verified command transaction is unavailable.');
1264
+ const verify = options.ordinaryVerify;
1265
+ return this.ordinaryJournalTransaction(journal, run, verify && (async () => void (await verify())));
1266
+ }
1267
+ return this.scopedTransact(options.scope, journal, run, options.verify, options.bound);
1268
+ }
1269
+ async ordinaryJournalTransaction(journal, run, verify) {
1270
+ /* The command host supplies target and receipt lanes through inWriteLanes.
1271
+ Extra records discovered inside the handler still rely on row locks and
1272
+ compare-and-set, as they do across processes. */
1273
+ const hint = writeAhead();
1274
+ const schema = hint ? await this.schema().catch(() => undefined) : undefined;
1275
+ return this.watched(this.writes.run([], () => this.connections.withWriteClient(async (leased) => {
1276
+ // Its own handle, as a scoped transaction's: writes nothing reads leave with its COMMIT (beforeCommit).
1277
+ // A writer that pipelines sends reads asked together in one flight (TransactionOperations.read).
1278
+ const write = transactionHandle(leased, this), operations = new TransactionOperations(sendsAhead(write));
1279
+ let began = false;
1280
+ let commitAttempted = false;
1281
+ let committed = false;
1282
+ try {
1283
+ const reads = await this.openPlain(write, schema, journal, hint, () => {
1284
+ began = true;
1285
+ });
1286
+ await this.attestJournal(write, journal);
1287
+ if (verify)
1288
+ await verify();
1289
+ const writes = this.historyWrites(write, {
1290
+ insert: (r) => this.insertNow(r, write, write),
1291
+ remove: (r) => this.removeNow(r, write, write),
1292
+ update: (r) => this.updateNow(r, write, write),
1293
+ }, reads);
1294
+ const records = {
1295
+ lockRecord: (r) => queueRecordLock(operations, r, (request) => this.lockRecordNow(request, write)),
1296
+ query: (r) => operations.read(() => reads(r)),
1297
+ count: (table, filter) => operations.read(() => this.countOn(table, filter, write)),
1298
+ insert: (r) => operations.run(() => writes.insert(r)),
1299
+ remove: (r) => operations.run(() => writes.remove(r)),
1300
+ update: (r) => operations.run(() => writes.update(r)),
1301
+ ...(writes.tell ? { tell: (r) => operations.run(() => writes.tell(r)) } : {}),
1302
+ };
1303
+ // The transaction's own work runs without the hint: a transaction it opens is not this one.
1304
+ const out = await withoutWriteAhead(() => run(Object.freeze(records), journal.open(write, operations)));
1305
+ await operations.finish();
1306
+ await writes.finish();
1307
+ if (verify) {
1308
+ // All storage work precedes the final native observation. No further
1309
+ // asynchronous work is allowed here except the COMMIT itself. Separate
1310
+ // auth/business databases still do not provide atomic revocation.
1311
+ await verify();
1312
+ }
1313
+ commitAttempted = true;
1314
+ await write.query('COMMIT');
1315
+ committed = true;
1316
+ writes.committed();
1317
+ return out;
1318
+ }
1319
+ catch (error) {
1320
+ await operations.finish().catch(() => { });
1321
+ let rolledBack = false;
1322
+ if (began) {
1323
+ try {
1324
+ await write.query('ROLLBACK');
1325
+ rolledBack = true;
1326
+ }
1327
+ catch {
1328
+ /* A failed acknowledgement cannot establish the outcome. */
1329
+ }
1330
+ }
1331
+ // A write sent with the COMMIT failed and the COMMIT rolled back: nothing committed, and its failure is what is said.
1332
+ if (error instanceof RolledBackAtCommit)
1333
+ throw journal.failure(error.cause, 'not_applied');
1334
+ throw journal.failure(error, commitAttempted || (began && !rolledBack) ? 'unknown' : 'not_applied');
1335
+ }
1336
+ finally {
1337
+ this.txEpochs.delete(write);
1338
+ reportWriteAhead(aheadReport(write), committed);
1339
+ }
1340
+ })));
1341
+ }
1342
+ /**
1343
+ * Private same-source row enforcement, with the SQLite guarantees.
1344
+ *
1345
+ * SQLite gets "the row the scope checked is the row the write changes"
1346
+ * from `BEGIN IMMEDIATE`, which excludes every other writer for the whole
1347
+ * transaction. Here the transaction is plain `BEGIN` at the server's
1348
+ * default READ COMMITTED, and the guarantee is made per row instead: before
1349
+ * a target is scope-checked its row is taken `FOR UPDATE`, and before a
1350
+ * parent is checked its row is taken `FOR SHARE`. A lock waits for any
1351
+ * in-flight writer of that row to finish, and the guard read that follows
1352
+ * takes a fresh snapshot — READ COMMITTED gives one per statement — so it
1353
+ * sees the version that writer committed and nobody can commit another
1354
+ * until this transaction ends. A row a concurrent writer moved out of scope
1355
+ * is therefore refused, whether it moved before the check or while the
1356
+ * check waited. SERIALIZABLE would prove the same thing by aborting one of
1357
+ * the two with 40001, which the SQLite path never does and every handler
1358
+ * would have to learn to retry; the explicit locks make the refusal an
1359
+ * ordinary one.
1360
+ *
1361
+ * The catalog is read on the writer inside the transaction after the
1362
+ * scope's tables are pinned against DDL (`postgresScopeCatalog.ts`), and
1363
+ * handed out as detached evidence for the caller to attest. Outcomes are
1364
+ * the transaction contract's: a failure after COMMIT was sent, or a
1365
+ * rollback that was not acknowledged, is unknown, and the client is
1366
+ * discarded rather than reused. Everything else did not commit.
1367
+ */
1368
+ async scopeTransaction(input, run) {
1369
+ const scope = captureRowScope(input);
1370
+ return this.scopedTransact(scope, undefined, (tx, _journal, catalog) => run(tx, catalog));
1371
+ }
1372
+ /**
1373
+ * Private source-owner boundary, deliberately absent from Adapter: the
1374
+ * PostgreSQL counterpart of `SqliteAdapter.scopeVerifiedTransaction`. The
1375
+ * verifier is trusted host code — the tenant write source's attestation and
1376
+ * the store's membership fence (`assertPostgresTenantMembershipFence`) —
1377
+ * never a business callback or a wire value, and must not retain the
1378
+ * client. It runs on the leased writer inside the transaction twice: after
1379
+ * the scope's tables are pinned and the catalog read, before the callback;
1380
+ * and after the callback's operations drain, before COMMIT. It is handed the
1381
+ * catalog evidence and the adapter's own introspection on the writer, for
1382
+ * anything it pins itself. SQLite also re-verifies around every operation,
1383
+ * because its fence is a snapshot check; here the fence is row locks held
1384
+ * to the end of the transaction, so the two checks bound its lifetime.
1385
+ */
1386
+ async scopeVerifiedTransaction(input, verify, run) {
1387
+ const scope = captureRowScope(input);
1388
+ if (typeof verify !== 'function' || typeof run !== 'function' || Object.keys(scope).some(isRuntimeRelation))
1389
+ throw new Refusal('This verified transaction is unavailable.');
1390
+ return this.scopedTransact(scope, undefined, (tx, _journal, catalog) => run(tx, catalog), verify);
1391
+ }
1392
+ /**
1393
+ * Private source-owner boundary, deliberately absent from Adapter: one
1394
+ * `REPEATABLE READ, READ ONLY` transaction on a leased reader, for trusted
1395
+ * host code that pins, attests and reads inside a single snapshot (the
1396
+ * PostgreSQL tenant read source). It is what a deferred `BEGIN` on its
1397
+ * read-only handle gives the SQLite tenant reader: every statement sees one
1398
+ * committed state. PostgreSQL takes that snapshot at the first query, not at
1399
+ * BEGIN, so an owner that takes its locks before its first query attests
1400
+ * what committed before them, and DDL after them waits. A ROLLBACK that
1401
+ * fails surfaces as TransactionOutcomeUnknown; any failure discards the
1402
+ * client rather than returning it to the pool.
1403
+ */
1404
+ async readSnapshot(run, observeCheckout = false, prologue, ahead = false,
1405
+ /* The prologue is the snapshot's whole question (a membership fence): BEGIN, the prologue and COMMIT in one
1406
+ round trip where they were two. `run` is handed its answers and nothing it can ask. */
1407
+ whole = false) {
1408
+ return readSnapshotOn(this.connections, (db, checkout) => (checkout ? PostgresAdapter.prototype.describe.call(this, db) : this.describeSnapshot(db)), run, observeCheckout, prologue, ahead, whole);
1409
+ }
1410
+ /** Private source-shape owner, not an authorization proof or generic Adapter
1411
+ * capability. Only this actual adapter's native snapshot runner is used. */
1412
+ capturePinnedReadSnapshot(dependencies, assertCurrent) {
1413
+ if (typeof assertCurrent !== 'function' || isProxy(assertCurrent)) {
1414
+ throw new Refusal('The pinned PostgreSQL read snapshot is unavailable.');
1415
+ }
1416
+ const connections = pinnedReaders.get(this);
1417
+ const nativeCurrent = () => {
1418
+ if (!connections)
1419
+ throw new Refusal('The pinned PostgreSQL read snapshot is unavailable.');
1420
+ const descriptor = Object.getOwnPropertyDescriptor(this, 'connections');
1421
+ if (!descriptor || !('value' in descriptor) || descriptor.value !== connections) {
1422
+ throw new Refusal('The pinned PostgreSQL read snapshot is unavailable.');
1423
+ }
1424
+ };
1425
+ nativeCurrent();
1426
+ return createPinnedPostgresReadOwner((run) => {
1427
+ nativeCurrent();
1428
+ return (PostgresAdapter.prototype.readSnapshot).call(this, run, true);
1429
+ }, dependencies, () => {
1430
+ nativeCurrent();
1431
+ assertCurrent();
1432
+ nativeCurrent();
1433
+ });
1434
+ }
1435
+ /* Scoped records and, for a command, the durable journal and fresh catalog
1436
+ evidence in one transaction. The journal is attested before the catalog
1437
+ is read, so an unmigrated layout refuses before any lock is taken. */
1438
+ async scopedTransact(scope, journal, run, verify, bound) {
1439
+ // This transaction's own handle: what its verifier learns is kept on it, never on the pooled client.
1440
+ /* No lane: every target is taken FOR UPDATE and every parent FOR SHARE
1441
+ before it is checked (below), which is what orders two of these
1442
+ across connections, in this process as across pods. */
1443
+ const hint = writeAhead();
1444
+ return this.writes.run([], () => this.connections.withWriteClient(async (leased) => {
1445
+ // A writer that pipelines sends reads asked together in one flight (TransactionOperations.read).
1446
+ const write = transactionHandle(leased, this), operations = new TransactionOperations(sendsAhead(write));
1447
+ let began = false;
1448
+ let commitAttempted = false;
1449
+ let committed = false;
1450
+ try {
1451
+ /* The scope catalog is kept per set of scoped tables under the epoch;
1452
+ the tables it pinned last time are pinned in the message that reads
1453
+ the epoch, so a hit asks the catalog nothing (openWrite). */
1454
+ const scopeKey = `scope:${Object.keys(scope).sort().join('\u0000')}`;
1455
+ const peeked = this.catalogCache.peek(scopeKey)?.value;
1456
+ const pinned = peeked?.pinned;
1457
+ const pin = verifierPin(verify);
1458
+ /* What the transaction opens with leaves behind its BEGIN, in the
1459
+ same round trip (writeAhead.ts): the verifier's fence, the
1460
+ journal's receipt lookup, each target's lock and row. Each answers
1461
+ the identical statement once when the transaction asks it, and a
1462
+ target's row answers its reads by key (keyRows.ts). */
1463
+ const keyRows = new KeyRows(() => generationOf(write));
1464
+ // The rows whose locks were sent ahead, held from the moment their answers came back (sendWriteAhead).
1465
+ const lockedAhead = new Set();
1466
+ let aheads = [];
1467
+ const opening = { lock: pinned, pin, digest: true };
1468
+ let opened = await this.openWrite(write, {
1469
+ ...opening,
1470
+ sent: () => void (aheads = this.sendWriteAhead(write, scope, peeked, keyRows, journal, verify, hint, lockedAhead)),
1471
+ }, () => {
1472
+ began = true;
1473
+ });
1474
+ if (aheads.length && (await aheadsFailed(aheads))) {
1475
+ /* One failed, which aborted the transaction before anything took
1476
+ an answer: it begins again without them. Rare, and counted. */
1477
+ lockedAhead.clear();
1478
+ redoWithoutAhead(write);
1479
+ began = false;
1480
+ await write.query('ROLLBACK');
1481
+ opened = await this.openWrite(write, opening, () => {
1482
+ began = true;
1483
+ });
1484
+ }
1485
+ if (journal)
1486
+ await this.attestJournal(write, journal);
1487
+ const catalog = this.catalogCache.get(opened.epoch, scopeKey) ??
1488
+ this.catalogCache.set(opened.epoch, scopeKey, await readPostgresScopeCatalog(write, scope, (db) => this.describeScoped(db)));
1489
+ // Rows read under another epoch's catalog answer nothing: the statements sent ahead were built from it.
1490
+ if (catalog !== peeked) {
1491
+ keyRows.forget();
1492
+ lockedAhead.clear();
1493
+ }
1494
+ const described = () => this.describeScoped(write);
1495
+ // The epoch is passed only when it was read after the verifier's own pin: then it vouches for what that pin holds.
1496
+ if (verify)
1497
+ await verify(write, catalog, described, pin !== undefined ? this.txEpochs.get(write) : undefined);
1498
+ if (journal?.admit)
1499
+ await journal.admit(write, catalog, scope);
1500
+ // Runtime state is not command-handler evidence; the complete graph
1501
+ // stays behind for row/FK enforcement exactly as on SQLite.
1502
+ const evidence = journal
1503
+ ? mapScopeCatalog(catalog.schema, new Map(catalog.schema.tables.filter((table) => !isRuntimeRelation(table.id)).map((table) => [table.id, table.id])))
1504
+ : captureScopeCatalog(catalog.schema);
1505
+ // Guards keep the stored scalars: a bytea's byte-count label is
1506
+ // not a key, and a numeric's text is exactly its value.
1507
+ const formatted = (guard, binary) => (row) => guard ? { ...row } : binary ? withBytes(row) : toJsonSafe(row);
1508
+ // Row locks this transaction holds: a lock is held to its end, so a row taken once is not taken again.
1509
+ const held = new Map();
1510
+ /* Whether this transaction holds a row FOR UPDATE: taken in its turn, or sent ahead behind the BEGIN. Such a
1511
+ row cannot move until the transaction ends, so a read of it by key may be kept to answer the reads after
1512
+ it (keyRows.ts, `widen`) until the transaction changes something of its own. */
1513
+ const holds = (table, key) => {
1514
+ const pk = table.columns.filter((column) => column.primaryKey).map((column) => column.name);
1515
+ const at = rowLockKey(table, { groups: [pk.map((column) => ({ column, op: '=', value: key[column] }))] });
1516
+ return held.get(at) === 'UPDATE' || lockedAhead.has(at);
1517
+ };
1518
+ const executor = (guard) => async (sql, hint) => {
1519
+ const started = performance.now();
1520
+ const result = await write.query(sql.text, sql.params);
1521
+ return {
1522
+ columns: result.fields.map((field) => field.name),
1523
+ rows: result.rows.map(formatted(guard, hint?.binary)),
1524
+ ms: Math.round(performance.now() - started),
1525
+ sql,
1526
+ };
1527
+ };
1528
+ /* Each scope this transaction reads under, checked against its catalog once. A command's scope arrives as
1529
+ a copy (captureRowScope), which no check has seen, and every scoped reader and statement checked it
1530
+ again in full: eleven times in one prepare. Checked here, each finds it checked (rowScope.ts). The rows
1531
+ kept by key are kept under the scope as given, the one the statements sent ahead were keyed by
1532
+ (keyRows.ts names a scope by its object), so that is the one they are asked under. */
1533
+ const checkedScopes = new WeakMap();
1534
+ const checkedScope = (given) => {
1535
+ let checked = checkedScopes.get(given);
1536
+ if (!checked)
1537
+ checkedScopes.set(given, (checked = snapshotRowScope(catalog.schema, given)));
1538
+ return checked;
1539
+ };
1540
+ const scopedReads = (guard, given = scope, within = checkedScope(given)) => keyRows.over(createScopedReads(catalog.schema, within, POSTGRES_STYLE, executor(guard)), given, catalog.schema, (request) => scopedStatement(catalog.schema, within, request), (row, request) => formatted(guard, request.binary)(row), guard, guard ? undefined : { held: holds, reads: createScopedReads(catalog.schema, within, POSTGRES_STYLE, executor(true)) });
1541
+ const lock = (mode) => async (table, filter) => {
1542
+ // A parent the role cannot update cannot be held; its FK's own KEY
1543
+ // SHARE still stops deletion, and the visibility check is one read.
1544
+ if (!catalog.lockable.has(table.id)) {
1545
+ if (mode === 'UPDATE')
1546
+ throw new Refusal('This operation is not available in the current row scope.');
1547
+ return;
1548
+ }
1549
+ const at = rowLockKey(table, filter);
1550
+ const had = held.get(at);
1551
+ if (had === 'UPDATE' || had === mode)
1552
+ return;
1553
+ const where = buildWhere(filter, POSTGRES_STYLE);
1554
+ const text = `SELECT 1 FROM ${qualified(table)}${where.text} FOR ${mode}`;
1555
+ /* Sent ahead behind the BEGIN and granted, its answer since dropped by a write of the transaction's own:
1556
+ the row is held all the same, to the transaction's end. While that answer still waits, asking takes it. */
1557
+ if (lockedAhead.has(at) && !waitsAhead(write, { text, values: where.params })) {
1558
+ held.set(at, 'UPDATE');
1559
+ return;
1560
+ }
1561
+ await write.query(text, where.params);
1562
+ held.set(at, mode);
1563
+ };
1564
+ const writes = this.historyWrites(write, {
1565
+ insert: (r) => this.insertNow(r, write, write, true),
1566
+ remove: (r) => this.removeNow(r, write, write),
1567
+ update: (r) => this.updateNow(r, write, write),
1568
+ });
1569
+ let records = createScopedTransaction(catalog, {
1570
+ query: (r) => this.queryOn(r, write),
1571
+ count: (table, filter) => this.countOn(table, filter, write),
1572
+ insert: writes.insert,
1573
+ remove: writes.remove,
1574
+ update: writes.update,
1575
+ }, scopedReads(false), scopedReads(true), {
1576
+ target: lock('UPDATE'),
1577
+ parent: lock('SHARE'),
1578
+ behind: (table, ride) => {
1579
+ const update = `UPDATE ${qualified(table)} SET `;
1580
+ return behindWrite(write, (text) => text.startsWith(update), ride);
1581
+ },
1582
+ together: sendsAhead(write),
1583
+ }, bound && { reads: scopedReads(true, bound), whole: wholeWithin(scope, bound) },
1584
+ // A public intake writes exactly its fixed rows; only a principal's own transaction counts its new parents in.
1585
+ journal?.fixedRecords ? undefined : { scope, reads: (within, guard) => scopedReads(guard, within) });
1586
+ if (journal?.fixedRecords)
1587
+ records = journal.fixedRecords(records);
1588
+ const invoke = scopedCommandInvoker(operations, true, journal !== undefined, isRuntimeRelation);
1589
+ // The transaction's own work runs without the hint: a transaction it opens is not this one.
1590
+ const out = await withoutWriteAhead(() => run(Object.freeze({
1591
+ // The scoped policy facade deliberately has no generic lock capability.
1592
+ query: (r) => invoke(r, (r) => records.query(r), true),
1593
+ count: (table, filter) => invoke({ table, filter }, (r) => records.count(r.table, r.filter), true),
1594
+ insert: (r) => invoke(r, (r) => records.insert(r)),
1595
+ remove: (r) => invoke(r, (r) => records.remove(r)),
1596
+ update: (r) => invoke(r, (r) => records.update(r)),
1597
+ // Beneath the row scope: the flows' command tells only of its own target, which it read through the scope.
1598
+ ...(writes.tell ? { tell: (r) => invoke(r, (r) => writes.tell(r)) } : {}),
1599
+ }), journal?.open(write, operations), evidence));
1600
+ await operations.finish();
1601
+ await writes.finish();
1602
+ if (verify)
1603
+ await verify(write, catalog, described);
1604
+ commitAttempted = true;
1605
+ await write.query('COMMIT');
1606
+ committed = true;
1607
+ writes.committed();
1608
+ return out;
1609
+ }
1610
+ catch (err) {
1611
+ await operations.finish().catch(() => { });
1612
+ let rolledBack = false;
1613
+ if (began) {
1614
+ try {
1615
+ await write.query('ROLLBACK');
1616
+ rolledBack = true;
1617
+ }
1618
+ catch {
1619
+ /* A failed acknowledgement cannot establish the outcome. */
1620
+ }
1621
+ }
1622
+ /* A write sent with the COMMIT failed, and the server answered the
1623
+ COMMIT as a ROLLBACK: nothing committed, and that write's failure is
1624
+ what is said. */
1625
+ const refused = err instanceof RolledBackAtCommit;
1626
+ const failure = refused ? err.cause : err;
1627
+ // withWriteClient destroys the client on any failure, so a connection
1628
+ // in an uncertain state is never handed to the next write.
1629
+ const unknown = !refused && (commitAttempted || (began && !rolledBack));
1630
+ // The verified owner composes native outcomes itself, as on SQLite.
1631
+ if (journal && !verify)
1632
+ throw journal.failure(failure, unknown ? 'unknown' : 'not_applied');
1633
+ if (unknown)
1634
+ throw new TransactionOutcomeUnknown(failure);
1635
+ throw failure;
1636
+ }
1637
+ finally {
1638
+ reportWriteAhead(aheadReport(write), committed);
1639
+ }
1640
+ }));
1641
+ }
1642
+ /**
1643
+ * The statements a scoped transaction opens with, sent behind its BEGIN
1644
+ * (openWrite's `sent`), from what it was told (writeAhead.ts) and the scope
1645
+ * catalog read last time: the verifier's own (its fence), the journal's
1646
+ * receipt lookup, and for each target its row lock, its row read in full
1647
+ * through the scope (kept to answer its reads by key, keyRows.ts), and,
1648
+ * where the write is captured for history, the capture's own lock and read
1649
+ * (historyWrites). Each is exactly the statement the transaction sends in
1650
+ * its turn, built by the same rules; one it never sends is left, counted,
1651
+ * and changes nothing. None where the client does not pipeline.
1652
+ */
1653
+ sendWriteAhead(write, scope, peeked, keyRows, journal, verify, hint, locked) {
1654
+ if (!sendsAhead(write) || (!hint && !verify?.ahead))
1655
+ return [];
1656
+ const opening = [];
1657
+ try {
1658
+ opening.push(...(verify?.ahead?.() ?? []));
1659
+ if (hint?.receipt !== undefined && journal?.ahead)
1660
+ opening.push(...journal.ahead(hint.receipt));
1661
+ }
1662
+ catch {
1663
+ /* Asked in its turn. */
1664
+ }
1665
+ const answers = sendAhead(write, opening);
1666
+ for (const target of (peeked && hint?.targets) || []) {
1667
+ const table = peeked.schema.tables.find((candidate) => candidate.id === catalogId(target.table));
1668
+ if (!table || !peeked.lockable.has(table.id))
1669
+ continue;
1670
+ const pk = table.columns.filter((column) => column.primaryKey).map((column) => column.name);
1671
+ const key = target.key;
1672
+ if (!pk.length ||
1673
+ Object.keys(key).length !== pk.length ||
1674
+ !pk.every((column) => Object.hasOwn(key, column) && (typeof key[column] === 'string' || typeof key[column] === 'number')))
1675
+ continue;
1676
+ // The scoped transaction's own key filter and lock (scopedTransaction.ts keyFilter, `lock` above).
1677
+ const filter = { groups: [pk.map((column) => ({ column, op: '=', value: key[column] }))] };
1678
+ const where = buildWhere(filter, POSTGRES_STYLE);
1679
+ if (!hint.readOnly) {
1680
+ answers.push(...sendAhead(write, [{ text: `SELECT 1 FROM ${qualified(table)}${where.text} FOR UPDATE`, values: where.params }]));
1681
+ locked.add(rowLockKey(table, filter));
1682
+ }
1683
+ const read = scopedStatement(peeked.schema, scope, { table: table.id, filter, limit: 2, offset: 0 });
1684
+ const row = read && holdAhead(write, { text: read.text, values: read.params });
1685
+ if (row) {
1686
+ answers.push(row.answer);
1687
+ keyRows.keep(scope, table, filter, row.answer.then((result) => result.rows), row.take);
1688
+ }
1689
+ if (hint.writes && !hint.readOnly && capturesWrites(this, table.id)) {
1690
+ // historyWrites' read: lockRecordNow, then queryOn of the whole row by the key as given.
1691
+ const given = buildWhere({ groups: [Object.entries(key).map(([column, value]) => ({ column, op: '=', value }))] }, POSTGRES_STYLE);
1692
+ const n = given.params.length;
1693
+ answers.push(...sendAhead(write, [
1694
+ {
1695
+ text: `SELECT ${pk.map(POSTGRES_STYLE.quote).join(', ')} FROM ${qualified(table)}${where.text} FOR UPDATE`,
1696
+ values: where.params,
1697
+ },
1698
+ { text: `SELECT * FROM ${qualified(table)}${given.text} LIMIT $${n + 1} OFFSET $${n + 2}`, values: [...given.params, 2, 0] },
1699
+ ]));
1700
+ }
1701
+ }
1702
+ return answers;
1703
+ }
1704
+ /**
1705
+ * Internal runtime-owner composition, deliberately absent from Adapter: the
1706
+ * PostgreSQL counterpart of `SqliteAdapter.runtimeMaintenanceTransaction`,
1707
+ * for trusted runtime code (tenant administration) that composes its own
1708
+ * bounded maintenance facades on the leased writer and the ordinary write
1709
+ * queue. `verify` runs after BEGIN and again after the work's operations
1710
+ * drain, before COMMIT; the synchronous `assertCurrent` handed to the work
1711
+ * refuses once the transaction has ended. Raw handles must never reach
1712
+ * business handlers, policies or request callbacks. Outcomes are the
1713
+ * transaction contract's: unknown after COMMIT was sent or an unacknowledged
1714
+ * ROLLBACK, and the client is discarded rather than reused.
1715
+ */
1716
+ async runtimeMaintenanceTransaction(verify, run) {
1717
+ if (typeof verify !== 'function' || typeof run !== 'function')
1718
+ throw new Refusal('Runtime maintenance is unavailable.');
1719
+ // No lane: `verify` fences what the work depends on, twice, on the writer (tenantAdministration.ts).
1720
+ return this.writes.run([], () => this.connections.withWriteClient(async (leased) => {
1721
+ const write = transactionHandle(leased, this), operations = new TransactionOperations();
1722
+ let began = false, commitAttempted = false, open = true;
1723
+ const assertCurrent = () => {
1724
+ if (!open)
1725
+ throw new Refusal('The maintenance transaction is no longer active.');
1726
+ };
1727
+ try {
1728
+ await write.query('BEGIN');
1729
+ began = true;
1730
+ await verify(write);
1731
+ const out = await run(write, operations, assertCurrent);
1732
+ await operations.finish();
1733
+ await verify(write);
1734
+ commitAttempted = true;
1735
+ await write.query('COMMIT');
1736
+ return out;
1737
+ }
1738
+ catch (err) {
1739
+ await operations.finish().catch(() => { });
1740
+ let rolledBack = false;
1741
+ if (began) {
1742
+ try {
1743
+ await write.query('ROLLBACK');
1744
+ rolledBack = true;
1745
+ }
1746
+ catch {
1747
+ /* A failed acknowledgement cannot establish the outcome. */
1748
+ }
1749
+ }
1750
+ if (commitAttempted || (began && !rolledBack))
1751
+ throw new TransactionOutcomeUnknown(err);
1752
+ throw err;
1753
+ }
1754
+ finally {
1755
+ open = false;
1756
+ }
1757
+ }));
1758
+ }
1759
+ async close() {
1760
+ await this.writes.run('all', () => this.connections.close());
1761
+ }
1762
+ async lockRecordNow(request, write) {
1763
+ const found = await this.assertTable(request.table, write);
1764
+ if (found.isView || !['r', 'p'].includes(found.kind))
1765
+ throw new Refusal('Record locks require a local keyed table.');
1766
+ const meta = await this.columnsOf(found.schema, found.name, write);
1767
+ const pk = meta.filter((column) => column.is_pk).map((column) => column.name);
1768
+ const where = buildWhere(recordLockFilter(request, pk), POSTGRES_STYLE);
1769
+ const text = `SELECT ${pk.map(POSTGRES_STYLE.quote).join(', ')} FROM ${qualified(found)}${where.text} FOR UPDATE`;
1770
+ // Locked already, behind this transaction's BEGIN (openPlain): it holds the lock until it ends.
1771
+ const held = heldLocks.get(write)?.get(lockKey(text, where.params));
1772
+ if (held)
1773
+ return held();
1774
+ const result = await write.query(text, where.params);
1775
+ if (result.rows.length > 1)
1776
+ throw new Refusal('That primary key does not identify a single record.');
1777
+ }
1778
+ /**
1779
+ * Identifiers cannot be bound, so a table name is checked against the
1780
+ * catalog before it is quoted into a statement. The check is a parameterised
1781
+ * query, which means the untrusted string never reaches SQL as syntax.
1782
+ */
1783
+ /**
1784
+ * The same check for a read, answered from the catalog read on connect when
1785
+ * it holds the name. That probe was a round trip before every list and
1786
+ * every count — two per page of rows, one per reference counted — to learn
1787
+ * what the catalog already said. A name the catalog does not hold (made
1788
+ * since) is still asked of the server; writes always are (assertTable),
1789
+ * because a view created since connecting must still refuse one.
1790
+ */
1791
+ async readTable(id) {
1792
+ const dot = id.indexOf('.');
1793
+ const schema = dot === -1 ? 'public' : id.slice(0, dot);
1794
+ if (RESERVED_SCHEMAS.includes(schema))
1795
+ throw new Refusal('Reserved resources are runtime-only.');
1796
+ const known = this.cachedSchema?.tables.find((table) => table.id === id);
1797
+ if (known?.schema && !RESERVED_SCHEMAS.includes(known.schema)) {
1798
+ return { schema: known.schema, name: known.name, kind: known.isView ? 'v' : 'r', isView: known.isView };
1799
+ }
1800
+ return this.assertTable(id);
1801
+ }
1802
+ epochOf = (db) => (db ? this.txEpochs.get(db) : undefined) ?? (this.epochStore === false ? epochlessEpoch() : undefined);
1803
+ async assertTable(id, db) {
1804
+ return assertPostgresTable(id, db, this.epochOf(db), this.catalogCache, () => this.conn());
1805
+ }
1806
+ }
1807
+ /** A verifier's pin, when it is only LOCK statements with no parameters (it rides in the simple-protocol message).
1808
+ Anything else is not taken: the transaction then runs as it would without one, and its verifier attests in full. */
1809
+ function verifierPin(verify) {
1810
+ const pin = verify?.pin;
1811
+ return typeof pin === 'string' && /^LOCK TABLE [^;$]+ IN [A-Z ]+ MODE(; LOCK TABLE [^;$]+ IN [A-Z ]+ MODE)*$/.test(pin) ? pin : undefined;
1812
+ }
1813
+ /* The row scope an unscoped transaction's kept rows are filed under (keyRows.ts): none, the same object every time. */
1814
+ const PLAIN_SCOPE = Object.freeze({});
1815
+ /* The record locks a transaction took behind its BEGIN (openPlain), by statement, each with what marks it taken.
1816
+ Only those: a lock taken later could be let go by a ROLLBACK TO SAVEPOINT, one taken at the opening cannot. */
1817
+ const heldLocks = new WeakMap();
1818
+ /* Of those, the ones that found their row: it is the transaction's, unmoving, until it ends (openPlain's `widen`). */
1819
+ const heldRows = new WeakMap();
1820
+ const lockKey = (text, values) => JSON.stringify([text, values]);
1821
+ /* lockRecordNow's statement for a row named by its whole primary key, and its values. */
1822
+ function rowLockStatement(table, key) {
1823
+ const pk = table.columns.filter((column) => column.primaryKey).map((column) => column.name);
1824
+ const where = buildWhere({ groups: [pk.map((column) => ({ column, op: '=', value: key[column] }))] }, POSTGRES_STYLE);
1825
+ return [`SELECT ${pk.map(POSTGRES_STYLE.quote).join(', ')} FROM ${qualified(table)}${where.text} FOR UPDATE`, where.params];
1826
+ }
1827
+ /* A table's schema and name from the name a request gives it, as assertPostgresTable reads it. */
1828
+ function plainTable(table) {
1829
+ const dot = table.indexOf('.');
1830
+ return dot === -1 ? { schema: 'public', name: table } : { schema: table.slice(0, dot), name: table.slice(dot + 1) };
1831
+ }
1832
+ /* A table's catalog id from the name a request gives it: a bare name is in the public schema, as assertPostgresTable reads it. */
1833
+ const catalogId = (table) => (table.includes('.') ? table : `public.${table}`);
1834
+ /* The statement queryOn sends for an unordered read, without sending it: the table as its id names it
1835
+ (assertPostgresTable), the read bounded as every read is. */
1836
+ function plainStatement(request) {
1837
+ if (request.orderBy?.length)
1838
+ return undefined;
1839
+ const at = plainTable(request.table);
1840
+ const where = buildWhere(request.filter, POSTGRES_STYLE);
1841
+ const cols = request.columns?.length ? request.columns.map((column) => POSTGRES_STYLE.quote(column)).join(', ') : '*';
1842
+ const n = where.params.length;
1843
+ return {
1844
+ text: `SELECT ${cols} FROM ${qualified(at)}${where.text} LIMIT $${n + 1} OFFSET $${n + 2}`,
1845
+ params: [...where.params, clampLimit(request.limit), clampOffset(request.offset)],
1846
+ };
1847
+ }
1848
+ /* A statement a scoped read would send, without sending it: createScopedReads
1849
+ compiles before it executes, so the executor is reached synchronously and
1850
+ says what it was handed. */
1851
+ /** A row lock as a scoped transaction keeps it: its table and the filter that names the row. */
1852
+ const rowLockKey = (table, filter) => `${table.id}\u0000${JSON.stringify(filter)}`;
1853
+ function scopedStatement(schema, within, request) {
1854
+ let said;
1855
+ void createScopedReads(schema, within, POSTGRES_STYLE, async (sql) => {
1856
+ said = sql;
1857
+ return { columns: [], rows: [], ms: 0, sql };
1858
+ })
1859
+ .query(request)
1860
+ .catch(() => { });
1861
+ return said;
1862
+ }
1863
+ /* Statements sent ahead that a committed transaction never asked: a hint
1864
+ that no longer says what the transaction does. Warned once per statement
1865
+ outside production; a refused transaction stops early, and leaves some by
1866
+ design. With MISTRI_AHEAD_REPORT set, every transaction's account is a line
1867
+ on stderr (e2e/write-storm.spec.ts reads it). */
1868
+ const warnedAhead = new Set();
1869
+ function reportWriteAhead(report, committed) {
1870
+ if (!report || (!report.sent && !report.missed))
1871
+ return;
1872
+ const said = (key) => {
1873
+ try {
1874
+ return String(JSON.parse(key)[0]).slice(0, 160);
1875
+ }
1876
+ catch {
1877
+ return key.slice(0, 160);
1878
+ }
1879
+ };
1880
+ if (process.env.MISTRI_AHEAD_REPORT)
1881
+ process.stderr.write(`mistri-ahead ${JSON.stringify({ at: Date.now(), committed, sent: report.sent, taken: report.taken, answered: report.answered, missed: report.missed, left: report.left.map(said), redone: report.redone, asked: report.asked })}\n`);
1882
+ if (!committed || process.env.NODE_ENV === 'production')
1883
+ return;
1884
+ for (const statement of report.left.map(said)) {
1885
+ if (warnedAhead.has(statement))
1886
+ continue;
1887
+ warnedAhead.add(statement);
1888
+ process.emitWarning(`A statement sent ahead was never asked by the transaction it was sent for: ${statement}`, 'MistriWriteAhead');
1889
+ }
1890
+ }