flowquery 1.0.64__tar.gz → 1.0.66__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 (200) hide show
  1. {flowquery-1.0.64 → flowquery-1.0.66}/PKG-INFO +142 -1
  2. {flowquery-1.0.64 → flowquery-1.0.66}/README.md +141 -0
  3. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/PKG-INFO +142 -1
  4. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/SOURCES.txt +5 -0
  5. {flowquery-1.0.64 → flowquery-1.0.66}/pyproject.toml +1 -1
  6. {flowquery-1.0.64 → flowquery-1.0.66}/src/__init__.py +40 -0
  7. {flowquery-1.0.64 → flowquery-1.0.66}/src/compute/flowquery.py +51 -1
  8. {flowquery-1.0.64 → flowquery-1.0.66}/src/compute/runner.py +101 -4
  9. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/data.py +5 -0
  10. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/data_cache.py +26 -5
  11. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/data_resolver.py +58 -2
  12. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/database.py +93 -1
  13. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/node.py +124 -3
  14. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/node_reference.py +3 -3
  15. flowquery-1.0.66/src/graph/pattern.py +399 -0
  16. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/pattern_expression.py +12 -4
  17. flowquery-1.0.66/src/graph/patterns.py +41 -0
  18. flowquery-1.0.66/src/graph/physical_node.py +85 -0
  19. flowquery-1.0.66/src/graph/physical_relationship.py +81 -0
  20. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/relationship.py +135 -2
  21. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/relationship_match_collector.py +34 -4
  22. flowquery-1.0.66/src/graph/source_access.py +700 -0
  23. flowquery-1.0.66/src/graph/source_context.py +473 -0
  24. flowquery-1.0.66/src/graph/source_provider.py +539 -0
  25. flowquery-1.0.66/src/graph/statement_source.py +70 -0
  26. flowquery-1.0.66/src/graph/virtual_statement.py +274 -0
  27. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/expression.py +28 -2
  28. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/aggregated_return.py +8 -0
  29. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/aggregated_with.py +8 -0
  30. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/create_node.py +18 -0
  31. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/create_relationship.py +24 -0
  32. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/limit.py +4 -0
  33. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/match.py +115 -7
  34. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/operation.py +7 -0
  35. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/return_op.py +30 -1
  36. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/unwind.py +6 -0
  37. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/where.py +4 -0
  38. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/with_op.py +4 -0
  39. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/parser.py +21 -2
  40. flowquery-1.0.64/src/graph/pattern.py +0 -116
  41. flowquery-1.0.64/src/graph/patterns.py +0 -34
  42. flowquery-1.0.64/src/graph/physical_node.py +0 -105
  43. flowquery-1.0.64/src/graph/physical_relationship.py +0 -97
  44. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/dependency_links.txt +0 -0
  45. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/entry_points.txt +0 -0
  46. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/requires.txt +0 -0
  47. {flowquery-1.0.64 → flowquery-1.0.66}/flowquery.egg-info/top_level.txt +0 -0
  48. {flowquery-1.0.64 → flowquery-1.0.66}/setup.cfg +0 -0
  49. {flowquery-1.0.64 → flowquery-1.0.66}/src/__main__.py +0 -0
  50. {flowquery-1.0.64 → flowquery-1.0.66}/src/compute/__init__.py +0 -0
  51. {flowquery-1.0.64 → flowquery-1.0.66}/src/compute/provenance.py +0 -0
  52. {flowquery-1.0.64 → flowquery-1.0.66}/src/extensibility.py +0 -0
  53. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/__init__.py +0 -0
  54. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/bindings.py +0 -0
  55. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/hops.py +0 -0
  56. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/node_data.py +0 -0
  57. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/relationship_data.py +0 -0
  58. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/relationship_reference.py +0 -0
  59. {flowquery-1.0.64 → flowquery-1.0.66}/src/graph/virtual_sources.py +0 -0
  60. {flowquery-1.0.64 → flowquery-1.0.66}/src/io/__init__.py +0 -0
  61. {flowquery-1.0.64 → flowquery-1.0.66}/src/io/command_line.py +0 -0
  62. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/__init__.py +0 -0
  63. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/alias.py +0 -0
  64. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/alias_option.py +0 -0
  65. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/ast_node.py +0 -0
  66. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/base_parser.py +0 -0
  67. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/__init__.py +0 -0
  68. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/csv.py +0 -0
  69. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/from_.py +0 -0
  70. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/headers.py +0 -0
  71. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/json.py +0 -0
  72. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/null.py +0 -0
  73. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/post.py +0 -0
  74. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/components/text.py +0 -0
  75. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/context.py +0 -0
  76. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/__init__.py +0 -0
  77. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/associative_array.py +0 -0
  78. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/json_array.py +0 -0
  79. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/key_value_pair.py +0 -0
  80. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/list_comprehension.py +0 -0
  81. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/lookup.py +0 -0
  82. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/data_structures/range_lookup.py +0 -0
  83. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/__init__.py +0 -0
  84. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/binding_reference.py +0 -0
  85. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/boolean.py +0 -0
  86. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/collect_subquery.py +0 -0
  87. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/count_subquery.py +0 -0
  88. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/exists_subquery.py +0 -0
  89. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/expression_map.py +0 -0
  90. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/f_string.py +0 -0
  91. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/identifier.py +0 -0
  92. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/number.py +0 -0
  93. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/operator.py +0 -0
  94. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/parameter_reference.py +0 -0
  95. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/reference.py +0 -0
  96. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/string.py +0 -0
  97. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/expressions/subquery_expression.py +0 -0
  98. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/__init__.py +0 -0
  99. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/aggregate_function.py +0 -0
  100. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/async_function.py +0 -0
  101. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/avg.py +0 -0
  102. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/coalesce.py +0 -0
  103. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/collect.py +0 -0
  104. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/count.py +0 -0
  105. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/date_.py +0 -0
  106. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/datetime_.py +0 -0
  107. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/duration.py +0 -0
  108. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/element_id.py +0 -0
  109. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/function.py +0 -0
  110. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/function_factory.py +0 -0
  111. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/function_metadata.py +0 -0
  112. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/functions.py +0 -0
  113. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/head.py +0 -0
  114. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/id_.py +0 -0
  115. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/join.py +0 -0
  116. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/keys.py +0 -0
  117. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/labels.py +0 -0
  118. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/last.py +0 -0
  119. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/length.py +0 -0
  120. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/localdatetime.py +0 -0
  121. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/localtime.py +0 -0
  122. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/log.py +0 -0
  123. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/log10.py +0 -0
  124. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/max_.py +0 -0
  125. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/min_.py +0 -0
  126. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/nodes.py +0 -0
  127. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/pow.py +0 -0
  128. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_all.py +0 -0
  129. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_any.py +0 -0
  130. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_function.py +0 -0
  131. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_none.py +0 -0
  132. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_single.py +0 -0
  133. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/predicate_sum.py +0 -0
  134. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/properties.py +0 -0
  135. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/rand.py +0 -0
  136. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/range_.py +0 -0
  137. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/reducer_element.py +0 -0
  138. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/relationships.py +0 -0
  139. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/replace.py +0 -0
  140. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/round_.py +0 -0
  141. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/schema.py +0 -0
  142. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/size.py +0 -0
  143. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/split.py +0 -0
  144. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/string_distance.py +0 -0
  145. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/stringify.py +0 -0
  146. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/substring.py +0 -0
  147. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/sum.py +0 -0
  148. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/tail.py +0 -0
  149. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/temporal_utils.py +0 -0
  150. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/time_.py +0 -0
  151. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/timestamp.py +0 -0
  152. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/to_float.py +0 -0
  153. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/to_integer.py +0 -0
  154. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/to_json.py +0 -0
  155. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/to_lower.py +0 -0
  156. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/to_string.py +0 -0
  157. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/trim.py +0 -0
  158. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/type_.py +0 -0
  159. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/functions/value_holder.py +0 -0
  160. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/__init__.py +0 -0
  161. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/case.py +0 -0
  162. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/else_.py +0 -0
  163. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/end.py +0 -0
  164. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/then.py +0 -0
  165. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/logic/when.py +0 -0
  166. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/__init__.py +0 -0
  167. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/call.py +0 -0
  168. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/delete_node.py +0 -0
  169. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/delete_relationship.py +0 -0
  170. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/drop_binding.py +0 -0
  171. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/group_by.py +0 -0
  172. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/let.py +0 -0
  173. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/load.py +0 -0
  174. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/merge.py +0 -0
  175. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/order_by.py +0 -0
  176. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/projection.py +0 -0
  177. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/refresh_binding.py +0 -0
  178. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/refresh_node.py +0 -0
  179. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/refresh_relationship.py +0 -0
  180. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/union.py +0 -0
  181. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/union_all.py +0 -0
  182. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/update.py +0 -0
  183. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/operations/update_delete.py +0 -0
  184. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/parser_state.py +0 -0
  185. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/statement_info_crawler.py +0 -0
  186. {flowquery-1.0.64 → flowquery-1.0.66}/src/parsing/token_to_node.py +0 -0
  187. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/__init__.py +0 -0
  188. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/keyword.py +0 -0
  189. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/operator.py +0 -0
  190. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/string_walker.py +0 -0
  191. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/symbol.py +0 -0
  192. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/token.py +0 -0
  193. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/token_mapper.py +0 -0
  194. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/token_type.py +0 -0
  195. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/tokenizer.py +0 -0
  196. {flowquery-1.0.64 → flowquery-1.0.66}/src/tokenization/trie.py +0 -0
  197. {flowquery-1.0.64 → flowquery-1.0.66}/src/utils/__init__.py +0 -0
  198. {flowquery-1.0.64 → flowquery-1.0.66}/src/utils/object_utils.py +0 -0
  199. {flowquery-1.0.64 → flowquery-1.0.66}/src/utils/string_utils.py +0 -0
  200. {flowquery-1.0.64 → flowquery-1.0.66}/tests/test_extensibility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowquery
