@intentsolutionsio/nosql-data-modeler 1.0.0

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.
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Migrates schema from one NoSQL database type to another.
4
+
5
+ This script converts schema definitions between different NoSQL database formats
6
+ (MongoDB, DynamoDB, Firebase, Firestore, etc.) while preserving semantics.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Dict, List, Any, Optional, Tuple
15
+
16
+
17
+ class SchemaTransformer:
18
+ """Transforms schemas between NoSQL database types."""
19
+
20
+ # Database schema patterns
21
+ DB_PATTERNS = {
22
+ "mongodb": {
23
+ "name": "MongoDB",
24
+ "root": "properties",
25
+ "type_mapping": {
26
+ "string": "string",
27
+ "number": "double",
28
+ "integer": "int",
29
+ "boolean": "boolean",
30
+ "date": "date",
31
+ "object": "object",
32
+ "array": "array",
33
+ "null": "null"
34
+ }
35
+ },
36
+ "dynamodb": {
37
+ "name": "DynamoDB",
38
+ "root": "AttributeDefinitions",
39
+ "type_mapping": {
40
+ "string": "S",
41
+ "number": "N",
42
+ "integer": "N",
43
+ "boolean": "BOOL",
44
+ "date": "S",
45
+ "object": "M",
46
+ "array": "L",
47
+ "binary": "B"
48
+ }
49
+ },
50
+ "firestore": {
51
+ "name": "Firestore",
52
+ "root": "fields",
53
+ "type_mapping": {
54
+ "string": "stringValue",
55
+ "number": "doubleValue",
56
+ "integer": "integerValue",
57
+ "boolean": "booleanValue",
58
+ "date": "timestampValue",
59
+ "object": "mapValue",
60
+ "array": "arrayValue",
61
+ "null": "nullValue"
62
+ }
63
+ },
64
+ "cosmosdb": {
65
+ "name": "Azure Cosmos DB",
66
+ "root": "properties",
67
+ "type_mapping": {
68
+ "string": "string",
69
+ "number": "number",
70
+ "integer": "integer",
71
+ "boolean": "boolean",
72
+ "date": "date",
73
+ "object": "object",
74
+ "array": "array"
75
+ }
76
+ }
77
+ }
78
+
79
+ def __init__(self, source_type: str, target_type: str):
80
+ """
81
+ Initialize transformer.
82
+
83
+ Args:
84
+ source_type: Source database type
85
+ target_type: Target database type
86
+
87
+ Raises:
88
+ ValueError: If database types are not supported
89
+ """
90
+ if source_type not in self.DB_PATTERNS:
91
+ raise ValueError(f"Unsupported source type: {source_type}")
92
+ if target_type not in self.DB_PATTERNS:
93
+ raise ValueError(f"Unsupported target type: {target_type}")
94
+
95
+ self.source_type = source_type
96
+ self.target_type = target_type
97
+ self.warnings = []
98
+
99
+ def transform(self, schema: Dict[str, Any]) -> Dict[str, Any]:
100
+ """
101
+ Transform schema from source to target format.
102
+
103
+ Args:
104
+ schema: Schema in source format
105
+
106
+ Returns:
107
+ Schema in target format
108
+ """
109
+ self.warnings = []
110
+
111
+ # Parse source schema
112
+ parsed = self._parse_source_schema(schema)
113
+
114
+ # Transform to target format
115
+ transformed = self._transform_to_target(parsed)
116
+
117
+ return transformed
118
+
119
+ def _parse_source_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]:
120
+ """
121
+ Parse source schema into normalized format.
122
+
123
+ Args:
124
+ schema: Source schema
125
+
126
+ Returns:
127
+ Normalized schema representation
128
+ """
129
+ parsed = {
130
+ "name": schema.get("$id", schema.get("title", "Schema")),
131
+ "description": schema.get("description", ""),
132
+ "fields": {}
133
+ }
134
+
135
+ # Extract fields based on source type
136
+ if self.source_type == "mongodb":
137
+ if "properties" in schema:
138
+ parsed["fields"] = self._extract_mongodb_fields(schema["properties"])
139
+
140
+ elif self.source_type == "dynamodb":
141
+ parsed["fields"] = self._extract_dynamodb_fields(schema)
142
+
143
+ elif self.source_type == "firestore":
144
+ parsed["fields"] = self._extract_firestore_fields(schema)
145
+
146
+ elif self.source_type == "cosmosdb":
147
+ if "properties" in schema:
148
+ parsed["fields"] = self._extract_mongodb_fields(schema["properties"])
149
+
150
+ return parsed
151
+
152
+ def _extract_mongodb_fields(self, properties: Dict[str, Any]) -> Dict[str, Any]:
153
+ """Extract fields from MongoDB schema."""
154
+ fields = {}
155
+
156
+ for field_name, field_def in properties.items():
157
+ fields[field_name] = {
158
+ "type": field_def.get("type", "string"),
159
+ "description": field_def.get("description", ""),
160
+ "required": "required" in str(field_def),
161
+ "indexed": field_def.get("indexed", False)
162
+ }
163
+
164
+ return fields
165
+
166
+ def _extract_dynamodb_fields(self, schema: Dict[str, Any]) -> Dict[str, Any]:
167
+ """Extract fields from DynamoDB schema."""
168
+ fields = {}
169
+
170
+ # DynamoDB representation varies; handle common patterns
171
+ if "AttributeDefinitions" in schema:
172
+ for attr in schema["AttributeDefinitions"]:
173
+ fields[attr["AttributeName"]] = {
174
+ "type": self._map_dynamodb_type(attr["AttributeType"]),
175
+ "required": attr["AttributeName"] in schema.get("KeySchema", [])
176
+ }
177
+
178
+ return fields
179
+
180
+ def _extract_firestore_fields(self, schema: Dict[str, Any]) -> Dict[str, Any]:
181
+ """Extract fields from Firestore schema."""
182
+ fields = {}
183
+
184
+ if "fields" in schema:
185
+ for field_name, field_def in schema["fields"].items():
186
+ field_type = self._extract_firestore_type(field_def)
187
+ fields[field_name] = {
188
+ "type": field_type,
189
+ "indexed": field_def.get("indexed", False)
190
+ }
191
+
192
+ return fields
193
+
194
+ def _transform_to_target(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
195
+ """Transform parsed schema to target format."""
196
+ if self.target_type == "mongodb":
197
+ return self._transform_to_mongodb(parsed)
198
+ elif self.target_type == "dynamodb":
199
+ return self._transform_to_dynamodb(parsed)
200
+ elif self.target_type == "firestore":
201
+ return self._transform_to_firestore(parsed)
202
+ elif self.target_type == "cosmosdb":
203
+ return self._transform_to_cosmosdb(parsed)
204
+
205
+ return parsed
206
+
207
+ def _transform_to_mongodb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
208
+ """Transform to MongoDB schema format."""
209
+ properties = {}
210
+
211
+ for field_name, field_info in parsed["fields"].items():
212
+ properties[field_name] = {
213
+ "type": field_info.get("type", "string"),
214
+ "description": field_info.get("description", "")
215
+ }
216
+
217
+ if field_info.get("indexed"):
218
+ properties[field_name]["indexed"] = True
219
+
220
+ schema = {
221
+ "$schema": "http://json-schema.org/draft-07/schema#",
222
+ "$id": parsed["name"],
223
+ "title": parsed["name"],
224
+ "type": "object",
225
+ "properties": properties
226
+ }
227
+
228
+ return schema
229
+
230
+ def _transform_to_dynamodb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
231
+ """Transform to DynamoDB schema format."""
232
+ attributes = []
233
+
234
+ for field_name, field_info in parsed["fields"].items():
235
+ field_type = field_info.get("type", "string")
236
+ dynamo_type = self._map_type_to_dynamodb(field_type)
237
+
238
+ attributes.append({
239
+ "AttributeName": field_name,
240
+ "AttributeType": dynamo_type
241
+ })
242
+
243
+ schema = {
244
+ "TableName": parsed["name"],
245
+ "AttributeDefinitions": attributes,
246
+ "KeySchema": [
247
+ {"AttributeName": "id", "KeyType": "HASH"}
248
+ ],
249
+ "BillingMode": "PAY_PER_REQUEST"
250
+ }
251
+
252
+ return schema
253
+
254
+ def _transform_to_firestore(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
255
+ """Transform to Firestore schema format."""
256
+ fields = {}
257
+
258
+ for field_name, field_info in parsed["fields"].items():
259
+ field_type = field_info.get("type", "string")
260
+
261
+ fields[field_name] = {
262
+ field_type + "Value": self._get_firestore_default(field_type)
263
+ }
264
+
265
+ if field_info.get("indexed"):
266
+ fields[field_name]["indexed"] = True
267
+
268
+ schema = {
269
+ "name": f"projects/PROJECT_ID/databases/(default)/documents/{parsed['name']}",
270
+ "fields": fields
271
+ }
272
+
273
+ return schema
274
+
275
+ def _transform_to_cosmosdb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
276
+ """Transform to Cosmos DB schema format (similar to MongoDB)."""
277
+ return self._transform_to_mongodb(parsed)
278
+
279
+ def _map_type_to_dynamodb(self, source_type: str) -> str:
280
+ """Map source type to DynamoDB type."""
281
+ type_map = {
282
+ "string": "S",
283
+ "number": "N",
284
+ "integer": "N",
285
+ "boolean": "BOOL",
286
+ "date": "S",
287
+ "object": "M",
288
+ "array": "L"
289
+ }
290
+ return type_map.get(source_type, "S")
291
+
292
+ def _map_dynamodb_type(self, dynamo_type: str) -> str:
293
+ """Map DynamoDB type to normalized type."""
294
+ type_map = {
295
+ "S": "string",
296
+ "N": "number",
297
+ "B": "binary",
298
+ "SS": "string",
299
+ "NS": "number",
300
+ "BS": "binary",
301
+ "M": "object",
302
+ "L": "array",
303
+ "BOOL": "boolean"
304
+ }
305
+ return type_map.get(dynamo_type, "string")
306
+
307
+ def _extract_firestore_type(self, field_def: Dict) -> str:
308
+ """Extract type from Firestore field definition."""
309
+ for key in field_def:
310
+ if key.endswith("Value"):
311
+ type_name = key.replace("Value", "")
312
+ type_map = {
313
+ "string": "string",
314
+ "double": "number",
315
+ "integer": "integer",
316
+ "boolean": "boolean",
317
+ "timestamp": "date",
318
+ "map": "object",
319
+ "array": "array"
320
+ }
321
+ return type_map.get(type_name, "string")
322
+
323
+ return "string"
324
+
325
+ def _get_firestore_default(self, field_type: str) -> Any:
326
+ """Get default value for Firestore field type."""
327
+ defaults = {
328
+ "string": "",
329
+ "number": 0,
330
+ "integer": 0,
331
+ "boolean": False,
332
+ "date": "",
333
+ "object": {},
334
+ "array": []
335
+ }
336
+ return defaults.get(field_type)
337
+
338
+
339
+ def load_schema(filepath: str) -> Dict[str, Any]:
340
+ """Load schema from JSON file."""
341
+ try:
342
+ with open(filepath, 'r') as f:
343
+ return json.load(f)
344
+ except FileNotFoundError:
345
+ print(f"Error: Schema file not found: {filepath}", file=sys.stderr)
346
+ sys.exit(1)
347
+ except json.JSONDecodeError as e:
348
+ print(f"Error: Invalid JSON: {e}", file=sys.stderr)
349
+ sys.exit(1)
350
+
351
+
352
+ def get_supported_databases() -> List[str]:
353
+ """Get list of supported database types."""
354
+ return list(SchemaTransformer.DB_PATTERNS.keys())
355
+
356
+
357
+ def main():
358
+ """Main entry point for schema migration."""
359
+ parser = argparse.ArgumentParser(
360
+ description="Migrate NoSQL schema between database types",
361
+ formatter_class=argparse.RawDescriptionHelpFormatter,
362
+ epilog=f"""
363
+ Supported database types: {', '.join(get_supported_databases())}
364
+
365
+ Examples:
366
+ # MongoDB to DynamoDB
367
+ %(prog)s --from mongodb --to dynamodb --schema user.json
368
+
369
+ # DynamoDB to Firestore
370
+ %(prog)s --from dynamodb --to firestore --schema product.json --output firestore-schema.json
371
+
372
+ # Firestore to MongoDB
373
+ %(prog)s --from firestore --to mongodb --schema order.json
374
+
375
+ # List supported types
376
+ %(prog)s --list-types
377
+ """
378
+ )
379
+
380
+ parser.add_argument(
381
+ "--from",
382
+ dest="source_type",
383
+ help="Source database type"
384
+ )
385
+ parser.add_argument(
386
+ "--to",
387
+ dest="target_type",
388
+ help="Target database type"
389
+ )
390
+ parser.add_argument(
391
+ "--schema",
392
+ help="Path to source schema file"
393
+ )
394
+ parser.add_argument(
395
+ "--output",
396
+ help="Output file for migrated schema"
397
+ )
398
+ parser.add_argument(
399
+ "--list-types",
400
+ action="store_true",
401
+ help="List supported database types"
402
+ )
403
+ parser.add_argument(
404
+ "--verbose",
405
+ action="store_true",
406
+ help="Print detailed output"
407
+ )
408
+
409
+ args = parser.parse_args()
410
+
411
+ if args.list_types:
412
+ print("Supported database types:")
413
+ for db_type, info in SchemaTransformer.DB_PATTERNS.items():
414
+ print(f" - {db_type}: {info['name']}")
415
+ sys.exit(0)
416
+
417
+ # Validate required arguments
418
+ if not args.source_type or not args.target_type or not args.schema:
419
+ parser.error("--from, --to, and --schema are required")
420
+
421
+ try:
422
+ if args.verbose:
423
+ print(f"Migrating schema from {args.source_type} to {args.target_type}...", file=sys.stderr)
424
+
425
+ # Load source schema
426
+ schema = load_schema(args.schema)
427
+
428
+ # Create transformer
429
+ transformer = SchemaTransformer(args.source_type, args.target_type)
430
+
431
+ # Transform schema
432
+ migrated = transformer.transform(schema)
433
+
434
+ # Output
435
+ output_json = json.dumps(migrated, indent=2)
436
+ print(output_json)
437
+
438
+ # Save to file if requested
439
+ if args.output:
440
+ with open(args.output, 'w') as f:
441
+ f.write(output_json)
442
+
443
+ if args.verbose:
444
+ print(f"✓ Schema migrated successfully", file=sys.stderr)
445
+ print(f"✓ Saved to {args.output}", file=sys.stderr)
446
+
447
+ sys.exit(0)
448
+
449
+ except Exception as e:
450
+ print(f"Error: {e}", file=sys.stderr)
451
+ sys.exit(1)
452
+
453
+
454
+ if __name__ == "__main__":
455
+ main()