@nicia-ai/typegraph 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/dist/backend/postgres/index.cjs +29 -29
  2. package/dist/backend/postgres/index.d.cts +39 -7
  3. package/dist/backend/postgres/index.d.ts +39 -7
  4. package/dist/backend/postgres/index.js +16 -16
  5. package/dist/backend/postgres/pglite.cjs +21 -21
  6. package/dist/backend/postgres/pglite.d.cts +4 -4
  7. package/dist/backend/postgres/pglite.d.ts +4 -4
  8. package/dist/backend/postgres/pglite.js +16 -16
  9. package/dist/backend/sqlite/index.cjs +25 -26
  10. package/dist/backend/sqlite/index.d.cts +8 -8
  11. package/dist/backend/sqlite/index.d.ts +8 -8
  12. package/dist/backend/sqlite/index.js +13 -14
  13. package/dist/backend/sqlite/libsql.cjs +19 -19
  14. package/dist/backend/sqlite/libsql.d.cts +4 -4
  15. package/dist/backend/sqlite/libsql.d.ts +4 -4
  16. package/dist/backend/sqlite/libsql.js +15 -15
  17. package/dist/backend/sqlite/local.cjs +101 -21
  18. package/dist/backend/sqlite/local.cjs.map +1 -1
  19. package/dist/backend/sqlite/local.d.cts +95 -5
  20. package/dist/backend/sqlite/local.d.ts +95 -5
  21. package/dist/backend/sqlite/local.js +95 -16
  22. package/dist/backend/sqlite/local.js.map +1 -1
  23. package/dist/{chunk-ZV5N4DBG.js → chunk-3TWF3SH4.js} +300 -58
  24. package/dist/chunk-3TWF3SH4.js.map +1 -0
  25. package/dist/chunk-4JEZZJ6S.cjs +1271 -0
  26. package/dist/chunk-4JEZZJ6S.cjs.map +1 -0
  27. package/dist/{chunk-UNNPKQUG.cjs → chunk-4TOD4LBZ.cjs} +246 -155
  28. package/dist/chunk-4TOD4LBZ.cjs.map +1 -0
  29. package/dist/{chunk-QV245VMN.js → chunk-55MZDDZV.js} +100 -6
  30. package/dist/chunk-55MZDDZV.js.map +1 -0
  31. package/dist/{chunk-GQANNXXT.cjs → chunk-5A6FNS6N.cjs} +75 -20
  32. package/dist/chunk-5A6FNS6N.cjs.map +1 -0
  33. package/dist/{chunk-3Z6TW3CT.cjs → chunk-5AQIKK56.cjs} +65 -25
  34. package/dist/chunk-5AQIKK56.cjs.map +1 -0
  35. package/dist/{chunk-BVTWD66D.js → chunk-5SEGFTZX.js} +3 -3
  36. package/dist/{chunk-BVTWD66D.js.map → chunk-5SEGFTZX.js.map} +1 -1
  37. package/dist/{chunk-6GNSY7AV.js → chunk-6FR2MMS6.js} +7 -7
  38. package/dist/chunk-6FR2MMS6.js.map +1 -0
  39. package/dist/chunk-7YSP4V4R.cjs +69 -0
  40. package/dist/chunk-7YSP4V4R.cjs.map +1 -0
  41. package/dist/chunk-A42GCBGX.cjs +28 -0
  42. package/dist/chunk-A42GCBGX.cjs.map +1 -0
  43. package/dist/{chunk-RHZWOJVA.cjs → chunk-BLZDPRXT.cjs} +52 -4
  44. package/dist/chunk-BLZDPRXT.cjs.map +1 -0
  45. package/dist/chunk-BYPOGQWK.js +25 -0
  46. package/dist/chunk-BYPOGQWK.js.map +1 -0
  47. package/dist/{chunk-6HVOHZHS.cjs → chunk-CK5BVQOW.cjs} +213 -168
  48. package/dist/chunk-CK5BVQOW.cjs.map +1 -0
  49. package/dist/{chunk-JJRCLAUN.cjs → chunk-CTTSDJLY.cjs} +116 -10
  50. package/dist/chunk-CTTSDJLY.cjs.map +1 -0
  51. package/dist/chunk-CUHM5CWJ.cjs +12 -0
  52. package/dist/chunk-CUHM5CWJ.cjs.map +1 -0
  53. package/dist/{chunk-A7SZZFAG.js → chunk-CZQ3FO2Q.js} +409 -99
  54. package/dist/chunk-CZQ3FO2Q.js.map +1 -0
  55. package/dist/{chunk-2JXB3NYJ.cjs → chunk-D7ZEOS56.cjs} +25 -32
  56. package/dist/chunk-D7ZEOS56.cjs.map +1 -0
  57. package/dist/chunk-DFVLCOP6.js +10 -0
  58. package/dist/chunk-DFVLCOP6.js.map +1 -0
  59. package/dist/{chunk-UJLOZYKT.cjs → chunk-EM3CWPND.cjs} +441 -115
  60. package/dist/chunk-EM3CWPND.cjs.map +1 -0
  61. package/dist/chunk-EMF62TZG.cjs +1176 -0
  62. package/dist/chunk-EMF62TZG.cjs.map +1 -0
  63. package/dist/chunk-EOLERH7Y.cjs +40 -0
  64. package/dist/chunk-EOLERH7Y.cjs.map +1 -0
  65. package/dist/{chunk-TGJE22RM.cjs → chunk-FACJJWGF.cjs} +24 -6
  66. package/dist/chunk-FACJJWGF.cjs.map +1 -0
  67. package/dist/{chunk-HK5SATYM.cjs → chunk-FL472AMI.cjs} +16 -109
  68. package/dist/chunk-FL472AMI.cjs.map +1 -0
  69. package/dist/{chunk-WVC2Q2FD.cjs → chunk-FN35DLCN.cjs} +31 -9
  70. package/dist/chunk-FN35DLCN.cjs.map +1 -0
  71. package/dist/{chunk-KUTNI6SI.js → chunk-FPIZFYHE.js} +10 -18
  72. package/dist/chunk-FPIZFYHE.js.map +1 -0
  73. package/dist/{chunk-VCGO4V26.js → chunk-FRV5Q5IO.js} +64 -9
  74. package/dist/chunk-FRV5Q5IO.js.map +1 -0
  75. package/dist/{chunk-K5P5OZXS.js → chunk-G6Q5UM7U.js} +2 -2
  76. package/dist/{chunk-K5P5OZXS.js.map → chunk-G6Q5UM7U.js.map} +1 -1
  77. package/dist/chunk-GSQ6NPH6.cjs +100 -0
  78. package/dist/chunk-GSQ6NPH6.cjs.map +1 -0
  79. package/dist/chunk-GUR2FNB5.cjs +12 -0
  80. package/dist/{chunk-HMFK4BXR.cjs.map → chunk-GUR2FNB5.cjs.map} +1 -1
  81. package/dist/{chunk-JQGIPGVL.cjs → chunk-H7YNSMA7.cjs} +43 -2
  82. package/dist/chunk-H7YNSMA7.cjs.map +1 -0
  83. package/dist/{chunk-2MRAX3UE.js → chunk-I7ZUZKFD.js} +47 -4
  84. package/dist/chunk-I7ZUZKFD.js.map +1 -0
  85. package/dist/{chunk-3BRVBQQD.cjs → chunk-J7WFGBJR.cjs} +2445 -2367
  86. package/dist/chunk-J7WFGBJR.cjs.map +1 -0
  87. package/dist/{chunk-V3QNTB7Z.js → chunk-JBTOYDMN.js} +194 -114
  88. package/dist/chunk-JBTOYDMN.js.map +1 -0
  89. package/dist/{chunk-K2TGS3EN.js → chunk-K3PPLHY7.js} +138 -89
  90. package/dist/chunk-K3PPLHY7.js.map +1 -0
  91. package/dist/{chunk-Z5XBMDXU.js → chunk-KBGQIPRQ.js} +42 -3
  92. package/dist/chunk-KBGQIPRQ.js.map +1 -0
  93. package/dist/chunk-KMI7LYAF.js +1252 -0
  94. package/dist/chunk-KMI7LYAF.js.map +1 -0
  95. package/dist/{chunk-JIEJCTDS.cjs → chunk-LGEALRC6.cjs} +21 -4
  96. package/dist/chunk-LGEALRC6.cjs.map +1 -0
  97. package/dist/{chunk-TQOYM4LN.js → chunk-MP2JQKSG.js} +21 -5
  98. package/dist/chunk-MP2JQKSG.js.map +1 -0
  99. package/dist/{chunk-ZP5WHI4C.cjs → chunk-O7UN7OXA.cjs} +358 -116
  100. package/dist/chunk-O7UN7OXA.cjs.map +1 -0
  101. package/dist/chunk-OHMP6N2J.js +47 -0
  102. package/dist/chunk-OHMP6N2J.js.map +1 -0
  103. package/dist/{chunk-4DIBPQNJ.js → chunk-OR5ZA5WP.js} +135 -25
  104. package/dist/chunk-OR5ZA5WP.js.map +1 -0
  105. package/dist/{chunk-DEQFL35G.js → chunk-Q5TEXCTB.js} +61 -7
  106. package/dist/chunk-Q5TEXCTB.js.map +1 -0
  107. package/dist/chunk-QNKSQTKQ.js +1312 -0
  108. package/dist/chunk-QNKSQTKQ.js.map +1 -0
  109. package/dist/{chunk-ZURTTSCM.js → chunk-QOTI55H7.js} +6 -94
  110. package/dist/chunk-QOTI55H7.js.map +1 -0
  111. package/dist/{chunk-CHXTTSC6.js → chunk-QWEUYILR.js} +104 -5
  112. package/dist/chunk-QWEUYILR.js.map +1 -0
  113. package/dist/{chunk-LANHCOXB.js → chunk-RGP33ZJ5.js} +485 -83
  114. package/dist/chunk-RGP33ZJ5.js.map +1 -0
  115. package/dist/chunk-RGPG7EOJ.js +94 -0
  116. package/dist/chunk-RGPG7EOJ.js.map +1 -0
  117. package/dist/{chunk-G6SOJEDI.cjs → chunk-S55OV6PW.cjs} +15 -15
  118. package/dist/chunk-S55OV6PW.cjs.map +1 -0
  119. package/dist/{chunk-3E7SNSIA.cjs → chunk-SJDMVITW.cjs} +4 -4
  120. package/dist/{chunk-3E7SNSIA.cjs.map → chunk-SJDMVITW.cjs.map} +1 -1
  121. package/dist/{chunk-XLUNB2TZ.js → chunk-SNXVDZAJ.js} +3 -3
  122. package/dist/{chunk-XLUNB2TZ.js.map → chunk-SNXVDZAJ.js.map} +1 -1
  123. package/dist/{chunk-RBV7YCQD.js → chunk-T2WUSPDA.js} +49 -9
  124. package/dist/chunk-T2WUSPDA.js.map +1 -0
  125. package/dist/{chunk-KGC4LY5T.cjs → chunk-TCOCYN2P.cjs} +75 -21
  126. package/dist/chunk-TCOCYN2P.cjs.map +1 -0
  127. package/dist/{chunk-FLRCVI2G.cjs → chunk-TDXIBCGY.cjs} +2 -2
  128. package/dist/{chunk-FLRCVI2G.cjs.map → chunk-TDXIBCGY.cjs.map} +1 -1
  129. package/dist/{chunk-ZY4GXIVN.cjs → chunk-TEDRJXOG.cjs} +5 -5
  130. package/dist/{chunk-ZY4GXIVN.cjs.map → chunk-TEDRJXOG.cjs.map} +1 -1
  131. package/dist/{chunk-WLRIWYHQ.js → chunk-UES55P7G.js} +1863 -1785
  132. package/dist/chunk-UES55P7G.js.map +1 -0
  133. package/dist/{chunk-WL2SG7ZV.cjs → chunk-UMDVATGY.cjs} +6 -6
  134. package/dist/{chunk-WL2SG7ZV.cjs.map → chunk-UMDVATGY.cjs.map} +1 -1
  135. package/dist/chunk-UT3S4SEN.js +31 -0
  136. package/dist/chunk-UT3S4SEN.js.map +1 -0
  137. package/dist/{chunk-RRKQAKD6.cjs → chunk-UUT7UN2N.cjs} +149 -39
  138. package/dist/chunk-UUT7UN2N.cjs.map +1 -0
  139. package/dist/{chunk-SHLY4MCM.js → chunk-UZV3AZKF.js} +21 -3
  140. package/dist/chunk-UZV3AZKF.js.map +1 -0
  141. package/dist/{chunk-CYX2JTDG.js → chunk-VHMPG6UL.js} +29 -7
  142. package/dist/chunk-VHMPG6UL.js.map +1 -0
  143. package/dist/{chunk-T6SPUYLX.cjs → chunk-VPYHY3LJ.cjs} +116 -10
  144. package/dist/chunk-VPYHY3LJ.cjs.map +1 -0
  145. package/dist/{chunk-T46GR2VY.js → chunk-VSPCZAXF.js} +3 -3
  146. package/dist/{chunk-T46GR2VY.js.map → chunk-VSPCZAXF.js.map} +1 -1
  147. package/dist/{chunk-K34OPSJK.js → chunk-W63OVYY4.js} +3 -3
  148. package/dist/{chunk-K34OPSJK.js.map → chunk-W63OVYY4.js.map} +1 -1
  149. package/dist/{chunk-RY2VIWAM.js → chunk-WDBCBWC4.js} +355 -32
  150. package/dist/chunk-WDBCBWC4.js.map +1 -0
  151. package/dist/chunk-WJT7OU7E.cjs +1334 -0
  152. package/dist/chunk-WJT7OU7E.cjs.map +1 -0
  153. package/dist/{chunk-5IQIX7LU.cjs → chunk-XOX22WHL.cjs} +476 -163
  154. package/dist/chunk-XOX22WHL.cjs.map +1 -0
  155. package/dist/{ddl-BrmPg61z.d.ts → ddl-C2rZ-Aen.d.ts} +4 -4
  156. package/dist/{ddl-j1-sH6lE.d.cts → ddl-CZz7a0E8.d.cts} +4 -4
  157. package/dist/{extension-types-YGFJPrzK.d.cts → extension-types-ixRpJVUp.d.cts} +24 -1
  158. package/dist/{extension-types-YGFJPrzK.d.ts → extension-types-ixRpJVUp.d.ts} +24 -1
  159. package/dist/{types-DkNnF-7K.d.cts → fulltext-strategy-B57yvC5Q.d.ts} +2562 -2133
  160. package/dist/{types-D1Sqeyxy.d.ts → fulltext-strategy-CDkSSYhq.d.cts} +2562 -2133
  161. package/dist/graph-extension/index.cjs +20 -19
  162. package/dist/graph-extension/index.d.cts +4 -4
  163. package/dist/graph-extension/index.d.ts +4 -4
  164. package/dist/graph-extension/index.js +5 -4
  165. package/dist/graph-merge/index.cjs +255 -164
  166. package/dist/graph-merge/index.cjs.map +1 -1
  167. package/dist/graph-merge/index.d.cts +63 -33
  168. package/dist/graph-merge/index.d.ts +63 -33
  169. package/dist/graph-merge/index.js +135 -44
  170. package/dist/graph-merge/index.js.map +1 -1
  171. package/dist/{index-DGX6D9j1.d.ts → index-6CSUAq-x.d.ts} +234 -93
  172. package/dist/{index-SEvUHDA6.d.cts → index-Bks_3HHK.d.cts} +234 -93
  173. package/dist/index.cjs +208 -188
  174. package/dist/index.d.cts +11 -11
  175. package/dist/index.d.ts +11 -11
  176. package/dist/index.js +33 -33
  177. package/dist/indexes/index.cjs +19 -20
  178. package/dist/indexes/index.d.cts +5 -5
  179. package/dist/indexes/index.d.ts +5 -5
  180. package/dist/indexes/index.js +10 -11
  181. package/dist/interchange/index.cjs +47 -26
  182. package/dist/interchange/index.d.cts +126 -12
  183. package/dist/interchange/index.d.ts +126 -12
  184. package/dist/interchange/index.js +14 -13
  185. package/dist/{manager-ms9cKdc6.d.cts → manager-DwQi8eV0.d.cts} +9 -2
  186. package/dist/{manager-BZUeN_BU.d.ts → manager-ejUCB52T.d.ts} +9 -2
  187. package/dist/{materialize-indexes-D20xNluI.d.cts → materialize-indexes-B4e9360H.d.cts} +20 -4
  188. package/dist/{materialize-indexes-CAkIYFV-.d.ts → materialize-indexes-te9TPMMJ.d.ts} +20 -4
  189. package/dist/{postgres-BhpxGMC5.d.cts → postgres-CfEqsRMp.d.cts} +152 -1
  190. package/dist/{postgres-CWHLwNl1.d.ts → postgres-DQ5R6rzV.d.ts} +152 -1
  191. package/dist/profiler/index.cjs +11 -11
  192. package/dist/profiler/index.d.cts +7 -7
  193. package/dist/profiler/index.d.ts +7 -7
  194. package/dist/profiler/index.js +4 -4
  195. package/dist/provenance/index.cjs +40 -41
  196. package/dist/provenance/index.cjs.map +1 -1
  197. package/dist/provenance/index.d.cts +5 -5
  198. package/dist/provenance/index.d.ts +5 -5
  199. package/dist/provenance/index.js +11 -12
  200. package/dist/provenance/index.js.map +1 -1
  201. package/dist/schema/index.cjs +53 -40
  202. package/dist/schema/index.cjs.map +1 -1
  203. package/dist/schema/index.d.cts +57 -57
  204. package/dist/schema/index.d.ts +57 -57
  205. package/dist/schema/index.js +28 -15
  206. package/dist/schema/index.js.map +1 -1
  207. package/dist/{sqlite-XrDxNaa7.d.cts → sqlite-D7zBinNR.d.cts} +92 -1
  208. package/dist/{sqlite-CGu5SLsw.d.ts → sqlite-DvQndLky.d.ts} +92 -1
  209. package/dist/{store-view-D09po4H6.d.cts → store-view-BSpqvAxK.d.cts} +415 -44
  210. package/dist/{store-view-BlgnhEYf.d.ts → store-view-CKbHFRnn.d.ts} +415 -44
  211. package/dist/{types-DiE1AO7p.d.cts → types-Bv9DcS_E.d.cts} +2 -2
  212. package/dist/{types-B1TbZJYS.d.ts → types-CoxuV7EJ.d.ts} +2 -2
  213. package/dist/{where-DKjuofPr.d.cts → where-7RKFOsXt.d.ts} +2 -2
  214. package/dist/{where-DrUIIWHb.d.ts → where-DdMIXOtH.d.cts} +2 -2
  215. package/package.json +14 -13
  216. package/dist/chunk-2EBMPJBO.js +0 -16
  217. package/dist/chunk-2EBMPJBO.js.map +0 -1
  218. package/dist/chunk-2JXB3NYJ.cjs.map +0 -1
  219. package/dist/chunk-2MRAX3UE.js.map +0 -1
  220. package/dist/chunk-3BRVBQQD.cjs.map +0 -1
  221. package/dist/chunk-3EUKJEQ3.js +0 -77
  222. package/dist/chunk-3EUKJEQ3.js.map +0 -1
  223. package/dist/chunk-3Z6TW3CT.cjs.map +0 -1
  224. package/dist/chunk-4DIBPQNJ.js.map +0 -1
  225. package/dist/chunk-4S2FXGBJ.js +0 -9
  226. package/dist/chunk-4S2FXGBJ.js.map +0 -1
  227. package/dist/chunk-5IQIX7LU.cjs.map +0 -1
  228. package/dist/chunk-6GNSY7AV.js.map +0 -1
  229. package/dist/chunk-6HVOHZHS.cjs.map +0 -1
  230. package/dist/chunk-6SCVW4AS.js +0 -20
  231. package/dist/chunk-6SCVW4AS.js.map +0 -1
  232. package/dist/chunk-A7SZZFAG.js.map +0 -1
  233. package/dist/chunk-CHXTTSC6.js.map +0 -1
  234. package/dist/chunk-CYX2JTDG.js.map +0 -1
  235. package/dist/chunk-DEQFL35G.js.map +0 -1
  236. package/dist/chunk-G6SOJEDI.cjs.map +0 -1
  237. package/dist/chunk-GQANNXXT.cjs.map +0 -1
  238. package/dist/chunk-GZCNZ7QW.js +0 -24
  239. package/dist/chunk-GZCNZ7QW.js.map +0 -1
  240. package/dist/chunk-HK5SATYM.cjs.map +0 -1
  241. package/dist/chunk-HMFK4BXR.cjs +0 -12
  242. package/dist/chunk-HTGDWIDE.cjs +0 -89
  243. package/dist/chunk-HTGDWIDE.cjs.map +0 -1
  244. package/dist/chunk-JIEJCTDS.cjs.map +0 -1
  245. package/dist/chunk-JJRCLAUN.cjs.map +0 -1
  246. package/dist/chunk-JQGIPGVL.cjs.map +0 -1
  247. package/dist/chunk-K2TGS3EN.js.map +0 -1
  248. package/dist/chunk-KGC4LY5T.cjs.map +0 -1
  249. package/dist/chunk-KNHYNQQ3.cjs +0 -663
  250. package/dist/chunk-KNHYNQQ3.cjs.map +0 -1
  251. package/dist/chunk-KUTNI6SI.js.map +0 -1
  252. package/dist/chunk-LANHCOXB.js.map +0 -1
  253. package/dist/chunk-N6WTUSBV.cjs +0 -23
  254. package/dist/chunk-N6WTUSBV.cjs.map +0 -1
  255. package/dist/chunk-NAUA4PW5.cjs +0 -28
  256. package/dist/chunk-NAUA4PW5.cjs.map +0 -1
  257. package/dist/chunk-QV245VMN.js.map +0 -1
  258. package/dist/chunk-RBV7YCQD.js.map +0 -1
  259. package/dist/chunk-RHZWOJVA.cjs.map +0 -1
  260. package/dist/chunk-RRKQAKD6.cjs.map +0 -1
  261. package/dist/chunk-RY2VIWAM.js.map +0 -1
  262. package/dist/chunk-SHLY4MCM.js.map +0 -1
  263. package/dist/chunk-T2DWPQNE.js +0 -649
  264. package/dist/chunk-T2DWPQNE.js.map +0 -1
  265. package/dist/chunk-T6SPUYLX.cjs.map +0 -1
  266. package/dist/chunk-TGJE22RM.cjs.map +0 -1
  267. package/dist/chunk-TOTPVGOG.cjs +0 -774
  268. package/dist/chunk-TOTPVGOG.cjs.map +0 -1
  269. package/dist/chunk-TQOYM4LN.js.map +0 -1
  270. package/dist/chunk-UJLOZYKT.cjs.map +0 -1
  271. package/dist/chunk-UNNPKQUG.cjs.map +0 -1
  272. package/dist/chunk-V3QNTB7Z.js.map +0 -1
  273. package/dist/chunk-VCGO4V26.js.map +0 -1
  274. package/dist/chunk-WLRIWYHQ.js.map +0 -1
  275. package/dist/chunk-WVC2Q2FD.cjs.map +0 -1
  276. package/dist/chunk-XXVEULWX.js +0 -39
  277. package/dist/chunk-XXVEULWX.js.map +0 -1
  278. package/dist/chunk-YAZE7QZR.cjs +0 -32
  279. package/dist/chunk-YAZE7QZR.cjs.map +0 -1
  280. package/dist/chunk-YC6PWTHY.cjs +0 -44
  281. package/dist/chunk-YC6PWTHY.cjs.map +0 -1
  282. package/dist/chunk-Z5HBYM7U.cjs +0 -11
  283. package/dist/chunk-Z5HBYM7U.cjs.map +0 -1
  284. package/dist/chunk-Z5XBMDXU.js.map +0 -1
  285. package/dist/chunk-ZP5WHI4C.cjs.map +0 -1
  286. package/dist/chunk-ZURTTSCM.js.map +0 -1
  287. package/dist/chunk-ZV5N4DBG.js.map +0 -1
@@ -1,24 +1,24 @@
1
- import { quoteIdentifier, EMPTY_REQUIRED_COLUMNS, addRequiredColumn, markSelectiveFieldAsRequired, markFieldRefAsRequired, NODE_COLUMNS, shouldProjectColumn, mapSelectiveSystemFieldToColumn, isIdFieldRef, isAggregateExpr, EDGE_COLUMNS } from './chunk-3EUKJEQ3.js';
2
- import { createEdgeRowMapper, createNodeRowMapper, POSTGRES_ROW_MAPPER_CONFIG, SQLITE_ROW_MAPPER_CONFIG } from './chunk-QV245VMN.js';
3
- import { runHookedWriteOperation, runInWriteTransaction } from './chunk-6SCVW4AS.js';
4
- import { loadAndMergeGraphExtensionDocument, ensureSchema, loadAndVerifyGraph, mergeGraphExtension, commitNewSchemaVersion, applyDeprecatedKinds, loadActiveSchemaWithBootstrap, parseSerializedSchema, computeClosuresFromOntology, createEmptyClosures, KindRegistry, extensionKindNames, serializeSchemaProperties, buildGraphExtensionOntologyKeySet, compileTimeOntologyKey, getTypeName, validateNodeProps, validateEdgeProps } from './chunk-K2TGS3EN.js';
1
+ import { createEdgeRowMapper, createNodeRowMapper, POSTGRES_ROW_MAPPER_CONFIG, SQLITE_ROW_MAPPER_CONFIG, quoteIdentifier, EMPTY_REQUIRED_COLUMNS, addRequiredColumn, mapSelectiveSystemFieldToColumn, markFieldRefAsRequired, findSelectivePropsFieldForFieldRef, markSelectiveFieldAsRequired, NODE_COLUMNS, shouldProjectColumn, isIdFieldRef, isAggregateExpr, EDGE_COLUMNS } from './chunk-55MZDDZV.js';
2
+ import { rowToNode, executeNodeBulkFindByIndex, executeNodeBulkFindByConstraint, executeNodeFindByConstraint, executeNodeBulkGetOrCreateByConstraint, executeNodeGetOrCreateByConstraint, executeNodeHardDelete, executeNodeDelete, executeNodeUpsertUpdate, executeNodeUpdate, executeNodeCreateNoReturnBatch, executeNodeCreateBatch, executeNodeCreate, rowToEdge, runInWriteTransaction, checkCardinalityConstraint, runHookedWriteOperation, rowToNodeMeta, rowToEdgeMeta } from './chunk-QNKSQTKQ.js';
3
+ import { loadAndMergeGraphExtensionDocument, ensureSchema, loadAndVerifyGraph, buildKindRegistry, mergeGraphExtension, commitNewSchemaVersion, applyDeprecatedKinds, loadActiveSchemaWithBootstrap, parseSerializedSchema, extensionKindNames, serializeSchemaProperties, buildGraphExtensionOntologyKeySet, compileTimeOntologyKey, getTypeName, serializeIndexDeclaration } from './chunk-K3PPLHY7.js';
5
4
  import { sortedReplacer, canonicalEqual } from './chunk-UR7W5YQK.js';
6
- import { isKnownKind, getNodeKinds, getEdgeKinds } from './chunk-CHXTTSC6.js';
7
- import { generateIndexDDL } from './chunk-SHLY4MCM.js';
8
- import { getDialect, assertVectorMinScore, compileNodeIndexFieldKeys, compileIndexWhere, vectorScoreExpression, vectorMinScoreCondition } from './chunk-KUTNI6SI.js';
9
- import { NODE_META_KEYS, EDGE_META_KEYS } from './chunk-4DIBPQNJ.js';
10
- import { sha256Hex, fnv1aBase36 } from './chunk-K34OPSJK.js';
11
- import { createSchemaIntrospector } from './chunk-BVTWD66D.js';
12
- import { IncompatibleChangeError, filterReservedKeys, RESERVED_NODE_KEYS, RESERVED_EDGE_KEYS, RemoveCompileTimeKindError, KindHasReferentsError, validateProjectionField } from './chunk-ZURTTSCM.js';
13
- import { compareStrings } from './chunk-4S2FXGBJ.js';
14
- import { resolveReadCoordinate, withRecordedCoordinate, describeCoordinate, coordinateContext, DEFAULT_SQL_SCHEMA, requireSqlSchema, recordedReadSchemaFor, asCompiledSelectSql, getSearchableFields, createSqlSchema, requireExternalRecordedReadSource, createRecordedReadBinding, createRecordedBackend, readRecordedClock, asRecordedInstant, recordedCaptureRequiresCallbackTransactionError, assertRecordedCaptureTransactionIsolation, createRecordedTransactionScope, lockRecordedGraphWrite, createHistoryUnsafeSqlRef, generateId, vectorSlotKey, getEmbeddingFields, computeFulltextContent, recordedReadSqlSchema, requireRecordedReadBinding, asCompiledRowsSql, getKindsForUniquenessCheck, checkWherePredicate, computeUniqueKey, applyNodeHardDelete, createNodeWriteContext, applyNodeSoftDelete, runInsertBatch, nodeInsertDispatch, runInsertBatchReturning, edgeInsertDispatch, getNodeRowsByIds, getEdgeRowsByIds, applyNodeUpdate, applyNodeInsertSideEffects, checkCardinality, checkUniqueEdge, validateEdgeEndpoints, assertValidRecordedInstant, checkUniquenessConstraints, createUniquenessContext, recordedBindParamBudget, checkDisjointness, closeRecordedHardDeletedKind, asCompiledStatementSql, EMBEDDINGS_CTE_ALIAS, FULLTEXT_CTE_ALIAS, HYBRID_CANDIDATES_CTE_ALIAS, ALIAS_CTE_PREFIX } from './chunk-A7SZZFAG.js';
15
- import { isParameterRef, fieldRef, buildFieldBuilderForTypeInfo, stringField, createFulltextAccessor, runOptionallyInTransaction, baseField, objectField, arrayField, dateField, numberField, isLiveNodeRow, createBackendOverlay, compileKindFilter, chunk, sqlValueList, buildPredicateIndex, compileFieldValue, extractVectorSimilarityPredicates, extractFulltextMatchPredicates, compilePredicateExpression, getPredicatesForAlias, getNodeKindsForAlias, getHybridTargetAlias, compilePredicateClauses } from './chunk-V3QNTB7Z.js';
16
- import { nowIso, validateOptionalCanonicalIsoDate } from './chunk-XXVEULWX.js';
17
- import { withRecordedRelationsPrecondition, isMissingTableError } from './chunk-2MRAX3UE.js';
18
- import { DEFAULT_PAGINATION_LIMIT, DEFAULT_STREAM_BATCH_SIZE, SQLITE_PATH_DELIMITER, PG_ARRAY_START, PG_ARRAY_END, PG_PATH_ELEMENT_SEPARATOR, MAX_PG_IDENTIFIER_LENGTH } from './chunk-2EBMPJBO.js';
19
- import { resolveEmbeddingFields } from './chunk-Z5XBMDXU.js';
20
- import { ConfigurationError, UnsupportedPredicateError, ValidationError, KindNotFoundError, EndpointError, EagerMaterializationError, CompilerInvariantError, UniquenessError, DatabaseOperationError, CardinalityError, EdgeNotFoundError, NodeIndexNotFoundError, NodeConstraintNotFoundError, NodeNotFoundError, EndpointNotFoundError } from './chunk-TQOYM4LN.js';
21
- import { jsonPointer, parseJsonPointer, resolveJsonPointer } from './chunk-K5P5OZXS.js';
5
+ import { isKnownKind, validateEdgeProps } from './chunk-QWEUYILR.js';
6
+ import { generateIndexDDL } from './chunk-UZV3AZKF.js';
7
+ import { NODE_META_KEYS, EDGE_META_KEYS } from './chunk-OR5ZA5WP.js';
8
+ import { sha256Hex, fnv1aBase36 } from './chunk-W63OVYY4.js';
9
+ import { getDialect, assertVectorMinScore, vectorScoreExpression, vectorMinScoreCondition } from './chunk-FPIZFYHE.js';
10
+ import { createSchemaIntrospector } from './chunk-5SEGFTZX.js';
11
+ import { IncompatibleChangeError, RemoveCompileTimeKindError, KindHasReferentsError } from './chunk-QOTI55H7.js';
12
+ import { validateProjectionField } from './chunk-RGPG7EOJ.js';
13
+ import { compareStrings, compareCodePoints } from './chunk-BYPOGQWK.js';
14
+ import { resolveReadCoordinate, withRecordedCoordinate, describeCoordinate, coordinateContext, DEFAULT_SQL_SCHEMA, requireSqlSchema, recordedReadSchemaFor, assertRevisionTrackableBackend, createSqlSchema, requireExternalRecordedReadSource, createRecordedReadBinding, createRecordedBackend, readRecordedClock, asRecordedInstant, ensureRevisionOrigin, withRecordedFlushObserver, recordedCaptureRequiresCallbackTransactionError, assertRecordedCaptureTransactionIsolation, createRecordedTransactionScope, lockRecordedGraphWrite, createHistoryUnsafeSqlRef, createRevisionTrackingUnsafeSqlRef, generateId, vectorSlotKey, computeFulltextContent, advanceRevisionClock, recordedReadSqlSchema, requireRecordedReadBinding, runInsertBatch, edgeInsertDispatch, runInsertBatchReturning, getEmbeddingFields, getNodeRowsByIds, getEdgeRowsByIds, assertValidRecordedInstant, validateEdgeEndpoints, recordedBindParamBudget, closeRecordedHardDeletedKind, ALIAS_CTE_PREFIX, EMBEDDINGS_CTE_ALIAS, FULLTEXT_CTE_ALIAS, HYBRID_CANDIDATES_CTE_ALIAS } from './chunk-CZQ3FO2Q.js';
15
+ import { markAnnIndexScan, asCompiledSelectSql, isRawExecutable, annIndexScanTypes, asCompiledRowsSql, markForceCustomPlan, asCompiledStatementSql } from './chunk-UT3S4SEN.js';
16
+ import { isParameterRef, fieldRef, buildFieldBuilderForTypeInfo, stringField, createFulltextAccessor, CURRENT_READ_INSTANT_PLACEHOLDER, withPinnedReadInstant, runOptionallyInTransaction, baseField, objectField, arrayField, dateField, numberField, rowPropsToObject, createBackendOverlay, compileKindFilter, currentReadInstant, compileTemporalFilter, sqlValueList, chunk, buildPredicateIndex, compileFieldValue, extractVectorSimilarityPredicates, extractFulltextMatchPredicates, currentReadInstantFor, extractTemporalOptions, compilePredicateExpression, getPredicatesForAlias, getNodeKindsForAlias, getHybridTargetAlias, compilePredicateClauses } from './chunk-JBTOYDMN.js';
17
+ import { withRecordedRelationsPrecondition, nowIso, validateOptionalCanonicalIsoDate, isMissingTableError } from './chunk-I7ZUZKFD.js';
18
+ import { DEFAULT_PAGINATION_LIMIT, DEFAULT_STREAM_BATCH_SIZE, SQLITE_PATH_DELIMITER, PG_ARRAY_START, PG_ARRAY_END, PG_PATH_ELEMENT_SEPARATOR, MAX_PG_IDENTIFIER_LENGTH } from './chunk-OHMP6N2J.js';
19
+ import { getSearchableFields, resolveEmbeddingFields, resolveDeclaredFulltextLanguage } from './chunk-KBGQIPRQ.js';
20
+ import { ConfigurationError, UnsupportedPredicateError, ValidationError, KindNotFoundError, EndpointError, EagerMaterializationError, CompilerInvariantError, CardinalityError, EdgeNotFoundError, DatabaseOperationError, EndpointNotFoundError } from './chunk-MP2JQKSG.js';
21
+ import { jsonPointer, parseJsonPointer, resolveJsonPointer } from './chunk-G6Q5UM7U.js';
22
22
  import { Placeholder, sql } from 'drizzle-orm';
