localtools-sql-formatter 1.0.0__tar.gz → 1.0.1__tar.gz
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.
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/PKG-INFO +3 -2
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/README.md +2 -1
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/localtools_sql_formatter/__main__.py +83 -6
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/localtools_sql_formatter/engine.py +25 -7
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/pyproject.toml +1 -1
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/index.html +29 -10
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/.gitignore +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/LICENSE +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/localtools_sql_formatter/__init__.py +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/localtools_sql_formatter/localtools.py +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/kit/fonts/OFL.txt +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/kit/fonts/jetbrains-mono-latin-bold.woff2 +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/kit/fonts/jetbrains-mono-latin-regular.woff2 +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/kit/localtools.css +0 -0
- {localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/ui/kit/localtools.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: localtools-sql-formatter
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Lint, fix and transpile SQL in any major dialect with sqlfluff and sqlglot (localtools, offline)
|
|
5
5
|
Project-URL: Source, https://github.com/chayprabs/localtools
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,7 +34,7 @@ The script installs a self-contained build (bundled Python) into `~/.localtools/
|
|
|
34
34
|
- **Fix**: the fixed SQL with a diff, Copy and Download, plus what still needs a human.
|
|
35
35
|
- **Transpile**: rewrite between dialects (e.g. T-SQL `TOP`/`ISNULL` to Postgres `LIMIT`/`COALESCE`), with notes on unsupported constructs.
|
|
36
36
|
|
|
37
|
-
Options: rule selection/exclusion (`LT01,CP01`, `core`, `layout
|
|
37
|
+
Options: rule selection/exclusion (`LT01,CP01`, `core`, `layout`, names, legacy aliases and wildcards), templater (Raw, Jinja, or placeholders `:name`, `?`, `$1`, `$name`, `%(name)s`, `%s`), and an uploaded `.sqlfluff` / `setup.cfg` / `tox.ini` / `pyproject.toml`. Config/default uses SQLFluff's Jinja default unless the uploaded config chooses another templater. Unknown rule selectors and placeholder styles are rejected.
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
localtools-sql-formatter lint models/*.sql --dialect postgres --format json # or human | sarif; exit 1 on violations
|
|
@@ -67,6 +67,7 @@ API: `POST /api/lint`, `/api/fix`, `/api/transpile` (multipart: `sql` and/or `fi
|
|
|
67
67
|
- Config files are parsed, never executed: `library_path`, macro/loader paths (any `*_path` / `*_dir` key), the python and
|
|
68
68
|
dbt templaters and `processes` are ignored and listed in the result. sqlfluff's 20 KB "skip large file" default is off.
|
|
69
69
|
- Files that do not parse are not fixed (like the sqlfluff CLI); the parse error shows line and column.
|
|
70
|
+
- In-place fixes use an atomic replacement and retain the file mode, UTF-8 BOM and CRLF convention. Old crashed request folders are swept after one hour at startup; active owners and linked folders are preserved.
|
|
70
71
|
|
|
71
72
|
## Cut
|
|
72
73
|
|
|
@@ -21,7 +21,7 @@ The script installs a self-contained build (bundled Python) into `~/.localtools/
|
|
|
21
21
|
- **Fix**: the fixed SQL with a diff, Copy and Download, plus what still needs a human.
|
|
22
22
|
- **Transpile**: rewrite between dialects (e.g. T-SQL `TOP`/`ISNULL` to Postgres `LIMIT`/`COALESCE`), with notes on unsupported constructs.
|
|
23
23
|
|
|
24
|
-
Options: rule selection/exclusion (`LT01,CP01`, `core`, `layout
|
|
24
|
+
Options: rule selection/exclusion (`LT01,CP01`, `core`, `layout`, names, legacy aliases and wildcards), templater (Raw, Jinja, or placeholders `:name`, `?`, `$1`, `$name`, `%(name)s`, `%s`), and an uploaded `.sqlfluff` / `setup.cfg` / `tox.ini` / `pyproject.toml`. Config/default uses SQLFluff's Jinja default unless the uploaded config chooses another templater. Unknown rule selectors and placeholder styles are rejected.
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
27
|
localtools-sql-formatter lint models/*.sql --dialect postgres --format json # or human | sarif; exit 1 on violations
|
|
@@ -54,6 +54,7 @@ API: `POST /api/lint`, `/api/fix`, `/api/transpile` (multipart: `sql` and/or `fi
|
|
|
54
54
|
- Config files are parsed, never executed: `library_path`, macro/loader paths (any `*_path` / `*_dir` key), the python and
|
|
55
55
|
dbt templaters and `processes` are ignored and listed in the result. sqlfluff's 20 KB "skip large file" default is off.
|
|
56
56
|
- Files that do not parse are not fixed (like the sqlfluff CLI); the parse error shows line and column.
|
|
57
|
+
- In-place fixes use an atomic replacement and retain the file mode, UTF-8 BOM and CRLF convention. Old crashed request folders are swept after one hour at startup; active owners and linked folders are preserved.
|
|
57
58
|
|
|
58
59
|
## Cut
|
|
59
60
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
4
7
|
import select
|
|
5
8
|
import socket
|
|
6
9
|
import subprocess
|
|
@@ -49,6 +52,50 @@ JOBS = int(os.environ.get("LOCALTOOLS_SQL_JOBS") or max(1, min(4, (os.cpu_count(
|
|
|
49
52
|
SLOTS = threading.BoundedSemaphore(JOBS)
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
def process_alive(pid):
|
|
56
|
+
if os.name == "nt":
|
|
57
|
+
import ctypes
|
|
58
|
+
from ctypes import wintypes
|
|
59
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
60
|
+
kernel.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
61
|
+
kernel.OpenProcess.restype = wintypes.HANDLE
|
|
62
|
+
kernel.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
|
|
63
|
+
kernel.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
64
|
+
handle = kernel.OpenProcess(0x1000, False, pid)
|
|
65
|
+
if not handle:
|
|
66
|
+
return ctypes.get_last_error() != 87 # access denied is not proof of death
|
|
67
|
+
try:
|
|
68
|
+
code = wintypes.DWORD()
|
|
69
|
+
return not kernel.GetExitCodeProcess(handle, ctypes.byref(code)) or code.value == 259
|
|
70
|
+
finally:
|
|
71
|
+
kernel.CloseHandle(handle)
|
|
72
|
+
try:
|
|
73
|
+
os.kill(pid, 0)
|
|
74
|
+
return True
|
|
75
|
+
except ProcessLookupError:
|
|
76
|
+
return False
|
|
77
|
+
except PermissionError:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sweep_jobs():
|
|
82
|
+
"""Recover crashed jobs only: exact tool names, >1h, never traverse links/junctions."""
|
|
83
|
+
for path in Path(tempfile.gettempdir()).iterdir():
|
|
84
|
+
match = re.fullmatch(r"lt-sqlf-(?:(\d+)-)?[a-z0-9_]{8}", path.name)
|
|
85
|
+
if not match:
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
info = path.lstat()
|
|
89
|
+
if not stat.S_ISDIR(info.st_mode) or path.is_symlink() or (hasattr(path, "is_junction") and path.is_junction()):
|
|
90
|
+
continue
|
|
91
|
+
if time.time() - info.st_mtime <= 3600 or (match[1] and process_alive(int(match[1]))):
|
|
92
|
+
continue
|
|
93
|
+
# rmtree does not follow directory symlinks (or Windows junctions on Python >=3.8).
|
|
94
|
+
shutil.rmtree(path)
|
|
95
|
+
except OSError:
|
|
96
|
+
pass # a busy/locked or concurrently removed folder is safe to leave for next startup
|
|
97
|
+
|
|
98
|
+
|
|
52
99
|
def text(data, name):
|
|
53
100
|
try:
|
|
54
101
|
return engine.newlines(data.decode("utf-8-sig"))
|
|
@@ -93,11 +140,13 @@ def client_gone(req):
|
|
|
93
140
|
|
|
94
141
|
def run_worker(req, job):
|
|
95
142
|
"""One worker process per request: killed at the deadline, when the client disconnects, and (stdin EOF)
|
|
96
|
-
when this server exits. At most JOBS run at once; more get 503.
|
|
97
|
-
|
|
143
|
+
when this server exits. At most JOBS run at once; more get 503.
|
|
144
|
+
An aborted fetch frees its slot on the next poll, after the follow-up click, so wait for that
|
|
145
|
+
cancellation instead of answering 503."""
|
|
146
|
+
if not (SLOTS.acquire(blocking=False) or SLOTS.acquire(timeout=1)):
|
|
98
147
|
raise HTTPError(503, f"busy: {JOBS} SQL job(s) already running; try again when they finish")
|
|
99
148
|
try:
|
|
100
|
-
with tempfile.TemporaryDirectory(prefix="lt-sqlf-") as d:
|
|
149
|
+
with tempfile.TemporaryDirectory(prefix=f"lt-sqlf-{os.getpid()}-") as d:
|
|
101
150
|
jp = Path(d, "job.json")
|
|
102
151
|
jp.write_text(json.dumps(job), encoding="utf-8")
|
|
103
152
|
cmd = [sys.executable, *(["-I"] if sys.flags.isolated else []), "-m", __package__, "_worker", str(jp)]
|
|
@@ -112,7 +161,7 @@ def run_worker(req, job):
|
|
|
112
161
|
if client_gone(req):
|
|
113
162
|
raise HTTPError(499, "client disconnected")
|
|
114
163
|
try:
|
|
115
|
-
p.wait(0.
|
|
164
|
+
p.wait(0.05)
|
|
116
165
|
except subprocess.TimeoutExpired:
|
|
117
166
|
pass
|
|
118
167
|
finally:
|
|
@@ -143,7 +192,12 @@ def job(op, files, f):
|
|
|
143
192
|
def api(op):
|
|
144
193
|
def handler(req):
|
|
145
194
|
files, f = read_form(req)
|
|
146
|
-
|
|
195
|
+
try:
|
|
196
|
+
res = run_worker(req, job(op, files, f))
|
|
197
|
+
except HTTPError as e:
|
|
198
|
+
if e.code == 499:
|
|
199
|
+
return # the cancelled browser is gone; sending its error would raise a socket traceback
|
|
200
|
+
raise
|
|
147
201
|
if op == "lint" and f.get("format") == "sarif":
|
|
148
202
|
body = json.dumps(engine.sarif(res["files"], sqlfluff_version()), indent=2).encode()
|
|
149
203
|
req.send_response(200)
|
|
@@ -173,6 +227,27 @@ def api_meta(req):
|
|
|
173
227
|
|
|
174
228
|
|
|
175
229
|
# ---------- CLI ----------
|
|
230
|
+
def atomic_rewrite(name, sql):
|
|
231
|
+
target = Path(name).resolve() # preserve a user's symlink and replace its actual file
|
|
232
|
+
original = target.read_bytes()
|
|
233
|
+
encoding = "utf-8-sig" if original.startswith(b"\xef\xbb\xbf") else "utf-8"
|
|
234
|
+
if b"\r\n" in original and b"\n" not in original.replace(b"\r\n", b""):
|
|
235
|
+
sql = sql.replace("\n", "\r\n")
|
|
236
|
+
tmp = None
|
|
237
|
+
try:
|
|
238
|
+
with tempfile.NamedTemporaryFile(prefix=f".{target.name}.", suffix=".tmp", dir=target.parent,
|
|
239
|
+
mode="w", encoding=encoding, newline="", delete=False) as f:
|
|
240
|
+
tmp = Path(f.name)
|
|
241
|
+
f.write(sql)
|
|
242
|
+
f.flush()
|
|
243
|
+
os.fsync(f.fileno())
|
|
244
|
+
os.chmod(tmp, stat.S_IMODE(target.stat().st_mode))
|
|
245
|
+
os.replace(tmp, target)
|
|
246
|
+
finally:
|
|
247
|
+
if tmp is not None:
|
|
248
|
+
tmp.unlink(missing_ok=True)
|
|
249
|
+
|
|
250
|
+
|
|
176
251
|
def read_inputs(paths):
|
|
177
252
|
out = []
|
|
178
253
|
for p in paths:
|
|
@@ -251,7 +326,7 @@ def cli_fix(args):
|
|
|
251
326
|
for f in res:
|
|
252
327
|
if a.in_place:
|
|
253
328
|
if f["changed"]:
|
|
254
|
-
|
|
329
|
+
atomic_rewrite(f["name"], f["fixed"])
|
|
255
330
|
print(f"{f['name']}: {'fixed' if f['changed'] else 'unchanged'}", file=sys.stderr)
|
|
256
331
|
elif a.diff:
|
|
257
332
|
sys.stdout.write(f["diff"])
|
|
@@ -304,6 +379,8 @@ tool = Tool(slug="sql-formatter", port=47117, usage=USAGE, ui=UI,
|
|
|
304
379
|
|
|
305
380
|
|
|
306
381
|
def main():
|
|
382
|
+
if len(sys.argv) == 1 or sys.argv[1] == "serve" or sys.argv[1] in ("--no-open", "--port"):
|
|
383
|
+
sweep_jobs()
|
|
307
384
|
sys.exit(run(tool))
|
|
308
385
|
|
|
309
386
|
|
{localtools_sql_formatter-1.0.0 → localtools_sql_formatter-1.0.1}/localtools_sql_formatter/engine.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""sqlfluff lint/fix and sqlglot transpile, safe config loading, SARIF output and the worker process entry."""
|
|
2
2
|
import difflib
|
|
3
|
+
import fnmatch
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
@@ -12,8 +13,9 @@ MAX_BYTES = 512 << 10 # per request (server); sqlfluff lints roughly 1-2 KB/s o
|
|
|
12
13
|
MAX_STATEMENTS = 2000
|
|
13
14
|
DOCS = "https://docs.sqlfluff.com/en/stable/reference/rules/{group}.html#{code}"
|
|
14
15
|
TEMPLATERS = ("raw", "jinja", "placeholder")
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
from sqlfluff.core.templaters.placeholder import KNOWN_STYLES
|
|
17
|
+
|
|
18
|
+
PARAM_STYLES = tuple(KNOWN_STYLES)
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class SQLError(ValueError):
|
|
@@ -84,7 +86,7 @@ def load_user_config(text, name):
|
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
def make_config(dialect="", rules="", exclude_rules="", templater="", config="", config_name=".sqlfluff"):
|
|
87
|
-
"""
|
|
89
|
+
"""Empty dialect/templater uses the uploaded config, then SQLFluff's ansi/Jinja defaults."""
|
|
88
90
|
from sqlfluff.core import FluffConfig
|
|
89
91
|
from sqlfluff.core.errors import SQLFluffUserError
|
|
90
92
|
cfg, dropped = load_user_config(config, config_name) if config.strip() else ({}, [])
|
|
@@ -108,7 +110,20 @@ def make_config(dialect="", rules="", exclude_rules="", templater="", config="",
|
|
|
108
110
|
if style:
|
|
109
111
|
cfg.setdefault("templater", {}).setdefault("placeholder", {})["param_style"] = style
|
|
110
112
|
try:
|
|
111
|
-
|
|
113
|
+
result = FluffConfig(configs=cfg, overrides=ov, ignore_local_config=True)
|
|
114
|
+
from sqlfluff.core.rules import get_ruleset
|
|
115
|
+
references = get_ruleset().rule_reference_map()
|
|
116
|
+
for key in ("rule_allowlist", "rule_denylist"):
|
|
117
|
+
for selector in result.get(key) or []:
|
|
118
|
+
if not fnmatch.filter(references, selector):
|
|
119
|
+
raise SQLError(f'unknown rule selector "{selector}" (use a SQLFluff code, name, alias or group)')
|
|
120
|
+
if result.get("templater") == "placeholder":
|
|
121
|
+
from sqlfluff.core.templaters.placeholder import PlaceholderTemplater
|
|
122
|
+
try:
|
|
123
|
+
PlaceholderTemplater().get_context(fname=None, config=result)
|
|
124
|
+
except (ValueError, KeyError) as e:
|
|
125
|
+
raise SQLError(f"invalid placeholder options: {e}") from None
|
|
126
|
+
return result, dropped
|
|
112
127
|
except SQLFluffUserError as e:
|
|
113
128
|
raise SQLError(str(e)) from None
|
|
114
129
|
|
|
@@ -128,8 +143,6 @@ def violation(v):
|
|
|
128
143
|
def lint_string(linter, sql, name, fix=False):
|
|
129
144
|
try:
|
|
130
145
|
return linter.lint_string(sql, fname=name, fix=fix)
|
|
131
|
-
except Exception:
|
|
132
|
-
raise
|
|
133
146
|
except BaseException as e: # sqlfluffrs raises pyo3 PanicException (a BaseException) when its parser gives up
|
|
134
147
|
if type(e).__name__ != "PanicException":
|
|
135
148
|
raise
|
|
@@ -171,10 +184,15 @@ def fix(files, cfg, passes=5):
|
|
|
171
184
|
out.append({"name": name, "fixed": cur, "changed": cur != sql, "unparsable": unparsable,
|
|
172
185
|
"fixed_count": max(0, len(first or []) - len(left)),
|
|
173
186
|
"remaining": [violation(v) for v in left],
|
|
174
|
-
"diff":
|
|
187
|
+
"diff": unified_diff(sql, cur, name)})
|
|
175
188
|
return out
|
|
176
189
|
|
|
177
190
|
|
|
191
|
+
def unified_diff(before, after, name):
|
|
192
|
+
return "".join(line if line.endswith("\n") else line + "\n\\n"
|
|
193
|
+
for line in difflib.unified_diff(before.splitlines(True), after.splitlines(True), f"a/{name}", f"b/{name}"))
|
|
194
|
+
|
|
195
|
+
|
|
178
196
|
class _Collect(logging.Handler):
|
|
179
197
|
def __init__(self):
|
|
180
198
|
super().__init__(logging.WARNING)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "localtools-sql-formatter"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Lint, fix and transpile SQL in any major dialect with sqlfluff and sqlglot (localtools, offline)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<link rel="stylesheet" href="kit/localtools.css">
|
|
8
8
|
<style>
|
|
9
9
|
#sql { min-height: 240px; }
|
|
10
|
+
#sql, .lt-pre { font-variant-ligatures: none; } /* show SQL operators (>=, <>) as typed */
|
|
10
11
|
.lt-files button { border: 0; background: none; color: var(--muted); font: 700 13px var(--mono); cursor: pointer; padding: 0 var(--s1); }
|
|
11
12
|
.lt-files button:hover { color: var(--accent); }
|
|
12
13
|
.lt-files li span:first-child { margin-right: auto; }
|
|
@@ -67,7 +68,8 @@
|
|
|
67
68
|
<div class="lt-field">
|
|
68
69
|
<label for="templater">Templater</label>
|
|
69
70
|
<select class="lt-select" id="templater">
|
|
70
|
-
<option value="">
|
|
71
|
+
<option value="">config / default (Jinja)</option>
|
|
72
|
+
<option value="raw">raw (no templating)</option>
|
|
71
73
|
<option value="jinja">jinja {{ }}</option>
|
|
72
74
|
<option value="placeholder:colon">:name</option>
|
|
73
75
|
<option value="placeholder:question_mark">?</option>
|
|
@@ -75,7 +77,6 @@
|
|
|
75
77
|
<option value="placeholder:dollar">$name</option>
|
|
76
78
|
<option value="placeholder:pyformat">%(name)s</option>
|
|
77
79
|
<option value="placeholder:percent">%s</option>
|
|
78
|
-
<option value="placeholder:at">@name</option>
|
|
79
80
|
</select>
|
|
80
81
|
</div>
|
|
81
82
|
<div class="lt-field">
|
|
@@ -133,6 +134,17 @@ const el = (tag, props = {}, ...kids) => { const e = Object.assign(document.crea
|
|
|
133
134
|
const MAX_ROWS = 2000;
|
|
134
135
|
const picked = [];
|
|
135
136
|
let cfgFile = null;
|
|
137
|
+
let revision = 0, pending = null;
|
|
138
|
+
function invalidate() {
|
|
139
|
+
revision++;
|
|
140
|
+
pending?.abort();
|
|
141
|
+
pending = null;
|
|
142
|
+
$("out").replaceChildren(el("div", { className: "lt-empty", textContent: "No output. Run with the current input and options." }));
|
|
143
|
+
$("status").textContent = "";
|
|
144
|
+
$("err").hidden = true;
|
|
145
|
+
}
|
|
146
|
+
for (const id of ["sql", "dialect", "templater", "rules", "exclude", "read", "write", "mode"])
|
|
147
|
+
$(id).addEventListener("input", invalidate);
|
|
136
148
|
|
|
137
149
|
function fileList(ul, list, onRemove) {
|
|
138
150
|
ul.replaceChildren(...list.map((f, i) => {
|
|
@@ -142,10 +154,10 @@ function fileList(ul, list, onRemove) {
|
|
|
142
154
|
return el("li", {}, el("span", { textContent: f.name }), el("span", { textContent: fmtBytes(f.size) }), x);
|
|
143
155
|
}));
|
|
144
156
|
}
|
|
145
|
-
const renderPicked = () => fileList($("files"), picked, (i) => { picked.splice(i, 1); renderPicked(); });
|
|
146
|
-
const renderCfg = () => fileList($("cfg-files"), cfgFile ? [cfgFile] : [], () => { cfgFile = null; renderCfg(); });
|
|
147
|
-
dropZone($("drop"), null, (fs) => { picked.push(...fs); $("file").value = ""; renderPicked(); });
|
|
148
|
-
dropZone($("cfg-drop"), null, (fs) => { cfgFile = fs[0] || cfgFile; $("cfg").value = ""; renderCfg(); });
|
|
157
|
+
const renderPicked = () => fileList($("files"), picked, (i) => { picked.splice(i, 1); invalidate(); renderPicked(); });
|
|
158
|
+
const renderCfg = () => fileList($("cfg-files"), cfgFile ? [cfgFile] : [], () => { cfgFile = null; invalidate(); renderCfg(); });
|
|
159
|
+
dropZone($("drop"), null, (fs) => { picked.push(...fs); $("file").value = ""; invalidate(); renderPicked(); });
|
|
160
|
+
dropZone($("cfg-drop"), null, (fs) => { cfgFile = fs[0] || cfgFile; $("cfg").value = ""; invalidate(); renderCfg(); });
|
|
149
161
|
|
|
150
162
|
const mode = () => document.querySelector('input[name="mode"]:checked').value;
|
|
151
163
|
$("mode").onchange = () => {
|
|
@@ -240,11 +252,11 @@ const render = {
|
|
|
240
252
|
...body, ...(r ? table(f.remaining) : []));
|
|
241
253
|
});
|
|
242
254
|
},
|
|
243
|
-
transpile(res) {
|
|
255
|
+
transpile(res, submitted) {
|
|
244
256
|
return res.files.map((f) => {
|
|
245
257
|
const blob = new Blob([f.sql], { type: "application/sql" });
|
|
246
258
|
return el("article", { className: "file" },
|
|
247
|
-
bar(f.name, [0, `${
|
|
259
|
+
bar(f.name, [0, `${submitted.read || "generic"} > ${submitted.write || "generic"}`],
|
|
248
260
|
btn("Copy", (b) => copy(f.sql, b)), btn("Download", () => download(blob, sqlName(f.name)))),
|
|
249
261
|
...f.warnings.map((w) => el("div", { className: "note", textContent: "Unsupported: " + w })),
|
|
250
262
|
el("pre", { className: "lt-pre", textContent: f.sql }));
|
|
@@ -253,6 +265,8 @@ const render = {
|
|
|
253
265
|
};
|
|
254
266
|
|
|
255
267
|
async function go(m = mode()) {
|
|
268
|
+
invalidate();
|
|
269
|
+
const submittedRevision = revision;
|
|
256
270
|
const t0 = performance.now();
|
|
257
271
|
const { files, sql } = sources();
|
|
258
272
|
const fields = { sql };
|
|
@@ -265,13 +279,18 @@ async function go(m = mode()) {
|
|
|
265
279
|
$("out").replaceChildren(el("div", { className: "lt-empty", textContent: m === "transpile" ? "Transpiling..." : "sqlfluff is working. Big files take a while." }));
|
|
266
280
|
$("status").textContent = "";
|
|
267
281
|
let res;
|
|
282
|
+
const controller = pending = new AbortController();
|
|
268
283
|
try {
|
|
269
|
-
const r = await fetch("/api/" + m, { method: "POST", body });
|
|
284
|
+
const r = await fetch("/api/" + m, { method: "POST", body, signal: controller.signal });
|
|
270
285
|
res = await r.json().catch(() => ({ error: `${r.status} ${r.statusText}` }));
|
|
286
|
+
if (submittedRevision !== revision) return;
|
|
271
287
|
if (!r.ok) throw new Error(res.error);
|
|
272
288
|
} catch (e) {
|
|
289
|
+
if (submittedRevision !== revision) return;
|
|
273
290
|
$("out").replaceChildren(el("div", { className: "lt-empty", textContent: "No output." }));
|
|
274
291
|
throw e.message === "Failed to fetch" ? new Error("Cannot reach the local server. Is the tool still running?") : e;
|
|
292
|
+
} finally {
|
|
293
|
+
if (pending === controller) pending = null;
|
|
275
294
|
}
|
|
276
295
|
const head = [];
|
|
277
296
|
if (res.ignored_config?.length) head.push(el("div", { className: "note", textContent: "Ignored config settings: " + res.ignored_config.join(", ") }));
|
|
@@ -285,7 +304,7 @@ async function go(m = mode()) {
|
|
|
285
304
|
} else {
|
|
286
305
|
$("status").textContent = `${res.files.length} file${res.files.length > 1 ? "s" : ""} · ${fmtMs(performance.now() - t0)}`;
|
|
287
306
|
}
|
|
288
|
-
$("out").replaceChildren(...head, ...render[m](res));
|
|
307
|
+
$("out").replaceChildren(...head, ...render[m](res, fields));
|
|
289
308
|
}
|
|
290
309
|
|
|
291
310
|
$("go").onclick = () => run($("go"), $("err"), () => go());
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|