55import inspect
66import json
77from concurrent .futures import Executor , Future , ThreadPoolExecutor
8- from dataclasses import asdict , replace
8+ from dataclasses import asdict , dataclass , replace
99from decimal import Decimal
1010from pathlib import Path
1111from typing import Any , cast
5454from sqlbuild .sql_values .types import SqlValueKind
5555
5656
57+ @dataclass (frozen = True )
58+ class _PreparedNativeRequest :
59+ identity : str | None
60+ reused : str | None
61+ request_text : str | None
62+
63+
5764def evaluate_native (
5865 * ,
5966 project : CompiledProject ,
@@ -74,31 +81,20 @@ def evaluate_native(
7481 if custom_payloads is None
7582 else custom_payloads
7683 )
77- request : dict [str , object ] = {
78- "version" : RULES_NATIVE_API_VERSION ,
79- "project_dir" : str (project_dir .resolve ()),
80- "dialect" : dialect ,
81- "config" : _config_payload (config ),
82- "models" : _model_payloads (
83- project = project ,
84- dialect = dialect ,
85- include_type_proof = any (
86- _rule_selected (config = config , code = code ) for code in TYPE_PROOF_RULE_CODES
87- ),
84+ prepared : _PreparedNativeRequest = _prepare_native_request (
85+ request_json = _serialise_native_request (
86+ _native_request (
87+ project = project ,
88+ config = config ,
89+ project_dir = project_dir ,
90+ dialect = dialect ,
91+ initial_findings = initial_findings ,
92+ custom_payloads = payloads ,
93+ )
8894 ),
89- "sql_tests" : _sql_test_payloads (project ),
90- "sql_scenarios" : _sql_scenario_payloads (project ),
91- "public_enums" : [
92- _enum_payload (declaration ) for declaration in project .public_enums .values ()
93- ],
94- "public_constants" : [
95- _constant_payload (declaration ) for declaration in project .public_constants .values ()
96- ],
97- "scope_index" : scope_metadata_projection (index = project .scope_index ),
98- "initial_findings" : [_finding_payload (finding ) for finding in initial_findings ],
99- "defer_suppressions" : True ,
100- "custom_rules" : payloads ,
101- }
95+ project_dir = project_dir ,
96+ cache_enabled = config .cache .enabled ,
97+ )
10298 with ThreadPoolExecutor (max_workers = 1 , thread_name_prefix = "sqlbuild-custom-rules" ) as pool :
10399 custom_future : Future [CustomRulesOutcome ] | None = (
104100 custom_outcome
@@ -114,15 +110,14 @@ def evaluate_native(
114110 verify_determinism = verify_determinism ,
115111 )
116112 )
113+ reused : bool = prepared .reused is not None
117114 try :
118- response_json : str
119- reused : bool
120- response_json , reused = _evaluate_request (
121- request = request , project_dir = project_dir , cache_enabled = config .cache .enabled
115+ response : object = orjson .loads (
116+ _evaluate_request (prepared = prepared , project_dir = project_dir )
122117 )
123- response : object = orjson .loads (response_json )
124118 except (ValueError , TypeError ) as error :
125119 raise RulesError (str (error )) from error
120+ del prepared
126121 custom : CustomRulesOutcome = (
127122 CustomRulesOutcome (findings = (), cache_hits = 0 , cache_misses = 0 , custom_ms = 0 )
128123 if custom_future is None
@@ -159,6 +154,42 @@ def evaluate_native(
159154 )
160155
161156
157+ def _native_request (
158+ * ,
159+ project : CompiledProject ,
160+ config : RulesConfig ,
161+ project_dir : Path ,
162+ dialect : str ,
163+ initial_findings : tuple [Finding , ...],
164+ custom_payloads : list [dict [str , object ]],
165+ ) -> dict [str , object ]:
166+ return {
167+ "version" : RULES_NATIVE_API_VERSION ,
168+ "project_dir" : str (project_dir .resolve ()),
169+ "dialect" : dialect ,
170+ "config" : _config_payload (config ),
171+ "models" : _model_payloads (
172+ project = project ,
173+ dialect = dialect ,
174+ include_type_proof = any (
175+ _rule_selected (config = config , code = code ) for code in TYPE_PROOF_RULE_CODES
176+ ),
177+ ),
178+ "sql_tests" : _sql_test_payloads (project ),
179+ "sql_scenarios" : _sql_scenario_payloads (project ),
180+ "public_enums" : [
181+ _enum_payload (declaration ) for declaration in project .public_enums .values ()
182+ ],
183+ "public_constants" : [
184+ _constant_payload (declaration ) for declaration in project .public_constants .values ()
185+ ],
186+ "scope_index" : scope_metadata_projection (index = project .scope_index ),
187+ "initial_findings" : [_finding_payload (finding ) for finding in initial_findings ],
188+ "defer_suppressions" : True ,
189+ "custom_rules" : custom_payloads ,
190+ }
191+
192+
162193def start_custom_rules (
163194 * ,
164195 executor : Executor ,
@@ -206,19 +237,42 @@ def _selected_custom_rules(
206237 return tuple (rule for rule in catalogue if rule .custom and rule .code in selected )
207238
208239
209- def _evaluate_request (
210- * , request : dict [str , object ], project_dir : Path , cache_enabled : bool
211- ) -> tuple [str , bool ]:
212- request_json : bytes = orjson .dumps (request , option = orjson .OPT_SORT_KEYS , default = str )
213- if not cache_enabled :
214- return _native .evaluate_json (request_json .decode ()), False
215- identity : str = native_request_identity (request_json )
216- reused : str | None = read_native_response (project_dir = project_dir , identity = identity )
217- if reused is not None :
218- return reused , True
219- response : str = _native .evaluate_json (request_json .decode ())
220- write_native_response (project_dir = project_dir , identity = identity , response = response )
221- return response , False
240+ def _serialise_native_request (request : dict [str , object ]) -> bytes :
241+ """Encode the native request, reporting values the encoder rejects as rules errors."""
242+
243+ try :
244+ return orjson .dumps (request , option = orjson .OPT_SORT_KEYS , default = str )
245+ except orjson .JSONEncodeError as error :
246+ raise RulesError (str (error )) from error
247+
248+
249+ def _prepare_native_request (
250+ * , request_json : bytes , project_dir : Path , cache_enabled : bool
251+ ) -> _PreparedNativeRequest :
252+ """Resolve the memo and decode once; the caller's bytes die when this returns."""
253+
254+ identity : str | None = native_request_identity (request_json ) if cache_enabled else None
255+ reused : str | None = (
256+ read_native_response (project_dir = project_dir , identity = identity )
257+ if identity is not None
258+ else None
259+ )
260+ return _PreparedNativeRequest (
261+ identity = identity ,
262+ reused = reused ,
263+ request_text = request_json .decode () if reused is None else None ,
264+ )
265+
266+
267+ def _evaluate_request (* , prepared : _PreparedNativeRequest , project_dir : Path ) -> str :
268+ if prepared .reused is not None :
269+ return prepared .reused
270+ response : str = _native .evaluate_json (cast (str , prepared .request_text ))
271+ if prepared .identity is not None :
272+ write_native_response (
273+ project_dir = project_dir , identity = prepared .identity , response = response
274+ )
275+ return response
222276
223277
224278def finalize_native_findings (
0 commit comments