# 删除错误的单列唯一索引 indexes = db.execute_sql(f"PRAGMA index_list('{table_name}')").fetchall() for idx in indexes: index_name = idx[1] is_unique = bool(idx[2]) ifnot is_unique: continue cols = get_index_columns(index_name) iflen(cols) == 1and cols[0] == "modbusdeviceconfig_id": db.execute_sql(f"DROP INDEX IF EXISTS {index_name}")
# 建联合唯一索引 indexes = db.execute_sql(f"PRAGMA index_list('{table_name}')").fetchall() has_pair_unique = False for idx in indexes: cols = set(get_index_columns(idx[1])) if cols == {"modbusdeviceconfig_id", "modbusdeviceregister_id"}: has_pair_unique = True break
ifnot has_pair_unique: db.execute_sql( f"CREATE UNIQUE INDEX IF NOT EXISTS idx_modbus_config_register_unique " f"ON {table_name}(modbusdeviceconfig_id, modbusdeviceregister_id)" )
为什么要先检查再建?因为迁移可能跑多次(重启就跑一次),不能假设索引一定不存在。CREATE UNIQUE INDEX IF NOT EXISTS 本身是幂等的,但前面的 DROP INDEX 不是——如果跑第二次,索引已经被删了,不需要再删。
表重命名:vl_recognition_task → inference_task
产品早期只有视觉识别任务,表叫 vl_recognition_task。后来加了其他推理任务,表名得改。SQLite 支持 ALTER TABLE ... RENAME TO:
1 2 3 4 5 6 7 8
if table_exists("vl_recognition_task") andnot table_exists("inference_task"): db.execute_sql("ALTER TABLE vl_recognition_task RENAME TO inference_task") add_column_if_missing("inference_task", "task_type", "VARCHAR(255) DEFAULT 'vl_recognition'") add_column_if_missing("inference_task", "source_type", "VARCHAR(255)") add_column_if_missing("inference_task", "source_id", "INTEGER") add_column_if_missing("inference_task", "input_params", "TEXT DEFAULT '{}'") add_column_if_missing("inference_task", "result_data", "TEXT") db.execute_sql("UPDATE inference_task SET task_type = 'vl_recognition' WHERE task_type IS NULL OR task_type = ''")
关键条件:table_exists("vl_recognition_task") and not table_exists("inference_task")。两个表不能同时存在——如果 inference_task 已经存在,说明迁移已经做过了,跳过。如果两个都不存在,说明是新部署,建表由 db.create_tables() 处理,也跳过。
defensure_runtime_config_version_unique_index(): # 先标准化异常版本号 db.execute_sql(f"UPDATE {table_name} SET version_no = 1 WHERE version_no IS NULL OR version_no <= 0")
# 查重 duplicate_groups = db.execute_sql( f"SELECT COUNT(1) FROM (" f" SELECT rover_id, version_no, COUNT(1) AS c" f" FROM {table_name} GROUP BY rover_id, version_no HAVING c > 1" f")" ).fetchone()
if dup_count > 0: # 同一 rover + version_no 只保留 id 最大的那一行 db.execute_sql( f"DELETE FROM {table_name} WHERE id IN (" f" SELECT t.id FROM {table_name} t" f" JOIN (" f" SELECT rover_id, version_no, MAX(id) AS keep_id" f" FROM {table_name} GROUP BY rover_id, version_no HAVING COUNT(1) > 1" f" ) d ON t.rover_id = d.rover_id AND t.version_no = d.version_no" f" WHERE t.id <> d.keep_id" f")" )
保留 MAX(id) 那一行——id 自增,最大的就是最新的。删完重复数据,再建索引就稳了。
然后还要把 rover 表里的 config_version 同步成实际最大版本号:
1 2 3 4 5 6
db.execute_sql( "UPDATE rover SET config_version = COALESCE(" " (SELECT MAX(v.version_no) FROM rover_runtime_config_version v WHERE v.rover_id = rover.id)," " CASE WHEN config_version IS NULL OR config_version <= 0 THEN 1 ELSE config_version END" ")" )
COALESCE 兜底:如果版本历史表里有数据就取最大值,没有就保持原值,原值为空就默认 1。
回填默认值的那些 SQL
迁移的最后一步往往是回填。历史数据里新加的字段全是 NULL,业务代码如果没做空值判断就会崩。
1 2 3 4 5 6
db.execute_sql("UPDATE rover SET config_version = 1 WHERE config_version IS NULL OR config_version <= 0") db.execute_sql("UPDATE rover SET rover_auth_enabled = 0 WHERE rover_auth_enabled IS NULL") db.execute_sql( "UPDATE rover_model_artifact SET conversion_status='completed' " "WHERE conversion_status IS NULL OR conversion_status = ''" )