cypher-graphdb 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 (284) hide show
  1. {cypher_graphdb-0.2.2/src/cypher_graphdb.egg-info → cypher_graphdb-0.2.4}/PKG-INFO +2 -5
  2. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/pyproject.toml +3 -6
  3. cypher_graphdb-0.2.4/src/cypher_graphdb/backends/age/agebulkwriter.py +322 -0
  4. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/agegraphdb.py +71 -33
  5. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/agerowfactories.py +9 -8
  6. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/ageserializer.py +20 -12
  7. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/agesqlbuilder.py +98 -0
  8. cypher_graphdb-0.2.4/src/cypher_graphdb/backends/age/agtype.py +24 -0
  9. cypher_graphdb-0.2.4/src/cypher_graphdb/backends/age/agtype_parser.py +194 -0
  10. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/execute_cypher_command.py +7 -35
  11. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/__init__.py +2 -1
  12. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/column_utils.py +50 -1
  13. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4/src/cypher_graphdb.egg-info}/PKG-INFO +2 -5
  14. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb.egg-info/SOURCES.txt +3 -0
  15. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb.egg-info/requires.txt +1 -5
  16. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_indexes_and_bulk.py +148 -0
  17. cypher_graphdb-0.2.4/tests/unit/test_agtype_parser.py +156 -0
  18. cypher_graphdb-0.2.4/uv.lock +1716 -0
  19. cypher_graphdb-0.2.2/src/cypher_graphdb/backends/age/agtype.py +0 -27
  20. cypher_graphdb-0.2.2/uv.lock +0 -1563
  21. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.env.age.example +0 -0
  22. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.env.example +0 -0
  23. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.env.memgraph.example +0 -0
  24. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.github/workflows/ci.yml +0 -0
  25. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.github/workflows/publish.yml +0 -0
  26. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.github/workflows/release.yml +0 -0
  27. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.gitignore +0 -0
  28. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/.pre-commit-config.yaml +0 -0
  29. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/AGENTS.md +0 -0
  30. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/CHANGELOG.md +0 -0
  31. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/CONTRIBUTING.md +0 -0
  32. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/LICENSE.md +0 -0
  33. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/README.md +0 -0
  34. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/Taskfile.yml +0 -0
  35. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/cli +0 -0
  36. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/TODO.md +0 -0
  37. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/changelog.md +0 -0
  38. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/css/material.css +0 -0
  39. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/css/mkdocstrings.css +0 -0
  40. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/css/style.css +0 -0
  41. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/duckdb-migration.md +0 -0
  42. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/falkordb-integration.md +0 -0
  43. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/hierarchical-export-format.md +0 -0
  44. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/multiple-statement-execution.md +0 -0
  45. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/parameterized-queries-for-typed-models.md +0 -0
  46. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/query-result-immutable-design.md +0 -0
  47. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/read-only-mode.md +0 -0
  48. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/design/stateless-multi-graph-support.md +0 -0
  49. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/documentation-guide.md +0 -0
  50. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/examples/docstring_examples.py +0 -0
  51. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/index.md +0 -0
  52. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/backends/age/index.md +0 -0
  53. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/backends/index.md +0 -0
  54. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/cli/index.md +0 -0
  55. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/cypher/index.md +0 -0
  56. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/index.md +0 -0
  57. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/reference/cypher_graphdb/tools/index.md +0 -0
  58. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/docs/usage/index.md +0 -0
  59. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/mkdocs.yml +0 -0
  60. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/scripts/gen_ref_nav.py +0 -0
  61. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/setup.cfg +0 -0
  62. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/__init__.py +0 -0
  63. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/__main__.py +0 -0
  64. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/args.py +0 -0
  65. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backend.py +0 -0
  66. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backendprovider.py +0 -0
  67. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/__init__.py +0 -0
  68. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/__init__.py +0 -0
  69. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/age/agesearch.py +0 -0
  70. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/memgraph/__init__.py +0 -0
  71. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/memgraph/memgraphdb.py +0 -0
  72. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/backends/memgraph/memgraphrowfactories.py +0 -0
  73. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cardinality.py +0 -0
  74. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/__init__.py +0 -0
  75. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/app.py +0 -0
  76. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/banner.py +0 -0
  77. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/command_manager.py +0 -0
  78. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/command_map.py +0 -0
  79. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/command_registry.py +0 -0
  80. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/__init__.py +0 -0
  81. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/add_graph_command.py +0 -0
  82. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/apply_config_command.py +0 -0
  83. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/base_command.py +0 -0
  84. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/clear_graph_command.py +0 -0
  85. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/commit_command.py +0 -0
  86. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/connect_command.py +0 -0
  87. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/create_edge_command.py +0 -0
  88. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/create_graph_command.py +0 -0
  89. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/create_linked_node_command.py +0 -0
  90. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/create_node_command.py +0 -0
  91. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/delete_graphobj_command.py +0 -0
  92. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/disconnect_command.py +0 -0
  93. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/drop_graph_command.py +0 -0
  94. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_backends_command.py +0 -0
  95. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_graphs_command.py +0 -0
  96. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_indexes_command.py +0 -0
  97. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_labels_command.py +0 -0
  98. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_models_command.py +0 -0
  99. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_parsed_query_command.py +0 -0
  100. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_schema_command.py +0 -0
  101. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/dump_statistics_command.py +0 -0
  102. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/execute_file_command.py +0 -0
  103. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/exit_command.py +0 -0
  104. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/export_graph_command.py +0 -0
  105. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/fetch_all_command.py +0 -0
  106. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/fetch_edges_command.py +0 -0
  107. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/fetch_nodes_command.py +0 -0
  108. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/format_output_command.py +0 -0
  109. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/get_command.py +0 -0
  110. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/gid_command.py +0 -0
  111. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/graph_exists_command.py +0 -0
  112. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/graph_op_command.py +0 -0
  113. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/graph_to_tree_command.py +0 -0
  114. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/help_command.py +0 -0
  115. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/import_graph_command.py +0 -0
  116. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/last_result_op_command.py +0 -0
  117. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/load_models_command.py +0 -0
  118. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/resolve_edges_command.py +0 -0
  119. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/rollback_command.py +0 -0
  120. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/search_command.py +0 -0
  121. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/set_command.py +0 -0
  122. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/sql_command.py +0 -0
  123. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/update_graphobj_command.py +0 -0
  124. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/commands/use_graph_command.py +0 -0
  125. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/completer.py +0 -0
  126. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/config.py +0 -0
  127. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/exporter.py +0 -0
  128. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/file_executor.py +0 -0
  129. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/graphdata.py +0 -0
  130. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/graphdb.py +0 -0
  131. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/_overview.md +0 -0
  132. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/_template.md +0 -0
  133. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/add_graph.md +0 -0
  134. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/exit.md +0 -0
  135. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/export_graph.md +0 -0
  136. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/import_graph.md +0 -0
  137. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help/last_result.md +0 -0
  138. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/help.py +0 -0
  139. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/importer.py +0 -0
  140. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/prompt.py +0 -0
  141. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/promptparser.py +0 -0
  142. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/provider.py +0 -0
  143. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/renderer.py +0 -0
  144. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/runtime.py +0 -0
  145. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/schema_cmd.py +0 -0
  146. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cli/settings.py +0 -0
  147. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/command_reader.py +0 -0
  148. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/config.py +0 -0
  149. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/Cypher.interp +0 -0
  150. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/Cypher.tokens +0 -0
  151. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherBaseListener.java +0 -0
  152. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherLexer.interp +0 -0
  153. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherLexer.java +0 -0
  154. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherLexer.tokens +0 -0
  155. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherListener.java +0 -0
  156. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/.antlr/CypherParser.java +0 -0
  157. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/Cypher.g4 +0 -0
  158. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/Cypher.interp +0 -0
  159. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/Cypher.tokens +0 -0
  160. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/CypherLexer.interp +0 -0
  161. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/CypherLexer.py +0 -0
  162. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/CypherLexer.tokens +0 -0
  163. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/CypherListener.py +0 -0
  164. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/CypherParser.py +0 -0
  165. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypher/__init__.py +0 -0
  166. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypherbuilder.py +0 -0
  167. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/__init__.py +0 -0
  168. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/batch.py +0 -0
  169. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/bulk_normalize.py +0 -0
  170. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/connection.py +0 -0
  171. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/criteria.py +0 -0
  172. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/cyphergraphdb.py +0 -0
  173. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/indexing.py +0 -0
  174. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/result.py +0 -0
  175. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/schema.py +0 -0
  176. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/search.py +0 -0
  177. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/sql.py +0 -0
  178. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cyphergraphdb/stream_mixin.py +0 -0
  179. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypherjson.py +0 -0
  180. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/cypherparser.py +0 -0
  181. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/dbpool.py +0 -0
  182. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/decorators.py +0 -0
  183. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/display.py +0 -0
  184. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/exceptions.py +0 -0
  185. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/graphops.py +0 -0
  186. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/main.py +0 -0
  187. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/modelinfo.py +0 -0
  188. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/modelprovider.py +0 -0
  189. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/models.py +0 -0
  190. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/options.py +0 -0
  191. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/schema/__init__.py +0 -0
  192. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/schema/converter.py +0 -0
  193. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/schema/core.py +0 -0
  194. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/schema/generator.py +0 -0
  195. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/settings.py +0 -0
  196. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/statistics.py +0 -0
  197. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/__init__.py +0 -0
  198. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/base_exporter.py +0 -0
  199. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/base_importer.py +0 -0
  200. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/csv_exporter.py +0 -0
  201. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/csv_importer.py +0 -0
  202. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/csv_source.py +0 -0
  203. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/data_flattener.py +0 -0
  204. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/excel_exporter.py +0 -0
  205. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/excel_importer.py +0 -0
  206. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/excel_row_source.py +0 -0
  207. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/file_exporter.py +0 -0
  208. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/file_importer.py +0 -0
  209. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/hierarchical_exporter.py +0 -0
  210. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/hierarchical_importer.py +0 -0
  211. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/hierarchical_row_source.py +0 -0
  212. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/json_importer.py +0 -0
  213. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/json_yaml_data_source.py +0 -0
  214. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/row_collector.py +0 -0
  215. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/row_set.py +0 -0
  216. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/row_source.py +0 -0
  217. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/tabular_importer.py +0 -0
  218. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/tools/yaml_importer.py +0 -0
  219. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/collection_utils.py +0 -0
  220. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/connection_utils.py +0 -0
  221. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/conversion_utils.py +0 -0
  222. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/core_utils.py +0 -0
  223. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/schema_merge.py +0 -0
  224. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/schema_to_llm.py +0 -0
  225. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/schema_utils.py +0 -0
  226. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/settings_repr.py +0 -0
  227. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb/utils/string_utils.py +0 -0
  228. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb.egg-info/dependency_links.txt +0 -0
  229. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb.egg-info/entry_points.txt +0 -0
  230. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/src/cypher_graphdb.egg-info/top_level.txt +0 -0
  231. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/README.md +0 -0
  232. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/conftest.py +0 -0
  233. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/__init__.py +0 -0
  234. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/conftest.py +0 -0
  235. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_basic_operations.py +0 -0
  236. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_create_or_merge.py +0 -0
  237. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_example.py +0 -0
  238. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_parameters.py +0 -0
  239. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_read_only_mode.py +0 -0
  240. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/integration/test_streaming.py +0 -0
  241. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/test_json_schema_loading.py +0 -0
  242. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/test_json_schema_vs_decorators.py +0 -0
  243. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/README.md +0 -0
  244. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/__init__.py +0 -0
  245. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/cli/__init__.py +0 -0
  246. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/cli/test_cmd_map.py +0 -0
  247. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/cli/test_command_registry.py +0 -0
  248. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/mock_backend.py +0 -0
  249. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_age_serializer.py +0 -0
  250. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_age_sqlbuilder.py +0 -0
  251. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_backend_capabilities.py +0 -0
  252. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_bulk_normalize.py +0 -0
  253. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_column_utils.py +0 -0
  254. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_command_reader.py +0 -0
  255. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_cypherbuilder.py +0 -0
  256. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_cypherparser.py +0 -0
  257. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_dbpool.py +0 -0
  258. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_dict_access_mixin.py +0 -0
  259. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_extend_relation_decorator.py +0 -0
  260. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_extend_relations.py +0 -0
  261. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_graph_id_zero.py +0 -0
  262. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_graphops.py +0 -0
  263. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_indexing.py +0 -0
  264. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_model_inheritance.py +0 -0
  265. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_modelinfo.py +0 -0
  266. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_modelprovider_loading.py +0 -0
  267. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_modelprovider_schemas.py +0 -0
  268. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_models.py +0 -0
  269. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/test_schema_converter.py +0 -0
  270. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/__init__.py +0 -0
  271. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_data_flattener.py +0 -0
  272. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_exporters.py +0 -0
  273. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_hierarchical_importer.py +0 -0
  274. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_json_yaml_data_source.py +0 -0
  275. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_resource_management.py +0 -0
  276. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/tools/test_tabular_import.py +0 -0
  277. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_collection_utils.py +0 -0
  278. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_connection_utils.py +0 -0
  279. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_conversion_utils.py +0 -0
  280. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_core_utils.py +0 -0
  281. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_schema_merge.py +0 -0
  282. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_schema_utils.py +0 -0
  283. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_settings_repr.py +0 -0
  284. {cypher_graphdb-0.2.2 → cypher_graphdb-0.2.4}/tests/unit/utils/test_string_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cypher_graphdb
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: ORM like library and CLI for cypher query language supporting graph databases.
5
5
  Author-email: Wolfgang Miller <wolfgang.miller@petrarca-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ Requires-Dist: art
