@harperfast/harper 5.3.0-beta.4 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/bin/backup.ts +6 -1
  2. package/components/Application.ts +486 -115
  3. package/components/DESIGN.md +109 -21
  4. package/components/deploymentProvenance.ts +28 -0
  5. package/components/operations.js +14 -7
  6. package/components/packageComponent.ts +5 -1
  7. package/config-root.schema.json +1 -1
  8. package/dataLayer/DESIGN.md +35 -7
  9. package/dataLayer/blobBackup.ts +71 -30
  10. package/dataLayer/harperBridge/ResourceBridge.ts +16 -2
  11. package/dataLayer/rocksdbBackup.ts +88 -34
  12. package/dataLayer/schemaDescribe.ts +45 -1
  13. package/dist/bin/backup.js +1 -1
  14. package/dist/bin/backup.js.map +1 -1
  15. package/dist/components/Application.d.ts +11 -21
  16. package/dist/components/Application.js +436 -105
  17. package/dist/components/Application.js.map +1 -1
  18. package/dist/components/deploymentProvenance.d.ts +4 -0
  19. package/dist/components/deploymentProvenance.js +32 -0
  20. package/dist/components/deploymentProvenance.js.map +1 -0
  21. package/dist/components/operations.js +14 -7
  22. package/dist/components/operations.js.map +1 -1
  23. package/dist/components/packageComponent.js +6 -1
  24. package/dist/components/packageComponent.js.map +1 -1
  25. package/dist/dataLayer/blobBackup.d.ts +30 -3
  26. package/dist/dataLayer/blobBackup.js +73 -36
  27. package/dist/dataLayer/blobBackup.js.map +1 -1
  28. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +1 -0
  29. package/dist/dataLayer/harperBridge/ResourceBridge.js +15 -3
  30. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  31. package/dist/dataLayer/rocksdbBackup.d.ts +4 -1
  32. package/dist/dataLayer/rocksdbBackup.js +74 -36
  33. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  34. package/dist/dataLayer/schemaDescribe.js +45 -1
  35. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  36. package/dist/resources/DatabaseTransaction.d.ts +1 -0
  37. package/dist/resources/DatabaseTransaction.js +33 -8
  38. package/dist/resources/DatabaseTransaction.js.map +1 -1
  39. package/dist/resources/ResourceInterface.d.ts +7 -3
  40. package/dist/resources/ResourceInterface.js +2 -0
  41. package/dist/resources/ResourceInterface.js.map +1 -1
  42. package/dist/resources/Table.d.ts +151 -445
  43. package/dist/resources/Table.js +209 -8
  44. package/dist/resources/Table.js.map +1 -1
  45. package/dist/resources/analytics/write.d.ts +11 -0
  46. package/dist/resources/analytics/write.js +3 -0
  47. package/dist/resources/analytics/write.js.map +1 -1
  48. package/dist/resources/blob.d.ts +30 -0
  49. package/dist/resources/blob.js +189 -1
  50. package/dist/resources/blob.js.map +1 -1
  51. package/dist/resources/databases.d.ts +1 -0
  52. package/dist/resources/databases.js +80 -15
  53. package/dist/resources/databases.js.map +1 -1
  54. package/dist/resources/derivedIndexRuntime.d.ts +13 -0
  55. package/dist/resources/derivedIndexRuntime.js +57 -5
  56. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  57. package/dist/resources/derivedIndexes.d.ts +1 -0
  58. package/dist/resources/derivedIndexes.js +116 -15
  59. package/dist/resources/derivedIndexes.js.map +1 -1
  60. package/dist/resources/fullTextSchema.d.ts +5 -3
  61. package/dist/resources/fullTextSchema.js +48 -5
  62. package/dist/resources/fullTextSchema.js.map +1 -1
  63. package/dist/resources/fullTextSchemaLifecycle.d.ts +2 -0
  64. package/dist/resources/fullTextSchemaLifecycle.js +33 -0
  65. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  66. package/dist/resources/graphql.js +30 -12
  67. package/dist/resources/graphql.js.map +1 -1
  68. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +21 -1
  69. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +114 -5
  70. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  71. package/dist/resources/indexes/fullTextDerivedIndex.d.ts +11 -2
  72. package/dist/resources/indexes/fullTextDerivedIndex.js +187 -24
  73. package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -1
  74. package/dist/resources/indexes/fullTextNativeBinding.d.ts +99 -1
  75. package/dist/resources/indexes/fullTextNativeBinding.js +35 -1
  76. package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
  77. package/dist/resources/indexes/fullTextQueryIndex.d.ts +58 -0
  78. package/dist/resources/indexes/fullTextQueryIndex.js +943 -0
  79. package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -0
  80. package/dist/resources/indexes/fullTextQueryProtocol.d.ts +6 -0
  81. package/dist/resources/indexes/fullTextQueryProtocol.js +32 -0
  82. package/dist/resources/indexes/fullTextQueryProtocol.js.map +1 -0
  83. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +4 -1
  84. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +30 -7
  85. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  86. package/dist/resources/search.d.ts +41 -0
  87. package/dist/resources/search.js +468 -26
  88. package/dist/resources/search.js.map +1 -1
  89. package/dist/resources/transactionBroadcast.js +101 -50
  90. package/dist/resources/transactionBroadcast.js.map +1 -1
  91. package/dist/resources/typeParity.d.ts +9 -0
  92. package/dist/resources/typeParity.js +3 -0
  93. package/dist/resources/typeParity.js.map +1 -0
  94. package/dist/server/REST.js.map +1 -1
  95. package/dist/server/itc/serverHandlers.js +48 -8
  96. package/dist/server/itc/serverHandlers.js.map +1 -1
  97. package/dist/server/serverHelpers/contentTypes.d.ts +7 -0
  98. package/dist/server/serverHelpers/contentTypes.js +36 -23
  99. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  100. package/dist/server/serverHelpers/uwsServer.js +28 -5
  101. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  102. package/dist/server/threads/itc.js +19 -0
  103. package/dist/server/threads/itc.js.map +1 -1
  104. package/dist/server/threads/manageThreads.d.ts +3 -0
  105. package/dist/server/threads/manageThreads.js +90 -23
  106. package/dist/server/threads/manageThreads.js.map +1 -1
  107. package/dist/utility/operation_authorization.js +25 -7
  108. package/dist/utility/operation_authorization.js.map +1 -1
  109. package/dist/utility/signalling.js +3 -0
  110. package/dist/utility/signalling.js.map +1 -1
  111. package/dist/validation/searchValidator.js +22 -2
  112. package/dist/validation/searchValidator.js.map +1 -1
  113. package/npm-shrinkwrap.json +22 -22
  114. package/package.json +2 -4
  115. package/resources/DESIGN.md +19 -2
  116. package/resources/DatabaseTransaction.ts +40 -10
  117. package/resources/FULL_TEXT.md +97 -0
  118. package/resources/ResourceInterface.ts +8 -2
  119. package/resources/Table.ts +767 -17
  120. package/resources/analytics/write.ts +3 -3
  121. package/resources/blob.ts +183 -1
  122. package/resources/databases.ts +126 -14
  123. package/resources/derivedIndexRuntime.ts +79 -6
  124. package/resources/derivedIndexes.ts +142 -18
  125. package/resources/fullTextSchema.ts +55 -9
  126. package/resources/fullTextSchemaLifecycle.ts +45 -0
  127. package/resources/graphql.ts +33 -12
  128. package/resources/indexes/DESIGN.md +167 -15
  129. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +125 -5
  130. package/resources/indexes/fullTextDerivedIndex.ts +216 -32
  131. package/resources/indexes/fullTextNativeBinding.ts +133 -3
  132. package/resources/indexes/fullTextQueryIndex.ts +1120 -0
  133. package/resources/indexes/fullTextQueryProtocol.ts +35 -0
  134. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +28 -8
  135. package/resources/search.ts +513 -26
  136. package/resources/transactionBroadcast.ts +86 -47
  137. package/resources/typeParity.ts +20 -0
  138. package/schema.graphql +18 -5
  139. package/server/DESIGN.md +18 -0
  140. package/server/REST.ts +1 -1
  141. package/server/itc/serverHandlers.js +61 -8
  142. package/server/serverHelpers/contentTypes.ts +31 -26
  143. package/server/serverHelpers/uwsServer.ts +35 -7
  144. package/server/threads/itc.js +21 -0
  145. package/server/threads/manageThreads.js +95 -21
  146. package/studio/web/assets/{Chat-Bq60AO-g.js → Chat-CNEKMllJ.js} +13 -13
  147. package/studio/web/assets/FloatingChat-CEnWhHeR.js +23 -0
  148. package/studio/web/assets/SearchDialog-CwhJdEdY.js +1 -0
  149. package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
  150. package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
  151. package/studio/web/assets/apiToken-DC5vsaaO.js +1 -0
  152. package/studio/web/assets/applications-DQZxDy0x.js +296 -0
  153. package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
  154. package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
  155. package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
  156. package/studio/web/assets/authStore-CJCaADw9.js +3 -0
  157. package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
  158. package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
  159. package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
  160. package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
  161. package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
  162. package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
  163. package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
  164. package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
  165. package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
  166. package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
  167. package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
  168. package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
  169. package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
  170. package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
  171. package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
  172. package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
  173. package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
  174. package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
  175. package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
  176. package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
  177. package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
  178. package/studio/web/assets/cn-6T8VranJ.js +1 -0
  179. package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
  180. package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
  181. package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
  182. package/studio/web/assets/{cssMode-DJyc9mHj.js → cssMode-Dz2DxF4e.js} +1 -1
  183. package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
  184. package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
  185. package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
  186. package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
  187. package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
  188. package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
  189. package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
  190. package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
  191. package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
  192. package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
  193. package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
  194. package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
  195. package/studio/web/assets/{editor-DDIsQfj6.js → editor-B3mjd3tz.js} +1 -1
  196. package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
  197. package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
  198. package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
  199. package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
  200. package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
  201. package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
  202. package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
  203. package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
  204. package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
  205. package/studio/web/assets/{html-g1BJHxLy.js → html-BsRGM7tp.js} +1 -1
  206. package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
  207. package/studio/web/assets/{htmlMode-Vh5IglCQ.js → htmlMode-tUKJaA3o.js} +1 -1
  208. package/studio/web/assets/index-72cEH474.css +1 -0
  209. package/studio/web/assets/index-BuqFjN5K.js +828 -0
  210. package/studio/web/assets/index.lazy-CcpfQFY_.js +2 -0
  211. package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
  212. package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
  213. package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
  214. package/studio/web/assets/{javascript-5VIolqf7.js → javascript-BZ_uRGHl.js} +1 -1
  215. package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
  216. package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
  217. package/studio/web/assets/{jsonMode-DG4zQraI.js → jsonMode-DgB-aFMj.js} +1 -1
  218. package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
  219. package/studio/web/assets/{languageServices-t5FBpX-i.js → languageServices-BleuBw17.js} +1 -1
  220. package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
  221. package/studio/web/assets/{lspLanguageFeatures-DgVnNPh-.js → lspLanguageFeatures-ByVYLSsQ.js} +1 -1
  222. package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
  223. package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
  224. package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
  225. package/studio/web/assets/notifications-D1Nf-Edn.js +1 -0
  226. package/studio/web/assets/notifications-TuzN0IMi.js +1 -0
  227. package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
  228. package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
  229. package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
  230. package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
  231. package/studio/web/assets/profile-D7C7Lupz.js +1 -0
  232. package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
  233. package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
  234. package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
  235. package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
  236. package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
  237. package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
  238. package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
  239. package/studio/web/assets/regions-nGQCm-v8.js +1 -0
  240. package/studio/web/assets/register-C3x9Ze6P.js +909 -0
  241. package/studio/web/assets/register-DjpsE8to.css +1 -0
  242. package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
  243. package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
  244. package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
  245. package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
  246. package/studio/web/assets/{setComponentFile-z4-qJCbA.js → setComponentFile-DpjrRqeG.js} +1 -1
  247. package/studio/web/assets/{setup-CS50uJ4x.js → setup-CGiKLZ9e.js} +2 -2
  248. package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
  249. package/studio/web/assets/status-CGJwrNPH.js +61 -0
  250. package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
  251. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
  252. package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
  253. package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
  254. package/studio/web/assets/toggleHighContrast-C-Ne6P7T.js +51 -0
  255. package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
  256. package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
  257. package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
  258. package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
  259. package/studio/web/assets/{tsMode-D83Gg3cq.js → tsMode-CSreYTys.js} +1 -1
  260. package/studio/web/assets/{typescript-Dfp-GrNq.js → typescript-MnxBk89W.js} +1 -1
  261. package/studio/web/assets/{useEntityRestURL-BKetlINf.js → useEntityRestURL-TNrh77-s.js} +1 -1
  262. package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
  263. package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
  264. package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
  265. package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
  266. package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
  267. package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
  268. package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
  269. package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
  270. package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
  271. package/studio/web/assets/{workers-CCvmDLH3.js → workers-BfZlUNVI.js} +3 -3
  272. package/studio/web/assets/{xml-06pZp00U.js → xml-BHd6qGH7.js} +1 -1
  273. package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
  274. package/studio/web/assets/{yaml-iSnWMVlV.js → yaml-C53R8OQq.js} +1 -1
  275. package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
  276. package/studio/web/index.html +15 -15
  277. package/utility/operation_authorization.ts +27 -7
  278. package/utility/signalling.ts +3 -0
  279. package/validation/searchValidator.ts +22 -13
  280. package/studio/web/assets/FloatingChat-D4560T0p.js +0 -23
  281. package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
  282. package/studio/web/assets/apiToken-BEvbFPMc.js +0 -1
  283. package/studio/web/assets/applications-CSUVTH4E.js +0 -296
  284. package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
  285. package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
  286. package/studio/web/assets/channel-BDtouPd6.js +0 -1
  287. package/studio/web/assets/dialog-DTepachC.js +0 -1
  288. package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
  289. package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
  290. package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
  291. package/studio/web/assets/index-CPoDzQTe.css +0 -1
  292. package/studio/web/assets/index-DGRAzvO4.js +0 -824
  293. package/studio/web/assets/index.lazy-vgSic7et.js +0 -2
  294. package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
  295. package/studio/web/assets/notifications-BeWcX04m.js +0 -1
  296. package/studio/web/assets/notifications-DUGWrWoO.js +0 -1
  297. package/studio/web/assets/profile-BvUEDBzW.js +0 -1
  298. package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
  299. package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
  300. package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
  301. package/studio/web/assets/regions-CB-_2m7M.js +0 -1
  302. package/studio/web/assets/register-1w1Zr3b2.js +0 -848
  303. package/studio/web/assets/register-jjEx9u7D.css +0 -1
  304. package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
  305. package/studio/web/assets/status-CI0wOdOE.js +0 -61
  306. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
  307. package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
  308. package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
  309. package/studio/web/assets/toggleHighContrast-BsAE4jPe.js +0 -49
  310. package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
  311. package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
  312. package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
  313. package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
  314. package/studio/web/assets/x-vYntCxm4.js +0 -1
