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
main.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# flake8: noqa
|
|
3
|
+
"""
|
|
4
|
+
SQL Database to Graph Migration Agent - Main Entry Point
|
|
5
|
+
|
|
6
|
+
This is the main entry point for the SQL database to graph migration agent.
|
|
7
|
+
Run with: uv run main.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Dict, Any, Optional
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Add current directory to Python path for absolute imports
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
|
+
|
|
20
|
+
from utils import ( # noqa: E402
|
|
21
|
+
MigrationEnvironmentError,
|
|
22
|
+
DatabaseConnectionError,
|
|
23
|
+
setup_and_validate_environment,
|
|
24
|
+
probe_all_connections,
|
|
25
|
+
print_environment_help,
|
|
26
|
+
print_troubleshooting_help,
|
|
27
|
+
)
|
|
28
|
+
from core import SQLToMemgraphAgent # noqa: E402
|
|
29
|
+
from core.hygm import GraphModelingStrategy, ModelingMode # noqa: E402
|
|
30
|
+
|
|
31
|
+
# Configure logging
|
|
32
|
+
logging.basicConfig(
|
|
33
|
+
level=logging.INFO,
|
|
34
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
MODE_CHOICES = {
|
|
40
|
+
"automatic": ModelingMode.AUTOMATIC,
|
|
41
|
+
"incremental": ModelingMode.INCREMENTAL,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
STRATEGY_CHOICES = {
|
|
45
|
+
"deterministic": GraphModelingStrategy.DETERMINISTIC,
|
|
46
|
+
"llm": GraphModelingStrategy.LLM_POWERED,
|
|
47
|
+
"llm_powered": GraphModelingStrategy.LLM_POWERED,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
META_GRAPH_POLICIES = {"auto", "skip", "reset"}
|
|
51
|
+
|
|
52
|
+
LOG_LEVEL_CHOICES = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
|
|
53
|
+
|
|
54
|
+
PROVIDER_CHOICES = ["openai", "anthropic", "gemini"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lower_env(name: str) -> Optional[str]:
|
|
58
|
+
value = os.getenv(name)
|
|
59
|
+
return value.lower() if value else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _upper_env(name: str) -> Optional[str]:
|
|
63
|
+
value = os.getenv(name)
|
|
64
|
+
return value.upper() if value else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_cli_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
|
68
|
+
"""Parse command-line arguments for the migration agent."""
|
|
69
|
+
|
|
70
|
+
env_mode = _lower_env("SQL2MG_MODE")
|
|
71
|
+
env_strategy = _lower_env("SQL2MG_STRATEGY")
|
|
72
|
+
env_meta_policy = _lower_env("SQL2MG_META_POLICY")
|
|
73
|
+
env_log_level = _upper_env("SQL2MG_LOG_LEVEL")
|
|
74
|
+
env_provider = _lower_env("LLM_PROVIDER")
|
|
75
|
+
env_model = os.getenv("LLM_MODEL")
|
|
76
|
+
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
description="SQL database to graph migration agent",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--mode",
|
|
83
|
+
choices=sorted(MODE_CHOICES.keys()),
|
|
84
|
+
default=env_mode,
|
|
85
|
+
type=str.lower,
|
|
86
|
+
help="Graph modeling mode (automatic|incremental). Overrides SQL2MG_MODE.",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--strategy",
|
|
91
|
+
choices=["deterministic", "llm"],
|
|
92
|
+
default=env_strategy,
|
|
93
|
+
type=str.lower,
|
|
94
|
+
help="Graph modeling strategy (deterministic|llm). Overrides SQL2MG_STRATEGY.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--provider",
|
|
99
|
+
choices=PROVIDER_CHOICES,
|
|
100
|
+
default=env_provider,
|
|
101
|
+
type=str.lower,
|
|
102
|
+
help=(
|
|
103
|
+
"LLM provider (openai|anthropic|gemini). "
|
|
104
|
+
"Overrides LLM_PROVIDER. Auto-detects if not specified."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--model",
|
|
110
|
+
default=env_model,
|
|
111
|
+
help=(
|
|
112
|
+
"LLM model name. Overrides LLM_MODEL. "
|
|
113
|
+
"Uses provider default if not specified."
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--meta-graph",
|
|
119
|
+
choices=sorted(META_GRAPH_POLICIES),
|
|
120
|
+
default=env_meta_policy,
|
|
121
|
+
type=str.lower,
|
|
122
|
+
help=(
|
|
123
|
+
"Meta graph policy: auto (default), skip stored metadata, or reset to "
|
|
124
|
+
"ignore previous migrations. Overrides SQL2MG_META_POLICY."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--log-level",
|
|
130
|
+
choices=LOG_LEVEL_CHOICES,
|
|
131
|
+
default=env_log_level,
|
|
132
|
+
type=str.upper,
|
|
133
|
+
help="Logging level for the agent. Overrides SQL2MG_LOG_LEVEL.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return parser.parse_args(argv)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _configure_log_level(level_name: Optional[str]) -> None:
|
|
140
|
+
"""Configure global logging level if provided."""
|
|
141
|
+
|
|
142
|
+
if not level_name:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
numeric_level = getattr(logging, level_name.upper(), None)
|
|
146
|
+
if not isinstance(numeric_level, int):
|
|
147
|
+
logger.warning("Unknown log level '%s'; falling back to INFO", level_name)
|
|
148
|
+
numeric_level = logging.INFO
|
|
149
|
+
|
|
150
|
+
logging.getLogger().setLevel(numeric_level)
|
|
151
|
+
for handler in logging.getLogger().handlers:
|
|
152
|
+
handler.setLevel(numeric_level)
|
|
153
|
+
logger.setLevel(numeric_level)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_mode(cli_mode: Optional[str]) -> Optional[ModelingMode]:
|
|
157
|
+
if not cli_mode:
|
|
158
|
+
return None
|
|
159
|
+
resolved = MODE_CHOICES.get(cli_mode)
|
|
160
|
+
if not resolved:
|
|
161
|
+
logger.warning("Unrecognised mode '%s'; falling back to prompt", cli_mode)
|
|
162
|
+
return resolved
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_strategy(cli_strategy: Optional[str]) -> Optional[GraphModelingStrategy]:
|
|
166
|
+
if not cli_strategy:
|
|
167
|
+
return None
|
|
168
|
+
resolved = STRATEGY_CHOICES.get(cli_strategy)
|
|
169
|
+
if not resolved:
|
|
170
|
+
logger.warning(
|
|
171
|
+
"Unrecognised strategy '%s'; falling back to prompt",
|
|
172
|
+
cli_strategy,
|
|
173
|
+
)
|
|
174
|
+
return resolved
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def print_banner() -> None:
|
|
178
|
+
"""Print application banner."""
|
|
179
|
+
print("=" * 60)
|
|
180
|
+
print("🚀 SQL Database to Graph Migration Agent")
|
|
181
|
+
print("=" * 60)
|
|
182
|
+
print("Intelligent database migration with LLM-powered analysis")
|
|
183
|
+
print()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_graph_modeling_mode() -> ModelingMode:
|
|
187
|
+
"""
|
|
188
|
+
Get user choice for graph modeling mode.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
ModelingMode: Selected modeling mode
|
|
192
|
+
"""
|
|
193
|
+
print("Graph modeling mode:")
|
|
194
|
+
print()
|
|
195
|
+
print(" 1. Automatic - Generate graph model without prompts")
|
|
196
|
+
print()
|
|
197
|
+
print(" 2. Incremental - Review each table with end-of-session refinement")
|
|
198
|
+
print()
|
|
199
|
+
|
|
200
|
+
while True:
|
|
201
|
+
try:
|
|
202
|
+
choice = input("Select mode (1-2) or press Enter for automatic: ").strip()
|
|
203
|
+
if not choice:
|
|
204
|
+
return ModelingMode.AUTOMATIC # Default to automatic
|
|
205
|
+
|
|
206
|
+
if choice == "1":
|
|
207
|
+
return ModelingMode.AUTOMATIC
|
|
208
|
+
elif choice == "2":
|
|
209
|
+
return ModelingMode.INCREMENTAL
|
|
210
|
+
else:
|
|
211
|
+
print("Invalid choice. Please select 1-2.")
|
|
212
|
+
except ValueError:
|
|
213
|
+
print("Invalid input. Please enter 1-2.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def get_graph_modeling_strategy() -> GraphModelingStrategy:
|
|
217
|
+
"""
|
|
218
|
+
Get user choice for graph modeling strategy.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
GraphModelingStrategy: Selected strategy
|
|
222
|
+
"""
|
|
223
|
+
print("Graph modeling strategy:")
|
|
224
|
+
print()
|
|
225
|
+
print(" 1. Deterministic - Rule-based graph model creation ")
|
|
226
|
+
print()
|
|
227
|
+
print(" 2. AI - LLM-based graph model creation (full HyGM capabilities)")
|
|
228
|
+
print()
|
|
229
|
+
print()
|
|
230
|
+
|
|
231
|
+
while True:
|
|
232
|
+
try:
|
|
233
|
+
choice = input(
|
|
234
|
+
"Select strategy (1-2) or press Enter for deterministic: "
|
|
235
|
+
).strip()
|
|
236
|
+
if not choice:
|
|
237
|
+
return GraphModelingStrategy.DETERMINISTIC # Default
|
|
238
|
+
|
|
239
|
+
if choice == "1":
|
|
240
|
+
return GraphModelingStrategy.DETERMINISTIC
|
|
241
|
+
elif choice == "2":
|
|
242
|
+
return GraphModelingStrategy.LLM_POWERED
|
|
243
|
+
else:
|
|
244
|
+
print("Invalid choice. Please select 1-2.")
|
|
245
|
+
except ValueError:
|
|
246
|
+
print("Invalid input. Please enter 1-2.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run_migration(
|
|
250
|
+
source_db_config: Dict[str, Any],
|
|
251
|
+
memgraph_config: Dict[str, Any],
|
|
252
|
+
modeling_mode: ModelingMode,
|
|
253
|
+
graph_modeling_strategy: GraphModelingStrategy,
|
|
254
|
+
meta_graph_policy: str,
|
|
255
|
+
llm_provider: Optional[str] = None,
|
|
256
|
+
llm_model: Optional[str] = None,
|
|
257
|
+
) -> Dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Run the migration with the specified configuration.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
source_db_config: Source database connection configuration
|
|
263
|
+
memgraph_config: Memgraph connection configuration
|
|
264
|
+
modeling_mode: Graph modeling mode (automatic or incremental)
|
|
265
|
+
graph_modeling_strategy: Strategy for graph model creation
|
|
266
|
+
meta_graph_policy: Meta graph handling policy (auto|skip|reset)
|
|
267
|
+
llm_provider: LLM provider (openai|anthropic|gemini)
|
|
268
|
+
llm_model: Specific LLM model name
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Migration result dictionary
|
|
272
|
+
"""
|
|
273
|
+
print("🔧 Creating migration agent...")
|
|
274
|
+
|
|
275
|
+
if modeling_mode == ModelingMode.INCREMENTAL:
|
|
276
|
+
mode_name = "incremental"
|
|
277
|
+
else:
|
|
278
|
+
mode_name = "automatic"
|
|
279
|
+
strategy_name = graph_modeling_strategy.value
|
|
280
|
+
print(f"🎯 Graph modeling: {mode_name} with {strategy_name} strategy")
|
|
281
|
+
|
|
282
|
+
if llm_provider:
|
|
283
|
+
print(f"🤖 LLM Provider: {llm_provider}")
|
|
284
|
+
if llm_model:
|
|
285
|
+
print(f"🎯 Model: {llm_model}")
|
|
286
|
+
print()
|
|
287
|
+
|
|
288
|
+
# Create agent with graph modeling settings
|
|
289
|
+
agent = SQLToMemgraphAgent(
|
|
290
|
+
modeling_mode=modeling_mode,
|
|
291
|
+
graph_modeling_strategy=graph_modeling_strategy,
|
|
292
|
+
meta_graph_policy=meta_graph_policy,
|
|
293
|
+
llm_provider=llm_provider,
|
|
294
|
+
llm_model=llm_model,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
print("🚀 Starting migration workflow...")
|
|
298
|
+
print("This will:")
|
|
299
|
+
print(" 1. 🔍 Analyze source database schema")
|
|
300
|
+
print(" 2. 🎯 Generate graph model with HyGM")
|
|
301
|
+
print(" 3. 📝 Create indexes and constraints")
|
|
302
|
+
print(" 4. ⚙️ Generate migration queries")
|
|
303
|
+
print(" 5. 🔄 Execute migration to Memgraph")
|
|
304
|
+
print(" 6. ✅ Verify the migration results")
|
|
305
|
+
print()
|
|
306
|
+
|
|
307
|
+
# Handle incremental vs automatic mode
|
|
308
|
+
if modeling_mode == ModelingMode.INCREMENTAL:
|
|
309
|
+
print("🔄 Incremental mode: Review LLM-generated graph changes table by table")
|
|
310
|
+
print(" then approve or tweak differences before refining the model")
|
|
311
|
+
print()
|
|
312
|
+
|
|
313
|
+
# Run the migration with the user's chosen settings
|
|
314
|
+
return agent.migrate(source_db_config, memgraph_config)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def print_migration_results(result: Dict[str, Any]) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Print formatted migration results.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
result: Migration result dictionary
|
|
323
|
+
"""
|
|
324
|
+
print("\n" + "=" * 60)
|
|
325
|
+
print("📊 MIGRATION RESULTS")
|
|
326
|
+
print("=" * 60)
|
|
327
|
+
|
|
328
|
+
if result.get("success", False):
|
|
329
|
+
print("✅ Migration completed successfully!")
|
|
330
|
+
else:
|
|
331
|
+
print("❌ Migration encountered errors")
|
|
332
|
+
|
|
333
|
+
# Print error details
|
|
334
|
+
if result.get("errors"):
|
|
335
|
+
print(f"\n🚨 Errors ({len(result['errors'])}):")
|
|
336
|
+
for i, error in enumerate(result["errors"], 1):
|
|
337
|
+
print(f" {i}. {error}")
|
|
338
|
+
|
|
339
|
+
# Print completion stats
|
|
340
|
+
completed = len(result.get("completed_tables", []))
|
|
341
|
+
total = result.get("total_tables", 0)
|
|
342
|
+
print(f"\n📋 Tables processed: {completed}/{total}")
|
|
343
|
+
|
|
344
|
+
# Print post-migration validation results
|
|
345
|
+
validation_report = result.get("validation_report")
|
|
346
|
+
if validation_report:
|
|
347
|
+
print("\n✅ Post-migration Validation:")
|
|
348
|
+
if validation_report.get("success"):
|
|
349
|
+
print(" 🎯 Status: PASSED")
|
|
350
|
+
else:
|
|
351
|
+
print(" ⚠️ Status: Issues found")
|
|
352
|
+
|
|
353
|
+
# Display validation score and metrics if available
|
|
354
|
+
validation_score = validation_report.get("validation_score", 0)
|
|
355
|
+
print(f" 📊 Validation Score: {int(validation_score)}/100")
|
|
356
|
+
|
|
357
|
+
metrics = validation_report.get("metrics")
|
|
358
|
+
if metrics:
|
|
359
|
+
print(f" 📁 Tables: {metrics.tables_covered}/{metrics.tables_total}")
|
|
360
|
+
print(
|
|
361
|
+
f" 🏷️ Properties: {metrics.properties_covered}/{metrics.properties_total}"
|
|
362
|
+
)
|
|
363
|
+
print(
|
|
364
|
+
f" 🔗 Relationships: {metrics.relationships_covered}/{metrics.relationships_total}"
|
|
365
|
+
)
|
|
366
|
+
print(f" 📇 Indexes: {metrics.indexes_covered}/{metrics.indexes_total}")
|
|
367
|
+
print(
|
|
368
|
+
f" 🔒 Constraints: {metrics.constraints_covered}/{metrics.constraints_total}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Show validation issues summary
|
|
372
|
+
issues = validation_report.get("issues", [])
|
|
373
|
+
if issues:
|
|
374
|
+
critical_count = sum(
|
|
375
|
+
1 for issue in issues if issue.get("severity") == "CRITICAL"
|
|
376
|
+
)
|
|
377
|
+
warning_count = sum(
|
|
378
|
+
1 for issue in issues if issue.get("severity") == "WARNING"
|
|
379
|
+
)
|
|
380
|
+
info_count = sum(1 for issue in issues if issue.get("severity") == "INFO")
|
|
381
|
+
|
|
382
|
+
print(
|
|
383
|
+
f" 🚨 Issues: {critical_count} critical, {warning_count} warnings, {info_count} info"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Show top critical issues
|
|
387
|
+
critical_issues = [
|
|
388
|
+
issue for issue in issues if issue.get("severity") == "CRITICAL"
|
|
389
|
+
]
|
|
390
|
+
if critical_issues:
|
|
391
|
+
print(" 📋 Top Critical Issues:")
|
|
392
|
+
for issue in critical_issues[:3]:
|
|
393
|
+
print(f" - {issue.get('message', 'Unknown issue')}")
|
|
394
|
+
else:
|
|
395
|
+
print(" ✅ No validation issues found")
|
|
396
|
+
|
|
397
|
+
# Print schema analysis details
|
|
398
|
+
if result.get("database_structure"):
|
|
399
|
+
structure = result["database_structure"]
|
|
400
|
+
print("\n🔍 Schema Analysis:")
|
|
401
|
+
print(f" 📁 Entity tables: {len(structure.get('entity_tables', {}))}")
|
|
402
|
+
print(f" 🔗 Join tables: {len(structure.get('join_tables', {}))}")
|
|
403
|
+
print(f" 👁️ Views (excluded): {len(structure.get('views', {}))}")
|
|
404
|
+
print(f" 🔄 Relationships: {len(structure.get('relationships', []))}")
|
|
405
|
+
|
|
406
|
+
# Show index/constraint creation results
|
|
407
|
+
if result.get("created_indexes") is not None:
|
|
408
|
+
index_count = len(result.get("created_indexes", []))
|
|
409
|
+
constraint_count = len(result.get("created_constraints", []))
|
|
410
|
+
print(f" 📇 Created indexes: {index_count}")
|
|
411
|
+
print(f" 🔒 Created constraints: {constraint_count}")
|
|
412
|
+
|
|
413
|
+
# Show excluded views
|
|
414
|
+
if structure.get("views"):
|
|
415
|
+
print("\n👁️ Excluded view tables:")
|
|
416
|
+
for table_name, table_info in structure["views"].items():
|
|
417
|
+
row_count = table_info.get("row_count", 0)
|
|
418
|
+
print(f" - {table_name}: {row_count} rows")
|
|
419
|
+
|
|
420
|
+
# Show detected join tables
|
|
421
|
+
if structure.get("join_tables"):
|
|
422
|
+
print("\n🔗 Detected join tables:")
|
|
423
|
+
for table_name, table_info in structure["join_tables"].items():
|
|
424
|
+
fk_count = len(table_info.get("foreign_keys", []))
|
|
425
|
+
row_count = table_info.get("row_count", 0)
|
|
426
|
+
print(f" - {table_name}: {fk_count} FKs, {row_count} rows")
|
|
427
|
+
|
|
428
|
+
# Show relationship breakdown
|
|
429
|
+
relationships_by_type = {}
|
|
430
|
+
for rel in structure.get("relationships", []):
|
|
431
|
+
rel_type = rel["type"]
|
|
432
|
+
if rel_type not in relationships_by_type:
|
|
433
|
+
relationships_by_type[rel_type] = []
|
|
434
|
+
relationships_by_type[rel_type].append(rel)
|
|
435
|
+
|
|
436
|
+
if relationships_by_type:
|
|
437
|
+
print("\n🔄 Relationship breakdown:")
|
|
438
|
+
for rel_type, rels in relationships_by_type.items():
|
|
439
|
+
print(f" - {rel_type}: {len(rels)} relationships")
|
|
440
|
+
|
|
441
|
+
print(f"\n🏁 Final status: {result.get('final_step', 'Unknown')}")
|
|
442
|
+
print("=" * 60)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def main(argv: Optional[list[str]] = None) -> None:
|
|
446
|
+
"""Main entry point for the migration agent."""
|
|
447
|
+
args = parse_cli_args(argv)
|
|
448
|
+
|
|
449
|
+
_configure_log_level(args.log_level)
|
|
450
|
+
|
|
451
|
+
print_banner()
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
# Setup and validate environment
|
|
455
|
+
print("🔧 Setting up environment...")
|
|
456
|
+
source_db_config, memgraph_config = setup_and_validate_environment()
|
|
457
|
+
print("✅ Environment validation completed")
|
|
458
|
+
print()
|
|
459
|
+
|
|
460
|
+
# Probe database connections
|
|
461
|
+
print("🔌 Testing database connections...")
|
|
462
|
+
probe_all_connections(source_db_config, memgraph_config)
|
|
463
|
+
print("✅ All connections verified")
|
|
464
|
+
print()
|
|
465
|
+
|
|
466
|
+
# Get user preferences
|
|
467
|
+
graph_mode = _resolve_mode(args.mode) or get_graph_modeling_mode()
|
|
468
|
+
graph_strategy = (
|
|
469
|
+
_resolve_strategy(args.strategy) or get_graph_modeling_strategy()
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
meta_graph_policy = (args.meta_graph or "auto").lower()
|
|
473
|
+
if meta_graph_policy not in META_GRAPH_POLICIES:
|
|
474
|
+
logger.warning(
|
|
475
|
+
"Unrecognised meta graph policy '%s'; defaulting to auto",
|
|
476
|
+
meta_graph_policy,
|
|
477
|
+
)
|
|
478
|
+
meta_graph_policy = "auto"
|
|
479
|
+
|
|
480
|
+
# Run migration
|
|
481
|
+
result = run_migration(
|
|
482
|
+
source_db_config,
|
|
483
|
+
memgraph_config,
|
|
484
|
+
graph_mode,
|
|
485
|
+
graph_strategy,
|
|
486
|
+
meta_graph_policy,
|
|
487
|
+
llm_provider=args.provider,
|
|
488
|
+
llm_model=args.model,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Display results
|
|
492
|
+
print_migration_results(result)
|
|
493
|
+
|
|
494
|
+
except MigrationEnvironmentError as e:
|
|
495
|
+
print("\n❌ Environment Setup Error:")
|
|
496
|
+
print(str(e))
|
|
497
|
+
print_environment_help()
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
|
|
500
|
+
except DatabaseConnectionError as e:
|
|
501
|
+
print("\n❌ Database Connection Error:")
|
|
502
|
+
print(str(e))
|
|
503
|
+
print_troubleshooting_help()
|
|
504
|
+
sys.exit(1)
|
|
505
|
+
|
|
506
|
+
except KeyboardInterrupt:
|
|
507
|
+
print("\n\n⚠️ Migration cancelled by user")
|
|
508
|
+
sys.exit(0)
|
|
509
|
+
|
|
510
|
+
except Exception as e: # pylint: disable=broad-except
|
|
511
|
+
print(f"\n❌ Unexpected Error: {e}")
|
|
512
|
+
logger.error("Unexpected error in main: %s", e, exc_info=True)
|
|
513
|
+
print_troubleshooting_help()
|
|
514
|
+
sys.exit(1)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
if __name__ == "__main__":
|
|
518
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query generation and schema utilities.
|
|
3
|
+
|
|
4
|
+
This package provides utilities for generating Cypher queries
|
|
5
|
+
and handling schema transformations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Add agents root to path for absolute imports
|
|
12
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from query_generation.cypher_generator import CypherGenerator
|
|
15
|
+
from query_generation.schema_utilities import SchemaUtilities
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CypherGenerator",
|
|
19
|
+
"SchemaUtilities",
|
|
20
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cypher query generation utilities for SQL to graph migration.
|
|
3
|
+
Provides label naming, relationship naming, and index generation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, List, Any, TYPE_CHECKING
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from core.hygm.models.graph_models import GraphIndex, GraphConstraint
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CypherGenerator:
|
|
16
|
+
"""Utilities for Cypher query generation in SQL to graph migration."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the Cypher query generator."""
|
|
20
|
+
|
|
21
|
+
def generate_index_queries_from_hygm(
|
|
22
|
+
self, hygm_indexes: List["GraphIndex"]
|
|
23
|
+
) -> List[str]:
|
|
24
|
+
"""Generate index creation queries from HyGM graph model indexes."""
|
|
25
|
+
queries = []
|
|
26
|
+
|
|
27
|
+
for graph_index in hygm_indexes:
|
|
28
|
+
# Handle node indexes
|
|
29
|
+
if graph_index.labels:
|
|
30
|
+
label = graph_index.labels[0] # Use first label
|
|
31
|
+
for prop in graph_index.properties:
|
|
32
|
+
query = f"CREATE INDEX ON :{label}({prop})"
|
|
33
|
+
queries.append(query.strip())
|
|
34
|
+
|
|
35
|
+
# Handle edge indexes (if supported in future)
|
|
36
|
+
elif graph_index.edge_type:
|
|
37
|
+
# Edge indexes are not commonly used in current versions
|
|
38
|
+
# but we can add support here if needed
|
|
39
|
+
logger.info("Skipping edge index for %s", graph_index.edge_type)
|
|
40
|
+
|
|
41
|
+
return queries
|
|
42
|
+
|
|
43
|
+
def generate_constraint_queries_from_hygm(
|
|
44
|
+
self, hygm_constraints: List["GraphConstraint"]
|
|
45
|
+
) -> List[str]:
|
|
46
|
+
"""Generate constraint creation queries from HyGM graph model."""
|
|
47
|
+
queries = []
|
|
48
|
+
|
|
49
|
+
for graph_constraint in hygm_constraints:
|
|
50
|
+
# Handle node constraints
|
|
51
|
+
if graph_constraint.labels:
|
|
52
|
+
label = graph_constraint.labels[0] # Use first label
|
|
53
|
+
|
|
54
|
+
if graph_constraint.type == "unique":
|
|
55
|
+
for prop in graph_constraint.properties:
|
|
56
|
+
query = (
|
|
57
|
+
f"CREATE CONSTRAINT ON (n:{label}) "
|
|
58
|
+
f"ASSERT n.{prop} IS UNIQUE"
|
|
59
|
+
)
|
|
60
|
+
queries.append(query)
|
|
61
|
+
|
|
62
|
+
# Add support for other constraint types if needed
|
|
63
|
+
elif graph_constraint.type == "existence":
|
|
64
|
+
for prop in graph_constraint.properties:
|
|
65
|
+
query = (
|
|
66
|
+
f"CREATE CONSTRAINT ON (n:{label}) "
|
|
67
|
+
f"ASSERT exists(n.{prop})"
|
|
68
|
+
)
|
|
69
|
+
queries.append(query)
|
|
70
|
+
|
|
71
|
+
return queries
|
|
72
|
+
|
|
73
|
+
def generate_index_queries(
|
|
74
|
+
self, table_name: str, schema: List[Dict[str, Any]]
|
|
75
|
+
) -> List[str]:
|
|
76
|
+
"""Generate index creation queries."""
|
|
77
|
+
queries = []
|
|
78
|
+
label = self._table_name_to_label(table_name)
|
|
79
|
+
|
|
80
|
+
for col in schema:
|
|
81
|
+
if col["key"] in ["PRI", "UNI", "MUL"]:
|
|
82
|
+
query = f"CREATE INDEX ON :{label}({col['field']})"
|
|
83
|
+
queries.append(query.strip())
|
|
84
|
+
|
|
85
|
+
return queries
|
|
86
|
+
|
|
87
|
+
def generate_constraint_queries(
|
|
88
|
+
self, table_name: str, schema: List[Dict[str, Any]]
|
|
89
|
+
) -> List[str]:
|
|
90
|
+
"""Generate constraint creation queries."""
|
|
91
|
+
queries = []
|
|
92
|
+
label = self._table_name_to_label(table_name)
|
|
93
|
+
|
|
94
|
+
# Primary key constraints
|
|
95
|
+
primary_keys = [col["field"] for col in schema if col["key"] == "PRI"]
|
|
96
|
+
for pk in primary_keys:
|
|
97
|
+
query = f"CREATE CONSTRAINT ON (n:{label}) ASSERT n.{pk} IS UNIQUE"
|
|
98
|
+
queries.append(query)
|
|
99
|
+
|
|
100
|
+
# Unique constraints
|
|
101
|
+
unique_keys = [col["field"] for col in schema if col["key"] == "UNI"]
|
|
102
|
+
for uk in unique_keys:
|
|
103
|
+
query = f"CREATE CONSTRAINT ON (n:{label}) ASSERT n.{uk} IS UNIQUE"
|
|
104
|
+
queries.append(query)
|
|
105
|
+
|
|
106
|
+
return queries
|
|
107
|
+
|
|
108
|
+
def _table_name_to_label(self, table_name: str) -> str:
|
|
109
|
+
"""Convert table name to Cypher label."""
|
|
110
|
+
# Convert to PascalCase
|
|
111
|
+
return "".join(word.capitalize() for word in table_name.split("_"))
|
|
112
|
+
|
|
113
|
+
def generate_relationship_type(
|
|
114
|
+
self, to_table: str, join_table: str | None = None
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Generate relationship type based on table names.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
to_table: Target table name
|
|
120
|
+
join_table: Join table name (for many-to-many relationships)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Relationship type in UPPER_CASE format
|
|
124
|
+
"""
|
|
125
|
+
# Table-based naming strategy
|
|
126
|
+
if join_table:
|
|
127
|
+
return self._table_name_to_label(join_table).upper()
|
|
128
|
+
else:
|
|
129
|
+
return f"HAS_{self._table_name_to_label(to_table).upper()}"
|