SuperModelingFactory 0.2.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.
Files changed (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,835 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DuckDB 兼容性检测工具
4
+
5
+ 遍历指定 SQL 文件夹下的所有 .sql 文件,逐行检测是否存在 DuckDB 不兼容的语法、
6
+ 函数或模式,生成详细的检测报告。
7
+
8
+ 检测覆盖以下维度:
9
+ 1. Hive/Spark 专属函数(DuckDB 中不存在或语法不同)
10
+ 2. Hive/Spark 专属语法(如 LATERAL VIEW EXPLODE, DISTRIBUTE BY 等)
11
+ 3. 模板占位符({xxx} 格式,需 Python 预处理)
12
+ 4. 隐式类型转换风险(DuckDB 比 Spark 更严格)
13
+ 5. 其他 DuckDB 方言差异
14
+
15
+ 使用方式:
16
+ python check_duckdb_compatibility.py [sql_folder_path]
17
+
18
+ sql_folder_path 可选,默认为当前目录下的 ./sql/
19
+ """
20
+
21
+ import os
22
+ import re
23
+ import sys
24
+ import json
25
+ from pathlib import Path
26
+ from typing import List, Dict, Tuple, Optional
27
+ from dataclasses import dataclass, field
28
+
29
+ # ═══════════════════════════════════════════════════════════════════════════════
30
+ # 数据结构定义
31
+ # ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+
34
+ @dataclass
35
+ class CompatibilityIssue:
36
+ """单条兼容性问题"""
37
+
38
+ severity: str # "error" | "warning" | "info"
39
+ category: str # 问题分类
40
+ line: int # 行号
41
+ column: int # 所在列(0 表示未知)
42
+ pattern: str # 匹配到的原始文本
43
+ message: str # 问题描述
44
+ suggestion: str # DuckDB 兼容建议
45
+
46
+
47
+ @dataclass
48
+ class FileReport:
49
+ """单个文件的检测报告"""
50
+
51
+ file_path: str
52
+ issues: List[CompatibilityIssue] = field(default_factory=list)
53
+
54
+ @property
55
+ def error_count(self) -> int:
56
+ return sum(1 for i in self.issues if i.severity == "error")
57
+
58
+ @property
59
+ def warning_count(self) -> int:
60
+ return sum(1 for i in self.issues if i.severity == "warning")
61
+
62
+ @property
63
+ def info_count(self) -> int:
64
+ return sum(1 for i in self.issues if i.severity == "info")
65
+
66
+ @property
67
+ def is_compatible(self) -> bool:
68
+ return self.error_count == 0
69
+
70
+
71
+ # ═══════════════════════════════════════════════════════════════════════════════
72
+ # 规则定义
73
+ # ═══════════════════════════════════════════════════════════════════════════════
74
+ #
75
+ # 每条规则是一个 dict:
76
+ # - pattern: 正则表达式(re.IGNORECASE 下匹配)
77
+ # - severity: "error" | "warning" | "info"
78
+ # - category: 问题分类标签
79
+ # - message: 问题描述模板(可用 {match} 引用匹配到的文本)
80
+ # - suggestion: DuckDB 兼容建议
81
+
82
+ RULES: List[dict] = [
83
+ # ── 1. Hive/Spark 专属函数 ───────────────────────────────────────────
84
+ {
85
+ "pattern": r"\bFROM_UNIXTIME\s*\(",
86
+ "severity": "error",
87
+ "category": "hive_function",
88
+ "message": "FROM_UNIXTIME 是 Hive/MySQL 函数,DuckDB 不支持",
89
+ "suggestion": "替换为 to_timestamp(epoch_seconds) 或 epoch_ms(milliseconds)。"
90
+ "例如: FROM_UNIXTIME(CAST(col/1000 AS BIGINT)) → to_timestamp(CAST(col/1000 AS BIGINT))",
91
+ },
92
+ {
93
+ "pattern": r"\bUNIX_TIMESTAMP\s*\(",
94
+ "severity": "error",
95
+ "category": "hive_function",
96
+ "message": "UNIX_TIMESTAMP 是 Hive/MySQL 函数,DuckDB 不支持",
97
+ "suggestion": "替换为 epoch(expr) 或 extract(epoch FROM timestamp_expr)",
98
+ },
99
+ {
100
+ "pattern": r"\bGET_JSON_OBJECT\s*\(",
101
+ "severity": "error",
102
+ "category": "hive_function",
103
+ "message": "GET_JSON_OBJECT 是 Hive/Spark 函数,DuckDB 不支持",
104
+ "suggestion": "替换为 json_extract_string(col, '$.path') 或 col->>'$.path'(简写)。"
105
+ "注意: DuckDB 的 JSON 路径语法以 '$.' 开头",
106
+ },
107
+ {
108
+ "pattern": r"\bNVL\s*\(",
109
+ "severity": "error",
110
+ "category": "hive_function",
111
+ "message": "NVL 是 Oracle/Hive 函数,DuckDB 不支持",
112
+ "suggestion": "替换为 coalesce(expr, default_value),两者语义等价",
113
+ },
114
+ {
115
+ "pattern": r"\bNVL2\s*\(",
116
+ "severity": "error",
117
+ "category": "hive_function",
118
+ "message": "NVL2 是 Oracle/Hive 函数,DuckDB 不支持",
119
+ "suggestion": "替换为 CASE WHEN expr IS NOT NULL THEN val1 ELSE val2 END",
120
+ },
121
+ {
122
+ "pattern": r"\bDECODE\s*\(",
123
+ "severity": "error",
124
+ "category": "hive_function",
125
+ "message": "DECODE 是 Oracle 函数,Hive 部分支持,DuckDB 不支持",
126
+ "suggestion": "替换为 CASE WHEN expr = v1 THEN r1 WHEN expr = v2 THEN r2 ... ELSE default END",
127
+ },
128
+ {
129
+ "pattern": r"\bTO_DATE\s*\(",
130
+ "severity": "warning",
131
+ "category": "hive_function",
132
+ "message": "TO_DATE 在 Hive 和 DuckDB 中语义可能不同",
133
+ "suggestion": "Hive: TO_DATE(string, format)。DuckDB: to_date(string) 或 strptime(str, fmt)::DATE。"
134
+ "请检查参数个数和格式字符串",
135
+ },
136
+ {
137
+ "pattern": r"\bDATE_FORMAT\s*\(",
138
+ "severity": "warning",
139
+ "category": "hive_function",
140
+ "message": "DATE_FORMAT 是 Hive/MySQL 函数,DuckDB 中不存在",
141
+ "suggestion": "替换为 strftime(timestamp, format_string),注意格式符有差异",
142
+ },
143
+ {
144
+ "pattern": r"\bDATE_ADD\s*\(",
145
+ "severity": "warning",
146
+ "category": "hive_function",
147
+ "message": "DATE_ADD 语法在 Hive 和 DuckDB 中不同",
148
+ "suggestion": "Hive: date_add(date, days)。DuckDB: date_add(date, INTERVAL n DAY) 或 date + INTERVAL n DAY",
149
+ },
150
+ {
151
+ "pattern": r"\bDATE_SUB\s*\(",
152
+ "severity": "warning",
153
+ "category": "hive_function",
154
+ "message": "DATE_SUB 语法在 Hive 和 DuckDB 中不同",
155
+ "suggestion": "Hive: date_sub(date, days)。DuckDB: date_sub(part, date, date) 或 date - INTERVAL n DAY",
156
+ },
157
+ {
158
+ "pattern": r"\bADD_MONTHS\s*\(",
159
+ "severity": "error",
160
+ "category": "hive_function",
161
+ "message": "ADD_MONTHS 是 Oracle/Hive 函数,DuckDB 不支持",
162
+ "suggestion": "替换为 date + INTERVAL n MONTH 或 date_add(date, INTERVAL n MONTH)",
163
+ },
164
+ {
165
+ "pattern": r"\bMONTHS_BETWEEN\s*\(",
166
+ "severity": "error",
167
+ "category": "hive_function",
168
+ "message": "MONTHS_BETWEEN 是 Oracle/Hive 函数,DuckDB 不支持",
169
+ "suggestion": "替换为 date_diff('month', date1, date2) 或手动计算月份差",
170
+ },
171
+ {
172
+ "pattern": r"\bLAST_DAY\s*\(",
173
+ "severity": "error",
174
+ "category": "hive_function",
175
+ "message": "LAST_DAY 是 Hive/MySQL 函数,DuckDB 不支持",
176
+ "suggestion": "替换为 last_day(date) — DuckDB 也支持 last_day 但语义略有不同,"
177
+ "或使用 date_trunc('month', date) + INTERVAL 1 MONTH - INTERVAL 1 DAY",
178
+ },
179
+ {
180
+ "pattern": r"\bINSTR\s*\(",
181
+ "severity": "warning",
182
+ "category": "hive_function",
183
+ "message": "INSTR 在 Hive 和 DuckDB 中存在但行为可能不同",
184
+ "suggestion": "DuckDB 使用 strpos(string, substring) 或 position(substring IN string)。"
185
+ "INSTR 在 DuckDB 中也可用但参数顺序与 Oracle 相反",
186
+ },
187
+ {
188
+ "pattern": r"\bCONCAT_WS\s*\(",
189
+ "severity": "info",
190
+ "category": "hive_function",
191
+ "message": "CONCAT_WS 在 Spark 和 DuckDB 中都支持,但参数行为略有不同",
192
+ "suggestion": "DuckDB 的 concat_ws(sep, str1, str2, ...) 要求至少 2 个字符串参数。"
193
+ "Spark 的 concat_ws 可以只传分隔符和单个数组。请确认参数类型",
194
+ },
195
+ {
196
+ "pattern": r"\bCOLLECT_LIST\s*\(",
197
+ "severity": "error",
198
+ "category": "hive_function",
199
+ "message": "COLLECT_LIST 是 Spark SQL 聚合函数,DuckDB 不支持",
200
+ "suggestion": "替换为 array_agg(expr) 或 list(expr)",
201
+ },
202
+ {
203
+ "pattern": r"\bCOLLECT_SET\s*\(",
204
+ "severity": "error",
205
+ "category": "hive_function",
206
+ "message": "COLLECT_SET 是 Spark SQL 聚合函数,DuckDB 不支持",
207
+ "suggestion": "替换为 array_agg(DISTINCT expr) 或 list(DISTINCT expr)",
208
+ },
209
+ {
210
+ "pattern": r"\bARRAY_CONTAINS\s*\(",
211
+ "severity": "warning",
212
+ "category": "hive_function",
213
+ "message": "ARRAY_CONTAINS 在 DuckDB 中的等价函数为 list_contains 或 array_contains",
214
+ "suggestion": "替换为 list_contains(array, element) 或 array_has(array, element)",
215
+ },
216
+ {
217
+ "pattern": r"\bSIZE\s*\(.*\)", # size(collection)
218
+ "severity": "warning",
219
+ "category": "hive_function",
220
+ "message": "SIZE 函数在 Hive/Spark 中用于集合,DuckDB 中用法不同",
221
+ "suggestion": "DuckDB 使用 len(array) 或 array_length(array)。"
222
+ "若 SIZE 用于字符串长度,请替换为 length(str)",
223
+ },
224
+ {
225
+ "pattern": r"\bEXPLODE\s*\(",
226
+ "severity": "error",
227
+ "category": "hive_syntax",
228
+ "message": "EXPLODE 是 Hive/Spark 表生成函数,DuckDB 不支持",
229
+ "suggestion": "替换为 UNNEST(array_column)。"
230
+ "例如: SELECT ... FROM t, LATERAL VIEW EXPLODE(col) AS x → SELECT ... FROM t, UNNEST(col) AS x",
231
+ },
232
+ {
233
+ "pattern": r"\bPOSEXPLODE\s*\(",
234
+ "severity": "error",
235
+ "category": "hive_syntax",
236
+ "message": "POSEXPLODE 是 Spark 表生成函数,DuckDB 不支持",
237
+ "suggestion": "替换为 UNNEST(array) WITH ORDINALITY。"
238
+ "例如: SELECT t.*, u.val, u.ordinal FROM t, UNNEST(col) WITH ORDINALITY AS u(val, idx)",
239
+ },
240
+
241
+ # ── 2. Hive/Spark 专属语法 ───────────────────────────────────────────
242
+ {
243
+ "pattern": r"\bLATERAL\s+VIEW\b",
244
+ "severity": "error",
245
+ "category": "hive_syntax",
246
+ "message": "LATERAL VIEW 是 Hive/Spark 表生成函数语法,DuckDB 不支持",
247
+ "suggestion": "替换为 CROSS JOIN LATERAL 或直接 , LATERAL 子查询。"
248
+ "也可使用 UNNEST 替代 EXPLODE",
249
+ },
250
+ {
251
+ "pattern": r"\bDISTRIBUTE\s+BY\b",
252
+ "severity": "error",
253
+ "category": "hive_syntax",
254
+ "message": "DISTRIBUTE BY 是 Hive 语法,DuckDB 不支持",
255
+ "suggestion": "无需直接替代(DuckDB 不使用 MapReduce 模型)。"
256
+ "若用于排序优化,可尝试 ORDER BY 代替",
257
+ },
258
+ {
259
+ "pattern": r"\bCLUSTER\s+BY\b",
260
+ "severity": "error",
261
+ "category": "hive_syntax",
262
+ "message": "CLUSTER BY 是 Hive 语法,DuckDB 不支持",
263
+ "suggestion": "无需直接替代。若需要排序输出,使用 ORDER BY",
264
+ },
265
+ {
266
+ "pattern": r"\bSORT\s+BY\b",
267
+ "severity": "error",
268
+ "category": "hive_syntax",
269
+ "message": "SORT BY 是 Hive 局部排序语法,DuckDB 不支持",
270
+ "suggestion": "替换为 ORDER BY。注意 SORT BY 只保证分区内有序,"
271
+ "ORDER BY 保证全局有序",
272
+ },
273
+ {
274
+ "pattern": r"\bTABLESAMPLE\s*\(",
275
+ "severity": "warning",
276
+ "category": "hive_syntax",
277
+ "message": "TABLESAMPLE 语法在 Hive 和 DuckDB 中不同",
278
+ "suggestion": "DuckDB: SELECT ... FROM table TABLESAMPLE SYSTEM(10 PERCENT) "
279
+ "或 USING SAMPLE reservoir(10 PERCENT)",
280
+ },
281
+ {
282
+ "pattern": r"\bANALYZE\s+TABLE\b",
283
+ "severity": "warning",
284
+ "category": "hive_syntax",
285
+ "message": "ANALYZE TABLE 语法在 Hive 和 DuckDB 中不同",
286
+ "suggestion": "DuckDB 使用 ANALYZE table_name 或 SUMMARIZE table_name",
287
+ },
288
+ {
289
+ "pattern": r"\bMSCK\s+REPAIR\b",
290
+ "severity": "error",
291
+ "category": "hive_syntax",
292
+ "message": "MSCK REPAIR TABLE 是 Hive 分区修复命令,DuckDB 不支持",
293
+ "suggestion": "DuckDB 不依赖 Hive Metastore,无需此命令。分区通过目录结构自动发现",
294
+ },
295
+ {
296
+ "pattern": r"\bREFRESH\s+TABLE\b",
297
+ "severity": "info",
298
+ "category": "hive_syntax",
299
+ "message": "REFRESH TABLE 是 Spark/Hive 命令,DuckDB 中无对应概念",
300
+ "suggestion": "DuckDB 自动感知文件变更,无需手动刷新",
301
+ },
302
+ {
303
+ "pattern": r"\bCOMPUTE\s+STATISTICS\b",
304
+ "severity": "info",
305
+ "category": "hive_syntax",
306
+ "message": "COMPUTE STATISTICS 是 Hive/Spark 命令",
307
+ "suggestion": "DuckDB 使用 ANALYZE 收集统计信息",
308
+ },
309
+
310
+ # ── 3. DATEDIFF 语法差异 ──────────────────────────────────────────────
311
+ {
312
+ "pattern": r"\bDATEDIFF\s*\((?![^)]*'day')",
313
+ "severity": "error",
314
+ "category": "datediff_syntax",
315
+ "message": "DATEDIFF 在 Hive 和 DuckDB 中语法不同: Hive DATEDIFF(end, start) 返回天数,"
316
+ "DuckDB 要求 datediff('day', start, end)",
317
+ "suggestion": "将 DATEDIFF(end, start) 替换为 datediff('day', start, end)。"
318
+ "注意: Hive 的参数是 (end, start),DuckDB 是 (part, start, end)",
319
+ },
320
+ {
321
+ "pattern": r"\bDATEDIFF\s*\(\s*'day'\s*,",
322
+ "severity": "quiet",
323
+ "category": "datediff_syntax",
324
+ "message": "DATEDIFF 已使用 DuckDB 兼容语法 (√)。请确认参数顺序为 (start, end)",
325
+ "suggestion": "",
326
+ },
327
+
328
+ # ── 4. 分区字段模式 ──────────────────────────────────────────────────
329
+ {
330
+ "pattern": r"\bDT\s*(<>|!=)\s*''",
331
+ "severity": "info",
332
+ "category": "partition_pruning",
333
+ "message": "DT <> '' 是 Hive 分区裁剪惯用写法。DuckDB 中可用同样的 WHERE 条件,"
334
+ "但不会触发分区裁剪(DuckDB 的分区机制不同)",
335
+ "suggestion": "如果使用 DuckDB 的分区表(partitioned write),通过目录结构自动感知分区。"
336
+ "WHERE DT IS NOT NULL AND DT != '' 可保留作为数据过滤条件",
337
+ },
338
+
339
+ # ── 5. 模板占位符 ──────────────────────────────────────────────────────
340
+ {
341
+ "pattern": r"\{[a-zA-Z_]\w*\}",
342
+ "severity": "warning",
343
+ "category": "template_placeholder",
344
+ "message": "检测到 Python 风格模板占位符,SQL 在执行前需经过字符串格式化预处理",
345
+ "suggestion": "确保在使用前通过 .format() 或 f-string 替换占位符。"
346
+ "注意: 若为字符串类型占位符,需确保替换后带引号",
347
+ },
348
+
349
+ # ── 6. 隐式类型转换风险(仅 verbose 模式显示)─────────────────────────
350
+ {
351
+ "pattern": r"CAST\s*\(\s*\S+\s+AS\s+FLOAT\s*\)",
352
+ "severity": "quiet",
353
+ "category": "type_conversion",
354
+ "message": "CAST(... AS FLOAT) 在 DuckDB 中为 32 位,Spark 中 DOUBLE 更常见",
355
+ "suggestion": "如需 64 位浮点,使用 CAST(... AS DOUBLE)。FLOAT=32-bit, DOUBLE=64-bit",
356
+ },
357
+
358
+ # ── 7. REGEXP_REPLACE 语义差异 ───────────────────────────────────────
359
+ {
360
+ "pattern": r"\bREGEXP_REPLACE\s*\(",
361
+ "severity": "warning",
362
+ "category": "function_semantics",
363
+ "message": "REGEXP_REPLACE 在 Hive 和 DuckDB 中都支持,但参数顺序和默认行为可能不同",
364
+ "suggestion": "Hive: regexp_replace(string, pattern, replacement)。"
365
+ "DuckDB: regexp_replace(string, pattern, replacement[, flags])。"
366
+ "DuckDB 默认使用全局替换(类似 Hive 的 global flag),请确认行为一致",
367
+ },
368
+
369
+ # ── 8. 中位数函数(DuckDB 已原生支持)────────────────────────────────
370
+ {
371
+ "pattern": r"\bMEDIAN\s*\(",
372
+ "severity": "info",
373
+ "category": "duckdb_supported",
374
+ "message": "MEDIAN 聚合函数: DuckDB 已原生支持 (√)",
375
+ "suggestion": "",
376
+ },
377
+
378
+ # ── 11. INSERT OVERWRITE 语法 ────────────────────────────────────────
379
+ {
380
+ "pattern": r"\bINSERT\s+OVERWRITE\b",
381
+ "severity": "error",
382
+ "category": "hive_syntax",
383
+ "message": "INSERT OVERWRITE 是 Hive/Spark 语法,DuckDB 不支持",
384
+ "suggestion": "替换为 CREATE OR REPLACE TABLE table_name AS ... 或 "
385
+ "DELETE FROM table_name; INSERT INTO table_name ...",
386
+ },
387
+ {
388
+ "pattern": r"\bINSERT\s+INTO\s+TABLE\b",
389
+ "severity": "warning",
390
+ "category": "hive_syntax",
391
+ "message": "INSERT INTO TABLE 语法中的 TABLE 关键字在 DuckDB 中可选",
392
+ "suggestion": "DuckDB 使用 INSERT INTO schema.table_name (无需 TABLE 关键字)",
393
+ },
394
+
395
+ # ── 12. PARTITION 子句(写入时)───────────────────────────────────────
396
+ {
397
+ "pattern": r"\bPARTITION\s*\(\s*\w+\s*\)",
398
+ "severity": "warning",
399
+ "category": "partition_syntax",
400
+ "message": "PARTITION(col) 在 INSERT/OVERWRITE 中是 Hive 语法,DuckDB 分区语法不同",
401
+ "suggestion": "DuckDB 分区写入使用 PARTITION_BY 而非 PARTITION。"
402
+ "例如: COPY ... TO ... (PARTITION_BY col) 或在 CREATE TABLE 时指定 PARTITION BY",
403
+ },
404
+
405
+ # ── 13. NULL 排序行为 ─────────────────────────────────────────────────
406
+ {
407
+ "pattern": r"\bORDER\s+BY\s+\S+\s+(ASC|DESC)\b",
408
+ "severity": "info",
409
+ "category": "null_ordering",
410
+ "message": "ORDER BY 中 NULL 的排序行为在 Hive 和 DuckDB 中不同",
411
+ "suggestion": "Hive 默认 NULLS FIRST (ASC 时)。DuckDB 默认 NULLS LAST (ASC 时)。"
412
+ "如需明确控制,添加 NULLS FIRST 或 NULLS LAST",
413
+ },
414
+ ]
415
+
416
+
417
+ # ═══════════════════════════════════════════════════════════════════════════════
418
+ # 核心检测函数
419
+ # ═══════════════════════════════════════════════════════════════════════════════
420
+
421
+
422
+ def scan_sql_content(
423
+ sql_content: str,
424
+ rules: List[dict] = None,
425
+ verbose: bool = False,
426
+ ) -> List[CompatibilityIssue]:
427
+ """
428
+ 逐行扫描 SQL 文本,应用所有规则,返回检测到的问题列表。
429
+
430
+ Args:
431
+ sql_content: SQL 文本内容(字符串)
432
+ rules: 规则列表,默认使用全局 RULES
433
+ verbose: 是否输出 quiet/info 级别的低优先级提示
434
+
435
+ Returns:
436
+ CompatibilityIssue 列表,按行号排序
437
+ """
438
+ if rules is None:
439
+ rules = RULES
440
+
441
+ issues: List[CompatibilityIssue] = []
442
+ lines = sql_content.split("\n")
443
+
444
+ for line_num, line in enumerate(lines, start=1):
445
+ # 跳过纯注释行和空行(减少噪音)
446
+ stripped = line.strip()
447
+ if not stripped or stripped.startswith("--"):
448
+ continue
449
+
450
+ for rule in rules:
451
+ pattern = re.compile(rule["pattern"], re.IGNORECASE)
452
+ for match in pattern.finditer(line):
453
+ matched_text = match.group(0)
454
+
455
+ # ── 非 verbose 模式下静默跳过的规则 ──
456
+ if not verbose:
457
+ if rule["severity"] == "quiet":
458
+ continue
459
+ # info 级别的 duckdb_supported 和 null_ordering 默认静默
460
+ if rule["category"] == "duckdb_supported" and rule["severity"] == "info":
461
+ continue
462
+ if rule["category"] == "null_ordering":
463
+ continue
464
+ # 分号结尾规则默认静默
465
+ if rule["category"] == "syntax_convention":
466
+ continue
467
+
468
+ # 安全格式化: 仅在 message/suggestion 包含 {match} 时才替换
469
+ formatted_message = rule["message"]
470
+ if "{match}" in formatted_message:
471
+ formatted_message = formatted_message.format(match=matched_text.strip())
472
+
473
+ formatted_suggestion = rule.get("suggestion", "")
474
+ if "{match}" in formatted_suggestion:
475
+ formatted_suggestion = formatted_suggestion.format(match=matched_text.strip())
476
+
477
+ issues.append(
478
+ CompatibilityIssue(
479
+ severity=rule["severity"],
480
+ category=rule["category"],
481
+ line=line_num,
482
+ column=match.start() + 1,
483
+ pattern=matched_text.strip(),
484
+ message=formatted_message,
485
+ suggestion=formatted_suggestion,
486
+ )
487
+ )
488
+
489
+ # 按行号排序
490
+ issues.sort(key=lambda x: (x.line, x.column))
491
+ return issues
492
+
493
+
494
+ def scan_sql_file(
495
+ file_path: str,
496
+ rules: List[dict] = None,
497
+ verbose: bool = False,
498
+ ) -> FileReport:
499
+ """
500
+ 扫描单个 SQL 文件,返回完整的 FileReport。
501
+
502
+ Args:
503
+ file_path: SQL 文件的绝对或相对路径
504
+ rules: 规则列表,默认使用全局 RULES
505
+
506
+ Returns:
507
+ FileReport 对象,包含所有检测到的问题
508
+ """
509
+ report = FileReport(file_path=file_path)
510
+
511
+ try:
512
+ with open(file_path, "r", encoding="utf-8") as f:
513
+ content = f.read()
514
+ except UnicodeDecodeError:
515
+ # 尝试其他编码
516
+ try:
517
+ with open(file_path, "r", encoding="latin-1") as f:
518
+ content = f.read()
519
+ except Exception as e:
520
+ report.issues.append(
521
+ CompatibilityIssue(
522
+ severity="error",
523
+ category="file_read",
524
+ line=0,
525
+ column=0,
526
+ pattern="",
527
+ message=f"无法读取文件: {e}",
528
+ suggestion="请检查文件编码(需 UTF-8 或 Latin-1)",
529
+ )
530
+ )
531
+ return report
532
+ except FileNotFoundError:
533
+ report.issues.append(
534
+ CompatibilityIssue(
535
+ severity="error",
536
+ category="file_read",
537
+ line=0,
538
+ column=0,
539
+ pattern="",
540
+ message=f"文件不存在: {file_path}",
541
+ suggestion="请检查文件路径是否正确",
542
+ )
543
+ )
544
+ return report
545
+
546
+ report.issues = scan_sql_content(content, rules, verbose=verbose)
547
+ return report
548
+
549
+
550
+ def collect_sql_files(sql_folder: str) -> List[str]:
551
+ """
552
+ 递归收集 sql_folder 下所有 .sql 文件(排除隐藏目录如 .ipynb_checkpoints)。
553
+
554
+ Args:
555
+ sql_folder: SQL 文件夹路径
556
+
557
+ Returns:
558
+ .sql 文件路径列表,按路径排序
559
+ """
560
+ sql_files = []
561
+ for root, dirs, files in os.walk(sql_folder):
562
+ # 排除隐藏目录
563
+ dirs[:] = [d for d in dirs if not d.startswith(".")]
564
+ for f in files:
565
+ if f.endswith(".sql"):
566
+ sql_files.append(os.path.join(root, f))
567
+ return sorted(sql_files)
568
+
569
+
570
+ # ═══════════════════════════════════════════════════════════════════════════════
571
+ # 主入口函数
572
+ # ═══════════════════════════════════════════════════════════════════════════════
573
+
574
+
575
+ def check_duckdb_compatibility(
576
+ sql_folder: str = "./sql",
577
+ fail_on_error: bool = False,
578
+ output_json: Optional[str] = None,
579
+ print_report: bool = True,
580
+ verbose: bool = False,
581
+ ) -> Dict:
582
+ """
583
+ 检测 SQL 文件夹下所有 .sql 文件的 DuckDB 兼容性。
584
+
585
+ 这是主要入口函数,供外部脚本 import 调用,也可从命令行直接运行。
586
+
587
+ Args:
588
+ sql_folder: SQL 文件夹路径,默认为 ./sql/
589
+ fail_on_error: 如果为 True,发现 error 级别问题时抛出 SystemExit
590
+ output_json: 可选,将报告输出到指定 JSON 文件路径
591
+ verbose: 是否打印详细报告到 stdout
592
+
593
+ Returns:
594
+ dict: {
595
+ "total_files": int, # 扫描的文件总数
596
+ "total_issues": int, # 问题总数
597
+ "error_count": int, # error 级别总数
598
+ "warning_count": int, # warning 级别总数
599
+ "info_count": int, # info 级别总数
600
+ "compatible_files": int, # 完全兼容(无 error)的文件数
601
+ "incompatible_files": int, # 存在 error 的文件数
602
+ "files": [FileReport, ...], # 每个文件的详细报告
603
+ "summary_by_category": dict, # 按类别汇总
604
+ }
605
+
606
+ Example:
607
+ >>> from check_duckdb_compatibility import check_duckdb_compatibility
608
+ >>> result = check_duckdb_compatibility("./sql")
609
+ >>> print(f"兼容文件: {result['compatible_files']}/{result['total_files']}")
610
+
611
+ >>> # 在 CI 中使用
612
+ >>> result = check_duckdb_compatibility("./sql", fail_on_error=True)
613
+ """
614
+ if not os.path.isdir(sql_folder):
615
+ raise FileNotFoundError(f"SQL 文件夹不存在: {sql_folder}")
616
+
617
+ # 1. 收集所有 SQL 文件
618
+ sql_files = collect_sql_files(sql_folder)
619
+ if not sql_files:
620
+ print(f"[WARN] 在 {sql_folder} 下未找到任何 .sql 文件")
621
+ return {
622
+ "total_files": 0,
623
+ "total_issues": 0,
624
+ "error_count": 0,
625
+ "warning_count": 0,
626
+ "info_count": 0,
627
+ "compatible_files": 0,
628
+ "incompatible_files": 0,
629
+ "files": [],
630
+ "summary_by_category": {},
631
+ }
632
+
633
+ # 2. 逐文件扫描
634
+ file_reports: List[FileReport] = []
635
+ for fpath in sql_files:
636
+ report = scan_sql_file(fpath, verbose=verbose)
637
+ file_reports.append(report)
638
+
639
+ # 3. 汇总统计
640
+ total_issues = sum(len(r.issues) for r in file_reports)
641
+ total_errors = sum(r.error_count for r in file_reports)
642
+ total_warnings = sum(r.warning_count for r in file_reports)
643
+ total_infos = sum(r.info_count for r in file_reports)
644
+ compatible = sum(1 for r in file_reports if r.is_compatible)
645
+ incompatible = sum(1 for r in file_reports if not r.is_compatible)
646
+
647
+ # 4. 按类别汇总
648
+ category_counts: Dict[str, int] = {}
649
+ for report in file_reports:
650
+ for issue in report.issues:
651
+ category_counts[issue.category] = category_counts.get(issue.category, 0) + 1
652
+
653
+ result = {
654
+ "total_files": len(sql_files),
655
+ "total_issues": total_issues,
656
+ "error_count": total_errors,
657
+ "warning_count": total_warnings,
658
+ "info_count": total_infos,
659
+ "compatible_files": compatible,
660
+ "incompatible_files": incompatible,
661
+ "files": file_reports,
662
+ "summary_by_category": category_counts,
663
+ }
664
+
665
+ # 5. 打印报告
666
+ if print_report:
667
+ _print_report(result)
668
+
669
+ # 6. 输出 JSON(可选)
670
+ if output_json:
671
+ _write_json_report(result, output_json)
672
+
673
+ # 7. 按需失败退出
674
+ if fail_on_error and total_errors > 0:
675
+ print(f"\n[FAIL] 发现 {total_errors} 个 DuckDB 兼容性错误,请修复后再试。")
676
+ sys.exit(1)
677
+
678
+ return result
679
+
680
+
681
+ # ═══════════════════════════════════════════════════════════════════════════════
682
+ # 报告输出辅助函数
683
+ # ═══════════════════════════════════════════════════════════════════════════════
684
+
685
+
686
+ def _print_report(result: Dict) -> None:
687
+ """打印人类可读的控制台报告。"""
688
+ print("=" * 80)
689
+ print(" DuckDB 兼容性检测报告")
690
+ print("=" * 80)
691
+ print(f" 扫描文件总数: {result['total_files']}")
692
+ print(f" 完全兼容文件数: {result['compatible_files']}")
693
+ print(f" 存在兼容问题文件数: {result['incompatible_files']}")
694
+ print(f" ─────────────────────────────")
695
+ print(f" Error 级别问题: {result['error_count']}")
696
+ print(f" Warning 级别问题: {result['warning_count']}")
697
+ print(f" Info 级别问题: {result['info_count']}")
698
+ print(f" 问题总数: {result['total_issues']}")
699
+ print()
700
+
701
+ # 按类别汇总
702
+ if result["summary_by_category"]:
703
+ print("─" * 80)
704
+ print(" 问题分类汇总:")
705
+ for cat, cnt in sorted(result["summary_by_category"].items()):
706
+ print(f" • {cat}: {cnt} 处")
707
+ print()
708
+
709
+ # 按文件逐一输出
710
+ for report in result["files"]:
711
+ if not report.issues:
712
+ continue
713
+ rel_path = os.path.relpath(report.file_path, os.getcwd())
714
+ print("─" * 80)
715
+ print(f" 📄 {rel_path}")
716
+ print(f" Error: {report.error_count} | Warning: {report.warning_count} | Info: {report.info_count}")
717
+ print()
718
+
719
+ for issue in report.issues:
720
+ icon = {"error": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
721
+ print(f" {icon} L{issue.line:04d}:{issue.column:03d} [{issue.severity.upper()}] [{issue.category}]")
722
+ print(f" 匹配: {issue.pattern}")
723
+ print(f" 说明: {issue.message}")
724
+ if issue.suggestion:
725
+ print(f" 建议: {issue.suggestion}")
726
+ print()
727
+
728
+ print("=" * 80)
729
+ if result["incompatible_files"] == 0:
730
+ print(" ✅ 所有 SQL 文件均未发现 DuckDB 错误级兼容性问题。")
731
+ else:
732
+ print(f" ❌ {result['incompatible_files']} 个文件存在错误级兼容性问题,需要手动修改。")
733
+ print("=" * 80)
734
+
735
+
736
+ def _write_json_report(result: Dict, output_path: str) -> None:
737
+ """将检测报告序列化为 JSON 写入文件。"""
738
+ serializable = {
739
+ "total_files": result["total_files"],
740
+ "total_issues": result["total_issues"],
741
+ "error_count": result["error_count"],
742
+ "warning_count": result["warning_count"],
743
+ "info_count": result["info_count"],
744
+ "compatible_files": result["compatible_files"],
745
+ "incompatible_files": result["incompatible_files"],
746
+ "summary_by_category": result["summary_by_category"],
747
+ "files": [],
748
+ }
749
+
750
+ for report in result["files"]:
751
+ file_entry = {
752
+ "path": report.file_path,
753
+ "error_count": report.error_count,
754
+ "warning_count": report.warning_count,
755
+ "info_count": report.info_count,
756
+ "is_compatible": report.is_compatible,
757
+ "issues": [],
758
+ }
759
+ for issue in report.issues:
760
+ file_entry["issues"].append(
761
+ {
762
+ "severity": issue.severity,
763
+ "category": issue.category,
764
+ "line": issue.line,
765
+ "column": issue.column,
766
+ "pattern": issue.pattern,
767
+ "message": issue.message,
768
+ "suggestion": issue.suggestion,
769
+ }
770
+ )
771
+ serializable["files"].append(file_entry)
772
+
773
+ with open(output_path, "w", encoding="utf-8") as f:
774
+ json.dump(serializable, f, ensure_ascii=False, indent=2)
775
+ print(f"[INFO] JSON 报告已写入: {output_path}")
776
+
777
+
778
+ # ═══════════════════════════════════════════════════════════════════════════════
779
+ # CLI 入口
780
+ # ═══════════════════════════════════════════════════════════════════════════════
781
+
782
+ if __name__ == "__main__":
783
+ import argparse
784
+
785
+ parser = argparse.ArgumentParser(
786
+ description="DuckDB 兼容性检测工具 — 扫描 SQL 文件夹并报告不兼容的语法/函数",
787
+ formatter_class=argparse.RawDescriptionHelpFormatter,
788
+ epilog="""
789
+ 使用示例:
790
+ python check_duckdb_compatibility.py
791
+ python check_duckdb_compatibility.py ./sql
792
+ python check_duckdb_compatibility.py ./sql --json report.json
793
+ python check_duckdb_compatibility.py ./sql --fail-on-error
794
+ """,
795
+ )
796
+ parser.add_argument(
797
+ "sql_folder",
798
+ nargs="?",
799
+ default="./sql",
800
+ help="SQL 文件夹路径(默认: ./sql)",
801
+ )
802
+ parser.add_argument(
803
+ "--json",
804
+ dest="output_json",
805
+ default=None,
806
+ help="将报告输出到指定的 JSON 文件",
807
+ )
808
+ parser.add_argument(
809
+ "--fail-on-error",
810
+ action="store_true",
811
+ default=False,
812
+ help="如果发现 error 级别问题,以非零退出码退出(适用于 CI)",
813
+ )
814
+ parser.add_argument(
815
+ "--quiet",
816
+ action="store_true",
817
+ default=False,
818
+ help="静默模式,不打印详细报告(通常和 --json 一起使用)",
819
+ )
820
+ parser.add_argument(
821
+ "--verbose",
822
+ action="store_true",
823
+ default=False,
824
+ help="详细模式,输出包括 quiet/info 在内的所有低优先级提示",
825
+ )
826
+
827
+ args = parser.parse_args()
828
+
829
+ check_duckdb_compatibility(
830
+ sql_folder=args.sql_folder,
831
+ fail_on_error=args.fail_on_error,
832
+ output_json=args.output_json,
833
+ print_report=not args.quiet,
834
+ verbose=args.verbose,
835
+ )