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