23
23
 
24
24
  // src/store/collection-surface.ts
@@ -27,11 +27,38 @@ var CURRENT_ONLY_READ_NAMES = [
27
27
  "bulkFindByConstraint",
28
28
  "bulkFindByIndex"
29
29
  ];
30
+ var NODE_WRITE_NAMES = [
31
+ "create",
32
+ "createFromRecord",
33
+ "update",
34
+ "delete",
35
+ "hardDelete",
36
+ "upsertById",
37
+ "upsertByIdFromRecord",
38
+ "bulkCreate",
39
+ "bulkUpsertById",
40
+ "bulkInsert",
41
+ "bulkDelete",
42
+ "getOrCreateByConstraint",
43
+ "bulkGetOrCreateByConstraint"
44
+ ];
30
45
  var EDGE_BATCH_READ_NAMES = [
31
46
  "batchFindFrom",
32
47
  "batchFindTo",
33
48
  "batchFindByEndpoints"
34
49
  ];
50
+ var EDGE_WRITE_NAMES = [
51
+ "create",
52
+ "update",
53
+ "delete",
54
+ "hardDelete",
55
+ "bulkCreate",
56
+ "bulkUpsertById",
57
+ "bulkInsert",
58
+ "bulkDelete",
59
+ "getOrCreateByEndpoints",
60
+ "bulkGetOrCreateByEndpoints"
61
+ ];
35
62
 
36
63
  // src/store/recorded-coordinate-guard.ts
