cypher-graphdb 0.2.4__tar.gz → 0.2.6__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 (282) hide show
  1. {cypher_graphdb-0.2.4/src/cypher_graphdb.egg-info → cypher_graphdb-0.2.6}/PKG-INFO +1 -1
  2. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backend.py +49 -4
  3. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agebulkwriter.py +108 -0
  4. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agegraphdb.py +74 -28
  5. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/memgraphdb.py +4 -4
  6. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/cyphergraphdb.py +5 -0
  7. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/indexing.py +22 -4
  8. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/dbpool.py +7 -0
  9. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6/src/cypher_graphdb.egg-info}/PKG-INFO +1 -1
  10. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_indexes_and_bulk.py +104 -1
  11. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/mock_backend.py +4 -0
  12. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_indexing.py +1 -1
  13. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.age.example +0 -0
  14. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.example +0 -0
  15. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.memgraph.example +0 -0
  16. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/ci.yml +0 -0
  17. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/publish.yml +0 -0
  18. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/release.yml +0 -0
  19. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.gitignore +0 -0
  20. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.pre-commit-config.yaml +0 -0
  21. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/AGENTS.md +0 -0
  22. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/CHANGELOG.md +0 -0
  23. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/CONTRIBUTING.md +0 -0
  24. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/LICENSE.md +0 -0
  25. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/README.md +0 -0
  26. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/Taskfile.yml +0 -0
  27. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/cli +0 -0
  28. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/TODO.md +0 -0
  29. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/changelog.md +0 -0
  30. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/material.css +0 -0
  31. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/mkdocstrings.css +0 -0
  32. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/style.css +0 -0
  33. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/duckdb-migration.md +0 -0
  34. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/falkordb-integration.md +0 -0
  35. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/hierarchical-export-format.md +0 -0
  36. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/multiple-statement-execution.md +0 -0
  37. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/parameterized-queries-for-typed-models.md +0 -0
  38. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/query-result-immutable-design.md +0 -0
  39. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/read-only-mode.md +0 -0
  40. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/stateless-multi-graph-support.md +0 -0
  41. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/documentation-guide.md +0 -0
  42. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/examples/docstring_examples.py +0 -0
  43. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/index.md +0 -0
  44. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/backends/age/index.md +0 -0
  45. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/backends/index.md +0 -0
  46. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/cli/index.md +0 -0
  47. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/cypher/index.md +0 -0
  48. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/index.md +0 -0
  49. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/tools/index.md +0 -0
  50. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/usage/index.md +0 -0
  51. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/mkdocs.yml +0 -0
  52. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/pyproject.toml +0 -0
  53. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/scripts/gen_ref_nav.py +0 -0
  54. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/setup.cfg +0 -0
  55. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/__init__.py +0 -0
  56. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/__main__.py +0 -0
  57. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/args.py +0 -0
  58. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backendprovider.py +0 -0
  59. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/__init__.py +0 -0
  60. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/__init__.py +0 -0
  61. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agerowfactories.py +0 -0
  62. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agesearch.py +0 -0
  63. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/ageserializer.py +0 -0
  64. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agesqlbuilder.py +0 -0
  65. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agtype.py +0 -0
  66. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agtype_parser.py +0 -0
  67. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/__init__.py +0 -0
  68. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/memgraphrowfactories.py +0 -0
  69. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cardinality.py +0 -0
  70. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/__init__.py +0 -0
  71. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/app.py +0 -0
  72. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/banner.py +0 -0
  73. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_manager.py +0 -0
  74. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_map.py +0 -0
  75. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_registry.py +0 -0
  76. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/__init__.py +0 -0
  77. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/add_graph_command.py +0 -0
  78. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/apply_config_command.py +0 -0
  79. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/base_command.py +0 -0
  80. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/clear_graph_command.py +0 -0
  81. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/commit_command.py +0 -0
  82. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/connect_command.py +0 -0
  83. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_edge_command.py +0 -0
  84. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_graph_command.py +0 -0
  85. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_linked_node_command.py +0 -0
  86. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_node_command.py +0 -0
  87. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/delete_graphobj_command.py +0 -0
  88. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/disconnect_command.py +0 -0
  89. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/drop_graph_command.py +0 -0
  90. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_backends_command.py +0 -0
  91. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_graphs_command.py +0 -0
  92. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_indexes_command.py +0 -0
  93. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_labels_command.py +0 -0
  94. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_models_command.py +0 -0
  95. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_parsed_query_command.py +0 -0
  96. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_schema_command.py +0 -0
  97. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_statistics_command.py +0 -0
  98. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/execute_cypher_command.py +0 -0
  99. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/execute_file_command.py +0 -0
  100. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/exit_command.py +0 -0
  101. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/export_graph_command.py +0 -0
  102. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_all_command.py +0 -0
  103. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_edges_command.py +0 -0
  104. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_nodes_command.py +0 -0
  105. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/format_output_command.py +0 -0
  106. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/get_command.py +0 -0
  107. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/gid_command.py +0 -0
  108. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_exists_command.py +0 -0
  109. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_op_command.py +0 -0
  110. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_to_tree_command.py +0 -0
  111. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/help_command.py +0 -0
  112. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/import_graph_command.py +0 -0
  113. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/last_result_op_command.py +0 -0
  114. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/load_models_command.py +0 -0
  115. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/resolve_edges_command.py +0 -0
  116. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/rollback_command.py +0 -0
  117. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/search_command.py +0 -0
  118. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/set_command.py +0 -0
  119. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/sql_command.py +0 -0
  120. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/update_graphobj_command.py +0 -0
  121. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/use_graph_command.py +0 -0
  122. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/completer.py +0 -0
  123. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/config.py +0 -0
  124. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/exporter.py +0 -0
  125. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/file_executor.py +0 -0
  126. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/graphdata.py +0 -0
  127. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/graphdb.py +0 -0
  128. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/_overview.md +0 -0
  129. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/_template.md +0 -0
  130. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/add_graph.md +0 -0
  131. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/exit.md +0 -0
  132. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/export_graph.md +0 -0
  133. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/import_graph.md +0 -0
  134. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/last_result.md +0 -0
  135. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help.py +0 -0
  136. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/importer.py +0 -0
  137. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/prompt.py +0 -0
  138. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/promptparser.py +0 -0
  139. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/provider.py +0 -0
  140. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/renderer.py +0 -0
  141. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/runtime.py +0 -0
  142. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/schema_cmd.py +0 -0
  143. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/settings.py +0 -0
  144. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/command_reader.py +0 -0
  145. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/config.py +0 -0
  146. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/Cypher.interp +0 -0
  147. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/Cypher.tokens +0 -0
  148. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherBaseListener.java +0 -0
  149. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.interp +0 -0
  150. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.java +0 -0
  151. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.tokens +0 -0
  152. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherListener.java +0 -0
  153. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherParser.java +0 -0
  154. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.g4 +0 -0
  155. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.interp +0 -0
  156. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.tokens +0 -0
  157. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.interp +0 -0
  158. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.py +0 -0
  159. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.tokens +0 -0
  160. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherListener.py +0 -0
  161. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherParser.py +0 -0
  162. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/__init__.py +0 -0
  163. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherbuilder.py +0 -0
  164. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/__init__.py +0 -0
  165. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/batch.py +0 -0
  166. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/bulk_normalize.py +0 -0
  167. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/connection.py +0 -0
  168. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/criteria.py +0 -0
  169. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/result.py +0 -0
  170. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/schema.py +0 -0
  171. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/search.py +0 -0
  172. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/sql.py +0 -0
  173. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/stream_mixin.py +0 -0
  174. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherjson.py +0 -0
  175. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherparser.py +0 -0
  176. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/decorators.py +0 -0
  177. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/display.py +0 -0
  178. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/exceptions.py +0 -0
  179. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/graphops.py +0 -0
  180. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/main.py +0 -0
  181. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/modelinfo.py +0 -0
  182. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/modelprovider.py +0 -0
  183. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/models.py +0 -0
  184. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/options.py +0 -0
  185. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/__init__.py +0 -0
  186. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/converter.py +0 -0
  187. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/core.py +0 -0
  188. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/generator.py +0 -0
  189. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/settings.py +0 -0
  190. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/statistics.py +0 -0
  191. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/__init__.py +0 -0
  192. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/base_exporter.py +0 -0
  193. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/base_importer.py +0 -0
  194. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_exporter.py +0 -0
  195. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_importer.py +0 -0
  196. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_source.py +0 -0
  197. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/data_flattener.py +0 -0
  198. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_exporter.py +0 -0
  199. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_importer.py +0 -0
  200. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_row_source.py +0 -0
  201. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/file_exporter.py +0 -0
  202. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/file_importer.py +0 -0
  203. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_exporter.py +0 -0
  204. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_importer.py +0 -0
  205. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_row_source.py +0 -0
  206. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/json_importer.py +0 -0
  207. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/json_yaml_data_source.py +0 -0
  208. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_collector.py +0 -0
  209. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_set.py +0 -0
  210. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_source.py +0 -0
  211. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/tabular_importer.py +0 -0
  212. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/yaml_importer.py +0 -0
  213. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/__init__.py +0 -0
  214. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/collection_utils.py +0 -0
  215. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/column_utils.py +0 -0
  216. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/connection_utils.py +0 -0
  217. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/conversion_utils.py +0 -0
  218. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/core_utils.py +0 -0
  219. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_merge.py +0 -0
  220. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_to_llm.py +0 -0
  221. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_utils.py +0 -0
  222. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/settings_repr.py +0 -0
  223. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/string_utils.py +0 -0
  224. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/SOURCES.txt +0 -0
  225. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/dependency_links.txt +0 -0
  226. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/entry_points.txt +0 -0
  227. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/requires.txt +0 -0
  228. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/top_level.txt +0 -0
  229. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/README.md +0 -0
  230. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/conftest.py +0 -0
  231. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/__init__.py +0 -0
  232. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/conftest.py +0 -0
  233. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_basic_operations.py +0 -0
  234. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_create_or_merge.py +0 -0
  235. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_example.py +0 -0
  236. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_parameters.py +0 -0
  237. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_read_only_mode.py +0 -0
  238. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_streaming.py +0 -0
  239. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/test_json_schema_loading.py +0 -0
  240. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/test_json_schema_vs_decorators.py +0 -0
  241. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/README.md +0 -0
  242. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/__init__.py +0 -0
  243. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/__init__.py +0 -0
  244. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/test_cmd_map.py +0 -0
  245. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/test_command_registry.py +0 -0
  246. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_age_serializer.py +0 -0
  247. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_age_sqlbuilder.py +0 -0
  248. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_agtype_parser.py +0 -0
  249. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_backend_capabilities.py +0 -0
  250. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_bulk_normalize.py +0 -0
  251. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_column_utils.py +0 -0
  252. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_command_reader.py +0 -0
  253. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_cypherbuilder.py +0 -0
  254. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_cypherparser.py +0 -0
  255. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_dbpool.py +0 -0
  256. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_dict_access_mixin.py +0 -0
  257. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_extend_relation_decorator.py +0 -0
  258. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_extend_relations.py +0 -0
  259. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_graph_id_zero.py +0 -0
  260. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_graphops.py +0 -0
  261. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_model_inheritance.py +0 -0
  262. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelinfo.py +0 -0
  263. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelprovider_loading.py +0 -0
  264. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelprovider_schemas.py +0 -0
  265. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_models.py +0 -0
  266. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_schema_converter.py +0 -0
  267. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/__init__.py +0 -0
  268. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_data_flattener.py +0 -0
  269. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_exporters.py +0 -0
  270. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_hierarchical_importer.py +0 -0
  271. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_json_yaml_data_source.py +0 -0
  272. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_resource_management.py +0 -0
  273. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_tabular_import.py +0 -0
  274. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_collection_utils.py +0 -0
  275. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_connection_utils.py +0 -0
  276. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_conversion_utils.py +0 -0
  277. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_core_utils.py +0 -0
  278. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_schema_merge.py +0 -0
  279. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_schema_utils.py +0 -0
  280. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_settings_repr.py +0 -0
  281. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_string_utils.py +0 -0
  282. {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cypher_graphdb
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -6,6 +6,7 @@ CypherBackend: Abstract base class for implementing graph database backends.
6
6
  """
7
7
 
8
8
  import abc
9
+ import re
9
10
  from enum import Enum, auto
10
11
  from typing import Any
11
12
 
@@ -18,6 +19,11 @@ from .modelprovider import ModelProvider
18
19
  from .models import TabularResult
19
20
  from .statistics import GraphStatistics, IndexInfo, LabelStatistics
20
21
 
22
+ # Regex for safe property names and values in the Cypher fallback.
23
+ # Matches alphanumeric, underscore, hyphen — sufficient for source_key / lang values.
24
+ _SAFE_FILTER_KEY_RE = re.compile(r"^[a-zA-Z0-9_]+$")
25
+ _SAFE_FILTER_VAL_RE = re.compile(r"^[a-zA-Z0-9_.:\-]+$")
26
+
21
27
 
22
28
  class BackendCapability(Enum):
23
29
  """Enumeration of backend capabilities for feature detection."""
@@ -29,6 +35,7 @@ class BackendCapability(Enum):
29
35
  UNIQUE_CONSTRAINT = auto() # Supports create_unique_constraint
30
36
  FULLTEXT_INDEX = auto() # Supports create_fulltext_index
31
37
  VECTOR_INDEX = auto() # Supports create_vector_index
38
+ BULK_DELETE = auto() # Supports bulk_delete_nodes (optimized batch deletion with edge cascade)
32
39
 
33
40
 
34
41
  class ExecStatistics(GraphStatistics):
@@ -356,8 +363,8 @@ class CypherBackend(abc.ABC):
356
363
  self,
357
364
  label: str,
358
365
  edges: list[dict],
359
- src_label: str = "",
360
- dst_label: str = "",
366
+ src_label: str,
367
+ dst_label: str,
361
368
  src_ref_prop: str = "id",
362
369
  dst_ref_prop: str = "id",
363
370
  batch_size: int = 500,
@@ -371,8 +378,8 @@ class CypherBackend(abc.ABC):
371
378
  Args:
372
379
  label: Edge label for all created edges.
373
380
  edges: List of dicts with "src", "dst", and optional edge properties.
374
- src_label: Label of source nodes (empty string for any label).
375
- dst_label: Label of destination nodes (empty string for any label).
381
+ src_label: Label of source nodes. Required for optimal performance on AGE.
382
+ dst_label: Label of destination nodes. Required for optimal performance on AGE.
376
383
  src_ref_prop: Property name on source nodes to match against "src".
377
384
  dst_ref_prop: Property name on destination nodes to match against "dst".
378
385
  batch_size: Number of edges per UNWIND batch.
@@ -385,6 +392,44 @@ class CypherBackend(abc.ABC):
385
392
  """
386
393
  raise NotImplementedError(f"Backend {self.name} does not support bulk_create_edges")
387
394
 
395
+ def bulk_delete_nodes(self, label: str, filters: dict[str, str]) -> int:
396
+ """Delete all nodes of a label matching property filters, cascading to edges.
397
+
398
+ Removes nodes where all specified property key=value pairs match, plus
399
+ all edges referencing the deleted nodes (equivalent to Cypher DETACH DELETE).
400
+
401
+ The default implementation uses Cypher DETACH DELETE. Backends that declare
402
+ ``BULK_DELETE`` capability override this with an optimized implementation
403
+ (e.g. direct table access).
404
+
405
+ Args:
406
+ label: Node label to delete from (e.g. "Method", "Class").
407
+ filters: Property filters to match. All must match (AND semantics).
408
+ Example: {"source_key": "nais-platform", "lang": "ts"}
409
+
410
+ Returns:
411
+ Number of nodes deleted.
412
+ """
413
+ if not filters:
414
+ raise ValueError("filters must not be empty (would delete all nodes of the label)")
415
+ if not _SAFE_FILTER_KEY_RE.match(label):
416
+ raise ValueError(f"label must be alphanumeric/underscore, got: {label!r}")
417
+ for k, v in filters.items():
418
+ if not _SAFE_FILTER_KEY_RE.match(k):
419
+ raise ValueError(f"filter key must be alphanumeric/underscore, got: {k!r}")
420
+ if not _SAFE_FILTER_VAL_RE.match(v):
421
+ raise ValueError(f"filter value must be alphanumeric/underscore/hyphen/dot/colon, got: {v!r}")
422
+ where_parts = " AND ".join(f"n.{k} = '{v}'" for k, v in filters.items())
423
+ count_cypher = f"MATCH (n:{label}) WHERE {where_parts} RETURN count(n)"
424
+ parsed = self.parse_cypher(count_cypher)
425
+ result, _ = self.execute_cypher(parsed)
426
+ count = result[0][0] if result else 0
427
+ if count > 0:
428
+ delete_cypher = f"MATCH (n:{label}) WHERE {where_parts} DETACH DELETE n"
429
+ parsed = self.parse_cypher(delete_cypher)
430
+ self.execute_cypher(parsed)
431
+ return count
432
+
388
433
  @property
389
434
  def __dict__(self):
390
435
  """Return backend state as a dictionary for introspection."""
@@ -19,6 +19,7 @@ from typing import Literal
19
19
  import psycopg
20
20
  from loguru import logger
21
21
  from psycopg.sql import SQL, Identifier
22
+ from psycopg.sql import Literal as SQLLiteral
22
23
 
23
24
  from cypher_graphdb.utils import chunk_list
24
25
 
@@ -26,6 +27,24 @@ from .ageserializer import escape_value
26
27
  from .agesqlbuilder import SQLBuilder
27
28
 
28
29
 
30
+ def _build_agtype_conditions(filters: dict[str, str], alias: str | None = None) -> str:
31
+ """Build a SQL WHERE expression matching AGE vertex properties.
32
+
33
+ Uses agtype_access_operator so the expression btree index is used.
34
+ Values are escaped via SQLLiteral to prevent injection.
35
+
36
+ Args:
37
+ filters: Property key=value pairs (all ANDed together).
38
+ alias: Optional table alias prefix (e.g. "n" produces "n.properties").
39
+ """
40
+ prefix = f"{alias}." if alias else ""
41
+ return " AND ".join(
42
+ f"ag_catalog.agtype_access_operator(VARIADIC ARRAY[{prefix}properties, "
43
+ f"{SQLLiteral(chr(34) + k + chr(34)).as_string(None)}::ag_catalog.agtype])::text = {SQLLiteral(v).as_string(None)}"
44
+ for k, v in filters.items()
45
+ )
46
+
47
+
29
48
  class AGEBulkWriter:
30
49
  """Direct SQL bulk writer for AGE label tables.
31
50
 
@@ -44,6 +63,10 @@ class AGEBulkWriter:
44
63
  # Invalidated by invalidate_graphid_cache() after node inserts that
45
64
  # change the graphid mapping.
46
65
  self._graphid_cache: dict[tuple[str, str], dict[str, int]] = {}
66
+ # Cached list of edge label names for bulk_delete_nodes.
67
+ # Edge labels are stable within a connection lifetime -- populated on
68
+ # first call to _list_edge_labels() and reused for all subsequent deletes.
69
+ self._edge_labels_cache: list[str] | None = None
47
70
 
48
71
  # -- Public API -----------------------------------------------------------
49
72
 
@@ -178,6 +201,91 @@ class AGEBulkWriter:
178
201
  logger.debug("Direct SQL: {} {} edges inserted", total, label)
179
202
  return total
180
203
 
204
+ def bulk_delete_nodes(self, label: str, filters: dict[str, str]) -> int:
205
+ """Delete nodes matching property filters via direct SQL, cascading to edges.
206
+
207
+ Uses server-side joined DELETEs -- PostgreSQL resolves the node filter
208
+ via the expression index on the vertex table, then joins to each edge
209
+ table via the indexed start_id/end_id columns. No large ID arrays are
210
+ transferred to Python; the entire operation runs inside the database.
211
+
212
+ On a fresh graph where the label table does not yet exist, returns 0.
213
+
214
+ Args:
215
+ label: Vertex label to delete from (e.g. "Method").
216
+ filters: Property key=value filters (AND semantics).
217
+
218
+ Returns:
219
+ Number of nodes deleted.
220
+ """
221
+ if not filters:
222
+ raise ValueError("filters must not be empty (would delete all nodes of the label)")
223
+
224
+ # Build filter conditions in two forms:
225
+ # - with alias "n." for the USING join (edge DELETE references vertex as n)
226
+ # - without alias for the plain vertex DELETE
227
+ join_conditions = _build_agtype_conditions(filters, alias="n")
228
+ node_conditions = _build_agtype_conditions(filters, alias=None)
229
+ edge_labels = self._list_edge_labels()
230
+
231
+ try:
232
+ with self._conn.cursor() as cursor:
233
+ # Step 1: delete edges via JOIN -- lets PG use expression indexes
234
+ # on the vertex table + start_id/end_id indexes on edge tables.
235
+ for elabel in edge_labels:
236
+ cursor.execute(
237
+ SQL(
238
+ "DELETE FROM {egraph}.{etable} e "
239
+ "USING {vgraph}.{vtable} n "
240
+ "WHERE (e.start_id = n.id OR e.end_id = n.id) "
241
+ "AND {cond}"
242
+ ).format(
243
+ egraph=Identifier(self._graph_name),
244
+ etable=Identifier(elabel),
245
+ vgraph=Identifier(self._graph_name),
246
+ vtable=Identifier(label),
247
+ cond=SQL(join_conditions),
248
+ )
249
+ )
250
+ # Step 2: delete the nodes
251
+ cursor.execute(
252
+ SQL("DELETE FROM {schema}.{table} WHERE {cond} RETURNING id").format(
253
+ schema=Identifier(self._graph_name),
254
+ table=Identifier(label),
255
+ cond=SQL(node_conditions),
256
+ )
257
+ )
258
+ deleted = len(cursor.fetchall())
259
+ except psycopg.errors.UndefinedTable:
260
+ # Label table does not exist (fresh graph, never written to).
261
+ self._conn.rollback()
262
+ return 0
263
+
264
+ self.invalidate_graphid_cache(label)
265
+ logger.debug("Direct SQL: {} {} nodes deleted (+ edges from {} edge tables)", deleted, label, len(edge_labels))
266
+ return deleted
267
+
268
+ def _list_edge_labels(self) -> list[str]:
269
+ """Return all edge label names in the graph, cached for the connection lifetime.
270
+
271
+ Edge labels are stable within a session -- they grow monotonically as new
272
+ labels are created, never shrink. Caching avoids repeated catalog queries
273
+ when bulk_delete_nodes is called many times in the same cleanup pass.
274
+ """
275
+ if self._edge_labels_cache is not None:
276
+ return self._edge_labels_cache
277
+ with self._conn.cursor() as cursor:
278
+ cursor.execute(
279
+ SQL(
280
+ "SELECT name FROM ag_catalog.ag_label "
281
+ "WHERE graph = (SELECT graphid FROM ag_catalog.ag_graph WHERE name = %s) "
282
+ "AND kind = 'e' AND name != '_ag_label_edge'"
283
+ ),
284
+ (self._graph_name,),
285
+ )
286
+ self._edge_labels_cache = [row[0] for row in cursor.fetchall()]
287
+ return self._edge_labels_cache
288
+
181
289
  # -- Internal helpers -----------------------------------------------------
182
290
 
183
291
  def _ensure_label(self, label: str, kind: Literal["v", "e"] = "v") -> int:
@@ -8,6 +8,7 @@ Classes:
8
8
  AGEGraphDB: Main backend class for Apache AGE database operations.
9
9
  """
10
10
 
11
+ import contextlib
11
12
  import hashlib
12
13
  import json
13
14
  import time
@@ -203,7 +204,11 @@ class AGEGraphDB(CypherBackend):
203
204
  return hashlib.md5(query.encode()).hexdigest()[:8]
204
205
 
205
206
  def _get_or_prepare_statement(self, cypher_sql, query: str) -> str:
206
- """Get existing prepared statement or create a new one."""
207
+ """Get existing prepared statement or create a new one.
208
+
209
+ Rolls back the connection if PREPARE fails so the connection is
210
+ returned to the pool in a clean state rather than left in INERROR.
211
+ """
207
212
  query_hash = self._get_query_hash(query)
208
213
 
209
214
  if query_hash not in self._prepared_statements:
@@ -226,9 +231,16 @@ class AGEGraphDB(CypherBackend):
226
231
  stmt_name = f"cypher_stmt_{query_hash}"
227
232
  logger.trace("Creating new prepared statement {} for query hash {}", stmt_name, query_hash)
228
233
 
229
- with self._fetch_cursor(row_factory=None) as cursor:
230
- prepare_sql = SQL("PREPARE {} AS {}").format(Identifier(stmt_name), cypher_sql)
231
- cursor.execute(prepare_sql)
234
+ try:
235
+ with self._fetch_cursor(row_factory=None) as cursor:
236
+ prepare_sql = SQL("PREPARE {} AS {}").format(Identifier(stmt_name), cypher_sql)
237
+ cursor.execute(prepare_sql)
238
+ except psycopg.Error as e:
239
+ # PREPARE failed -- roll back so the connection is not left in
240
+ # INERROR state (which would corrupt the connection pool).
241
+ with contextlib.suppress(Exception):
242
+ self._connection.rollback()
243
+ raise e
232
244
 
233
245
  self._prepared_statements[query_hash] = stmt_name
234
246
  logger.trace("Prepared statement cached. Total cached: {}", len(self._prepared_statements))
@@ -262,25 +274,29 @@ class AGEGraphDB(CypherBackend):
262
274
  execute_sql = SQL("EXECUTE {} (%s)").format(Identifier(stmt_name))
263
275
 
264
276
  try:
265
- with self._fetch_cursor(
266
- row_factory=age_row_factory(exec_stats, self._model_provider) if not raw_data else None
267
- ) as exec_cursor:
268
- exec_cursor.execute(execute_sql, (params_json,))
269
-
270
- if fetch_one:
271
- row = exec_cursor.fetchone()
272
- result = [row] if row is not None else []
273
- else:
274
- result = exec_cursor.fetchall()
275
-
276
- col_names = [col.name for col in exec_cursor.description] if exec_cursor.description else []
277
- sql_stats = SqlStatistics(sql_stmt=cypher_sql.as_string(), col_names=col_names)
278
-
279
- if self.autocommit:
280
- self._connection.commit()
281
-
277
+ result, sql_stats = self._run_prepared(execute_sql, params_json, cypher_sql, exec_stats, fetch_one, raw_data)
278
+ except psycopg.errors.InvalidSqlStatementName:
279
+ # Server-side prepared statement was dropped (connection recycled,
280
+ # PgBouncer reset, idle timeout). Roll back, evict the stale cache
281
+ # entry, re-prepare, and retry once.
282
+ logger.debug("Prepared statement {} gone from server, re-preparing", stmt_name)
283
+ with contextlib.suppress(Exception):
284
+ self._connection.rollback()
285
+ query_hash = self._get_query_hash(cypher_query.parsed_query)
286
+ self._prepared_statements.pop(query_hash, None)
287
+ stmt_name = self._get_or_prepare_statement(cypher_sql, cypher_query.parsed_query)
288
+ execute_sql = SQL("EXECUTE {} (%s)").format(Identifier(stmt_name))
289
+ try:
290
+ result, sql_stats = self._run_prepared(execute_sql, params_json, cypher_sql, exec_stats, fetch_one, raw_data)
291
+ except Exception as e:
292
+ error_details = f"AGE query execution failed after re-prepare: {e}"
293
+ self._prepared_statements.clear()
294
+ self._connection.close()
295
+ self._connection = None
296
+ raise AGEExecutionError(error_details) from e
282
297
  except Exception as e:
283
298
  error_details = f"AGE query execution failed: {e}"
299
+ self._prepared_statements.clear()
284
300
  self._connection.close()
285
301
  self._connection = None
286
302
  raise AGEExecutionError(error_details) from e
@@ -290,6 +306,27 @@ class AGEGraphDB(CypherBackend):
290
306
 
291
307
  return (result, exec_stats, sql_stats)
292
308
 
309
+ def _run_prepared(self, execute_sql, params_json, cypher_sql, exec_stats, fetch_one, raw_data):
310
+ """Execute a prepared statement and return (result, sql_stats)."""
311
+ with self._fetch_cursor(
312
+ row_factory=age_row_factory(exec_stats, self._model_provider) if not raw_data else None
313
+ ) as exec_cursor:
314
+ exec_cursor.execute(execute_sql, (params_json,))
315
+
316
+ if fetch_one:
317
+ row = exec_cursor.fetchone()
318
+ result = [row] if row is not None else []
319
+ else:
320
+ result = exec_cursor.fetchall()
321
+
322
+ col_names = [col.name for col in exec_cursor.description] if exec_cursor.description else []
323
+ sql_stats = SqlStatistics(sql_stmt=cypher_sql.as_string(), col_names=col_names)
324
+
325
+ if self.autocommit:
326
+ self._connection.commit()
327
+
328
+ return result, sql_stats
329
+
293
330
  def _cleanup_prepared_statements(self):
294
331
  """Deallocate all cached prepared statements."""
295
332
  if not self._connection or not self._prepared_statements:
@@ -525,6 +562,8 @@ class AGEGraphDB(CypherBackend):
525
562
  case BackendCapability.PROPERTY_INDEX:
526
563
  # AGE supports GIN property indexes via PostgreSQL SQL
527
564
  return True
565
+ case BackendCapability.BULK_DELETE:
566
+ return True
528
567
  case _:
529
568
  # Delegate unknown capabilities to superclass
530
569
  return super().get_capability(capability)
@@ -674,6 +713,13 @@ class AGEGraphDB(CypherBackend):
674
713
  # Edge traversal indexes -- auto-created on edge label tables
675
714
  return bool(indexname.endswith("_start_id_idx") or indexname.endswith("_end_id_idx"))
676
715
 
716
+ # ── Bulk delete operations ─────────────────────────────────────────────
717
+
718
+ def bulk_delete_nodes(self, label: str, filters: dict[str, str]) -> int:
719
+ """Delete nodes matching property filters, cascading to edges."""
720
+ self._require_connection()
721
+ return self._get_bulk_writer().bulk_delete_nodes(label, filters)
722
+
677
723
  # ── Bulk write operations ─────────────────────────────────────────────
678
724
 
679
725
  def bulk_create_nodes(self, label: str, rows: list[dict], batch_size: int = 200) -> int:
@@ -708,8 +754,8 @@ class AGEGraphDB(CypherBackend):
708
754
  self,
709
755
  label: str,
710
756
  edges: list[dict],
711
- src_label: str = "",
712
- dst_label: str = "",
757
+ src_label: str,
758
+ dst_label: str,
713
759
  src_ref_prop: str = "id",
714
760
  dst_ref_prop: str = "id",
715
761
  batch_size: int = 500,
@@ -723,8 +769,8 @@ class AGEGraphDB(CypherBackend):
723
769
  Args:
724
770
  label: Edge label for all created edges.
725
771
  edges: List of dicts with at least "src" and "dst" keys.
726
- src_label: Label of source nodes (empty string for any label).
727
- dst_label: Label of destination nodes (empty string for any label).
772
+ src_label: Label of source nodes. Required for optimal performance on AGE.
773
+ dst_label: Label of destination nodes. Required for optimal performance on AGE.
728
774
  src_ref_prop: Property name on source nodes to match against "src".
729
775
  dst_ref_prop: Property name on destination nodes to match against "dst".
730
776
  batch_size: Number of edges per batch.
@@ -741,9 +787,9 @@ class AGEGraphDB(CypherBackend):
741
787
  label, edges, src_label, dst_label, src_ref_prop, dst_ref_prop, batch_size
742
788
  )
