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.
Files changed (41) hide show
  1. __init__.py +47 -0
  2. core/__init__.py +23 -0
  3. core/hygm/__init__.py +74 -0
  4. core/hygm/hygm.py +2351 -0
  5. core/hygm/models/__init__.py +82 -0
  6. core/hygm/models/graph_models.py +667 -0
  7. core/hygm/models/llm_models.py +229 -0
  8. core/hygm/models/operations.py +176 -0
  9. core/hygm/models/sources.py +68 -0
  10. core/hygm/models/user_operations.py +139 -0
  11. core/hygm/strategies/__init__.py +17 -0
  12. core/hygm/strategies/base.py +36 -0
  13. core/hygm/strategies/deterministic.py +262 -0
  14. core/hygm/strategies/llm.py +904 -0
  15. core/hygm/validation/__init__.py +38 -0
  16. core/hygm/validation/base.py +194 -0
  17. core/hygm/validation/graph_schema_validator.py +687 -0
  18. core/hygm/validation/memgraph_data_validator.py +991 -0
  19. core/migration_agent.py +1369 -0
  20. core/schema/spec.json +155 -0
  21. core/utils/meta_graph.py +108 -0
  22. database/__init__.py +36 -0
  23. database/adapters/__init__.py +11 -0
  24. database/adapters/memgraph.py +318 -0
  25. database/adapters/mysql.py +311 -0
  26. database/adapters/postgresql.py +335 -0
  27. database/analyzer.py +396 -0
  28. database/factory.py +219 -0
  29. database/models.py +209 -0
  30. main.py +518 -0
  31. query_generation/__init__.py +20 -0
  32. query_generation/cypher_generator.py +129 -0
  33. query_generation/schema_utilities.py +88 -0
  34. structured2graph-0.1.1.dist-info/METADATA +197 -0
  35. structured2graph-0.1.1.dist-info/RECORD +41 -0
  36. structured2graph-0.1.1.dist-info/WHEEL +4 -0
  37. structured2graph-0.1.1.dist-info/entry_points.txt +2 -0
  38. structured2graph-0.1.1.dist-info/licenses/LICENSE +21 -0
  39. utils/__init__.py +57 -0
  40. utils/config.py +235 -0
  41. 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()}"