starrocks-br 0.4.0__tar.gz → 0.5.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.
Files changed (46) hide show
  1. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/PKG-INFO +3 -2
  2. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/pyproject.toml +4 -3
  3. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/cli.py +63 -37
  4. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/db.py +3 -3
  5. starrocks_br-0.5.0/src/starrocks_br/error_handler.py +265 -0
  6. starrocks_br-0.5.0/src/starrocks_br/exceptions.py +93 -0
  7. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/executor.py +4 -4
  8. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/history.py +4 -6
  9. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/labels.py +2 -2
  10. starrocks_br-0.5.0/src/starrocks_br/logger.py +66 -0
  11. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/planner.py +2 -3
  12. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/restore.py +13 -16
  13. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/timezone.py +1 -2
  14. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/PKG-INFO +3 -2
  15. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/SOURCES.txt +3 -0
  16. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/requires.txt +1 -0
  17. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_cli.py +131 -7
  18. starrocks_br-0.5.0/tests/test_error_handler.py +169 -0
  19. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_executor.py +3 -3
  20. starrocks_br-0.5.0/tests/test_logger.py +78 -0
  21. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_restore.py +39 -11
  22. starrocks_br-0.4.0/src/starrocks_br/logger.py +0 -36
  23. starrocks_br-0.4.0/tests/test_logger.py +0 -276
  24. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/README.md +0 -0
  25. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/setup.cfg +0 -0
  26. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/__init__.py +0 -0
  27. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/concurrency.py +0 -0
  28. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/config.py +0 -0
  29. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/health.py +0 -0
  30. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/repository.py +0 -0
  31. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/schema.py +0 -0
  32. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br/utils.py +0 -0
  33. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/dependency_links.txt +0 -0
  34. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/entry_points.txt +0 -0
  35. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/src/starrocks_br.egg-info/top_level.txt +0 -0
  36. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_concurrency.py +0 -0
  37. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_config.py +0 -0
  38. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_db.py +0 -0
  39. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_health_checks.py +0 -0
  40. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_history.py +0 -0
  41. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_labels.py +0 -0
  42. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_planner.py +0 -0
  43. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_repository_sql.py +0 -0
  44. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_schema_setup.py +0 -0
  45. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_timezone.py +0 -0
  46. {starrocks_br-0.4.0 → starrocks_br-0.5.0}/tests/test_utils.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starrocks-br
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: StarRocks Backup and Restore automation tool
5
- Requires-Python: >=3.9
5
+ Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: click<9,>=8.1.7
8
8
  Requires-Dist: PyYAML<7,>=6.0.1
@@ -12,6 +12,7 @@ Requires-Dist: pytest<9,>=8.3.2; extra == "dev"
12
12
  Requires-Dist: pytest-mock<4,>=3.14.0; extra == "dev"
13
13
  Requires-Dist: pytest-cov<6,>=5.0.0; extra == "dev"
14
14
  Requires-Dist: ruff<1,>=0.8.0; extra == "dev"
15
+ Requires-Dist: pre-commit<5,>=4.0.0; extra == "dev"
15
16
 
16
17
  # StarRocks Backup & Restore
17
18
 
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "starrocks-br"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "StarRocks Backup and Restore automation tool"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "click>=8.1.7,<9",
13
13
  "PyYAML>=6.0.1,<7",
@@ -20,6 +20,7 @@ dev = [
20
20
  "pytest-mock>=3.14.0,<4",
21
21
  "pytest-cov>=5.0.0,<6",
22
22
  "ruff>=0.8.0,<1",
23
+ "pre-commit>=4.0.0,<5",
23
24
  ]
24
25
 
25
26
  [project.scripts]
@@ -47,7 +48,7 @@ line-length = 100
47
48
 
48
49
  [tool.ruff]
49
50
  line-length = 100
50
- target-version = "py39"
51
+ target-version = "py310"
51
52
 
52
53
  [tool.ruff.lint]
