springbootAI 1.8.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 (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,900 @@
1
+ """
2
+ PyMyBatis动态SQL处理模块
3
+
4
+ 实现MyBatis风格的动态SQL标签,核心安全特性:
5
+ - 严格区分 #{} 预编译占位符 和 ${} 字符串直接拼接
6
+ - ${} 默认拦截,仅允许白名单内的表名、字段名等常量
7
+ - 所有参数强制使用数据库驱动预编译语句
8
+ - 使用AST解析表达式,避免eval()安全漏洞
9
+ """
10
+
11
+ import re
12
+ import ast
13
+ import logging
14
+ from collections.abc import Mapping, Sequence
15
+ from typing import Dict, Any, List, Set, Optional
16
+ from enum import Enum
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SqlParameterType(Enum):
22
+ """SQL参数类型"""
23
+ PREPARED = 'prepared' # #{} 预编译占位符
24
+ RAW = 'raw' # ${} 字符串直接拼接
25
+
26
+
27
+ class DynamicSQLProcessor:
28
+ """
29
+ 动态SQL处理器
30
+
31
+ 将包含MyBatis风格标签的SQL模板转换为可执行的SQL语句
32
+
33
+ 安全特性:
34
+ 1. #{param} -> ? 预编译参数,安全
35
+ 2. ${param} -> 直接拼接,默认拦截,需配置白名单
36
+ """
37
+
38
+ # 参数占位符正则(支持嵌套属性如 #{user.username})
39
+ PREPARED_PARAM_PATTERN = re.compile(r'#\{([^}]+)\}')
40
+ RAW_PARAM_PATTERN = re.compile(r'\$\{([^}]+)\}')
41
+
42
+ def __init__(self, placeholder: str = '%s'):
43
+ """初始化动态SQL处理器
44
+
45
+ Args:
46
+ placeholder: 参数占位符,'%s' 用于 MySQL/PostgreSQL,'?' 用于 SQLite/Oracle
47
+ """
48
+ self.tag_handlers = {
49
+ 'if': self._handle_if,
50
+ 'where': self._handle_where,
51
+ 'foreach': self._handle_foreach,
52
+ 'choose': self._handle_choose,
53
+ 'when': self._handle_when,
54
+ 'otherwise': self._handle_otherwise,
55
+ 'set': self._handle_set,
56
+ 'trim': self._handle_trim,
57
+ }
58
+
59
+ # ${}白名单配置
60
+ self.raw_param_whitelist: Set[str] = set()
61
+ self.raw_param_allowed_patterns: List[re.Pattern] = []
62
+
63
+ # 启用${}的场景(表名、字段名等)
64
+ self.allow_raw_params = False
65
+
66
+ # 参数占位符
67
+ self.placeholder = placeholder
68
+
69
+ def set_raw_param_whitelist(self, whitelist: Set[str]) -> None:
70
+ """
71
+ 设置${}参数白名单
72
+
73
+ Args:
74
+ whitelist: 允许使用${}的参数名集合
75
+ """
76
+ self.raw_param_whitelist = whitelist
77
+
78
+ def add_raw_param_pattern(self, pattern: str) -> None:
79
+ """
80
+ 添加${}参数允许的正则模式
81
+
82
+ Args:
83
+ pattern: 正则表达式模式
84
+ """
85
+ self.raw_param_allowed_patterns.append(re.compile(pattern))
86
+
87
+ def enable_raw_params(self, enabled: bool = True) -> None:
88
+ """
89
+ 启用/禁用${}参数
90
+
91
+ Args:
92
+ enabled: 是否启用
93
+ """
94
+ self.allow_raw_params = enabled
95
+
96
+ def _is_raw_param_allowed(self, param_name: str, param_value: Any) -> bool:
97
+ """
98
+ 检查${}参数是否允许使用
99
+
100
+ Args:
101
+ param_name: 参数名
102
+ param_value: 参数值
103
+
104
+ Returns:
105
+ 是否允许
106
+ """
107
+ # 检查参数名白名单(优先级最高)
108
+ if param_name in self.raw_param_whitelist:
109
+ return True
110
+
111
+ # 如果未启用${},直接拒绝
112
+ if not self.allow_raw_params:
113
+ return False
114
+
115
+ # 检查参数值模式
116
+ if isinstance(param_value, str):
117
+ for pattern in self.raw_param_allowed_patterns:
118
+ if pattern.match(param_value):
119
+ return True
120
+
121
+ # 如果没有配置允许模式,只允许字母数字下划线的参数值(表名、字段名等)
122
+ if not self.raw_param_allowed_patterns:
123
+ if isinstance(param_value, str) and re.fullmatch(
124
+ r'[a-zA-Z_][a-zA-Z0-9_]*', param_value
125
+ ):
126
+ return True
127
+
128
+ # 不在白名单且不匹配任何允许模式,拒绝使用
129
+ return False
130
+
131
+ def _get_nested_value(self, params: Dict[str, Any], param_name: str) -> Any:
132
+ """
133
+ 获取嵌套属性值
134
+
135
+ Args:
136
+ params: 参数字典
137
+ param_name: 参数名,支持嵌套如 'user.username'
138
+
139
+ Returns:
140
+ 参数值
141
+
142
+ Raises:
143
+ ValueError: 参数不存在
144
+ """
145
+ if '.' not in param_name:
146
+ if param_name in params:
147
+ return params[param_name]
148
+ raise ValueError(f"参数不存在: {param_name}")
149
+
150
+ # 处理嵌套属性
151
+ parts = param_name.split('.')
152
+ value = params
153
+ for part in parts:
154
+ if isinstance(value, dict) and part in value:
155
+ value = value[part]
156
+ elif hasattr(value, part):
157
+ value = getattr(value, part)
158
+ else:
159
+ raise ValueError(f"参数不存在: {param_name}")
160
+ return value
161
+
162
+ @staticmethod
163
+ def _get_value(value: Any, path: str) -> Any:
164
+ """Resolve a dotted property from a mapping or a regular Python object."""
165
+ for part in path.split('.'):
166
+ if isinstance(value, Mapping):
167
+ if part not in value:
168
+ raise ValueError(f"参数不存在: {path}")
169
+ value = value[part]
170
+ elif hasattr(value, part):
171
+ value = getattr(value, part)
172
+ else:
173
+ raise ValueError(f"参数不存在: {path}")
174
+ return value
175
+
176
+ def _process_prepared_params(
177
+ self,
178
+ sql: str,
179
+ params: Dict[str, Any],
180
+ foreach_values: Optional[Dict[str, Any]] = None,
181
+ ) -> tuple:
182
+ """
183
+ 处理#{param}预编译占位符
184
+
185
+ Args:
186
+ sql: SQL模板
187
+ params: 参数字典
188
+
189
+ Returns:
190
+ (处理后的SQL, 参数列表)
191
+ """
192
+ param_order = []
193
+ foreach_values = foreach_values or {}
194
+ marker_pattern = '|'.join(re.escape(key) for key in foreach_values)
195
+ combined_pattern = re.compile(
196
+ rf'#\{{([^}}]+)\}}|({marker_pattern})' if marker_pattern
197
+ else r'#\{([^}]+)\}'
198
+ )
199
+
200
+ def replace_param(match):
201
+ marker = match.group(2) if marker_pattern else None
202
+ if marker:
203
+ param_order.append(foreach_values[marker])
204
+ return self.placeholder
205
+ param_name = match.group(1)
206
+ value = self._get_nested_value(params, param_name)
207
+ param_order.append(value)
208
+ return self.placeholder
209
+
210
+ processed_sql = combined_pattern.sub(replace_param, sql)
211
+ return processed_sql, param_order
212
+
213
+ def _process_raw_params(self, sql: str, params: Dict[str, Any]) -> str:
214
+ """
215
+ 处理${param}字符串拼接
216
+
217
+ Args:
218
+ sql: SQL模板
219
+ params: 参数字典
220
+
221
+ Returns:
222
+ 处理后的SQL
223
+
224
+ Raises:
225
+ SecurityError: ${}参数不在白名单中
226
+ """
227
+ def replace_raw(match):
228
+ param_name = match.group(1)
229
+ if param_name not in params:
230
+ raise ValueError(f"参数不存在: {param_name}")
231
+
232
+ param_value = params[param_name]
233
+
234
+ # 安全检查:${}必须在白名单中
235
+ if not self._is_raw_param_allowed(param_name, param_value):
236
+ raise SecurityError(
237
+ f"${{{param_name}}} 不在白名单中,禁止使用字符串拼接。"
238
+ f"允许的参数: {self.raw_param_whitelist}"
239
+ )
240
+
241
+ # 对${}参数值进行安全过滤
242
+ return self._sanitize_raw_param(param_value)
243
+
244
+ return self.RAW_PARAM_PATTERN.sub(replace_raw, sql)
245
+
246
+ def _sanitize_raw_param(self, value: Any) -> str:
247
+ """
248
+ 清理${}参数值,防止注入
249
+
250
+ Args:
251
+ value: 参数值
252
+
253
+ Returns:
254
+ 清理后的字符串
255
+ """
256
+ if value is None:
257
+ return ''
258
+
259
+ if not isinstance(value, str):
260
+ value = str(value)
261
+
262
+ # 移除危险字符(保留单引号用于表名/字段名,由调用方负责安全)
263
+ dangerous_chars = [';', '--', '/*', '*/', '\x00']
264
+ for char in dangerous_chars:
265
+ value = value.replace(char, '')
266
+
267
+ # 移除换行符和制表符(防止堆叠查询)
268
+ value = value.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
269
+
270
+ # 移除连续空格
271
+ value = re.sub(r'\s+', ' ', value)
272
+
273
+ return value.strip()
274
+
275
+ def process(self, sql: str, params: Dict[str, Any]) -> tuple:
276
+ """
277
+ 处理动态SQL模板
278
+
279
+ Args:
280
+ sql: 包含动态SQL标签的SQL模板
281
+ params: 参数值字典
282
+
283
+ Returns:
284
+ (处理后的SQL语句, 参数列表)
285
+ """
286
+ # <bind> must not mutate the caller-owned parameter mapping. This also
287
+ # makes a mapper call safe to reuse in retry and cache paths.
288
+ params = dict(params or {})
289
+
290
+ # ``foreach`` markers are bound together with normal ``#{}`` tokens at
291
+ # the end, preserving the exact left-to-right parameter order.
292
+ foreach_values = {}
293
+ foreach_index = 0
294
+
295
+ # Process MyBatis <bind/> declarations before structural tags. They
296
+ # only add derived values to the current execution's parameter scope.
297
+ processed_sql = self._process_bind_tags(sql, params)
298
+
299
+ # 循环处理直到没有标签
300
+ while True:
301
+ # 查找最内层的标签
302
+ tag_match = re.search(r'<(\w+)([^>]*)>(.*?)</\1>', processed_sql, re.DOTALL)
303
+ if not tag_match:
304
+ break
305
+
306
+ tag_name = tag_match.group(1)
307
+ tag_attrs = tag_match.group(2)
308
+ tag_content = tag_match.group(3)
309
+
310
+ if tag_name in self.tag_handlers:
311
+ # 处理标签
312
+ result = self.tag_handlers[tag_name](tag_attrs, tag_content, params)
313
+ if isinstance(result, tuple):
314
+ # foreach返回元组 (sql, params)
315
+ result_sql, collected_params = result
316
+ for value in collected_params:
317
+ marker = f"__PYMB_FOREACH_{foreach_index}__"
318
+ foreach_index += 1
319
+ foreach_values[marker] = value
320
+ result_sql = result_sql.replace(self.placeholder, marker, 1)
321
+ processed_sql = processed_sql.replace(tag_match.group(0), result_sql)
322
+ else:
323
+ processed_sql = processed_sql.replace(tag_match.group(0), result)
324
+ else:
325
+ raise ValueError(f"不支持的动态 SQL 标签: <{tag_name}>")
326
+
327
+ # 处理${}参数(字符串拼接)
328
+ processed_sql = self._process_raw_params(processed_sql, params)
329
+
330
+ # 处理#{}参数(预编译占位符)- 收集非foreach参数
331
+ processed_sql, param_order = self._process_prepared_params(
332
+ processed_sql,
333
+ params,
334
+ foreach_values,
335
+ )
336
+
337
+ # 清理多余的空格和逗号
338
+ processed_sql = self._clean_sql(processed_sql)
339
+
340
+ logger.debug(f"处理后的SQL: {processed_sql}, 参数: {param_order}")
341
+
342
+ return processed_sql, param_order
343
+
344
+ def _process_bind_tags(self, sql: str, params: Dict[str, Any]) -> str:
345
+ bind_pattern = re.compile(r'<bind\s+([^>]*?)/\s*>', flags=re.DOTALL)
346
+
347
+ def replace_bind(match: re.Match) -> str:
348
+ attrs = self._parse_attributes(match.group(1))
349
+ name = attrs.get('name')
350
+ expression = attrs.get('value')
351
+ if not name or expression is None:
352
+ raise ValueError("<bind> 必须同时设置 name 和 value")
353
+ if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name):
354
+ raise ValueError(f"<bind> 参数名无效: {name}")
355
+ params[name] = self._evaluate_value_expression(expression, params)
356
+ return ''
357
+
358
+ return bind_pattern.sub(replace_bind, sql)
359
+
360
+ def _parse_attributes(self, attrs_str: str) -> Dict[str, str]:
361
+ """
362
+ 解析标签属性
363
+
364
+ Args:
365
+ attrs_str: 属性字符串
366
+
367
+ Returns:
368
+ 属性字典
369
+ """
370
+ attrs = {}
371
+ # Keep the quote delimiter paired so a double-quoted attribute can
372
+ # contain the single quotes commonly used by OGNL <bind> expressions.
373
+ pattern = re.compile(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\')')
374
+ for name, double_quoted, single_quoted in pattern.findall(attrs_str):
375
+ attrs[name] = double_quoted if double_quoted != '' else single_quoted
376
+ return attrs
377
+
378
+ def _evaluate_expression(self, expression: str, params: Dict[str, Any]) -> bool:
379
+ """
380
+ 评估OGNL表达式(使用AST安全解析,避免eval()安全漏洞)
381
+
382
+ Args:
383
+ expression: OGNL表达式
384
+ params: 参数值字典
385
+
386
+ Returns:
387
+ 表达式结果(布尔值)
388
+ """
389
+ try:
390
+ return bool(self._evaluate_value_expression(expression, params))
391
+ except Exception:
392
+ return False
393
+
394
+ def _evaluate_value_expression(self, expression: str, params: Dict[str, Any]) -> Any:
395
+ """Evaluate the restricted OGNL subset used by ``<if>`` and ``<bind>``.
396
+
397
+ The execution namespace only contains mapper parameters and no Python
398
+ builtins. AST validation is deliberately performed before compilation;
399
+ this is not a general expression evaluator.
400
+ """
401
+ safe_expr = self._translate_ognl_to_python(expression)
402
+ tree = ast.parse(safe_expr, mode='eval')
403
+ self._validate_ast(tree)
404
+ code = compile(tree, '<expression>', 'eval')
405
+ return eval(code, {'__builtins__': {}}, dict(params))
406
+
407
+ def _translate_ognl_to_python(self, expression: str) -> str:
408
+ """
409
+ 将OGNL表达式转换为安全的Python表达式
410
+
411
+ Args:
412
+ expression: OGNL表达式
413
+
414
+ Returns:
415
+ 安全的Python表达式
416
+ """
417
+ expr = expression.strip()
418
+
419
+ # 将OGNL的null转换为Python的None
420
+ expr = expr.replace('null', 'None')
421
+
422
+ # 将OGNL方法调用转换为Python表达式
423
+ # isEmpty(value) -> not value
424
+ expr = re.sub(r'isEmpty\(([^)]+)\)', r'(not (\1))', expr)
425
+ # isNotEmpty(value) -> bool(value)
426
+ expr = re.sub(r'isNotEmpty\(([^)]+)\)', r'bool(\1)', expr)
427
+
428
+ # 将OGNL操作符转换为Python操作符(使用正则确保只替换独立单词)
429
+ expr = re.sub(r'\beq\b', '==', expr)
430
+ expr = re.sub(r'\bne\b', '!=', expr)
431
+ expr = re.sub(r'\blt\b', '<', expr)
432
+ expr = re.sub(r'\bgt\b', '>', expr)
433
+ expr = re.sub(r'\ble\b', '<=', expr)
434
+ expr = re.sub(r'\bge\b', '>=', expr)
435
+
436
+ return expr
437
+
438
+ def _validate_ast(self, tree: ast.AST) -> None:
439
+ """
440
+ 验证AST节点,确保没有危险操作
441
+
442
+ 只允许:
443
+ - 变量访问(参数)
444
+ - 常量(字符串、数字、None)
445
+ - 比较运算符(==, !=, <, >, <=, >=)
446
+ - 逻辑运算符(and, or, not)
447
+ - 属性访问(.)
448
+ - 索引访问([])
449
+
450
+ Args:
451
+ tree: AST节点
452
+
453
+ Raises:
454
+ SecurityError: 如果发现危险操作
455
+ """
456
+ allowed_nodes = (
457
+ ast.Expression, ast.BoolOp, ast.BinOp, ast.UnaryOp, ast.Compare,
458
+ ast.Name, ast.Load, ast.Constant, ast.Attribute, ast.Subscript,
459
+ ast.List, ast.Tuple, ast.Dict, ast.And, ast.Or, ast.Not,
460
+ ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In,
461
+ ast.NotIn, ast.Is, ast.IsNot, ast.Add, ast.Sub, ast.Mult,
462
+ ast.Div, ast.FloorDiv, ast.Mod, ast.USub, ast.UAdd, ast.Call,
463
+ )
464
+ for node in ast.walk(tree):
465
+ if not isinstance(node, allowed_nodes):
466
+ raise SecurityError(f"表达式包含不允许的语法: {ast.dump(node)}")
467
+ if isinstance(node, ast.Name) and node.id.startswith('__'):
468
+ raise SecurityError("表达式不能访问 dunder 名称")
469
+ if isinstance(node, ast.Attribute) and node.attr.startswith('__'):
470
+ raise SecurityError("表达式不能访问 dunder 属性")
471
+ # 禁止函数调用(除了内置的bool)
472
+ if isinstance(node, ast.Call):
473
+ if isinstance(node.func, ast.Name) and node.func.id == 'bool':
474
+ continue
475
+ raise SecurityError(f"表达式中禁止函数调用: {ast.dump(node)}")
476
+
477
+ # 禁止导入
478
+ if isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
479
+ raise SecurityError(f"表达式中禁止导入: {ast.dump(node)}")
480
+
481
+ # 禁止赋值
482
+ if isinstance(node, (ast.Assign, ast.AugAssign, ast.AnnAssign)):
483
+ raise SecurityError(f"表达式中禁止赋值: {ast.dump(node)}")
484
+
485
+ # 禁止删除
486
+ if isinstance(node, ast.Delete):
487
+ raise SecurityError(f"表达式中禁止删除操作: {ast.dump(node)}")
488
+
489
+ # 禁止执行语句
490
+ if isinstance(node, ast.Expr):
491
+ # ast.Exec 在Python 3.8+已移除,Expr用于语句模式,eval模式中不应该出现
492
+ raise SecurityError(f"表达式中禁止执行语句: {ast.dump(node)}")
493
+
494
+ # 禁止生成器表达式
495
+ if isinstance(node, ast.GeneratorExp):
496
+ raise SecurityError(f"表达式中禁止生成器: {ast.dump(node)}")
497
+
498
+ # 禁止lambda
499
+ if isinstance(node, ast.Lambda):
500
+ raise SecurityError(f"表达式中禁止lambda: {ast.dump(node)}")
501
+
502
+ # 禁止切片(防止复杂操作)
503
+ if isinstance(node, ast.Slice):
504
+ raise SecurityError(f"表达式中禁止切片: {ast.dump(node)}")
505
+
506
+ def _handle_if(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
507
+ """
508
+ 处理<if>标签
509
+
510
+ Args:
511
+ attrs: 属性字符串
512
+ content: 标签内容
513
+ params: 参数值字典
514
+
515
+ Returns:
516
+ 处理后的内容或空字符串
517
+ """
518
+ attrs_dict = self._parse_attributes(attrs)
519
+ test_expr = attrs_dict.get('test', '')
520
+
521
+ if self._evaluate_expression(test_expr, params):
522
+ # 保留#{}占位符,由主process方法统一处理
523
+ return content.strip()
524
+ return ''
525
+
526
+ def _handle_where(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
527
+ """
528
+ 处理<where>标签
529
+
530
+ Args:
531
+ attrs: 属性字符串
532
+ content: 标签内容
533
+ params: 参数值字典
534
+
535
+ Returns:
536
+ 处理后的WHERE子句
537
+ """
538
+ # 处理内容中的标签(保留#{}占位符)
539
+ temp_content = content
540
+
541
+ # 处理嵌套的if标签
542
+ while True:
543
+ if_match = re.search(r'<if([^>]*)>(.*?)</if>', temp_content, re.DOTALL)
544
+ if not if_match:
545
+ break
546
+ if_attrs = if_match.group(1)
547
+ if_content = if_match.group(2)
548
+ if_result = self._handle_if(if_attrs, if_content, params)
549
+ temp_content = temp_content.replace(if_match.group(0), if_result)
550
+
551
+ if not temp_content.strip():
552
+ return ''
553
+
554
+ # 移除开头的AND/OR
555
+ temp_content = re.sub(r'^\s*(AND|OR)\s+', '', temp_content.strip(), flags=re.IGNORECASE)
556
+
557
+ # 移除结尾的AND/OR
558
+ temp_content = re.sub(r'\s*(AND|OR)\s*$', '', temp_content.strip(), flags=re.IGNORECASE)
559
+
560
+ if temp_content:
561
+ return f'WHERE {temp_content}'
562
+ return ''
563
+
564
+ def _handle_foreach(self, attrs: str, content: str, params: Dict[str, Any]) -> tuple:
565
+ """
566
+ 处理<foreach>标签
567
+
568
+ Args:
569
+ attrs: 属性字符串
570
+ content: 标签内容
571
+ params: 参数值字典
572
+
573
+ Returns:
574
+ (处理后的内容, 收集的参数值列表)
575
+ """
576
+ attrs_dict = self._parse_attributes(attrs)
577
+ collection = attrs_dict.get('collection', '')
578
+ item = attrs_dict.get('item', 'item')
579
+ index = attrs_dict.get('index', 'index')
580
+ open_tag = attrs_dict.get('open', '')
581
+ close_tag = attrs_dict.get('close', '')
582
+ separator = attrs_dict.get('separator', ',')
583
+
584
+ # 获取集合值
585
+ if collection not in params:
586
+ return '', []
587
+
588
+ collection_value = params[collection]
589
+ if isinstance(collection_value, Mapping):
590
+ entries = list(collection_value.items())
591
+ elif isinstance(collection_value, Sequence) and not isinstance(collection_value, (str, bytes, bytearray)):
592
+ entries = list(enumerate(collection_value))
593
+ elif isinstance(collection_value, (set, frozenset)):
594
+ entries = list(enumerate(collection_value))
595
+ else:
596
+ raise ValueError(f"foreach collection 必须是序列、集合或映射: {collection}")
597
+
598
+ if len(entries) == 0:
599
+ return '', []
600
+
601
+ # 检查foreach数量限制(防止全表操作)
602
+ max_foreach_size = 1000
603
+ if len(entries) > max_foreach_size:
604
+ raise SecurityError(
605
+ f"foreach集合大小({len(entries)})超过限制({max_foreach_size}),"
606
+ "请分批处理"
607
+ )
608
+
609
+ # 收集参数值
610
+ collected_params = []
611
+
612
+ # 生成结果(展开#{}为占位符并收集参数)
613
+ results = []
614
+ for index_value, val in entries:
615
+ iteration_params = {**params, item: val, index: index_value}
616
+ item_content = self._render_foreach_conditions(content, iteration_params)
617
+
618
+ def replace_token(match: re.Match) -> str:
619
+ token_type, expression = match.group(1), match.group(2)
620
+ if expression == item:
621
+ value = val
622
+ name = item
623
+ elif expression.startswith(item + '.'):
624
+ value = self._get_value(val, expression[len(item) + 1:])
625
+ name = item
626
+ elif expression == index:
627
+ value = index_value
628
+ name = index
629
+ else:
630
+ return match.group(0)
631
+
632
+ if token_type == '#':
633
+ collected_params.append(value)
634
+ return self.placeholder
635
+ if not self._is_raw_param_allowed(name, value):
636
+ raise SecurityError(f"${{{expression}}} 不在白名单中")
637
+ return self._sanitize_raw_param(value)
638
+
639
+ item_content = re.sub(r'([#$])\{([^}]+)\}', replace_token, item_content)
640
+ results.append(item_content.strip())
641
+
642
+ return f'{open_tag}{separator.join(results)}{close_tag}', collected_params
643
+
644
+ def _render_foreach_conditions(self, content: str, params: Dict[str, Any]) -> str:
645
+ """Resolve conditional tags inside one foreach iteration before binding."""
646
+ rendered = content
647
+ while True:
648
+ if_match = re.search(r'<if([^>]*)>(.*?)</if>', rendered, re.DOTALL)
649
+ if not if_match:
650
+ break
651
+ replacement = self._handle_if(if_match.group(1), if_match.group(2), params)
652
+ rendered = rendered.replace(if_match.group(0), replacement, 1)
653
+ while True:
654
+ choose_match = re.search(r'<choose([^>]*)>(.*?)</choose>', rendered, re.DOTALL)
655
+ if not choose_match:
656
+ break
657
+ replacement = self._handle_choose(
658
+ choose_match.group(1), choose_match.group(2), params
659
+ )
660
+ rendered = rendered.replace(choose_match.group(0), replacement, 1)
661
+ return rendered
662
+
663
+ def _handle_choose(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
664
+ """
665
+ 处理<choose>标签
666
+
667
+ Args:
668
+ attrs: 属性字符串
669
+ content: 标签内容
670
+ params: 参数值字典
671
+
672
+ Returns:
673
+ 处理后的内容
674
+ """
675
+ # 处理when标签
676
+ when_pattern = re.compile(r'<when([^>]*)>(.*?)</when>', re.DOTALL)
677
+ when_matches = when_pattern.findall(content)
678
+
679
+ for when_attrs, when_content in when_matches:
680
+ when_attrs_dict = self._parse_attributes(when_attrs)
681
+ test_expr = when_attrs_dict.get('test', '')
682
+
683
+ if self._evaluate_expression(test_expr, params):
684
+ # Leave #{} placeholders for the outer binding pass so the
685
+ # selected branch contributes values in final SQL order.
686
+ return when_content.strip()
687
+
688
+ # 处理otherwise标签
689
+ otherwise_pattern = re.compile(r'<otherwise>(.*?)</otherwise>', re.DOTALL)
690
+ otherwise_match = otherwise_pattern.search(content)
691
+ if otherwise_match:
692
+ return otherwise_match.group(1).strip()
693
+
694
+ return ''
695
+
696
+ def _handle_when(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
697
+ """
698
+ 处理<when>标签(由choose标签调用)
699
+
700
+ Args:
701
+ attrs: 属性字符串
702
+ content: 标签内容
703
+ params: 参数值字典
704
+
705
+ Returns:
706
+ 处理后的内容
707
+ """
708
+ return content
709
+
710
+ def _handle_otherwise(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
711
+ """
712
+ 处理<otherwise>标签(由choose标签调用)
713
+
714
+ Args:
715
+ attrs: 属性字符串
716
+ content: 标签内容
717
+ params: 参数值字典
718
+
719
+ Returns:
720
+ 处理后的内容
721
+ """
722
+ return content
723
+
724
+ def _handle_set(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
725
+ """
726
+ 处理<set>标签
727
+
728
+ Args:
729
+ attrs: 属性字符串
730
+ content: 标签内容
731
+ params: 参数值字典
732
+
733
+ Returns:
734
+ 处理后的SET子句
735
+ """
736
+ # 处理内容中的标签(不收集参数)
737
+ temp_content = content
738
+
739
+ # 处理嵌套的if标签
740
+ while True:
741
+ if_match = re.search(r'<if([^>]*)>(.*?)</if>', temp_content, re.DOTALL)
742
+ if not if_match:
743
+ break
744
+ if_attrs = if_match.group(1)
745
+ if_content = if_match.group(2)
746
+ if_result = self._handle_if(if_attrs, if_content, params)
747
+ temp_content = temp_content.replace(if_match.group(0), if_result)
748
+
749
+ if not temp_content.strip():
750
+ return ''
751
+
752
+ # 移除结尾的逗号
753
+ temp_content = re.sub(r',\s*$', '', temp_content.strip())
754
+
755
+ if temp_content:
756
+ return f'SET {temp_content}'
757
+ return ''
758
+
759
+ def _handle_trim(self, attrs: str, content: str, params: Dict[str, Any]) -> str:
760
+ """
761
+ 处理<trim>标签
762
+
763
+ Args:
764
+ attrs: 属性字符串
765
+ content: 标签内容
766
+ params: 参数值字典
767
+
768
+ Returns:
769
+ 处理后的内容
770
+ """
771
+ attrs_dict = self._parse_attributes(attrs)
772
+ prefix = attrs_dict.get('prefix', '')
773
+ suffix = attrs_dict.get('suffix', '')
774
+ prefix_overrides = attrs_dict.get('prefixOverrides', '')
775
+ suffix_overrides = attrs_dict.get('suffixOverrides', '')
776
+
777
+ processed_content = content
778
+ # The main loop encounters outer trim before its child if tags. Resolve
779
+ # those conditions here, but leave #{} untouched for ordered binding.
780
+ while True:
781
+ if_match = re.search(r'<if([^>]*)>(.*?)</if>', processed_content, re.DOTALL)
782
+ if not if_match:
783
+ break
784
+ if_result = self._handle_if(
785
+ if_match.group(1), if_match.group(2), params
786
+ )
787
+ processed_content = processed_content.replace(
788
+ if_match.group(0), if_result
789
+ )
790
+
791
+ if not processed_content.strip():
792
+ return ''
793
+
794
+ # 移除前缀覆盖
795
+ if prefix_overrides:
796
+ for override in prefix_overrides.split('|'):
797
+ token = override.strip()
798
+ if token:
799
+ processed_content = re.sub(
800
+ r'^\s*' + re.escape(token) + r'(?=\s|$)',
801
+ '',
802
+ processed_content,
803
+ count=1,
804
+ flags=re.IGNORECASE,
805
+ )
806
+
807
+ # 移除后缀覆盖
808
+ if suffix_overrides:
809
+ for override in suffix_overrides.split('|'):
810
+ token = override.strip()
811
+ if token:
812
+ processed_content = re.sub(
813
+ re.escape(token) + r'\s*$',
814
+ '',
815
+ processed_content,
816
+ count=1,
817
+ flags=re.IGNORECASE,
818
+ )
819
+
820
+ result = processed_content.strip()
821
+ if prefix:
822
+ prefix = prefix.strip()
823
+ separator = '' if prefix.endswith(('(', '[', '{')) else ' '
824
+ result = f'{prefix}{separator}{result}'
825
+ if suffix:
826
+ suffix = suffix.strip()
827
+ separator = '' if suffix.startswith((')', ']', '}', ',', ';')) else ' '
828
+ result = f'{result}{separator}{suffix}'
829
+ return result
830
+
831
+ def _clean_sql(self, sql: str) -> str:
832
+ """
833
+ 清理SQL语句
834
+
835
+ Args:
836
+ sql: SQL语句
837
+
838
+ Returns:
839
+ 清理后的SQL语句
840
+ """
841
+ # 移除多余的空格
842
+ sql = re.sub(r'\s+', ' ', sql)
843
+
844
+ # 移除多余的逗号
845
+ sql = re.sub(r',\s*,', ',', sql)
846
+
847
+ # 移除WHERE/HAVING子句中多余的AND/OR
848
+ sql = re.sub(r'(WHERE|HAVING)\s+(AND|OR)\s+', r'\1 ', sql, flags=re.IGNORECASE)
849
+
850
+ # 转义 % 字符以兼容 pymysql 的 % 格式化
851
+ # pymysql 使用 Python % 操作符进行参数绑定,SQL 字面量中的 % 必须转义为 %%
852
+ sql = self._escape_mysql_percent(sql)
853
+
854
+ return sql.strip()
855
+
856
+ def _escape_mysql_percent(self, sql: str) -> str:
857
+ """将 SQL 字面量中的 % 转义为 %% 以兼容 pymysql 的 % 格式化。
858
+
859
+ pymysql cursor.execute() 内部使用 Python 的 % 格式化,
860
+ 如果 SQL 中包含字面量 %(如 LIKE '%%keyword%%'、CONCAT('%%', ...)),
861
+ 会引发 ValueError。此方法将 #{} 占位符替换生成的 %s 保留不动,
862
+ 其他所有 % 替换为 %%。
863
+
864
+ Args:
865
+ sql: 处理后的 SQL(已包含 %s 占位符)
866
+
867
+ Returns:
868
+ 转义后的 SQL
869
+ """
870
+ if self.placeholder != '%s':
871
+ return sql
872
+ # 用唯一标记替换所有 %s 占位符 → 转义剩余 % → 恢复 %s
873
+ marker = '\x00PYM_PH\x00'
874
+ sql = sql.replace('%s', marker)
875
+ sql = sql.replace('%', '%%')
876
+ sql = sql.replace(marker, '%s')
877
+ return sql
878
+
879
+
880
+ class SecurityError(Exception):
881
+ """安全异常"""
882
+ pass
883
+
884
+
885
+ # 全局默认处理器实例
886
+ DEFAULT_PROCESSOR = DynamicSQLProcessor()
887
+
888
+
889
+ def process_dynamic_sql(sql: str, params: Dict[str, Any]) -> tuple:
890
+ """
891
+ 便捷函数:处理动态SQL
892
+
893
+ Args:
894
+ sql: 包含动态SQL标签的SQL模板
895
+ params: 参数值字典
896
+
897
+ Returns:
898
+ (处理后的SQL语句, 参数列表)
899
+ """
900
+ return DEFAULT_PROCESSOR.process(sql, params)