lysync 1.0.0__py3-none-any.whl
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.
- lysync/__init__.py +0 -0
- lysync/admin.py +3 -0
- lysync/apps.py +25 -0
- lysync/export/__init__.py +10 -0
- lysync/export/collector.py +72 -0
- lysync/export/engine.py +306 -0
- lysync/export/jinja_env.py +110 -0
- lysync/export/processor.py +40 -0
- lysync/export/utils.py +330 -0
- lysync/export/writer.py +143 -0
- lysync/funcs/__init__.py +25 -0
- lysync/funcs/collect.py +25 -0
- lysync/funcs/process.py +286 -0
- lysync/funcs/registry.py +116 -0
- lysync/migrations/0001_initial.py +207 -0
- lysync/migrations/0002_alter_syncexporttask_explain_json.py +19 -0
- lysync/migrations/__init__.py +0 -0
- lysync/models.py +383 -0
- lysync/signals/__init__.py +3 -0
- lysync/signals/datasource.py +46 -0
- lysync/tasks.py +453 -0
- lysync/tests.py +3 -0
- lysync/urls.py +31 -0
- lysync/utils.py +22 -0
- lysync/views.py +765 -0
- lysync-1.0.0.dist-info/METADATA +12 -0
- lysync-1.0.0.dist-info/RECORD +29 -0
- lysync-1.0.0.dist-info/WHEEL +5 -0
- lysync-1.0.0.dist-info/top_level.txt +1 -0
lysync/tasks.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Celery 任务(移植自 costexcel/tasks/export_task.py 并适配新模型)。
|
|
2
|
+
|
|
3
|
+
与旧版差异:
|
|
4
|
+
* 任务名统一加 ``lysync_`` 前缀,避免与主项目其它 app 冲突。
|
|
5
|
+
* 队列名为 ``lysync_export_queue``。
|
|
6
|
+
* 导出逻辑改为直接写入目标库(见 :mod:`lysync.export.engine`),移除 Excel/OSS。
|
|
7
|
+
* ``init_periodic_tasks`` 在 app 的 ``ready()`` 中调用,初始化定时任务。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
from celery import shared_task
|
|
17
|
+
from celery.exceptions import SoftTimeLimitExceeded
|
|
18
|
+
from django.conf import settings
|
|
19
|
+
from django.db import close_old_connections
|
|
20
|
+
from django.utils import timezone
|
|
21
|
+
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
|
22
|
+
from loguru import logger
|
|
23
|
+
from sqlglot import exp
|
|
24
|
+
from sqlglot import parse_one
|
|
25
|
+
|
|
26
|
+
from lysync.export.engine import exec_sync_export_check_item, exec_sync_export_task_item
|
|
27
|
+
from lysync.export.utils import (
|
|
28
|
+
add_limit_with_sqlglot,
|
|
29
|
+
fetch_data_from_temp_db,
|
|
30
|
+
format_datasource_error_message,
|
|
31
|
+
get_sql_context_for_ai,
|
|
32
|
+
grade_order,
|
|
33
|
+
render_sql_template,
|
|
34
|
+
sql_performance_grade,
|
|
35
|
+
)
|
|
36
|
+
from lysync.models import (
|
|
37
|
+
EXPLAIN_STATUS_CHECKING,
|
|
38
|
+
EXPLAIN_STATUS_FAILED,
|
|
39
|
+
EXPORT_RECORD_STATUS_CHECKING,
|
|
40
|
+
EXPORT_RECORD_STATUS_FAIL,
|
|
41
|
+
EXPORT_RECORD_STATUS_RUNNING,
|
|
42
|
+
EXPORT_RECORD_STATUS_STOPPED,
|
|
43
|
+
EXPORT_RECORD_STATUS_STOPPING,
|
|
44
|
+
EXPORT_RECORD_STATUS_SUCCESS,
|
|
45
|
+
EXPORT_RECORD_STATUS_WAIT,
|
|
46
|
+
SyncDataSource,
|
|
47
|
+
SyncExportRecord,
|
|
48
|
+
SyncExportTask,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
EXPORT_TASK_LOCK_PREFIX = "lysync_export_task_lock_"
|
|
52
|
+
QUEUE_NAME = "lysync_export_queue"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _patch_logger(request_id=None):
|
|
56
|
+
try:
|
|
57
|
+
from util.middleware import get_request_id
|
|
58
|
+
|
|
59
|
+
return logger.patch(lambda record: record["extra"].update(**get_request_id()))
|
|
60
|
+
except Exception:
|
|
61
|
+
return logger
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ============================ 定时任务初始化 ============================
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def init_periodic_tasks():
|
|
68
|
+
"""初始化周期性任务(在 app ready() 时调用)。"""
|
|
69
|
+
try:
|
|
70
|
+
try:
|
|
71
|
+
schedule, _ = CrontabSchedule.objects.get_or_create(
|
|
72
|
+
minute="1", hour="1", day_of_week="*", day_of_month="*", month_of_year="*"
|
|
73
|
+
)
|
|
74
|
+
except CrontabSchedule.MultipleObjectsReturned:
|
|
75
|
+
schedule = CrontabSchedule.objects.filter(
|
|
76
|
+
minute="1", hour="1", day_of_week="*", day_of_month="*", month_of_year="*"
|
|
77
|
+
).first()
|
|
78
|
+
|
|
79
|
+
PeriodicTask.objects.get_or_create(
|
|
80
|
+
name="lysync导出数据", task="lysync_export_data", defaults={"crontab": schedule}
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.exception(f"初始化导出定时任务失败: {e}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
try:
|
|
87
|
+
schedule, _ = CrontabSchedule.objects.get_or_create(
|
|
88
|
+
minute="*", hour="*", day_of_week="*", day_of_month="*", month_of_year="*"
|
|
89
|
+
)
|
|
90
|
+
except CrontabSchedule.MultipleObjectsReturned:
|
|
91
|
+
schedule = CrontabSchedule.objects.filter(
|
|
92
|
+
minute="*", hour="*", day_of_week="*", day_of_month="*", month_of_year="*"
|
|
93
|
+
).first()
|
|
94
|
+
|
|
95
|
+
PeriodicTask.objects.get_or_create(
|
|
96
|
+
name="lysync校验中断任务", task="lysync_check_export_task_interrupt", defaults={"crontab": schedule}
|
|
97
|
+
)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.exception(f"初始化校验中断定时任务失败: {e}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ============================ 中断校验 ============================
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@shared_task(name="lysync_check_export_task_interrupt")
|
|
106
|
+
def check_export_task_interrupt():
|
|
107
|
+
from util.lock import RedisLock
|
|
108
|
+
|
|
109
|
+
close_old_connections()
|
|
110
|
+
logger.info("开始校验中断任务")
|
|
111
|
+
query = SyncExportTask.objects.filter(last_status=EXPORT_RECORD_STATUS_RUNNING, is_active=True)
|
|
112
|
+
for export_task in query:
|
|
113
|
+
if not RedisLock.check_lock_pattern(f"{EXPORT_TASK_LOCK_PREFIX}{export_task.id}"):
|
|
114
|
+
SyncExportTask.objects.filter(
|
|
115
|
+
id=export_task.id,
|
|
116
|
+
last_status=EXPORT_RECORD_STATUS_RUNNING,
|
|
117
|
+
update_time=export_task.update_time,
|
|
118
|
+
).update(
|
|
119
|
+
last_status=EXPORT_RECORD_STATUS_FAIL,
|
|
120
|
+
last_message="任务被中断",
|
|
121
|
+
update_time=datetime.datetime.now(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ============================ 导出主流程 ============================
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@shared_task(name="lysync_export_data")
|
|
129
|
+
def exec_export_task(export_task_id=None, export_all=True):
|
|
130
|
+
request_id = uuid.uuid4().hex
|
|
131
|
+
logger.info(f"[{request_id}]开始执行导出任务: export_task_id:{export_task_id} export_all:{export_all}")
|
|
132
|
+
close_old_connections()
|
|
133
|
+
query_export_task = SyncExportTask.objects.filter(is_active=True).order_by("model_type__sorted")
|
|
134
|
+
now = datetime.datetime.now()
|
|
135
|
+
if export_task_id:
|
|
136
|
+
query_export_task = query_export_task.filter(id=export_task_id)
|
|
137
|
+
if not query_export_task.exists():
|
|
138
|
+
logger.info(f"[{request_id}]没有待处理的导出任务")
|
|
139
|
+
return
|
|
140
|
+
export_task_id_list = list(query_export_task.values_list("id", flat=True))
|
|
141
|
+
exec_export_task_queue.apply_async(
|
|
142
|
+
kwargs={
|
|
143
|
+
"request_id": request_id,
|
|
144
|
+
"export_task_id_list": export_task_id_list,
|
|
145
|
+
"export_all": export_all,
|
|
146
|
+
"now": now,
|
|
147
|
+
"index": 0,
|
|
148
|
+
},
|
|
149
|
+
queue=QUEUE_NAME,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@shared_task(name="lysync_export_task_queue")
|
|
154
|
+
def exec_export_task_queue(export_task_id_list, export_all, now, index=0, request_id=None):
|
|
155
|
+
try:
|
|
156
|
+
close_old_connections()
|
|
157
|
+
exec_export_task_queue_func(
|
|
158
|
+
export_task_id_list, export_all=export_all, now=now, index=index, request_id=request_id
|
|
159
|
+
)
|
|
160
|
+
except SoftTimeLimitExceeded:
|
|
161
|
+
logger.error(f"[{request_id}]任务超时: export_task_id_list={export_task_id_list}")
|
|
162
|
+
SyncExportTask.objects.filter(id=export_task_id_list[0], last_status=EXPORT_RECORD_STATUS_RUNNING).update(
|
|
163
|
+
last_status=EXPORT_RECORD_STATUS_FAIL, last_message="任务执行超时"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def exec_export_task_queue_func(export_task_id_list, export_all, now, index, request_id=None):
|
|
168
|
+
from util.lock import RedisLock
|
|
169
|
+
|
|
170
|
+
if not export_task_id_list:
|
|
171
|
+
return
|
|
172
|
+
lock = None
|
|
173
|
+
try:
|
|
174
|
+
lock = RedisLock(f"{EXPORT_TASK_LOCK_PREFIX}{export_task_id_list[0]}", blocking=False)
|
|
175
|
+
if not lock.acquire():
|
|
176
|
+
logger.error(f"[{request_id}][SKIP]任务执行中:id:{export_task_id_list[0]}")
|
|
177
|
+
return
|
|
178
|
+
res = SyncExportTask.objects.filter(id=export_task_id_list[0], is_active=True).update(
|
|
179
|
+
last_status=EXPORT_RECORD_STATUS_RUNNING, update_time=timezone.now(), last_message=""
|
|
180
|
+
)
|
|
181
|
+
export_task = SyncExportTask.objects.filter(id=export_task_id_list[0], is_active=True).first()
|
|
182
|
+
if not res:
|
|
183
|
+
logger.error(f"[{request_id}]未找到导出任务: {export_task_id_list[0]}")
|
|
184
|
+
if len(export_task_id_list) > 1:
|
|
185
|
+
exec_export_task_queue.apply_async(
|
|
186
|
+
kwargs={
|
|
187
|
+
"request_id": request_id,
|
|
188
|
+
"export_task_id_list": export_task_id_list[1:],
|
|
189
|
+
"export_all": export_all,
|
|
190
|
+
"now": now,
|
|
191
|
+
"index": index + 1,
|
|
192
|
+
},
|
|
193
|
+
queue=QUEUE_NAME,
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
success = False
|
|
197
|
+
stop_task = False
|
|
198
|
+
start_new_task = False
|
|
199
|
+
message = ""
|
|
200
|
+
try:
|
|
201
|
+
success, message, has_more = exec_sync_export_task_item(export_task, request_id=request_id)
|
|
202
|
+
if export_all and has_more:
|
|
203
|
+
export_task = SyncExportTask.objects.filter(
|
|
204
|
+
id=export_task_id_list[0], last_status=EXPORT_RECORD_STATUS_RUNNING
|
|
205
|
+
).first()
|
|
206
|
+
if not export_task:
|
|
207
|
+
stop_task = True
|
|
208
|
+
logger.error(f"[{request_id}]任务已终止: {export_task_id_list}")
|
|
209
|
+
else:
|
|
210
|
+
start_new_task = True
|
|
211
|
+
finally:
|
|
212
|
+
if stop_task:
|
|
213
|
+
SyncExportTask.objects.filter(
|
|
214
|
+
id=export_task_id_list[0], last_status=EXPORT_RECORD_STATUS_STOPPING
|
|
215
|
+
).update(last_status=EXPORT_RECORD_STATUS_STOPPED, last_message="已停止", update_time=timezone.now())
|
|
216
|
+
elif success:
|
|
217
|
+
SyncExportTask.objects.filter(
|
|
218
|
+
id=export_task_id_list[0], last_status=EXPORT_RECORD_STATUS_RUNNING
|
|
219
|
+
).update(last_status=EXPORT_RECORD_STATUS_SUCCESS, last_message=message, update_time=timezone.now())
|
|
220
|
+
else:
|
|
221
|
+
SyncExportTask.objects.filter(
|
|
222
|
+
id=export_task_id_list[0], last_status=EXPORT_RECORD_STATUS_RUNNING
|
|
223
|
+
).update(last_status=EXPORT_RECORD_STATUS_FAIL, last_message=message, update_time=timezone.now())
|
|
224
|
+
|
|
225
|
+
if start_new_task:
|
|
226
|
+
logger.info(f"[{request_id}]继续执行本任务的后续数据: id={export_task.id}, name={export_task.name}")
|
|
227
|
+
exec_export_task_queue.apply_async(
|
|
228
|
+
kwargs={
|
|
229
|
+
"request_id": request_id,
|
|
230
|
+
"export_task_id_list": export_task_id_list,
|
|
231
|
+
"export_all": export_all,
|
|
232
|
+
"now": now,
|
|
233
|
+
"index": index + 1,
|
|
234
|
+
},
|
|
235
|
+
queue=QUEUE_NAME,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
exec_export_task_check.apply_async(kwargs={"export_task_id": export_task_id_list[0]}, queue=QUEUE_NAME)
|
|
239
|
+
if len(export_task_id_list) > 1:
|
|
240
|
+
logger.info(f"[{request_id}]继续执行下一个任务:{export_task_id_list[1:]}")
|
|
241
|
+
exec_export_task_queue.apply_async(
|
|
242
|
+
kwargs={
|
|
243
|
+
"request_id": request_id,
|
|
244
|
+
"export_task_id_list": export_task_id_list[1:],
|
|
245
|
+
"export_all": export_all,
|
|
246
|
+
"now": now,
|
|
247
|
+
"index": index + 1,
|
|
248
|
+
},
|
|
249
|
+
queue=QUEUE_NAME,
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
logger.info(f"[{request_id}]无其他任务,本次请求完成")
|
|
253
|
+
finally:
|
|
254
|
+
if lock:
|
|
255
|
+
lock.release()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ============================ 增量补录(check) ============================
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@shared_task(name="lysync_export_task_check")
|
|
262
|
+
def exec_export_task_check(export_task_id=None):
|
|
263
|
+
close_old_connections()
|
|
264
|
+
logger.info("开始执行导出数据补录任务")
|
|
265
|
+
query_export_task = SyncExportTask.objects.filter(is_active=True)
|
|
266
|
+
now = datetime.datetime.now()
|
|
267
|
+
if export_task_id:
|
|
268
|
+
query_export_task = query_export_task.filter(id=export_task_id)
|
|
269
|
+
if not query_export_task.exists():
|
|
270
|
+
logger.info("没有待处理的任务")
|
|
271
|
+
return
|
|
272
|
+
for export_task in query_export_task:
|
|
273
|
+
exec_export_task_check_one_task.apply_async(
|
|
274
|
+
kwargs={"export_task_id": export_task.id, "now": now}, queue=QUEUE_NAME
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@shared_task(name="lysync_export_task_check_one_task")
|
|
279
|
+
def exec_export_task_check_one_task(export_task_id, now):
|
|
280
|
+
export_record_query = SyncExportRecord.objects.filter(
|
|
281
|
+
export_task_id=export_task_id,
|
|
282
|
+
is_active=True,
|
|
283
|
+
check_status__in=[EXPORT_RECORD_STATUS_WAIT, EXPORT_RECORD_STATUS_CHECKING],
|
|
284
|
+
)
|
|
285
|
+
if not export_record_query.exists():
|
|
286
|
+
logger.info(f"未找到导出记录: {export_task_id}")
|
|
287
|
+
return
|
|
288
|
+
for index, export_record in enumerate(export_record_query):
|
|
289
|
+
exec_export_task_check_one_record.apply_async(
|
|
290
|
+
kwargs={
|
|
291
|
+
"export_task_id": export_task_id,
|
|
292
|
+
"export_record_id": export_record.id,
|
|
293
|
+
"now": now,
|
|
294
|
+
"index": index,
|
|
295
|
+
},
|
|
296
|
+
queue=QUEUE_NAME,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@shared_task(name="lysync_export_task_check_one_record")
|
|
301
|
+
def exec_export_task_check_one_record(export_task_id, export_record_id, now, index=0):
|
|
302
|
+
try:
|
|
303
|
+
close_old_connections()
|
|
304
|
+
export_record = SyncExportRecord.objects.filter(id=export_record_id, is_active=True).first()
|
|
305
|
+
if not export_record:
|
|
306
|
+
return
|
|
307
|
+
exec_sync_export_check_item(export_record)
|
|
308
|
+
except SoftTimeLimitExceeded:
|
|
309
|
+
logger.error(f"导出数据补录_单个任务超时: id={export_record_id}")
|
|
310
|
+
SyncExportRecord.objects.filter(id=export_record_id, check_status=EXPORT_RECORD_STATUS_CHECKING).update(
|
|
311
|
+
check_status=EXPORT_RECORD_STATUS_WAIT, last_check_message="任务执行超时"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ============================ 数据源连接校验 ============================
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@shared_task(name="lysync_check_datasource_connect", ignore_result=True)
|
|
319
|
+
def check_datasource_connect(datasource_id):
|
|
320
|
+
logger.info(f"开始检查数据源连接: {datasource_id}")
|
|
321
|
+
for i in range(3):
|
|
322
|
+
obj = SyncDataSource.objects.filter(
|
|
323
|
+
id=datasource_id,
|
|
324
|
+
source_type=SyncDataSource.SOURCE_TYPE_DB,
|
|
325
|
+
is_active=True,
|
|
326
|
+
).first()
|
|
327
|
+
if not obj:
|
|
328
|
+
logger.error(f"未找到数据源: {datasource_id},第{i+1}次尝试")
|
|
329
|
+
time.sleep(3)
|
|
330
|
+
else:
|
|
331
|
+
break
|
|
332
|
+
if obj.db_connect_status != SyncDataSource.CONNECT_STATUS_CHECKING:
|
|
333
|
+
logger.info(f"数据源 {datasource_id}未启动检查")
|
|
334
|
+
return
|
|
335
|
+
try:
|
|
336
|
+
fetch_data_from_temp_db(
|
|
337
|
+
obj.db_type,
|
|
338
|
+
obj.db_host,
|
|
339
|
+
obj.db_port,
|
|
340
|
+
obj.db_user,
|
|
341
|
+
obj.db_password,
|
|
342
|
+
obj.db_name,
|
|
343
|
+
obj.db_options,
|
|
344
|
+
"select 1",
|
|
345
|
+
)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
error_msg = format_datasource_error_message(str(e), obj.db_name, obj.db_host, obj.db_port)
|
|
348
|
+
obj.db_connect_status = SyncDataSource.CONNECT_STATUS_FAIL
|
|
349
|
+
obj.db_connect_message = error_msg
|
|
350
|
+
obj.save(update_fields=["db_connect_status", "db_connect_message"])
|
|
351
|
+
return
|
|
352
|
+
obj.db_connect_status = SyncDataSource.CONNECT_STATUS_SUCCESS
|
|
353
|
+
obj.db_connect_message = "连接成功"
|
|
354
|
+
obj.save(update_fields=["db_connect_status", "db_connect_message"])
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ============================ SQL 性能分析 / AI 优化 ============================
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@shared_task(name="lysync_explain_item", ignore_result=True)
|
|
361
|
+
def explain_item(export_task_id):
|
|
362
|
+
export_task = SyncExportTask.objects.filter(id=export_task_id, is_active=True).first()
|
|
363
|
+
if not export_task:
|
|
364
|
+
return
|
|
365
|
+
export_task.explain_status = EXPLAIN_STATUS_CHECKING
|
|
366
|
+
export_task.explain_json = None
|
|
367
|
+
export_task.save(update_fields=["explain_status", "explain_json", "update_time"])
|
|
368
|
+
try:
|
|
369
|
+
data_source = export_task.data_source
|
|
370
|
+
if data_source.db_type == "tsql":
|
|
371
|
+
export_task.explain_status = EXPLAIN_STATUS_FAILED
|
|
372
|
+
export_task.explain_json = {"error_message": "不支持 SQL Server"}
|
|
373
|
+
export_task.save(update_fields=["explain_status", "explain_json", "update_time"])
|
|
374
|
+
return
|
|
375
|
+
sql = export_task.sql
|
|
376
|
+
# SQL 现在是 jinja2 模板,先渲染成可解析的 SQL 再做 EXPLAIN
|
|
377
|
+
rendered_sql = render_sql_template(
|
|
378
|
+
sql, start_time=datetime.datetime(2020, 1, 1), end_time=datetime.datetime(2020, 1, 2)
|
|
379
|
+
)
|
|
380
|
+
parsed = parse_one(rendered_sql, dialect=data_source.db_type)
|
|
381
|
+
if not isinstance(parsed, exp.Select):
|
|
382
|
+
export_task.explain_status = EXPLAIN_STATUS_FAILED
|
|
383
|
+
export_task.explain_json = {"error_message": "外层应为SELECT语句"}
|
|
384
|
+
export_task.save(update_fields=["explain_status", "explain_json", "update_time"])
|
|
385
|
+
return
|
|
386
|
+
if settings.EXPORT_DATA_LIMIT:
|
|
387
|
+
rendered_sql = add_limit_with_sqlglot(data_source.db_type, rendered_sql, settings.EXPORT_DATA_LIMIT)
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
explain_sql = f"EXPLAIN FORMAT=TRADITIONAL {rendered_sql}"
|
|
391
|
+
explain_res = fetch_data_from_temp_db(
|
|
392
|
+
data_source.db_type,
|
|
393
|
+
data_source.db_host,
|
|
394
|
+
data_source.db_port,
|
|
395
|
+
data_source.db_user,
|
|
396
|
+
data_source.db_password,
|
|
397
|
+
data_source.db_name,
|
|
398
|
+
data_source.db_options,
|
|
399
|
+
explain_sql,
|
|
400
|
+
)
|
|
401
|
+
except Exception:
|
|
402
|
+
explain_sql = f"EXPLAIN {rendered_sql}"
|
|
403
|
+
explain_res = fetch_data_from_temp_db(
|
|
404
|
+
data_source.db_type,
|
|
405
|
+
data_source.db_host,
|
|
406
|
+
data_source.db_port,
|
|
407
|
+
data_source.db_user,
|
|
408
|
+
data_source.db_password,
|
|
409
|
+
data_source.db_name,
|
|
410
|
+
data_source.db_options,
|
|
411
|
+
explain_sql,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
worst_index = 0
|
|
415
|
+
for item in explain_res:
|
|
416
|
+
item["p"] = sql_performance_grade(item)
|
|
417
|
+
order = grade_order.index(item["p"])
|
|
418
|
+
worst_index = max(worst_index, order)
|
|
419
|
+
export_task.explain_status = grade_order[worst_index]
|
|
420
|
+
export_task.explain_json = {"explain": explain_res}
|
|
421
|
+
export_task.save(update_fields=["explain_status", "explain_json", "update_time"])
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.exception(f"explain_item执行失败: {e}")
|
|
424
|
+
export_task.explain_status = EXPLAIN_STATUS_FAILED
|
|
425
|
+
export_task.explain_json = {"error_message": str(e)}
|
|
426
|
+
export_task.save(update_fields=["explain_status", "explain_json", "update_time"])
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@shared_task(name="lysync_ai_sql_optimization_task")
|
|
430
|
+
def ai_sql_optimization_task(export_task_id):
|
|
431
|
+
logger.info("开始执行:lysync_ai_sql_optimization_task")
|
|
432
|
+
export_task = SyncExportTask.objects.filter(id=export_task_id).first()
|
|
433
|
+
if not export_task:
|
|
434
|
+
raise Exception("未找到任务")
|
|
435
|
+
sql = export_task.sql
|
|
436
|
+
data_source = export_task.data_source
|
|
437
|
+
if data_source.db_type == "tsql":
|
|
438
|
+
return "# 不支持 SQL Server"
|
|
439
|
+
context = get_sql_context_for_ai(
|
|
440
|
+
sql,
|
|
441
|
+
data_source.db_type,
|
|
442
|
+
data_source.db_host,
|
|
443
|
+
data_source.db_port,
|
|
444
|
+
data_source.db_user,
|
|
445
|
+
data_source.db_password,
|
|
446
|
+
data_source.db_name,
|
|
447
|
+
data_source.db_options,
|
|
448
|
+
)
|
|
449
|
+
try:
|
|
450
|
+
from dify import call as dify_call
|
|
451
|
+
except Exception:
|
|
452
|
+
return "# 未配置 dify 调用(宿主项目需提供 dify.call)"
|
|
453
|
+
return dify_call(context)
|
lysync/tests.py
ADDED
lysync/urls.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
|
|
4
|
+
"""lysync 导出功能 URL 配置。
|
|
5
|
+
|
|
6
|
+
宿主项目可在根 urlconf 中通过 ``path("lysync/", include("lysync.urls"))`` 引入。
|
|
7
|
+
"""
|
|
8
|
+
from django.urls import re_path
|
|
9
|
+
|
|
10
|
+
from lysync import views
|
|
11
|
+
|
|
12
|
+
urlpatterns = [
|
|
13
|
+
re_path(r"^create_export_task$", views.create_export_task),
|
|
14
|
+
re_path(r"^update_export_task$", views.update_export_task),
|
|
15
|
+
re_path(r"^recover_export_task$", views.recover_export_task),
|
|
16
|
+
re_path(r"^exec_task_immediately$", views.exec_task_immediately),
|
|
17
|
+
re_path(r"^reset_task_last_export$", views.reset_task_last_export),
|
|
18
|
+
re_path(r"^reset_task_status$", views.reset_task_status),
|
|
19
|
+
re_path(r"^update_sync_data_source$", views.update_sync_data_source),
|
|
20
|
+
re_path(r"^get_collect_funcs$", views.get_collect_funcs),
|
|
21
|
+
re_path(r"^save_sync_model_type$", views.save_sync_model_type),
|
|
22
|
+
re_path(r"^get_process_funcs$", views.get_process_funcs),
|
|
23
|
+
re_path(r"^get_content_types$", views.get_content_types),
|
|
24
|
+
re_path(r"^get_model_fields$", views.get_model_fields),
|
|
25
|
+
re_path(r"^query_raw_data$", views.query_raw_data),
|
|
26
|
+
re_path(r"^check_explains$", views.check_explains),
|
|
27
|
+
re_path(r"^data_source_check_connect$", views.data_source_check_connect),
|
|
28
|
+
re_path(r"^stop_export_task$", views.stop_export_task),
|
|
29
|
+
re_path(r"^ai_sql_optimization$", views.ai_sql_optimization),
|
|
30
|
+
re_path(r"^get_ai_sql_optimization_result$", views.get_ai_sql_optimization_result),
|
|
31
|
+
]
|
lysync/utils.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_queue_name():
|
|
8
|
+
return getattr(settings, "QUEUE_QUEUE_EXPORT", "celery")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def local_today():
|
|
12
|
+
"""返回“今天”的本地日期,兼容宿主项目 ``USE_TZ=True/False``。
|
|
13
|
+
|
|
14
|
+
当宿主项目 ``USE_TZ=False`` 时,``timezone.now()`` 返回 naive datetime,
|
|
15
|
+
此时调用 ``timezone.localdate()`` 会抛
|
|
16
|
+
``ValueError: localdate() cannot be applied to a naive datetime``。
|
|
17
|
+
这里做兼容:检测到 naive 时直接用 ``datetime.date.today()``,
|
|
18
|
+
避免该异常破坏 SQL 模板渲染 / 任务推进逻辑。
|
|
19
|
+
"""
|
|
20
|
+
if timezone.is_naive(timezone.now()):
|
|
21
|
+
return datetime.date.today()
|
|
22
|
+
return timezone.localdate()
|