Firemedic15's picture
download
raw
4.85 kB
"""Metrics for the Squirrel-mini toy reproduction.
Implements:
- script complexity stats (lines, tokens, function count, AST width/depth) for
comparison with Table 1 of the paper.
- Exact Match (EM) and a Graph-Match PROXY (GM-proxy) for comparison with
Table 2. The paper's GM uses Apache Calcite optimized-plan graph isomorphism;
we approximate it with sqlglot's parsed+optimized AST compared structurally,
which is a weaker but self-contained proxy (documented as a substitution).
"""
from __future__ import annotations
import re
from typing import Optional
import sqlglot
import sqlglot.expressions as sge
import tiktoken
_ENC = tiktoken.get_encoding("cl100k_base")
FUNC_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(")
SQL_KEYWORDS_NOT_FUNCS = {
"select", "from", "where", "group", "order", "having", "join", "on", "as",
"case", "when", "then", "partition", "over", "union", "insert", "into",
"values", "with",
}
def count_lines(sql: str) -> int:
return len([ln for ln in sql.splitlines() if ln.strip()])
def count_tokens(sql: str) -> int:
return len(_ENC.encode(sql))
def count_functions(sql: str) -> int:
matches = FUNC_RE.findall(sql)
return sum(1 for m in matches if m.lower() not in SQL_KEYWORDS_NOT_FUNCS)
def ast_width_depth(sql: str, dialect: str = "hive") -> tuple[Optional[int], Optional[int]]:
"""Return (max width, max depth) of the sqlglot AST tree, as a proxy for the
paper's Apache-Calcite-based AST width/depth (Eq. 2)."""
try:
tree = sqlglot.parse_one(sql, read=dialect)
except Exception:
return None, None
depths: dict[int, int] = {}
max_depth = 0
def children(node):
for value in node.args.values():
items = value if isinstance(value, list) else [value]
for item in items:
if isinstance(item, sge.Expression):
yield item
def walk(node, depth=0):
nonlocal max_depth
max_depth = max(max_depth, depth)
depths[depth] = depths.get(depth, 0) + 1
for child in children(node):
walk(child, depth + 1)
walk(tree, 0)
max_width = max(depths.values()) if depths else 0
return max_width, max_depth
def script_stats(sql: str, dialect: str = "hive") -> dict:
width, depth = ast_width_depth(sql, dialect)
return {
"lines": count_lines(sql),
"tokens": count_tokens(sql),
"functions": count_functions(sql),
"ast_width": width,
"ast_depth": depth,
}
def normalize_sql(sql: str) -> str:
return re.sub(r"\s+", " ", sql.strip().lower())
def exact_match(pred: str, ref: str) -> bool:
return normalize_sql(pred) == normalize_sql(ref)
_COMMENT_RE = re.compile(r"--[^\n]*|/\*.*?\*/", re.S)
def strip_comments(sql: str) -> str:
return _COMMENT_RE.sub(" ", sql)
def graph_match_proxy(pred: str, ref: str, dialect: str = "hive") -> bool:
"""Weak proxy for the paper's Graph Match score: parse both queries with
sqlglot, canonicalize (strip comments, normalize identifiers), and compare
the resulting AST dumps for structural equality. Falls back to False if
either side fails to parse.
Comments are stripped before comparison because the paper's real GM score
operates on Apache Calcite's optimized relational-algebra plan, which has
no notion of SQL comments at all (unlike its strict, string-level EM
score) -- this is exactly the EM-vs-GM gap the paper's Table 2 is built
around, so a comment-only diff should read as a GM hit, not a miss."""
try:
pred_tree = sqlglot.parse_one(strip_comments(pred), read=dialect)
ref_tree = sqlglot.parse_one(strip_comments(ref), read=dialect)
except Exception:
return False
try:
pred_norm = pred_tree.sql(dialect=dialect, normalize=True, pretty=False)
ref_norm = ref_tree.sql(dialect=dialect, normalize=True, pretty=False)
except Exception:
pred_norm, ref_norm = str(pred_tree), str(ref_tree)
if normalize_sql(pred_norm) == normalize_sql(ref_norm):
return True
# Fall back to comparing the raw AST repr (catches semantically-irrelevant
# whitespace/casing differences that .sql() normalization might not fully collapse)
return pred_tree.dump() == ref_tree.dump()
def modify_better_proxy(pred: str, ref: str, buggy: str, dialect: str = "hive") -> Optional[bool]:
"""Weak proxy for Modify-Better score: character-edit-distance based since a
full AST edit-distance is out of scope for this toy harness."""
import difflib
def dist(a: str, b: str) -> float:
return 1 - difflib.SequenceMatcher(None, a, b).ratio()
ref_n, pred_n, buggy_n = normalize_sql(ref), normalize_sql(pred), normalize_sql(buggy)
return dist(pred_n, ref_n) < dist(buggy_n, ref_n)

Xet Storage Details

Size:
4.85 kB
·
Xet hash:
031fda09dd5e00254906003688f048a651e4be3d885810e3ae233a31ca19174f

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.