@noy-db/hub 0.1.0-pre.9 → 0.2.0-pre.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/aggregate/index.cjs +100 -36
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +2 -2
  4. package/dist/aggregate/index.d.ts +2 -2
  5. package/dist/aggregate/index.js +16 -9
  6. package/dist/aggregate/index.js.map +1 -1
  7. package/dist/attestation/index.cjs +305 -0
  8. package/dist/attestation/index.cjs.map +1 -0
  9. package/dist/attestation/index.d.cts +52 -0
  10. package/dist/attestation/index.d.ts +52 -0
  11. package/dist/attestation/index.js +36 -0
  12. package/dist/attestation/index.js.map +1 -0
  13. package/dist/blobs/index.cjs.map +1 -1
  14. package/dist/blobs/index.d.cts +7 -6
  15. package/dist/blobs/index.d.ts +7 -6
  16. package/dist/blobs/index.js +10 -8
  17. package/dist/blobs/index.js.map +1 -1
  18. package/dist/bundle/index.cjs +19121 -60
  19. package/dist/bundle/index.cjs.map +1 -1
  20. package/dist/bundle/index.d.cts +175 -6
  21. package/dist/bundle/index.d.ts +175 -6
  22. package/dist/bundle/index.js +543 -4
  23. package/dist/bundle/index.js.map +1 -1
  24. package/dist/chunk-26NK23DZ.js +296 -0
  25. package/dist/chunk-26NK23DZ.js.map +1 -0
  26. package/dist/{chunk-TDR6T5CJ.js → chunk-2LPPNWF6.js} +91 -132
  27. package/dist/chunk-2LPPNWF6.js.map +1 -0
  28. package/dist/{chunk-PTVMYYON.js → chunk-2N62W5YP.js} +3 -3
  29. package/dist/{chunk-QGZRWRSL.js → chunk-3LPV6BXR.js} +4 -4
  30. package/dist/{chunk-QAVUREFT.js → chunk-4CLICFEY.js} +12 -6
  31. package/dist/chunk-4CLICFEY.js.map +1 -0
  32. package/dist/chunk-4USCAEDT.js +10529 -0
  33. package/dist/chunk-4USCAEDT.js.map +1 -0
  34. package/dist/chunk-5IXJGFF2.js +83 -0
  35. package/dist/chunk-5IXJGFF2.js.map +1 -0
  36. package/dist/chunk-5OEJ6GOT.js +124 -0
  37. package/dist/chunk-5OEJ6GOT.js.map +1 -0
  38. package/dist/{chunk-4PWAI7Q4.js → chunk-5OX6XVNS.js} +5 -5
  39. package/dist/{chunk-2CSJGFCB.js → chunk-6EOXTJS2.js} +6 -229
  40. package/dist/chunk-6EOXTJS2.js.map +1 -0
  41. package/dist/chunk-6T2UDBKG.js +53 -0
  42. package/dist/chunk-6T2UDBKG.js.map +1 -0
  43. package/dist/{chunk-GOUT6DND.js → chunk-6YLPHBKR.js} +382 -95
  44. package/dist/chunk-6YLPHBKR.js.map +1 -0
  45. package/dist/chunk-7CEGU63S.js +179 -0
  46. package/dist/chunk-7CEGU63S.js.map +1 -0
  47. package/dist/chunk-A3JMGXPG.js +125 -0
  48. package/dist/chunk-A3JMGXPG.js.map +1 -0
  49. package/dist/chunk-BB27JMWB.js +795 -0
  50. package/dist/chunk-BB27JMWB.js.map +1 -0
  51. package/dist/{chunk-SCZXXXU4.js → chunk-BDV7INMP.js} +7 -32
  52. package/dist/chunk-BDV7INMP.js.map +1 -0
  53. package/dist/chunk-C3WE6UJY.js +19 -0
  54. package/dist/chunk-C3WE6UJY.js.map +1 -0
  55. package/dist/chunk-CH22FZHT.js +96 -0
  56. package/dist/chunk-CH22FZHT.js.map +1 -0
  57. package/dist/chunk-CXFOITNS.js +34 -0
  58. package/dist/chunk-CXFOITNS.js.map +1 -0
  59. package/dist/chunk-CXJG63MA.js +109 -0
  60. package/dist/chunk-CXJG63MA.js.map +1 -0
  61. package/dist/chunk-DAP2XL7Q.js +51 -0
  62. package/dist/chunk-DAP2XL7Q.js.map +1 -0
  63. package/dist/{chunk-AVVPZ4BC.js → chunk-DJRWA3Q5.js} +4 -4
  64. package/dist/chunk-DRXIZOFV.js +233 -0
  65. package/dist/chunk-DRXIZOFV.js.map +1 -0
  66. package/dist/chunk-FO3UEG4S.js +313 -0
  67. package/dist/chunk-FO3UEG4S.js.map +1 -0
  68. package/dist/chunk-GAUEWM7D.js +147 -0
  69. package/dist/chunk-GAUEWM7D.js.map +1 -0
  70. package/dist/{chunk-MDDTIZUO.js → chunk-GNHAC43Q.js} +218 -119
  71. package/dist/chunk-GNHAC43Q.js.map +1 -0
  72. package/dist/chunk-HHOO7HGH.js +57 -0
  73. package/dist/chunk-HHOO7HGH.js.map +1 -0
  74. package/dist/{chunk-WDM5XGGS.js → chunk-HQSQC2XL.js} +182 -12
  75. package/dist/chunk-HQSQC2XL.js.map +1 -0
  76. package/dist/chunk-IMYKDWB4.js +139 -0
  77. package/dist/chunk-IMYKDWB4.js.map +1 -0
  78. package/dist/{chunk-M62XNWRA.js → chunk-LSTBFLL2.js} +2 -2
  79. package/dist/{chunk-ACLDOTNQ.js → chunk-O6EJ6WTI.js} +436 -3
  80. package/dist/chunk-O6EJ6WTI.js.map +1 -0
  81. package/dist/chunk-PC6ZEDRL.js +71 -0
  82. package/dist/chunk-PC6ZEDRL.js.map +1 -0
  83. package/dist/chunk-PM3QYWUU.js +251 -0
  84. package/dist/chunk-PM3QYWUU.js.map +1 -0
  85. package/dist/chunk-PVUUIWHY.js +73 -0
  86. package/dist/chunk-PVUUIWHY.js.map +1 -0
  87. package/dist/chunk-PXTQPZO4.js +830 -0
  88. package/dist/chunk-PXTQPZO4.js.map +1 -0
  89. package/dist/{chunk-ZFKD4QMV.js → chunk-QSOYKKMD.js} +4 -4
  90. package/dist/chunk-QSOYKKMD.js.map +1 -0
  91. package/dist/{chunk-MR4424N3.js → chunk-R233SLY3.js} +2 -2
  92. package/dist/chunk-RC6SU5NO.js +36 -0
  93. package/dist/chunk-RC6SU5NO.js.map +1 -0
  94. package/dist/{chunk-USKYUS74.js → chunk-RRNA5GKT.js} +2 -2
  95. package/dist/{chunk-R36SIKES.js → chunk-RYIL3PI2.js} +2 -2
  96. package/dist/chunk-STNPB3UM.js +9 -0
  97. package/dist/chunk-STNPB3UM.js.map +1 -0
  98. package/dist/{chunk-M5INGEFC.js → chunk-TV3YZ35S.js} +7 -1
  99. package/dist/chunk-TV3YZ35S.js.map +1 -0
  100. package/dist/chunk-TY32C732.js +59 -0
  101. package/dist/chunk-TY32C732.js.map +1 -0
  102. package/dist/chunk-UMLVJTYV.js +20 -0
  103. package/dist/chunk-UMLVJTYV.js.map +1 -0
  104. package/dist/{chunk-NPC4LFV5.js → chunk-WIBHRONM.js} +2 -2
  105. package/dist/chunk-WIBHRONM.js.map +1 -0
  106. package/dist/{chunk-RKJ6OL7K.js → chunk-WIRRPTFH.js} +1 -1
  107. package/dist/chunk-WIRRPTFH.js.map +1 -0
  108. package/dist/{chunk-VQBTTTUN.js → chunk-Y26YV5R3.js} +4 -4
  109. package/dist/{chunk-VQBTTTUN.js.map → chunk-Y26YV5R3.js.map} +1 -1
  110. package/dist/{chunk-NXFEYLVG.js → chunk-YM7LFCG7.js} +5 -4
  111. package/dist/{chunk-NXFEYLVG.js.map → chunk-YM7LFCG7.js.map} +1 -1
  112. package/dist/{chunk-CIMZBAZB.js → chunk-Z6FNBOTC.js} +1 -1
  113. package/dist/chunk-Z6FNBOTC.js.map +1 -0
  114. package/dist/chunk-ZROPXHJY.js +82 -0
  115. package/dist/chunk-ZROPXHJY.js.map +1 -0
  116. package/dist/consent/index.cjs.map +1 -1
  117. package/dist/consent/index.d.cts +7 -6
  118. package/dist/consent/index.d.ts +7 -6
  119. package/dist/consent/index.js +3 -3
  120. package/dist/{crypto-IVKU7YTT.js → crypto-2CRLG4F4.js} +3 -3
  121. package/dist/{delegation-2DBS2EOH.js → delegation-ZTRT2PRV.js} +5 -4
  122. package/dist/derivations/index.cjs +368 -0
  123. package/dist/derivations/index.cjs.map +1 -0
  124. package/dist/derivations/index.d.cts +72 -0
  125. package/dist/derivations/index.d.ts +72 -0
  126. package/dist/derivations/index.js +27 -0
  127. package/dist/{dev-unlock-Da1B0TIK.d.cts → dev-unlock-AglVnkPY.d.cts} +1 -1
  128. package/dist/{dev-unlock-BdPp68qn.d.ts → dev-unlock-BOEYl1xl.d.ts} +1 -1
  129. package/dist/discriminant-BN9REW3o.d.cts +60 -0
  130. package/dist/discriminant-BN9REW3o.d.ts +60 -0
  131. package/dist/executor-S76VN45G.js +8 -0
  132. package/dist/executor-UCXLIGLW.js +11 -0
  133. package/dist/executor-UCXLIGLW.js.map +1 -0
  134. package/dist/executor-ZCNZJMGR.js +8 -0
  135. package/dist/executor-ZCNZJMGR.js.map +1 -0
  136. package/dist/fanout-sidecar-OKPMMPLG.js +51 -0
  137. package/dist/fanout-sidecar-OKPMMPLG.js.map +1 -0
  138. package/dist/guards/index.cjs +322 -0
  139. package/dist/guards/index.cjs.map +1 -0
  140. package/dist/guards/index.d.cts +31 -0
  141. package/dist/guards/index.d.ts +31 -0
  142. package/dist/guards/index.js +29 -0
  143. package/dist/guards/index.js.map +1 -0
  144. package/dist/{hash-lsoL3eEW.d.ts → hash-B9m3_fhj.d.ts} +1 -1
  145. package/dist/{hash-BEfzPKwo.d.cts → hash-RVqz2zi8.d.cts} +1 -1
  146. package/dist/history/index.cjs +9 -2
  147. package/dist/history/index.cjs.map +1 -1
  148. package/dist/history/index.d.cts +8 -7
  149. package/dist/history/index.d.ts +8 -7
  150. package/dist/history/index.js +6 -6
  151. package/dist/i18n/index.cjs +368 -27
  152. package/dist/i18n/index.cjs.map +1 -1
  153. package/dist/i18n/index.d.cts +7 -6
  154. package/dist/i18n/index.d.ts +7 -6
  155. package/dist/i18n/index.js +34 -6
  156. package/dist/i18n/index.js.map +1 -1
  157. package/dist/{index-DJTf9yxn.d.ts → index-B8bjExET.d.cts} +508 -14
  158. package/dist/{index-6xNpPsxR.d.cts → index-DfUbNad8.d.ts} +508 -14
  159. package/dist/index.cjs +8779 -1260
  160. package/dist/index.cjs.map +1 -1
  161. package/dist/index.d.cts +231 -19
  162. package/dist/index.d.ts +231 -19
  163. package/dist/index.js +311 -7370
  164. package/dist/index.js.map +1 -1
  165. package/dist/indexing/index.cjs +7 -1
  166. package/dist/indexing/index.cjs.map +1 -1
  167. package/dist/indexing/index.d.cts +3 -3
  168. package/dist/indexing/index.d.ts +3 -3
  169. package/dist/indexing/index.js +4 -4
  170. package/dist/issue-3W6IVLKH.js +12 -0
  171. package/dist/issue-3W6IVLKH.js.map +1 -0
  172. package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Ci5_YG73.d.cts} +2 -2
  173. package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-D5GU14TS.d.ts} +2 -2
  174. package/dist/{ledger-QZTTHQAQ.js → ledger-O7FXOG3D.js} +6 -6
  175. package/dist/ledger-O7FXOG3D.js.map +1 -0
  176. package/dist/materialized-views/index.cjs +856 -0
  177. package/dist/materialized-views/index.cjs.map +1 -0
  178. package/dist/materialized-views/index.d.cts +186 -0
  179. package/dist/materialized-views/index.d.ts +186 -0
  180. package/dist/materialized-views/index.js +45 -0
  181. package/dist/materialized-views/index.js.map +1 -0
  182. package/dist/noydb-YAZNH5TI.js +34 -0
  183. package/dist/noydb-YAZNH5TI.js.map +1 -0
  184. package/dist/overlay-views/index.cjs +369 -0
  185. package/dist/overlay-views/index.cjs.map +1 -0
  186. package/dist/overlay-views/index.d.cts +82 -0
  187. package/dist/overlay-views/index.d.ts +82 -0
  188. package/dist/overlay-views/index.js +25 -0
  189. package/dist/overlay-views/index.js.map +1 -0
  190. package/dist/periods/index.cjs +7 -1
  191. package/dist/periods/index.cjs.map +1 -1
  192. package/dist/periods/index.d.cts +7 -6
  193. package/dist/periods/index.d.ts +7 -6
  194. package/dist/periods/index.js +6 -6
  195. package/dist/{predicate-SBHmi6D0.d.cts → predicate-Bt5ft-9c.d.cts} +51 -2
  196. package/dist/{predicate-SBHmi6D0.d.ts → predicate-Bt5ft-9c.d.ts} +51 -2
  197. package/dist/{public-envelope-6JTACYJV.js → public-envelope-HMYHZIRH.js} +4 -4
  198. package/dist/public-envelope-HMYHZIRH.js.map +1 -0
  199. package/dist/query/index.cjs +555 -128
  200. package/dist/query/index.cjs.map +1 -1
  201. package/dist/query/index.d.cts +3 -3
  202. package/dist/query/index.d.ts +3 -3
  203. package/dist/query/index.js +32 -11
  204. package/dist/read-only-facade-ITU6L7BL.js +7 -0
  205. package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
  206. package/dist/registry-DKEXOJVO.js +7 -0
  207. package/dist/registry-DKEXOJVO.js.map +1 -0
  208. package/dist/registry-ST2VNFZC.js +10 -0
  209. package/dist/registry-ST2VNFZC.js.map +1 -0
  210. package/dist/registry-UFIK7CSR.js +8 -0
  211. package/dist/registry-UFIK7CSR.js.map +1 -0
  212. package/dist/registry-ZGYYSM5I.js +8 -0
  213. package/dist/registry-ZGYYSM5I.js.map +1 -0
  214. package/dist/revoke-S6JMSLUN.js +17 -0
  215. package/dist/revoke-S6JMSLUN.js.map +1 -0
  216. package/dist/session/index.cjs +7 -1
  217. package/dist/session/index.cjs.map +1 -1
  218. package/dist/session/index.d.cts +8 -7
  219. package/dist/session/index.d.ts +8 -7
  220. package/dist/session/index.js +10 -3
  221. package/dist/session/index.js.map +1 -1
  222. package/dist/shadow/index.cjs.map +1 -1
  223. package/dist/shadow/index.d.cts +7 -6
  224. package/dist/shadow/index.d.ts +7 -6
  225. package/dist/shadow/index.js +2 -2
  226. package/dist/signer-7NPTB3SQ.js +18 -0
  227. package/dist/signer-7NPTB3SQ.js.map +1 -0
  228. package/dist/snapshots/index.cjs +937 -0
  229. package/dist/snapshots/index.cjs.map +1 -0
  230. package/dist/snapshots/index.d.cts +28 -0
  231. package/dist/snapshots/index.d.ts +28 -0
  232. package/dist/snapshots/index.js +152 -0
  233. package/dist/snapshots/index.js.map +1 -0
  234. package/dist/stale-VKXSXJF4.js +13 -0
  235. package/dist/stale-VKXSXJF4.js.map +1 -0
  236. package/dist/store/index.cjs +14 -0
  237. package/dist/store/index.cjs.map +1 -1
  238. package/dist/store/index.d.cts +7 -6
  239. package/dist/store/index.d.ts +7 -6
  240. package/dist/store/index.js +5 -2
  241. package/dist/{strategy-D-SrOLCl.d.ts → strategy-CT2LCKAX.d.cts} +84 -19
  242. package/dist/{strategy-D-SrOLCl.d.cts → strategy-CT2LCKAX.d.ts} +84 -19
  243. package/dist/sync/index.cjs.map +1 -1
  244. package/dist/sync/index.d.cts +6 -5
  245. package/dist/sync/index.d.ts +6 -5
  246. package/dist/sync/index.js +4 -4
  247. package/dist/team/index.cjs +1554 -2
  248. package/dist/team/index.cjs.map +1 -1
  249. package/dist/team/index.d.cts +7 -6
  250. package/dist/team/index.d.ts +7 -6
  251. package/dist/team/index.js +77 -8
  252. package/dist/tx/index.cjs +375 -43
  253. package/dist/tx/index.cjs.map +1 -1
  254. package/dist/tx/index.d.cts +8 -7
  255. package/dist/tx/index.d.ts +8 -7
  256. package/dist/tx/index.js +56 -3
  257. package/dist/tx/index.js.map +1 -1
  258. package/dist/{types-Bo7NSXJr.d.ts → types-CaNQm4i8.d.ts} +3902 -614
  259. package/dist/{types-Bnb82f5R.d.cts → types-n2_IfwlQ.d.cts} +3902 -614
  260. package/dist/{index-CywCC1qZ.d.cts → ulid-B9SMWj5i.d.ts} +216 -27
  261. package/dist/{index-8QDuznDr.d.ts → ulid-CLMjmyhG.d.cts} +216 -27
  262. package/dist/util/index.cjs +7 -0
  263. package/dist/util/index.cjs.map +1 -1
  264. package/dist/util/index.d.cts +2 -0
  265. package/dist/util/index.d.ts +2 -0
  266. package/dist/util/index.js +5 -1
  267. package/dist/util/index.js.map +1 -1
  268. package/dist/with-derivation-CVIOPTUf.d.ts +13 -0
  269. package/dist/with-derivation-aKrtS7Jj.d.cts +13 -0
  270. package/dist/with-guard-DZQbPzoP.d.cts +18 -0
  271. package/dist/with-guard-DseETUrF.d.ts +18 -0
  272. package/dist/with-materialized-view-C1eA1_T_.d.cts +27 -0
  273. package/dist/with-materialized-view-DaYaE8-Q.d.ts +27 -0
  274. package/dist/with-overlayed-view-DQsh2p8H.d.ts +13 -0
  275. package/dist/with-overlayed-view-DleJfKcV.d.cts +13 -0
  276. package/package.json +77 -3
  277. package/dist/chunk-2CSJGFCB.js.map +0 -1
  278. package/dist/chunk-ACLDOTNQ.js.map +0 -1
  279. package/dist/chunk-BTDCBVJW.js +0 -160
  280. package/dist/chunk-BTDCBVJW.js.map +0 -1
  281. package/dist/chunk-CIMZBAZB.js.map +0 -1
  282. package/dist/chunk-EXHNQEV4.js +0 -392
  283. package/dist/chunk-EXHNQEV4.js.map +0 -1
  284. package/dist/chunk-GOUT6DND.js.map +0 -1
  285. package/dist/chunk-M5INGEFC.js.map +0 -1
  286. package/dist/chunk-MDDTIZUO.js.map +0 -1
  287. package/dist/chunk-NPC4LFV5.js.map +0 -1
  288. package/dist/chunk-QAVUREFT.js.map +0 -1
  289. package/dist/chunk-RKJ6OL7K.js.map +0 -1
  290. package/dist/chunk-SCZXXXU4.js.map +0 -1
  291. package/dist/chunk-TDR6T5CJ.js.map +0 -1
  292. package/dist/chunk-WDM5XGGS.js.map +0 -1
  293. package/dist/chunk-ZFKD4QMV.js.map +0 -1
  294. /package/dist/{chunk-PTVMYYON.js.map → chunk-2N62W5YP.js.map} +0 -0
  295. /package/dist/{chunk-QGZRWRSL.js.map → chunk-3LPV6BXR.js.map} +0 -0
  296. /package/dist/{chunk-4PWAI7Q4.js.map → chunk-5OX6XVNS.js.map} +0 -0
  297. /package/dist/{chunk-AVVPZ4BC.js.map → chunk-DJRWA3Q5.js.map} +0 -0
  298. /package/dist/{chunk-M62XNWRA.js.map → chunk-LSTBFLL2.js.map} +0 -0
  299. /package/dist/{chunk-MR4424N3.js.map → chunk-R233SLY3.js.map} +0 -0
  300. /package/dist/{chunk-USKYUS74.js.map → chunk-RRNA5GKT.js.map} +0 -0
  301. /package/dist/{chunk-R36SIKES.js.map → chunk-RYIL3PI2.js.map} +0 -0
  302. /package/dist/{crypto-IVKU7YTT.js.map → crypto-2CRLG4F4.js.map} +0 -0
  303. /package/dist/{delegation-2DBS2EOH.js.map → delegation-ZTRT2PRV.js.map} +0 -0
  304. /package/dist/{ledger-QZTTHQAQ.js.map → derivations/index.js.map} +0 -0
  305. /package/dist/{public-envelope-6JTACYJV.js.map → executor-S76VN45G.js.map} +0 -0
