rosetta-sql 1.0.0__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.
rosetta/cli/main.py ADDED
@@ -0,0 +1,617 @@
1
+ """
2
+ Main CLI entry point with subcommand structure.
3
+
4
+ This module provides a modern CLI architecture that is friendly to both
5
+ AI Agents (JSON output via -j/--json) and humans (default output).
6
+ """
7
+
8
+ import argparse
9
+ import logging
10
+ import sys
11
+ from typing import List, Optional
12
+
13
+ from .. import __version__
14
+ from .output import OutputFormatter
15
+ from .result import CommandResult
16
+
17
+
18
+ def _add_global_options(parser: argparse.ArgumentParser) -> None:
19
+ """Add global options (-j/--json, -c/--config, -v/--verbose) to a parser.
20
+
21
+ Called on every subcommand parser so that flags like ``--json`` can appear
22
+ after the subcommand name (e.g. ``rosetta status --json``).
23
+ """
24
+ parser.add_argument(
25
+ "-j", "--json",
26
+ action="store_true",
27
+ default=False,
28
+ help="JSON output (AI Agent friendly)",
29
+ )
30
+ parser.add_argument(
31
+ "--config", "-c",
32
+ default=None,
33
+ help="Path to DBMS config JSON (default: dbms_config.json)",
34
+ )
35
+ parser.add_argument(
36
+ "--verbose", "-v",
37
+ action="store_true",
38
+ default=False,
39
+ help="Enable verbose / debug logging",
40
+ )
41
+ parser.add_argument(
42
+ "--version",
43
+ action="store_true",
44
+ default=False,
45
+ help="Show rosetta version and exit",
46
+ )
47
+
48
+
49
+ # A lightweight parser that only knows the global flags.
50
+ # Used for a first pass so that ``rosetta -j status`` works
51
+ # (the main parser sees -j *before* the subcommand name).
52
+ _global_preparser = argparse.ArgumentParser(add_help=False)
53
+ _global_preparser.add_argument("-j", "--json", action="store_true", default=False)
54
+ _global_preparser.add_argument("--config", "-c", default=None)
55
+ _global_preparser.add_argument("--verbose", "-v", action="store_true", default=False)
56
+ _global_preparser.add_argument("-V", "--version", action="store_true", default=False)
57
+
58
+
59
+ def create_parser() -> argparse.ArgumentParser:
60
+ """
61
+ Create the main argument parser with subcommands.
62
+
63
+ Returns:
64
+ argparse.ArgumentParser: The configured parser
65
+ """
66
+ parser = argparse.ArgumentParser(
67
+ prog="rosetta",
68
+ description=(
69
+ "Rosetta — Cross-DBMS SQL testing & benchmarking toolkit.\n\n"
70
+ "Human-readable output by default.\n"
71
+ "Use -j/--json for JSON output (AI Agent friendly)."
72
+ ),
73
+ formatter_class=argparse.RawDescriptionHelpFormatter,
74
+ # Inherit global flags so that ``rosetta -j status`` works
75
+ parents=[_global_preparser],
76
+ )
77
+
78
+ # Create subparsers
79
+ subparsers = parser.add_subparsers(
80
+ dest="command",
81
+ title="commands",
82
+ description="Available subcommands",
83
+ )
84
+
85
+ # Add subcommands
86
+ _add_mtr_subparser(subparsers)
87
+ _add_bench_subparser(subparsers)
88
+ _add_status_subparser(subparsers)
89
+ _add_exec_subparser(subparsers)
90
+ _add_config_subparser(subparsers)
91
+ _add_result_subparser(subparsers)
92
+ _add_interactive_subparser(subparsers)
93
+
94
+ return parser
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Shared argument helpers
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def _add_mtr_arguments(parser):
102
+ """Add MTR-specific arguments to a parser."""
103
+ parser.add_argument(
104
+ "-t", "--test",
105
+ required=True,
106
+ help="Path to .test file",
107
+ )
108
+ parser.add_argument(
109
+ "--dbms",
110
+ required=True,
111
+ help="DBMS targets, comma-separated (e.g. tdsql,mysql,tidb)",
112
+ )
113
+ parser.add_argument(
114
+ "--database", "-d",
115
+ default="rosetta_mtr_test",
116
+ help="Test database name (default: rosetta_mtr_test)",
117
+ )
118
+ parser.add_argument(
119
+ "--baseline", "-b",
120
+ default="tdsql",
121
+ help="Baseline DBMS name for diff (default: tdsql)",
122
+ )
123
+ parser.add_argument(
124
+ "--output-dir", "-o",
125
+ default="results",
126
+ help="Output directory for reports (default: results)",
127
+ )
128
+ parser.add_argument(
129
+ "--output-format", "-f",
130
+ default="all",
131
+ choices=["text", "html", "all"],
132
+ help="Report format (default: all)",
133
+ )
134
+ parser.add_argument(
135
+ "--skip-explain",
136
+ action="store_true",
137
+ default=True,
138
+ help="Skip EXPLAIN statements (default: on)",
139
+ )
140
+ parser.add_argument(
141
+ "--skip-analyze",
142
+ action="store_true",
143
+ help="Skip ANALYZE TABLE statements",
144
+ )
145
+ parser.add_argument(
146
+ "--skip-show-create",
147
+ action="store_true",
148
+ help="Skip SHOW CREATE TABLE statements",
149
+ )
150
+ parser.add_argument(
151
+ "--parse-only",
152
+ action="store_true",
153
+ help="Only parse .test file and print statements (no execution)",
154
+ )
155
+ parser.add_argument(
156
+ "--diff-only",
157
+ action="store_true",
158
+ help="Re-generate reports from existing .result files (no DB execution)",
159
+ )
160
+ parser.add_argument(
161
+ "--serve", "-s",
162
+ action="store_true",
163
+ help="Start a local HTTP server to view HTML reports",
164
+ )
165
+ parser.add_argument(
166
+ "--port", "-p",
167
+ type=int,
168
+ default=19527,
169
+ help="HTTP server port (default: 19527)",
170
+ )
171
+
172
+
173
+ def _add_bench_arguments(parser):
174
+ """Add benchmark-specific arguments to a parser."""
175
+ parser.add_argument(
176
+ "--dbms",
177
+ required=True,
178
+ help="DBMS targets, comma-separated (e.g. tdsql,mysql)",
179
+ )
180
+ parser.add_argument(
181
+ "--file",
182
+ dest="bench_file",
183
+ help="Benchmark definition file (.json or .sql)",
184
+ )
185
+ parser.add_argument(
186
+ "--template",
187
+ help="Use a built-in template (e.g. oltp_read_write, oltp_read_only)",
188
+ )
189
+ parser.add_argument(
190
+ "--mode",
191
+ choices=["SERIAL", "CONCURRENT"],
192
+ default="SERIAL",
193
+ help="Execution mode: SERIAL or CONCURRENT (default: SERIAL)",
194
+ )
195
+ parser.add_argument(
196
+ "--database", "-d",
197
+ default="rosetta_bench_test",
198
+ help="Benchmark database name (default: rosetta_bench_test)",
199
+ )
200
+ parser.add_argument(
201
+ "--output-dir", "-o",
202
+ default="results",
203
+ help="Output directory for reports (default: results)",
204
+ )
205
+ parser.add_argument(
206
+ "--output-format", "-f",
207
+ default="all",
208
+ choices=["text", "html", "all"],
209
+ help="Report format (default: all)",
210
+ )
211
+ # Serial mode options
212
+ parser.add_argument(
213
+ "--iterations",
214
+ type=int,
215
+ default=1,
216
+ help="Number of iterations per query — serial mode (default: 1)",
217
+ )
218
+ # Concurrent mode options
219
+ parser.add_argument(
220
+ "--concurrency",
221
+ type=int,
222
+ default=10,
223
+ help="Number of concurrent threads — concurrent mode (default: 10)",
224
+ )
225
+ parser.add_argument(
226
+ "--duration",
227
+ type=float,
228
+ default=30.0,
229
+ help="Duration in seconds — concurrent mode (default: 30)",
230
+ )
231
+ # Shared options
232
+ parser.add_argument(
233
+ "--warmup",
234
+ type=int,
235
+ default=0,
236
+ help="Warmup iterations (serial) or warmup duration in seconds (concurrent) (default: 0)",
237
+ )
238
+ parser.add_argument(
239
+ "--ramp-up",
240
+ type=float,
241
+ default=0.0,
242
+ help="Ramp-up seconds — concurrent mode (default: 0)",
243
+ )
244
+ parser.add_argument(
245
+ "--query-timeout",
246
+ type=int,
247
+ default=5,
248
+ help="Query timeout in seconds (default: 5, 0 to disable)",
249
+ )
250
+ parser.add_argument(
251
+ "--bench-filter",
252
+ help="Run only queries matching these names (comma-separated)",
253
+ )
254
+ parser.add_argument(
255
+ "--repeat",
256
+ type=int,
257
+ default=1,
258
+ help="Number of benchmark rounds (default: 1)",
259
+ )
260
+ parser.add_argument(
261
+ "--skip-setup",
262
+ action="store_true",
263
+ default=False,
264
+ help="Skip setup phase (reuse existing tables)",
265
+ )
266
+ parser.add_argument(
267
+ "--skip-teardown",
268
+ action="store_true",
269
+ default=False,
270
+ help="Skip teardown (keep tables for next run)",
271
+ )
272
+ parser.add_argument(
273
+ "--no-parallel-dbms",
274
+ dest="parallel_dbms",
275
+ action="store_false",
276
+ help="Run DBMS targets sequentially instead of in parallel",
277
+ )
278
+ parser.set_defaults(parallel_dbms=True)
279
+ parser.add_argument(
280
+ "--profile",
281
+ action="store_true",
282
+ default=True,
283
+ help="Enable flame-graph capture (default: on)",
284
+ )
285
+ parser.add_argument(
286
+ "--no-profile",
287
+ action="store_false",
288
+ dest="profile",
289
+ help="Disable flame-graph capture",
290
+ )
291
+ parser.add_argument(
292
+ "--perf-freq",
293
+ type=int,
294
+ default=99,
295
+ help="perf sampling frequency in Hz (default: 99)",
296
+ )
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # Subparser registration
301
+ # ---------------------------------------------------------------------------
302
+
303
+ def _add_mtr_subparser(subparsers):
304
+ """Add the 'mtr' top-level subcommand."""
305
+ mtr_parser = subparsers.add_parser(
306
+ "mtr",
307
+ help="Run MTR consistency test",
308
+ description="Execute .test files and compare SQL results across databases",
309
+ )
310
+ _add_global_options(mtr_parser)
311
+ _add_mtr_arguments(mtr_parser)
312
+
313
+
314
+ def _add_bench_subparser(subparsers):
315
+ """Add the 'bench' top-level subcommand."""
316
+ bench_parser = subparsers.add_parser(
317
+ "bench",
318
+ help="Run performance benchmark",
319
+ description="Compare query performance across databases with custom workloads",
320
+ )
321
+ _add_global_options(bench_parser)
322
+ _add_bench_arguments(bench_parser)
323
+
324
+
325
+ def _add_list_subparser(subparsers):
326
+ """Add the 'list' subcommand."""
327
+ list_parser = subparsers.add_parser(
328
+ "list",
329
+ help="List resources (configs, history, templates)",
330
+ description="List databases, execution history, or benchmark templates",
331
+ )
332
+ list_parser.add_argument(
333
+ "resource",
334
+ nargs="?",
335
+ default="dbms",
336
+ choices=["dbms", "history", "templates"],
337
+ help="Resource to list: dbms (databases), history (runs), templates (benchmarks) (default: dbms)",
338
+ )
339
+ list_parser.add_argument(
340
+ "--limit",
341
+ type=int,
342
+ default=20,
343
+ help="Maximum number of items to show (default: 20)",
344
+ )
345
+
346
+
347
+ def _add_status_subparser(subparsers):
348
+ """Add the 'status' subcommand."""
349
+ status_parser = subparsers.add_parser(
350
+ "status",
351
+ help="Check DBMS connection status",
352
+ description="Check connection status for all configured databases",
353
+ )
354
+ _add_global_options(status_parser)
355
+ status_parser.add_argument(
356
+ "--timeout",
357
+ type=int,
358
+ default=5,
359
+ help="Connection timeout in seconds (default: 5)",
360
+ )
361
+
362
+
363
+ def _add_exec_subparser(subparsers):
364
+ """Add the 'exec' subcommand."""
365
+ exec_parser = subparsers.add_parser(
366
+ "exec",
367
+ help="Execute SQL statements",
368
+ description="Execute SQL statements on specified databases (CLI playground)",
369
+ )
370
+ _add_global_options(exec_parser)
371
+ exec_parser.add_argument(
372
+ "--sql",
373
+ help="SQL statement to execute",
374
+ )
375
+ exec_parser.add_argument(
376
+ "--file",
377
+ help="File containing SQL statements",
378
+ )
379
+ exec_parser.add_argument(
380
+ "--dbms",
381
+ help="DBMS targets, comma-separated (default: all from config)",
382
+ )
383
+ exec_parser.add_argument(
384
+ "--database", "-d",
385
+ help="Database name (default: from config)",
386
+ )
387
+
388
+
389
+ def _add_config_subparser(subparsers):
390
+ """Add the 'config' subcommand."""
391
+ config_parser = subparsers.add_parser(
392
+ "config",
393
+ help="Manage configurations",
394
+ description="View, validate, or generate configuration files",
395
+ )
396
+ _add_global_options(config_parser)
397
+ config_parser.add_argument(
398
+ "action",
399
+ choices=["show", "validate", "init"],
400
+ help="Action: show (display config), validate (check config), init (generate sample)",
401
+ )
402
+ config_parser.add_argument(
403
+ "--output",
404
+ help="Output file path (for init action)",
405
+ )
406
+
407
+
408
+ def _add_result_subparser(subparsers):
409
+ """Add the 'result' subcommand with sub-actions via subparsers."""
410
+ result_parser = subparsers.add_parser(
411
+ "result",
412
+ help="Manage execution results",
413
+ description="Browse, view, and export historical execution results",
414
+ )
415
+ _add_global_options(result_parser)
416
+
417
+ result_sub = result_parser.add_subparsers(dest="result_action")
418
+
419
+ # result list (also the default when no action given)
420
+ list_p = result_sub.add_parser(
421
+ "list", help="List historical runs",
422
+ description="Show a table of past MTR / bench runs",
423
+ )
424
+ _add_global_options(list_p)
425
+ list_p.add_argument(
426
+ "-n", "--limit", type=int, default=20,
427
+ help="Max rows per page (default: 20)",
428
+ )
429
+ list_p.add_argument(
430
+ "-p", "--page", type=int, default=1,
431
+ help="Page number (default: 1)",
432
+ )
433
+ list_p.add_argument(
434
+ "--type", choices=["all", "mtr", "bench"], default="all",
435
+ help="Filter by run type (default: all)",
436
+ )
437
+ list_p.add_argument(
438
+ "--output-dir", "-o", default="results",
439
+ help="Results directory (default: results)",
440
+ )
441
+
442
+ # result show <run_id>
443
+ show_p = result_sub.add_parser(
444
+ "show", help="Show details of a run",
445
+ description="Display detailed information for a specific run",
446
+ )
447
+ _add_global_options(show_p)
448
+ show_p.add_argument(
449
+ "run_id", nargs="?", default=None,
450
+ help="Run ID or path (default: latest)",
451
+ )
452
+ show_p.add_argument(
453
+ "--output-dir", "-o", default="results",
454
+ help="Results directory (default: results)",
455
+ )
456
+
457
+
458
+ def _add_interactive_subparser(subparsers):
459
+ """Add the 'interactive' subcommand (with aliases 'repl' and 'i')."""
460
+ for name in ["interactive", "repl", "i"]:
461
+ interp_parser = subparsers.add_parser(
462
+ name,
463
+ help="Launch interactive REPL" + (" (alias)" if name != "interactive" else ""),
464
+ description="Start an interactive session for repeated test execution",
465
+ )
466
+ _add_global_options(interp_parser)
467
+ interp_parser.add_argument(
468
+ "--dbms",
469
+ help="DBMS targets, comma-separated (default: auto-detect reachable DBMS)",
470
+ )
471
+ interp_parser.add_argument(
472
+ "--database", "-d",
473
+ default="cross_dbms_test_db",
474
+ help="Test database name (default: cross_dbms_test_db)",
475
+ )
476
+ interp_parser.add_argument(
477
+ "--output-dir", "-o",
478
+ default="results",
479
+ help="Output directory for reports (default: results)",
480
+ )
481
+ interp_parser.add_argument(
482
+ "--serve", "-s",
483
+ action="store_true",
484
+ default=True,
485
+ help="Start a local HTTP server to view HTML reports (default: on)",
486
+ )
487
+ interp_parser.add_argument(
488
+ "--no-serve",
489
+ action="store_false",
490
+ dest="serve",
491
+ help="Do not start HTTP server",
492
+ )
493
+ interp_parser.add_argument(
494
+ "--port", "-p",
495
+ type=int,
496
+ default=19527,
497
+ help="HTTP server port (default: 19527)",
498
+ )
499
+
500
+
501
+ def main(argv: Optional[List[str]] = None) -> int:
502
+ """
503
+ Main entry point for the rosetta CLI.
504
+
505
+ Args:
506
+ argv: Command-line arguments (default: sys.argv[1:])
507
+
508
+ Returns:
509
+ Exit code (0 for success, non-zero for failure)
510
+ """
511
+ if argv is None:
512
+ argv = sys.argv[1:]
513
+
514
+ parser = create_parser()
515
+
516
+ # Two-phase parse: first extract global flags that may appear before
517
+ # the subcommand (e.g. ``rosetta -j status``), then let the full
518
+ # parser handle everything. The subcommand parsers also accept
519
+ # the same flags, so ``rosetta status -j`` works too.
520
+ pre_args, _ = _global_preparser.parse_known_args(argv)
521
+ args = parser.parse_args(argv)
522
+
523
+ # Merge: if the flag was set in *either* position, honour it.
524
+ args.json = args.json or pre_args.json
525
+ args.verbose = args.verbose or pre_args.verbose
526
+ args.version = args.version or pre_args.version
527
+ if args.config is None:
528
+ args.config = pre_args.config if pre_args.config is not None else "dbms_config.json"
529
+
530
+ # Derive output format from -j/--json flag
531
+ fmt = "json" if args.json else "human"
532
+ output = OutputFormatter(format=fmt)
533
+
534
+ # Compatibility: keep `-v` as verbose for subcommands, but when used
535
+ # alone at top level treat it as a convenient version shortcut.
536
+ if not args.command and args.verbose and not args.version:
537
+ args.version = True
538
+
539
+ if args.version:
540
+ if args.json:
541
+ output.print(CommandResult.success(
542
+ "version",
543
+ {"name": "rosetta", "version": __version__},
544
+ ))
545
+ else:
546
+ print(f"rosetta {__version__}")
547
+ return 0
548
+
549
+ # Configure logging - only show ERROR to console by default
550
+ log_level = logging.DEBUG if args.verbose else logging.ERROR
551
+ logging.basicConfig(
552
+ level=log_level,
553
+ format="%(asctime)s [%(levelname)s] %(message)s",
554
+ datefmt="%Y-%m-%d %H:%M:%S",
555
+ )
556
+
557
+ # No command provided — default to interactive mode
558
+ if not args.command:
559
+ from .interactive_cmd import handle_interactive
560
+ # Set default values for interactive mode
561
+ args.dbms = getattr(args, 'dbms', None)
562
+ args.database = getattr(args, 'database', 'cross_dbms_test_db')
563
+ args.output_dir = getattr(args, 'output_dir', 'results')
564
+ args.serve = getattr(args, 'serve', True)
565
+ args.port = getattr(args, 'port', 19527)
566
+ result = handle_interactive(args, output)
567
+ output.print(result)
568
+ return result.exit_code()
569
+
570
+ # Dispatch to command handlers
571
+ try:
572
+ if args.command == "mtr":
573
+ from .run import handle_mtr
574
+ result = handle_mtr(args, output)
575
+ elif args.command == "bench":
576
+ from .run import handle_bench
577
+ result = handle_bench(args, output)
578
+ elif args.command == "status":
579
+ from .status import handle_status
580
+ result = handle_status(args, output)
581
+ elif args.command == "exec":
582
+ from .exec import handle_exec
583
+ result = handle_exec(args, output)
584
+ elif args.command == "config":
585
+ from .config_cmd import handle_config
586
+ result = handle_config(args, output)
587
+ elif args.command == "result":
588
+ from .result_cmd import handle_result
589
+ result = handle_result(args, output)
590
+ elif args.command in ["interactive", "repl", "i"]:
591
+ from .interactive_cmd import handle_interactive
592
+ result = handle_interactive(args, output)
593
+ else:
594
+ result = CommandResult.failure(
595
+ f"Unknown command: {args.command}",
596
+ )
597
+
598
+ # Print result
599
+ output.print(result)
600
+ return result.exit_code()
601
+
602
+ except Exception as e:
603
+ error_result = CommandResult.failure(
604
+ f"Unexpected error: {str(e)}",
605
+ )
606
+ output.print(error_result)
607
+ if args.verbose:
608
+ import traceback
609
+ traceback.print_exc()
610
+ return 1
611
+ finally:
612
+ # Always restore cursor visibility
613
+ try:
614
+ from rich.console import Console
615
+ Console().show_cursor()
616
+ except Exception:
617
+ pass