743
789
 
744
- # Cypher UNWIND fallback (when labels not specified or direct insert disabled)
745
- src_pat = f"(a:{src_label} {{{src_ref_prop}: e.src}})" if src_label else f"(a {{{src_ref_prop}: e.src}})"
746
- dst_pat = f"(b:{dst_label} {{{dst_ref_prop}: e.dst}})" if dst_label else f"(b {{{dst_ref_prop}: e.dst}})"
790
+ # Cypher UNWIND fallback (direct_bulk_insert disabled).
791
+ src_pat = f"(a:{src_label} {{{src_ref_prop}: e.src}})"
792
+ dst_pat = f"(b:{dst_label} {{{dst_ref_prop}: e.dst}})"
747
793
 
748
794
  total = 0
749
795
  for batch in chunk_list(edges, batch_size):
@@ -541,8 +541,8 @@ class MemgraphDB(CypherBackend):
541
541
  self,
542
542
  label: str,
543
543
  edges: list[dict],
544
- src_label: str = "",
545
- dst_label: str = "",
544
+ src_label: str,
545
+ dst_label: str,
546
546
  src_ref_prop: str = "id",
547
547
  dst_ref_prop: str = "id",
548
548
  batch_size: int = 500,
@@ -555,8 +555,8 @@ class MemgraphDB(CypherBackend):
555
555
  Args:
