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.
Files changed (26) hide show
  1. {pgbelt-0.7.10 → pgbelt-0.8.0}/PKG-INFO +4 -3
  2. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/schema.py +15 -0
  3. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/status.py +11 -1
  4. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/sync.py +16 -38
  5. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/postgres.py +28 -6
  6. {pgbelt-0.7.10 → pgbelt-0.8.0}/pyproject.toml +11 -11
  7. {pgbelt-0.7.10 → pgbelt-0.8.0}/LICENSE +0 -0
  8. {pgbelt-0.7.10 → pgbelt-0.8.0}/README.md +0 -0
  9. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/__init__.py +0 -0
  10. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/__init__.py +0 -0
  11. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/convenience.py +0 -0
  12. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/helpers.py +0 -0
  13. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/login.py +0 -0
  14. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/preflight.py +0 -0
  15. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/setup.py +0 -0
  16. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/cmd/teardown.py +0 -0
  17. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/__init__.py +0 -0
  18. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/config.py +0 -0
  19. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/models.py +0 -0
  20. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/config/remote.py +0 -0
  21. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/main.py +0 -0
  22. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/__init__.py +0 -0
  23. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/asyncfuncs.py +0 -0
  24. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/dump.py +0 -0
  25. {pgbelt-0.7.10 → pgbelt-0.8.0}/pgbelt/util/logs.py +0 -0
  26. {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.7.10
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.30)
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.13)
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
- if result[0]["pg1_pg2"] == "replicating":
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(conf.dst.root_uri, min_size=1) as dst_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(dst_owner_pool, dst_logger),
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
- raise AssertionError(
175
- "Row match failure between source and destination.\n"
176
- f"Table: {full_table_name}\n"
177
- f"Source Row: {src_row}\n"
178
- f"Dest Row: {dst_row}"
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.7.10"
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.30"
15
+ asyncpg = ">=0.27,<0.31"
16
16
  pydantic = ">=2.0,<3.0"
17
17
  tabulate = "^0.9.0"
18
- typer = ">=0.9,<0.13"
18
+ typer = ">=0.9,<0.14"
19
19
 
20
20
  [tool.poetry.dev-dependencies]
21
- black = "~24.8.0"
22
- pre-commit = "~3.8.0"
21
+ black = "~24.10.0"
22
+ pre-commit = "~4.0.1"
23
23
  flake8 = "^7.1.1"
24
- pytest-cov = "~5.0.0"
24
+ pytest-cov = "~6.0.0"
25
25
  pytest = "^8.3.3"
26
26
  coverage = {extras = ["toml"], version = "^7.6"}
27
- safety = "^3.2.7"
28
- mypy = "^1.11"
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.13.0"
37
- pre-commit-hooks = "^4.6.0"
36
+ reorder-python-imports = "^3.14.0"
37
+ pre-commit-hooks = "^5.0.0"
38
38
  Pygments = "^2.18.0"
39
- pyupgrade = "^3.17.0"
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