|
| 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