11#!/usr/bin/env python3
22
33import argparse
4+ import copy
45import json
6+ import os
7+ from pathlib import Path
58
69if __package__ :
710 from .common import atomic_write_text , read_jsonl , statement_id
811else :
912 from common import atomic_write_text , read_jsonl , statement_id
1013
1114
15+ VALID_RESULTS = frozenset (
16+ (
17+ "DEEP_SUPPORTED" ,
18+ "CLASSIFIED_ONLY" ,
19+ "PARTIAL" ,
20+ "ERROR" ,
21+ "TRAILING_INPUT" ,
22+ "TYPE_MISMATCH" ,
23+ "ORACLE_REJECTED" ,
24+ )
25+ )
1226REQUIRED_INVENTORY_FIELDS = (
1327 "result" ,
1428 "normalized_sql" ,
2034)
2135
2236
23- def _validate_row (row , row_index , required_fields ):
37+ def _validate_row_object (row , row_index ):
2438 if not isinstance (row , dict ):
2539 raise ValueError (f"row { row_index } : expected a JSON object" )
2640
41+ for field in row :
42+ if not isinstance (field , str ):
43+ raise ValueError (
44+ f"row { row_index } : field name { field !r} must be a string"
45+ )
46+
47+
48+ def _validate_required_fields (row , row_index , required_fields ):
2749 for field in required_fields :
2850 if field not in row :
2951 raise ValueError (
3052 f"row { row_index } : missing required field { field !r} "
3153 )
3254
3355
34- def _serialized_row_key (row , row_index ):
35- try :
36- return json .dumps (
37- row ,
38- ensure_ascii = False ,
39- separators = ("," , ":" ),
40- sort_keys = True ,
56+ def _validate_string_field (row , row_index , field ):
57+ if not isinstance (row [field ], str ):
58+ raise ValueError (
59+ f"row { row_index } : field { field !r} must be a string"
4160 )
42- except (TypeError , ValueError ) as error :
61+
62+
63+ def _validate_integer_field (row , row_index , field , minimum ):
64+ value = row [field ]
65+ if isinstance (value , bool ) or not isinstance (value , int ):
66+ raise ValueError (
67+ f"row { row_index } : field { field !r} must be an integer"
68+ )
69+ if value < minimum :
4370 raise ValueError (
44- f"row { row_index } : not JSON serializable: { error } "
45- ) from error
71+ f"row { row_index } : field { field !r} must be at least { minimum } "
72+ )
73+
74+
75+ def _strict_json_dumps (value ):
76+ return json .dumps (
77+ value ,
78+ allow_nan = False ,
79+ ensure_ascii = False ,
80+ separators = ("," , ":" ),
81+ sort_keys = True ,
82+ )
83+
84+
85+ def _validate_json_fields (row , row_index ):
86+ for field , value in row .items ():
87+ try :
88+ serialized = _strict_json_dumps (value )
89+ except (OverflowError , RecursionError , TypeError , ValueError ) as error :
90+ raise ValueError (
91+ f"row { row_index } : field { field !r} is not strict JSON "
92+ f"serializable: { error } "
93+ ) from error
94+
95+ try :
96+ serialized .encode ("utf-8" )
97+ except UnicodeError as error :
98+ raise ValueError (
99+ f"row { row_index } : field { field !r} is not UTF-8 encodable: "
100+ f"{ error } "
101+ ) from error
102+
103+
104+ def _validate_result (row , row_index ):
105+ _validate_required_fields (row , row_index , ("result" ,))
106+ _validate_string_field (row , row_index , "result" )
107+ if row ["result" ] not in VALID_RESULTS :
108+ raise ValueError (
109+ f"row { row_index } : field 'result' has unsupported value "
110+ f"{ row ['result' ]!r} "
111+ )
112+
113+
114+ def _validate_optional_provenance (row , row_index ):
115+ for field in ("branch" , "commit" ):
116+ if field in row :
117+ _validate_string_field (row , row_index , field )
118+
119+
120+ def _validate_accepted_row (row , row_index ):
121+ _validate_row_object (row , row_index )
122+ _validate_required_fields (row , row_index , REQUIRED_INVENTORY_FIELDS )
123+ _validate_result (row , row_index )
124+ if row ["result" ] == "ORACLE_REJECTED" :
125+ raise ValueError (
126+ f"row { row_index } : field 'result' must describe an accepted row"
127+ )
128+
129+ for field in ("normalized_sql" , "oracle_node" , "source_file" , "sql" ):
130+ _validate_string_field (row , row_index , field )
131+ _validate_integer_field (row , row_index , "offset" , 0 )
132+ _validate_integer_field (row , row_index , "line" , 1 )
133+ _validate_optional_provenance (row , row_index )
134+ _validate_json_fields (row , row_index )
135+
136+
137+ def _validate_diagnostic_row (row , row_index ):
138+ _validate_row_object (row , row_index )
139+ _validate_result (row , row_index )
140+ if row ["result" ] != "ORACLE_REJECTED" :
141+ raise ValueError (
142+ f"row { row_index } : field 'result' must be 'ORACLE_REJECTED'"
143+ )
144+
145+ for field in ("source_file" , "sql" , "oracle_error" ):
146+ if field in row :
147+ _validate_string_field (row , row_index , field )
148+ if "offset" in row :
149+ _validate_integer_field (row , row_index , "offset" , 0 )
150+ if "line" in row :
151+ _validate_integer_field (row , row_index , "line" , 1 )
152+ _validate_optional_provenance (row , row_index )
153+ _validate_json_fields (row , row_index )
154+
155+
156+ def _validate_and_classify_row (row , row_index ):
157+ _validate_row_object (row , row_index )
158+ _validate_result (row , row_index )
159+ if row ["result" ] == "ORACLE_REJECTED" :
160+ _validate_diagnostic_row (row , row_index )
161+ return "diagnostic"
162+
163+ _validate_accepted_row (row , row_index )
164+ return "accepted"
165+
166+
167+ def _serialized_row_key (row ):
168+ try :
169+ return _strict_json_dumps (row )
170+ except (OverflowError , RecursionError , TypeError , ValueError ) as error :
171+ raise ValueError (f"row is not strict JSON serializable: { error } " ) from error
46172
47173
48174def partition_rows (rows ):
49175 accepted_rows = []
50176 diagnostics = []
51177
52178 for row_index , row in enumerate (rows ):
53- _validate_row (row , row_index , ("result" ,))
54- if row ["result" ] == "ORACLE_REJECTED" :
55- diagnostics .append (row )
179+ classification = _validate_and_classify_row (row , row_index )
180+ row_copy = copy .deepcopy (row )
181+ if classification == "diagnostic" :
182+ diagnostics .append (row_copy )
56183 else :
57- accepted_rows .append (row )
184+ accepted_rows .append (row_copy )
58185
59186 return accepted_rows , diagnostics
60187
@@ -63,19 +190,18 @@ def build_inventory(rows):
63190 groups = {}
64191
65192 for row_index , row in enumerate (rows ):
66- _validate_row (row , row_index , ( "result" ,) )
67- if row [ "result" ] == "ORACLE_REJECTED " :
193+ classification = _validate_and_classify_row (row , row_index )
194+ if classification == "diagnostic " :
68195 continue
69196
70- _validate_row ( row , row_index , REQUIRED_INVENTORY_FIELDS )
71- group_key = (row ["oracle_node" ], row ["normalized_sql" ])
197+ row_copy = copy . deepcopy ( row )
198+ group_key = (row_copy ["oracle_node" ], row_copy ["normalized_sql" ])
72199 occurrence_key = (
73- row ["source_file" ],
74- row ["offset" ],
75- row ["line" ],
200+ row_copy ["source_file" ],
201+ row_copy ["offset" ],
202+ row_copy ["line" ],
76203 )
77- row_copy = dict (row )
78- serialized_key = _serialized_row_key (row_copy , row_index )
204+ serialized_key = _serialized_row_key (row_copy )
79205 occurrences = groups .setdefault (group_key , {})
80206 existing = occurrences .get (occurrence_key )
81207 if existing is None or serialized_key < existing [0 ]:
@@ -112,18 +238,45 @@ def build_inventory(rows):
112238
113239
114240def _jsonl_text (rows ):
115- serialized_rows = [
116- json .dumps (
117- row ,
118- ensure_ascii = False ,
119- separators = ("," , ":" ),
120- sort_keys = True ,
121- )
122- for row in rows
123- ]
241+ serialized_rows = [_strict_json_dumps (row ) for row in rows ]
124242 if not serialized_rows :
125243 return ""
126- return "\n " .join (serialized_rows ) + "\n "
244+
245+ text = "\n " .join (serialized_rows ) + "\n "
246+ try :
247+ text .encode ("utf-8" )
248+ except UnicodeError as error :
249+ raise ValueError (f"output is not UTF-8 encodable: { error } " ) from error
250+ return text
251+
252+
253+ def _validate_distinct_paths (input_path , inventory_path , diagnostics_path ):
254+ named_paths = (
255+ ("--input" , Path (input_path )),
256+ ("--inventory" , Path (inventory_path )),
257+ ("--diagnostics" , Path (diagnostics_path )),
258+ )
259+ resolved_paths = []
260+ for option , path in named_paths :
261+ try :
262+ resolved_paths .append ((option , path , path .resolve (strict = False )))
263+ except (OSError , RuntimeError ) as error :
264+ raise ValueError (f"{ option } path cannot be resolved: { error } " ) from error
265+
266+ for index , (left_option , left_path , left_resolved ) in enumerate (
267+ resolved_paths
268+ ):
269+ for right_option , right_path , right_resolved in resolved_paths [index + 1 :]:
270+ aliases = left_resolved == right_resolved
271+ if not aliases :
272+ try :
273+ aliases = os .path .samefile (left_path , right_path )
274+ except OSError :
275+ aliases = False
276+ if aliases :
277+ raise ValueError (
278+ f"{ left_option } and { right_option } must identify distinct files"
279+ )
127280
128281
129282def _argument_parser ():
@@ -139,6 +292,11 @@ def main(argv=None):
139292 arguments = parser .parse_args (argv )
140293
141294 try :
295+ _validate_distinct_paths (
296+ arguments .input ,
297+ arguments .inventory ,
298+ arguments .diagnostics ,
299+ )
142300 rows = list (read_jsonl (arguments .input ))
143301 accepted_rows , diagnostics = partition_rows (rows )
144302 inventory = build_inventory (accepted_rows )
@@ -147,8 +305,11 @@ def main(argv=None):
147305 except (OSError , TypeError , ValueError ) as error :
148306 parser .exit (1 , f"error: { error } \n " )
149307
150- atomic_write_text (arguments .inventory , inventory_text )
151- atomic_write_text (arguments .diagnostics , diagnostics_text )
308+ try :
309+ atomic_write_text (arguments .inventory , inventory_text )
310+ atomic_write_text (arguments .diagnostics , diagnostics_text )
311+ except (OSError , UnicodeError ) as error :
312+ parser .exit (1 , f"error: cannot publish outputs: { error } \n " )
152313
153314
154315if __name__ == "__main__" :
0 commit comments