frogql 0.2.2__tar.gz → 0.2.4__tar.gz

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 (332) hide show
  1. frogql-0.2.4/.github/workflows/pages.yml +39 -0
  2. frogql-0.2.4/.github/workflows/release-wasm.yml +78 -0
  3. {frogql-0.2.2 → frogql-0.2.4}/CLAUDE.md +110 -17
  4. {frogql-0.2.2 → frogql-0.2.4}/Cargo.lock +134 -2
  5. {frogql-0.2.2 → frogql-0.2.4}/PKG-INFO +1 -1
  6. {frogql-0.2.2 → frogql-0.2.4}/README.md +58 -2
  7. frogql-0.2.4/assets/frogql-logo-512.png +0 -0
  8. frogql-0.2.4/assets/frogql-logo.png +0 -0
  9. frogql-0.2.4/bench/REPEAT_RANGE_REPORT.md +166 -0
  10. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/README.md +65 -3
  11. frogql-0.2.4/bench/cross-system/_lib/memrun.py +297 -0
  12. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/compare_results.py +42 -1
  13. frogql-0.2.4/bench/cross-system/grafeo/DIVERGENCES.md +98 -0
  14. frogql-0.2.4/bench/cross-system/grafeo/README.md +80 -0
  15. frogql-0.2.4/bench/cross-system/grafeo/ic1.gql +46 -0
  16. frogql-0.2.4/bench/cross-system/grafeo/ic11.gql +19 -0
  17. frogql-0.2.4/bench/cross-system/grafeo/ic12.gql +26 -0
  18. frogql-0.2.4/bench/cross-system/grafeo/ic13.gql +13 -0
  19. frogql-0.2.4/bench/cross-system/grafeo/ic2.gql +24 -0
  20. frogql-0.2.4/bench/cross-system/grafeo/ic3.gql +42 -0
  21. frogql-0.2.4/bench/cross-system/grafeo/ic4.gql +24 -0
  22. frogql-0.2.4/bench/cross-system/grafeo/ic5.gql +19 -0
  23. frogql-0.2.4/bench/cross-system/grafeo/ic6.gql +15 -0
  24. frogql-0.2.4/bench/cross-system/grafeo/ic7.gql +41 -0
  25. frogql-0.2.4/bench/cross-system/grafeo/ic8.gql +15 -0
  26. frogql-0.2.4/bench/cross-system/grafeo/ic9.gql +16 -0
  27. frogql-0.2.4/bench/cross-system/grafeo/requirements.txt +5 -0
  28. frogql-0.2.4/bench/cross-system/grafeo/run.py +321 -0
  29. frogql-0.2.4/bench/cross-system/grafeo/setup.py +277 -0
  30. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/DIVERGENCES.md +36 -0
  31. frogql-0.2.4/bench/cross-system/kuzu/ic1.cypher +49 -0
  32. frogql-0.2.4/bench/cross-system/kuzu/ic12.cypher +30 -0
  33. frogql-0.2.4/bench/cross-system/kuzu/ic13.cypher +19 -0
  34. frogql-0.2.4/bench/cross-system/kuzu/ic3.cypher +43 -0
  35. frogql-0.2.4/bench/cross-system/kuzu/ic4.cypher +24 -0
  36. frogql-0.2.4/bench/cross-system/kuzu/ic7.cypher +47 -0
  37. frogql-0.2.4/bench/cross-system/plot_cpu_mem.py +198 -0
  38. frogql-0.2.4/bench/cross-system/plot_results.py +136 -0
  39. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/run_all.sh +127 -19
  40. frogql-0.2.4/bench/cross-system/run_server.sh +166 -0
  41. frogql-0.2.4/bench/grafeo-vs-frogql/README.md +88 -0
  42. frogql-0.2.4/bench/grafeo-vs-frogql/bench.py +246 -0
  43. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic1.toml +9 -26
  44. frogql-0.2.4/bench/ldbc-queries/ic10.toml +35 -0
  45. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic11.toml +1 -3
  46. frogql-0.2.4/bench/ldbc-queries/ic12.toml +33 -0
  47. frogql-0.2.4/bench/ldbc-queries/ic13.toml +20 -0
  48. frogql-0.2.4/bench/ldbc-queries/ic14.toml +50 -0
  49. frogql-0.2.4/bench/ldbc-queries/ic3.toml +42 -0
  50. frogql-0.2.4/bench/ldbc-queries/ic4.toml +29 -0
  51. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic5.toml +1 -2
  52. frogql-0.2.4/bench/ldbc-queries/ic7.toml +50 -0
  53. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic9.toml +1 -2
  54. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/implemented-optimizations.md +42 -0
  55. frogql-0.2.4/docs/internals/iso-gql-gaps.md +176 -0
  56. frogql-0.2.4/docs/internals/shortest-path-fix-plan.md +184 -0
  57. frogql-0.2.4/docs/internals/wasm-browser-plan.md +177 -0
  58. {frogql-0.2.2 → frogql-0.2.4}/pyproject.toml +1 -1
  59. {frogql-0.2.2 → frogql-0.2.4}/python/Cargo.toml +1 -1
  60. {frogql-0.2.2 → frogql-0.2.4}/python/src/lib.rs +13 -3
  61. frogql-0.2.4/site/.nojekyll +0 -0
  62. frogql-0.2.4/site/assets/favicon.png +0 -0
  63. frogql-0.2.4/site/assets/frogql-logo.png +0 -0
  64. frogql-0.2.4/site/assets/movies.json +1 -0
  65. frogql-0.2.4/site/assets/og-image.png +0 -0
  66. frogql-0.2.4/site/index.html +1058 -0
  67. frogql-0.2.4/src/bin/bench_repeat.rs +267 -0
  68. {frogql-0.2.2 → frogql-0.2.4}/src/bin/frogql.rs +27 -12
  69. {frogql-0.2.2 → frogql-0.2.4}/src/bin/ldbc_bench.rs +3 -2
  70. {frogql-0.2.2 → frogql-0.2.4}/src/bin/orderby_bench.rs +24 -11
  71. {frogql-0.2.2 → frogql-0.2.4}/src/elaborate/mod.rs +161 -6
  72. {frogql-0.2.2 → frogql-0.2.4}/src/lib.rs +3 -15
  73. {frogql-0.2.2 → frogql-0.2.4}/src/model/csv_loader.rs +8 -8
  74. frogql-0.2.4/src/model/graph.rs +962 -0
  75. {frogql-0.2.2 → frogql-0.2.4}/src/model/value.rs +21 -1
  76. {frogql-0.2.2 → frogql-0.2.4}/src/optimizer/existential.rs +58 -4
  77. {frogql-0.2.2 → frogql-0.2.4}/src/optimizer/order_by_alias.rs +12 -1
  78. {frogql-0.2.2 → frogql-0.2.4}/src/optimizer/pushdown.rs +144 -4
  79. {frogql-0.2.2 → frogql-0.2.4}/src/optimizer/unroll_repeat.rs +10 -0
  80. {frogql-0.2.2 → frogql-0.2.4}/src/parser/grammar.rs +703 -128
  81. {frogql-0.2.2 → frogql-0.2.4}/src/parser/lexer.rs +49 -1
  82. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/engine.rs +2096 -202
  83. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/pattern_extract.rs +19 -4
  84. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/triple_index.rs +2 -2
  85. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/mod.rs +1 -0
  86. frogql-0.2.4/src/runtime/path_select.rs +265 -0
  87. {frogql-0.2.2 → frogql-0.2.4}/src/store/disk.rs +2 -2
  88. {frogql-0.2.2 → frogql-0.2.4}/src/store/dump.rs +12 -12
  89. {frogql-0.2.2 → frogql-0.2.4}/src/store/io.rs +20 -19
  90. {frogql-0.2.2 → frogql-0.2.4}/src/store/lazy.rs +15 -57
  91. {frogql-0.2.2 → frogql-0.2.4}/src/store/overlay.rs +49 -1
  92. {frogql-0.2.2 → frogql-0.2.4}/src/store/secondary_index.rs +6 -6
  93. {frogql-0.2.2 → frogql-0.2.4}/src/syntax/dm.rs +8 -0
  94. frogql-0.2.4/src/syntax/expr.rs +527 -0
  95. {frogql-0.2.2 → frogql-0.2.4}/src/syntax/mod.rs +1 -0
  96. frogql-0.2.4/src/syntax/path_pattern.rs +205 -0
  97. frogql-0.2.4/src/syntax/path_prefix.rs +151 -0
  98. {frogql-0.2.2 → frogql-0.2.4}/src/syntax/query.rs +43 -5
  99. {frogql-0.2.2 → frogql-0.2.4}/src/typing/checker.rs +532 -29
  100. {frogql-0.2.2 → frogql-0.2.4}/src/typing/format.rs +2 -0
  101. {frogql-0.2.2 → frogql-0.2.4}/src/typing/inference.rs +13 -13
  102. {frogql-0.2.2 → frogql-0.2.4}/src/typing/path_type.rs +4 -1
  103. {frogql-0.2.2 → frogql-0.2.4}/src/typing/simple_type.rs +7 -0
  104. {frogql-0.2.2 → frogql-0.2.4}/src/typing/validate.rs +7 -7
  105. {frogql-0.2.2 → frogql-0.2.4}/src/typing/variable_type.rs +16 -0
  106. {frogql-0.2.2 → frogql-0.2.4}/tests/aggregates_proptest.rs +5 -5
  107. {frogql-0.2.2 → frogql-0.2.4}/tests/bench_test.rs +15 -15
  108. frogql-0.2.4/tests/builtin_floor_cast_test.rs +81 -0
  109. {frogql-0.2.2 → frogql-0.2.4}/tests/coalesce_test.rs +5 -5
  110. frogql-0.2.4/tests/collect_list_test.rs +163 -0
  111. frogql-0.2.4/tests/correlated_subquery_test.rs +172 -0
  112. {frogql-0.2.2 → frogql-0.2.4}/tests/count_test.rs +123 -11
  113. frogql-0.2.4/tests/division_test.rs +63 -0
  114. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_default_test.rs +3 -3
  115. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_delete_expr_test.rs +2 -2
  116. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_label_test.rs +4 -4
  117. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_persistence_test.rs +2 -2
  118. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_remove_test.rs +2 -2
  119. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_runtime_test.rs +2 -2
  120. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_schema_test.rs +2 -2
  121. {frogql-0.2.2 → frogql-0.2.4}/tests/dm_set_test.rs +2 -2
  122. {frogql-0.2.2 → frogql-0.2.4}/tests/dump_test.rs +6 -6
  123. {frogql-0.2.2 → frogql-0.2.4}/tests/elaborate_test.rs +3 -3
  124. {frogql-0.2.2 → frogql-0.2.4}/tests/exists_fold_test.rs +9 -3
  125. {frogql-0.2.2 → frogql-0.2.4}/tests/exists_runtime_test.rs +5 -5
  126. {frogql-0.2.2 → frogql-0.2.4}/tests/first_class_node_edge_values_test.rs +16 -17
  127. {frogql-0.2.2 → frogql-0.2.4}/tests/float_test.rs +86 -4
  128. {frogql-0.2.2 → frogql-0.2.4}/tests/graph_type_test.rs +3 -3
  129. frogql-0.2.4/tests/group_by_alias_test.rs +117 -0
  130. frogql-0.2.4/tests/group_by_var_test.rs +96 -0
  131. frogql-0.2.4/tests/ic7_test.rs +113 -0
  132. frogql-0.2.4/tests/iso_expr_test.rs +121 -0
  133. {frogql-0.2.2 → frogql-0.2.4}/tests/lattice_proptest.rs +1 -0
  134. {frogql-0.2.2 → frogql-0.2.4}/tests/lazy_mut_test.rs +2 -2
  135. frogql-0.2.4/tests/list_comprehension_test.rs +114 -0
  136. {frogql-0.2.2 → frogql-0.2.4}/tests/list_test.rs +5 -5
  137. {frogql-0.2.2 → frogql-0.2.4}/tests/ltj_label_disjunction_test.rs +3 -3
  138. frogql-0.2.4/tests/ltj_reverse_path_test.rs +123 -0
  139. frogql-0.2.4/tests/memory_mut_test.rs +375 -0
  140. {frogql-0.2.2 → frogql-0.2.4}/tests/multi_match_proptest.rs +4 -4
  141. {frogql-0.2.2 → frogql-0.2.4}/tests/multi_match_test.rs +4 -4
  142. frogql-0.2.4/tests/named_path_test.rs +176 -0
  143. {frogql-0.2.2 → frogql-0.2.4}/tests/null_test.rs +4 -4
  144. {frogql-0.2.2 → frogql-0.2.4}/tests/optional_match_test.rs +7 -7
  145. {frogql-0.2.2 → frogql-0.2.4}/tests/order_by_optimization_test.rs +10 -10
  146. {frogql-0.2.2 → frogql-0.2.4}/tests/order_by_test.rs +6 -6
  147. {frogql-0.2.2 → frogql-0.2.4}/tests/parallel_edge_test.rs +5 -5
  148. {frogql-0.2.2 → frogql-0.2.4}/tests/parse_and_run_test.rs +30 -3
  149. {frogql-0.2.2 → frogql-0.2.4}/tests/parser_test.rs +47 -2
  150. frogql-0.2.4/tests/path_prefix_test.rs +606 -0
  151. frogql-0.2.4/tests/record_expr_test.rs +94 -0
  152. {frogql-0.2.2 → frogql-0.2.4}/tests/record_test.rs +6 -6
  153. {frogql-0.2.2 → frogql-0.2.4}/tests/runtime_test.rs +6 -6
  154. frogql-0.2.4/tests/shortest_bfs_test.rs +221 -0
  155. {frogql-0.2.2 → frogql-0.2.4}/tests/store_runtime_test.rs +7 -7
  156. {frogql-0.2.2 → frogql-0.2.4}/tests/text2gql_test.rs +7 -7
  157. {frogql-0.2.2 → frogql-0.2.4}/tests/typecheck_gaps_order_by_test.rs +7 -7
  158. frogql-0.2.4/tests/value_subquery_test.rs +99 -0
  159. frogql-0.2.2/bench/ldbc-queries/ic10.toml +0 -48
  160. frogql-0.2.2/bench/ldbc-queries/ic12.toml +0 -41
  161. frogql-0.2.2/bench/ldbc-queries/ic13.toml +0 -31
  162. frogql-0.2.2/bench/ldbc-queries/ic14.toml +0 -61
  163. frogql-0.2.2/bench/ldbc-queries/ic3.toml +0 -48
  164. frogql-0.2.2/bench/ldbc-queries/ic4.toml +0 -38
  165. frogql-0.2.2/bench/ldbc-queries/ic7.toml +0 -70
  166. frogql-0.2.2/docs/internals/iso-gql-gaps.md +0 -99
  167. frogql-0.2.2/src/model/graph.rs +0 -598
  168. frogql-0.2.2/src/syntax/expr.rs +0 -263
  169. frogql-0.2.2/src/syntax/path_pattern.rs +0 -102
  170. {frogql-0.2.2 → frogql-0.2.4}/.github/workflows/ci.yml +0 -0
  171. {frogql-0.2.2 → frogql-0.2.4}/.github/workflows/release-npm.yml +0 -0
  172. {frogql-0.2.2 → frogql-0.2.4}/.github/workflows/release.yml +0 -0
  173. {frogql-0.2.2 → frogql-0.2.4}/.gitignore +0 -0
  174. {frogql-0.2.2 → frogql-0.2.4}/ARCHITECTURE.md +0 -0
  175. {frogql-0.2.2 → frogql-0.2.4}/Cargo.toml +0 -0
  176. {frogql-0.2.2 → frogql-0.2.4}/LICENSE +0 -0
  177. {frogql-0.2.2 → frogql-0.2.4}/MANUAL.md +0 -0
  178. {frogql-0.2.2 → frogql-0.2.4}/bench/.gitignore +0 -0
  179. {frogql-0.2.2 → frogql-0.2.4}/bench/BENCHMARK_PLAN.md +0 -0
  180. {frogql-0.2.2 → frogql-0.2.4}/bench/INTERNAL_BENCHMARK.md +0 -0
  181. {frogql-0.2.2 → frogql-0.2.4}/bench/LDBC_BENCHMARK.md +0 -0
  182. {frogql-0.2.2 → frogql-0.2.4}/bench/LDBC_BENCH_PLAN.md +0 -0
  183. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/QUERIES.md +0 -0
  184. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/SURVEY.md +0 -0
  185. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/_lib/row_hash.py +0 -0
  186. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/gqlite/run.sh +0 -0
  187. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/DIVERGENCES.md +0 -0
  188. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic11.cypher +0 -0
  189. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic2.cypher +0 -0
  190. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic5.cypher +0 -0
  191. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic6.cypher +0 -0
  192. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic8.cypher +0 -0
  193. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/ic9.cypher +0 -0
  194. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/requirements.txt +0 -0
  195. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/run.py +0 -0
  196. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/graphqlite/setup.py +0 -0
  197. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/install_python_deps.sh +0 -0
  198. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/README.md +0 -0
  199. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic11.cypher +0 -0
  200. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic2.cypher +0 -0
  201. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic5.cypher +0 -0
  202. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic6.cypher +0 -0
  203. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic8.cypher +0 -0
  204. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/ic9.cypher +0 -0
  205. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/requirements.txt +0 -0
  206. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/run.py +0 -0
  207. {frogql-0.2.2 → frogql-0.2.4}/bench/cross-system/kuzu/setup.py +0 -0
  208. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic2.toml +0 -0
  209. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic6.toml +0 -0
  210. {frogql-0.2.2 → frogql-0.2.4}/bench/ldbc-queries/ic8.toml +0 -0
  211. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/1-tree.gql +0 -0
  212. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/2-3-lollipop.gql +0 -0
  213. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/2-comb.gql +0 -0
  214. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/2-tree.gql +0 -0
  215. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/3-4-lollipop.gql +0 -0
  216. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/3-clique.gql +0 -0
  217. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/3-cycle.gql +0 -0
  218. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/3-path.gql +0 -0
  219. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/4-clique.gql +0 -0
  220. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/4-cycle.gql +0 -0
  221. {frogql-0.2.2 → frogql-0.2.4}/bench/queries/4-path.gql +0 -0
  222. {frogql-0.2.2 → frogql-0.2.4}/bench/scripts/csv_to_json.py +0 -0
  223. {frogql-0.2.2 → frogql-0.2.4}/bench/scripts/download_livejournal.sh +0 -0
  224. {frogql-0.2.2 → frogql-0.2.4}/bench/scripts/generate_queries.py +0 -0
  225. {frogql-0.2.2 → frogql-0.2.4}/bench/scripts/run_bench.sh +0 -0
  226. {frogql-0.2.2 → frogql-0.2.4}/docs/data-import.md +0 -0
  227. {frogql-0.2.2 → frogql-0.2.4}/docs/data-modification.md +0 -0
  228. {frogql-0.2.2 → frogql-0.2.4}/docs/graph-types.md +0 -0
  229. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/JOIN_STRATEGY_NOTES.md +0 -0
  230. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/graph-type-catalog-plan.md +0 -0
  231. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/possible-optimizations.md +0 -0
  232. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/rules.md +0 -0
  233. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/storage-architecture.md +0 -0
  234. {frogql-0.2.2 → frogql-0.2.4}/docs/internals/typechecker_migration.md +0 -0
  235. {frogql-0.2.2 → frogql-0.2.4}/docs/query-language.md +0 -0
  236. {frogql-0.2.2 → frogql-0.2.4}/docs/secondary-indexes.md +0 -0
  237. {frogql-0.2.2 → frogql-0.2.4}/examples/address_queries.json +0 -0
  238. {frogql-0.2.2 → frogql-0.2.4}/examples/bom.gdb +0 -0
  239. {frogql-0.2.2 → frogql-0.2.4}/examples/books_queries.json +0 -0
  240. {frogql-0.2.2 → frogql-0.2.4}/examples/disney.gdb +0 -0
  241. {frogql-0.2.2 → frogql-0.2.4}/examples/disney_queries.json +0 -0
  242. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_financial_management.gdb +0 -0
  243. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_financial_management_queries.json +0 -0
  244. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_fraud_detection.gdb +0 -0
  245. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_fraud_detection_queries.json +0 -0
  246. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_payment.gdb +0 -0
  247. {frogql-0.2.2 → frogql-0.2.4}/examples/financial_payment_queries.json +0 -0
  248. {frogql-0.2.2 → frogql-0.2.4}/examples/fraud_detection.gdb +0 -0
  249. {frogql-0.2.2 → frogql-0.2.4}/examples/gameofthrones.gdb +0 -0
  250. {frogql-0.2.2 → frogql-0.2.4}/examples/grandstack.gdb +0 -0
  251. {frogql-0.2.2 → frogql-0.2.4}/examples/hockey.gdb +0 -0
  252. {frogql-0.2.2 → frogql-0.2.4}/examples/hockey_queries.json +0 -0
  253. {frogql-0.2.2 → frogql-0.2.4}/examples/information_data_lineage.gdb +0 -0
  254. {frogql-0.2.2 → frogql-0.2.4}/examples/information_data_lineage_queries.json +0 -0
  255. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_identity_and_access_management.gdb +0 -0
  256. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_identity_and_access_management_queries.json +0 -0
  257. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_iot.gdb +0 -0
  258. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_iot_queries.json +0 -0
  259. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_it_asset_management.gdb +0 -0
  260. {frogql-0.2.2 → frogql-0.2.4}/examples/information_technology_it_asset_management_queries.json +0 -0
  261. {frogql-0.2.2 → frogql-0.2.4}/examples/knowledge_general_knowledge.gdb +0 -0
  262. {frogql-0.2.2 → frogql-0.2.4}/examples/knowledge_general_knowledge_queries.json +0 -0
  263. {frogql-0.2.2 → frogql-0.2.4}/examples/knowledge_graph_geography.gdb +0 -0
  264. {frogql-0.2.2 → frogql-0.2.4}/examples/knowledge_graph_geography_queries.json +0 -0
  265. {frogql-0.2.2 → frogql-0.2.4}/examples/manufacturing_bombill_of_materials.gdb +0 -0
  266. {frogql-0.2.2 → frogql-0.2.4}/examples/manufacturing_bombill_of_materials_queries.json +0 -0
  267. {frogql-0.2.2 → frogql-0.2.4}/examples/manufacturing_production_process.gdb +0 -0
  268. {frogql-0.2.2 → frogql-0.2.4}/examples/manufacturing_production_process_queries.json +0 -0
  269. {frogql-0.2.2 → frogql-0.2.4}/examples/moivelens.gdb +0 -0
  270. {frogql-0.2.2 → frogql-0.2.4}/examples/moivelens_queries.json +0 -0
  271. {frogql-0.2.2 → frogql-0.2.4}/examples/movies.gdb +0 -0
  272. {frogql-0.2.2 → frogql-0.2.4}/examples/northwind.gdb +0 -0
  273. {frogql-0.2.2 → frogql-0.2.4}/examples/olympics_queries.json +0 -0
  274. {frogql-0.2.2 → frogql-0.2.4}/examples/soccer_2016.gdb +0 -0
  275. {frogql-0.2.2 → frogql-0.2.4}/examples/soccer_2016_queries.json +0 -0
  276. {frogql-0.2.2 → frogql-0.2.4}/examples/social_network_recommendation.gdb +0 -0
  277. {frogql-0.2.2 → frogql-0.2.4}/examples/social_network_recommendation_queries.json +0 -0
  278. {frogql-0.2.2 → frogql-0.2.4}/examples/social_network_twitter.gdb +0 -0
  279. {frogql-0.2.2 → frogql-0.2.4}/examples/social_network_twitter_queries.json +0 -0
  280. {frogql-0.2.2 → frogql-0.2.4}/examples/stackoverflow2.gdb +0 -0
  281. {frogql-0.2.2 → frogql-0.2.4}/examples/student_loan.gdb +0 -0
  282. {frogql-0.2.2 → frogql-0.2.4}/examples/student_loan_queries.json +0 -0
  283. {frogql-0.2.2 → frogql-0.2.4}/examples/typecheck_demo.rs +0 -0
  284. {frogql-0.2.2 → frogql-0.2.4}/examples/typecheck_repl_smoke.rs +0 -0
  285. {frogql-0.2.2 → frogql-0.2.4}/examples/video_games_queries.json +0 -0
  286. {frogql-0.2.2 → frogql-0.2.4}/examples/world.gdb +0 -0
  287. {frogql-0.2.2 → frogql-0.2.4}/examples/world_queries.json +0 -0
  288. {frogql-0.2.2 → frogql-0.2.4}/python/LICENSE +0 -0
  289. {frogql-0.2.2 → frogql-0.2.4}/python/README.md +0 -0
  290. {frogql-0.2.2 → frogql-0.2.4}/scripts/convert_dev_datasets.py +0 -0
  291. {frogql-0.2.2 → frogql-0.2.4}/src/bin/bench_queries.rs +0 -0
  292. {frogql-0.2.2 → frogql-0.2.4}/src/bin/bench_setup.rs +0 -0
  293. {frogql-0.2.2 → frogql-0.2.4}/src/bin/convert_edgelist.rs +0 -0
  294. {frogql-0.2.2 → frogql-0.2.4}/src/bin/internal_bench.rs +0 -0
  295. {frogql-0.2.2 → frogql-0.2.4}/src/model/graph_access.rs +0 -0
  296. {frogql-0.2.2 → frogql-0.2.4}/src/model/mod.rs +0 -0
  297. {frogql-0.2.2 → frogql-0.2.4}/src/optimizer/mod.rs +0 -0
  298. {frogql-0.2.2 → frogql-0.2.4}/src/pager/header.rs +0 -0
  299. {frogql-0.2.2 → frogql-0.2.4}/src/pager/mod.rs +0 -0
  300. {frogql-0.2.2 → frogql-0.2.4}/src/pager/page.rs +0 -0
  301. {frogql-0.2.2 → frogql-0.2.4}/src/pager/pager.rs +0 -0
  302. {frogql-0.2.2 → frogql-0.2.4}/src/parser/mod.rs +0 -0
  303. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/assignment.rs +0 -0
  304. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/catalog.rs +0 -0
  305. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/dm.rs +0 -0
  306. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/algorithm.rs +0 -0
  307. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/iterator.rs +0 -0
  308. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/mod.rs +0 -0
  309. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/ltj/veo.rs +0 -0
  310. {frogql-0.2.2 → frogql-0.2.4}/src/runtime/result.rs +0 -0
  311. {frogql-0.2.2 → frogql-0.2.4}/src/store/catalog_io.rs +0 -0
  312. {frogql-0.2.2 → frogql-0.2.4}/src/store/disk_index.rs +0 -0
  313. {frogql-0.2.2 → frogql-0.2.4}/src/store/mod.rs +0 -0
  314. {frogql-0.2.2 → frogql-0.2.4}/src/store/record.rs +0 -0
  315. {frogql-0.2.2 → frogql-0.2.4}/src/store/secondary_index_io.rs +0 -0
  316. {frogql-0.2.2 → frogql-0.2.4}/src/store/string_table.rs +0 -0
  317. {frogql-0.2.2 → frogql-0.2.4}/src/syntax/descriptor.rs +0 -0
  318. {frogql-0.2.2 → frogql-0.2.4}/src/syntax/statement.rs +0 -0
  319. {frogql-0.2.2 → frogql-0.2.4}/src/typing/descriptor_type.rs +0 -0
  320. {frogql-0.2.2 → frogql-0.2.4}/src/typing/label_type.rs +0 -0
  321. {frogql-0.2.2 → frogql-0.2.4}/src/typing/mod.rs +0 -0
  322. {frogql-0.2.2 → frogql-0.2.4}/src/typing/property_type.rs +0 -0
  323. {frogql-0.2.2 → frogql-0.2.4}/src/typing/type_environment.rs +0 -0
  324. {frogql-0.2.2 → frogql-0.2.4}/test_data/fraud.json +0 -0
  325. {frogql-0.2.2 → frogql-0.2.4}/test_data/ltj_label_disjunction.json +0 -0
  326. {frogql-0.2.2 → frogql-0.2.4}/test_data/movies.json +0 -0
  327. {frogql-0.2.2 → frogql-0.2.4}/test_data/social-network.json +0 -0
  328. {frogql-0.2.2 → frogql-0.2.4}/tests/aggregates_proptest.proptest-regressions +0 -0
  329. {frogql-0.2.2 → frogql-0.2.4}/tests/compile_diagnostics.rs +0 -0
  330. {frogql-0.2.2 → frogql-0.2.4}/tests/parser_dm_test.rs +0 -0
  331. {frogql-0.2.2 → frogql-0.2.4}/tests/typecheck_smoke.rs +0 -0
  332. {frogql-0.2.2 → frogql-0.2.4}/tests/typecheck_test.rs +0 -0
