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.
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/PKG-INFO +10 -1
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/README.md +9 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/pyproject.toml +1 -1
- sqlserverconnector-0.1.7/src/connector.py +232 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/PKG-INFO +10 -1
- sqlserverconnector-0.1.5/src/connector.py +0 -222
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/setup.cfg +0 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/__init__.py +0 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/SOURCES.txt +0 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/dependency_links.txt +0 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/requires.txt +0 -0
- {sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlServerConnector
|
|
3
|
-
Version: 0.1.
|
|
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
|
+
```
|
|
@@ -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()
|
{sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlServerConnector
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
File without changes
|
|
File without changes
|
{sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/requires.txt
RENAMED
|
File without changes
|
{sqlserverconnector-0.1.5 → sqlserverconnector-0.1.7}/src/sqlServerConnector.egg-info/top_level.txt
RENAMED
|
File without changes
|