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 &amp; 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>