@@ -0,0 +1,39 @@
1
+ name: Deploy website (GitHub Pages)
2
+
3
+ # Publishes the static landing page in site/ to GitHub Pages.
4
+ # Served at https://pleiad.github.io/frogql/
5
+ #
6
+ # One-time setup: repo Settings → Pages → "Build and deployment" →
7
+ # Source: "GitHub Actions".
8
+
9
+ on:
10
+ push:
11
+ branches: [ main ]
12
+ paths:
13
+ - "site/**"
14
+ - ".github/workflows/pages.yml"
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+ pages: write
20
+ id-token: write
21
+
22
+ concurrency:
23
+ group: pages
24
+ cancel-in-progress: true
25
+
26
+ jobs:
27
+ deploy:
28
+ runs-on: ubuntu-latest
29
+ environment:
30
+ name: github-pages
31
+ url: ${{ steps.deployment.outputs.page_url }}
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/configure-pages@v5
35
+ - uses: actions/upload-pages-artifact@v3
36
+ with:
37
+ path: site
38
+ - id: deployment
39
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,78 @@
1
+ name: Release wasm
2
+
3
+ # Build the browser WASM package with wasm-pack and publish it to npm as
4
+ # `frogql-wasm`. Fires on a `v*` tag (alongside `release.yml` for PyPI and
5
+ # `release-npm.yml` for native Node) AND on manual `workflow_dispatch`.
6
+ #
7
+ # The manual trigger matters: the version is read from `wasm/Cargo.toml`
8
+ # (via the built package.json), NOT from the tag ref, so you can ship
9
+ # `frogql-wasm` on its own — e.g. catch it up to a version the other
10
+ # registries already published — without re-tagging the whole suite. The
11
+ # publish is idempotent (skips if the version already exists on npm).
12
+ #
13
+ # Unlike the napi package there are no per-platform binaries: WebAssembly
14
+ # is portable, so this is one build + one publish. Reuses the `npm`
15
+ # GitHub Environment and the `NPM_TOKEN` secret.
16
+
17
+ on:
18
+ push:
19
+ tags:
20
+ - "v*"
21
+ workflow_dispatch:
22
+
23
+ permissions:
24
+ contents: read
25
+ id-token: write # npm provenance
26
+
27
+ jobs:
28
+ publish:
29
+ name: Build + publish frogql-wasm
30
+ runs-on: ubuntu-22.04
31
+ environment: npm
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Setup Rust
36
+ uses: dtolnay/rust-toolchain@stable
37
+ with:
38
+ targets: wasm32-unknown-unknown
39
+
40
+ - name: Install wasm-pack
41
+ uses: jetli/wasm-pack-action@v0.4.0
42
+ with:
43
+ version: latest
44
+
45
+ - name: Setup Node
46
+ uses: actions/setup-node@v4
47
+ with:
48
+ node-version: 20
49
+ check-latest: true
50
+ registry-url: https://registry.npmjs.org
51
+
52
+ - name: Build package
53
+ # `web` target (not `bundler`): it fetches the .wasm via
54
+ # `import.meta.url`, which Vite/Rollup/esbuild handle natively with
55
+ # no extra plugin. The `bundler` target requires consumers to add
56
+ # `vite-plugin-wasm`, which is friction for the most common setup.
57
+ # Consumers call `await init()` once, then use the API.
58
+ run: wasm-pack build wasm --target web --out-dir pkg
59
+
60
+ - name: Publish to npm (idempotent)
61
+ working-directory: wasm/pkg
62
+ env:
63
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
64
+ NPM_CONFIG_PROVENANCE: "true"
65
+ run: |
66
+ npm config set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}"
67
+ name=$(node -p "require('./package.json').name")
68
+ version=$(node -p "require('./package.json').version")
69
+ # dist-tag from the version itself (not the git ref), so manual
70
+ # dispatch and tag pushes behave identically: a pre-release
71
+ # (contains '-') lands on `next`, a clean version on `latest`.
72
+ if [[ "$version" == *-* ]]; then DIST_TAG=next; else DIST_TAG=latest; fi
73
+ echo "Publishing ${name}@${version} → dist-tag=${DIST_TAG}"
74
+ if npm view "${name}@${version}" version >/dev/null 2>&1; then
75
+ echo "::notice::${name}@${version} already on npm — skipping"
76
+ else
77
+ npm publish --tag "${DIST_TAG}" --access public --provenance
78
+ fi
@@ -24,7 +24,8 @@ cargo test --test parser_test --test runtime_test --test store_runtime_test \
24
24
  --test parser_dm_test --test lazy_mut_test --test dm_runtime_test \