53
54
  select = [
@@ -6,6 +6,8 @@ import click
6
6
  from . import (
7
7
  concurrency,
8
8
  db,
9
+ error_handler,
10
+ exceptions,
9
11
  executor,
10
12
  health,
11
13
  labels,
@@ -63,9 +65,20 @@ def _handle_snapshot_exists_error(
63
65
 
64
66
 
65
67
  @click.group()
66
- def cli():
68
+ @click.option("--verbose", is_flag=True, help="Enable verbose debug logging")
69
+ @click.pass_context
70
+ def cli(ctx, verbose):
67
71
  """StarRocks Backup & Restore automation tool."""
68
- pass
72
+ ctx.ensure_object(dict)
73
+ ctx.obj["verbose"] = verbose
74
+
75
+ if verbose:
76
+ import logging
77
+
78
+ logger.setup_logging(level=logging.DEBUG)
79
+ logger.debug("Verbose logging enabled")
80
+ else:
81
+ logger.setup_logging()
69
82
 
70
83
 
71
84
  @cli.command("init")
@@ -429,14 +442,13 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
429
442
  if table:
430
443
  table = table.strip()
431
444
  if not table:
432
- logger.error("Table name cannot be empty")
433
- sys.exit(1)
445
+ raise exceptions.InvalidTableNameError("", "Table name cannot be empty")
434
446
 
435
447
  if "." in table:
436
- logger.error(
437
- "Table name must not include database prefix. Use 'table_name' not 'database.table_name'. Database comes from config file."
448
+ raise exceptions.InvalidTableNameError(
449
+ table,
450
+ "Table name must not include database prefix. Use 'table_name' not 'database.table_name'",
438
451
  )
439
- sys.exit(1)
440
452
 
441
453
  cfg = config_module.load_config(config)
442
454
  config_module.validate_config(cfg)
@@ -472,39 +484,21 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
472
484
 
473
485
  logger.info(f"Finding restore sequence for target backup: {target_label}")
474
486
 
475
- try:
476
- restore_pair = restore.find_restore_pair(database, target_label)
477
- logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
478
- except ValueError as e:
479
- logger.error(f"Failed to find restore sequence: {e}")
480
- sys.exit(1)
487
+ restore_pair = restore.find_restore_pair(database, target_label)
488
+ logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
481
489
 
482
490
  logger.info("Determining tables to restore from backup manifest...")
483
491
 
484
- try:
485
- tables_to_restore = restore.get_tables_from_backup(
486
- database,
487
- target_label,
488
- group=group,
489
- table=table,
490
- database=cfg["database"] if table else None,
491
- )
492
- except ValueError as e:
493
- logger.error(str(e))
494
- sys.exit(1)
492
+ tables_to_restore = restore.get_tables_from_backup(
493
+ database,
494
+ target_label,
495
+ group=group,
496
+ table=table,
497
+ database=cfg["database"] if table else None,
498
+ )
495
499
 
496
500
  if not tables_to_restore:
497
- if group:
498
- logger.warning(
499
- f"No tables found in backup '{target_label}' for group '{group}'"
500
- )
501
- elif table:
502
- logger.warning(
503
- f"No tables found in backup '{target_label}' for table '{table}'"
504
- )
505
- else:
506
- logger.warning(f"No tables found in backup '{target_label}'")
507
- sys.exit(1)
501
+ raise exceptions.NoTablesFoundError(group=group, label=target_label)
508
502
 
509
503
  logger.success(
510
504
  f"Found {len(tables_to_restore)} table(s) to restore: {', '.join(tables_to_restore)}"
@@ -527,11 +521,43 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
527
521
  logger.error(f"Restore failed: {result['error_message']}")
528
522
  sys.exit(1)
529
523
 
524
+ except exceptions.InvalidTableNameError as e:
525
+ error_handler.handle_invalid_table_name_error(e)
526
+ sys.exit(1)
527
+ except exceptions.BackupLabelNotFoundError as e:
528
+ error_handler.handle_backup_label_not_found_error(e, config)
529
+ sys.exit(1)
530
+ except exceptions.NoSuccessfulFullBackupFoundError as e:
531
+ error_handler.handle_no_successful_full_backup_found_error(e, config)
532
+ sys.exit(1)
533
+ except exceptions.TableNotFoundInBackupError as e:
534
+ error_handler.handle_table_not_found_in_backup_error(e, config)
535
+ sys.exit(1)
536
+ except exceptions.NoTablesFoundError as e:
537
+ error_handler.handle_no_tables_found_error(e, config, target_label)
538
+ sys.exit(1)
539
+ except exceptions.SnapshotNotFoundError as e:
540
+ error_handler.handle_snapshot_not_found_error(e, config)
541
+ sys.exit(1)
542
+ except exceptions.RestoreOperationCancelledError:
543
+ error_handler.handle_restore_operation_cancelled_error()
544
+ sys.exit(1)
545
+ except exceptions.ConfigFileNotFoundError as e:
546
+ error_handler.handle_config_file_not_found_error(e)
547
+ sys.exit(1)
548
+ except exceptions.ConfigValidationError as e:
549
+ error_handler.handle_config_validation_error(e, config)
550
+ sys.exit(1)
551
+ except exceptions.ClusterHealthCheckFailedError as e:
552
+ error_handler.handle_cluster_health_check_failed_error(e, config)
553
+ sys.exit(1)
530
554
  except FileNotFoundError as e:
531
- logger.error(f"Config file not found: {e}")
555
+ error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
532
556
  sys.exit(1)
533
557
  except ValueError as e:
534
- logger.error(f"Configuration error: {e}")
558
+ error_handler.handle_config_validation_error(
559
+ exceptions.ConfigValidationError(str(e)), config
560
+ )
535
561
  sys.exit(1)
536
562
  except RuntimeError as e:
537
563
  logger.error(f"{e}")
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  import mysql.connector
4
4
 
@@ -13,7 +13,7 @@ class StarRocksDB:
13
13
  user: str,
14
14
  password: str,
15
15
  database: str,
16
- tls_config: Optional[dict[str, Any]] = None,
16
+ tls_config: dict[str, Any] | None = None,
17
17
  ):
18
18
  """Initialize database connection.
19
19
 
@@ -31,7 +31,7 @@ class StarRocksDB:
31
31
  self.database = database
32
32
  self._connection = None
33
33
  self.tls_config = tls_config or {}
34
- self._timezone: Optional[str] = None
34
+ self._timezone: str | None = None
35
35
 
36
36
  def connect(self) -> None:
37
37
  """Establish database connection."""
@@ -0,0 +1,265 @@
1
+ import click
2
+
3
+ from . import exceptions
4
+
5
+
6
+ def display_structured_error(
7
+ title: str,
8
+ reason: str,
9
+ what_to_do: list[str],
10
+ inputs: dict = None,
11
+ help_links: list[str] = None,
12
+ ) -> None:
13
+ click.echo()
14
+ click.echo(click.style(f"❌ {title}", fg="red", bold=True), err=True)
15
+ click.echo(reason, err=True)
16
+ click.echo()
17
+
18
+ click.echo(click.style("REASON", fg="yellow", bold=True), err=True)
19
+ click.echo(reason, err=True)
20
+ click.echo()
21
+
22
+ click.echo(click.style("WHAT YOU CAN DO", fg="cyan", bold=True), err=True)
23
+ for i, action in enumerate(what_to_do, 1):
24
+ click.echo(f"{i}) {action}", err=True)
25
+ click.echo()
26
+
27
+ if inputs:
28
+ click.echo(click.style("INPUT YOU PROVIDED", fg="magenta", bold=True), err=True)
29
+ for key, value in inputs.items():
30
+ if value is None:
31
+ click.echo(f" Missing: {key}", err=True)
32
+ else:
33
+ click.echo(f" {key}: {value}", err=True)
34
+ click.echo()
35
+
36
+ if help_links:
37
+ click.echo(click.style("NEED HELP?", fg="green", bold=True), err=True)
38
+ for link in help_links:
39
+ click.echo(f" → {link}", err=True)
40
+ click.echo()
41
+
42
+
43
+ def handle_missing_option_error(exc: exceptions.MissingOptionError, config: str = None) -> None:
44
+ display_structured_error(
45
+ title="OPERATION FAILED",
46
+ reason=f'The "{exc.missing_option}" option was not provided.\nThis parameter is required for the operation.',
47
+ what_to_do=[
48
+ f"Add the missing parameter: {exc.missing_option}",
49
+ "Run the command with --help to see all required options",
50
+ f"Example: starrocks-br <command> {exc.missing_option} <value>"
51
+ + (f" --config {config}" if config else ""),
52
+ ],
53
+ inputs={"--config": config, "Missing": exc.missing_option}
54
+ if config
55
+ else {"Missing": exc.missing_option},
56
+ help_links=["Run with --help for more information"],
57
+ )
58
+
59
+
60
+ def handle_backup_label_not_found_error(
61
+ exc: exceptions.BackupLabelNotFoundError, config: str = None
62
+ ) -> None:
63
+ display_structured_error(
64
+ title="RESTORE FAILED",
65
+ reason=f'The backup label "{exc.label}" does not exist in the repository'
66
+ + (f' "{exc.repository}"' if exc.repository else "")
67
+ + ",\nor the backup did not complete successfully.",
68
+ what_to_do=[
69
+ "List available backups by querying the backup history table:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history ORDER BY finished_at DESC;",
70
+ "Check whether the backup completed successfully using StarRocks SQL:"
71
+ + (
72
+ f"\n SHOW BACKUP FROM `{exc.repository}`;"
73
+ if exc.repository
74
+ else "\n SHOW BACKUP;"
75
+ ),
76
+ "Verify that the backup label spelling is correct.",
77
+ ],
78
+ inputs={"--config": config, "--target-label": exc.label, "Repository": exc.repository},
79
+ help_links=["starrocks-br restore --help"],
80
+ )
81
+
82
+
83
+ def handle_no_successful_full_backup_found_error(
84
+ exc: exceptions.NoSuccessfulFullBackupFoundError, config: str = None
85
+ ) -> None:
86
+ display_structured_error(
87
+ title="RESTORE FAILED",
88
+ reason=f'No successful full backup was found before the incremental backup "{exc.incremental_label}".\nIncremental backups require a base full backup to restore from.',
89
+ what_to_do=[
90
+ "Verify that a full backup was created before this incremental backup:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history WHERE backup_type = 'full' AND status = 'FINISHED' ORDER BY finished_at DESC;",
91
+ "Run a full backup first:\n starrocks-br backup full --config "
92
+ + (config if config else "<config.yaml>")
93
+ + " --group <group_name>",
94
+ "Check that the full backup completed successfully before running the incremental backup",
95
+ ],
96
+ inputs={"--target-label": exc.incremental_label, "--config": config},
97
+ help_links=["starrocks-br backup full --help"],
98
+ )
99
+
100
+
101
+ def handle_table_not_found_in_backup_error(
102
+ exc: exceptions.TableNotFoundInBackupError, config: str = None
103
+ ) -> None:
104
+ display_structured_error(
105
+ title="TABLE NOT FOUND",
106
+ reason=f'Table "{exc.table}" was not found in backup "{exc.label}" for database "{exc.database}".',
107
+ what_to_do=[
108
+ "List all tables in the backup:"
109
+ + (
110
+ f"\n SELECT DISTINCT database_name, table_name FROM ops.backup_partitions WHERE label = '{exc.label}';"
111
+ if config
112
+ else ""
113
+ ),
114
+ "Verify the table name spelling is correct",
115
+ f"Ensure the table was included in the backup {exc.label}",
116
+ ],
117
+ inputs={
118
+ "--table": exc.table,
119
+ "--target-label": exc.label,
120
+ "Database": exc.database,
121
+ "--config": config,
122
+ },
123
+ help_links=["starrocks-br restore --help"],
124
+ )
125
+
126
+
127
+ def handle_invalid_table_name_error(exc: exceptions.InvalidTableNameError) -> None:
128
+ display_structured_error(
129
+ title="INVALID TABLE NAME",
130
+ reason=f'The table name "{exc.table_name}" is invalid.\n{exc.reason}',
131
+ what_to_do=[
132
+ "Use only the table name without database prefix",
133
+ "Example: Use 'my_table' instead of 'database.my_table'",
134
+ "The database name should come from the config file",
135
+ ],
136
+ inputs={"--table": exc.table_name},
137
+ help_links=["starrocks-br restore --help"],
138
+ )
139
+
140
+
141
+ def handle_config_file_not_found_error(exc: exceptions.ConfigFileNotFoundError) -> None:
142
+ display_structured_error(
143
+ title="CONFIG FILE NOT FOUND",
144
+ reason=f'The configuration file "{exc.config_path}" could not be found.',
145
+ what_to_do=[
146
+ "Verify the config file path is correct",
147
+ "Ensure the file exists at the specified location",
148
+ "Create a config file with the required settings:\n host, port, user, database, repository",
149
+ ],
150
+ inputs={"--config": exc.config_path},
151
+ help_links=["Check the documentation for config file format"],
152
+ )
153
+
154
+
155
+ def handle_config_validation_error(
156
+ exc: exceptions.ConfigValidationError, config: str = None
157
+ ) -> None:
158
+ display_structured_error(
159
+ title="CONFIGURATION ERROR",
160
+ reason=str(exc),
161
+ what_to_do=[
162
+ "Review your configuration file for missing or invalid settings",
163
+ "Ensure all required fields are present: host, port, user, database, repository",
164
+ "Check that values are in the correct format",
165
+ ],
166
+ inputs={"--config": config},
167
+ help_links=["Check the documentation for config file requirements"],
168
+ )
169
+
170
+
171
+ def handle_cluster_health_check_failed_error(
172
+ exc: exceptions.ClusterHealthCheckFailedError, config: str = None
173
+ ) -> None:
174
+ display_structured_error(
175
+ title="CLUSTER HEALTH CHECK FAILED",
176
+ reason=f"The StarRocks cluster health check failed: {exc.health_message}",
177
+ what_to_do=[
178
+ "Check that the StarRocks cluster is running",
179
+ "Verify database connectivity settings in your config file",
180
+ "Check cluster status with: SHOW PROC '/frontends'; SHOW PROC '/backends';",
181
+ ],
182
+ inputs={"--config": config},
183
+ help_links=["Check StarRocks documentation for troubleshooting cluster issues"],
184
+ )
185
+
186
+
187
+ def handle_snapshot_not_found_error(
188
+ exc: exceptions.SnapshotNotFoundError, config: str = None
189
+ ) -> None:
190
+ display_structured_error(
191
+ title="SNAPSHOT NOT FOUND",
192
+ reason=f'Snapshot "{exc.snapshot_name}" was not found in repository "{exc.repository}".',
193
+ what_to_do=[
194
+ f"List available snapshots:\n SHOW SNAPSHOT ON {exc.repository};",
195
+ "Verify the snapshot name spelling is correct",
196
+ "Ensure the backup completed successfully:\n SELECT * FROM ops.backup_history WHERE label = '"
197
+ + exc.snapshot_name
198
+ + "';",
199
+ ],
200
+ inputs={"Snapshot": exc.snapshot_name, "Repository": exc.repository, "--config": config},
201
+ help_links=["starrocks-br restore --help"],
202
+ )
203
+
204
+
205
+ def handle_no_partitions_found_error(
206
+ exc: exceptions.NoPartitionsFoundError, config: str = None, group: str = None
207
+ ) -> None:
208
+ display_structured_error(
209
+ title="NO PARTITIONS FOUND",
210
+ reason="No partitions were found to backup"
211
+ + (f" for group '{exc.group_name}'" if exc.group_name else "")
212
+ + ".",
213
+ what_to_do=[
214
+ "Verify that the inventory group exists in ops.table_inventory:\n SELECT * FROM ops.table_inventory WHERE inventory_group = "
215
+ + (f"'{exc.group_name}';" if exc.group_name else "'<your_group>';"),
216
+ "Check that the tables in the group have partitions",
217
+ "Ensure the baseline backup date is correct",
218
+ ],
219
+ inputs={"--group": exc.group_name or group, "--config": config},
220
+ help_links=["starrocks-br backup incremental --help"],
221
+ )
222
+
223
+
224
+ def handle_no_tables_found_error(
225
+ exc: exceptions.NoTablesFoundError, config: str = None, target_label: str = None
226
+ ) -> None:
227
+ display_structured_error(
228
+ title="NO TABLES FOUND",
229
+ reason="No tables were found"
230
+ + (
231
+ f" in backup '{exc.label}' for group '{exc.group}'"
232
+ if exc.group and exc.label
233
+ else f" in backup '{exc.label}'"
234
+ if exc.label
235
+ else ""
236
+ )
237
+ + ".",
238
+ what_to_do=[
239
+ "Verify that tables exist in the backup manifest:\n SELECT DISTINCT database_name, table_name FROM ops.backup_partitions WHERE label = "
240
+ + (f"'{exc.label}';" if exc.label else "'<label>';"),
241
+ "Check that the group name is correct in ops.table_inventory"
242
+ if exc.group
243
+ else "Verify the backup completed successfully",
244
+ "List available backups:\n SELECT label, backup_type, status, finished_at FROM ops.backup_history ORDER BY finished_at DESC;",
245
+ ],
246
+ inputs={
247
+ "--target-label": exc.label or target_label,
248
+ "--group": exc.group,
249
+ "--config": config,
250
+ },
251
+ help_links=["starrocks-br restore --help"],
252
+ )
253
+
254
+
255
+ def handle_restore_operation_cancelled_error() -> None:
256
+ display_structured_error(
257
+ title="OPERATION CANCELLED",
258
+ reason="The restore operation was cancelled by the user.",
259
+ what_to_do=[
260
+ "Review the restore plan carefully",
261
+ "Run again and confirm with 'Y' to proceed",
262
+ "Use the --yes flag to skip the confirmation prompt:\n starrocks-br restore --yes ...",
263
+ ],
264
+ help_links=["starrocks-br restore --help"],
265
+ )
@@ -0,0 +1,93 @@
1
+ class StarRocksBRError(Exception):
2
+ pass
3
+
4
+
5
+ class MissingOptionError(StarRocksBRError):
6
+ def __init__(self, missing_option: str):
7
+ self.missing_option = missing_option
8
+ super().__init__(f"Missing required option: {missing_option}")
9
+
10
+
11
+ class BackupLabelNotFoundError(StarRocksBRError):
12
+ def __init__(self, label: str, repository: str = None):
13
+ self.label = label
14
+ self.repository = repository
15
+ if repository:
16
+ super().__init__(f"Backup label '{label}' not found in repository '{repository}'")
17
+ else:
18
+ super().__init__(f"Backup label '{label}' not found")
19
+
20
+
21
+ class NoSuccessfulFullBackupFoundError(StarRocksBRError):
22
+ def __init__(self, incremental_label: str):
23
+ self.incremental_label = incremental_label
24
+ super().__init__(
25
+ f"No successful full backup found before incremental '{incremental_label}'"
26
+ )
27
+
28
+
29
+ class TableNotFoundInBackupError(StarRocksBRError):
30
+ def __init__(self, table: str, label: str, database: str):
31
+ self.table = table
32
+ self.label = label
33
+ self.database = database
34
+ super().__init__(f"Table '{table}' not found in backup '{label}' for database '{database}'")
35
+
36
+
37
+ class InvalidTableNameError(StarRocksBRError):
38
+ def __init__(self, table_name: str, reason: str):
39
+ self.table_name = table_name
40
+ self.reason = reason
41
+ super().__init__(f"Invalid table name '{table_name}': {reason}")
42
+
43
+
44
+ class ConfigFileNotFoundError(StarRocksBRError):
45
+ def __init__(self, config_path: str):
46
+ self.config_path = config_path
47
+ super().__init__(f"Config file not found: {config_path}")
48
+
49
+
50
+ class ConfigValidationError(StarRocksBRError):
51
+ def __init__(self, message: str):
52
+ super().__init__(f"Configuration error: {message}")
53
+
54
+
55
+ class ClusterHealthCheckFailedError(StarRocksBRError):
56
+ def __init__(self, message: str):
57
+ self.health_message = message
58
+ super().__init__(f"Cluster health check failed: {message}")
59
+
60
+
61
+ class SnapshotNotFoundError(StarRocksBRError):
62
+ def __init__(self, snapshot_name: str, repository: str):
63
+ self.snapshot_name = snapshot_name
64
+ self.repository = repository
65
+ super().__init__(f"Snapshot '{snapshot_name}' not found in repository '{repository}'")
66
+
67
+
68
+ class NoPartitionsFoundError(StarRocksBRError):
69
+ def __init__(self, group_name: str = None):
70
+ self.group_name = group_name
71
+ if group_name:
72
+ super().__init__(f"No partitions found to backup for group '{group_name}'")
73
+ else:
74
+ super().__init__("No partitions found to backup")
75
+
76
+
77
+ class NoTablesFoundError(StarRocksBRError):
78
+ def __init__(self, group: str = None, label: str = None):
79
+ self.group = group
80
+ self.label = label
81
+ if group and label:
82
+ super().__init__(f"No tables found in backup '{label}' for group '{group}'")
83
+ elif group:
84
+ super().__init__(f"No tables found for group '{group}'")
85
+ elif label:
86
+ super().__init__(f"No tables found in backup '{label}'")
87
+ else:
88
+ super().__init__("No tables found")
89
+
90
+
91
+ class RestoreOperationCancelledError(StarRocksBRError):
92
+ def __init__(self):
93
+ super().__init__("Restore operation cancelled by user")
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  import time
3
- from typing import Literal, Optional
3
+ from typing import Literal
4
4
 
5
5
  from . import concurrency, history, logger, timezone
6
6
 
@@ -22,7 +22,7 @@ def _calculate_next_interval(current_interval: float, max_interval: float) -> fl
22
22
 
23
23
  def submit_backup_command(
24
24
  db, backup_command: str
25
- ) -> tuple[bool, Optional[str], Optional[dict[str, str]]]:
25
+ ) -> tuple[bool, str | None, dict[str, str] | None]:
26
26
  """Submit a backup command to StarRocks.
27
27
 
28
28
  Returns (success, error_message, error_details).
@@ -50,7 +50,7 @@ def submit_backup_command(
50
50
  return False, error_msg, None
51
51
 
52
52
 
53
- def _check_snapshot_exists_error(exception: Exception, error_str: str) -> Optional[str]:
53
+ def _check_snapshot_exists_error(exception: Exception, error_str: str) -> str | None:
54
54
  """Check if the error is a 'snapshot already exists' error and extract snapshot name.
55
55
 
56
56
  Args:
@@ -166,7 +166,7 @@ def execute_backup(
166
166
  repository: str,
167
167
  backup_type: Literal["incremental", "full"] = None,
168
168
  scope: str = "backup",
169
- database: Optional[str] = None,
169
+ database: str | None = None,
170
170
  ) -> dict:
171
171
  """Execute a complete backup workflow: submit command and monitor progress.
172
172
 
@@ -1,9 +1,7 @@
1
- from typing import Optional
2
-
3
1
  from . import logger
4
2
 
5
3
 
6
- def log_backup(db, entry: dict[str, Optional[str]]) -> None:
4
+ def log_backup(db, entry: dict[str, str | None]) -> None:
7
5
  """Write a backup history entry to ops.backup_history.
8
6
 
9
7
  Expected keys in entry:
@@ -24,7 +22,7 @@ def log_backup(db, entry: dict[str, Optional[str]]) -> None:
24
22
  finished_at = entry.get("finished_at", "NULL")
25
23
  error_message = entry.get("error_message")
26
24
 
27
- def esc(val: Optional[str]) -> str:
25
+ def esc(val: str | None) -> str:
28
26
  if val is None:
29
27
  return "NULL"
30
28
  return "'" + str(val).replace("'", "''") + "'"
@@ -45,7 +43,7 @@ def log_backup(db, entry: dict[str, Optional[str]]) -> None:
45
43
  raise
46
44
 
47
45
 
48
- def log_restore(db, entry: dict[str, Optional[str]]) -> None:
46
+ def log_restore(db, entry: dict[str, str | None]) -> None:
49
47
  """Write a restore history entry to ops.restore_history.
50
48
 
51
49
  Expected keys in entry:
@@ -69,7 +67,7 @@ def log_restore(db, entry: dict[str, Optional[str]]) -> None:
69
67
  error_message = entry.get("error_message")
70
68
  verification_checksum = entry.get("verification_checksum")
71
69
 
72
- def esc(val: Optional[str]) -> str:
70
+ def esc(val: str | None) -> str:
73
71
  if val is None:
74
72
  return "NULL"
75
73
  return "'" + str(val).replace("'", "''") + "'"
@@ -1,12 +1,12 @@
1
1
  from datetime import datetime
2
- from typing import Literal, Optional
2
+ from typing import Literal
3
3
 
4
4
 
5
5
  def determine_backup_label(
6
6
  db,
7
7
  backup_type: Literal["incremental", "full"],
8
8
  database_name: str,
9
- custom_name: Optional[str] = None,
9
+ custom_name: str | None = None,
10
10
  ) -> str:
11
11
  """Determine a unique backup label for the given parameters.
12
12