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.
@@ -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
+ )