25
25
  --test dm_persistence_test --test dm_schema_test --test dm_default_test \
26
26
  --test dump_test --test dm_set_test --test dm_remove_test \
27
- --test dm_label_test --test dm_delete_expr_test
27
+ --test dm_label_test --test dm_delete_expr_test --test memory_mut_test \
28
+ --test path_prefix_test --test shortest_bfs_test
28
29
 
29
30
  # Single test
30
31
  cargo test --test runtime_test test_join_star_any_label -- --exact
@@ -44,6 +45,13 @@ cargo build --release
44
45
  cd python && source <your-venv>/bin/activate && pip install maturin && maturin develop --release
45
46
  # For wheels to ship to other machines:
46
47
  cd python && maturin build --release # output in target/wheels/
48
+
49
+ # Browser WASM bindings (frogql-wasm)
50
+ rustup target add wasm32-unknown-unknown # one-time
51
+ cargo test -p frogql-wasm # host tests of the engine core (query_json/dm_json)
52
+ cargo build -p frogql-wasm --target wasm32-unknown-unknown
53
+ # Generate the publishable npm package (web target); release-wasm.yml runs the same:
54
+ cargo install wasm-pack && wasm-pack build wasm --target web --out-dir pkg
47
55
  ```
48
56
 
49
57
  ### Pre-commit checklist for Rust changes (non-negotiable)
@@ -55,7 +63,7 @@ cd python && maturin build --release # output in target/wheels/
55
63
 
56
64
  ## Workspace layout
57
65
 
58
- Cargo workspace with three members and `resolver = "2"`:
66
+ Cargo workspace with four members and `resolver = "2"`:
59
67
  - `.` (root) — the `gqlrust` library crate + CLI binaries under `src/bin/`:
60
68
  - `frogql` — interactive REPL with line editing (rustyline). **Requires `repl` feature.**
61
69
  - `bench_queries` — generic benchmark runner
@@ -65,6 +73,7 @@ Cargo workspace with three members and `resolver = "2"`:
65
73
  - `convert_edgelist` — edge-list format converter
66
74
  - `python/` — the `frogql-py` crate: a `cdylib` exposing a PyO3 extension module named `frogql`. Depends on `gqlrust = { path = "..", default-features = false }` so the wheel ships only the library half (no rustyline/ureq/etc.). Built and installed with maturin (`maturin develop` for local dev, `maturin build --release` for wheels). Maturin installs into whichever venv is active.
67
75
  - `node/` — the `frogql-node` crate: a `cdylib` exposing a napi-rs extension named `frogql`. Same `default-features = false` discipline as `python/`. Built and packaged via `@napi-rs/cli` (see `node/package.json` scripts). Distributed on npm as a host package (`frogql`) plus five platform sub-packages (`frogql-darwin-x64`, `frogql-darwin-arm64`, `frogql-linux-x64-gnu`, `frogql-linux-arm64-gnu`, `frogql-win32-x64-msvc`) declared in `optionalDependencies`; npm picks the right one at install time. The host's `index.js` (platform dispatcher) and `index.d.ts` (TS types) are auto-generated by `napi build` but **checked into git** — required so the publish job can ship them without rebuilding.
76
+ - `wasm/` — the `frogql-wasm` crate: a `cdylib` (+ `rlib` for host tests) exposing a `wasm-bindgen` module for the **browser**. Same `default-features = false` discipline. Wraps **`MemoryGraphStore`** only (no filesystem in the browser): `open_json(json)` → `Connection` with `execute(query, limit?)`, `to_json()`, `schema()`, `node_count`, `edge_count`. Read queries return row objects; INSERT/SET/DELETE work in RAM via the overlay; DDL / `CREATE INDEX` are rejected (no catalog/index in-memory). Persistence is the JSON string from `to_json()` round-tripped through `open_json` (store it in IndexedDB). Build for the browser with `wasm-pack build --target bundler` (needs `wasm-pack` + the `wasm32-unknown-unknown` target); the engine core (`query_json`/`dm_json`) has host-target unit tests runnable via `cargo test -p frogql-wasm`. See `docs/internals/wasm-browser-plan.md`.
68
77
 
69
78
  `resolver = "2"` is required: with v1, building the python crate `--target X` (cross-compile) unifies features globally and drags in `gqlrust`'s default `repl` + `bench` features even when `default-features = false` is set on the dep. That pulled `ureq → ring`, which fails to cross-build on the manylinux2014 aarch64 container. Resolver v2 computes features per-target and isolates the wheel build. Same trick keeps the node crate's wheels clean.
70
79
 
@@ -88,15 +97,17 @@ Dev: `proptest` (used by `aggregates_proptest`, `lattice_proptest`, `multi_match
88
97
 
