pgbelt 0.6.2__tar.gz → 0.7.1__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 (27) hide show
  1. {pgbelt-0.6.2 → pgbelt-0.7.1}/PKG-INFO +5 -1
  2. {pgbelt-0.6.2 → pgbelt-0.7.1}/README.md +4 -0
  3. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/convenience.py +5 -7
  4. pgbelt-0.7.1/pgbelt/cmd/preflight.py +631 -0
  5. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/setup.py +26 -7
  6. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/status.py +36 -0
  7. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/sync.py +40 -15
  8. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/teardown.py +2 -2
  9. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/config/models.py +5 -1
  10. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/dump.py +9 -5
  11. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/pglogical.py +27 -14
  12. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/postgres.py +177 -43
  13. {pgbelt-0.6.2 → pgbelt-0.7.1}/pyproject.toml +7 -7
  14. pgbelt-0.6.2/pgbelt/cmd/preflight.py +0 -238
  15. {pgbelt-0.6.2 → pgbelt-0.7.1}/LICENSE +0 -0
  16. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/__init__.py +0 -0
  17. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/__init__.py +0 -0
  18. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/helpers.py +0 -0
  19. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/login.py +0 -0
  20. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/cmd/schema.py +0 -0
  21. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/config/__init__.py +0 -0
  22. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/config/config.py +0 -0
  23. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/config/remote.py +0 -0
  24. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/main.py +0 -0
  25. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/__init__.py +0 -0
  26. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/asyncfuncs.py +0 -0
  27. {pgbelt-0.6.2 → pgbelt-0.7.1}/pgbelt/util/logs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pgbelt
3
- Version: 0.6.2
3
+ Version: 0.7.1
4
4
  Summary: A CLI tool used to manage Postgres data migrations from beginning to end, for a single database or a fleet, leveraging pglogical replication.
5
5
  Author: Varjitt Jeeva
6
6
  Author-email: varjitt.jeeva@autodesk.com
@@ -65,6 +65,10 @@ Install pgbelt locally:
65
65
 
66
66
  See [this doc](docs/quickstart.md)!
67
67
 
68
+ ## Playbook
69
+
70
+ This playbook gets updated actively. If you have any issues, solutions could be found in [this playbook](docs/playbook.md).
71
+
68
72
  ## Contributing
69
73
 
70
74
  We welcome contributions! See [this doc](CONTRIBUTING.md) on how to do so, including setting up your local development environment.
@@ -46,6 +46,10 @@ Install pgbelt locally:
46
46
 
47
47
  See [this doc](docs/quickstart.md)!
48
48
 
49
+ ## Playbook
50
+
51
+ This playbook gets updated actively. If you have any issues, solutions could be found in [this playbook](docs/playbook.md).
52
+
49
53
  ## Contributing
50
54
 
51
55
  We welcome contributions! See [this doc](CONTRIBUTING.md) on how to do so, including setting up your local development environment.