15
15
  Requires-Dist: python-dotenv
16
16
  Requires-Dist: prompt-toolkit
17
17
  Requires-Dist: rich
18
- Requires-Dist: apache-age-python
18
+ Requires-Dist: antlr4-python3-runtime==4.11.1
19
19
  Requires-Dist: openpyxl
20
20
  Requires-Dist: numpy
21
21
  Requires-Dist: pydantic
@@ -57,9 +57,6 @@ Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == "docs"
57
57
  Requires-Dist: pymdown-extensions; extra == "docs"
58
58
  Requires-Dist: termynal; extra == "docs"
59
59
  Requires-Dist: black; extra == "docs"
60
- Provides-Extra: examples
61
- Requires-Dist: fastapi; extra == "examples"
62
- Requires-Dist: uvicorn; extra == "examples"
63
60
  Dynamic: license-file
64
61
 
65
62
  # CypherGraphDB Library
@@ -23,7 +23,7 @@ dependencies = [
23
23
  "python-dotenv",
24
24
  "prompt-toolkit",
25
25
  "rich",
26
- "apache-age-python",
26
+ "antlr4-python3-runtime==4.11.1",
27
27
  "openpyxl",
28
28
  "numpy",
29
29
  "pydantic",
@@ -76,13 +76,10 @@ docs = [
76
76
  "black",
77
77
  ]
78
78
 
79
- examples = [
80
- "fastapi",
81
- "uvicorn",
82
- ]
79
+
83
80
 
84
81
  [tool.uv.sources]
85
- apache-age-python = { git = "https://github.com/apache/age.git", tag = "PG18/v1.7.0-rc0", subdirectory = "drivers/python" }
82
+
86
83
 
87
84
  [project.scripts]
88
85
  cgdb-cli = "cypher_graphdb.args:typer_main"
@@ -0,0 +1,322 @@
1
+ """Direct SQL bulk writer for Apache AGE label tables.
2
+
3
+ Bypasses the Cypher parser by INSERTing directly into AGE's PostgreSQL label
4
+ tables using _graphid() + nextval() for ID generation and ::agtype for
5
+ properties. This is the same pattern AGE's own test suite uses
6
+ (regress/sql/age_global_graph.sql) and is 10-30x faster than Cypher UNWIND
7
+ for large bulk loads.
8
+
9
+ The writer is stateless: it receives a connection and graph_name, performs
10
+ the INSERT, and returns. Transaction control (commit/rollback) is the
11
+ caller's responsibility, with one exception: when a label does not yet exist,
12
+ ``_ensure_label`` creates it via DDL inside a savepoint so that the DDL is
13
+ committed immediately (DDL cannot run inside a user transaction in PostgreSQL)
14
+ without affecting any surrounding data already written by the caller.
15
+ """
16
+
17
+ from typing import Literal
18
+
19
+ import psycopg
20
+ from loguru import logger
21
+ from psycopg.sql import SQL, Identifier
22
+
23
+ from cypher_graphdb.utils import chunk_list
24
+
25
+ from .ageserializer import escape_value
26
+ from .agesqlbuilder import SQLBuilder
27
+
28
+
29
+ class AGEBulkWriter:
30
+ """Direct SQL bulk writer for AGE label tables.
31
+
32
+ Args:
33
+ connection: An open psycopg connection to the AGE database.
34
+ graph_name: The AGE graph name (= PostgreSQL schema name).
35
+ """
36
+
37
+ def __init__(self, connection: psycopg.Connection, graph_name: str) -> None:
38
+ self._conn = connection
39
+ self._graph_name = graph_name
40
+ # Cache for {(label, ref_prop): {ref_value: graphid}} maps.
41
+ # Avoids repeated full-table scans when bulk_insert_edges is called
42
+ # many times with the same src/dst labels (e.g. 400+ batches of CALLS
43
+ # edges all resolving against Method.qualified_name).
44
+ # Invalidated by invalidate_graphid_cache() after node inserts that
45
+ # change the graphid mapping.
46
+ self._graphid_cache: dict[tuple[str, str], dict[str, int]] = {}
47
+
48
+ # -- Public API -----------------------------------------------------------
49
+
50
+ def bulk_insert_nodes(self, label: str, rows: list[dict], batch_size: int = 200) -> int:
51
+ """Insert nodes via direct SQL INSERT into the AGE label table.
52
+
53
+ Creates the vlabel if it doesn't exist yet (AGE normally does this
54
+ lazily on first Cypher CREATE).
55
+
56
+ Uses multi-row INSERT with inline agtype literals (not parameterized)
57
+ because AGE's agtype_in text-input function does not interpret JSON
58
+ escape sequences correctly when received as psycopg text parameters.
59
+ The agtype values are built using the same escape_value function as
60
+ the Cypher UNWIND path, ensuring identical storage.
61
+
62
+ Args:
63
+ label: Vertex label name (e.g. "Method").
64
+ rows: List of property dicts, one per node.
65
+ batch_size: Number of rows per multi-row INSERT.
66
+
67
+ Returns:
68
+ Total number of nodes inserted.
69
+ """
70
+ if not rows:
71
+ return 0
72
+
73
+ label_id = self._ensure_label(label, kind="v")
74
+
75
+ total = 0
76
+ with self._conn.cursor() as cursor:
77
+ for batch in chunk_list(rows, batch_size):
78
+ values = ", ".join(
79
+ f"(ag_catalog._graphid({label_id}, nextval('{self._seq_name(label)}'::regclass)), "
80
+ f"{self._to_agtype_literal(row)}::ag_catalog.agtype)"
81
+ for row in batch
82
+ )
83
+ cursor.execute(
84
+ SQL("INSERT INTO {schema}.{table} (id, properties) VALUES {vals}").format(
85
+ schema=Identifier(self._graph_name),
86
+ table=Identifier(label),
87
+ vals=SQL(values),
88
+ )
89
+ )
90
+ total += len(batch)
91
+
92
+ logger.debug("Direct SQL: {} {} nodes inserted", total, label)
93
+ # Invalidate cached graphid maps for this label so subsequent edge
94
+ # inserts see the newly created nodes.
95
+ self.invalidate_graphid_cache(label)
96
+ return total
97
+
98
+ def bulk_insert_edges(
99
+ self,
100
+ label: str,
101
+ edges: list[dict],
102
+ src_label: str,
103
+ dst_label: str,
104
+ src_ref_prop: str = "id",
105
+ dst_ref_prop: str = "id",
106
+ batch_size: int = 500,
107
+ ) -> int:
108
+ """Insert edges via direct SQL INSERT into the AGE edge label table.
109
+
110
+ Pre-resolves source and destination graphids via a single SQL query
111
+ per label, then INSERTs edges with known start_id/end_id -- no Cypher
112
+ MATCH at write time.
113
+
114
+ Edges whose src or dst reference cannot be resolved are silently
115
+ skipped (same behaviour as Cypher MATCH -- unmatched patterns produce
116
+ no rows).
117
+
118
+ Args:
119
+ label: Edge label name (e.g. "CALLS").
120
+ edges: List of dicts with at least "src" and "dst" keys.
121
+ src_label: Vertex label of source nodes.
122
+ dst_label: Vertex label of destination nodes.
123
+ src_ref_prop: Property name on source nodes to match "src" values.
124
+ dst_ref_prop: Property name on destination nodes to match "dst" values.
125
+ batch_size: Number of rows per executemany batch.
126
+
127
+ Returns:
128
+ Total number of edges inserted (excludes skipped).
129
+ """
130
+ if not edges:
131
+ return 0
132
+
133
+ label_id = self._ensure_label(label, kind="e")
134
+
135
+ # Collect the unique ref values we actually need -- avoids loading
136
+ # the entire label table (which can be 200K+ rows in a shared graph).
137
+ src_refs = {e["src"] for e in edges}
138
+ dst_refs = {e["dst"] for e in edges}
139
+
140
+ # Build {ref_value: graphid} maps for only the referenced nodes
141
+ src_map = self._build_graphid_index(src_label, src_ref_prop, ref_values=src_refs)
142
+ if src_label == dst_label and src_ref_prop == dst_ref_prop:
143
+ dst_map = self._build_graphid_index(dst_label, dst_ref_prop, ref_values=src_refs | dst_refs)
144
+ else:
145
+ dst_map = self._build_graphid_index(dst_label, dst_ref_prop, ref_values=dst_refs)
146
+
147
+ total = 0
148
+ skipped = 0
149
+ with self._conn.cursor() as cursor:
150
+ for batch in chunk_list(edges, batch_size):
151
+ value_parts = []
152
+ for edge in batch:
153
+ src_gid = src_map.get(edge["src"])
154
+ dst_gid = dst_map.get(edge["dst"])
155
+ if src_gid is None or dst_gid is None:
156
+ skipped += 1
157
+ continue
158
+ props = {k: v for k, v in edge.items() if k not in ("src", "dst")}
159
+ value_parts.append(
160
+ f"(ag_catalog._graphid({label_id}, nextval('{self._seq_name(label)}'::regclass)), "
161
+ f"'{src_gid}'::ag_catalog.graphid, '{dst_gid}'::ag_catalog.graphid, "
162
+ f"{self._to_agtype_literal(props)}::ag_catalog.agtype)"
163
+ )
164
+ if value_parts:
165
+ values = ", ".join(value_parts)
166
+ cursor.execute(
167
+ SQL("INSERT INTO {schema}.{table} (id, start_id, end_id, properties) VALUES {vals}").format(
168
+ schema=Identifier(self._graph_name),
169
+ table=Identifier(label),
170
+ vals=SQL(values),
171
+ )
172
+ )
173
+ total += len(value_parts)
174
+
175
+ if skipped:
176
+ logger.debug("Direct SQL: {} {} edges inserted, {} skipped (unresolved endpoints)", total, label, skipped)
177
+ else:
178
+ logger.debug("Direct SQL: {} {} edges inserted", total, label)
179
+ return total
180
+
181
+ # -- Internal helpers -----------------------------------------------------
182
+
183
+ def _ensure_label(self, label: str, kind: Literal["v", "e"] = "v") -> int:
184
+ """Look up the numeric label_id, creating the label if needed.
185
+
186
+ When the label does not exist, creates it via a savepoint so that the
187
+ DDL commit is isolated. PostgreSQL DDL (CREATE TABLE, which AGE's
188
+ create_vlabel/create_elabel issue internally) cannot run inside an open
189
+ transaction; committing via savepoint releases the DDL without
190
+ discarding any data already written by the caller in the outer
191
+ transaction.
192
+
193
+ Args:
194
+ label: Label name (e.g. "Method", "CALLS").
195
+ kind: ``"v"`` for vertex label, ``"e"`` for edge label.
196
+
197
+ Returns:
198
+ The integer label_id for the label.
199
+
200
+ Raises:
201
+ ValueError: If the label cannot be created or looked up.
202
+ """
203
+ lookup_sql = SQLBuilder.lookup_label_id_sql()
204
+ with self._conn.cursor() as cursor:
205
+ cursor.execute(lookup_sql, (self._graph_name, label))
206
+ row = cursor.fetchone()
207
+ if row:
208
+ return row[0]
209
+
210
+ # Label doesn't exist -- create it inside a savepoint so that the
211
+ # DDL commit does not affect the caller's surrounding transaction.
212
+ cursor.execute("SAVEPOINT _age_ensure_label")
213
+ try:
214
+ if kind == "v":
215
+ cursor.execute(SQLBuilder.create_vlabel(self._graph_name, label))
216
+ else:
217
+ cursor.execute(SQLBuilder.create_elabel(self._graph_name, label))
218
+ cursor.execute("RELEASE SAVEPOINT _age_ensure_label")
219
+ self._conn.commit()
220
+ except Exception:
221
+ cursor.execute("ROLLBACK TO SAVEPOINT _age_ensure_label")
222
+ raise
223
+
224
+ cursor.execute(lookup_sql, (self._graph_name, label))
225
+ row = cursor.fetchone()
226
+
227
+ if not row:
228
+ raise ValueError(f"Failed to create label '{label}' in graph '{self._graph_name}'")
229
+ return row[0]
230
+
231
+ def _seq_name(self, label: str) -> str:
232
+ """Return the sequence name for a label's entry IDs."""
233
+ return f'{self._graph_name}."{label}_id_seq"'
234
+
235
+ @staticmethod
236
+ def _to_agtype_literal(props: dict) -> str:
237
+ """Serialize a dict to a PostgreSQL string literal for ``::agtype`` cast.
238
+
239
+ The agtype text-input parser interprets ``\\"`` as an escaped double
240
+ quote inside strings (same as JSON). When the literal is embedded in a
241
+ standard PostgreSQL ``'...'`` string, backslashes are passed through
242
+ verbatim — so the ``\\"`` from ``escape_value`` reaches the agtype
243
+ parser intact. This produces identical stored values to the Cypher
244
+ UNWIND path.
245
+
246
+ Single quotes inside values are escaped as ``''`` (PostgreSQL standard).
247
+
248
+ Note: standard ``'...'`` strings (not ``E'...'``) are used intentionally.
249
+ ``E'...'`` would consume the ``\\`` escape layer before agtype sees it,
250
+ breaking ``\\"`` → ``"`` prematurely.
251
+ """
252
+ parts = ", ".join(f"{escape_value(k)}: {escape_value(v)}" for k, v in props.items())
253
+ inner = "{" + parts + "}"
254
+ # Escape single quotes for PostgreSQL standard string literal
255
+ inner = inner.replace("'", "''")
256
+ return "'" + inner + "'"
257
+
258
+ def invalidate_graphid_cache(self, label: str | None = None) -> None:
259
+ """Drop cached graphid maps so the next lookup re-reads from the database.
260
+
261
+ Call after inserting nodes into a label that is later used as an edge
262
+ endpoint, so the graphid map includes the newly inserted nodes.
263
+
264
+ Args:
265
+ label: If given, only invalidate caches for this label.
266
+ If None, clear the entire cache.
267
+ """
268
+ if label is None:
269
+ self._graphid_cache.clear()
270
+ else:
271
+ keys_to_drop = [k for k in self._graphid_cache if k[0] == label]
272
+ for k in keys_to_drop:
273
+ del self._graphid_cache[k]
274
+
275
+ def _build_graphid_index(self, label: str, ref_prop: str, ref_values: set[str] | None = None) -> dict[str, int]:
276
+ """Load a {ref_prop_value: graphid} mapping for nodes of a label.
277
+
278
+ When ``ref_values`` is provided, only loads graphids for those specific
279
+ property values (using a WHERE IN clause). This is critical for shared
280
+ graphs where a label table may contain hundreds of thousands of rows
281
+ across all sources, but only a small subset is needed for the current
282
+ edge batch.
283
+
284
+ When ``ref_values`` is None, loads the entire table (used for small
285
+ labels like structural edges).
286
+
287
+ Results are cached per (label, ref_prop) and incrementally extended
288
+ when new ref_values are requested.
289
+
290
+ Values returned by agtype_access_operator are agtype-quoted strings
291
+ (e.g. '"mth:foo.bar"'), so we strip the surrounding quotes.
292
+ """
293
+ cache_key = (label, ref_prop)
294
+ cached = self._graphid_cache.get(cache_key)
295
+
296
+ if ref_values is not None and cached is not None:
297
+ # Check if all requested values are already in the cache
298
+ missing = ref_values - cached.keys()
299
+ if not missing:
300
+ return cached
301
+ # Only query the missing values
302
+ ref_values = missing
303
+
304
+ if ref_values is not None and ref_values:
305
+ sql_stmt = SQLBuilder.lookup_node_graphids_filtered_sql(self._graph_name, label, ref_prop, ref_values)
306
+ else:
307
+ sql_stmt = SQLBuilder.lookup_node_graphids_sql(self._graph_name, label, ref_prop)
308
+
309
+ with self._conn.cursor() as cursor:
310
+ cursor.execute(sql_stmt)
311
+ rows = cursor.fetchall()
312
+ result = {str(r[0]).strip('"'): r[1] for r in rows}
313
+
314
+ # Merge into cache
315
+ if cached is not None:
316
+ cached.update(result)
317
+ logger.debug("Graphid index extended for {}.{}: +{} entries (total {})", label, ref_prop, len(result), len(cached))
318
+ return cached
319
+
320
+ self._graphid_cache[cache_key] = result
321
+ logger.debug("Graphid index built for {}.{}: {} entries", label, ref_prop, len(result))
322
+ return result
@@ -29,6 +29,7 @@ from cypher_graphdb.models import GraphObject, GraphObjectType, TabularResult
29
29
  from cypher_graphdb.statistics import IndexInfo, IndexType, LabelStatistics
30
30
  from cypher_graphdb.utils import chunk_list, sanitize_connection_params_for_logging, sanitize_connection_string_for_logging
31
31
 
32
+ from .agebulkwriter import AGEBulkWriter
32
33
  from .agerowfactories import age_row_factory
33
34
  from .agesearch import convert_to_fts_query
34
35
  from .ageserializer import to_cypher_list
@@ -83,6 +84,17 @@ class AGEGraphDB(CypherBackend):
83
84
  self._prepared_statements = {} # query_hash -> stmt_name
84
85
  self._max_cached_statements = 10
85
86
 
87
+ # When True, bulk_create_nodes and bulk_create_edges use direct SQL
88
+ # INSERT into AGE label tables instead of Cypher UNWIND. This bypasses
89
+ # the Cypher parser overhead (~3ms per statement) and is 10-30x faster
90
+ # for large bulk loads. The Cypher path stays as the fallback.
91
+ # Set to False to revert to the original Cypher UNWIND path.
92
+ self.direct_bulk_insert: bool = True
93
+
94
+ # Lazy AGEBulkWriter instance -- created on first bulk operation and
95
+ # reused for the lifetime of the connection. Cleared on disconnect/reconnect.
96
+ self._bulk_writer: AGEBulkWriter | None = None
97
+
86
98
  if args or kwargs:
87
99
  self.connect(*args, **kwargs)
88
100
 
@@ -412,12 +424,14 @@ class AGEGraphDB(CypherBackend):
412
424
  self._connection = None
413
425
  self._cinfo = None
414
426
  self._ckwargs = None
427
+ self._bulk_writer = None
415
428
 
416
429
  def reconnect(self):
417
430
  """Reconnect to the database using stored connection info."""
418
431
  assert self._cinfo is not None, "Can only reconnect if already successfully connected!"
419
432
 
420
433
  logger.debug("Try to reconnect")
434
+ self._bulk_writer = None
421
435
  self.connect_to_db()
422
436
 
423
437
  def commit(self):
@@ -556,17 +570,22 @@ class AGEGraphDB(CypherBackend):
556
570
  # ── Index management ────────────────────────────────────────────────
557
571
 
558
572
  def create_property_index(self, label: str, *property_names: str) -> None:
559
- """Create a GIN index on the properties column of a label table.
573
+ """Create indexes on a label table for fast property lookups.
560
574
 
561
- AGE stores all node properties in a single agtype JSON column.
562
- A GIN index on this column accelerates all property-based MATCH
563
- lookups (the @> containment operator). The property_names parameter
564
- is accepted for API compatibility but ignored -- one GIN index
565
- covers all properties.
575
+ Creates two kinds of indexes:
576
+
577
+ 1. **GIN index** on the whole ``properties`` column -- covers the
578
+ ``@>`` containment operator used by some internal AGE operations.
579
+ 2. **Btree expression indexes** on each specified property -- covers
580
+ the ``agtype_access_operator`` expression that AGE generates for
581
+ Cypher ``WHERE n.prop = ...`` clauses. Without these, every property
582
+ lookup is a sequential scan regardless of the GIN index.
566
583
 
567
584
  Args:
568
585
  label: Node label to index (e.g. "Method").
569
- *property_names: Ignored for AGE (GIN covers all properties).
586
+ *property_names: Property names to create expression indexes for.
587
+ Each gets its own btree index. If empty, only the GIN index
588
+ is created.
570
589
  """
571
590
  self._require_connection()
572
591
  graph_name = self._graph_name
@@ -579,17 +598,24 @@ class AGEGraphDB(CypherBackend):
579
598
  return
580
599
 
581
600
  with self._fetch_cursor(row_factory=None) as cursor:
601
+ # GIN on whole properties column (covers @> containment operator)
582
602
  cursor.execute(SQLBuilder.create_gin_index(graph_name, label))
603
+ # Btree expression index per property (covers agtype_access_operator in WHERE)
604
+ for prop in property_names:
605
+ cursor.execute(SQLBuilder.create_expression_index(graph_name, label, prop))
583
606
  self._connection.commit()
584
607
 
585
- logger.debug("GIN property index created for label '{}' in graph '{}'", label, graph_name)
608
+ logger.debug("Property indexes created for label '{}': GIN + {} expression indexes", label, len(property_names))
586
609
 
587
610
  def drop_index(self, label: str, *property_names: str) -> None:
588
- """Drop the GIN property index on a label table.
611
+ """Drop the GIN and expression indexes on a label table.
612
+
613
+ Drops the GIN index on the whole ``properties`` column, plus any
614
+ btree expression indexes for the specified properties.
589
615
 
590
616
  Args:
591
- label: Node label whose index to drop.
592
- *property_names: Ignored for AGE.
617
+ label: Node label whose indexes to drop.
618
+ *property_names: Property names whose expression indexes to drop.
593
619
  """
594
620
  self._require_connection()
595
621
  graph_name = self._graph_name
@@ -597,9 +623,11 @@ class AGEGraphDB(CypherBackend):
597
623
 
598
624
  with self._fetch_cursor(row_factory=None) as cursor:
599
625
  cursor.execute(SQLBuilder.drop_gin_index(graph_name, label))
626
+ for prop in property_names:
627
+ cursor.execute(SQLBuilder.drop_expression_index(graph_name, label, prop))
600
628
  self._connection.commit()
601
629
 
602
- logger.debug("GIN property index dropped for label '{}' in graph '{}'", label, graph_name)
630
+ logger.debug("Property indexes dropped for label '{}': GIN + {} expression indexes", label, len(property_names))
603
631
 
604
632
  def list_indexes(self, include_internal: bool = False) -> list[IndexInfo]:
605
633
  """List all indexes on the current graph.
@@ -649,15 +677,16 @@ class AGEGraphDB(CypherBackend):
649
677
  # ── Bulk write operations ─────────────────────────────────────────────
650
678
 
651
679
  def bulk_create_nodes(self, label: str, rows: list[dict], batch_size: int = 200) -> int:
652
- """Create nodes in batches using UNWIND with inline Cypher literals.
680
+ """Create nodes in batches.
653
681
 
654
- AGE does not support $params in UNWIND. Rows are serialized as
655
- inline Cypher map literals and executed in batches.
682
+ When ``direct_bulk_insert`` is True (default), uses direct SQL INSERT
683
+ via :class:`AGEBulkWriter` -- bypasses the Cypher parser for 10-30x
684
+ faster bulk loads. Falls back to Cypher UNWIND when disabled.
656
685
 
657
686
  Args:
658
687
  label: Node label for all created nodes.
659
688
  rows: List of property dicts, one per node.
660
- batch_size: Number of nodes per UNWIND batch.
689
+ batch_size: Number of nodes per batch.
661
690
 
662
691
  Returns:
663
692
  Total number of nodes created.
@@ -666,11 +695,13 @@ class AGEGraphDB(CypherBackend):
666
695
  if not rows:
667
696
  return 0
668
697
 
698
+ if self.direct_bulk_insert:
699
+ return self._get_bulk_writer().bulk_insert_nodes(label, rows, batch_size)
700
+
669
701
  total = 0
670
702
  for batch in chunk_list(rows, batch_size):
671
703
  self._write_node_batch(label, batch)
672
704
  total += len(batch)
673
-
674
705
  return total
675
706
 
676
707
  def bulk_create_edges(
@@ -685,8 +716,9 @@ class AGEGraphDB(CypherBackend):
685
716
  ) -> int:
686
717
  """Create edges in batches by matching src/dst nodes on a reference property.
687
718
 
688
- Each dict in edges must have "src" and "dst" keys whose values
689
- match the src_ref_prop/dst_ref_prop properties on source/destination nodes.
719
+ When ``direct_bulk_insert`` is True and both ``src_label`` and ``dst_label``
720
+ are specified, uses direct SQL INSERT via :class:`AGEBulkWriter` with
721
+ pre-resolved graphids. Falls back to Cypher UNWIND MATCH CREATE otherwise.
690
722
 
691
723
  Args:
692
724
  label: Edge label for all created edges.
@@ -695,7 +727,7 @@ class AGEGraphDB(CypherBackend):
695
727
  dst_label: Label of destination nodes (empty string for any label).
696
728
  src_ref_prop: Property name on source nodes to match against "src".
697
729
  dst_ref_prop: Property name on destination nodes to match against "dst".
698
- batch_size: Number of edges per UNWIND batch.
730
+ batch_size: Number of edges per batch.
699
731
 
700
732
  Returns:
701
733
  Total number of edges created.
@@ -704,7 +736,12 @@ class AGEGraphDB(CypherBackend):
704
736
  if not edges:
705
737
  return 0
706
738
 
707
- # Build MATCH patterns from label/prop names (not user values -- safe to inline)
739
+ if self.direct_bulk_insert and src_label and dst_label:
740
+ return self._get_bulk_writer().bulk_insert_edges(
741
+ label, edges, src_label, dst_label, src_ref_prop, dst_ref_prop, batch_size
742
+ )
743
+
744
+ # Cypher UNWIND fallback (when labels not specified or direct insert disabled)
708
745
  src_pat = f"(a:{src_label} {{{src_ref_prop}: e.src}})" if src_label else f"(a {{{src_ref_prop}: e.src}})"
709
746
  dst_pat = f"(b:{dst_label} {{{dst_ref_prop}: e.dst}})" if dst_label else f"(b {{{dst_ref_prop}: e.dst}})"
710
747
 
@@ -712,17 +749,22 @@ class AGEGraphDB(CypherBackend):
712
749
  for batch in chunk_list(edges, batch_size):
713
750
  self._write_edge_batch(label, batch, src_pat, dst_pat)
714
751
  total += len(batch)
715
-
716
752
  return total
717
753
 
718
- # ── Private helpers for bulk write ────────────────────────────────────
719
-
720
- def _write_node_batch(self, label: str, batch: list[dict]) -> None:
721
- """Write one batch of node dicts via UNWIND CREATE.
754
+ def _get_bulk_writer(self) -> AGEBulkWriter:
755
+ """Return the shared AGEBulkWriter, creating it lazily on first call.
722
756
 
723
- All dicts in batch must have the same set of keys -- the SET clause
724
- is derived from the first row and applied to all rows.
757
+ The writer is tied to the current connection and graph. It is cleared
758
+ automatically on disconnect() and reconnect() so it is never stale.
725
759
  """
760
+ if self._bulk_writer is None:
761
+ self._bulk_writer = AGEBulkWriter(self._connection, self._graph_name)
762
+ return self._bulk_writer
763
+
764
+ # ── Cypher UNWIND helpers (fallback path) ─────────────────────────────
765
+
766
+ def _write_node_batch(self, label: str, batch: list[dict]) -> None:
767
+ """Write one batch of node dicts via Cypher UNWIND CREATE."""
726
768
  props_keys = list(batch[0].keys())
727
769
  set_clause = ", ".join(f"n.{k} = props.{k}" for k in props_keys)
728
770
  cypher = f"UNWIND {to_cypher_list(batch)} AS props CREATE (n:{label}) SET {set_clause}"
@@ -731,11 +773,7 @@ class AGEGraphDB(CypherBackend):
731
773
  self.execute_cypher(parsed)
732
774
 
733
775
  def _write_edge_batch(self, label: str, batch: list[dict], src_pat: str, dst_pat: str) -> None:
734
- """Write one batch of edges via UNWIND MATCH CREATE.
735
-
736
- Any keys in batch dicts beyond 'src' and 'dst' are set as edge properties.
737
- """
738
- # Detect extra property keys (beyond src/dst) from first row
776
+ """Write one batch of edges via Cypher UNWIND MATCH CREATE."""
739
777
  edge_prop_keys = [k for k in batch[0] if k not in ("src", "dst")]
740
778
  if edge_prop_keys:
741
779
  set_clause = " SET " + ", ".join(f"r.{k} = e.{k}" for k in edge_prop_keys)
@@ -8,7 +8,6 @@ providers.
8
8
  from collections.abc import Sequence
9
9
  from typing import Any
10
10
 
11
- import age
12
11
  from psycopg.cursor import BaseCursor
13
12
  from psycopg.rows import BaseRowFactory, RowMaker, T
14
13
 
@@ -16,15 +15,17 @@ from cypher_graphdb.backend import ExecStatistics
16
15
  from cypher_graphdb.modelprovider import ModelProvider
17
16
  from cypher_graphdb.models import GraphEdge, GraphNode, GraphPath
18
17
 
18
+ from . import agtype_parser
19
19
 
20
- def _create_graph_node(value: age.models.Vertex, stats: ExecStatistics, provider: ModelProvider) -> GraphNode:
20
+
21
+ def _create_graph_node(value: agtype_parser.Vertex, stats: ExecStatistics, provider: ModelProvider) -> GraphNode:
21
22
  """Create a GraphNode from an AGE Vertex."""
22
23
  node = provider.create_node(value.label, value.properties, id_=value.id)
23
24
  stats.track_node(node)
24
25
  return node
25
26
 
26
27
 
27
- def _create_graph_edge(value: age.models.Edge, stats: ExecStatistics, provider: ModelProvider) -> GraphEdge:
28
+ def _create_graph_edge(value: agtype_parser.Edge, stats: ExecStatistics, provider: ModelProvider) -> GraphEdge:
28
29
  """Create a GraphEdge from an AGE Edge."""
29
30
  edge = provider.create_edge(value.label, value.start_id, value.end_id, value.properties, id_=value.id)
30
31
  stats.track_edge(edge)
@@ -36,11 +37,11 @@ def _create_graph_path(value, stats: ExecStatistics, provider: ModelProvider) ->
36
37
  path = GraphPath()
37
38
 
38
39
  for entity in value.entities:
39
- if isinstance(entity, age.models.Vertex):
40
+ if isinstance(entity, agtype_parser.Vertex):
40
41
  # Don't update stats here, will be updated when path stats are updated
41
42
  node = provider.create_node(entity.label, entity.properties, id_=entity.id)
42
43
  path.append(node)
43
- elif isinstance(entity, age.models.Edge):
44
+ elif isinstance(entity, agtype_parser.Edge):
44
45
  # Don't update stats here, will be updated when path stats are updated
45
46
  edge = provider.create_edge(entity.label, entity.start_id, entity.end_id, entity.properties, id_=entity.id)
46
47
  path.append(edge)
@@ -53,11 +54,11 @@ def _create_graph_path(value, stats: ExecStatistics, provider: ModelProvider) ->
53
54
 
54
55
  def _map_age_value(value, stats: ExecStatistics, provider: ModelProvider):
55
56
  """Map an AGE value to the appropriate graph object type."""
56
- if isinstance(value, age.models.Vertex):
57
+ if isinstance(value, agtype_parser.Vertex):
57
58
  return _create_graph_node(value, stats, provider)
58
- elif isinstance(value, age.models.Edge):
59
+ elif isinstance(value, agtype_parser.Edge):
59
60
  return _create_graph_edge(value, stats, provider)
60
- elif isinstance(value, age.models.Path):
61
+ elif isinstance(value, agtype_parser.Path):
61
62
  return _create_graph_path(value, stats, provider)
62
63
  else:
63
64
  # Only count non-null values (matching Memgraph behavior)