556
556
  label: Edge label for all created edges.
557
557
  edges: List of dicts with "src", "dst", and optional edge properties.
558
- src_label: Label of source nodes (empty string for any label).
559
- dst_label: Label of destination nodes (empty string for any label).
558
+ src_label: Label of source nodes. Required for optimal performance on AGE.
559
+ dst_label: Label of destination nodes. Required for optimal performance on AGE.
560
560
  src_ref_prop: Property name on source nodes to match against "src".
561
561
  dst_ref_prop: Property name on destination nodes to match against "dst".
562
562
  batch_size: Number of edges per UNWIND batch.
@@ -255,6 +255,11 @@ class CypherGraphDB(ConnectionMixin, BatchMixin, IndexingMixin, SchemaMixin, Sea
255
255
  # Property for convenience access
256
256
  settings = property(lambda self: self.get_settings())
257
257
 
258
+ @property
259
+ def connected(self) -> bool:
260
+ """Return True if the backend has an active connection."""
261
+ return self._backend.connected if self._backend else False
262
+
258
263
  @property
259
264
  def read_only(self) -> bool:
260
265
  """Check if the connection is in read-only mode.
@@ -93,6 +93,24 @@ class IndexingMixin:
93
93
  assert self._backend
94
94
  return self._backend.list_indexes(include_internal=include_internal)
95
95
 
96
+ def bulk_delete_nodes(self, label: str, filters: dict[str, str]) -> int:
97
+ """Delete all nodes of a label matching property filters, cascading to edges.
98
+
99
+ Delegates to the backend's ``bulk_delete_nodes``. Backends that declare
100
+ ``BULK_DELETE`` capability provide an optimized implementation; others
101
+ fall back to Cypher DETACH DELETE.
102
+
103
+ Args:
104
+ label: Node label to delete from (e.g. "Method", "Class").
105
+ filters: Property key=value filters (AND semantics).
106
+ Example: {"source_key": "nais-platform", "lang": "ts"}
107
+
108
+ Returns:
109
+ Number of nodes deleted.
110
+ """
111
+ assert self._backend
112
+ return self._backend.bulk_delete_nodes(label, filters)
113
+
96
114
  def bulk_create_nodes(
97
115
  self,
98
116
  rows: Sequence[dict] | Sequence[GraphNode],
@@ -138,11 +156,11 @@ class IndexingMixin:
138
156
  def bulk_create_edges(
139
157
  self,
140
158
  edges: Sequence[dict] | Sequence[GraphEdge],
159
+ src_label: str,
160
+ dst_label: str,
141
161
  src_refs: Sequence[Any] | None = None,
142
162
  dst_refs: Sequence[Any] | None = None,
143
163
  label: str | None = None,
144
- src_label: str = "",
145
- dst_label: str = "",
146
164
  src_ref_prop: str = "id",
147
165
  dst_ref_prop: str = "id",
148
166
  batch_size: int = 500,
@@ -169,8 +187,8 @@ class IndexingMixin:
169
187
  dst_refs: Parallel list of destination match values. Required for
170
188
  typed input; must be None for dict input.
171
189
  label: Edge label. Required for dicts; optional for typed input.
172
- src_label: Label of source nodes (empty string for any label).
173
- dst_label: Label of destination nodes (empty string for any label).
190
+ src_label: Label of source nodes. Required for optimal performance on AGE.
191
+ dst_label: Label of destination nodes. Required for optimal performance on AGE.
174
192
  src_ref_prop: Property name on source nodes to match against src refs.
175
193
  dst_ref_prop: Property name on destination nodes to match against dst refs.
176
194
  batch_size: Number of edges per batch.
@@ -249,6 +249,8 @@ class CypherGraphDBPool:
249
249
 
250
250
  Silently ignores connections not tracked by this pool. If the pool
251
251
  has been closed the connection is disconnected instead of re-queued.
252
+ Disconnected instances (connection lost during use) are discarded
253
+ rather than re-queued to prevent stale connections re-entering idle.
252
254
  """
253
255
  if db is None:
254
256
  return
@@ -262,6 +264,11 @@ class CypherGraphDBPool:
262
264
  db.disconnect()
263
265
  logger.trace("Release disconnect (closed) id={}", id(db))
264
266
  return
267
+ # Discard instances that lost their connection during use.
268
+ if not db.connected:
269
+ logger.debug("Release discard id={} (disconnected)", id(db))
270
+ self._not_empty.notify()
271
+ return
265
272
  # compute and append expiry (0.0 sentinel if no TTL)
266
273
  self._idle.append((self._expiry(), db))
267
274
  self._stats.released += 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cypher_graphdb
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -235,7 +235,7 @@ class TestBulkCreateEdges:
235
235
  @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
236
236
  def test_bulk_create_edges_empty(self, clean_db):
237
237
  """bulk_create_edges with empty list should return 0."""
238
- count = clean_db.bulk_create_edges([], label="KNOWS")
238
+ count = clean_db.bulk_create_edges([], label="KNOWS", src_label="Person", dst_label="Person")
239
239
  assert count == 0
240
240
 
241
241
  @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
@@ -494,3 +494,106 @@ class TestBulkDataCorrectness:
494
494
  # Reverse direction should not match
495
495
  rev = clean_db.execute("MATCH (a:DirNode {id: 'dst_node'})-[:POINTS_TO]->(b) RETURN b.id")
496
496
  assert rev == [] or rev is None
497
+
498
+
499
+ # ── Bulk delete nodes ─────────────────────────────────────────────────────
500
+
501
+
502
+ class TestBulkDeleteNodes:
503
+ """Test bulk_delete_nodes for both backends."""
504
+
505
+ @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
506
+ def test_delete_by_single_filter(self, clean_db):
507
+ """bulk_delete_nodes should remove only matching nodes."""
508
+ clean_db.bulk_create_nodes(
509
+ [{"id": f"a{i}", "source_key": "alpha", "lang": "py"} for i in range(5)],
510
+ label="DelNode",
511
+ )
512
+ clean_db.bulk_create_nodes(
513
+ [{"id": f"b{i}", "source_key": "beta", "lang": "py"} for i in range(3)],
514
+ label="DelNode",
515
+ )
516
+ clean_db.commit()
517
+
518
+ deleted = clean_db.bulk_delete_nodes("DelNode", {"source_key": "alpha"})
519
+ clean_db.commit()
520
+
521
+ assert deleted == 5
522
+ remaining = clean_db.execute("MATCH (n:DelNode) RETURN count(n)", unnest_result=True)
523
+ assert remaining == 3
524
+
525
+ @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
526
+ def test_delete_by_multiple_filters(self, clean_db):
527
+ """bulk_delete_nodes with multiple filters should AND them."""
528
+ clean_db.bulk_create_nodes(
529
+ [{"id": f"a{i}", "source_key": "src", "lang": "py"} for i in range(4)],
530
+ label="MFNode",
531
+ )
532
+ clean_db.bulk_create_nodes(
533
+ [{"id": f"b{i}", "source_key": "src", "lang": "ts"} for i in range(3)],
534
+ label="MFNode",
535
+ )
536
+ clean_db.commit()
537
+
538
+ deleted = clean_db.bulk_delete_nodes("MFNode", {"source_key": "src", "lang": "py"})
539
+ clean_db.commit()
540
+
541
+ assert deleted == 4
542
+ remaining = clean_db.execute("MATCH (n:MFNode) RETURN count(n)", unnest_result=True)
543
+ assert remaining == 3
544
+
545
+ @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
546
+ def test_delete_cascades_to_edges(self, clean_db):
547
+ """bulk_delete_nodes should also remove edges referencing deleted nodes."""
548
+ clean_db.bulk_create_nodes(
549
+ [{"id": "x1", "source_key": "s"}, {"id": "x2", "source_key": "s"}],
550
+ label="CascNode",
551
+ )
552
+ clean_db.bulk_create_nodes(
553
+ [{"id": "y1", "source_key": "other"}],
554
+ label="CascNode",
555
+ )
556
+ clean_db.create_property_index("CascNode", "id")
557
+ clean_db.commit()
558
+
559
+ clean_db.bulk_create_edges(
560
+ [{"src": "x1", "dst": "x2"}, {"src": "x1", "dst": "y1"}],
561
+ label="LINKS",
562
+ src_label="CascNode",
563
+ dst_label="CascNode",
564
+ )
565
+ clean_db.commit()
566
+
567
+ # Verify edges exist
568
+ edge_count = clean_db.execute("MATCH ()-[r:LINKS]->() RETURN count(r)", unnest_result=True)
569
+ assert edge_count == 2
570
+
571
+ # Delete only source_key='s' nodes
572
+ deleted = clean_db.bulk_delete_nodes("CascNode", {"source_key": "s"})
573
+ clean_db.commit()
574
+
575
+ assert deleted == 2
576
+ # Both edges should be gone (x1 was start of both)
577
+ edge_count = clean_db.execute("MATCH ()-[r:LINKS]->() RETURN count(r)", unnest_result=True)
578
+ assert edge_count == 0
579
+ # y1 should still exist
580
+ remaining = clean_db.execute("MATCH (n:CascNode) RETURN count(n)", unnest_result=True)
581
+ assert remaining == 1
582
+
583
+ @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
584
+ def test_delete_no_match_returns_zero(self, clean_db):
585
+ """bulk_delete_nodes with no matching nodes should return 0."""
586
+ clean_db.bulk_create_nodes([{"id": "z1", "source_key": "keep"}], label="NoMatch")
587
+ clean_db.commit()
588
+
589
+ deleted = clean_db.bulk_delete_nodes("NoMatch", {"source_key": "nonexistent"})
590
+ assert deleted == 0
591
+
592
+ remaining = clean_db.execute("MATCH (n:NoMatch) RETURN count(n)", unnest_result=True)
593
+ assert remaining == 1
594
+
595
+ @pytest.mark.parametrize("test_db", ["memgraph_db", "age_db"], indirect=True)
596
+ def test_delete_empty_filters_raises(self, clean_db):
597
+ """bulk_delete_nodes with empty filters should raise ValueError."""
598
+ with pytest.raises(ValueError, match="filters must not be empty"):
599
+ clean_db.bulk_delete_nodes("SomeLabel", {})
@@ -29,6 +29,10 @@ class MockBackend(CypherBackend):
29
29
  self.next_edge_id = 1000
30
30
  self._commits = 0
31
31
 
32
+ @property
33
+ def connected(self) -> bool:
34
+ return True
35
+
32
36
  # ---- abstract method implementations (no-op/trivial) ----
33
37
  def connect(self, *args, **kwargs): # noqa: D401
34
38
  return None
@@ -170,7 +170,7 @@ class TestDefaultBackendMethods:
170
170
 
171
171
  def test_bulk_create_edges_raises(self):
172
172
  with pytest.raises(NotImplementedError, match="does not support bulk_create_edges"):
173
- self.backend.bulk_create_edges("Label", [{"src": "a", "dst": "b"}])
173
+ self.backend.bulk_create_edges("Label", [{"src": "a", "dst": "b"}], "SrcLabel", "DstLabel")
174
174
 
175
175
  def test_has_capability_false_for_property_index(self):
176
176
  """MinimalBackend doesn't override get_capability, so PROPERTY_INDEX is unsupported."""