Files
BLD_sync/db_utils.py
Misaka_Company 1ae901a040 feat: add auto table creation and per-PK sync verification
- Add new Access table mappings for transmitter contracts (25/26年变送器) and tube bending workshop (弯管车间烘洗)
- Add auto-create target table from Access schema when table does not exist (db_utils + init_full_sync)
- Implement per-primary-key verification in incremental sync: verify deletes are gone and inserts are present before marking Synced=1
- Add post-commit re-verification with Synced rollback on failure for automatic retry
- Batch IN clause parameters to stay under SQL Server limit
- Adjust poll interval from 5s to 30s
- Improve IDENTITY_INSERT cleanup in finally blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-12 10:50:01 +08:00

110 lines
3.8 KiB
Python

# db_utils.py
import pyodbc
import datetime
import decimal
from config import SQL_SERVER_CONN, ACCESS_DRIVER
# ================= 表存在性检查 & 自动建表 =================
def table_exists(cursor, schema, table):
"""检查 SQL Server 表是否存在"""
query = """
SELECT COUNT(*)
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
WHERE s.name = ? AND t.name = ?
"""
cursor.execute(query, (schema, table))
return cursor.fetchone()[0] > 0
def ensure_schema(cursor, schema):
"""确保 schema 存在,不存在则创建"""
cursor.execute("SELECT SCHEMA_ID(?)", (schema,))
if cursor.fetchone()[0] is None:
cursor.execute(f"CREATE SCHEMA [{schema}]")
def _access_col_to_sql(col_info, pk_col):
"""将 Access 列描述 (cursor.description 元组) 转为 SQL Server 列定义
Access ODBC 驱动的 type_code 是 Python 类型对象:
int → INT
str → NVARCHAR(size) (size > 4000 时为 Memo 字段 → NVARCHAR(MAX))
datetime.datetime → DATETIME
float → FLOAT
decimal.Decimal → DECIMAL(p,s)
bool → BIT
"""
col_name, type_code, _, size, precision, scale, nullable = col_info
is_pk = (col_name == pk_col)
if type_code is int:
sql_type = "INT"
if is_pk:
sql_type += " IDENTITY(1,1) PRIMARY KEY"
elif type_code is float:
sql_type = "FLOAT"
elif type_code is bool:
sql_type = "BIT"
elif type_code is datetime.datetime:
sql_type = "DATETIME"
elif type_code is decimal.Decimal:
sql_type = f"DECIMAL({precision or 18}, {scale or 0})"
elif type_code is str:
# size > 4000 → Access Memo 字段,用 NVARCHAR(MAX)
if not size or size <= 0 or size > 4000:
sql_type = "NVARCHAR(MAX)"
else:
sql_type = f"NVARCHAR({size})"
if is_pk:
sql_type += " PRIMARY KEY"
else:
sql_type = "NVARCHAR(255)"
if is_pk:
sql_type += " PRIMARY KEY"
return f"[{col_name}] {sql_type}"
def create_table_from_access(sql_cursor, target_schema, target_table,
acc_description, pk_col):
"""根据 Access cursor.description 在 SQL Server 自动建表"""
col_defs = [_access_col_to_sql(col, pk_col) for col in acc_description]
full_name = fmt_table(target_schema, target_table)
col_str = ",\n ".join(col_defs)
create_sql = f"CREATE TABLE {full_name} (\n {col_str}\n)"
sql_cursor.execute(create_sql)
def get_sql_conn():
"""获取 SQL Server 连接"""
# 显式添加 TrustServerCertificate=yes 以兼容 ODBC Driver 18+
conn_str = (
f"DRIVER={SQL_SERVER_CONN['driver']};SERVER={SQL_SERVER_CONN['server']};"
f"DATABASE={SQL_SERVER_CONN['database']};UID={SQL_SERVER_CONN['uid']};PWD={SQL_SERVER_CONN['pwd']};"
"Encrypt=yes;TrustServerCertificate=yes;"
)
return pyodbc.connect(conn_str)
def get_access_conn(file_path):
"""获取 Access 连接"""
conn_str = f"DRIVER={ACCESS_DRIVER};DBQ={file_path};"
return pyodbc.connect(conn_str)
def fmt_table(schema, table):
"""格式化 SQL Server 表名 [schema].[table]"""
return f"[{schema}].[{table}]"
def get_columns(cursor, table_name):
"""获取 Access 表的列名"""
# Access 查询表名加 []
cursor.execute(f"SELECT TOP 1 * FROM [{table_name}]")
return [column[0] for column in cursor.description]
def generate_insert_sql(target_schema, target_table, columns):
"""生成带架构的 INSERT 语句"""
full_table_name = fmt_table(target_schema, target_table)
col_str = ",".join([f"[{col}]" for col in columns])
placeholders = ",".join(["?"] * len(columns))
return f"INSERT INTO {full_table_name} ({col_str}) VALUES ({placeholders})"