sqlServerConnector 0.1.5__tar.gz → 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlServerConnector
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A custom SQL Server Connector for ETL processes with Pandas
5
5
  Author-email: Nguyen Minh Son <nguyen.minhson1511@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/johnnyb1509/sqlServerConnector
@@ -20,6 +20,9 @@ Requires-Dist: jupyterlab
20
20
  # SQL Server Connector
21
21
 
22
22
  Thư viện kết nối SQL Server chuyên dụng cho các tác vụ ETL, được tối ưu hóa cho **Pandas**, hỗ trợ **Tiếng Việt (Unicode)** và **Upsert (Merge)** hiệu năng cao.
23
+ ## Update 0.1.7
24
+ > Sửa lỗi nhỏ liên quan đến việc upsert với các bảng có cột chứa Tiếng Việt
25
+
23
26
 
24
27
  ## 🚀 Tính năng nổi bật
25
28
 
@@ -128,3 +131,9 @@ db.dispose()
128
131
  1. **Primary Key:** Khi dùng upsert_data, bắt buộc phải cung cấp primary_key. Nếu bảng chưa có Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
129
132
 
130
133
  2. **Date Time:** Các cột ngày tháng nên được convert sang datetime64[ns] trong Pandas trước khi đẩy vào để đảm bảo tính chính xác.
134
+
135
+ 3. **Upgrade version:** Luôn kiểm tra và cập nhật lên phiên bản mới nhất để tận dụng các tính năng và sửa lỗi mới nhất. For developer, change version in `pyproject.toml` and build & upload to PyPI:
136
+ ```bash
137
+ python -m build
138
+ python -m twine upload dist/*
139
+ ```
@@ -1,6 +1,9 @@
1
1
  # SQL Server Connector
2
2
 
3
3
  Thư viện kết nối SQL Server chuyên dụng cho các tác vụ ETL, được tối ưu hóa cho **Pandas**, hỗ trợ **Tiếng Việt (Unicode)** và **Upsert (Merge)** hiệu năng cao.
4
+ ## Update 0.1.7
5
+ > Sửa lỗi nhỏ liên quan đến việc upsert với các bảng có cột chứa Tiếng Việt
6
+
4
7
 
5
8
  ## 🚀 Tính năng nổi bật
6
9
 
@@ -109,3 +112,9 @@ db.dispose()
109
112
  1. **Primary Key:** Khi dùng upsert_data, bắt buộc phải cung cấp primary_key. Nếu bảng chưa có Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
110
113
 
111
114
  2. **Date Time:** Các cột ngày tháng nên được convert sang datetime64[ns] trong Pandas trước khi đẩy vào để đảm bảo tính chính xác.
115
+
116
+ 3. **Upgrade version:** Luôn kiểm tra và cập nhật lên phiên bản mới nhất để tận dụng các tính năng và sửa lỗi mới nhất. For developer, change version in `pyproject.toml` and build & upload to PyPI:
117
+ ```bash
118
+ python -m build
119
+ python -m twine upload dist/*
120
+ ```
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "sqlServerConnector"
9
- version = "0.1.5"
9
+ version = "0.1.7"
10
10
  description = "A custom SQL Server Connector for ETL processes with Pandas"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.8"
