Spaces:
Running
Running
File size: 16,994 Bytes
45256e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OraShift</title>
<meta name="description" content="Oracle SQL to PostgreSQL with a QLoRA fine-tuned LLM, judged by execution.">
<style>
:root {
--bg: #f7f7f5; --surface: #ffffff; --surface-2: #f0efeb; --text: #1c1c1a; --muted: #62615c;
--border: #dddcd6; --accent: #2f5bd3; --accent-text: #ffffff;
--ok-bg: #e5f3ea; --ok-text: #1d6b3a; --warn-bg: #fdf1dc; --warn-text: #8a5300;
--info-bg: #e8eefc; --info-text: #23439e;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #151514; --surface: #1e1e1c; --surface-2: #272724; --text: #ecebe6; --muted: #a3a29b;
--border: #3a3a36; --accent: #7d9cf0; --accent-text: #10131c;
--ok-bg: #173323; --ok-text: #8fdcaa; --warn-bg: #3a2a10; --warn-text: #f3c477;
--info-bg: #1c2645; --info-text: #a9bdf5;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.55 var(--sans); }
main { max-width: 1040px; margin: 0 auto; padding: 32px 16px 64px; }
a { color: var(--accent); }
h1 { font-size: 2rem; margin: 0 0 4px; letter-spacing: -0.02em; }
h2 { font-size: 1.25rem; margin: 40px 0 12px; }
h3 { font-size: 1rem; margin: 0 0 8px; }
p { margin: 0 0 12px; }
.lede { color: var(--muted); font-size: 1.05rem; max-width: 720px; }
.links { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 0; }
.links a { text-decoration: none; border: 1px solid var(--border); background: var(--surface);
padding: 6px 12px; border-radius: 999px; font-size: 0.9rem; color: var(--text); }
.links a:hover { border-color: var(--accent); }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 28px; }
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; }
.stat .v { font-size: 1.7rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.stat .l { color: var(--muted); font-size: 0.88rem; }
.stat.hi { border-color: var(--accent); }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 18px; }
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin: 40px 0 16px; }
.tabs button { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted);
font: inherit; font-weight: 600; padding: 8px 14px; cursor: pointer; }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }
textarea, select, input[type=search] { font: inherit; color: var(--text); background: var(--surface);
border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; }
textarea { width: 100%; min-height: 150px; font-family: var(--mono); font-size: 0.9rem; resize: vertical; }
.btn { background: var(--accent); color: var(--accent-text); border: 0; border-radius: 8px;
padding: 9px 18px; font: inherit; font-weight: 600; cursor: pointer; }
.btn:disabled { opacity: 0.55; cursor: wait; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 14px; }
.chip { background: var(--surface-2); border: 1px solid var(--border); color: var(--text); border-radius: 999px;
padding: 3px 10px; font-size: 0.82rem; cursor: pointer; font-family: var(--mono); }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
pre { margin: 0; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
padding: 10px 12px; font: 0.85rem/1.5 var(--mono); white-space: pre-wrap; word-break: break-word; }
.label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
margin: 14px 0 4px; font-weight: 600; }
.badge { display: inline-block; border-radius: 6px; padding: 2px 8px; font-size: 0.82rem; font-weight: 600; }
.b-ok { background: var(--ok-bg); color: var(--ok-text); }
.b-warn { background: var(--warn-bg); color: var(--warn-text); }
.b-info { background: var(--info-bg); color: var(--info-text); }
.note { color: var(--muted); font-size: 0.9rem; margin-top: 8px; }
.status { color: var(--muted); font-size: 0.88rem; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 0.92rem; font-variant-numeric: tabular-nums; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
th { color: var(--muted); font-weight: 600; font-size: 0.82rem; }
td.num, th.num { text-align: right; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 720px) { .grid2 { grid-template-columns: 1fr; } h1 { font-size: 1.6rem; } }
.unit { margin-bottom: 14px; }
.unit .meta { display: flex; gap: 8px; flex-wrap: wrap; font-size: 0.82rem; color: var(--muted); }
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.filters input[type=search] { flex: 1 1 220px; }
.hidden { display: none; }
ul.lim { padding-left: 20px; margin: 0; }
ul.lim li { margin-bottom: 6px; }
code { font-family: var(--mono); font-size: 0.88em; background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
</style>
</head>
<body>
<main>
<header>
<h1>OraShift</h1>
<p class="lede">Oracle SQL to PostgreSQL with a QLoRA fine-tune of Qwen2.5-Coder-3B, and every
translation judged by <strong>executing it</strong> on both databases. A translation counts only
if it runs on PostgreSQL and returns what the original returns on Oracle.</p>
<nav class="links">
<a href="https://github.com/devansh173/orashift" target="_blank" rel="noopener">Code & write-up on GitHub</a>
<a href="https://huggingface.co/devansh173/orashift-qlora-adapter" target="_blank" rel="noopener">Model adapter</a>
<a href="https://huggingface.co/datasets/devansh173/orashift-pairs" target="_blank" rel="noopener">Dataset</a>
</nav>
</header>
<section class="stats" id="stats" aria-label="Execution accuracy"></section>
<p class="note">Execution accuracy over 330 held-out test units. Hybrid keeps sqlglot's output when it
verifies against Oracle and uses the fine-tuned model otherwise.</p>
<div class="tabs" role="tablist">
<button role="tab" aria-selected="true" data-tab="translate">Translate</button>
<button role="tab" aria-selected="false" data-tab="explore">Explore the test set</button>
<button role="tab" aria-selected="false" data-tab="results">Results</button>
</div>
<section id="tab-translate">
<div class="card">
<h3>Paste Oracle SQL</h3>
<p class="note" style="margin-top:0">The rules run live in your browser. This free demo does not run the
3B model; for test-set statements it shows the model's real saved output.</p>
<div class="chips" id="examples"></div>
<textarea id="input" spellcheck="false" aria-label="Oracle SQL"></textarea>
<div class="row" style="margin-top:10px">
<button class="btn" id="go" disabled>Translate</button>
<span class="status" id="status">Loading Python in your browser…</span>
</div>
<div id="out" class="hidden"></div>
</div>
</section>
<section id="tab-explore" class="hidden">
<p class="note" style="margin-top:0">All 330 held-out units with the base and fine-tuned models' actual
outputs. Pass or fail was decided by execution, so text that differs from the reference can still be
correct. Per-construct accuracy is shown for the selected construct.</p>
<div class="filters">
<select id="f-split" aria-label="Split"></select>
<select id="f-cat" aria-label="Construct"></select>
<input type="search" id="f-q" placeholder="Search SQL…" aria-label="Search SQL">
</div>
<div id="cat-acc" class="note"></div>
<div id="units"></div>
<button class="btn hidden" id="more" style="background:var(--surface-2);color:var(--text)">Show more</button>
</section>
<section id="tab-results" class="hidden">
<h3>By generalisation axis</h3>
<p class="note" style="margin-top:0">Two things are held out: the <code>logistics</code> schema, and 25 of
123 templates. The fine-tune generalises to unseen table names, not to unseen constructs.</p>
<div class="card table-wrap"><table id="t-split"></table></div>
<h2>By construct</h2>
<div class="card table-wrap"><table id="t-cat"></table></div>
<h2>Limitations</h2>
<ul class="lim">
<li>On an unseen construct variant the fine-tune is no better than the base model, and on
<code>SYSDATE</code> it is far worse (2/26 against 23/26) because it echoes unfamiliar input unchanged.</li>
<li>The data is synthetic and template-generated, so it is more uniform than real-world SQL.</li>
<li>This page cannot execute anything. Every accuracy figure comes from the offline evaluation.</li>
</ul>
</section>
</main>
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"></script>
<script>
const $ = (id) => document.getElementById(id);
const pct = (c) => c && c.total ? `${Math.round(c.rate * 100)}%` : "n/a";
const frac = (c) => c && c.total ? `${c.verified}/${c.total} (${pct(c)})` : "n/a";
const norm = (s) => (s || "").trim().replace(/;\s*$/, "").replace(/\s+/g, " ").toLowerCase();
const el = (tag, attrs = {}, ...kids) => {
const n = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) k === "class" ? (n.className = v) : n.setAttribute(k, v);
for (const k of kids) if (k != null) n.append(k);
return n;
};
const block = (label, sql) => [el("div", { class: "label" }, label), el("pre", {}, sql || "(empty)")];
let DATA, py, byOracle = new Map();
// Tabs
document.querySelectorAll(".tabs button").forEach((b) => b.addEventListener("click", () => {
document.querySelectorAll(".tabs button").forEach((x) => x.setAttribute("aria-selected", x === b));
for (const t of ["translate", "explore", "results"]) $("tab-" + t).classList.toggle("hidden", t !== b.dataset.tab);
}));
function renderStats(m) {
const a = m.execution_accuracy;
const items = [["Hybrid", a.hybrid, true], ["Fine-tuned", a.finetuned], ["Base model", a.base], ["sqlglot rules", a.sqlglot]];
for (const [name, c, hi] of items)
$("stats").append(el("div", { class: "stat" + (hi ? " hi" : "") },
el("div", { class: "v" }, pct(c)), el("div", { class: "l" }, `${name} · ${c.verified}/${c.total}`)));
}
function renderTables(m) {
const cols = [["base", "Base"], ["finetuned", "Fine-tuned"], ["sqlglot", "sqlglot"], ["hybrid", "Hybrid"]];
const head = (first) => el("tr", {}, el("th", {}, first), ...cols.map(([, n]) => el("th", { class: "num" }, n)));
$("t-split").append(head("Split"));
for (const [k, label] of Object.entries(m.split_labels))
$("t-split").append(el("tr", {}, el("td", {}, label), ...cols.map(([c]) => el("td", { class: "num" }, frac(m.by_split[k][c])))));
$("t-cat").append(head("Construct"));
for (const [k, v] of Object.entries(m.by_category))
$("t-cat").append(el("tr", {}, el("td", {}, el("code", {}, k)), ...cols.map(([c]) => el("td", { class: "num" }, frac(v[c])))));
}
// Explore
let shown = 20;
function filtered() {
const s = $("f-split").value, c = $("f-cat").value, q = $("f-q").value.toLowerCase();
return DATA.units.filter((u) => (!s || u.split === s) && (!c || u.category === c) &&
(!q || (u.oracle + " " + u.reference).toLowerCase().includes(q)));
}
function renderUnits() {
const list = filtered();
const box = $("units"); box.replaceChildren();
const c = $("f-cat").value;
$("cat-acc").textContent = c
? `${c}: fine-tuned ${frac(DATA.metrics.by_category[c].finetuned)}, base ${frac(DATA.metrics.by_category[c].base)} by execution.`
: `${list.length} units.`;
for (const u of list.slice(0, shown)) {
box.append(el("div", { class: "card unit" },
el("div", { class: "meta" }, el("span", { class: "badge b-info" }, u.category),
el("span", {}, DATA.metrics.split_labels[u.split]), el("span", {}, "schema: " + u.schema)),
...block("Oracle input", u.oracle),
el("div", { class: "grid2" },
el("div", {}, ...block("Fine-tuned output", u.finetuned)),
el("div", {}, ...block("Base model output", u.base))),
...block("Hand-written reference (execution-verified)", u.reference)));
}
$("more").classList.toggle("hidden", list.length <= shown);
}
function setupExplore() {
const splits = [["", "All splits"], ...Object.entries(DATA.metrics.split_labels)];
for (const [v, l] of splits) $("f-split").append(el("option", { value: v }, l));
const cats = [...new Set(DATA.units.map((u) => u.category))].sort();
$("f-cat").append(el("option", { value: "" }, "All constructs"));
for (const c of cats) $("f-cat").append(el("option", { value: c }, c));
for (const id of ["f-split", "f-cat", "f-q"]) $(id).addEventListener("input", () => { shown = 20; renderUnits(); });
$("more").addEventListener("click", () => { shown += 20; renderUnits(); });
renderUnits();
}
// Translate
function setupExamples() {
const want = ["rownum", "connect_by", "add_months", "trunc_date", "nvl", "decode", "outer_join_plus", "merge"];
for (const cat of want) {
const u = DATA.units.find((x) => x.category === cat && x.split === "test_unseen_schema") || DATA.units.find((x) => x.category === cat);
if (!u) continue;
const b = el("button", { class: "chip", type: "button" }, cat);
b.addEventListener("click", () => { $("input").value = u.oracle; });
$("examples").append(b);
}
$("input").value = DATA.units.find((x) => x.category === "rownum").oracle;
}
function translate() {
const sql = $("input").value;
const out = $("out"); out.replaceChildren(); out.classList.remove("hidden");
if (!sql.trim()) return;
const [rules, isms] = JSON.parse(py.globals.get("run")(sql));
const saved = byOracle.get(norm(sql));
const parts = [];
if (!isms.length && rules) {
parts.push(el("div", { class: "label" }, "Path"), el("span", { class: "badge b-ok" }, "sqlglot rules"),
el("p", { class: "note" }, "No Oracle-specific constructs found, so the rules answer. sqlglot scores 100% on constructs like NVL, DECODE and TO_CHAR."),
...block("PostgreSQL", rules));
} else if (saved) {
parts.push(el("div", { class: "label" }, "Path"), el("span", { class: "badge b-info" }, "fine-tuned model (saved test-set output)"),
el("p", { class: "note" }, `Contains ${isms.join(", ")}, which the rules cannot handle. This exact statement is in the held-out test set (${DATA.metrics.split_labels[saved.split]}), so this is the model's real output from the evaluation run.`),
...block("PostgreSQL (fine-tuned model)", saved.finetuned),
el("div", { class: "grid2" },
el("div", {}, ...block("Base model, for comparison", saved.base)),
el("div", {}, ...block("sqlglot rules, for comparison", rules || "(could not parse)"))));
} else {
parts.push(el("div", { class: "label" }, "Path"), el("span", { class: "badge b-warn" }, "needs the model, not run here"),
el("p", { class: "note" }, `Contains ${isms.join(", ") || "an unsupported construct"}, which sqlglot usually passes through unchanged. This free demo does not run the 3B model; the rules' attempt is shown below and is likely wrong. Pick an example above to see real model output, or run the adapter yourself.`),
...block("PostgreSQL (sqlglot, unreliable here)", rules || "(sqlglot could not parse this)"));
}
out.append(...parts);
}
async function boot() {
DATA = await (await fetch("data.json")).json();
for (const u of DATA.units) byOracle.set(norm(u.oracle), u);
renderStats(DATA.metrics); renderTables(DATA.metrics); setupExplore(); setupExamples();
try {
py = await loadPyodide();
$("status").textContent = "Installing sqlglot…";
await py.loadPackage("micropip");
await py.runPythonAsync('import micropip\nawait micropip.install("sqlglot==30.21.0")');
py.FS.writeFile("/home/pyodide/translate.py", await (await fetch("translate.py")).text());
py.runPython(`
import sys, json
sys.path.insert(0, "/home/pyodide")
from translate import needs_model, try_sqlglot
def run(sql):
sql = sql.strip().rstrip(";")
return json.dumps([try_sqlglot(sql), needs_model(sql)])
`);
$("status").textContent = "Ready.";
$("go").disabled = false;
} catch (e) {
$("status").textContent = "Could not load Python in this browser: " + e.message;
}
}
$("go").addEventListener("click", translate);
boot();
</script>
</body>
</html>
|