starrocks-br 0.5.1__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/__init__.py +14 -0
- starrocks_br/cli.py +117 -36
- starrocks_br/concurrency.py +47 -23
- starrocks_br/config.py +89 -0
- starrocks_br/db.py +14 -0
- starrocks_br/error_handler.py +73 -12
- starrocks_br/exceptions.py +29 -0
- starrocks_br/executor.py +29 -6
- starrocks_br/health.py +15 -0
- starrocks_br/history.py +23 -9
- starrocks_br/labels.py +19 -3
- starrocks_br/logger.py +14 -0
- starrocks_br/planner.py +70 -13
- starrocks_br/repository.py +15 -1
- starrocks_br/restore.py +211 -40
- starrocks_br/schema.py +103 -43
- starrocks_br/timezone.py +14 -0
- starrocks_br/utils.py +15 -0
- {starrocks_br-0.5.1.dist-info → starrocks_br-0.6.0.dist-info}/METADATA +34 -19
- starrocks_br-0.6.0.dist-info/RECORD +24 -0
- {starrocks_br-0.5.1.dist-info → starrocks_br-0.6.0.dist-info}/WHEEL +1 -1
- starrocks_br-0.6.0.dist-info/licenses/LICENSE +201 -0
- starrocks_br-0.5.1.dist-info/RECORD +0 -23
- {starrocks_br-0.5.1.dist-info → starrocks_br-0.6.0.dist-info}/entry_points.txt +0 -0
- {starrocks_br-0.5.1.dist-info → starrocks_br-0.6.0.dist-info}/top_level.txt +0 -0
starrocks_br/repository.py
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 deep-bi
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
1
15
|
from __future__ import annotations
|
|
2
16
|
|
|
3
17
|
|
|
@@ -16,7 +30,7 @@ def ensure_repository(db, name: str) -> None:
|
|
|
16
30
|
raise RuntimeError(
|
|
17
31
|
f"Repository '{name}' not found. Please create it first using:\n"
|
|
18
32
|
f" CREATE REPOSITORY {name} WITH BROKER ON LOCATION '...' PROPERTIES(...)\n"
|
|
19
|
-
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/"
|
|
20
34
|
)
|
|
21
35
|
|
|
22
36
|
# SHOW REPOSITORIES returns: RepoId, RepoName, CreateTime, IsReadOnly, Location, Broker, ErrMsg
|
starrocks_br/restore.py
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 deep-bi
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
1
15
|
import datetime
|
|
2
16
|
import time
|
|
3
17
|
|
|
@@ -187,9 +201,22 @@ def execute_restore(
|
|
|
187
201
|
max_polls: int = MAX_POLLS,
|
|
188
202
|
poll_interval: float = 1.0,
|
|
189
203
|
scope: str = "restore",
|
|
204
|
+
ops_database: str = "ops",
|
|
190
205
|
) -> dict:
|
|
191
206
|
"""Execute a complete restore workflow: submit command and monitor progress.
|
|
192
207
|
|
|
208
|
+
Args:
|
|
209
|
+
db: Database connection
|
|
210
|
+
restore_command: Restore SQL command to execute
|
|
211
|
+
backup_label: Label of the backup being restored
|
|
212
|
+
restore_type: Type of restore operation
|
|
213
|
+
repository: Repository name
|
|
214
|
+
database: Database name
|
|
215
|
+
max_polls: Maximum polling attempts
|
|
216
|
+
poll_interval: Seconds between polls
|
|
217
|
+
scope: Job scope (for concurrency control)
|
|
218
|
+
ops_database: Name of ops database (default: "ops")
|
|
219
|
+
|
|
193
220
|
Returns dictionary with keys: success, final_status, error_message
|
|
194
221
|
"""
|
|
195
222
|
cluster_tz = db.timezone
|
|
@@ -226,13 +253,18 @@ def execute_restore(
|
|
|
226
253
|
"finished_at": finished_at,
|
|
227
254
|
"error_message": None if success else final_status["state"],
|
|
228
255
|
},
|
|
256
|
+
ops_database=ops_database,
|
|
229
257
|
)
|
|
230
258
|
except Exception as e:
|
|
231
259
|
logger.error(f"Failed to log restore history: {str(e)}")
|
|
232
260
|
|
|
233
261
|
try:
|
|
234
262
|
concurrency.complete_job_slot(
|
|
235
|
-
db,
|
|
263
|
+
db,
|
|
264
|
+
scope=scope,
|
|
265
|
+
label=label,
|
|
266
|
+
final_state=final_status["state"],
|
|
267
|
+
ops_database=ops_database,
|
|
236
268
|
)
|
|
237
269
|
except Exception as e:
|
|
238
270
|
logger.error(f"Failed to complete job slot: {str(e)}")
|
|
@@ -250,7 +282,7 @@ def execute_restore(
|
|
|
250
282
|
return {"success": False, "final_status": None, "error_message": str(e)}
|
|
251
283
|
|
|
252
284
|
|
|
253
|
-
def find_restore_pair(db, target_label: str) -> list[str]:
|
|
285
|
+
def find_restore_pair(db, target_label: str, ops_database: str = "ops") -> list[str]:
|
|
254
286
|
"""Find the correct sequence of backups needed for restore.
|
|
255
287
|
|
|
256
288
|
Args:
|
|
@@ -266,7 +298,7 @@ def find_restore_pair(db, target_label: str) -> list[str]:
|
|
|
266
298
|
"""
|
|
267
299
|
query = f"""
|
|
268
300
|
SELECT label, backup_type, finished_at
|
|
269
|
-
FROM
|
|
301
|
+
FROM {ops_database}.backup_history
|
|
270
302
|
WHERE label = {utils.quote_value(target_label)}
|
|
271
303
|
AND status = 'FINISHED'
|
|
272
304
|
"""
|
|
@@ -285,7 +317,7 @@ def find_restore_pair(db, target_label: str) -> list[str]:
|
|
|
285
317
|
|
|
286
318
|
full_backup_query = f"""
|
|
287
319
|
SELECT label, backup_type, finished_at
|
|
288
|
-
FROM
|
|
320
|
+
FROM {ops_database}.backup_history
|
|
289
321
|
WHERE backup_type = 'full'
|
|
290
322
|
AND status = 'FINISHED'
|
|
291
323
|
AND label LIKE {utils.quote_value(f"{database_name}_%")}
|
|
@@ -312,6 +344,7 @@ def get_tables_from_backup(
|
|
|
312
344
|
group: str | None = None,
|
|
313
345
|
table: str | None = None,
|
|
314
346
|
database: str | None = None,
|
|
347
|
+
ops_database: str = "ops",
|
|
315
348
|
) -> list[str]:
|
|
316
349
|
"""Get list of tables to restore from backup manifest.
|
|
317
350
|
|
|
@@ -340,7 +373,7 @@ def get_tables_from_backup(
|
|
|
340
373
|
|
|
341
374
|
query = f"""
|
|
342
375
|
SELECT DISTINCT database_name, table_name
|
|
343
|
-
FROM
|
|
376
|
+
FROM {ops_database}.backup_partitions
|
|
344
377
|
WHERE label = {utils.quote_value(label)}
|
|
345
378
|
ORDER BY database_name, table_name
|
|
346
379
|
"""
|
|
@@ -363,7 +396,7 @@ def get_tables_from_backup(
|
|
|
363
396
|
if group:
|
|
364
397
|
group_query = f"""
|
|
365
398
|
SELECT database_name, table_name
|
|
366
|
-
FROM
|
|
399
|
+
FROM {ops_database}.table_inventory
|
|
367
400
|
WHERE inventory_group = {utils.quote_value(group)}
|
|
368
401
|
"""
|
|
369
402
|
|
|
@@ -390,6 +423,35 @@ def get_tables_from_backup(
|
|
|
390
423
|
return tables
|
|
391
424
|
|
|
392
425
|
|
|
426
|
+
def get_partitions_from_backup(
|
|
427
|
+
db, label: str, table: str, ops_database: str = "ops"
|
|
428
|
+
) -> list[str]:
|
|
429
|
+
"""Get list of partitions for a specific table from backup manifest.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
db: Database connection
|
|
433
|
+
label: Backup label
|
|
434
|
+
table: Table name in format 'database.table'
|
|
435
|
+
ops_database: Operations database name
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
List of partition names for the table in this backup
|
|
439
|
+
"""
|
|
440
|
+
database_name, table_name = table.split(".", 1)
|
|
441
|
+
|
|
442
|
+
query = f"""
|
|
443
|
+
SELECT partition_name
|
|
444
|
+
FROM {ops_database}.backup_partitions
|
|
445
|
+
WHERE label = {utils.quote_value(label)}
|
|
446
|
+
AND database_name = {utils.quote_value(database_name)}
|
|
447
|
+
AND table_name = {utils.quote_value(table_name)}
|
|
448
|
+
ORDER BY partition_name
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
rows = db.query(query)
|
|
452
|
+
return [row[0] for row in rows]
|
|
453
|
+
|
|
454
|
+
|
|
393
455
|
def execute_restore_flow(
|
|
394
456
|
db,
|
|
395
457
|
repo_name: str,
|
|
@@ -397,6 +459,7 @@ def execute_restore_flow(
|
|
|
397
459
|
tables_to_restore: list[str],
|
|
398
460
|
rename_suffix: str = "_restored",
|
|
399
461
|
skip_confirmation: bool = False,
|
|
462
|
+
ops_database: str = "ops",
|
|
400
463
|
) -> dict:
|
|
401
464
|
"""Execute the complete restore flow with safety measures.
|
|
402
465
|
|
|
@@ -407,6 +470,7 @@ def execute_restore_flow(
|
|
|
407
470
|
tables_to_restore: List of tables to restore (format: database.table)
|
|
408
471
|
rename_suffix: Suffix for temporary tables
|
|
409
472
|
skip_confirmation: If True, skip interactive confirmation prompt
|
|
473
|
+
ops_database: Name of ops database (default: "ops")
|
|
410
474
|
|
|
411
475
|
Returns:
|
|
412
476
|
Dictionary with success status and details
|
|
@@ -438,59 +502,122 @@ def execute_restore_flow(
|
|
|
438
502
|
database_name = tables_to_restore[0].split(".")[0]
|
|
439
503
|
|
|
440
504
|
base_label = restore_pair[0]
|
|
441
|
-
logger.info("")
|
|
442
|
-
logger.info(f"Step 1: Restoring base backup '{base_label}'...")
|
|
443
|
-
|
|
444
|
-
base_timestamp = get_snapshot_timestamp(db, repo_name, base_label)
|
|
445
|
-
|
|
446
|
-
base_restore_command = _build_restore_command_with_rename(
|
|
447
|
-
base_label, repo_name, tables_to_restore, rename_suffix, database_name, base_timestamp
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
base_result = execute_restore(
|
|
451
|
-
db, base_restore_command, base_label, "full", repo_name, database_name, scope="restore"
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
if not base_result["success"]:
|
|
455
|
-
return {
|
|
456
|
-
"success": False,
|
|
457
|
-
"error_message": f"Base restore failed: {base_result['error_message']}",
|
|
458
|
-
}
|
|
459
505
|
|
|
460
|
-
|
|
506
|
+
tables_in_base = get_tables_from_backup(db, base_label, ops_database=ops_database)
|
|
507
|
+
tables_to_restore_from_base = [t for t in tables_to_restore if t in tables_in_base]
|
|
461
508
|
|
|
462
|
-
if
|
|
463
|
-
incremental_label = restore_pair[1]
|
|
509
|
+
if tables_to_restore_from_base:
|
|
464
510
|
logger.info("")
|
|
465
|
-
logger.info(f"Step
|
|
511
|
+
logger.info(f"Step 1: Restoring base backup '{base_label}'...")
|
|
466
512
|
|
|
467
|
-
|
|
513
|
+
base_timestamp = get_snapshot_timestamp(db, repo_name, base_label)
|
|
468
514
|
|
|
469
|
-
|
|
470
|
-
|
|
515
|
+
base_restore_command = _build_restore_command_with_rename(
|
|
516
|
+
base_label,
|
|
471
517
|
repo_name,
|
|
472
|
-
|
|
518
|
+
tables_to_restore_from_base,
|
|
519
|
+
rename_suffix,
|
|
473
520
|
database_name,
|
|
474
|
-
|
|
521
|
+
base_timestamp,
|
|
475
522
|
)
|
|
476
523
|
|
|
477
|
-
|
|
524
|
+
base_result = execute_restore(
|
|
478
525
|
db,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"
|
|
526
|
+
base_restore_command,
|
|
527
|
+
base_label,
|
|
528
|
+
"full",
|
|
482
529
|
repo_name,
|
|
483
530
|
database_name,
|
|
484
531
|
scope="restore",
|
|
532
|
+
ops_database=ops_database,
|
|
485
533
|
)
|
|
486
534
|
|
|
487
|
-
if not
|
|
535
|
+
if not base_result["success"]:
|
|
488
536
|
return {
|
|
489
537
|
"success": False,
|
|
490
|
-
"error_message": f"
|
|
538
|
+
"error_message": f"Base restore failed: {base_result['error_message']}",
|
|
491
539
|
}
|
|
492
540
|
|
|
493
|
-
logger.success("
|
|
541
|
+
logger.success("Base restore completed successfully")
|
|
542
|
+
else:
|
|
543
|
+
logger.info("")
|
|
544
|
+
logger.info(
|
|
545
|
+
f"Step 1: Skipping base backup '{base_label}' (no requested tables in this backup)"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if len(restore_pair) > 1:
|
|
549
|
+
incremental_label = restore_pair[1]
|
|
550
|
+
|
|
551
|
+
tables_in_incremental = get_tables_from_backup(
|
|
552
|
+
db, incremental_label, ops_database=ops_database
|
|
553
|
+
)
|
|
554
|
+
tables_to_restore_from_incremental = [
|
|
555
|
+
t for t in tables_to_restore if t in tables_in_incremental
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
if not tables_to_restore_from_incremental:
|
|
559
|
+
logger.info("")
|
|
560
|
+
logger.info(
|
|
561
|
+
f"Step 2: Skipping incremental backup '{incremental_label}' (no requested tables in this backup)"
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
logger.info("")
|
|
565
|
+
logger.info(f"Step 2: Applying incremental backup '{incremental_label}'...")
|
|
566
|
+
|
|
567
|
+
incremental_timestamp = get_snapshot_timestamp(db, repo_name, incremental_label)
|
|
568
|
+
|
|
569
|
+
for table in tables_to_restore_from_incremental:
|
|
570
|
+
partitions = get_partitions_from_backup(
|
|
571
|
+
db, incremental_label, table, ops_database=ops_database
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
if not partitions:
|
|
575
|
+
logger.warning(f"No partitions found for {table} in {incremental_label}, skipping")
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
table_was_in_base = table in tables_to_restore_from_base
|
|
579
|
+
|
|
580
|
+
if table_was_in_base:
|
|
581
|
+
_, table_name = table.split(".", 1)
|
|
582
|
+
target_table_name = f"{table_name}{rename_suffix}"
|
|
583
|
+
incremental_restore_command = _build_partition_restore_command(
|
|
584
|
+
incremental_label,
|
|
585
|
+
repo_name,
|
|
586
|
+
f"{database_name}.{target_table_name}",
|
|
587
|
+
partitions,
|
|
588
|
+
database_name,
|
|
589
|
+
incremental_timestamp,
|
|
590
|
+
rename_suffix=None,
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
incremental_restore_command = _build_partition_restore_command(
|
|
594
|
+
incremental_label,
|
|
595
|
+
repo_name,
|
|
596
|
+
table,
|
|
597
|
+
partitions,
|
|
598
|
+
database_name,
|
|
599
|
+
incremental_timestamp,
|
|
600
|
+
rename_suffix=rename_suffix,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
incremental_result = execute_restore(
|
|
604
|
+
db,
|
|
605
|
+
incremental_restore_command,
|
|
606
|
+
incremental_label,
|
|
607
|
+
"incremental",
|
|
608
|
+
repo_name,
|
|
609
|
+
database_name,
|
|
610
|
+
scope="restore",
|
|
611
|
+
ops_database=ops_database,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if not incremental_result["success"]:
|
|
615
|
+
return {
|
|
616
|
+
"success": False,
|
|
617
|
+
"error_message": f"Incremental restore failed for {table}: {incremental_result['error_message']}",
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
logger.success("Incremental restore completed successfully")
|
|
494
621
|
|
|
495
622
|
logger.info("")
|
|
496
623
|
logger.info("Step 3: Performing atomic rename...")
|
|
@@ -557,6 +684,50 @@ def _build_restore_command_without_rename(
|
|
|
557
684
|
PROPERTIES ("backup_timestamp" = "{backup_timestamp}")"""
|
|
558
685
|
|
|
559
686
|
|
|
687
|
+
def _build_partition_restore_command(
|
|
688
|
+
backup_label: str,
|
|
689
|
+
repo_name: str,
|
|
690
|
+
table: str,
|
|
691
|
+
partitions: list[str],
|
|
692
|
+
database: str,
|
|
693
|
+
backup_timestamp: str,
|
|
694
|
+
rename_suffix: str | None = None,
|
|
695
|
+
) -> str:
|
|
696
|
+
"""Build partition-level restore command with optional AS clause.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
backup_label: Backup snapshot label
|
|
700
|
+
repo_name: Repository name
|
|
701
|
+
table: Table name in format 'database.table'
|
|
702
|
+
partitions: List of partition names to restore
|
|
703
|
+
database: Database name
|
|
704
|
+
backup_timestamp: Backup timestamp
|
|
705
|
+
rename_suffix: Optional suffix for AS clause (e.g., '_restored')
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
SQL RESTORE command string
|
|
709
|
+
"""
|
|
710
|
+
_, table_name = table.split(".", 1)
|
|
711
|
+
|
|
712
|
+
# Build partition list
|
|
713
|
+
partition_list = ", ".join([utils.quote_identifier(p) for p in partitions])
|
|
714
|
+
|
|
715
|
+
# Build table clause
|
|
716
|
+
if rename_suffix:
|
|
717
|
+
# Table only in incremental: use AS clause
|
|
718
|
+
temp_table_name = f"{table_name}{rename_suffix}"
|
|
719
|
+
table_clause = f"TABLE {utils.quote_identifier(table_name)} PARTITION ({partition_list}) AS {utils.quote_identifier(temp_table_name)}"
|
|
720
|
+
else:
|
|
721
|
+
# Table in base: target the _restored table directly (no AS)
|
|
722
|
+
table_clause = f"TABLE {utils.quote_identifier(table_name)} PARTITION ({partition_list})"
|
|
723
|
+
|
|
724
|
+
return f"""RESTORE SNAPSHOT {utils.quote_identifier(backup_label)}
|
|
725
|
+
FROM {utils.quote_identifier(repo_name)}
|
|
726
|
+
DATABASE {utils.quote_identifier(database)}
|
|
727
|
+
ON ({table_clause})
|
|
728
|
+
PROPERTIES ("backup_timestamp" = "{backup_timestamp}")"""
|
|
729
|
+
|
|
730
|
+
|
|
560
731
|
def _generate_timestamped_backup_name(table_name: str) -> str:
|
|
561
732
|
"""Generate a timestamped backup table name.
|
|
562
733
|
|
starrocks_br/schema.py
CHANGED
|
@@ -1,71 +1,131 @@
|
|
|
1
|
+
# Copyright 2025 deep-bi
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
1
15
|
from . import logger
|
|
2
16
|
|
|
3
17
|
|
|
4
|
-
def initialize_ops_schema(
|
|
18
|
+
def initialize_ops_schema(
|
|
19
|
+
db, ops_database: str = "ops", table_inventory_entries: list[tuple[str, str, str]] | None = None
|
|
20
|
+
) -> None:
|
|
5
21
|
"""Initialize the ops database and all required control tables.
|
|
6
22
|
|
|
7
|
-
Creates empty ops tables.
|
|
8
|
-
|
|
23
|
+
Creates empty ops tables. Optionally populates table_inventory from entries.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
db: Database connection
|
|
27
|
+
ops_database: Name of the ops database (defaults to "ops")
|
|
28
|
+
table_inventory_entries: Optional list of (group, database, table) tuples to bootstrap
|
|
9
29
|
"""
|
|
10
30
|
|
|
11
|
-
logger.info("Creating
|
|
12
|
-
db.execute("CREATE DATABASE IF NOT EXISTS
|
|
13
|
-
logger.success("
|
|
31
|
+
logger.info(f"Creating {ops_database} database...")
|
|
32
|
+
db.execute(f"CREATE DATABASE IF NOT EXISTS {ops_database}")
|
|
33
|
+
logger.success(f"{ops_database} database created")
|
|
34
|
+
|
|
35
|
+
logger.info(f"Creating {ops_database}.table_inventory...")
|
|
36
|
+
db.execute(get_table_inventory_schema(ops_database=ops_database))
|
|
37
|
+
logger.success(f"{ops_database}.table_inventory created")
|
|
14
38
|
|
|
15
|
-
logger.info("Creating
|
|
16
|
-
db.execute(
|
|
17
|
-
logger.success("
|
|
39
|
+
logger.info(f"Creating {ops_database}.backup_history...")
|
|
40
|
+
db.execute(get_backup_history_schema(ops_database=ops_database))
|
|
41
|
+
logger.success(f"{ops_database}.backup_history created")
|
|
18
42
|
|
|
19
|
-
logger.info("Creating
|
|
20
|
-
db.execute(
|
|
21
|
-
logger.success("
|
|
43
|
+
logger.info(f"Creating {ops_database}.restore_history...")
|
|
44
|
+
db.execute(get_restore_history_schema(ops_database=ops_database))
|
|
45
|
+
logger.success(f"{ops_database}.restore_history created")
|
|
22
46
|
|
|
23
|
-
logger.info("Creating
|
|
24
|
-
db.execute(
|
|
25
|
-
logger.success("
|
|
47
|
+
logger.info(f"Creating {ops_database}.run_status...")
|
|
48
|
+
db.execute(get_run_status_schema(ops_database=ops_database))
|
|
49
|
+
logger.success(f"{ops_database}.run_status created")
|
|
26
50
|
|
|
27
|
-
logger.info("Creating
|
|
28
|
-
db.execute(
|
|
29
|
-
logger.success("
|
|
51
|
+
logger.info(f"Creating {ops_database}.backup_partitions...")
|
|
52
|
+
db.execute(get_backup_partitions_schema(ops_database=ops_database))
|
|
53
|
+
logger.success(f"{ops_database}.backup_partitions created")
|
|
54
|
+
|
|
55
|
+
if table_inventory_entries:
|
|
56
|
+
logger.info(f"Bootstrapping {ops_database}.table_inventory from configuration...")
|
|
57
|
+
bootstrap_table_inventory(db, table_inventory_entries, ops_database=ops_database)
|
|
58
|
+
logger.success(
|
|
59
|
+
f"{ops_database}.table_inventory bootstrapped with {len(table_inventory_entries)} entries"
|
|
60
|
+
)
|
|
30
61
|
|
|
31
|
-
logger.info("Creating ops.backup_partitions...")
|
|
32
|
-
db.execute(get_backup_partitions_schema())
|
|
33
|
-
logger.success("ops.backup_partitions created")
|
|
34
62
|
logger.info("")
|
|
35
63
|
logger.success("Schema initialized successfully!")
|
|
36
64
|
|
|
37
65
|
|
|
38
|
-
def ensure_ops_schema(db) -> bool:
|
|
66
|
+
def ensure_ops_schema(db, ops_database: str = "ops") -> bool:
|
|
39
67
|
"""Ensure ops schema exists, creating it if necessary.
|
|
40
68
|
|
|
41
69
|
Returns True if schema was created, False if it already existed.
|
|
42
70
|
This is called automatically before backup/restore operations.
|
|
43
71
|
"""
|
|
44
72
|
try:
|
|
45
|
-
result = db.query("SHOW DATABASES LIKE '
|
|
73
|
+
result = db.query(f"SHOW DATABASES LIKE '{ops_database}'")
|
|
46
74
|
|
|
47
75
|
if not result:
|
|
48
|
-
initialize_ops_schema(db)
|
|
76
|
+
initialize_ops_schema(db, ops_database=ops_database)
|
|
49
77
|
return True
|
|
50
78
|
|
|
51
|
-
db.execute("USE
|
|
79
|
+
db.execute(f"USE {ops_database}")
|
|
52
80
|
tables_result = db.query("SHOW TABLES")
|
|
53
81
|
|
|
54
82
|
if not tables_result or len(tables_result) < 5:
|
|
55
|
-
initialize_ops_schema(db)
|
|
83
|
+
initialize_ops_schema(db, ops_database=ops_database)
|
|
56
84
|
return True
|
|
57
85
|
|
|
58
86
|
return False
|
|
59
87
|
|
|
60
88
|
except Exception:
|
|
61
|
-
initialize_ops_schema(db)
|
|
89
|
+
initialize_ops_schema(db, ops_database=ops_database)
|
|
62
90
|
return True
|
|
63
91
|
|
|
64
92
|
|
|
65
|
-
def
|
|
93
|
+
def bootstrap_table_inventory(
|
|
94
|
+
db, entries: list[tuple[str, str, str]], ops_database: str = "ops"
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Bootstrap table_inventory table with entries from configuration.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
db: Database connection
|
|
100
|
+
entries: List of (group, database, table) tuples
|
|
101
|
+
ops_database: Name of the ops database (defaults to "ops")
|
|
102
|
+
"""
|
|
103
|
+
if not entries:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
unique_databases = {database for _, database, _ in entries}
|
|
107
|
+
|
|
108
|
+
for database_name in unique_databases:
|
|
109
|
+
result = db.query(f"SHOW DATABASES LIKE '{database_name}'")
|
|
110
|
+
if not result:
|
|
111
|
+
logger.warning(
|
|
112
|
+
f"Database '{database_name}' does not exist. "
|
|
113
|
+
f"Table inventory entries will be created, but backups will fail until the database is created."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
for group, database, table in entries:
|
|
117
|
+
sql = f"""
|
|
118
|
+
INSERT INTO {ops_database}.table_inventory
|
|
119
|
+
(inventory_group, database_name, table_name)
|
|
120
|
+
VALUES ('{group}', '{database}', '{table}')
|
|
121
|
+
"""
|
|
122
|
+
db.execute(sql)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_table_inventory_schema(ops_database: str = "ops") -> str:
|
|
66
126
|
"""Get CREATE TABLE statement for table_inventory."""
|
|
67
|
-
return """
|
|
68
|
-
CREATE TABLE IF NOT EXISTS
|
|
127
|
+
return f"""
|
|
128
|
+
CREATE TABLE IF NOT EXISTS {ops_database}.table_inventory (
|
|
69
129
|
inventory_group STRING NOT NULL COMMENT "Group name for a set of tables",
|
|
70
130
|
database_name STRING NOT NULL COMMENT "Database name",
|
|
71
131
|
table_name STRING NOT NULL COMMENT "Table name, or '*' for all tables in database",
|
|
@@ -78,10 +138,10 @@ def get_table_inventory_schema() -> str:
|
|
|
78
138
|
"""
|
|
79
139
|
|
|
80
140
|
|
|
81
|
-
def get_backup_history_schema() -> str:
|
|
141
|
+
def get_backup_history_schema(ops_database: str = "ops") -> str:
|
|
82
142
|
"""Get CREATE TABLE statement for backup_history."""
|
|
83
|
-
return """
|
|
84
|
-
CREATE TABLE IF NOT EXISTS
|
|
143
|
+
return f"""
|
|
144
|
+
CREATE TABLE IF NOT EXISTS {ops_database}.backup_history (
|
|
85
145
|
label STRING NOT NULL COMMENT "Unique backup snapshot label",
|
|
86
146
|
backup_type STRING NOT NULL COMMENT "Type of backup: full or incremental",
|
|
87
147
|
status STRING NOT NULL COMMENT "Final backup status: FINISHED, FAILED, CANCELLED, TIMEOUT",
|
|
@@ -96,10 +156,10 @@ def get_backup_history_schema() -> str:
|
|
|
96
156
|
"""
|
|
97
157
|
|
|
98
158
|
|
|
99
|
-
def get_restore_history_schema() -> str:
|
|
159
|
+
def get_restore_history_schema(ops_database: str = "ops") -> str:
|
|
100
160
|
"""Get CREATE TABLE statement for restore_history."""
|
|
101
|
-
return """
|
|
102
|
-
CREATE TABLE IF NOT EXISTS
|
|
161
|
+
return f"""
|
|
162
|
+
CREATE TABLE IF NOT EXISTS {ops_database}.restore_history (
|
|
103
163
|
job_id STRING NOT NULL COMMENT "Unique restore job identifier",
|
|
104
164
|
backup_label STRING NOT NULL COMMENT "Source backup snapshot label",
|
|
105
165
|
restore_type STRING NOT NULL COMMENT "Type of restore: partition, table, or database",
|
|
@@ -116,10 +176,10 @@ def get_restore_history_schema() -> str:
|
|
|
116
176
|
"""
|
|
117
177
|
|
|
118
178
|
|
|
119
|
-
def get_run_status_schema() -> str:
|
|
179
|
+
def get_run_status_schema(ops_database: str = "ops") -> str:
|
|
120
180
|
"""Get CREATE TABLE statement for run_status."""
|
|
121
|
-
return """
|
|
122
|
-
CREATE TABLE IF NOT EXISTS
|
|
181
|
+
return f"""
|
|
182
|
+
CREATE TABLE IF NOT EXISTS {ops_database}.run_status (
|
|
123
183
|
scope STRING NOT NULL COMMENT "Job scope: backup or restore",
|
|
124
184
|
label STRING NOT NULL COMMENT "Job label or identifier",
|
|
125
185
|
state STRING NOT NULL DEFAULT "ACTIVE" COMMENT "Job state: ACTIVE, FINISHED, FAILED, or CANCELLED",
|
|
@@ -131,12 +191,12 @@ def get_run_status_schema() -> str:
|
|
|
131
191
|
"""
|
|
132
192
|
|
|
133
193
|
|
|
134
|
-
def get_backup_partitions_schema() -> str:
|
|
194
|
+
def get_backup_partitions_schema(ops_database: str = "ops") -> str:
|
|
135
195
|
"""Get CREATE TABLE statement for backup_partitions."""
|
|
136
|
-
return """
|
|
137
|
-
CREATE TABLE IF NOT EXISTS
|
|
196
|
+
return f"""
|
|
197
|
+
CREATE TABLE IF NOT EXISTS {ops_database}.backup_partitions (
|
|
138
198
|
key_hash STRING NOT NULL COMMENT "MD5 hash of composite key (label, database_name, table_name, partition_name)",
|
|
139
|
-
label STRING NOT NULL COMMENT "The backup label this partition belongs to. FK to
|
|
199
|
+
label STRING NOT NULL COMMENT "The backup label this partition belongs to. FK to {ops_database}.backup_history.label.",
|
|
140
200
|
database_name STRING NOT NULL COMMENT "The name of the database the partition belongs to.",
|
|
141
201
|
table_name STRING NOT NULL COMMENT "The name of the table the partition belongs to.",
|
|
142
202
|
partition_name STRING NOT NULL COMMENT "The name of the specific partition.",
|
starrocks_br/timezone.py
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 deep-bi
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
1
15
|
import datetime
|
|
2
16
|
from zoneinfo import ZoneInfo
|
|
3
17
|
|
starrocks_br/utils.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2025 deep-bi
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
def quote_identifier(identifier):
|
|
2
17
|
"""
|
|
3
18
|
Quote a SQL identifier (database, table, or column name) with backticks.
|