@@ -0,0 +1,232 @@
1
+ import os
2
+ import pandas as pd
3
+ import numpy as np
4
+ import uuid
5
+ import sqlalchemy
6
+ from typing import List, Optional, Dict, Union, Any, Literal
7
+ from loguru import logger
8
+ from sqlalchemy import create_engine, text, URL, inspect
9
+ from sqlalchemy.types import NVARCHAR, FLOAT, INTEGER, DATE, DATETIME, BIGINT
10
+
11
+ class SQLServerConnector:
12
+ """
13
+ Trình kết nối SQL Server chuẩn hóa (Full Features).
14
+ Tích hợp:
15
+ - Fast Executemany (Tốc độ cao).
16
+ - Unicode Support (NVARCHAR).
17
+ - Upsert Strategy (Last/Sum/Skip).
18
+ - Schema Evolution (Tự động thêm cột).
19
+ """
20
+
21
+ def __init__(self, server: str, database: str, username: str, password: str, driver: str = 'ODBC Driver 17 for SQL Server', **kwargs):
22
+ self.server = server
23
+ self.database = database
24
+ self.username = username
25
+ self.password = password
26
+ self.driver = driver
27
+
28
+ # Tạo URL kết nối
29
+ self.connection_url = URL.create(
30
+ "mssql+pyodbc",
31
+ query={
32
+ "odbc_connect": (
33
+ f"DRIVER={self.driver};"
34
+ f"SERVER={self.server};"
35
+ f"DATABASE={self.database};"
36
+ f"UID={self.username};"
37
+ f"PWD={self.password};"
38
+ "Encrypt=no;TrustServerCertificate=yes;"
39
+ )
40
+ }
41
+ )
42
+
43
+ # Engine với fast_executemany=True
44
+ self.engine = create_engine(
45
+ self.connection_url,
46
+ fast_executemany=True,
47
+ pool_pre_ping=True
48
+ )
49
+
50
+ def get_data(self, query: str, params: Optional[Dict] = None) -> pd.DataFrame:
51
+ """Thực thi SELECT và trả về DataFrame"""
52
+ try:
53
+ with self.engine.connect() as conn:
54
+ return pd.read_sql(text(query), conn, params=params)
55
+ except Exception as e:
56
+ logger.error(f"Get data error: {e}")
57
+ raise e
58
+
59
+ def execute_query(self, query: str, params: Optional[Dict] = None):
60
+ """Thực thi lệnh không trả về dữ liệu (UPDATE, DELETE, etc.)"""
61
+ try:
62
+ with self.engine.begin() as conn:
63
+ conn.execute(text(query), params or {})
64
+ except Exception as e:
65
+ logger.error(f"Execute query error: {e}")
66
+ raise e
67
+
68
+ def _generate_dtype_mapping(self, df: pd.DataFrame) -> Dict:
69
+ """Tự động map kiểu dữ liệu (NVARCHAR cho string)"""
70
+ dtype_map = {}
71
+ for col in df.columns:
72
+ if df[col].dtype == 'object' or pd.api.types.is_string_dtype(df[col]):
73
+ dtype_map[col] = NVARCHAR(length=None)
74
+ elif pd.api.types.is_datetime64_any_dtype(df[col]):
75
+ dtype_map[col] = DATETIME()
76
+ elif pd.api.types.is_float_dtype(df[col]):
77
+ dtype_map[col] = FLOAT()
78
+ elif pd.api.types.is_integer_dtype(df[col]):
79
+ dtype_map[col] = BIGINT()
80
+ return dtype_map
81
+
82
+ def _get_table_columns(self, table_name: str, conn) -> List[str]:
83
+ """Lấy danh sách cột hiện có trong DB"""
84
+ inspector = inspect(conn)
85
+ columns = [col['name'] for col in inspector.get_columns(table_name)]
86
+ return columns
87
+
88
+ def _add_missing_columns(self, table_name: str, missing_cols: List[str], dtype_map: Dict, conn):
89
+ """Alter table để thêm cột thiếu (Schema Evolution)"""
90
+ for col in missing_cols:
91
+ col_type = dtype_map.get(col, NVARCHAR(255))
92
+ # SQLAlchemy type to string conversion logic simplified
93
+ type_str = "NVARCHAR(MAX)" # Default safe fallback
94
+ if isinstance(col_type, FLOAT): type_str = "FLOAT"
95
+ elif isinstance(col_type, BIGINT): type_str = "BIGINT"
96
+ elif isinstance(col_type, DATETIME): type_str = "DATETIME"
97
+ elif isinstance(col_type, DATE): type_str = "DATE"
98
+
99
+ alter_sql = f"ALTER TABLE [{table_name}] ADD [{col}] {type_str}"
100
+ conn.execute(text(alter_sql))
101
+ logger.info(f"Auto-evolve: Added column '{col}' to table '{table_name}'")
102
+
103
+ def upsert_data(self,
104
+ df: pd.DataFrame,
105
+ target_table: str,
106
+ match_columns: List[str],
107
+ conflict_strategy: Literal['last', 'skip'] = 'last',
108
+ auto_evolve_schema: bool = False):
109
+ """
110
+ Hàm Upsert mạnh mẽ (Khôi phục đầy đủ tính năng cũ).
111
+
112
+ Args:
113
+ df: DataFrame cần upload.
114
+ target_table: Tên bảng đích.
115
+ match_columns: Danh sách cột dùng làm Key so khớp (Primary Key).
116
+ conflict_strategy:
117
+ - 'last': Update ghi đè dữ liệu mới vào dòng cũ (Default).
118
+ - 'skip': Nếu trùng key thì bỏ qua, không update.
119
+ auto_evolve_schema:
120
+ - True: Tự động thêm cột vào DB nếu DF có cột mới.
121
+ - False: Bỏ qua các cột trong DF mà DB không có (Strict Schema).
122
+ """
123
+ if df.empty:
124
+ logger.warning(f"DataFrame for {target_table} is empty. Skip.")
125
+ return
126
+
127
+ # 1. Map Unicode Types
128
+ dtype_mapping = self._generate_dtype_mapping(df)
129
+
130
+ # 2. Staging Table Name
131
+ staging_table = f"##Staging_{uuid.uuid4().hex[:8]}"
132
+
133
+ try:
134
+ with self.engine.begin() as conn:
135
+ # --- A. Kiểm tra Schema & Table ---
136
+ inspector = inspect(conn)
137
+ if not inspector.has_table(target_table):
138
+ logger.info(f"Table {target_table} not found. Creating new...")
139
+ df.to_sql(target_table, conn, index=False, dtype=dtype_mapping)
140
+ # Tạo Primary Key nếu cần
141
+ if match_columns:
142
+ pk_str = ", ".join([f"[{c}]" for c in match_columns])
143
+ try:
144
+ conn.execute(text(f"ALTER TABLE [{target_table}] ADD CONSTRAINT PK_{target_table.replace('.','_')}_{uuid.uuid4().hex[:4]} PRIMARY KEY ({pk_str})"))
145
+ except Exception as e:
146
+ logger.warning(f"Could not create PK: {e}")
147
+ return
148
+
149
+ # --- B. Xử lý Schema Evolution ---
150
+ db_cols = self._get_table_columns(target_table, conn)
151
+ df_cols = list(df.columns)
152
+
153
+ # Tìm cột có trong DF mà không có trong DB
154
+ new_cols = [c for c in df_cols if c not in db_cols]
155
+
156
+ if new_cols:
157
+ if auto_evolve_schema:
158
+ self._add_missing_columns(target_table, new_cols, dtype_mapping, conn)
159
+ db_cols.extend(new_cols) # Update danh sách cột DB
160
+ else:
161
+ # Nếu không auto evolve, chỉ giữ lại các cột khớp với DB
162
+ valid_cols = [c for c in df_cols if c in db_cols]
163
+ if len(valid_cols) < len(df_cols):
164
+ logger.warning(f"Schema strict: Dropping columns {new_cols} because they are not in DB.")
165
+ df = df[valid_cols]
166
+
167
+ # --- C. Đẩy vào Staging (Fast Executemany) ---
168
+ df.to_sql(
169
+ name=staging_table,
170
+ con=conn,
171
+ if_exists='replace',
172
+ index=False,
173
+ dtype=dtype_mapping
174
+ )
175
+
176
+ # --- D. Thực hiện MERGE ---
177
+ # Chỉ lấy các cột chung giữa DF và DB để Merge (tránh lỗi cột không tồn tại)
178
+ common_cols = [c for c in df.columns if c in db_cols]
179
+
180
+ on_clause = " AND ".join([f"Target.[{col}] = Source.[{col}]" for col in match_columns])
181
+
182
+ # Logic Insert
183
+ insert_cols = ", ".join([f"[{col}]" for col in common_cols])
184
+ insert_vals = ", ".join([f"Source.[{col}]" for col in common_cols])
185
+
186
+ # Logic Update
187
+ merge_sql = ""
188
+
189
+ # Trường hợp 1: Update ('last')
190
+ if conflict_strategy == 'last':
191
+ update_cols = [c for c in common_cols if c not in match_columns]
192
+ if update_cols:
193
+ update_set = ", ".join([f"Target.[{col}] = Source.[{col}]" for col in update_cols])
194
+ merge_sql = f"""
195
+ MERGE [{target_table}] AS Target
196
+ USING {staging_table} AS Source
197
+ ON {on_clause}
198
+ WHEN MATCHED THEN
199
+ UPDATE SET {update_set}
200
+ WHEN NOT MATCHED BY TARGET THEN
201
+ INSERT ({insert_cols}) VALUES ({insert_vals});
202
+ """
203
+ else:
204
+ # Nếu chỉ có cột PK, không có gì để update -> Chỉ Insert if not exists
205
+ merge_sql = f"""
206
+ MERGE [{target_table}] AS Target
207
+ USING {staging_table} AS Source
208
+ ON {on_clause}
209
+ WHEN NOT MATCHED BY TARGET THEN
210
+ INSERT ({insert_cols}) VALUES ({insert_vals});
211
+ """
212
+
213
+ # Trường hợp 2: Skip (Chỉ Insert, không Update)
214
+ elif conflict_strategy == 'skip':
215
+ merge_sql = f"""
216
+ MERGE [{target_table}] AS Target
217
+ USING {staging_table} AS Source
218
+ ON {on_clause}
219
+ WHEN NOT MATCHED BY TARGET THEN
220
+ INSERT ({insert_cols}) VALUES ({insert_vals});
221
+ """
222
+
223
+ conn.execute(text(merge_sql))
224
+ conn.execute(text(f"DROP TABLE IF EXISTS {staging_table}"))
225
+ logger.info(f"Upserted {len(df)} rows to {target_table} (Strategy: {conflict_strategy})")
226
+
227
+ except Exception as e:
228
+ logger.error(f"Upsert failed for {target_table}: {e}")
229
+ raise e
230
+
231
+ def dispose(self):
232
+ self.engine.dispose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlServerConnector
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A custom SQL Server Connector for ETL processes with Pandas
5
5
  Author-email: Nguyen Minh Son <nguyen.minhson1511@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/johnnyb1509/sqlServerConnector
