starrocks-br 0.5.2__py3-none-any.whl → 0.6.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.
- starrocks_br/cli.py +103 -36
- starrocks_br/concurrency.py +33 -23
- starrocks_br/config.py +75 -0
- starrocks_br/error_handler.py +59 -12
- starrocks_br/exceptions.py +14 -0
- starrocks_br/executor.py +9 -2
- starrocks_br/history.py +9 -9
- starrocks_br/labels.py +5 -3
- starrocks_br/planner.py +56 -13
- starrocks_br/repository.py +1 -1
- starrocks_br/restore.py +197 -40
- starrocks_br/schema.py +89 -43
- {starrocks_br-0.5.2.dist-info → starrocks_br-0.6.0.dist-info}/METADATA +15 -2
- starrocks_br-0.6.0.dist-info/RECORD +24 -0
- {starrocks_br-0.5.2.dist-info → starrocks_br-0.6.0.dist-info}/WHEEL +1 -1
- starrocks_br-0.5.2.dist-info/RECORD +0 -24
- {starrocks_br-0.5.2.dist-info → starrocks_br-0.6.0.dist-info}/entry_points.txt +0 -0
- {starrocks_br-0.5.2.dist-info → starrocks_br-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {starrocks_br-0.5.2.dist-info → starrocks_br-0.6.0.dist-info}/top_level.txt +0 -0
starrocks_br/error_handler.py
CHANGED
|
@@ -14,9 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
import click
|
|
16
16
|
|
|
17
|
+
from . import config as config_module
|
|
17
18
|
from . import exceptions
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def _get_ops_database_name(config_path: str | None) -> str:
|
|
22
|
+
if not config_path:
|
|
23
|
+
return "ops"
|
|
24
|
+
try:
|
|
25
|
+
cfg = config_module.load_config(config_path)
|
|
26
|
+
return config_module.get_ops_database(cfg)
|
|
27
|
+
except Exception:
|
|
28
|
+
return "ops"
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
def display_structured_error(
|
|
21
32
|
title: str,
|
|
22
33
|
reason: str,
|
|
@@ -74,13 +85,14 @@ def handle_missing_option_error(exc: exceptions.MissingOptionError, config: str
|
|
|
74
85
|
def handle_backup_label_not_found_error(
|
|
75
86
|
exc: exceptions.BackupLabelNotFoundError, config: str = None
|
|
76
87
|
) -> None:
|
|
88
|
+
ops_db = _get_ops_database_name(config)
|
|
77
89
|
display_structured_error(
|
|
78
90
|
title="RESTORE FAILED",
|
|
79
91
|
reason=f'The backup label "{exc.label}" does not exist in the repository'
|
|
80
92
|
+ (f' "{exc.repository}"' if exc.repository else "")
|
|
81
93
|
+ ",\nor the backup did not complete successfully.",
|
|
82
94
|
what_to_do=[
|
|
83
|
-
"List available backups by querying the backup history table:\n SELECT label, backup_type, status, finished_at FROM
|
|
95
|
+
f"List available backups by querying the backup history table:\n SELECT label, backup_type, status, finished_at FROM {ops_db}.backup_history ORDER BY finished_at DESC;",
|
|
84
96
|
"Check whether the backup completed successfully using StarRocks SQL:"
|
|
85
97
|
+ (
|
|
86
98
|
f"\n SHOW BACKUP FROM `{exc.repository}`;"
|
|
@@ -97,11 +109,12 @@ def handle_backup_label_not_found_error(
|
|
|
97
109
|
def handle_no_successful_full_backup_found_error(
|
|
98
110
|
exc: exceptions.NoSuccessfulFullBackupFoundError, config: str = None
|
|
99
111
|
) -> None:
|
|
112
|
+
ops_db = _get_ops_database_name(config)
|
|
100
113
|
display_structured_error(
|
|
101
114
|
title="RESTORE FAILED",
|
|
102
115
|
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.',
|
|
103
116
|
what_to_do=[
|
|
104
|
-
"Verify that a full backup was created before this incremental backup:\n SELECT label, backup_type, status, finished_at FROM
|
|
117
|
+
f"Verify that a full backup was created before this incremental backup:\n SELECT label, backup_type, status, finished_at FROM {ops_db}.backup_history WHERE backup_type = 'full' AND status = 'FINISHED' ORDER BY finished_at DESC;",
|
|
105
118
|
"Run a full backup first:\n starrocks-br backup full --config "
|
|
106
119
|
+ (config if config else "<config.yaml>")
|
|
107
120
|
+ " --group <group_name>",
|
|
@@ -115,13 +128,14 @@ def handle_no_successful_full_backup_found_error(
|
|
|
115
128
|
def handle_table_not_found_in_backup_error(
|
|
116
129
|
exc: exceptions.TableNotFoundInBackupError, config: str = None
|
|
117
130
|
) -> None:
|
|
131
|
+
ops_db = _get_ops_database_name(config)
|
|
118
132
|
display_structured_error(
|
|
119
133
|
title="TABLE NOT FOUND",
|
|
120
134
|
reason=f'Table "{exc.table}" was not found in backup "{exc.label}" for database "{exc.database}".',
|
|
121
135
|
what_to_do=[
|
|
122
136
|
"List all tables in the backup:"
|
|
123
137
|
+ (
|
|
124
|
-
f"\n SELECT DISTINCT database_name, table_name FROM
|
|
138
|
+
f"\n SELECT DISTINCT database_name, table_name FROM {ops_db}.backup_partitions WHERE label = '{exc.label}';"
|
|
125
139
|
if config
|
|
126
140
|
else ""
|
|
127
141
|
),
|
|
@@ -201,13 +215,14 @@ def handle_cluster_health_check_failed_error(
|
|
|
201
215
|
def handle_snapshot_not_found_error(
|
|
202
216
|
exc: exceptions.SnapshotNotFoundError, config: str = None
|
|
203
217
|
) -> None:
|
|
218
|
+
ops_db = _get_ops_database_name(config)
|
|
204
219
|
display_structured_error(
|
|
205
220
|
title="SNAPSHOT NOT FOUND",
|
|
206
221
|
reason=f'Snapshot "{exc.snapshot_name}" was not found in repository "{exc.repository}".',
|
|
207
222
|
what_to_do=[
|
|
208
223
|
f"List available snapshots:\n SHOW SNAPSHOT ON {exc.repository};",
|
|
209
224
|
"Verify the snapshot name spelling is correct",
|
|
210
|
-
"Ensure the backup completed successfully:\n SELECT * FROM
|
|
225
|
+
f"Ensure the backup completed successfully:\n SELECT * FROM {ops_db}.backup_history WHERE label = '"
|
|
211
226
|
+ exc.snapshot_name
|
|
212
227
|
+ "';",
|
|
213
228
|
],
|
|
@@ -219,13 +234,14 @@ def handle_snapshot_not_found_error(
|
|
|
219
234
|
def handle_no_partitions_found_error(
|
|
220
235
|
exc: exceptions.NoPartitionsFoundError, config: str = None, group: str = None
|
|
221
236
|
) -> None:
|
|
237
|
+
ops_db = _get_ops_database_name(config)
|
|
222
238
|
display_structured_error(
|
|
223
239
|
title="NO PARTITIONS FOUND",
|
|
224
240
|
reason="No partitions were found to backup"
|
|
225
241
|
+ (f" for group '{exc.group_name}'" if exc.group_name else "")
|
|
226
242
|
+ ".",
|
|
227
243
|
what_to_do=[
|
|
228
|
-
"Verify that the inventory group exists in
|
|
244
|
+
f"Verify that the inventory group exists in {ops_db}.table_inventory:\n SELECT * FROM {ops_db}.table_inventory WHERE inventory_group = "
|
|
229
245
|
+ (f"'{exc.group_name}';" if exc.group_name else "'<your_group>';"),
|
|
230
246
|
"Check that the tables in the group have partitions",
|
|
231
247
|
"Ensure the baseline backup date is correct",
|
|
@@ -238,6 +254,7 @@ def handle_no_partitions_found_error(
|
|
|
238
254
|
def handle_no_tables_found_error(
|
|
239
255
|
exc: exceptions.NoTablesFoundError, config: str = None, target_label: str = None
|
|
240
256
|
) -> None:
|
|
257
|
+
ops_db = _get_ops_database_name(config)
|
|
241
258
|
display_structured_error(
|
|
242
259
|
title="NO TABLES FOUND",
|
|
243
260
|
reason="No tables were found"
|
|
@@ -250,12 +267,12 @@ def handle_no_tables_found_error(
|
|
|
250
267
|
)
|
|
251
268
|
+ ".",
|
|
252
269
|
what_to_do=[
|
|
253
|
-
"Verify that tables exist in the backup manifest:\n SELECT DISTINCT database_name, table_name FROM
|
|
270
|
+
f"Verify that tables exist in the backup manifest:\n SELECT DISTINCT database_name, table_name FROM {ops_db}.backup_partitions WHERE label = "
|
|
254
271
|
+ (f"'{exc.label}';" if exc.label else "'<label>';"),
|
|
255
|
-
"Check that the group name is correct in
|
|
272
|
+
f"Check that the group name is correct in {ops_db}.table_inventory"
|
|
256
273
|
if exc.group
|
|
257
274
|
else "Verify the backup completed successfully",
|
|
258
|
-
"List available backups:\n SELECT label, backup_type, status, finished_at FROM
|
|
275
|
+
f"List available backups:\n SELECT label, backup_type, status, finished_at FROM {ops_db}.backup_history ORDER BY finished_at DESC;",
|
|
259
276
|
],
|
|
260
277
|
inputs={
|
|
261
278
|
"--target-label": exc.label or target_label,
|
|
@@ -282,6 +299,7 @@ def handle_restore_operation_cancelled_error() -> None:
|
|
|
282
299
|
def handle_concurrency_conflict_error(
|
|
283
300
|
exc: exceptions.ConcurrencyConflictError, config: str = None
|
|
284
301
|
) -> None:
|
|
302
|
+
ops_db = _get_ops_database_name(config)
|
|
285
303
|
active_job_strings = [f"{job[0]}:{job[1]}" for job in exc.active_jobs]
|
|
286
304
|
first_label = exc.active_labels[0] if exc.active_labels else "unknown"
|
|
287
305
|
|
|
@@ -290,8 +308,8 @@ def handle_concurrency_conflict_error(
|
|
|
290
308
|
reason=f"Another '{exc.scope}' job is already running.\nOnly one job of the same type can run at a time to prevent conflicts.",
|
|
291
309
|
what_to_do=[
|
|
292
310
|
f"Wait for the active job to complete: {', '.join(active_job_strings)}",
|
|
293
|
-
f"Check the job status in
|
|
294
|
-
f"If the job is stuck, cancel it manually:\n UPDATE
|
|
311
|
+
f"Check the job status in {ops_db}.run_status:\n SELECT * FROM {ops_db}.run_status WHERE label = '{first_label}' AND state = 'ACTIVE';",
|
|
312
|
+
f"If the job is stuck, cancel it manually:\n UPDATE {ops_db}.run_status SET state = 'CANCELLED' WHERE label = '{first_label}' AND state = 'ACTIVE';",
|
|
295
313
|
"Verify the job is not actually running in StarRocks before cancelling it",
|
|
296
314
|
],
|
|
297
315
|
inputs={
|
|
@@ -299,13 +317,14 @@ def handle_concurrency_conflict_error(
|
|
|
299
317
|
"Scope": exc.scope,
|
|
300
318
|
"Active jobs": ", ".join(active_job_strings),
|
|
301
319
|
},
|
|
302
|
-
help_links=["Check
|
|
320
|
+
help_links=[f"Check {ops_db}.run_status table for job status"],
|
|
303
321
|
)
|
|
304
322
|
|
|
305
323
|
|
|
306
324
|
def handle_no_full_backup_found_error(
|
|
307
325
|
exc: exceptions.NoFullBackupFoundError, config: str = None, group: str = None
|
|
308
326
|
) -> None:
|
|
327
|
+
ops_db = _get_ops_database_name(config)
|
|
309
328
|
display_structured_error(
|
|
310
329
|
title="NO FULL BACKUP FOUND",
|
|
311
330
|
reason=f"No successful full backup was found for database '{exc.database}'.\nIncremental backups require a baseline full backup to compare against.",
|
|
@@ -313,9 +332,37 @@ def handle_no_full_backup_found_error(
|
|
|
313
332
|
"Run a full backup first:\n starrocks-br backup full --config "
|
|
314
333
|
+ (config if config else "<config.yaml>")
|
|
315
334
|
+ f" --group {group if group else '<group_name>'}",
|
|
316
|
-
f"Verify no full backups exist for this database:\n SELECT label, backup_type, status, finished_at FROM
|
|
335
|
+
f"Verify no full backups exist for this database:\n SELECT label, backup_type, status, finished_at FROM {ops_db}.backup_history WHERE backup_type = 'full' AND label LIKE '{exc.database}_%' ORDER BY finished_at DESC;",
|
|
317
336
|
"After the full backup completes successfully, retry the incremental backup",
|
|
318
337
|
],
|
|
319
338
|
inputs={"Database": exc.database, "--config": config, "--group": group},
|
|
320
339
|
help_links=["starrocks-br backup full --help"],
|
|
321
340
|
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def handle_invalid_tables_in_inventory_error(
|
|
344
|
+
exc: exceptions.InvalidTablesInInventoryError, config: str = None
|
|
345
|
+
) -> None:
|
|
346
|
+
ops_db = _get_ops_database_name(config)
|
|
347
|
+
invalid_tables_str = ", ".join(f"'{t}'" for t in exc.invalid_tables)
|
|
348
|
+
|
|
349
|
+
display_structured_error(
|
|
350
|
+
title="INVALID TABLES IN INVENTORY",
|
|
351
|
+
reason=f"The following table(s) in the inventory do not exist in database '{exc.database}':\n{invalid_tables_str}\n\nThese tables are referenced in the table inventory but cannot be found in the actual database.",
|
|
352
|
+
what_to_do=[
|
|
353
|
+
f"Remove invalid tables from the table inventory:\n DELETE FROM {ops_db}.table_inventory WHERE database_name = '{exc.database}' AND table_name IN ({invalid_tables_str});",
|
|
354
|
+
"Verify the table names are spelled correctly in the inventory",
|
|
355
|
+
f"Check which tables exist in the database:\n SHOW TABLES FROM `{exc.database}`;",
|
|
356
|
+
f"Update the inventory with correct table names:\n UPDATE {ops_db}.table_inventory SET table_name = '<correct_name>' WHERE database_name = '{exc.database}' AND table_name = '<wrong_name>';",
|
|
357
|
+
],
|
|
358
|
+
inputs={
|
|
359
|
+
"Database": exc.database,
|
|
360
|
+
"Invalid tables": invalid_tables_str,
|
|
361
|
+
"Group": exc.group,
|
|
362
|
+
"--config": config,
|
|
363
|
+
},
|
|
364
|
+
help_links=[
|
|
365
|
+
f"Check {ops_db}.table_inventory for your inventory configuration",
|
|
366
|
+
"Run 'SHOW TABLES' to see available tables",
|
|
367
|
+
],
|
|
368
|
+
)
|
starrocks_br/exceptions.py
CHANGED
|
@@ -122,3 +122,17 @@ class NoFullBackupFoundError(StarRocksBRError):
|
|
|
122
122
|
def __init__(self, database: str):
|
|
123
123
|
self.database = database
|
|
124
124
|
super().__init__(f"No successful full backup found for database '{database}'")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class InvalidTablesInInventoryError(StarRocksBRError):
|
|
128
|
+
def __init__(self, database: str, invalid_tables: list[str], group: str = None):
|
|
129
|
+
self.database = database
|
|
130
|
+
self.invalid_tables = invalid_tables
|
|
131
|
+
self.group = group
|
|
132
|
+
tables_str = ", ".join(f"'{t}'" for t in invalid_tables)
|
|
133
|
+
if group:
|
|
134
|
+
super().__init__(
|
|
135
|
+
f"Invalid tables in inventory group '{group}' for database '{database}': {tables_str}"
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
super().__init__(f"Invalid tables for database '{database}': {tables_str}")
|
starrocks_br/executor.py
CHANGED
|
@@ -181,6 +181,7 @@ def execute_backup(
|
|
|
181
181
|
backup_type: Literal["incremental", "full"] = None,
|
|
182
182
|
scope: str = "backup",
|
|
183
183
|
database: str | None = None,
|
|
184
|
+
ops_database: str = "ops",
|
|
184
185
|
) -> dict:
|
|
185
186
|
"""Execute a complete backup workflow: submit command and monitor progress.
|
|
186
187
|
|
|
@@ -193,6 +194,7 @@ def execute_backup(
|
|
|
193
194
|
backup_type: Type of backup (for logging)
|
|
194
195
|
scope: Job scope (for concurrency control)
|
|
195
196
|
database: Database name (required for SHOW BACKUP)
|
|
197
|
+
ops_database: Name of ops database (default: "ops")
|
|
196
198
|
|
|
197
199
|
Returns dictionary with keys: success, final_status, error_message
|
|
198
200
|
"""
|
|
@@ -233,13 +235,18 @@ def execute_backup(
|
|
|
233
235
|
"finished_at": finished_at,
|
|
234
236
|
"error_message": None if success else (final_status["state"] or ""),
|
|
235
237
|
},
|
|
238
|
+
ops_database=ops_database,
|
|
236
239
|
)
|
|
237
240
|
except Exception:
|
|
238
241
|
pass
|
|
239
242
|
|
|
240
243
|
try:
|
|
241
244
|
concurrency.complete_job_slot(
|
|
242
|
-
db,
|
|
245
|
+
db,
|
|
246
|
+
scope=scope,
|
|
247
|
+
label=label,
|
|
248
|
+
final_state=final_status["state"],
|
|
249
|
+
ops_database=ops_database,
|
|
243
250
|
)
|
|
244
251
|
except Exception:
|
|
245
252
|
pass
|
|
@@ -271,7 +278,7 @@ def _build_error_message(final_status: dict, label: str, database: str) -> str:
|
|
|
271
278
|
f"Backup tracking lost for '{label}' in database '{database}'. "
|
|
272
279
|
f"Another backup operation overwrote the last backup status visible in SHOW BACKUP. "
|
|
273
280
|
f"This indicates a concurrency issue - only one backup per database should run at a time. "
|
|
274
|
-
f"Recommendation: Use
|
|
281
|
+
f"Recommendation: Use run_status concurrency control to prevent simultaneous backups, "
|
|
275
282
|
f"or verify if another tool/user is running backups on this database."
|
|
276
283
|
)
|
|
277
284
|
elif state == "CANCELLED":
|
starrocks_br/history.py
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
from . import logger
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def log_backup(db, entry: dict[str, str | None]) -> None:
|
|
19
|
-
"""Write a backup history entry to
|
|
18
|
+
def log_backup(db, entry: dict[str, str | None], ops_database: str = "ops") -> None:
|
|
19
|
+
"""Write a backup history entry to the backup_history table.
|
|
20
20
|
|
|
21
21
|
Expected keys in entry:
|
|
22
22
|
- job_id (optional; auto-generated if missing)
|
|
@@ -42,7 +42,7 @@ def log_backup(db, entry: dict[str, str | None]) -> None:
|
|
|
42
42
|
return "'" + str(val).replace("'", "''") + "'"
|
|
43
43
|
|
|
44
44
|
sql = f"""
|
|
45
|
-
INSERT INTO
|
|
45
|
+
INSERT INTO {ops_database}.backup_history (
|
|
46
46
|
label, backup_type, status, repository, started_at, finished_at, error_message
|
|
47
47
|
) VALUES (
|
|
48
48
|
{esc(label)}, {esc(backup_type)}, {esc(status)}, {esc(repository)},
|
|
@@ -57,8 +57,8 @@ def log_backup(db, entry: dict[str, str | None]) -> None:
|
|
|
57
57
|
raise
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def log_restore(db, entry: dict[str, str | None]) -> None:
|
|
61
|
-
"""Write a restore history entry to
|
|
60
|
+
def log_restore(db, entry: dict[str, str | None], ops_database: str = "ops") -> None:
|
|
61
|
+
"""Write a restore history entry to the restore_history table.
|
|
62
62
|
|
|
63
63
|
Expected keys in entry:
|
|
64
64
|
- job_id
|
|
@@ -87,12 +87,12 @@ def log_restore(db, entry: dict[str, str | None]) -> None:
|
|
|
87
87
|
return "'" + str(val).replace("'", "''") + "'"
|
|
88
88
|
|
|
89
89
|
sql = f"""
|
|
90
|
-
INSERT INTO
|
|
91
|
-
job_id, backup_label, restore_type, status, repository,
|
|
90
|
+
INSERT INTO {ops_database}.restore_history (
|
|
91
|
+
job_id, backup_label, restore_type, status, repository,
|
|
92
92
|
started_at, finished_at, error_message, verification_checksum
|
|
93
93
|
) VALUES (
|
|
94
|
-
{esc(job_id)}, {esc(backup_label)}, {esc(restore_type)}, {esc(status)},
|
|
95
|
-
{esc(repository)}, {esc(started_at)}, {esc(finished_at)},
|
|
94
|
+
{esc(job_id)}, {esc(backup_label)}, {esc(restore_type)}, {esc(status)},
|
|
95
|
+
{esc(repository)}, {esc(started_at)}, {esc(finished_at)},
|
|
96
96
|
{esc(error_message)}, {esc(verification_checksum)}
|
|
97
97
|
)
|
|
98
98
|
"""
|
starrocks_br/labels.py
CHANGED
|
@@ -21,12 +21,13 @@ def determine_backup_label(
|
|
|
21
21
|
backup_type: Literal["incremental", "full"],
|
|
22
22
|
database_name: str,
|
|
23
23
|
custom_name: str | None = None,
|
|
24
|
+
ops_database: str = "ops",
|
|
24
25
|
) -> str:
|
|
25
26
|
"""Determine a unique backup label for the given parameters.
|
|
26
27
|
|
|
27
28
|
This is the single entry point for all backup label generation. It handles both
|
|
28
29
|
custom names and auto-generated date-based labels, ensuring uniqueness by checking
|
|
29
|
-
the
|
|
30
|
+
the backup_history table in the configured ops database.
|
|
30
31
|
|
|
31
32
|
Args:
|
|
32
33
|
db: Database connection
|
|
@@ -34,6 +35,7 @@ def determine_backup_label(
|
|
|
34
35
|
database_name: Name of the database being backed up
|
|
35
36
|
custom_name: Optional custom name for the backup. If provided, this becomes
|
|
36
37
|
the base label. If None, generates a date-based label.
|
|
38
|
+
ops_database: Name of the database containing operational tables. Defaults to "ops".
|
|
37
39
|
|
|
38
40
|
Returns:
|
|
39
41
|
Unique label string that doesn't conflict with existing backups
|
|
@@ -44,9 +46,9 @@ def determine_backup_label(
|
|
|
44
46
|
today = datetime.now().strftime("%Y%m%d")
|
|
45
47
|
base_label = f"{database_name}_{today}_{backup_type}"
|
|
46
48
|
|
|
47
|
-
query = """
|
|
49
|
+
query = f"""
|
|
48
50
|
SELECT label
|
|
49
|
-
FROM
|
|
51
|
+
FROM {ops_database}.backup_history
|
|
50
52
|
WHERE label LIKE %s
|
|
51
53
|
ORDER BY label
|
|
52
54
|
"""
|
starrocks_br/planner.py
CHANGED
|
@@ -18,7 +18,7 @@ import hashlib
|
|
|
18
18
|
from starrocks_br import exceptions, logger, timezone, utils
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def find_latest_full_backup(db, database: str) -> dict[str, str] | None:
|
|
21
|
+
def find_latest_full_backup(db, database: str, ops_database: str = "ops") -> dict[str, str] | None:
|
|
22
22
|
"""Find the latest successful full backup for a database.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
@@ -31,7 +31,7 @@ def find_latest_full_backup(db, database: str) -> dict[str, str] | None:
|
|
|
31
31
|
"""
|
|
32
32
|
query = f"""
|
|
33
33
|
SELECT label, backup_type, finished_at
|
|
34
|
-
FROM
|
|
34
|
+
FROM {ops_database}.backup_history
|
|
35
35
|
WHERE backup_type = 'full'
|
|
36
36
|
AND status = 'FINISHED'
|
|
37
37
|
AND label LIKE {utils.quote_value(f"{database}_%")}
|
|
@@ -56,7 +56,7 @@ def find_latest_full_backup(db, database: str) -> dict[str, str] | None:
|
|
|
56
56
|
return {"label": row[0], "backup_type": row[1], "finished_at": finished_at}
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def find_tables_by_group(db, group_name: str) -> list[dict[str, str]]:
|
|
59
|
+
def find_tables_by_group(db, group_name: str, ops_database: str = "ops") -> list[dict[str, str]]:
|
|
60
60
|
"""Find tables belonging to a specific inventory group.
|
|
61
61
|
|
|
62
62
|
Returns list of dictionaries with keys: database, table.
|
|
@@ -64,7 +64,7 @@ def find_tables_by_group(db, group_name: str) -> list[dict[str, str]]:
|
|
|
64
64
|
"""
|
|
65
65
|
query = f"""
|
|
66
66
|
SELECT database_name, table_name
|
|
67
|
-
FROM
|
|
67
|
+
FROM {ops_database}.table_inventory
|
|
68
68
|
WHERE inventory_group = {utils.quote_value(group_name)}
|
|
69
69
|
ORDER BY database_name, table_name
|
|
70
70
|
"""
|
|
@@ -72,8 +72,49 @@ def find_tables_by_group(db, group_name: str) -> list[dict[str, str]]:
|
|
|
72
72
|
return [{"database": row[0], "table": row[1]} for row in rows]
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def validate_tables_exist(
|
|
76
|
+
db, database: str, tables: list[dict[str, str]], group: str = None
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Validate that tables in the inventory actually exist in the database.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
db: Database connection
|
|
82
|
+
database: Database name to validate tables against
|
|
83
|
+
tables: List of tables with keys: database, table
|
|
84
|
+
group: Optional inventory group name for better error messages
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
InvalidTablesInInventoryError: If any tables don't exist in the database
|
|
88
|
+
"""
|
|
89
|
+
if not tables:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
db_tables = [t for t in tables if t["database"] == database and t["table"] != "*"]
|
|
93
|
+
|
|
94
|
+
if not db_tables:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
show_tables_query = f"SHOW TABLES FROM {utils.quote_identifier(database)}"
|
|
98
|
+
existing_tables_rows = db.query(show_tables_query)
|
|
99
|
+
existing_tables = {row[0] for row in existing_tables_rows}
|
|
100
|
+
|
|
101
|
+
invalid_tables = []
|
|
102
|
+
for table_entry in db_tables:
|
|
103
|
+
table_name = table_entry["table"]
|
|
104
|
+
if table_name not in existing_tables:
|
|
105
|
+
invalid_tables.append(table_name)
|
|
106
|
+
|
|
107
|
+
if invalid_tables:
|
|
108
|
+
raise exceptions.InvalidTablesInInventoryError(database, invalid_tables, group)
|
|
109
|
+
|
|
110
|
+
|
|
75
111
|
def find_recent_partitions(
|
|
76
|
-
db,
|
|
112
|
+
db,
|
|
113
|
+
database: str,
|
|
114
|
+
baseline_backup_label: str | None = None,
|
|
115
|
+
*,
|
|
116
|
+
group_name: str,
|
|
117
|
+
ops_database: str = "ops",
|
|
77
118
|
) -> list[dict[str, str]]:
|
|
78
119
|
"""Find partitions updated since baseline for tables in the given inventory group.
|
|
79
120
|
|
|
@@ -91,7 +132,7 @@ def find_recent_partitions(
|
|
|
91
132
|
if baseline_backup_label:
|
|
92
133
|
baseline_query = f"""
|
|
93
134
|
SELECT finished_at
|
|
94
|
-
FROM
|
|
135
|
+
FROM {ops_database}.backup_history
|
|
95
136
|
WHERE label = {utils.quote_value(baseline_backup_label)}
|
|
96
137
|
AND status = 'FINISHED'
|
|
97
138
|
"""
|
|
@@ -100,7 +141,7 @@ def find_recent_partitions(
|
|
|
100
141
|
raise exceptions.BackupLabelNotFoundError(baseline_backup_label)
|
|
101
142
|
baseline_time_raw = baseline_rows[0][0]
|
|
102
143
|
else:
|
|
103
|
-
latest_backup = find_latest_full_backup(db, database)
|
|
144
|
+
latest_backup = find_latest_full_backup(db, database, ops_database)
|
|
104
145
|
if not latest_backup:
|
|
105
146
|
raise exceptions.NoFullBackupFoundError(database)
|
|
106
147
|
baseline_time_raw = latest_backup["finished_at"]
|
|
@@ -114,7 +155,7 @@ def find_recent_partitions(
|
|
|
114
155
|
|
|
115
156
|
baseline_dt = timezone.parse_datetime_with_tz(baseline_time_str, cluster_tz)
|
|
116
157
|
|
|
117
|
-
group_tables = find_tables_by_group(db, group_name)
|
|
158
|
+
group_tables = find_tables_by_group(db, group_name, ops_database)
|
|
118
159
|
|
|
119
160
|
if not group_tables:
|
|
120
161
|
return []
|
|
@@ -218,7 +259,7 @@ def build_incremental_backup_command(
|
|
|
218
259
|
|
|
219
260
|
|
|
220
261
|
def build_full_backup_command(
|
|
221
|
-
db, group_name: str, repository: str, label: str, database: str
|
|
262
|
+
db, group_name: str, repository: str, label: str, database: str, ops_database: str = "ops"
|
|
222
263
|
) -> str:
|
|
223
264
|
"""Build BACKUP command for an inventory group.
|
|
224
265
|
|
|
@@ -226,7 +267,7 @@ def build_full_backup_command(
|
|
|
226
267
|
simple BACKUP DATABASE command. Otherwise, generate ON (TABLE ...) list for
|
|
227
268
|
the specific tables within the database.
|
|
228
269
|
"""
|
|
229
|
-
tables = find_tables_by_group(db, group_name)
|
|
270
|
+
tables = find_tables_by_group(db, group_name, ops_database)
|
|
230
271
|
|
|
231
272
|
db_entries = [t for t in tables if t["database"] == database]
|
|
232
273
|
if not db_entries:
|
|
@@ -245,8 +286,10 @@ def build_full_backup_command(
|
|
|
245
286
|
ON ({on_clause})"""
|
|
246
287
|
|
|
247
288
|
|
|
248
|
-
def record_backup_partitions(
|
|
249
|
-
|
|
289
|
+
def record_backup_partitions(
|
|
290
|
+
db, label: str, partitions: list[dict[str, str]], ops_database: str = "ops"
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Record partition metadata for a backup in the backup_partitions table.
|
|
250
293
|
|
|
251
294
|
Args:
|
|
252
295
|
db: Database connection
|
|
@@ -263,7 +306,7 @@ def record_backup_partitions(db, label: str, partitions: list[dict[str, str]]) -
|
|
|
263
306
|
key_hash = hashlib.md5(composite_key.encode("utf-8")).hexdigest()
|
|
264
307
|
|
|
265
308
|
db.execute(f"""
|
|
266
|
-
INSERT INTO
|
|
309
|
+
INSERT INTO {ops_database}.backup_partitions
|
|
267
310
|
(key_hash, label, database_name, table_name, partition_name)
|
|
268
311
|
VALUES ({utils.quote_value(key_hash)}, {utils.quote_value(label)}, {utils.quote_value(partition["database"])}, {utils.quote_value(partition["table"])}, {utils.quote_value(partition["partition_name"])})
|
|
269
312
|
""")
|
starrocks_br/repository.py
CHANGED
|
@@ -30,7 +30,7 @@ def ensure_repository(db, name: str) -> None:
|
|
|
30
30
|
raise RuntimeError(
|
|
31
31
|
f"Repository '{name}' not found. Please create it first using:\n"
|
|
32
32
|
f" CREATE REPOSITORY {name} WITH BROKER ON LOCATION '...' PROPERTIES(...)\n"
|
|
33
|
-
f"For examples, see: https://docs.starrocks.io/docs/sql-reference/sql-statements/
|
|
33
|
+
f"For examples, see: https://docs.starrocks.io/docs/sql-reference/sql-statements/backup_restore/CREATE_REPOSITORY/"
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
# SHOW REPOSITORIES returns: RepoId, RepoName, CreateTime, IsReadOnly, Location, Broker, ErrMsg
|