rosetta-sql 1.0.3__tar.gz → 1.2.0__tar.gz
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_sql-1.0.3/rosetta_sql.egg-info → rosetta_sql-1.2.0}/PKG-INFO +1 -3
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/pyproject.toml +1 -2
- rosetta_sql-1.2.0/rosetta/__init__.py +11 -0
- rosetta_sql-1.2.0/rosetta/__main__.py +6 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/exec.py +2 -2
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/main.py +17 -16
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/mtr_cmd.py +7 -14
- rosetta_sql-1.2.0/rosetta/cli/update_cmd.py +129 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/comparator.py +14 -5
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/interactive.py +802 -4
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/html.py +1 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/runner.py +384 -6
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0/rosetta_sql.egg-info}/PKG-INFO +1 -3
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta_sql.egg-info/SOURCES.txt +1 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta_sql.egg-info/requires.txt +0 -3
- rosetta_sql-1.0.3/rosetta/__init__.py +0 -3
- rosetta_sql-1.0.3/rosetta/__main__.py +0 -8
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/LICENSE +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/README.md +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/benchmark.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/__init__.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/config_cmd.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/interactive_cmd.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/list_cmd.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/output.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/result.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/result_cmd.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/run.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/cli/status.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/config.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/executor.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/flamegraph.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/models.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/__init__.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/adapter.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/connection.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/error_handler.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/executor.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/nodes.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/parser.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/result_processor.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/mtr/variable.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/paths.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/__init__.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/bench_html.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/bench_text.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/history.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/reporter/text.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/serve.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta/ui.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta_sql.egg-info/dependency_links.txt +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta_sql.egg-info/entry_points.txt +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/rosetta_sql.egg-info/top_level.txt +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/setup.cfg +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/setup.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/skills/rosetta/scripts/install_rosetta.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/skills/rosetta/scripts/rosetta_wrapper.py +0 -0
- {rosetta_sql-1.0.3 → rosetta_sql-1.2.0}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rosetta-sql
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Cross-DBMS SQL behavioral consistency verification tool
|
|
5
5
|
Author: yangshijie
|
|
6
6
|
License: MIT
|
|
@@ -27,8 +27,6 @@ Requires-Dist: rich>=13.0
|
|
|
27
27
|
Requires-Dist: prompt_toolkit>=3.0.52
|
|
28
28
|
Provides-Extra: mysql-connector
|
|
29
29
|
Requires-Dist: mysql-connector-python>=8.0; extra == "mysql-connector"
|
|
30
|
-
Provides-Extra: gcov
|
|
31
|
-
Requires-Dist: fastcov>=1.13; extra == "gcov"
|
|
32
30
|
Dynamic: license-file
|
|
33
31
|
|
|
34
32
|
# Rosetta
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rosetta-sql"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Cross-DBMS SQL behavioral consistency verification tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -34,7 +34,6 @@ dependencies = [
|
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
36
|
mysql-connector = ["mysql-connector-python>=8.0"]
|
|
37
|
-
gcov = ["fastcov>=1.13"]
|
|
38
37
|
|
|
39
38
|
[project.scripts]
|
|
40
39
|
rosetta = "rosetta.cli:main"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Rosetta — Cross-DBMS SQL behavioral consistency verification tool."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _get_version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = _get_version("rosetta-sql")
|
|
7
|
+
except Exception:
|
|
8
|
+
# Fallback for editable install / development mode
|
|
9
|
+
import tomllib
|
|
10
|
+
with open(__file__ + "/../pyproject.toml", "rb") as _f:
|
|
11
|
+
__version__ = tomllib.load(_f)["project"]["version"]
|
|
@@ -82,10 +82,10 @@ def handle_exec(args, output: "OutputFormatter") -> CommandResult:
|
|
|
82
82
|
# so the MTR parser treats each as a separate statement.
|
|
83
83
|
parts = [p.strip() for p in sql_text.split(";") if p.strip()]
|
|
84
84
|
sql_text_for_parse = ";\n".join(parts) + ";\n"
|
|
85
|
-
mtr_parser = MtrParser()
|
|
85
|
+
mtr_parser = MtrParser("<inline>")
|
|
86
86
|
parsed = mtr_parser.parse_text(sql_text_for_parse)
|
|
87
87
|
else:
|
|
88
|
-
mtr_parser = MtrParser()
|
|
88
|
+
mtr_parser = MtrParser("<inline>")
|
|
89
89
|
parsed = mtr_parser.parse_text(sql_text)
|
|
90
90
|
statements = [cmd.argument for cmd in parsed.commands
|
|
91
91
|
if cmd.cmd_type == MtrCommandType.SQL]
|
|
@@ -94,7 +94,8 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
94
94
|
_add_config_subparser(subparsers)
|
|
95
95
|
_add_result_subparser(subparsers)
|
|
96
96
|
_add_interactive_subparser(subparsers)
|
|
97
|
-
|
|
97
|
+
_add_update_subparser(subparsers)
|
|
98
|
+
|
|
98
99
|
return parser
|
|
99
100
|
|
|
100
101
|
|
|
@@ -192,27 +193,14 @@ def _add_mtr_arguments(parser):
|
|
|
192
193
|
default=False,
|
|
193
194
|
help="Enable optimistic transaction mode (--mysqld=--tdsql_trans_type=1)",
|
|
194
195
|
)
|
|
195
|
-
parser.add_argument(
|
|
196
|
-
"-v", "--vector",
|
|
197
|
-
action="store_true",
|
|
198
|
-
default=False,
|
|
199
|
-
help="Enable vector engine mode (--ve-protocol)",
|
|
200
|
-
)
|
|
201
|
-
parser.add_argument(
|
|
202
|
-
"-pq", "--parallel-query",
|
|
203
|
-
action="store_true",
|
|
204
|
-
default=False,
|
|
205
|
-
help="Enable parallel query mode (--parallel-query)",
|
|
206
|
-
)
|
|
207
196
|
parser.add_argument(
|
|
208
197
|
"-m", "--mode",
|
|
209
198
|
type=str,
|
|
210
199
|
default=None,
|
|
211
200
|
help="Run multiple MTR modes in parallel. "
|
|
212
|
-
"Comma-separated list of: row
|
|
201
|
+
"Comma-separated list of: row, col, pq, "
|
|
213
202
|
"or 'all' (equivalent to row,col,pq). "
|
|
214
|
-
"Example: --mode row,col,pq
|
|
215
|
-
"When specified, --vector and --parallel-query flags are ignored.",
|
|
203
|
+
"Example: --mode row,col,pq",
|
|
216
204
|
)
|
|
217
205
|
parser.add_argument(
|
|
218
206
|
"-r", "--record",
|
|
@@ -588,6 +576,16 @@ def _add_interactive_subparser(subparsers):
|
|
|
588
576
|
)
|
|
589
577
|
|
|
590
578
|
|
|
579
|
+
def _add_update_subparser(subparsers):
|
|
580
|
+
"""Add the 'update' subcommand."""
|
|
581
|
+
update_parser = subparsers.add_parser(
|
|
582
|
+
"update",
|
|
583
|
+
help="Update rosetta to the latest version",
|
|
584
|
+
description="Pull latest code from git and reinstall",
|
|
585
|
+
)
|
|
586
|
+
_add_global_options(update_parser)
|
|
587
|
+
|
|
588
|
+
|
|
591
589
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
592
590
|
"""
|
|
593
591
|
Main entry point for the rosetta CLI.
|
|
@@ -687,6 +685,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
687
685
|
elif args.command in ["interactive", "repl", "i"]:
|
|
688
686
|
from .interactive_cmd import handle_interactive
|
|
689
687
|
result = handle_interactive(args, output)
|
|
688
|
+
elif args.command == "update":
|
|
689
|
+
from .update_cmd import handle_update
|
|
690
|
+
result = handle_update(args, output)
|
|
690
691
|
else:
|
|
691
692
|
result = CommandResult.failure(
|
|
692
693
|
f"Unknown command: {args.command}",
|
|
@@ -858,8 +858,6 @@ def _run_native_mtr(args, output: "OutputFormatter") -> CommandResult:
|
|
|
858
858
|
"suite_timeout": file_cfg["suite_timeout"],
|
|
859
859
|
"optimistic": False,
|
|
860
860
|
"record": False,
|
|
861
|
-
"vector": False,
|
|
862
|
-
"parallel_query": False,
|
|
863
861
|
"suite": None,
|
|
864
862
|
"cases": [],
|
|
865
863
|
}
|
|
@@ -900,8 +898,6 @@ def _run_native_mtr(args, output: "OutputFormatter") -> CommandResult:
|
|
|
900
898
|
|
|
901
899
|
cfg["optimistic"] = getattr(args, "optimistic", False)
|
|
902
900
|
cfg["record"] = getattr(args, "record", False)
|
|
903
|
-
cfg["vector"] = getattr(args, "vector", False)
|
|
904
|
-
cfg["parallel_query"] = getattr(args, "parallel_query", False)
|
|
905
901
|
cfg["suite"] = getattr(args, "suite", None)
|
|
906
902
|
cfg["cases"] = getattr(args, "cases", [])
|
|
907
903
|
|
|
@@ -948,16 +944,19 @@ def _run_native_mtr(args, output: "OutputFormatter") -> CommandResult:
|
|
|
948
944
|
info_lines.append(f"[bold]Record[/bold] : ON")
|
|
949
945
|
if cfg["optimistic"]:
|
|
950
946
|
info_lines.append(f"[bold]Optimistic[/bold] : ON")
|
|
951
|
-
if cfg["vector"]:
|
|
952
|
-
info_lines.append(f"[bold]Vector[/bold] : ON")
|
|
953
|
-
if cfg["parallel_query"]:
|
|
954
|
-
info_lines.append(f"[bold]PQ[/bold] : ON")
|
|
955
947
|
console_plan.print(_Panel(
|
|
956
948
|
"\n".join(info_lines),
|
|
957
949
|
title="[bold cyan]MTR Execution Plan[/bold cyan]",
|
|
958
950
|
title_align="left",
|
|
959
951
|
padding=(0, 1),
|
|
960
952
|
))
|
|
953
|
+
# Print actual MTR command
|
|
954
|
+
console_plan.print(_Panel(
|
|
955
|
+
f"[dim]{cmd}[/dim]",
|
|
956
|
+
title="[bold cyan]Command[/bold cyan]",
|
|
957
|
+
title_align="left",
|
|
958
|
+
padding=(0, 1),
|
|
959
|
+
))
|
|
961
960
|
|
|
962
961
|
# --- 4. Execute MTR ---
|
|
963
962
|
original_dir = os.getcwd()
|
|
@@ -965,10 +964,6 @@ def _run_native_mtr(args, output: "OutputFormatter") -> CommandResult:
|
|
|
965
964
|
|
|
966
965
|
# Build mode label for progress display
|
|
967
966
|
mode_parts = []
|
|
968
|
-
if cfg["vector"]:
|
|
969
|
-
mode_parts.append("ve-protocol")
|
|
970
|
-
if cfg["parallel_query"]:
|
|
971
|
-
mode_parts.append("parallel-query")
|
|
972
967
|
if cfg["optimistic"]:
|
|
973
968
|
mode_parts.append("optimistic")
|
|
974
969
|
mode_label = "+".join(mode_parts) if mode_parts else "row (default)"
|
|
@@ -1137,8 +1132,6 @@ def _run_native_mtr(args, output: "OutputFormatter") -> CommandResult:
|
|
|
1137
1132
|
"cases": cfg["cases"],
|
|
1138
1133
|
"record": cfg["record"],
|
|
1139
1134
|
"optimistic": cfg["optimistic"],
|
|
1140
|
-
"vector": cfg["vector"],
|
|
1141
|
-
"parallel_query": cfg["parallel_query"],
|
|
1142
1135
|
"exit_code": exit_code,
|
|
1143
1136
|
"elapsed_seconds": total_elapsed,
|
|
1144
1137
|
"log_dir": log_dir,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Handler for 'rosetta update' — self-update to latest PyPI version."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.request
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
from .result import CommandResult
|
|
10
|
+
|
|
11
|
+
_PYPI_JSON_URL = "https://pypi.org/pypi/rosetta-sql/json"
|
|
12
|
+
_PACKAGE_NAME = "rosetta-sql"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_update(args, output) -> CommandResult:
|
|
16
|
+
"""Check for updates and upgrade rosetta to the latest PyPI release.
|
|
17
|
+
|
|
18
|
+
Steps:
|
|
19
|
+
1. Query PyPI JSON API for latest version
|
|
20
|
+
2. Compare with locally installed version
|
|
21
|
+
3. If behind → pip install --upgrade rosetta-sql
|
|
22
|
+
4. If up-to-date → inform user
|
|
23
|
+
"""
|
|
24
|
+
is_json = getattr(args, "json", False)
|
|
25
|
+
|
|
26
|
+
# Step 1: Get local version
|
|
27
|
+
local_ver = _get_local_version()
|
|
28
|
+
|
|
29
|
+
# Step 2: Get remote (PyPI) latest version
|
|
30
|
+
if not is_json:
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
Console().print(" [dim]Checking for updates...[/dim]")
|
|
33
|
+
pypi_info = _fetch_pypi_info()
|
|
34
|
+
if pypi_info is None:
|
|
35
|
+
return CommandResult.failure(
|
|
36
|
+
"Failed to fetch version info from PyPI. Check your network.",
|
|
37
|
+
)
|
|
38
|
+
remote_ver = pypi_info["info"]["version"]
|
|
39
|
+
|
|
40
|
+
# Step 3: Compare
|
|
41
|
+
if _version_tuple(local_ver) >= _version_tuple(remote_ver):
|
|
42
|
+
if is_json:
|
|
43
|
+
return CommandResult.success(
|
|
44
|
+
"update",
|
|
45
|
+
{"status": "up_to_date", "version": local_ver,
|
|
46
|
+
"message": f"Already at latest version ({local_ver})"},
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
from rich.console import Console
|
|
50
|
+
Console().print(
|
|
51
|
+
f"\n [green bold]✓[/green bold] Already up to date: "
|
|
52
|
+
f"rosetta {local_ver}\n",
|
|
53
|
+
)
|
|
54
|
+
return CommandResult.success("update")
|
|
55
|
+
|
|
56
|
+
# Step 4: Upgrade
|
|
57
|
+
if not is_json:
|
|
58
|
+
from rich.console import Console
|
|
59
|
+
console = Console()
|
|
60
|
+
console.print(f"\n [yellow]Update available:[/yellow]")
|
|
61
|
+
console.print(f" Current : {local_ver}")
|
|
62
|
+
console.print(f" Latest : {remote_ver}")
|
|
63
|
+
console.print()
|
|
64
|
+
Console().print(" [dim]Upgrading...[/dim]")
|
|
65
|
+
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[sys.executable, "-m", "pip", "install", "--upgrade",
|
|
68
|
+
_PACKAGE_NAME, "--quiet"],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
timeout=120,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
return CommandResult.failure(
|
|
74
|
+
f"pip install failed:\n{result.stderr.decode(errors='replace')}",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
final_ver = _get_local_version()
|
|
78
|
+
if is_json:
|
|
79
|
+
return CommandResult.success(
|
|
80
|
+
"update",
|
|
81
|
+
{
|
|
82
|
+
"status": "updated",
|
|
83
|
+
"old_version": local_ver,
|
|
84
|
+
"new_version": final_ver,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
from rich.console import Console
|
|
89
|
+
Console().print(
|
|
90
|
+
f"\n [green bold]✓[/green bold] Updated to "
|
|
91
|
+
f"[bold cyan]{final_ver}[/bold cyan]\n",
|
|
92
|
+
)
|
|
93
|
+
return CommandResult.success("update")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Helpers
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _get_local_version() -> str:
|
|
101
|
+
try:
|
|
102
|
+
return version(_PACKAGE_NAME)
|
|
103
|
+
except Exception:
|
|
104
|
+
return "unknown"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _fetch_pypi_info():
|
|
108
|
+
"""Query PyPI JSON API. Returns parsed dict or None on failure."""
|
|
109
|
+
try:
|
|
110
|
+
req = urllib.request.Request(_PYPI_JSON_URL, headers={"Accept": "application/json"})
|
|
111
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
112
|
+
return json.loads(resp.read().decode())
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _version_tuple(v: str) -> tuple:
|
|
118
|
+
"""Parse '1.2.3' into comparable tuple of ints."""
|
|
119
|
+
parts = []
|
|
120
|
+
for part in v.split("."):
|
|
121
|
+
# Handle suffixes like .dev1, .a1, .rc1 — treat as 0
|
|
122
|
+
num = ""
|
|
123
|
+
for ch in part:
|
|
124
|
+
if ch.isdigit():
|
|
125
|
+
num += ch
|
|
126
|
+
else:
|
|
127
|
+
break
|
|
128
|
+
parts.append(int(num) if num else 0)
|
|
129
|
+
return tuple(parts) if parts else (0,)
|
|
@@ -13,7 +13,12 @@ _RE_ENGINE = re.compile(r"ENGINE\s*=\s*\w+")
|
|
|
13
13
|
_RE_CHARSET_COLLATE = re.compile(
|
|
14
14
|
r"DEFAULT CHARSET=\w+(\s+COLLATE=\w+)?"
|
|
15
15
|
)
|
|
16
|
-
_RE_ERROR_LINE = re.compile(r"^ERROR\b
|
|
16
|
+
_RE_ERROR_LINE = re.compile(r"^ERROR\b(?:\s*(\d+)|\D*\((\d+)[,:\)])")
|
|
17
|
+
# OceanBase appends trace_id lines after errors like [IP:PORT] [timestamp] or [YB4Z...]
|
|
18
|
+
_RE_OB_TRACE = re.compile(
|
|
19
|
+
r"^\[\d+\.\d+\.\d+\.\d+:\d+\]" # [IP:PORT]
|
|
20
|
+
r"|\[[A-Z0-9]{8,}(-[\w]+){2,}\]" # [OB-trace-id]
|
|
21
|
+
)
|
|
17
22
|
_RE_AUTO_INCREMENT = re.compile(r"\s*AUTO_INCREMENT=\d+")
|
|
18
23
|
_RE_ROW_FORMAT = re.compile(r"\s*ROW_FORMAT=\w+")
|
|
19
24
|
_RE_STATS_PERSISTENT = re.compile(r"\s*STATS_PERSISTENT=\d+")
|
|
@@ -30,14 +35,16 @@ def normalize_line(line: str) -> str:
|
|
|
30
35
|
"""Normalize a single output line to ignore known non-functional diffs.
|
|
31
36
|
|
|
32
37
|
Handled cases:
|
|
33
|
-
- ERROR lines: only keep error code
|
|
38
|
+
- ERROR lines: only keep error code (supports both MTR "ERROR(code,"
|
|
39
|
+
and MySQL-style "ERROR 1662:" formats)
|
|
34
40
|
- TDSQL tail (txid, sql-node, error-store-node)
|
|
35
41
|
- ENGINE=, CHARSET=, AUTO_INCREMENT=, ROW_FORMAT=, DEFINER=, etc.
|
|
36
42
|
"""
|
|
37
43
|
s = line
|
|
38
44
|
m = _RE_ERROR_LINE.match(s)
|
|
39
45
|
if m:
|
|
40
|
-
|
|
46
|
+
code = m.group(1) or m.group(2)
|
|
47
|
+
return f"ERROR: ({code})"
|
|
41
48
|
if s.startswith("ERROR"):
|
|
42
49
|
return "ERROR: (unknown)"
|
|
43
50
|
s = _RE_TDSQL_TAIL.sub("", s)
|
|
@@ -53,12 +60,14 @@ def normalize_line(line: str) -> str:
|
|
|
53
60
|
def normalize_block(block: List[str]) -> List[str]:
|
|
54
61
|
"""Normalize all lines in a block for comparison.
|
|
55
62
|
|
|
56
|
-
Filters out Warning lines
|
|
63
|
+
Filters out Warning lines, the "Warnings:" header, and OceanBase
|
|
64
|
+
trace_id appendages that appear after error messages.
|
|
57
65
|
"""
|
|
58
66
|
lines = [normalize_line(l) for l in block]
|
|
59
67
|
return [l for l in lines
|
|
60
68
|
if l.strip() != "Warnings:"
|
|
61
|
-
and not _RE_WARNING_LINE.match(l.strip())
|
|
69
|
+
and not _RE_WARNING_LINE.match(l.strip())
|
|
70
|
+
and not _RE_OB_TRACE.match(l.strip())]
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def filter_warnings(block: List[str]) -> List[str]:
|