localtools-sql-formatter 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ /* localtools UI kit. Swiss industrial: ruled grid, square corners, one red accent.
2
+ Light/dark follow the system. No network requests: fonts are vendored in ./fonts. */
3
+
4
+ @font-face { font-family: "JetBrains Mono"; font-weight: 400; font-display: swap;
5
+ src: url("fonts/jetbrains-mono-latin-regular.woff2") format("woff2"); }
6
+ @font-face { font-family: "JetBrains Mono"; font-weight: 700; font-display: swap;
7
+ src: url("fonts/jetbrains-mono-latin-bold.woff2") format("woff2"); }
8
+
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #f1f1ec;
12
+ --panel: #fafaf7;
13
+ --ink: #121211;
14
+ --muted: #55554f;
15
+ --rule: #121211;
16
+ --hair: #c6c6bd;
17
+ --accent: #c8161b;
18
+ --on-ink: #f1f1ec;
19
+ --warn: #8a5300;
20
+ --ok: #1b6b2e;
21
+ --info: #1f4f9a;
22
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
23
+ --mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
24
+ --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 40px;
25
+ }
26
+ @media (prefers-color-scheme: dark) {
27
+ :root {
28
+ --bg: #111110;
29
+ --panel: #191917;
30
+ --ink: #ebebe5;
31
+ --muted: #a5a59c;
32
+ --rule: #ebebe5;
33
+ --hair: #3a3a36;
34
+ --accent: #ff4d40;
35
+ --on-ink: #111110;
36
+ --warn: #e6a832;
37
+ --ok: #5cc46e;
38
+ --info: #7aa7ff;
39
+ }
40
+ }
41
+
42
+ *, *::before, *::after { box-sizing: border-box; }
43
+ html { -webkit-text-size-adjust: 100%; }
44
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 var(--sans); }
45
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
46
+ ::selection { background: var(--accent); color: #fff; }
47
+ [hidden] { display: none !important; }
48
+ a { color: inherit; text-underline-offset: 3px; }
49
+ a:hover { color: var(--accent); }
50
+ code, kbd, samp, pre { font-family: var(--mono); font-size: 13px; }
51
+
52
+ /* ---------- page + header ---------- */
53
+ .lt-page { max-width: 1120px; margin: 0 auto; padding: var(--s5) var(--s4) var(--s6); }
54
+ .lt-header { position: relative; border-bottom: 3px solid var(--rule); padding: var(--s5) 0 var(--s4); }
55
+ .lt-title { margin: 0; font: 700 clamp(30px, 6vw, 56px)/0.95 var(--sans); letter-spacing: -0.035em; text-transform: uppercase; overflow-wrap: anywhere; }
56
+ .lt-desc { margin: var(--s3) 0 0; max-width: 65ch; color: var(--muted); }
57
+ .lt-issue { position: absolute; top: 0; right: 0; font: 11px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--muted); padding: var(--s1) 0; }
58
+
59
+ /* ---------- sections ---------- */
60
+ .lt-section { border-bottom: 1px solid var(--rule); padding: var(--s4) 0 var(--s5); }
61
+ .lt-section:last-child { border-bottom: 0; }
62
+ .lt-label, .lt-field > label, .lt-field > legend { display: block; font: 700 11px/1.3 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: var(--s2); }
63
+ h2.lt-label { color: var(--ink); font-size: 12px; margin: 0 0 var(--s3); }
64
+ .lt-hint { font-size: 13px; color: var(--muted); margin: var(--s2) 0 0; }
65
+
66
+ /* ---------- inputs ---------- */
67
+ .lt-textarea, .lt-input, .lt-select {
68
+ width: 100%; background: var(--panel); color: var(--ink); border: 1px solid var(--rule);
69
+ border-radius: 0; font: 14px/1.5 var(--mono); padding: var(--s2) var(--s3);
70
+ }
71
+ .lt-textarea { min-height: 200px; resize: vertical; display: block; }
72
+ .lt-input, .lt-select { min-height: 38px; }
73
+ .lt-select { appearance: none; padding-right: 32px; cursor: pointer;
74
+ background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(-45deg, transparent 50%, var(--ink) 50%);
75
+ background-position: right 16px top 50%, right 11px top 50%; background-size: 5px 5px; background-repeat: no-repeat; }
76
+ .lt-textarea::placeholder, .lt-input::placeholder { color: var(--muted); }
77
+
78
+ .lt-drop {
79
+ display: grid; place-items: center; gap: var(--s1); min-height: 120px; padding: var(--s5) var(--s4);
80
+ border: 1px dashed var(--rule); background: var(--panel); text-align: center; cursor: pointer;
81
+ font: 13px/1.4 var(--mono); color: var(--muted);
82
+ }
83
+ .lt-drop strong { color: var(--ink); font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
84
+ .lt-drop:hover, .lt-drop[data-over] { border-style: solid; border-color: var(--accent); color: var(--ink); }
85
+ .lt-drop:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
86
+ .lt-drop input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }
87
+ .lt-files { list-style: none; margin: var(--s2) 0 0; padding: 0; font: 13px/1.4 var(--mono); }
88
+ .lt-files li { display: flex; justify-content: space-between; gap: var(--s3); padding: var(--s1) 0; border-bottom: 1px solid var(--hair); }
89
+ .lt-files li span:first-child { overflow-wrap: anywhere; }
90
+ .lt-files li span:last-child { color: var(--muted); white-space: nowrap; }
91
+ .lt-or { display: flex; align-items: center; gap: var(--s3); margin: var(--s3) 0; font: 11px var(--mono); color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
92
+ .lt-or::before, .lt-or::after { content: ""; flex: 1; border-top: 1px solid var(--hair); }
93
+
94
+ /* ---------- options ---------- */
95
+ .lt-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--s4); align-items: end; }
96
+ .lt-field { margin: 0; min-width: 0; border: 0; padding: 0; }
97
+ .lt-field > legend { padding: 0; }
98
+ .lt-check { display: flex; align-items: center; gap: var(--s2); min-height: 38px; cursor: pointer; font-size: 14px; }
99
+ .lt-check input { width: 18px; height: 18px; margin: 0; accent-color: var(--accent); }
100
+
101
+ .lt-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--rule); background: var(--panel); }
102
+ .lt-seg label { position: relative; cursor: pointer; }
103
+ .lt-seg input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
104
+ .lt-seg span { display: block; padding: 0 var(--s3); line-height: 36px; font: 700 12px/36px var(--mono); letter-spacing: .04em; text-transform: uppercase; }
105
+ .lt-seg label + label { border-left: 1px solid var(--rule); }
106
+ .lt-seg input:checked + span { background: var(--ink); color: var(--on-ink); }
107
+ .lt-seg input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -4px; }
108
+
109
+ /* ---------- buttons ---------- */
110
+ .lt-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); }
111
+ .lt-btn {
112
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
113
+ min-height: 34px; padding: 0 var(--s3); border: 1px solid var(--rule); border-radius: 0;
114
+ background: transparent; color: var(--ink); font: 700 12px/1 var(--mono); letter-spacing: .06em;
115
+ text-transform: uppercase; white-space: nowrap; cursor: pointer;
116
+ }
117
+ .lt-btn:hover { background: var(--ink); color: var(--on-ink); }
118
+ .lt-btn:active { transform: translateY(1px); }
119
+ .lt-btn:disabled { opacity: .45; cursor: not-allowed; }
120
+ .lt-btn-primary { min-height: 48px; padding: 0 var(--s5); font-size: 14px; background: var(--ink); color: var(--on-ink); border-width: 2px; }
121
+ .lt-btn-primary:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
122
+ .lt-btn[aria-busy=true] { cursor: progress; opacity: 1; }
123
+ .lt-btn[aria-busy=true]::after { content: ""; width: .6em; height: 1em; background: currentColor; animation: lt-blink 1s steps(1) infinite; }
124
+ @keyframes lt-blink { 50% { opacity: 0; } }
125
+ .lt-btn[data-done]::before { content: "\2713"; }
126
+
127
+ /* ---------- feedback ---------- */
128
+ .lt-error { margin: var(--s4) 0 0; padding: var(--s3) var(--s4); border: 2px solid var(--accent); border-left-width: 8px; background: var(--panel); font: 13px/1.5 var(--mono); overflow-wrap: anywhere; }
129
+ .lt-error::before { content: "Error "; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .06em; }
130
+ .lt-status { font: 12px/1.4 var(--mono); color: var(--muted); }
131
+
132
+ /* ---------- output ---------- */
133
+ .lt-output { margin-top: var(--s4); border: 1px solid var(--rule); background: var(--panel); }
134
+ .lt-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); padding: var(--s2); border-bottom: 1px solid var(--rule); }
135
+ .lt-bar > :first-child { margin-right: auto; }
136
+ .lt-bar .lt-status { padding: 0 var(--s1); }
137
+ .lt-pre { margin: 0; padding: var(--s4); max-height: 60vh; overflow: auto; white-space: pre; line-height: 1.55; tab-size: 4; }
138
+ .lt-pre.lt-wrap { white-space: pre-wrap; overflow-wrap: anywhere; }
139
+ .lt-empty { padding: var(--s6) var(--s4); text-align: center; font: 13px var(--mono); color: var(--muted); }
140
+ .lt-preview { padding: var(--s4); display: grid; place-items: center;
141
+ background: repeating-conic-gradient(var(--hair) 0 25%, transparent 0 50%) 0 0 / 16px 16px; }
142
+ .lt-preview img, .lt-preview svg, .lt-preview video { max-width: 100%; max-height: 60vh; height: auto; display: block; }
143
+ .lt-frame { display: block; width: 100%; height: 70vh; border: 0; background: #fff; }
144
+
145
+ .lt-table-wrap { max-height: 60vh; overflow: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; }
146
+ .lt-table { width: 100%; border-collapse: collapse; font: 13px/1.4 var(--mono); }
147
+ .lt-table th { position: sticky; top: 0; background: var(--ink); color: var(--on-ink); text-align: left; font-weight: 700; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
148
+ .lt-table th, .lt-table td { padding: var(--s2) var(--s3); vertical-align: top; }
149
+ .lt-table td { border-bottom: 1px solid var(--hair); overflow-wrap: break-word; }
150
+ /* Narrow screens: keep columns readable and scroll the table sideways inside its panel instead of crushing it. */
151
+ @media (max-width: 720px) {
152
+ .lt-table { min-width: 640px; }
153
+ .lt-table td { min-width: 10ch; }
154
+ }
155
+ .lt-table td.lt-num, .lt-table th.lt-num { text-align: right; font-variant-numeric: tabular-nums; }
156
+
157
+ .lt-kv { display: grid; grid-template-columns: minmax(120px, max-content) 1fr; margin: 0; font: 13px/1.4 var(--mono); }
158
+ .lt-kv dt, .lt-kv dd { margin: 0; padding: var(--s2) var(--s3); border-bottom: 1px solid var(--hair); }
159
+ .lt-kv dt { color: var(--muted); text-transform: uppercase; font-size: 11px; letter-spacing: .06em; padding-top: 10px; }
160
+ .lt-kv dd { overflow-wrap: anywhere; }
161
+
162
+ /* ---------- severity ---------- */
163
+ .lt-badge { display: inline-block; min-width: 5ch; padding: 2px 6px; border: 1px solid currentColor; font: 700 10px/1.3 var(--mono); letter-spacing: .08em; text-transform: uppercase; text-align: center; white-space: nowrap; }
164
+ .lt-badge[data-sev=error] { color: var(--accent); }
165
+ .lt-badge[data-sev=warn] { color: var(--warn); }
166
+ .lt-badge[data-sev=info] { color: var(--info); }
167
+ .lt-badge[data-sev=ok] { color: var(--ok); }
168
+ .lt-findings { list-style: none; margin: 0; padding: 0; font: 13px/1.5 var(--mono); }
169
+ .lt-findings li { display: grid; grid-template-columns: 64px 1fr; gap: var(--s1) var(--s3); padding: var(--s2) var(--s4); border-bottom: 1px solid var(--hair); align-items: baseline; }
170
+ .lt-findings .lt-badge { justify-self: start; }
171
+ .lt-findings small { grid-column: 2; color: var(--muted); font-size: 12px; }
172
+
173
+ /* ---------- tabs ---------- */
174
+ .lt-tabs { display: flex; overflow-x: auto; }
175
+ .lt-tabs [role=tab] { border: 0; border-bottom: 3px solid transparent; background: none; color: var(--muted); padding: var(--s2) var(--s3); font: 700 12px/1.2 var(--mono); letter-spacing: .06em; text-transform: uppercase; cursor: pointer; white-space: nowrap; }
176
+ .lt-tabs [role=tab]:hover { color: var(--ink); }
177
+ .lt-tabs [role=tab][aria-selected=true] { color: var(--ink); border-bottom-color: var(--accent); }
178
+
179
+ @media (max-width: 560px) {
180
+ .lt-page { padding-top: var(--s4); }
181
+ .lt-header { padding-top: var(--s5); }
182
+ .lt-options { grid-template-columns: 1fr; }
183
+ .lt-btn-primary { width: 100%; }
184
+ .lt-kv { grid-template-columns: 1fr; }
185
+ .lt-kv dt { border-bottom: 0; padding-bottom: 0; }
186
+ }
187
+ @media (prefers-reduced-motion: reduce) {
188
+ *, *::before, *::after { animation: none !important; transition: none !important; }
189
+ }
@@ -0,0 +1,191 @@
1
+ // localtools UI kit helpers. ES module, no dependencies.
2
+
3
+ export function fmtBytes(n) {
4
+ const u = ["B", "KB", "MB", "GB", "TB"];
5
+ let i = 0;
6
+ while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
7
+ return (i ? n.toFixed(n < 10 ? 1 : 0) : n) + " " + u[i];
8
+ }
9
+
10
+ export function fmtMs(ms) {
11
+ if (ms < 1000) return Math.round(ms) + " ms";
12
+ if (ms < 60000) return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + " s";
13
+ return Math.floor(ms / 60000) + " m " + Math.round((ms % 60000) / 1000) + " s";
14
+ }
15
+
16
+ // Wire a <label class="lt-drop"> containing <input type=file>. Lists chosen files in `list`
17
+ // (a <ul class="lt-files">). Returns a function giving the current File[].
18
+ export function dropZone(zone, list, onChange) {
19
+ const input = zone.querySelector("input[type=file]");
20
+ const render = () => {
21
+ const files = [...input.files];
22
+ if (list) list.replaceChildren(...files.map((f) => {
23
+ const li = document.createElement("li");
24
+ li.append(span(f.name), span(fmtBytes(f.size)));
25
+ return li;
26
+ }));
27
+ onChange?.(files);
28
+ };
29
+ const over = (on) => (e) => { e.preventDefault(); zone.toggleAttribute("data-over", on); };
30
+ zone.addEventListener("dragenter", over(true));
31
+ zone.addEventListener("dragover", over(true));
32
+ zone.addEventListener("dragleave", over(false));
33
+ zone.addEventListener("drop", (e) => {
34
+ over(false)(e);
35
+ const dt = new DataTransfer();
36
+ const dropped = [...e.dataTransfer.files];
37
+ for (const f of input.multiple ? dropped : dropped.slice(0, 1)) dt.items.add(f);
38
+ input.files = dt.files;
39
+ render();
40
+ });
41
+ input.addEventListener("change", render);
42
+ return () => [...input.files];
43
+ }
44
+
45
+ // POST to /api/<action>. `json` sends JSON; otherwise multipart from files + fields.
46
+ // Resolves to the parsed JSON, or { blob, filename } for file responses. Throws Error(message).
47
+ export async function api(action, { files = [], fields = {}, json } = {}) {
48
+ let body, headers = {};
49
+ if (json !== undefined) {
50
+ body = JSON.stringify(json);
51
+ headers["Content-Type"] = "application/json";
52
+ } else {
53
+ body = new FormData();
54
+ for (const f of files) body.append("file", f, f.name);
55
+ for (const [k, v] of Object.entries(fields)) body.append(k, String(v));
56
+ }
57
+ let res;
58
+ try {
59
+ res = await fetch("/api/" + action, { method: "POST", body, headers });
60
+ } catch {
61
+ throw new Error("Cannot reach the local server. Is the tool still running?");
62
+ }
63
+ const type = res.headers.get("Content-Type") || "";
64
+ if (!res.ok) {
65
+ let msg = res.status + " " + res.statusText;
66
+ try { msg = type.includes("json") ? (await res.json()).error || msg : (await res.text()) || msg; } catch {}
67
+ throw new Error(msg);
68
+ }
69
+ // Integers beyond 2^53 keep their exact digits (as strings) where the browser exposes the source text.
70
+ if (type.includes("application/json"))
71
+ return JSON.parse(await res.text(), (k, v, c) => (Number.isInteger(v) && !Number.isSafeInteger(v) && c?.source ? c.source : v));
72
+ const cd = res.headers.get("Content-Disposition") || "";
73
+ const m = /filename\*=UTF-8''([^;]+)/i.exec(cd) || /filename="?([^";]+)"?/i.exec(cd);
74
+ return { blob: await res.blob(), filename: m ? decodeURIComponent(m[1]) : "download" };
75
+ }
76
+
77
+ // Run `fn` with `button` in busy state. Shows thrown errors in `errorEl` (class lt-error).
78
+ // Resolves to fn's result, or undefined on error.
79
+ export async function run(button, errorEl, fn) {
80
+ if (errorEl) errorEl.hidden = true;
81
+ button.disabled = true;
82
+ button.setAttribute("aria-busy", "true");
83
+ try {
84
+ return await fn();
85
+ } catch (e) {
86
+ showError(errorEl, e.message || String(e));
87
+ } finally {
88
+ button.disabled = false;
89
+ button.removeAttribute("aria-busy");
90
+ }
91
+ }
92
+
93
+ export function showError(el, msg) {
94
+ if (!el) return alert(msg);
95
+ el.textContent = msg;
96
+ el.hidden = false;
97
+ }
98
+
99
+ function flash(button, text) {
100
+ const old = button.textContent;
101
+ button.textContent = text;
102
+ button.toggleAttribute("data-done", text === "Copied");
103
+ setTimeout(() => { button.textContent = old; button.removeAttribute("data-done"); }, 1400);
104
+ }
105
+
106
+ export async function copy(text, button) {
107
+ try {
108
+ await navigator.clipboard.writeText(text);
109
+ if (button) flash(button, "Copied");
110
+ } catch {
111
+ if (button) flash(button, "Copy failed");
112
+ }
113
+ }
114
+
115
+ export function download(blob, filename) {
116
+ const a = document.createElement("a");
117
+ a.href = URL.createObjectURL(blob);
118
+ a.download = filename;
119
+ a.click();
120
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
121
+ }
122
+
123
+ // rows: array of objects (headers = keys of first row) or arrays (pass headers).
124
+ // Columns whose first-row value is a number are right-aligned.
125
+ export function renderTable(el, rows, headers) {
126
+ if (!headers) headers = rows.length ? Object.keys(rows[0]) : [];
127
+ const table = document.createElement("table");
128
+ table.className = "lt-table";
129
+ const cellsOf = (row) => (Array.isArray(row) ? row : headers.map((h) => row[h]));
130
+ const num = rows.length ? cellsOf(rows[0]).map((c) => typeof c === "number") : [];
131
+ const tr = (cells, tag) => {
132
+ const r = document.createElement("tr");
133
+ cells.forEach((c, i) => {
134
+ const cell = document.createElement(tag);
135
+ cell.textContent = c ?? "";
136
+ if (num[i]) cell.className = "lt-num";
137
+ r.append(cell);
138
+ });
139
+ return r;
140
+ };
141
+ table.createTHead().append(tr(headers, "th"));
142
+ const body = table.createTBody();
143
+ for (const row of rows) body.append(tr(cellsOf(row), "td"));
144
+ el.replaceChildren(table);
145
+ }
146
+
147
+ // findings: [{ severity: "error"|"warn"|"info"|"ok", message, where? }]
148
+ export function renderFindings(el, findings) {
149
+ const ul = document.createElement("ul");
150
+ ul.className = "lt-findings";
151
+ for (const f of findings) {
152
+ const li = document.createElement("li");
153
+ const b = span(f.severity);
154
+ b.className = "lt-badge";
155
+ b.dataset.sev = f.severity;
156
+ li.append(b, span(f.message));
157
+ if (f.where) {
158
+ const s = document.createElement("small");
159
+ s.textContent = f.where;
160
+ li.append(s);
161
+ }
162
+ ul.append(li);
163
+ }
164
+ el.replaceChildren(ul);
165
+ }
166
+
167
+ // Wire a [role=tablist].lt-tabs: each [role=tab] has aria-controls pointing at its panel.
168
+ export function tabs(list) {
169
+ const all = [...list.querySelectorAll("[role=tab]")];
170
+ const select = (t) => all.forEach((x) => {
171
+ const on = x === t;
172
+ x.setAttribute("aria-selected", on);
173
+ x.tabIndex = on ? 0 : -1;
174
+ document.getElementById(x.getAttribute("aria-controls")).hidden = !on;
175
+ });
176
+ list.addEventListener("click", (e) => { const t = e.target.closest("[role=tab]"); if (t) select(t); });
177
+ list.addEventListener("keydown", (e) => {
178
+ const d = { ArrowRight: 1, ArrowLeft: -1 }[e.key];
179
+ if (!d) return;
180
+ const t = all[(all.indexOf(document.activeElement) + d + all.length) % all.length];
181
+ select(t);
182
+ t.focus();
183
+ });
184
+ select(all.find((t) => t.getAttribute("aria-selected") === "true") || all[0]);
185
+ }
186
+
187
+ function span(text) {
188
+ const s = document.createElement("span");
189
+ s.textContent = text;
190
+ return s;
191
+ }
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: localtools-sql-formatter
3
+ Version: 1.0.0
4
+ Summary: Lint, fix and transpile SQL in any major dialect with sqlfluff and sqlglot (localtools, offline)
5
+ Project-URL: Source, https://github.com/chayprabs/localtools
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: multipart>=1.2
10
+ Requires-Dist: sqlfluff[rs]<5,>=4.4
11
+ Requires-Dist: sqlglot>=30
12
+ Description-Content-Type: text/markdown
13
+
14
+ # SQL Formatter (localtools)
15
+
16
+ Lint, auto-fix and format SQL in 28 dialects with **sqlfluff**, and transpile between dialects with **sqlglot**.
17
+ Runs on your machine: a one-page web UI on `http://127.0.0.1:47117`, plus a CLI for scripts and CI.
18
+
19
+ ## Install
20
+
21
+ | OS | command |
22
+ |---|---|
23
+ | Windows (PowerShell) | `irm https://raw.githubusercontent.com/chayprabs/localtools/main/sql-formatter/install.ps1 \| iex` |
24
+ | macOS / Linux | `curl -fsSL https://raw.githubusercontent.com/chayprabs/localtools/main/sql-formatter/install.sh \| bash` |
25
+ | PyPI | `pipx install localtools-sql-formatter` |
26
+
27
+ The script installs a self-contained build (bundled Python) into `~/.localtools/sql-formatter` and adds a launcher.
28
+
29
+ ## Use
30
+
31
+ `localtools-sql-formatter` opens the UI. Paste SQL or drop `.sql` files (several at once), pick the dialect, then:
32
+
33
+ - **Lint**: every violation with line:column, rule code (linked to the sqlfluff docs) and whether it can be auto-fixed. Download the report as JSON.
34
+ - **Fix**: the fixed SQL with a diff, Copy and Download, plus what still needs a human.
35
+ - **Transpile**: rewrite between dialects (e.g. T-SQL `TOP`/`ISNULL` to Postgres `LIMIT`/`COALESCE`), with notes on unsupported constructs.
36
+
37
+ Options: rule selection/exclusion (`LT01,CP01`, `core`, `layout`), templater (Jinja, or placeholders `:name`, `?`, `$1`, `%(name)s`, `@name`), and an uploaded `.sqlfluff` / `setup.cfg` / `tox.ini` / `pyproject.toml`.
38
+
39
+ ```sh
40
+ localtools-sql-formatter lint models/*.sql --dialect postgres --format json # or human | sarif; exit 1 on violations
41
+ localtools-sql-formatter fix query.sql --dialect snowflake # fixed SQL to stdout
42
+ localtools-sql-formatter fix *.sql --in-place --config .sqlfluff # rewrite files; exit 1 if anything is left
43
+ localtools-sql-formatter fix - --diff < query.sql # stdin, unified diff
44
+ localtools-sql-formatter transpile q.sql --read tsql --write duckdb
45
+ localtools-sql-formatter dialects | rules
46
+ ```
47
+
48
+ API: `POST /api/lint`, `/api/fix`, `/api/transpile` (multipart: `sql` and/or `file`..., `dialect`, `rules`, `exclude_rules`,
49
+ `templater`, `config` file; transpile: `read`, `write`), `GET /api/meta`. `/api/lint` with `format=sarif` returns SARIF 2.1.0.
50
+
51
+ ## Engine and why
52
+
53
+ - **sqlfluff 4** (with its Rust parser, sqlfluffrs) is the most complete open-source SQL linter: dialect-aware parsing,
54
+ ~70 rules, real auto-fix, Jinja/placeholder templating. Results are identical to the `sqlfluff` CLI with the same config
55
+ (tested on TPC-H, sqlfluff's dialect fixtures, dbt's jaffle_shop and GitLab's schema).
56
+ - `fix` repeats sqlfluff's fix pass until nothing changes, so running it twice gives the same SQL; when sqlfluff
57
+ oscillates between two layouts (it happens), the tool settles on one of them deterministically.
58
+ - **sqlglot** transpiles (sqlfluff cannot): it parses into an AST and regenerates SQL for 31 dialects. Output equals the
59
+ `python -m sqlglot --identify false` CLI. It reformats in its own style, which is why lint/fix stay with sqlfluff.
60
+
61
+ ## Limits and safety
62
+
63
+ - The web UI/API takes up to 512 KB of SQL and about 2,000 statements per run, and each run has a 120 s deadline
64
+ (sqlfluff is thorough but slow: a messy 20 KB file can take 10+ s). Every run is a separate worker process that is
65
+ killed at the deadline, when the browser disconnects, or when the server exits; at most 1–4 run at once (else HTTP 503).
66
+ On Linux each worker is capped at 2 GB of memory. The CLI has no size limit or deadline.
67
+ - Config files are parsed, never executed: `library_path`, macro/loader paths (any `*_path` / `*_dir` key), the python and
68
+ dbt templaters and `processes` are ignored and listed in the result. sqlfluff's 20 KB "skip large file" default is off.
69
+ - Files that do not parse are not fixed (like the sqlfluff CLI); the parse error shows line and column.
70
+
71
+ ## Cut
72
+
73
+ - dbt templater (needs a dbt project, adapters and a database connection) and the python templater (runs code): use
74
+ `jinja`, which provides dbt's `ref`/`source`/`config` builtins.
75
+ - Automatic discovery of `.sqlfluff` files next to the SQL: pass `--config` explicitly.
76
+ - winget/Homebrew/Scoop packages: Python-runtime tools ship through the install scripts and PyPI.
77
+
78
+ ## Update / uninstall
79
+
80
+ `localtools-sql-formatter update` · `localtools-sql-formatter uninstall` (pipx: `pipx upgrade|uninstall localtools-sql-formatter`).
81
+
82
+ ## License
83
+
84
+ MIT. Bundled components: see THIRD_PARTY_NOTICES.md.
@@ -0,0 +1,15 @@
1
+ localtools_sql_formatter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ localtools_sql_formatter/__main__.py,sha256=IaEO7qivn2CPwS9wjnkLZsSLwkuI-cvIVul0dqSoNrU,12585
3
+ localtools_sql_formatter/engine.py,sha256=2Qv4W2i7m4W4IsO5P1CBCu5OqJJvQv9hb1a4mY0fgFY,13349
4
+ localtools_sql_formatter/localtools.py,sha256=MbEqWmk5jV_mgJ7qbkJhrEdqxSaFFAqVAB_CEvLJop8,9365
5
+ localtools_sql_formatter/ui/index.html,sha256=RzL1-uBVeVEi_sxmSMSpcS0RQ7aeYRElXuoNyczg9O8,14778
6
+ localtools_sql_formatter/ui/kit/localtools.css,sha256=NgBrTzgoYRmbCXdR_pw7zQUKhAU5L2oDBOhvahbUnQk,11745
7
+ localtools_sql_formatter/ui/kit/localtools.js,sha256=imvk6LjKtqq7w3oSDaSmiC3RRilOWYHoE9WAOLjX-wU,6952
8
+ localtools_sql_formatter/ui/kit/fonts/OFL.txt,sha256=MPDBNuPIjkItB5Gs2XI4hw-QVKlym8NM8v8NTtjKxK0,4399
9
+ localtools_sql_formatter/ui/kit/fonts/jetbrains-mono-latin-bold.woff2,sha256=Mg99end-3OPuni9FiJPYQ3jc0UefDFMD8b5wjfFInRU,28312
10
+ localtools_sql_formatter/ui/kit/fonts/jetbrains-mono-latin-regular.woff2,sha256=5mWll5qAq7tYNYt5PLlvl6J3eXwOrrJd7PMXoL57kF0,27264
11
+ localtools_sql_formatter-1.0.0.dist-info/METADATA,sha256=UXytFrpEnxBNJbH_F709k-wYTUYlOAngDvwpcjpf7ZM,4897
12
+ localtools_sql_formatter-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
13
+ localtools_sql_formatter-1.0.0.dist-info/entry_points.txt,sha256=M0KcUoUfj7LQpGX0sasZ7txMcUqKUlUf9_ai27JO5pQ,84
14
+ localtools_sql_formatter-1.0.0.dist-info/licenses/LICENSE,sha256=gJAae3942rCeAsztX3njV-wLkC5woX6Bug8e1odqZHc,1076
15
+ localtools_sql_formatter-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ localtools-sql-formatter = localtools_sql_formatter.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chaitanya Prabuddha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.