@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.
- package/bin/backup.ts +6 -1
- package/components/Application.ts +486 -115
- package/components/DESIGN.md +109 -21
- package/components/deploymentProvenance.ts +28 -0
- package/components/operations.js +14 -7
- package/components/packageComponent.ts +5 -1
- package/config-root.schema.json +1 -1
- package/dataLayer/DESIGN.md +35 -7
- package/dataLayer/blobBackup.ts +71 -30
- package/dataLayer/harperBridge/ResourceBridge.ts +16 -2
- package/dataLayer/rocksdbBackup.ts +88 -34
- package/dataLayer/schemaDescribe.ts +45 -1
- package/dist/bin/backup.js +1 -1
- package/dist/bin/backup.js.map +1 -1
- package/dist/components/Application.d.ts +11 -21
- package/dist/components/Application.js +436 -105
- package/dist/components/Application.js.map +1 -1
- package/dist/components/deploymentProvenance.d.ts +4 -0
- package/dist/components/deploymentProvenance.js +32 -0
- package/dist/components/deploymentProvenance.js.map +1 -0
- package/dist/components/operations.js +14 -7
- package/dist/components/operations.js.map +1 -1
- package/dist/components/packageComponent.js +6 -1
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/dataLayer/blobBackup.d.ts +30 -3
- package/dist/dataLayer/blobBackup.js +73 -36
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +15 -3
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +4 -1
- package/dist/dataLayer/rocksdbBackup.js +74 -36
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +45 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +1 -0
- package/dist/resources/DatabaseTransaction.js +33 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +7 -3
- package/dist/resources/ResourceInterface.js +2 -0
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Table.d.ts +151 -445
- package/dist/resources/Table.js +209 -8
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +11 -0
- package/dist/resources/analytics/write.js +3 -0
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +30 -0
- package/dist/resources/blob.js +189 -1
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +80 -15
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +13 -0
- package/dist/resources/derivedIndexRuntime.js +57 -5
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +1 -0
- package/dist/resources/derivedIndexes.js +116 -15
- package/dist/resources/derivedIndexes.js.map +1 -1
- package/dist/resources/fullTextSchema.d.ts +5 -3
- package/dist/resources/fullTextSchema.js +48 -5
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +2 -0
- package/dist/resources/fullTextSchemaLifecycle.js +33 -0
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +30 -12
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +21 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +114 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +11 -2
- package/dist/resources/indexes/fullTextDerivedIndex.js +187 -24
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +99 -1
- package/dist/resources/indexes/fullTextNativeBinding.js +35 -1
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +58 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +943 -0
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextQueryProtocol.d.ts +6 -0
- package/dist/resources/indexes/fullTextQueryProtocol.js +32 -0
- package/dist/resources/indexes/fullTextQueryProtocol.js.map +1 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +4 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +30 -7
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/search.d.ts +41 -0
- package/dist/resources/search.js +468 -26
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transactionBroadcast.js +101 -50
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/resources/typeParity.d.ts +9 -0
- package/dist/resources/typeParity.js +3 -0
- package/dist/resources/typeParity.js.map +1 -0
- package/dist/server/REST.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +48 -8
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +7 -0
- package/dist/server/serverHelpers/contentTypes.js +36 -23
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +28 -5
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/itc.js +19 -0
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -0
- package/dist/server/threads/manageThreads.js +90 -23
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/utility/operation_authorization.js +25 -7
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/signalling.js +3 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/searchValidator.js +22 -2
- package/dist/validation/searchValidator.js.map +1 -1
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -4
- package/resources/DESIGN.md +19 -2
- package/resources/DatabaseTransaction.ts +40 -10
- package/resources/FULL_TEXT.md +97 -0
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Table.ts +767 -17
- package/resources/analytics/write.ts +3 -3
- package/resources/blob.ts +183 -1
- package/resources/databases.ts +126 -14
- package/resources/derivedIndexRuntime.ts +79 -6
- package/resources/derivedIndexes.ts +142 -18
- package/resources/fullTextSchema.ts +55 -9
- package/resources/fullTextSchemaLifecycle.ts +45 -0
- package/resources/graphql.ts +33 -12
- package/resources/indexes/DESIGN.md +167 -15
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +125 -5
- package/resources/indexes/fullTextDerivedIndex.ts +216 -32
- package/resources/indexes/fullTextNativeBinding.ts +133 -3
- package/resources/indexes/fullTextQueryIndex.ts +1120 -0
- package/resources/indexes/fullTextQueryProtocol.ts +35 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +28 -8
- package/resources/search.ts +513 -26
- package/resources/transactionBroadcast.ts +86 -47
- package/resources/typeParity.ts +20 -0
- package/schema.graphql +18 -5
- package/server/DESIGN.md +18 -0
- package/server/REST.ts +1 -1
- package/server/itc/serverHandlers.js +61 -8
- package/server/serverHelpers/contentTypes.ts +31 -26
- package/server/serverHelpers/uwsServer.ts +35 -7
- package/server/threads/itc.js +21 -0
- package/server/threads/manageThreads.js +95 -21
- package/studio/web/assets/{Chat-Bq60AO-g.js → Chat-CNEKMllJ.js} +13 -13
- package/studio/web/assets/FloatingChat-CEnWhHeR.js +23 -0
- package/studio/web/assets/SearchDialog-CwhJdEdY.js +1 -0
- package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
- package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
- package/studio/web/assets/apiToken-DC5vsaaO.js +1 -0
- package/studio/web/assets/applications-DQZxDy0x.js +296 -0
- package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
- package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
- package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
- package/studio/web/assets/authStore-CJCaADw9.js +3 -0
- package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
- package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
- package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
- package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
- package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
- package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
- package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
- package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
- package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
- package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
- package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
- package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
- package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
- package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
- package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
- package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
- package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
- package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
- package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
- package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
- package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
- package/studio/web/assets/cn-6T8VranJ.js +1 -0
- package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
- package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
- package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
- package/studio/web/assets/{cssMode-DJyc9mHj.js → cssMode-Dz2DxF4e.js} +1 -1
- package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
- package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
- package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
- package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
- package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
- package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
- package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
- package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
- package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
- package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
- package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
- package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
- package/studio/web/assets/{editor-DDIsQfj6.js → editor-B3mjd3tz.js} +1 -1
- package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
- package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
- package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
- package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
- package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
- package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
- package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
- package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
- package/studio/web/assets/{html-g1BJHxLy.js → html-BsRGM7tp.js} +1 -1
- package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
- package/studio/web/assets/{htmlMode-Vh5IglCQ.js → htmlMode-tUKJaA3o.js} +1 -1
- package/studio/web/assets/index-72cEH474.css +1 -0
- package/studio/web/assets/index-BuqFjN5K.js +828 -0
- package/studio/web/assets/index.lazy-CcpfQFY_.js +2 -0
- package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
- package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
- package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
- package/studio/web/assets/{javascript-5VIolqf7.js → javascript-BZ_uRGHl.js} +1 -1
- package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
- package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
- package/studio/web/assets/{jsonMode-DG4zQraI.js → jsonMode-DgB-aFMj.js} +1 -1
- package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
- package/studio/web/assets/{languageServices-t5FBpX-i.js → languageServices-BleuBw17.js} +1 -1
- package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
- package/studio/web/assets/{lspLanguageFeatures-DgVnNPh-.js → lspLanguageFeatures-ByVYLSsQ.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
- package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
- package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
- package/studio/web/assets/notifications-D1Nf-Edn.js +1 -0
- package/studio/web/assets/notifications-TuzN0IMi.js +1 -0
- package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
- package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
- package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
- package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
- package/studio/web/assets/profile-D7C7Lupz.js +1 -0
- package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
- package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
- package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
- package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
- package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
- package/studio/web/assets/regions-nGQCm-v8.js +1 -0
- package/studio/web/assets/register-C3x9Ze6P.js +909 -0
- package/studio/web/assets/register-DjpsE8to.css +1 -0
- package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
- package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
- package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
- package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
- package/studio/web/assets/{setComponentFile-z4-qJCbA.js → setComponentFile-DpjrRqeG.js} +1 -1
- package/studio/web/assets/{setup-CS50uJ4x.js → setup-CGiKLZ9e.js} +2 -2
- package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
- package/studio/web/assets/status-CGJwrNPH.js +61 -0
- package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
- package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
- package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
- package/studio/web/assets/toggleHighContrast-C-Ne6P7T.js +51 -0
- package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
- package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
- package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
- package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
- package/studio/web/assets/{tsMode-D83Gg3cq.js → tsMode-CSreYTys.js} +1 -1
- package/studio/web/assets/{typescript-Dfp-GrNq.js → typescript-MnxBk89W.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BKetlINf.js → useEntityRestURL-TNrh77-s.js} +1 -1
- package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
- package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
- package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
- package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
- package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
- package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
- package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
- package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
- package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
- package/studio/web/assets/{workers-CCvmDLH3.js → workers-BfZlUNVI.js} +3 -3
- package/studio/web/assets/{xml-06pZp00U.js → xml-BHd6qGH7.js} +1 -1
- package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
- package/studio/web/assets/{yaml-iSnWMVlV.js → yaml-C53R8OQq.js} +1 -1
- package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
- package/studio/web/index.html +15 -15
- package/utility/operation_authorization.ts +27 -7
- package/utility/signalling.ts +3 -0
- package/validation/searchValidator.ts +22 -13
- package/studio/web/assets/FloatingChat-D4560T0p.js +0 -23
- package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
- package/studio/web/assets/apiToken-BEvbFPMc.js +0 -1
- package/studio/web/assets/applications-CSUVTH4E.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
- package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
- package/studio/web/assets/channel-BDtouPd6.js +0 -1
- package/studio/web/assets/dialog-DTepachC.js +0 -1
- package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
- package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
- package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
- package/studio/web/assets/index-CPoDzQTe.css +0 -1
- package/studio/web/assets/index-DGRAzvO4.js +0 -824
- package/studio/web/assets/index.lazy-vgSic7et.js +0 -2
- package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
- package/studio/web/assets/notifications-BeWcX04m.js +0 -1
- package/studio/web/assets/notifications-DUGWrWoO.js +0 -1
- package/studio/web/assets/profile-BvUEDBzW.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
- package/studio/web/assets/regions-CB-_2m7M.js +0 -1
- package/studio/web/assets/register-1w1Zr3b2.js +0 -848
- package/studio/web/assets/register-jjEx9u7D.css +0 -1
- package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
- package/studio/web/assets/status-CI0wOdOE.js +0 -61
- package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
- package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
- package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
- package/studio/web/assets/toggleHighContrast-BsAE4jPe.js +0 -49
- package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
- package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
- package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
- package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
- package/studio/web/assets/x-vYntCxm4.js +0 -1
package/resources/search.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
200
|
-
const
|
|
201
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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:
|
|
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 =
|
|
601
|
-
|
|
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)
|
|
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([
|
|
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
|
|
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
|
|
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 =
|
|
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
|