3
- Version: 1.0.64
3
+ Version: 1.0.66
4
4
  Summary: A declarative query language for data processing pipelines
5
5
  Author-email: Team Microsoft FlowQuery <flowquery@microsoft.com>
6
6
  License: MIT
@@ -307,6 +307,147 @@ The same chain is reflected statically in `info.sources` so callers
307
307
  can introspect the upstream dependency graph without running the
308
308
  query.
309
309
 
310
+ ## Backend-Aware Virtual Sources
311
+
312
+ By default a virtual definition is loaded eagerly: its sub-query runs
313
+ once per query and all rows are held in memory. For large or remote
314
+ backends, a **source provider** lets FlowQuery read only what a pattern
315
+ needs. This is opt-in: existing `CREATE VIRTUAL` definitions behave
316
+ exactly as before.
317
+
318
+ The simplest way to write a provider is to subclass `NodeSource` or
319
+ `RelationshipSource` and implement `scan(request, context)`. Implement
320
+ `lookup` (nodes by id) or `expand` (relationships by `left_id`/`right_id`)
321
+ when the backend can fetch selectively; the base class then declares the
322
+ matching capability for you. `version` defaults to the current contract
323
+ version, and capability keys may be written in snake_case
324
+ (`max_batch_size`, `string_ordering`). A method may return a list of
325
+ rows, an awaitable list, or an (async) iterable of pages.
326
+
327
+ The `request` is a `SourceRequest`: a dict with the same camelCase keys
328
+ as the TypeScript implementation (`request["pageSize"]`) that also
329
+ accepts snake_case keys (`request["page_size"]`) and attribute access
330
+ (`request.ids`, `request.side`, `request.predicate`, `request.page_size`).
331
+
332
+ ```python
333
+ from flowquery import FlowQuery, NodeSource, RelationshipSource, cursor_pages
334
+
335
+ class People(NodeSource):
336
+ capabilities = {
337
+ "cacheable": True, # identical reads within one query may be cached
338
+ "max_batch_size": 50, # ids per lookup/expand request
339
+ "predicates": {"operators": ["eq", "in", "gt", "gte", "lt", "lte"]},
340
+ }
341
+
342
+ def scan(self, request, context):
343
+ async def fetch(cursor):
344
+ page = await api.people(filter=request.predicate, cursor=cursor)
345
+ return {"rows": page.rows, "cursor": page.next_cursor}
346
+ return cursor_pages(fetch)
347
+
348
+ async def lookup(self, request, context): # declares "lookup": True
349
+ return await api.people_by_ids(request.ids)
350
+
351
+ class Knows(RelationshipSource):
352
+ async def scan(self, request, context):
353
+ return await api.all_knows()
354
+
355
+ async def expand(self, request, context): # declares both expand sides
356
+ key = "from" if request.side == "left" else "to"
357
+ return await api.knows(**{key: request.ids})
358
+
359
+ FlowQuery.register_node_source("Person", People())
360
+ FlowQuery.register_relationship_source("KNOWS", "Person", "Person", Knows())
361
+
362
+ fq = FlowQuery("MATCH (a:Person {id: 7})-[:KNOWS]->(b:Person) RETURN b.name LIMIT 10")
363
+ await fq.run()
364
+ print(fq.source_stats) # calls, pages, rows, cacheHits, batches, plans...
365
+ ```
366
+
367
+ Subclassing is optional: any object with a `capabilities` dict and a
368
+ `scan` method works, and `SourceProvider` is a `typing.Protocol`
369
+ describing that shape.
370
+
371
+ The engine then loads correlated rows in batches instead of reading
372
+ every person and relationship:
373
+
374
+ - **Correlated loading and batching.** It issues id lookups, expands per
375
+ page of anchor nodes, and makes batched lookups for target nodes.
376
+ - **Selective start points.** A chain may start from its more selective
377
+ end. The decision is recorded in `source_stats["plans"]`, and it can
378
+ change result order when there is no `ORDER BY`.
379
+ - **Predicate push-down.** Pattern properties and `AND`-ed `WHERE`
380
+ comparisons (`=`, `<`, `<=`, `>`, `>=`, `IN`) against literals or
381
+ parameters are sent as `request["predicate"]`.
382
+ - **Streaming with early stop under `LIMIT`.** Reading stops once the
383
+ limit is reached and the provider's iterator is closed.
384
+ - **Query-scoped caching.**
385
+ - **Cancellation.** Call `runner.cancel()` or pass
386
+ `SourceOptions(signal=...)` (an `asyncio.Event` or `SourceSignal`).
387
+ The query raises `QueryCancelledError`.
388
+ - **Read contexts.** Optional `open_read_context()` and
389
+ `close_read_context(snapshot)` methods are called once per query.
390
+
391
+ Providers may return a **superset**, because FlowQuery re-checks every
392
+ row. `matches_predicate(predicate, row)` is a reference implementation
393
+ of the predicate semantics.
394
+
395
+ Errors are reported through typed exceptions:
396
+
397
+ - `SourceCapabilityError`: an invalid declaration, raised at
398
+ registration.
399
+ - `SourceResponseError`: malformed pages or a repeated cursor.
400
+ - `SourceError`: any other provider failure.
401
+
402
+ A provider may raise `SourceUnsupportedError` from `lookup`/`expand` to
403
+ fall back to a scan.
404
+
405
+ Query-level tuning goes in `RunnerOptions(sources=SourceOptions(...))`:
406
+ `page_size` (256), `max_batch_size` (100), `cache_capacity` (10000),
407
+ `max_prefetch_rows` (1024) and `reorder` (`True`).
408
+
409
+ **In-memory sources.** `in_memory_node_source(rows)` and
410
+ `in_memory_relationship_source(rows)` are ready-made providers over
411
+ lists. They support lookup, expansion from both sides, every predicate
412
+ operator and paging, which makes them handy for tests, examples and
413
+ small reference data. Keyword options are `cacheable` (default `True`)
414
+ and `max_batch_size`.
415
+
416
+ ```python
417
+ from flowquery import in_memory_node_source, in_memory_relationship_source
418
+
419
+ FlowQuery.register_node_source(
420
+ "Person", in_memory_node_source([{"id": 1, "name": "Ada"}, {"id": 2, "name": "Bob"}])
421
+ )
422
+ FlowQuery.register_relationship_source(
423
+ "KNOWS", "Person", "Person", in_memory_relationship_source([{"left_id": 1, "right_id": 2}])
424
+ )
425
+ ```
426
+
427
+ **Statement-backed sources.** A `CREATE VIRTUAL` definition can opt into
428
+ the same contract declaratively with a `WITH CAPABILITIES { ... }` clause
429
+ between the pattern and `AS`. Each request runs its sub-query with
430
+ `$request` bound to the request (`operation`, `label`/`type`, `ids`,
431
+ `side`, `predicate`, `pageSize`):
432
+
433
+ ```python
434
+ await FlowQuery("""
435
+ CREATE VIRTUAL (:Ticket) WITH CAPABILITIES {lookup: true, streaming: true} AS {
436
+ LOAD JSON FROM f"https://api.example.com/tickets?ids={join(coalesce($request.ids, []), ',')}" AS t
437
+ RETURN t.id AS id, t.title AS title
438
+ }
439
+ """).run()
440
+ ```
441
+
442
+ The map takes the same (camelCase) keys as the capabilities dict;
443
+ `version` defaults to the current contract version. Invalid capabilities
444
+ reject the whole definition. Redefining the label without
445
+ `WITH CAPABILITIES`, or deleting it, restores eager loading. The same
446
+ opt-in is available programmatically with
447
+ `FlowQuery.set_node_source_capabilities(label, capabilities)` and
448
+ `FlowQuery.set_relationship_source_capabilities(...)`; pass `None` to
449
+ opt out.
450
+
310
451
  ## Documentation
