pgbelt 0.7.10__tar.gz → 0.8.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.
- {pgbelt-0.7.10 → pgbelt-0.8.0}/PKG-INFO +4 -3
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/schema.py +15 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/status.py +11 -1
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/sync.py +16 -38
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/postgres.py +28 -6
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pyproject.toml +11 -11
- {pgbelt-0.7.10 → pgbelt-0.8.0}/LICENSE +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/README.md +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/__init__.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/__init__.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/convenience.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/helpers.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/login.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/preflight.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/setup.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/teardown.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/__init__.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/config.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/models.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/remote.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/main.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/__init__.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/asyncfuncs.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/dump.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/logs.py +0 -0
- {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/pglogical.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pgbelt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -10,11 +10,12 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
14
|
Requires-Dist: aiofiles (>=0.8,<24.2)
|
|
14
|
-
Requires-Dist: asyncpg (>=0.27,<0.
|
|
15
|
+
Requires-Dist: asyncpg (>=0.27,<0.31)
|
|
15
16
|
Requires-Dist: pydantic (>=2.0,<3.0)
|
|
16
17
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
17
|
-
Requires-Dist: typer (>=0.9,<0.
|
|
18
|
+
Requires-Dist: typer (>=0.9,<0.14)
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
# Pgbelt
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Awaitable
|
|
2
|
+
from asyncpg import create_pool
|
|
2
3
|
|
|
3
4
|
from pgbelt.cmd.helpers import run_with_configs
|
|
4
5
|
from pgbelt.config.models import DbupgradeConfig
|
|
@@ -11,6 +12,7 @@ from pgbelt.util.dump import dump_dst_create_index
|
|
|
11
12
|
from pgbelt.util.dump import remove_dst_not_valid_constraints
|
|
12
13
|
from pgbelt.util.dump import remove_dst_indexes
|
|
13
14
|
from pgbelt.util.logs import get_logger
|
|
15
|
+
from pgbelt.util.postgres import run_analyze
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@run_with_configs
|
|
@@ -109,11 +111,24 @@ async def create_indexes(config_future: Awaitable[DbupgradeConfig]) -> None:
|
|
|
109
111
|
as the owner user. This must only be done after most data is synchronized
|
|
110
112
|
(at minimum after the initializing phase) from the source to the destination
|
|
111
113
|
database.
|
|
114
|
+
|
|
115
|
+
After creating indexes, the destination database should be analyzed to ensure
|
|
116
|
+
the query planner has the most up-to-date statistics for the indexes.
|
|
112
117
|
"""
|
|
113
118
|
conf = await config_future
|
|
114
119
|
logger = get_logger(conf.db, conf.dc, "schema.dst")
|
|
115
120
|
await create_target_indexes(conf, logger, during_sync=False)
|
|
116
121
|
|
|
122
|
+
# Run ANALYZE after creating indexes (without statement timeout)
|
|
123
|
+
async with create_pool(
|
|
124
|
+
conf.dst.root_uri,
|
|
125
|
+
min_size=1,
|
|
126
|
+
server_settings={
|
|
127
|
+
"statement_timeout": "0",
|
|
128
|
+
},
|
|
129
|
+
) as dst_pool:
|
|
130
|
+
await run_analyze(dst_pool, logger)
|
|
131
|
+
|
|
117
132
|
|
|
118
133
|
COMMANDS = [
|
|
119
134
|
dump_schema,
|
|
@@ -116,10 +116,20 @@ async def status(conf_future: Awaitable[DbupgradeConfig]) -> dict[str, str]:
|
|
|
116
116
|
|
|
117
117
|
result[0].update(result[1])
|
|
118
118
|
result[0]["db"] = conf.db
|
|
119
|
-
|
|
119
|
+
|
|
120
|
+
# We should hide the progress in the following cases:
|
|
121
|
+
# 1. When src -> dst is replicating and dst -> src is any state (replicating, unconfigured, down)
|
|
122
|
+
# a. We do this because the size when done still will be a tad smaller than SRC, showing <100%
|
|
123
|
+
# 2. When src -> dst is unconfigured and dst -> src is replicating (not down or unconfigured)
|
|
124
|
+
# a. We do this because reverse-only occurs at the start of cutover and onwards, and seeing the progress at that stage is not useful.
|
|
125
|
+
if (result[0]["pg1_pg2"] == "replicating") or ( # 1
|
|
126
|
+
result[0]["pg1_pg2"] == "unconfigured"
|
|
127
|
+
and result[0]["pg2_pg1"] == "replicating"
|
|
128
|
+
): # 2
|
|
120
129
|
result[2]["src_dataset_size"] = "n/a"
|
|
121
130
|
result[2]["dst_dataset_size"] = "n/a"
|
|
122
131
|
result[2]["progress"] = "n/a"
|
|
132
|
+
|
|
123
133
|
result[0].update(result[2])
|
|
124
134
|
return result[0]
|
|
125
135
|
finally:
|
|
@@ -108,40 +108,6 @@ async def load_tables(
|
|
|
108
108
|
await load_dumped_tables(conf, tables, logger)
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
@run_with_configs
|
|
112
|
-
async def sync_tables(
|
|
113
|
-
config_future: Awaitable[DbupgradeConfig],
|
|
114
|
-
tables: list[str] = Option([], help="Specific tables to sync"),
|
|
115
|
-
):
|
|
116
|
-
"""
|
|
117
|
-
Dump and load all tables from the source database to the destination database.
|
|
118
|
-
Equivalent to running dump-tables followed by load-tables. Table data will be
|
|
119
|
-
saved locally in files.
|
|
120
|
-
|
|
121
|
-
You may also provide a list of tables to sync with the
|
|
122
|
-
--tables option and only these tables will be synced.
|
|
123
|
-
"""
|
|
124
|
-
conf = await config_future
|
|
125
|
-
src_logger = get_logger(conf.db, conf.dc, "sync.src")
|
|
126
|
-
dst_logger = get_logger(conf.db, conf.dc, "sync.dst")
|
|
127
|
-
|
|
128
|
-
if tables:
|
|
129
|
-
dump_tables = tables.split(",")
|
|
130
|
-
else:
|
|
131
|
-
async with create_pool(conf.src.pglogical_uri, min_size=1) as src_pool:
|
|
132
|
-
_, dump_tables, _ = await analyze_table_pkeys(
|
|
133
|
-
src_pool, conf.schema_name, src_logger
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if conf.tables:
|
|
137
|
-
dump_tables = [t for t in dump_tables if t in conf.tables]
|
|
138
|
-
|
|
139
|
-
await dump_source_tables(conf, dump_tables)
|
|
140
|
-
await load_dumped_tables(
|
|
141
|
-
conf, [] if not tables and not conf.tables else dump_tables, dst_logger
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
111
|
@run_with_configs(skip_src=True)
|
|
146
112
|
async def analyze(config_future: Awaitable[DbupgradeConfig]) -> None:
|
|
147
113
|
"""
|
|
@@ -150,7 +116,13 @@ async def analyze(config_future: Awaitable[DbupgradeConfig]) -> None:
|
|
|
150
116
|
"""
|
|
151
117
|
conf = await config_future
|
|
152
118
|
logger = get_logger(conf.db, conf.dc, "sync.dst")
|
|
153
|
-
async with create_pool(
|
|
119
|
+
async with create_pool(
|
|
120
|
+
conf.dst.root_uri,
|
|
121
|
+
min_size=1,
|
|
122
|
+
server_settings={
|
|
123
|
+
"statement_timeout": "0",
|
|
124
|
+
},
|
|
125
|
+
) as dst_pool:
|
|
154
126
|
await run_analyze(dst_pool, logger)
|
|
155
127
|
|
|
156
128
|
|
|
@@ -208,8 +180,15 @@ async def sync(
|
|
|
208
180
|
create_pool(conf.src.pglogical_uri, min_size=1),
|
|
209
181
|
create_pool(conf.dst.root_uri, min_size=1),
|
|
210
182
|
create_pool(conf.dst.owner_uri, min_size=1),
|
|
183
|
+
create_pool(
|
|
184
|
+
conf.dst.root_uri,
|
|
185
|
+
min_size=1,
|
|
186
|
+
server_settings={
|
|
187
|
+
"statement_timeout": "0",
|
|
188
|
+
},
|
|
189
|
+
),
|
|
211
190
|
)
|
|
212
|
-
src_pool, dst_root_pool, dst_owner_pool = pools
|
|
191
|
+
src_pool, dst_root_pool, dst_owner_pool, dst_root_no_timeout_pool = pools
|
|
213
192
|
|
|
214
193
|
try:
|
|
215
194
|
src_logger = get_logger(conf.db, conf.dc, "sync.src")
|
|
@@ -253,7 +232,7 @@ async def sync(
|
|
|
253
232
|
conf.schema_name,
|
|
254
233
|
validation_logger,
|
|
255
234
|
),
|
|
256
|
-
run_analyze(
|
|
235
|
+
run_analyze(dst_root_no_timeout_pool, dst_logger),
|
|
257
236
|
)
|
|
258
237
|
finally:
|
|
259
238
|
await gather(*[p.close() for p in pools])
|
|
@@ -263,7 +242,6 @@ COMMANDS = [
|
|
|
263
242
|
sync_sequences,
|
|
264
243
|
dump_tables,
|
|
265
244
|
load_tables,
|
|
266
|
-
sync_tables,
|
|
267
245
|
analyze,
|
|
268
246
|
validate_data,
|
|
269
247
|
sync,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from logging import Logger
|
|
2
2
|
|
|
3
|
+
from decimal import Decimal
|
|
3
4
|
from asyncpg import Pool
|
|
4
5
|
from asyncpg import Record
|
|
5
6
|
from asyncpg.exceptions import UndefinedObjectError
|
|
@@ -171,12 +172,33 @@ async def compare_data(
|
|
|
171
172
|
# Check each row for exact match
|
|
172
173
|
for src_row, dst_row in zip(src_rows, dst_rows):
|
|
173
174
|
if src_row != dst_row:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
|
|
176
|
+
# Addresses #571, AsyncPG is decoding numeric NaN as Python Decimal('NaN').
|
|
177
|
+
# Decimal('NaN') != Decimal('NaN'), breaks comparison. Convert those NaNs to None.
|
|
178
|
+
src_row_d = {
|
|
179
|
+
key: (
|
|
180
|
+
value
|
|
181
|
+
if not (isinstance(value, Decimal) and value.is_nan())
|
|
182
|
+
else None
|
|
183
|
+
)
|
|
184
|
+
for key, value in row.items()
|
|
185
|
+
}
|
|
186
|
+
dst_row_d = {
|
|
187
|
+
key: (
|
|
188
|
+
value
|
|
189
|
+
if not (isinstance(value, Decimal) and value.is_nan())
|
|
190
|
+
else None
|
|
191
|
+
)
|
|
192
|
+
for key, value in row.items()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if src_row_d != dst_row_d:
|
|
196
|
+
raise AssertionError(
|
|
197
|
+
"Row match failure between source and destination.\n"
|
|
198
|
+
f"Table: {full_table_name}\n"
|
|
199
|
+
f"Source Row: {src_row}\n"
|
|
200
|
+
f"Dest Row: {dst_row}"
|
|
201
|
+
)
|
|
180
202
|
|
|
181
203
|
# Just a paranoia check. If this throws, then it's possible pgbelt didn't migrate any data.
|
|
182
204
|
# This was found in issue #420, and previous commands threw errors before this issue could arise.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pgbelt"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "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
|
authors = ["Varjitt Jeeva <varjitt.jeeva@autodesk.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -12,20 +12,20 @@ packages = [
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
13
|
python = ">=3.9,<4.0"
|
|
14
14
|
aiofiles = ">=0.8,<24.2"
|
|
15
|
-
asyncpg = ">=0.27,<0.
|
|
15
|
+
asyncpg = ">=0.27,<0.31"
|
|
16
16
|
pydantic = ">=2.0,<3.0"
|
|
17
17
|
tabulate = "^0.9.0"
|
|
18
|
-
typer = ">=0.9,<0.
|
|
18
|
+
typer = ">=0.9,<0.14"
|
|
19
19
|
|
|
20
20
|
[tool.poetry.dev-dependencies]
|
|
21
|
-
black = "~24.
|
|
22
|
-
pre-commit = "~
|
|
21
|
+
black = "~24.10.0"
|
|
22
|
+
pre-commit = "~4.0.1"
|
|
23
23
|
flake8 = "^7.1.1"
|
|
24
|
-
pytest-cov = "~
|
|
24
|
+
pytest-cov = "~6.0.0"
|
|
25
25
|
pytest = "^8.3.3"
|
|
26
26
|
coverage = {extras = ["toml"], version = "^7.6"}
|
|
27
|
-
safety = "^3.2.
|
|
28
|
-
mypy = "^1.
|
|
27
|
+
safety = "^3.2.11"
|
|
28
|
+
mypy = "^1.13"
|
|
29
29
|
xdoctest = {extras = ["colors"], version = "^1.2.0"}
|
|
30
30
|
flake8-bandit = "~4.1.1"
|
|
31
31
|
flake8-bugbear = ">=21.9.2"
|
|
@@ -33,10 +33,10 @@ flake8-docstrings = "^1.6.0"
|
|
|
33
33
|
flake8-rst-docstrings = "^0.3.0"
|
|
34
34
|
pep8-naming = "^0.14.1"
|
|
35
35
|
darglint = "^1.8.1"
|
|
36
|
-
reorder-python-imports = "^3.
|
|
37
|
-
pre-commit-hooks = "^
|
|
36
|
+
reorder-python-imports = "^3.14.0"
|
|
37
|
+
pre-commit-hooks = "^5.0.0"
|
|
38
38
|
Pygments = "^2.18.0"
|
|
39
|
-
pyupgrade = "^3.
|
|
39
|
+
pyupgrade = "^3.19.0"
|
|
40
40
|
pylint = "^3.3.1"
|
|
41
41
|
pytest-asyncio = "~0.24.0"
|
|
42
42
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|