feather-cdc-postgres 0.1.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.
- feather_cdc_postgres/__init__.py +6 -0
- feather_cdc_postgres/__main__.py +9 -0
- feather_cdc_postgres/adapters/__init__.py +44 -0
- feather_cdc_postgres/adapters/featherbase.py +126 -0
- feather_cdc_postgres/cli.py +247 -0
- feather_cdc_postgres/destination.py +640 -0
- feather_cdc_postgres/discovery.py +216 -0
- feather_cdc_postgres/drift.py +207 -0
- feather_cdc_postgres/generate_dbt.py +106 -0
- feather_cdc_postgres/lifecycle.py +79 -0
- feather_cdc_postgres/manifest.py +587 -0
- feather_cdc_postgres/ownership.py +103 -0
- feather_cdc_postgres/publication.py +157 -0
- feather_cdc_postgres/runner.py +1399 -0
- feather_cdc_postgres/schema_registry.py +283 -0
- feather_cdc_postgres/selectors.py +347 -0
- feather_cdc_postgres/snapshot.py +448 -0
- feather_cdc_postgres/stream.py +162 -0
- feather_cdc_postgres/telemetry.py +172 -0
- feather_cdc_postgres-0.1.0.dist-info/METADATA +239 -0
- feather_cdc_postgres-0.1.0.dist-info/RECORD +30 -0
- feather_cdc_postgres-0.1.0.dist-info/WHEEL +4 -0
- feather_cdc_postgres-0.1.0.dist-info/entry_points.txt +2 -0
- pg_replication/LICENSE +201 -0
- pg_replication/README.md +100 -0
- pg_replication/__init__.py +106 -0
- pg_replication/decoders.py +428 -0
- pg_replication/exceptions.py +6 -0
- pg_replication/helpers.py +1405 -0
- pg_replication/schema_types.py +156 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
from dlt.common import Decimal, logger
|
|
6
|
+
from dlt.common.data_types.typing import TDataType
|
|
7
|
+
from dlt.common.data_types.type_helpers import coerce_value
|
|
8
|
+
from dlt.common.schema.typing import TColumnSchema, TColumnType
|
|
9
|
+
|
|
10
|
+
from dlt.destinations.impl.postgres.factory import PostgresTypeMapper
|
|
11
|
+
|
|
12
|
+
from .decoders import ColumnType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_DUMMY_VALS: Dict[TDataType, Any] = {
|
|
16
|
+
"bigint": 0,
|
|
17
|
+
"binary": b" ",
|
|
18
|
+
"bool": True,
|
|
19
|
+
"json": [0],
|
|
20
|
+
"date": "2000-01-01",
|
|
21
|
+
"decimal": Decimal(0),
|
|
22
|
+
"double": 0.0,
|
|
23
|
+
"text": "",
|
|
24
|
+
"time": "00:00:00",
|
|
25
|
+
"timestamp": "2000-01-01T00:00:00",
|
|
26
|
+
"wei": 0,
|
|
27
|
+
}
|
|
28
|
+
"""Dummy values used to replace NULLs in NOT NULL columns in key-only delete records."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_PG_TYPES: Dict[int, str] = {
|
|
32
|
+
16: "boolean",
|
|
33
|
+
17: "bytea",
|
|
34
|
+
19: "text", # name
|
|
35
|
+
20: "bigint",
|
|
36
|
+
21: "smallint",
|
|
37
|
+
23: "integer",
|
|
38
|
+
25: "text",
|
|
39
|
+
26: "bigint", # oid
|
|
40
|
+
114: "json",
|
|
41
|
+
700: "double precision", # float4 widened: PostgresTypeMapper maps "real" to TEXT
|
|
42
|
+
701: "double precision",
|
|
43
|
+
1042: "character", # bpchar
|
|
44
|
+
1043: "character varying",
|
|
45
|
+
1082: "date",
|
|
46
|
+
1083: "time without time zone",
|
|
47
|
+
1114: "timestamp without time zone",
|
|
48
|
+
1184: "timestamp with time zone",
|
|
49
|
+
1186: "text", # interval — lossy, kept as its text rendering
|
|
50
|
+
1266: "text", # timetz — lossy, kept as its text rendering
|
|
51
|
+
1700: "numeric",
|
|
52
|
+
2950: "text", # uuid — exact as text
|
|
53
|
+
3802: "jsonb",
|
|
54
|
+
}
|
|
55
|
+
"""Maps postgres type OID to type string. Only includes types present in PostgresTypeMapper."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_precision(type_id: int, atttypmod: int) -> Optional[int]:
|
|
59
|
+
"""Get precision from postgres type attributes."""
|
|
60
|
+
# https://stackoverflow.com/a/3351120
|
|
61
|
+
if type_id == 21: # smallint
|
|
62
|
+
return 16
|
|
63
|
+
elif type_id == 23: # integer
|
|
64
|
+
return 32
|
|
65
|
+
elif type_id == 20: # bigint
|
|
66
|
+
return 64
|
|
67
|
+
if atttypmod != -1:
|
|
68
|
+
if type_id == 1700: # numeric
|
|
69
|
+
return ((atttypmod - 4) >> 16) & 65535
|
|
70
|
+
elif type_id in (
|
|
71
|
+
1083,
|
|
72
|
+
1184,
|
|
73
|
+
): # time without time zone, timestamp with time zone
|
|
74
|
+
return atttypmod
|
|
75
|
+
elif type_id == 1043: # character varying
|
|
76
|
+
return atttypmod - 4
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_scale(type_id: int, atttypmod: int) -> Optional[int]:
|
|
81
|
+
"""Get scale from postgres type attributes."""
|
|
82
|
+
# https://stackoverflow.com/a/3351120
|
|
83
|
+
if atttypmod != -1:
|
|
84
|
+
if type_id in (21, 23, 20): # smallint, integer, bigint
|
|
85
|
+
return 0
|
|
86
|
+
if type_id == 1700: # numeric
|
|
87
|
+
return (atttypmod - 4) & 65535
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=None)
|
|
92
|
+
def _type_mapper() -> PostgresTypeMapper:
|
|
93
|
+
from dlt.destinations import postgres
|
|
94
|
+
|
|
95
|
+
return PostgresTypeMapper(postgres().capabilities())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _to_dlt_column_type(type_id: int, atttypmod: int) -> TColumnType:
|
|
99
|
+
"""Converts postgres type OID to dlt column type.
|
|
100
|
+
|
|
101
|
+
Type OIDs not in _PG_TYPES mapping default to "text" type.
|
|
102
|
+
"""
|
|
103
|
+
pg_type = _PG_TYPES.get(type_id)
|
|
104
|
+
if pg_type is None:
|
|
105
|
+
logger.warning(
|
|
106
|
+
f"Type OID {type_id} is unknown and will be converted to `text` dlt type"
|
|
107
|
+
)
|
|
108
|
+
pg_type = "character varying"
|
|
109
|
+
precision = _get_precision(type_id, atttypmod)
|
|
110
|
+
scale = _get_scale(type_id, atttypmod)
|
|
111
|
+
dlt_type = _type_mapper().from_destination_type(pg_type, precision, scale)
|
|
112
|
+
|
|
113
|
+
# Integers are pinned to plain "bigint" (no precision). dlt renders precision 32 as
|
|
114
|
+
# INTEGER and precision 64 as BIGINT on duckdb, while the snapshot path reflects every
|
|
115
|
+
# integer as BIGINT — bronze_featherbase.public.comment.position is BIGINT and its
|
|
116
|
+
# staging twin INTEGER for exactly this reason (#3079). Widening is the safe direction.
|
|
117
|
+
if type_id in (21, 23, 20, 26):
|
|
118
|
+
dlt_type.pop("precision", None)
|
|
119
|
+
dlt_type.pop("scale", None)
|
|
120
|
+
|
|
121
|
+
# Set timezone flag for timestamp types
|
|
122
|
+
if type_id == 1114: # timestamp without time zone
|
|
123
|
+
dlt_type["timezone"] = False
|
|
124
|
+
elif type_id == 1184: # timestamp with time zone
|
|
125
|
+
dlt_type["timezone"] = True
|
|
126
|
+
return dlt_type
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _to_dlt_column_schema(col: ColumnType) -> TColumnSchema:
|
|
130
|
+
"""Converts pypgoutput ColumnType to dlt column schema."""
|
|
131
|
+
dlt_column_type = _to_dlt_column_type(col.type_id, col.atttypmod)
|
|
132
|
+
partial_column_schema = {
|
|
133
|
+
"name": col.name,
|
|
134
|
+
"primary_key": bool(col.part_of_pkey),
|
|
135
|
+
}
|
|
136
|
+
return {**dlt_column_type, **partial_column_schema} # type: ignore[typeddict-item]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _to_dlt_val(val: str, data_type: TDataType, byte1: str, for_delete: bool) -> Any:
|
|
140
|
+
"""Converts pgoutput's text-formatted value into dlt-compatible data value."""
|
|
141
|
+
if byte1 == "n":
|
|
142
|
+
if for_delete:
|
|
143
|
+
# replace None with dummy value to prevent NOT NULL violations in staging table
|
|
144
|
+
return _DUMMY_VALS[data_type]
|
|
145
|
+
return None
|
|
146
|
+
elif byte1 == "t":
|
|
147
|
+
if data_type == "binary":
|
|
148
|
+
# https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT
|
|
149
|
+
return bytes.fromhex(val.replace("\\x", ""))
|
|
150
|
+
elif data_type == "json":
|
|
151
|
+
return json.loads(val)
|
|
152
|
+
return coerce_value(data_type, "text", val)
|
|
153
|
+
else:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Byte1 in replication message must be 'n' or 't', not '{byte1}'."
|
|
156
|
+
)
|