pgsync 3.1.0__tar.gz → 3.2.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.
- {pgsync-3.1.0 → pgsync-3.2.0}/PKG-INFO +23 -22
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/__init__.py +1 -1
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/base.py +57 -43
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/constants.py +2 -1
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/helper.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/node.py +7 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/plugin.py +5 -1
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/querybuilder.py +23 -1
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/redisqueue.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/search_client.py +14 -11
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/settings.py +2 -1
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/sync.py +20 -19
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/transform.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/utils.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/view.py +16 -9
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync.egg-info/PKG-INFO +23 -22
- pgsync-3.2.0/pgsync.egg-info/requires.txt +30 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/setup.py +3 -7
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/conftest.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_base.py +26 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_constants.py +2 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_env_vars.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_helper.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_log_handlers.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_node.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_query_builder.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_search_client.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_settings.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_sync.py +14 -8
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_sync_root.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_transform.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_trigger.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_urls.py +1 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_utils.py +1 -0
- pgsync-3.1.0/pgsync.egg-info/requires.txt +0 -29
- {pgsync-3.1.0 → pgsync-3.2.0}/AUTHORS.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/CONTRIBUTING.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/HISTORY.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/LICENSE +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/MANIFEST.in +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/README.md +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/README.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/bin/bootstrap +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/bin/parallel_sync +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/bin/pgsync +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/Makefile +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/authors.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/changelog.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/conf.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/contributing.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/history.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/index.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/installation.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/logo.png +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/make.bat +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/readme.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/docs/usage.rst +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/exc.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/singleton.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/trigger.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync/urls.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync.egg-info/SOURCES.txt +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync.egg-info/dependency_links.txt +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync.egg-info/not-zip-safe +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pgsync.egg-info/top_level.txt +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/pyproject.toml +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/setup.cfg +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/__init__.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/fixtures/schema.json +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_redisqueue.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_sync_nested_children.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_sync_single_child_fk_on_child.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_sync_single_child_fk_on_parent.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_unique_behaviour.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/test_view.py +0 -0
- {pgsync-3.1.0 → pgsync-3.2.0}/tests/testing_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pgsync
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Postgres to Elasticsearch/OpenSearch sync
|
|
5
5
|
Home-page: https://github.com/toluaina/pgsync
|
|
6
6
|
Author: Tolu Aina
|
|
@@ -13,7 +13,7 @@ Project-URL: Funding, https://github.com/sponsors/toluaina
|
|
|
13
13
|
Project-URL: Source, https://github.com/toluaina/pgsync
|
|
14
14
|
Project-URL: Web, https://pgsync.com
|
|
15
15
|
Project-URL: Documentation, https://pgsync.com
|
|
16
|
-
Keywords:
|
|
16
|
+
Keywords: change data capture,elasticsearch,opensearch,pgsync,postgres
|
|
17
17
|
Classifier: Development Status :: 5 - Production/Stable
|
|
18
18
|
Classifier: Intended Audience :: Developers
|
|
19
19
|
Classifier: Natural Language :: English
|
|
@@ -31,34 +31,35 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
License-File: AUTHORS.rst
|
|
33
33
|
Requires-Dist: async-timeout==4.0.3
|
|
34
|
-
Requires-Dist: boto3==1.34.
|
|
35
|
-
Requires-Dist: botocore==1.34.
|
|
36
|
-
Requires-Dist: certifi==
|
|
34
|
+
Requires-Dist: boto3==1.34.142
|
|
35
|
+
Requires-Dist: botocore==1.34.142
|
|
36
|
+
Requires-Dist: certifi==2024.7.4
|
|
37
37
|
Requires-Dist: charset-normalizer==3.3.2
|
|
38
38
|
Requires-Dist: click==8.1.7
|
|
39
|
-
Requires-Dist: elastic-transport==8.
|
|
40
|
-
Requires-Dist: elasticsearch==8.
|
|
41
|
-
Requires-Dist: elasticsearch-dsl==8.
|
|
42
|
-
Requires-Dist: environs==
|
|
39
|
+
Requires-Dist: elastic-transport==8.13.1
|
|
40
|
+
Requires-Dist: elasticsearch==8.14.0
|
|
41
|
+
Requires-Dist: elasticsearch-dsl==8.14.0
|
|
42
|
+
Requires-Dist: environs==11.0.0
|
|
43
|
+
Requires-Dist: events==0.5
|
|
43
44
|
Requires-Dist: greenlet==3.0.3
|
|
44
|
-
Requires-Dist: idna==3.
|
|
45
|
+
Requires-Dist: idna==3.7
|
|
45
46
|
Requires-Dist: jmespath==1.0.1
|
|
46
|
-
Requires-Dist: marshmallow==3.
|
|
47
|
+
Requires-Dist: marshmallow==3.21.3
|
|
47
48
|
Requires-Dist: opensearch-dsl==2.1.0
|
|
48
|
-
Requires-Dist: opensearch-py==2.
|
|
49
|
-
Requires-Dist: packaging==
|
|
49
|
+
Requires-Dist: opensearch-py==2.6.0
|
|
50
|
+
Requires-Dist: packaging==24.1
|
|
50
51
|
Requires-Dist: psycopg2-binary==2.9.9
|
|
51
|
-
Requires-Dist: python-dateutil==2.
|
|
52
|
-
Requires-Dist: python-dotenv==1.0.
|
|
53
|
-
Requires-Dist: redis==5.0.
|
|
54
|
-
Requires-Dist: requests==2.
|
|
52
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
53
|
+
Requires-Dist: python-dotenv==1.0.1
|
|
54
|
+
Requires-Dist: redis==5.0.7
|
|
55
|
+
Requires-Dist: requests==2.32.3
|
|
55
56
|
Requires-Dist: requests-aws4auth==1.2.3
|
|
56
|
-
Requires-Dist: s3transfer==0.10.
|
|
57
|
+
Requires-Dist: s3transfer==0.10.2
|
|
57
58
|
Requires-Dist: six==1.16.0
|
|
58
|
-
Requires-Dist: sqlalchemy==2.0.
|
|
59
|
-
Requires-Dist: sqlparse==0.
|
|
60
|
-
Requires-Dist: typing-extensions==4.
|
|
61
|
-
Requires-Dist: urllib3==1.26.
|
|
59
|
+
Requires-Dist: sqlalchemy==2.0.31
|
|
60
|
+
Requires-Dist: sqlparse==0.5.0
|
|
61
|
+
Requires-Dist: typing-extensions==4.12.2
|
|
62
|
+
Requires-Dist: urllib3==1.26.19
|
|
62
63
|
|
|
63
64
|
# PostgreSQL to Elasticsearch/OpenSearch sync
|
|
64
65
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""PGSync Base."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import typing as t
|
|
@@ -48,6 +49,15 @@ except ImportError:
|
|
|
48
49
|
|
|
49
50
|
logger = logging.getLogger(__name__)
|
|
50
51
|
|
|
52
|
+
SSL_MODES = (
|
|
53
|
+
"allow",
|
|
54
|
+
"disable",
|
|
55
|
+
"prefer",
|
|
56
|
+
"require",
|
|
57
|
+
"verify-ca",
|
|
58
|
+
"verify-full",
|
|
59
|
+
)
|
|
60
|
+
|
|
51
61
|
|
|
52
62
|
class Payload(object):
|
|
53
63
|
"""
|
|
@@ -141,6 +151,36 @@ class TupleIdentifierType(sa.types.UserDefinedType):
|
|
|
141
151
|
|
|
142
152
|
|
|
143
153
|
class Base(object):
|
|
154
|
+
INT_TYPES = (
|
|
155
|
+
"bigint",
|
|
156
|
+
"bigserial",
|
|
157
|
+
"int",
|
|
158
|
+
"int2",
|
|
159
|
+
"int4",
|
|
160
|
+
"int8",
|
|
161
|
+
"integer",
|
|
162
|
+
"serial",
|
|
163
|
+
"serial2",
|
|
164
|
+
"serial4",
|
|
165
|
+
"serial8",
|
|
166
|
+
"smallint",
|
|
167
|
+
"smallserial",
|
|
168
|
+
)
|
|
169
|
+
FLOAT_TYPES = (
|
|
170
|
+
"double precision",
|
|
171
|
+
"float4",
|
|
172
|
+
"float8",
|
|
173
|
+
"real",
|
|
174
|
+
)
|
|
175
|
+
CHAR_TYPES = (
|
|
176
|
+
"char",
|
|
177
|
+
"character",
|
|
178
|
+
"character varying",
|
|
179
|
+
"text",
|
|
180
|
+
"uuid",
|
|
181
|
+
"varchar",
|
|
182
|
+
)
|
|
183
|
+
|
|
144
184
|
def __init__(
|
|
145
185
|
self, database: str, verbose: bool = False, *args, **kwargs
|
|
146
186
|
) -> None:
|
|
@@ -433,7 +473,7 @@ class Base(object):
|
|
|
433
473
|
func: sa.sql.functions._FunctionGenerator,
|
|
434
474
|
txmin: t.Optional[int] = None,
|
|
435
475
|
txmax: t.Optional[int] = None,
|
|
436
|
-
upto_lsn: t.Optional[
|
|
476
|
+
upto_lsn: t.Optional[str] = None,
|
|
437
477
|
upto_nchanges: t.Optional[int] = None,
|
|
438
478
|
limit: t.Optional[int] = None,
|
|
439
479
|
offset: t.Optional[int] = None,
|
|
@@ -446,7 +486,7 @@ class Base(object):
|
|
|
446
486
|
func (sa.sql.functions._FunctionGenerator): The function to use to read from the slot.
|
|
447
487
|
txmin (Optional[int], optional): The minimum transaction ID to read from. Defaults to None.
|
|
448
488
|
txmax (Optional[int], optional): The maximum transaction ID to read from. Defaults to None.
|
|
449
|
-
upto_lsn (Optional[
|
|
489
|
+
upto_lsn (Optional[str], optional): The maximum LSN to read up to. Defaults to None.
|
|
450
490
|
upto_nchanges (Optional[int], optional): The maximum number of changes to read. Defaults to None.
|
|
451
491
|
limit (Optional[int], optional): The maximum number of rows to return. Defaults to None.
|
|
452
492
|
offset (Optional[int], optional): The number of rows to skip before returning. Defaults to None.
|
|
@@ -489,12 +529,20 @@ class Base(object):
|
|
|
489
529
|
statement = statement.offset(offset)
|
|
490
530
|
return statement
|
|
491
531
|
|
|
532
|
+
@property
|
|
533
|
+
def current_wal_lsn(self) -> str:
|
|
534
|
+
return self.fetchone(
|
|
535
|
+
sa.select(sa.func.MAX(sa.text("pg_current_wal_lsn"))).select_from(
|
|
536
|
+
sa.func.PG_CURRENT_WAL_LSN()
|
|
537
|
+
)
|
|
538
|
+
)[0]
|
|
539
|
+
|
|
492
540
|
def logical_slot_get_changes(
|
|
493
541
|
self,
|
|
494
542
|
slot_name: str,
|
|
495
543
|
txmin: t.Optional[int] = None,
|
|
496
544
|
txmax: t.Optional[int] = None,
|
|
497
|
-
upto_lsn: t.Optional[
|
|
545
|
+
upto_lsn: t.Optional[str] = None,
|
|
498
546
|
upto_nchanges: t.Optional[int] = None,
|
|
499
547
|
limit: t.Optional[int] = None,
|
|
500
548
|
offset: t.Optional[int] = None,
|
|
@@ -524,7 +572,7 @@ class Base(object):
|
|
|
524
572
|
slot_name: str,
|
|
525
573
|
txmin: t.Optional[int] = None,
|
|
526
574
|
txmax: t.Optional[int] = None,
|
|
527
|
-
upto_lsn: t.Optional[
|
|
575
|
+
upto_lsn: t.Optional[str] = None,
|
|
528
576
|
upto_nchanges: t.Optional[int] = None,
|
|
529
577
|
limit: t.Optional[int] = None,
|
|
530
578
|
offset: t.Optional[int] = None,
|
|
@@ -550,7 +598,7 @@ class Base(object):
|
|
|
550
598
|
slot_name: str,
|
|
551
599
|
txmin: t.Optional[int] = None,
|
|
552
600
|
txmax: t.Optional[int] = None,
|
|
553
|
-
upto_lsn: t.Optional[
|
|
601
|
+
upto_lsn: t.Optional[str] = None,
|
|
554
602
|
upto_nchanges: t.Optional[int] = None,
|
|
555
603
|
) -> int:
|
|
556
604
|
statement: sa.sql.Select = self._logical_slot_changes(
|
|
@@ -732,43 +780,16 @@ class Base(object):
|
|
|
732
780
|
"""
|
|
733
781
|
if value.lower() == "null":
|
|
734
782
|
return None
|
|
735
|
-
|
|
736
|
-
if type_.lower() in (
|
|
737
|
-
"bigint",
|
|
738
|
-
"bigserial",
|
|
739
|
-
"int",
|
|
740
|
-
"int2",
|
|
741
|
-
"int4",
|
|
742
|
-
"int8",
|
|
743
|
-
"integer",
|
|
744
|
-
"serial",
|
|
745
|
-
"serial2",
|
|
746
|
-
"serial4",
|
|
747
|
-
"serial8",
|
|
748
|
-
"smallint",
|
|
749
|
-
"smallserial",
|
|
750
|
-
):
|
|
783
|
+
if type_.lower() in self.INT_TYPES:
|
|
751
784
|
try:
|
|
752
785
|
value = int(value)
|
|
753
786
|
except ValueError:
|
|
754
787
|
raise
|
|
755
|
-
if type_.lower() in
|
|
756
|
-
"char",
|
|
757
|
-
"character",
|
|
758
|
-
"character varying",
|
|
759
|
-
"text",
|
|
760
|
-
"uuid",
|
|
761
|
-
"varchar",
|
|
762
|
-
):
|
|
788
|
+
if type_.lower() in self.CHAR_TYPES:
|
|
763
789
|
value = value.lstrip("'").rstrip("'")
|
|
764
790
|
if type_.lower() == "boolean":
|
|
765
791
|
value = bool(value)
|
|
766
|
-
if type_.lower() in
|
|
767
|
-
"double precision",
|
|
768
|
-
"float4",
|
|
769
|
-
"float8",
|
|
770
|
-
"real",
|
|
771
|
-
):
|
|
792
|
+
if type_.lower() in self.FLOAT_TYPES:
|
|
772
793
|
try:
|
|
773
794
|
value = float(value)
|
|
774
795
|
except ValueError:
|
|
@@ -999,14 +1020,7 @@ def _pg_engine(
|
|
|
999
1020
|
sslrootcert = sslrootcert or PG_SSLROOTCERT
|
|
1000
1021
|
|
|
1001
1022
|
if sslmode:
|
|
1002
|
-
if sslmode not in
|
|
1003
|
-
"allow",
|
|
1004
|
-
"disable",
|
|
1005
|
-
"prefer",
|
|
1006
|
-
"require",
|
|
1007
|
-
"verify-ca",
|
|
1008
|
-
"verify-full",
|
|
1009
|
-
):
|
|
1023
|
+
if sslmode not in SSL_MODES:
|
|
1010
1024
|
raise ValueError(f'Invalid sslmode: "{sslmode}"')
|
|
1011
1025
|
connect_args["sslmode"] = sslmode
|
|
1012
1026
|
|
|
@@ -89,6 +89,7 @@ ELASTICSEARCH_TYPES = [
|
|
|
89
89
|
"constant_keyword",
|
|
90
90
|
"date",
|
|
91
91
|
"date_range",
|
|
92
|
+
"dense_vector",
|
|
92
93
|
"double",
|
|
93
94
|
"double_range",
|
|
94
95
|
"flattened",
|
|
@@ -207,5 +208,5 @@ LOGICAL_SLOT_PREFIX = re.compile(
|
|
|
207
208
|
r"table\s\"?(?P<schema>[\w-]+)\"?.\"?(?P<table>[\w-]+)\"?:\s(?P<tg_op>[A-Z]+):" # noqa E501
|
|
208
209
|
)
|
|
209
210
|
LOGICAL_SLOT_SUFFIX = re.compile(
|
|
210
|
-
'\s(?P<key>"?\w+"?)\[(?P<type>[\w\s]+)\]:(?P<value>[\w\'"
|
|
211
|
+
'\s(?P<key>"?\w+"?)\[(?P<type>[\w\s]+)\]:(?P<value>[\w\'"\-\\.\\+]+)'
|
|
211
212
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""PGSync Node class representation."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import re
|
|
@@ -68,6 +69,9 @@ class ForeignKey:
|
|
|
68
69
|
def __str__(self):
|
|
69
70
|
return f"foreign_key: {self.parent}:{self.child}"
|
|
70
71
|
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return self.__str__()
|
|
74
|
+
|
|
71
75
|
|
|
72
76
|
@dataclass
|
|
73
77
|
class Relationship:
|
|
@@ -113,6 +117,9 @@ class Relationship:
|
|
|
113
117
|
def __str__(self):
|
|
114
118
|
return f"relationship: {self.variant}.{self.type}:{self.tables}"
|
|
115
119
|
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
return self.__str__()
|
|
122
|
+
|
|
116
123
|
|
|
117
124
|
@dataclass
|
|
118
125
|
class Node(object):
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""PGSync Plugin."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
5
|
+
import sys
|
|
4
6
|
import typing as t
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
6
8
|
from importlib import import_module
|
|
@@ -42,7 +44,9 @@ class Plugins(object):
|
|
|
42
44
|
self.plugins: list = []
|
|
43
45
|
self._paths: list = []
|
|
44
46
|
logger.debug(f"Reloading plugins from package: {self.package}")
|
|
45
|
-
|
|
47
|
+
# skip in test
|
|
48
|
+
if "test" not in sys.argv[0]:
|
|
49
|
+
self.walk(self.package)
|
|
46
50
|
|
|
47
51
|
def walk(self, package: str) -> None:
|
|
48
52
|
"""Recursively walk the supplied package and fetch all plugins."""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""PGSync QueryBuilder."""
|
|
2
|
+
|
|
2
3
|
import threading
|
|
3
4
|
import typing as t
|
|
4
5
|
from collections import defaultdict
|
|
@@ -20,6 +21,23 @@ class QueryBuilder(threading.local):
|
|
|
20
21
|
self.isouter: bool = True
|
|
21
22
|
self._cache: dict = {}
|
|
22
23
|
|
|
24
|
+
def _eval_expression(
|
|
25
|
+
self, expression: sa.sql.elements.BinaryExpression
|
|
26
|
+
) -> sa.sql.elements.BinaryExpression:
|
|
27
|
+
if isinstance(
|
|
28
|
+
expression.left.type, sa.dialects.postgresql.UUID
|
|
29
|
+
) or isinstance(expression.right.type, sa.dialects.postgresql.UUID):
|
|
30
|
+
if not isinstance(
|
|
31
|
+
expression.left.type, sa.dialects.postgresql.UUID
|
|
32
|
+
) or not isinstance(
|
|
33
|
+
expression.right.type, sa.dialects.postgresql.UUID
|
|
34
|
+
):
|
|
35
|
+
# handle UUID typed expressions:
|
|
36
|
+
# psycopg2.errors.UndefinedFunction: operator does not exist: uuid = integer
|
|
37
|
+
return expression.left is None
|
|
38
|
+
|
|
39
|
+
return expression
|
|
40
|
+
|
|
23
41
|
def _build_filters(
|
|
24
42
|
self, filters: t.Dict[str, t.List[dict]], node: Node
|
|
25
43
|
) -> t.Optional[sa.sql.elements.BooleanClauseList]:
|
|
@@ -45,7 +63,11 @@ class QueryBuilder(threading.local):
|
|
|
45
63
|
for values in filters.get(node.table):
|
|
46
64
|
where: t.List = []
|
|
47
65
|
for column, value in values.items():
|
|
48
|
-
where.append(
|
|
66
|
+
where.append(
|
|
67
|
+
self._eval_expression(
|
|
68
|
+
node.model.c[column] == value
|
|
69
|
+
)
|
|
70
|
+
)
|
|
49
71
|
# and clause is applied for composite primary keys
|
|
50
72
|
clause.append(sa.and_(*where))
|
|
51
73
|
return sa.or_(*clause)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""PGSync SearchClient helper."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import typing as t
|
|
4
5
|
from collections import defaultdict
|
|
@@ -245,7 +246,7 @@ class SearchClient(object):
|
|
|
245
246
|
if "is out of range for a long" not in str(e):
|
|
246
247
|
raise
|
|
247
248
|
|
|
248
|
-
def search(self, index: str, body: dict):
|
|
249
|
+
def search(self, index: str, body: dict) -> t.Any:
|
|
249
250
|
"""
|
|
250
251
|
Search in Elasticsearch/OpenSearch.
|
|
251
252
|
|
|
@@ -259,6 +260,7 @@ class SearchClient(object):
|
|
|
259
260
|
tree: Tree,
|
|
260
261
|
setting: t.Optional[dict] = None,
|
|
261
262
|
mapping: t.Optional[dict] = None,
|
|
263
|
+
mappings: t.Optional[dict] = None,
|
|
262
264
|
routing: t.Optional[str] = None,
|
|
263
265
|
) -> None:
|
|
264
266
|
"""Create Elasticsearch/OpenSearch setting and mapping if required."""
|
|
@@ -267,7 +269,8 @@ class SearchClient(object):
|
|
|
267
269
|
if not self.__client.indices.exists(index=index):
|
|
268
270
|
if setting:
|
|
269
271
|
body.update(**{"settings": {"index": setting}})
|
|
270
|
-
|
|
272
|
+
if mappings:
|
|
273
|
+
body.update(**{"mappings": {"index": mappings}})
|
|
271
274
|
if mapping:
|
|
272
275
|
if "dynamic_templates" in mapping:
|
|
273
276
|
body.update(**{"mappings": mapping})
|
|
@@ -404,9 +407,9 @@ def get_search_client(
|
|
|
404
407
|
# API
|
|
405
408
|
cloud_id: t.Optional[str] = settings.ELASTICSEARCH_CLOUD_ID
|
|
406
409
|
api_key: t.Optional[t.Union[str, t.Tuple[str, str]]] = None
|
|
407
|
-
http_auth: t.Optional[
|
|
408
|
-
|
|
409
|
-
|
|
410
|
+
http_auth: t.Optional[t.Union[str, t.Tuple[str, str]]] = (
|
|
411
|
+
settings.ELASTICSEARCH_HTTP_AUTH
|
|
412
|
+
)
|
|
410
413
|
if (
|
|
411
414
|
settings.ELASTICSEARCH_API_KEY_ID
|
|
412
415
|
and settings.ELASTICSEARCH_API_KEY
|
|
@@ -424,12 +427,12 @@ def get_search_client(
|
|
|
424
427
|
ca_certs: t.Optional[str] = settings.ELASTICSEARCH_CA_CERTS
|
|
425
428
|
client_cert: t.Optional[str] = settings.ELASTICSEARCH_CLIENT_CERT
|
|
426
429
|
client_key: t.Optional[str] = settings.ELASTICSEARCH_CLIENT_KEY
|
|
427
|
-
ssl_assert_hostname: t.Optional[
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
ssl_assert_fingerprint: t.Optional[
|
|
431
|
-
|
|
432
|
-
|
|
430
|
+
ssl_assert_hostname: t.Optional[str] = (
|
|
431
|
+
settings.ELASTICSEARCH_SSL_ASSERT_HOSTNAME
|
|
432
|
+
)
|
|
433
|
+
ssl_assert_fingerprint: t.Optional[str] = (
|
|
434
|
+
settings.ELASTICSEARCH_SSL_ASSERT_FINGERPRINT
|
|
435
|
+
)
|
|
433
436
|
ssl_version: t.Optional[int] = settings.ELASTICSEARCH_SSL_VERSION
|
|
434
437
|
ssl_context: t.Optional[t.Any] = settings.ELASTICSEARCH_SSL_CONTEXT
|
|
435
438
|
ssl_show_warn: bool = settings.ELASTICSEARCH_SSL_SHOW_WARN
|
|
@@ -4,6 +4,7 @@ This module contains the settings for PGSync.
|
|
|
4
4
|
It reads environment variables from a .env file and sets default values for each variable.
|
|
5
5
|
The variables are used to configure various parameters such as block size, checkpoint path, polling interval, etc.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import logging
|
|
8
9
|
import logging.config
|
|
9
10
|
import os
|
|
@@ -148,7 +149,7 @@ elif ELASTICSEARCH:
|
|
|
148
149
|
OPENSEARCH_AWS_HOSTED = env.bool("OPENSEARCH_AWS_HOSTED", default=False)
|
|
149
150
|
OPENSEARCH_AWS_SERVERLESS = env.bool(
|
|
150
151
|
"OPENSEARCH_AWS_SERVERLESS", default=False
|
|
151
|
-
)
|
|
152
|
+
)
|
|
152
153
|
|
|
153
154
|
# Postgres:
|
|
154
155
|
PG_HOST = env.str("PG_HOST", default="localhost")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Sync module."""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
@@ -81,6 +82,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
81
82
|
self.nodes: dict = doc.get("nodes", {})
|
|
82
83
|
self.setting: dict = doc.get("setting")
|
|
83
84
|
self.mapping: dict = doc.get("mapping")
|
|
85
|
+
self.mappings: dict = doc.get("mappings")
|
|
84
86
|
self.routing: str = doc.get("routing")
|
|
85
87
|
super().__init__(
|
|
86
88
|
doc.get("database", self.index), verbose=verbose, **kwargs
|
|
@@ -254,6 +256,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
254
256
|
self.tree,
|
|
255
257
|
setting=self.setting,
|
|
256
258
|
mapping=self.mapping,
|
|
259
|
+
mappings=self.mappings,
|
|
257
260
|
routing=self.routing,
|
|
258
261
|
)
|
|
259
262
|
|
|
@@ -347,6 +350,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
347
350
|
txmin: t.Optional[int] = None,
|
|
348
351
|
txmax: t.Optional[int] = None,
|
|
349
352
|
upto_nchanges: t.Optional[int] = None,
|
|
353
|
+
upto_lsn: t.Optional[str] = None,
|
|
350
354
|
) -> None:
|
|
351
355
|
"""
|
|
352
356
|
Process changes from the db logical replication logs.
|
|
@@ -374,25 +378,15 @@ class Sync(Base, metaclass=Singleton):
|
|
|
374
378
|
# minimize the tmp file disk usage when calling
|
|
375
379
|
# PG_LOGICAL_SLOT_PEEK_CHANGES and PG_LOGICAL_SLOT_GET_CHANGES
|
|
376
380
|
# by limiting to a smaller batch size.
|
|
377
|
-
offset: int = 0
|
|
378
|
-
total: int = 0
|
|
379
|
-
limit: int = settings.LOGICAL_SLOT_CHUNK_SIZE
|
|
380
|
-
count: int = self.logical_slot_count_changes(
|
|
381
|
-
self.__name,
|
|
382
|
-
txmin=txmin,
|
|
383
|
-
txmax=txmax,
|
|
384
|
-
upto_nchanges=upto_nchanges,
|
|
385
|
-
)
|
|
386
381
|
while True:
|
|
387
382
|
changes: int = self.logical_slot_peek_changes(
|
|
388
383
|
self.__name,
|
|
389
384
|
txmin=txmin,
|
|
390
385
|
txmax=txmax,
|
|
391
386
|
upto_nchanges=upto_nchanges,
|
|
392
|
-
|
|
393
|
-
offset=offset,
|
|
387
|
+
upto_lsn=upto_lsn,
|
|
394
388
|
)
|
|
395
|
-
if not changes
|
|
389
|
+
if not changes:
|
|
396
390
|
break
|
|
397
391
|
|
|
398
392
|
rows: list = []
|
|
@@ -447,11 +441,8 @@ class Sync(Base, metaclass=Singleton):
|
|
|
447
441
|
txmin=txmin,
|
|
448
442
|
txmax=txmax,
|
|
449
443
|
upto_nchanges=upto_nchanges,
|
|
450
|
-
|
|
451
|
-
offset=offset,
|
|
444
|
+
upto_lsn=upto_lsn,
|
|
452
445
|
)
|
|
453
|
-
offset += limit
|
|
454
|
-
total += len(changes)
|
|
455
446
|
self.count["xlog"] += len(rows)
|
|
456
447
|
|
|
457
448
|
def _root_primary_key_resolver(
|
|
@@ -1120,7 +1111,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
1120
1111
|
notification: t.AnyStr = conn.notifies.pop(0)
|
|
1121
1112
|
if notification.channel == self.database:
|
|
1122
1113
|
payload = json.loads(notification.payload)
|
|
1123
|
-
if self.index in payload["indices"]:
|
|
1114
|
+
if payload["indices"] and self.index in payload["indices"]:
|
|
1124
1115
|
payloads.append(payload)
|
|
1125
1116
|
logger.debug(f"poll_db: {payload}")
|
|
1126
1117
|
self.count["db"] += 1
|
|
@@ -1142,7 +1133,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
1142
1133
|
notification: t.AnyStr = self.conn.notifies.pop(0)
|
|
1143
1134
|
if notification.channel == self.database:
|
|
1144
1135
|
payload = json.loads(notification.payload)
|
|
1145
|
-
if self.index in payload["indices"]:
|
|
1136
|
+
if payload["indices"] and self.index in payload["indices"]:
|
|
1146
1137
|
self.redis.push([payload])
|
|
1147
1138
|
logger.debug(f"async_poll: {payload}")
|
|
1148
1139
|
self.count["db"] += 1
|
|
@@ -1224,13 +1215,22 @@ class Sync(Base, metaclass=Singleton):
|
|
|
1224
1215
|
"""Pull data from db."""
|
|
1225
1216
|
txmin: int = self.checkpoint
|
|
1226
1217
|
txmax: int = self.txid_current
|
|
1218
|
+
# this is the max lsn we should go upto
|
|
1219
|
+
upto_lsn: str = self.current_wal_lsn
|
|
1220
|
+
upto_nchanges: int = settings.LOGICAL_SLOT_CHUNK_SIZE
|
|
1221
|
+
|
|
1227
1222
|
logger.debug(f"pull txmin: {txmin} - txmax: {txmax}")
|
|
1228
1223
|
# forward pass sync
|
|
1229
1224
|
self.search_client.bulk(
|
|
1230
1225
|
self.index, self.sync(txmin=txmin, txmax=txmax)
|
|
1231
1226
|
)
|
|
1232
1227
|
# now sync up to txmax to capture everything we may have missed
|
|
1233
|
-
self.logical_slot_changes(
|
|
1228
|
+
self.logical_slot_changes(
|
|
1229
|
+
txmin=txmin,
|
|
1230
|
+
txmax=txmax,
|
|
1231
|
+
upto_nchanges=upto_nchanges,
|
|
1232
|
+
upto_lsn=upto_lsn,
|
|
1233
|
+
)
|
|
1234
1234
|
self.checkpoint: int = txmax or self.txid_current
|
|
1235
1235
|
self._truncate = True
|
|
1236
1236
|
|
|
@@ -1267,6 +1267,7 @@ class Sync(Base, metaclass=Singleton):
|
|
|
1267
1267
|
await asyncio.sleep(settings.LOG_INTERVAL)
|
|
1268
1268
|
|
|
1269
1269
|
def _status(self, label: str) -> None:
|
|
1270
|
+
# TODO: indicate if we are processing logical logs or not
|
|
1270
1271
|
sys.stdout.write(
|
|
1271
1272
|
f"{label} {self.database}:{self.index} "
|
|
1272
1273
|
f"Xlog: [{self.count['xlog']:,}] => "
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""PGSync views."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import typing as t
|
|
4
5
|
import warnings
|
|
@@ -457,15 +458,21 @@ def create_view(
|
|
|
457
458
|
[
|
|
458
459
|
(
|
|
459
460
|
table_name,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
461
|
+
(
|
|
462
|
+
array(fields["primary_keys"])
|
|
463
|
+
if fields.get("primary_keys")
|
|
464
|
+
else None
|
|
465
|
+
),
|
|
466
|
+
(
|
|
467
|
+
array(fields.get("foreign_keys"))
|
|
468
|
+
if fields.get("foreign_keys")
|
|
469
|
+
else None
|
|
470
|
+
),
|
|
471
|
+
(
|
|
472
|
+
array(fields.get("indices"))
|
|
473
|
+
if fields.get("indices")
|
|
474
|
+
else None
|
|
475
|
+
),
|
|
469
476
|
)
|
|
470
477
|
for table_name, fields in rows.items()
|
|
471
478
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pgsync
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Postgres to Elasticsearch/OpenSearch sync
|
|
5
5
|
Home-page: https://github.com/toluaina/pgsync
|
|
6
6
|
Author: Tolu Aina
|
|
@@ -13,7 +13,7 @@ Project-URL: Funding, https://github.com/sponsors/toluaina
|
|
|
13
13
|
Project-URL: Source, https://github.com/toluaina/pgsync
|
|
14
14
|
Project-URL: Web, https://pgsync.com
|
|
15
15
|
Project-URL: Documentation, https://pgsync.com
|
|
16
|
-
Keywords:
|
|
16
|
+
Keywords: change data capture,elasticsearch,opensearch,pgsync,postgres
|
|
17
17
|
Classifier: Development Status :: 5 - Production/Stable
|
|
18
18
|
Classifier: Intended Audience :: Developers
|
|
19
19
|
Classifier: Natural Language :: English
|
|
@@ -31,34 +31,35 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
License-File: AUTHORS.rst
|
|
33
33
|
Requires-Dist: async-timeout==4.0.3
|
|
34
|
-
Requires-Dist: boto3==1.34.
|
|
35
|
-
Requires-Dist: botocore==1.34.
|
|
36
|
-
Requires-Dist: certifi==
|
|
34
|
+
Requires-Dist: boto3==1.34.142
|
|
35
|
+
Requires-Dist: botocore==1.34.142
|
|
36
|
+
Requires-Dist: certifi==2024.7.4
|
|
37
37
|
Requires-Dist: charset-normalizer==3.3.2
|
|
38
38
|
Requires-Dist: click==8.1.7
|
|
39
|
-
Requires-Dist: elastic-transport==8.
|
|
40
|
-
Requires-Dist: elasticsearch==8.
|
|
41
|
-
Requires-Dist: elasticsearch-dsl==8.
|
|
42
|
-
Requires-Dist: environs==
|
|
39
|
+
Requires-Dist: elastic-transport==8.13.1
|
|
40
|
+
Requires-Dist: elasticsearch==8.14.0
|
|
41
|
+
Requires-Dist: elasticsearch-dsl==8.14.0
|
|
42
|
+
Requires-Dist: environs==11.0.0
|
|
43
|
+
Requires-Dist: events==0.5
|
|
43
44
|
Requires-Dist: greenlet==3.0.3
|
|
44
|
-
Requires-Dist: idna==3.
|
|
45
|
+
Requires-Dist: idna==3.7
|
|
45
46
|
Requires-Dist: jmespath==1.0.1
|
|
46
|
-
Requires-Dist: marshmallow==3.
|
|
47
|
+
Requires-Dist: marshmallow==3.21.3
|
|
47
48
|
Requires-Dist: opensearch-dsl==2.1.0
|
|
48
|
-
Requires-Dist: opensearch-py==2.
|
|
49
|
-
Requires-Dist: packaging==
|
|
49
|
+
Requires-Dist: opensearch-py==2.6.0
|
|
50
|
+
Requires-Dist: packaging==24.1
|
|
50
51
|
Requires-Dist: psycopg2-binary==2.9.9
|
|
51
|
-
Requires-Dist: python-dateutil==2.
|
|
52
|
-
Requires-Dist: python-dotenv==1.0.
|
|
53
|
-
Requires-Dist: redis==5.0.
|
|
54
|
-
Requires-Dist: requests==2.
|
|
52
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
53
|
+
Requires-Dist: python-dotenv==1.0.1
|
|
54
|
+
Requires-Dist: redis==5.0.7
|
|
55
|
+
Requires-Dist: requests==2.32.3
|
|
55
56
|
Requires-Dist: requests-aws4auth==1.2.3
|
|
56
|
-
Requires-Dist: s3transfer==0.10.
|
|
57
|
+
Requires-Dist: s3transfer==0.10.2
|
|
57
58
|
Requires-Dist: six==1.16.0
|
|
58
|
-
Requires-Dist: sqlalchemy==2.0.
|
|
59
|
-
Requires-Dist: sqlparse==0.
|
|
60
|
-
Requires-Dist: typing-extensions==4.
|
|
61
|
-
Requires-Dist: urllib3==1.26.
|
|
59
|
+
Requires-Dist: sqlalchemy==2.0.31
|
|
60
|
+
Requires-Dist: sqlparse==0.5.0
|
|
61
|
+
Requires-Dist: typing-extensions==4.12.2
|
|
62
|
+
Requires-Dist: urllib3==1.26.19
|
|
62
63
|
|
|
63
64
|
# PostgreSQL to Elasticsearch/OpenSearch sync
|
|
64
65
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
async-timeout==4.0.3
|
|
2
|
+
boto3==1.34.142
|
|
3
|
+
botocore==1.34.142
|
|
4
|
+
certifi==2024.7.4
|
|
5
|
+
charset-normalizer==3.3.2
|
|
6
|
+
click==8.1.7
|
|
7
|
+
elastic-transport==8.13.1
|
|
8
|
+
elasticsearch==8.14.0
|
|
9
|
+
elasticsearch-dsl==8.14.0
|
|
10
|
+
environs==11.0.0
|
|
11
|
+
events==0.5
|
|
12
|
+
greenlet==3.0.3
|
|
13
|
+
idna==3.7
|
|
14
|
+
jmespath==1.0.1
|
|
15
|
+
marshmallow==3.21.3
|
|
16
|
+
opensearch-dsl==2.1.0
|
|
17
|
+
opensearch-py==2.6.0
|
|
18
|
+
packaging==24.1
|
|
19
|
+
psycopg2-binary==2.9.9
|
|
20
|
+
python-dateutil==2.9.0.post0
|
|
21
|
+
python-dotenv==1.0.1
|
|
22
|
+
redis==5.0.7
|
|
23
|
+
requests==2.32.3
|
|
24
|
+
requests-aws4auth==1.2.3
|
|
25
|
+
s3transfer==0.10.2
|
|
26
|
+
six==1.16.0
|
|
27
|
+
sqlalchemy==2.0.31
|
|
28
|
+
sqlparse==0.5.0
|
|
29
|
+
typing-extensions==4.12.2
|
|
30
|
+
urllib3==1.26.19
|
|
@@ -28,11 +28,11 @@ PYTHON_REQUIRES = ">=3.8.0"
|
|
|
28
28
|
VERSION = get_version()
|
|
29
29
|
INSTALL_REQUIRES = []
|
|
30
30
|
KEYWORDS = [
|
|
31
|
-
"
|
|
31
|
+
"change data capture",
|
|
32
32
|
"elasticsearch",
|
|
33
33
|
"opensearch",
|
|
34
|
+
"pgsync",
|
|
34
35
|
"postgres",
|
|
35
|
-
"change data capture",
|
|
36
36
|
]
|
|
37
37
|
CLASSIFIERS = [
|
|
38
38
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -48,11 +48,7 @@ CLASSIFIERS = [
|
|
|
48
48
|
"License :: OSI Approved :: MIT License",
|
|
49
49
|
"Operating System :: OS Independent",
|
|
50
50
|
]
|
|
51
|
-
SCRIPTS = [
|
|
52
|
-
"bin/pgsync",
|
|
53
|
-
"bin/bootstrap",
|
|
54
|
-
"bin/parallel_sync",
|
|
55
|
-
]
|
|
51
|
+
SCRIPTS = ["bin/bootstrap", "bin/parallel_sync", "bin/pgsync"]
|
|
56
52
|
SETUP_REQUIRES = ["pytest-runner"]
|
|
57
53
|
TESTS_REQUIRE = ["pytest"]
|
|
58
54
|
|
|
@@ -462,6 +462,32 @@ class TestBase(object):
|
|
|
462
462
|
pg_base.parse_logical_slot(row)
|
|
463
463
|
assert '"Unknown UNKNOWN operation for row:' in str(excinfo.value)
|
|
464
464
|
|
|
465
|
+
def test_parse_logical_slot_with_double_precision(
|
|
466
|
+
self,
|
|
467
|
+
connection,
|
|
468
|
+
):
|
|
469
|
+
pg_base = Base(connection.engine.url.database)
|
|
470
|
+
row = """
|
|
471
|
+
table public.book: UPDATE: id[integer]:1 isbn[character varying]:'001' title[character varying]:'It' description[character varying]:'Stephens Kings It' copyright[character varying]:null tags[jsonb]:'["a", "b", "c"]' doc[jsonb]:'{"a": {"b": {"c": [0, 1, 2, 3, 4]}}, "i": 73, "x": [{"y": 0, "z": 5}, {"y": 1, "z": 6}], "bool": true, "lastname": "Judye", "firstname": "Glenda", "generation": {"name": "X"}, "nick_names": ["Beatriz", "Jean", "Carilyn", "Carol-Jean", "Sara-Ann"], "coordinates": {"lat": 21.1, "lon": 32.9}}' publisher_id[integer]:1 publish_date[timestamp without time zone]:'1980-01-01 00:00:00' quad[double precision]:2e+58
|
|
472
|
+
""" # noqa E501
|
|
473
|
+
payload = pg_base.parse_logical_slot(row)
|
|
474
|
+
assert payload.data == {
|
|
475
|
+
"id": 1,
|
|
476
|
+
"isbn": "001",
|
|
477
|
+
"title": "It",
|
|
478
|
+
"description": "Stephens",
|
|
479
|
+
"copyright": None,
|
|
480
|
+
"tags": "'",
|
|
481
|
+
"doc": "'",
|
|
482
|
+
"publisher_id": 1,
|
|
483
|
+
"publish_date": "'1980-01-01",
|
|
484
|
+
"quad": 2e58,
|
|
485
|
+
}
|
|
486
|
+
assert payload.old == {}
|
|
487
|
+
assert payload.schema == "public"
|
|
488
|
+
assert payload.table == "book"
|
|
489
|
+
assert payload.tg_op == "UPDATE"
|
|
490
|
+
|
|
465
491
|
def test_fetchone(self, connection):
|
|
466
492
|
pg_base = Base(connection.engine.url.database, verbose=True)
|
|
467
493
|
with patch("pgsync.base.compiled_query") as mock_compiled_query:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Constants tests."""
|
|
2
|
+
|
|
2
3
|
from pgsync.constants import (
|
|
3
4
|
ELASTICSEARCH_MAPPING_PARAMETERS,
|
|
4
5
|
ELASTICSEARCH_TYPES,
|
|
@@ -98,6 +99,7 @@ class TestConstants(object):
|
|
|
98
99
|
"constant_keyword",
|
|
99
100
|
"date",
|
|
100
101
|
"double",
|
|
102
|
+
"dense_vector",
|
|
101
103
|
"float",
|
|
102
104
|
"geo_point",
|
|
103
105
|
"geo_shape",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Sync tests."""
|
|
2
|
+
|
|
2
3
|
import importlib
|
|
3
4
|
import os
|
|
4
5
|
import typing as t
|
|
@@ -81,8 +82,7 @@ class TestSync(object):
|
|
|
81
82
|
txmin=None,
|
|
82
83
|
txmax=None,
|
|
83
84
|
upto_nchanges=None,
|
|
84
|
-
|
|
85
|
-
offset=0,
|
|
85
|
+
upto_lsn=None,
|
|
86
86
|
)
|
|
87
87
|
mock_sync.assert_not_called()
|
|
88
88
|
|
|
@@ -98,8 +98,7 @@ class TestSync(object):
|
|
|
98
98
|
txmin=None,
|
|
99
99
|
txmax=None,
|
|
100
100
|
upto_nchanges=None,
|
|
101
|
-
|
|
102
|
-
offset=0,
|
|
101
|
+
upto_lsn=None,
|
|
103
102
|
)
|
|
104
103
|
mock_sync.assert_not_called()
|
|
105
104
|
|
|
@@ -127,8 +126,7 @@ class TestSync(object):
|
|
|
127
126
|
txmin=None,
|
|
128
127
|
txmax=None,
|
|
129
128
|
upto_nchanges=None,
|
|
130
|
-
|
|
131
|
-
offset=0,
|
|
129
|
+
upto_lsn=None,
|
|
132
130
|
)
|
|
133
131
|
mock_get.assert_called_once()
|
|
134
132
|
mock_sync.assert_called_once()
|
|
@@ -355,7 +353,10 @@ class TestSync(object):
|
|
|
355
353
|
txmin = None
|
|
356
354
|
txmax = sync.txid_current - 1
|
|
357
355
|
mock_get.assert_called_once_with(
|
|
358
|
-
txmin=txmin,
|
|
356
|
+
txmin=txmin,
|
|
357
|
+
txmax=txmax,
|
|
358
|
+
upto_nchanges=settings.LOGICAL_SLOT_CHUNK_SIZE,
|
|
359
|
+
upto_lsn=ANY,
|
|
359
360
|
)
|
|
360
361
|
mock_logger.debug.assert_called_once_with(
|
|
361
362
|
f"pull txmin: {txmin} - txmax: {txmax}"
|
|
@@ -762,7 +763,12 @@ class TestSync(object):
|
|
|
762
763
|
def test_create_setting(self, mock_es, sync):
|
|
763
764
|
sync.create_setting()
|
|
764
765
|
mock_es.assert_called_once_with(
|
|
765
|
-
"testdb",
|
|
766
|
+
"testdb",
|
|
767
|
+
ANY,
|
|
768
|
+
setting=None,
|
|
769
|
+
mapping=None,
|
|
770
|
+
mappings=None,
|
|
771
|
+
routing=None,
|
|
766
772
|
)
|
|
767
773
|
|
|
768
774
|
@patch("pgsync.sync.Sync.teardown")
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
async-timeout==4.0.3
|
|
2
|
-
boto3==1.34.11
|
|
3
|
-
botocore==1.34.11
|
|
4
|
-
certifi==2023.11.17
|
|
5
|
-
charset-normalizer==3.3.2
|
|
6
|
-
click==8.1.7
|
|
7
|
-
elastic-transport==8.11.0
|
|
8
|
-
elasticsearch==8.11.1
|
|
9
|
-
elasticsearch-dsl==8.11.0
|
|
10
|
-
environs==10.0.0
|
|
11
|
-
greenlet==3.0.3
|
|
12
|
-
idna==3.6
|
|
13
|
-
jmespath==1.0.1
|
|
14
|
-
marshmallow==3.20.1
|
|
15
|
-
opensearch-dsl==2.1.0
|
|
16
|
-
opensearch-py==2.4.2
|
|
17
|
-
packaging==23.2
|
|
18
|
-
psycopg2-binary==2.9.9
|
|
19
|
-
python-dateutil==2.8.2
|
|
20
|
-
python-dotenv==1.0.0
|
|
21
|
-
redis==5.0.1
|
|
22
|
-
requests==2.31.0
|
|
23
|
-
requests-aws4auth==1.2.3
|
|
24
|
-
s3transfer==0.10.0
|
|
25
|
-
six==1.16.0
|
|
26
|
-
sqlalchemy==2.0.25
|
|
27
|
-
sqlparse==0.4.4
|
|
28
|
-
typing-extensions==4.9.0
|
|
29
|
-
urllib3==1.26.18
|
|
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
|
|
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
|
|
File without changes
|