sqlServerConnector 0.1.6__tar.gz → 0.1.8__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.6 → sqlserverconnector-0.1.8}/PKG-INFO +42 -19
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/README.md +41 -18
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/pyproject.toml +1 -1
- sqlserverconnector-0.1.8/src/connector.py +244 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/PKG-INFO +42 -19
- sqlserverconnector-0.1.6/src/connector.py +0 -192
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/setup.cfg +0 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/__init__.py +0 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/SOURCES.txt +0 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/dependency_links.txt +0 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/requires.txt +0 -0
- {sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/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.8
|
|
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,8 +20,8 @@ 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.
|
|
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
|
|
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
25
|
|
|
26
26
|
|
|
27
27
|
## 🚀 Tính năng nổi bật
|
|
@@ -69,9 +69,10 @@ db_info:
|
|
|
69
69
|
## 📝 Hướng dẫn sử dụng nhanh
|
|
70
70
|
|
|
71
71
|
1. Khởi tạo kết nối
|
|
72
|
-
```python
|
|
72
|
+
```python
|
|
73
73
|
import yaml
|
|
74
74
|
from connector import SQLServerConnector
|
|
75
|
+
|
|
75
76
|
# Load config
|
|
76
77
|
with open('config/db_config.yaml', 'r') as f:
|
|
77
78
|
conf = yaml.safe_load(f)['db_info']
|
|
@@ -88,46 +89,68 @@ db = SQLServerConnector(
|
|
|
88
89
|
2. Lấy dữ liệu (Read)
|
|
89
90
|
```python
|
|
90
91
|
# Cách 1: Lấy toàn bộ bảng
|
|
91
|
-
df = db.get_data("DM_KhachHang")
|
|
92
|
+
df = db.get_data("SELECT * FROM DM_KhachHang")
|
|
92
93
|
|
|
93
|
-
# Cách 2: Dùng câu lệnh SQL
|
|
94
|
+
# Cách 2: Dùng câu lệnh SQL tuy bien
|
|
94
95
|
query = """
|
|
95
|
-
SELECT TOP 100 * FROM Sales_Transaction
|
|
96
|
-
WHERE created_date >=
|
|
96
|
+
SELECT TOP 100 * FROM Sales_Transaction
|
|
97
|
+
WHERE created_date >= :from_date
|
|
97
98
|
"""
|
|
98
|
-
df_sales = db.get_data(query)
|
|
99
|
+
df_sales = db.get_data(query, params={"from_date": "2023-01-01"})
|
|
99
100
|
print(df_sales.head())
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
3.
|
|
103
|
+
3. Kiểm tra bảng tồn tại
|
|
104
|
+
```python
|
|
105
|
+
if not db.check_table_exists("Fact_Sales"):
|
|
106
|
+
print("Bang Fact_Sales chua ton tai")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
4. Ghi du lieu (Upsert)
|
|
103
110
|
```python
|
|
104
111
|
import pandas as pd
|
|
105
112
|
|
|
106
|
-
#
|
|
113
|
+
# Gia lap du lieu
|
|
107
114
|
data = {
|
|
108
115
|
'TransactionID': [101, 102],
|
|
109
|
-
'Product': ['Laptop Dell', '
|
|
116
|
+
'Product': ['Laptop Dell', 'Chuot Logitech'], # Ho tro tieng Viet
|
|
110
117
|
'Amount': [15000000, 250000]
|
|
111
118
|
}
|
|
112
119
|
df_new = pd.DataFrame(data)
|
|
113
120
|
|
|
114
|
-
#
|
|
121
|
+
# Day vao DB
|
|
115
122
|
db.upsert_data(
|
|
116
123
|
df=df_new,
|
|
117
124
|
target_table="Fact_Sales",
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
match_columns=["TransactionID"], # Khoa so khop (Primary Key)
|
|
126
|
+
conflict_strategy="last", # "last" hoac "skip"
|
|
127
|
+
auto_evolve_schema=True # Tu dong them cot neu thieu
|
|
128
|
+
)
|
|
129
|
+
print("Du lieu da duoc upsert thanh cong!")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
5. Thuc thi cau lenh khong tra ve du lieu
|
|
133
|
+
```python
|
|
134
|
+
# Vi du: xoa du lieu cu
|
|
135
|
+
db.execute_query(
|
|
136
|
+
"DELETE FROM Fact_Sales WHERE created_date < :cutoff",
|
|
137
|
+
params={"cutoff": "2023-01-01"}
|
|
120
138
|
)
|
|
121
|
-
print("Dữ liệu đã được upsert thành công!")
|
|
122
139
|
```
|
|
123
140
|
|
|
124
|
-
|
|
141
|
+
6. Dong ket noi
|
|
125
142
|
```python
|
|
126
|
-
#
|
|
143
|
+
# Luon dong ket noi khi hoan tat de giai phong tai nguyen
|
|
127
144
|
db.dispose()
|
|
128
145
|
```
|
|
129
146
|
|
|
130
147
|
## ⚠️ Lưu ý quan trọng
|
|
131
|
-
1. **Primary Key:** Khi dùng upsert_data,
|
|
148
|
+
1. **Primary Key:** Khi dùng upsert_data, bat buoc phai cung cap `match_columns`. Neu bang chua co Primary Key, thu vien se co gang set cac cot nay lam khoa chinh khi tao bang moi.
|
|
132
149
|
|
|
133
150
|
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.
|
|
151
|
+
|
|
152
|
+
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:
|
|
153
|
+
```bash
|
|
154
|
+
python -m build
|
|
155
|
+
python -m twine upload dist/*
|
|
156
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
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.
|
|
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
|
|
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
6
|
|
|
7
7
|
|
|
8
8
|
## 🚀 Tính năng nổi bật
|
|
@@ -50,9 +50,10 @@ db_info:
|
|
|
50
50
|
## 📝 Hướng dẫn sử dụng nhanh
|
|
51
51
|
|
|
52
52
|
1. Khởi tạo kết nối
|
|
53
|
-
```python
|
|
53
|
+
```python
|
|
54
54
|
import yaml
|
|
55
55
|
from connector import SQLServerConnector
|
|
56
|
+
|
|
56
57
|
# Load config
|
|
57
58
|
with open('config/db_config.yaml', 'r') as f:
|
|
58
59
|
conf = yaml.safe_load(f)['db_info']
|
|
@@ -69,46 +70,68 @@ db = SQLServerConnector(
|
|
|
69
70
|
2. Lấy dữ liệu (Read)
|
|
70
71
|
```python
|
|
71
72
|
# Cách 1: Lấy toàn bộ bảng
|
|
72
|
-
df = db.get_data("DM_KhachHang")
|
|
73
|
+
df = db.get_data("SELECT * FROM DM_KhachHang")
|
|
73
74
|
|
|
74
|
-
# Cách 2: Dùng câu lệnh SQL
|
|
75
|
+
# Cách 2: Dùng câu lệnh SQL tuy bien
|
|
75
76
|
query = """
|
|
76
|
-
SELECT TOP 100 * FROM Sales_Transaction
|
|
77
|
-
WHERE created_date >=
|
|
77
|
+
SELECT TOP 100 * FROM Sales_Transaction
|
|
78
|
+
WHERE created_date >= :from_date
|
|
78
79
|
"""
|
|
79
|
-
df_sales = db.get_data(query)
|
|
80
|
+
df_sales = db.get_data(query, params={"from_date": "2023-01-01"})
|
|
80
81
|
print(df_sales.head())
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
3.
|
|
84
|
+
3. Kiểm tra bảng tồn tại
|
|
85
|
+
```python
|
|
86
|
+
if not db.check_table_exists("Fact_Sales"):
|
|
87
|
+
print("Bang Fact_Sales chua ton tai")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
4. Ghi du lieu (Upsert)
|
|
84
91
|
```python
|
|
85
92
|
import pandas as pd
|
|
86
93
|
|
|
87
|
-
#
|
|
94
|
+
# Gia lap du lieu
|
|
88
95
|
data = {
|
|
89
96
|
'TransactionID': [101, 102],
|
|
90
|
-
'Product': ['Laptop Dell', '
|
|
97
|
+
'Product': ['Laptop Dell', 'Chuot Logitech'], # Ho tro tieng Viet
|
|
91
98
|
'Amount': [15000000, 250000]
|
|
92
99
|
}
|
|
93
100
|
df_new = pd.DataFrame(data)
|
|
94
101
|
|
|
95
|
-
#
|
|
102
|
+
# Day vao DB
|
|
96
103
|
db.upsert_data(
|
|
97
104
|
df=df_new,
|
|
98
105
|
target_table="Fact_Sales",
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
match_columns=["TransactionID"], # Khoa so khop (Primary Key)
|
|
107
|
+
conflict_strategy="last", # "last" hoac "skip"
|
|
108
|
+
auto_evolve_schema=True # Tu dong them cot neu thieu
|
|
101
109
|
)
|
|
102
|
-
print("
|
|
110
|
+
print("Du lieu da duoc upsert thanh cong!")
|
|
103
111
|
```
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
5. Thuc thi cau lenh khong tra ve du lieu
|
|
106
114
|
```python
|
|
107
|
-
#
|
|
115
|
+
# Vi du: xoa du lieu cu
|
|
116
|
+
db.execute_query(
|
|
117
|
+
"DELETE FROM Fact_Sales WHERE created_date < :cutoff",
|
|
118
|
+
params={"cutoff": "2023-01-01"}
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
6. Dong ket noi
|
|
123
|
+
```python
|
|
124
|
+
# Luon dong ket noi khi hoan tat de giai phong tai nguyen
|
|
108
125
|
db.dispose()
|
|
109
126
|
```
|
|
110
127
|
|
|
111
128
|
## ⚠️ Lưu ý quan trọng
|
|
112
|
-
1. **Primary Key:** Khi dùng upsert_data,
|
|
129
|
+
1. **Primary Key:** Khi dùng upsert_data, bat buoc phai cung cap `match_columns`. Neu bang chua co Primary Key, thu vien se co gang set cac cot nay lam khoa chinh khi tao bang moi.
|
|
113
130
|
|
|
114
131
|
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.
|
|
132
|
+
|
|
133
|
+
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:
|
|
134
|
+
```bash
|
|
135
|
+
python -m build
|
|
136
|
+
python -m twine upload dist/*
|
|
137
|
+
```
|
|
@@ -0,0 +1,244 @@
|
|
|
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 - Fixed Missing Attribute).
|
|
14
|
+
Tích hợp:
|
|
15
|
+
- Fast Executemany (Tốc độ cao).
|
|
16
|
+
- Unicode Support (NVARCHAR).
|
|
17
|
+
- Upsert Strategy (Last/Skip).
|
|
18
|
+
- Schema Evolution.
|
|
19
|
+
- Helper methods: check_table_exists.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, server: str, database: str, username: str, password: str, driver: str = 'ODBC Driver 17 for SQL Server', **kwargs):
|
|
23
|
+
self.server = server
|
|
24
|
+
self.database = database
|
|
25
|
+
self.username = username
|
|
26
|
+
self.password = password
|
|
27
|
+
self.driver = driver
|
|
28
|
+
|
|
29
|
+
# Tạo URL kết nối
|
|
30
|
+
self.connection_url = URL.create(
|
|
31
|
+
"mssql+pyodbc",
|
|
32
|
+
query={
|
|
33
|
+
"odbc_connect": (
|
|
34
|
+
f"DRIVER={self.driver};"
|
|
35
|
+
f"SERVER={self.server};"
|
|
36
|
+
f"DATABASE={self.database};"
|
|
37
|
+
f"UID={self.username};"
|
|
38
|
+
f"PWD={self.password};"
|
|
39
|
+
"Encrypt=no;TrustServerCertificate=yes;"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Engine với fast_executemany=True
|
|
45
|
+
self.engine = create_engine(
|
|
46
|
+
self.connection_url,
|
|
47
|
+
fast_executemany=True,
|
|
48
|
+
pool_pre_ping=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def get_data(self, query: str, params: Optional[Dict] = None) -> pd.DataFrame:
|
|
52
|
+
"""Thực thi SELECT và trả về DataFrame"""
|
|
53
|
+
try:
|
|
54
|
+
with self.engine.connect() as conn:
|
|
55
|
+
return pd.read_sql(text(query), conn, params=params)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Get data error: {e}")
|
|
58
|
+
raise e
|
|
59
|
+
|
|
60
|
+
def execute_query(self, query: str, params: Optional[Dict] = None):
|
|
61
|
+
"""Thực thi lệnh không trả về dữ liệu (UPDATE, DELETE, etc.)"""
|
|
62
|
+
try:
|
|
63
|
+
with self.engine.begin() as conn:
|
|
64
|
+
conn.execute(text(query), params or {})
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Execute query error: {e}")
|
|
67
|
+
raise e
|
|
68
|
+
|
|
69
|
+
# --- [ĐÃ BỔ SUNG LẠI HÀM NÀY] ---
|
|
70
|
+
def check_table_exists(self, table_name: str) -> bool:
|
|
71
|
+
"""Kiểm tra bảng có tồn tại trong database không"""
|
|
72
|
+
try:
|
|
73
|
+
inspector = inspect(self.engine)
|
|
74
|
+
return inspector.has_table(table_name)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Check table exists failed: {e}")
|
|
77
|
+
return False
|
|
78
|
+
# --------------------------------
|
|
79
|
+
|
|
80
|
+
def _generate_dtype_mapping(self, df: pd.DataFrame) -> Dict:
|
|
81
|
+
"""Tự động map kiểu dữ liệu (NVARCHAR cho string)"""
|
|
82
|
+
dtype_map = {}
|
|
83
|
+
for col in df.columns:
|
|
84
|
+
if df[col].dtype == 'object' or pd.api.types.is_string_dtype(df[col]):
|
|
85
|
+
dtype_map[col] = NVARCHAR(length=None)
|
|
86
|
+
elif pd.api.types.is_datetime64_any_dtype(df[col]):
|
|
87
|
+
dtype_map[col] = DATETIME()
|
|
88
|
+
elif pd.api.types.is_float_dtype(df[col]):
|
|
89
|
+
dtype_map[col] = FLOAT()
|
|
90
|
+
elif pd.api.types.is_integer_dtype(df[col]):
|
|
91
|
+
dtype_map[col] = BIGINT()
|
|
92
|
+
return dtype_map
|
|
93
|
+
|
|
94
|
+
def _get_table_columns(self, table_name: str, conn) -> List[str]:
|
|
95
|
+
"""Lấy danh sách cột hiện có trong DB"""
|
|
96
|
+
inspector = inspect(conn)
|
|
97
|
+
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
|
98
|
+
return columns
|
|
99
|
+
|
|
100
|
+
def _add_missing_columns(self, table_name: str, missing_cols: List[str], dtype_map: Dict, conn):
|
|
101
|
+
"""Alter table để thêm cột thiếu (Schema Evolution)"""
|
|
102
|
+
for col in missing_cols:
|
|
103
|
+
col_type = dtype_map.get(col, NVARCHAR(255))
|
|
104
|
+
# SQLAlchemy type to string conversion logic simplified
|
|
105
|
+
type_str = "NVARCHAR(MAX)" # Default safe fallback
|
|
106
|
+
if isinstance(col_type, FLOAT): type_str = "FLOAT"
|
|
107
|
+
elif isinstance(col_type, BIGINT): type_str = "BIGINT"
|
|
108
|
+
elif isinstance(col_type, DATETIME): type_str = "DATETIME"
|
|
109
|
+
elif isinstance(col_type, DATE): type_str = "DATE"
|
|
110
|
+
|
|
111
|
+
alter_sql = f"ALTER TABLE [{table_name}] ADD [{col}] {type_str}"
|
|
112
|
+
conn.execute(text(alter_sql))
|
|
113
|
+
logger.info(f"Auto-evolve: Added column '{col}' to table '{table_name}'")
|
|
114
|
+
|
|
115
|
+
def upsert_data(self,
|
|
116
|
+
df: pd.DataFrame,
|
|
117
|
+
target_table: str,
|
|
118
|
+
match_columns: List[str],
|
|
119
|
+
conflict_strategy: Literal['last', 'skip'] = 'last',
|
|
120
|
+
auto_evolve_schema: bool = False):
|
|
121
|
+
"""
|
|
122
|
+
Hàm Upsert mạnh mẽ.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
df: DataFrame cần upload.
|
|
126
|
+
target_table: Tên bảng đích.
|
|
127
|
+
match_columns: Danh sách cột dùng làm Key so khớp (Primary Key).
|
|
128
|
+
conflict_strategy:
|
|
129
|
+
- 'last': Update ghi đè dữ liệu mới vào dòng cũ (Default).
|
|
130
|
+
- 'skip': Nếu trùng key thì bỏ qua, không update.
|
|
131
|
+
auto_evolve_schema:
|
|
132
|
+
- True: Tự động thêm cột vào DB nếu DF có cột mới.
|
|
133
|
+
- False: Bỏ qua các cột trong DF mà DB không có (Strict Schema).
|
|
134
|
+
"""
|
|
135
|
+
if df.empty:
|
|
136
|
+
logger.warning(f"DataFrame for {target_table} is empty. Skip.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# 1. Map Unicode Types
|
|
140
|
+
dtype_mapping = self._generate_dtype_mapping(df)
|
|
141
|
+
|
|
142
|
+
# 2. Staging Table Name
|
|
143
|
+
staging_table = f"##Staging_{uuid.uuid4().hex[:8]}"
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with self.engine.begin() as conn:
|
|
147
|
+
# --- A. Kiểm tra Schema & Table ---
|
|
148
|
+
inspector = inspect(conn)
|
|
149
|
+
if not inspector.has_table(target_table):
|
|
150
|
+
logger.info(f"Table {target_table} not found. Creating new...")
|
|
151
|
+
df.to_sql(target_table, conn, index=False, dtype=dtype_mapping)
|
|
152
|
+
# Tạo Primary Key nếu cần
|
|
153
|
+
if match_columns:
|
|
154
|
+
pk_str = ", ".join([f"[{c}]" for c in match_columns])
|
|
155
|
+
try:
|
|
156
|
+
conn.execute(text(f"ALTER TABLE [{target_table}] ADD CONSTRAINT PK_{target_table.replace('.','_')}_{uuid.uuid4().hex[:4]} PRIMARY KEY ({pk_str})"))
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.warning(f"Could not create PK: {e}")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# --- B. Xử lý Schema Evolution ---
|
|
162
|
+
db_cols = self._get_table_columns(target_table, conn)
|
|
163
|
+
df_cols = list(df.columns)
|
|
164
|
+
|
|
165
|
+
# Tìm cột có trong DF mà không có trong DB
|
|
166
|
+
new_cols = [c for c in df_cols if c not in db_cols]
|
|
167
|
+
|
|
168
|
+
if new_cols:
|
|
169
|
+
if auto_evolve_schema:
|
|
170
|
+
self._add_missing_columns(target_table, new_cols, dtype_mapping, conn)
|
|
171
|
+
db_cols.extend(new_cols) # Update danh sách cột DB
|
|
172
|
+
else:
|
|
173
|
+
# Nếu không auto evolve, chỉ giữ lại các cột khớp với DB
|
|
174
|
+
valid_cols = [c for c in df_cols if c in db_cols]
|
|
175
|
+
if len(valid_cols) < len(df_cols):
|
|
176
|
+
logger.warning(f"Schema strict: Dropping columns {new_cols} because they are not in DB.")
|
|
177
|
+
df = df[valid_cols]
|
|
178
|
+
|
|
179
|
+
# --- C. Đẩy vào Staging (Fast Executemany) ---
|
|
180
|
+
df.to_sql(
|
|
181
|
+
name=staging_table,
|
|
182
|
+
con=conn,
|
|
183
|
+
if_exists='replace',
|
|
184
|
+
index=False,
|
|
185
|
+
dtype=dtype_mapping
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# --- D. Thực hiện MERGE ---
|
|
189
|
+
# 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)
|
|
190
|
+
common_cols = [c for c in df.columns if c in db_cols]
|
|
191
|
+
|
|
192
|
+
on_clause = " AND ".join([f"Target.[{col}] = Source.[{col}]" for col in match_columns])
|
|
193
|
+
|
|
194
|
+
# Logic Insert
|
|
195
|
+
insert_cols = ", ".join([f"[{col}]" for col in common_cols])
|
|
196
|
+
insert_vals = ", ".join([f"Source.[{col}]" for col in common_cols])
|
|
197
|
+
|
|
198
|
+
# Logic Update
|
|
199
|
+
merge_sql = ""
|
|
200
|
+
|
|
201
|
+
# Trường hợp 1: Update ('last')
|
|
202
|
+
if conflict_strategy == 'last':
|
|
203
|
+
update_cols = [c for c in common_cols if c not in match_columns]
|
|
204
|
+
if update_cols:
|
|
205
|
+
update_set = ", ".join([f"Target.[{col}] = Source.[{col}]" for col in update_cols])
|
|
206
|
+
merge_sql = f"""
|
|
207
|
+
MERGE [{target_table}] AS Target
|
|
208
|
+
USING {staging_table} AS Source
|
|
209
|
+
ON {on_clause}
|
|
210
|
+
WHEN MATCHED THEN
|
|
211
|
+
UPDATE SET {update_set}
|
|
212
|
+
WHEN NOT MATCHED BY TARGET THEN
|
|
213
|
+
INSERT ({insert_cols}) VALUES ({insert_vals});
|
|
214
|
+
"""
|
|
215
|
+
else:
|
|
216
|
+
# Nếu chỉ có cột PK, không có gì để update -> Chỉ Insert if not exists
|
|
217
|
+
merge_sql = f"""
|
|
218
|
+
MERGE [{target_table}] AS Target
|
|
219
|
+
USING {staging_table} AS Source
|
|
220
|
+
ON {on_clause}
|
|
221
|
+
WHEN NOT MATCHED BY TARGET THEN
|
|
222
|
+
INSERT ({insert_cols}) VALUES ({insert_vals});
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
# Trường hợp 2: Skip (Chỉ Insert, không Update)
|
|
226
|
+
elif conflict_strategy == 'skip':
|
|
227
|
+
merge_sql = f"""
|
|
228
|
+
MERGE [{target_table}] AS Target
|
|
229
|
+
USING {staging_table} AS Source
|
|
230
|
+
ON {on_clause}
|
|
231
|
+
WHEN NOT MATCHED BY TARGET THEN
|
|
232
|
+
INSERT ({insert_cols}) VALUES ({insert_vals});
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
conn.execute(text(merge_sql))
|
|
236
|
+
conn.execute(text(f"DROP TABLE IF EXISTS {staging_table}"))
|
|
237
|
+
logger.info(f"Upserted {len(df)} rows to {target_table} (Strategy: {conflict_strategy})")
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Upsert failed for {target_table}: {e}")
|
|
241
|
+
raise e
|
|
242
|
+
|
|
243
|
+
def dispose(self):
|
|
244
|
+
self.engine.dispose()
|
{sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/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.8
|
|
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,8 +20,8 @@ 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.
|
|
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
|
|
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
25
|
|
|
26
26
|
|
|
27
27
|
## 🚀 Tính năng nổi bật
|
|
@@ -69,9 +69,10 @@ db_info:
|
|
|
69
69
|
## 📝 Hướng dẫn sử dụng nhanh
|
|
70
70
|
|
|
71
71
|
1. Khởi tạo kết nối
|
|
72
|
-
```python
|
|
72
|
+
```python
|
|
73
73
|
import yaml
|
|
74
74
|
from connector import SQLServerConnector
|
|
75
|
+
|
|
75
76
|
# Load config
|
|
76
77
|
with open('config/db_config.yaml', 'r') as f:
|
|
77
78
|
conf = yaml.safe_load(f)['db_info']
|
|
@@ -88,46 +89,68 @@ db = SQLServerConnector(
|
|
|
88
89
|
2. Lấy dữ liệu (Read)
|
|
89
90
|
```python
|
|
90
91
|
# Cách 1: Lấy toàn bộ bảng
|
|
91
|
-
df = db.get_data("DM_KhachHang")
|
|
92
|
+
df = db.get_data("SELECT * FROM DM_KhachHang")
|
|
92
93
|
|
|
93
|
-
# Cách 2: Dùng câu lệnh SQL
|
|
94
|
+
# Cách 2: Dùng câu lệnh SQL tuy bien
|
|
94
95
|
query = """
|
|
95
|
-
SELECT TOP 100 * FROM Sales_Transaction
|
|
96
|
-
WHERE created_date >=
|
|
96
|
+
SELECT TOP 100 * FROM Sales_Transaction
|
|
97
|
+
WHERE created_date >= :from_date
|
|
97
98
|
"""
|
|
98
|
-
df_sales = db.get_data(query)
|
|
99
|
+
df_sales = db.get_data(query, params={"from_date": "2023-01-01"})
|
|
99
100
|
print(df_sales.head())
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
3.
|
|
103
|
+
3. Kiểm tra bảng tồn tại
|
|
104
|
+
```python
|
|
105
|
+
if not db.check_table_exists("Fact_Sales"):
|
|
106
|
+
print("Bang Fact_Sales chua ton tai")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
4. Ghi du lieu (Upsert)
|
|
103
110
|
```python
|
|
104
111
|
import pandas as pd
|
|
105
112
|
|
|
106
|
-
#
|
|
113
|
+
# Gia lap du lieu
|
|
107
114
|
data = {
|
|
108
115
|
'TransactionID': [101, 102],
|
|
109
|
-
'Product': ['Laptop Dell', '
|
|
116
|
+
'Product': ['Laptop Dell', 'Chuot Logitech'], # Ho tro tieng Viet
|
|
110
117
|
'Amount': [15000000, 250000]
|
|
111
118
|
}
|
|
112
119
|
df_new = pd.DataFrame(data)
|
|
113
120
|
|
|
114
|
-
#
|
|
121
|
+
# Day vao DB
|
|
115
122
|
db.upsert_data(
|
|
116
123
|
df=df_new,
|
|
117
124
|
target_table="Fact_Sales",
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
match_columns=["TransactionID"], # Khoa so khop (Primary Key)
|
|
126
|
+
conflict_strategy="last", # "last" hoac "skip"
|
|
127
|
+
auto_evolve_schema=True # Tu dong them cot neu thieu
|
|
128
|
+
)
|
|
129
|
+
print("Du lieu da duoc upsert thanh cong!")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
5. Thuc thi cau lenh khong tra ve du lieu
|
|
133
|
+
```python
|
|
134
|
+
# Vi du: xoa du lieu cu
|
|
135
|
+
db.execute_query(
|
|
136
|
+
"DELETE FROM Fact_Sales WHERE created_date < :cutoff",
|
|
137
|
+
params={"cutoff": "2023-01-01"}
|
|
120
138
|
)
|
|
121
|
-
print("Dữ liệu đã được upsert thành công!")
|
|
122
139
|
```
|
|
123
140
|
|
|
124
|
-
|
|
141
|
+
6. Dong ket noi
|
|
125
142
|
```python
|
|
126
|
-
#
|
|
143
|
+
# Luon dong ket noi khi hoan tat de giai phong tai nguyen
|
|
127
144
|
db.dispose()
|
|
128
145
|
```
|
|
129
146
|
|
|
130
147
|
## ⚠️ Lưu ý quan trọng
|
|
131
|
-
1. **Primary Key:** Khi dùng upsert_data,
|
|
148
|
+
1. **Primary Key:** Khi dùng upsert_data, bat buoc phai cung cap `match_columns`. Neu bang chua co Primary Key, thu vien se co gang set cac cot nay lam khoa chinh khi tao bang moi.
|
|
132
149
|
|
|
133
150
|
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.
|
|
151
|
+
|
|
152
|
+
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:
|
|
153
|
+
```bash
|
|
154
|
+
python -m build
|
|
155
|
+
python -m twine upload dist/*
|
|
156
|
+
```
|
|
@@ -1,192 +0,0 @@
|
|
|
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
|
|
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
|
-
from sqlalchemy.exc import SQLAlchemyError
|
|
11
|
-
|
|
12
|
-
class SQLServerConnector:
|
|
13
|
-
"""
|
|
14
|
-
Trình kết nối SQL Server tối ưu cho ETL (Extract-Transform-Load).
|
|
15
|
-
|
|
16
|
-
Tính năng:
|
|
17
|
-
- Hỗ trợ Upsert (Merge) hiệu năng cao qua bảng tạm.
|
|
18
|
-
- Hỗ trợ Unicode (Tiếng Việt) tự động bằng NVARCHAR.
|
|
19
|
-
- Tự động quản lý Schema và Primary Key.
|
|
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
|
-
# Tạo URL kết nối
|
|
30
|
-
self.connection_url = URL.create(
|
|
31
|
-
"mssql+pyodbc",
|
|
32
|
-
query={
|
|
33
|
-
"odbc_connect": (
|
|
34
|
-
f"DRIVER={self.driver};"
|
|
35
|
-
f"SERVER={self.server};"
|
|
36
|
-
f"DATABASE={self.database};"
|
|
37
|
-
f"UID={self.username};"
|
|
38
|
-
f"PWD={self.password};"
|
|
39
|
-
"Encrypt=no;TrustServerCertificate=yes;" # Cấu hình SSL linh hoạt
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
# Tạo Engine với fast_executemany=True để tăng tốc độ Insert/Upsert
|
|
45
|
-
self.engine = create_engine(
|
|
46
|
-
self.connection_url,
|
|
47
|
-
fast_executemany=True, # QUAN TRỌNG: Tăng tốc độ ghi gấp nhiều lần
|
|
48
|
-
pool_pre_ping=True # Tự động kết nối lại nếu mất kết nối
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
def get_data(self, query: str, params: Optional[Dict] = None, chunksize: Optional[int] = None) -> Union[pd.DataFrame, Any]:
|
|
52
|
-
"""
|
|
53
|
-
Thực thi câu lệnh SELECT và trả về DataFrame.
|
|
54
|
-
"""
|
|
55
|
-
try:
|
|
56
|
-
with self.engine.connect() as conn:
|
|
57
|
-
# Dùng text() để đảm bảo tương thích SQLAlchemy 2.0
|
|
58
|
-
sql_query = text(query)
|
|
59
|
-
return pd.read_sql(sql_query, conn, params=params, chunksize=chunksize)
|
|
60
|
-
except Exception as e:
|
|
61
|
-
logger.error(f"Failed to retrieve data: {e}")
|
|
62
|
-
raise
|
|
63
|
-
|
|
64
|
-
def execute_query(self, query: str, params: Optional[Dict] = None):
|
|
65
|
-
"""Thực thi câu lệnh không trả về dữ liệu (UPDATE, DELETE, SP...)."""
|
|
66
|
-
try:
|
|
67
|
-
with self.engine.begin() as conn: # Tự động commit
|
|
68
|
-
conn.execute(text(query), params or {})
|
|
69
|
-
except Exception as e:
|
|
70
|
-
logger.error(f"Failed to execute query: {e}")
|
|
71
|
-
raise
|
|
72
|
-
|
|
73
|
-
def _generate_dtype_mapping(self, df: pd.DataFrame) -> Dict:
|
|
74
|
-
"""
|
|
75
|
-
Tự động tạo mapping kiểu dữ liệu cho SQL.
|
|
76
|
-
QUAN TRỌNG: Map tất cả cột string/object sang NVARCHAR để hỗ trợ Tiếng Việt.
|
|
77
|
-
"""
|
|
78
|
-
dtype_map = {}
|
|
79
|
-
for col in df.columns:
|
|
80
|
-
# Nếu là chuỗi -> NVARCHAR (hỗ trợ Unicode)
|
|
81
|
-
if df[col].dtype == 'object' or pd.api.types.is_string_dtype(df[col]):
|
|
82
|
-
# Tính độ dài max thực tế để tối ưu, hoặc để None (NVARCHAR(MAX))
|
|
83
|
-
max_len = df[col].astype(str).map(len).max()
|
|
84
|
-
if pd.isna(max_len) or max_len == 0:
|
|
85
|
-
length = 255
|
|
86
|
-
else:
|
|
87
|
-
length = int(max_len * 1.5) + 50 # Buffer thêm
|
|
88
|
-
if length > 4000: length = None # NVARCHAR(MAX)
|
|
89
|
-
|
|
90
|
-
dtype_map[col] = NVARCHAR(length=length)
|
|
91
|
-
|
|
92
|
-
# Nếu là ngày tháng
|
|
93
|
-
elif pd.api.types.is_datetime64_any_dtype(df[col]):
|
|
94
|
-
dtype_map[col] = DATETIME()
|
|
95
|
-
|
|
96
|
-
# Số thực
|
|
97
|
-
elif pd.api.types.is_float_dtype(df[col]):
|
|
98
|
-
dtype_map[col] = FLOAT()
|
|
99
|
-
|
|
100
|
-
# Số nguyên
|
|
101
|
-
elif pd.api.types.is_integer_dtype(df[col]):
|
|
102
|
-
dtype_map[col] = BIGINT()
|
|
103
|
-
|
|
104
|
-
return dtype_map
|
|
105
|
-
|
|
106
|
-
def upsert_data(self, df: pd.DataFrame, table_name: str, pk_cols: List[str]):
|
|
107
|
-
"""
|
|
108
|
-
Thực hiện Upsert (Insert hoặc Update) dữ liệu vào bảng SQL Server.
|
|
109
|
-
Sử dụng cơ chế Bảng Tạm (Staging Table) + MERGE Statement.
|
|
110
|
-
"""
|
|
111
|
-
if df.empty:
|
|
112
|
-
logger.warning(f"DataFrame for table {table_name} is empty. Skipping upsert.")
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
# 1. Chuẩn bị tên bảng tạm
|
|
116
|
-
staging_table = f"##Staging_{uuid.uuid4().hex[:8]}"
|
|
117
|
-
|
|
118
|
-
# 2. Tạo mapping kiểu dữ liệu (Fix lỗi Unicode)
|
|
119
|
-
dtype_mapping = self._generate_dtype_mapping(df)
|
|
120
|
-
|
|
121
|
-
try:
|
|
122
|
-
with self.engine.begin() as conn:
|
|
123
|
-
# A. Đẩy dữ liệu vào bảng tạm (Staging)
|
|
124
|
-
# fast_executemany=True ở engine sẽ làm bước này cực nhanh
|
|
125
|
-
df.to_sql(
|
|
126
|
-
name=staging_table,
|
|
127
|
-
con=conn,
|
|
128
|
-
if_exists='replace',
|
|
129
|
-
index=False,
|
|
130
|
-
dtype=dtype_mapping # Ép kiểu NVARCHAR tại đây
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# B. Kiểm tra bảng đích có tồn tại không
|
|
134
|
-
inspector = inspect(conn)
|
|
135
|
-
if not inspector.has_table(table_name):
|
|
136
|
-
logger.info(f"Table {table_name} does not exist. Creating from staging...")
|
|
137
|
-
# Tạo bảng chính từ bảng tạm (Copy cấu trúc và dữ liệu)
|
|
138
|
-
# Lưu ý: SELECT INTO sẽ tạo bảng mới
|
|
139
|
-
create_sql = f"SELECT * INTO {table_name} FROM {staging_table}"
|
|
140
|
-
conn.execute(text(create_sql))
|
|
141
|
-
|
|
142
|
-
# Tạo Primary Key cho bảng mới
|
|
143
|
-
if pk_cols:
|
|
144
|
-
pk_str = ", ".join([f"[{c}]" for c in pk_cols])
|
|
145
|
-
try:
|
|
146
|
-
alter_pk = f"ALTER TABLE {table_name} ADD CONSTRAINT PK_{table_name}_{uuid.uuid4().hex[:4]} PRIMARY KEY ({pk_str})"
|
|
147
|
-
conn.execute(text(alter_pk))
|
|
148
|
-
except Exception as ex_pk:
|
|
149
|
-
logger.warning(f"Could not create PK: {ex_pk}")
|
|
150
|
-
else:
|
|
151
|
-
# C. Thực hiện MERGE (Upsert)
|
|
152
|
-
# Lấy danh sách cột
|
|
153
|
-
cols = [c for c in df.columns]
|
|
154
|
-
|
|
155
|
-
# 1. Điều kiện ON (Primary Keys)
|
|
156
|
-
on_clause = " AND ".join([f"Target.[{col}] = Source.[{col}]" for col in pk_cols])
|
|
157
|
-
|
|
158
|
-
# 2. Điều kiện UPDATE (Các cột không phải PK)
|
|
159
|
-
update_cols = [col for col in cols if col not in pk_cols]
|
|
160
|
-
if update_cols:
|
|
161
|
-
update_clause = ", ".join([f"Target.[{col}] = Source.[{col}]" for col in update_cols])
|
|
162
|
-
matched_action = f"WHEN MATCHED THEN UPDATE SET {update_clause}"
|
|
163
|
-
else:
|
|
164
|
-
# Trường hợp bảng chỉ có PK (ít gặp), không làm gì khi match
|
|
165
|
-
matched_action = ""
|
|
166
|
-
|
|
167
|
-
# 3. Điều kiện INSERT (Tất cả cột)
|
|
168
|
-
insert_cols_str = ", ".join([f"[{col}]" for col in cols])
|
|
169
|
-
insert_vals_str = ", ".join([f"Source.[{col}]" for col in cols])
|
|
170
|
-
|
|
171
|
-
merge_sql = f"""
|
|
172
|
-
MERGE [{table_name}] AS Target
|
|
173
|
-
USING {staging_table} AS Source
|
|
174
|
-
ON {on_clause}
|
|
175
|
-
{matched_action}
|
|
176
|
-
WHEN NOT MATCHED BY TARGET THEN
|
|
177
|
-
INSERT ({insert_cols_str})
|
|
178
|
-
VALUES ({insert_vals_str});
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
conn.execute(text(merge_sql))
|
|
182
|
-
logger.info(f"Upserted {len(df)} rows into {table_name}.")
|
|
183
|
-
|
|
184
|
-
# D. Xóa bảng tạm (Optional, vì temp table ## tự hủy khi đóng conn, nhưng xóa cho sạch)
|
|
185
|
-
conn.execute(text(f"DROP TABLE IF EXISTS {staging_table}"))
|
|
186
|
-
|
|
187
|
-
except Exception as e:
|
|
188
|
-
logger.error(f"Upsert failed for {table_name}: {e}")
|
|
189
|
-
raise
|
|
190
|
-
|
|
191
|
-
def dispose(self):
|
|
192
|
-
self.engine.dispose()
|
|
File without changes
|
|
File without changes
|
{sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/requires.txt
RENAMED
|
File without changes
|
{sqlserverconnector-0.1.6 → sqlserverconnector-0.1.8}/src/sqlServerConnector.egg-info/top_level.txt
RENAMED
|
File without changes
|