@@ -10,6 +10,10 @@ import { writeKeyId, getReadTransactionGuard } from './DatabaseTransaction.ts';
10
10
  import { recordAction } from './analytics/write';
11
11
  import { RocksDatabase } from '@harperfast/rocksdb-js';
12
12
  import { appendHeader } from '../server/serverHelpers/Headers.ts';
13
+ import type { FullTextCondition } from './indexes/fullTextQueryIndex.ts';
14
+ import { FULL_TEXT_POSITIVE_COMPARATORS, fullTextComparatorMode } from './indexes/fullTextQueryProtocol.ts';
15
+
16
+ export { fullTextComparatorMode };
13
17
 
14
18
  // these are ratios/percentages of overall table size
15
19
  const OPEN_RANGE_ESTIMATE = 0.3;
@@ -72,7 +76,11 @@ export function executeConditions(
72
76
  filtered,
73
77
  recordAccess?
74
78
  ) {
79
+ conditions = combineFullTextConditions(conditions, operator, table);
75
80
  const firstSearch = conditions[0];
81
+ // A same-index full-text OR is already one native expression. Route that single traversal through
82
+ // the AND path so row and vector guards are pushed down before the requested page is filled.
83
+ if (operator === 'or' && conditions.length === 1 && firstSearch.fullTextQuery) operator = 'and';
76
84
  // Record-level guards (caller-supplied vectorFilter + rowFilter) apply to every record
77
85
  // the query returns, independent of which condition leads (#1241). `recordAccess` is supplied only on
78
86
  // the top-level executeConditions call (Table.search) and deliberately NOT threaded into the recursive
@@ -105,19 +113,29 @@ export function executeConditions(
105
113
  return recordGuards ? transformToEntries(deduped, recordGuards) : deduped;
106
114
  } else {
107
115
  // AND: use the indexed query for the first condition and filter by all subsequent conditions.
108
- if (isFilterablePushdown(firstSearch, table)) {
116
+ const pushdownIndex = filterablePushdownIndex(firstSearch, table);
117
+ if (pushdownIndex) {
109
118
  // The lead is a filterable custom index (HNSW). It doesn't populate the join map (`filtered`)
110
119
  // that sibling filters read, so — unlike the general case below — we can build the sibling
111
120
  // filters BEFORE executing the lead and push them into the traversal, so filtering happens
112
121
  // during the graph walk instead of post-filtering an under-filled candidate set (#1241). They
113
122
  // also stay in the post-filter chain as a deterministic, harmless re-check.
114
- const filters = mapConditionsToFilters(conditions.slice(1), true, firstSearch.estimated_count);
123
+ const siblings = conditions.slice(1);
124
+ const filters = mapConditionsToFilters(siblings, true, firstSearch.estimated_count);
115
125
  const recordFilters = recordGuards ? filters.concat(recordGuards) : filters;
126
+ // The sibling conditions a secondary-index range scan answers exactly can seed an allow-set
127
+ // the index admits from, instead of loading and decoding a record at every visited node.
128
+ // The post-filter chain below stays whole, so the set only has to be a SUPERSET of the true
129
+ // matches — the index decides whether building it beats evaluating the predicate.
130
+ const candidateKeys =
131
+ pushdownIndex.candidateKeyFilter && siblings.length > 0
132
+ ? planCandidateKeys(siblings, table, txn, !recordGuards)
133
+ : undefined;
116
134
  // Execute a COPY carrying the pushed-down predicate, never mutating the caller's condition
117
135
  // object (which may be reused across requests with different users / guards).
118
136
  const lead =
119
137
  recordFilters.length > 0
120
- ? { ...firstSearch, recordFilter: composeRecordFilter(recordFilters, table, context) }
138
+ ? { ...firstSearch, recordFilter: composeRecordFilter(recordFilters, table, context), candidateKeys }
121
139
  : firstSearch;
122
140
  const results = executeCondition(lead);
123
141
  return recordFilters.length > 0 ? transformToEntries(results, recordFilters) : results;
@@ -148,6 +166,7 @@ export function executeConditions(
148
166
  context,
149
167
  minResults:
150
168
  request.limit === 0 ? 0 : request.limit !== undefined ? (request.offset || 0) + request.limit : undefined,
169
+ resultOffset: request.offset || 0,
151
170
  });
152
171
  }
153
172
  function mapConditionsToFilters(conditions, intersection, estimatedIncomingCount) {
@@ -193,12 +212,164 @@ function buildRecordGuards(recordAccess): ((record: any) => boolean)[] | undefin
193
212
  return guards.length > 0 ? guards : undefined;
194
213
  }
195
214
 
196
- /** True when a condition's index is a custom index that participates in predicate-aware traversal (HNSW). */
197
- function isFilterablePushdown(condition, table): boolean {
215
+ function conditionIndex(table: any, attributeName: any, preferFullText = false): any {
216
+ if (Array.isArray(attributeName) && attributeName.length === 1) attributeName = attributeName[0];
217
+ if (typeof attributeName !== 'string') return;
218
+ return preferFullText ? table.fullTextQueryIndexes?.[attributeName] : table.indices?.[attributeName];
219
+ }
220
+
221
+ function combineFullTextConditions(conditions: any[], operator: string | undefined, table: any): any[] {
222
+ if (table.hasFullTextQueryIndexes === false || !containsFullTextCondition(conditions, table)) return conditions;
223
+ let indexName: string | undefined;
224
+ let positive = false;
225
+ const expressions: any[] = [];
226
+ const leaves: FullTextCondition['fullTextLeaves'] = [];
227
+ const remaining: any[] = [];
228
+ let includeHighlights = false;
229
+ let maxIndexLagMilliseconds: number | undefined;
230
+ let waitForIndexMilliseconds: number | undefined;
231
+ for (const condition of conditions) {
232
+ const compiled = compileFullTextExpression(condition, table);
233
+ if (!compiled) {
234
+ remaining.push(condition);
235
+ continue;
236
+ }
237
+ if (operator === 'or' && compiled.remaining?.length)
238
+ throw new ClientError('A full-text OR group cannot mix full-text and record conditions', 400);
239
+ if (indexName && indexName !== compiled.indexName)
240
+ throw new ClientError('One query cannot combine conditions from different full-text indexes', 400);
241
+ indexName = compiled.indexName;
242
+ expressions.push(compiled.expression);
243
+ leaves.push(...compiled.leaves);
244
+ positive ||= compiled.positive;
245
+ includeHighlights ||= compiled.includeHighlights;
246
+ maxIndexLagMilliseconds = mergeFullTextOption(
247
+ 'maxIndexLagMilliseconds',
248
+ maxIndexLagMilliseconds,
249
+ compiled.maxIndexLagMilliseconds
250
+ );
251
+ waitForIndexMilliseconds = mergeFullTextOption(
252
+ 'waitForIndexMilliseconds',
253
+ waitForIndexMilliseconds,
254
+ compiled.waitForIndexMilliseconds
255
+ );
256
+ if (compiled.remaining) remaining.push(...compiled.remaining);
257
+ }
258
+ if (!indexName) return conditions;
259
+ if (operator === 'or' && remaining.length > 0)
260
+ throw new ClientError('A full-text OR group can only contain conditions on the same full-text index', 400);
261
+ if (!positive) throw new ClientError('A full-text query requires at least one non-negated condition', 400);
262
+ const expression =
263
+ expressions.length === 1 ? expressions[0] : { operator: operator === 'or' ? 'or' : 'and', clauses: expressions };
264
+ const combined = {
265
+ attribute: indexName,
266
+ comparator: 'matches',
267
+ value: '',
268
+ fullTextQuery: expression,
269
+ fullTextLeaves: leaves,
270
+ includeHighlights,
271
+ ...(maxIndexLagMilliseconds === undefined ? null : { maxIndexLagMilliseconds }),
272
+ ...(waitForIndexMilliseconds === undefined ? null : { waitForIndexMilliseconds }),
273
+ };
274
+ return operator === 'or' ? [combined] : [combined, ...remaining];
275
+ }
276
+
277
+ function containsFullTextCondition(conditions: any[], table: any): boolean {
278
+ for (const condition of conditions) {
279
+ if (condition.conditions) {
280
+ if (containsFullTextCondition(condition.conditions, table)) return true;
281
+ continue;
282
+ }
283
+ const indexName = condition[0] ?? condition.attribute;
284
+ if (
285
+ typeof indexName === 'string' &&
286
+ table.fullTextQueryIndexes?.[indexName] &&
287
+ fullTextComparatorMode(condition.comparator)
288
+ )
289
+ return true;
290
+ }
291
+ return false;
292
+ }
293
+
294
+ function compileFullTextExpression(
295
+ condition: any,
296
+ table: any
297
+ ):
298
+ | {
299
+ indexName: string;
300
+ expression: any;
301
+ leaves: NonNullable<FullTextCondition['fullTextLeaves']>;
302
+ positive: boolean;
303
+ includeHighlights: boolean;
304
+ maxIndexLagMilliseconds?: number;
305
+ waitForIndexMilliseconds?: number;
306
+ remaining?: any[];
307
+ }
308
+ | undefined {
309
+ if (condition.conditions) {
310
+ const children = condition.conditions.map((child) => compileFullTextExpression(child, table));
311
+ const fullTextChildren = children.filter(Boolean) as Array<NonNullable<(typeof children)[number]>>;
312
+ if (fullTextChildren.length === 0) return;
313
+ const groupOperator = condition.operator === 'or' ? 'or' : 'and';
314
+ const remaining = condition.conditions.filter((_child, index) => !children[index]);
315
+ for (const child of fullTextChildren) if (child.remaining) remaining.push(...child.remaining);
316
+ if (groupOperator === 'or' && remaining.length > 0)
317
+ throw new ClientError('A full-text OR group cannot mix full-text and record conditions', 400);
318
+ const indexName = fullTextChildren[0].indexName;
319
+ if (fullTextChildren.some((child) => child.indexName !== indexName))
320
+ throw new ClientError('One query cannot combine conditions from different full-text indexes', 400);
321
+ return {
322
+ indexName,
323
+ expression: { operator: groupOperator, clauses: fullTextChildren.map((child) => child.expression) },
324
+ leaves: fullTextChildren.flatMap((child) => child.leaves),
325
+ positive: fullTextChildren.some((child) => child.positive),
326
+ includeHighlights: fullTextChildren.some((child) => child.includeHighlights),
327
+ maxIndexLagMilliseconds: fullTextChildren.reduce(
328
+ (value, child) => mergeFullTextOption('maxIndexLagMilliseconds', value, child.maxIndexLagMilliseconds),
329
+ undefined as number | undefined
330
+ ),
331
+ waitForIndexMilliseconds: fullTextChildren.reduce(
332
+ (value, child) => mergeFullTextOption('waitForIndexMilliseconds', value, child.waitForIndexMilliseconds),
333
+ undefined as number | undefined
334
+ ),
335
+ ...(remaining.length > 0 ? { remaining } : null),
336
+ };
337
+ }
338
+ const indexName = condition[0] ?? condition.attribute;
339
+ if (typeof indexName !== 'string' || !table.fullTextQueryIndexes?.[indexName]) return;
340
+ const mode = fullTextComparatorMode(condition.comparator);
341
+ if (!mode) return;
342
+ if (typeof (condition[1] ?? condition.value) !== 'string')
343
+ throw new ClientError(`Full-text index '${indexName}' requires a full-text comparator and string value`, 400);
344
+ const text = condition[1] ?? condition.value;
345
+ const leaf = { text, mode, ...(condition.fields ? { fields: condition.fields } : null) };
346
+ return {
347
+ indexName,
348
+ expression: condition.negated ? { operator: 'not', clause: leaf } : leaf,
349
+ leaves: condition.negated ? [] : [leaf],
350
+ positive: !condition.negated,
351
+ includeHighlights: condition.includeHighlights === true,
352
+ maxIndexLagMilliseconds: condition.maxIndexLagMilliseconds,
353
+ waitForIndexMilliseconds: condition.waitForIndexMilliseconds,
354
+ };
355
+ }
356
+
357
+ function mergeFullTextOption(name: string, current: number | undefined, next: number | undefined): number | undefined {
358
+ if (next === undefined) return current;
359
+ if (current !== undefined && current !== next)
360
+ throw new ClientError(`Combined full-text conditions must use the same ${name}`, 400);
361
+ return next;
362
+ }
363
+
364
+ function filterablePushdownIndex(condition, table): any {
198
365
  const attributeName = condition?.attribute ?? condition?.[0];
199
- if (attributeName == null || table == null) return false;
200
- const index = attributeName === table.primaryKey ? table.primaryStore : table.indices?.[attributeName];
201
- return Boolean(index?.customIndex?.filteredSearch);
366
+ if (attributeName == null || table == null) return undefined;
367
+ const fullTextMode = Boolean(fullTextComparatorMode(condition?.comparator));
368
+ const index =
369
+ attributeName === table.primaryKey && !fullTextMode
370
+ ? table.primaryStore
371
+ : conditionIndex(table, attributeName, fullTextMode);
372
+ return index?.customIndex?.filteredSearch ? index.customIndex : undefined;
202
373
  }
203
374
 
204
375
  /**
@@ -207,14 +378,14 @@ function isFilterablePushdown(condition, table): boolean {
207
378
  * `primaryStore.getEntry` and frozen before the predicates see it. Verdicts are memoized per query since
208
379
  * the graph can reach a node from multiple neighbors. A missing/deleted record fails the predicate.
209
380
  */
210
- function composeRecordFilter(recordFilters, table, context): (primaryKey: Id) => boolean {
381
+ function composeRecordFilter(recordFilters, table, context): (primaryKey: Id, suppliedEntry?: any) => boolean {
211
382
  const memo = new Map<Id, boolean>();
212
383
  const transaction = context && table._readTxnForContext ? table._readTxnForContext(context) : undefined;
213
- return (primaryKey: Id) => {
384
+ return (primaryKey: Id, suppliedEntry?: any) => {
214
385
  const cached = memo.get(primaryKey);
215
386
  if (cached !== undefined) return cached;
216
387
  let verdict = false;
217
- const entry = table.primaryStore.getEntry(primaryKey, { transaction });
388
+ const entry = suppliedEntry ?? table.primaryStore.getEntry(primaryKey, { transaction });
218
389
  const record = entry?.value;
219
390
  if (record != null) {
220
391
  freezeRecord(record);
@@ -231,6 +402,279 @@ function composeRecordFilter(recordFilters, table, context): (primaryKey: Id) =>
231
402
  };
232
403
  }
233
404
 
405
+ /**
406
+ * The candidate-key plan a filterable custom index receives alongside its pushed-down predicate:
407
+ * the sibling AND conditions this query can answer from secondary-index range scans alone.
408
+ * The index admits from the keys instead of re-deriving the condition per visited node, so a
409
+ * condition on an indexed attribute costs a key-only index-entry read per matching record rather
410
+ * than a record load and decode per node the traversal reaches.
411
+ */
412
+ export interface CandidateKeyPlan {
413
+ /** Planner estimate of the matching record count; the index sizes its work budget from it. */
414
+ estimatedCount: number;
415
+ /**
416
+ * The matching primary keys, with `complete` true when the set alone decides admission, so the
417
+ * index may drop the traversal predicate. That needs every pushed-down condition covered, no
418
+ * opaque record guard (`rowFilter` / `vectorFilter`), and every term actually scanned — a term
419
+ * too wide for `maxKeys` is skipped rather than abandoning the whole set, leaving a narrower
420
+ * superset that gates the predicate instead of replacing it. Null when even the narrowest term
421
+ * overruns, which means the whole plan was not worth what it read.
422
+ */
423
+ collect(maxKeys: number): { keys: CandidateKeySet; complete: boolean } | null;
424
+ }
425
+
426
+ /**
427
+ * Primary keys by storage identity. Strings and non-zero numbers are their own identity, which spares
428
+ * encoding the common key shapes, and the stores encode -0 and 0 apart where Set membership does not.
429
+ * The rest go by writeKeyId, the encoder the stores index by — flattenKey joins composite keys with
430
+ * NUL and collapses ['a', 'b\0c'] and ['a\0b', 'c'] — kept apart from the values because an encoded
431
+ * identity is a string that can equal a string key: writeKeyId(['a', 'b']) is 'a\0b'. An index scan
432
+ * decodes an int64 as the number it encodes as when that is exact, while a traversal may report the
433
+ * BigInt, so the two are the same key.
434
+ */
435
+ export class CandidateKeySet {
436
+ #byValue = new Set<Id>();
437
+ #encoded = new Set<unknown>();
438
+ #zero = false;
439
+ #negativeZero = false;
440
+ #holdsBigInt = false;
441
+ add(primaryKey: Id): void {
442
+ if (typeof primaryKey === 'string' || (typeof primaryKey === 'number' && primaryKey !== 0))
443
+ this.#byValue.add(primaryKey);
444
+ else if (primaryKey === 0) {
445
+ if (Object.is(primaryKey, -0)) this.#negativeZero = true;
446
+ else this.#zero = true;
447
+ } else {
448
+ if (typeof primaryKey === 'bigint') this.#holdsBigInt = true;
449
+ this.#encoded.add(writeKeyId(primaryKey));
450
+ }
451
+ }
452
+ has(primaryKey: Id): boolean {
453
+ if (typeof primaryKey === 'string') return this.#byValue.has(primaryKey);
454
+ if (typeof primaryKey === 'number') {
455
+ if (primaryKey === 0) return Object.is(primaryKey, -0) ? this.#negativeZero : this.#zero;
456
+ return this.#byValue.has(primaryKey) || (this.#holdsBigInt && this.#encoded.has(writeKeyId(primaryKey)));
457
+ }
458
+ if (typeof primaryKey === 'bigint') {
459
+ const number = Number(primaryKey);
460
+ if (BigInt(number) === primaryKey && (number === 0 ? this.#zero : this.#byValue.has(number))) return true;
461
+ }
462
+ return this.#encoded.has(writeKeyId(primaryKey));
463
+ }
464
+ get size(): number {
465
+ return this.#byValue.size + this.#encoded.size + (this.#zero ? 1 : 0) + (this.#negativeZero ? 1 : 0);
466
+ }
467
+ }
468
+
469
+ interface CandidateKeyScan {
470
+ index: any;
471
+ range: any;
472
+ estimatedCount: number;
473
+ }
474
+ /** Scans whose UNION is one AND term; a single-element `scans` is a leaf condition. */
475
+ interface CandidateKeyTerm {
476
+ scans: CandidateKeyScan[];
477
+ estimatedCount: number;
478
+ }
479
+
480
+ // Comparators whose secondary-index range is EXACTLY the condition over the values an index holds,
481
+ // so scanning that range yields every indexed match and nothing else. `ne`, `in`, `contains`,
482
+ // `ends_with` and any negated condition are excluded because searchByIndex answers them with a scan
483
+ // plus a record filter.
484
+ const CANDIDATE_KEY_COMPARATORS = new Set([
485
+ 'equals',
486
+ 'lt',
487
+ 'le',
488
+ 'gt',
489
+ 'ge',
490
+ 'between',
491
+ 'gele',
492
+ 'gelt',
493
+ 'gtlt',
494
+ 'gtle',
495
+ ]);
496
+
497
+ /**
498
+ * Plan one leaf condition as an index range scan, or undefined when its matches cannot be read off
499
+ * an index exactly, or when the scan's size cannot be estimated.
500
+ */
501
+ function planCandidateKeyScan(condition, table): CandidateKeyScan | undefined {
502
+ if (condition.negated) return undefined;
503
+ const attributeName = condition.attribute ?? condition[0];
504
+ if (typeof attributeName !== 'string' || attributeName === table.primaryKey) return undefined;
505
+ const index = usableIndex(table, attributeName);
506
+ if (!index || index.customIndex) return undefined;
507
+ const comparator = ALTERNATE_COMPARATOR_NAMES[condition.comparator] ?? condition.comparator ?? 'equals';
508
+ if (!CANDIDATE_KEY_COMPARATORS.has(comparator)) return undefined;
509
+ // The scan has to hold every key the record predicate admits, or results would depend on whether
510
+ // the plan was built. The index cannot supply a record lacking the attribute, a null one unless it
511
+ // indexes nulls, or one written as -0: ordered-binary encodes -0 past every number, and that entry's
512
+ // primary key decodes wrong. compareKeys ranks undefined below every scalar, so `rank < 100` admits
513
+ // a record with no rank at all; a condition admitting any of these is left to the predicate.
514
+ const predicate = filterByType(condition, table, undefined, null);
515
+ if (predicate({}) || predicate({ [attributeName]: 0 }) || (!index.indexNulls && predicate({ [attributeName]: null })))
516
+ return undefined;
517
+ let value = condition[1] ?? condition.value;
518
+ if (value instanceof Date) value = value.getTime();
519
+ let start;
520
+ let end;
521
+ let inclusiveEnd = false;
522
+ let exclusiveStart = false;
523
+ switch (comparator) {
524
+ case 'lt':
525
+ // null, not searchByIndex's `true`: `true` sorts ABOVE null and false, so starting there
526
+ // would drop indexed rows the record predicate's compareKeys admits — an omission the
527
+ // post-filter cannot undo.
528
+ start = null;
529
+ end = value;
530
+ break;
531
+ case 'le':
532
+ start = null;
533
+ end = value;
534
+ inclusiveEnd = true;
535
+ break;
536
+ case 'gt':
537
+ start = value;
538
+ exclusiveStart = true;
539
+ break;
540
+ case 'ge':
541
+ start = value;
542
+ break;
543
+ case 'between':
544
+ case 'gele':
545
+ case 'gelt':
546
+ case 'gtlt':
547
+ case 'gtle':
548
+ if (!Array.isArray(value)) return undefined;
549
+ start = value[0] instanceof Date ? value[0].getTime() : value[0];
550
+ end = value[1] instanceof Date ? value[1].getTime() : value[1];
551
+ inclusiveEnd = comparator === 'gele' || comparator === 'gtle' || comparator === 'between';
552
+ exclusiveStart = comparator === 'gtlt' || comparator === 'gtle';
553
+ break;
554
+ default:
555
+ start = value;
556
+ end = value;
557
+ inclusiveEnd = true;
558
+ }
559
+ // ordered-binary encodes -0 above every number, where the predicate's compareKeys calls it 0.
560
+ if (start === 0) start = 0;
561
+ if (end === 0) end = 0;
562
+ // An over-length string key is stored truncated behind an overflow marker, which makes the range
563
+ // wider than the condition and needs a record filter to narrow again.
564
+ if (
565
+ (typeof start === 'string' && start.length > MAX_SEARCH_KEY_LENGTH) ||
566
+ (typeof end === 'string' && end.length > MAX_SEARCH_KEY_LENGTH)
567
+ )
568
+ return undefined;
569
+ if (condition.estimated_count === undefined) estimateCondition(table)(condition);
570
+ let estimatedCount = condition.estimated_count;
571
+ if (!(estimatedCount >= 0) || !Number.isFinite(estimatedCount)) return undefined;
572
+ // estimateRangeCondition measures the range searchByIndex would run, which starts at `true`. Only
573
+ // lt/le widen below that, and the block they add is read FIRST and can dwarf the rest of the
574
+ // range, so it has to be counted or the scan overruns a budget sized without it. A statistical
575
+ // estimate, never getValuesCount: that one steps the whole block, which is the unbounded
576
+ // per-query cost this is here to avoid. The other comparators' own estimates already cover
577
+ // whatever they start at.
578
+ if (comparator === 'lt' || comparator === 'le') {
579
+ let belowTrue: number | undefined;
580
+ try {
581
+ belowTrue = index.estimateCount?.({ start: null, end: true })?.count;
582
+ } catch {
583
+ return undefined;
584
+ }
585
+ if (!(belowTrue >= 0)) return undefined;
586
+ estimatedCount += belowTrue;
587
+ }
588
+ return { index, range: { start, end, inclusiveEnd, exclusiveStart }, estimatedCount };
589
+ }
590
+
591
+ function planCandidateKeys(conditions, table, transaction, guardFree: boolean): CandidateKeyPlan | undefined {
592
+ const terms: CandidateKeyTerm[] = [];
593
+ const planned = collectCandidateKeyTerms(conditions, table, terms) && guardFree;
594
+ if (terms.length === 0) return undefined;
595
+ const estimatedCount = terms.reduce((narrowest, term) => Math.min(narrowest, term.estimatedCount), Infinity);
596
+ return {
597
+ estimatedCount,
598
+ collect: (maxKeys) => collectCandidateKeys(terms, transaction, maxKeys, planned),
599
+ };
600
+ }
601
+
602
+ /** Returns whether every condition was covered. */
603
+ function collectCandidateKeyTerms(conditions, table, terms: CandidateKeyTerm[]): boolean {
604
+ let complete = true;
605
+ for (const condition of conditions) {
606
+ if (condition.conditions) {
607
+ if (condition.operator === 'or') {
608
+ // A union is only sound in full: dropping one branch would exclude the records only
609
+ // that branch matches, and admission may over-admit but never omit.
610
+ const scans: CandidateKeyScan[] = [];
611
+ for (const child of condition.conditions) {
612
+ const scan = child.conditions ? undefined : planCandidateKeyScan(child, table);
613
+ if (!scan) {
614
+ complete = false;
615
+ break;
616
+ }
617
+ scans.push(scan);
618
+ }
619
+ if (scans.length === condition.conditions.length) {
620
+ terms.push({ scans, estimatedCount: scans.reduce((total, scan) => total + scan.estimatedCount, 0) });
621
+ }
622
+ } else if (!collectCandidateKeyTerms(condition.conditions, table, terms)) complete = false;
623
+ continue;
624
+ }
625
+ const scan = planCandidateKeyScan(condition, table);
626
+ if (scan) terms.push({ scans: [scan], estimatedCount: scan.estimatedCount });
627
+ else complete = false;
628
+ }
629
+ return complete;
630
+ }
631
+
632
+ /**
633
+ * Run the planned scans narrowest-estimate first, intersecting on storage key identity. `maxKeys`
634
+ * bounds index entries READ, not keys retained — a wide later scan whose intersection is tiny still
635
+ * reads its whole range, and that is the cost the caller budgeted against. A term whose estimate
636
+ * exceeds what is left of that budget is dropped instead: fewer AND terms is a wider superset, which
637
+ * the predicate then narrows, and spending the budget on it only to abandon the set is pure loss.
638
+ */
639
+ function collectCandidateKeys(
640
+ terms: CandidateKeyTerm[],
641
+ transaction,
642
+ maxKeys: number,
643
+ planned: boolean
644
+ ): { keys: CandidateKeySet; complete: boolean } | null {
645
+ const ordered = terms.length > 1 ? terms.slice().sort((a, b) => a.estimatedCount - b.estimatedCount) : terms;
646
+ let matched: CandidateKeySet | undefined;
647
+ let complete = planned;
648
+ let scanned = 0;
649
+ for (const term of ordered) {
650
+ if (matched && term.estimatedCount > maxKeys - scanned) {
651
+ complete = false;
652
+ continue;
653
+ }
654
+ const next = new CandidateKeySet();
655
+ let overran = false;
656
+ for (const scan of term.scans) {
657
+ for (const { value: primaryKey } of scan.index.getRange({ ...scan.range, values: true, transaction })) {
658
+ if (++scanned > maxKeys) {
659
+ overran = true;
660
+ break;
661
+ }
662
+ if (!matched || matched.has(primaryKey)) next.add(primaryKey);
663
+ }
664
+ if (overran) break;
665
+ }
666
+ // A half-scanned term is not a superset of its own matches, so it is dropped whole.
667
+ if (overran) {
668
+ if (!matched) return null;
669
+ complete = false;
670
+ break;
671
+ }
672
+ matched = next;
673
+ if (matched.size === 0) break;
674
+ }
675
+ return { keys: matched!, complete };
676
+ }
677
+
234
678
  /**
235
679
  * A record's index entries are not adjacent — the composite `[indexedValue, primaryKey]` key sorts
236
680
  * on the indexed value first — so comparing neighbours cannot collapse them and the scan has to
@@ -289,16 +733,22 @@ export function searchByIndex(
289
733
  // approximate index returns a fixed-size candidate list, so without this a query asking for more
290
734
  // rows than that list holds silently gets a short result set. Only custom indexes read it.
291
735
  minResults?: number;
736
+ // Rows before this offset are needed for correct paging but do not need highlight metadata.
737
+ resultOffset?: number;
292
738
  } = {}
293
739
  ): AsyncIterable<Id | { key: Id; value: any }> {
294
740
  // A stale positional caller passes `allowFullScan` here. Type checking only covers .ts callers, so
295
741
  // fail loud rather than silently reading every option as undefined.
296
742
  if (typeof options !== 'object' || options === null)
297
743
  throw new TypeError('searchByIndex: the 5th argument is an options object (#2165), not a positional value');
298
- const { allowFullScan, filtered, context, minResults } = options;
744
+ const { allowFullScan, filtered, context, minResults, resultOffset } = options;
299
745
  let attribute_name = searchCondition[0] ?? searchCondition.attribute;
300
746
  let value = searchCondition[1] ?? searchCondition.value;
301
747
  const comparator = searchCondition.comparator;
748
+ const fullTextMode = fullTextComparatorMode(comparator);
749
+ if (fullTextMode && typeof attribute_name !== 'string')
750
+ throw new ClientError('Full-text comparator requires an index directly on the queried table', 400);
751
+ if (!fullTextMode) Table.assertFullTextRecordField?.(attribute_name);
302
752
  if (value === undefined && (comparator as any) !== 'sort') {
303
753
  throw new ClientError(`Search condition for ${attribute_name} must have a value`);
304
754
  }
@@ -368,8 +818,14 @@ export function searchByIndex(
368
818
  needFullScan = true;
369
819
  }
370
820
  }
371
- const isPrimaryKey = attribute_name === Table.primaryKey || attribute_name == null;
372
- const index = isPrimaryKey ? Table.primaryStore : Table.indices[attribute_name];
821
+ if (fullTextMode) {
822
+ if (typeof attribute_name !== 'string' || !Table.fullTextIndexes?.some(({ name }) => name === attribute_name))
823
+ throw new ClientError('Full-text comparator requires a declared @fullText index', 400);
824
+ if (!Table.fullTextQueryIndexes?.[attribute_name]?.customIndex)
825
+ throw new IndexRebuildingError(`Full-text index '${attribute_name}' is not ready`);
826
+ }
827
+ const isPrimaryKey = !fullTextMode && (attribute_name === Table.primaryKey || attribute_name == null);
828
+ const index = isPrimaryKey ? Table.primaryStore : conditionIndex(Table, attribute_name, Boolean(fullTextMode));
373
829
  let start;
374
830
  let end, inclusiveEnd, exclusiveStart, stringPrefix;
375
831
  if (value instanceof Date) value = value.getTime();
@@ -379,6 +835,8 @@ export function searchByIndex(
379
835
  // the bounded index iteration would only visit *included* rows.
380
836
  start = true;
381
837
  needFullScan = true;
838
+ } else if (fullTextMode) {
839
+ start = true;
382
840
  } else
383
841
  switch (ALTERNATE_COMPARATOR_NAMES[comparator] || comparator) {
384
842
  case 'lt':
@@ -554,6 +1012,7 @@ export function searchByIndex(
554
1012
  // exploring until it has enough MATCHING results, rather than post-filtering an under-filled
555
1013
  // candidate set. Only indexes that opt in (filteredSearch) receive it; others post-filter as before.
556
1014
  const recordFilter = index.customIndex.filteredSearch ? searchCondition.recordFilter : undefined;
1015
+ const candidateKeys = index.customIndex.candidateKeyFilter ? (searchCondition as any).candidateKeys : undefined;
557
1016
  const waiting = index.customIndex.filePrimary && searchCondition.waitForIndexMilliseconds > 0;
558
1017
  const start = waiting ? Promise.withResolvers<void>() : undefined;
559
1018
  const controller = waiting ? new AbortController() : undefined;
@@ -563,22 +1022,27 @@ export function searchByIndex(
563
1022
  ? AbortSignal.any([context.signal, controller.signal])
564
1023
  : controller.signal
565
1024
  : undefined;
1025
+ const indexSearchStart = waiting ? start.promise.then(() => checkActive?.()) : undefined;
1026
+ indexSearchStart?.catch(() => {});
566
1027
  const searchContext = waiting
567
1028
  ? Object.create(context, {
568
1029
  signal: { value: signal },
569
- indexSearchStart: { value: start.promise.then(() => checkActive?.()) },
1030
+ indexSearchStart: { value: indexSearchStart },
570
1031
  })
571
1032
  : context;
572
1033
  const searched = index.customIndex.search(searchCondition, searchContext, {
573
1034
  filter:
574
1035
  waiting && recordFilter
575
- ? (id) => {
1036
+ ? (id, entry) => {
576
1037
  if (signal.aborted) return false;
577
1038
  checkActive?.();
578
- return recordFilter(id);
1039
+ return recordFilter(id, entry);
579
1040
  }
580
1041
  : recordFilter,
581
1042
  minResults,
1043
+ resultOffset,
1044
+ assertTransactionActive: checkActive,
1045
+ candidateKeys,
582
1046
  });
583
1047
  const coverage = (searched as any).indexCoverage;
584
1048
  if (!waiting && coverage && context?.responseHeaders) {
@@ -595,11 +1059,13 @@ export function searchByIndex(
595
1059
  .map((entry) => {
596
1060
  // if the custom index returns an entry with metadata, merge it with the loaded entry
597
1061
  if (typeof entry === 'object' && entry) {
598
- const { key, ...otherProps } = entry;
1062
+ const { key, loadedEntry: suppliedEntry, ...otherProps } = entry;
599
1063
  if (key == null) return SKIP; // primaryKey missing from HNSW node — skip rather than crash
600
- const loadedEntry = Table.primaryStore.getEntry(key, {
601
- transaction: waiting ? transaction : context && Table._readTxnForContext(context),
602
- });
1064
+ const loadedEntry =
1065
+ suppliedEntry ??
1066
+ Table.primaryStore.getEntry(key, {
1067
+ transaction: waiting ? transaction : context && Table._readTxnForContext(context),
1068
+ });
603
1069
  if (!loadedEntry) return SKIP; // record was deleted/expired or not yet visible
604
1070
  freezeRecord(loadedEntry?.value);
605
1071
  recordRead(loadedEntry);
@@ -653,7 +1119,10 @@ export function searchByIndex(
653
1119
  },
654
1120
  return(value?: any) {
655
1121
  closed = true;
656
- if (!settled) controller?.abort();
1122
+ if (!settled) {
1123
+ start?.resolve();
1124
+ controller?.abort();
1125
+ }
657
1126
  iteratorPromise.then(
658
1127
  (inner) => (inner as any).return?.(value),
659
1128
  () => {}
@@ -919,7 +1388,15 @@ const LIST_VALUE_COMPARATORS = new Set(['in', 'between']);
919
1388
 
920
1389
  // Base comparators that accept the `not_` prefix to produce a negated form.
921
1390
  // `not_equal` is an existing alias for `ne` and keeps its existing semantics.
922
- const NEGATABLE_BASE_COMPARATORS = new Set(['in', 'between', 'starts_with', 'ends_with', 'contains', 'equals']);
1391
+ const NEGATABLE_BASE_COMPARATORS = new Set([
1392
+ 'in',
1393
+ 'between',
1394
+ 'starts_with',
1395
+ 'ends_with',
1396
+ 'contains',
1397
+ 'equals',
1398
+ ...FULL_TEXT_POSITIVE_COMPARATORS,
1399
+ ]);
923
1400
 
924
1401
  /**
925
1402
  * Resolve a comparator name to a (possibly stripped) base comparator and a
@@ -1342,7 +1819,7 @@ function estimateRangeCondition(table, condition, searchType, fraction) {
1342
1819
 
1343
1820
  /** The index a condition can be driven by; searchByIndex refuses a rebuilding one, so it reads as absent. */
1344
1821
  function usableIndex(table, attributeName): any {
1345
- const index = attributeName == null ? undefined : table.indices[attributeName];
1822
+ const index = attributeName == null ? undefined : conditionIndex(table, attributeName);
1346
1823
  return index?.isIndexing ? undefined : index;
1347
1824
  }
1348
1825
 
@@ -1352,7 +1829,9 @@ function usableIndex(table, attributeName): any {
1352
1829
  */
1353
1830
  function drivesRebuildingIndex(table, attributeName, relationshipOffset = 0): boolean {
1354
1831
  if (!Array.isArray(attributeName))
1355
- return attributeName != null && attributeName !== table.primaryKey && !!table.indices[attributeName]?.isIndexing;
1832
+ return (
1833
+ attributeName != null && attributeName !== table.primaryKey && !!conditionIndex(table, attributeName)?.isIndexing
1834
+ );
1356
1835
  if (relationshipOffset >= attributeName.length - 1)
1357
1836
  return drivesRebuildingIndex(table, attributeName[relationshipOffset]);
1358
1837
  const attribute = findAttribute(table.attributes, attributeName[relationshipOffset]);
@@ -1430,6 +1909,12 @@ export function estimateCondition(table) {
1430
1909
  const index = usableIndex(table, attribute_name);
1431
1910
  condition.estimated_count = index ? index.getValuesCount(condition[1] ?? condition.value) : Infinity;
1432
1911
  }
1912
+ } else if (fullTextComparatorMode(searchType)) {
1913
+ const attributeName = condition[0] ?? condition.attribute;
1914
+ const index = conditionIndex(table, attributeName, true);
1915
+ condition.estimated_count = index?.customIndex?.estimateCount
1916
+ ? index.customIndex.estimateCount(condition.value)
1917
+ : Infinity;
1433
1918
  } else if (searchType === 'contains' || searchType === 'ends_with' || searchType === 'ne') {
1434
1919
  const attribute_name = condition[0] ?? condition.attribute;
1435
1920
  const index = usableIndex(table, attribute_name);
@@ -1557,7 +2042,9 @@ function parseBlock(query, expectedEnd) {
1557
2042
  // caught at execution time with a clearer error.
1558
2043
  if (FIQL_OPERATOR_NAME.test(value)) comparator = value;
1559
2044
  else recordError(`invalid FIQL operator ${value}`);
1560
- valueDecoder = typedDecoding; // use typed/auto-cast decoding for FIQL operators
2045
+ valueDecoder = fullTextComparatorMode(resolveComparator(comparator).comparator)
2046
+ ? decodeURIComponent
2047
+ : typedDecoding;
1561
2048
  } else {
1562
2049
  // standard equal comparison
1563
2050
  valueDecoder = decodeURIComponent; // use strict decoding