structured2graph 0.1.1__py3-none-any.whl
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.
- __init__.py +47 -0
- core/__init__.py +23 -0
- core/hygm/__init__.py +74 -0
- core/hygm/hygm.py +2351 -0
- core/hygm/models/__init__.py +82 -0
- core/hygm/models/graph_models.py +667 -0
- core/hygm/models/llm_models.py +229 -0
- core/hygm/models/operations.py +176 -0
- core/hygm/models/sources.py +68 -0
- core/hygm/models/user_operations.py +139 -0
- core/hygm/strategies/__init__.py +17 -0
- core/hygm/strategies/base.py +36 -0
- core/hygm/strategies/deterministic.py +262 -0
- core/hygm/strategies/llm.py +904 -0
- core/hygm/validation/__init__.py +38 -0
- core/hygm/validation/base.py +194 -0
- core/hygm/validation/graph_schema_validator.py +687 -0
- core/hygm/validation/memgraph_data_validator.py +991 -0
- core/migration_agent.py +1369 -0
- core/schema/spec.json +155 -0
- core/utils/meta_graph.py +108 -0
- database/__init__.py +36 -0
- database/adapters/__init__.py +11 -0
- database/adapters/memgraph.py +318 -0
- database/adapters/mysql.py +311 -0
- database/adapters/postgresql.py +335 -0
- database/analyzer.py +396 -0
- database/factory.py +219 -0
- database/models.py +209 -0
- main.py +518 -0
- query_generation/__init__.py +20 -0
- query_generation/cypher_generator.py +129 -0
- query_generation/schema_utilities.py +88 -0
- structured2graph-0.1.1.dist-info/METADATA +197 -0
- structured2graph-0.1.1.dist-info/RECORD +41 -0
- structured2graph-0.1.1.dist-info/WHEEL +4 -0
- structured2graph-0.1.1.dist-info/entry_points.txt +2 -0
- structured2graph-0.1.1.dist-info/licenses/LICENSE +21 -0
- utils/__init__.py +57 -0
- utils/config.py +235 -0
- utils/environment.py +404 -0
core/schema/spec.json
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": [
|
|
3
|
+
{
|
|
4
|
+
"labels": ["NodeLabel"],
|
|
5
|
+
"count": 1,
|
|
6
|
+
"properties": [
|
|
7
|
+
{
|
|
8
|
+
"key": "property_key_0",
|
|
9
|
+
"count": 1,
|
|
10
|
+
"filling_factor": 100.0,
|
|
11
|
+
"types": [
|
|
12
|
+
{ "type": "Null", "count": 1, "examples": [] },
|
|
13
|
+
{ "type": "String", "count": 1, "examples": [] },
|
|
14
|
+
{ "type": "Boolean", "count": 1, "examples": [] },
|
|
15
|
+
{ "type": "Integer", "count": 1, "examples": [] },
|
|
16
|
+
{ "type": "Float", "count": 1, "examples": [] },
|
|
17
|
+
{ "type": "List", "count": 1, "examples": [] },
|
|
18
|
+
{ "type": "Map", "count": 1, "examples": [] },
|
|
19
|
+
{ "type": "Duration", "count": 1, "examples": [] },
|
|
20
|
+
{ "type": "Date", "count": 1, "examples": [] },
|
|
21
|
+
{ "type": "LocalTime", "count": 1, "examples": [] },
|
|
22
|
+
{ "type": "LocalDateTime", "count": 1, "examples": [] },
|
|
23
|
+
{ "type": "ZonedDateTime", "count": 1, "examples": [] },
|
|
24
|
+
{ "type": "Enum::Name", "count": 1, "examples": [] },
|
|
25
|
+
{ "type": "Point2D", "count": 1, "examples": [] },
|
|
26
|
+
{ "type": "Point3D", "count": 1, "examples": [] }
|
|
27
|
+
],
|
|
28
|
+
"source": {
|
|
29
|
+
"field": "source_table.column_name",
|
|
30
|
+
"transformation": "UPPER(column_name) | null"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"examples": [{}],
|
|
35
|
+
"source": {
|
|
36
|
+
"type": "table|view|file|api|manual",
|
|
37
|
+
"name": "source_table_name",
|
|
38
|
+
"location": "database.schema.table_name",
|
|
39
|
+
"mapping": {
|
|
40
|
+
"labels": ["source_table.type_column | NodeLabel"],
|
|
41
|
+
"id_field": "source_table.id"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
"edges": [
|
|
48
|
+
{
|
|
49
|
+
"edge_type": "RELATIONSHIP_TYPE",
|
|
50
|
+
"start_node_labels": ["StartNodeLabel"],
|
|
51
|
+
"end_node_labels": ["EndNodeLabel"],
|
|
52
|
+
"count": 1,
|
|
53
|
+
"properties": [
|
|
54
|
+
{
|
|
55
|
+
"key": "edge_property",
|
|
56
|
+
"count": 1,
|
|
57
|
+
"filling_factor": 100.0,
|
|
58
|
+
"types": [{ "type": "String", "count": 1, "examples": [] }],
|
|
59
|
+
"source": {
|
|
60
|
+
"field": "many_to_many_table.property_column",
|
|
61
|
+
"transformation": "DATE(created_at) | null"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"examples": [{}],
|
|
66
|
+
"source": {
|
|
67
|
+
"type": "table|view|many_to_many|derived",
|
|
68
|
+
"name": "relationships_table",
|
|
69
|
+
"location": "database.schema.relationships_table",
|
|
70
|
+
"mapping": {
|
|
71
|
+
"start_node": "relationships_table.start_id",
|
|
72
|
+
"end_node": "relationships_table.end_id",
|
|
73
|
+
"edge_type": "RELATIONSHIP_TYPE | relationships_table.type_column"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
|
|
79
|
+
"node_indexes": [
|
|
80
|
+
{
|
|
81
|
+
"labels": ["NodeLabel"],
|
|
82
|
+
"properties": ["indexed_property"],
|
|
83
|
+
"count": 0,
|
|
84
|
+
"examples": [{}],
|
|
85
|
+
"type": "label|label+property|label+property_point|label_text|label+property_text|label+property_vector",
|
|
86
|
+
"source": {
|
|
87
|
+
"origin": "migration_requirement|performance_optimization|source_database_index",
|
|
88
|
+
"reason": "unique_constraint|query_performance|full_text_search|vector_similarity",
|
|
89
|
+
"created_by": "migration_agent|dba_team|application_requirement",
|
|
90
|
+
"index_name": "idx_node_property | null",
|
|
91
|
+
"migrated_from": "database.schema.table_name | null"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
|
|
96
|
+
"edge_indexes": [
|
|
97
|
+
{
|
|
98
|
+
"edge_type": "RELATIONSHIP_TYPE",
|
|
99
|
+
"properties": ["indexed_edge_property"],
|
|
100
|
+
"count": 0,
|
|
101
|
+
"examples": [{}],
|
|
102
|
+
"type": "edge_type|edge_type+property|edge_type+property_point|edge_type_text|edge_type+property_text|edge_type+property_vector",
|
|
103
|
+
"source": {
|
|
104
|
+
"origin": "migration_requirement|performance_optimization|source_database_index",
|
|
105
|
+
"reason": "temporal_queries|relationship_traversal|full_text_search|vector_similarity",
|
|
106
|
+
"created_by": "migration_agent|dba_team|application_requirement",
|
|
107
|
+
"index_name": "idx_edge_property | null",
|
|
108
|
+
"migrated_from": "database.schema.table_name | null"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
|
|
113
|
+
"node_constraints": [
|
|
114
|
+
{
|
|
115
|
+
"type": "unique|existence|data_type",
|
|
116
|
+
"labels": ["NodeLabel"],
|
|
117
|
+
"properties": ["constrained_property"],
|
|
118
|
+
"data_type": "String|Integer|Float|Boolean|Date|etc",
|
|
119
|
+
"source": {
|
|
120
|
+
"origin": "source_database_constraint|migration_requirement|data_integrity",
|
|
121
|
+
"constraint_name": "original_constraint_name | null",
|
|
122
|
+
"migrated_from": "database.schema.table_name | null",
|
|
123
|
+
"created_by": "migration_agent|dba_team|data_governance"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
|
|
128
|
+
"edge_constraints": [
|
|
129
|
+
{
|
|
130
|
+
"type": "unique|existence|data_type",
|
|
131
|
+
"edge_type": "RELATIONSHIP_TYPE",
|
|
132
|
+
"properties": ["constrained_edge_property"],
|
|
133
|
+
"data_type": "String|Integer|Float|Boolean|Date|etc",
|
|
134
|
+
"source": {
|
|
135
|
+
"origin": "source_database_constraint|migration_requirement|data_integrity",
|
|
136
|
+
"constraint_name": "original_constraint_name | null",
|
|
137
|
+
"migrated_from": "database.schema.table_name | null",
|
|
138
|
+
"created_by": "migration_agent|dba_team|data_governance"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
|
|
143
|
+
"enums": [
|
|
144
|
+
{
|
|
145
|
+
"name": "EnumName",
|
|
146
|
+
"values": [],
|
|
147
|
+
"source": {
|
|
148
|
+
"origin": "source_database_enum|application_requirement|manual_definition",
|
|
149
|
+
"enum_name": "original_enum_name | null",
|
|
150
|
+
"migrated_from": "database.schema.enum_type | null",
|
|
151
|
+
"created_by": "migration_agent|application_developer|data_architect"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
core/utils/meta_graph.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Shared helpers for HyGM meta graph summaries and keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def node_key(node: Any) -> str:
|
|
9
|
+
"""Generate a stable key for a graph node definition."""
|
|
10
|
+
source = getattr(node, "source", None)
|
|
11
|
+
source_name = getattr(source, "name", None)
|
|
12
|
+
if source_name:
|
|
13
|
+
return f"source::{source_name}"
|
|
14
|
+
labels = sorted(getattr(node, "labels", []))
|
|
15
|
+
return "labels::" + "|".join(labels)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def relationship_key(rel: Any) -> str:
|
|
19
|
+
"""Generate a stable key for a graph relationship."""
|
|
20
|
+
source = getattr(rel, "source", None)
|
|
21
|
+
source_name = getattr(source, "name", None)
|
|
22
|
+
if source_name:
|
|
23
|
+
return f"source::{source_name}"
|
|
24
|
+
edge_type = getattr(rel, "edge_type", "")
|
|
25
|
+
start = "|".join(sorted(getattr(rel, "start_node_labels", [])))
|
|
26
|
+
end = "|".join(sorted(getattr(rel, "end_node_labels", [])))
|
|
27
|
+
return f"edge::{edge_type}:{start}->{end}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def summarize_node(node: Any) -> Dict[str, Any]:
|
|
31
|
+
"""Create a JSON-serializable summary for a node definition."""
|
|
32
|
+
properties = sorted(
|
|
33
|
+
{prop.key for prop in getattr(node, "properties", []) if hasattr(prop, "key")}
|
|
34
|
+
)
|
|
35
|
+
node_source = getattr(node, "source", None)
|
|
36
|
+
mapping: Dict[str, Any] = {}
|
|
37
|
+
source_name = None
|
|
38
|
+
id_field = None
|
|
39
|
+
if node_source:
|
|
40
|
+
mapping = dict(getattr(node_source, "mapping", {}) or {})
|
|
41
|
+
source_name = getattr(node_source, "name", None)
|
|
42
|
+
id_field = mapping.get("id_field")
|
|
43
|
+
return {
|
|
44
|
+
"labels": sorted(getattr(node, "labels", [])),
|
|
45
|
+
"properties": properties,
|
|
46
|
+
"id_field": id_field,
|
|
47
|
+
"source": source_name,
|
|
48
|
+
"mapping": mapping,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def summarize_relationship(rel: Any) -> Dict[str, Any]:
|
|
53
|
+
"""Create a JSON-serializable summary for a relationship."""
|
|
54
|
+
rel_source = getattr(rel, "source", None)
|
|
55
|
+
mapping: Dict[str, Any] = {}
|
|
56
|
+
source_name = None
|
|
57
|
+
source_type = None
|
|
58
|
+
if rel_source:
|
|
59
|
+
mapping = dict(getattr(rel_source, "mapping", {}) or {})
|
|
60
|
+
source_name = getattr(rel_source, "name", None)
|
|
61
|
+
source_type = getattr(rel_source, "type", None)
|
|
62
|
+
start_labels = sorted(getattr(rel, "start_node_labels", []))
|
|
63
|
+
end_labels = sorted(getattr(rel, "end_node_labels", []))
|
|
64
|
+
start_table = mapping.get("from_table")
|
|
65
|
+
end_table = mapping.get("to_table")
|
|
66
|
+
if not start_table and mapping.get("start_node"):
|
|
67
|
+
start_table = str(mapping["start_node"]).split(".")[0]
|
|
68
|
+
if not end_table and mapping.get("end_node"):
|
|
69
|
+
end_table = str(mapping["end_node"]).split(".")[0]
|
|
70
|
+
return {
|
|
71
|
+
"edge_type": getattr(rel, "edge_type", ""),
|
|
72
|
+
"start": start_labels,
|
|
73
|
+
"end": end_labels,
|
|
74
|
+
"source": source_name,
|
|
75
|
+
"source_type": source_type,
|
|
76
|
+
"mapping": mapping,
|
|
77
|
+
"start_table": start_table,
|
|
78
|
+
"end_table": end_table,
|
|
79
|
+
"join_table": mapping.get("join_table"),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def summarize_nodes(nodes: List[Any]) -> Dict[str, Dict[str, Any]]:
|
|
84
|
+
"""Build summaries for all provided nodes."""
|
|
85
|
+
summaries: Dict[str, Dict[str, Any]] = {}
|
|
86
|
+
for node in nodes:
|
|
87
|
+
summaries[node_key(node)] = summarize_node(node)
|
|
88
|
+
return summaries
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def summarize_relationships(
|
|
92
|
+
relationships: List[Any],
|
|
93
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
94
|
+
"""Build summaries for all provided relationships."""
|
|
95
|
+
summaries: Dict[str, Dict[str, Any]] = {}
|
|
96
|
+
for relationship in relationships:
|
|
97
|
+
summaries[relationship_key(relationship)] = summarize_relationship(relationship)
|
|
98
|
+
return summaries
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = [
|
|
102
|
+
"node_key",
|
|
103
|
+
"relationship_key",
|
|
104
|
+
"summarize_node",
|
|
105
|
+
"summarize_relationship",
|
|
106
|
+
"summarize_nodes",
|
|
107
|
+
"summarize_relationships",
|
|
108
|
+
]
|
database/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database analysis package interface.
|
|
3
|
+
|
|
4
|
+
This module provides the main entry point for database analysis functionality,
|
|
5
|
+
importing and exposing all the necessary components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Core data models
|
|
9
|
+
from .models import (
|
|
10
|
+
TableType,
|
|
11
|
+
ColumnInfo,
|
|
12
|
+
ForeignKeyInfo,
|
|
13
|
+
TableInfo,
|
|
14
|
+
RelationshipInfo,
|
|
15
|
+
DatabaseStructure,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Base interfaces
|
|
19
|
+
from .analyzer import DatabaseAnalyzer
|
|
20
|
+
|
|
21
|
+
# Factory (imported from existing factory module)
|
|
22
|
+
from .factory import DatabaseAnalyzerFactory
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Data models
|
|
26
|
+
"TableType",
|
|
27
|
+
"ColumnInfo",
|
|
28
|
+
"ForeignKeyInfo",
|
|
29
|
+
"TableInfo",
|
|
30
|
+
"RelationshipInfo",
|
|
31
|
+
"DatabaseStructure",
|
|
32
|
+
# Interfaces
|
|
33
|
+
"DatabaseAnalyzer",
|
|
34
|
+
# Factory
|
|
35
|
+
"DatabaseAnalyzerFactory",
|
|
36
|
+
]
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memgraph database adapter for schema validation.
|
|
3
|
+
|
|
4
|
+
This adapter provides connection and query capabilities for Memgraph
|
|
5
|
+
to support post-migration validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import neo4j
|
|
14
|
+
except ImportError:
|
|
15
|
+
neo4j = None
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import mgclient
|
|
19
|
+
except ImportError:
|
|
20
|
+
mgclient = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MemgraphConnectionConfig:
|
|
28
|
+
"""Configuration for Memgraph connection."""
|
|
29
|
+
|
|
30
|
+
host: str = "localhost"
|
|
31
|
+
port: int = 7687
|
|
32
|
+
username: str = ""
|
|
33
|
+
password: str = ""
|
|
34
|
+
use_ssl: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MemgraphAdapter:
|
|
38
|
+
"""
|
|
39
|
+
Adapter for connecting to and querying Memgraph database.
|
|
40
|
+
|
|
41
|
+
Supports both mgclient and neo4j driver connections.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config: MemgraphConnectionConfig):
|
|
45
|
+
"""
|
|
46
|
+
Initialize Memgraph adapter.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config: Connection configuration
|
|
50
|
+
"""
|
|
51
|
+
self.config = config
|
|
52
|
+
self.connection = None
|
|
53
|
+
self.driver = None
|
|
54
|
+
self._connection_type = None
|
|
55
|
+
|
|
56
|
+
def connect(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Establish connection to Memgraph.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if connection successful, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
# Try mgclient first (native Memgraph client)
|
|
64
|
+
if mgclient and self._try_mgclient_connection():
|
|
65
|
+
self._connection_type = "mgclient"
|
|
66
|
+
logger.info("Connected to Memgraph using mgclient")
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Fallback to neo4j driver (compatible with Memgraph)
|
|
70
|
+
if neo4j and self._try_neo4j_connection():
|
|
71
|
+
self._connection_type = "neo4j"
|
|
72
|
+
logger.info("Connected to Memgraph using neo4j driver")
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
logger.error("Failed to connect to Memgraph with available drivers")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _try_mgclient_connection(self) -> bool:
|
|
79
|
+
"""Try to connect using mgclient."""
|
|
80
|
+
try:
|
|
81
|
+
self.connection = mgclient.connect(
|
|
82
|
+
host=self.config.host,
|
|
83
|
+
port=self.config.port,
|
|
84
|
+
username=self.config.username,
|
|
85
|
+
password=self.config.password,
|
|
86
|
+
sslmode=mgclient.SSLMode.REQUIRE
|
|
87
|
+
if self.config.use_ssl
|
|
88
|
+
else mgclient.SSLMode.DISABLE,
|
|
89
|
+
)
|
|
90
|
+
# Test the connection
|
|
91
|
+
cursor = self.connection.cursor()
|
|
92
|
+
cursor.execute("RETURN 1")
|
|
93
|
+
cursor.fetchall()
|
|
94
|
+
return True
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.debug(f"mgclient connection failed: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def _try_neo4j_connection(self) -> bool:
|
|
100
|
+
"""Try to connect using neo4j driver."""
|
|
101
|
+
try:
|
|
102
|
+
uri = f"{'bolt+s' if self.config.use_ssl else 'bolt'}://{self.config.host}:{self.config.port}"
|
|
103
|
+
self.driver = neo4j.GraphDatabase.driver(
|
|
104
|
+
uri,
|
|
105
|
+
auth=(self.config.username, self.config.password)
|
|
106
|
+
if self.config.username
|
|
107
|
+
else None,
|
|
108
|
+
)
|
|
109
|
+
# Test the connection
|
|
110
|
+
with self.driver.session() as session:
|
|
111
|
+
session.run("RETURN 1")
|
|
112
|
+
return True
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.debug(f"neo4j driver connection failed: {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def execute_query(self, query: str) -> List[Tuple]:
|
|
118
|
+
"""
|
|
119
|
+
Execute a query and return results.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
query: Cypher query to execute
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of result tuples
|
|
126
|
+
"""
|
|
127
|
+
if not self.connection and not self.driver:
|
|
128
|
+
raise RuntimeError("Not connected to Memgraph")
|
|
129
|
+
|
|
130
|
+
if self._connection_type == "mgclient":
|
|
131
|
+
return self._execute_mgclient_query(query)
|
|
132
|
+
elif self._connection_type == "neo4j":
|
|
133
|
+
return self._execute_neo4j_query(query)
|
|
134
|
+
else:
|
|
135
|
+
raise RuntimeError("Unknown connection type")
|
|
136
|
+
|
|
137
|
+
def _execute_mgclient_query(self, query: str) -> List[Tuple]:
|
|
138
|
+
"""Execute query using mgclient."""
|
|
139
|
+
cursor = self.connection.cursor()
|
|
140
|
+
cursor.execute(query)
|
|
141
|
+
return cursor.fetchall()
|
|
142
|
+
|
|
143
|
+
def _execute_neo4j_query(self, query: str) -> List[Tuple]:
|
|
144
|
+
"""Execute query using neo4j driver."""
|
|
145
|
+
with self.driver.session() as session:
|
|
146
|
+
result = session.run(query)
|
|
147
|
+
return [tuple(record.values()) for record in result]
|
|
148
|
+
|
|
149
|
+
def get_schema_info(self) -> List[Tuple]:
|
|
150
|
+
"""
|
|
151
|
+
Get schema information from Memgraph.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Schema information as list of tuples
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
# Try SHOW SCHEMA INFO first (newer Memgraph versions)
|
|
158
|
+
return self.execute_query("SHOW SCHEMA INFO")
|
|
159
|
+
except Exception:
|
|
160
|
+
# Fallback to alternative schema queries
|
|
161
|
+
logger.warning(
|
|
162
|
+
"SHOW SCHEMA INFO not available, using alternative schema detection"
|
|
163
|
+
)
|
|
164
|
+
return self._get_schema_info_alternative()
|
|
165
|
+
|
|
166
|
+
def _get_schema_info_alternative(self) -> List[Tuple]:
|
|
167
|
+
"""
|
|
168
|
+
Alternative method to get schema info for older Memgraph versions.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Schema information as list of tuples
|
|
172
|
+
"""
|
|
173
|
+
schema_info = []
|
|
174
|
+
|
|
175
|
+
# Get node labels and their properties
|
|
176
|
+
try:
|
|
177
|
+
node_labels_result = self.execute_query("CALL db.labels()")
|
|
178
|
+
for label_row in node_labels_result:
|
|
179
|
+
label = label_row[0]
|
|
180
|
+
|
|
181
|
+
# Get properties for this label
|
|
182
|
+
props_query = f"MATCH (n:{label}) RETURN DISTINCT keys(n) LIMIT 1"
|
|
183
|
+
props_result = self.execute_query(props_query)
|
|
184
|
+
properties = (
|
|
185
|
+
props_result[0][0] if props_result and props_result[0] else []
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Format as schema info tuple
|
|
189
|
+
schema_info.append(
|
|
190
|
+
("node", label, {prop: "Unknown" for prop in properties})
|
|
191
|
+
)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.warning(f"Failed to get node labels: {e}")
|
|
194
|
+
|
|
195
|
+
# Get relationship types and their properties
|
|
196
|
+
try:
|
|
197
|
+
rel_types_result = self.execute_query("CALL db.relationshipTypes()")
|
|
198
|
+
for rel_row in rel_types_result:
|
|
199
|
+
rel_type = rel_row[0]
|
|
200
|
+
|
|
201
|
+
# Get properties for this relationship type
|
|
202
|
+
props_query = (
|
|
203
|
+
f"MATCH ()-[r:{rel_type}]-() RETURN DISTINCT keys(r) LIMIT 1"
|
|
204
|
+
)
|
|
205
|
+
props_result = self.execute_query(props_query)
|
|
206
|
+
properties = (
|
|
207
|
+
props_result[0][0] if props_result and props_result[0] else []
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Format as schema info tuple
|
|
211
|
+
schema_info.append(
|
|
212
|
+
("relationship", rel_type, {prop: "Unknown" for prop in properties})
|
|
213
|
+
)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"Failed to get relationship types: {e}")
|
|
216
|
+
|
|
217
|
+
return schema_info
|
|
218
|
+
|
|
219
|
+
def get_indexes(self) -> List[Dict[str, Any]]:
|
|
220
|
+
"""
|
|
221
|
+
Get index information from Memgraph.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of index information dictionaries
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
indexes_result = self.execute_query("SHOW INDEX INFO")
|
|
228
|
+
indexes = []
|
|
229
|
+
|
|
230
|
+
for index_row in indexes_result:
|
|
231
|
+
# Parse index information based on Memgraph's SHOW INDEX INFO format
|
|
232
|
+
if len(index_row) >= 3:
|
|
233
|
+
index_info = {
|
|
234
|
+
"name": index_row[0],
|
|
235
|
+
"type": index_row[1],
|
|
236
|
+
"properties": index_row[2] if len(index_row) > 2 else [],
|
|
237
|
+
}
|
|
238
|
+
indexes.append(index_info)
|
|
239
|
+
|
|
240
|
+
return indexes
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning(f"Failed to get index information: {e}")
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
def get_constraints(self) -> List[Dict[str, Any]]:
|
|
246
|
+
"""
|
|
247
|
+
Get constraint information from Memgraph.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
List of constraint information dictionaries
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
constraints_result = self.execute_query("SHOW CONSTRAINT INFO")
|
|
254
|
+
constraints = []
|
|
255
|
+
|
|
256
|
+
for constraint_row in constraints_result:
|
|
257
|
+
# Parse constraint information based on Memgraph's format
|
|
258
|
+
if len(constraint_row) >= 3:
|
|
259
|
+
constraint_info = {
|
|
260
|
+
"name": constraint_row[0],
|
|
261
|
+
"type": constraint_row[1],
|
|
262
|
+
"properties": constraint_row[2]
|
|
263
|
+
if len(constraint_row) > 2
|
|
264
|
+
else [],
|
|
265
|
+
}
|
|
266
|
+
constraints.append(constraint_info)
|
|
267
|
+
|
|
268
|
+
return constraints
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.warning(f"Failed to get constraint information: {e}")
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
def close(self):
|
|
274
|
+
"""Close the database connection."""
|
|
275
|
+
if self.connection:
|
|
276
|
+
self.connection.close()
|
|
277
|
+
self.connection = None
|
|
278
|
+
if self.driver:
|
|
279
|
+
self.driver.close()
|
|
280
|
+
self.driver = None
|
|
281
|
+
|
|
282
|
+
logger.info("Memgraph connection closed")
|
|
283
|
+
|
|
284
|
+
def __enter__(self):
|
|
285
|
+
"""Context manager entry."""
|
|
286
|
+
self.connect()
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
290
|
+
"""Context manager exit."""
|
|
291
|
+
self.close()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def create_memgraph_connection(
|
|
295
|
+
host: str = "localhost",
|
|
296
|
+
port: int = 7687,
|
|
297
|
+
username: str = "",
|
|
298
|
+
password: str = "",
|
|
299
|
+
use_ssl: bool = False,
|
|
300
|
+
) -> MemgraphAdapter:
|
|
301
|
+
"""
|
|
302
|
+
Create a Memgraph connection adapter.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
host: Memgraph host
|
|
306
|
+
port: Memgraph port
|
|
307
|
+
username: Username for authentication
|
|
308
|
+
password: Password for authentication
|
|
309
|
+
use_ssl: Whether to use SSL connection
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
MemgraphAdapter instance
|
|
313
|
+
"""
|
|
314
|
+
config = MemgraphConnectionConfig(
|
|
315
|
+
host=host, port=port, username=username, password=password, use_ssl=use_ssl
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return MemgraphAdapter(config)
|