311
452
 
312
453
  - [Language Reference](https://github.com/microsoft/FlowQuery#language-reference) (clauses, expressions, functions, graph operations, and more)
@@ -274,6 +274,147 @@ The same chain is reflected statically in `info.sources` so callers
274
274
  can introspect the upstream dependency graph without running the
275
275
  query.
276
276
 
277
+ ## Backend-Aware Virtual Sources
278
+
279
+ By default a virtual definition is loaded eagerly: its sub-query runs
280
+ once per query and all rows are held in memory. For large or remote
281
+ backends, a **source provider** lets FlowQuery read only what a pattern
282
+ needs. This is opt-in: existing `CREATE VIRTUAL` definitions behave
283
+ exactly as before.
284
+
285
+ The simplest way to write a provider is to subclass `NodeSource` or
286
+ `RelationshipSource` and implement `scan(request, context)`. Implement
287
+ `lookup` (nodes by id) or `expand` (relationships by `left_id`/`right_id`)
288
+ when the backend can fetch selectively; the base class then declares the
289
+ matching capability for you. `version` defaults to the current contract
290
+ version, and capability keys may be written in snake_case
291
+ (`max_batch_size`, `string_ordering`). A method may return a list of
292
+ rows, an awaitable list, or an (async) iterable of pages.
293
+
294
+ The `request` is a `SourceRequest`: a dict with the same camelCase keys
295
+ as the TypeScript implementation (`request["pageSize"]`) that also
296
+ accepts snake_case keys (`request["page_size"]`) and attribute access
297
+ (`request.ids`, `request.side`, `request.predicate`, `request.page_size`).
298
+
299
+ ```python
300
+ from flowquery import FlowQuery, NodeSource, RelationshipSource, cursor_pages
301
+
302
+ class People(NodeSource):
303
+ capabilities = {
304
+ "cacheable": True, # identical reads within one query may be cached
305
+ "max_batch_size": 50, # ids per lookup/expand request
306
+ "predicates": {"operators": ["eq", "in", "gt", "gte", "lt", "lte"]},
307
+ }
308
+
309
+ def scan(self, request, context):
310
+ async def fetch(cursor):
311
+ page = await api.people(filter=request.predicate, cursor=cursor)
312
+ return {"rows": page.rows, "cursor": page.next_cursor}
313
+ return cursor_pages(fetch)
314
+
315
+ async def lookup(self, request, context): # declares "lookup": True
316
+ return await api.people_by_ids(request.ids)
317
+
318
+ class Knows(RelationshipSource):
319
+ async def scan(self, request, context):
320
+ return await api.all_knows()
321
+
322
+ async def expand(self, request, context): # declares both expand sides
323
+ key = "from" if request.side == "left" else "to"
324
+ return await api.knows(**{key: request.ids})
325
+
326
+ FlowQuery.register_node_source("Person", People())
327
+ FlowQuery.register_relationship_source("KNOWS", "Person", "Person", Knows())
328
+
329
+ fq = FlowQuery("MATCH (a:Person {id: 7})-[:KNOWS]->(b:Person) RETURN b.name LIMIT 10")
330
+ await fq.run()
331
+ print(fq.source_stats) # calls, pages, rows, cacheHits, batches, plans...
332
+ ```
333
+
334
+ Subclassing is optional: any object with a `capabilities` dict and a
335
+ `scan` method works, and `SourceProvider` is a `typing.Protocol`
336
+ describing that shape.
337
+
338
+ The engine then loads correlated rows in batches instead of reading
339
+ every person and relationship:
340
+
341
+ - **Correlated loading and batching.** It issues id lookups, expands per
342
+ page of anchor nodes, and makes batched lookups for target nodes.
343
+ - **Selective start points.** A chain may start from its more selective
344
+ end. The decision is recorded in `source_stats["plans"]`, and it can
345
+ change result order when there is no `ORDER BY`.
346
+ - **Predicate push-down.** Pattern properties and `AND`-ed `WHERE`
347
+ comparisons (`=`, `<`, `<=`, `>`, `>=`, `IN`) against literals or
348
+ parameters are sent as `request["predicate"]`.
349
+ - **Streaming with early stop under `LIMIT`.** Reading stops once the
350
+ limit is reached and the provider's iterator is closed.
351
+ - **Query-scoped caching.**
352
+ - **Cancellation.** Call `runner.cancel()` or pass
353
+ `SourceOptions(signal=...)` (an `asyncio.Event` or `SourceSignal`).
354
+ The query raises `QueryCancelledError`.
355
+ - **Read contexts.** Optional `open_read_context()` and
356
+ `close_read_context(snapshot)` methods are called once per query.
357
+
358
+ Providers may return a **superset**, because FlowQuery re-checks every
359
+ row. `matches_predicate(predicate, row)` is a reference implementation
360
+ of the predicate semantics.
361
+
362
+ Errors are reported through typed exceptions:
363
+
364
+ - `SourceCapabilityError`: an invalid declaration, raised at
365
+ registration.
366
+ - `SourceResponseError`: malformed pages or a repeated cursor.
367
+ - `SourceError`: any other provider failure.
368
+
369
+ A provider may raise `SourceUnsupportedError` from `lookup`/`expand` to
370
+ fall back to a scan.
371
+
372
+ Query-level tuning goes in `RunnerOptions(sources=SourceOptions(...))`:
373
+ `page_size` (256), `max_batch_size` (100), `cache_capacity` (10000),
374
+ `max_prefetch_rows` (1024) and `reorder` (`True`).
375
+
376
+ **In-memory sources.** `in_memory_node_source(rows)` and
377
+ `in_memory_relationship_source(rows)` are ready-made providers over
378
+ lists. They support lookup, expansion from both sides, every predicate
379
+ operator and paging, which makes them handy for tests, examples and
380
+ small reference data. Keyword options are `cacheable` (default `True`)
381
+ and `max_batch_size`.
382
+
383
+ ```python
384
+ from flowquery import in_memory_node_source, in_memory_relationship_source
385
+
386
+ FlowQuery.register_node_source(
387
+ "Person", in_memory_node_source([{"id": 1, "name": "Ada"}, {"id": 2, "name": "Bob"}])
388
+ )
389
+ FlowQuery.register_relationship_source(
390
+ "KNOWS", "Person", "Person", in_memory_relationship_source([{"left_id": 1, "right_id": 2}])
391
+ )
392
+ ```
393
+
394
+ **Statement-backed sources.** A `CREATE VIRTUAL` definition can opt into
395
+ the same contract declaratively with a `WITH CAPABILITIES { ... }` clause
396
+ between the pattern and `AS`. Each request runs its sub-query with
397
+ `$request` bound to the request (`operation`, `label`/`type`, `ids`,
398
+ `side`, `predicate`, `pageSize`):
399
+
400
+ ```python
401
+ await FlowQuery("""
402
+ CREATE VIRTUAL (:Ticket) WITH CAPABILITIES {lookup: true, streaming: true} AS {
403
+ LOAD JSON FROM f"https://api.example.com/tickets?ids={join(coalesce($request.ids, []), ',')}" AS t
404
+ RETURN t.id AS id, t.title AS title
405
+ }
406
+ """).run()
407
+ ```
408
+
409
+ The map takes the same (camelCase) keys as the capabilities dict;
410
+ `version` defaults to the current contract version. Invalid capabilities
411
+ reject the whole definition. Redefining the label without
412
+ `WITH CAPABILITIES`, or deleting it, restores eager loading. The same
413
+ opt-in is available programmatically with
414
+ `FlowQuery.set_node_source_capabilities(label, capabilities)` and
415
+ `FlowQuery.set_relationship_source_capabilities(...)`; pass `None` to
416
+ opt out.
417
+
277
418
  ## Documentation
278
419
 
279
420
  - [Language Reference](https://github.com/microsoft/FlowQuery#language-reference) (clauses, expressions, functions, graph operations, and more)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowquery
3
- Version: 1.0.64
3
+ Version: 1.0.66
4
4
  Summary: A declarative query language for data processing pipelines
5
5
  Author-email: Team Microsoft FlowQuery <flowquery@microsoft.com>
6
6
  License: MIT
@@ -307,6 +307,147 @@ The same chain is reflected statically in `info.sources` so callers
307
307
  can introspect the upstream dependency graph without running the
308
308
  query.
309
309
 
310
+ ## Backend-Aware Virtual Sources
311
+
312
+ By default a virtual definition is loaded eagerly: its sub-query runs
313
+ once per query and all rows are held in memory. For large or remote
314
+ backends, a **source provider** lets FlowQuery read only what a pattern
315
+ needs. This is opt-in: existing `CREATE VIRTUAL` definitions behave
316
+ exactly as before.
317
+
318
+ The simplest way to write a provider is to subclass `NodeSource` or
319
+ `RelationshipSource` and implement `scan(request, context)`. Implement
320
+ `lookup` (nodes by id) or `expand` (relationships by `left_id`/`right_id`)
321
+ when the backend can fetch selectively; the base class then declares the
322
+ matching capability for you. `version` defaults to the current contract
323
+ version, and capability keys may be written in snake_case
324
+ (`max_batch_size`, `string_ordering`). A method may return a list of
325
+ rows, an awaitable list, or an (async) iterable of pages.
326
+
327
+ The `request` is a `SourceRequest`: a dict with the same camelCase keys
328
+ as the TypeScript implementation (`request["pageSize"]`) that also
329
+ accepts snake_case keys (`request["page_size"]`) and attribute access
330
+ (`request.ids`, `request.side`, `request.predicate`, `request.page_size`).
331
+
332
+ ```python
333
+ from flowquery import FlowQuery, NodeSource, RelationshipSource, cursor_pages
334
+
335
+ class People(NodeSource):
336
+ capabilities = {
337
+ "cacheable": True, # identical reads within one query may be cached
338
+ "max_batch_size": 50, # ids per lookup/expand request
339
+ "predicates": {"operators": ["eq", "in", "gt", "gte", "lt", "lte"]},
340
+ }
341
+
342
+ def scan(self, request, context):
343
+ async def fetch(cursor):
344
+ page = await api.people(filter=request.predicate, cursor=cursor)
345
+ return {"rows": page.rows, "cursor": page.next_cursor}
346
+ return cursor_pages(fetch)
347
+
348
+ async def lookup(self, request, context): # declares "lookup": True
349
+ return await api.people_by_ids(request.ids)
350
+
351
+ class Knows(RelationshipSource):
352
+ async def scan(self, request, context):
353
+ return await api.all_knows()
354
+
355
+ async def expand(self, request, context): # declares both expand sides
356
+ key = "from" if request.side == "left" else "to"
357
+ return await api.knows(**{key: request.ids})
358
+
359
+ FlowQuery.register_node_source("Person", People())
360
+ FlowQuery.register_relationship_source("KNOWS", "Person", "Person", Knows())
361
+
362
+ fq = FlowQuery("MATCH (a:Person {id: 7})-[:KNOWS]->(b:Person) RETURN b.name LIMIT 10")
363
+ await fq.run()
364
+ print(fq.source_stats) # calls, pages, rows, cacheHits, batches, plans...
365
+ ```
366
+
367
+ Subclassing is optional: any object with a `capabilities` dict and a
368
+ `scan` method works, and `SourceProvider` is a `typing.Protocol`
369
+ describing that shape.
370
+
371
+ The engine then loads correlated rows in batches instead of reading
372
+ every person and relationship:
373
+
374
+ - **Correlated loading and batching.** It issues id lookups, expands per
375
+ page of anchor nodes, and makes batched lookups for target nodes.
376
+ - **Selective start points.** A chain may start from its more selective
377
+ end. The decision is recorded in `source_stats["plans"]`, and it can
378
+ change result order when there is no `ORDER BY`.
379
+ - **Predicate push-down.** Pattern properties and `AND`-ed `WHERE`
380
+ comparisons (`=`, `<`, `<=`, `>`, `>=`, `IN`) against literals or
381
+ parameters are sent as `request["predicate"]`.
382
+ - **Streaming with early stop under `LIMIT`.** Reading stops once the
383
+ limit is reached and the provider's iterator is closed.
384
+ - **Query-scoped caching.**
385
+ - **Cancellation.** Call `runner.cancel()` or pass
386
+ `SourceOptions(signal=...)` (an `asyncio.Event` or `SourceSignal`).
387
+ The query raises `QueryCancelledError`.
388
+ - **Read contexts.** Optional `open_read_context()` and
389
+ `close_read_context(snapshot)` methods are called once per query.
390
+
391
+ Providers may return a **superset**, because FlowQuery re-checks every
392
+ row. `matches_predicate(predicate, row)` is a reference implementation
393
+ of the predicate semantics.
394
+
395
+ Errors are reported through typed exceptions:
396
+
397
+ - `SourceCapabilityError`: an invalid declaration, raised at
398
+ registration.
399
+ - `SourceResponseError`: malformed pages or a repeated cursor.
400
+ - `SourceError`: any other provider failure.
401
+
402
+ A provider may raise `SourceUnsupportedError` from `lookup`/`expand` to
403
+ fall back to a scan.
404
+
405
+ Query-level tuning goes in `RunnerOptions(sources=SourceOptions(...))`:
406
+ `page_size` (256), `max_batch_size` (100), `cache_capacity` (10000),
407
+ `max_prefetch_rows` (1024) and `reorder` (`True`).
408
+
409
+ **In-memory sources.** `in_memory_node_source(rows)` and
410
+ `in_memory_relationship_source(rows)` are ready-made providers over
411
+ lists. They support lookup, expansion from both sides, every predicate
412
+ operator and paging, which makes them handy for tests, examples and
413
+ small reference data. Keyword options are `cacheable` (default `True`)
414
+ and `max_batch_size`.
415
+
416
+ ```python
417
+ from flowquery import in_memory_node_source, in_memory_relationship_source
418
+
419
+ FlowQuery.register_node_source(
420
+ "Person", in_memory_node_source([{"id": 1, "name": "Ada"}, {"id": 2, "name": "Bob"}])
421
+ )
422
+ FlowQuery.register_relationship_source(
423
+ "KNOWS", "Person", "Person", in_memory_relationship_source([{"left_id": 1, "right_id": 2}])
424
+ )
425
+ ```
426
+
427
+ **Statement-backed sources.** A `CREATE VIRTUAL` definition can opt into
428
+ the same contract declaratively with a `WITH CAPABILITIES { ... }` clause
429
+ between the pattern and `AS`. Each request runs its sub-query with
430
+ `$request` bound to the request (`operation`, `label`/`type`, `ids`,
431
+ `side`, `predicate`, `pageSize`):
432
+
433
+ ```python
434
+ await FlowQuery("""
435
+ CREATE VIRTUAL (:Ticket) WITH CAPABILITIES {lookup: true, streaming: true} AS {
436
+ LOAD JSON FROM f"https://api.example.com/tickets?ids={join(coalesce($request.ids, []), ',')}" AS t
437
+ RETURN t.id AS id, t.title AS title
438
+ }
439
+ """).run()
440
+ ```
441
+
442
+ The map takes the same (camelCase) keys as the capabilities dict;
443
+ `version` defaults to the current contract version. Invalid capabilities
444
+ reject the whole definition. Redefining the label without
445
+ `WITH CAPABILITIES`, or deleting it, restores eager loading. The same
446
+ opt-in is available programmatically with
447
+ `FlowQuery.set_node_source_capabilities(label, capabilities)` and
448
+ `FlowQuery.set_relationship_source_capabilities(...)`; pass `None` to
449
+ opt out.
450
+
310
451
  ## Documentation
311
452
 
312
453
  - [Language Reference](https://github.com/microsoft/FlowQuery#language-reference) (clauses, expressions, functions, graph operations, and more)
@@ -32,7 +32,12 @@ src/graph/relationship.py
32
32
  src/graph/relationship_data.py
33
33
  src/graph/relationship_match_collector.py
34
34
  src/graph/relationship_reference.py
35
+ src/graph/source_access.py
36
+ src/graph/source_context.py
37
+ src/graph/source_provider.py
38
+ src/graph/statement_source.py
35
39
  src/graph/virtual_sources.py
40
+ src/graph/virtual_statement.py
36
41
  src/io/__init__.py
37
42
  src/io/command_line.py
38
43
  src/parsing/__init__.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.64"
3
+ version = "1.0.66"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -24,6 +24,27 @@ from .compute.runner import (
24
24
  RunnerMetadata,
25
25
  RunnerOptions,
26
26
  )
27
+ from .graph.source_context import SourceOptions
28
+ from .graph.source_provider import (
29
+ PREDICATE_OPERATORS,
30
+ SOURCE_CONTRACT_VERSION,
31
+ NodeSource,
32
+ QueryCancelledError,
33
+ RelationshipSource,
34
+ SourceCapabilityError,
35
+ SourceError,
36
+ SourceProvider,
37
+ SourceReadContext,
38
+ SourceRequest,
39
+ SourceResponseError,
40
+ SourceResult,
41
+ SourceSignal,
42
+ SourceUnsupportedError,
43
+ cursor_pages,
44
+ in_memory_node_source,
45
+ in_memory_relationship_source,
46
+ matches_predicate,
47
+ )
27
48
  from .io.command_line import CommandLine
28
49
  from .parsing.functions.aggregate_function import AggregateFunction
29
50
  from .parsing.functions.async_function import AsyncFunction
@@ -80,5 +101,24 @@ __all__ = [
80
101
  "FunctionDef",
81
102
  "FunctionMetadata",
82
103
  "FunctionCategory",
104
+ "SOURCE_CONTRACT_VERSION",
105
+ "PREDICATE_OPERATORS",
106
+ "SourceOptions",
107
+ "SourceSignal",
108
+ "SourceReadContext",
109
+ "SourceError",
110
+ "SourceCapabilityError",
111
+ "SourceResponseError",
112
+ "SourceUnsupportedError",
113
+ "QueryCancelledError",
114
+ "SourceProvider",
115
+ "SourceRequest",
116
+ "SourceResult",
117
+ "NodeSource",
118
+ "RelationshipSource",
119
+ "cursor_pages",
120
+ "matches_predicate",
121
+ "in_memory_node_source",
122
+ "in_memory_relationship_source",
83
123
  ]
84
124
 
@@ -4,7 +4,7 @@ Extends Runner with extensibility features such as function listing
4
4
  and plugin registration, keeping the Runner focused on execution.
5
5
  """
6
6
 
7
- from typing import List, Optional, Type
7
+ from typing import Any, Dict, List, Optional, Type
8
8
 
9
9
  from ..parsing.functions.function import Function
10
10
  from ..parsing.functions.function_factory import FunctionFactory
@@ -66,3 +66,53 @@ class FlowQuery(Runner):
66
66
  Function metadata or None
67
67
  """
68
68
  return get_function_metadata(name.lower())
69
+
70
+ @staticmethod
71
+ def register_node_source(label: str, provider: Any) -> None:
72
+ """Register a backend-aware node source for ``label``.
73
+
74
+ Replaces any existing virtual definition for the label. The provider
75
+ exposes ``capabilities`` and ``scan`` (and optionally ``lookup``).
76
+ """
77
+ from ..graph.database import Database
78
+
79
+ Database.get_instance().register_node_source(label, provider)
80
+
81
+ @staticmethod
82
+ def register_relationship_source(
83
+ rel_type: str,
84
+ source_label: Optional[str],
85
+ target_label: Optional[str],
86
+ provider: Any,
87
+ ) -> None:
88
+ """Register a backend-aware relationship source for
89
+ ``(:source_label)-[:rel_type]->(:target_label)``."""
90
+ from ..graph.database import Database
91
+
92
+ Database.get_instance().register_relationship_source(
93
+ rel_type, source_label, target_label, provider
94
+ )
95
+
96
+ @staticmethod
97
+ def set_node_source_capabilities(
98
+ label: str, capabilities: Optional[Dict[str, Any]]
99
+ ) -> None:
100
+ """Opt a ``CREATE VIRTUAL (:label) AS { ... }`` definition into the
101
+ source contract with explicit capabilities, or pass ``None`` to opt out."""
102
+ from ..graph.database import Database
103
+
104
+ Database.get_instance().set_node_source_capabilities(label, capabilities)
105
+
106
+ @staticmethod
107
+ def set_relationship_source_capabilities(
108
+ rel_type: str,
109
+ source_label: Optional[str],
110
+ target_label: Optional[str],
111
+ capabilities: Optional[Dict[str, Any]],
112
+ ) -> None:
113
+ """Relationship counterpart of :meth:`set_node_source_capabilities`."""
114
+ from ..graph.database import Database
115
+
116
+ Database.get_instance().set_relationship_source_capabilities(
117
+ rel_type, source_label, target_label, capabilities
118
+ )