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.
- {cypher_graphdb-0.2.4/src/cypher_graphdb.egg-info → cypher_graphdb-0.2.6}/PKG-INFO +1 -1
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backend.py +49 -4
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agebulkwriter.py +108 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agegraphdb.py +74 -28
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/memgraphdb.py +4 -4
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/cyphergraphdb.py +5 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/indexing.py +22 -4
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/dbpool.py +7 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6/src/cypher_graphdb.egg-info}/PKG-INFO +1 -1
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_indexes_and_bulk.py +104 -1
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/mock_backend.py +4 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_indexing.py +1 -1
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.age.example +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.example +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.env.memgraph.example +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/ci.yml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/publish.yml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.github/workflows/release.yml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.gitignore +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/.pre-commit-config.yaml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/AGENTS.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/CHANGELOG.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/CONTRIBUTING.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/LICENSE.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/README.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/Taskfile.yml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/cli +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/TODO.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/changelog.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/material.css +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/mkdocstrings.css +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/css/style.css +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/duckdb-migration.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/falkordb-integration.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/hierarchical-export-format.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/multiple-statement-execution.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/parameterized-queries-for-typed-models.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/query-result-immutable-design.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/read-only-mode.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/design/stateless-multi-graph-support.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/documentation-guide.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/examples/docstring_examples.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/backends/age/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/backends/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/cli/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/cypher/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/reference/cypher_graphdb/tools/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/docs/usage/index.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/mkdocs.yml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/pyproject.toml +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/scripts/gen_ref_nav.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/setup.cfg +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/__main__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/args.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backendprovider.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agerowfactories.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agesearch.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/ageserializer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agesqlbuilder.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agtype.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agtype_parser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/memgraphrowfactories.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cardinality.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/app.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/banner.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_manager.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_map.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/command_registry.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/add_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/apply_config_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/base_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/clear_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/commit_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/connect_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_edge_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_linked_node_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/create_node_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/delete_graphobj_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/disconnect_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/drop_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_backends_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_graphs_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_indexes_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_labels_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_models_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_parsed_query_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_schema_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/dump_statistics_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/execute_cypher_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/execute_file_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/exit_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/export_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_all_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_edges_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/fetch_nodes_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/format_output_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/get_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/gid_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_exists_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_op_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/graph_to_tree_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/help_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/import_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/last_result_op_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/load_models_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/resolve_edges_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/rollback_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/search_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/set_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/sql_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/update_graphobj_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/commands/use_graph_command.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/completer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/config.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/file_executor.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/graphdata.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/graphdb.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/_overview.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/_template.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/add_graph.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/exit.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/export_graph.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/import_graph.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help/last_result.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/help.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/prompt.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/promptparser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/provider.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/renderer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/runtime.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/schema_cmd.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cli/settings.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/command_reader.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/config.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/Cypher.interp +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/Cypher.tokens +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherBaseListener.java +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.interp +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.java +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherLexer.tokens +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherListener.java +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/.antlr/CypherParser.java +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.g4 +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.interp +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/Cypher.tokens +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.interp +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherLexer.tokens +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherListener.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/CypherParser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypher/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherbuilder.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/batch.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/bulk_normalize.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/connection.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/criteria.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/result.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/schema.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/search.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/sql.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/stream_mixin.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherjson.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cypherparser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/decorators.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/display.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/exceptions.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/graphops.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/main.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/modelinfo.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/modelprovider.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/models.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/options.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/converter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/core.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/schema/generator.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/settings.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/statistics.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/base_exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/base_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/csv_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/data_flattener.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/excel_row_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/file_exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/file_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_exporter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/hierarchical_row_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/json_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/json_yaml_data_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_collector.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_set.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/row_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/tabular_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/tools/yaml_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/collection_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/column_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/connection_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/conversion_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/core_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_merge.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_to_llm.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/schema_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/settings_repr.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/utils/string_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/SOURCES.txt +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/dependency_links.txt +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/entry_points.txt +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/requires.txt +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb.egg-info/top_level.txt +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/README.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/conftest.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/conftest.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_basic_operations.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_create_or_merge.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_example.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_parameters.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_read_only_mode.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/integration/test_streaming.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/test_json_schema_loading.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/test_json_schema_vs_decorators.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/README.md +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/test_cmd_map.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/cli/test_command_registry.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_age_serializer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_age_sqlbuilder.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_agtype_parser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_backend_capabilities.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_bulk_normalize.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_column_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_command_reader.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_cypherbuilder.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_cypherparser.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_dbpool.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_dict_access_mixin.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_extend_relation_decorator.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_extend_relations.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_graph_id_zero.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_graphops.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_model_inheritance.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelinfo.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelprovider_loading.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_modelprovider_schemas.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_models.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/test_schema_converter.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/__init__.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_data_flattener.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_exporters.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_hierarchical_importer.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_json_yaml_data_source.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_resource_management.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/tools/test_tabular_import.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_collection_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_connection_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_conversion_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_core_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_schema_merge.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_schema_utils.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_settings_repr.py +0 -0
- {cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/tests/unit/utils/test_string_utils.py +0 -0
- {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.
|
|
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
|
|
375
|
-
dst_label: Label of destination nodes
|
|
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."""
|
{cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/age/agebulkwriter.py
RENAMED
|
@@ -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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
727
|
-
dst_label: Label of destination nodes
|
|
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 (
|
|
745
|
-
src_pat = f"(a:{src_label} {{{src_ref_prop}: e.src}})"
|
|
746
|
-
dst_pat = f"(b:{dst_label} {{{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):
|
{cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/backends/memgraph/memgraphdb.py
RENAMED
|
@@ -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
|
|
559
|
-
dst_label: Label of destination nodes
|
|
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.
|
{cypher_graphdb-0.2.4 → cypher_graphdb-0.2.6}/src/cypher_graphdb/cyphergraphdb/cyphergraphdb.py
RENAMED
|
@@ -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
|
|
173
|
-
dst_label: Label of destination nodes
|
|
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.
|
|
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."""
|