@@ -34,9 +34,7 @@ def src_dsn(
34
34
  echo(
35
35
  conf.src.owner_dsn
36
36
  if owner
37
- else conf.src.pglogical_dsn
38
- if pglogical
39
- else conf.src.root_dsn
37
+ else conf.src.pglogical_dsn if pglogical else conf.src.root_dsn
40
38
  )
41
39
 
42
40
 
@@ -56,9 +54,7 @@ def dst_dsn(
56
54
  echo(
57
55
  conf.dst.owner_dsn
58
56
  if owner
59
- else conf.dst.pglogical_dsn
60
- if pglogical
61
- else conf.dst.root_dsn
57
+ else conf.dst.pglogical_dsn if pglogical else conf.dst.root_dsn
62
58
  )
63
59
 
64
60
 
@@ -66,7 +62,9 @@ async def _check_pkeys(
66
62
  conf: DbupgradeConfig, logger: Logger
67
63
  ) -> tuple[list[str], list[str]]:
68
64
  async with create_pool(conf.src.root_uri, min_size=1) as pool:
69
- pkey_tables, no_pkey_tables, _ = await analyze_table_pkeys(pool, logger)
65
+ pkey_tables, no_pkey_tables, _ = await analyze_table_pkeys(
66
+ pool, conf.schema_name, logger
67
+ )
70
68
  return pkey_tables, no_pkey_tables
71
69
 
72
70
 
@@ -0,0 +1,631 @@
1
+ from asyncio import gather
2
+ from collections.abc import Awaitable
3
+
4
+ from asyncpg import create_pool
5
+ from pgbelt.cmd.helpers import run_with_configs
6
+ from pgbelt.config.models import DbupgradeConfig
7
+ from pgbelt.util.logs import get_logger
8
+ from pgbelt.util.postgres import analyze_table_pkeys
9
+ from pgbelt.util.postgres import precheck_info
10
+ from tabulate import tabulate
11
+ from typer import echo
12
+ from typer import style
13
+
14
+
15
+ def _summary_table(results: dict, compared_extensions: list[str] = None) -> list[list]:
16
+ """
17
+ Takes a dict of precheck results for all databases and returns a summary table for echo.
18
+
19
+ The summary table alters slightly if the results are for a destination database.
20
+
21
+ results format:
22
+ [
23
+ {
24
+ "server_version": "9.6.20",
25
+ "max_replication_slots": "10",
26
+ "max_worker_processes": "10",
27
+ "max_wal_senders": "10",
28
+ "shared_preload_libraries": ["pg_stat_statements", ...],
29
+ "rds.logical_replication": "on",
30
+ "schema: "public",
31
+ "extensions": ["uuid-ossp", ...],
32
+ "users": { // See pgbelt.util.postgres.precheck_info results["users"] for more info.
33
+ "root": {
34
+ "rolname": "root",
35
+ "rolcanlogin": True,
36
+ "rolcreaterole": True,
37
+ "rolinherit": True,
38
+ "rolsuper": True,
39
+ "memberof": ["rds_superuser", ...]
40
+ },
41
+ "owner": {
42
+ "rolname": "owner",
43
+ "rolcanlogin": True,
44
+ "rolcreaterole": False,
45
+ "rolinherit": True,
46
+ "rolsuper": False,
47
+ "memberof": ["rds_superuser", ...]
48
+ }
49
+ }
50
+ },
51
+ ...
52
+ ]
53
+ """
54
+
55
+ summary_table = [
56
+ [
57
+ style("database", "yellow"),
58
+ style("server_version", "yellow"),
59
+ style("max_replication_slots", "yellow"),
60
+ style("max_worker_processes", "yellow"),
61
+ style("max_wal_senders", "yellow"),
62
+ style("shared_preload_libraries", "yellow"),
63
+ style("rds.logical_replication", "yellow"),
64
+ style("root user ok", "yellow"),
65
+ style("owner user ok", "yellow"),
66
+ style("targeted schema", "yellow"),
67
+ style("extensions ok", "yellow"),
68
+ ]
69
+ ]
70
+
71
+ results.sort(key=lambda d: d["db"])
72
+
73
+ for r in results:
74
+ root_ok = (
75
+ r["users"]["root"]["rolcanlogin"]
76
+ and r["users"]["root"]["rolcreaterole"]
77
+ and r["users"]["root"]["rolinherit"]
78
+ ) and (
79
+ "rds_superuser" in r["users"]["root"]["memberof"]
80
+ or r["users"]["root"]["rolsuper"]
81
+ )
82
+
83
+ # Interestingly enough, we can tell if this is being run for a destination database if the compared_extensions is not None.
84
+ # This is because it is only set when we are ensuring all source extensions are in the destination.
85
+ is_dest_db = compared_extensions is not None
86
+
87
+ # If this is a destination database, we need to check if the owner can create objects.
88
+
89
+ if is_dest_db:
90
+ owner_ok = (r["users"]["owner"]["rolcanlogin"]) and (
91
+ r["users"]["owner"]["can_create"]
92
+ )
93
+ else:
94
+ owner_ok = r["users"]["owner"]["rolcanlogin"]
95
+
96
+ shared_preload_libraries = "ok"
97
+ missing = []
98
+ if "pg_stat_statements" not in r["shared_preload_libraries"]:
99
+ missing.append("pg_stat_statements")
100
+ if "pglogical" not in r["shared_preload_libraries"]:
101
+ missing.append("pglogical")
102
+ if missing:
103
+ shared_preload_libraries = ", ".join(missing) + " are missing!"
104
+
105
+ summary_table.append(
106
+ [
107
+ style(r["db"], "green"),
108
+ style(
109
+ r["server_version"],
110
+ (
111
+ "green"
112
+ if float(
113
+ r["server_version"].rsplit(" ", 1)[0].rsplit(".", 1)[0]
114
+ )
115
+ >= 9.6
116
+ else "red"
117
+ ),
118
+ ),
119
+ style(
120
+ r["max_replication_slots"],
121
+ "green" if int(r["max_replication_slots"]) >= 2 else "red",
122
+ ),
123
+ style(
124
+ r["max_worker_processes"],
125
+ "green" if int(r["max_worker_processes"]) >= 2 else "red",
126
+ ),
127
+ style(
128
+ r["max_wal_senders"],
129
+ "green" if int(r["max_wal_senders"]) >= 10 else "red",
130
+ ),
131
+ style(
132
+ shared_preload_libraries,
133
+ "green" if shared_preload_libraries == "ok" else "red",
134
+ ),
135
+ style(
136
+ r["rds.logical_replication"],
137
+ (
138
+ "green"
139
+ if r["rds.logical_replication"] in ["on", "Not Applicable"]
140
+ else "red"
141
+ ),
142
+ ),
143
+ style(root_ok, "green" if root_ok else "red"),
144
+ style(owner_ok, "green" if owner_ok else "red"),
145
+ style(r["schema"], "green"),
146
+ ]
147
+ )
148
+
149
+ # If this is a destinatino DB, we are ensuring all source extensions are in the destination.
150
+ # If not, we don't want this column in the table.
151
+ if is_dest_db:
152
+ extensions_ok = all(
153
+ [e in r["extensions"] for e in compared_extensions]
154
+ ) and all([e in compared_extensions for e in r["extensions"]])
155
+ summary_table[-1].append(
156
+ style(extensions_ok, "green" if extensions_ok else "red")
157
+ )
158
+
159
+ return summary_table
160
+
161
+
162
+ def _users_table(users: dict, is_dest_db: bool = False) -> list[list]:
163
+ """
164
+ Takes a dict of user info and returns a table of the users for echo.
165
+
166
+ The users table alters slightly if the results are for a destination database.
167
+
168
+ users format:
169
+ {
170
+ "root": {
171
+ "rolname": "root",
172
+ "rolcanlogin": True,
173
+ "rolcreaterole": True,
174
+ "rolinherit": True,
175
+ "rolsuper": True,
176
+ "memberof": ["rds_superuser", ...]
177
+ },
178
+ "owner": {
179
+ "rolname": "owner",
180
+ "rolcanlogin": True,
181
+ "rolcreaterole": False,
182
+ "rolinherit": True,
183
+ "rolsuper": False,
184
+ "memberof": ["rds_superuser", ...]
185
+ }
186
+ }
187
+
188
+ See pgbelt.util.postgres.precheck_info results["users"] for more info..
189
+ """
190
+
191
+ users_table = [
192
+ [
193
+ style("user", "yellow"),
194
+ style("name", "yellow"),
195
+ style("can log in", "yellow"),
196
+ style("can make roles", "yellow"),
197
+ style("is superuser", "yellow"),
198
+ ]
199
+ ]
200
+
201
+ if is_dest_db:
202
+ users_table[0].insert(
203
+ 5, style("can create objects in targeted schema", "yellow")
204
+ )
205
+
206
+ root_in_superusers = (
207
+ "rds_superuser" in users["root"]["memberof"] and users["root"]["rolinherit"]
208
+ ) or (users["root"]["rolsuper"])
209
+
210
+ users_table.append(
211
+ [
212
+ style("root", "green"),
213
+ style(users["root"]["rolname"], "green"),
214
+ style(
215
+ users["root"]["rolcanlogin"],
216
+ "green" if users["root"]["rolcanlogin"] else "red",
217
+ ),
218
+ style(
219
+ users["root"]["rolcreaterole"],
220
+ "green" if users["root"]["rolcreaterole"] else "red",
221
+ ),
222
+ style(root_in_superusers, "green" if root_in_superusers else "red"),
223
+ style("not required", "green"),
224
+ ]
225
+ )
226
+
227
+ users_table.append(
228
+ [
229
+ style("owner", "green"),
230
+ style(users["owner"]["rolname"], "green"),
231
+ style(
232
+ users["owner"]["rolcanlogin"],
233
+ "green" if users["owner"]["rolcanlogin"] else "red",
234
+ ),
235
+ style("not required", "green"),
236
+ style("not required", "green"),
237
+ style(
238
+ users["owner"]["can_create"],
239
+ "green" if users["owner"]["can_create"] else "red",
240
+ ),
241
+ ]
242
+ )
243
+
244
+ return users_table
245
+
246
+
247
+ def _tables_table(
248
+ tables: list[dict], pkeys: list[dict], owner_name: str, schema_name: str
249
+ ) -> list[list]:
250
+ """
251
+ Takes a list of table dicts and returns a table of the tables for echo.
252
+
253
+ tables format:
254
+ [
255
+ {
256
+ "Name": "table_name",
257
+ "Schema": "schema_name",
258
+ "Owner": "owner_name"
259
+ },
260
+ ...
261
+ ]
262
+ """
263
+
264
+ tables_table = [
265
+ [
266
+ style("table name", "yellow"),
267
+ style("can replicate", "yellow"),
268
+ style("replication type", "yellow"),
269
+ style("schema", "yellow"),
270
+ style("owner", "yellow"),
271
+ ]
272
+ ]
273
+
274
+ for t in tables:
275
+ can_replicate = t["Schema"] == schema_name and t["Owner"] == owner_name
276
+ replication = (
277
+ ("pglogical" if t["Name"] in pkeys else "dump and load")
278
+ if can_replicate
279
+ else "unavailable"
280
+ )
281
+ tables_table.append(
282
+ [
283
+ style(t["Name"], "green"),
284
+ style(can_replicate, "green" if can_replicate else "red"),
285
+ style(replication, "green" if can_replicate else "red"),
286
+ style(t["Schema"], "green" if t["Schema"] == schema_name else "red"),
287
+ style(t["Owner"], "green" if t["Owner"] == owner_name else "red"),
288
+ ]
289
+ )
290
+
291
+ return tables_table
292
+
293
+
294
+ def _sequences_table(
295
+ sequences: list[dict], owner_name: str, schema_name: str
296
+ ) -> list[list]:
297
+ """
298
+ Takes a list of sequence dicts and returns a table of the sequences for echo.
299
+
300
+ sequences format:
301
+ [
302
+ {
303
+ "Name": "sequence_name",
304
+ "Schema": "schema_name",
305
+ "Owner": "owner_name"
306
+ },
307
+ ...
308
+ ]
309
+ """
310
+
311
+ sequences_table = [
312
+ [
313
+ style("sequence name", "yellow"),
314
+ style("can replicate", "yellow"),
315
+ style("schema", "yellow"),
316
+ style("owner", "yellow"),
317
+ ]
318
+ ]
319
+
320
+ for s in sequences:
321
+ can_replicate = s["Schema"] == schema_name and s["Owner"] == owner_name
322
+ sequences_table.append(
323
+ [
324
+ style(s["Name"], "green"),
325
+ style(can_replicate, "green" if can_replicate else "red"),
326
+ style(s["Schema"], "green" if s["Schema"] == schema_name else "red"),
327
+ style(s["Owner"], "green" if s["Owner"] == owner_name else "red"),
328
+ ]
329
+ )
330
+
331
+ return sequences_table
332
+
333
+
334
+ def _extensions_table(
335
+ source_extensions: list[str], destination_extensions: list[str]
336
+ ) -> list[list]:
337
+ """
338
+
339
+ Takes a list of source and destination extensions and returns a table of the extensions for echo.
340
+ It will flag any extensions that are not in the destination database but are in the source database.
341
+
342
+ <source/destination>_extensions format:
343
+ [
344
+ "uuid-ossp",
345
+ ...
346
+ ]
347
+
348
+ """
349
+
350
+ extensions_table = [
351
+ [
352
+ style("extension in source DB", "yellow"),
353
+ style("is in destination", "yellow"),
354
+ ]
355
+ ]
356
+
357
+ for e in source_extensions:
358
+ extensions_table.append(
359
+ [
360
+ style(e["extname"], "green"),
361
+ style(
362
+ e in destination_extensions,
363
+ "green" if e in destination_extensions else "red",
364
+ ),
365
+ ]
366
+ )
367
+
368
+ return extensions_table
369
+
370
+
371
+ async def _print_prechecks(results: list[dict]) -> list[list]:
372
+ """
373
+ Print out the results of the prechecks in a human readable format.
374
+ If there are multiple databases, only print the summary table.
375
+ If there is only one database, print the summary table and more detailed info.
376
+
377
+ results format:
378
+ [
379
+ {
380
+ "db": "db_name",
381
+ "src": {
382
+ "server_version": "9.6.20",
383
+ "max_replication_slots": "10",
384
+ "max_worker_processes": "10",
385
+ "max_wal_senders": "10",
386
+ "pg_stat_statements": "installed",
387
+ "pglogical": "installed",
388
+ "rds.logical_replication": "on",
389
+ "schema: "public",
390
+ "users": { // See pgbelt.util.postgres.precheck_info results["users"] for more info.
391
+ "root": {
392
+ "rolname": "root",
393
+ "rolcanlogin": True,
394
+ "rolcreaterole": True,
395
+ "rolinherit": True,
396
+ "rolsuper": True,
397
+ "memberof": ["rds_superuser", ...]
398
+ },
399
+ "owner": {
400
+ "rolname": "owner",
401
+ "rolcanlogin": True,
402
+ "rolcreaterole": False,
403
+ "rolinherit": True,
404
+ "rolsuper": False,
405
+ "memberof": ["rds_superuser", ...],
406
+ "can_create": True
407
+ }
408
+ }
409
+ },
410
+ "dst": {
411
+ "server_version": "9.6.20",
412
+ "max_replication_slots": "10",
413
+ "max_worker_processes": "10",
414
+ "max_wal_senders": "10",
415
+ "pg_stat_statements": "installed",
416
+ "pglogical": "installed",
417
+ "rds.logical_replication": "on",
418
+ "schema: "public",
419
+ "users": { // See pgbelt.util.postgres.precheck_info results["users"] for more info.
420
+ "root": {
421
+ "rolname": "root",
422
+ "rolcanlogin": True,
423
+ "rolcreaterole": True,
424
+ "rolinherit": True,
425
+ "rolsuper": True,
426
+ "memberof": ["rds_superuser", ...]
427
+ },
428
+ "owner": {
429
+ "rolname": "owner",
430
+ "rolcanlogin": True,
431
+ "rolcreaterole": False,
432
+ "rolinherit": True,
433
+ "rolsuper": False,
434
+ "memberof": ["rds_superuser", ...],
435
+ "can_create": True
436
+ }
437
+ }
438
+ }
439
+ },
440
+ ...
441
+ ]
442
+ """
443
+
444
+ src_summaries = []
445
+ dst_summaries = []
446
+ for r in results:
447
+ src_summaries.append(r["src"])
448
+ dst_summaries.append(r["dst"])
449
+
450
+ src_summary_table = _summary_table(src_summaries)
451
+ dst_summary_table = _summary_table(
452
+ dst_summaries, compared_extensions=r["src"]["extensions"]
453
+ )
454
+
455
+ if len(results) != 1:
456
+
457
+ # For mulitple databases, we only print the summary table.
458
+
459
+ src_multi_display_string = (
460
+ style("\nSource DB Configuration Summary", "blue")
461
+ + "\n"
462
+ + tabulate(src_summary_table, headers="firstrow")
463
+ )
464
+ echo(src_multi_display_string)
465
+ dst_multi_display_string = (
466
+ style("\nDestination DB Configuration Summary", "blue")
467
+ + "\n"
468
+ + tabulate(dst_summary_table, headers="firstrow")
469
+ )
470
+ echo(dst_multi_display_string)
471
+
472
+ return src_multi_display_string, dst_multi_display_string
473
+
474
+ # If we ran only on one db print more detailed info
475
+ r = results[0]
476
+
477
+ # TODO: We should confirm the named schema exists in the database and alert the user if it does not (red in column if not found).
478
+
479
+ # Source DB Tables
480
+
481
+ src_users_table = _users_table(r["src"]["users"])
482
+ src_tables_table = _tables_table(
483
+ r["src"]["tables"],
484
+ r["src"]["pkeys"],
485
+ r["src"]["users"]["owner"]["rolname"],
486
+ r["src"]["schema"],
487
+ )
488
+ src_sequences_table = _sequences_table(
489
+ r["src"]["sequences"], r["src"]["users"]["owner"]["rolname"], r["src"]["schema"]
490
+ )
491
+
492
+ if len(src_tables_table) == 1:
493
+ src_tables_table = [
494
+ [
495
+ style(
496
+ "ALERT: Not able to find tables to replicate, check your config's 'schema_name'",
497
+ "red",
498
+ )
499
+ ]
500
+ ]
501
+
502
+ if len(src_sequences_table) == 1:
503
+ src_sequences_table = [
504
+ [
505
+ style(
506
+ "ALERT: Not able to find sequences to replicate, check your config's 'schema_name'",
507
+ "red",
508
+ )
509
+ ]
510
+ ]
511
+
512
+ source_display_string = (
513
+ style("\nSource DB Configuration Summary", "blue")
514
+ + "\n"
515
+ + "\n"
516
+ + tabulate(src_summary_table, headers="firstrow")
517
+ + "\n"
518
+ + style("\nRequired Users Summary", "yellow")
519
+ + "\n"
520
+ + tabulate(src_users_table, headers="firstrow")
521
+ + "\n"
522
+ + style("\nTable Compatibility Summary", "yellow")
523
+ + "\n"
524
+ + tabulate(
525
+ src_tables_table, headers="firstrow" if len(src_tables_table) > 1 else ""
526
+ )
527
+ + "\n"
528
+ + style("\nSequence Compatibility Summary", "yellow")
529
+ + "\n"
530
+ + tabulate(
531
+ src_sequences_table,
532
+ headers="firstrow" if len(src_sequences_table) > 1 else "",
533
+ )
534
+ )
535
+
536
+ echo(source_display_string)
537
+
538
+ echo("\n" + "=" * 80)
539
+
540
+ # Destination DB Tables
541
+
542
+ dst_users_table = _users_table(r["dst"]["users"], is_dest_db=True)
543
+ extenstions_table = _extensions_table(
544
+ r["src"]["extensions"], r["dst"]["extensions"]
545
+ )
546
+
547
+ destination_display_string = (
548
+ style("\nDestination DB Configuration Summary", "blue")
549
+ + "\n"
550
+ + "\n"
551
+ + tabulate(dst_summary_table, headers="firstrow")
552
+ + "\n"
553
+ + style("\nExtension Matchup Summary", "yellow")
554
+ + "\n"
555
+ + tabulate(extenstions_table, headers="firstrow")
556
+ + "\n"
557
+ + style("\nRequired Users Summary", "yellow")
558
+ + "\n"
559
+ + tabulate(dst_users_table, headers="firstrow")
560
+ )
561
+
562
+ echo(destination_display_string)
563
+
564
+ return src_summary_table, dst_summary_table
565
+
566
+
567
+ @run_with_configs(skip_dst=True, results_callback=_print_prechecks)
568
+ async def precheck(config_future: Awaitable[DbupgradeConfig]) -> dict:
569
+ """
570
+ Report whether your source database meets the basic requirements for pgbelt.
571
+ Any red item in a row in the table indicates a requirement not satisfied by your db.
572
+ This command can not check network connectivity between your source and destination!
573
+
574
+ If a dbname is given this will also show whether the configuration of
575
+ the root and owner users seems ok and a summary of whether each
576
+ table and sequence in the database can be replicated.
577
+ If a row contains any red that sequence or table can not be replicated.
578
+ """
579
+
580
+ conf = await config_future
581
+ pools = await gather(
582
+ create_pool(conf.src.root_uri, min_size=1),
583
+ create_pool(conf.src.owner_uri, min_size=1),
584
+ create_pool(conf.dst.root_uri, min_size=1),
585
+ )
586
+ src_root_pool, src_owner_pool, dst_root_pool = pools
587
+
588
+ try:
589
+ src_logger = get_logger(conf.db, conf.dc, "preflight.src")
590
+ dst_logger = get_logger(conf.db, conf.dc, "preflight.dst")
591
+
592
+ result = {}
593
+
594
+ # Source DB Data
595
+ result["src"] = await precheck_info(
596
+ src_root_pool,
597
+ conf.src.root_user.name,
598
+ conf.src.owner_user.name,
599
+ conf.tables,
600
+ conf.sequences,
601
+ conf.schema_name,
602
+ src_logger,
603
+ )
604
+ result["src"]["pkeys"], _, _ = await analyze_table_pkeys(
605
+ src_owner_pool, conf.schema_name, src_logger
606
+ )
607
+ result["src"]["schema"] = conf.schema_name
608
+
609
+ # Destination DB Data
610
+ result["dst"] = await precheck_info(
611
+ dst_root_pool,
612
+ conf.dst.root_user.name,
613
+ conf.dst.owner_user.name,
614
+ conf.tables,
615
+ conf.sequences,
616
+ conf.schema_name,
617
+ dst_logger,
618
+ )
619
+ # No need to analyze pkeys for the destination database (we use this to determine replication method in only the forward case).
620
+ result["dst"]["schema"] = conf.schema_name
621
+
622
+ # The precheck view code treats "db" as the name of the database pair, not the logical dbname of the database.
623
+ result["src"]["db"] = conf.db
624
+ result["dst"]["db"] = conf.db
625
+
626
+ return result
627
+ finally:
628
+ await gather(*[p.close() for p in pools])
629
+
630
+
631
+ COMMANDS = [precheck]