Skip to content

Commit 7663f51

Browse files
committed
feat: generate PostgreSQL compatibility reports
1 parent 1f9122e commit 7663f51

2 files changed

Lines changed: 476 additions & 0 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
#!/usr/bin/env python3
2+
3+
from collections import Counter, defaultdict
4+
5+
if __package__:
6+
from .baseline import RESULTS
7+
else:
8+
from baseline import RESULTS
9+
10+
11+
MAX_SQL_DISPLAY = 200
12+
13+
14+
def _major(pg_version):
15+
return str(pg_version).split(".", maxsplit=1)[0]
16+
17+
18+
def _escape_cell(value):
19+
text = "" if value is None else str(value)
20+
return (
21+
text.replace("\\", "\\\\")
22+
.replace("|", "\\|")
23+
.replace("\r", " ")
24+
.replace("\n", " ")
25+
)
26+
27+
28+
def _sql_excerpt(sql):
29+
text = " ".join(str(sql).split())
30+
if len(text) <= MAX_SQL_DISPLAY:
31+
return text
32+
return text[: MAX_SQL_DISPLAY - 3] + "..."
33+
34+
35+
def _table(headers, rows):
36+
output = []
37+
output.append("| " + " | ".join(headers) + " |")
38+
output.append("| " + " | ".join(["---"] * len(headers)) + " |")
39+
if not rows:
40+
output.append("| " + " | ".join(["-"] * len(headers)) + " |")
41+
return output
42+
for row in rows:
43+
output.append(
44+
"| "
45+
+ " | ".join(_escape_cell(value) for value in row)
46+
+ " |"
47+
)
48+
return output
49+
50+
51+
def _sorted_statement_rows(rows):
52+
return sorted(
53+
rows,
54+
key=lambda row: (
55+
str(row.get("result", "")),
56+
str(row.get("oracle_node", "")),
57+
str(row.get("id", "")),
58+
),
59+
)
60+
61+
62+
def _statement_table(rows, include_transition=False):
63+
headers = ["Result", "Oracle Node", "ID", "SQL"]
64+
if include_transition:
65+
headers = ["Previous", "Current", "Oracle Node", "ID", "SQL"]
66+
output_rows = []
67+
for row in rows:
68+
if include_transition:
69+
output_rows.append(
70+
[
71+
row.get("previous_result", ""),
72+
row.get("current_result", row.get("result", "")),
73+
row.get("oracle_node", ""),
74+
row.get("id", ""),
75+
_sql_excerpt(row.get("sql", "")),
76+
]
77+
)
78+
else:
79+
output_rows.append(
80+
[
81+
row.get("result", ""),
82+
row.get("oracle_node", ""),
83+
row.get("id", ""),
84+
_sql_excerpt(row.get("sql", "")),
85+
]
86+
)
87+
return _table(headers, output_rows)
88+
89+
90+
def _classified_routing_rows(results):
91+
counts = Counter(
92+
row.get("oracle_node", "")
93+
for row in results
94+
if row.get("result") == "CLASSIFIED_ONLY"
95+
)
96+
return [
97+
[oracle_node, count]
98+
for oracle_node, count in sorted(counts.items())
99+
]
100+
101+
102+
def _witness_links(witness_result):
103+
links = defaultdict(list)
104+
for witness in witness_result.get("witnesses", []):
105+
for feature_id in witness.get("structural_feature_ids", []):
106+
links[feature_id].append(witness.get("id", ""))
107+
return {
108+
feature_id: sorted(witness_ids)
109+
for feature_id, witness_ids in sorted(links.items())
110+
}
111+
112+
113+
def _structural_rows(structural_features, links, *, only_unwitnessed):
114+
rows = []
115+
for feature in sorted(
116+
structural_features,
117+
key=lambda row: (
118+
str(row.get("kind", "")),
119+
str(row.get("symbol", "")),
120+
str(row.get("id", "")),
121+
),
122+
):
123+
feature_id = feature.get("id", "")
124+
witness_ids = links.get(feature_id, [])
125+
if only_unwitnessed and witness_ids:
126+
continue
127+
if not only_unwitnessed and not witness_ids:
128+
continue
129+
rows.append(
130+
[
131+
feature_id,
132+
feature.get("kind", ""),
133+
feature.get("symbol", ""),
134+
", ".join(witness_ids) if witness_ids else "-",
135+
feature.get("target", ""),
136+
]
137+
)
138+
return rows
139+
140+
141+
def _newly_supported_rows(baseline_evaluation):
142+
rows = []
143+
for row in baseline_evaluation.get("allowed", []):
144+
if (
145+
row.get("previous_result") != "DEEP_SUPPORTED"
146+
and row.get("current_result") == "DEEP_SUPPORTED"
147+
):
148+
rows.append(row)
149+
return sorted(rows, key=lambda row: str(row.get("id", "")))
150+
151+
152+
def generate_report(
153+
context,
154+
results,
155+
release_delta,
156+
structural_features,
157+
baseline_evaluation,
158+
witness_result,
159+
):
160+
pins = context["pins"]["versions"]
161+
previous = pins["previous"]
162+
target = pins["target"]
163+
target_major = _major(target["pg_version"])
164+
counts = Counter(row.get("result") for row in results)
165+
links = _witness_links(witness_result)
166+
167+
lines = [
168+
f"# PostgreSQL {target_major} Compatibility",
169+
"",
170+
f"Generated: {context['generated_at']}",
171+
f"ParserSQL commit: `{context['parsersql_commit']}`",
172+
(
173+
"libpg_query previous: "
174+
f"`{previous['branch']}` `{previous['commit']}` "
175+
f"(PostgreSQL {previous['pg_version']})"
176+
),
177+
(
178+
"libpg_query target: "
179+
f"`{target['branch']}` `{target['commit']}` "
180+
f"(PostgreSQL {target['pg_version']})"
181+
),
182+
"",
183+
"## Result Totals",
184+
"",
185+
]
186+
lines.extend(
187+
_table(
188+
["Result", "Count"],
189+
[[result, counts[result]] for result in RESULTS],
190+
)
191+
)
192+
lines.extend(
193+
[
194+
"",
195+
"## PG18 Backlog",
196+
"",
197+
]
198+
)
199+
lines.extend(
200+
_statement_table(
201+
_sorted_statement_rows(
202+
row for row in results if row.get("result") != "DEEP_SUPPORTED"
203+
)
204+
)
205+
)
206+
lines.extend(
207+
[
208+
"",
209+
"## CLASSIFIED_ONLY Routing Coverage",
210+
"",
211+
]
212+
)
213+
lines.extend(
214+
_table(
215+
["Oracle Node", "Count"],
216+
_classified_routing_rows(results),
217+
)
218+
)
219+
lines.extend(
220+
[
221+
"",
222+
"## PG17 to PG18 Release Delta",
223+
"",
224+
]
225+
)
226+
lines.extend(_statement_table(_sorted_statement_rows(release_delta)))
227+
lines.extend(
228+
[
229+
"",
230+
"## Newly Supported Baseline Transitions",
231+
"",
232+
]
233+
)
234+
lines.extend(
235+
_statement_table(
236+
_newly_supported_rows(baseline_evaluation),
237+
include_transition=True,
238+
)
239+
)
240+
lines.extend(
241+
[
242+
"",
243+
"## Regressed Baseline Transitions",
244+
"",
245+
]
246+
)
247+
lines.extend(
248+
_statement_table(
249+
sorted(
250+
baseline_evaluation.get("regressions", []),
251+
key=lambda row: str(row.get("id", "")),
252+
),
253+
include_transition=True,
254+
)
255+
)
256+
lines.extend(
257+
[
258+
"",
259+
"## Structural Feature Witness Links",
260+
"",
261+
]
262+
)
263+
lines.extend(
264+
_table(
265+
["Feature ID", "Kind", "Symbol", "Witnesses", "Target"],
266+
_structural_rows(structural_features, links, only_unwitnessed=False),
267+
)
268+
)
269+
lines.extend(
270+
[
271+
"",
272+
"## Unwitnessed Structural Features",
273+
"",
274+
]
275+
)
276+
lines.extend(
277+
_table(
278+
["Feature ID", "Kind", "Symbol", "Witnesses", "Target"],
279+
_structural_rows(structural_features, links, only_unwitnessed=True),
280+
)
281+
)
282+
lines.extend(
283+
[
284+
"",
285+
"## Unlinked Witnesses",
286+
"",
287+
]
288+
)
289+
lines.extend(
290+
_table(
291+
["Witness ID"],
292+
[
293+
[witness_id]
294+
for witness_id in sorted(
295+
witness_result.get("unlinked_witnesses", [])
296+
)
297+
],
298+
)
299+
)
300+
lines.extend(
301+
[
302+
"",
303+
"## Reproduction",
304+
"",
305+
]
306+
)
307+
for command in context.get("commands", []):
308+
lines.append(f"- `{command}`")
309+
310+
return "\n".join(lines) + "\n"

0 commit comments

Comments
 (0)