37
64
  function hasOwnRecordedAsOf(options) {
@@ -211,9 +238,6 @@ function pinnedNodeCollection(live, coordinate) {
211
238
  const reads = {
212
239
  getById: (id) => live.getById(id, temporal),
213
240
  getByIds: (ids) => live.getByIds(ids, temporal),
214
- // `live.find` is the NodeCollection read, not Array#find — the
215
- // (filter, temporal) shape trips the array-method heuristics.
216
- // eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
217
241
  find: (filter) => live.find(filter, temporal),
218
242
  count: () => live.count(temporal)
219
243
  };
@@ -229,9 +253,6 @@ function pinnedEdgeCollection(live, coordinate) {
229
253
  const reads = {
230
254
  getById: (id) => live.getById(id, temporal),
231
255
  getByIds: (ids) => live.getByIds(ids, temporal),
232
- // `live.find` is the EdgeCollection read, not Array#find — the
233
- // (filter, temporal) shape trips the array-method heuristics.
234
- // eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
235
256
  find: (filter) => live.find(filter, temporal),
236
257
  count: (filter) => live.count(filter, temporal),
237
258
  findFrom: (from) => live.findFrom(from, temporal),
@@ -885,6 +906,29 @@ function emitStandardQuerySql(input) {
885
906
  return sql.join(parts, sql` `);
886
907
  }
887
908
 
909
+ // src/utils/identifier.ts
910
+ var TEXT_ENCODER = new TextEncoder();
911
+ function truncateToBytes(value, maxBytes) {
912
+ const encoded = TEXT_ENCODER.encode(value);
913
+ if (encoded.byteLength <= maxBytes) return value;
914
+ let end = maxBytes;
915
+ while (end > 0 && encoded[end] >= 128 && encoded[end] < 192) {
916
+ end--;
917
+ }
918
+ return new TextDecoder().decode(encoded.slice(0, end));
919
+ }
920
+ function boundPgIdentifier(name, hashInput) {
921
+ if (TEXT_ENCODER.encode(name).byteLength <= MAX_PG_IDENTIFIER_LENGTH) {
922
+ return name;
923
+ }
924
+ const hash = fnv1aBase36(hashInput);
925
+ const truncated = truncateToBytes(
926
+ name,
927
+ MAX_PG_IDENTIFIER_LENGTH - 1 - hash.length
928
+ );
929
+ return `${truncated}_${hash}`;
930
+ }
931
+
888
932
  // src/query/ast.ts
889
933
  var DEFAULT_RRF_K = 60;
890
934
  var DEFAULT_RRF_WEIGHT = 1;
@@ -926,6 +970,14 @@ function compileTypedJsonExtract(input) {
926
970
  }
927
971
  }
928
972
  }
973
+ function compileSelectivePropsExtraction(field2, column, dialect) {
974
+ return compileTypedJsonExtract({
975
+ column,
976
+ dialect,
977
+ pointer: jsonPointer([field2.field]),
978
+ valueType: field2.valueType
979
+ });
980
+ }
929
981
 
930
982
  // src/query/compiler/emitter/standard-builders.ts
931
983
  function compileColumnReference(tableAlias, column) {
@@ -938,6 +990,11 @@ function qualifyColumn(owner, name) {
938
990
  return sql`${quoteIdentifier(owner)}.${quoteIdentifier(name)}`;
939
991
  }
940
992
  var SCOPED_RELEVANCE_NODES_ALIAS = "scoped_relevance_nodes";
993
+ function selectivePropsCteColumnName(field2) {
994
+ const { alias, field: fieldName } = field2;
995
+ const encoded = `__tg_${alias.length}:${alias}:${fieldName.length}:${fieldName}`;
996
+ return boundPgIdentifier(encoded, `${alias}\0${fieldName}`);
997
+ }
941
998
  function buildScopedNodeIdsSubquery(nodeAlias) {
942
999
  const cteAlias = `${ALIAS_CTE_PREFIX}${nodeAlias}`;
943
1000
  return sql`
@@ -949,19 +1006,52 @@ function buildScopedNodeIdsSubquery(nodeAlias) {
949
1006
  ) AS ${sql.raw(SCOPED_RELEVANCE_NODES_ALIAS)}
950
1007
  `;
951
1008
  }
952
- function compileNodeSelectColumns(tableAlias, alias, requiredColumns) {
953
- return NODE_COLUMNS.filter(
1009
+ function compileSelectivePropsSelectColumns(input) {
1010
+ const { alias, dialect, selectiveFields, tableAlias } = input;
1011
+ const propsFields = selectiveFields?.filter(
1012
+ (field2) => !field2.isSystemField && field2.alias === alias
1013
+ ) ?? [];
1014
+ if (propsFields.length === 0) return [];
1015
+ const propsColumn = compileColumnReference(tableAlias, "props");
1016
+ return propsFields.map((field2) => {
1017
+ const extracted = compileSelectivePropsExtraction(
1018
+ field2,
1019
+ propsColumn,
1020
+ dialect
1021
+ );
1022
+ return sql`${extracted} AS ${quoteIdentifier(selectivePropsCteColumnName(field2))}`;
1023
+ });
1024
+ }
1025
+ function appendSelectivePropsColumns(baseColumns, input) {
1026
+ return [...baseColumns, ...compileSelectivePropsSelectColumns(input)];
1027
+ }
1028
+ function compileNodeSelectColumns(input) {
1029
+ const { alias, dialect, requiredColumns, selectiveFields, tableAlias } = input;
1030
+ const baseColumns = NODE_COLUMNS.filter(
954
1031
  (column) => column === "id" || column === "kind" || shouldProjectColumn(requiredColumns, column)
955
1032
  ).map(
956
1033
  (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
957
1034
  );
1035
+ return appendSelectivePropsColumns(baseColumns, {
1036
+ alias,
1037
+ dialect,
1038
+ selectiveFields,
1039
+ tableAlias
1040
+ });
958
1041
  }
959
- function compileEdgeSelectColumns(tableAlias, alias, requiredColumns) {
960
- return EDGE_COLUMNS.filter(
1042
+ function compileEdgeSelectColumns(input) {
1043
+ const { alias, dialect, requiredColumns, selectiveFields, tableAlias } = input;
1044
+ const baseColumns = EDGE_COLUMNS.filter(
961
1045
  (column) => shouldProjectColumn(requiredColumns, column)
962
1046
  ).map(
963
1047
  (column) => sql`${compileColumnReference(tableAlias, column)} AS ${sql.raw(`${alias}_${column}`)}`
964
1048
  );
1049
+ return appendSelectivePropsColumns(baseColumns, {
1050
+ alias,
1051
+ dialect,
1052
+ selectiveFields,
1053
+ tableAlias
1054
+ });
965
1055
  }
966
1056
  function buildStandardStartCte(input) {
967
1057
  const { ast, ctx, graphId, predicateIndex, requiredColumnsByAlias } = input;
@@ -990,7 +1080,13 @@ function buildStandardStartCte(input) {
990
1080
  return sql`
991
1081
  cte_${sql.raw(alias)} AS ${cteMaterialization}(
992
1082
  SELECT ${sql.join(
993
- compileNodeSelectColumns(void 0, alias, effectiveRequiredColumns),
1083
+ compileNodeSelectColumns({
1084
+ alias,
1085
+ dialect: ctx.dialect,
1086
+ requiredColumns: effectiveRequiredColumns,
1087
+ selectiveFields: ast.selectiveFields,
1088
+ tableAlias: void 0
1089
+ }),
994
1090
  sql`, `
995
1091
  )}
996
1092
  FROM ${ctx.schema.nodesTable}
@@ -1055,8 +1151,20 @@ function buildStandardTraversalCte(input) {
1055
1151
  ];
1056
1152
  const selectColumns = [
1057
1153
  ...previousRowColumns,
1058
- ...compileEdgeSelectColumns("e", edgeAlias, requiredEdgeColumns),
1059
- ...compileNodeSelectColumns("n", nodeAlias, requiredNodeColumns)
1154
+ ...compileEdgeSelectColumns({
1155
+ alias: edgeAlias,
1156
+ dialect: ctx.dialect,
1157
+ requiredColumns: requiredEdgeColumns,
1158
+ selectiveFields: ast.selectiveFields,
1159
+ tableAlias: "e"
1160
+ }),
1161
+ ...compileNodeSelectColumns({
1162
+ alias: nodeAlias,
1163
+ dialect: ctx.dialect,
1164
+ requiredColumns: requiredNodeColumns,
1165
+ selectiveFields: ast.selectiveFields,
1166
+ tableAlias: "n"
1167
+ })
1060
1168
  ];
1061
1169
  const cteMaterialization = materializeCte ? sql`MATERIALIZED ` : ctx.dialect.capabilities.emitNotMaterializedHint ? sql`NOT MATERIALIZED ` : sql``;
1062
1170
  function compileTraversalBranch(branch) {
@@ -1196,7 +1304,6 @@ function buildStandardProjection(input) {
1196
1304
  if (ast.selectiveFields && ast.selectiveFields.length > 0) {
1197
1305
  return compileSelectiveProjection(
1198
1306
  ast.selectiveFields,
1199
- dialect,
1200
1307
  ast,
1201
1308
  collapsedTraversalCteAlias
1202
1309
  );
@@ -1221,7 +1328,7 @@ function buildAliasToCteMap(ast) {
1221
1328
  }
1222
1329
  return map;
1223
1330
  }
1224
- function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteAlias) {
1331
+ function compileSelectiveProjection(fields, ast, collapsedTraversalCteAlias) {
1225
1332
  const aliasToCte = buildAliasToCteMap(ast);
1226
1333
  const columns = fields.map((field2) => {
1227
1334
  const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(field2.alias) ?? `cte_${field2.alias}`;
@@ -1229,19 +1336,20 @@ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteA
1229
1336
  const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
1230
1337
  return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
1231
1338
  }
1232
- const propsColumn = `${field2.alias}_props`;
1233
- const column = sql`${sql.raw(cteAlias)}.${sql.raw(propsColumn)}`;
1234
- const pointer = jsonPointer([field2.field]);
1235
- const extracted = compileTypedJsonExtract({
1236
- column,
1237
- dialect,
1238
- pointer,
1239
- valueType: field2.valueType
1240
- });
1241
- return sql`${extracted} AS ${quoteIdentifier(field2.outputName)}`;
1339
+ return sql`${sql.raw(cteAlias)}.${quoteIdentifier(selectivePropsCteColumnName(field2))} AS ${quoteIdentifier(field2.outputName)}`;
1242
1340
  });
1243
1341
  return sql.join(columns, sql`, `);
1244
1342
  }
1343
+ function compileOrderFieldValue(ast, field2, dialect, cteAlias) {
1344
+ const selectiveField = findSelectivePropsFieldForFieldRef(
1345
+ ast.selectiveFields,
1346
+ field2
1347
+ );
1348
+ if (selectiveField !== void 0) {
1349
+ return sql`${sql.raw(cteAlias)}.${quoteIdentifier(selectivePropsCteColumnName(selectiveField))}`;
1350
+ }
1351
+ return compileFieldValue(field2, dialect, field2.valueType, cteAlias);
1352
+ }
1245
1353
  function buildRelevanceJoins(vectorPredicate, fulltextPredicate) {
1246
1354
  const hybridTargetAlias = getHybridTargetAlias(
1247
1355
  vectorPredicate,
@@ -1309,23 +1417,25 @@ function buildStandardFromClause(input) {
1309
1417
  }
1310
1418
  function buildStandardOrderBy(input) {
1311
1419
  const { ast, collapsedTraversalCteAlias, dialect } = input;
1312
- if (!ast.orderBy || ast.orderBy.length === 0) {
1420
+ const fieldOrderBy = ast.orderBy ?? [];
1421
+ const aggregateOrderBy = ast.aggregateOrderBy ?? [];
1422
+ if (fieldOrderBy.length === 0 && aggregateOrderBy.length === 0) {
1313
1423
  return void 0;
1314
1424
  }
1315
- const aliasToCte = buildAliasToCteMap(ast);
1425
+ const aliasToCte = fieldOrderBy.length > 0 ? buildAliasToCteMap(ast) : void 0;
1316
1426
  const parts = [];
1317
- for (const orderSpec of ast.orderBy) {
1427
+ for (const orderSpec of fieldOrderBy) {
1318
1428
  const valueType = orderSpec.field.valueType;
1319
1429
  if (valueType === "array" || valueType === "object") {
1320
1430
  throw new UnsupportedPredicateError(
1321
1431
  "Ordering by JSON arrays or objects is not supported"
1322
1432
  );
1323
1433
  }
1324
- const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(orderSpec.field.alias) ?? `cte_${orderSpec.field.alias}`;
1325
- const field2 = compileFieldValue(
1434
+ const cteAlias = collapsedTraversalCteAlias ?? aliasToCte?.get(orderSpec.field.alias) ?? `cte_${orderSpec.field.alias}`;
1435
+ const field2 = compileOrderFieldValue(
1436
+ ast,
1326
1437
  orderSpec.field,
1327
1438
  dialect,
1328
- valueType,
1329
1439
  cteAlias
1330
1440
  );
1331
1441
  const direction = sql.raw(orderSpec.direction.toUpperCase());
@@ -1336,6 +1446,15 @@ function buildStandardOrderBy(input) {
1336
1446
  sql`${field2} ${direction}`
1337
1447
  );
1338
1448
  }
1449
+ for (const orderSpec of aggregateOrderBy) {
1450
+ const column = quoteIdentifier(orderSpec.outputName);
1451
+ const direction = sql.raw(orderSpec.direction.toUpperCase());
1452
+ const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
1453
+ const nullsKeyword = sql.raw(
1454
+ nulls === "first" ? "NULLS FIRST" : "NULLS LAST"
1455
+ );
1456
+ parts.push(sql`${column} ${direction} ${nullsKeyword}`);
1457
+ }
1339
1458
  return sql`ORDER BY ${sql.join(parts, sql`, `)}`;
1340
1459
  }
1341
1460
  function fieldRefKey(field2) {
@@ -1418,12 +1537,48 @@ function buildStandardEmbeddingsCte(input) {
1418
1537
  );
1419
1538
  const branches = declaringKinds.map((kind) => {
1420
1539
  const tableName = vectorStrategy.tableName(graphId, kind, fieldPath);
1421
- const branchMetric = metric ?? vectorSlots?.get(vectorSlotKey(kind, fieldPath))?.metric ?? "cosine";
1540
+ const slotDescriptor = vectorSlots?.get(vectorSlotKey(kind, fieldPath));
1541
+ const branchMetric = metric ?? slotDescriptor?.metric ?? "cosine";
1542
+ const engineFormEligible = vectorPredicate.approximate === true || vectorStrategy.searchIsExact === true;
1543
+ const annSlot = engineFormEligible && branchMetric === slotDescriptor?.metric ? slotDescriptor : void 0;
1544
+ if (annSlot !== void 0) {
1545
+ ctx.annIndexTypes?.add(annSlot.indexType);
1546
+ const candidateCte = `${ALIAS_CTE_PREFIX}${field2.alias}`;
1547
+ const kindCandidates = sql`SELECT ${qualifyColumn(candidateCte, `${field2.alias}_id`)} FROM ${quoteIdentifier(candidateCte)} WHERE ${qualifyColumn(candidateCte, `${field2.alias}_kind`)} = ${kind}`;
1548
+ const annSql = vectorStrategy.buildSearch(
1549
+ {
1550
+ graphId,
1551
+ nodeKind: kind,
1552
+ fieldPath,
1553
+ dimensions: annSlot.dimensions,
1554
+ metric: branchMetric,
1555
+ indexType: annSlot.indexType
1556
+ },
1557
+ {
1558
+ graphId,
1559
+ nodeKind: kind,
1560
+ fieldPath,
1561
+ queryEmbedding,
1562
+ metric: branchMetric,
1563
+ dimensions: annSlot.dimensions,
1564
+ indexType: annSlot.indexType,
1565
+ limit: vectorPredicate.limit,
1566
+ ...minScore === void 0 ? {} : { minScore }
1567
+ },
1568
+ kindCandidates
1569
+ );
1570
+ const distanceFromScore = branchMetric === "cosine" ? sql.raw("(1.0 - score)") : sql.raw("score");
1571
+ return sql`
1572
+ SELECT node_id, ${kind} AS node_kind, ${distanceFromScore} AS distance, score
1573
+ FROM (${annSql}) AS tg_ann_src
1574
+ `;
1575
+ }
1422
1576
  const distanceExpr = vectorStrategy.distanceExpression(
1423
1577
  qualifyColumn(tableName, "embedding"),
1424
1578
  queryEmbedding,
1425
1579
  branchMetric
1426
1580
  );
1581
+ const exactDistanceExpr = sql`(${distanceExpr} + 0.0)`;
1427
1582
  const scoreExpr = vectorScoreExpression(distanceExpr, branchMetric);
1428
1583
  const conditions = [
1429
1584
  sql`${qualifyColumn(tableName, "graph_id")} = ${graphId}`
@@ -1437,7 +1592,7 @@ function buildStandardEmbeddingsCte(input) {
1437
1592
  SELECT
1438
1593
  ${qualifyColumn(tableName, "node_id")} AS node_id,
1439
1594
  ${kind} AS node_kind,
1440
- ${distanceExpr} AS distance,
1595
+ ${exactDistanceExpr} AS distance,
1441
1596
  ${scoreExpr} AS score
1442
1597
  FROM ${quoteIdentifier(tableName)}
1443
1598
  INNER JOIN ${scopedNodes}
@@ -1496,17 +1651,18 @@ function buildStandardFulltextCte(input) {
1496
1651
  `Fulltext match predicates are not supported for dialect "${dialect.name}"`
1497
1652
  );
1498
1653
  }
1654
+ const effectiveLanguage = language ?? sharedDeclaredLanguage(ctx.fulltextLanguages, nodeKinds);
1499
1655
  const matchCondition = fulltextStrategy.matchCondition(
1500
1656
  tableName,
1501
1657
  query,
1502
1658
  mode,
1503
- language
1659
+ effectiveLanguage
1504
1660
  );
1505
1661
  const rankExpression = fulltextStrategy.rankExpression(
1506
1662
  tableName,
1507
1663
  query,
1508
1664
  mode,
1509
- language
1665
+ effectiveLanguage
1510
1666
  );
1511
1667
  const conditions = [
1512
1668
  sql`${qualifyColumn(tableName, "graph_id")} = ${graphId}`,
@@ -1541,6 +1697,16 @@ function buildStandardFulltextCte(input) {
1541
1697
  )
1542
1698
  `;
1543
1699
  }
1700
+ function sharedDeclaredLanguage(fulltextLanguages, nodeKinds) {
1701
+ if (fulltextLanguages === void 0) return void 0;
1702
+ const languages = /* @__PURE__ */ new Set();
1703
+ for (const kind of nodeKinds) {
1704
+ const language = fulltextLanguages.get(kind);
1705
+ if (language !== void 0) languages.add(language);
1706
+ }
1707
+ if (languages.size !== 1) return void 0;
1708
+ return [...languages][0];
1709
+ }
1544
1710
  function buildStandardHybridCandidateCte() {
1545
1711
  return sql`
1546
1712
  ${sql.raw(HYBRID_CANDIDATES_CTE_ALIAS)} AS (
@@ -1562,10 +1728,10 @@ function compileUserOrderBy(ast, dialect) {
1562
1728
  );
1563
1729
  }
1564
1730
  const cteAlias = aliasToCte.get(orderSpec.field.alias) ?? `${ALIAS_CTE_PREFIX}${orderSpec.field.alias}`;
1565
- const field2 = compileFieldValue(
1731
+ const field2 = compileOrderFieldValue(
1732
+ ast,
1566
1733
  orderSpec.field,
1567
1734
  dialect,
1568
- valueType,
1569
1735
  cteAlias
1570
1736
  );
1571
1737
  const direction = sql.raw(orderSpec.direction.toUpperCase());
@@ -1712,51 +1878,10 @@ function runCompilerPass(state, pass) {
1712
1878
  state: pass.update(state, output)
1713
1879
  };
1714
1880
  }
1715
- function compileTemporalFilter(options) {
1716
- const { mode, asOf, tableAlias, currentTimestamp, recordedAsOf } = options;
1717
- const prefix = tableAlias ? sql.raw(`${tableAlias}.`) : sql.raw("");
1718
- const deletedAt = sql`${prefix}deleted_at`;
1719
- const validFrom = sql`${prefix}valid_from`;
1720
- const validTo = sql`${prefix}valid_to`;
1721
- const validFilter = (() => {
1722
- switch (mode) {
1723
- case "current": {
1724
- const now = recordedAsOf === void 0 ? currentTimestamp ?? sql`CURRENT_TIMESTAMP` : sql`${recordedAsOf}`;
1725
- return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${now}) AND (${validTo} IS NULL OR ${validTo} > ${now})`;
1726
- }
1727
- case "asOf": {
1728
- if (asOf === void 0) {
1729
- throw new Error(
1730
- `asOf timestamp is required for temporal mode "asOf"`
1731
- );
1732
- }
1733
- const timestamp = asOf;
1734
- return sql`${deletedAt} IS NULL AND (${validFrom} IS NULL OR ${validFrom} <= ${timestamp}) AND (${validTo} IS NULL OR ${validTo} > ${timestamp})`;
1735
- }
1736
- case "includeEnded": {
1737
- return sql`${deletedAt} IS NULL`;
1738
- }
1739
- case "includeTombstones": {
1740
- return sql.raw("1=1");
1741
- }
1742
- }
1743
- })();
1744
- if (recordedAsOf === void 0) return validFilter;
1745
- const recordedFrom = sql`${prefix}recorded_from`;
1746
- const recordedTo = sql`${prefix}recorded_to`;
1747
- return sql`(${validFilter}) AND ${recordedFrom} <= ${recordedAsOf} AND ${recordedAsOf} < ${recordedTo}`;
1748
- }
1749
- function extractTemporalOptions(ast, tableAlias) {
1750
- return {
1751
- mode: ast.temporalMode.mode,
1752
- asOf: ast.temporalMode.asOf,
1753
- recordedAsOf: ast.recordedAsOf,
1754
- tableAlias
1755
- };
1756
- }
1757
1881
 
1758
1882
  // src/query/compiler/passes/temporal.ts
1759
- function createTemporalFilterPass(ast, currentTimestamp) {
1883
+ function createTemporalFilterPass(ast, readInstant = "literal") {
1884
+ const currentTimestamp = currentReadInstantFor(readInstant);
1760
1885
  return {
1761
1886
  forAlias(tableAlias) {
1762
1887
  return compileTemporalFilter({
@@ -1873,12 +1998,14 @@ function appendAggregateSortLimitAndProjectNodes(currentNode, ast, nextPlanNodeI
1873
1998
  };
1874
1999
  node = ast.having === void 0 ? aggregateNode : { ...aggregateNode, having: ast.having };
1875
2000
  }
1876
- if (ast.orderBy !== void 0 && ast.orderBy.length > 0) {
2001
+ const hasFieldOrderBy = ast.orderBy !== void 0 && ast.orderBy.length > 0;
2002
+ const hasAggregateOrderBy = ast.aggregateOrderBy !== void 0 && ast.aggregateOrderBy.length > 0;
2003
+ if (hasFieldOrderBy || hasAggregateOrderBy) {
1877
2004
  node = {
1878
2005
  id: nextPlanNodeId(),
1879
2006
  input: node,
1880
2007
  op: "sort",
1881
- orderBy: ast.orderBy
2008
+ orderBy: ast.orderBy ?? []
1882
2009
  };
1883
2010
  }
1884
2011
  if (limit !== void 0 || ast.offset !== void 0) {
@@ -2237,7 +2364,7 @@ function runRecursiveQueryPassPipeline(ast, graphId, ctx) {
2237
2364
  execute(currentState) {
2238
2365
  return createTemporalFilterPass(
2239
2366
  currentState.ast,
2240
- currentState.ctx.dialect.currentTimestamp()
2367
+ currentState.ctx.readInstant
2241
2368
  );
2242
2369
  },
2243
2370
  update(currentState, temporalFilterPass) {
@@ -2645,12 +2772,7 @@ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
2645
2772
  return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier(field2.outputName)}`;
2646
2773
  }
2647
2774
  const column = sql.raw(`${field2.alias}_props`);
2648
- const extracted = compileTypedJsonExtract({
2649
- column,
2650
- dialect,
2651
- pointer: jsonPointer([field2.field]),
2652
- valueType: field2.valueType
2653
- });
2775
+ const extracted = compileSelectivePropsExtraction(field2, column, dialect);
2654
2776
  return sql`${extracted} AS ${quoteIdentifier(field2.outputName)}`;
2655
2777
  });
2656
2778
  const vl = traversal.variableLength;
@@ -2758,7 +2880,7 @@ function buildReachableCte(options) {
2758
2880
  sql.raw("e.kind"),
2759
2881
  options.edgeKinds
2760
2882
  );
2761
- const currentTimestamp = options.dialect.currentTimestamp();
2883
+ const currentTimestamp = currentReadInstant();
2762
2884
  const nodeTemporalFilter = compileTemporalFilter({
2763
2885
  mode: options.temporalMode,
2764
2886
  asOf: options.asOf,
@@ -2871,66 +2993,8 @@ function buildBidirectionalBranch(options) {
2871
2993
  return sql`${options.selectClause} FROM reachable r JOIN ${options.schema.edgesTable} e ON (e.from_id = r.id OR e.to_id = r.id) ${nodeJoin} WHERE ${sql.join([...options.whereClauses], sql` AND `)}`;
2872
2994
  }
2873
2995
 
2874
- // src/store/row-mappers.ts
2875
- function nullToUndefined2(value) {
2876
- return value === null ? void 0 : value;
2877
- }
2878
- function rowToNode(row) {
2879
- const rawProps = JSON.parse(row.props);
2880
- const props = filterReservedKeys(rawProps, RESERVED_NODE_KEYS);
2881
- return {
2882
- kind: row.kind,
2883
- id: row.id,
2884
- meta: rowToNodeMeta(row),
2885
- ...props
2886
- };
2887
- }
2888
- function rowToNodeMeta(row) {
2889
- return {
2890
- version: row.version,
2891
- validFrom: nullToUndefined2(row.valid_from),
2892
- validTo: nullToUndefined2(row.valid_to),
2893
- createdAt: row.created_at,
2894
- updatedAt: row.updated_at,
2895
- deletedAt: nullToUndefined2(row.deleted_at)
2896
- };
2897
- }
2898
- function rowToEdge(row) {
2899
- const rawProps = JSON.parse(row.props);
2900
- const props = filterReservedKeys(rawProps, RESERVED_EDGE_KEYS);
2901
- return {
2902
- id: row.id,
2903
- kind: row.kind,
2904
- fromKind: row.from_kind,
2905
- fromId: row.from_id,
2906
- toKind: row.to_kind,
2907
- toId: row.to_id,
2908
- meta: rowToEdgeMeta(row),
2909
- ...props
2910
- };
2911
- }
2912
- function rowToEdgeMeta(row) {
2913
- return {
2914
- validFrom: nullToUndefined2(row.valid_from),
2915
- validTo: nullToUndefined2(row.valid_to),
2916
- createdAt: row.created_at,
2917
- updatedAt: row.updated_at,
2918
- deletedAt: nullToUndefined2(row.deleted_at)
2919
- };
2920
- }
2921
-
2922
2996
  // src/store/subgraph.ts
2923
2997
  var DEFAULT_SUBGRAPH_MAX_DEPTH = 10;
2924
- var TEXT_ENCODER = new TextEncoder();
2925
- function truncateToBytes(value, maxBytes) {
2926
- const encoded = TEXT_ENCODER.encode(value);
2927
- if (encoded.byteLength <= maxBytes) return value;
2928
- let end = maxBytes;
2929
- while (end > 0 && encoded[end] >= 128 && encoded[end] < 192) {
2930
- end--;
2931
- }
2932
- return new TextDecoder().decode(encoded.slice(0, end));
2933
- }
2934
2998
  function projectionAlias(entityPrefix, kind, field2) {
2935
2999
  const prefix = entityPrefix === "node" ? "sg_n" : "sg_e";
2936
3000
  const hash = fnv1aBase36(`${kind}\0${field2}`);
@@ -3010,9 +3074,31 @@ async function executeSubgraph(params) {
3010
3074
  ...ctx.recordedReadBinding === void 0 ? {} : { recordedReadBinding: ctx.recordedReadBinding }
3011
3075
  });
3012
3076
  const includedIdsCte = buildIncludedIdsCte(ctx);
3077
+ let membership;
3078
+ if (ctx.dialect.name === "postgres") {
3079
+ const includedIds = await fetchIncludedIds(
3080
+ ctx,
3081
+ reachableCte,
3082
+ includedIdsCte
3083
+ );
3084
+ const idsArray = textArrayParam(includedIds);
3085
+ membership = {
3086
+ prefix: sql``,
3087
+ idFilter: (column) => sql`EXISTS (SELECT 1 FROM unnest(${idsArray}) AS tg_included(id) WHERE tg_included.id = ${column})`,
3088
+ parameterDependentPlan: true
3089
+ };
3090
+ } else {
3091
+ membership = {
3092
+ prefix: sql`${reachableCte}${includedIdsCte} `,
3093
+ // SQLite: `column IN (subquery)` evaluates via a transient index —
3094
+ // optimal as-is. (Postgres takes the parameterized text[] form above.)
3095
+ idFilter: (column) => sql`${column} IN (SELECT id FROM included_ids)`,
3096
+ parameterDependentPlan: false
3097
+ };
3098
+ }
3013
3099
  const [nodeRows, edgeRows] = await Promise.all([
3014
- fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, nodeProjectionPlan),
3015
- fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, edgeProjectionPlan)
3100
+ fetchSubgraphNodes(ctx, membership, nodeProjectionPlan),
3101
+ fetchSubgraphEdges(ctx, membership, edgeProjectionPlan)
3016
3102
  ]);
3017
3103
  const nodesMap = /* @__PURE__ */ new Map();
3018
3104
  for (const row of nodeRows) {
@@ -3112,13 +3198,27 @@ function buildIncludedIdsCte(ctx) {
3112
3198
  const whereClause = filters.length > 0 ? sql` WHERE ${sql.join(filters, sql` AND `)}` : sql``;
3113
3199
  return sql`, included_ids AS (SELECT DISTINCT id FROM reachable${whereClause})`;
3114
3200
  }
3115
- async function fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, projectionPlan) {
3201
+ function textArrayParam(values) {
3202
+ const elements = values.map(
3203
+ (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', String.raw`\"`)}"`
3204
+ );
3205
+ return sql`${`{${elements.join(",")}}`}::text[]`;
3206
+ }
3207
+ async function fetchIncludedIds(ctx, reachableCte, includedIdsCte) {
3208
+ const rows = await ctx.backend.execute(
3209
+ asCompiledRowsSql(
3210
+ sql`${reachableCte}${includedIdsCte} SELECT id FROM included_ids`
3211
+ )
3212
+ );
3213
+ return rows.map((row) => row.id);
3214
+ }
3215
+ async function fetchSubgraphNodes(ctx, membership, projectionPlan) {
3116
3216
  const nodeTemporalFilter = compileTemporalFilter({
3117
3217
  mode: ctx.temporalMode,
3118
3218
  asOf: ctx.asOf,
3119
3219
  recordedAsOf: ctx.recordedAsOf,
3120
3220
  tableAlias: "n",
3121
- currentTimestamp: ctx.dialect.currentTimestamp()
3221
+ currentTimestamp: currentReadInstant()
3122
3222
  });
3123
3223
  const columns = [
3124
3224
  sql`n.kind`,
@@ -3134,19 +3234,20 @@ async function fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, projectionP
3134
3234
  ]),
3135
3235
  ...buildProjectedPropertyColumns("n", projectionPlan, ctx.dialect)
3136
3236
  ];
3137
- const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND ${nodeTemporalFilter} AND n.id IN (SELECT id FROM included_ids)`;
3237
+ const query = sql`${membership.prefix}SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND ${nodeTemporalFilter} AND ${membership.idFilter(sql.raw("n.id"))}`;
3238
+ if (membership.parameterDependentPlan) markForceCustomPlan(query);
3138
3239
  return ctx.backend.execute(
3139
3240
  asCompiledRowsSql(query)
3140
3241
  );
3141
3242
  }
3142
- async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, projectionPlan) {
3243
+ async function fetchSubgraphEdges(ctx, membership, projectionPlan) {
3143
3244
  const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
3144
3245
  const edgeTemporalFilter = compileTemporalFilter({
3145
3246
  mode: ctx.temporalMode,
3146
3247
  asOf: ctx.asOf,
3147
3248
  recordedAsOf: ctx.recordedAsOf,
3148
3249
  tableAlias: "e",
3149
- currentTimestamp: ctx.dialect.currentTimestamp()
3250
+ currentTimestamp: currentReadInstant()
3150
3251
  });
3151
3252
  const columns = [
3152
3253
  sql`e.id`,
@@ -3165,7 +3266,8 @@ async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, projectionP
3165
3266
  ]),
3166
3267
  ...buildProjectedPropertyColumns("e", projectionPlan, ctx.dialect)
3167
3268
  ];
3168
- const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND ${edgeTemporalFilter} AND e.from_id IN (SELECT id FROM included_ids) AND e.to_id IN (SELECT id FROM included_ids)`;
3269
+ const query = sql`${membership.prefix}SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND ${edgeTemporalFilter} AND ${membership.idFilter(sql.raw("e.from_id"))} AND ${membership.idFilter(sql.raw("e.to_id"))}`;
3270
+ if (membership.parameterDependentPlan) markForceCustomPlan(query);
3169
3271
  return ctx.backend.execute(
3170
3272
  asCompiledRowsSql(query)
3171
3273
  );
@@ -3515,9 +3617,16 @@ function collectRequiredColumnsByAlias2(ast) {
3515
3617
  for (const traversal of ast.traversals) {
3516
3618
  addRequiredColumn(requiredColumnsByAlias, traversal.nodeAlias, "id");
3517
3619
  }
3518
- if (ast.selectiveFields && ast.selectiveFields.length > 0) {
3519
- for (const field2 of ast.selectiveFields) {
3520
- markSelectiveFieldAsRequired(requiredColumnsByAlias, field2);
3620
+ const hasSelectiveFields = (ast.selectiveFields?.length ?? 0) > 0;
3621
+ if (hasSelectiveFields) {
3622
+ for (const field2 of ast.selectiveFields ?? []) {
3623
+ if (field2.isSystemField) {
3624
+ addRequiredColumn(
3625
+ requiredColumnsByAlias,
3626
+ field2.alias,
3627
+ mapSelectiveSystemFieldToColumn(field2.field)
3628
+ );
3629
+ }
3521
3630
  }
3522
3631
  } else {
3523
3632
  for (const projectedField of ast.projection.fields) {
@@ -3537,14 +3646,25 @@ function collectRequiredColumnsByAlias2(ast) {
3537
3646
  }
3538
3647
  if (ast.orderBy) {
3539
3648
  for (const orderSpec of ast.orderBy) {
3649
+ if (findSelectivePropsFieldForFieldRef(
3650
+ ast.selectiveFields,
3651
+ orderSpec.field
3652
+ ) !== void 0) {
3653
+ continue;
3654
+ }
3540
3655
  markFieldRefAsRequired(requiredColumnsByAlias, orderSpec.field);
3541
3656
  }
3542
3657
  }
3543
3658
  if (ast.having) {
3544
3659
  markPredicateFieldsAsRequired(requiredColumnsByAlias, ast.having);
3545
3660
  }
3546
- for (const predicate of ast.predicates) {
3547
- markPredicateFieldsAsRequired(requiredColumnsByAlias, predicate.expression);
3661
+ if (!hasSelectiveFields) {
3662
+ for (const predicate of ast.predicates) {
3663
+ markPredicateFieldsAsRequired(
3664
+ requiredColumnsByAlias,
3665
+ predicate.expression
3666
+ );
3667
+ }
3548
3668
  }
3549
3669
  return requiredColumnsByAlias;
3550
3670
  }
@@ -3724,10 +3844,7 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
3724
3844
  const temporalPass = runCompilerPass(state, {
3725
3845
  name: "temporal_filters",
3726
3846
  execute(currentState) {
3727
- return createTemporalFilterPass(
3728
- currentState.ast,
3729
- currentState.ctx.dialect.currentTimestamp()
3730
- );
3847
+ return createTemporalFilterPass(currentState.ast, ctx.readInstant);
3731
3848
  },
3732
3849
  update(currentState, temporalFilterPass) {
3733
3850
  return {
@@ -3838,9 +3955,12 @@ function compileQuery(ast, graphId, options = "sqlite") {
3838
3955
  "recorded-query"
3839
3956
  );
3840
3957
  const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
3958
+ const annIndexTypes = /* @__PURE__ */ new Set();
3841
3959
  const ctx = {
3842
3960
  dialect: adapter,
3843
3961
  schema,
3962
+ annIndexTypes,
3963
+ readInstant: options_.readInstant ?? "literal",
3844
3964
  compileQuery: (subAst, subGraphId) => compileQuery(
3845
3965
  inheritRecordedAsOf(subAst, ast.recordedAsOf),
3846
3966
  subGraphId,
@@ -3848,16 +3968,23 @@ function compileQuery(ast, graphId, options = "sqlite") {
3848
3968
  ),
3849
3969
  ...options_.vectorStrategy === void 0 ? {} : { vectorStrategy: options_.vectorStrategy },
3850
3970
  windowFunctions: options_.windowFunctions ?? true,
3851
- ...options_.vectorSlots === void 0 ? {} : { vectorSlots: options_.vectorSlots }
3971
+ ...options_.vectorSlots === void 0 ? {} : { vectorSlots: options_.vectorSlots },
3972
+ ...options_.fulltextLanguages === void 0 ? {} : { fulltextLanguages: options_.fulltextLanguages }
3852
3973
  };
3974
+ function finish(compiled) {
3975
+ if (annIndexTypes.size > 0) {
3976
+ markAnnIndexScan(compiled, [...annIndexTypes]);
3977
+ }
3978
+ return asCompiledSelectSql(compiled);
3979
+ }
3853
3980
  if (hasVariableLengthTraversal(ast)) {
3854
3981
  const lowered = tryLowerSingleHopRecursiveTraversal(ast);
3855
3982
  if (lowered !== void 0) {
3856
- return asCompiledSelectSql(compileStandardQuery(lowered, graphId, ctx));
3983
+ return finish(compileStandardQuery(lowered, graphId, ctx));
3857
3984
  }
3858
- return asCompiledSelectSql(compileVariableLengthQuery(ast, graphId, ctx));
3985
+ return finish(compileVariableLengthQuery(ast, graphId, ctx));
3859
3986
  }
3860
- return asCompiledSelectSql(compileStandardQuery(ast, graphId, ctx));
3987
+ return finish(compileStandardQuery(ast, graphId, ctx));
3861
3988
  }
3862
3989
  function tryLowerSingleHopRecursiveTraversal(ast) {
3863
3990
  if (ast.traversals.length !== 1) {
@@ -3884,15 +4011,21 @@ function compileSetOperation2(op, graphId, options = "sqlite") {
3884
4011
  const options_ = typeof options === "string" ? { dialect: options } : options;
3885
4012
  const dialect = options_.dialect ?? "sqlite";
3886
4013
  const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
3887
- return asCompiledSelectSql(
3888
- compileSetOperation(
3889
- op,
3890
- graphId,
3891
- adapter,
3892
- (ast, gid) => compileQuery(ast, gid, propagateOptions(options_)),
3893
- options_.vectorStrategy
3894
- )
4014
+ const annIndexTypes = /* @__PURE__ */ new Set();
4015
+ const compileOperand = (ast, gid) => {
4016
+ const compiled = compileQuery(ast, gid, propagateOptions(options_));
4017
+ for (const indexType of annIndexScanTypes(compiled) ?? []) {
4018
+ annIndexTypes.add(indexType);
4019
+ }
4020
+ return compiled;
4021
+ };
4022
+ const combined = withPinnedReadInstant(
4023
+ () => compileSetOperation(op, graphId, adapter, compileOperand, options_.vectorStrategy)
3895
4024
  );
4025
+ if (annIndexTypes.size > 0) {
4026
+ markAnnIndexScan(combined, [...annIndexTypes]);
4027
+ }
4028
+ return asCompiledSelectSql(combined);
3896
4029
  }
3897
4030
  function resolveDialectAdapter(dialect, fulltextStrategy) {
3898
4031
  const baseAdapter = getDialect(dialect);
@@ -3935,7 +4068,9 @@ function propagateOptions(options_) {
3935
4068
  ...options_.vectorStrategy === void 0 ? {} : { vectorStrategy: options_.vectorStrategy },
3936
4069
  windowFunctions: options_.windowFunctions ?? true,
3937
4070
  ...options_.vectorSlots === void 0 ? {} : { vectorSlots: options_.vectorSlots },
3938
- ...options_.recordedReadBinding === void 0 ? {} : { recordedReadBinding: options_.recordedReadBinding }
4071
+ ...options_.fulltextLanguages === void 0 ? {} : { fulltextLanguages: options_.fulltextLanguages },
4072
+ ...options_.recordedReadBinding === void 0 ? {} : { recordedReadBinding: options_.recordedReadBinding },
4073
+ ...options_.readInstant === void 0 ? {} : { readInstant: options_.readInstant }
3939
4074
  };
3940
4075
  }
3941
4076
  function resolveCountAggregateFastPath(ast) {
@@ -4097,7 +4232,7 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
4097
4232
  sql`COUNT(DISTINCT e.id) AS ${sql.raw(edgeCountDistinctColumn)}`
4098
4233
  );
4099
4234
  }
4100
- const startCteLimit = traversal.optional && ast.orderBy === void 0 && ast.limit !== void 0 ? { limit: ast.limit, offset: ast.offset } : void 0;
4235
+ const startCteLimit = traversal.optional && ast.orderBy === void 0 && ast.aggregateOrderBy === void 0 && ast.limit !== void 0 ? { limit: ast.limit, offset: ast.offset } : void 0;
4101
4236
  const startCte = buildStandardStartCte({
4102
4237
  ast,
4103
4238
  ctx,
@@ -4437,9 +4572,9 @@ function transformPathColumns(rows, state, _dialect) {
4437
4572
  }
4438
4573
  return changed ? result : rows;
4439
4574
  }
4440
- var RESERVED_NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
4441
- var RESERVED_EDGE_KEYS2 = /* @__PURE__ */ new Set(["id", "kind", "fromId", "toId", "meta"]);
4442
- function nullToUndefined3(value) {
4575
+ var RESERVED_NODE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
4576
+ var RESERVED_EDGE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "fromId", "toId", "meta"]);
4577
+ function nullToUndefined2(value) {
4443
4578
  return value === null ? void 0 : value;
4444
4579
  }
4445
4580
  function assignPropsExcludingReserved(target, props, reservedKeys) {
@@ -4455,13 +4590,13 @@ function buildSelectableNode(row, alias) {
4455
4590
  const propsRaw = row[`${alias}_props`];
4456
4591
  const rawProps = typeof propsRaw === "string" ? JSON.parse(propsRaw) : propsRaw ?? {};
4457
4592
  const version = row[`${alias}_version`];
4458
- const validFrom = nullToUndefined3(
4593
+ const validFrom = nullToUndefined2(
4459
4594
  row[`${alias}_valid_from`]
4460
4595
  );
4461
- const validTo = nullToUndefined3(row[`${alias}_valid_to`]);
4596
+ const validTo = nullToUndefined2(row[`${alias}_valid_to`]);
4462
4597
  const createdAt = row[`${alias}_created_at`];
4463
4598
  const updatedAt = row[`${alias}_updated_at`];
4464
- const deletedAt = nullToUndefined3(
4599
+ const deletedAt = nullToUndefined2(
4465
4600
  row[`${alias}_deleted_at`]
4466
4601
  );
4467
4602
  const result = {
@@ -4476,7 +4611,7 @@ function buildSelectableNode(row, alias) {
4476
4611
  deletedAt
4477
4612
  }
4478
4613
  };
4479
- assignPropsExcludingReserved(result, rawProps, RESERVED_NODE_KEYS2);
4614
+ assignPropsExcludingReserved(result, rawProps, RESERVED_NODE_KEYS);
4480
4615
  return result;
4481
4616
  }
4482
4617
  function buildSelectableNodeOrUndefined(row, alias) {
@@ -4496,13 +4631,13 @@ function buildSelectableEdge(row, alias) {
4496
4631
  const toId = row[`${alias}_to_id`];
4497
4632
  const propsRaw = row[`${alias}_props`];
4498
4633
  const rawProps = typeof propsRaw === "string" ? JSON.parse(propsRaw) : propsRaw ?? {};
4499
- const validFrom = nullToUndefined3(
4634
+ const validFrom = nullToUndefined2(
4500
4635
  row[`${alias}_valid_from`]
4501
4636
  );
4502
- const validTo = nullToUndefined3(row[`${alias}_valid_to`]);
4637
+ const validTo = nullToUndefined2(row[`${alias}_valid_to`]);
4503
4638
  const createdAt = row[`${alias}_created_at`];
4504
4639
  const updatedAt = row[`${alias}_updated_at`];
4505
- const deletedAt = nullToUndefined3(
4640
+ const deletedAt = nullToUndefined2(
4506
4641
  row[`${alias}_deleted_at`]
4507
4642
  );
4508
4643
  const result = {
@@ -4518,7 +4653,7 @@ function buildSelectableEdge(row, alias) {
4518
4653
  deletedAt
4519
4654
  }
4520
4655
  };
4521
- assignPropsExcludingReserved(result, rawProps, RESERVED_EDGE_KEYS2);
4656
+ assignPropsExcludingReserved(result, rawProps, RESERVED_EDGE_KEYS);
4522
4657
  return result;
4523
4658
  }
4524
4659
  function buildSelectContext(row, startAlias, traversals) {
@@ -5259,6 +5394,61 @@ function createGuardedProxy(target, debugPath) {
5259
5394
  }
5260
5395
  });
5261
5396
  }
5397
+ function templateHasReadInstant(template) {
5398
+ return template.params.some(
5399
+ (parameter) => parameter instanceof Placeholder && parameter.name === CURRENT_READ_INSTANT_PLACEHOLDER
5400
+ );
5401
+ }
5402
+ function queryAstNeedsCurrentReadInstant(ast) {
5403
+ return ast.temporalMode.mode === "current" && ast.recordedAsOf === void 0;
5404
+ }
5405
+ function composableNeedsCurrentReadInstant(query) {
5406
+ if ("__type" in query) {
5407
+ return composableNeedsCurrentReadInstant(query.left) || composableNeedsCurrentReadInstant(query.right);
5408
+ }
5409
+ return queryAstNeedsCurrentReadInstant(query);
5410
+ }
5411
+ function buildReadInstantTemplate(args) {
5412
+ const { backend } = args;
5413
+ if (backend?.compileSql === void 0) return void 0;
5414
+ const compiled = args.compile();
5415
+ if (!isRawExecutable(compiled)) return void 0;
5416
+ const template = backend.compileSql(compiled);
5417
+ if (args.needsReadInstant && !templateHasReadInstant(template)) {
5418
+ return void 0;
5419
+ }
5420
+ return template;
5421
+ }
5422
+ function buildQueryTemplate(ast, graphId, compileOptions, backend) {
5423
+ return buildReadInstantTemplate({
5424
+ compile: () => compileQuery(ast, graphId, {
5425
+ ...compileOptions,
5426
+ readInstant: "placeholder"
5427
+ }),
5428
+ backend,
5429
+ needsReadInstant: queryAstNeedsCurrentReadInstant(ast)
5430
+ });
5431
+ }
5432
+ function fillTemplateParams(params, bindings, dialect) {
5433
+ const adapter = getDialect(dialect);
5434
+ let readInstant;
5435
+ return params.map((parameter) => {
5436
+ if (!(parameter instanceof Placeholder)) return parameter;
5437
+ const name = parameter.name;
5438
+ if (name === CURRENT_READ_INSTANT_PLACEHOLDER) {
5439
+ readInstant ??= nowIso();
5440
+ return readInstant;
5441
+ }
5442
+ const value = bindings[name];
5443
+ if (value === void 0) {
5444
+ throw new ConfigurationError(`Missing binding for parameter "${name}"`, {
5445
+ parameterName: name
5446
+ });
5447
+ }
5448
+ if (value instanceof Date) return value.toISOString();
5449
+ return adapter.bindValue(value);
5450
+ });
5451
+ }
5262
5452
 
5263
5453
  // src/query/builder/prepared-query.ts
5264
5454
  function toLiteral(value) {
@@ -5399,31 +5589,9 @@ function substituteParameters(ast, bindings) {
5399
5589
  }
5400
5590
  };
5401
5591
  }
5402
- function fillPlaceholders(params, bindings, dialect) {
5403
- const adapter = getDialect(dialect);
5404
- return params.map((parameter) => {
5405
- if (parameter instanceof Placeholder) {
5406
- const name = parameter.name;
5407
- const value = bindings[name];
5408
- if (value === void 0) {
5409
- throw new ConfigurationError(
5410
- `Missing binding for parameter "${name}"`,
5411
- { parameterName: name }
5412
- );
5413
- }
5414
- if (value instanceof Date) return value.toISOString();
5415
- return adapter.bindValue(value);
5416
- }
5417
- return parameter;
5418
- });
5419
- }
5420
5592
  var PreparedQuery = class {
5421
5593
  #ast;
5422
5594
  #unoptimizedAst;
5423
- #sqlText;
5424
- #sqlParams;
5425
- #unoptimizedSqlText;
5426
- #unoptimizedSqlParams;
5427
5595
  #backend;
5428
5596
  #dialect;
5429
5597
  #graphId;
@@ -5433,13 +5601,17 @@ var PreparedQuery = class {
5433
5601
  #selectFn;
5434
5602
  #schemaIntrospector;
5435
5603
  #parameterMetadata;
5604
+ /**
5605
+ * Per-AST cached placeholder template, keyed by AST reference so the
5606
+ * optimized (`#ast`) and unoptimized (`#unoptimizedAst`) variants cache
5607
+ * independently. A cached `undefined` records "no fast-path template" (the
5608
+ * backend lacks `compileSql`, or the statement was not safely cacheable) so
5609
+ * we don't rebuild it on every call.
5610
+ */
5611
+ #templateCache = /* @__PURE__ */ new Map();
5436
5612
  constructor(config) {
5437
5613
  this.#ast = config.ast;
5438
5614
  this.#unoptimizedAst = config.unoptimizedAst;
5439
- this.#sqlText = config.sqlText;
5440
- this.#sqlParams = config.sqlParams;
5441
- this.#unoptimizedSqlText = config.unoptimizedSqlText;
5442
- this.#unoptimizedSqlParams = config.unoptimizedSqlParams;
5443
5615
  this.#backend = config.backend;
5444
5616
  this.#dialect = config.dialect;
5445
5617
  this.#graphId = config.graphId;
@@ -5450,6 +5622,26 @@ var PreparedQuery = class {
5450
5622
  this.#schemaIntrospector = config.schemaIntrospector;
5451
5623
  this.#parameterMetadata = collectParameterMetadata(this.#ast);
5452
5624
  }
5625
+ /**
5626
+ * The cached placeholder template for `ast`, or `undefined` when no fast
5627
+ * path applies. Compiled once (in placeholder mode) and reused across every
5628
+ * `execute()`; the read instant is refreshed per call by
5629
+ * {@link fillTemplateParams}, never frozen into the cache.
5630
+ */
5631
+ #template(ast) {
5632
+ if (this.#templateCache.has(ast)) return this.#templateCache.get(ast);
5633
+ const template = this.#buildTemplate(ast);
5634
+ this.#templateCache.set(ast, template);
5635
+ return template;
5636
+ }
5637
+ #buildTemplate(ast) {
5638
+ return buildQueryTemplate(
5639
+ ast,
5640
+ this.#graphId,
5641
+ this.#compileOptions,
5642
+ this.#backend
5643
+ );
5644
+ }
5453
5645
  /** The set of parameter names required by this prepared query. */
5454
5646
  get parameterNames() {
5455
5647
  return this.#parameterMetadata.names;
@@ -5482,26 +5674,7 @@ var PreparedQuery = class {
5482
5674
  return this.#executeUnoptimized(bindings);
5483
5675
  }
5484
5676
  async #executeSelectiveRows(bindings) {
5485
- if (this.#sqlText !== void 0 && this.#sqlParams !== void 0 && this.#backend.executeRaw !== void 0) {
5486
- const filledParams = fillPlaceholders(
5487
- this.#sqlParams,
5488
- bindings,
5489
- this.#dialect
5490
- );
5491
- const rawRows2 = await this.#backend.executeRaw(
5492
- this.#sqlText,
5493
- filledParams
5494
- );
5495
- return transformPathColumns(rawRows2, this.#state, this.#dialect);
5496
- }
5497
- const concreteAst = substituteParameters(this.#ast, bindings);
5498
- const compiled = compileQuery(
5499
- concreteAst,
5500
- this.#graphId,
5501
- this.#compileOptions
5502
- );
5503
- const rawRows = await this.#backend.execute(compiled);
5504
- return transformPathColumns(rawRows, this.#state, this.#dialect);
5677
+ return this.#executeRows(this.#ast, bindings);
5505
5678
  }
5506
5679
  async #executeUnoptimized(bindings) {
5507
5680
  const rows = await this.#executeUnoptimizedRows(bindings);
@@ -5513,19 +5686,29 @@ var PreparedQuery = class {
5513
5686
  );
5514
5687
  }
5515
5688
  async #executeUnoptimizedRows(bindings) {
5516
- if (this.#unoptimizedSqlText !== void 0 && this.#unoptimizedSqlParams !== void 0 && this.#backend.executeRaw !== void 0) {
5517
- const filledParams = fillPlaceholders(
5518
- this.#unoptimizedSqlParams,
5689
+ return this.#executeRows(this.#unoptimizedAst, bindings);
5690
+ }
5691
+ /**
5692
+ * Runs one AST variant with the given bindings. Prefers the cached template
5693
+ * + `executeRaw` fast path; falls back to substituting bindings into the AST
5694
+ * and compiling fresh when the backend cannot execute raw SQL text.
5695
+ */
5696
+ async #executeRows(ast, bindings) {
5697
+ const executeRaw = this.#backend.executeRaw;
5698
+ const template = executeRaw === void 0 ? void 0 : this.#template(ast);
5699
+ if (template !== void 0 && executeRaw !== void 0) {
5700
+ const params = fillTemplateParams(
5701
+ template.params,
5519
5702
  bindings,
5520
5703
  this.#dialect
5521
5704
  );
5522
5705
  const rawRows2 = await this.#backend.executeRaw(
5523
- this.#unoptimizedSqlText,
5524
- filledParams
5706
+ template.sql,
5707
+ params
5525
5708
  );
5526
5709
  return transformPathColumns(rawRows2, this.#state, this.#dialect);
5527
5710
  }
5528
- const concreteAst = substituteParameters(this.#unoptimizedAst, bindings);
5711
+ const concreteAst = substituteParameters(ast, bindings);
5529
5712
  const compiled = compileQuery(
5530
5713
  concreteAst,
5531
5714
  this.#graphId,
@@ -5969,6 +6152,9 @@ function buildQueryAst(config, state) {
5969
6152
  ...state.offset !== void 0 && { offset: state.offset },
5970
6153
  ...state.groupBy !== void 0 && { groupBy: state.groupBy },
5971
6154
  ...state.having !== void 0 && { having: state.having },
6155
+ ...state.aggregateOrderBy.length > 0 && {
6156
+ aggregateOrderBy: state.aggregateOrderBy
6157
+ },
5972
6158
  ...state.fusion !== void 0 && { fusion: state.fusion }
5973
6159
  };
5974
6160
  }
@@ -5994,12 +6180,27 @@ function buildCompileOptions(config) {
5994
6180
  dialect: config.dialect ?? "sqlite",
5995
6181
  schema: config.schema,
5996
6182
  windowFunctions: config.backend?.capabilities.windowFunctions ?? true,
5997
- ...fulltextStrategy === void 0 ? {} : { fulltextStrategy },
6183
+ ...fulltextStrategy === void 0 ? {} : {
6184
+ fulltextStrategy,
6185
+ fulltextLanguages: buildFulltextLanguages(config.registry)
6186
+ },
5998
6187
  ...vectorStrategy === void 0 ? {} : { vectorStrategy, vectorSlots: buildVectorSlots(config.registry) },
5999
6188
  ...recordedReadBinding === void 0 ? {} : { recordedReadBinding }
6000
6189
  };
6001
6190
  }
6002
6191
  var vectorSlotsCache = /* @__PURE__ */ new WeakMap();
6192
+ var fulltextLanguagesCache = /* @__PURE__ */ new WeakMap();
6193
+ function buildFulltextLanguages(registry) {
6194
+ const cached = fulltextLanguagesCache.get(registry);
6195
+ if (cached !== void 0) return cached;
6196
+ const languages = /* @__PURE__ */ new Map();
6197
+ for (const [nodeKind, nodeType] of registry.nodeKinds) {
6198
+ const language = resolveDeclaredFulltextLanguage(nodeType.schema);
6199
+ if (language !== void 0) languages.set(nodeKind, language);
6200
+ }
6201
+ fulltextLanguagesCache.set(registry, languages);
6202
+ return languages;
6203
+ }
6003
6204
  function buildVectorSlots(registry) {
6004
6205
  const cached = vectorSlotsCache.get(registry);
6005
6206
  if (cached !== void 0) return cached;
@@ -6027,20 +6228,35 @@ var ExecutableQuery = class _ExecutableQuery {
6027
6228
  #config;
6028
6229
  #state;
6029
6230
  #selectFn;
6030
- #cachedCompiled = NOT_COMPUTED;
6031
- #cachedOptimizedCompiled = NOT_COMPUTED;
6032
6231
  #cachedSelectiveFieldsForExecute = NOT_COMPUTED;
6033
6232
  #cachedSelectiveFieldsForPagination = NOT_COMPUTED;
6233
+ // The instance is immutable (every builder method returns a new instance),
6234
+ // so the AST and its param-ref check are invariant — compute each once and
6235
+ // reuse across execute()/paginate()/stream() instead of rebuilding per call.
6236
+ #cachedAst;
6237
+ #cachedHasParameterReferences;
6238
+ // Per-instance compiled placeholder templates for the full and
6239
+ // selective-field ASTs (NOT_COMPUTED = not yet built; undefined = no fast
6240
+ // path). Reused across execute()/executeOn() calls so a repeated query
6241
+ // compiles once; the read instant is filled fresh per call, never cached.
6242
+ #fullTemplate = NOT_COMPUTED;
6243
+ #selectiveTemplate = NOT_COMPUTED;
6034
6244
  constructor(config, state, selectFunction) {
6035
6245
  this.#config = config;
6036
6246
  this.#state = state;
6037
6247
  this.#selectFn = selectFunction;
6038
6248
  }
6039
6249
  /**
6040
- * Builds the query AST.
6250
+ * Builds the query AST (memoized — the instance is immutable).
6041
6251
  */
6042
6252
  toAst() {
6043
- return buildQueryAst(this.#config, this.#state);
6253
+ return this.#cachedAst ??= buildQueryAst(this.#config, this.#state);
6254
+ }
6255
+ /** Whether this query uses `param()` refs (memoized). */
6256
+ #hasParameterReferences() {
6257
+ return this.#cachedHasParameterReferences ??= hasParameterReferences(
6258
+ this.toAst()
6259
+ );
6044
6260
  }
6045
6261
  /**
6046
6262
  * Orders results.
@@ -6214,19 +6430,16 @@ var ExecutableQuery = class _ExecutableQuery {
6214
6430
  * with db.all(), db.get(), etc.
6215
6431
  */
6216
6432
  compile() {
6217
- if (this.#cachedCompiled !== NOT_COMPUTED) return this.#cachedCompiled;
6218
6433
  const ast = this.toAst();
6219
- const compiled = compileQuery(
6220
- ast,
6221
- this.#config.graphId,
6222
- this.#compileOptions()
6223
- );
6224
- this.#cachedCompiled = compiled;
6225
- return compiled;
6434
+ return compileQuery(ast, this.#config.graphId, this.#compileOptions());
6226
6435
  }
6227
6436
  /**
6228
- * Creates a prepared (pre-compiled) query that can be executed
6229
- * multiple times with different parameter bindings.
6437
+ * Creates a prepared (pre-validated) query that can be executed multiple
6438
+ * times with different parameter bindings. Builds and structurally
6439
+ * validates the AST once (a malformed query fails fast, here, instead of on
6440
+ * first use); the prepared query then compiles once into a reusable template
6441
+ * and fills a fresh read instant per execute() — see PreparedQuery's class
6442
+ * doc comment.
6230
6443
  *
6231
6444
  * Use `param("name")` in predicates to create parameterized slots,
6232
6445
  * then pass values via `prepared.execute({ name: "value" })`.
@@ -6253,36 +6466,16 @@ var ExecutableQuery = class _ExecutableQuery {
6253
6466
  "Cannot prepare query: no backend configured. Use store.query() or pass a backend to createQueryBuilder()."
6254
6467
  );
6255
6468
  }
6256
- const baseAst = buildQueryAst(this.#config, this.#state);
6469
+ const baseAst = this.toAst();
6257
6470
  const selectiveFields = this.#getSelectiveFieldsForExecute();
6258
6471
  const ast = selectiveFields === void 0 ? baseAst : { ...baseAst, selectiveFields };
6259
6472
  const unoptimizedAst = baseAst;
6260
6473
  const compileOptions = this.#compileOptions();
6261
- const compiled = compileQuery(ast, this.#config.graphId, compileOptions);
6262
- let sqlText;
6263
- let sqlParams;
6264
- let unoptimizedSqlText;
6265
- let unoptimizedSqlParams;
6266
- if (this.#config.backend.compileSql !== void 0) {
6267
- const result = this.#config.backend.compileSql(compiled);
6268
- sqlText = result.sql;
6269
- sqlParams = result.params;
6270
- const unoptimizedCompiled = compileQuery(
6271
- unoptimizedAst,
6272
- this.#config.graphId,
6273
- compileOptions
6274
- );
6275
- const unoptimizedResult = this.#config.backend.compileSql(unoptimizedCompiled);
6276
- unoptimizedSqlText = unoptimizedResult.sql;
6277
- unoptimizedSqlParams = unoptimizedResult.params;
6278
- }
6474
+ compileQuery(ast, this.#config.graphId, compileOptions);
6475
+ compileQuery(unoptimizedAst, this.#config.graphId, compileOptions);
6279
6476
  return new PreparedQuery({
6280
6477
  ast,
6281
6478
  unoptimizedAst,
6282
- sqlText,
6283
- sqlParams,
6284
- unoptimizedSqlText,
6285
- unoptimizedSqlParams,
6286
6479
  backend: this.#config.backend,
6287
6480
  dialect: this.#config.dialect ?? "sqlite",
6288
6481
  graphId: this.#config.graphId,
@@ -6316,6 +6509,59 @@ var ExecutableQuery = class _ExecutableQuery {
6316
6509
  surface
6317
6510
  });
6318
6511
  }
6512
+ #dialect() {
6513
+ return this.#config.dialect ?? "sqlite";
6514
+ }
6515
+ /**
6516
+ * The cached placeholder template for one AST variant (`"full"` or
6517
+ * `"selective"`), or `undefined` when no fast path applies. Built once per
6518
+ * instance from the store backend's compiler; reusable across executions and
6519
+ * across store/transaction backends of the same dialect, since SQL text and
6520
+ * parameters depend only on the AST and dialect. The read instant is filled
6521
+ * fresh per call by {@link fillTemplateParams}, never frozen into the cache.
6522
+ */
6523
+ #templateFor(ast, slot) {
6524
+ const cached = slot === "full" ? this.#fullTemplate : this.#selectiveTemplate;
6525
+ if (cached !== NOT_COMPUTED) return cached;
6526
+ const built = this.#buildTemplate(ast);
6527
+ if (slot === "full") this.#fullTemplate = built;
6528
+ else this.#selectiveTemplate = built;
6529
+ return built;
6530
+ }
6531
+ #buildTemplate(ast) {
6532
+ return buildQueryTemplate(
6533
+ ast,
6534
+ this.#config.graphId,
6535
+ this.#compileOptions(),
6536
+ this.#config.backend
6537
+ );
6538
+ }
6539
+ /**
6540
+ * Fetches raw rows for one AST variant, path-column-normalized. Prefers the
6541
+ * cached template + `executeRaw` fast path; falls back to a fresh literal
6542
+ * compile via `backend.execute` when the backend cannot run raw SQL text (in
6543
+ * which case no template is built). Callers map the returned rows to typed
6544
+ * results.
6545
+ */
6546
+ async #fetchRows(backend, ast, slot, surface) {
6547
+ const executeRaw = backend.executeRaw;
6548
+ const template = executeRaw === void 0 ? void 0 : this.#templateFor(ast, slot);
6549
+ const rawRows = await this.#executeOnBackend(
6550
+ backend,
6551
+ template !== void 0 && executeRaw !== void 0 ? (
6552
+ // Method call (not the detached `executeRaw` local) so a this-using
6553
+ // backend implementation keeps its receiver.
6554
+ backend.executeRaw(
6555
+ template.sql,
6556
+ fillTemplateParams(template.params, {}, this.#dialect())
6557
+ )
6558
+ ) : backend.execute(
6559
+ compileQuery(ast, this.#config.graphId, this.#compileOptions())
6560
+ ),
6561
+ surface
6562
+ );
6563
+ return transformPathColumns(rawRows, this.#state, this.#dialect());
6564
+ }
6319
6565
  /**
6320
6566
  * Executes the query and returns typed results.
6321
6567
  *
@@ -6333,7 +6579,8 @@ var ExecutableQuery = class _ExecutableQuery {
6333
6579
  );
6334
6580
  }
6335
6581
  const backend = this.#config.backend;
6336
- if (hasParameterReferences(this.toAst())) {
6582
+ const ast = this.toAst();
6583
+ if (this.#hasParameterReferences()) {
6337
6584
  throw new Error(
6338
6585
  "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
6339
6586
  );
@@ -6342,14 +6589,7 @@ var ExecutableQuery = class _ExecutableQuery {
6342
6589
  if (optimizedResult !== void 0) {
6343
6590
  return optimizedResult;
6344
6591
  }
6345
- const compiled = this.compile();
6346
- const rawRows = await this.#executeOnBackend(
6347
- backend,
6348
- backend.execute(compiled),
6349
- "recorded-query"
6350
- );
6351
- this.#config.dialect ?? "sqlite";
6352
- const rows = transformPathColumns(rawRows, this.#state);
6592
+ const rows = await this.#fetchRows(backend, ast, "full", "recorded-query");
6353
6593
  return mapResults(
6354
6594
  rows,
6355
6595
  this.#state.startAlias,
@@ -6366,7 +6606,7 @@ var ExecutableQuery = class _ExecutableQuery {
6366
6606
  */
6367
6607
  async executeOn(backend) {
6368
6608
  const ast = this.toAst();
6369
- if (hasParameterReferences(ast)) {
6609
+ if (this.#hasParameterReferences()) {
6370
6610
  throw new Error(
6371
6611
  "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
6372
6612
  );
@@ -6375,14 +6615,12 @@ var ExecutableQuery = class _ExecutableQuery {
6375
6615
  if (optimizedResult !== void 0) {
6376
6616
  return optimizedResult;
6377
6617
  }
6378
- const compiled = this.compile();
6379
- const rawRows = await this.#executeOnBackend(
6618
+ const rows = await this.#fetchRows(
6380
6619
  backend,
6381
- backend.execute(compiled),
6620
+ ast,
6621
+ "full",
6382
6622
  "recorded-batch-query"
6383
6623
  );
6384
- this.#config.dialect ?? "sqlite";
6385
- const rows = transformPathColumns(rawRows, this.#state);
6386
6624
  return mapResults(
6387
6625
  rows,
6388
6626
  this.#state.startAlias,
@@ -6401,30 +6639,15 @@ var ExecutableQuery = class _ExecutableQuery {
6401
6639
  if (selectiveFields === void 0) {
6402
6640
  return void 0;
6403
6641
  }
6404
- let compiled;
6405
- if (this.#cachedOptimizedCompiled === NOT_COMPUTED) {
6406
- const baseAst = buildQueryAst(this.#config, this.#state);
6407
- const selectiveAst = {
6408
- ...baseAst,
6409
- selectiveFields
6410
- };
6411
- compiled = compileQuery(
6412
- selectiveAst,
6413
- this.#config.graphId,
6414
- this.#compileOptions()
6415
- );
6416
- this.#cachedOptimizedCompiled = compiled;
6417
- } else {
6418
- compiled = this.#cachedOptimizedCompiled;
6419
- }
6642
+ const baseAst = this.toAst();
6643
+ const selectiveAst = { ...baseAst, selectiveFields };
6420
6644
  const backend = this.#requireBackend();
6421
- const rawSelectiveRows = await this.#executeOnBackend(
6645
+ const rows = await this.#fetchRows(
6422
6646
  backend,
6423
- backend.execute(compiled),
6647
+ selectiveAst,
6648
+ "selective",
6424
6649
  "recorded-query"
6425
6650
  );
6426
- this.#config.dialect ?? "sqlite";
6427
- const rows = transformPathColumns(rawSelectiveRows, this.#state);
6428
6651
  try {
6429
6652
  return mapSelectiveResults(
6430
6653
  rows,
@@ -6436,12 +6659,10 @@ var ExecutableQuery = class _ExecutableQuery {
6436
6659
  } catch (error) {
6437
6660
  if (error instanceof MissingSelectiveFieldError) {
6438
6661
  this.#cachedSelectiveFieldsForExecute = void 0;
6439
- this.#cachedOptimizedCompiled = NOT_COMPUTED;
6440
6662
  return void 0;
6441
6663
  }
6442
6664
  if (error instanceof UnsupportedPredicateError) {
6443
6665
  this.#cachedSelectiveFieldsForExecute = void 0;
6444
- this.#cachedOptimizedCompiled = NOT_COMPUTED;
6445
6666
  return void 0;
6446
6667
  }
6447
6668
  throw error;
@@ -6456,29 +6677,14 @@ var ExecutableQuery = class _ExecutableQuery {
6456
6677
  if (selectiveFields === void 0) {
6457
6678
  return void 0;
6458
6679
  }
6459
- let compiled;
6460
- if (this.#cachedOptimizedCompiled === NOT_COMPUTED) {
6461
- const baseAst = buildQueryAst(this.#config, this.#state);
6462
- const selectiveAst = {
6463
- ...baseAst,
6464
- selectiveFields
6465
- };
6466
- compiled = compileQuery(
6467
- selectiveAst,
6468
- this.#config.graphId,
6469
- this.#compileOptions()
6470
- );
6471
- this.#cachedOptimizedCompiled = compiled;
6472
- } else {
6473
- compiled = this.#cachedOptimizedCompiled;
6474
- }
6475
- const rawSelectiveRows = await this.#executeOnBackend(
6680
+ const baseAst = this.toAst();
6681
+ const selectiveAst = { ...baseAst, selectiveFields };
6682
+ const rows = await this.#fetchRows(
6476
6683
  backend,
6477
- backend.execute(compiled),
6684
+ selectiveAst,
6685
+ "selective",
6478
6686
  "recorded-batch-query"
6479
6687
  );
6480
- this.#config.dialect ?? "sqlite";
6481
- const rows = transformPathColumns(rawSelectiveRows, this.#state);
6482
6688
  try {
6483
6689
  return mapSelectiveResults(
6484
6690
  rows,
@@ -6490,12 +6696,10 @@ var ExecutableQuery = class _ExecutableQuery {
6490
6696
  } catch (error) {
6491
6697
  if (error instanceof MissingSelectiveFieldError) {
6492
6698
  this.#cachedSelectiveFieldsForExecute = void 0;
6493
- this.#cachedOptimizedCompiled = NOT_COMPUTED;
6494
6699
  return void 0;
6495
6700
  }
6496
6701
  if (error instanceof UnsupportedPredicateError) {
6497
6702
  this.#cachedSelectiveFieldsForExecute = void 0;
6498
- this.#cachedOptimizedCompiled = NOT_COMPUTED;
6499
6703
  return void 0;
6500
6704
  }
6501
6705
  throw error;
@@ -7003,7 +7207,10 @@ var ExecutableAggregateQuery = class _ExecutableAggregateQuery {
7003
7207
  #config;
7004
7208
  #state;
7005
7209
  #fields;
7006
- #cachedCompiled = NOT_COMPUTED2;
7210
+ // Per-instance compiled placeholder template, reused across execute() calls
7211
+ // (see #resolveTemplate). NOT_COMPUTED = not yet built; undefined = no fast
7212
+ // path.
7213
+ #template = NOT_COMPUTED2;
7007
7214
  constructor(config, state, fields) {
7008
7215
  this.#config = config;
7009
7216
  this.#state = state;
@@ -7015,6 +7222,43 @@ var ExecutableAggregateQuery = class _ExecutableAggregateQuery {
7015
7222
  toAst() {
7016
7223
  return buildQueryAst(this.#config, this.#state);
7017
7224
  }
7225
+ /**
7226
+ * Orders results by a grouped field or aggregate alias.
7227
+ *
7228
+ * `key` is one of the output names passed to `.aggregate({...})` — either
7229
+ * a grouped field (e.g. `genre`) or an aggregate alias (e.g. `bookCount`).
7230
+ * Both are ordered the same way: by referencing the SELECT-list output
7231
+ * column, since every `.aggregate()` field is projected with an alias.
7232
+ *
7233
+ * Chain multiple calls to sort by more than one key, in call order:
7234
+ * `.orderBy("genre").orderBy("bookCount", "desc")` sorts by genre first,
7235
+ * then by book count within each genre.
7236
+ *
7237
+ * @example
7238
+ * ```typescript
7239
+ * // Top 2 authors by book count
7240
+ * store.query()
7241
+ * .from("Author", "a")
7242
+ * .traverse("wrote", "e")
7243
+ * .to("Book", "b")
7244
+ * .groupByNode("a")
7245
+ * .aggregate({ author: field("a", "name"), bookCount: count("b") })
7246
+ * .orderBy("bookCount", "desc")
7247
+ * .limit(2)
7248
+ * .execute();
7249
+ * ```
7250
+ */
7251
+ orderBy(key, direction = "asc") {
7252
+ const orderSpec = { outputName: key, direction };
7253
+ return new _ExecutableAggregateQuery(
7254
+ this.#config,
7255
+ {
7256
+ ...this.#state,
7257
+ aggregateOrderBy: [...this.#state.aggregateOrderBy, orderSpec]
7258
+ },
7259
+ this.#fields
7260
+ );
7261
+ }
7018
7262
  /**
7019
7263
  * Limits the number of results.
7020
7264
  */
@@ -7053,34 +7297,61 @@ var ExecutableAggregateQuery = class _ExecutableAggregateQuery {
7053
7297
  * Compiles the query to a Drizzle SQL object.
7054
7298
  */
7055
7299
  compile() {
7056
- if (this.#cachedCompiled !== NOT_COMPUTED2) {
7057
- return this.#cachedCompiled;
7058
- }
7059
7300
  const ast = this.toAst();
7060
- const compiled = compileQuery(
7061
- ast,
7062
- this.#config.graphId,
7063
- this.#compileOptions()
7064
- );
7065
- this.#cachedCompiled = compiled;
7066
- return compiled;
7301
+ return compileQuery(ast, this.#config.graphId, this.#compileOptions());
7067
7302
  }
7068
7303
  #compileOptions() {
7069
7304
  return buildCompileOptions(this.#config);
7070
7305
  }
7306
+ /**
7307
+ * The cached placeholder template for this aggregate query, or `undefined`
7308
+ * when no fast path applies. Built once per instance; the read instant is
7309
+ * filled fresh per execution by {@link fillTemplateParams}.
7310
+ */
7311
+ #resolveTemplate(ast) {
7312
+ if (this.#template !== NOT_COMPUTED2) return this.#template;
7313
+ this.#template = buildQueryTemplate(
7314
+ ast,
7315
+ this.#config.graphId,
7316
+ this.#compileOptions(),
7317
+ this.#config.backend
7318
+ );
7319
+ return this.#template;
7320
+ }
7071
7321
  /**
7072
7322
  * Executes the query and returns typed results.
7073
7323
  *
7074
7324
  * @throws Error if no backend is configured
7075
7325
  */
7076
7326
  async execute() {
7077
- if (!this.#config.backend) {
7327
+ const backend = this.#config.backend;
7328
+ if (!backend) {
7078
7329
  throw new Error(
7079
7330
  "Cannot execute query: no backend configured. Use store.query() or pass a backend to createQueryBuilder()."
7080
7331
  );
7081
7332
  }
7082
- const compiled = this.compile();
7083
- const rows = await this.#config.backend.execute(compiled);
7333
+ const ast = this.toAst();
7334
+ if (hasParameterReferences(ast)) {
7335
+ throw new Error(
7336
+ "Aggregate queries do not support param() references; bind a concrete value instead."
7337
+ );
7338
+ }
7339
+ const executeRaw = backend.executeRaw;
7340
+ const template = executeRaw === void 0 ? void 0 : this.#resolveTemplate(ast);
7341
+ const rows = template !== void 0 && executeRaw !== void 0 ? (
7342
+ // Method call (not the detached `executeRaw` local) so a this-using
7343
+ // backend implementation keeps its receiver.
7344
+ await backend.executeRaw(
7345
+ template.sql,
7346
+ fillTemplateParams(
7347
+ template.params,
7348
+ {},
7349
+ this.#config.dialect ?? "sqlite"
7350
+ )
7351
+ )
7352
+ ) : await backend.execute(
7353
+ compileQuery(ast, this.#config.graphId, this.#compileOptions())
7354
+ );
7084
7355
  return this.#mapResults(rows);
7085
7356
  }
7086
7357
  /**
@@ -8234,7 +8505,6 @@ var QueryBuilder = class _QueryBuilder {
8234
8505
  };
8235
8506
 
8236
8507
  // src/query/builder/unionable-query.ts
8237
- var NOT_COMPUTED3 = /* @__PURE__ */ Symbol("NOT_COMPUTED");
8238
8508
  function executeOnBackend(backend, recordedAsOf, promise, surface) {
8239
8509
  if (recordedAsOf === void 0) return promise;
8240
8510
  return withRecordedRelationsPrecondition(promise, {
@@ -8248,10 +8518,14 @@ function recordedAsOfForComposableQuery(query) {
8248
8518
  }
8249
8519
  return query.recordedAsOf;
8250
8520
  }
8521
+ var NOT_COMPUTED3 = /* @__PURE__ */ Symbol("NOT_COMPUTED");
8251
8522
  var UnionableQuery = class _UnionableQuery {
8252
8523
  #config;
8253
8524
  #state;
8254
- #cachedCompiled = NOT_COMPUTED3;
8525
+ // Per-instance compiled placeholder template, reused across
8526
+ // execute()/executeOn() calls; the read instant is filled fresh per call.
8527
+ // NOT_COMPUTED = not yet built; undefined = no fast path.
8528
+ #template = NOT_COMPUTED3;
8255
8529
  constructor(config, state) {
8256
8530
  this.#config = config;
8257
8531
  this.#state = state;
@@ -8379,20 +8653,76 @@ var UnionableQuery = class _UnionableQuery {
8379
8653
  * Compiles the set operation to SQL.
8380
8654
  */
8381
8655
  compile() {
8382
- if (this.#cachedCompiled !== NOT_COMPUTED3) {
8383
- return this.#cachedCompiled;
8384
- }
8385
- const compiled = compileSetOperation2(
8656
+ return compileSetOperation2(
8386
8657
  this.toAst(),
8387
8658
  this.#config.graphId,
8388
8659
  this.#compileOptions()
8389
8660
  );
8390
- this.#cachedCompiled = compiled;
8391
- return compiled;
8392
8661
  }
8393
8662
  #compileOptions() {
8394
8663
  return buildCompileOptions(this.#config);
8395
8664
  }
8665
+ /**
8666
+ * The cached placeholder template for this set operation, or `undefined`
8667
+ * when no fast path applies. Built once per instance; every operand's read
8668
+ * instant is the shared placeholder, filled fresh per execution by
8669
+ * {@link fillTemplateParams}.
8670
+ */
8671
+ #resolveTemplate(ast) {
8672
+ if (this.#template !== NOT_COMPUTED3) return this.#template;
8673
+ this.#template = buildReadInstantTemplate({
8674
+ compile: () => compileSetOperation2(ast, this.#config.graphId, {
8675
+ ...this.#compileOptions(),
8676
+ readInstant: "placeholder"
8677
+ }),
8678
+ backend: this.#config.backend,
8679
+ needsReadInstant: composableNeedsCurrentReadInstant(ast)
8680
+ });
8681
+ return this.#template;
8682
+ }
8683
+ /**
8684
+ * Fetches raw rows for the set operation: cached template + `executeRaw`
8685
+ * when available, else a fresh literal compile via `backend.execute`.
8686
+ */
8687
+ async #fetchRows(backend, ast, surface) {
8688
+ const executeRaw = backend.executeRaw;
8689
+ const template = executeRaw === void 0 ? void 0 : this.#resolveTemplate(ast);
8690
+ return executeOnBackend(
8691
+ backend,
8692
+ recordedAsOfForComposableQuery(ast),
8693
+ template !== void 0 && executeRaw !== void 0 ? (
8694
+ // Method call (not the detached `executeRaw` local) so a this-using
8695
+ // backend implementation keeps its receiver.
8696
+ backend.executeRaw(
8697
+ template.sql,
8698
+ fillTemplateParams(
8699
+ template.params,
8700
+ {},
8701
+ this.#config.dialect ?? "sqlite"
8702
+ )
8703
+ )
8704
+ ) : backend.execute(
8705
+ compileSetOperation2(
8706
+ ast,
8707
+ this.#config.graphId,
8708
+ this.#compileOptions()
8709
+ )
8710
+ ),
8711
+ surface
8712
+ );
8713
+ }
8714
+ /** Applies the select-function transformation, if one is attached. */
8715
+ #mapRows(rows) {
8716
+ if (this.#state.selectFn && this.#state.startAlias) {
8717
+ return mapResults(
8718
+ rows,
8719
+ this.#state.startAlias,
8720
+ this.#state.traversals ?? [],
8721
+ this.#state.selectFn
8722
+ );
8723
+ }
8724
+ return rows;
8725
+ }
8396
8726
  /**
8397
8727
  * Executes the combined query.
8398
8728
  */
@@ -8409,22 +8739,7 @@ var UnionableQuery = class _UnionableQuery {
8409
8739
  "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
8410
8740
  );
8411
8741
  }
8412
- const compiled = this.compile();
8413
- const rows = await executeOnBackend(
8414
- backend,
8415
- recordedAsOfForComposableQuery(ast),
8416
- backend.execute(compiled),
8417
- "recorded-query"
8418
- );
8419
- if (this.#state.selectFn && this.#state.startAlias) {
8420
- return mapResults(
8421
- rows,
8422
- this.#state.startAlias,
8423
- this.#state.traversals ?? [],
8424
- this.#state.selectFn
8425
- );
8426
- }
8427
- return rows;
8742
+ return this.#mapRows(await this.#fetchRows(backend, ast, "recorded-query"));
8428
8743
  }
8429
8744
  /**
8430
8745
  * Executes the combined query against a provided backend.
@@ -8438,23 +8753,9 @@ var UnionableQuery = class _UnionableQuery {
8438
8753
  "Query contains param() references. Use .prepare().execute({...}) instead of .execute()."
8439
8754
  );
8440
8755
  }
8441
- const compiled = this.compile();
8442
- const recordedAsOf = recordedAsOfForComposableQuery(ast);
8443
- const rows = await executeOnBackend(
8444
- backend,
8445
- recordedAsOf,
8446
- backend.execute(compiled),
8447
- "recorded-batch-query"
8756
+ return this.#mapRows(
8757
+ await this.#fetchRows(backend, ast, "recorded-batch-query")
8448
8758
  );
8449
- if (this.#state.selectFn && this.#state.startAlias) {
8450
- return mapResults(
8451
- rows,
8452
- this.#state.startAlias,
8453
- this.#state.traversals ?? [],
8454
- this.#state.selectFn
8455
- );
8456
- }
8457
- return rows;
8458
8759
  }
8459
8760
  };
8460
8761
 
@@ -8674,6 +8975,7 @@ function createQueryBuilderWithContext(graphId, registry, options) {
8674
8975
  predicates: [],
8675
8976
  projection: [],
8676
8977
  orderBy: [],
8978
+ aggregateOrderBy: [],
8677
8979
  limit: void 0,
8678
8980
  offset: void 0,
8679
8981
  temporalMode: sealed?.mode ?? "current",
@@ -8836,7 +9138,7 @@ function processPage(schema, rows) {
8836
9138
  }
8837
9139
  let parsed;
8838
9140
  try {
8839
- parsed = JSON.parse(row.props);
9141
+ parsed = rowPropsToObject(row.props);
8840
9142
  } catch {
8841
9143
  skipped += 1;
8842
9144
  skippedIds.push(row.id);
@@ -8860,21 +9162,84 @@ function processPage(schema, rows) {
8860
9162
  }
8861
9163
  return { toUpsert, toDelete, skipped, skippedIds };
8862
9164
  }
8863
-
8864
- // src/store/search.ts
8865
- function resolveSearchSlot(ctx, nodeKind, fieldPath) {
9165
+ var SEARCH_CANDIDATE_ALIAS = "_sc";
9166
+ function buildKindCandidates(ctx, nodeKind, where) {
9167
+ if (where === void 0) return void 0;
9168
+ if (ctx.createQuery === void 0) {
9169
+ throw new ConfigurationError(
9170
+ "search with a where predicate requires a query-capable store",
9171
+ { capability: "search", graphId: ctx.graphId }
9172
+ );
9173
+ }
9174
+ const chain = ctx.createQuery().from(nodeKind, SEARCH_CANDIDATE_ALIAS).whereNode(SEARCH_CANDIDATE_ALIAS, where);
9175
+ const compiled = chain.select(
9176
+ (aliases) => aliases[SEARCH_CANDIDATE_ALIAS]
9177
+ ).compile();
9178
+ return sql`SELECT ${sql.raw(`"${SEARCH_CANDIDATE_ALIAS}_id"`)} AS node_id FROM (${compiled}) AS tg_search_candidates`;
9179
+ }
9180
+ function resolveSearchKinds(ctx, nodeKind, includeSubClasses) {
9181
+ if (includeSubClasses !== true) return [nodeKind];
9182
+ if (ctx.createQuery === void 0) {
9183
+ throw new ConfigurationError(
9184
+ "search with includeSubClasses requires a query-capable store",
9185
+ { capability: "search", graphId: ctx.graphId }
9186
+ );
9187
+ }
9188
+ return ctx.registry.expandSubClasses(nodeKind);
9189
+ }
9190
+ function assertSearchOffset(offset, label) {
9191
+ if (offset === void 0) return;
9192
+ if (!Number.isInteger(offset) || offset < 0) {
9193
+ throw new RangeError(
9194
+ `${label} must be a non-negative integer, got: ${offset}`
9195
+ );
9196
+ }
9197
+ }
9198
+ function effectiveFulltextLanguage(ctx, nodeKind, override) {
9199
+ if (override !== void 0) return override;
8866
9200
  const nodeType = ctx.registry.getNodeType(nodeKind);
8867
- if (nodeType !== void 0) {
8868
- const fields = getEmbeddingFields(nodeType.schema);
8869
- const field2 = fields.find((entry2) => entry2.fieldPath === fieldPath);
8870
- if (field2 !== void 0) {
8871
- return {
8872
- dimensions: field2.dimensions,
8873
- metric: field2.metric,
8874
- indexType: field2.indexType
8875
- };
8876
- }
9201
+ if (nodeType === void 0) return void 0;
9202
+ return resolveDeclaredFulltextLanguage(nodeType.schema);
9203
+ }
9204
+ function searchNodeKey(kind, nodeId) {
9205
+ return `${kind}\0${nodeId}`;
9206
+ }
9207
+ function scoreDescending(metric) {
9208
+ return metric === "cosine";
9209
+ }
9210
+ async function fetchNodesForRows(backend, graphId, rows) {
9211
+ const idsByKind = /* @__PURE__ */ new Map();
9212
+ for (const row of rows) {
9213
+ const ids = idsByKind.get(row.kind) ?? /* @__PURE__ */ new Set();
9214
+ ids.add(row.nodeId);
9215
+ idsByKind.set(row.kind, ids);
8877
9216
  }
9217
+ const map = /* @__PURE__ */ new Map();
9218
+ await Promise.all(
9219
+ [...idsByKind].map(async ([kind, ids]) => {
9220
+ const kindMap = await fetchNodesByIds(backend, graphId, kind, [...ids]);
9221
+ for (const [id, node] of kindMap) {
9222
+ map.set(searchNodeKey(kind, id), node);
9223
+ }
9224
+ })
9225
+ );
9226
+ return map;
9227
+ }
9228
+ function tryResolveSearchSlot(ctx, nodeKind, fieldPath) {
9229
+ const nodeType = ctx.registry.getNodeType(nodeKind);
9230
+ if (nodeType === void 0) return void 0;
9231
+ const fields = getEmbeddingFields(nodeType.schema);
9232
+ const field2 = fields.find((entry2) => entry2.fieldPath === fieldPath);
9233
+ if (field2 === void 0) return void 0;
9234
+ return {
9235
+ dimensions: field2.dimensions,
9236
+ metric: field2.metric,
9237
+ indexType: field2.indexType
9238
+ };
9239
+ }
9240
+ function resolveSearchSlot(ctx, nodeKind, fieldPath) {
9241
+ const slot = tryResolveSearchSlot(ctx, nodeKind, fieldPath);
9242
+ if (slot !== void 0) return slot;
8878
9243
  throw new ConfigurationError(
8879
9244
  `Node kind "${nodeKind}" has no embedding field "${fieldPath}". Declare it with embedding(dimensions) on the node schema.`,
8880
9245
  { capability: "vector", graphId: ctx.graphId }
@@ -8893,29 +9258,50 @@ async function executeFulltextSearch(ctx, nodeKind, options) {
8893
9258
  `fulltextSearch.limit must be a positive integer, got: ${options.limit}`
8894
9259
  );
8895
9260
  }
9261
+ assertSearchOffset(options.offset, "fulltextSearch.offset");
8896
9262
  validateFulltextCallOptions(backend, {
8897
9263
  mode: options.mode,
8898
9264
  includeSnippets: options.includeSnippets,
8899
9265
  language: options.language
8900
9266
  });
8901
- const params = {
8902
- graphId,
8903
- nodeKind,
8904
- query: options.query,
8905
- limit: options.limit,
8906
- ...options.mode ? { mode: options.mode } : {},
8907
- ...options.language ? { language: options.language } : {},
8908
- ...options.minScore === void 0 ? {} : { minScore: options.minScore },
8909
- ...options.includeSnippets === void 0 ? {} : { includeSnippets: options.includeSnippets }
8910
- };
8911
- const rows = await backend.fulltextSearch(params);
8912
- if (rows.length === 0) return [];
8913
- const ids = rows.map((row) => row.nodeId);
8914
- const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
9267
+ const kinds = resolveSearchKinds(ctx, nodeKind, options.includeSubClasses);
9268
+ const offset = options.offset ?? 0;
9269
+ const singleKind = kinds.length === 1;
9270
+ const perKindRows = await Promise.all(
9271
+ kinds.map(async (kind) => {
9272
+ const candidates = buildKindCandidates(ctx, kind, options.where);
9273
+ const language = effectiveFulltextLanguage(ctx, kind, options.language);
9274
+ const rows = await backend.fulltextSearch({
9275
+ graphId,
9276
+ nodeKind: kind,
9277
+ query: options.query,
9278
+ // A single kind pushes the page into SQL; multiple kinds fetch
9279
+ // each kind's full page-covering prefix and re-slice after merge.
9280
+ limit: singleKind ? options.limit : options.limit + offset,
9281
+ ...singleKind && offset > 0 ? { offset } : {},
9282
+ ...candidates === void 0 ? {} : { candidates },
9283
+ ...options.mode ? { mode: options.mode } : {},
9284
+ ...language === void 0 ? {} : { language },
9285
+ ...options.minScore === void 0 ? {} : { minScore: options.minScore },
9286
+ ...options.includeSnippets === void 0 ? {} : { includeSnippets: options.includeSnippets }
9287
+ });
9288
+ return rows.map((row) => ({
9289
+ kind,
9290
+ nodeId: row.nodeId,
9291
+ score: row.score,
9292
+ ...row.snippet === void 0 ? {} : { snippet: row.snippet }
9293
+ }));
9294
+ })
9295
+ );
9296
+ const merged = singleKind ? perKindRows[0] : perKindRows.flat().toSorted(
9297
+ (a, b) => b.score - a.score || compareCodePoints(a.kind, b.kind) || compareCodePoints(a.nodeId, b.nodeId)
9298
+ ).slice(offset, offset + options.limit);
9299
+ if (merged.length === 0) return [];
9300
+ const nodeMap = await fetchNodesForRows(backend, graphId, merged);
8915
9301
  const hits = [];
8916
9302
  let rank = 1;
8917
- for (const row of rows) {
8918
- const node = nodeMap.get(row.nodeId);
9303
+ for (const row of merged) {
9304
+ const node = nodeMap.get(searchNodeKey(row.kind, row.nodeId));
8919
9305
  if (!node) continue;
8920
9306
  hits.push({
8921
9307
  node,
@@ -8941,46 +9327,106 @@ async function executeVectorSearch(ctx, nodeKind, options) {
8941
9327
  );
8942
9328
  }
8943
9329
  assertEfSearch(options.efSearch, "vectorSearch.efSearch");
8944
- const slot = resolveSearchSlot(ctx, nodeKind, options.fieldPath);
8945
- assertVectorQueryCompatible(
8946
- backend,
8947
- slot,
8948
- { metric: options.metric, queryEmbedding: options.queryEmbedding },
9330
+ assertSearchOffset(options.offset, "vectorSearch.offset");
9331
+ const searchKinds = resolveVectorSearchKinds(
9332
+ ctx,
9333
+ nodeKind,
9334
+ options.fieldPath,
9335
+ options.includeSubClasses,
8949
9336
  "vectorSearch"
8950
9337
  );
8951
- assertMinScore(
8952
- options.minScore,
8953
- options.metric ?? slot.metric,
8954
- "vectorSearch.minScore"
9338
+ for (const { slot } of searchKinds) {
9339
+ assertVectorQueryCompatible(
9340
+ backend,
9341
+ slot,
9342
+ { metric: options.metric, queryEmbedding: options.queryEmbedding },
9343
+ "vectorSearch"
9344
+ );
9345
+ assertMinScore(
9346
+ options.minScore,
9347
+ options.metric ?? slot.metric,
9348
+ "vectorSearch.minScore"
9349
+ );
9350
+ }
9351
+ const offset = options.offset ?? 0;
9352
+ const singleKind = searchKinds.length === 1;
9353
+ const metric = options.metric ?? searchKinds[0].slot.metric;
9354
+ const perKindRows = await Promise.all(
9355
+ searchKinds.map(
9356
+ async ({ kind, slot }) => {
9357
+ const candidates = buildKindCandidates(ctx, kind, options.where);
9358
+ const rows = await backend.vectorSearch({
9359
+ graphId,
9360
+ nodeKind: kind,
9361
+ fieldPath: options.fieldPath,
9362
+ queryEmbedding: options.queryEmbedding,
9363
+ // Default to the field's DECLARED metric (the metric its index was
9364
+ // built for); only an explicit caller override changes it.
9365
+ // Defaulting to cosine here would mis-rank l2 / inner_product
9366
+ // fields and bypass their ANN index.
9367
+ metric,
9368
+ dimensions: slot.dimensions,
9369
+ indexType: slot.indexType,
9370
+ // A single kind pushes the page into SQL; multiple kinds fetch
9371
+ // each kind's full page-covering prefix and re-slice after merge.
9372
+ limit: singleKind ? options.limit : options.limit + offset,
9373
+ ...singleKind && offset > 0 ? { offset } : {},
9374
+ ...candidates === void 0 ? {} : { candidates },
9375
+ ...options.minScore === void 0 ? {} : { minScore: options.minScore },
9376
+ ...options.efSearch === void 0 ? {} : { efSearch: options.efSearch }
9377
+ });
9378
+ return rows.map((row) => ({
9379
+ kind,
9380
+ nodeId: row.nodeId,
9381
+ score: row.score
9382
+ }));
9383
+ }
9384
+ )
9385
+ );
9386
+ const merged = singleKind ? perKindRows[0] : mergeVectorRows(perKindRows, metric).slice(
9387
+ offset,
9388
+ offset + options.limit
8955
9389
  );
8956
- const rows = await backend.vectorSearch({
8957
- graphId,
8958
- nodeKind,
8959
- fieldPath: options.fieldPath,
8960
- queryEmbedding: options.queryEmbedding,
8961
- // Default to the field's DECLARED metric (the metric its index was built
8962
- // for); only an explicit caller override changes it. Defaulting to cosine
8963
- // here would mis-rank l2 / inner_product fields and bypass their ANN index.
8964
- metric: options.metric ?? slot.metric,
8965
- dimensions: slot.dimensions,
8966
- indexType: slot.indexType,
8967
- limit: options.limit,
8968
- ...options.minScore === void 0 ? {} : { minScore: options.minScore },
8969
- ...options.efSearch === void 0 ? {} : { efSearch: options.efSearch }
8970
- });
8971
- if (rows.length === 0) return [];
8972
- const ids = rows.map((row) => row.nodeId);
8973
- const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
9390
+ if (merged.length === 0) return [];
9391
+ const nodeMap = await fetchNodesForRows(backend, graphId, merged);
8974
9392
  const hits = [];
8975
9393
  let rank = 1;
8976
- for (const row of rows) {
8977
- const node = nodeMap.get(row.nodeId);
9394
+ for (const row of merged) {
9395
+ const node = nodeMap.get(searchNodeKey(row.kind, row.nodeId));
8978
9396
  if (!node) continue;
8979
9397
  hits.push({ node, score: row.score, rank });
8980
9398
  rank += 1;
8981
9399
  }
8982
9400
  return hits;
8983
9401
  }
9402
+ function resolveVectorSearchKinds(ctx, nodeKind, fieldPath, includeSubClasses, label) {
9403
+ const kinds = resolveSearchKinds(ctx, nodeKind, includeSubClasses);
9404
+ const resolved = [];
9405
+ for (const kind of kinds) {
9406
+ const slot = tryResolveSearchSlot(ctx, kind, fieldPath);
9407
+ if (slot !== void 0) resolved.push({ kind, slot });
9408
+ }
9409
+ if (resolved.length === 0) resolveSearchSlot(ctx, nodeKind, fieldPath);
9410
+ const metrics = new Set(resolved.map(({ slot }) => slot.metric));
9411
+ if (metrics.size > 1) {
9412
+ throw new ConfigurationError(
9413
+ `${label}: kinds expanded from "${nodeKind}" declare different metrics for "${fieldPath}" (${[...metrics].join(", ")}). Cross-kind vector ranking requires one shared declared metric \u2014 search the kinds separately.`,
9414
+ { capability: "vector", graphId: ctx.graphId }
9415
+ );
9416
+ }
9417
+ return resolved;
9418
+ }
9419
+ function mergeVectorRows(perKindRows, metric) {
9420
+ const descending = scoreDescending(metric);
9421
+ return perKindRows.flat().toSorted(
9422
+ (a, b) => (descending ? b.score - a.score : a.score - b.score) || compareCodePoints(a.kind, b.kind) || compareCodePoints(a.nodeId, b.nodeId)
9423
+ );
9424
+ }
9425
+ function mergeFulltextRows(perKindRows) {
9426
+ return perKindRows.flat().toSorted(
9427
+ (a, b) => b.score - a.score || compareCodePoints(a.kind, b.kind) || compareCodePoints(a.nodeId, b.nodeId)
9428
+ );
9429
+ }
8984
9430
  async function executeHybridSearch(ctx, nodeKind, options) {
8985
9431
  const { backend, graphId } = ctx;
8986
9432
  if (!backend.vectorSearch) {
@@ -9009,50 +9455,165 @@ async function executeHybridSearch(ctx, nodeKind, options) {
9009
9455
  includeSnippets: options.fulltext.includeSnippets,
9010
9456
  language: options.fulltext.language
9011
9457
  });
9458
+ assertSearchOffset(options.offset, "hybridSearch.offset");
9012
9459
  const fusionK = options.fusion?.k ?? DEFAULT_RRF_K;
9013
9460
  const vectorWeight = options.fusion?.weights?.vector ?? DEFAULT_RRF_WEIGHT;
9014
9461
  const fulltextWeight = options.fusion?.weights?.fulltext ?? DEFAULT_RRF_WEIGHT;
9462
+ const offset = options.offset ?? 0;
9015
9463
  const overFetchMultiplier = 4;
9016
- const vectorK = options.vector.k ?? options.limit * overFetchMultiplier;
9017
- const fulltextK = options.fulltext.k ?? options.limit * overFetchMultiplier;
9018
- const vectorSlot = resolveSearchSlot(ctx, nodeKind, options.vector.fieldPath);
9019
- assertVectorQueryCompatible(
9020
- backend,
9021
- vectorSlot,
9022
- {
9023
- metric: options.vector.metric,
9024
- queryEmbedding: options.vector.queryEmbedding
9025
- },
9026
- "hybridSearch.vector"
9027
- );
9028
- assertMinScore(
9029
- options.vector.minScore,
9030
- options.vector.metric ?? vectorSlot.metric,
9031
- "hybridSearch.vector.minScore"
9032
- );
9033
- const vectorPromise = backend.vectorSearch({
9034
- graphId,
9464
+ const pageLimit = options.limit + offset;
9465
+ const vectorK = options.vector.k ?? pageLimit * overFetchMultiplier;
9466
+ const fulltextK = options.fulltext.k ?? pageLimit * overFetchMultiplier;
9467
+ const fulltextKinds = resolveSearchKinds(
9468
+ ctx,
9035
9469
  nodeKind,
9036
- fieldPath: options.vector.fieldPath,
9037
- queryEmbedding: options.vector.queryEmbedding,
9038
- // Default to the field's declared metric (see executeVectorSearch).
9039
- metric: options.vector.metric ?? vectorSlot.metric,
9040
- dimensions: vectorSlot.dimensions,
9041
- indexType: vectorSlot.indexType,
9042
- limit: vectorK,
9043
- ...options.vector.minScore === void 0 ? {} : { minScore: options.vector.minScore },
9044
- ...options.vector.efSearch === void 0 ? {} : { efSearch: options.vector.efSearch }
9045
- });
9046
- const fulltextPromise = backend.fulltextSearch({
9047
- graphId,
9470
+ options.includeSubClasses
9471
+ );
9472
+ const vectorKinds = resolveVectorSearchKinds(
9473
+ ctx,
9048
9474
  nodeKind,
9049
- query: options.fulltext.query,
9050
- limit: fulltextK,
9051
- ...options.fulltext.mode ? { mode: options.fulltext.mode } : {},
9052
- ...options.fulltext.language ? { language: options.fulltext.language } : {},
9053
- ...options.fulltext.minScore === void 0 ? {} : { minScore: options.fulltext.minScore },
9054
- ...options.fulltext.includeSnippets === void 0 ? {} : { includeSnippets: options.fulltext.includeSnippets }
9055
- });
9475
+ options.vector.fieldPath,
9476
+ options.includeSubClasses,
9477
+ "hybridSearch.vector"
9478
+ );
9479
+ for (const { slot } of vectorKinds) {
9480
+ assertVectorQueryCompatible(
9481
+ backend,
9482
+ slot,
9483
+ {
9484
+ metric: options.vector.metric,
9485
+ queryEmbedding: options.vector.queryEmbedding
9486
+ },
9487
+ "hybridSearch.vector"
9488
+ );
9489
+ assertMinScore(
9490
+ options.vector.minScore,
9491
+ options.vector.metric ?? slot.metric,
9492
+ "hybridSearch.vector.minScore"
9493
+ );
9494
+ }
9495
+ const vectorMetric = options.vector.metric ?? vectorKinds[0].slot.metric;
9496
+ const candidatesByKind = /* @__PURE__ */ new Map();
9497
+ for (const kind of fulltextKinds) {
9498
+ candidatesByKind.set(kind, buildKindCandidates(ctx, kind, options.where));
9499
+ }
9500
+ for (const { kind } of vectorKinds) {
9501
+ if (!candidatesByKind.has(kind)) {
9502
+ candidatesByKind.set(kind, buildKindCandidates(ctx, kind, options.where));
9503
+ }
9504
+ }
9505
+ if (backend.hybridSearch !== void 0 && fulltextKinds.length === 1 && vectorKinds.length === 1 && fulltextKinds[0] === vectorKinds[0].kind) {
9506
+ const kind = fulltextKinds[0];
9507
+ const { slot } = vectorKinds[0];
9508
+ const candidates = candidatesByKind.get(kind);
9509
+ const hybridFulltextLanguage = effectiveFulltextLanguage(
9510
+ ctx,
9511
+ kind,
9512
+ options.fulltext.language
9513
+ );
9514
+ const rows = await backend.hybridSearch({
9515
+ graphId,
9516
+ nodeKind: kind,
9517
+ vector: {
9518
+ fieldPath: options.vector.fieldPath,
9519
+ queryEmbedding: options.vector.queryEmbedding,
9520
+ metric: vectorMetric,
9521
+ dimensions: slot.dimensions,
9522
+ indexType: slot.indexType,
9523
+ k: vectorK,
9524
+ ...options.vector.minScore === void 0 ? {} : { minScore: options.vector.minScore },
9525
+ ...options.vector.efSearch === void 0 ? {} : { efSearch: options.vector.efSearch }
9526
+ },
9527
+ fulltext: {
9528
+ query: options.fulltext.query,
9529
+ k: fulltextK,
9530
+ ...options.fulltext.mode ? { mode: options.fulltext.mode } : {},
9531
+ ...hybridFulltextLanguage === void 0 ? {} : { language: hybridFulltextLanguage },
9532
+ ...options.fulltext.minScore === void 0 ? {} : { minScore: options.fulltext.minScore },
9533
+ ...options.fulltext.includeSnippets === void 0 ? {} : { includeSnippets: options.fulltext.includeSnippets }
9534
+ },
9535
+ fusion: { k: fusionK, vectorWeight, fulltextWeight },
9536
+ limit: options.limit,
9537
+ ...offset === 0 ? {} : { offset },
9538
+ ...candidates === void 0 ? {} : { candidates }
9539
+ });
9540
+ return rows.map((row, index) => {
9541
+ const typedNode = rowToNode(row.node);
9542
+ return {
9543
+ node: typedNode,
9544
+ score: row.fusedScore,
9545
+ rank: index + 1,
9546
+ ...row.vectorRank !== void 0 && row.vectorScore !== void 0 ? {
9547
+ vector: {
9548
+ node: typedNode,
9549
+ score: row.vectorScore,
9550
+ rank: row.vectorRank
9551
+ }
9552
+ } : {},
9553
+ ...row.fulltextRank !== void 0 && row.fulltextScore !== void 0 ? {
9554
+ fulltext: {
9555
+ node: typedNode,
9556
+ score: row.fulltextScore,
9557
+ rank: row.fulltextRank,
9558
+ ...row.snippet === void 0 ? {} : { snippet: row.snippet }
9559
+ }
9560
+ } : {}
9561
+ };
9562
+ });
9563
+ }
9564
+ const vectorPromise = Promise.all(
9565
+ vectorKinds.map(
9566
+ async ({ kind, slot }) => {
9567
+ const candidates = candidatesByKind.get(kind);
9568
+ const rows = await backend.vectorSearch({
9569
+ graphId,
9570
+ nodeKind: kind,
9571
+ fieldPath: options.vector.fieldPath,
9572
+ queryEmbedding: options.vector.queryEmbedding,
9573
+ // Default to the field's declared metric (see executeVectorSearch).
9574
+ metric: vectorMetric,
9575
+ dimensions: slot.dimensions,
9576
+ indexType: slot.indexType,
9577
+ limit: vectorK,
9578
+ ...candidates === void 0 ? {} : { candidates },
9579
+ ...options.vector.minScore === void 0 ? {} : { minScore: options.vector.minScore },
9580
+ ...options.vector.efSearch === void 0 ? {} : { efSearch: options.vector.efSearch }
9581
+ });
9582
+ return rows.map((row) => ({
9583
+ kind,
9584
+ nodeId: row.nodeId,
9585
+ score: row.score
9586
+ }));
9587
+ }
9588
+ )
9589
+ ).then((perKind) => mergeVectorRows(perKind, vectorMetric).slice(0, vectorK));
9590
+ const fulltextPromise = Promise.all(
9591
+ fulltextKinds.map(async (kind) => {
9592
+ const candidates = candidatesByKind.get(kind);
9593
+ const language = effectiveFulltextLanguage(
9594
+ ctx,
9595
+ kind,
9596
+ options.fulltext.language
9597
+ );
9598
+ const rows = await backend.fulltextSearch({
9599
+ graphId,
9600
+ nodeKind: kind,
9601
+ query: options.fulltext.query,
9602
+ limit: fulltextK,
9603
+ ...candidates === void 0 ? {} : { candidates },
9604
+ ...options.fulltext.mode ? { mode: options.fulltext.mode } : {},
9605
+ ...language === void 0 ? {} : { language },
9606
+ ...options.fulltext.minScore === void 0 ? {} : { minScore: options.fulltext.minScore },
9607
+ ...options.fulltext.includeSnippets === void 0 ? {} : { includeSnippets: options.fulltext.includeSnippets }
9608
+ });
9609
+ return rows.map((row) => ({
9610
+ kind,
9611
+ nodeId: row.nodeId,
9612
+ score: row.score,
9613
+ ...row.snippet === void 0 ? {} : { snippet: row.snippet }
9614
+ }));
9615
+ })
9616
+ ).then((perKind) => mergeFulltextRows(perKind).slice(0, fulltextK));
9056
9617
  const [vectorRows, fulltextRows] = await Promise.all([
9057
9618
  vectorPromise,
9058
9619
  fulltextPromise
@@ -9061,19 +9622,23 @@ async function executeHybridSearch(ctx, nodeKind, options) {
9061
9622
  for (const [index, row] of vectorRows.entries()) {
9062
9623
  const rank2 = index + 1;
9063
9624
  const contribution = vectorWeight / (fusionK + rank2);
9064
- const entry2 = fused.get(row.nodeId) ?? {
9625
+ const key = searchNodeKey(row.kind, row.nodeId);
9626
+ const entry2 = fused.get(key) ?? {
9627
+ kind: row.kind,
9065
9628
  nodeId: row.nodeId,
9066
9629
  fusedScore: 0
9067
9630
  };
9068
9631
  entry2.fusedScore += contribution;
9069
9632
  entry2.vectorRank = rank2;
9070
9633
  entry2.vectorScore = row.score;
9071
- fused.set(row.nodeId, entry2);
9634
+ fused.set(key, entry2);
9072
9635
  }
9073
9636
  for (const [index, row] of fulltextRows.entries()) {
9074
9637
  const rank2 = index + 1;
9075
9638
  const contribution = fulltextWeight / (fusionK + rank2);
9076
- const entry2 = fused.get(row.nodeId) ?? {
9639
+ const key = searchNodeKey(row.kind, row.nodeId);
9640
+ const entry2 = fused.get(key) ?? {
9641
+ kind: row.kind,
9077
9642
  nodeId: row.nodeId,
9078
9643
  fusedScore: 0
9079
9644
  };
@@ -9083,17 +9648,24 @@ async function executeHybridSearch(ctx, nodeKind, options) {
9083
9648
  if (row.snippet !== void 0) {
9084
9649
  entry2.fulltextSnippet = row.snippet;
9085
9650
  }
9086
- fused.set(row.nodeId, entry2);
9651
+ fused.set(key, entry2);
9087
9652
  }
9088
9653
  const ranked = [...fused.values()].toSorted(
9089
- (a, b) => b.fusedScore - a.fusedScore || compareStrings(a.nodeId, b.nodeId)
9090
- ).slice(0, options.limit);
9091
- const ids = ranked.map((entry2) => entry2.nodeId);
9092
- const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
9654
+ (a, b) => b.fusedScore - a.fusedScore || compareCodePoints(a.kind, b.kind) || compareCodePoints(a.nodeId, b.nodeId)
9655
+ ).slice(offset, offset + options.limit);
9656
+ const nodeMap = await fetchNodesForRows(
9657
+ backend,
9658
+ graphId,
9659
+ ranked.map((entry2) => ({
9660
+ kind: entry2.kind,
9661
+ nodeId: entry2.nodeId,
9662
+ score: entry2.fusedScore
9663
+ }))
9664
+ );
9093
9665
  const hits = [];
9094
9666
  let rank = 1;
9095
9667
  for (const entry2 of ranked) {
9096
- const node = nodeMap.get(entry2.nodeId);
9668
+ const node = nodeMap.get(searchNodeKey(entry2.kind, entry2.nodeId));
9097
9669
  if (!node) continue;
9098
9670
  const typedNode = node;
9099
9671
  const hit = {
@@ -9259,12 +9831,10 @@ var StoreSearch = class {
9259
9831
  async fulltext(nodeKind, options) {
9260
9832
  this.#assertKindRegistered(nodeKind);
9261
9833
  return executeFulltextSearch(
9262
- {
9263
- graphId: this.#context.graphId,
9264
- backend: this.#context.backend,
9265
- registry: this.#context.registry
9266
- },
9834
+ this.#context,
9267
9835
  nodeKind,
9836
+ // Contravariance: the narrowed accessor callback is intentionally
9837
+ // wider than the base instantiation the core helpers take.
9268
9838
  options
9269
9839
  );
9270
9840
  }
@@ -9282,12 +9852,10 @@ var StoreSearch = class {
9282
9852
  async vector(nodeKind, options) {
9283
9853
  this.#assertKindRegistered(nodeKind);
9284
9854
  return executeVectorSearch(
9285
- {
9286
- graphId: this.#context.graphId,
9287
- backend: this.#context.backend,
9288
- registry: this.#context.registry
9289
- },
9855
+ this.#context,
9290
9856
  nodeKind,
9857
+ // Contravariance: the narrowed accessor callback is intentionally
9858
+ // wider than the base instantiation the core helpers take.
9291
9859
  options
9292
9860
  );
9293
9861
  }
@@ -9303,12 +9871,10 @@ var StoreSearch = class {
9303
9871
  async hybrid(nodeKind, options) {
9304
9872
  this.#assertKindRegistered(nodeKind);
9305
9873
  return executeHybridSearch(
9306
- {
9307
- graphId: this.#context.graphId,
9308
- backend: this.#context.backend,
9309
- registry: this.#context.registry
9310
- },
9874
+ this.#context,
9311
9875
  nodeKind,
9876
+ // Contravariance: the narrowed accessor callback is intentionally
9877
+ // wider than the base instantiation the core helpers take.
9312
9878
  options
9313
9879
  );
9314
9880
  }
@@ -9888,30 +10454,6 @@ function buildCompileTimeReferentIndex(graph, runtimeEdgeNames) {
9888
10454
  return index;
9889
10455
  }
9890
10456
 
9891
- // src/registry/builders.ts
9892
- function buildKindRegistry(graph) {
9893
- const nodeTypes = extractNodeTypes(graph);
9894
- const edgeTypes = extractEdgeTypes(graph);
9895
- const closures = graph.ontology.length > 0 ? computeClosuresFromOntology(graph.ontology) : createEmptyClosures();
9896
- return new KindRegistry(nodeTypes, edgeTypes, closures);
9897
- }
9898
- function extractNodeTypes(graph) {
9899
- const result = /* @__PURE__ */ new Map();
9900
- for (const typeName of getNodeKinds(graph)) {
9901
- const registration = graph.nodes[typeName];
9902
- result.set(typeName, registration.type);
9903
- }
9904
- return result;
9905
- }
9906
- function extractEdgeTypes(graph) {
9907
- const result = /* @__PURE__ */ new Map();
9908
- for (const typeName of getEdgeKinds(graph)) {
9909
- const registration = graph.edges[typeName];
9910
- result.set(typeName, registration.type);
9911
- }
9912
- return result;
9913
- }
9914
-
9915
10457
  // src/store/algorithms/context.ts
9916
10458
  var DEFAULT_ALGORITHM_MAX_HOPS = 10;
9917
10459
  var DEFAULT_NEIGHBOR_DEPTH = 1;
@@ -9944,7 +10486,7 @@ function resolveTemporalFilter(ctx, options, tableAlias) {
9944
10486
  asOf: resolved.asOf,
9945
10487
  recordedAsOf: resolved.recordedAsOf,
9946
10488
  tableAlias,
9947
- currentTimestamp: ctx.dialect.currentTimestamp()
10489
+ currentTimestamp: currentReadInstant()
9948
10490
  };
9949
10491
  return compileTemporalFilter(filterOptions);
9950
10492
  }
@@ -9982,15 +10524,17 @@ function assertEdgeKinds(edges) {
9982
10524
  // src/store/algorithms/degree.ts
9983
10525
  async function executeDegree(ctx, nodeId, options = {}) {
9984
10526
  const direction = options.direction ?? "both";
10527
+ const edgeKinds = options.edges ?? Object.keys(ctx.graph.edges);
10528
+ if (edgeKinds.length === 0) return 0;
10529
+ const schema = resolveReadSchema(ctx, options);
9985
10530
  const whereClauses = [
9986
10531
  sql`graph_id = ${ctx.graphId}`,
9987
10532
  resolveTemporalFilter(ctx, options),
9988
- compileDirectionFilter(direction, nodeId)
10533
+ compileDirectionFilter(ctx, direction, nodeId, schema.nodesTable)
9989
10534
  ];
9990
10535
  if (options.edges !== void 0) {
9991
10536
  whereClauses.push(compileKindFilter(sql.raw("kind"), options.edges));
9992
10537
  }
9993
- const schema = resolveReadSchema(ctx, options);
9994
10538
  const countExpr = direction === "both" ? sql`COUNT(DISTINCT id)` : sql`COUNT(*)`;
9995
10539
  const query = sql`SELECT ${countExpr} AS count FROM ${schema.edgesTable} WHERE ${sql.join(whereClauses, sql` AND `)}`;
9996
10540
  const rows = await ctx.backend.execute(
@@ -9998,16 +10542,22 @@ async function executeDegree(ctx, nodeId, options = {}) {
9998
10542
  );
9999
10543
  return Number(rows[0]?.count ?? 0);
10000
10544
  }
10001
- function compileDirectionFilter(direction, nodeId) {
10545
+ function nodeKindSubquery(ctx, nodeId, nodesTable) {
10546
+ return sql`(SELECT kind FROM ${nodesTable} WHERE graph_id = ${ctx.graphId} AND id = ${nodeId} LIMIT 1)`;
10547
+ }
10548
+ function compileDirectionFilter(ctx, direction, nodeId, nodesTable) {
10549
+ const nodeKind = nodeKindSubquery(ctx, nodeId, nodesTable);
10550
+ const fromSide = sql`(from_kind = ${nodeKind} AND from_id = ${nodeId})`;
10551
+ const toSide = sql`(to_kind = ${nodeKind} AND to_id = ${nodeId})`;
10002
10552
  switch (direction) {
10003
10553
  case "out": {
10004
- return sql`from_id = ${nodeId}`;
10554
+ return fromSide;
10005
10555
  }
10006
10556
  case "in": {
10007
- return sql`to_id = ${nodeId}`;
10557
+ return toSide;
10008
10558
  }
10009
10559
  case "both": {
10010
- return sql`(from_id = ${nodeId} OR to_id = ${nodeId})`;
10560
+ return sql`(${fromSide} OR ${toSide})`;
10011
10561
  }
10012
10562
  }
10013
10563
  }
@@ -10161,6 +10711,8 @@ function createGraphAlgorithms(params) {
10161
10711
  const allowRecordedAsOf = params.allowRecordedAsOf === true;
10162
10712
  const ctx = {
10163
10713
  graphId: params.graphId,
10714
+ graph: params.graph,
10715
+ registry: params.registry,
10164
10716
  backend: params.backend,
10165
10717
  dialect: getDialect(params.backend.dialect),
10166
10718
  schema: params.schema ?? createSqlSchema(params.backend.tableNames),
@@ -10391,9 +10943,11 @@ function createEdgeCollection(config) {
10391
10943
  const results2 = await backend.transaction(
10392
10944
  async (txBackend) => executeEdgeCreateBatch2(batchInputs, txBackend)
10393
10945
  );
10946
+ await config.maybeRefreshStatisticsAfterBulk?.(results2.length);
10394
10947
  return narrowEdges(results2);
10395
10948
  }
10396
10949
  const results = await executeEdgeCreateBatch2(batchInputs, backend);
10950
+ await config.maybeRefreshStatisticsAfterBulk?.(results.length);
10397
10951
  return narrowEdges(results);
10398
10952
  },
10399
10953
  async bulkUpsertById(items) {
@@ -10426,26 +10980,25 @@ function createEdgeCollection(config) {
10426
10980
  }
10427
10981
  itemIndex++;
10428
10982
  }
10429
- const results = Array.from({ length: items.length });
10983
+ const results2 = Array.from({ length: items.length });
10430
10984
  if (toCreate.length > 0) {
10431
10985
  const createInputs = toCreate.map((entry2) => entry2.input);
10432
10986
  const created = await executeEdgeCreateBatch2(createInputs, target);
10433
10987
  for (const [index, entry2] of toCreate.entries()) {
10434
- results[entry2.index] = narrowEdge(created[index]);
10988
+ results2[entry2.index] = narrowEdge(created[index]);
10435
10989
  }
10436
10990
  }
10437
10991
  for (const entry2 of toUpdate) {
10438
10992
  const result = await executeEdgeUpsertUpdate2(entry2.input, target, {
10439
10993
  clearDeleted: entry2.clearDeleted
10440
10994
  });
10441
- results[entry2.index] = narrowEdge(result);
10995
+ results2[entry2.index] = narrowEdge(result);
10442
10996
  }
10443
- return results;
10997
+ return results2;
10444
10998
  };
10445
- if (backend.capabilities.transactions && "transaction" in backend) {
10446
- return backend.transaction(async (txBackend) => upsertAll(txBackend));
10447
- }
10448
- return upsertAll(backend);
10999
+ const results = backend.capabilities.transactions && "transaction" in backend ? await backend.transaction(async (txBackend) => upsertAll(txBackend)) : await upsertAll(backend);
11000
+ await config.maybeRefreshStatisticsAfterBulk?.(results.length);
11001
+ return results;
10449
11002
  },
10450
11003
  async bulkInsert(items) {
10451
11004
  const batchInputs = mapBulkEdgeInputs(kind, items);
@@ -10453,9 +11006,11 @@ function createEdgeCollection(config) {
10453
11006
  await backend.transaction(async (txBackend) => {
10454
11007
  await executeEdgeCreateNoReturnBatch2(batchInputs, txBackend);
10455
11008
  });
11009
+ await config.maybeRefreshStatisticsAfterBulk?.(batchInputs.length);
10456
11010
  return;
10457
11011
  }
10458
11012
  await executeEdgeCreateNoReturnBatch2(batchInputs, backend);
11013
+ await config.maybeRefreshStatisticsAfterBulk?.(batchInputs.length);
10459
11014
  },
10460
11015
  async bulkDelete(ids) {
10461
11016
  if (ids.length === 0) return;
@@ -10683,9 +11238,11 @@ function createNodeCollection(config) {
10683
11238
  const results2 = await backend.transaction(
10684
11239
  async (txBackend) => executeNodeCreateBatch2(batchInputs, txBackend)
10685
11240
  );
11241
+ await config.maybeRefreshStatisticsAfterBulk?.(results2.length);
10686
11242
  return narrowNodes(results2);
10687
11243
  }
10688
11244
  const results = await executeNodeCreateBatch2(batchInputs, backend);
11245
+ await config.maybeRefreshStatisticsAfterBulk?.(results.length);
10689
11246
  return narrowNodes(results);
10690
11247
  },
10691
11248
  async bulkUpsertById(items) {
@@ -10725,26 +11282,25 @@ function createNodeCollection(config) {
10725
11282
  }
10726
11283
  itemIndex++;
10727
11284
  }
10728
- const results = Array.from({ length: items.length });
11285
+ const results2 = Array.from({ length: items.length });
10729
11286
  if (toCreate.length > 0) {
10730
11287
  const createInputs = toCreate.map((entry2) => entry2.input);
10731
11288
  const created = await executeNodeCreateBatch2(createInputs, target);
10732
11289
  for (const [index, entry2] of toCreate.entries()) {
10733
- results[entry2.index] = narrowNode(created[index]);
11290
+ results2[entry2.index] = narrowNode(created[index]);
10734
11291
  }
10735
11292
  }
10736
11293
  for (const entry2 of toUpdate) {
10737
11294
  const result = await executeNodeUpsertUpdate2(entry2.input, target, {
10738
11295
  clearDeleted: entry2.clearDeleted
10739
11296
  });
10740
- results[entry2.index] = narrowNode(result);
11297
+ results2[entry2.index] = narrowNode(result);
10741
11298
  }
10742
- return results;
11299
+ return results2;
10743
11300
  };
10744
- if (backend.capabilities.transactions && "transaction" in backend) {
10745
- return backend.transaction(async (txBackend) => upsertAll(txBackend));
10746
- }
10747
- return upsertAll(backend);
11301
+ const results = backend.capabilities.transactions && "transaction" in backend ? await backend.transaction(async (txBackend) => upsertAll(txBackend)) : await upsertAll(backend);
11302
+ await config.maybeRefreshStatisticsAfterBulk?.(results.length);
11303
+ return results;
10748
11304
  },
10749
11305
  async bulkInsert(items) {
10750
11306
  const batchInputs = mapBulkNodeInputs(kind, items);
@@ -10752,9 +11308,11 @@ function createNodeCollection(config) {
10752
11308
  await backend.transaction(async (txBackend) => {
10753
11309
  await executeNodeCreateNoReturnBatch2(batchInputs, txBackend);
10754
11310
  });
11311
+ await config.maybeRefreshStatisticsAfterBulk?.(batchInputs.length);
10755
11312
  return;
10756
11313
  }
10757
11314
  await executeNodeCreateNoReturnBatch2(batchInputs, backend);
11315
+ await config.maybeRefreshStatisticsAfterBulk?.(batchInputs.length);
10758
11316
  },
10759
11317
  async bulkDelete(ids) {
10760
11318
  if (ids.length === 0) return;
@@ -10969,6 +11527,17 @@ async function runMaterialization(items, options, runOne) {
10969
11527
  }
10970
11528
 
10971
11529
  // src/store/materialize-indexes.ts
11530
+ var CLAIM_LEASE_MS = 15 * 6e4;
11531
+ var CLAIM_RETRY_DELAY_MS = 200;
11532
+ var CLAIM_WAIT_TIMEOUT_MS = CLAIM_LEASE_MS + 6e4;
11533
+ function hasIndexBuildClaimProtocol(backend) {
11534
+ return backend.claimIndexMaterialization !== void 0 && backend.releaseIndexMaterializationClaim !== void 0;
11535
+ }
11536
+ function delay(ms) {
11537
+ return new Promise((resolve) => {
11538
+ setTimeout(resolve, ms);
11539
+ });
11540
+ }
10972
11541
  async function materializeIndexes(context, options = {}) {
10973
11542
  const { graph, graphId, backend, schemaVersion } = context;
10974
11543
  if (backend.executeDdl === void 0) {
@@ -11024,12 +11593,18 @@ async function materializeIndexes(context, options = {}) {
11024
11593
  backend,
11025
11594
  statusKeys
11026
11595
  );
11596
+ const relationalPhysicalNames = candidates.filter((declaration) => declaration.entity !== "vector").map((declaration) => declaration.name);
11597
+ const invalidLeftovers = await preloadInvalidIndexLeftovers(
11598
+ backend,
11599
+ relationalPhysicalNames
11600
+ );
11027
11601
  const materializeEntry = (declaration) => declaration.entity === "vector" ? materializeVectorIndex(
11028
11602
  declaration,
11029
11603
  backend,
11030
11604
  graphId,
11031
11605
  schemaVersion,
11032
- existingByStatusKey
11606
+ existingByStatusKey,
11607
+ invalidLeftovers
11033
11608
  ) : materializeRelationalIndex(
11034
11609
  declaration,
11035
11610
  backend,
@@ -11037,16 +11612,23 @@ async function materializeIndexes(context, options = {}) {
11037
11612
  ddlOptions,
11038
11613
  graphId,
11039
11614
  schemaVersion,
11040
- existingByStatusKey
11615
+ existingByStatusKey,
11616
+ invalidLeftovers
11041
11617
  );
11042
11618
  if (options.stopOnError === true) {
11043
- const results = [];
11619
+ const results2 = [];
11044
11620
  for (const declaration of candidates) {
11045
11621
  const entry2 = await materializeEntry(declaration);
11046
- results.push(entry2);
11622
+ results2.push(entry2);
11047
11623
  if (entry2.status === "failed") break;
11048
11624
  }
11049
- return { results };
11625
+ await refreshStatisticsAfterCreation(
11626
+ backend,
11627
+ results2,
11628
+ options,
11629
+ ddlOptions.concurrent
11630
+ );
11631
+ return { results: results2 };
11050
11632
  }
11051
11633
  const buckets = /* @__PURE__ */ new Map();
11052
11634
  for (const declaration of candidates) {
@@ -11073,14 +11655,46 @@ async function materializeIndexes(context, options = {}) {
11073
11655
  }
11074
11656
  bucketIndex += 1;
11075
11657
  }
11076
- return {
11077
- results: candidates.map((declaration) => byDeclaration.get(declaration))
11078
- };
11658
+ const results = candidates.map(
11659
+ (declaration) => byDeclaration.get(declaration)
11660
+ );
11661
+ await refreshStatisticsAfterCreation(
11662
+ backend,
11663
+ results,
11664
+ options,
11665
+ ddlOptions.concurrent
11666
+ );
11667
+ return { results };
11668
+ }
11669
+ async function refreshStatisticsAfterCreation(backend, results, options, usesConcurrentBuilds) {
11670
+ if (options.refreshStatistics === false) return;
11671
+ if (usesConcurrentBuilds && !hasIndexBuildClaimProtocol(backend)) return;
11672
+ if (!results.some((entry2) => entry2.status === "created")) return;
11673
+ try {
11674
+ await backend.refreshStatistics();
11675
+ } catch (error) {
11676
+ if (typeof console === "undefined" || typeof console.warn !== "function") {
11677
+ return;
11678
+ }
11679
+ console.warn(
11680
+ "[typegraph] materializeIndexes created its indexes but the follow-up statistics refresh failed; run store.refreshStatistics() to give the planner fresh statistics.",
11681
+ error
11682
+ );
11683
+ }
11079
11684
  }
11080
11685
  function parallelBucketKey(declaration) {
11081
11686
  return declaration.entity;
11082
11687
  }
11083
- async function materializeRelationalIndex(declaration, backend, dialect, ddlOptions, graphId, schemaVersion, existingByStatusKey) {
11688
+ async function materializeRelationalIndex(declaration, backend, dialect, ddlOptions, graphId, schemaVersion, existingByStatusKey, invalidLeftovers) {
11689
+ if (declaration.method !== void 0 && dialect !== "postgres") {
11690
+ return {
11691
+ indexName: declaration.name,
11692
+ entity: declaration.entity,
11693
+ kind: declaration.kind,
11694
+ status: "skipped",
11695
+ reason: `Index method "${declaration.method}" requires PostgreSQL (expression GIN${declaration.method === "trigram" ? " + pg_trgm" : ""}); SQLite serves substring search via FTS5 fulltext instead.`
11696
+ };
11697
+ }
11084
11698
  const ddl = generateIndexDDL(declaration, dialect, ddlOptions);
11085
11699
  const targetTable = declaration.entity === "node" ? ddlOptions.nodesTableName ?? "typegraph_nodes" : ddlOptions.edgesTableName ?? "typegraph_edges";
11086
11700
  const signature = await computeIndexSignature(
@@ -11092,11 +11706,17 @@ async function materializeRelationalIndex(declaration, backend, dialect, ddlOpti
11092
11706
  statusKey: declaration.name,
11093
11707
  signature,
11094
11708
  driftLabel: "Index",
11095
- run: () => backend.executeDdl(ddl),
11096
- existingByStatusKey
11709
+ run: async () => {
11710
+ if (declaration.method === "trigram") {
11711
+ await backend.executeDdl("CREATE EXTENSION IF NOT EXISTS pg_trgm;");
11712
+ }
11713
+ await backend.executeDdl(ddl);
11714
+ },
11715
+ existingByStatusKey,
11716
+ invalidLeftovers
11097
11717
  });
11098
11718
  }
11099
- async function materializeVectorIndex(declaration, backend, graphId, schemaVersion, existingByStatusKey) {
11719
+ async function materializeVectorIndex(declaration, backend, graphId, schemaVersion, existingByStatusKey, invalidLeftovers) {
11100
11720
  if (declaration.indexType === "none") {
11101
11721
  return skippedEntry(
11102
11722
  declaration,
@@ -11150,33 +11770,35 @@ async function materializeVectorIndex(declaration, backend, graphId, schemaVersi
11150
11770
  signature,
11151
11771
  driftLabel: "Vector index",
11152
11772
  run: () => backend.createVectorIndex(params),
11153
- existingByStatusKey
11773
+ existingByStatusKey,
11774
+ invalidLeftovers
11154
11775
  });
11155
11776
  }
11156
11777
  async function materializeOne(declaration, backend, graphId, schemaVersion, action) {
11157
11778
  const recordIndexMaterialization = backend.recordIndexMaterialization;
11158
11779
  const { statusKey, signature, driftLabel, run, existingByStatusKey } = action;
11159
11780
  const statusOverride = statusKey === declaration.name ? void 0 : { statusName: statusKey };
11160
- const existing = existingByStatusKey.get(statusKey);
11161
- if (existing?.materializedAt !== void 0) {
11162
- if (existing.signature === signature) {
11163
- return entry(declaration, "alreadyMaterialized");
11164
- }
11165
- const error = new Error(
11166
- `${driftLabel} "${declaration.name}" already materialized with a different signature (recorded by graph "${existing.graphId}" at version ${existing.schemaVersion}). Drop the index manually and retry, or rename the new declaration.`
11167
- );
11168
- await recordIndexMaterialization(
11169
- buildAttempt({
11170
- declaration,
11171
- graphId,
11172
- signature,
11173
- schemaVersion,
11174
- materializedAt: void 0,
11175
- error,
11176
- ...statusOverride
11177
- })
11178
- );
11179
- return entry(declaration, "failed", error);
11781
+ const settled = await settleAgainstExisting(
11782
+ existingByStatusKey.get(statusKey),
11783
+ declaration,
11784
+ backend,
11785
+ graphId,
11786
+ schemaVersion,
11787
+ {
11788
+ statusKey,
11789
+ signature,
11790
+ driftLabel,
11791
+ isInvalidLeftover: (physicalIndexName) => action.invalidLeftovers.has(physicalIndexName)
11792
+ }
11793
+ );
11794
+ if (settled !== void 0) return settled;
11795
+ if (hasIndexBuildClaimProtocol(backend)) {
11796
+ return materializeWithClaim(declaration, backend, graphId, schemaVersion, {
11797
+ statusKey,
11798
+ signature,
11799
+ driftLabel,
11800
+ run
11801
+ });
11180
11802
  }
11181
11803
  try {
11182
11804
  await run();
@@ -11210,6 +11832,161 @@ async function materializeOne(declaration, backend, graphId, schemaVersion, acti
11210
11832
  return entry(declaration, "failed", error);
11211
11833
  }
11212
11834
  }
11835
+ async function settleAgainstExisting(existing, declaration, backend, graphId, schemaVersion, action) {
11836
+ if (existing?.materializedAt === void 0) return void 0;
11837
+ const { statusKey, signature, driftLabel, isInvalidLeftover } = action;
11838
+ if (existing.signature === signature) {
11839
+ if (declaration.entity !== "vector" && hasIndexBuildClaimProtocol(backend) && await isInvalidLeftover(declaration.name)) {
11840
+ return void 0;
11841
+ }
11842
+ return entry(declaration, "alreadyMaterialized");
11843
+ }
11844
+ const error = new Error(
11845
+ `${driftLabel} "${declaration.name}" already materialized with a different signature (recorded by graph "${existing.graphId}" at version ${existing.schemaVersion}). Drop the index manually and retry, or rename the new declaration.`
11846
+ );
11847
+ await backend.recordIndexMaterialization(
11848
+ buildAttempt({
11849
+ declaration,
11850
+ graphId,
11851
+ signature,
11852
+ schemaVersion,
11853
+ materializedAt: void 0,
11854
+ error,
11855
+ ...statusKey === declaration.name ? {} : { statusName: statusKey }
11856
+ })
11857
+ );
11858
+ return entry(declaration, "failed", error);
11859
+ }
11860
+ async function materializeWithClaim(declaration, backend, graphId, schemaVersion, action) {
11861
+ const { statusKey, signature, driftLabel, run } = action;
11862
+ const statusOverride = statusKey === declaration.name ? void 0 : { statusName: statusKey };
11863
+ const recordIndexMaterialization = backend.recordIndexMaterialization;
11864
+ const token = `${statusKey}:${nowIso()}:${Math.floor(performance.now() * 1e3)}`;
11865
+ const deadline = Date.now() + CLAIM_WAIT_TIMEOUT_MS;
11866
+ for (; ; ) {
11867
+ const claimed = await backend.claimIndexMaterialization({
11868
+ indexName: statusKey,
11869
+ graphId,
11870
+ entity: declaration.entity,
11871
+ kind: declaration.kind,
11872
+ signature,
11873
+ schemaVersion,
11874
+ token,
11875
+ leaseMs: CLAIM_LEASE_MS
11876
+ });
11877
+ if (!claimed) {
11878
+ if (Date.now() >= deadline) {
11879
+ return entry(
11880
+ declaration,
11881
+ "failed",
11882
+ new Error(
11883
+ `Timed out waiting for a concurrent materializer's claim on "${statusKey}" (waited ${String(CLAIM_WAIT_TIMEOUT_MS)}ms). If its holder crashed, retry after the lease expires.`
11884
+ )
11885
+ );
11886
+ }
11887
+ await delay(CLAIM_RETRY_DELAY_MS);
11888
+ continue;
11889
+ }
11890
+ try {
11891
+ const fresh = await backend.getIndexMaterialization(statusKey);
11892
+ const settled = await settleAgainstExisting(
11893
+ fresh,
11894
+ declaration,
11895
+ backend,
11896
+ graphId,
11897
+ schemaVersion,
11898
+ {
11899
+ statusKey,
11900
+ signature,
11901
+ driftLabel,
11902
+ // Post-claim the bulk preload is stale (we may have waited on
11903
+ // another builder's claim), so re-check this index fresh.
11904
+ isInvalidLeftover: (physicalIndexName) => hasInvalidIndexLeftover(backend, physicalIndexName)
11905
+ }
11906
+ );
11907
+ if (settled !== void 0) return settled;
11908
+ if (declaration.entity !== "vector") {
11909
+ await dropInvalidIndexLeftover(backend, declaration.name);
11910
+ }
11911
+ try {
11912
+ await run();
11913
+ const attemptedAt = nowIso();
11914
+ await recordIndexMaterialization(
11915
+ buildAttempt({
11916
+ declaration,
11917
+ graphId,
11918
+ signature,
11919
+ schemaVersion,
11920
+ materializedAt: attemptedAt,
11921
+ error: void 0,
11922
+ attemptedAt,
11923
+ ...statusOverride
11924
+ })
11925
+ );
11926
+ return entry(declaration, "created");
11927
+ } catch (error_) {
11928
+ const error = error_ instanceof Error ? error_ : new Error(String(error_));
11929
+ await recordIndexMaterialization(
11930
+ buildAttempt({
11931
+ declaration,
11932
+ graphId,
11933
+ signature,
11934
+ schemaVersion,
11935
+ materializedAt: void 0,
11936
+ error,
11937
+ ...statusOverride
11938
+ })
11939
+ );
11940
+ return entry(declaration, "failed", error);
11941
+ }
11942
+ } finally {
11943
+ try {
11944
+ await backend.releaseIndexMaterializationClaim({
11945
+ indexName: statusKey,
11946
+ token
11947
+ });
11948
+ } catch (releaseError) {
11949
+ console.warn(
11950
+ `typegraph: failed to release the index materialization claim for "${statusKey}"; it will expire on its lease and be reclaimed automatically.`,
11951
+ releaseError
11952
+ );
11953
+ }
11954
+ }
11955
+ }
11956
+ }
11957
+ async function hasInvalidIndexLeftover(backend, physicalIndexName) {
11958
+ if (backend.dialect !== "postgres") return false;
11959
+ const rows = await backend.execute(
11960
+ asCompiledRowsSql(sql`
11961
+ SELECT NOT i.indisvalid AS invalid
11962
+ FROM pg_class c
11963
+ JOIN pg_index i ON i.indexrelid = c.oid
11964
+ WHERE c.relname = ${physicalIndexName}
11965
+ `)
11966
+ );
11967
+ return rows[0]?.invalid === true;
11968
+ }
11969
+ async function preloadInvalidIndexLeftovers(backend, physicalIndexNames) {
11970
+ if (backend.dialect !== "postgres" || physicalIndexNames.length === 0) {
11971
+ return /* @__PURE__ */ new Set();
11972
+ }
11973
+ const rows = await backend.execute(
11974
+ asCompiledRowsSql(sql`
11975
+ SELECT c.relname AS name
11976
+ FROM pg_class c
11977
+ JOIN pg_index i ON i.indexrelid = c.oid
11978
+ WHERE c.relname IN (${sqlValueList(physicalIndexNames)})
11979
+ AND NOT i.indisvalid
11980
+ `)
11981
+ );
11982
+ return new Set(rows.map((row) => row.name));
11983
+ }
11984
+ async function dropInvalidIndexLeftover(backend, physicalIndexName) {
11985
+ if (backend.executeDdl === void 0) return;
11986
+ if (!await hasInvalidIndexLeftover(backend, physicalIndexName)) return;
11987
+ const quoted = `"${physicalIndexName.replaceAll('"', '""')}"`;
11988
+ await backend.executeDdl(`DROP INDEX CONCURRENTLY IF EXISTS ${quoted};`);
11989
+ }
11213
11990
  function entry(declaration, status, error) {
11214
11991
  return {
11215
11992
  indexName: declaration.name,
@@ -11263,7 +12040,11 @@ function buildAttempt(args) {
11263
12040
  };
11264
12041
  }
11265
12042
  async function computeIndexSignature(dialect, targetTableName, declaration) {
11266
- const hashable = { dialect, targetTableName, declaration };
12043
+ const hashable = {
12044
+ dialect,
12045
+ targetTableName,
12046
+ declaration: serializeIndexDeclaration(declaration)
12047
+ };
11267
12048
  const json = JSON.stringify(hashable, sortedReplacer);
11268
12049
  return sha256Hex(json, 16);
11269
12050
  }
@@ -11607,85 +12388,6 @@ function literal(value) {
11607
12388
  return `'${value.replaceAll("'", "''")}'`;
11608
12389
  }
11609
12390
 
11610
- // src/store/constraints.ts
11611
- async function checkDisjointnessConstraint(ctx, kind, id) {
11612
- const disjointKinds = ctx.registry.getDisjointKinds(kind);
11613
- for (const disjointKind of disjointKinds) {
11614
- const existing = await ctx.backend.getNode(ctx.graphId, disjointKind, id);
11615
- if (existing && !existing.deleted_at) {
11616
- const error = checkDisjointness(id, kind, [disjointKind], ctx.registry);
11617
- if (error) throw error;
11618
- }
11619
- }
11620
- }
11621
- async function checkCardinalityConstraint(ctx, edgeKind, cardinality, fromKind, fromId, toKind, toId, validTo) {
11622
- switch (cardinality) {
11623
- case "many": {
11624
- return;
11625
- }
11626
- case "one": {
11627
- const count2 = await ctx.backend.countEdgesFrom({
11628
- graphId: ctx.graphId,
11629
- edgeKind,
11630
- fromKind,
11631
- fromId
11632
- });
11633
- const error = checkCardinality(
11634
- edgeKind,
11635
- fromKind,
11636
- fromId,
11637
- "one",
11638
- count2,
11639
- false
11640
- );
11641
- if (error) throw error;
11642
- return;
11643
- }
11644
- case "unique": {
11645
- const exists = await ctx.backend.edgeExistsBetween({
11646
- graphId: ctx.graphId,
11647
- edgeKind,
11648
- fromKind,
11649
- fromId,
11650
- toKind,
11651
- toId
11652
- });
11653
- const error = checkUniqueEdge(
11654
- edgeKind,
11655
- fromKind,
11656
- fromId,
11657
- toKind,
11658
- toId,
11659
- exists ? 1 : 0
11660
- );
11661
- if (error) throw error;
11662
- return;
11663
- }
11664
- case "oneActive": {
11665
- if (validTo !== void 0) {
11666
- return;
11667
- }
11668
- const count2 = await ctx.backend.countEdgesFrom({
11669
- graphId: ctx.graphId,
11670
- edgeKind,
11671
- fromKind,
11672
- fromId,
11673
- activeOnly: true
11674
- });
11675
- const error = checkCardinality(
11676
- edgeKind,
11677
- fromKind,
11678
- fromId,
11679
- "oneActive",
11680
- count2,
11681
- count2 > 0
11682
- );
11683
- if (error) throw error;
11684
- return;
11685
- }
11686
- }
11687
- }
11688
-
11689
12391
  // src/store/operations/edge-operations.ts
11690
12392
  function getEdgeRegistration(graph, kind) {
11691
12393
  const registration = graph.edges[kind];
@@ -11740,6 +12442,11 @@ function createEdgeBatchValidationBackend(backend) {
11740
12442
  endpointCache.set(cacheKey, node);
11741
12443
  return node;
11742
12444
  }
12445
+ function seedEndpointRow(graphId, kind, id, row) {
12446
+ const cacheKey = buildEdgeEndpointCacheKey(graphId, kind, id);
12447
+ if (endpointCache.has(cacheKey)) return;
12448
+ endpointCache.set(cacheKey, row);
12449
+ }
11743
12450
  async function countEdgesFromCached(params) {
11744
12451
  const cacheKey = buildCountEdgesFromCacheKey(params);
11745
12452
  let baseCount = countEdgesFromCache.get(cacheKey);
@@ -11811,7 +12518,8 @@ function createEdgeBatchValidationBackend(backend) {
11811
12518
  });
11812
12519
  return {
11813
12520
  backend: validationBackend,
11814
- registerPendingEdgeForCardinality
12521
+ registerPendingEdgeForCardinality,
12522
+ seedEndpointRow
11815
12523
  };
11816
12524
  }
11817
12525
  async function validateAndPrepareEdgeCreate(ctx, input, id, backend) {
@@ -11911,29 +12619,63 @@ async function executeEdgeCreate(ctx, input, backend) {
11911
12619
  }
11912
12620
  return result;
11913
12621
  }
12622
+ async function primeEdgeBatchValidationCache(ctx, inputs, backend, seedEndpointRow) {
12623
+ if (backend.getNodes === void 0) return;
12624
+ const idsByKind = /* @__PURE__ */ new Map();
12625
+ const addEndpoint = (kind, id) => {
12626
+ const ids = idsByKind.get(kind) ?? /* @__PURE__ */ new Set();
12627
+ ids.add(id);
12628
+ idsByKind.set(kind, ids);
12629
+ };
12630
+ for (const input of inputs) {
12631
+ addEndpoint(input.fromKind, input.fromId);
12632
+ addEndpoint(input.toKind, input.toId);
12633
+ }
12634
+ for (const [kind, ids] of idsByKind) {
12635
+ const orderedIds = [...ids];
12636
+ const rows = await backend.getNodes(ctx.graphId, kind, orderedIds);
12637
+ const rowsById = new Map(rows.map((row) => [row.id, row]));
12638
+ for (const id of orderedIds) {
12639
+ seedEndpointRow(ctx.graphId, kind, id, rowsById.get(id));
12640
+ }
12641
+ }
12642
+ }
12643
+ async function prepareEdgeBatchCreates(ctx, inputs, backend) {
12644
+ const {
12645
+ backend: validationBackend,
12646
+ registerPendingEdgeForCardinality,
12647
+ seedEndpointRow
12648
+ } = createEdgeBatchValidationBackend(backend);
12649
+ await primeEdgeBatchValidationCache(ctx, inputs, backend, seedEndpointRow);
12650
+ const preparedCreates = [];
12651
+ for (const input of inputs) {
12652
+ const id = input.id ?? generateId();
12653
+ const prepared = await validateAndPrepareEdgeCreate(
12654
+ ctx,
12655
+ input,
12656
+ id,
12657
+ validationBackend
12658
+ );
12659
+ preparedCreates.push(prepared);
12660
+ registerPendingEdgeForCardinality(
12661
+ prepared.insertParams,
12662
+ prepared.cardinality
12663
+ );
12664
+ }
12665
+ const batchInsertParams = preparedCreates.map(
12666
+ (prepared) => prepared.insertParams
12667
+ );
12668
+ return { preparedCreates, batchInsertParams };
12669
+ }
11914
12670
  async function executeEdgeCreateNoReturnBatch(ctx, inputs, backend) {
11915
12671
  if (inputs.length === 0) {
11916
12672
  return;
11917
12673
  }
11918
12674
  await runInWriteTransaction(ctx, backend, async (target) => {
11919
- const { backend: validationBackend, registerPendingEdgeForCardinality } = createEdgeBatchValidationBackend(target);
11920
- const preparedCreates = [];
11921
- for (const input of inputs) {
11922
- const id = input.id ?? generateId();
11923
- const prepared = await validateAndPrepareEdgeCreate(
11924
- ctx,
11925
- input,
11926
- id,
11927
- validationBackend
11928
- );
11929
- preparedCreates.push(prepared);
11930
- registerPendingEdgeForCardinality(
11931
- prepared.insertParams,
11932
- prepared.cardinality
11933
- );
11934
- }
11935
- const batchInsertParams = preparedCreates.map(
11936
- (prepared) => prepared.insertParams
12675
+ const { batchInsertParams } = await prepareEdgeBatchCreates(
12676
+ ctx,
12677
+ inputs,
12678
+ target
11937
12679
  );
11938
12680
  await runInsertBatch(edgeInsertDispatch(target), batchInsertParams);
11939
12681
  });
@@ -11943,24 +12685,10 @@ async function executeEdgeCreateBatch(ctx, inputs, backend) {
11943
12685
  return [];
11944
12686
  }
11945
12687
  return runInWriteTransaction(ctx, backend, async (target) => {
11946
- const { backend: validationBackend, registerPendingEdgeForCardinality } = createEdgeBatchValidationBackend(target);
11947
- const preparedCreates = [];
11948
- for (const input of inputs) {
11949
- const id = input.id ?? generateId();
11950
- const prepared = await validateAndPrepareEdgeCreate(
11951
- ctx,
11952
- input,
11953
- id,
11954
- validationBackend
11955
- );
11956
- preparedCreates.push(prepared);
11957
- registerPendingEdgeForCardinality(
11958
- prepared.insertParams,
11959
- prepared.cardinality
11960
- );
11961
- }
11962
- const batchInsertParams = preparedCreates.map(
11963
- (prepared) => prepared.insertParams
12688
+ const { batchInsertParams } = await prepareEdgeBatchCreates(
12689
+ ctx,
12690
+ inputs,
12691
+ target
11964
12692
  );
11965
12693
  const rows = await runInsertBatchReturning(
11966
12694
  edgeInsertDispatch(target),
@@ -11977,7 +12705,7 @@ async function performEdgeUpdate(ctx, input, target, options) {
11977
12705
  }
11978
12706
  const registration = getEdgeRegistration(ctx.graph, existing.kind);
11979
12707
  const edgeKind = registration.type;
11980
- const existingProps = JSON.parse(existing.props);
12708
+ const existingProps = rowPropsToObject(existing.props);
11981
12709
  const mergedProps = { ...existingProps, ...input.props };
11982
12710
  const validatedProps = validateEdgeProps(edgeKind.schema, mergedProps, {
11983
12711
  kind: existing.kind,
@@ -12021,10 +12749,6 @@ async function executeEdgeDelete(ctx, id, backend) {
12021
12749
  if (!gate || gate.deleted_at) return;
12022
12750
  const opContext = ctx.createOperationContext("delete", "edge", gate.kind, id);
12023
12751
  return runHookedWriteOperation(ctx, opContext, backend, async (target) => {
12024
- const existing = await target.getEdge(ctx.graphId, id);
12025
- if (!existing || existing.deleted_at) {
12026
- return;
12027
- }
12028
12752
  await target.deleteEdge({
12029
12753
  graphId: ctx.graphId,
12030
12754
  id
@@ -12036,10 +12760,6 @@ async function executeEdgeHardDelete(ctx, id, backend) {
12036
12760
  if (!gate) return;
12037
12761
  const opContext = ctx.createOperationContext("delete", "edge", gate.kind, id);
12038
12762
  return runHookedWriteOperation(ctx, opContext, backend, async (target) => {
12039
- const existing = await target.getEdge(ctx.graphId, id);
12040
- if (!existing) {
12041
- return;
12042
- }
12043
12763
  await target.hardDeleteEdge({
12044
12764
  graphId: ctx.graphId,
12045
12765
  id
@@ -12109,7 +12829,7 @@ function findMatchingEdge(rows, matchOn, inputProps) {
12109
12829
  let deletedRow;
12110
12830
  for (const row of rows) {
12111
12831
  if (matchOn.length > 0) {
12112
- const rowProps = JSON.parse(row.props);
12832
+ const rowProps = rowPropsToObject(row.props);
12113
12833
  const matches = matchOn.every(
12114
12834
  (field2) => stableStringify(rowProps[field2]) === stableStringify(inputProps[field2])
12115
12835
  );
@@ -12446,1033 +13166,64 @@ async function executeEdgeBulkGetOrCreateByEndpoints(ctx, kind, items, backend,
12446
13166
  return results;
12447
13167
  });
12448
13168
  }
12449
- function getNodeRegistration(graph, kind) {
12450
- const registration = graph.nodes[kind];
12451
- if (registration === void 0) throw new KindNotFoundError(kind, "node");
12452
- return registration;
13169
+ var RECORDED_POINT_READ_FIXED_BIND_PARAMS = 8;
13170
+ function recordedPointReadIdChunk(backend) {
13171
+ const budget = recordedBindParamBudget(backend);
13172
+ return Math.max(1, budget - RECORDED_POINT_READ_FIXED_BIND_PARAMS);
13173
+ }
13174
+ function recordedRelationErrorDetails(backend, surface) {
13175
+ return { dialect: backend.dialect, surface };
13176
+ }
13177
+ function withRelationsPrecondition(backend, promise, surface) {
13178
+ return withRecordedRelationsPrecondition(
13179
+ promise,
13180
+ recordedRelationErrorDetails(backend, surface)
13181
+ );
12453
13182
  }
12454
- var CACHE_KEY_SEPARATOR = "\0";
12455
- function buildNodeCacheKey(graphId, kind, id) {
12456
- return `${graphId}${CACHE_KEY_SEPARATOR}${kind}${CACHE_KEY_SEPARATOR}${id}`;
13183
+ function createRecordedReadBackend(backend, surface) {
13184
+ return createBackendOverlay(backend, {
13185
+ execute: (query) => withRelationsPrecondition(backend, backend.execute(query), surface),
13186
+ ...backend.executeRaw === void 0 ? {} : {
13187
+ executeRaw: (sqlText, params) => withRelationsPrecondition(
13188
+ backend,
13189
+ backend.executeRaw(sqlText, params),
13190
+ surface
13191
+ )
13192
+ }
13193
+ });
12457
13194
  }
12458
- function buildUniqueCacheKey(graphId, nodeKind, constraintName, key) {
12459
- return `${graphId}${CACHE_KEY_SEPARATOR}${nodeKind}${CACHE_KEY_SEPARATOR}${constraintName}${CACHE_KEY_SEPARATOR}${key}`;
13195
+ function recordedTemporalFilter(backend, coordinate, tableAlias) {
13196
+ const recordedAsOf = coordinate.recorded?.asOf;
13197
+ if (recordedAsOf === void 0) {
13198
+ throw new ConfigurationError(
13199
+ "Recorded point reads require a recorded-time coordinate.",
13200
+ { code: "RECORDED_POINT_READ_MISSING_COORDINATE" }
13201
+ );
13202
+ }
13203
+ return compileTemporalFilter({
13204
+ mode: coordinate.valid.mode,
13205
+ asOf: coordinate.valid.asOf,
13206
+ recordedAsOf,
13207
+ tableAlias,
13208
+ currentTimestamp: currentReadInstant()
13209
+ });
12460
13210
  }
12461
- function createNodeAlreadyExistsError(kind, id) {
12462
- return new ValidationError(
12463
- `Node already exists: ${kind}/${id}`,
13211
+ function recordedRelationInvariantError(params) {
13212
+ return new ConfigurationError(
13213
+ "Recorded relation invariant violation: more than one recorded row matched the requested point read.",
12464
13214
  {
12465
- entityType: "node",
12466
- kind,
12467
- operation: "create",
12468
- id,
12469
- issues: [{ path: "id", message: "A node with this ID already exists" }]
13215
+ code: "RECORDED_RELATION_INVARIANT_VIOLATION",
13216
+ graphId: params.graphId,
13217
+ entity: params.entity,
13218
+ kind: params.kind,
13219
+ id: params.id,
13220
+ validMode: params.coordinate.valid.mode,
13221
+ validAsOf: params.coordinate.valid.asOf,
13222
+ recordedAsOf: params.coordinate.recorded?.asOf
12470
13223
  },
12471
- { suggestion: `Use a different ID or update the existing node.` }
12472
- );
12473
- }
12474
- function buildInsertNodeParams(graphId, kind, id, props, validFrom, validTo) {
12475
- const insertParams = {
12476
- graphId,
12477
- kind,
12478
- id,
12479
- props
12480
- };
12481
- if (validFrom !== void 0) insertParams.validFrom = validFrom;
12482
- if (validTo !== void 0) insertParams.validTo = validTo;
12483
- return insertParams;
12484
- }
12485
- function createPendingUniqueRow(graphId, nodeKind, constraintName, key, nodeId) {
12486
- return {
12487
- graph_id: graphId,
12488
- node_kind: nodeKind,
12489
- constraint_name: constraintName,
12490
- key,
12491
- node_id: nodeId,
12492
- concrete_kind: nodeKind,
12493
- deleted_at: void 0
12494
- };
12495
- }
12496
- function resolveConstraint(graph, kind, constraintName) {
12497
- const registration = getNodeRegistration(graph, kind);
12498
- const constraints = registration.unique ?? [];
12499
- const constraint = constraints.find(
12500
- (candidate) => candidate.name === constraintName
12501
- );
12502
- if (constraint === void 0) {
12503
- throw new NodeConstraintNotFoundError(constraintName, kind);
12504
- }
12505
- return constraint;
12506
- }
12507
- function createNodeBatchValidationBackend(graphId, registry, backend) {
12508
- const nodeCache = /* @__PURE__ */ new Map();
12509
- const pendingNodes = /* @__PURE__ */ new Map();
12510
- const uniqueCache = /* @__PURE__ */ new Map();
12511
- const pendingUniqueOwners = /* @__PURE__ */ new Map();
12512
- async function getNodeCached(lookupGraphId, kind, id) {
12513
- const cacheKey = buildNodeCacheKey(lookupGraphId, kind, id);
12514
- const pendingNode = pendingNodes.get(cacheKey);
12515
- if (pendingNode !== void 0) return pendingNode;
12516
- if (nodeCache.has(cacheKey)) return nodeCache.get(cacheKey);
12517
- const existing = await backend.getNode(lookupGraphId, kind, id);
12518
- nodeCache.set(cacheKey, existing);
12519
- return existing;
12520
- }
12521
- async function checkUniqueCached(params) {
12522
- const cacheKey = buildUniqueCacheKey(
12523
- params.graphId,
12524
- params.nodeKind,
12525
- params.constraintName,
12526
- params.key
12527
- );
12528
- const pendingOwner = pendingUniqueOwners.get(cacheKey);
12529
- if (pendingOwner !== void 0) {
12530
- return createPendingUniqueRow(
12531
- params.graphId,
12532
- params.nodeKind,
12533
- params.constraintName,
12534
- params.key,
12535
- pendingOwner
12536
- );
12537
- }
12538
- if (uniqueCache.has(cacheKey)) return uniqueCache.get(cacheKey);
12539
- const existing = await backend.checkUnique(params);
12540
- uniqueCache.set(cacheKey, existing);
12541
- return existing;
12542
- }
12543
- function registerPendingNode(params) {
12544
- const cacheKey = buildNodeCacheKey(params.graphId, params.kind, params.id);
12545
- pendingNodes.set(cacheKey, {
12546
- graph_id: params.graphId,
12547
- kind: params.kind,
12548
- id: params.id,
12549
- props: JSON.stringify(params.props),
12550
- version: 1,
12551
- valid_from: params.validFrom,
12552
- valid_to: params.validTo,
12553
- created_at: "",
12554
- updated_at: "",
12555
- deleted_at: void 0
12556
- });
12557
- }
12558
- function registerPendingUniqueEntries(kind, id, props, constraints) {
12559
- for (const constraint of constraints) {
12560
- if (!checkWherePredicate(constraint, props)) continue;
12561
- const key = computeUniqueKey(
12562
- props,
12563
- constraint.fields,
12564
- constraint.collation
12565
- );
12566
- const concreteEntryKey = buildUniqueCacheKey(
12567
- graphId,
12568
- kind,
12569
- constraint.name,
12570
- key
12571
- );
12572
- pendingUniqueOwners.set(concreteEntryKey, id);
12573
- if (constraint.scope !== "kind") {
12574
- const kindsToCheck = getKindsForUniquenessCheck(
12575
- kind,
12576
- constraint.scope,
12577
- registry
12578
- );
12579
- for (const kindToCheck of kindsToCheck) {
12580
- const inheritedEntryKey = buildUniqueCacheKey(
12581
- graphId,
12582
- kindToCheck,
12583
- constraint.name,
12584
- key
12585
- );
12586
- pendingUniqueOwners.set(inheritedEntryKey, id);
12587
- }
12588
- }
12589
- }
12590
- }
12591
- const validationBackend = createBackendOverlay(backend, {
12592
- getNode: getNodeCached,
12593
- checkUnique: checkUniqueCached
12594
- });
12595
- return {
12596
- backend: validationBackend,
12597
- registerPendingNode,
12598
- registerPendingUniqueEntries
12599
- };
12600
- }
12601
- async function validateAndPrepareNodeCreate(ctx, input, id, backend) {
12602
- const kind = input.kind;
12603
- const registration = getNodeRegistration(ctx.graph, kind);
12604
- const nodeKind = registration.type;
12605
- const validatedProps = validateNodeProps(nodeKind.schema, input.props, {
12606
- kind,
12607
- operation: "create"
12608
- });
12609
- const validFrom = validateOptionalCanonicalIsoDate(
12610
- input.validFrom,
12611
- "validFrom"
12612
- );
12613
- const validTo = validateOptionalCanonicalIsoDate(input.validTo, "validTo");
12614
- const existingNode = await backend.getNode(ctx.graphId, kind, id);
12615
- if (existingNode && !existingNode.deleted_at) {
12616
- throw createNodeAlreadyExistsError(kind, id);
12617
- }
12618
- const constraintContext = {
12619
- graphId: ctx.graphId,
12620
- registry: ctx.registry,
12621
- backend
12622
- };
12623
- await checkDisjointnessConstraint(constraintContext, kind, id);
12624
- const uniqueConstraints = registration.unique ?? [];
12625
- await checkUniquenessConstraints(
12626
- createUniquenessContext(ctx.graphId, ctx.registry, backend),
12627
- kind,
12628
- id,
12629
- validatedProps,
12630
- uniqueConstraints
12631
- );
12632
- return {
12633
- kind,
12634
- id,
12635
- nodeKind,
12636
- validatedProps,
12637
- uniqueConstraints,
12638
- insertParams: buildInsertNodeParams(
12639
- ctx.graphId,
12640
- kind,
12641
- id,
12642
- validatedProps,
12643
- validFrom,
12644
- validTo
12645
- )
12646
- };
12647
- }
12648
- async function finalizeNodeCreate(ctx, prepared, backend, lock) {
12649
- await applyNodeInsertSideEffects(
12650
- createNodeWriteContext(ctx.graphId, ctx.registry, lock),
12651
- {
12652
- kind: prepared.kind,
12653
- id: prepared.id,
12654
- schema: prepared.nodeKind.schema,
12655
- props: prepared.validatedProps,
12656
- uniqueConstraints: prepared.uniqueConstraints
12657
- },
12658
- backend
12659
- );
12660
- }
12661
- async function performNodeUpdate(ctx, input, backend, lock, options) {
12662
- const { kind, id } = input;
12663
- const registration = getNodeRegistration(ctx.graph, kind);
12664
- const existing = await backend.getNode(ctx.graphId, kind, id);
12665
- if (!existing) throw new NodeNotFoundError(kind, id);
12666
- const existingProps = JSON.parse(existing.props);
12667
- const mergedProps = { ...existingProps, ...input.props };
12668
- const nodeKind = registration.type;
12669
- const validatedProps = validateNodeProps(nodeKind.schema, mergedProps, {
12670
- kind,
12671
- operation: "update",
12672
- id
12673
- });
12674
- const validTo = validateOptionalCanonicalIsoDate(input.validTo, "validTo");
12675
- const writeContext = createNodeWriteContext(ctx.graphId, ctx.registry, lock);
12676
- const shared = {
12677
- schema: nodeKind.schema,
12678
- validatedProps,
12679
- uniqueConstraints: registration.unique ?? [],
12680
- ...validTo !== void 0 && { validTo }
12681
- };
12682
- if (options?.clearDeleted) {
12683
- const row2 = await applyNodeUpdate(
12684
- writeContext,
12685
- { ...shared, existing, clearDeleted: true },
12686
- backend
12687
- );
12688
- return rowToNode(row2);
12689
- }
12690
- if (!isLiveNodeRow(existing)) throw new NodeNotFoundError(kind, id);
12691
- const row = await applyNodeUpdate(
12692
- writeContext,
12693
- { ...shared, existing },
12694
- backend
12695
- );
12696
- return rowToNode(row);
12697
- }
12698
- async function prepareBatchCreates(ctx, inputs, backend) {
12699
- const {
12700
- backend: validationBackend,
12701
- registerPendingNode,
12702
- registerPendingUniqueEntries
12703
- } = createNodeBatchValidationBackend(ctx.graphId, ctx.registry, backend);
12704
- const preparedCreates = [];
12705
- for (const input of inputs) {
12706
- const id = input.id ?? generateId();
12707
- const prepared = await validateAndPrepareNodeCreate(
12708
- ctx,
12709
- input,
12710
- id,
12711
- validationBackend
12712
- );
12713
- preparedCreates.push(prepared);
12714
- registerPendingNode(prepared.insertParams);
12715
- registerPendingUniqueEntries(
12716
- prepared.kind,
12717
- prepared.id,
12718
- prepared.validatedProps,
12719
- prepared.uniqueConstraints
12720
- );
12721
- }
12722
- const batchInsertParams = preparedCreates.map(
12723
- (prepared) => prepared.insertParams
12724
- );
12725
- return { preparedCreates, batchInsertParams };
12726
- }
12727
- async function findUniqueRowAcrossKinds(backend, graphId, constraintName, key, kindsToCheck, includeDeleted) {
12728
- for (const kindToCheck of kindsToCheck) {
12729
- const row = await backend.checkUnique({
12730
- graphId,
12731
- nodeKind: kindToCheck,
12732
- constraintName,
12733
- key,
12734
- includeDeleted
12735
- });
12736
- if (row !== void 0) return row;
12737
- }
12738
- return void 0;
12739
- }
12740
- async function batchCheckUniqueAcrossKinds(backend, graphId, constraintName, uniqueKeys, kindsToCheck, includeDeleted) {
12741
- const existingByKey = /* @__PURE__ */ new Map();
12742
- for (const kindToCheck of kindsToCheck) {
12743
- if (backend.checkUniqueBatch === void 0) {
12744
- for (const key of uniqueKeys) {
12745
- if (existingByKey.has(key)) continue;
12746
- const row = await backend.checkUnique({
12747
- graphId,
12748
- nodeKind: kindToCheck,
12749
- constraintName,
12750
- key,
12751
- includeDeleted
12752
- });
12753
- if (row !== void 0) {
12754
- existingByKey.set(row.key, row);
12755
- }
12756
- }
12757
- } else {
12758
- const rows = await backend.checkUniqueBatch({
12759
- graphId,
12760
- nodeKind: kindToCheck,
12761
- constraintName,
12762
- keys: uniqueKeys,
12763
- includeDeleted
12764
- });
12765
- for (const row of rows) {
12766
- if (!existingByKey.has(row.key)) {
12767
- existingByKey.set(row.key, row);
12768
- }
12769
- }
12770
- }
12771
- }
12772
- return existingByKey;
12773
- }
12774
- async function executeNodeCreateInternal(ctx, input, backend, options) {
12775
- const kind = input.kind;
12776
- const id = input.id ?? generateId();
12777
- const opContext = ctx.createOperationContext("create", "node", kind, id);
12778
- return runHookedWriteOperation(
12779
- ctx,
12780
- opContext,
12781
- backend,
12782
- async (target, lock) => {
12783
- const prepared = await validateAndPrepareNodeCreate(
12784
- ctx,
12785
- input,
12786
- id,
12787
- target
12788
- );
12789
- let row;
12790
- {
12791
- row = await target.insertNode(prepared.insertParams);
12792
- }
12793
- await finalizeNodeCreate(ctx, prepared, target, lock);
12794
- if (row === void 0) return;
12795
- return rowToNode(row);
12796
- }
12797
- );
12798
- }
12799
- async function executeNodeCreate(ctx, input, backend) {
12800
- const result = await executeNodeCreateInternal(ctx, input, backend);
12801
- if (!result) {
12802
- throw new DatabaseOperationError(
12803
- "Node create failed: expected created node row",
12804
- { operation: "insert", entity: "node" }
12805
- );
12806
- }
12807
- return result;
12808
- }
12809
- async function executeNodeCreateNoReturnBatch(ctx, inputs, backend) {
12810
- if (inputs.length === 0) return;
12811
- await runInWriteTransaction(ctx, backend, async (target, lock) => {
12812
- const { preparedCreates, batchInsertParams } = await prepareBatchCreates(
12813
- ctx,
12814
- inputs,
12815
- target
12816
- );
12817
- await runInsertBatch(nodeInsertDispatch(target), batchInsertParams);
12818
- for (const prepared of preparedCreates) {
12819
- await finalizeNodeCreate(ctx, prepared, target, lock);
12820
- }
12821
- });
12822
- }
12823
- async function executeNodeCreateBatch(ctx, inputs, backend) {
12824
- if (inputs.length === 0) return [];
12825
- return runInWriteTransaction(ctx, backend, async (target, lock) => {
12826
- const { preparedCreates, batchInsertParams } = await prepareBatchCreates(
12827
- ctx,
12828
- inputs,
12829
- target
12830
- );
12831
- const rows = await runInsertBatchReturning(
12832
- nodeInsertDispatch(target),
12833
- batchInsertParams
12834
- );
12835
- for (const prepared of preparedCreates) {
12836
- await finalizeNodeCreate(ctx, prepared, target, lock);
12837
- }
12838
- return rows.map((row) => rowToNode(row));
12839
- });
12840
- }
12841
- async function executeNodeUpdate(ctx, input, backend, options) {
12842
- const opContext = ctx.createOperationContext(
12843
- "update",
12844
- "node",
12845
- input.kind,
12846
- input.id
12847
- );
12848
- return runHookedWriteOperation(
12849
- ctx,
12850
- opContext,
12851
- backend,
12852
- (target, lock) => performNodeUpdate(ctx, input, target, lock, options)
12853
- );
12854
- }
12855
- async function executeNodeUpsertUpdate(ctx, input, backend, options) {
12856
- return runInWriteTransaction(
12857
- ctx,
12858
- backend,
12859
- (target, lock) => performNodeUpdate(ctx, input, target, lock, options)
12860
- );
12861
- }
12862
- async function executeNodeDelete(ctx, kind, id, backend) {
12863
- const gate = await backend.getNode(ctx.graphId, kind, id);
12864
- if (!gate || gate.deleted_at) return;
12865
- const opContext = ctx.createOperationContext("delete", "node", kind, id);
12866
- return runHookedWriteOperation(
12867
- ctx,
12868
- opContext,
12869
- backend,
12870
- async (target, lock) => {
12871
- const registration = getNodeRegistration(ctx.graph, kind);
12872
- const preflight = await target.getNode(ctx.graphId, kind, id);
12873
- if (!preflight || !isLiveNodeRow(preflight)) return;
12874
- await applyNodeSoftDelete(
12875
- createNodeWriteContext(ctx.graphId, ctx.registry, lock),
12876
- {
12877
- existing: preflight,
12878
- schema: registration.type.schema,
12879
- uniqueConstraints: registration.unique ?? [],
12880
- onDelete: registration.onDelete
12881
- },
12882
- target
12883
- );
12884
- }
12885
- );
12886
- }
12887
- async function executeNodeHardDelete(ctx, kind, id, backend) {
12888
- const gate = await backend.getNode(ctx.graphId, kind, id);
12889
- if (!gate) return;
12890
- const opContext = ctx.createOperationContext("delete", "node", kind, id);
12891
- return runHookedWriteOperation(
12892
- ctx,
12893
- opContext,
12894
- backend,
12895
- async (target, lock) => {
12896
- const registration = getNodeRegistration(ctx.graph, kind);
12897
- const preflight = await target.getNode(ctx.graphId, kind, id);
12898
- if (!preflight) return;
12899
- await applyNodeHardDelete(
12900
- createNodeWriteContext(ctx.graphId, ctx.registry, lock),
12901
- {
12902
- kind,
12903
- id,
12904
- schema: registration.type.schema,
12905
- onDelete: registration.onDelete
12906
- },
12907
- target
12908
- );
12909
- }
12910
- );
12911
- }
12912
- async function executeNodeGetOrCreateByConstraint(ctx, kind, constraintName, props, backend, options) {
12913
- const ifExists = options?.ifExists ?? "return";
12914
- const registration = getNodeRegistration(ctx.graph, kind);
12915
- const nodeKind = registration.type;
12916
- const validatedProps = validateNodeProps(nodeKind.schema, props, {
12917
- kind,
12918
- operation: "create"
12919
- });
12920
- const constraint = resolveConstraint(ctx.graph, kind, constraintName);
12921
- if (!checkWherePredicate(constraint, validatedProps)) {
12922
- const node = await executeNodeCreate(
12923
- ctx,
12924
- { kind, props: validatedProps },
12925
- backend
12926
- );
12927
- return { node, action: "created" };
12928
- }
12929
- const key = computeUniqueKey(
12930
- validatedProps,
12931
- constraint.fields,
12932
- constraint.collation
12933
- );
12934
- const kindsToCheck = getKindsForUniquenessCheck(
12935
- kind,
12936
- constraint.scope,
12937
- ctx.registry
12938
- );
12939
- async function attempt() {
12940
- const existingUniqueRow = await findUniqueRowAcrossKinds(
12941
- backend,
12942
- ctx.graphId,
12943
- constraint.name,
12944
- key,
12945
- kindsToCheck,
12946
- true
12947
- );
12948
- if (existingUniqueRow === void 0) {
12949
- const node = await executeNodeCreate(
12950
- ctx,
12951
- { kind, props: validatedProps },
12952
- backend
12953
- );
12954
- return { node, action: "created" };
12955
- }
12956
- const existingRow = await backend.getNode(
12957
- ctx.graphId,
12958
- existingUniqueRow.concrete_kind,
12959
- existingUniqueRow.node_id
12960
- );
12961
- if (existingRow === void 0) {
12962
- const node = await executeNodeCreate(
12963
- ctx,
12964
- { kind, props: validatedProps },
12965
- backend
12966
- );
12967
- return { node, action: "created" };
12968
- }
12969
- const isSoftDeleted = existingRow.deleted_at !== void 0;
12970
- if (isSoftDeleted || ifExists === "update") {
12971
- const concreteKind = existingUniqueRow.concrete_kind;
12972
- const node = await executeNodeUpsertUpdate(
12973
- ctx,
12974
- {
12975
- kind: concreteKind,
12976
- id: existingRow.id,
12977
- props: validatedProps
12978
- },
12979
- backend,
12980
- { clearDeleted: isSoftDeleted }
12981
- );
12982
- return { node, action: isSoftDeleted ? "resurrected" : "updated" };
12983
- }
12984
- return { node: rowToNode(existingRow), action: "found" };
12985
- }
12986
- try {
12987
- return await attempt();
12988
- } catch (error) {
12989
- if (!(error instanceof UniquenessError)) throw error;
12990
- return attempt();
12991
- }
12992
- }
12993
- async function executeNodeFindByConstraint(ctx, kind, constraintName, props, backend) {
12994
- const registration = getNodeRegistration(ctx.graph, kind);
12995
- const nodeKind = registration.type;
12996
- const validatedProps = validateNodeProps(nodeKind.schema, props, {
12997
- kind,
12998
- operation: "create"
12999
- });
13000
- const constraint = resolveConstraint(ctx.graph, kind, constraintName);
13001
- if (!checkWherePredicate(constraint, validatedProps)) return void 0;
13002
- const key = computeUniqueKey(
13003
- validatedProps,
13004
- constraint.fields,
13005
- constraint.collation
13006
- );
13007
- const kindsToCheck = getKindsForUniquenessCheck(
13008
- kind,
13009
- constraint.scope,
13010
- ctx.registry
13011
- );
13012
- const existingUniqueRow = await findUniqueRowAcrossKinds(
13013
- backend,
13014
- ctx.graphId,
13015
- constraint.name,
13016
- key,
13017
- kindsToCheck,
13018
- false
13019
- );
13020
- if (existingUniqueRow === void 0) return void 0;
13021
- const existingRow = await backend.getNode(
13022
- ctx.graphId,
13023
- existingUniqueRow.concrete_kind,
13024
- existingUniqueRow.node_id
13025
- );
13026
- if (existingRow === void 0 || existingRow.deleted_at !== void 0)
13027
- return void 0;
13028
- return rowToNode(existingRow);
13029
- }
13030
- function validateAndComputeKeys(nodeKind, kind, constraint, items) {
13031
- const validated = [];
13032
- for (const item of items) {
13033
- const validatedProps = validateNodeProps(nodeKind.schema, item.props, {
13034
- kind,
13035
- operation: "create"
13036
- });
13037
- const applies = checkWherePredicate(constraint, validatedProps);
13038
- const key = applies ? computeUniqueKey(
13039
- validatedProps,
13040
- constraint.fields,
13041
- constraint.collation
13042
- ) : void 0;
13043
- validated.push({ validatedProps, key });
13044
- }
13045
- return validated;
13046
- }
13047
- function collectUniqueKeys(validated) {
13048
- return [
13049
- ...new Set(
13050
- validated.map((entry2) => entry2.key).filter((key) => key !== void 0)
13051
- )
13052
- ];
13053
- }
13054
- async function executeNodeBulkFindByConstraint(ctx, kind, constraintName, items, backend) {
13055
- if (items.length === 0) return [];
13056
- const registration = getNodeRegistration(ctx.graph, kind);
13057
- const nodeKind = registration.type;
13058
- const constraint = resolveConstraint(ctx.graph, kind, constraintName);
13059
- const validated = validateAndComputeKeys(nodeKind, kind, constraint, items);
13060
- const uniqueKeys = collectUniqueKeys(validated);
13061
- const kindsToCheck = getKindsForUniquenessCheck(
13062
- kind,
13063
- constraint.scope,
13064
- ctx.registry
13065
- );
13066
- const existingByKey = uniqueKeys.length > 0 ? await batchCheckUniqueAcrossKinds(
13067
- backend,
13068
- ctx.graphId,
13069
- constraint.name,
13070
- uniqueKeys,
13071
- kindsToCheck,
13072
- false
13073
- ) : /* @__PURE__ */ new Map();
13074
- const results = Array.from({ length: items.length });
13075
- const seenKeys = /* @__PURE__ */ new Map();
13076
- for (const [index, { key }] of validated.entries()) {
13077
- if (key === void 0) {
13078
- results[index] = void 0;
13079
- continue;
13080
- }
13081
- const previousIndex = seenKeys.get(key);
13082
- if (previousIndex !== void 0) {
13083
- results[index] = results[previousIndex];
13084
- continue;
13085
- }
13086
- seenKeys.set(key, index);
13087
- const existing = existingByKey.get(key);
13088
- if (existing === void 0) {
13089
- results[index] = void 0;
13090
- continue;
13091
- }
13092
- const existingRow = await backend.getNode(
13093
- ctx.graphId,
13094
- existing.concrete_kind,
13095
- existing.node_id
13096
- );
13097
- if (existingRow === void 0 || existingRow.deleted_at !== void 0) {
13098
- results[index] = void 0;
13099
- continue;
13100
- }
13101
- results[index] = rowToNode(existingRow);
13102
- }
13103
- return results;
13104
- }
13105
- function resolveNodeIndex(graph, kind, indexName) {
13106
- getNodeRegistration(graph, kind);
13107
- const declaration = graph.indexes?.find(
13108
- (candidate) => candidate.entity === "node" && candidate.kind === kind && candidate.name === indexName
13109
- );
13110
- if (declaration?.entity !== "node") {
13111
- throw new NodeIndexNotFoundError(indexName, kind);
13112
- }
13113
- return declaration;
13114
- }
13115
- var INDEX_PROBE_EXPECTED_TYPEOF = {
13116
- string: "string",
13117
- number: "number",
13118
- boolean: "boolean"
13119
- };
13120
- function validateIndexProbeValue(value, valueType, pointer, kind) {
13121
- if (value === void 0 || value === null) return;
13122
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && !(value instanceof Date)) {
13123
- throw indexProbeTypeError(
13124
- pointer,
13125
- kind,
13126
- "a scalar (string, number, boolean, or Date)",
13127
- value
13128
- );
13129
- }
13130
- if (valueType === "date") {
13131
- if (value instanceof Date || typeof value === "string") return;
13132
- throw indexProbeTypeError(
13133
- pointer,
13134
- kind,
13135
- "date (Date or ISO string)",
13136
- value
13137
- );
13138
- }
13139
- const expected = INDEX_PROBE_EXPECTED_TYPEOF[valueType ?? "unknown"];
13140
- if (expected === void 0) return;
13141
- if (typeof value !== expected) {
13142
- throw indexProbeTypeError(pointer, kind, expected, value);
13143
- }
13144
- }
13145
- function indexProbeTypeError(pointer, kind, expected, value) {
13146
- return new ValidationError(
13147
- `Index probe value for "${pointer}" on node kind "${kind}" has an incompatible type`,
13148
- {
13149
- entityType: "node",
13150
- kind,
13151
- issues: [
13152
- {
13153
- path: pointer,
13154
- message: `Expected ${expected}, received ${typeof value}`,
13155
- code: "invalid_type"
13156
- }
13157
- ]
13158
- }
13159
- );
13160
- }
13161
- function coerceIndexProbeBind(value, adapter) {
13162
- return adapter.bindValue(normalizeProbeScalar(value));
13163
- }
13164
- function normalizeProbeScalar(value) {
13165
- return value instanceof Date ? value.toISOString() : value;
13166
- }
13167
- var PROBE_NULL_TAG = 0;
13168
- var PROBE_VALUE_TAG = 1;
13169
- function canonicalIndexProbeKey(probe) {
13170
- return JSON.stringify(
13171
- probe.map(
13172
- (value) => value === void 0 || value === null ? [PROBE_NULL_TAG] : [PROBE_VALUE_TAG, normalizeProbeScalar(value)]
13173
- )
13174
- );
13175
- }
13176
- async function executeNodeBulkFindByIndex(ctx, kind, indexName, items, backend, options) {
13177
- if (items.length === 0) return [];
13178
- const index = resolveNodeIndex(ctx.graph, kind, indexName);
13179
- if (index.fieldValueTypes.includes("date")) {
13180
- throw new ConfigurationError(
13181
- `bulkFindByIndex does not support date-typed key fields on index "${indexName}" (node kind "${kind}")`,
13182
- { indexName, kind },
13183
- {
13184
- suggestion: "Date index keys compare differently across SQLite and PostgreSQL. Use a string-encoded key field, or query date predicates via store.query(...).where(...)."
13185
- }
13186
- );
13187
- }
13188
- const limitPerInput = options?.limitPerInput;
13189
- if (limitPerInput !== void 0 && (!Number.isInteger(limitPerInput) || limitPerInput <= 0)) {
13190
- throw new ValidationError(
13191
- "bulkFindByIndex limitPerInput must be a positive integer",
13192
- {
13193
- entityType: "node",
13194
- kind,
13195
- issues: [
13196
- {
13197
- path: "limitPerInput",
13198
- message: `Expected a positive integer, received ${String(limitPerInput)}`,
13199
- code: "invalid_value"
13200
- }
13201
- ]
13202
- }
13203
- );
13204
- }
13205
- const adapter = getDialect(backend.dialect);
13206
- const probes = items.map(
13207
- (item) => index.fields.map((pointer, position) => {
13208
- const value = resolveJsonPointer(item.props, pointer);
13209
- validateIndexProbeValue(
13210
- value,
13211
- index.fieldValueTypes[position],
13212
- pointer,
13213
- kind
13214
- );
13215
- return value;
13216
- })
13217
- );
13218
- const groupByKey = /* @__PURE__ */ new Map();
13219
- const groupProbes = [];
13220
- const groupToInputs = [];
13221
- for (const [inputIndex, probe] of probes.entries()) {
13222
- const key = canonicalIndexProbeKey(probe);
13223
- const existing = groupByKey.get(key);
13224
- if (existing === void 0) {
13225
- groupByKey.set(key, groupProbes.length);
13226
- groupProbes.push(probe);
13227
- groupToInputs.push([inputIndex]);
13228
- continue;
13229
- }
13230
- groupToInputs[existing]?.push(inputIndex);
13231
- }
13232
- const schema = backend.tableNames ? createSqlSchema(backend.tableNames) : DEFAULT_SQL_SCHEMA;
13233
- const compileContext = {
13234
- dialect: backend.dialect,
13235
- propsColumn: sql.raw(`"props"`),
13236
- systemColumn: (column) => sql.raw(`"${column}"`)
13237
- };
13238
- const fieldKeys = compileNodeIndexFieldKeys(index, compileContext);
13239
- const groupPredicates = groupProbes.map(
13240
- (probe) => sql`(${sql.join(
13241
- fieldKeys.map((fieldKey, position) => {
13242
- const value = probe[position];
13243
- if (value === void 0 || value === null) {
13244
- return sql`${fieldKey} IS NULL`;
13245
- }
13246
- return adapter.nullSafeEquals(
13247
- fieldKey,
13248
- sql`${coerceIndexProbeBind(value, adapter)}`
13249
- );
13250
- }),
13251
- sql` AND `
13252
- )})`
13253
- );
13254
- const caseBranches = groupPredicates.map(
13255
- (predicate, group) => sql`WHEN ${predicate} THEN ${sql.raw(String(group))}`
13256
- );
13257
- const probeIndexExpr = sql`CASE ${sql.join(caseBranches, sql` `)} ELSE NULL END`;
13258
- const conditions = [
13259
- sql`"graph_id" = ${ctx.graphId}`,
13260
- sql`"kind" = ${kind}`,
13261
- sql`"deleted_at" IS NULL`
13262
- ];
13263
- if (index.where !== void 0) {
13264
- conditions.push(compileIndexWhere(compileContext, index.where));
13265
- }
13266
- conditions.push(sql`(${sql.join(groupPredicates, sql` OR `)})`);
13267
- const whereClause = sql.join(conditions, sql` AND `);
13268
- const probedSelect = sql`SELECT "id", ${probeIndexExpr} AS probe_idx FROM ${schema.nodesTable} WHERE ${whereClause}`;
13269
- const capInSql = limitPerInput !== void 0 && backend.capabilities.windowFunctions;
13270
- const query = capInSql ? sql`SELECT "id", probe_idx FROM (SELECT "id", probe_idx, ROW_NUMBER() OVER (PARTITION BY probe_idx ORDER BY "id") AS probe_rank FROM (${probedSelect}) AS probed) AS ranked WHERE probe_rank <= ${limitPerInput} ORDER BY probe_idx, "id"` : sql`${probedSelect} ORDER BY probe_idx, "id"`;
13271
- const rawMatches = await backend.execute(
13272
- asCompiledRowsSql(query)
13273
- );
13274
- const matches = limitPerInput !== void 0 && !capInSql ? capMatchesPerGroup(rawMatches, limitPerInput) : rawMatches;
13275
- const nodesById = await hydrateNodesById(
13276
- backend,
13277
- ctx.graphId,
13278
- kind,
13279
- matches.map((match) => match.id)
13280
- );
13281
- const results = Array.from({ length: items.length }, () => []);
13282
- for (const match of matches) {
13283
- const node = nodesById.get(match.id);
13284
- if (node === void 0) continue;
13285
- const inputs = groupToInputs[match.probe_idx];
13286
- if (inputs === void 0) continue;
13287
- for (const inputIndex of inputs) {
13288
- results[inputIndex]?.push(node);
13289
- }
13290
- }
13291
- return results;
13292
- }
13293
- function capMatchesPerGroup(matches, limitPerInput) {
13294
- const perGroupCount = /* @__PURE__ */ new Map();
13295
- const capped = [];
13296
- for (const match of matches) {
13297
- const count2 = perGroupCount.get(match.probe_idx) ?? 0;
13298
- if (count2 >= limitPerInput) continue;
13299
- perGroupCount.set(match.probe_idx, count2 + 1);
13300
- capped.push(match);
13301
- }
13302
- return capped;
13303
- }
13304
- async function hydrateNodesById(backend, graphId, kind, ids) {
13305
- const rowsById = await getNodeRowsByIds(backend, graphId, kind, ids);
13306
- const nodesById = /* @__PURE__ */ new Map();
13307
- for (const [id, row] of rowsById) {
13308
- if (row.deleted_at !== void 0) continue;
13309
- nodesById.set(id, rowToNode(row));
13310
- }
13311
- return nodesById;
13312
- }
13313
- async function executeNodeBulkGetOrCreateByConstraint(ctx, kind, constraintName, items, backend, options) {
13314
- if (items.length === 0) return [];
13315
- const ifExists = options?.ifExists ?? "return";
13316
- const registration = getNodeRegistration(ctx.graph, kind);
13317
- const nodeKind = registration.type;
13318
- const constraint = resolveConstraint(ctx.graph, kind, constraintName);
13319
- const validated = validateAndComputeKeys(nodeKind, kind, constraint, items);
13320
- const uniqueKeys = collectUniqueKeys(validated);
13321
- const kindsToCheck = getKindsForUniquenessCheck(
13322
- kind,
13323
- constraint.scope,
13324
- ctx.registry
13325
- );
13326
- const existingByKey = uniqueKeys.length > 0 ? await batchCheckUniqueAcrossKinds(
13327
- backend,
13328
- ctx.graphId,
13329
- constraint.name,
13330
- uniqueKeys,
13331
- kindsToCheck,
13332
- true
13333
- ) : /* @__PURE__ */ new Map();
13334
- const toCreate = [];
13335
- const toFetch = [];
13336
- const duplicateOf = [];
13337
- const seenKeys = /* @__PURE__ */ new Map();
13338
- for (const [index, { validatedProps, key }] of validated.entries()) {
13339
- if (key === void 0) {
13340
- toCreate.push({ index, input: { kind, props: validatedProps } });
13341
- continue;
13342
- }
13343
- const previousIndex = seenKeys.get(key);
13344
- if (previousIndex !== void 0) {
13345
- duplicateOf.push({ index, sourceIndex: previousIndex });
13346
- continue;
13347
- }
13348
- seenKeys.set(key, index);
13349
- const existing = existingByKey.get(key);
13350
- if (existing === void 0) {
13351
- toCreate.push({ index, input: { kind, props: validatedProps } });
13352
- } else {
13353
- toFetch.push({
13354
- index,
13355
- nodeId: existing.node_id,
13356
- concreteKind: existing.concrete_kind,
13357
- validatedProps,
13358
- isSoftDeleted: existing.deleted_at !== void 0
13359
- });
13360
- }
13361
- }
13362
- const results = Array.from({ length: items.length });
13363
- if (toCreate.length > 0) {
13364
- const createInputs = toCreate.map((entry2) => entry2.input);
13365
- const createdNodes = await executeNodeCreateBatch(
13366
- ctx,
13367
- createInputs,
13368
- backend
13369
- );
13370
- for (const [batchIndex, entry2] of toCreate.entries()) {
13371
- results[entry2.index] = {
13372
- node: createdNodes[batchIndex],
13373
- action: "created"
13374
- };
13375
- }
13376
- }
13377
- for (const entry2 of toFetch) {
13378
- const { index, concreteKind, validatedProps, isSoftDeleted, nodeId } = entry2;
13379
- const existingRow = await backend.getNode(
13380
- ctx.graphId,
13381
- concreteKind,
13382
- nodeId
13383
- );
13384
- if (existingRow === void 0) {
13385
- const node = await executeNodeCreate(
13386
- ctx,
13387
- { kind, props: validatedProps },
13388
- backend
13389
- );
13390
- results[index] = { node, action: "created" };
13391
- continue;
13392
- }
13393
- if (isSoftDeleted || ifExists === "update") {
13394
- const node = await executeNodeUpsertUpdate(
13395
- ctx,
13396
- {
13397
- kind: concreteKind,
13398
- id: existingRow.id,
13399
- props: validatedProps
13400
- },
13401
- backend,
13402
- { clearDeleted: isSoftDeleted }
13403
- );
13404
- results[index] = {
13405
- node,
13406
- action: isSoftDeleted ? "resurrected" : "updated"
13407
- };
13408
- } else {
13409
- results[index] = { node: rowToNode(existingRow), action: "found" };
13410
- }
13411
- }
13412
- for (const { index, sourceIndex } of duplicateOf) {
13413
- const sourceResult = results[sourceIndex];
13414
- results[index] = { node: sourceResult.node, action: "found" };
13415
- }
13416
- return results;
13417
- }
13418
- var RECORDED_POINT_READ_FIXED_BIND_PARAMS = 8;
13419
- function recordedPointReadIdChunk(backend) {
13420
- const budget = recordedBindParamBudget(backend);
13421
- return Math.max(1, budget - RECORDED_POINT_READ_FIXED_BIND_PARAMS);
13422
- }
13423
- function recordedRelationErrorDetails(backend, surface) {
13424
- return { dialect: backend.dialect, surface };
13425
- }
13426
- function withRelationsPrecondition(backend, promise, surface) {
13427
- return withRecordedRelationsPrecondition(
13428
- promise,
13429
- recordedRelationErrorDetails(backend, surface)
13430
- );
13431
- }
13432
- function createRecordedReadBackend(backend, surface) {
13433
- return createBackendOverlay(backend, {
13434
- execute: (query) => withRelationsPrecondition(backend, backend.execute(query), surface),
13435
- ...backend.executeRaw === void 0 ? {} : {
13436
- executeRaw: (sqlText, params) => withRelationsPrecondition(
13437
- backend,
13438
- backend.executeRaw(sqlText, params),
13439
- surface
13440
- )
13441
- }
13442
- });
13443
- }
13444
- function recordedTemporalFilter(backend, coordinate, tableAlias) {
13445
- const recordedAsOf = coordinate.recorded?.asOf;
13446
- if (recordedAsOf === void 0) {
13447
- throw new ConfigurationError(
13448
- "Recorded point reads require a recorded-time coordinate.",
13449
- { code: "RECORDED_POINT_READ_MISSING_COORDINATE" }
13450
- );
13451
- }
13452
- return compileTemporalFilter({
13453
- mode: coordinate.valid.mode,
13454
- asOf: coordinate.valid.asOf,
13455
- recordedAsOf,
13456
- tableAlias,
13457
- currentTimestamp: getDialect(backend.dialect).currentTimestamp()
13458
- });
13459
- }
13460
- function recordedRelationInvariantError(params) {
13461
- return new ConfigurationError(
13462
- "Recorded relation invariant violation: more than one recorded row matched the requested point read.",
13463
- {
13464
- code: "RECORDED_RELATION_INVARIANT_VIOLATION",
13465
- graphId: params.graphId,
13466
- entity: params.entity,
13467
- kind: params.kind,
13468
- id: params.id,
13469
- validMode: params.coordinate.valid.mode,
13470
- validAsOf: params.coordinate.valid.asOf,
13471
- recordedAsOf: params.coordinate.recorded?.asOf
13472
- },
13473
- {
13474
- suggestion: "Repair overlapping recorded_from/recorded_to intervals for this entity before using recorded-time reads."
13475
- }
13224
+ {
13225
+ suggestion: "Repair overlapping recorded_from/recorded_to intervals for this entity before using recorded-time reads."
13226
+ }
13476
13227
  );
13477
13228
  }
13478
13229
  function createRecordedReadService(params) {
@@ -13573,6 +13324,155 @@ function createRecordedReadService(params) {
13573
13324
  };
13574
13325
  }
13575
13326
 
13327
+ // src/store/transaction-receipt.ts
13328
+ function inputLength(value) {
13329
+ return Array.isArray(value) ? value.length : 0;
13330
+ }
13331
+ function countSingleWrite() {
13332
+ return 1;
13333
+ }
13334
+ function countBulkInputAt(index) {
13335
+ return (args) => inputLength(args[index]);
13336
+ }
13337
+ var NODE_WRITE_INTENT_COUNTERS = {
13338
+ create: countSingleWrite,
13339
+ createFromRecord: countSingleWrite,
13340
+ update: countSingleWrite,
13341
+ delete: countSingleWrite,
13342
+ hardDelete: countSingleWrite,
13343
+ upsertById: countSingleWrite,
13344
+ upsertByIdFromRecord: countSingleWrite,
13345
+ bulkCreate: countBulkInputAt(0),
13346
+ bulkUpsertById: countBulkInputAt(0),
13347
+ bulkInsert: countBulkInputAt(0),
13348
+ bulkDelete: countBulkInputAt(0),
13349
+ getOrCreateByConstraint: countSingleWrite,
13350
+ bulkGetOrCreateByConstraint: countBulkInputAt(1)
13351
+ };
13352
+ var EDGE_WRITE_INTENT_COUNTERS = {
13353
+ create: countSingleWrite,
13354
+ update: countSingleWrite,
13355
+ delete: countSingleWrite,
13356
+ hardDelete: countSingleWrite,
13357
+ bulkCreate: countBulkInputAt(0),
13358
+ bulkUpsertById: countBulkInputAt(0),
13359
+ bulkInsert: countBulkInputAt(0),
13360
+ bulkDelete: countBulkInputAt(0),
13361
+ getOrCreateByEndpoints: countSingleWrite,
13362
+ bulkGetOrCreateByEndpoints: countBulkInputAt(0)
13363
+ };
13364
+ function createCountBucket() {
13365
+ return /* @__PURE__ */ Object.create(null);
13366
+ }
13367
+ function increment(counts, kind, count2) {
13368
+ if (count2 === 0) return;
13369
+ counts[kind] = (counts[kind] ?? 0) + count2;
13370
+ }
13371
+ function createTransactionReceiptRecorder() {
13372
+ const nodes = createCountBucket();
13373
+ const edges = createCountBucket();
13374
+ let total = 0;
13375
+ return {
13376
+ recordNode(kind, count2) {
13377
+ increment(nodes, kind, count2);
13378
+ total += count2;
13379
+ },
13380
+ recordEdge(kind, count2) {
13381
+ increment(edges, kind, count2);
13382
+ total += count2;
13383
+ },
13384
+ snapshot(recorded) {
13385
+ return Object.freeze({
13386
+ writes: Object.freeze({
13387
+ nodes: Object.freeze(Object.assign(createCountBucket(), nodes)),
13388
+ edges: Object.freeze(Object.assign(createCountBucket(), edges)),
13389
+ total
13390
+ }),
13391
+ ...recorded === void 0 ? {} : { recorded }
13392
+ });
13393
+ }
13394
+ };
13395
+ }
13396
+ function isWrappedMethod(value) {
13397
+ return typeof value === "function";
13398
+ }
13399
+ function isObject(value) {
13400
+ return typeof value === "object" && Boolean(value);
13401
+ }
13402
+ var NODE_WRITE_SURFACE = {
13403
+ methodNames: new Set(NODE_WRITE_NAMES),
13404
+ counters: NODE_WRITE_INTENT_COUNTERS
13405
+ };
13406
+ var EDGE_WRITE_SURFACE = {
13407
+ methodNames: new Set(EDGE_WRITE_NAMES),
13408
+ counters: EDGE_WRITE_INTENT_COUNTERS
13409
+ };
13410
+ function memoizedProxyGet(target, shouldWrap, wrap) {
13411
+ const cache = /* @__PURE__ */ new Map();
13412
+ return new Proxy(target, {
13413
+ get(proxyTarget, property, receiver) {
13414
+ if (typeof property !== "string" || !shouldWrap(property)) {
13415
+ return Reflect.get(proxyTarget, property, receiver);
13416
+ }
13417
+ const cached = cache.get(property);
13418
+ if (cached !== void 0) return cached;
13419
+ const value = Reflect.get(proxyTarget, property, receiver);
13420
+ const wrapped = wrap(value, property);
13421
+ cache.set(property, wrapped);
13422
+ return wrapped;
13423
+ }
13424
+ });
13425
+ }
13426
+ function wrapWriteCollection(collection, kind, surface, record) {
13427
+ return memoizedProxyGet(
13428
+ collection,
13429
+ (property) => surface.methodNames.has(property),
13430
+ (value, property) => {
13431
+ if (!isWrappedMethod(value)) return value;
13432
+ const method = property;
13433
+ const wrapped = async (...args) => {
13434
+ const count2 = surface.counters[method](args);
13435
+ const result = await Reflect.apply(value, collection, args);
13436
+ record(kind, count2);
13437
+ return result;
13438
+ };
13439
+ return wrapped;
13440
+ }
13441
+ );
13442
+ }
13443
+ function wrapCollections(collections, wrapOne) {
13444
+ return memoizedProxyGet(
13445
+ collections,
13446
+ () => true,
13447
+ (collection, kind) => isObject(collection) ? wrapOne(collection, kind) : collection
13448
+ );
13449
+ }
13450
+ function wrapTransactionCollections(nodes, edges, recorder) {
13451
+ return {
13452
+ nodes: wrapCollections(
13453
+ nodes,
13454
+ (collection, kind) => wrapWriteCollection(
13455
+ collection,
13456
+ kind,
13457
+ NODE_WRITE_SURFACE,
13458
+ recorder.recordNode
13459
+ )
13460
+ ),
13461
+ edges: wrapCollections(
13462
+ edges,
13463
+ (collection, kind) => wrapWriteCollection(
13464
+ collection,
13465
+ kind,
13466
+ EDGE_WRITE_SURFACE,
13467
+ recorder.recordEdge
13468
+ )
13469
+ )
13470
+ };
13471
+ }
13472
+
13473
+ // src/store/types.ts
13474
+ var AUTO_REFRESH_STATISTICS_ROW_THRESHOLD = 1e3;
13475
+
13576
13476
  // src/store/store.ts
13577
13477
  var UNKNOWN_SCHEMA_METADATA = Object.freeze({
13578
13478
  schemaVersion: void 0,
@@ -13605,6 +13505,15 @@ function rowMapperConfigFor(backend) {
13605
13505
  function asError(error) {
13606
13506
  return error instanceof Error ? error : new Error(String(error));
13607
13507
  }
13508
+ function transactionOutcome(result, recorder, recordedByGraph, graphId) {
13509
+ const recorded = recordedByGraph?.get(graphId);
13510
+ return {
13511
+ result,
13512
+ receipt: recorder.snapshot(
13513
+ recorded === void 0 ? void 0 : asRecordedInstant(recorded)
13514
+ )
13515
+ };
13516
+ }
13608
13517
  var Store = class _Store {
13609
13518
  #graph;
13610
13519
  /**
@@ -13614,6 +13523,8 @@ var Store = class _Store {
13614
13523
  #baseBackend;
13615
13524
  #backend;
13616
13525
  #captureEnabled;
13526
+ #revisionTrackingEnabled;
13527
+ #revisionOrigin;
13617
13528
  #recordedReadBinding;
13618
13529
  #registry;
13619
13530
  #hooks;
@@ -13634,6 +13545,10 @@ var Store = class _Store {
13634
13545
  this.#graph = graph;
13635
13546
  this.#baseBackend = asRawBackend(backend);
13636
13547
  this.#captureEnabled = options?.history === true;
13548
+ this.#revisionTrackingEnabled = this.#captureEnabled || options?.revisionTracking === true;
13549
+ if (this.#revisionTrackingEnabled) {
13550
+ assertRevisionTrackableBackend(backend);
13551
+ }
13637
13552
  const explicitSchema = options?.schema === void 0 ? void 0 : requireSqlSchema(options.schema, "store schema");
13638
13553
  const resolvedSchema = explicitSchema ?? (backend.tableNames ? createSqlSchema(backend.tableNames) : void 0);
13639
13554
  const readSchema = resolvedSchema ?? createSqlSchema(backend.tableNames);
@@ -13691,6 +13606,22 @@ var Store = class _Store {
13691
13606
  get historyEnabled() {
13692
13607
  return this.#captureEnabled;
13693
13608
  }
13609
+ /**
13610
+ * Whether this Store advances a durable revision anchor for graph writes.
13611
+ *
13612
+ * @internal
13613
+ */
13614
+ get revisionTrackingEnabled() {
13615
+ return this.#revisionTrackingEnabled;
13616
+ }
13617
+ /**
13618
+ * SQL schema that owns the durable revision clock relation.
13619
+ *
13620
+ * @internal
13621
+ */
13622
+ get revisionSchema() {
13623
+ return this.#sqlSchema();
13624
+ }
13694
13625
  /**
13695
13626
  * Whether this store has a recorded read relation bound for reconstruction.
13696
13627
  *
@@ -13780,6 +13711,8 @@ var Store = class _Store {
13780
13711
  if (this.#algorithms === void 0) {
13781
13712
  this.#algorithms = createGraphAlgorithms({
13782
13713
  graphId: this.graphId,
13714
+ graph: this.#graph,
13715
+ registry: this.#registry,
13783
13716
  backend: this.#backend,
13784
13717
  schema: this.#schema,
13785
13718
  recordedReadBinding: this.#recordedReadBinding,
@@ -13789,6 +13722,19 @@ var Store = class _Store {
13789
13722
  return this.#algorithms;
13790
13723
  }
13791
13724
  // === Dynamic Collection Access ===
13725
+ /**
13726
+ * Resolves `kind` against `collections` (either `this.nodes` or a
13727
+ * transaction-scoped — and possibly receipt-wrapped — node collection map),
13728
+ * or `undefined` when the kind is not registered in this graph. Shared by
13729
+ * `getNodeCollection` and both transaction contexts' `getNodeCollection` so
13730
+ * a receipt-wrapped transaction counts writes made through the dynamic
13731
+ * lookup exactly like `this.#graph.nodes` membership is checked everywhere
13732
+ * else.
13733
+ */
13734
+ #resolveDynamicNodeCollection(collections, kind) {
13735
+ if (!Object.hasOwn(this.#graph.nodes, kind)) return void 0;
13736
+ return collections[kind];
13737
+ }
13792
13738
  /**
13793
13739
  * Returns the node collection for the given kind, or undefined if the kind
13794
13740
  * is not registered in this graph.
@@ -13800,8 +13746,7 @@ var Store = class _Store {
13800
13746
  * throws `KindNotFoundError` instead of forcing a null-check.
13801
13747
  */
13802
13748
  getNodeCollection(kind) {
13803
- if (!Object.hasOwn(this.#graph.nodes, kind)) return void 0;
13804
- return this.nodes[kind];
13749
+ return this.#resolveDynamicNodeCollection(this.nodes, kind);
13805
13750
  }
13806
13751
  /**
13807
13752
  * Returns the node collection for the given kind. Throws
@@ -13920,10 +13865,30 @@ var Store = class _Store {
13920
13865
  get #nodeOperations() {
13921
13866
  return this.#buildNodeOperations(this.#createNodeOperationContext());
13922
13867
  }
13868
+ /**
13869
+ * Refreshes planner statistics after an autocommit bulk write that
13870
+ * reached the configured row threshold. A refresh failure must never
13871
+ * fail the (already committed) write — it degrades to a warning.
13872
+ */
13873
+ async #maybeRefreshStatisticsAfterBulk(rowCount) {
13874
+ const configured = this.#options?.autoRefreshStatistics;
13875
+ if (configured === false) return;
13876
+ const threshold = configured ?? AUTO_REFRESH_STATISTICS_ROW_THRESHOLD;
13877
+ if (rowCount < threshold) return;
13878
+ try {
13879
+ await this.refreshStatistics();
13880
+ } catch (error) {
13881
+ console.warn(
13882
+ "typegraph: statistics refresh after bulk write failed; run store.refreshStatistics() to avoid stale planner statistics.",
13883
+ error
13884
+ );
13885
+ }
13886
+ }
13923
13887
  #buildNodeOperations(ctx) {
13924
13888
  return {
13925
13889
  defaultTemporalMode: this.#graph.defaults.temporalMode,
13926
13890
  rowToNode: (row) => rowToNode(row),
13891
+ maybeRefreshStatisticsAfterBulk: (rowCount) => this.#maybeRefreshStatisticsAfterBulk(rowCount),
13927
13892
  executeCreate: (input, backend) => executeNodeCreate(ctx, input, backend),
13928
13893
  executeCreateBatch: (inputs, backend) => executeNodeCreateBatch(ctx, inputs, backend),
13929
13894
  executeCreateNoReturnBatch: (inputs, backend) => executeNodeCreateNoReturnBatch(ctx, inputs, backend),
@@ -13982,6 +13947,7 @@ var Store = class _Store {
13982
13947
  return {
13983
13948
  defaultTemporalMode: this.#graph.defaults.temporalMode,
13984
13949
  rowToEdge: (row) => rowToEdge(row),
13950
+ maybeRefreshStatisticsAfterBulk: (rowCount) => this.#maybeRefreshStatisticsAfterBulk(rowCount),
13985
13951
  executeCreate: (input, backend) => executeEdgeCreate(ctx, input, backend),
13986
13952
  executeCreateBatch: (inputs, backend) => executeEdgeCreateBatch(ctx, inputs, backend),
13987
13953
  executeCreateNoReturnBatch: (inputs, backend) => executeEdgeCreateNoReturnBatch(ctx, inputs, backend),
@@ -14181,6 +14147,44 @@ var Store = class _Store {
14181
14147
  );
14182
14148
  return recordedAt === void 0 ? void 0 : asRecordedInstant(recordedAt);
14183
14149
  }
14150
+ /**
14151
+ * Returns the durable graph revision used by graph branching. It is undefined
14152
+ * until the first successful tracked write, which is itself a stable initial
14153
+ * anchor. Unlike {@link recordedNow}, this is also available on a live Store
14154
+ * created with `{ revisionTracking: true }`.
14155
+ *
14156
+ * @internal
14157
+ */
14158
+ async revisionNow() {
14159
+ if (!this.#revisionTrackingEnabled) return void 0;
14160
+ return readRecordedClock(this.#backend, this.#sqlSchema(), this.graphId);
14161
+ }
14162
+ /**
14163
+ * Returns the durable random namespace for this graph's revision clock. A
14164
+ * tracked base token combines it with {@link revisionNow}, preventing a
14165
+ * branch from one independent store from matching a coincident timestamp in
14166
+ * another store.
14167
+ *
14168
+ * @internal
14169
+ */
14170
+ async revisionOriginNow() {
14171
+ if (!this.#revisionTrackingEnabled) {
14172
+ throw new ConfigurationError(
14173
+ "revisionOriginNow() requires revisionTracking: true or history: true.",
14174
+ { code: "REVISION_ORIGIN_REQUIRES_TRACKING" }
14175
+ );
14176
+ }
14177
+ const pendingOrigin = this.#revisionOrigin ?? ensureRevisionOrigin(this.#baseBackend, this.#sqlSchema(), this.graphId);
14178
+ this.#revisionOrigin = pendingOrigin;
14179
+ try {
14180
+ return await pendingOrigin;
14181
+ } catch (error) {
14182
+ if (this.#revisionOrigin === pendingOrigin) {
14183
+ this.#revisionOrigin = void 0;
14184
+ }
14185
+ throw error;
14186
+ }
14187
+ }
14184
14188
  /**
14185
14189
  * Returns a read-only {@link StoreView} pinned to an arbitrary public
14186
14190
  * temporal mode. Use {@link Store.asOf} for the common valid-time case;
@@ -14251,7 +14255,8 @@ var Store = class _Store {
14251
14255
  this.#search = new StoreSearch({
14252
14256
  graphId: this.graphId,
14253
14257
  backend: this.#backend,
14254
- registry: this.#registry
14258
+ registry: this.#registry,
14259
+ createQuery: () => this.query()
14255
14260
  });
14256
14261
  }
14257
14262
  return this.#search;
@@ -14374,6 +14379,8 @@ var Store = class _Store {
14374
14379
  algorithmsAtCoordinate(coordinate) {
14375
14380
  return createGraphAlgorithms({
14376
14381
  graphId: this.graphId,
14382
+ graph: this.#graph,
14383
+ registry: this.#registry,
14377
14384
  backend: this.#recordedReads.backendForCoordinate(
14378
14385
  coordinate,
14379
14386
  "recorded-graph-algorithm"
@@ -14386,12 +14393,40 @@ var Store = class _Store {
14386
14393
  }
14387
14394
  async transaction(fn, options) {
14388
14395
  const invoke = fn;
14396
+ const { result } = await this.#runTransaction(invoke, options, void 0);
14397
+ return result;
14398
+ }
14399
+ async transactionWithReceipt(fn, options) {
14400
+ const invoke = fn;
14401
+ const receiptRecorder = createTransactionReceiptRecorder();
14402
+ const { result, recordedByGraph } = await this.#runTransaction(
14403
+ invoke,
14404
+ options,
14405
+ receiptRecorder
14406
+ );
14407
+ return transactionOutcome(
14408
+ result,
14409
+ receiptRecorder,
14410
+ recordedByGraph,
14411
+ this.graphId
14412
+ );
14413
+ }
14414
+ async #runTransaction(invoke, backendOptions, receiptRecorder) {
14389
14415
  if (!this.#backend.capabilities.transactions) {
14390
- return invoke({
14391
- nodes: this.nodes,
14392
- edges: this.edges,
14416
+ let nodes = this.nodes;
14417
+ let edges = this.edges;
14418
+ if (receiptRecorder !== void 0) {
14419
+ ({ nodes, edges } = wrapTransactionCollections(
14420
+ nodes,
14421
+ edges,
14422
+ receiptRecorder
14423
+ ));
14424
+ }
14425
+ const result = await invoke({
14426
+ nodes,
14427
+ edges,
14393
14428
  backend: this.#backend,
14394
- getNodeCollection: (kind) => this.getNodeCollection(kind),
14429
+ getNodeCollection: (kind) => this.#resolveDynamicNodeCollection(nodes, kind),
14395
14430
  // No transaction, no deferral: operations apply as they happen, so
14396
14431
  // their hooks fire immediately.
14397
14432
  runNodeOperationHooks: (operation, kind, id, hookedFunction) => this.#withOperationHooks(
@@ -14399,20 +14434,32 @@ var Store = class _Store {
14399
14434
  hookedFunction
14400
14435
  )
14401
14436
  });
14437
+ return { result, recordedByGraph: void 0 };
14402
14438
  }
14403
14439
  const pending = [];
14404
14440
  const runHooks = this.#createBufferedHookRunner(pending);
14441
+ let recordedByGraph;
14442
+ const transactionOptions = receiptRecorder !== void 0 && this.#captureEnabled ? withRecordedFlushObserver(backendOptions, (instants) => {
14443
+ recordedByGraph = instants;
14444
+ }) : backendOptions;
14405
14445
  try {
14406
14446
  const result = await this.#backend.transaction(
14407
- async (txBackend, sql17) => invoke(this.#buildTransactionContext(txBackend, sql17, runHooks)),
14408
- options
14447
+ async (txBackend, sql17) => invoke(
14448
+ this.#buildTransactionContext(
14449
+ txBackend,
14450
+ sql17,
14451
+ runHooks,
14452
+ receiptRecorder
14453
+ )
14454
+ ),
14455
+ transactionOptions
14409
14456
  );
14410
14457
  for (const outcome of pending) {
14411
14458
  this.#hooks.onOperationEnd?.(outcome.ctx, {
14412
14459
  durationMs: outcome.durationMs
14413
14460
  });
14414
14461
  }
14415
- return result;
14462
+ return { result, recordedByGraph };
14416
14463
  } catch (error) {
14417
14464
  const failure = asError(error);
14418
14465
  for (const outcome of pending) {
@@ -14463,6 +14510,14 @@ var Store = class _Store {
14463
14510
  * }); // one COMMIT / ROLLBACK across both layers
14464
14511
  * ```
14465
14512
  *
14513
+ * **The caller owns the connection — don't overlap writes on it.** The graph
14514
+ * store and your Drizzle writes share the one connection the caller's
14515
+ * transaction pinned. TypeGraph serializes the statements *its* collections
14516
+ * issue, but your raw Drizzle statements (and any graph write run alongside
14517
+ * them) are yours to sequence: a `Promise.all` mixing the two races two
14518
+ * queries on that connection — the overlap Postgres removes in `pg@9`. Await
14519
+ * each write before starting the next.
14520
+ *
14466
14521
  * Not available when the store was created with `{ history: true }`: a
14467
14522
  * caller-owned transaction context would let writes happen after capture has
14468
14523
  * lost its flush point, so this throws `ConfigurationError`
@@ -14539,34 +14594,40 @@ var Store = class _Store {
14539
14594
  * (#140); when history capture is enabled it is replaced with a fail-loud
14540
14595
  * guard because raw graph writes would bypass recorded-time capture.
14541
14596
  */
14542
- #buildTransactionContext(txBackend, sql17, runHooks = this.#immediateHookRunner()) {
14597
+ #buildTransactionContext(txBackend, sql17, runHooks = this.#immediateHookRunner(), receiptRecorder) {
14543
14598
  const txNodeOperations = {
14544
14599
  ...this.#buildNodeOperations(this.#createNodeOperationContext(runHooks)),
14545
- createQuery: () => this.#createQueryForBackend(txBackend)
14600
+ createQuery: () => this.#createQueryForBackend(txBackend),
14601
+ maybeRefreshStatisticsAfterBulk: void 0
14546
14602
  };
14547
14603
  const txEdgeOperations = {
14548
14604
  ...this.#buildEdgeOperations(this.#createEdgeOperationContext(runHooks)),
14549
- createQuery: () => this.#createQueryForBackend(txBackend)
14605
+ createQuery: () => this.#createQueryForBackend(txBackend),
14606
+ maybeRefreshStatisticsAfterBulk: void 0
14550
14607
  };
14551
14608
  const runNodeOperationHooks = (operation, kind, id, fn) => runHooks(this.#createOperationContext(operation, "node", kind, id), fn);
14552
- const nodes = createNodeCollectionsProxy(
14609
+ let nodes = createNodeCollectionsProxy(
14553
14610
  this.#graph,
14554
14611
  this.graphId,
14555
14612
  this.#registry,
14556
14613
  txBackend,
14557
14614
  txNodeOperations
14558
14615
  );
14559
- const edges = createEdgeCollectionsProxy(
14616
+ let edges = createEdgeCollectionsProxy(
14560
14617
  this.#graph,
14561
14618
  this.graphId,
14562
14619
  this.#registry,
14563
14620
  txBackend,
14564
14621
  txEdgeOperations
14565
14622
  );
14566
- const getNodeCollection = (kind) => {
14567
- if (!Object.hasOwn(this.#graph.nodes, kind)) return void 0;
14568
- return nodes[kind];
14569
- };
14623
+ if (receiptRecorder !== void 0) {
14624
+ ({ nodes, edges } = wrapTransactionCollections(
14625
+ nodes,
14626
+ edges,
14627
+ receiptRecorder
14628
+ ));
14629
+ }
14630
+ const getNodeCollection = (kind) => this.#resolveDynamicNodeCollection(nodes, kind);
14570
14631
  if (sql17 === void 0) {
14571
14632
  return {
14572
14633
  nodes,
@@ -14576,7 +14637,7 @@ var Store = class _Store {
14576
14637
  runNodeOperationHooks
14577
14638
  };
14578
14639
  }
14579
- const exposedSql = this.#captureEnabled ? createHistoryUnsafeSqlRef() : sql17;
14640
+ const exposedSql = this.#captureEnabled ? createHistoryUnsafeSqlRef() : this.#revisionTrackingEnabled ? createRevisionTrackingUnsafeSqlRef() : sql17;
14580
14641
  return {
14581
14642
  nodes,
14582
14643
  edges,
@@ -14598,7 +14659,20 @@ var Store = class _Store {
14598
14659
  */
14599
14660
  async clear() {
14600
14661
  const doClear = async (target) => {
14662
+ if (this.#revisionTrackingEnabled) {
14663
+ await lockRecordedGraphWrite(target, this.graphId);
14664
+ }
14665
+ const previousRevision = this.#revisionTrackingEnabled && !this.#captureEnabled ? await readRecordedClock(target, this.#sqlSchema(), this.graphId) : void 0;
14601
14666
  await target.clearGraph(this.graphId);
14667
+ if (this.#revisionTrackingEnabled && !this.#captureEnabled) {
14668
+ await advanceRevisionClock(
14669
+ target,
14670
+ this.#sqlSchema(),
14671
+ this.graphId,
14672
+ this.#baseBackend.capabilities.transactions,
14673
+ previousRevision
14674
+ );
14675
+ }
14602
14676
  };
14603
14677
  await (this.#baseBackend.capabilities.transactions ? this.#baseBackend.transaction(async (tx) => doClear(tx)) : doClear(this.#baseBackend));
14604
14678
  await this.#clearVectorStorage();
@@ -15253,6 +15327,8 @@ var Store = class _Store {
15253
15327
  graph: this.#graph,
15254
15328
  graphId: this.graphId,
15255
15329
  historyEnabled: this.#captureEnabled,
15330
+ revisionTrackingEnabled: this.#revisionTrackingEnabled,
15331
+ revisionSchema: this.#sqlSchema(),
15256
15332
  registry: this.#registry,
15257
15333
  createOperationContext: (operation, entity, kind, id) => this.#createOperationContext(operation, entity, kind, id),
15258
15334
  withOperationHooks: runHooks
@@ -15263,6 +15339,8 @@ var Store = class _Store {
15263
15339
  graph: this.#graph,
15264
15340
  graphId: this.graphId,
15265
15341
  historyEnabled: this.#captureEnabled,
15342
+ revisionTrackingEnabled: this.#revisionTrackingEnabled,
15343
+ revisionSchema: this.#sqlSchema(),
15266
15344
  registry: this.#registry,
15267
15345
  createOperationContext: (operation, entity, kind, id) => this.#createOperationContext(operation, entity, kind, id),
15268
15346
  withOperationHooks: runHooks
@@ -15434,5 +15512,5 @@ async function createVerifiedStore(graph, backend, options) {
15434
15512
  }
15435
15513
 
15436
15514
  export { ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, RecordedStoreView, StoreSearch, StoreView, UnionableQuery, avg, composeFragments, count, countDistinct, countDistinctEdges, countEdges, createFragment, createQueryBuilder, createStore, createStoreWithSchema, createVerifiedStore, defineSubgraphProject, field, having, havingEq, havingGt, havingGte, havingLt, havingLte, limitFragment, max, min, offsetFragment, orderByFragment, sum };
15437
- //# sourceMappingURL=chunk-WLRIWYHQ.js.map
15438
- //# sourceMappingURL=chunk-WLRIWYHQ.js.map
15515
+ //# sourceMappingURL=chunk-UES55P7G.js.map
15516
+ //# sourceMappingURL=chunk-UES55P7G.js.map