89
98
  ## Releases (PyPI + npm in lock-step)
90
99
 
91
- One git tag fires both registries. Pushing `v*` triggers:
100
+ One git tag fires all three registries (PyPI wheel, native npm, browser WASM npm). Pushing `v*` triggers:
92
101
  - `.github/workflows/release.yml` → builds wheels (Linux x86_64+aarch64, macOS x86_64+arm64, Windows x86_64; manylinux2014, abi3-py38) + sdist, uploads via `MATURIN_PYPI_TOKEN`, runs in the `pypi` GitHub Environment for required-reviewers gating.
93
102
  - `.github/workflows/release-npm.yml` → 5-target build matrix (mac arm64 native, mac x64 cross-compiled from arm64, linux x64 native, linux arm64 via zig, windows x64 native), publishes the host `frogql` package plus the 5 platform sub-packages via `NPM_TOKEN`, runs in the `npm` GitHub Environment. Pre-release versions (any with a `-` like `0.2.0-rc.3`) land on dist-tag `next`; clean `v0.2.0` lands on `latest`.
103
+ - `.github/workflows/release-wasm.yml` → single platform-independent build (`wasm-pack build wasm --target web`), publishes the **`frogql-wasm`** npm package (unscoped, consumed as `import init, { open_json } from "frogql-wasm"`). Reuses the `npm` Environment + `NPM_TOKEN`. WebAssembly is portable, so there's no build matrix. Same dist-tag logic and idempotent skip-if-exists as the napi job. The `web` target (not `bundler`) is deliberate: it needs no `vite-plugin-wasm` in the consumer.
94
104
 
95
- Cut a release by bumping **four files** in lock-step plus regenerating `Cargo.lock` (auto on any `cargo build`):
105
+ Cut a release by bumping **five files** in lock-step plus regenerating `Cargo.lock` (auto on any `cargo build`):
96
106
  - `python/pyproject.toml` (PEP 440 form: `0.2.0rc3`)
97
107
  - `python/Cargo.toml` (semver: `0.2.0-rc.3`)
98
108
  - `node/Cargo.toml`
99
109
  - `node/package.json` (host version + the 5 `optionalDependencies` versions)
110
+ - `wasm/Cargo.toml` (semver; the published `frogql-wasm` version is derived from it by wasm-pack)
100
111
 
101
112
  Then `git tag vX.Y.Z && git push origin vX.Y.Z`. Both registries reject re-publishing, so always bump. The npm release also requires `node/index.js` + `node/index.d.ts` to be committed at the tagged SHA; regenerate them with `npm run build` inside `node/` whenever the API surface changes and commit the diff.
102
113
 
@@ -133,7 +144,7 @@ Local downstream dev:
133
144
  - `runtime::dm::run_dm(&store, &dm, schema_for_validation)` — execute one ISO §13 data-modifying statement (INSERT / DELETE / DETACH DELETE). `schema_for_validation` is `Some(&Schema)` only when G2000 should fire (active type is neither DEFAULT nor absent)
134
145
  - `LazyGraphStore::open_or_create(path)` — sqlite3-style; creates an empty `.gdb` if `path` doesn't exist, then opens it
135
146
  - `LazyGraphStore::save(path)` — atomic save of the merged base+overlay view (tmp+rename); refreshes DEFAULT before persisting
136
- - `LazyGraphStore::materialize_to_graph()` — decode the merged view into an in-RAM `Graph` with compacted IDs; used by `save` and the dump utility
147
+ - `LazyGraphStore::materialize_to_graph()` — decode the merged view into an in-RAM `MemoryGraphStore` with compacted IDs; used by `save` and the dump utility
137
148
  - `LazyGraphStore::refresh_default_if_dirty()` — re-run `infer_simple_schema` if the catalog's `default_dirty` flag is set; idempotent
138
149
 
139
150
  ### Graph-type catalog
@@ -164,11 +175,11 @@ Operational invariants Claude must keep in mind:
164
175
  - **DEFAULT lifecycle**: `GraphTypeCatalog.default_dirty` (in-RAM only, `#[serde(skip)]`) flips after every successful DML; `refresh_default_if_dirty` re-runs `infer_simple_schema` lazily on `handle_show("DEFAULT")` and `LazyGraphStore::save`. Eager refresh would cost O(N+E) per mutation.
165
176
  - **Cache invalidation**: every successful DML calls `Runtime::invalidate_caches()` (REPL) or clears `Connection.triple_index` (Python); next query rebuilds the six-ordering index from base+overlay (~670 ms SF0.1).
166
177
 
167
- **Persistence (`.save`).** `LazyGraphStore::save(path)` materialises merged base+overlay into a temporary `Graph` and calls `save_graph_with_catalog_and_indexes_atomic` (writes graph + catalog + persisted DDL index list to `<path>.tmp`, atomic `rename`). `LazyGraphStore::open_or_create(path)` mirrors SQLite — non-existent path writes an empty `.gdb` first.
178
+ **Persistence (`.save`).** `LazyGraphStore::save(path)` materialises merged base+overlay into a temporary `MemoryGraphStore` and calls `save_graph_with_catalog_and_indexes_atomic` (writes graph + catalog + persisted DDL index list to `<path>.tmp`, atomic `rename`). `LazyGraphStore::open_or_create(path)` mirrors SQLite — non-existent path writes an empty `.gdb` first.
168
179
 
