✨ 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>
This commit is contained in:
75
db_utils.py
75
db_utils.py
@@ -1,7 +1,82 @@
|
||||
# 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+
|
||||
|
||||
Reference in New Issue
Block a user