@@ -22,11 +22,20 @@ var query_exports = {};
22
22
  __export(query_exports, {
23
23
  Aggregation: () => Aggregation,
24
24
  CollectionIndexes: () => CollectionIndexes,
25
+ CrossJoinSourceUnknownError: () => CrossJoinSourceUnknownError,
26
+ CrossJoinTooLargeError: () => CrossJoinTooLargeError,
27
+ DEFAULT_CROSS_JOIN_MAX_ROWS: () => DEFAULT_CROSS_JOIN_MAX_ROWS,
25
28
  DEFAULT_JOIN_MAX_ROWS: () => DEFAULT_JOIN_MAX_ROWS,
29
+ DanglingReferenceError: () => DanglingReferenceError,
26
30
  GROUPBY_MAX_CARDINALITY: () => GROUPBY_MAX_CARDINALITY,
27
31
  GROUPBY_WARN_CARDINALITY: () => GROUPBY_WARN_CARDINALITY,
32
+ GroupCardinalityError: () => GroupCardinalityError,
28
33
  GroupedAggregation: () => GroupedAggregation,
29
34
  GroupedQuery: () => GroupedQuery,
35
+ GroupedQueryN: () => GroupedQueryN,
36
+ IndexRequiredError: () => IndexRequiredError,
37
+ IndexWriteFailureError: () => IndexWriteFailureError,
38
+ JoinTooLargeError: () => JoinTooLargeError,
30
39
  Query: () => Query,
31
40
  ScanBuilder: () => ScanBuilder,
32
41
  applyJoins: () => applyJoins,
@@ -111,6 +120,12 @@ function evaluateClause(record, clause) {
111
120
  return evaluateFieldClause(record, clause);
112
121
  case "filter":
113
122
  return clause.fn(record);
123
+ case "wherePredicate":
124
+ return clause.fn(record, clause.ctx);
125
+ case "crossJoin":
126
+ throw new Error(
127
+ `evaluateClause: 'crossJoin' clauses are expansion primitives and are not evaluated per-record. This is a query planner routing error \u2014 crossJoin clauses must be extracted from the clause list before calling evaluateClause or filterRecords.`
128
+ );
114
129
  case "group":
115
130
  if (clause.op === "and") {
116
131
  for (const child of clause.clauses) {
@@ -154,6 +169,38 @@ var GroupCardinalityError = class extends NoydbError {
154
169
  this.maxGroups = maxGroups;
155
170
  }
156
171
  };
172
+ var IndexRequiredError = class extends NoydbError {
173
+ collection;
174
+ touchedFields;
175
+ missingFields;
176
+ constructor(args) {
177
+ super(
178
+ "INDEX_REQUIRED",
179
+ `Collection "${args.collection}": query references unindexed fields in lazy mode (missing: ${args.missingFields.join(", ")}). Declare an index on each field, or use collection.scan() for non-indexed iteration.`
180
+ );
181
+ this.name = "IndexRequiredError";
182
+ this.collection = args.collection;
183
+ this.touchedFields = [...args.touchedFields];
184
+ this.missingFields = [...args.missingFields];
185
+ }
186
+ };
187
+ var IndexWriteFailureError = class extends NoydbError {
188
+ recordId;
189
+ field;
190
+ op;
191
+ cause;
192
+ constructor(args) {
193
+ super(
194
+ "INDEX_WRITE_FAILURE",
195
+ `Index side-car ${args.op} failed for field "${args.field}" on record "${args.recordId}"`
196
+ );
197
+ this.name = "IndexWriteFailureError";
198
+ this.recordId = args.recordId;
199
+ this.field = args.field;
200
+ this.op = args.op;
201
+ this.cause = args.cause;
202
+ }
203
+ };
157
204
  var JoinTooLargeError = class extends NoydbError {
158
205
  leftRows;
159
206
  rightRows;
@@ -168,6 +215,34 @@ var JoinTooLargeError = class extends NoydbError {
168
215
  this.side = opts.side;
169
216
  }
170
217
  };
218
+ var CrossJoinTooLargeError = class extends NoydbError {
219
+ target;
220
+ expected;
221
+ limit;
222
+ constructor(opts) {
223
+ super(
224
+ "CROSS_JOIN_TOO_LARGE",
225
+ `crossJoin("${opts.target}"): would produce ${opts.expected} rows, exceeding the limit of ${opts.limit}. Narrow the left side with .where() first, or raise the ceiling with crossJoin("${opts.target}", { ..., maxRows: ${opts.expected} }).`
226
+ );
227
+ this.name = "CrossJoinTooLargeError";
228
+ this.target = opts.target;
229
+ this.expected = opts.expected;
230
+ this.limit = opts.limit;
231
+ }
232
+ };
233
+ var CrossJoinSourceUnknownError = class extends NoydbError {
234
+ target;
235
+ leftCollection;
236
+ constructor(target, leftCollection) {
237
+ super(
238
+ "CROSS_JOIN_SOURCE_UNKNOWN",
239
+ `crossJoin("${target}"): collection "${target}" is not known in the vault (cross-joining from "${leftCollection}"). Make sure "${target}" is open in the same vault before executing this query.`
240
+ );
241
+ this.name = "CrossJoinSourceUnknownError";
242
+ this.target = target;
243
+ this.leftCollection = leftCollection;
244
+ }
245
+ };
171
246
  var DanglingReferenceError = class extends NoydbError {
172
247
  field;
173
248
  target;
@@ -418,6 +493,9 @@ var NO_AGGREGATE = {
418
493
  groupBy() {
419
494
  throw NOT_ENABLED;
420
495
  },
496
+ groupByN() {
497
+ throw NOT_ENABLED;
498
+ },
421
499
  scanAggregate() {
422
500
  throw NOT_ENABLED;
423
501
  }
@@ -431,16 +509,89 @@ var EMPTY_PLAN = {
431
509
  offset: 0,
432
510
  joins: []
433
511
  };
512
+ var DEFAULT_CROSS_JOIN_MAX_ROWS = 5e4;
434
513
  var Query = class _Query {
435
514
  source;
436
515
  plan;
437
516
  joinContext;
438
517
  aggregateStrategy;
439
- constructor(source, plan = EMPTY_PLAN, joinContext, aggregateStrategy = NO_AGGREGATE) {
518
+ predicates;
519
+ constructor(source, plan = EMPTY_PLAN, joinContext, aggregateStrategy = NO_AGGREGATE, predicates) {
440
520
  this.source = source;
441
521
  this.plan = plan;
442
522
  this.joinContext = joinContext;
443
523
  this.aggregateStrategy = aggregateStrategy;
524
+ this.predicates = predicates;
525
+ }
526
+ /**
527
+ * @internal — accessor for the materialized-view dependency
528
+ * analyzer. Not part of the public API; consumers should use the
529
+ * builder methods, not inspect the plan directly.
530
+ */
531
+ _plan() {
532
+ return this.plan;
533
+ }
534
+ /**
535
+ * @internal — accessor for the materialized-view dependency
536
+ * analyzer. Returns the join resolution context (or `undefined` for
537
+ * queries constructed without a Collection backing).
538
+ */
539
+ _joinContext() {
540
+ return this.joinContext;
541
+ }
542
+ /**
543
+ * @internal — clone this Query with a declared-predicate map
544
+ * attached. Used by the materialized-view registry to enable
545
+ * `.wherePredicate(name, ctx?)` for the MV's query callback.
546
+ * Consumers don't call this directly.
547
+ */
548
+ _withPredicates(predicates) {
549
+ return new _Query(
550
+ this.source,
551
+ this.plan,
552
+ this.joinContext,
553
+ this.aggregateStrategy,
554
+ predicates
555
+ );
556
+ }
557
+ /**
558
+ * Filter by a registered deterministic predicate. Requires
559
+ * the Query to have been augmented with a predicates map (typically
560
+ * via the materialized-view registry — bare Queries constructed
561
+ * outside an MV throw on `.wherePredicate()`).
562
+ *
563
+ * `ctx` is an optional opaque value passed verbatim to the predicate
564
+ * function. Both `predicateHash` (from the registration) and a
565
+ * canonical-JSON hash of `ctx` fold into the MV's `queryHash`, so
566
+ * either changing forces refresh on next visit.
567
+ */
568
+ wherePredicate(name, ctx) {
569
+ if (!this.predicates) {
570
+ throw new Error(
571
+ `.wherePredicate("${name}"): no predicates registered on this Query. Function-based predicates require the Query to be obtained from inside a materialized-view query() callback whose strategy declares \`predicates: { ${name}: { hash, fn } }\`.`
572
+ );
573
+ }
574
+ const decl = this.predicates.get(name);
575
+ if (!decl) {
576
+ throw new Error(
577
+ `.wherePredicate("${name}"): predicate not registered. Available: ${[...this.predicates.keys()].join(", ") || "(none)"}.`
578
+ );
579
+ }
580
+ const clause = {
581
+ type: "wherePredicate",
582
+ name,
583
+ ctx,
584
+ predicateHash: decl.hash,
585
+ ctxHash: canonicalCtxHash(ctx),
586
+ fn: decl.fn
587
+ };
588
+ return new _Query(
589
+ this.source,
590
+ { ...this.plan, clauses: [...this.plan.clauses, clause] },
591
+ this.joinContext,
592
+ this.aggregateStrategy,
593
+ this.predicates
594
+ );
444
595
  }
445
596
  /** Add a field comparison. Multiple where() calls are AND-combined. */
446
597
  where(field, op, value) {
@@ -449,7 +600,8 @@ var Query = class _Query {
449
600
  this.source,
450
601
  { ...this.plan, clauses: [...this.plan.clauses, clause] },
451
602
  this.joinContext,
452
- this.aggregateStrategy
603
+ this.aggregateStrategy,
604
+ this.predicates
453
605
  );
454
606
  }
455
607
  /**
@@ -459,7 +611,7 @@ var Query = class _Query {
459
611
  */
460
612
  or(builder) {
461
613
  const sub = builder(
462
- new _Query(this.source, EMPTY_PLAN, this.joinContext, this.aggregateStrategy)
614
+ new _Query(this.source, EMPTY_PLAN, this.joinContext, this.aggregateStrategy, this.predicates)
463
615
  );
464
616
  const group = {
465
617
  type: "group",
@@ -470,7 +622,8 @@ var Query = class _Query {
470
622
  this.source,
471
623
  { ...this.plan, clauses: [...this.plan.clauses, group] },
472
624
  this.joinContext,
473
- this.aggregateStrategy
625
+ this.aggregateStrategy,
626
+ this.predicates
474
627
  );
475
628
  }
476
629
  /**
@@ -479,7 +632,7 @@ var Query = class _Query {
479
632
  */
480
633
  and(builder) {
481
634
  const sub = builder(
482
- new _Query(this.source, EMPTY_PLAN, this.joinContext, this.aggregateStrategy)
635
+ new _Query(this.source, EMPTY_PLAN, this.joinContext, this.aggregateStrategy, this.predicates)
483
636
  );
484
637
  const group = {
485
638
  type: "group",
@@ -490,7 +643,8 @@ var Query = class _Query {
490
643
  this.source,
491
644
  { ...this.plan, clauses: [...this.plan.clauses, group] },
492
645
  this.joinContext,
493
- this.aggregateStrategy
646
+ this.aggregateStrategy,
647
+ this.predicates
494
648
  );
495
649
  }
496
650
  /** Escape hatch: add an arbitrary predicate function. Not serializable. */
@@ -503,7 +657,8 @@ var Query = class _Query {
503
657
  this.source,
504
658
  { ...this.plan, clauses: [...this.plan.clauses, clause] },
505
659
  this.joinContext,
506
- this.aggregateStrategy
660
+ this.aggregateStrategy,
661
+ this.predicates
507
662
  );
508
663
  }
509
664
  /** Sort by a field. Subsequent calls are tie-breakers. */
@@ -512,7 +667,8 @@ var Query = class _Query {
512
667
  this.source,
513
668
  { ...this.plan, orderBy: [...this.plan.orderBy, { field, direction }] },
514
669
  this.joinContext,
515
- this.aggregateStrategy
670
+ this.aggregateStrategy,
671
+ this.predicates
516
672
  );
517
673
  }
518
674
  /** Cap the result size. */
@@ -521,7 +677,8 @@ var Query = class _Query {
521
677
  this.source,
522
678
  { ...this.plan, limit: n },
523
679
  this.joinContext,
524
- this.aggregateStrategy
680
+ this.aggregateStrategy,
681
+ this.predicates
525
682
  );
526
683
  }
527
684
  /** Skip the first N matching records (after ordering). */
@@ -530,7 +687,8 @@ var Query = class _Query {
530
687
  this.source,
531
688
  { ...this.plan, offset: n },
532
689
  this.joinContext,
533
- this.aggregateStrategy
690
+ this.aggregateStrategy,
691
+ this.predicates
534
692
  );
535
693
  }
536
694
  /**
@@ -629,7 +787,80 @@ var Query = class _Query {
629
787
  this.source,
630
788
  { ...this.plan, joins: [...this.plan.joins, leg] },
631
789
  this.joinContext,
632
- this.aggregateStrategy
790
+ this.aggregateStrategy,
791
+ this.predicates
792
+ );
793
+ }
794
+ /**
795
+ * Cartesian-product cross-join against `target` collection. Each result row
796
+ * carries the original `T` fields plus `result[as]` populated from every
797
+ * right-side row (or the filtered subset when `on:` is supplied).
798
+ *
799
+ * **Order matters:** `.where().crossJoin()` filters BEFORE expanding (cheaper);
800
+ * `.crossJoin().where('alias.field', ...)` filters AFTER (required when the
801
+ * where clause references the aliased fields).
802
+ *
803
+ * **Cost ceiling:** `CrossJoinTooLargeError` fires before allocation when
804
+ * `leftRows × rightRows` (or the cumulative lateral count) exceeds the limit.
805
+ * Default: 50,000 rows. Override per-clause with `{ maxRows: N }`.
806
+ *
807
+ * **`on:` shapes:**
808
+ * - `on: (left) => TTarget[]` — subset form (most efficient)
809
+ * - `on: (left) => (right) => boolean` — predicate form
810
+ * - `on: { predicate: 'name' }` — MV-safe, hash-tracked form
811
+ * (requires the Query to have been augmented via `_withPredicates`)
812
+ *
813
+ * Requires a JoinContext (constructed via `collection.query()`).
814
+ */
815
+ crossJoin(target, opts) {
816
+ if (!this.joinContext) {
817
+ throw new Error(
818
+ `Query.crossJoin("${target}"): requires a join context. Use collection.query() to construct a cross-join-capable Query instead of the Query constructor directly.`
819
+ );
820
+ }
821
+ let onFn;
822
+ let onPredicateName;
823
+ if (opts.on !== void 0) {
824
+ if (typeof opts.on === "function") {
825
+ onFn = opts.on;
826
+ if (this.predicates) {
827
+ console.warn(
828
+ `Query.crossJoin("${target}", { on: callback }): inline on: callback inside a withMaterializedView query() disables queryHash drift detection for this cross-join. Use on: { predicate: '<name>' } to enable it.`
829
+ );
830
+ }
831
+ } else {
832
+ const predName = opts.on.predicate;
833
+ if (!this.predicates) {
834
+ throw new Error(
835
+ `Query.crossJoin("${target}", { on: { predicate: "${predName}" } }): the { predicate } form requires a predicates map. Use this form inside a withMaterializedView query() callback that declares predicates: { ${predName}: { hash, fn } }.`
836
+ );
837
+ }
838
+ const decl = this.predicates.get(predName);
839
+ if (!decl) {
840
+ throw new Error(
841
+ `Query.crossJoin("${target}"): predicate "${predName}" not registered. Available: ${[...this.predicates.keys()].join(", ") || "(none)"}.`
842
+ );
843
+ }
844
+ const as = opts.as;
845
+ const predicateFn = decl.fn;
846
+ onFn = (_left) => (right) => predicateFn({ ..._left, [as]: right });
847
+ onPredicateName = predName;
848
+ }
849
+ }
850
+ const clause = {
851
+ type: "crossJoin",
852
+ target,
853
+ as: opts.as,
854
+ ...onFn !== void 0 && { on: onFn },
855
+ ...onPredicateName !== void 0 && { onPredicateName },
856
+ ...opts.maxRows !== void 0 && { maxRows: opts.maxRows }
857
+ };
858
+ return new _Query(
859
+ this.source,
860
+ { ...this.plan, clauses: [...this.plan.clauses, clause] },
861
+ this.joinContext,
862
+ this.aggregateStrategy,
863
+ this.predicates
633
864
  );
634
865
  }
635
866
  /**
@@ -639,7 +870,7 @@ var Query = class _Query {
639
870
  * for the ordering rationale.
640
871
  */
641
872
  toArray() {
642
- const base = executePlanWithSource(this.source, this.plan);
873
+ const base = executePlanWithSource(this.source, this.plan, this.joinContext);
643
874
  if (this.plan.joins.length === 0) return base;
644
875
  if (!this.joinContext) {
645
876
  throw new Error(
@@ -663,6 +894,14 @@ var Query = class _Query {
663
894
  * intent is purely to count.
664
895
  */
665
896
  count() {
897
+ if (this.plan.clauses.some((c) => c.type === "crossJoin")) {
898
+ if (!this.joinContext) {
899
+ throw new Error(
900
+ `Query.count(): plan contains crossJoin clauses but no JoinContext is attached.`
901
+ );
902
+ }
903
+ return executeClausePipeline(this.source, this.plan.clauses, this.joinContext).length;
904
+ }
666
905
  const { candidates, remainingClauses } = candidateRecords(this.source, this.plan.clauses);
667
906
  if (remainingClauses.length === 0) return candidates.length;
668
907
  return filterRecords(candidates, remainingClauses).length;
@@ -710,7 +949,13 @@ var Query = class _Query {
710
949
  aggregate(spec) {
711
950
  const source = this.source;
712
951
  const clauses = this.plan.clauses;
952
+ const joinCtx = this.joinContext;
953
+ const hasCrossJoins = clauses.some((c) => c.type === "crossJoin");
713
954
  const executeRecords = () => {
955
+ if (hasCrossJoins) {
956
+ if (!joinCtx) throw new Error("Query.aggregate(): crossJoin requires a join context");
957
+ return executeClausePipeline(source, clauses, joinCtx);
958
+ }
714
959
  const { candidates, remainingClauses } = candidateRecords(source, clauses);
715
960
  return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses);
716
961
  };
@@ -721,56 +966,19 @@ var Query = class _Query {
721
966
  }
722
967
  return this.aggregateStrategy.aggregate(executeRecords, spec, upstreams);
723
968
  }
724
- /**
725
- * Partition matching records into buckets keyed by a field, then
726
- * terminate with `.aggregate(spec)` to compute per-bucket
727
- * reducers..
728
- *
729
- * ```ts
730
- * const byClient = invoices.query()
731
- * .where('status', '==', 'open')
732
- * .groupBy('clientId')
733
- * .aggregate({ total: sum('amount'), n: count() })
734
- * .run()
735
- * // → [ { clientId: 'c1', total: 5250, n: 3 }, … ]
736
- * ```
737
- *
738
- * Result rows carry the group key value under the grouping field
739
- * name plus every reducer output from the spec. Buckets are
740
- * emitted in first-seen order — consumers who want a specific
741
- * ordering should `.sort()` downstream.
742
- *
743
- * **Cardinality caps:** a one-shot warning fires at 10_000
744
- * distinct groups; `GroupCardinalityError` throws at 100_000.
745
- * Grouping on a high-uniqueness field like `id` or `createdAt` is
746
- * almost always a query mistake — the error message names the
747
- * field and observed cardinality and suggests narrowing with
748
- * `.where()` first.
749
- *
750
- * **Null / undefined keys:** records with a missing or explicitly
751
- * `null` group field get their own buckets. `Map`-based
752
- * partitioning distinguishes `undefined` from `null`, so the two
753
- * cases do NOT merge. Consumers who want them merged should
754
- * coalesce upstream with `.filter()`.
755
- *
756
- * **Joins are not applied** — same rationale as `.count()` and
757
- * `.aggregate()`. Joined fields in are projection-only, so
758
- * running a join inside a grouping pipeline would be wasteful and
759
- * could trigger `DanglingReferenceError` in strict mode for a
760
- * call whose intent is purely to bucket-and-reduce. Grouping by
761
- * a joined field is explicitly out of scope for — file an
762
- * issue if a real consumer needs it.
763
- *
764
- * **Filter clauses (`.filter(fn)`):** grouped queries still
765
- * support filter clauses in the underlying plan — they run in
766
- * the same candidate/filter pipeline that `.aggregate()` uses.
767
- * The performance caveat is the same: filter clauses cost O(N)
768
- * per record and can't be index-accelerated.
769
- */
770
- groupBy(field) {
969
+ groupBy(...fields) {
970
+ if (fields.length === 0) {
971
+ throw new Error(".groupBy() requires at least one field");
972
+ }
771
973
  const source = this.source;
772
974
  const clauses = this.plan.clauses;
975
+ const joinCtx = this.joinContext;
976
+ const hasCrossJoins = clauses.some((c) => c.type === "crossJoin");
773
977
  const executeRecords = () => {
978
+ if (hasCrossJoins) {
979
+ if (!joinCtx) throw new Error("Query.groupBy(): crossJoin requires a join context");
980
+ return executeClausePipeline(source, clauses, joinCtx);
981
+ }
774
982
  const { candidates, remainingClauses } = candidateRecords(source, clauses);
775
983
  return remainingClauses.length === 0 ? candidates : filterRecords(candidates, remainingClauses);
776
984
  };
@@ -779,36 +987,21 @@ var Query = class _Query {
779
987
  const subscribe = source.subscribe.bind(source);
780
988
  upstreams.push({ subscribe: (cb) => subscribe(cb) });
781
989
  }
782
- const joinCtx = this.joinContext;
783
- const dictLabelResolver = joinCtx?.resolveDictSource ? (() => {
784
- const dictSource = joinCtx.resolveDictSource(field);
785
- if (!dictSource) return void 0;
786
- const snapshot = dictSource.snapshot();
787
- const dictMap = /* @__PURE__ */ new Map();
788
- for (const entry of snapshot) {
789
- const k = entry["key"];
790
- const labels = entry["labels"];
791
- if (typeof k === "string" && labels && typeof labels === "object") {
792
- dictMap.set(k, labels);
793
- }
794
- }
795
- return async (key, locale, fallback) => {
796
- const labels = dictMap.get(key);
797
- if (!labels) return void 0;
798
- if (labels[locale] !== void 0) return labels[locale];
799
- const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
800
- for (const fb of chain) {
801
- if (fb === "any") {
802
- const any = Object.values(labels)[0];
803
- if (any !== void 0) return any;
804
- } else if (labels[fb] !== void 0) {
805
- return labels[fb];
806
- }
807
- }
808
- return void 0;
809
- };
810
- })() : void 0;
811
- return this.aggregateStrategy.groupBy(executeRecords, field, upstreams, dictLabelResolver);
990
+ if (fields.length === 1) {
991
+ const field = fields[0];
992
+ const dictLabelResolver = buildDictLabelResolver(this.joinContext, field);
993
+ return this.aggregateStrategy.groupBy(
994
+ executeRecords,
995
+ field,
996
+ upstreams,
997
+ dictLabelResolver
998
+ );
999
+ }
1000
+ return this.aggregateStrategy.groupByN(
1001
+ executeRecords,
1002
+ fields,
1003
+ upstreams
1004
+ );
812
1005
  }
813
1006
  /**
814
1007
  * Re-run the query whenever the source notifies of changes.
@@ -899,6 +1092,21 @@ var Query = class _Query {
899
1092
  }
900
1093
  }
901
1094
  }
1095
+ if (this.joinContext) {
1096
+ const subscribedCross = /* @__PURE__ */ new Set();
1097
+ for (const clause of this.plan.clauses) {
1098
+ if (clause.type !== "crossJoin") continue;
1099
+ if (subscribedCross.has(clause.target)) continue;
1100
+ subscribedCross.add(clause.target);
1101
+ const rightSource = this.joinContext.resolveSource(clause.target);
1102
+ if (rightSource?.subscribe) {
1103
+ const rightSubscribe = rightSource.subscribe.bind(rightSource);
1104
+ upstreams.push({
1105
+ subscribe: (cb) => rightSubscribe(cb)
1106
+ });
1107
+ }
1108
+ }
1109
+ }
902
1110
  return buildLiveQuery(() => this.toArray(), upstreams);
903
1111
  }
904
1112
  /**
@@ -910,9 +1118,20 @@ var Query = class _Query {
910
1118
  return serializePlan(this.plan);
911
1119
  }
912
1120
  };
913
- function executePlanWithSource(source, plan) {
914
- const { candidates, remainingClauses } = candidateRecords(source, plan.clauses);
915
- let result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses);
1121
+ function executePlanWithSource(source, plan, joinContext) {
1122
+ const hasCrossJoins = plan.clauses.some((c) => c.type === "crossJoin");
1123
+ let result;
1124
+ if (hasCrossJoins) {
1125
+ if (!joinContext) {
1126
+ throw new Error(
1127
+ `Query.toArray(): plan contains crossJoin clauses but no JoinContext is attached. Use collection.query() instead of new Query() for cross-join support.`
1128
+ );
1129
+ }
1130
+ result = executeClausePipeline(source, plan.clauses, joinContext);
1131
+ } else {
1132
+ const { candidates, remainingClauses } = candidateRecords(source, plan.clauses);
1133
+ result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses);
1134
+ }
916
1135
  if (plan.orderBy.length > 0) {
917
1136
  result = sortRecords(result, plan.orderBy);
918
1137
  }
@@ -962,6 +1181,11 @@ function materializeIds(ids, lookupById) {
962
1181
  return out;
963
1182
  }
964
1183
  function executePlan(records, plan) {
1184
+ if (plan.clauses.some((c) => c.type === "crossJoin")) {
1185
+ throw new Error(
1186
+ `executePlan(): does not support crossJoin clauses. executePlan is a stateless pure function \u2014 it cannot resolve cross-join right-side collections. Use Query.toArray() (via collection.query()) instead.`
1187
+ );
1188
+ }
965
1189
  let result = filterRecords(records, plan.clauses);
966
1190
  if (plan.orderBy.length > 0) {
967
1191
  result = sortRecords(result, plan.orderBy);
@@ -989,6 +1213,74 @@ function filterRecords(records, clauses) {
989
1213
  }
990
1214
  return out;
991
1215
  }
1216
+ function executeClausePipeline(source, clauses, joinContext) {
1217
+ let rel = [...source.snapshot()];
1218
+ let filterBatch = [];
1219
+ for (const clause of clauses) {
1220
+ if (clause.type === "crossJoin") {
1221
+ if (filterBatch.length > 0) {
1222
+ rel = filterRecords(rel, filterBatch);
1223
+ filterBatch = [];
1224
+ }
1225
+ const rightSource = joinContext.resolveSource(clause.target);
1226
+ if (!rightSource) {
1227
+ throw new CrossJoinSourceUnknownError(clause.target, joinContext.leftCollection);
1228
+ }
1229
+ rel = applyCrossJoin(rel, clause, rightSource);
1230
+ } else {
1231
+ filterBatch.push(clause);
1232
+ }
1233
+ }
1234
+ if (filterBatch.length > 0) {
1235
+ rel = filterRecords(rel, filterBatch);
1236
+ }
1237
+ return rel;
1238
+ }
1239
+ function applyCrossJoin(leftRel, clause, rightSource) {
1240
+ const rightRows = rightSource.snapshot();
1241
+ const maxRows = clause.maxRows ?? DEFAULT_CROSS_JOIN_MAX_ROWS;
1242
+ const { as } = clause;
1243
+ if (!clause.on) {
1244
+ const product = leftRel.length * rightRows.length;
1245
+ if (product > maxRows) {
1246
+ throw new CrossJoinTooLargeError({ target: clause.target, expected: product, limit: maxRows });
1247
+ }
1248
+ const expanded2 = [];
1249
+ for (const left of leftRel) {
1250
+ const leftObj = left;
1251
+ for (const right of rightRows) {
1252
+ expanded2.push({ ...leftObj, [as]: right });
1253
+ }
1254
+ }
1255
+ return expanded2;
1256
+ }
1257
+ const expanded = [];
1258
+ let cumulative = 0;
1259
+ for (const left of leftRel) {
1260
+ const callbackResult = clause.on(left);
1261
+ let filteredRight;
1262
+ if (Array.isArray(callbackResult)) {
1263
+ filteredRight = callbackResult;
1264
+ } else {
1265
+ filteredRight = rightRows.filter(
1266
+ callbackResult
1267
+ );
1268
+ }
1269
+ cumulative += filteredRight.length;
1270
+ if (cumulative > maxRows) {
1271
+ throw new CrossJoinTooLargeError({
1272
+ target: clause.target,
1273
+ expected: cumulative,
1274
+ limit: maxRows
1275
+ });
1276
+ }
1277
+ const leftObj = left;
1278
+ for (const right of filteredRight) {
1279
+ expanded.push({ ...leftObj, [as]: right });
1280
+ }
1281
+ }
1282
+ return expanded;
1283
+ }
992
1284
  function sortRecords(records, orderBy) {
993
1285
  return [...records].sort((a, b) => {
994
1286
  for (const { field, direction } of orderBy) {
@@ -1034,6 +1326,16 @@ function serializeClause(clause) {
1034
1326
  if (clause.type === "filter") {
1035
1327
  return { type: "filter", fn: "[function]" };
1036
1328
  }
1329
+ if (clause.type === "wherePredicate") {
1330
+ return {
1331
+ type: "wherePredicate",
1332
+ name: clause.name,
1333
+ ctx: clause.ctx,
1334
+ predicateHash: clause.predicateHash,
1335
+ ctxHash: clause.ctxHash,
1336
+ fn: "[function]"
1337
+ };
1338
+ }
1037
1339
  if (clause.type === "group") {
1038
1340
  return {
1039
1341
  type: "group",
@@ -1041,8 +1343,65 @@ function serializeClause(clause) {
1041
1343
  clauses: clause.clauses.map(serializeClause)
1042
1344
  };
1043
1345
  }
1346
+ if (clause.type === "crossJoin") {
1347
+ return {
1348
+ type: "crossJoin",
1349
+ target: clause.target,
1350
+ as: clause.as,
1351
+ on: clause.on ? "[function]" : void 0,
1352
+ onPredicateName: clause.onPredicateName,
1353
+ maxRows: clause.maxRows
1354
+ };
1355
+ }
1044
1356
  return clause;
1045
1357
  }
1358
+ function canonicalCtxHash(ctx) {
1359
+ if (ctx === void 0) return "";
1360
+ const canonical = JSON.stringify(ctx, (_key, value) => {
1361
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1362
+ const sorted = {};
1363
+ for (const k of Object.keys(value).sort()) {
1364
+ sorted[k] = value[k];
1365
+ }
1366
+ return sorted;
1367
+ }
1368
+ return value;
1369
+ });
1370
+ let h = 5381;
1371
+ for (let i = 0; i < canonical.length; i++) {
1372
+ h = (h << 5) + h ^ canonical.charCodeAt(i);
1373
+ }
1374
+ return (h >>> 0).toString(16).padStart(8, "0");
1375
+ }
1376
+ function buildDictLabelResolver(joinCtx, field) {
1377
+ if (!joinCtx?.resolveDictSource) return void 0;
1378
+ const dictSource = joinCtx.resolveDictSource(field);
1379
+ if (!dictSource) return void 0;
1380
+ const snapshot = dictSource.snapshot();
1381
+ const dictMap = /* @__PURE__ */ new Map();
1382
+ for (const entry of snapshot) {
1383
+ const k = entry["key"];
1384
+ const labels = entry["labels"];
1385
+ if (typeof k === "string" && labels && typeof labels === "object") {
1386
+ dictMap.set(k, labels);
1387
+ }
1388
+ }
1389
+ return async (key, locale, fallback) => {
1390
+ const labels = dictMap.get(key);
1391
+ if (!labels) return void 0;
1392
+ if (labels[locale] !== void 0) return labels[locale];
1393
+ const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
1394
+ for (const fb of chain) {
1395
+ if (fb === "any") {
1396
+ const any = Object.values(labels)[0];
1397
+ if (any !== void 0) return any;
1398
+ } else if (labels[fb] !== void 0) {
1399
+ return labels[fb];
1400
+ }
1401
+ }
1402
+ return void 0;
1403
+ };
1404
+ }
1046
1405
 
1047
1406
  // src/indexing/eager-indexes.ts
1048
1407
  var CollectionIndexes = class {
@@ -1173,6 +1532,7 @@ function count(opts) {
1173
1532
  const _seed = opts?.seed;
1174
1533
  void _seed;
1175
1534
  return {
1535
+ op: "count",
1176
1536
  init: () => 0,
1177
1537
  step: (state) => state + 1,
1178
1538
  remove: (state) => state - 1,
@@ -1183,6 +1543,8 @@ function sum(field, opts) {
1183
1543
  const _seed = opts?.seed;
1184
1544
  void _seed;
1185
1545
  return {
1546
+ op: "sum",
1547
+ field,
1186
1548
  init: () => 0,
1187
1549
  step: (state, record) => state + readNumber(record, field),
1188
1550
  remove: (state, record) => state - readNumber(record, field),
@@ -1193,6 +1555,8 @@ function avg(field, opts) {
1193
1555
  const _seed = opts?.seed;
1194
1556
  void _seed;
1195
1557
  return {
1558
+ op: "avg",
1559
+ field,
1196
1560
  init: () => ({ sum: 0, count: 0 }),
1197
1561
  step: (state, record) => ({
1198
1562
  sum: state.sum + readNumber(record, field),
@@ -1219,6 +1583,8 @@ function min(field, opts) {
1219
1583
  const _seed = opts?.seed;
1220
1584
  void _seed;
1221
1585
  return {
1586
+ op: "min",
1587
+ field,
1222
1588
  init: () => ({ values: [] }),
1223
1589
  step: (state, record) => pushValue(state, readNumber(record, field)),
1224
1590
  remove: (state, record) => removeValue(state, readNumber(record, field)),
@@ -1237,6 +1603,8 @@ function max(field, opts) {
1237
1603
  const _seed = opts?.seed;
1238
1604
  void _seed;
1239
1605
  return {
1606
+ op: "max",
1607
+ field,
1240
1608
  init: () => ({ values: [] }),
1241
1609
  step: (state, record) => pushValue(state, readNumber(record, field)),
1242
1610
  remove: (state, record) => removeValue(state, readNumber(record, field)),
@@ -1379,31 +1747,53 @@ function buildLiveAggregation(recompute, upstreams) {
1379
1747
  return new LiveAggregationImpl(recompute, upstreams);
1380
1748
  }
1381
1749
 
1750
+ // src/aggregate/canonical-key.ts
1751
+ function canonicalGroupKey(fields, row) {
1752
+ const sorted = [...fields].sort();
1753
+ const parts = [];
1754
+ for (const name of sorted) {
1755
+ const v = row[name];
1756
+ const serialised = v === void 0 ? "undefined" : JSON.stringify(v);
1757
+ parts.push(`${name}=${serialised}`);
1758
+ }
1759
+ return parts.join("|");
1760
+ }
1761
+
1382
1762
  // src/aggregate/groupby.ts
1383
1763
  var GROUPBY_WARN_CARDINALITY = 1e4;
1384
1764
  var GROUPBY_MAX_CARDINALITY = 1e5;
1385
1765
  var warnedCardinalityFields = /* @__PURE__ */ new Set();
1386
- function warnCardinalityApproaching(field, observed) {
1387
- if (warnedCardinalityFields.has(field)) return;
1388
- warnedCardinalityFields.add(field);
1766
+ function warnCardinalityApproaching(fields, observed) {
1767
+ const key = JSON.stringify([...fields].sort());
1768
+ if (warnedCardinalityFields.has(key)) return;
1769
+ warnedCardinalityFields.add(key);
1770
+ const label = `[${fields.join(", ")}]`;
1389
1771
  console.warn(
1390
- `[noy-db] .groupBy("${field}") produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
1772
+ `[noy-db] .groupBy(${label}) produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
1391
1773
  );
1392
1774
  }
1393
1775
  function resetGroupByWarnings() {
1394
1776
  warnedCardinalityFields.clear();
1395
1777
  }
1396
- var GroupedQuery = class {
1397
- constructor(executeRecords, field, upstreams, dictLabelResolver) {
1778
+ var GroupedQueryBase = class {
1779
+ constructor(executeRecords, fieldOrFields, upstreams, dictLabelResolver) {
1398
1780
  this.executeRecords = executeRecords;
1399
- this.field = field;
1400
1781
  this.upstreams = upstreams;
1401
1782
  this.dictLabelResolver = dictLabelResolver;
1783
+ this.fields = typeof fieldOrFields === "string" ? [fieldOrFields] : [...fieldOrFields];
1402
1784
  }
1403
1785
  executeRecords;
1404
- field;
1405
1786
  upstreams;
1406
1787
  dictLabelResolver;
1788
+ /**
1789
+ * Field set this grouped query buckets on. Stored in declaration
1790
+ * order — the same order is preserved on every result row by
1791
+ * `groupAndReduce`. For the single-field constructor, this is
1792
+ * `[field]`.
1793
+ */
1794
+ fields;
1795
+ };
1796
+ var GroupedQuery = class extends GroupedQueryBase {
1407
1797
  /**
1408
1798
  * Build a grouped aggregation. Returns a `GroupedAggregation`
1409
1799
  * with `.run()`, `.runAsync()`, and `.live()` terminals — same shape
@@ -1413,70 +1803,93 @@ var GroupedQuery = class {
1413
1803
  aggregate(spec) {
1414
1804
  return new GroupedAggregation(
1415
1805
  this.executeRecords,
1416
- this.field,
1806
+ this.fields,
1807
+ spec,
1808
+ this.upstreams,
1809
+ this.dictLabelResolver
1810
+ );
1811
+ }
1812
+ };
1813
+ var GroupedQueryN = class extends GroupedQueryBase {
1814
+ aggregate(spec) {
1815
+ return new GroupedAggregation(
1816
+ this.executeRecords,
1817
+ this.fields,
1417
1818
  spec,
1418
1819
  this.upstreams,
1419
1820
  this.dictLabelResolver
1420
1821
  );
1421
1822
  }
1422
1823
  };
1423
- function groupAndReduce(records, field, spec) {
1824
+ function groupAndReduce(records, fieldOrFields, spec) {
1825
+ const fields = typeof fieldOrFields === "string" ? [fieldOrFields] : fieldOrFields;
1826
+ if (fields.length === 0) {
1827
+ throw new Error(".groupBy() requires at least one field");
1828
+ }
1424
1829
  const buckets = /* @__PURE__ */ new Map();
1830
+ const fieldLabel = fields.length === 1 ? fields[0] : `[${fields.join(", ")}]`;
1425
1831
  for (const record of records) {
1426
- const key = readPath(record, field);
1427
- let bucket = buckets.get(key);
1832
+ const keyValues = {};
1833
+ for (const f of fields) {
1834
+ keyValues[f] = readPath(record, f);
1835
+ }
1836
+ const dedupKey = canonicalGroupKey(fields, keyValues);
1837
+ let bucket = buckets.get(dedupKey);
1428
1838
  if (bucket === void 0) {
1429
1839
  if (buckets.size >= GROUPBY_MAX_CARDINALITY) {
1430
1840
  throw new GroupCardinalityError(
1431
- field,
1841
+ fieldLabel,
1432
1842
  buckets.size + 1,
1433
1843
  GROUPBY_MAX_CARDINALITY
1434
1844
  );
1435
1845
  }
1436
- bucket = [];
1437
- buckets.set(key, bucket);
1846
+ bucket = { keyValues, records: [] };
1847
+ buckets.set(dedupKey, bucket);
1438
1848
  }
1439
- bucket.push(record);
1849
+ bucket.records.push(record);
1440
1850
  }
1441
1851
  if (buckets.size >= GROUPBY_WARN_CARDINALITY) {
1442
- warnCardinalityApproaching(field, buckets.size);
1852
+ warnCardinalityApproaching(fields, buckets.size);
1443
1853
  }
1444
- const keys = Object.keys(spec);
1854
+ const reducerKeys = Object.keys(spec);
1445
1855
  const out = [];
1446
- for (const [groupKey, bucketRecords] of buckets) {
1856
+ for (const bucket of buckets.values()) {
1447
1857
  const state = {};
1448
- for (const key of keys) {
1449
- state[key] = spec[key].init();
1858
+ for (const rk of reducerKeys) {
1859
+ state[rk] = spec[rk].init();
1450
1860
  }
1451
- for (const record of bucketRecords) {
1452
- for (const key of keys) {
1453
- state[key] = spec[key].step(state[key], record);
1861
+ for (const record of bucket.records) {
1862
+ for (const rk of reducerKeys) {
1863
+ state[rk] = spec[rk].step(state[rk], record);
1454
1864
  }
1455
1865
  }
1456
- const row = { [field]: groupKey };
1457
- for (const key of keys) {
1458
- row[key] = spec[key].finalize(state[key]);
1866
+ const row = {};
1867
+ for (const f of fields) {
1868
+ row[f] = bucket.keyValues[f];
1869
+ }
1870
+ for (const rk of reducerKeys) {
1871
+ row[rk] = spec[rk].finalize(state[rk]);
1459
1872
  }
1460
1873
  out.push(row);
1461
1874
  }
1462
1875
  return out;
1463
1876
  }
1464
1877
  var GroupedAggregation = class {
1465
- constructor(executeRecords, field, spec, upstreams, dictLabelResolver) {
1878
+ constructor(executeRecords, fields, spec, upstreams, dictLabelResolver) {
1466
1879
  this.executeRecords = executeRecords;
1467
- this.field = field;
1468
1880
  this.spec = spec;
1469
1881
  this.upstreams = upstreams;
1470
1882
  this.dictLabelResolver = dictLabelResolver;
1883
+ this.fields = typeof fields === "string" ? [fields] : [...fields];
1471
1884
  }
1472
1885
  executeRecords;
1473
- field;
1474
1886
  spec;
1475
1887
  upstreams;
1476
1888
  dictLabelResolver;
1889
+ fields;
1477
1890
  /** Execute the query, group, reduce, and return an array of rows. */
1478
1891
  run() {
1479
- return groupAndReduce(this.executeRecords(), this.field, this.spec);
1892
+ return groupAndReduce(this.executeRecords(), this.fields, this.spec);
1480
1893
  }
1481
1894
  /**
1482
1895
  * Execute the query, group, reduce, and resolve `<field>Label` for
@@ -1486,17 +1899,22 @@ var GroupedAggregation = class {
1486
1899
  *
1487
1900
  * The `<field>Label` field is appended to each row. Rows whose group
1488
1901
  * key has no dictionary entry get `<field>Label: undefined`.
1902
+ *
1903
+ * Dict-label resolution is single-field only — multi-key groupings
1904
+ * do not produce a `<field>Label`. The resolver is only attached
1905
+ * by the builder when `fields.length === 1`.
1489
1906
  */
1490
1907
  async runAsync(opts) {
1491
- const rows = groupAndReduce(this.executeRecords(), this.field, this.spec);
1492
- if (!opts?.locale || !this.dictLabelResolver) return rows;
1908
+ const rows = groupAndReduce(this.executeRecords(), this.fields, this.spec);
1909
+ if (!opts?.locale || !this.dictLabelResolver || this.fields.length !== 1) return rows;
1493
1910
  const resolve = this.dictLabelResolver;
1494
1911
  const locale = opts.locale;
1495
1912
  const fallback = opts.fallback;
1496
- const labelKey = `${this.field}Label`;
1913
+ const field = this.fields[0];
1914
+ const labelKey = `${field}Label`;
1497
1915
  return Promise.all(
1498
1916
  rows.map(async (row) => {
1499
- const key = row[this.field];
1917
+ const key = row[field];
1500
1918
  if (typeof key !== "string") return row;
1501
1919
  const label = await resolve(key, locale, fallback);
1502
1920
  return { ...row, [labelKey]: label };
@@ -1520,7 +1938,7 @@ var GroupedAggregation = class {
1520
1938
  * Always call `live.stop()` when finished.
1521
1939
  */
1522
1940
  live() {
1523
- const recompute = () => groupAndReduce(this.executeRecords(), this.field, this.spec);
1941
+ const recompute = () => groupAndReduce(this.executeRecords(), this.fields, this.spec);
1524
1942
  return buildLiveAggregation(recompute, this.upstreams);
1525
1943
  }
1526
1944
  };
@@ -1930,11 +2348,20 @@ function coerceRefKey2(value) {
1930
2348
  0 && (module.exports = {
1931
2349
  Aggregation,
1932
2350
  CollectionIndexes,
2351
+ CrossJoinSourceUnknownError,
2352
+ CrossJoinTooLargeError,
2353
+ DEFAULT_CROSS_JOIN_MAX_ROWS,
1933
2354
  DEFAULT_JOIN_MAX_ROWS,
2355
+ DanglingReferenceError,
1934
2356
  GROUPBY_MAX_CARDINALITY,
1935
2357
  GROUPBY_WARN_CARDINALITY,
2358
+ GroupCardinalityError,
1936
2359
  GroupedAggregation,
1937
2360
  GroupedQuery,
2361
+ GroupedQueryN,
2362
+ IndexRequiredError,
2363
+ IndexWriteFailureError,
2364
+ JoinTooLargeError,
1938
2365
  Query,
1939
2366
  ScanBuilder,
1940
2367
  applyJoins,