169
180
  **Auto-commit is OFF by design.** Forgetting `.save` loses the overlay (DML) AND any DDL declared this session (`CREATE INDEX` / `DROP INDEX` only mutate the in-memory `RefCell<SecondaryIndex>` via `build_declared`; the persisted list at `header.secondary_index_root` rewrites only on `.save`). Same trade-off as SQLite's explicit-commit model — lets users experiment without writing.
170
181
 
171
- **Dump**: `store::dump::dump_to_json_file` (round-trips through `Graph::from_json_value`) and `dump_to_gql_file` (emits an `INSERT`-per-node + `MATCH+INSERT`-per-edge script; uses a synthetic `_dump_id` property). Available via `.dump-json` / `.dump-gql` REPL meta-commands.
182
+ **Dump**: `store::dump::dump_to_json_file` (round-trips through `MemoryGraphStore::from_json_value`) and `dump_to_gql_file` (emits an `INSERT`-per-node + `MATCH+INSERT`-per-edge script; uses a synthetic `_dump_id` property). Available via `.dump-json` / `.dump-gql` REPL meta-commands.
172
183
 
173
184
  Tests: `parser_dm_test.rs`, `lazy_mut_test.rs`, `dm_runtime_test.rs`, `dm_persistence_test.rs`, `dm_schema_test.rs`, `dm_default_test.rs`, `dump_test.rs`, `dm_set_test.rs`, `dm_remove_test.rs`, `dm_label_test.rs`, `dm_delete_expr_test.rs`.
174
185
 
@@ -179,22 +190,32 @@ All node/edge IDs are `u32` internally (`pub type Id = u32` in `model/value.rs`)
179
190
  ### GraphAccess trait
180
191
 
181
192
  The runtime is generic over `GraphAccess`. Node and edge methods are separate: `node_labels(id)` / `edge_labels(id)`, `node_props(id)` / `edge_props(id)`. The runtime knows which to call from context (filtering nodes vs edges). Three backends:
182
- - `Graph` — in-memory from JSON, all data in RAM
193
+ - `MemoryGraphStore` — in-memory from JSON, all data in RAM. Full read + DML backend: implements `GraphAccess` and `GraphAccessMut` via the same `RefCell<MutationOverlay>` as `LazyGraphStore` (reads merge base + overlay; mutations stage in the overlay). Parity covered by `tests/memory_mut_test.rs`.
183
194
  - `LazyGraphStore` — topology (edge_src/tgt) + label index in RAM, labels/props read from disk via LRU page cache. No string names in memory.
184
195
  - `DiskGraphStore` — topology in RAM, everything else from disk
185
196
 
186
197
  ### Parser grammar hierarchy
187
198
 
188
199
  ```
189
- full_query = MATCH? query (WHERE expr)? (RETURN items)? (LIMIT INT)?
190
- query = path_pattern ("," path_pattern)* ← Join (lowest precedence)
200
+ full_query = MATCH? query (WHERE expr)? (RETURN items)? (GROUP BY ...)? (ORDER BY ...)? (LIMIT INT)?
201
+ query = operand ("," operand)* ← Join (lowest precedence)
202
+ operand = path_prefix? path_pattern ← Selected (ISO §16.6 path-pattern prefix)
191
203
  path_pattern = path_term ("|" path_term)* ← Union
192
- path_term = path_factor+ ← Concat (juxtaposition)
193
- path_factor = path_primary quantifier? ← Repeat {n,m}
204
+ path_term = path_factor+ ← Concat (juxtaposition)
205
+ path_factor = path_primary quantifier? ← Repeat {n,m} / `*` / `+` / `?`
194
206
  ```
195
207
 
196
208
  `MATCH` keyword is optional — bare path patterns like `(x)-[]->(y)` still work. `OPTIONAL MATCH` is supported as a top-level match clause. `is` and `IS` are aliases for the `typed`/`TYPED` type-predicate keyword; `IS NULL` / `IS NOT NULL` are dedicated null tests detected via lookahead before the type-predicate path. The `AS` keyword is ambiguous between type cast (in expressions) and alias (in RETURN); `return_comparison()` excludes `AS` from operators so it's available for aliases.
197
209
 
210
+ #### Path-pattern prefixes (ISO §16.6)
211
+
212
+ A `path_prefix` is parsed per comma operand (`parse_path_prefix` in `path_pattern_operand`), so it scopes to one `<path pattern>` and does not leak across a comma-join or union. A non-trivial prefix wraps its pattern in `PathPattern::Selected { prefix, pattern }`; the trivial `WALK ALL` is dropped (stored as a plain pattern) so the runtime skips the materialize-and-select pass. The prefix carries a `PathMode` (restrictive) and a `PathSearch` (selective), both in `src/syntax/path_prefix.rs`:
213
+
214
+ - **Path modes** (`WALK` default, `TRAIL`, `SIMPLE`, `ACYCLIC`) constrain which walks count: TRAIL forbids repeated edges, ACYCLIC forbids repeated nodes, SIMPLE forbids repeated nodes except a closing first==last cycle.
215
+ - **Path searches** (`ALL` default, `ANY [N]`, `SHORTEST [N] [PATHS]`, `SHORTEST N GROUPS`) pick a subset per `(first node, last node)` boundary partition. The surface forms `ANY SHORTEST` / `ALL SHORTEST` normalize to `SHORTEST 1 PATHS` / `SHORTEST 1 GROUPS` (ISO §16.6 SR 2c).
216
+
217
+ `ANY` lexes to its own `Token::Any` (so `ANY <pattern>` is distinct from a `*` type wildcard); in label position `(x:ANY)` it stays an alias for the `*` any-label wildcard (`label_primary` accepts both). `TRAIL/SIMPLE/ACYCLIC/SHORTEST/GROUPS/PATHS/WALK` are soft keywords, matched case-insensitively only in prefix position, so they remain usable as labels and variable names elsewhere. Tests in `tests/path_prefix_test.rs`.
218
+
198
219
  `LIMIT N` populates `Query.limit: Option<u32>`; the runtime combines it with any caller-supplied cap via `min` (smaller wins). `LIMIT 0` short-circuits to an empty binding table per ISO/IEC 39075:2024.
199
220
 
200
221
  #### Comments and operator aliases (ISO §3.10)
@@ -232,7 +253,7 @@ Primary strategy for joins and concatenations of directed/undirected edges. Wors
232
253
  **In-loop filters** (`FilterKind`): `NodeLabel`, `NodeProperty`, `NodeAttrCmp` (`=`, `!=`, `<`, `<=`, `>`, `>=`), `NodeInSet` (btree-resolved range). Placed at the VEO level where all dependencies are bound; pushed down by the optimizer from WHERE conjuncts.
233
254
 
234
255
  **Current limits**:
235
- 1. Repetitions `{n,m}`: unrolled by `optimizer::unroll_repeat` for bounded ranges with no named inner variables and single-edge inner. Other shapes (unbounded `{n,}`, named edge/node vars, range > `MAX_UNROLL = 4`) stay on the hash-join repetition path.
256
+ 1. Repetitions `{n,m}`: unrolled by `optimizer::unroll_repeat` for bounded ranges with no named inner variables and single-edge inner. Other bounded shapes (named edge/node vars, range > `MAX_UNROLL = 8`) stay on the hash-join repetition path. Unbounded repetition (`*`/`+`/`{n,}`) is not an LTJ shape; it requires a §16.6 prefix and runs through the dedicated finite searches (`run_repetition_shortest` / `run_repetition_unbounded_mode`, see *Path-pattern prefixes*).
236
257
  2. Any-direction edges (without tilde): not modelled as triples.
237
258
  3. WHERE: label and pushed value predicates run inside the loop; arbitrary WHERE post-filters. Var-vs-var predicates (`a <> b`) are not pushed into the LTJ filter set yet — they evaluate post-pattern via `PathPattern::Filter`.
238
259
  4. TripleIndex not persisted: cached on `Runtime` via `RefCell<Option<Arc<TripleIndex>>>`, built once per Runtime (eagerly at REPL/Connection open via `warm_triple_index()`).
@@ -244,7 +265,27 @@ When LTJ can't decompose, the pairwise hash-join takes over: both sides evaluate
244
265
 
245
266
  `-[x]->{n,m}` binds `x` to a `Group` of matched edges, not a single edge. `to_group()` wraps each value in a singleton group; `concat_group()` concatenates groups. Nested repetitions produce nested groups: `(-[x]->{1,2}){1,2}` gives `x ↦ [[e1], [e2, e3]]`. The zero-repetition base case fills variables with empty groups.
246
267
 
