quantum-framework 0.9.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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,304 @@
1
+ """
2
+ Quantum Query Validators - Type validation and conversion for query parameters
3
+ """
4
+
5
+ from typing import Any, Tuple, Optional
6
+ from datetime import datetime, date, time as datetime_time
7
+ import json
8
+
9
+
10
+ class QueryValidationError(Exception):
11
+ """Raised when query parameter validation fails"""
12
+ pass
13
+
14
+
15
+ class QueryValidator:
16
+ """Validates and converts query parameters"""
17
+
18
+ @staticmethod
19
+ def validate_param(value: Any, param_type: str, attributes: dict) -> Any:
20
+ """
21
+ Validate and convert parameter value
22
+
23
+ Args:
24
+ value: Parameter value
25
+ param_type: Parameter type (string, integer, decimal, boolean, datetime, etc.)
26
+ attributes: Additional constraints (maxLength, scale, null)
27
+
28
+ Returns:
29
+ Converted and validated value
30
+
31
+ Raises:
32
+ QueryValidationError: If validation fails
33
+ """
34
+ # Check null
35
+ null_allowed = attributes.get('null', False)
36
+ if value is None:
37
+ if not null_allowed:
38
+ raise QueryValidationError("Parameter cannot be null")
39
+ return None
40
+
41
+ # Convert based on type
42
+ try:
43
+ if param_type == 'string':
44
+ return QueryValidator._validate_string(value, attributes)
45
+ elif param_type == 'integer':
46
+ return QueryValidator._validate_integer(value, attributes)
47
+ elif param_type == 'decimal':
48
+ return QueryValidator._validate_decimal(value, attributes)
49
+ elif param_type == 'boolean':
50
+ return QueryValidator._validate_boolean(value, attributes)
51
+ elif param_type == 'datetime':
52
+ return QueryValidator._validate_datetime(value, attributes)
53
+ elif param_type == 'date':
54
+ return QueryValidator._validate_date(value, attributes)
55
+ elif param_type == 'time':
56
+ return QueryValidator._validate_time(value, attributes)
57
+ elif param_type == 'array':
58
+ return QueryValidator._validate_array(value, attributes)
59
+ elif param_type == 'json':
60
+ return QueryValidator._validate_json(value, attributes)
61
+ else:
62
+ raise QueryValidationError(f"Unknown parameter type: {param_type}")
63
+
64
+ except QueryValidationError:
65
+ raise
66
+ except Exception as e:
67
+ raise QueryValidationError(f"Validation error: {e}")
68
+
69
+ @staticmethod
70
+ def _validate_string(value: Any, attributes: dict) -> str:
71
+ """Validate and convert to string"""
72
+ str_value = str(value)
73
+
74
+ # Check maxLength
75
+ max_length = attributes.get('max_length')
76
+ if max_length and len(str_value) > max_length:
77
+ raise QueryValidationError(
78
+ f"String length {len(str_value)} exceeds maximum {max_length}"
79
+ )
80
+
81
+ return str_value
82
+
83
+ @staticmethod
84
+ def _validate_integer(value: Any, attributes: dict) -> int:
85
+ """Validate and convert to integer"""
86
+ if isinstance(value, int):
87
+ return value
88
+
89
+ if isinstance(value, str):
90
+ # Remove whitespace
91
+ value = value.strip()
92
+
93
+ try:
94
+ return int(value)
95
+ except (ValueError, TypeError):
96
+ raise QueryValidationError(f"Cannot convert '{value}' to integer")
97
+
98
+ @staticmethod
99
+ def _validate_decimal(value: Any, attributes: dict) -> float:
100
+ """Validate and convert to decimal (float)"""
101
+ if isinstance(value, (int, float)):
102
+ float_value = float(value)
103
+ elif isinstance(value, str):
104
+ try:
105
+ float_value = float(value.strip())
106
+ except ValueError:
107
+ raise QueryValidationError(f"Cannot convert '{value}' to decimal")
108
+ else:
109
+ raise QueryValidationError(f"Cannot convert '{value}' to decimal")
110
+
111
+ # Apply scale if specified
112
+ scale = attributes.get('scale')
113
+ if scale is not None:
114
+ float_value = round(float_value, scale)
115
+
116
+ return float_value
117
+
118
+ @staticmethod
119
+ def _validate_boolean(value: Any, attributes: dict) -> bool:
120
+ """Validate and convert to boolean"""
121
+ if isinstance(value, bool):
122
+ return value
123
+
124
+ if isinstance(value, str):
125
+ value_lower = value.strip().lower()
126
+ if value_lower in ['true', '1', 'yes', 'on']:
127
+ return True
128
+ elif value_lower in ['false', '0', 'no', 'off']:
129
+ return False
130
+ else:
131
+ raise QueryValidationError(f"Cannot convert '{value}' to boolean")
132
+
133
+ if isinstance(value, (int, float)):
134
+ return bool(value)
135
+
136
+ raise QueryValidationError(f"Cannot convert '{value}' to boolean")
137
+
138
+ @staticmethod
139
+ def _validate_datetime(value: Any, attributes: dict) -> datetime:
140
+ """Validate and convert to datetime"""
141
+ if isinstance(value, datetime):
142
+ return value
143
+
144
+ if isinstance(value, str):
145
+ # Try common datetime formats
146
+ formats = [
147
+ '%Y-%m-%d %H:%M:%S',
148
+ '%Y-%m-%d %H:%M:%S.%f',
149
+ '%Y-%m-%dT%H:%M:%S',
150
+ '%Y-%m-%dT%H:%M:%S.%f',
151
+ '%Y-%m-%dT%H:%M:%SZ',
152
+ '%Y-%m-%dT%H:%M:%S.%fZ',
153
+ ]
154
+
155
+ for fmt in formats:
156
+ try:
157
+ return datetime.strptime(value.strip(), fmt)
158
+ except ValueError:
159
+ continue
160
+
161
+ raise QueryValidationError(
162
+ f"Cannot parse datetime '{value}'. Expected format: YYYY-MM-DD HH:MM:SS"
163
+ )
164
+
165
+ raise QueryValidationError(f"Cannot convert '{value}' to datetime")
166
+
167
+ @staticmethod
168
+ def _validate_date(value: Any, attributes: dict) -> date:
169
+ """Validate and convert to date"""
170
+ if isinstance(value, date):
171
+ return value
172
+
173
+ if isinstance(value, datetime):
174
+ return value.date()
175
+
176
+ if isinstance(value, str):
177
+ # Try common date formats
178
+ formats = ['%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y']
179
+
180
+ for fmt in formats:
181
+ try:
182
+ return datetime.strptime(value.strip(), fmt).date()
183
+ except ValueError:
184
+ continue
185
+
186
+ raise QueryValidationError(
187
+ f"Cannot parse date '{value}'. Expected format: YYYY-MM-DD"
188
+ )
189
+
190
+ raise QueryValidationError(f"Cannot convert '{value}' to date")
191
+
192
+ @staticmethod
193
+ def _validate_time(value: Any, attributes: dict) -> datetime_time:
194
+ """Validate and convert to time"""
195
+ if isinstance(value, datetime_time):
196
+ return value
197
+
198
+ if isinstance(value, datetime):
199
+ return value.time()
200
+
201
+ if isinstance(value, str):
202
+ # Try common time formats
203
+ formats = ['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
204
+
205
+ for fmt in formats:
206
+ try:
207
+ return datetime.strptime(value.strip(), fmt).time()
208
+ except ValueError:
209
+ continue
210
+
211
+ raise QueryValidationError(
212
+ f"Cannot parse time '{value}'. Expected format: HH:MM:SS"
213
+ )
214
+
215
+ raise QueryValidationError(f"Cannot convert '{value}' to time")
216
+
217
+ @staticmethod
218
+ def _validate_array(value: Any, attributes: dict) -> list:
219
+ """Validate and convert to array"""
220
+ if isinstance(value, list):
221
+ return value
222
+
223
+ if isinstance(value, tuple):
224
+ return list(value)
225
+
226
+ if isinstance(value, str):
227
+ # Try to parse as JSON
228
+ try:
229
+ parsed = json.loads(value)
230
+ if isinstance(parsed, list):
231
+ return parsed
232
+ else:
233
+ raise QueryValidationError("JSON value is not an array")
234
+ except json.JSONDecodeError:
235
+ # Try comma-separated values
236
+ return [item.strip() for item in value.split(',')]
237
+
238
+ raise QueryValidationError(f"Cannot convert '{value}' to array")
239
+
240
+ @staticmethod
241
+ def _validate_json(value: Any, attributes: dict) -> dict:
242
+ """Validate and convert to JSON object"""
243
+ if isinstance(value, dict):
244
+ return value
245
+
246
+ if isinstance(value, str):
247
+ try:
248
+ return json.loads(value)
249
+ except json.JSONDecodeError as e:
250
+ raise QueryValidationError(f"Invalid JSON: {e}")
251
+
252
+ raise QueryValidationError(f"Cannot convert '{value}' to JSON")
253
+
254
+ @staticmethod
255
+ def sanitize_sql(sql: str) -> str:
256
+ """
257
+ Basic SQL sanitization to prevent comment injection and other attacks
258
+
259
+ Args:
260
+ sql: SQL query string
261
+
262
+ Returns:
263
+ Sanitized SQL
264
+
265
+ Note:
266
+ This is a basic check. Parameter binding is the primary defense against SQL injection.
267
+ """
268
+ import re
269
+
270
+ # A regex denylist over SQL text used to live here, matching
271
+ # ';.*DROP TABLE', ';.*DELETE FROM', ';.*INSERT INTO' and
272
+ # 'UNION.*SELECT'. It was removed because it was worse than nothing:
273
+ #
274
+ # BLOCKED "SELECT name FROM a UNION SELECT email FROM b" (legitimate)
275
+ # ALLOWED "SELECT 1 ... UNION\nSELECT password FROM users --"
276
+ # ALLOWED "SELECT 1;\nDROP TABLE users"
277
+ #
278
+ # `.` does not match a newline, so one line break walked past all four
279
+ # patterns — while a perfectly ordinary UNION query was rejected with
280
+ # "SQL contains suspicious pattern". It broke real queries and stopped
281
+ # nothing, which is the shape of defence SECURITY.md already argues
282
+ # against.
283
+ #
284
+ # It was also guarding a threat this architecture prevents by
285
+ # construction, verified by execution: node.sql is NEVER databound
286
+ # (the executor passes it through untouched), q:param values are bound
287
+ # by the driver, and page/page_size are ints read from the template at
288
+ # parse time. A '{form.name}' written into the SQL body reaches the
289
+ # database as literal text and matches nothing.
290
+ #
291
+ # Which is the real problem worth catching, and what this now does.
292
+ # That silent empty result is the mistake people actually make, and it
293
+ # used to give them no signal at all.
294
+ placeholder = re.search(r'\{[^}\n]+\}', sql)
295
+ if placeholder:
296
+ raise QueryValidationError(
297
+ f"SQL contains the databinding placeholder {placeholder.group(0)}. "
298
+ f"Values are not interpolated into SQL — that is what makes "
299
+ f"injection impossible here. Use a bound parameter instead:\n"
300
+ f" WHERE col = :name\n"
301
+ f" <q:param name=\"name\" value=\"{placeholder.group(0)}\" />"
302
+ )
303
+
304
+ return sql