inline-sql-toolkit 0.4.13 → 0.5.0
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.
- package/CHANGELOG.md +33 -0
- package/README.md +65 -25
- package/dist/cli.js +509 -160
- package/package.json +2 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Changed: `inlineSql.format.replaceOrdinals` now defaults to `false`, since it
|
|
6
|
+
rewrites the query rather than its layout. GROUP BY ordinals are replaced by
|
|
7
|
+
the copied expression (never an alias); ordinals at or after `*`, after
|
|
8
|
+
UNION / EXCEPT / INTERSECT, with SELECT modifiers, or naming volatile calls
|
|
9
|
+
are kept. The CLI gains `--ordinals`.
|
|
10
|
+
- Fixed: keyword-only candidates need a SQL statement shape and a single-case
|
|
11
|
+
keyword, so prose such as "Update available" is no longer formatted.
|
|
12
|
+
- Fixed: comment-, continuation- and `+ name +`-joined literals are treated as
|
|
13
|
+
concatenation (unsupported) instead of standalone SQL; single-quoted literals
|
|
14
|
+
keep their edge spaces.
|
|
15
|
+
- Fixed: documents with an unterminated string, and f-strings that reuse their
|
|
16
|
+
own quote, are skipped.
|
|
17
|
+
- Safety: every candidate whose SQL tokens (strings, identifiers, numbers,
|
|
18
|
+
symbols, comments) would change is skipped as `FORMATTER_FAILED`. DISTRIBUTE
|
|
19
|
+
splitting and f-string field markers no longer touch strings or comments.
|
|
20
|
+
- Fixed: UTF-16 positions on lines with emoji; SQL marker matched in any case;
|
|
21
|
+
SQL-language notebook cells are no longer formatted as Python.
|
|
22
|
+
- Fixed: notebook Format All re-checks cells, cancellation and trust before
|
|
23
|
+
one WorkspaceEdit; size limits report `RESOURCE_LIMIT_EXCEEDED`.
|
|
24
|
+
- CLI: skipped candidates are reported on stderr (`-q` suppresses).
|
|
25
|
+
- Performance: each candidate is formatted on its own text; Code Action scans
|
|
26
|
+
are cached per document version.
|
|
27
|
+
|
|
28
|
+
## 0.4.14 - 2026-09-25
|
|
29
|
+
|
|
30
|
+
- Changed: `inlineSql.format.commaPosition: "before"` emits the moved comma
|
|
31
|
+
with a space after it again (`id` / ` , name`). 0.4.13 removed the space
|
|
32
|
+
for a single release; the spaced form is the wanted style. The default
|
|
33
|
+
`after` output is unchanged, and `src/` and `test/` are byte-identical to
|
|
34
|
+
v0.4.12.
|
|
35
|
+
|
|
3
36
|
## 0.4.13 - 2026-09-25
|
|
4
37
|
|
|
5
38
|
- Changed: `inlineSql.format.commaPosition: "before"` now emits the moved
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Inline SQL Toolkit
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Inline SQL Toolkit highlights SQL embedded in Python strings and formats selected
|
|
4
6
|
SQL with the bundled `sql-formatter` layout engine. It supports ordinary `.py`
|
|
5
7
|
files, marimo
|
|
@@ -106,13 +108,20 @@ later atomic precondition.
|
|
|
106
108
|
- `inlineSql.format.useSpaceAroundOperators`: add spaces around operators
|
|
107
109
|
(default `true`).
|
|
108
110
|
- `inlineSql.format.replaceOrdinals`: replace `GROUP BY` / `ORDER BY` ordinal
|
|
109
|
-
numbers (`1`, `2`, ...) with the referenced column
|
|
110
|
-
|
|
111
|
+
numbers (`1`, `2`, ...) with the referenced select-list column (default
|
|
112
|
+
`false`). This rewrites the query rather than its layout, so it is opt-in.
|
|
113
|
+
`GROUP BY` copies the column's expression, because PostgreSQL, MySQL, and
|
|
114
|
+
SQLite resolve a `GROUP BY` name to an input column before an alias;
|
|
115
|
+
`ORDER BY` uses the alias when it is unique. An ordinal is kept when its
|
|
116
|
+
position is unknown (at or after `*` / `t.*`), when it follows `UNION`,
|
|
117
|
+
`EXCEPT`, or `INTERSECT` (`ORDER BY`), when the replacement would name
|
|
118
|
+
another output column, or when the expression is an aggregate without an
|
|
119
|
+
alias or calls a volatile function such as `random()`.
|
|
111
120
|
- `inlineSql.format.dialect`: SQL dialect used by the formatter (`sql`,
|
|
112
121
|
`mysql`, `postgresql`, or `sqlite`; default `postgresql`).
|
|
113
122
|
- `inlineSql.format.commaPosition`: `after` (default) keeps a wrapping comma at
|
|
114
123
|
the end of the previous line; `before` moves it to the start of the next
|
|
115
|
-
line
|
|
124
|
+
line with a space after it (`id` / ` , name`).
|
|
116
125
|
- `inlineSql.format.keepFunctionsInline`: re-join the formatter's line breaks
|
|
117
126
|
inside `word(...)` groups so `SUM(...)`, `COUNT(CASE ... END)`, and nested
|
|
118
127
|
calls stay on one line (default `false`). Newlines inside string literals and
|
|
@@ -129,7 +138,7 @@ disappears, disable semantic highlighting for the language server
|
|
|
129
138
|
|
|
130
139
|
## Command line (CLI)
|
|
131
140
|
|
|
132
|
-
The `inline-sql-toolkit` command-line tool formats Python files or standard input using the exact same formatting engine and safety checks as the VS Code extension's **Format All** command. Unsafe candidates (such as
|
|
141
|
+
The `inline-sql-toolkit` command-line tool formats Python files or standard input using the exact same formatting engine and safety checks as the VS Code extension's **Format All** command. Unsafe candidates (such as unsupported literals, unparseable f-strings, or any candidate in a document with an unterminated string) are skipped rather than producing corrupt output.
|
|
133
142
|
|
|
134
143
|
Run directly via `npx` or install with `bun add -d inline-sql-toolkit` (or `npm install -D inline-sql-toolkit`):
|
|
135
144
|
|
|
@@ -141,24 +150,28 @@ When run without file arguments, the CLI reads Python source from standard input
|
|
|
141
150
|
|
|
142
151
|
### CLI Options
|
|
143
152
|
|
|
144
|
-
| Option | Description
|
|
145
|
-
| ----------------------------- |
|
|
146
|
-
| `-w, --write` | Rewrite files in place (no write if unchanged)
|
|
147
|
-
| `--check` | Exit with code 1 if any file would change
|
|
148
|
-
| `--dialect <name>` | SQL dialect: `sql`, `mysql`, `postgresql`, or `sqlite`
|
|
149
|
-
| `--keyword-case <case>` | Case for SQL keywords: `upper`, `lower`, or `preserve`
|
|
150
|
-
| `--indent-width <1-8>` | SQL indentation width in spaces
|
|
151
|
-
| `--wrap-after <20-500>` | Preferred expression line width
|
|
152
|
-
| `--no-space-around-operators` | Keep dense operators
|
|
153
|
-
| `--
|
|
154
|
-
| `--
|
|
155
|
-
| `--
|
|
156
|
-
|
|
|
157
|
-
| `-
|
|
158
|
-
|
|
|
153
|
+
| Option | Description | Default |
|
|
154
|
+
| ----------------------------- | ---------------------------------------------------------- | -------------------------- |
|
|
155
|
+
| `-w, --write` | Rewrite files in place (no write if unchanged) | off |
|
|
156
|
+
| `--check` | Exit with code 1 if any file would change | off |
|
|
157
|
+
| `--dialect <name>` | SQL dialect: `sql`, `mysql`, `postgresql`, or `sqlite` | `postgresql` |
|
|
158
|
+
| `--keyword-case <case>` | Case for SQL keywords: `upper`, `lower`, or `preserve` | `upper` |
|
|
159
|
+
| `--indent-width <1-8>` | SQL indentation width in spaces | `2` |
|
|
160
|
+
| `--wrap-after <20-500>` | Preferred expression line width | `88` |
|
|
161
|
+
| `--no-space-around-operators` | Keep dense operators | spaced |
|
|
162
|
+
| `--ordinals` | Replace `GROUP BY` / `ORDER BY` ordinals with column names | off |
|
|
163
|
+
| `--no-ordinals` | Do not replace ordinals (overrides the configuration file) | |
|
|
164
|
+
| `--comma-position <pos>` | Where a wrapping comma sits: `after` or `before` | `after` |
|
|
165
|
+
| `--keep-functions-inline` | Keep `SUM(...)` / `COUNT(CASE ... END)` on one line | off |
|
|
166
|
+
| `-c, --config <file>` | Configuration JSON file | Nearest `.inline-sql.json` |
|
|
167
|
+
| `-q, --quiet` | Do not report skipped candidates on stderr | report |
|
|
168
|
+
| `-h, --help` | Show usage help | |
|
|
169
|
+
| `--version` | Show version number | |
|
|
159
170
|
|
|
160
171
|
Exit codes: `0` on success, `1` when `--check` finds unformatted files, and `2` on usage, configuration, I/O, or formatting errors.
|
|
161
172
|
|
|
173
|
+
A detected SQL candidate that cannot be formatted safely is left as it is and does not change the exit code. Unless `--quiet` is given, the CLI prints one line per affected file on stderr, such as `inline-sql-toolkit: app.py: skipped 2 SQL candidates (UNSUPPORTED_LITERAL x2)`; [SUPPORT.md](SUPPORT.md) lists the reason codes.
|
|
174
|
+
|
|
162
175
|
### Configuration file (`.inline-sql.json`)
|
|
163
176
|
|
|
164
177
|
The CLI automatically searches for a `.inline-sql.json` file in the current working directory and its parent directories. The configuration structure matches VS Code settings:
|
|
@@ -170,7 +183,7 @@ The CLI automatically searches for a `.inline-sql.json` file in the current work
|
|
|
170
183
|
"indentWidth": 2,
|
|
171
184
|
"wrapAfter": 88,
|
|
172
185
|
"useSpaceAroundOperators": true,
|
|
173
|
-
"replaceOrdinals":
|
|
186
|
+
"replaceOrdinals": false,
|
|
174
187
|
"dialect": "postgresql",
|
|
175
188
|
"commaPosition": "after",
|
|
176
189
|
"keepFunctionsInline": false
|
|
@@ -194,7 +207,21 @@ is found when either condition holds:
|
|
|
194
207
|
characters, the source starts with one of `SELECT`, `WITH`, `INSERT`,
|
|
195
208
|
`UPDATE`, `DELETE`, `MERGE`, `CREATE`, `ALTER`, `DROP`, `TRUNCATE`, or
|
|
196
209
|
`EXPLAIN`, followed by a word boundary. The two source characters `\n` are
|
|
197
|
-
not treated as whitespace.
|
|
210
|
+
not treated as whitespace before the keyword. The keyword must be written
|
|
211
|
+
entirely in upper or lower case (`SELECT` / `select`, never `Select`), and
|
|
212
|
+
it must open a recognizable statement so that prose and bare values are
|
|
213
|
+
left alone:
|
|
214
|
+
- `SELECT` is followed by a `FROM` clause or by a select-list expression
|
|
215
|
+
(`*`, a number, a string, a parameter, a function call such as `now()`,
|
|
216
|
+
`CASE`, ...), so `SELECT 1` matches but `Select an option` does not.
|
|
217
|
+
- `WITH name AS (`, `INSERT INTO`, `UPDATE table SET`, `DELETE FROM`,
|
|
218
|
+
`MERGE INTO`, `CREATE`/`ALTER`/`DROP` followed by an object kind such as
|
|
219
|
+
`TABLE`, `VIEW`, or `INDEX`, `TRUNCATE [TABLE] table`, and `EXPLAIN`
|
|
220
|
+
followed by a statement.
|
|
221
|
+
|
|
222
|
+
Inside this shape check, the escapes `\n`, `\r`, `\t`, and a
|
|
223
|
+
backslash-newline count as whitespace. Use the `--sql` marker for any
|
|
224
|
+
statement that does not fit these shapes.
|
|
198
225
|
|
|
199
226
|
Standalone plain and raw strings, f-strings, and raw f-strings (`f`, `rf`, and
|
|
200
227
|
`fr`, in either case) are supported with single, double, and triple delimiters
|
|
@@ -202,9 +229,22 @@ Standalone plain and raw strings, f-strings, and raw f-strings (`f`, `rf`, and
|
|
|
202
229
|
also the candidate used by the syntax highlighting grammar.
|
|
203
230
|
|
|
204
231
|
The following are intentionally skipped: bytes and byte strings (`b`/`rb`),
|
|
205
|
-
implicit or explicit string concatenation, t-strings,
|
|
206
|
-
|
|
207
|
-
|
|
232
|
+
implicit or explicit string concatenation, t-strings, f-strings whose
|
|
233
|
+
replacement field reuses the literal's own quote (`f"{row["id"]}"`), dynamic or
|
|
234
|
+
non-literal SQL, and SQL-language cells. The extension does not run a Python
|
|
235
|
+
parser: it scans string literals only. A document with an unterminated string
|
|
236
|
+
literal is not formatted at all, because the scan can no longer tell code from
|
|
237
|
+
string text; other syntax errors outside a literal do not stop formatting. Concatenation covers adjacent
|
|
238
|
+
literals even across comments or `\` line continuations, and any literal joined
|
|
239
|
+
with `+` or `+=`, whatever the other operand is. A candidate that cannot be
|
|
240
|
+
restored without changing Python source is reported as unsafe and is not
|
|
241
|
+
edited. Before any edit, the formatted SQL is lexed again and compared with
|
|
242
|
+
the original: apart from whitespace and keyword case, every string, quoted
|
|
243
|
+
identifier, number, operator, and comment must be unchanged and in the same
|
|
244
|
+
order, or the candidate is skipped (`FORMATTER_FAILED`).
|
|
245
|
+
|
|
246
|
+
A single-quoted literal stays on one line and keeps its leading and trailing
|
|
247
|
+
spaces, because the string may be joined to other text at runtime.
|
|
208
248
|
|
|
209
249
|
## Trust, privacy, and offline behavior
|
|
210
250
|
|
|
@@ -221,7 +261,7 @@ over the network, passed to a shell/database, or executed. The bundled
|
|
|
221
261
|
the first logical line has `-- sql`/`--sql`, or that a listed keyword is at
|
|
222
262
|
the source-level start with a word boundary.
|
|
223
263
|
- **Unsupported literal or unsafe f-string:** remove concatenation, bytes or
|
|
224
|
-
t-string syntax, and
|
|
264
|
+
t-string syntax, and close every string literal in the document. Complex f-string
|
|
225
265
|
expressions are skipped when their source spans cannot be restored exactly.
|
|
226
266
|
- **Formatting is unavailable:** use a trusted workspace and check the
|
|
227
267
|
diagnostic reason shown by the extension (`WORKSPACE_UNTRUSTED`,
|
package/dist/cli.js
CHANGED
|
@@ -473,6 +473,7 @@ __export(cli_exports, {
|
|
|
473
473
|
buildRawOptions: () => buildRawOptions,
|
|
474
474
|
findConfigFile: () => findConfigFile,
|
|
475
475
|
formatPythonSource: () => formatPythonSource,
|
|
476
|
+
formatPythonSourceWithReport: () => formatPythonSourceWithReport,
|
|
476
477
|
getPackageVersion: () => getPackageVersion,
|
|
477
478
|
loadConfigFile: () => loadConfigFile,
|
|
478
479
|
runCli: () => runCli
|
|
@@ -492,7 +493,7 @@ function resolveFormatOptions(raw) {
|
|
|
492
493
|
const indentWidth = raw.indentWidth === void 0 ? 2 : raw.indentWidth;
|
|
493
494
|
const wrapAfter = raw.wrapAfter === void 0 ? 88 : raw.wrapAfter;
|
|
494
495
|
const useSpaceAroundOperators = raw.useSpaceAroundOperators === void 0 ? true : raw.useSpaceAroundOperators;
|
|
495
|
-
const replaceOrdinals2 = raw.replaceOrdinals === void 0 ?
|
|
496
|
+
const replaceOrdinals2 = raw.replaceOrdinals === void 0 ? false : raw.replaceOrdinals;
|
|
496
497
|
const dialect = raw.dialect === void 0 ? "postgresql" : raw.dialect;
|
|
497
498
|
const commaPosition = raw.commaPosition === void 0 ? "after" : raw.commaPosition;
|
|
498
499
|
const keepFunctionsInline = raw.keepFunctionsInline === void 0 ? false : raw.keepFunctionsInline;
|
|
@@ -610,7 +611,8 @@ var SourceMap = class _SourceMap {
|
|
|
610
611
|
/** Convert a zero-based VS Code line and UTF-16 column to an offset. */
|
|
611
612
|
offsetFromVscode(line, utf16Col) {
|
|
612
613
|
const record = this.line(line);
|
|
613
|
-
|
|
614
|
+
exactIndex(record.utf16AtCodepoint, utf16Col);
|
|
615
|
+
return record.start + utf16Col;
|
|
614
616
|
}
|
|
615
617
|
/** Convert a source offset to a strict zero-based VS Code position. */
|
|
616
618
|
vscodeFromOffset(offset) {
|
|
@@ -625,11 +627,8 @@ var SourceMap = class _SourceMap {
|
|
|
625
627
|
if (offset > record.contentEnd) {
|
|
626
628
|
throw new PositionMappingError("offset is inside a line terminator");
|
|
627
629
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
if (character === void 0) {
|
|
631
|
-
throw new PositionMappingError("column is not a code-point boundary");
|
|
632
|
-
}
|
|
630
|
+
const character = offset - record.start;
|
|
631
|
+
exactIndex(record.utf16AtCodepoint, character);
|
|
633
632
|
return { line: lineIndex, character };
|
|
634
633
|
}
|
|
635
634
|
/** Convert a source span to a half-open VS Code range. */
|
|
@@ -640,20 +639,74 @@ var SourceMap = class _SourceMap {
|
|
|
640
639
|
|
|
641
640
|
// src/python-analysis/detection.ts
|
|
642
641
|
var MARKERS = /* @__PURE__ */ new Set(["-- sql", "--sql"]);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"insert",
|
|
647
|
-
"update",
|
|
648
|
-
"delete",
|
|
649
|
-
"merge",
|
|
650
|
-
"create",
|
|
651
|
-
"alter",
|
|
652
|
-
"drop",
|
|
653
|
-
"truncate",
|
|
654
|
-
"explain"
|
|
655
|
-
];
|
|
642
|
+
function isSqlMarker(line) {
|
|
643
|
+
return MARKERS.has(line.trim().toLowerCase());
|
|
644
|
+
}
|
|
656
645
|
var ASCII_WHITESPACE = /* @__PURE__ */ new Set([" ", " ", "\r", "\n"]);
|
|
646
|
+
var IDENT = String.raw`(?:[\p{L}_][\p{L}\p{N}_$]*|"[^"]*"|\x60[^\x60]*\x60|\[[^\]]*\]|\{[^{}]*\}|%(?:\([^)]*\))?s)`;
|
|
647
|
+
var TABLE_REF = String.raw`${IDENT}(?:\s*\.\s*${IDENT})*`;
|
|
648
|
+
var OBJECT = String.raw`(?:table|view|index|schema|database|function|procedure|trigger|sequence|type|extension|domain|policy|aggregate|collation|tablespace|publication|subscription|event|server|rule|statistics)`;
|
|
649
|
+
var CREATE_MODIFIER = String.raw`(?:global|local|temp|temporary|unlogged|unique|materialized|recursive|virtual|external|fulltext|spatial|secure|transient)`;
|
|
650
|
+
var SELECT_LIST_START = new RegExp(
|
|
651
|
+
String.raw`^\s*(?:[*(\d'"\x60{:@$?%-]|[\p{L}_][\p{L}\p{N}_$.]*\s*\(|(?:case|cast|exists|not|null|true|false|current_\w+|localtime\w*)\b)`,
|
|
652
|
+
"iu"
|
|
653
|
+
);
|
|
654
|
+
var KEYWORD_SHAPES = [
|
|
655
|
+
[
|
|
656
|
+
"select",
|
|
657
|
+
(text) => {
|
|
658
|
+
const rest = text.slice("select".length);
|
|
659
|
+
return /\bfrom\b/i.test(rest) || SELECT_LIST_START.test(rest);
|
|
660
|
+
}
|
|
661
|
+
],
|
|
662
|
+
[
|
|
663
|
+
"with",
|
|
664
|
+
shape(
|
|
665
|
+
String.raw`with\s+(?:recursive\s+)?${IDENT}\s*(?:\([^)]*\)\s*)?as\s*(?:(?:not\s+)?materialized\s*)?\(`
|
|
666
|
+
)
|
|
667
|
+
],
|
|
668
|
+
[
|
|
669
|
+
"insert",
|
|
670
|
+
shape(
|
|
671
|
+
String.raw`insert\s+(?:or\s+\w+\s+)?(?:(?:low_priority|delayed|high_priority|ignore)\s+)*(?:into|overwrite)\b`
|
|
672
|
+
)
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"update",
|
|
676
|
+
shape(
|
|
677
|
+
String.raw`update\s+(?:(?:only|low_priority|ignore)\s+|or\s+\w+\s+)*${TABLE_REF}(?:\s+(?:as\s+)?${IDENT})?\s+set\b`
|
|
678
|
+
)
|
|
679
|
+
],
|
|
680
|
+
[
|
|
681
|
+
"delete",
|
|
682
|
+
shape(
|
|
683
|
+
String.raw`delete\s+(?:(?:low_priority|quick|ignore)\s+)*(?:from\b|${TABLE_REF}(?:\s*,\s*${TABLE_REF})*\s+from\b)`
|
|
684
|
+
)
|
|
685
|
+
],
|
|
686
|
+
["merge", shape(String.raw`merge\s+into\b`)],
|
|
687
|
+
["create", shape(String.raw`create\s+(?:or\s+replace\s+)?(?:${CREATE_MODIFIER}\s+)*${OBJECT}\b`)],
|
|
688
|
+
["alter", shape(String.raw`alter\s+(?:materialized\s+)?${OBJECT}\b`)],
|
|
689
|
+
["drop", shape(String.raw`drop\s+(?:${CREATE_MODIFIER}\s+)*${OBJECT}\b`)],
|
|
690
|
+
[
|
|
691
|
+
"truncate",
|
|
692
|
+
shape(
|
|
693
|
+
String.raw`truncate\s+(?:table\b|(?:only\s+)?${TABLE_REF}(?:\s*,\s*${TABLE_REF})*\s*(?:;|$|(?:restart|continue|cascade|restrict)\b))`
|
|
694
|
+
)
|
|
695
|
+
],
|
|
696
|
+
[
|
|
697
|
+
"explain",
|
|
698
|
+
shape(
|
|
699
|
+
String.raw`explain\s+(?:(?:analyze|analyse|verbose|extended|query\s+plan)\s+|\([^)]*\)\s*)*(?:select|with|insert|update|delete|merge|values|table|execute|create|declare)\b`
|
|
700
|
+
)
|
|
701
|
+
]
|
|
702
|
+
];
|
|
703
|
+
function shape(pattern) {
|
|
704
|
+
const regex = new RegExp(`^${pattern}`, "iu");
|
|
705
|
+
return (text) => regex.test(text);
|
|
706
|
+
}
|
|
707
|
+
function shapeText(text) {
|
|
708
|
+
return text.replace(/\\(?:[nrt]|\r\n|\r|\n)/g, " ");
|
|
709
|
+
}
|
|
657
710
|
function continuesIdentifier(character) {
|
|
658
711
|
if (character === "") return false;
|
|
659
712
|
if (character === "_") return true;
|
|
@@ -675,7 +728,7 @@ function detectSourceSlice(text, base) {
|
|
|
675
728
|
cursor += line.length;
|
|
676
729
|
continue;
|
|
677
730
|
}
|
|
678
|
-
if (
|
|
731
|
+
if (isSqlMarker(body)) {
|
|
679
732
|
const marker = new SourceSpan(base + cursor, base + cursor + body.length);
|
|
680
733
|
return {
|
|
681
734
|
matched: true,
|
|
@@ -690,11 +743,12 @@ function detectSourceSlice(text, base) {
|
|
|
690
743
|
while (significant < text.length && ASCII_WHITESPACE.has(text[significant] ?? "")) {
|
|
691
744
|
significant++;
|
|
692
745
|
}
|
|
693
|
-
const
|
|
694
|
-
for (const keyword of
|
|
695
|
-
|
|
746
|
+
const statement = text.slice(significant);
|
|
747
|
+
for (const [keyword, matchesShape] of KEYWORD_SHAPES) {
|
|
748
|
+
const written = statement.slice(0, keyword.length);
|
|
749
|
+
if (written !== keyword && written !== keyword.toUpperCase()) continue;
|
|
696
750
|
const following = text[significant + keyword.length] ?? "";
|
|
697
|
-
if (!continuesIdentifier(following)) {
|
|
751
|
+
if (!continuesIdentifier(following) && matchesShape(shapeText(statement))) {
|
|
698
752
|
return {
|
|
699
753
|
matched: true,
|
|
700
754
|
markerSpan: void 0,
|
|
@@ -752,8 +806,9 @@ function stringSurfaceAt(source, index) {
|
|
|
752
806
|
contentSpan: new SourceSpan(contentStart, end - delimiter.length)
|
|
753
807
|
};
|
|
754
808
|
}
|
|
755
|
-
function
|
|
809
|
+
function scanSource(source) {
|
|
756
810
|
const surfaces = [];
|
|
811
|
+
let unterminated = false;
|
|
757
812
|
let index = 0;
|
|
758
813
|
while (index < source.length) {
|
|
759
814
|
const char = source[index] ?? "";
|
|
@@ -773,6 +828,7 @@ function scanStringSurfaces(source) {
|
|
|
773
828
|
index = surface.span.end;
|
|
774
829
|
continue;
|
|
775
830
|
}
|
|
831
|
+
if (char === '"' || char === "'") unterminated = true;
|
|
776
832
|
}
|
|
777
833
|
if (NAME.test(source.slice(index))) {
|
|
778
834
|
const name = NAME.exec(source.slice(index))?.[0];
|
|
@@ -781,9 +837,9 @@ function scanStringSurfaces(source) {
|
|
|
781
837
|
}
|
|
782
838
|
index++;
|
|
783
839
|
}
|
|
784
|
-
return surfaces;
|
|
840
|
+
return { surfaces, unterminated };
|
|
785
841
|
}
|
|
786
|
-
function
|
|
842
|
+
function scanFstringFields(source, contentSpan) {
|
|
787
843
|
const fields = [];
|
|
788
844
|
let fieldStart = -1;
|
|
789
845
|
const closers = [];
|
|
@@ -853,7 +909,7 @@ function scanFstringFieldSpans(source, contentSpan) {
|
|
|
853
909
|
}
|
|
854
910
|
index++;
|
|
855
911
|
}
|
|
856
|
-
return fields;
|
|
912
|
+
return { fields, unclosed: fieldStart !== -1 };
|
|
857
913
|
}
|
|
858
914
|
function fstringKind(prefix) {
|
|
859
915
|
const normalized = prefix.toLowerCase();
|
|
@@ -866,25 +922,26 @@ function fstringKind(prefix) {
|
|
|
866
922
|
function onlyWhitespace(text) {
|
|
867
923
|
return /^[\s]*$/.test(text);
|
|
868
924
|
}
|
|
925
|
+
function codeOnly(text) {
|
|
926
|
+
return text.replace(/#.*$/gm, "").replace(/\\(?:\r\n|\r|\n)/g, " ");
|
|
927
|
+
}
|
|
928
|
+
var OPERATOR_ONLY = /^\s*[+\-*/%&|^<>]+\s*$/;
|
|
869
929
|
function isConcatenated(source, surfaces, index) {
|
|
870
930
|
const current = surfaces[index];
|
|
871
931
|
if (current === void 0) return false;
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (onlyWhitespace(code)) return false;
|
|
882
|
-
return /^\s*[+\-*/%&|^<>]+\s*$/.test(code);
|
|
883
|
-
});
|
|
932
|
+
const previous = surfaces[index - 1];
|
|
933
|
+
const next = surfaces[index + 1];
|
|
934
|
+
const before = codeOnly(source.slice(previous?.span.end ?? 0, current.span.start));
|
|
935
|
+
const after = codeOnly(source.slice(current.span.end, next?.span.start ?? source.length));
|
|
936
|
+
if (previous !== void 0 && onlyWhitespace(before)) return true;
|
|
937
|
+
if (next !== void 0 && onlyWhitespace(after)) return true;
|
|
938
|
+
if (/\+=?\s*$/.test(before) || /^\s*\+/.test(after)) return true;
|
|
939
|
+
if (previous !== void 0 && OPERATOR_ONLY.test(before)) return true;
|
|
940
|
+
return next !== void 0 && OPERATOR_ONLY.test(after);
|
|
884
941
|
}
|
|
885
942
|
function analyzeDocument(source) {
|
|
886
943
|
const sourceMap = SourceMap.fromText(source);
|
|
887
|
-
const surfaces =
|
|
944
|
+
const { surfaces, unterminated } = scanSource(source);
|
|
888
945
|
const supported = [];
|
|
889
946
|
const unsupported = [];
|
|
890
947
|
surfaces.forEach((surface, index) => {
|
|
@@ -915,10 +972,12 @@ function analyzeDocument(source) {
|
|
|
915
972
|
}
|
|
916
973
|
if (surface.kind === "fstring") {
|
|
917
974
|
const kind = fstringKind(surface.prefix);
|
|
918
|
-
|
|
975
|
+
const fields = scanFstringFields(source, surface.contentSpan);
|
|
976
|
+
if (kind === void 0 || fields.unclosed) {
|
|
919
977
|
unsupported.push({
|
|
920
978
|
span: surface.span,
|
|
921
|
-
|
|
979
|
+
// Still SQL to the user: report it as skipped rather than ignore it.
|
|
980
|
+
detectionContentSpan: kind === void 0 ? void 0 : surface.contentSpan,
|
|
922
981
|
reason: "UNSUPPORTED_LITERAL"
|
|
923
982
|
});
|
|
924
983
|
return;
|
|
@@ -929,7 +988,7 @@ function analyzeDocument(source) {
|
|
|
929
988
|
prefix: surface.prefix,
|
|
930
989
|
delimiter: surface.delimiter,
|
|
931
990
|
kind,
|
|
932
|
-
fieldSpans:
|
|
991
|
+
fieldSpans: fields.fields
|
|
933
992
|
});
|
|
934
993
|
return;
|
|
935
994
|
}
|
|
@@ -944,6 +1003,7 @@ function analyzeDocument(source) {
|
|
|
944
1003
|
});
|
|
945
1004
|
return {
|
|
946
1005
|
sourceMap,
|
|
1006
|
+
unterminated,
|
|
947
1007
|
supported: [...supported].sort((left, right) => left.span.start - right.span.start),
|
|
948
1008
|
unsupported: [...unsupported].sort((left, right) => left.span.start - right.span.start)
|
|
949
1009
|
};
|
|
@@ -1231,6 +1291,31 @@ var AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
|
1231
1291
|
"sum",
|
|
1232
1292
|
"xmlagg"
|
|
1233
1293
|
]);
|
|
1294
|
+
var VOLATILE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1295
|
+
"clock_timestamp",
|
|
1296
|
+
"gen_random_uuid",
|
|
1297
|
+
"newid",
|
|
1298
|
+
"nextval",
|
|
1299
|
+
"rand",
|
|
1300
|
+
"random",
|
|
1301
|
+
"setval",
|
|
1302
|
+
"timeofday",
|
|
1303
|
+
"uuid",
|
|
1304
|
+
"uuid_generate_v1",
|
|
1305
|
+
"uuid_generate_v4"
|
|
1306
|
+
]);
|
|
1307
|
+
var SELECT_MODIFIERS = /* @__PURE__ */ new Set([
|
|
1308
|
+
"all",
|
|
1309
|
+
"distinct",
|
|
1310
|
+
"distinctrow",
|
|
1311
|
+
"high_priority",
|
|
1312
|
+
"sql_big_result",
|
|
1313
|
+
"sql_buffer_result",
|
|
1314
|
+
"sql_calc_found_rows",
|
|
1315
|
+
"sql_no_cache",
|
|
1316
|
+
"sql_small_result",
|
|
1317
|
+
"straight_join"
|
|
1318
|
+
]);
|
|
1234
1319
|
var ORDINAL_SUFFIXES = /* @__PURE__ */ new Set(["asc", "desc", "nulls", "first", "last"]);
|
|
1235
1320
|
var RESERVED = /* @__PURE__ */ new Set([
|
|
1236
1321
|
"as",
|
|
@@ -1298,9 +1383,9 @@ function isSelectStart(tokens, index) {
|
|
|
1298
1383
|
if (index === 0) return true;
|
|
1299
1384
|
const previous = tokens[index - 1];
|
|
1300
1385
|
const text = previous?.text.toLowerCase() ?? "";
|
|
1301
|
-
if (text === "all") {
|
|
1386
|
+
if (text === "all" || text === "distinct") {
|
|
1302
1387
|
const before = tokens[index - 2]?.text.toLowerCase() ?? "";
|
|
1303
|
-
return before === "union" || before === "except";
|
|
1388
|
+
return before === "union" || before === "except" || before === "intersect";
|
|
1304
1389
|
}
|
|
1305
1390
|
return text === "(" || text === ";" || text === ")" || text === "union" || text === "except" || text === "intersect" || text.startsWith("--") || text.startsWith("{");
|
|
1306
1391
|
}
|
|
@@ -1310,12 +1395,51 @@ function isOperator(token) {
|
|
|
1310
1395
|
function isCommentToken(token) {
|
|
1311
1396
|
return token.text.startsWith("--") || token.text.startsWith("/*");
|
|
1312
1397
|
}
|
|
1398
|
+
function isSetOperand(tokens, index) {
|
|
1399
|
+
const operator2 = (offset) => ["union", "except", "intersect"].includes(tokens[index - offset]?.text.toLowerCase() ?? "");
|
|
1400
|
+
const previous = tokens[index - 1]?.text.toLowerCase() ?? "";
|
|
1401
|
+
return operator2(1) || (previous === "all" || previous === "distinct") && operator2(2);
|
|
1402
|
+
}
|
|
1403
|
+
function firstItemStart(tokens, start) {
|
|
1404
|
+
let index = start;
|
|
1405
|
+
while (index < tokens.length) {
|
|
1406
|
+
const token = tokens[index];
|
|
1407
|
+
if (token === void 0 || isCommentToken(token)) {
|
|
1408
|
+
index += 1;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (!SELECT_MODIFIERS.has(token.text.toLowerCase())) break;
|
|
1412
|
+
index += 1;
|
|
1413
|
+
const on = tokens[index];
|
|
1414
|
+
const open = tokens[index + 1];
|
|
1415
|
+
if (token.text.toLowerCase() === "distinct" && on !== void 0 && isKeyword(on, "on")) {
|
|
1416
|
+
if (open?.text !== "(") break;
|
|
1417
|
+
const close = tokens.findIndex(
|
|
1418
|
+
(candidate, at) => at > index + 1 && candidate.text === ")" && candidate.depth === open.depth + 1
|
|
1419
|
+
);
|
|
1420
|
+
if (close === -1) return tokens.length;
|
|
1421
|
+
index = close + 1;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return index;
|
|
1425
|
+
}
|
|
1426
|
+
function normalizeName(text) {
|
|
1427
|
+
return text.replace(/^["`]|["`]$/g, "").toLowerCase();
|
|
1428
|
+
}
|
|
1313
1429
|
function columnOf(tokens, start, end) {
|
|
1314
1430
|
const visible = tokens.slice(start, end).filter((token) => !isCommentToken(token));
|
|
1315
1431
|
const first = visible[0];
|
|
1316
1432
|
const last2 = visible[visible.length - 1];
|
|
1317
1433
|
if (first === void 0 || last2 === void 0) {
|
|
1318
|
-
return {
|
|
1434
|
+
return {
|
|
1435
|
+
expressionStart: 0,
|
|
1436
|
+
expressionEnd: 0,
|
|
1437
|
+
alias: void 0,
|
|
1438
|
+
aggregate: false,
|
|
1439
|
+
star: false,
|
|
1440
|
+
outputName: void 0,
|
|
1441
|
+
names: []
|
|
1442
|
+
};
|
|
1319
1443
|
}
|
|
1320
1444
|
const secondLast = visible[visible.length - 2];
|
|
1321
1445
|
const simpleColumn = visible.length % 2 === 1 && visible.every((token, i) => i % 2 === 0 ? isNameToken(token) : token.text === ".");
|
|
@@ -1329,34 +1453,70 @@ function columnOf(tokens, start, end) {
|
|
|
1329
1453
|
expressionEnd = last2.start;
|
|
1330
1454
|
}
|
|
1331
1455
|
const aggregate = AGGREGATE_FUNCTIONS.has(first.text.toLowerCase()) || first.text === "*";
|
|
1456
|
+
const star = last2.text === "*" && (visible.length === 1 || secondLast?.text === ".");
|
|
1457
|
+
const outputName = alias !== void 0 ? normalizeName(alias) : simpleColumn ? normalizeName(last2.text) : void 0;
|
|
1458
|
+
const names = visible.filter(
|
|
1459
|
+
(token) => token.end <= expressionEnd && (isNameToken(token) || /^["`]/.test(token.text))
|
|
1460
|
+
).map((token) => normalizeName(token.text));
|
|
1332
1461
|
return {
|
|
1333
1462
|
expressionStart: first.start,
|
|
1334
1463
|
expressionEnd,
|
|
1335
1464
|
alias,
|
|
1336
|
-
aggregate
|
|
1465
|
+
aggregate,
|
|
1466
|
+
star,
|
|
1467
|
+
outputName,
|
|
1468
|
+
names
|
|
1337
1469
|
};
|
|
1338
1470
|
}
|
|
1339
|
-
function
|
|
1340
|
-
|
|
1471
|
+
function ordinalReplacement(sql2, tokens, scope, clause, ordinal) {
|
|
1472
|
+
const column = scope.columns[ordinal - 1];
|
|
1473
|
+
if (column === void 0) return void 0;
|
|
1474
|
+
if (scope.columns.slice(0, ordinal).some((candidate) => candidate.star)) return void 0;
|
|
1475
|
+
if (clause === "order" && scope.setOperand) return void 0;
|
|
1476
|
+
const others = scope.columns.filter((candidate) => candidate !== column);
|
|
1477
|
+
const otherNames = new Set(others.map((candidate) => candidate.outputName));
|
|
1478
|
+
if (clause === "order" && column.alias !== void 0) {
|
|
1479
|
+
return otherNames.has(column.outputName) ? void 0 : column.alias;
|
|
1480
|
+
}
|
|
1481
|
+
if (column.aggregate || column.names.some((name) => VOLATILE_FUNCTIONS.has(name))) {
|
|
1482
|
+
return void 0;
|
|
1483
|
+
}
|
|
1484
|
+
if (clause === "order" && column.names.some((name) => otherNames.has(name))) return void 0;
|
|
1485
|
+
const expression = expressionText(sql2, tokens, column);
|
|
1486
|
+
if (expression.length === 0 || expression.includes("{") || expression.includes("%")) {
|
|
1487
|
+
return void 0;
|
|
1488
|
+
}
|
|
1489
|
+
return expression;
|
|
1341
1490
|
}
|
|
1342
1491
|
function expressionText(sql2, tokens, column) {
|
|
1343
1492
|
let result = "";
|
|
1344
1493
|
let cursor = column.expressionStart;
|
|
1494
|
+
let spaced = false;
|
|
1345
1495
|
for (const token of tokens) {
|
|
1346
1496
|
if (token.start < column.expressionStart || token.end > column.expressionEnd) continue;
|
|
1347
|
-
|
|
1348
|
-
|
|
1497
|
+
const gap = sql2.slice(cursor, token.start);
|
|
1498
|
+
if (gap.trim() !== "") return "";
|
|
1499
|
+
spaced ||= gap !== "";
|
|
1349
1500
|
cursor = token.end;
|
|
1501
|
+
if (isCommentToken(token)) {
|
|
1502
|
+
spaced = true;
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (spaced && result !== "" && !result.endsWith("(") && token.text !== ")" && token.text !== ",") {
|
|
1506
|
+
result += " ";
|
|
1507
|
+
}
|
|
1508
|
+
result += token.text;
|
|
1509
|
+
spaced = false;
|
|
1350
1510
|
}
|
|
1351
|
-
|
|
1352
|
-
return flatten(result);
|
|
1511
|
+
return sql2.slice(cursor, column.expressionEnd).trim() === "" ? result : "";
|
|
1353
1512
|
}
|
|
1354
1513
|
function replaceOrdinals(sql2) {
|
|
1355
1514
|
const tokens = tokenize(sql2);
|
|
1356
1515
|
const scopes = [];
|
|
1357
1516
|
const replacements = [];
|
|
1358
|
-
const newScope = (depth, columnStart) => ({
|
|
1517
|
+
const newScope = (depth, columnStart, setOperand) => ({
|
|
1359
1518
|
depth,
|
|
1519
|
+
setOperand,
|
|
1360
1520
|
columns: [],
|
|
1361
1521
|
phase: "select",
|
|
1362
1522
|
columnStart,
|
|
@@ -1379,7 +1539,10 @@ function replaceOrdinals(sql2) {
|
|
|
1379
1539
|
};
|
|
1380
1540
|
const finishScope = (scope, endIndex) => {
|
|
1381
1541
|
closeClause(scope, endIndex);
|
|
1382
|
-
for (const clause of [
|
|
1542
|
+
for (const [kind, clause] of [
|
|
1543
|
+
["group", scope.groupBy],
|
|
1544
|
+
["order", scope.orderBy]
|
|
1545
|
+
]) {
|
|
1383
1546
|
if (clause === void 0) continue;
|
|
1384
1547
|
for (const [start, end] of clause.itemRanges) {
|
|
1385
1548
|
const itemTokens = tokens.slice(start, end);
|
|
@@ -1392,16 +1555,8 @@ function replaceOrdinals(sql2) {
|
|
|
1392
1555
|
}
|
|
1393
1556
|
if (ordinalToken === void 0 || !isNumberToken(ordinalToken)) continue;
|
|
1394
1557
|
const ordinal = Number.parseInt(ordinalToken.text, 10);
|
|
1395
|
-
const
|
|
1396
|
-
if (
|
|
1397
|
-
let text;
|
|
1398
|
-
if (column.alias !== void 0) {
|
|
1399
|
-
text = column.alias;
|
|
1400
|
-
} else if (!column.aggregate) {
|
|
1401
|
-
const expression = expressionText(sql2, tokens, column);
|
|
1402
|
-
if (!expression.includes("{") && !expression.includes("%")) text = expression;
|
|
1403
|
-
}
|
|
1404
|
-
if (text === void 0 || text.length === 0) continue;
|
|
1558
|
+
const text = ordinalReplacement(sql2, tokens, scope, kind, ordinal);
|
|
1559
|
+
if (text === void 0) continue;
|
|
1405
1560
|
replacements.push({ start: ordinalToken.start, end: ordinalToken.end, text });
|
|
1406
1561
|
}
|
|
1407
1562
|
}
|
|
@@ -1421,7 +1576,9 @@ function replaceOrdinals(sql2) {
|
|
|
1421
1576
|
finishScope(top, index);
|
|
1422
1577
|
scopes.pop();
|
|
1423
1578
|
}
|
|
1424
|
-
scopes.push(
|
|
1579
|
+
scopes.push(
|
|
1580
|
+
newScope(token.depth, firstItemStart(tokens, index + 1), isSetOperand(tokens, index))
|
|
1581
|
+
);
|
|
1425
1582
|
continue;
|
|
1426
1583
|
}
|
|
1427
1584
|
const scope = scopes[scopes.length - 1];
|
|
@@ -1488,6 +1645,138 @@ function replaceOrdinals(sql2) {
|
|
|
1488
1645
|
return result;
|
|
1489
1646
|
}
|
|
1490
1647
|
|
|
1648
|
+
// src/python-analysis/sql-lexer.ts
|
|
1649
|
+
var SPACE = /\s+/y;
|
|
1650
|
+
var WORD = /[\p{L}_][\p{L}\p{N}_$]*/uy;
|
|
1651
|
+
var NUMBER2 = /(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/y;
|
|
1652
|
+
var DOLLAR_TAG = /\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/y;
|
|
1653
|
+
function quotedEnd(text, start, quote, backslash) {
|
|
1654
|
+
let index = start + 1;
|
|
1655
|
+
while (index < text.length) {
|
|
1656
|
+
const char = text[index];
|
|
1657
|
+
if (backslash && char === "\\") {
|
|
1658
|
+
index += 2;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (char === quote) {
|
|
1662
|
+
if (text[index + 1] === quote) {
|
|
1663
|
+
index += 2;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
return index + 1;
|
|
1667
|
+
}
|
|
1668
|
+
index += 1;
|
|
1669
|
+
}
|
|
1670
|
+
return text.length;
|
|
1671
|
+
}
|
|
1672
|
+
function lexSql(text, dialect) {
|
|
1673
|
+
const mysql2 = dialect === "mysql";
|
|
1674
|
+
const tokens = [];
|
|
1675
|
+
const push = (kind, start, end) => {
|
|
1676
|
+
tokens.push({ kind, text: text.slice(start, end), start, end });
|
|
1677
|
+
};
|
|
1678
|
+
const sticky = (pattern, at) => {
|
|
1679
|
+
pattern.lastIndex = at;
|
|
1680
|
+
return pattern.exec(text) === null ? void 0 : pattern.lastIndex;
|
|
1681
|
+
};
|
|
1682
|
+
let index = 0;
|
|
1683
|
+
while (index < text.length) {
|
|
1684
|
+
const char = text[index] ?? "";
|
|
1685
|
+
const next = text[index + 1] ?? "";
|
|
1686
|
+
const spaceEnd = sticky(SPACE, index);
|
|
1687
|
+
if (spaceEnd !== void 0) {
|
|
1688
|
+
push("space", index, spaceEnd);
|
|
1689
|
+
index = spaceEnd;
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
if (char === "-" && next === "-" || mysql2 && char === "#") {
|
|
1693
|
+
const newline = text.slice(index).search(/[\r\n]/);
|
|
1694
|
+
const end = newline === -1 ? text.length : index + newline;
|
|
1695
|
+
push("line_comment", index, end);
|
|
1696
|
+
index = end;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (char === "/" && next === "*") {
|
|
1700
|
+
const close = text.indexOf("*/", index + 2);
|
|
1701
|
+
const end = close === -1 ? text.length : close + 2;
|
|
1702
|
+
push("block_comment", index, end);
|
|
1703
|
+
index = end;
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (char === "'") {
|
|
1707
|
+
const previous = tokens[tokens.length - 1];
|
|
1708
|
+
const escaped = mysql2 || previous?.kind === "word" && /^[eE]$/.test(previous.text) && previous.end === index;
|
|
1709
|
+
const end = quotedEnd(text, index, "'", escaped);
|
|
1710
|
+
push("string", index, end);
|
|
1711
|
+
index = end;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
if (char === '"' || char === "`") {
|
|
1715
|
+
const end = quotedEnd(text, index, char, mysql2 && char === '"');
|
|
1716
|
+
push(mysql2 && char === '"' ? "string" : "quoted", index, end);
|
|
1717
|
+
index = end;
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (char === "$") {
|
|
1721
|
+
const tagEnd = sticky(DOLLAR_TAG, index);
|
|
1722
|
+
if (tagEnd !== void 0) {
|
|
1723
|
+
const tag = text.slice(index, tagEnd);
|
|
1724
|
+
const close = text.indexOf(tag, tagEnd);
|
|
1725
|
+
const end = close === -1 ? text.length : close + tag.length;
|
|
1726
|
+
push("dollar", index, end);
|
|
1727
|
+
index = end;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const wordEnd = sticky(WORD, index);
|
|
1732
|
+
if (wordEnd !== void 0) {
|
|
1733
|
+
push("word", index, wordEnd);
|
|
1734
|
+
index = wordEnd;
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const numberEnd = sticky(NUMBER2, index);
|
|
1738
|
+
if (numberEnd !== void 0) {
|
|
1739
|
+
push("number", index, numberEnd);
|
|
1740
|
+
index = numberEnd;
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
const codePoint = text.codePointAt(index) ?? 0;
|
|
1744
|
+
const width = codePoint > 65535 ? 2 : 1;
|
|
1745
|
+
push("symbol", index, index + width);
|
|
1746
|
+
index += width;
|
|
1747
|
+
}
|
|
1748
|
+
return tokens;
|
|
1749
|
+
}
|
|
1750
|
+
function commentText(token) {
|
|
1751
|
+
return token.text.replace(/\s+/g, " ").trim();
|
|
1752
|
+
}
|
|
1753
|
+
function sqlTokenDifference(before, after, dialect) {
|
|
1754
|
+
const signature = (text) => {
|
|
1755
|
+
const code = [];
|
|
1756
|
+
const comments = [];
|
|
1757
|
+
for (const token of lexSql(text, dialect)) {
|
|
1758
|
+
if (token.kind === "space") continue;
|
|
1759
|
+
if (token.kind === "line_comment" || token.kind === "block_comment") {
|
|
1760
|
+
comments.push(commentText(token));
|
|
1761
|
+
} else {
|
|
1762
|
+
code.push(`${token.kind}:${token.kind === "word" ? token.text.toLowerCase() : token.text}`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return { code, comments };
|
|
1766
|
+
};
|
|
1767
|
+
const left = signature(before);
|
|
1768
|
+
const right = signature(after);
|
|
1769
|
+
for (const part of ["code", "comments"]) {
|
|
1770
|
+
const length = Math.max(left[part].length, right[part].length);
|
|
1771
|
+
for (let index = 0; index < length; index += 1) {
|
|
1772
|
+
if (left[part][index] !== right[part][index]) {
|
|
1773
|
+
return `${part} token ${index} (${left[part].length} before, ${right[part].length} after)`;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return void 0;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1491
1780
|
// src/python-analysis/validation.ts
|
|
1492
1781
|
var CandidateFailure = class extends Error {
|
|
1493
1782
|
reason;
|
|
@@ -1513,7 +1802,7 @@ function applyBaseIndent(text, baseIndent, extraIndent, tripleQuoted) {
|
|
|
1513
1802
|
const first = nonEmpty[0];
|
|
1514
1803
|
if (first === void 0) return text;
|
|
1515
1804
|
const firstTrimmed = first.line.trim();
|
|
1516
|
-
const isMarker =
|
|
1805
|
+
const isMarker = isSqlMarker(firstTrimmed);
|
|
1517
1806
|
const keepsFirstLine = !tripleQuoted || isMarker;
|
|
1518
1807
|
const shifted = keepsFirstLine ? nonEmpty.slice(1) : nonEmpty;
|
|
1519
1808
|
const minIndent = shifted.length === 0 ? 0 : Math.min(...shifted.map(({ line }) => /^[ \t]*/.exec(line)?.[0].length ?? 0));
|
|
@@ -1526,12 +1815,9 @@ function normalizeFrame(content, literal, analysis, baseIndent) {
|
|
|
1526
1815
|
if (literal.delimiter.length !== 3 || !content.includes("\n")) return content;
|
|
1527
1816
|
const sourceContent = analysis.sourceMap.slice(literal.contentSpan);
|
|
1528
1817
|
const firstNonEmptyLine = sourceContent.split("\n").find((line) => line.trim() !== "");
|
|
1529
|
-
const startsWithMarker = firstNonEmptyLine
|
|
1818
|
+
const startsWithMarker = firstNonEmptyLine !== void 0 && isSqlMarker(firstNonEmptyLine);
|
|
1530
1819
|
const lines = content.split("\n");
|
|
1531
|
-
const markerIndex = lines.findIndex(
|
|
1532
|
-
const trimmed = line.trim();
|
|
1533
|
-
return trimmed.startsWith("--sql") || trimmed.startsWith("-- sql");
|
|
1534
|
-
});
|
|
1820
|
+
const markerIndex = lines.findIndex(isSqlMarker);
|
|
1535
1821
|
let normalized = content;
|
|
1536
1822
|
if (startsWithMarker) {
|
|
1537
1823
|
if (markerIndex > 0) {
|
|
@@ -1548,11 +1834,15 @@ ${normalized}`;
|
|
|
1548
1834
|
}
|
|
1549
1835
|
return `${normalized}${baseIndent}`;
|
|
1550
1836
|
}
|
|
1551
|
-
function breakTrailingFieldMarkers(text) {
|
|
1837
|
+
function breakTrailingFieldMarkers(text, dialect) {
|
|
1552
1838
|
const markerPattern = /(__INLINE_SQL_[0-9a-f]{32}_[A-Z_]+_[0-9]+__)\s*$/;
|
|
1553
1839
|
const match = markerPattern.exec(text);
|
|
1554
1840
|
if (match === null || match[1] === void 0) return text;
|
|
1555
1841
|
const markerStart = match.index;
|
|
1842
|
+
const token = lexSql(text, dialect).find(
|
|
1843
|
+
(candidate) => candidate.start <= markerStart && markerStart < candidate.end
|
|
1844
|
+
);
|
|
1845
|
+
if (token?.kind !== "word") return text;
|
|
1556
1846
|
const lineStart = text.lastIndexOf("\n", markerStart - 1) + 1;
|
|
1557
1847
|
const before = text.slice(lineStart, markerStart);
|
|
1558
1848
|
if (before.trim() === "") return text;
|
|
@@ -1581,12 +1871,25 @@ function moveCommasBeforeLineComments(text) {
|
|
|
1581
1871
|
}
|
|
1582
1872
|
return result.join("\n");
|
|
1583
1873
|
}
|
|
1584
|
-
function breakTrailingDistributeLines(text) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1874
|
+
function breakTrailingDistributeLines(text, dialect) {
|
|
1875
|
+
const tokens = lexSql(text, dialect);
|
|
1876
|
+
const splits = [];
|
|
1877
|
+
tokens.forEach((token, index) => {
|
|
1878
|
+
if (token.kind !== "word" || token.text.toLowerCase() !== "distribute") return;
|
|
1879
|
+
const gap = tokens[index + 1];
|
|
1880
|
+
const clause = tokens[index + 2];
|
|
1881
|
+
if (gap?.kind !== "space" || /[\r\n]/.test(gap.text) || clause?.kind !== "word") return;
|
|
1882
|
+
const lineStart = text.lastIndexOf("\n", token.start - 1) + 1;
|
|
1883
|
+
if (text.slice(lineStart, token.start).trim() !== "") splits.push(token.start);
|
|
1884
|
+
});
|
|
1885
|
+
let result = text;
|
|
1886
|
+
for (const start of splits.reverse()) {
|
|
1887
|
+
const lineStart = result.lastIndexOf("\n", start - 1) + 1;
|
|
1888
|
+
const indent = /^[ \t]*/.exec(result.slice(lineStart))?.[0] ?? "";
|
|
1889
|
+
result = `${result.slice(0, start).trimEnd()}
|
|
1890
|
+
${indent}${result.slice(start)}`;
|
|
1891
|
+
}
|
|
1892
|
+
return result;
|
|
1590
1893
|
}
|
|
1591
1894
|
function wrappingComma(line) {
|
|
1592
1895
|
let comma = -1;
|
|
@@ -1616,7 +1919,7 @@ function moveCommasToLineStarts(text) {
|
|
|
1616
1919
|
const indent = nextParts?.[1] ?? "";
|
|
1617
1920
|
const content = nextParts?.[2] ?? "";
|
|
1618
1921
|
lines[index] = `${line.slice(0, comma)}${line.slice(comma + 1)}`.trimEnd();
|
|
1619
|
-
lines[index + 1] = `${indent}
|
|
1922
|
+
lines[index + 1] = `${indent}, ${content}`;
|
|
1620
1923
|
}
|
|
1621
1924
|
return lines.join("\n");
|
|
1622
1925
|
}
|
|
@@ -1726,7 +2029,7 @@ function rejoinFunctionCalls(text) {
|
|
|
1726
2029
|
}
|
|
1727
2030
|
return out;
|
|
1728
2031
|
}
|
|
1729
|
-
function formatOnce(analysis, literal, detection, options, nonce, sqlFormatter) {
|
|
2032
|
+
function formatOnce(analysis, literal, detection, options, nonce, sqlFormatter, baseIndent) {
|
|
1730
2033
|
const plan = buildProtectionPlan(analysis.sourceMap, literal, detection, nonce);
|
|
1731
2034
|
let formatted = sqlFormatter(plan.protectedSql, {
|
|
1732
2035
|
tripleQuoted: literal.delimiter.length === 3,
|
|
@@ -1738,7 +2041,7 @@ function formatOnce(analysis, literal, detection, options, nonce, sqlFormatter)
|
|
|
1738
2041
|
formatted = formatted.replace(fragment.marker + lineEnding, fragment.marker);
|
|
1739
2042
|
}
|
|
1740
2043
|
}
|
|
1741
|
-
formatted = breakTrailingFieldMarkers(formatted);
|
|
2044
|
+
formatted = breakTrailingFieldMarkers(formatted, options.dialect);
|
|
1742
2045
|
formatted = moveCommasBeforeLineComments(formatted);
|
|
1743
2046
|
if (options.keepFunctionsInline) {
|
|
1744
2047
|
formatted = rejoinFunctionCalls(formatted);
|
|
@@ -1746,76 +2049,71 @@ function formatOnce(analysis, literal, detection, options, nonce, sqlFormatter)
|
|
|
1746
2049
|
if (options.commaPosition === "before") {
|
|
1747
2050
|
formatted = moveCommasToLineStarts(formatted);
|
|
1748
2051
|
}
|
|
1749
|
-
formatted = breakTrailingDistributeLines(formatted);
|
|
2052
|
+
formatted = breakTrailingDistributeLines(formatted, options.dialect);
|
|
2053
|
+
const difference = sqlTokenDifference(plan.protectedSql, formatted, options.dialect);
|
|
2054
|
+
if (difference !== void 0) {
|
|
2055
|
+
throw new CandidateFailure("FORMATTER_FAILED", `formatting changed the SQL: ${difference}`);
|
|
2056
|
+
}
|
|
1750
2057
|
const restored = restoreProtected(formatted, plan);
|
|
1751
2058
|
const resolved = options.replaceOrdinals ? replaceOrdinals(restored) : restored;
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
);
|
|
2059
|
+
if (literal.delimiter.length !== 3) {
|
|
2060
|
+
return literalText(
|
|
2061
|
+
literal,
|
|
2062
|
+
singleLineContent(analysis.sourceMap.slice(literal.contentSpan), resolved)
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
const indented = applyBaseIndent(resolved, baseIndent, " ".repeat(options.indentWidth), true);
|
|
1759
2066
|
return literalText(literal, normalizeFrame(indented, literal, analysis, baseIndent));
|
|
1760
2067
|
}
|
|
1761
|
-
function
|
|
1762
|
-
|
|
2068
|
+
function singleLineContent(sourceContent, formatted) {
|
|
2069
|
+
const leading = /^[ \t]*/.exec(sourceContent)?.[0] ?? "";
|
|
2070
|
+
const trailing = /[ \t]*$/.exec(sourceContent)?.[0] ?? "";
|
|
2071
|
+
return `${leading}${formatted.replace(/\s*\n\s*/g, " ").trim()}${trailing}`;
|
|
1763
2072
|
}
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2073
|
+
function analyzeLiteral(text, original) {
|
|
2074
|
+
let analysis;
|
|
2075
|
+
try {
|
|
2076
|
+
analysis = analyzeDocument(text);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
2079
|
+
throw new CandidateFailure("FORMATTER_FAILED", `reparse threw: ${message}`);
|
|
1768
2080
|
}
|
|
1769
|
-
const
|
|
1770
|
-
if (
|
|
1771
|
-
if (result.prefix !== original.prefix || result.delimiter !== original.delimiter || result.kind !== original.kind) {
|
|
2081
|
+
const literal = analysis.supported[0];
|
|
2082
|
+
if (literal === void 0 || analysis.supported.length !== 1 || analysis.unsupported.length !== 0 || literal.span.start !== 0 || literal.span.end !== text.length) {
|
|
1772
2083
|
throw new CandidateFailure("UNSAFE_RAW_STRING");
|
|
1773
2084
|
}
|
|
1774
|
-
|
|
2085
|
+
if (literal.prefix !== original.prefix || literal.delimiter !== original.delimiter || literal.kind !== original.kind) {
|
|
2086
|
+
throw new CandidateFailure("UNSAFE_RAW_STRING");
|
|
2087
|
+
}
|
|
2088
|
+
return { analysis, literal };
|
|
1775
2089
|
}
|
|
1776
2090
|
function fieldTexts(analysis, literal) {
|
|
1777
2091
|
return literal.fieldSpans.map((span) => analysis.sourceMap.slice(span));
|
|
1778
2092
|
}
|
|
1779
|
-
function convergeToFixedPoint(
|
|
1780
|
-
|
|
2093
|
+
function convergeToFixedPoint(analysis, literal, detection, options, nonce, sqlFormatter, baseIndent) {
|
|
2094
|
+
const fieldsBefore = fieldTexts(analysis, literal).join("\0");
|
|
2095
|
+
let current = formatOnce(analysis, literal, detection, options, nonce, sqlFormatter, baseIndent);
|
|
1781
2096
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
throw new CandidateFailure("UNSAFE_RAW_STRING");
|
|
1786
|
-
}
|
|
1787
|
-
} catch (error) {
|
|
1788
|
-
if (error instanceof CandidateFailure) throw error;
|
|
1789
|
-
throw new CandidateFailure("UNSAFE_RAW_STRING");
|
|
1790
|
-
}
|
|
1791
|
-
const updatedSource = replaceSource(source, literal.span, current);
|
|
1792
|
-
let updatedAnalysis;
|
|
1793
|
-
try {
|
|
1794
|
-
updatedAnalysis = analyzeDocument(updatedSource);
|
|
1795
|
-
} catch (error) {
|
|
1796
|
-
const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
1797
|
-
throw new CandidateFailure("FORMATTER_FAILED", `reparse threw: ${message}`);
|
|
1798
|
-
}
|
|
1799
|
-
const updatedLiteral = replacementLiteral(updatedAnalysis, literal);
|
|
1800
|
-
const before = fieldTexts(analysis, literal).join("\0");
|
|
1801
|
-
const after = fieldTexts(updatedAnalysis, updatedLiteral).join("\0");
|
|
1802
|
-
if (before !== after) {
|
|
2097
|
+
const updated = analyzeLiteral(current, literal);
|
|
2098
|
+
const fieldsAfter = fieldTexts(updated.analysis, updated.literal).join("\0");
|
|
2099
|
+
if (fieldsBefore !== fieldsAfter) {
|
|
1803
2100
|
throw new CandidateFailure(
|
|
1804
2101
|
"UNSAFE_FSTRING_RESTORE",
|
|
1805
|
-
`field texts changed: before=[${
|
|
2102
|
+
`field texts changed: before=[${fieldsBefore.split("\0").join(", ")}] after=[${fieldsAfter.split("\0").join(", ")}]`
|
|
1806
2103
|
);
|
|
1807
2104
|
}
|
|
1808
|
-
const updatedDetection = detectSql(
|
|
2105
|
+
const updatedDetection = detectSql(updated.literal, updated.analysis.sourceMap);
|
|
1809
2106
|
if (!updatedDetection.matched) {
|
|
1810
2107
|
throw new CandidateFailure("FORMATTER_FAILED", "updated candidate no longer matches --sql");
|
|
1811
2108
|
}
|
|
1812
2109
|
const next = formatOnce(
|
|
1813
|
-
|
|
1814
|
-
|
|
2110
|
+
updated.analysis,
|
|
2111
|
+
updated.literal,
|
|
1815
2112
|
updatedDetection,
|
|
1816
2113
|
options,
|
|
1817
2114
|
nonce,
|
|
1818
|
-
sqlFormatter
|
|
2115
|
+
sqlFormatter,
|
|
2116
|
+
baseIndent
|
|
1819
2117
|
);
|
|
1820
2118
|
if (next === current) return current;
|
|
1821
2119
|
current = next;
|
|
@@ -1837,14 +2135,15 @@ function formatCandidate(source, analysis, literal, detection, options, nonce, s
|
|
|
1837
2135
|
}
|
|
1838
2136
|
let first;
|
|
1839
2137
|
try {
|
|
2138
|
+
const local = analyzeLiteral(expected, literal);
|
|
1840
2139
|
first = convergeToFixedPoint(
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
literal,
|
|
1844
|
-
detection,
|
|
2140
|
+
local.analysis,
|
|
2141
|
+
local.literal,
|
|
2142
|
+
detectSql(local.literal, local.analysis.sourceMap),
|
|
1845
2143
|
options,
|
|
1846
2144
|
nonce,
|
|
1847
|
-
sqlFormatter
|
|
2145
|
+
sqlFormatter,
|
|
2146
|
+
baseIndentOf(analysis, literal)
|
|
1848
2147
|
);
|
|
1849
2148
|
} catch (error) {
|
|
1850
2149
|
if (error instanceof CandidateFailure) {
|
|
@@ -1874,6 +2173,8 @@ function formatCandidate(source, analysis, literal, detection, options, nonce, s
|
|
|
1874
2173
|
var MAX_DOCUMENT_BYTES = 5 * 1024 * 1024;
|
|
1875
2174
|
var MAX_CANDIDATE_BYTES = 1024 * 1024;
|
|
1876
2175
|
var MAX_CANDIDATES = 1e3;
|
|
2176
|
+
var ResourceLimitError = class extends Error {
|
|
2177
|
+
};
|
|
1877
2178
|
function discover(analysis) {
|
|
1878
2179
|
const literals = [
|
|
1879
2180
|
...analysis.supported,
|
|
@@ -1919,14 +2220,39 @@ function combinedSource(source, edits) {
|
|
|
1919
2220
|
}
|
|
1920
2221
|
return result;
|
|
1921
2222
|
}
|
|
2223
|
+
function literalsOf(analysis) {
|
|
2224
|
+
return [...analysis.supported, ...analysis.unsupported].sort(
|
|
2225
|
+
(left, right) => left.span.start - right.span.start
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
function preservesDocumentShape(source, analysis, edits) {
|
|
2229
|
+
const before = literalsOf(analysis);
|
|
2230
|
+
const after = literalsOf(analyzeDocument(combinedSource(source, edits)));
|
|
2231
|
+
if (before.length !== after.length) return false;
|
|
2232
|
+
const editAt = new Map(edits.map((edit) => [edit.sourceSpan.start, edit]));
|
|
2233
|
+
let delta = 0;
|
|
2234
|
+
return before.every((original, index) => {
|
|
2235
|
+
const updated = after[index];
|
|
2236
|
+
if (updated === void 0 || updated.span.start !== original.span.start + delta) return false;
|
|
2237
|
+
const edit = editAt.get(original.span.start);
|
|
2238
|
+
if (edit !== void 0) {
|
|
2239
|
+
delta += edit.replacementText.length - (original.span.end - original.span.start);
|
|
2240
|
+
}
|
|
2241
|
+
if (updated.span.end !== original.span.end + delta) return false;
|
|
2242
|
+
if (!("contentSpan" in original) || !("contentSpan" in updated)) {
|
|
2243
|
+
return !("contentSpan" in original) && !("contentSpan" in updated);
|
|
2244
|
+
}
|
|
2245
|
+
return updated.prefix === original.prefix && updated.delimiter === original.delimiter && updated.kind === original.kind;
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
1922
2248
|
function formatDocument(source, options, target, nonce, sqlFormatter, logger) {
|
|
1923
2249
|
if (Buffer.byteLength(source, "utf8") > MAX_DOCUMENT_BYTES) {
|
|
1924
|
-
throw new
|
|
2250
|
+
throw new ResourceLimitError("document exceeds the size limit");
|
|
1925
2251
|
}
|
|
1926
2252
|
const analysis = analyzeDocument(source);
|
|
1927
2253
|
const units = discover(analysis);
|
|
1928
2254
|
if (units.length > MAX_CANDIDATES) {
|
|
1929
|
-
throw new
|
|
2255
|
+
throw new ResourceLimitError("candidate count exceeds the limit");
|
|
1930
2256
|
}
|
|
1931
2257
|
const selected = selectUnits(units, target, analysis.sourceMap);
|
|
1932
2258
|
const edits = [];
|
|
@@ -1934,7 +2260,7 @@ function formatDocument(source, options, target, nonce, sqlFormatter, logger) {
|
|
|
1934
2260
|
let changed = 0;
|
|
1935
2261
|
let unchanged = 0;
|
|
1936
2262
|
for (const unit of selected) {
|
|
1937
|
-
if (!("contentSpan" in unit.literal)) {
|
|
2263
|
+
if (analysis.unterminated || !("contentSpan" in unit.literal)) {
|
|
1938
2264
|
skipReasons.push("UNSUPPORTED_LITERAL");
|
|
1939
2265
|
continue;
|
|
1940
2266
|
}
|
|
@@ -1961,8 +2287,17 @@ function formatDocument(source, options, target, nonce, sqlFormatter, logger) {
|
|
|
1961
2287
|
unchanged++;
|
|
1962
2288
|
}
|
|
1963
2289
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
2290
|
+
if (!preservesDocumentShape(source, analysis, edits)) {
|
|
2291
|
+
const kept = edits.filter((edit) => preservesDocumentShape(source, analysis, [edit]));
|
|
2292
|
+
const accepted = preservesDocumentShape(source, analysis, kept) ? kept : [];
|
|
2293
|
+
for (const edit of edits) {
|
|
2294
|
+
if (accepted.includes(edit)) continue;
|
|
2295
|
+
logger?.("candidate skipped (FORMATTER_FAILED): the document no longer reads back the same");
|
|
2296
|
+
skipReasons.push("FORMATTER_FAILED");
|
|
2297
|
+
changed--;
|
|
2298
|
+
}
|
|
2299
|
+
edits.splice(0, edits.length, ...accepted);
|
|
2300
|
+
}
|
|
1966
2301
|
const summary = {
|
|
1967
2302
|
discovered: units.length,
|
|
1968
2303
|
selected: selected.length,
|
|
@@ -1984,12 +2319,6 @@ function formatDocument(source, options, target, nonce, sqlFormatter, logger) {
|
|
|
1984
2319
|
};
|
|
1985
2320
|
}
|
|
1986
2321
|
|
|
1987
|
-
// src/replacement.ts
|
|
1988
|
-
function collapseReplacement(literalText2, replacement) {
|
|
1989
|
-
if (literalText2.includes("\n")) return replacement;
|
|
1990
|
-
return replacement.replace(/\s*\n\s*/g, " ").trim();
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
2322
|
// node_modules/sql-formatter/dist/esm/allDialects.js
|
|
1994
2323
|
var allDialects_exports = {};
|
|
1995
2324
|
__export(allDialects_exports, {
|
|
@@ -22606,7 +22935,7 @@ function formatProtectedSql(sql2, options) {
|
|
|
22606
22935
|
return format(sql2, {
|
|
22607
22936
|
language: options.dialect,
|
|
22608
22937
|
tabWidth: options.indentWidth,
|
|
22609
|
-
keywordCase: options.keywordCase
|
|
22938
|
+
keywordCase: options.keywordCase,
|
|
22610
22939
|
linesBetweenQueries: 1,
|
|
22611
22940
|
expressionWidth: options.wrapAfter,
|
|
22612
22941
|
denseOperators: !options.useSpaceAroundOperators
|
|
@@ -22623,10 +22952,12 @@ var USAGE = `Usage: inline-sql-toolkit [options] [files...]
|
|
|
22623
22952
|
--indent-width <1-8> (default: 2)
|
|
22624
22953
|
--wrap-after <20-500> (default: 88)
|
|
22625
22954
|
--no-space-around-operators keep dense operators (default: spaced)
|
|
22626
|
-
--
|
|
22955
|
+
--ordinals replace GROUP BY / ORDER BY ordinals (default: off)
|
|
22956
|
+
--no-ordinals do not replace ordinals (overrides the config file)
|
|
22627
22957
|
--comma-position <pos> after | before (default: after)
|
|
22628
22958
|
--keep-functions-inline keep SUM(...) / COUNT(CASE ...) on one line
|
|
22629
22959
|
-c, --config <file> config JSON (default: nearest .inline-sql.json)
|
|
22960
|
+
-q, --quiet do not report skipped candidates on stderr
|
|
22630
22961
|
-h, --help / --version`;
|
|
22631
22962
|
function printError(message) {
|
|
22632
22963
|
process.stderr.write(`${message}
|
|
@@ -22684,12 +23015,12 @@ function buildRawOptions(fileOptions, cliValues) {
|
|
|
22684
23015
|
indentWidth: cliValues["indent-width"] !== void 0 ? Number(cliValues["indent-width"]) : fileOptions.indentWidth,
|
|
22685
23016
|
wrapAfter: cliValues["wrap-after"] !== void 0 ? Number(cliValues["wrap-after"]) : fileOptions.wrapAfter,
|
|
22686
23017
|
useSpaceAroundOperators: cliValues["no-space-around-operators"] ? false : fileOptions.useSpaceAroundOperators,
|
|
22687
|
-
replaceOrdinals: cliValues["no-ordinals"] ? false : fileOptions.replaceOrdinals,
|
|
23018
|
+
replaceOrdinals: cliValues.ordinals ? true : cliValues["no-ordinals"] ? false : fileOptions.replaceOrdinals,
|
|
22688
23019
|
commaPosition: cliValues["comma-position"] ?? fileOptions.commaPosition,
|
|
22689
23020
|
keepFunctionsInline: cliValues["keep-functions-inline"] ? true : fileOptions.keepFunctionsInline
|
|
22690
23021
|
};
|
|
22691
23022
|
}
|
|
22692
|
-
function
|
|
23023
|
+
function formatPythonSourceWithReport(text, options, logger) {
|
|
22693
23024
|
const nonce = allocateNonce(text, () => (0, import_node_crypto.randomBytes)(16).toString("hex"));
|
|
22694
23025
|
const result = formatDocument(
|
|
22695
23026
|
text,
|
|
@@ -22699,14 +23030,17 @@ function formatPythonSource(text, options, logger) {
|
|
|
22699
23030
|
(sql2, formatterOptions) => formatProtectedSql(sql2, formatterOptions.options),
|
|
22700
23031
|
logger
|
|
22701
23032
|
);
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
|
|
22708
|
-
|
|
22709
|
-
|
|
23033
|
+
return { output: combinedSource(text, result.edits), skipReasons: result.skipReasons };
|
|
23034
|
+
}
|
|
23035
|
+
function formatPythonSource(text, options, logger) {
|
|
23036
|
+
return formatPythonSourceWithReport(text, options, logger).output;
|
|
23037
|
+
}
|
|
23038
|
+
function skipNote(name, reasons) {
|
|
23039
|
+
const counts = /* @__PURE__ */ new Map();
|
|
23040
|
+
for (const reason of reasons) counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
23041
|
+
const detail = [...counts].map(([reason, count]) => `${reason} x${count}`).join(", ");
|
|
23042
|
+
const noun = reasons.length === 1 ? "candidate" : "candidates";
|
|
23043
|
+
return `inline-sql-toolkit: ${name}: skipped ${reasons.length} SQL ${noun} (${detail})`;
|
|
22710
23044
|
}
|
|
22711
23045
|
function runCli(argv) {
|
|
22712
23046
|
let parsed;
|
|
@@ -22721,10 +23055,12 @@ function runCli(argv) {
|
|
|
22721
23055
|
"indent-width": { type: "string" },
|
|
22722
23056
|
"wrap-after": { type: "string" },
|
|
22723
23057
|
"no-space-around-operators": { type: "boolean", default: false },
|
|
23058
|
+
ordinals: { type: "boolean", default: false },
|
|
22724
23059
|
"no-ordinals": { type: "boolean", default: false },
|
|
22725
23060
|
"comma-position": { type: "string" },
|
|
22726
23061
|
"keep-functions-inline": { type: "boolean", default: false },
|
|
22727
23062
|
config: { type: "string", short: "c" },
|
|
23063
|
+
quiet: { type: "boolean", short: "q", default: false },
|
|
22728
23064
|
help: { type: "boolean", short: "h", default: false },
|
|
22729
23065
|
version: { type: "boolean", default: false }
|
|
22730
23066
|
},
|
|
@@ -22749,6 +23085,10 @@ function runCli(argv) {
|
|
|
22749
23085
|
printError("inline-sql-toolkit: cannot use --write and --check together");
|
|
22750
23086
|
return 2;
|
|
22751
23087
|
}
|
|
23088
|
+
if (values.ordinals && values["no-ordinals"]) {
|
|
23089
|
+
printError("inline-sql-toolkit: cannot use --ordinals and --no-ordinals together");
|
|
23090
|
+
return 2;
|
|
23091
|
+
}
|
|
22752
23092
|
let fileConfig = {};
|
|
22753
23093
|
if (values.config) {
|
|
22754
23094
|
try {
|
|
@@ -22789,7 +23129,11 @@ function runCli(argv) {
|
|
|
22789
23129
|
}
|
|
22790
23130
|
let output;
|
|
22791
23131
|
try {
|
|
22792
|
-
|
|
23132
|
+
const report = formatPythonSourceWithReport(input, options);
|
|
23133
|
+
output = report.output;
|
|
23134
|
+
if (!values.quiet && report.skipReasons.length > 0) {
|
|
23135
|
+
printError(skipNote("<stdin>", report.skipReasons));
|
|
23136
|
+
}
|
|
22793
23137
|
} catch (err) {
|
|
22794
23138
|
printError(`inline-sql-toolkit: ${err.message}`);
|
|
22795
23139
|
return 2;
|
|
@@ -22815,7 +23159,11 @@ function runCli(argv) {
|
|
|
22815
23159
|
}
|
|
22816
23160
|
let output;
|
|
22817
23161
|
try {
|
|
22818
|
-
|
|
23162
|
+
const report = formatPythonSourceWithReport(input, options);
|
|
23163
|
+
output = report.output;
|
|
23164
|
+
if (!values.quiet && report.skipReasons.length > 0) {
|
|
23165
|
+
printError(skipNote(file, report.skipReasons));
|
|
23166
|
+
}
|
|
22819
23167
|
} catch (err) {
|
|
22820
23168
|
printError(`inline-sql-toolkit: ${file}: ${err.message}`);
|
|
22821
23169
|
return 2;
|
|
@@ -22861,6 +23209,7 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
|
|
|
22861
23209
|
buildRawOptions,
|
|
22862
23210
|
findConfigFile,
|
|
22863
23211
|
formatPythonSource,
|
|
23212
|
+
formatPythonSourceWithReport,
|
|
22864
23213
|
getPackageVersion,
|
|
22865
23214
|
loadConfigFile,
|
|
22866
23215
|
runCli
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/hidenobunagai/inline-sql-toolkit/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.5.0",
|
|
15
15
|
"icon": "icon.png",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": {
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"inlineSql.format.replaceOrdinals": {
|
|
91
91
|
"type": "boolean",
|
|
92
|
-
"default":
|
|
92
|
+
"default": false,
|
|
93
93
|
"scope": "resource",
|
|
94
94
|
"description": "%configuration.format.replaceOrdinals.description%"
|
|
95
95
|
},
|
|
@@ -135,69 +135,6 @@
|
|
|
135
135
|
"meta.embedded.sql": "sql"
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
],
|
|
139
|
-
"semanticTokenTypes": [
|
|
140
|
-
{
|
|
141
|
-
"id": "inlineSqlKeyword",
|
|
142
|
-
"superType": "keyword",
|
|
143
|
-
"description": "%semanticTokenTypes.inlineSqlKeyword.description%"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
"id": "inlineSqlString",
|
|
147
|
-
"superType": "string",
|
|
148
|
-
"description": "%semanticTokenTypes.inlineSqlString.description%"
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
"id": "inlineSqlNumber",
|
|
152
|
-
"superType": "number",
|
|
153
|
-
"description": "%semanticTokenTypes.inlineSqlNumber.description%"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
"id": "inlineSqlOperator",
|
|
157
|
-
"superType": "operator",
|
|
158
|
-
"description": "%semanticTokenTypes.inlineSqlOperator.description%"
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
"id": "inlineSqlComment",
|
|
162
|
-
"superType": "comment",
|
|
163
|
-
"description": "%semanticTokenTypes.inlineSqlComment.description%"
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
"id": "inlineSqlIdentifier",
|
|
167
|
-
"description": "%semanticTokenTypes.inlineSqlIdentifier.description%"
|
|
168
|
-
}
|
|
169
|
-
],
|
|
170
|
-
"semanticTokenScopes": [
|
|
171
|
-
{
|
|
172
|
-
"scopes": {
|
|
173
|
-
"inlineSqlKeyword": [
|
|
174
|
-
"keyword.other.sql",
|
|
175
|
-
"keyword"
|
|
176
|
-
],
|
|
177
|
-
"inlineSqlString": [
|
|
178
|
-
"string.quoted.sql",
|
|
179
|
-
"string.quoted.double.sql",
|
|
180
|
-
"string.quoted.single.sql",
|
|
181
|
-
"string"
|
|
182
|
-
],
|
|
183
|
-
"inlineSqlNumber": [
|
|
184
|
-
"constant.numeric.sql",
|
|
185
|
-
"constant.numeric"
|
|
186
|
-
],
|
|
187
|
-
"inlineSqlOperator": [
|
|
188
|
-
"keyword.operator.sql",
|
|
189
|
-
"keyword.operator"
|
|
190
|
-
],
|
|
191
|
-
"inlineSqlComment": [
|
|
192
|
-
"comment.line.double-dash.sql",
|
|
193
|
-
"comment.block.sql",
|
|
194
|
-
"comment"
|
|
195
|
-
],
|
|
196
|
-
"inlineSqlIdentifier": [
|
|
197
|
-
"source.sql"
|
|
198
|
-
]
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
138
|
]
|
|
202
139
|
},
|
|
203
140
|
"packageManager": "bun@1.3.8",
|