Skip to content

Commit 1978240

Browse files
committed
Upgrade dlt to 1.20
Datetime handling has been overhauled and as a result databases such as mssql now properly reflect tz-naive columns. We add in a mapping for the tz-naive column to the Iceberg type.
1 parent 0a56faf commit 1978240

4 files changed

Lines changed: 72 additions & 24 deletions

File tree

elt-common/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ version = "0.1.0"
88
description = "A set of common utility routines for data pipelines."
99
readme = "README.md"
1010
requires-python = ">=3.13"
11-
dependencies = ["dlt[parquet,s3]~=1.15.0", "pyiceberg~=0.9.1"]
11+
dependencies = [
12+
"dlt[parquet,s3]~=1.20.0",
13+
"pyiceberg~=0.9.1",
14+
]
1215

1316

1417
[project.optional-dependencies]

elt-common/src/elt_common/dlt_destinations/pyiceberg/helpers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
PrimitiveType,
3030
StringType,
3131
TimeType,
32+
TimestampType,
3233
TimestamptzType,
3334
)
3435
from pyiceberg.typedef import Identifier
@@ -107,7 +108,10 @@ def dlt_type_to_iceberg(column: TColumnType) -> PrimitiveType:
107108
raise TypeError(
108109
f"Iceberg v1 & v2 does not support timestamps in '{TIMESTAMP_PRECISION_TO_UNIT[9]}' precision." # type:ignore
109110
)
110-
return TimestamptzType()
111+
if column.get("timezone", True):
112+
return TimestamptzType()
113+
else:
114+
return TimestampType()
111115
elif dlt_type == "binary":
112116
return BinaryType()
113117
else:

elt-common/uv.lock

Lines changed: 9 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

warehouses/accelerator/extract_load/opralogweb/extract_and_load.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
from dlt.sources.sql_database import sql_table
2222
from html2text import html2text
2323
import pyarrow as pa
24-
from sqlalchemy.sql import Select
24+
import sqlalchemy as sa
2525

2626
import elt_common.cli as cli_utils
2727

28-
OPRALOG_EPOCH = dt.datetime(2017, 4, 25, 0, 0, 0, tzinfo=dt.UTC)
28+
OPRALOG_EPOCH = dt.datetime(2017, 4, 25, 0, 0, 0)
2929
SQL_TABLE_KWARGS = dict(
3030
schema=dlt.config.value,
3131
backend="pyarrow",
@@ -36,8 +36,19 @@
3636
EXTRACTED_ENTRY_IDS: List[int] = []
3737

3838

39+
def with_resource_limit(
40+
resource: DltResource, limit_max_items: int | None = None
41+
) -> DltResource:
42+
if limit_max_items is not None:
43+
resource.add_limit(limit_max_items)
44+
45+
return resource
46+
47+
3948
@dlt.source()
40-
def opralogwebdb(chunk_size: int = 50000) -> Generator[DltResource]:
49+
def opralogwebdb(
50+
chunk_size: int = 50000, limit_max_items: int | None = None
51+
) -> Generator[DltResource]:
4152
"""Opralog usage began in 04/2017. We split tables into two categories:
4253
4354
- append-only tables: previous records are never updated, use 'append' write_disposition
@@ -48,6 +59,8 @@ def opralogwebdb(chunk_size: int = 50000) -> Generator[DltResource]:
4859
were updated. We use the 'LastChangedDate' column of 'Entries' to find the list of
4960
new or updated EntryId values and load the MoreEntryColumn records for these Entries
5061
into the destination.
62+
63+
`chunk_size` and `limit_max_items` are primarily used for testing and debugging
5164
"""
5265

5366
tables_append_records = {
@@ -65,19 +78,20 @@ def opralogwebdb(chunk_size: int = 50000) -> Generator[DltResource]:
6578
chunk_size=chunk_size,
6679
**SQL_TABLE_KWARGS,
6780
)
68-
yield resource
81+
yield with_resource_limit(resource, limit_max_items)
6982

7083
# Now the Entries table, with incremental cursor, that tells us what EntryIds have been updated
71-
yield entries_table(chunk_size)
84+
yield with_resource_limit(entries_table(chunk_size), limit_max_items)
7285

7386
# Finally the MoreEntryColumns table based on the loaded EntryIds
74-
yield more_entry_columns_table(chunk_size)
87+
yield with_resource_limit(more_entry_columns_table(chunk_size), limit_max_items)
7588

7689

7790
def entries_table(chunk_size: int) -> DltResource:
7891
"""Return a resource wrapper for the Entries table"""
7992
resource = sql_table(
8093
table="Entries",
94+
# query_adapter_callback=entries_query_adapter_callback,
8195
incremental=dlt.sources.incremental(
8296
"LastChangedDate",
8397
initial_value=OPRALOG_EPOCH,
@@ -92,6 +106,39 @@ def entries_table(chunk_size: int) -> DltResource:
92106
)
93107

94108

109+
def entries_query_adapter_callback(
110+
query, table, incremental=None, _engine=None
111+
) -> sa.Select:
112+
"""Generate a SQLAlchemy text clause compatible with SQL Server datetime columns.
113+
114+
Ensures timezone-aware datetimes from DLT are cast/converted safely.
115+
"""
116+
if not incremental:
117+
return query
118+
119+
# Handle incremental extraction
120+
select_clause = table.select()
121+
cursor_col = incremental.cursor_path
122+
start_value = incremental.start_value
123+
124+
# DLT may store UTC-aware datetime, which SQL Server datetime cannot handle
125+
if isinstance(start_value, dt.datetime) and start_value.tzinfo is not None:
126+
start_value = start_value.replace(tzinfo=None)
127+
128+
# truncate to milliseconds (3 digits)
129+
if isinstance(start_value, dt.datetime):
130+
start_value = start_value.replace(
131+
microsecond=(start_value.microsecond // 1000) * 1000
132+
)
133+
formatted = start_value.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
134+
else:
135+
formatted = start_value
136+
137+
# Construct WHERE clause with an explicit CAST for safety
138+
where_clause = sa.text(f"{cursor_col} >= CAST('{formatted}' AS datetime)")
139+
return select_clause.where(where_clause)
140+
141+
95142
def additional_comment_to_markdown(table: pa.Table) -> pa.Table:
96143
"""Transform the AdditionalComment column to markdown"""
97144
column_name = "AdditionalComment"
@@ -123,7 +170,7 @@ def store_extracted_entry_ids(table: pa.Table) -> pa.Table:
123170
def more_entry_columns_table(chunk_size: int) -> DltResource:
124171
"""Return a resource wrapper for the MoreEntryColumns table"""
125172

126-
def more_entry_columns_query(query: Select, table):
173+
def more_entry_columns_query(query: sa.Select, table):
127174
return query.filter(table.c.EntryId.in_(EXTRACTED_ENTRY_IDS))
128175

129176
resource = sql_table(

0 commit comments

Comments
 (0)