247
- `engine.rs::run_repetition_range` evaluates `{lb,ub}` in a single pass: the inner pattern runs once, the `first → indices` hash is built once, and every level `1..=ub` grows in a single `rows` buffer reusing the previous level's slice by index. Levels below `lb` get drained at the end via one `Vec::drain`. Replaces an earlier per-length loop that scaled as `O((ub-lb+1) × ub)` with `O(ub)`.
268
+ `engine.rs::run_repetition_range` evaluates bounded `{lb,ub}` in a single pass: the inner pattern runs once, the `first → indices` hash is built once, and every level `1..=ub` grows in a single `rows` buffer reusing the previous level's slice by index. Levels below `lb` get drained at the end via one `Vec::drain`. Replaces an earlier per-length loop that scaled as `O((ub-lb+1) × ub)` with `O(ub)`.
269
+
270
+ **Unbounded repetition** (`*`, `+`, `{n,}` with no upper bound) is infinite under plain `WALK ALL`, so the typechecker rejects it unless a §16.6 prefix makes it finite (see *Path-pattern prefixes* below). The `Repeat` arm of `run_path_pattern` dispatches on `Runtime::unbounded_policy` (a `Cell` set while evaluating a `Selected` operand): `Shortest { count, groups }` routes to `run_repetition_shortest`, `Mode(mode)` to `run_repetition_unbounded_mode`, and `Forbidden` panics (an invariant the typechecker guarantees, so it is only reachable via `compile_query_unchecked`). **The inner pattern must contribute ≥1 edge per application**: an empty-matching inner (e.g. a bare `(x)` node) under unbounded repetition is a hard typecheck error, since a zero-length lap never advances the length-ordered search and would loop forever (the bounded case stays a warning, since it terminates regardless).
271
+
272
+ ### Path-pattern prefixes (ISO §16.6): modes, search, unbounded repetition
273
+
274
+ `PathPattern::Selected { prefix, pattern }` is evaluated in `engine.rs::run_path_pattern` and `src/runtime/path_select.rs`. The inner pattern runs with no LIMIT (selection ranks the full candidate set), then `apply_path_prefix` filters by mode and reduces by search, and any caller LIMIT is applied afterward. Selection partitions rows by the `(first node id, last node id)` boundary key and acts per partition:
275
+
276
+ - **Mode filter** (`path_satisfies_mode`): drops rows whose path repeats an edge (TRAIL) or node (ACYCLIC; SIMPLE allows only a closing first==last).
277
+ - **`ANY N`** (`select_any`): keep up to `N` rows per partition in production order.
278
+ - **`SHORTEST N [PATHS]`** (`select_shortest_paths`): the `N` shortest rows per partition, stable-sorted by edge length (ties broken by production order).
279
+ - **`SHORTEST N GROUPS`** (`select_shortest_groups`): every row whose length is among the `N` shortest distinct lengths in its partition.
280
+
281
+ For **bounded** patterns, `apply_path_prefix` materializes all rows then selects. For **unbounded** repetition, a dedicated finite search avoids materializing the infinite walk set:
282
+
283
+ - `run_repetition_shortest` (WALK + SHORTEST) is a length-ordered k-shortest **walk** search: a `BinaryHeap` (min-heap on path length, monotone `seq` tie-break) expands paths in non-decreasing length, with a per-`(first,last)` budget that admits ≤`count` paths (PATHS) or ≤`count` distinct lengths (GROUPS) and prunes the rest. It terminates on cycles because per-pair lengths strictly grow, and pruning is sound by optimal substructure (the prefix of a k-shortest walk to a node is itself among the k-shortest walks to its predecessor — `first` is fixed along a concat chain, so the per-pair budget is the per-node k-shortest-walk budget).
284
+ - `run_repetition_unbounded_mode` (TRAIL / SIMPLE / ACYCLIC) enumerates with a worklist, pruning any partial path that already violates the mode; bounded by `|E|` (TRAIL) or `|V|` (SIMPLE/ACYCLIC). A restrictive mode takes precedence over a co-present search (`SHORTEST 2 TRAIL …*` enumerates TRAIL-valid paths, then `apply_path_prefix` reduces that finite set by `SHORTEST 2`).
285
+
286
+ **BFS fast-path for single-edge shortest** (`try_shortest_bfs`, runtime side, `engine.rs`). The walk-enumeration heap above grows like `b^d` and OOMs on social graphs, so the `Selected` arm first tries a node-dominated BFS that settles each node once (O(V+E) per source). It activates only for the canonical `(src) [single-edge]{lb,ub} (tgt)` shape under WALK + `SHORTEST 1 PATHS|GROUPS` (`ANY`/`ALL SHORTEST`), with `lb ≤ 1` and an unlabelled-or-single-label edge that binds no variable; any other shape returns `None` and falls back to `run_repetition_shortest`, so semantics never regress. It drives the BFS from the smaller endpoint set (reverse-walking the adjacency when driven from the target), reconstructs paths through a predecessor DAG (all minimum-length paths for `GROUPS`, one for `PATHS`), and reproduces the generic enumerator's coincident-endpoint behavior under `+`/`{1,…}` — the length-0 self path for `*`, and the trivial undirected closed walk (`n-e-m-e-n`, ub-gated) under a non-zero lower bound; a directed coincident pair defers to the generic path. `GQLITE_DISABLE_SHORTEST_BFS=1` forces it off (the `shortest_bfs_test` differential suite asserts BFS ≡ generic). Collapses LDBC IC1/IC13 from tens of seconds to ~20–30 ms and unblocks IC14's OOM (now ~0.7 s median, real results).
287
+
288
+ **Typechecker gates** (`src/typing/checker.rs`): `check_unbounded_repetition` rejects an unbounded repeat whose nearest enclosing prefix does not license it (`PathPrefix::unbounded_support`), and rejects `SHORTEST` over `{n,}` with `n ≥ 2` (only `*`/`+` are supported; use a restrictive mode for higher lower bounds). `check_selective_isolation` enforces ISO §16.6 SR 5–8: a selective pattern (any non-`ALL` search) may share only its boundary (endpoint) variables with the rest of the query, so it stays evaluable in isolation; sharing an interior variable is an error. A `Selected` wrapper does not change variable types (`check_path_pattern` recurses through it).
248
289
 
249
290
  ### EXISTS / NOT EXISTS
250
291
 
@@ -254,12 +295,44 @@ When LTJ can't decompose, the pairwise hash-join takes over: both sides evaluate
254
295
 
255
296
  **Optimisation** (`src/optimizer/existential.rs`): runs after the per-pattern pushdown passes. Walks every `Expr` reachable from `Query` (WHERE filters, GROUP BY, RETURN, recursive into nested existentials), runs the typechecker on each body against the active schema, and rewrites empty bodies to literals — `false` for `Exists`, `true` for `NotExists`. Catches shape-driven emptiness (a label or property the schema rejects). The pass does not thread outer-scope correlation into the body, so refinement-aware emptiness is left for a future pass; no literal-Boolean propagation either, so an inner-only fold inside an outer body does not collapse the outer.
256
297
 
257
- **Runtime** (`engine.rs::eval_exists`): two regimes share `Runtime::exists_cache: RefCell<HashMap<usize, ExistsCache>>` keyed by the body's heap address.
298
+ **Runtime** (`engine.rs::eval_exists`): three regimes share `Runtime::exists_cache: RefCell<HashMap<usize, ExistsCache>>` keyed by the body's heap address.
258
299
  - *Uncorrelated* (no shared variable with outer μ): `run_match_chain(body, limit=1)` once, cache the bool; subsequent rows reuse it.
