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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlServerConnector
3
- Version: 0.1.6
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.6
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
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 tùy ý
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 >= '2023-01-01'
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. Ghi dữ liệu (Upsert)
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
- # Giả lập dữ liệu
113
+ # Gia lap du lieu
107
114
  data = {
108
115
  'TransactionID': [101, 102],
109
- 'Product': ['Laptop Dell', 'Chuột Logitech'], # Hỗ trợ tiếng Việt
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
- # Đẩy vào DB
121
+ # Day vao DB
115
122
  db.upsert_data(
116
123
  df=df_new,
117
124
  target_table="Fact_Sales",
118
- primary_key="TransactionID", # Cột dùng để định danh (tránh trùng lặp)
119
- auto_evolve_schema=True # Tự động thêm cột nếu thiếu
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
- 4. Đóng kết nối
141
+ 6. Dong ket noi
125
142
  ```python
126
- # Luôn đóng kết nối khi hoàn tất để giải phóng tài nguyên
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, bắt buộc phải cung cấp primary_key. Nếu bảng chưa Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
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.6
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
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 tùy ý
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 >= '2023-01-01'
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. Ghi dữ liệu (Upsert)
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
- # Giả lập dữ liệu
94
+ # Gia lap du lieu
88
95
  data = {
89
96
  'TransactionID': [101, 102],
90
- 'Product': ['Laptop Dell', 'Chuột Logitech'], # Hỗ trợ tiếng Việt
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
- # Đẩy vào DB
102
+ # Day vao DB
96
103
  db.upsert_data(
97
104
  df=df_new,
98
105
  target_table="Fact_Sales",
99
- primary_key="TransactionID", # Cột dùng để định danh (tránh trùng lặp)
100
- auto_evolve_schema=True # Tự động thêm cột nếu thiếu
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("Dữ liệu đã được upsert thành công!")
110
+ print("Du lieu da duoc upsert thanh cong!")
103
111
  ```
104
112
 
105
- 4. Đóng kết nối
113
+ 5. Thuc thi cau lenh khong tra ve du lieu
106
114
  ```python
107
- # Luôn đóng kết nối khi hoàn tất để giải phóng tài nguyên
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, bắt buộc phải cung cấp primary_key. Nếu bảng chưa Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
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
+ ```
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "sqlServerConnector"
9
- version = "0.1.6"
9
+ version = "0.1.8"
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,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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlServerConnector
3
- Version: 0.1.6
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.6
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
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 tùy ý
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 >= '2023-01-01'
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. Ghi dữ liệu (Upsert)
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
- # Giả lập dữ liệu
113
+ # Gia lap du lieu
107
114
  data = {
108
115
  'TransactionID': [101, 102],
109
- 'Product': ['Laptop Dell', 'Chuột Logitech'], # Hỗ trợ tiếng Việt
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
- # Đẩy vào DB
121
+ # Day vao DB
115
122
  db.upsert_data(
116
123
  df=df_new,
117
124
  target_table="Fact_Sales",
118
- primary_key="TransactionID", # Cột dùng để định danh (tránh trùng lặp)
119
- auto_evolve_schema=True # Tự động thêm cột nếu thiếu
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
- 4. Đóng kết nối
141
+ 6. Dong ket noi
125
142
  ```python
126
- # Luôn đóng kết nối khi hoàn tất để giải phóng tài nguyên
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, bắt buộc phải cung cấp primary_key. Nếu bảng chưa Primary Key, thư viện sẽ tự set cột đó làm khóa chính khi tạo bảng mới.
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()