@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,987 @@
1
+ /**
2
+ * Multi-table views, built out of **reference paths**.
3
+ *
4
+ * The `Filter` in filter.ts addresses exactly one table and must keep doing
5
+ * so: it is the shape every read goes through, and widening it to carry joins
6
+ * would put join-building into every code path that filters anything. So the
7
+ * multi-table story is a separate idea rather than a bigger filter, and the
8
+ * idea is dot-walking:
9
+ *
10
+ * invoice customer_id.name contains Harbour
11
+ * work_order assigned_to.last_name = Petrova
12
+ * and customer_id.country_code.name = Australia
13
+ *
14
+ * A path is a chain of foreign keys starting at the base table, ending at a
15
+ * column. `customer_id.name` means "follow invoice.customer_id to customer,
16
+ * then take name". Nothing about it is guessed: the foreign key graph is
17
+ * already introspected, so every hop is derivable, checkable, and reversible
18
+ * into SQL — a path compiles to a LEFT JOIN chain and a qualified column.
19
+ *
20
+ * One mechanism answers three separate questions. Filtering across a long
21
+ * walk, showing a field from a related table, and the composer UI are all the
22
+ * same thing seen from three angles; the composer is a visual editor for the
23
+ * path set.
24
+ *
25
+ * Two properties are load-bearing and are pinned by tests:
26
+ *
27
+ * - **LEFT JOIN, never INNER.** A null foreign key must leave the base row
28
+ * in the result with nulls beside it. A composer that quietly drops rows
29
+ * is worse than no composer at all, because the number it shows you is
30
+ * wrong in a direction you cannot see.
31
+ * - **Every value is a bound parameter.** Only identifiers are ever quoted
32
+ * into the text, and they are quoted by the dialect's `SqlStyle`. The
33
+ * WHERE clause is still built by `buildWhere`, which now takes an
34
+ * identifier resolver so a path can become `"customer_1"."name"` without
35
+ * a second predicate builder existing to disagree with the first.
36
+ */
37
+ import { ANSI_STYLE, buildWhere, collate, orderClauseSql, } from './filter.js';
38
+ import { clampLimit, clampOffset, countStatement } from './reads.js';
39
+ import { aggregateInput, declaredOrder, findTable, } from './schema.js';
40
+ import { ANY_CONDITION, anyConditions, expandSegments, parseQuery, splitClauseTail } from './query.js';
41
+ import { stitchedListProblem } from './stitchedPaths.js';
42
+ import { qualifiedTable } from './sqlRelation.js';
43
+ import { ScopedRelations } from './rowScope.js';
44
+ import { slug } from './slug.js';
45
+ import { maskQuoted } from './maskQuoted.js';
46
+ import { PathError, AGGREGATE_FNS, resolveAggregate, tryResolvePath, labelColumnMeta, backrefsTo, } from './viewPaths.js';
47
+ import { viewPaths, withViewComputed, withViewLabels } from './viewLabels.js';
48
+ import { rollupColumnMeta } from './rollups.js';
49
+ import { TAGS_COLUMN, tagsColumnMeta } from './tagLists.js';
50
+ import { computedColumnMeta } from './computed.js';
51
+ import { withViewReferences } from './declaredReferences.js';
52
+ export { withViewComputed, withViewLabels } from './viewLabels.js';
53
+ export { MAX_PATH_DEPTH, PathError, hopName, hopsFrom, resolvePath, backrefsTo, resolveAggregate, tableAtPrefix, tryResolvePath, } from './viewPaths.js';
54
+ /* ---------- compilation ---------- */
55
+ /* A view's filter as a column filter, for `buildWhere`. A condition's
56
+ collation travels with it: dropped here, a keyset comparison would fall
57
+ back to the column's collation while its ORDER BY kept the binary one. */
58
+ function viewConditions(view) {
59
+ return view.filter?.groups.length
60
+ ? {
61
+ groups: view.filter.groups.map((group) => group.map((c) => ({
62
+ column: c.path,
63
+ op: c.op,
64
+ value: c.value,
65
+ ...(c.collation === undefined ? {} : { collation: c.collation }),
66
+ ...(c.other === undefined ? {} : { other: c.other }),
67
+ ...(c.any === undefined ? {} : { any: c.any }),
68
+ }))),
69
+ }
70
+ : undefined;
71
+ }
72
+ /**
73
+ * Alias generation.
74
+ *
75
+ * Two requirements pull in opposite directions. Aliases must never collide —
76
+ * `work_order` in the demo has three separate references to `employee`, so a
77
+ * scheme keyed on the target table alone produces three joins all called
78
+ * `employee` and a statement that does not parse. And they must be stable, so
79
+ * the same view compiles to the same SQL every time and a diff of the explain
80
+ * panel means something.
81
+ *
82
+ * Both fall out of keying on the *path prefix* rather than the table: a prefix
83
+ * identifies a join uniquely, so the same path used twice reuses one join and
84
+ * two paths through the same table get two. The readable table name is kept as
85
+ * a prefix and made unique by an ordinal assigned in first-use order.
86
+ */
87
+ export class JoinPlan {
88
+ schema;
89
+ style;
90
+ relation;
91
+ joins = [];
92
+ byPrefix = new Map();
93
+ n = 0;
94
+ constructor(schema, base, style, relation = (id) => qualifiedTable(schema, id, style)) {
95
+ this.schema = schema;
96
+ this.style = style;
97
+ this.relation = relation;
98
+ this.byPrefix.set('', alias(base.name, 0));
99
+ }
100
+ get baseAlias() {
101
+ return this.byPrefix.get('');
102
+ }
103
+ /** The alias for a path prefix, creating the join chain if it is new. */
104
+ aliasFor(hops, hopNames) {
105
+ let prefix = '';
106
+ for (let i = 0; i < hops.length; i++) {
107
+ prefix = prefix ? `${prefix}.${hopNames[i]}` : hopNames[i];
108
+ /* Already planned: the same path used twice must reuse one join, not
109
+ add a second copy of the same table under a different alias. */
110
+ if (this.byPrefix.has(prefix))
111
+ continue;
112
+ const target = findTable(this.schema, hops[i].to.table);
113
+ this.byPrefix.set(prefix, alias(target.name, ++this.n));
114
+ this.joins.push({ alias: this.byPrefix.get(prefix), table: target.id, path: prefix });
115
+ }
116
+ return this.byPrefix.get(prefix) ?? this.baseAlias;
117
+ }
118
+ /** The fully qualified, quoted SQL for a resolved path's final column. */
119
+ qualify(resolved) {
120
+ const owner = this.aliasFor(resolved.hops, resolved.hopNames);
121
+ return `${this.style.quote(owner)}.${this.style.quote(resolved.column.name)}`;
122
+ }
123
+ /** The relation a table is read as in this statement: its scoped CTE, or the table itself. */
124
+ relationOf(id) {
125
+ return this.relation(id);
126
+ }
127
+ /** The alias of a path prefix that has already been planned, or the base. */
128
+ aliasOfPrefix(prefix) {
129
+ return this.byPrefix.get(prefix) ?? this.baseAlias;
130
+ }
131
+ /**
132
+ * A fresh alias for something that is not a join — a subquery's table.
133
+ *
134
+ * It draws from the same counter as the joins, which is what guarantees a
135
+ * correlated subquery over `employee` cannot pick the alias a LEFT JOIN
136
+ * over `employee` is already using. Two `employee_3`s in one statement is
137
+ * a self-join nobody asked for, and the wrong answer rather than an error.
138
+ */
139
+ subAlias(name) {
140
+ return alias(name, ++this.n);
141
+ }
142
+ /**
143
+ * The JOIN clauses, in the order the aliases were created.
144
+ *
145
+ * Order matters for more than tidiness: a join may only reference an alias
146
+ * introduced before it, and creation order is exactly path order, so a
147
+ * nested hop always follows the hop it hangs off.
148
+ */
149
+ clauses() {
150
+ return this.joins.map((join) => {
151
+ const parts = join.path.split('.');
152
+ const parentPrefix = parts.slice(0, -1).join('.');
153
+ const parent = this.byPrefix.get(parentPrefix);
154
+ const fk = this.fkFor(join.path);
155
+ const target = findTable(this.schema, join.table);
156
+ /* Every column of the key, not just the first. A composite foreign key
157
+ joined on one column is a cross product wearing a join's clothes. */
158
+ const on = fk.to.columns
159
+ .map((toCol, i) => `${this.style.quote(join.alias)}.${this.style.quote(toCol)} = ` +
160
+ `${this.style.quote(parent)}.${this.style.quote(fk.from.columns[i] ?? fk.from.columns[0])}`)
161
+ .join(' AND ');
162
+ /* LEFT, always. An INNER JOIN here would drop every base row whose
163
+ foreign key is null — silently, and in a direction that looks like
164
+ the data simply is not there. */
165
+ return ` LEFT JOIN ${this.relation(target.id)} ${this.style.quote(join.alias)} ON ${on}`;
166
+ });
167
+ }
168
+ fkChain = new Map();
169
+ /** Remember which key produced each join, so `clauses` can rebuild the ON. */
170
+ record(path, fk) {
171
+ if (!this.fkChain.has(path))
172
+ this.fkChain.set(path, fk);
173
+ }
174
+ fkFor(path) {
175
+ return this.fkChain.get(path);
176
+ }
177
+ }
178
+ /** Alias text: the table's own name, made unique by an ordinal. */
179
+ function alias(name, n) {
180
+ /* Truncated because Postgres silently cuts identifiers at 63 bytes, and a
181
+ silent cut is exactly how two long table names become one alias. */
182
+ const safe = name.replace(/[^A-Za-z0-9_]/g, '_').slice(0, 40) || 't';
183
+ return `${safe}_${n}`;
184
+ }
185
+ /**
186
+ * Compile a view to one parameterised SELECT.
187
+ *
188
+ * Order of work is deliberate and is what makes the output deterministic:
189
+ * columns, then the filter, then the sort. Aliases are handed out on first
190
+ * use, so the same view always produces the same statement.
191
+ */
192
+ export function compileView(schema, view, style, offset = 0, scope) {
193
+ // The App's soft keys, where this catalog is the database's own (declaredReferences.ts).
194
+ schema = withViewReferences(schema, view.references);
195
+ const base = findTable(schema, view.base);
196
+ if (!base) {
197
+ throw new PathError(`No table called "${view.base}" in this database, so the view has nothing to start from.`, view.base, view.base, 0);
198
+ }
199
+ const relations = new ScopedRelations(schema, style, scope);
200
+ const baseRelation = relations.table(base.id);
201
+ const plan = new JoinPlan(schema, base, style, relations.table);
202
+ const { resolve, qualify, membership, exists } = viewPaths(schema, view, plan, style, relations);
203
+ /* An empty column list means the base table's own columns. Expanding it
204
+ here rather than emitting `SELECT *` keeps the result's column list
205
+ knowable without running the statement, which is what the composer's
206
+ field checklist reads. */
207
+ const wanted = view.columns?.length ? view.columns : base.columns.map((c) => ({ path: c.name }));
208
+ const seen = new Map();
209
+ const columns = [];
210
+ const select = [];
211
+ for (const col of wanted) {
212
+ const resolved = resolve(col.path);
213
+ const sql = qualify(col.path);
214
+ /* Two paths can legitimately end on the same name — `customer_id.name`
215
+ and `sold_by.name` are both "name" if aliased carelessly. The path is
216
+ already unique, so it is the default; an explicit collision gets a
217
+ numeric suffix rather than one column quietly overwriting the other. */
218
+ let name = col.alias?.trim() || resolved.path;
219
+ const count = seen.get(name) ?? 0;
220
+ seen.set(name, count + 1);
221
+ if (count)
222
+ name = `${name}_${count + 1}`;
223
+ columns.push({
224
+ path: resolved.path,
225
+ name,
226
+ table: resolved.table.id,
227
+ column: resolved.column.name,
228
+ type: resolved.column.type,
229
+ /* A column on a joined table is nullable in the result even when the
230
+ column itself is NOT NULL: the LEFT JOIN can produce no row at all.
231
+ Saying otherwise would have the UI render a real null as an error. */
232
+ nullable: resolved.column.nullable || resolved.hops.length > 0,
233
+ primaryKey: resolved.column.primaryKey,
234
+ hops: resolved.hops.length,
235
+ // A rollup reads like a column and is a summary: nothing edits it.
236
+ ...(resolved.rollup && !resolved.rollup.through ? { aggregate: resolved.rollup.fn } : {}),
237
+ });
238
+ select.push(`${sql} AS ${style.quote(name)}`);
239
+ }
240
+ /* Aggregates come after the plain columns, and after them on purpose: a
241
+ summary hanging off a joined table needs that join's alias to exist, and
242
+ resolving columns first is what puts it there. */
243
+ for (const aggregate of view.aggregates ?? []) {
244
+ const resolved = resolveAggregate(schema, view.base, aggregate);
245
+ /* The parent side is a path prefix, so an aggregate can hang off a joined
246
+ table as easily as off the base — "invoice lines on the invoice this
247
+ shipment belongs to" is the same construction one hop along. */
248
+ if (resolved.on) {
249
+ /* Qualifying any column on the parent is what plans the joins leading
250
+ to it and records their keys. Doing it through the same `qualify`
251
+ the columns use is deliberate: two ways of planning a join is two
252
+ ways for the aliases to disagree. */
253
+ qualify(`${resolved.on}.${resolved.parent.columns[0].name}`);
254
+ }
255
+ const parentAlias = resolved.on ? plan.aliasOfPrefix(resolved.on) : plan.baseAlias;
256
+ const childAlias = plan.subAlias(resolved.child.name);
257
+ const on = resolved.fk.to.columns
258
+ .map((toCol, i) => `${style.quote(childAlias)}.${style.quote(resolved.fk.from.columns[i] ?? resolved.fk.from.columns[0])} = ` +
259
+ `${style.quote(parentAlias)}.${style.quote(toCol)}`)
260
+ .join(' AND ');
261
+ /* COUNT(*) rather than COUNT(column): a column count skips nulls, which
262
+ turns "how many parts" into "how many parts have a quantity" without
263
+ saying so. SUM over no rows is left as NULL rather than coerced to 0 —
264
+ a work order with no parts has no total, and 0 is a claim about money
265
+ that nobody made. */
266
+ const expression = resolved.aggregate.fn === 'count'
267
+ ? 'COUNT(*)'
268
+ : `${resolved.aggregate.fn.toUpperCase()}(${aggregateInput(`${style.quote(childAlias)}.${style.quote(resolved.column.name)}`, resolved.column.type, style.dialect)})`;
269
+ let name = resolved.name;
270
+ const count = seen.get(name) ?? 0;
271
+ seen.set(name, count + 1);
272
+ if (count)
273
+ name = `${name}_${count + 1}`;
274
+ select.push(`(SELECT ${expression} FROM ${relations.table(resolved.child.id)} ${style.quote(childAlias)}` +
275
+ ` WHERE ${on}) AS ${style.quote(name)}`);
276
+ columns.push({
277
+ path: resolved.name,
278
+ name,
279
+ table: resolved.child.id,
280
+ column: resolved.column?.name ?? '*',
281
+ type: resolved.aggregate.fn === 'count' ? 'integer' : (resolved.column?.type ?? 'integer'),
282
+ // COUNT is never null; every other summary is, over an empty child set.
283
+ nullable: resolved.aggregate.fn !== 'count',
284
+ primaryKey: false,
285
+ hops: resolved.on ? resolved.on.split('.').length : 0,
286
+ aggregate: resolved.aggregate.fn,
287
+ });
288
+ }
289
+ /* The filter is compiled by `buildWhere`, not beside it. One predicate
290
+ builder means one place where a value could stop being a parameter. */
291
+ const conditions = viewConditions(view);
292
+ // Preserve first-use alias ordering, but discover filter and sort joins
293
+ // before freezing the scope prefix and numbering ordinary parameters.
294
+ for (const group of conditions?.groups ?? []) {
295
+ for (const condition of group) {
296
+ if (condition.any) {
297
+ exists(condition.any);
298
+ continue;
299
+ }
300
+ qualify(condition.column);
301
+ if (condition.other !== undefined)
302
+ qualify(condition.other);
303
+ }
304
+ }
305
+ const aliasNames = new Set((view.aggregates ?? []).map((a) => a.alias?.trim()).filter(Boolean));
306
+ const order = (view.orderBy ?? []).flatMap((o) => {
307
+ if (o.nulls === undefined)
308
+ return [orderTerm(o)];
309
+ if (o.nulls !== 'last' || o.alias || o.near !== undefined) {
310
+ throw new PathError(`"${o.path}" puts nulls last only when it is a column.`, o.path, o.path, 0);
311
+ }
312
+ /* NULLS LAST in both directions, said the one way every dialect reads:
313
+ a null sorts after every value whichever way the column runs. */
314
+ return [`${qualify(o.path)} IS NULL ASC`, orderTerm(o)];
315
+ });
316
+ function orderTerm(o) {
317
+ if (o.collation !== undefined && (o.alias || o.near !== undefined)) {
318
+ throw new PathError(`"${o.path}" orders by a column's own text only when it is a column.`, o.path, o.path, 0);
319
+ }
320
+ if (o.alias) {
321
+ /* Refused by name rather than left to the engine: SQLite quietly
322
+ treats an unknown ORDER BY identifier as a new error anyway, but
323
+ "no such column: invoicez" would not say the fix is the alias two
324
+ words back. */
325
+ if (!aliasNames.has(o.path)) {
326
+ throw new PathError(`"${o.path}" is not an aggregate alias on this view.`, o.path, o.path, 0);
327
+ }
328
+ return `${style.quote(o.path)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
329
+ }
330
+ if (o.near !== undefined) {
331
+ /* Closest first, as a literal rather than a parameter: the ORDER BY is
332
+ written before the WHERE's parameters are numbered, and a finite,
333
+ safe number carries nothing but itself. Parenthesized, so a negative
334
+ value never meets the minus as "--", a comment in SQL. */
335
+ if (typeof o.near !== 'number' || !Number.isFinite(o.near) || Math.abs(o.near) > Number.MAX_SAFE_INTEGER) {
336
+ throw new PathError(`"near" on "${o.path}" must be a finite number.`, o.path, o.path, 0);
337
+ }
338
+ return `ABS(${qualify(o.path)} - (${String(o.near)})) ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
339
+ }
340
+ /* A vocabulary sorts as declared (schema.ts declaredOrder); an order by
341
+ bytes asked for the bytes, for a cursor that compares them. */
342
+ if (o.collation === undefined)
343
+ return { expression: qualify(o.path), direction: o.direction, declared: declaredOrder(resolve(o.path).column) };
344
+ return `${collate(qualify(o.path), o.collation, style)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;
345
+ }
346
+ const limit = clampLimit(view.limit);
347
+ const skip = clampOffset(offset);
348
+ const joins = plan.clauses();
349
+ const prefix = relations.finish();
350
+ /* The other side of a column comparison is a path like any other: resolved
351
+ against this catalog, so a reader's pruned graph refuses one it may not
352
+ read exactly as it refuses the left. */
353
+ const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists);
354
+ /* Placeholder numbering continues across the whole statement, which is the
355
+ one thing Postgres needs and SQLite does not — and the exact place a join
356
+ builder gets it wrong, because the joins sit between the SELECT list and
357
+ the WHERE clause and are easy to count as if they carried parameters. */
358
+ const sorted = orderClauseSql(order, style, prefix.params.length + where.params.length + 1);
359
+ const n = prefix.params.length + where.params.length + sorted.params.length;
360
+ const lines = [`SELECT ${select.join(',\n ')}`, ` FROM ${baseRelation} ${style.quote(plan.baseAlias)}`, ...joins];
361
+ if (where.text)
362
+ lines.push(where.text.replace(/^ WHERE /, ' WHERE '));
363
+ if (sorted.text)
364
+ lines.push(sorted.text);
365
+ lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);
366
+ return {
367
+ text: prefix.text + lines.join('\n'),
368
+ params: [...prefix.params, ...where.params, ...sorted.params, limit, skip],
369
+ columns,
370
+ joins: plan.joins,
371
+ limit,
372
+ };
373
+ }
374
+ /* ---------- the query language, over paths ----------
375
+
376
+ The composer's filter box takes the same language as the bar at the top,
377
+ because two languages for one idea is one too many. It is parsed by the
378
+ same parser, too: `parseQuery` already handles and/or, parentheses, quoting,
379
+ relative dates and per-column value coercion, and a second implementation
380
+ of those would be a second set of bugs.
381
+
382
+ The trick that makes it work is a *synthetic* table. `parseQuery` resolves
383
+ every field against a table's column list, so the base table is handed to it
384
+ with one extra column per path mentioned in the text, each carrying the
385
+ declared type of the column the path actually ends on. Coercion, date
386
+ handling and "did you mean" then work over paths for free.
387
+
388
+ Paths are resolved on demand rather than enumerated: `work_order.parent_id`
389
+ points back at `work_order`, so the set of paths four hops deep is tens of
390
+ thousands of entries and building it per keystroke would be absurd. */
391
+ /* One segment of a path: a column name, or — for a composite key — the
392
+ columns of it joined with `+`, exactly as `hopName` writes them.
393
+
394
+ `+` has to be admitted here as well as in `parseQuery`'s FIELD, or the two
395
+ disagree about where a token begins: the scanner reads only `line_no.sku`
396
+ out of `invoice_id+line_no.sku`, builds the synthetic column for a path that
397
+ does not exist, and a composite hop becomes something you can show but never
398
+ filter. It is legal only *between* identifier characters, never leading —
399
+ a leading `+` would let a signed number start a path. */
400
+ const SEGMENT = String.raw `[A-Za-z_$][\w$]*(?:\+[A-Za-z_$][\w$]*)*`;
401
+ /* A dotted token. `@` and `+` are excluded from the lookbehind on purpose:
402
+ without `@`, `email = ops@harbour.example` reads its own value as a path and
403
+ reports a confident error about a table that was never mentioned; without
404
+ `+`, the tail of a composite hop matches on its own and reports the same
405
+ kind of confident error about half a key. */
406
+ const DOTTED_TOKEN = new RegExp(String.raw `(?<![\w.$@+])(${SEGMENT}(?:\.${SEGMENT})+)`, 'g');
407
+ /** What makes a token a *field* rather than a value: an operator after it. */
408
+ const OPERATOR_AFTER = /^\s*(?:!=|<=|>=|=|<|>|\bcontains\b|\bstartswith\b|\bendswith\b|\blike\b|\bnot\s+in\b|\bin\b|\bis\b)/i;
409
+ /** …or a word before it that can only be followed by a field. */
410
+ const FIELD_BEFORE = /(?:^|[\s(,])(?:and|or|where|filter|show|select|sort|by)\s*$|(?:^|,)\s*$/i;
411
+ /**
412
+ * Parse the composer's filter/clause text against a base table.
413
+ *
414
+ * Returns errors rather than throwing, because this runs on every keystroke
415
+ * and a half-typed path is the normal state of the input, not an exception.
416
+ */
417
+ export function parseViewQuery(schema, baseId, text, now, options = {}) {
418
+ const empty = { filter: { groups: [] }, columns: [], orderBy: [], errors: [] };
419
+ const base = findTable(schema, baseId);
420
+ if (!base) {
421
+ return { ...empty, errors: [{ message: `No table called "${baseId}" in this database.` }] };
422
+ }
423
+ const written = String(text ?? '').trim();
424
+ if (!written)
425
+ return empty;
426
+ const errors = [];
427
+ /* A segment's name is written out first (query.ts expandSegments), so the paths, rollups and computed
428
+ columns its conditions name are found below as if they had been typed; a position is said in the text
429
+ as written. */
430
+ let body = written;
431
+ let said = (at) => at;
432
+ if (base.segments) {
433
+ const { head } = splitClauseTail(written);
434
+ const start = head ? written.indexOf(head) : -1;
435
+ if (start >= 0) {
436
+ const expanded = expandSegments(head, base.segments);
437
+ if (expanded.error)
438
+ return { ...empty, errors: [{ message: expanded.error.message, at: start + (expanded.error.at ?? 0) }] };
439
+ if (expanded.text !== head) {
440
+ body = written.slice(0, start) + expanded.text + written.slice(start + head.length);
441
+ const end = start + expanded.text.length;
442
+ said = (at) => (at < start ? at : at < end ? start + expanded.at(at - start) : at - (expanded.text.length - head.length));
443
+ }
444
+ }
445
+ }
446
+ const extra = [];
447
+ /* Each `any`/`no` condition is read on its own, against the child, and stands in the text the parser reads as a
448
+ synthetic column asked `is (not) empty`; it becomes the condition it is when the parse comes back. */
449
+ const anys = new Map();
450
+ if (/\b(?:any|no)\s/i.test(body)) {
451
+ const read = anyRowsOf(schema, base, body, now, options, anys, extra);
452
+ if (read.errors.length)
453
+ return { ...empty, errors: read.errors.map((error) => ({ ...error, at: error.at === undefined ? undefined : said(error.at) })) };
454
+ if (read.text !== body) {
455
+ const outer = said;
456
+ body = read.text;
457
+ said = (at) => outer(read.at(at));
458
+ }
459
+ }
460
+ const added = new Set();
461
+ /* Paths already reported as broken. The synthetic table cannot contain
462
+ them, so the parser will reach them too and say "not a column on invoice"
463
+ — true, useless, and directly under a message that named the real
464
+ problem. Two errors for one mistake reads as two mistakes. */
465
+ const failed = new Set();
466
+ const scanned = maskQuoted(body);
467
+ for (const match of scanned.matchAll(DOTTED_TOKEN)) {
468
+ const path = match[1];
469
+ const start = match.index ?? 0;
470
+ /* Whether this token is being used as a field decides whether a failure
471
+ to resolve is worth reporting. In value position — `external_ref =
472
+ a.b` — a dotted word that is not a path is simply a value, and an error
473
+ about it would be noise on a query that is perfectly correct. */
474
+ /* …or a path on the right of a comparison whose first hop is a column
475
+ of the base: `on_hand < product_id.reorder_level` compares with it,
476
+ so a hop that goes nowhere is a mistake to name, not text. */
477
+ const isField = OPERATOR_AFTER.test(scanned.slice(start + path.length)) ||
478
+ FIELD_BEFORE.test(scanned.slice(0, start)) ||
479
+ (/(?:!=|<=|>=|=|<|>)\s*$/.test(scanned.slice(0, start)) && base.columns.some((c) => c.name === path.split('.')[0].split('+')[0]));
480
+ if (added.has(path))
481
+ continue;
482
+ added.add(path);
483
+ const { resolved, error } = tryResolvePath(schema, baseId, path);
484
+ if (error) {
485
+ /* Located at the token so the composer can underline the right word.
486
+ The offset is into the filter text, and the caller shifts it. */
487
+ if (isField) {
488
+ errors.push({ message: error.message, at: said(start) });
489
+ failed.add(path);
490
+ }
491
+ continue;
492
+ }
493
+ if (!resolved)
494
+ continue;
495
+ /* The synthetic column keeps the *declared* type of the real column, so
496
+ `signed_up = last 30 days` through a path behaves exactly as it does
497
+ on the base table. Nullability is widened because the join can miss. */
498
+ extra.push({
499
+ name: resolved.path,
500
+ type: resolved.column.type,
501
+ nullable: true,
502
+ primaryKey: false,
503
+ references: resolved.column.references,
504
+ });
505
+ }
506
+ /* The base's own record label (`show $label`, `$label contains 12`) is a
507
+ column of the synthetic table where the catalog serves a template. */
508
+ if (base.labelTemplate && /(?<![\w.$])\$label(?![\w$])/.test(scanned)) {
509
+ extra.push(labelColumnMeta());
510
+ }
511
+ /* The base's rollups (rollups.ts) are columns of the synthetic table where
512
+ the text names one, typed as the catalog serves them, so `sort
513
+ line_total desc` and `open_deals > 0` parse and coerce like any number.
514
+ A rollup through a reference is a dotted path, found above. */
515
+ for (const [name, rollup] of Object.entries(base.rollups ?? {})) {
516
+ if (added.has(name) || base.columns.some((c) => c.name === name))
517
+ continue;
518
+ if (new RegExp(`(?<![\\w.$])${name}(?![\\w$])`).test(scanned))
519
+ extra.push(rollupColumnMeta(name, rollup));
520
+ }
521
+ /* And its computed columns (computed.ts), typed as they come out: `is_overdue = true`, `sort days_open desc`. */
522
+ for (const [name, computed] of Object.entries(base.computed ?? {})) {
523
+ if (added.has(name) || base.columns.some((c) => c.name === name))
524
+ continue;
525
+ if (new RegExp(`(?<![\\w.$])${name}(?![\\w$])`).test(scanned))
526
+ extra.push(computedColumnMeta(name, computed));
527
+ }
528
+ /* And its tags (tagLists.ts), a column only `has` may ask: `tags has bug`, `show name, tags`. */
529
+ if (base.tags &&
530
+ !added.has(TAGS_COLUMN) &&
531
+ !base.columns.some((c) => c.name === TAGS_COLUMN) &&
532
+ new RegExp(`(?<![\\w.$])${TAGS_COLUMN}(?![\\w$])`).test(scanned))
533
+ extra.push(tagsColumnMeta());
534
+ /* Aggregate aliases join the synthetic table so `sort invoices desc`
535
+ parses — and only sort. A WHERE runs before the SELECT list exists, so
536
+ filtering by an alias is refused below with the reason, not with
537
+ "not a column", which is true and useless. */
538
+ const aliases = new Set(options.aggregateAliases ?? []);
539
+ for (const alias of aliases) {
540
+ if (!added.has(alias))
541
+ extra.push({ name: alias, type: 'NUMERIC', nullable: true, primaryKey: false });
542
+ }
543
+ const synthetic = {
544
+ ...schema,
545
+ tables: schema.tables.map((t) => (t.id === base.id ? { ...t, columns: [...t.columns, ...extra] } : t)),
546
+ };
547
+ /* The parser wants `<table> <clauses>`, which is also what the composer's
548
+ hint text shows, so the base name is prepended rather than the grammar
549
+ being special-cased. */
550
+ const offset = base.id.length + 1;
551
+ const parsed = parseQuery(`${base.id} ${body}`, {
552
+ schema: synthetic,
553
+ now,
554
+ columnValues: options.columnValues !== false,
555
+ ...(options.me !== undefined ? { me: options.me } : {}),
556
+ });
557
+ for (const err of parsed.errors) {
558
+ if ([...failed].some((path) => err.message.includes(`"${path}"`)))
559
+ continue;
560
+ errors.push({ message: err.message, at: err.at === undefined ? undefined : said(Math.max(0, err.at - offset)) });
561
+ }
562
+ const query = parsed.query;
563
+ if (!query)
564
+ return { ...empty, errors };
565
+ for (const group of query.filter.groups) {
566
+ for (const c of group) {
567
+ if (aliases.has(c.column)) {
568
+ errors.push({
569
+ message: `"${c.column}" is an aggregate result, and a filter runs before it exists. ` + 'Filter on the child table instead.',
570
+ });
571
+ }
572
+ }
573
+ }
574
+ for (const column of query.columns ?? []) {
575
+ if (aliases.has(column)) {
576
+ errors.push({ message: `"${column}" is already shown — the aggregate adds its own column.` });
577
+ }
578
+ }
579
+ return {
580
+ filter: {
581
+ groups: query.filter.groups.map((group) => group.map((c) => anys.get(c.column) ??
582
+ (c.other === undefined ? { path: c.column, op: c.op, value: c.value } : { path: c.column, op: c.op, other: c.other }))),
583
+ },
584
+ columns: query.columns,
585
+ orderBy: query.orderBy.map((o) => aliases.has(o.column) ? { path: o.column, direction: o.direction, alias: true } : { path: o.column, direction: o.direction }),
586
+ limit: query.limit,
587
+ errors,
588
+ };
589
+ }
590
+ /* The name an `any` condition stands under while the parser reads the rest: no column can be called it. */
591
+ const ANY_STANDIN = '$any';
592
+ /**
593
+ * The `any`/`no` conditions of a view's text, each read against its child
594
+ * table: the text with each written as a stand-in column asked `is (not)
595
+ * empty`, the stand-ins (in `extra`) and the conditions they become (in
596
+ * `anys`), and a position in the new text said in the old.
597
+ *
598
+ * The child is the table whose one reference points at the base, or the one
599
+ * `via` names; its conditions are its own columns and paths up from it, and
600
+ * its segments — what a row scope's narrowing reads (rowScope.ts `filtered`),
601
+ * which is how the EXISTS is written. A second `any` inside, a rollup, a
602
+ * computed column, tags or a record label are refused by name rather than
603
+ * left to fail as SQL.
604
+ */
605
+ function anyRowsOf(schema, base, body, now, options, anys, extra) {
606
+ const same = { text: body, at: (position) => position, errors: [] };
607
+ const { head } = splitClauseTail(body);
608
+ const start = head ? body.indexOf(head) : -1;
609
+ if (start < 0)
610
+ return same;
611
+ const found = anyConditions(head);
612
+ if (!found.length)
613
+ return same;
614
+ const errors = [];
615
+ const splices = [];
616
+ let text = '';
617
+ let last = 0;
618
+ for (const condition of found) {
619
+ const at = start + condition.at;
620
+ const [, word, childName, via, rest] = ANY_CONDITION.exec(condition.text);
621
+ const fail = (message, offset = 0) => errors.push({ message, at: at + offset });
622
+ const named = backrefsTo(schema, base.id).filter((b) => {
623
+ const table = findTable(schema, b.child);
624
+ return table && (table.id === childName || table.name === childName);
625
+ });
626
+ const through = via === undefined ? named : named.filter((b) => b.fk.from.columns.join('+') === via);
627
+ if (!named.length) {
628
+ fail(findTable(schema, childName) || schema.tables.some((t) => t.name === childName)
629
+ ? `${childName} has no reference to ${base.name}, so "${word} ${childName}" has no rows to ask about.`
630
+ : `No table called "${childName}" here.`, word.length + 1);
631
+ continue;
632
+ }
633
+ if (!through.length) {
634
+ fail(`${childName} points at ${base.name} through ${named.map((b) => b.fk.from.columns.join('+')).join(', ')}, not "${via}".`, condition.text.indexOf(via, word.length + childName.length + 1));
635
+ continue;
636
+ }
637
+ if (through.length > 1) {
638
+ const columns = through.map((b) => b.fk.from.columns.join('+'));
639
+ fail(`${childName} points at ${base.name} through ${columns.join(' and ')}: say which — ${word} ${childName} via ${columns[0]}.`, word.length + 1);
640
+ continue;
641
+ }
642
+ const { fk } = through[0];
643
+ if (fk.stitched) {
644
+ fail(`${childName} is in another source, on another connection, so a list of ${base.name} cannot ask about its rows: ` +
645
+ 'each database filters what it holds.', word.length + 1);
646
+ continue;
647
+ }
648
+ const child = findTable(schema, fk.from.table);
649
+ let filter;
650
+ if (rest !== undefined) {
651
+ const restAt = condition.text.length - rest.length;
652
+ const parsed = parseViewQuery(schema, child.id, rest, now, { me: options.me, columnValues: options.columnValues });
653
+ for (const error of parsed.errors)
654
+ fail(error.message, restAt + (error.at ?? 0));
655
+ if (parsed.errors.length)
656
+ continue;
657
+ if (parsed.columns.length || parsed.orderBy.length || parsed.limit !== undefined) {
658
+ fail(`"${word} ${childName}" takes conditions only: a show, sort or limit there has no rows to shape.`, restAt);
659
+ continue;
660
+ }
661
+ const unread = parsed.filter.groups
662
+ .flat()
663
+ .flatMap((c) => (c.any ? [c.path] : [c.path, ...(c.other === undefined ? [] : [c.other])]))
664
+ .find((path) => {
665
+ if (path.startsWith('any ') || path.startsWith('no '))
666
+ return true;
667
+ const { resolved } = tryResolvePath(schema, child.id, path);
668
+ return !resolved || Boolean(resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey || path.includes('$'));
669
+ });
670
+ if (unread !== undefined) {
671
+ fail(/^(?:any|no)\s/i.test(unread)
672
+ ? `"${word} ${childName}" cannot ask about rows of its own rows: say that as its own condition.`
673
+ : `"${unread}" is not a stored column of ${child.name}, and "${word} ${childName}" reads only those.`, restAt + Math.max(0, rest.indexOf(unread)));
674
+ continue;
675
+ }
676
+ const stitched = stitchedListProblem(schema, child.id, { filter: parsed.filter });
677
+ if (stitched) {
678
+ fail(stitched, restAt);
679
+ continue;
680
+ }
681
+ filter = {
682
+ groups: parsed.filter.groups.map((group) => group.map((c) => c.other === undefined ? { column: c.path, op: c.op, value: c.value } : { column: c.path, op: c.op, other: c.other })),
683
+ };
684
+ }
685
+ const standin = `${ANY_STANDIN}${anys.size}`;
686
+ const said = condition.text.replace(/\s+/g, ' ');
687
+ anys.set(standin, {
688
+ path: said,
689
+ op: word.toLowerCase() === 'no' ? 'isNull' : 'isNotNull',
690
+ any: { via: `${child.id}.${fk.from.columns.join('+')}`, ...(filter ? { filter } : {}) },
691
+ });
692
+ extra.push({ name: standin, type: 'INTEGER', nullable: true, primaryKey: false });
693
+ const written = `${standin} ${word.toLowerCase() === 'no' ? 'is empty' : 'is not empty'}`;
694
+ text += body.slice(last, at);
695
+ splices.push({ at, was: condition.text.length, from: text.length, length: written.length });
696
+ text += written;
697
+ last = at + condition.text.length;
698
+ }
699
+ if (errors.length)
700
+ return { ...same, errors };
701
+ text += body.slice(last);
702
+ return {
703
+ text,
704
+ errors,
705
+ at: (position) => {
706
+ let shift = 0;
707
+ for (const splice of splices) {
708
+ if (position < splice.from)
709
+ break;
710
+ if (position < splice.from + splice.length)
711
+ return splice.at;
712
+ shift = splice.from + splice.length - (splice.at + splice.was);
713
+ }
714
+ return position - shift;
715
+ },
716
+ };
717
+ }
718
+ /**
719
+ * How many rows the view matches, ignoring the page.
720
+ *
721
+ * A separate compilation rather than a wrapper around the other one, because
722
+ * the two differ in more than a keyword: the SELECT list, the ORDER BY and the
723
+ * page are all irrelevant here, and every aggregate in the view would be a
724
+ * correlated subquery evaluated per row for a number nobody reads.
725
+ *
726
+ * The count is exact rather than estimated, and it is *base rows* — which is
727
+ * only true because every join is to-one. That is the same property the whole
728
+ * design rests on: a view never multiplies, so counting it is counting the
729
+ * table it started from. With `limit`, it stops after that many.
730
+ */
731
+ export function compileViewCount(schema, view, style, scope, limit) {
732
+ schema = withViewReferences(schema, view.references);
733
+ const base = findTable(schema, view.base);
734
+ if (!base) {
735
+ throw new PathError(`No table called "${view.base}" in this database, so the view has nothing to start from.`, view.base, view.base, 0);
736
+ }
737
+ const relations = new ScopedRelations(schema, style, scope);
738
+ const baseRelation = relations.table(base.id);
739
+ const plan = new JoinPlan(schema, base, style, relations.table);
740
+ const { qualify, membership, exists } = viewPaths(schema, view, plan, style, relations);
741
+ const conditions = viewConditions(view);
742
+ for (const group of conditions?.groups ?? []) {
743
+ for (const condition of group) {
744
+ if (condition.any) {
745
+ exists(condition.any);
746
+ continue;
747
+ }
748
+ qualify(condition.column);
749
+ if (condition.other !== undefined)
750
+ qualify(condition.other);
751
+ }
752
+ }
753
+ const joins = plan.clauses();
754
+ const prefix = relations.finish();
755
+ const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists);
756
+ /* Only filter joins affect row count. Projection-only joins and scalar
757
+ aggregates deliberately do not register relations in this statement. */
758
+ const relation = `${baseRelation} ${style.quote(plan.baseAlias)}` + (joins.length ? `\n${joins.join('\n')}` : '') + where.text;
759
+ /* A bound stops the count the way every bounded count stops
760
+ (countStatement): at most `limit` base rows are read. */
761
+ const text = limit === undefined ? prefix.text + `SELECT COUNT(*) AS n\n FROM ${relation}` : prefix.text + countStatement(relation, limit);
762
+ return { text, params: [...prefix.params, ...where.params] };
763
+ }
764
+ /**
765
+ * Render a view as the SQL it will become, values inline.
766
+ *
767
+ * The mirror of `explain` in query.ts and used the same way: this string never
768
+ * reaches a database, and showing `"customer_1"."name" LIKE '%Harbour%'` next
769
+ * to the path that produced it is how someone learns what the composer is
770
+ * doing on their behalf.
771
+ */
772
+ export function explainView(schema, view, style) {
773
+ const compiled = compileView(schema, withViewComputed(schema, withViewLabels(schema, view)), style);
774
+ let i = 0;
775
+ return compiled.text.replace(/\$\d+|\?/g, () => sqlLiteral(compiled.params[i++]));
776
+ }
777
+ /**
778
+ * One parameter, written the way SQL would have to spell it.
779
+ *
780
+ * Exported because a breakdown explains itself the same way a view does, and
781
+ * two spellings of `NULL` — or two different ideas about escaping a quote —
782
+ * would show up as a difference between two panes that claim to be the same
783
+ * statement.
784
+ */
785
+ export function sqlLiteral(value) {
786
+ if (value === null || value === undefined)
787
+ return 'NULL';
788
+ if (typeof value === 'number' || typeof value === 'boolean')
789
+ return String(value);
790
+ return `'${String(value).replace(/'/g, "''")}'`;
791
+ }
792
+ /* ---------- config file support ----------
793
+
794
+ Views can be predefined in `tablewalk.json` beside `connections`, and are
795
+ validated the same strict way: a malformed entry is an error naming the
796
+ entry, never a silent skip. Quietly dropping a view someone wrote is the
797
+ worst outcome — they see a shorter list and no reason for it. */
798
+ export function parseViewsConfig(raw, where) {
799
+ const views = raw?.views;
800
+ if (views === undefined)
801
+ return [];
802
+ if (!Array.isArray(views))
803
+ throw new Error(`${where}: "views" must be an array.`);
804
+ const seen = new Set();
805
+ return views.map((entry, i) => {
806
+ const at = `${where}: view ${i + 1}`;
807
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
808
+ throw new Error(`${at} is not an object.`);
809
+ }
810
+ const v = entry;
811
+ if (typeof v.name !== 'string' || !v.name.trim())
812
+ throw new Error(`${at} has no "name".`);
813
+ /* A view may be written down instead of built out of objects. The text
814
+ form needs a schema to resolve, which a config file does not have, so
815
+ it is carried through and expanded when a connection is open — and any
816
+ error it holds is reported against that view rather than swallowed. */
817
+ const text = v.text === undefined ? undefined : String(v.text).trim();
818
+ if (v.text !== undefined && (typeof v.text !== 'string' || !text)) {
819
+ throw new Error(`${at} ("${v.name}") has a "text" that is not a non-empty string.`);
820
+ }
821
+ if (text && v.base !== undefined) {
822
+ throw new Error(`${at} ("${v.name}") has both "text" and "base". The text form already names its table.`);
823
+ }
824
+ if (!text && (typeof v.base !== 'string' || !v.base.trim())) {
825
+ throw new Error(`${at} ("${v.name}") has no "base" table.`);
826
+ }
827
+ const id = v.id === undefined ? `config-${slug(v.name, 'view')}` : String(v.id);
828
+ if (v.id !== undefined && (typeof v.id !== 'string' || !v.id.trim())) {
829
+ throw new Error(`${at} ("${v.name}") has an "id" that is not a non-empty string.`);
830
+ }
831
+ if (seen.has(id))
832
+ throw new Error(`${at} ("${v.name}") repeats the id "${id}".`);
833
+ seen.add(id);
834
+ const columns = parseColumns(v.columns, at, String(v.name));
835
+ const aggregates = parseAggregates(v.aggregates, at, String(v.name));
836
+ const filter = parseFilter(v.filter, at, String(v.name));
837
+ const orderBy = parseOrder(v.orderBy, at, String(v.name));
838
+ if (v.limit !== undefined && (typeof v.limit !== 'number' || !Number.isFinite(v.limit))) {
839
+ throw new Error(`${at} ("${v.name}") has a "limit" that is not a number.`);
840
+ }
841
+ if (v.connection !== undefined && typeof v.connection !== 'string') {
842
+ throw new Error(`${at} ("${v.name}") has a "connection" that is not a string.`);
843
+ }
844
+ return {
845
+ id,
846
+ name: v.name.trim(),
847
+ base: text ? text.trim().split(/\s+/)[0] : v.base.trim(),
848
+ text,
849
+ columns,
850
+ aggregates,
851
+ filter,
852
+ orderBy,
853
+ limit: v.limit,
854
+ connection: v.connection,
855
+ source: 'config',
856
+ };
857
+ });
858
+ }
859
+ function parseColumns(raw, at, name) {
860
+ if (raw === undefined)
861
+ return [];
862
+ if (!Array.isArray(raw))
863
+ throw new Error(`${at} ("${name}") has "columns" that is not an array.`);
864
+ return raw.map((col, j) => {
865
+ const where = `${at} ("${name}"), column ${j + 1}`;
866
+ if (typeof col === 'string') {
867
+ if (!col.trim())
868
+ throw new Error(`${where} is empty.`);
869
+ return { path: col.trim() };
870
+ }
871
+ if (!col || typeof col !== 'object')
872
+ throw new Error(`${where} is not a string or an object.`);
873
+ const c = col;
874
+ if (typeof c.path !== 'string' || !c.path.trim())
875
+ throw new Error(`${where} has no "path".`);
876
+ if (c.alias !== undefined && (typeof c.alias !== 'string' || !c.alias.trim())) {
877
+ throw new Error(`${where} has an "alias" that is not a non-empty string.`);
878
+ }
879
+ return { path: c.path.trim(), alias: c.alias };
880
+ });
881
+ }
882
+ function parseAggregates(raw, at, name) {
883
+ if (raw === undefined)
884
+ return undefined;
885
+ if (!Array.isArray(raw))
886
+ throw new Error(`${at} ("${name}") has "aggregates" that is not an array.`);
887
+ return raw.map((entry, j) => {
888
+ const where = `${at} ("${name}"), aggregate ${j + 1}`;
889
+ if (!entry || typeof entry !== 'object')
890
+ throw new Error(`${where} is not an object.`);
891
+ const a = entry;
892
+ if (typeof a.fn !== 'string' || !AGGREGATE_FNS.includes(a.fn)) {
893
+ throw new Error(`${where} has an unknown "fn". Use one of: ${AGGREGATE_FNS.join(', ')}.`);
894
+ }
895
+ if (typeof a.via !== 'string' || !a.via.trim())
896
+ throw new Error(`${where} has no "via".`);
897
+ if (a.fn !== 'count' && (typeof a.column !== 'string' || !a.column.trim())) {
898
+ throw new Error(`${where} is a ${a.fn} and needs a "column" to summarise.`);
899
+ }
900
+ if (a.on !== undefined && typeof a.on !== 'string') {
901
+ throw new Error(`${where} has an "on" that is not a string.`);
902
+ }
903
+ if (a.alias !== undefined && (typeof a.alias !== 'string' || !a.alias.trim())) {
904
+ throw new Error(`${where} has an "alias" that is not a non-empty string.`);
905
+ }
906
+ return {
907
+ fn: a.fn,
908
+ via: a.via.trim(),
909
+ on: a.on,
910
+ column: typeof a.column === 'string' ? a.column.trim() : undefined,
911
+ alias: a.alias,
912
+ };
913
+ });
914
+ }
915
+ const OPS = ['=', '!=', '>', '>=', '<', '<=', 'contains', 'startsWith', 'endsWith', 'like', 'in', 'isNull', 'isNotNull'];
916
+ function parseFilter(raw, at, name) {
917
+ if (raw === undefined)
918
+ return undefined;
919
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
920
+ throw new Error(`${at} ("${name}") has a "filter" that is not an object.`);
921
+ }
922
+ const groups = raw.groups;
923
+ if (!Array.isArray(groups))
924
+ throw new Error(`${at} ("${name}") has a "filter" with no "groups" array.`);
925
+ return {
926
+ groups: groups.map((group, g) => {
927
+ if (!Array.isArray(group))
928
+ throw new Error(`${at} ("${name}"), filter group ${g + 1} is not an array.`);
929
+ return group.map((cond, c) => {
930
+ const where = `${at} ("${name}"), filter group ${g + 1} condition ${c + 1}`;
931
+ if (!cond || typeof cond !== 'object')
932
+ throw new Error(`${where} is not an object.`);
933
+ const x = cond;
934
+ if (typeof x.path !== 'string' || !x.path.trim())
935
+ throw new Error(`${where} has no "path".`);
936
+ if (typeof x.op !== 'string' || !OPS.includes(x.op)) {
937
+ throw new Error(`${where} has an unknown operator "${String(x.op)}". Use one of: ${OPS.join(', ')}.`);
938
+ }
939
+ if (x.other !== undefined && (typeof x.other !== 'string' || !x.other.trim())) {
940
+ throw new Error(`${where} has an "other" that is not a path.`);
941
+ }
942
+ return typeof x.other === 'string'
943
+ ? { path: x.path.trim(), op: x.op, other: x.other.trim() }
944
+ : { path: x.path.trim(), op: x.op, value: x.value };
945
+ });
946
+ }),
947
+ };
948
+ }
949
+ function parseOrder(raw, at, name) {
950
+ if (raw === undefined)
951
+ return undefined;
952
+ if (!Array.isArray(raw))
953
+ throw new Error(`${at} ("${name}") has an "orderBy" that is not an array.`);
954
+ return raw.map((entry, j) => {
955
+ const where = `${at} ("${name}"), orderBy ${j + 1}`;
956
+ if (typeof entry === 'string')
957
+ return { path: entry, direction: 'asc' };
958
+ if (!entry || typeof entry !== 'object')
959
+ throw new Error(`${where} is not a string or an object.`);
960
+ const o = entry;
961
+ if (typeof o.path !== 'string' || !o.path.trim())
962
+ throw new Error(`${where} has no "path".`);
963
+ const direction = o.direction === undefined ? 'asc' : o.direction;
964
+ if (direction !== 'asc' && direction !== 'desc') {
965
+ throw new Error(`${where} has a "direction" that is not "asc" or "desc".`);
966
+ }
967
+ return { path: o.path.trim(), direction };
968
+ });
969
+ }
970
+ /**
971
+ * Check a view against a schema without running it.
972
+ *
973
+ * Used by `GET /api/views` so a config view naming a table that does not exist
974
+ * on this connection is listed *with its reason*, rather than either vanishing
975
+ * or blowing up the endpoint for every other view beside it.
976
+ */
977
+ export function validateView(schema, view) {
978
+ try {
979
+ /* Any style will do: this compiles to throw away the text and keep the
980
+ error, and no dialect refuses a view another accepts. */
981
+ compileView(schema, view, ANSI_STYLE);
982
+ return undefined;
983
+ }
984
+ catch (err) {
985
+ return err.message;
986
+ }
987
+ }