2121from dlt .sources .sql_database import sql_table
2222from html2text import html2text
2323import pyarrow as pa
24- from sqlalchemy . sql import Select
24+ import sqlalchemy as sa
2525
2626import 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 )
2929SQL_TABLE_KWARGS = dict (
3030 schema = dlt .config .value ,
3131 backend = "pyarrow" ,
3636EXTRACTED_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
7790def 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+
95142def 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:
123170def 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