Skip to content

Commit 1d064c5

Browse files
committed
feat: build deterministic PostgreSQL statement inventories
1 parent f99923c commit 1d064c5

2 files changed

Lines changed: 479 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
6+
if __package__:
7+
from .common import atomic_write_text, read_jsonl, statement_id
8+
else:
9+
from common import atomic_write_text, read_jsonl, statement_id
10+
11+
12+
REQUIRED_INVENTORY_FIELDS = (
13+
"result",
14+
"normalized_sql",
15+
"oracle_node",
16+
"source_file",
17+
"offset",
18+
"line",
19+
"sql",
20+
)
21+
22+
23+
def _validate_row(row, row_index, required_fields):
24+
if not isinstance(row, dict):
25+
raise ValueError(f"row {row_index}: expected a JSON object")
26+
27+
for field in required_fields:
28+
if field not in row:
29+
raise ValueError(
30+
f"row {row_index}: missing required field {field!r}"
31+
)
32+
33+
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,
41+
)
42+
except (TypeError, ValueError) as error:
43+
raise ValueError(
44+
f"row {row_index}: not JSON serializable: {error}"
45+
) from error
46+
47+
48+
def partition_rows(rows):
49+
accepted_rows = []
50+
diagnostics = []
51+
52+
for row_index, row in enumerate(rows):
53+
_validate_row(row, row_index, ("result",))
54+
if row["result"] == "ORACLE_REJECTED":
55+
diagnostics.append(row)
56+
else:
57+
accepted_rows.append(row)
58+
59+
return accepted_rows, diagnostics
60+
61+
62+
def build_inventory(rows):
63+
groups = {}
64+
65+
for row_index, row in enumerate(rows):
66+
_validate_row(row, row_index, ("result",))
67+
if row["result"] == "ORACLE_REJECTED":
68+
continue
69+
70+
_validate_row(row, row_index, REQUIRED_INVENTORY_FIELDS)
71+
group_key = (row["oracle_node"], row["normalized_sql"])
72+
occurrence_key = (
73+
row["source_file"],
74+
row["offset"],
75+
row["line"],
76+
)
77+
row_copy = dict(row)
78+
serialized_key = _serialized_row_key(row_copy, row_index)
79+
occurrences = groups.setdefault(group_key, {})
80+
existing = occurrences.get(occurrence_key)
81+
if existing is None or serialized_key < existing[0]:
82+
occurrences[occurrence_key] = (serialized_key, row_copy)
83+
84+
inventory = []
85+
ids = {}
86+
for group_key, rows_by_occurrence in groups.items():
87+
oracle_node, normalized_sql = group_key
88+
record_id = statement_id(normalized_sql, oracle_node)
89+
existing_key = ids.get(record_id)
90+
if existing_key is not None and existing_key != group_key:
91+
raise ValueError(
92+
"statement ID collision: "
93+
f"{record_id} maps to both {existing_key!r} and {group_key!r}"
94+
)
95+
ids[record_id] = group_key
96+
97+
sorted_occurrence_keys = sorted(rows_by_occurrence)
98+
canonical_row = rows_by_occurrence[sorted_occurrence_keys[0]][1]
99+
record = dict(canonical_row)
100+
record["id"] = record_id
101+
record["occurrences"] = [
102+
{
103+
"source_file": source_file,
104+
"offset": offset,
105+
"line": line,
106+
}
107+
for source_file, offset, line in sorted_occurrence_keys
108+
]
109+
inventory.append(record)
110+
111+
return sorted(inventory, key=lambda record: record["id"])
112+
113+
114+
def _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+
]
124+
if not serialized_rows:
125+
return ""
126+
return "\n".join(serialized_rows) + "\n"
127+
128+
129+
def _argument_parser():
130+
parser = argparse.ArgumentParser()
131+
parser.add_argument("--input", required=True)
132+
parser.add_argument("--inventory", required=True)
133+
parser.add_argument("--diagnostics", required=True)
134+
return parser
135+
136+
137+
def main(argv=None):
138+
parser = _argument_parser()
139+
arguments = parser.parse_args(argv)
140+
141+
try:
142+
rows = list(read_jsonl(arguments.input))
143+
accepted_rows, diagnostics = partition_rows(rows)
144+
inventory = build_inventory(accepted_rows)
145+
inventory_text = _jsonl_text(inventory)
146+
diagnostics_text = _jsonl_text(diagnostics)
147+
except (OSError, TypeError, ValueError) as error:
148+
parser.exit(1, f"error: {error}\n")
149+
150+
atomic_write_text(arguments.inventory, inventory_text)
151+
atomic_write_text(arguments.diagnostics, diagnostics_text)
152+
153+
154+
if __name__ == "__main__":
155+
main()

0 commit comments

Comments
 (0)