@@ -20,6 +20,9 @@ Requires-Dist: jupyterlab
20
20
  # SQL Server Connector
21
21
 
22
22
  Thư viện kết nối SQL Server chuyên dụng cho các tác vụ ETL, được tối ưu hóa cho **Pandas**, hỗ trợ **Tiếng Việt (Unicode)** và **Upsert (Merge)** hiệu năng cao.
23
+ ## Update 0.1.7
24
+ > Sửa lỗi nhỏ liên quan đến việc upsert với các bảng có cột chứa Tiếng Việt
25
+
23
26
 
24
27
  ## 🚀 Tính năng nổi bật
25
28
 
@@ -128,3 +131,9 @@ db.dispose()
128
131
  1. **Primary Key:** Khi dùng upsert_data, bắt buộc phải cung cấp primary_key. Nếu bảng chưa có Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
129
132
 
130
133
  2. **Date Time:** Các cột ngày tháng nên được convert sang datetime64[ns] trong Pandas trước khi đẩy vào để đảm bảo tính chính xác.
134
+
135
+ 3. **Upgrade version:** Luôn kiểm tra và cập nhật lên phiên bản mới nhất để tận dụng các tính năng và sửa lỗi mới nhất. For developer, change version in `pyproject.toml` and build & upload to PyPI:
136
+ ```bash
137
+ python -m build
138
+ python -m twine upload dist/*
139
+ ```
@@ -1,222 +0,0 @@
1
- import os
2
- import numpy as np
3
- import pandas as pd
4
- import uuid
5
- from typing import List, Optional, Dict, Union, Any
6
- from loguru import logger
7
- from sqlalchemy import create_engine, inspect, text, URL
8
- from sqlalchemy.types import NVARCHAR, FLOAT, INTEGER, DATE, DATETIME, BIGINT
9
- from sqlalchemy.exc import SQLAlchemyError
10
-
11
- class SQLServerConnector:
12
- """
13
- A robust, SQLAlchemy 2.0 compliant connector for SQL Server designed for ETL processes.
14
-
15
- Features:
16
- - High-performance Upserts (Merge) using Unique Staging Tables.
17
- - Advanced Conflict Resolution: 'sum' (for finance) or 'last' (for metadata).
18
- - Automatic Schema Evolution and Primary Key management.
19
- - Unicode/Vietnamese support (NVARCHAR + UTF8).
20
- """
21
-
22
- def __init__(self, server: str, database: str, username: str, password: str, driver: str = 'ODBC Driver 17 for SQL Server'):
23
- self.server = server
24
- self.database = database
25
- self.username = username
26
- self.password = password
27
- self.driver = driver
28
-
29
- self.connection_url = URL.create(
30
- "mssql+pyodbc",
31
- query={
32
- "odbc_connect": (
33
- f"DRIVER={self.driver};"
34
- f"SERVER={self.server};"
35
- f"DATABASE={self.database};"
36
- f"UID={self.username};"
37
- f"PWD={self.password};"
38
- "Charsets=UTF-8;"
39
- ),
40
- "fast_executemany": "True"
41
- }
42
- )
43
-
44
- self.engine = create_engine(
45
- self.connection_url,
46
- pool_pre_ping=True,
47
- pool_size=20,
48
- max_overflow=10
49
- )
50
-
51
- def dispose(self):
52
- self.engine.dispose()
53
- logger.info("Database engine disposed.")
54
-
55
- # ========================================================
56
- # SCHEMA HELPERS
57
- # ========================================================
58
-
59
- def check_table_exists(self, table_name: str) -> bool:
60
- return inspect(self.engine).has_table(table_name)
61
-
62
- def get_columns_info(self, table_name: str) -> Dict[str, str]:
63
- inspector = inspect(self.engine)
64
- return {col['name']: str(col['type']) for col in inspector.get_columns(table_name)}
65
-
66
- # ========================================================
67
- # CORE ETL METHODS
68
- # ========================================================
69
-
70
- def upsert_data(self, df: pd.DataFrame, target_table: str, primary_key: str = None,
71
- match_columns: Optional[List[str]] = None, auto_evolve_schema: bool = True,
72
- conflict_strategy: str = 'sum'):
73
- if df.empty: return
74
-
75
- join_keys = match_columns if match_columns else ([primary_key] if primary_key else [])
76
-
77
- # 1. Sanitize & lọc lấy các cột cần thiết
78
- # Chỉ giữ lại join_keys và các cột có dữ liệu để tránh "phân mảnh" dữ liệu khi gộp
79
- df_clean = self._sanitize_dataframe(df, exclude_cols=join_keys)
80
-
81
- # 2. Xử lý trùng lặp triệt để
82
- initial_len = len(df_clean)
83
- if conflict_strategy == 'sum':
84
- # Xác định cột số để cộng dồn
85
- num_cols = df_clean.select_dtypes(include=[np.number]).columns.tolist()
86
- num_cols = [c for c in num_cols if c not in join_keys]
87
-
88
- # Chỉ gộp trên các cột số, các cột text khác key sẽ bị loại bỏ hoặc lấy dòng đầu
89
- # Điều này đảm bảo kết quả trả về CHỈ CÓ 1 DÒNG cho mỗi cặp Key
90
- agg_logic = {col: 'sum' for col in num_cols}
91
-
92
- # Đối với các cột không phải số và không phải key, chúng ta lấy dòng đầu tiên
93
- other_cols = [c for c in df_clean.columns if c not in join_keys and c not in num_cols]
94
- for c in other_cols:
95
- agg_logic[c] = 'first'
96
-
97
- df_clean = df_clean.groupby(join_keys, as_index=False).agg(agg_logic)
98
- else:
99
- df_clean = df_clean.drop_duplicates(subset=join_keys, keep='last')
100
-
101
- if len(df_clean) < initial_len:
102
- logger.info(f"Conflict Resolution ({conflict_strategy}): Combined {initial_len} -> {len(df_clean)} rows.")
103
-
104
- # 3. Schema Management
105
- if not self.check_table_exists(target_table):
106
- self._create_table_from_df(df_clean, target_table, primary_key)
107
- elif auto_evolve_schema:
108
- self._sync_columns(df_clean, target_table)
109
-
110
- # 4. Execute Merge
111
- self._execute_merge_upsert(df_clean, target_table, join_keys)
112
-
113
- def _execute_merge_upsert(self, df: pd.DataFrame, target_table: str, join_keys: List[str]):
114
- # Use a unique staging name to support parallel tasks
115
- unique_id = str(uuid.uuid4()).replace('-', '')[:10]
116
- staging_table = f"##stg_{unique_id}_{target_table[:20]}"
117
-
118
- with self.engine.begin() as conn:
119
- try:
120
- # Explicit mapping for Unicode
121
- dtype_map = {col: NVARCHAR(None) for col in df.columns if df[col].dtype == 'object'}
122
- df.to_sql(staging_table, conn, if_exists='replace', index=False, dtype=dtype_map)
123
-
124
- source_cols = list(df.columns)
125
- on_clause = " AND ".join([f"Target.[{k}] = Source.[{k}]" for k in join_keys])
126
- update_stmts = [f"Target.[{col}] = Source.[{col}]" for col in source_cols if col not in join_keys]
127
-
128
- insert_cols = ", ".join([f"[{col}]" for col in source_cols])
129
- insert_vals = ", ".join([f"Source.[{col}]" for col in source_cols])
130
-
131
- sql = f"""
132
- MERGE [{target_table}] AS Target USING [{staging_table}] AS Source
133
- ON ({on_clause})
134
- {f"WHEN MATCHED THEN UPDATE SET {', '.join(update_stmts)}" if update_stmts else ""}
135
- WHEN NOT MATCHED BY TARGET THEN INSERT ({insert_cols}) VALUES ({insert_vals});
136
- """
137
- conn.execute(text(sql))
138
- conn.execute(text(f"DROP TABLE [{staging_table}]"))
139
- logger.success(f"Successfully upserted {len(df)} rows to {target_table}.")
140
- except Exception as e:
141
- logger.error(f"Merge execution failed for {target_table}: {e}")
142
- raise
143
-
144
- # ========================================================
145
- # UTILS: CLEANING & SCHEMA
146
- # ========================================================
147
-
148
- def _sanitize_dataframe(self, df: pd.DataFrame, exclude_cols: List[str]) -> pd.DataFrame:
149
- df = df.copy()
150
- # Clean Dates
151
- for col in df.select_dtypes(include=['datetime']).columns:
152
- df[col] = df[col].replace({pd.NaT: None})
153
- # Clean NaN/None
154
- df = df.replace({np.nan: None})
155
- df = df.where(pd.notnull(df), None)
156
- return df
157
-
158
- def _create_table_from_df(self, df: pd.DataFrame, table_name: str, primary_key: Optional[str]):
159
- dtype_map = {col: NVARCHAR(None) for col in df.columns if df[col].dtype == 'object'}
160
- df.to_sql(table_name, self.engine, index=False, dtype=dtype_map)
161
- if primary_key and primary_key in df.columns:
162
- self.set_primary_key(table_name, primary_key, df[primary_key].dtype)
163
-
164
- def set_primary_key(self, table_name: str, column_name: str, source_dtype):
165
- sql_type = "NVARCHAR(450)" if pd.api.types.is_string_dtype(source_dtype) else "BIGINT"
166
- with self.engine.connect() as conn:
167
- with conn.begin():
168
- conn.execute(text(f"ALTER TABLE [{table_name}] ALTER COLUMN [{column_name}] {sql_type} NOT NULL"))
169
- conn.execute(text(f"ALTER TABLE [{table_name}] ADD PRIMARY KEY ([{column_name}])"))
170
-
171
- def _sync_columns(self, df: pd.DataFrame, table_name: str):
172
- db_cols = {k.lower() for k in self.get_columns_info(table_name).keys()}
173
- new_cols = [c for c in df.columns if c.lower() not in db_cols]
174
-
175
- if new_cols:
176
- with self.engine.connect() as conn:
177
- for col in new_cols:
178
- sql_type = "NVARCHAR(MAX)" if df[col].dtype == 'object' else "FLOAT"
179
- conn.execute(text(f"ALTER TABLE [{table_name}] ADD [{col}] {sql_type} NULL"))
180
- conn.commit()
181
-
182
- # ========================================================
183
- # DATA RETRIEVAL METHODS
184
- # ========================================================
185
- def get_data(self, query: str, params: Optional[Dict[str, Any]] = None, chunksize: Optional[int] = None) -> Union[pd.DataFrame, Any]:
186
- """
187
- Executes a SQL query and returns a Pandas DataFrame.
188
-
189
- Args:
190
- query (str): The SQL query string. Use :param_name for parameters.
191
- params (dict, optional): Dictionary of parameters to bind to the query.
192
- chunksize (int, optional): If specified, returns an iterator where each chunk is the given size.
193
-
194
- Returns:
195
- pd.DataFrame or Iterator[pd.DataFrame]
196
- """
197
- try:
198
- with self.engine.connect() as conn:
199
- # Use text() explicitly for SQLAlchemy 2.0 compatibility
200
- sql_query = text(query)
201
-
202
- # If chunksize is provided, read_sql returns a generator
203
- result = pd.read_sql(
204
- sql_query,
205
- conn,
206
- params=params,
207
- chunksize=chunksize
208
- )
209
-
210
- if chunksize is None:
211
- logger.info(f"Successfully retrieved {len(result)} rows.")
212
- else:
213
- logger.info(f"Retrieving data in chunks of {chunksize} rows.")
214
-
215
- return result
216
-
217
- except SQLAlchemyError as e:
218
- logger.error(f"Failed to retrieve data: {e}")
219
- raise
220
- except Exception as e:
221
- logger.error(f"An unexpected error occurred during get_data: {e}")
222
- raise