259
- - *Correlated*: `run_match_chain(body, 0)` once (full body), project every row onto the correlation set (sorted variable names), store as `HashSet<Vec<PathValue>>`. Per outer row, build the probe key from μ and check membership semi-join for `EXISTS`, anti-join for `NOT EXISTS`. The body runs at most once per `Runtime` regardless of how many outer rows pass through.
300
+ - *Correlated — pinned* (`ExistsCache::CorrelatedPinned`, the default when the body is LTJ-pinnable): per outer row, collapse the body to one pattern and run it with the correlation variables pinned to that row's node ids via `try_ltj_with_pins` (`pinned_run_multi`, which unwraps the `Filter` carrying the body's WHERE so the predicate still runs), then memoise the non-emptiness verdict by correlation tuple. The body is evaluated once per *distinct correlation tuple*, never over the whole graph. This is the LDBC IC4 / IC10 anti-join shape (one person → a handful of friends): collapses the per-param IC4 cost from a fixed full-graph materialisation (~3 s on a slow host, the old floor) to a few targeted probes. `exists_body_pinned` returns `None` (→ fall back to materialise-once) when a correlation value is not a `Node`, the body is OPTIONAL/`Selected`/non-decomposable, **or the body contains an undirected edge** (`PathPattern::has_undirected_edge` pinning both endpoints of a `~[...]~` does not constrain the LTJ to that specific pair, so it would yield false positives; the undirected `~[:knows]~` in IC7's `isNew` takes this fallback). Force off with `GQLITE_DISABLE_EXISTS_PIN=1`.
301
+ - *Correlated — materialise-once* (`ExistsCache::Correlated`, fallback): `build_correlated_set` runs `run_match_chain(body, 0)` once (full body), projects every row onto the correlation set (sorted variable names), stores a `HashSet<Vec<PathValue>>`. Per outer row, build the probe key from μ and check membership — semi-join for `EXISTS`, anti-join for `NOT EXISTS`. Wins when the outer side binds many distinct correlation tuples (amortises the one scan); the pinned regime wins when it binds few.
260
302
 
261
303
  The four phases live in commits `4d13327` (parse + typecheck), `163ee30` (fold optimiser), `134890f` (uncorrelated runtime), `d5b4a45` (correlated runtime). Tests in `tests/parser_test.rs`, `tests/typecheck_test.rs`, `tests/exists_fold_test.rs`, `tests/exists_runtime_test.rs`. Formal type rules in `latex/extension/main.tex` (`\textsc{TExists}`, `\textsc{TNotExists}`, plus `\isEmpty(\matchseq) \equiv e \lor \mathsf{empty}(\Gamma')` and the rewrite rules `\textsc{ExistsEmpty}` / `\textsc{NotExistsEmpty}`).
262
304
 
305
+ ### Aggregates and RETURN arithmetic
306
+
307
+ `RETURN` items are `ReturnItem::Expr { expr, alias }` or `ReturnItem::Aggregate { agg, alias }`. Aggregates also compose **inside** value expressions via `Expr::Agg(Box<Aggregator>)`, so arithmetic over aggregate results works: `COUNT(DISTINCT x) + COUNT(DISTINCT y) AS total`. The parser recognises an aggregate call in `primary_expr` (so it can be a `Binop` operand); a *bare* top-level aggregate is re-folded back into `ReturnItem::Aggregate` so the existing aggregate-projection and ORDER BY-matching paths stay unchanged. `Expr::contains_agg()` classifies a RETURN expr as reduced-over-group (not a grouping key).
308
+
309
+ Runtime (`engine.rs::run_aggregated`): an aggregate-bearing RETURN expr is evaluated per group by `fold_aggs` — each `Expr::Agg` node is reduced over the group to an `Expr::Const`, then the residual arithmetic runs against the group's representative row (so non-aggregate leaves like `otherPerson.firstName` resolve to their grouped value). The typechecker types `Expr::Agg` as the reducer's result (`COUNT`→Int, `AVG`→Float, `SUM`→numeric, `MIN`/`MAX`→element type) and exempts aggregate-bearing exprs from the GROUP BY key check. This unblocked LDBC IC3 (`COUNT(DISTINCT messageX) + COUNT(DISTINCT messageY) AS totalCount`). Tests in `tests/count_test.rs`.
310
+
311
+ ### Value subqueries, RECORD, scalar builtins, GROUP BY by variable (IC7 cluster)
312
+
313
+ Six value-expression primitives added together to unblock LDBC IC7 (`bench/ldbc-queries/ic7.toml`, `tests/ic7_test.rs`):
314
+
315
+ - **Division `/`** — ISO `<solidus>`; `Token::Slash`, `BinOp::Div`, lexed past the `/* */` comment (consumed earlier in `skip_whitespace`). Runtime `eval_binop`: Int/Int truncates, mixed/Float widen, div-by-zero → `Failure` → Null (3VL).
316
+ - **`FLOOR` / `CAST`** — a generic `Expr::Call { name, args }` with dispatch in `engine.rs::eval_call`. `FLOOR(x)→Float`. `CAST(x AS INTEGER|FLOAT)` *converts* the value (the target rides as `Expr::Type` in `args[1]`); distinct from `BinOp::As`, which only type-*asserts*. Both are soft keywords (only before `(`). Parser parses the CAST operand with `return_comparison()` so the `AS` separator is not swallowed. This is the home for future builtins (CEIL/ABS/SIZE/…); the path functions (`ELEMENTS`/`PATH_LENGTH`/`CARDINALITY`) also live in this dispatch — see *Named paths and path functions*.
317
+ - **`RECORD { k: <expr>, ... }`** — `Expr::Record { fields: Vec<(String, Expr)> }` with expression values. `RECORD` keyword optional (soft, before `{`); shares the brace parser with the bare `{k: v}` literal via `parse_brace_record`, keeping the `:`-lookahead value-vs-type split and a const fast-path (all-`Const` fields fold to `Value::Record`). Types as `SimpleType::Record`; `FieldAccess` already resolves fields.
318
+ - **`VALUE { MATCH ... RETURN <1 item> ORDER BY ... LIMIT 1 }`** — `Expr::ValueSubquery { body: Box<Query> }`, a correlated scalar/record subquery (arg-max per group). Parser `parse_value_body` (allows RETURN/ORDER BY/LIMIT, rejects GROUP BY/DISTINCT, exactly one item). Typecheck reuses `check_subquery_body` then types the single RETURN item. Runtime `eval_value_subquery` has two regimes, same split as correlated EXISTS (`ValueSubqueryCache { map, pinned }`, keyed by body heap ptr, cleared per top-level `run_query`): **pinned** (default when pinnable, `value_subquery_pinned`) runs the body per outer row with the correlation vars pinned to that row's node ids (`pinned_run_multi`), projects + ORDER BY + LIMIT 1, and memoises the value by correlation tuple — body evaluated once per *distinct tuple*, never globally; **materialise-once** (fallback) runs the body once, buckets all rows by correlation key, projects each bucket. Pinned bails (→ materialise-once) on the same conditions as `exists_body_pinned` (non-Node correlation value, OPTIONAL/`Selected`, non-decomposable, undirected edge) plus a parameter correlation or a grouping/aggregate body (those need `run_aggregated`, not the plain arg-max projection). Force off with `GQLITE_DISABLE_VALUE_SUBQUERY_PIN=1`. This is IC7's dominant cost: its `VALUE { (person)<-[:hasCreator]-(m)<-[l:likes]-(liker) ... LIMIT 1 }` body, run uncorrelated, materialised the whole graph's like relation (109 k triples / param); pinned to `(person, liker)` it touches a handful — IC7 1.3 s → ~9 ms.
319
+ - **`GROUP BY <binding variable>`** — ISO `<grouping element> ::= <binding variable reference>`, so `GROUP BY liker, person` groups by node identity. `run_query` routes a GROUP-BY-without-aggregates query through `run_aggregated` (`needs_grouping = has_aggs || group_by.is_some()`). The typechecker's functional-dependency check (`check_returns_match_group_by`) accepts a non-aggregate projection when it structurally equals a key OR references only bare-`Expr::Var` grouping keys; subquery/aggregate-bearing items are exempt (evaluated on the group's representative row). **Grouping by a property** (`GROUP BY x.city`) only licenses the structural-match shape, not sibling attributes.
320
+ - **`ORDER BY <alias>.<field>`** — `SortKey::ColumnField { col, path }` (post-projection): walks a record-valued projected column. Needed because IC7 orders by `latestLike.likeCreationDate` where `latestLike` is a VALUE-subquery record column.
321
+
322
+ Latent fix made here: `eq_value` (the `GroupKey` equality backing grouping/DISTINCT) had no `Value::Node`/`Edge`/`Null` arms, so two equal nodes compared unequal — grouping by a node variable never collapsed. Now compares reference values by id. Elaboration recurses into subquery bodies (`elaborate_expr` over RETURN / ORDER BY / Filter exprs) so a subquery's own descriptor `value_filters` lower. Tests: `tests/{division,builtin_floor_cast,record_expr,group_by_var,value_subquery,ic7}_test.rs`. Remaining LDBC IC blockers + roadmap in `docs/internals/iso-gql-gaps.md`.
323
+
324
+ ### Named paths and path functions (ISO §16.6 + §20.16)
325
+
326
+ `MATCH p = (a)-[:knows]->(b)` binds a whole comma operand's matched path to a path variable, materialized as a `Value::Path` for the §20.16 path functions. Unblocks the shared prerequisite of LDBC IC1 / IC13 / IC14 (each still has one further gap: `COLLECT_LIST`, `CASE WHEN`, list comprehension).
327
+
328
+ - **AST**: `PathPattern::Named { var, pattern }` wraps one operand, *outside* any §16.6 prefix (`Named { var, Selected { prefix, pattern } }`). Parsed in `path_pattern_operand` by an ISO `<path variable declaration>` lookahead (`Name` `=` at the operand start — a comparison `=` never begins an operand). The wrapper is transparent everywhere it is walked (elaborate, optimizer pushdown, the typechecker's isolation/unbounded/var-count passes); it never appears below a Concat, so LTJ on a single operand still fires and only a comma-joined Named operand falls back to hash-join.
329
+ - **Value model**: `PathValue::Path(Vec<PathValue>)` (binding-table form, distinct from `Group` so it projects to `Value::Path`, not `Value::List`) and `Value::Path(Vec<Value>)` (projected form, alternating node/edge reference values in match order). Both wired into `path_value_to_value`, `hash_value`, `eq_value`. A path is never a property value — `value_to_prop` (store) errors on it.
330
+ - **Runtime** (`engine.rs::run_path_pattern` `Named` arm): evaluate the inner operand, then bind `var → PathValue::Path(row.path().0.clone())` per row. The path is captured from the `ResultRow.paths` the runtime already builds (including the LTJ and SHORTEST paths), not recomputed.
331
+ - **Path functions** (`eval_call`): `ELEMENTS` (all elements), `PATH_LENGTH` (edge count), `CARDINALITY` (node + edge count) are ISO §20.16. `NODES` / `EDGES` (node-only / edge-only projections) are **not** ISO — they are a documented translation divergence for the LDBC queries; mark them as such in the toml. All are soft keywords resolved by `path_function_name` (only `NAME(` is special, so `nodes`/`elements`/… stay usable as variables and labels).
332
+ - **Types**: terminal `SimpleType::Path` + `VariableType::Path` (only meets/subtypes itself, never refines against schema, `is_empty == false` so a path binding never empties an environment). The checker binds `var → VariableType::Path` in the `TypeEnvironment` and requires each path-function argument to type as `Path` — a provably non-path argument (e.g. a node variable) is a hard type error.
333
+
334
+ Tests in `tests/named_path_test.rs`. Full ISO context + roadmap in `docs/internals/iso-gql-gaps.md` §2.9.
335
+
263
336
  ### Null semantics
264
337
 
265
338
  `Value::Null` is a first-class variant. Properties that are absent from a node/edge map are treated as null at query time, and explicit nulls round-trip through the on-disk format.
@@ -313,6 +386,7 @@ Passes the runtime relies on (each one-line summary; flags + file refs are the o
313
386
  - **LTJ multi-way join + concat** — see *Join strategy* above.
314
387
  - **Type-predicate pushdown** (`is T` → descriptor `PropertyType`).
315
388
  - **Value-predicate pushdown** (`x.attr <op> literal` for `=`, `!=`, `<`, `<=`, `>`, `>=` → `value_preds` on node descriptor; emitted as `FilterKind::NodeAttrCmp`; nodes only).
389
+ - **Selected-path boundary pushdown** (`pushdown.rs`, ISO §16.6) — into a `Selected` pattern, a mode-only prefix (`PathSearch::All`, e.g. `ACYCLIC`) admits *all* constraints, but a selective prefix (`ANY`/`SHORTEST`) admits **only boundary (endpoint) variable** constraints; interior-node predicates stay as a post-selection `Filter`. Sound because selection partitions per `(source, target)` pair: restricting endpoints before the search equals filtering after, whereas filtering an interior node before would change which paths are shortest. `Constraints::retain_vars` keeps only the boundary vars; `walk_kinds` and `merge_constraints` carry the split.
316
390
  - **Index-driven constant folding** — `Eq` predicates that hit a hash index pre-bind the variable and drop it from VEO; empty hit short-circuits to zero rows. `pattern_extract::fold_indexed_constants`.
317
391
  - **Range index folding** — `<` / `<=` / `>` / `>=` predicates that hit a btree precompute the matching set and replace `NodeAttrCmp` with `FilterKind::NodeInSet`. `pattern_extract::fold_range_filters`.
318
392
  - **Repeat unrolling** (`unroll_repeat.rs`) — `(P){lb, ub}` → `Union(P^lb..P^ub)` for bounded ranges with single-edge inner and empty freevars; inserts anonymous `Node(None)` boundaries and distributes Union over Concat. `MAX_UNROLL = 8` (covers `{1,8}` and tighter). Fixed-length `lb == ub` short-circuits to a single flat concat with no Union envelope. `GQLITE_DISABLE_REPEAT_UNROLL=1`.
@@ -327,7 +401,7 @@ Passes the runtime relies on (each one-line summary; flags + file refs are the o
327
401
 
328
402
  DDL: `CREATE [HASH | BTREE] INDEX [<name>] ON :Label(prop) [USING HASH | BTREE]`, `DROP INDEX <name>`, `SHOW INDEXES` (or `.indexes`). Both prefix and suffix syntaxes work; HASH is the default kind. Re-declaring the same kind on the same `(label, prop)` is the only conflict.
329
403
 
330
- GraphAccess trait methods: `lookup_node_eq(label, prop, value) -> Option<Vec<Id>>`, `lookup_node_range(label, prop, lo, hi) -> Option<Vec<Id>>`, `lookup_node_ordered(label, prop, asc) -> Option<Vec<Id>>`. `Graph` (in-RAM JSON fixture) returns `None` from all and falls back to scan.
404
+ GraphAccess trait methods: `lookup_node_eq(label, prop, value) -> Option<Vec<Id>>`, `lookup_node_range(label, prop, lo, hi) -> Option<Vec<Id>>`, `lookup_node_ordered(label, prop, asc) -> Option<Vec<Id>>`. `MemoryGraphStore` (in-RAM JSON backend) returns `None` from all three and falls back to scan — it has no secondary index, so queries stay correct but unaccelerated.
331
405
 
332
406
  **Persistence (commit `2153319`).** Auto entries are memory-only (rebuilt every open, deterministic). DDL entries (`auto = false`) ARE persisted in the `.gdb` via `header.secondary_index_root` → chained `PageType::SecondaryIndex` pages → JSON-encoded `Vec<PersistedSpec>`. Save side: `save_graph_with_catalog_and_indexes_atomic` (`store/io.rs`). Load side: `LazyGraphStore::open` reads the list and replays each entry via `build_declared` after the auto-build. See `store/secondary_index_io.rs` and `docs/secondary-indexes.md`.
333
407
 
@@ -337,6 +411,25 @@ Diagnostic env vars: `GQLITE_DEBUG_INDEXES=1` (auto-built indexes + pinned varia
337
411
 
338
412
  LDBC IC2 on `bench/data/ldbc-sf0.1.gdb` (15 params × 3 iters, lazy backend, `--limit 20`): 2417 ms (no indexes) → 1377 ms (auto hash+btree) → **8.7 ms** (TripleIndex cached + warmed at open, 276× total). Reference: GraphQLite (SQLite + Cypher) measures 32.8 ms median on the same query.
339
413
 
414
+ ## Benchmarks
415
+
416
+ Two benches, deliberately split (full operational doc in `bench/cross-system/README.md`):
417
+
418
+ - **Internal bench** (`internal_bench` bin, `bench/INTERNAL_BENCHMARK.md`) — gqlite's own components in isolation (typechecker on/off, lazy/disk backend, RSS). Engine diagnostics, not cross-engine comparisons.
419
+ - **External / cross-system bench** (`bench/cross-system/`) — gqlite vs other graph databases on LDBC SNB Interactive Complex (IC) query latency. The headline numbers.
420
+
421
+ ### Cross-system harness (`bench/cross-system/`)
422
+
423
+ `run_all.sh` orchestrates **systems on the outer loop, ICs on the inner**: per system, set up once (load full LDBC SF0.1 into its native format), run each requested IC, then exit (per-system memory reclaimed at process exit). Output lands in `results/<timestamp>/` — per-(system,IC) CSV (`query;backend;params;row;iter;result_count;elapsed_ns`), `comparison.txt`, `setup_times.txt`, `run_info.txt`.
424
+
425
+ - **IC source-of-truth** is `bench/ldbc-queries/ic<n>.toml` — the canonical GQL the gqlite path runs directly via `ldbc_bench`. Each external system translates it to its own dialect file (`<system>/ic<n>.cypher` or `.gql`); per-system deviations live in `<system>/DIVERGENCES.md`. Only ICs with `status = "implemented"` run (currently IC2,5,6,8,9,11).
426
+ - **Row-equivalence oracle**: every runner sha256-hashes its iter-0 result and emits a `ROW … hash=<hex>` stderr line. `_lib/row_hash.py` **must byte-mirror** the `canonicalize_*` functions in `src/bin/ldbc_bench.rs` so all runners produce identical hashes for the same logical rows; `compare_results.py` cross-checks them. A mismatch is a real per-system translation bug, not noise — with ORDER BY in every toml the iter-0 result is deterministic, so byte-equal blobs ⇒ byte-equal results. This is what makes a cross-system latency comparison legitimate.
427
+ - **Measurement caveat** (quote it when quoting numbers): gqlite is benched through its Rust binary (`ldbc_bench`, no Python in the path); external systems through their Python wheels (~1–2 ms FFI per call). Each is measured via its primary user-facing interface, not normalized — latency is indicative, the row-equivalence check is the apples-to-apples part.
428
+
429
+ **Adding a system**: create `bench/cross-system/<sys>/` with `setup.py` (load the full LDBC SF0.1, IC-agnostic — counts must match gqlite's 327 588 nodes / 1 477 965 edges), `run.py` (emit the CSV schema + the ROW hash via `_lib/row_hash.py`), `requirements.txt`, `ic<n>.{cypher,gql}` translations, `README.md`, `DIVERGENCES.md`; then register it in `run_all.sh` (`ALL_SYSTEMS`, `SETUP_CMD`, `SETUP_MARKER`, `RUNNER`, `REBUILD_FLAG`) and add the `backend`→system mapping in `compare_results.py`'s `_normalize`. `kuzu/` is the closest template for an embedded Python engine; `grafeo/` is the GQL-native one (and shows the dialect carve-outs: no second top-level `MATCH`, no `(:A|B)` pattern alternation, `GROUP BY` by property-expression not alias, sub-labels via `type=` filter).
430
+
431
+ Run + chart: `bench_setup` (downloads LDBC SF0.1) → `install_python_deps.sh` → `bench/cross-system/run_all.sh --only gqlite,<sys> --ics 2,5,6,8,9,11` → `python bench/cross-system/plot_results.py` (median-per-IC grouped bars, PNG + SVG). A separate synthetic micro-bench (same-data, same-query, result-verified latency on a generated social graph) lives in `bench/grafeo-vs-frogql/`. `bench/data/` and `results/` are gitignored.
432
+
340
433
  ## Conventions
341
434
 
342
435
  - Labels in patterns require the `:` prefix: `-[:Transfer]->`, not `-[Transfer]->`.
@@ -59,6 +59,12 @@ dependencies = [
59
59
  "generic-array",
60
60
  ]
61
61
 
62
+ [[package]]
63
+ name = "bumpalo"
64
+ version = "3.20.3"
65
+ source = "registry+https://github.com/rust-lang/crates.io-index"
66
+ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
67
+
62
68
  [[package]]
63
69
  name = "cc"
64
70
  version = "1.2.61"
@@ -92,6 +98,16 @@ dependencies = [
92
98
  "error-code",
93
99
  ]
94
100
 
101
+ [[package]]
102
+ name = "console_error_panic_hook"
103
+ version = "0.1.7"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
106
+ dependencies = [
107
+ "cfg-if",
108
+ "wasm-bindgen",
109
+ ]
110
+
95
111
  [[package]]
96
112
  name = "convert_case"
97
113
  version = "0.6.0"
@@ -273,7 +289,7 @@ dependencies = [
273
289
 
274
290
  [[package]]
275
291
  name = "frogql-node"
276
- version = "0.2.2"
292
+ version = "0.2.4"
277
293
  dependencies = [
278
294
  "gqlrust",
279
295
  "napi",
@@ -284,12 +300,48 @@ dependencies = [
284
300
 
285
301
  [[package]]
286
302
  name = "frogql-py"
287
- version = "0.2.2"
303
+ version = "0.2.4"
288
304
  dependencies = [
289
305
  "gqlrust",
290
306
  "pyo3",
291
307
  ]
292
308
 
309
+ [[package]]
310
+ name = "frogql-wasm"
311
+ version = "0.2.4"
312
+ dependencies = [
313
+ "console_error_panic_hook",
314
+ "gqlrust",
315
+ "serde",
316
+ "serde-wasm-bindgen",
317
+ "serde_json",
318
+ "wasm-bindgen",
319
+ ]
320
+
321
+ [[package]]
322
+ name = "futures-core"
323
+ version = "0.3.32"
324
+ source = "registry+https://github.com/rust-lang/crates.io-index"
325
+ checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
326
+
327
+ [[package]]
328
+ name = "futures-task"
329
+ version = "0.3.32"
330
+ source = "registry+https://github.com/rust-lang/crates.io-index"
331
+ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
332
+
333
+ [[package]]
334
+ name = "futures-util"
335
+ version = "0.3.32"
336
+ source = "registry+https://github.com/rust-lang/crates.io-index"
337
+ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
338
+ dependencies = [
339
+ "futures-core",
340
+ "futures-task",
341
+ "pin-project-lite",
342
+ "slab",
343
+ ]
344
+
293
345
  [[package]]
294
346
  name = "generic-array"
295
347
  version = "0.14.7"
@@ -529,6 +581,18 @@ dependencies = [
529
581
  "libc",
530
582
  ]
531
583
 
584
+ [[package]]
585
+ name = "js-sys"
586
+ version = "0.3.99"
587
+ source = "registry+https://github.com/rust-lang/crates.io-index"
588
+ checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
589
+ dependencies = [
590
+ "cfg-if",
591
+ "futures-util",
592
+ "once_cell",
593
+ "wasm-bindgen",
594
+ ]
595
+
532
596
  [[package]]
533
597
  name = "leb128fmt"
534
598
  version = "0.1.0"
@@ -706,6 +770,12 @@ version = "2.3.2"
706
770
  source = "registry+https://github.com/rust-lang/crates.io-index"
707
771
  checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
708
772
 
773
+ [[package]]
774
+ name = "pin-project-lite"
775
+ version = "0.2.17"
776
+ source = "registry+https://github.com/rust-lang/crates.io-index"
777
+ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
778
+
709
779
  [[package]]
710
780
  name = "pkg-config"
711
781
  version = "0.3.33"
@@ -1094,6 +1164,17 @@ dependencies = [
1094
1164
  "serde_derive",
1095
1165
  ]
1096
1166
 
1167
+ [[package]]
1168
+ name = "serde-wasm-bindgen"
1169
+ version = "0.6.5"
1170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1171
+ checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
1172
+ dependencies = [
1173
+ "js-sys",
1174
+ "serde",
1175
+ "wasm-bindgen",
1176
+ ]
1177
+
1097
1178
  [[package]]
1098
1179
  name = "serde_core"
1099
1180
  version = "1.0.228"
@@ -1153,6 +1234,12 @@ version = "1.3.0"
1153
1234
  source = "registry+https://github.com/rust-lang/crates.io-index"
1154
1235
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1155
1236
 
1237
+ [[package]]
1238
+ name = "slab"
1239
+ version = "0.4.12"
1240
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1241
+ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1242
+
1156
1243
  [[package]]
1157
1244
  name = "smallvec"
1158
1245
  version = "1.15.1"
@@ -1435,6 +1522,51 @@ dependencies = [
1435
1522
  "wit-bindgen 0.51.0",
1436
1523
  ]
1437
1524
 
1525
+ [[package]]
1526
+ name = "wasm-bindgen"
1527
+ version = "0.2.122"
1528
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1529
+ checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
1530
+ dependencies = [
1531
+ "cfg-if",
1532
+ "once_cell",
1533
+ "rustversion",
1534
+ "wasm-bindgen-macro",
1535
+ "wasm-bindgen-shared",
1536
+ ]
1537
+
1538
+ [[package]]
1539
+ name = "wasm-bindgen-macro"
1540
+ version = "0.2.122"
1541
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1542
+ checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
1543
+ dependencies = [
1544
+ "quote",
1545
+ "wasm-bindgen-macro-support",
1546
+ ]
1547
+
1548
+ [[package]]
1549
+ name = "wasm-bindgen-macro-support"
1550
+ version = "0.2.122"
1551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1552
+ checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
1553
+ dependencies = [
1554
+ "bumpalo",
1555
+ "proc-macro2",
1556
+ "quote",
1557
+ "syn",
1558
+ "wasm-bindgen-shared",
1559
+ ]
1560
+
1561
+ [[package]]
1562
+ name = "wasm-bindgen-shared"
1563
+ version = "0.2.122"
1564
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1565
+ checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
1566
+ dependencies = [
1567
+ "unicode-ident",
1568
+ ]
1569
+
1438
1570
  [[package]]
1439
1571
  name = "wasm-encoder"
1440
1572
  version = "0.244.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frogql
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research