sql-fmt-cli 1.0.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/README.md +61 -0
- package/index.js +410 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# sql-fmt-cli
|
|
2
|
+
|
|
3
|
+
> Format SQL queries from stdin or file — uppercase keywords, one clause per line, indent subqueries. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/sql-fmt-cli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g sql-fmt-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Format a file in-place (prints to stdout)
|
|
18
|
+
sqlfmt query.sql
|
|
19
|
+
|
|
20
|
+
# Pipe from stdin
|
|
21
|
+
echo "select id,name from users where active=1 order by name" | sqlfmt
|
|
22
|
+
|
|
23
|
+
# CI check — exits 1 if not formatted
|
|
24
|
+
sqlfmt query.sql --check
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
Input:
|
|
30
|
+
```sql
|
|
31
|
+
select id,name from users where active=1 order by name
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Output:
|
|
35
|
+
```sql
|
|
36
|
+
SELECT id,
|
|
37
|
+
name
|
|
38
|
+
FROM users
|
|
39
|
+
WHERE active = 1
|
|
40
|
+
ORDER BY name
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Uppercases all SQL keywords (`SELECT`, `FROM`, `WHERE`, `JOIN`, `GROUP BY`, …)
|
|
46
|
+
- Puts each clause on its own line
|
|
47
|
+
- Indents subqueries inside parentheses
|
|
48
|
+
- Handles quoted strings and identifiers without corrupting them
|
|
49
|
+
- Preserves `--` line comments and `/* */` block comments
|
|
50
|
+
- `--check` flag for CI pipelines
|
|
51
|
+
- Zero runtime dependencies — pure Node.js
|
|
52
|
+
|
|
53
|
+
## Supported SQL
|
|
54
|
+
|
|
55
|
+
Works with MySQL, PostgreSQL, SQLite, and most ANSI SQL dialects.
|
|
56
|
+
|
|
57
|
+
Supports: `SELECT`, `FROM`, `WHERE`, `JOIN` variants, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, `OFFSET`, `UNION`, `INSERT`, `UPDATE`, `DELETE`, `CREATE TABLE`, `ALTER TABLE`, `WITH` (CTEs), window functions, `CASE` expressions, and more.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// SQL keywords to uppercase — ordered longest-first to avoid partial matches
|
|
8
|
+
const KEYWORDS = [
|
|
9
|
+
'SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY', 'HAVING', 'ORDER BY',
|
|
10
|
+
'LIMIT', 'OFFSET', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL JOIN',
|
|
11
|
+
'FULL OUTER JOIN', 'LEFT OUTER JOIN', 'RIGHT OUTER JOIN', 'CROSS JOIN',
|
|
12
|
+
'JOIN', 'ON', 'AS', 'AND', 'OR', 'NOT', 'IN', 'EXISTS', 'BETWEEN',
|
|
13
|
+
'LIKE', 'ILIKE', 'IS NULL', 'IS NOT NULL', 'NULL', 'TRUE', 'FALSE',
|
|
14
|
+
'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'UNION ALL', 'UNION', 'INTERSECT',
|
|
15
|
+
'EXCEPT', 'INSERT INTO', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET',
|
|
16
|
+
'DELETE FROM', 'DELETE', 'CREATE TABLE', 'CREATE INDEX', 'CREATE VIEW',
|
|
17
|
+
'CREATE', 'DROP TABLE', 'DROP', 'ALTER TABLE', 'ALTER', 'ADD COLUMN',
|
|
18
|
+
'ADD', 'COLUMN', 'PRIMARY KEY', 'FOREIGN KEY', 'REFERENCES', 'UNIQUE',
|
|
19
|
+
'DEFAULT', 'NOT NULL', 'AUTO_INCREMENT', 'AUTOINCREMENT', 'SERIAL',
|
|
20
|
+
'WITH', 'RECURSIVE', 'OVER', 'PARTITION BY', 'ROW_NUMBER', 'RANK',
|
|
21
|
+
'DENSE_RANK', 'COALESCE', 'NULLIF', 'CAST', 'CONVERT', 'COUNT', 'SUM',
|
|
22
|
+
'AVG', 'MIN', 'MAX', 'IF', 'IFNULL',
|
|
23
|
+
'ASC', 'DESC', 'ALL', 'ANY', 'SOME', 'TRUNCATE', 'EXPLAIN', 'ANALYZE',
|
|
24
|
+
'RETURNING', 'CONFLICT', 'DO NOTHING', 'DO UPDATE',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Clause-level keywords that each get their own line (top-level only)
|
|
28
|
+
const CLAUSE_KEYWORDS = [
|
|
29
|
+
'SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY', 'HAVING', 'ORDER BY',
|
|
30
|
+
'LIMIT', 'OFFSET',
|
|
31
|
+
'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL JOIN',
|
|
32
|
+
'FULL OUTER JOIN', 'LEFT OUTER JOIN', 'RIGHT OUTER JOIN', 'CROSS JOIN',
|
|
33
|
+
'JOIN', 'ON',
|
|
34
|
+
'UNION ALL', 'UNION', 'INTERSECT', 'EXCEPT',
|
|
35
|
+
'INSERT INTO', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET',
|
|
36
|
+
'DELETE FROM', 'DELETE',
|
|
37
|
+
'CREATE TABLE', 'CREATE INDEX', 'CREATE VIEW', 'CREATE',
|
|
38
|
+
'DROP TABLE', 'DROP', 'ALTER TABLE', 'ALTER',
|
|
39
|
+
'WITH', 'RETURNING', 'EXPLAIN', 'ANALYZE',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Sort by length descending so multi-word phrases match before single words
|
|
43
|
+
CLAUSE_KEYWORDS.sort((a, b) => b.length - a.length);
|
|
44
|
+
KEYWORDS.sort((a, b) => b.length - a.length);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tokenize SQL into an array of tokens: { type, value }
|
|
48
|
+
* Types: string, comment_line, comment_block, paren_open, paren_close,
|
|
49
|
+
* comma, semicolon, whitespace, word
|
|
50
|
+
*/
|
|
51
|
+
function tokenize(sql) {
|
|
52
|
+
const tokens = [];
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < sql.length) {
|
|
55
|
+
// Single-quoted string
|
|
56
|
+
if (sql[i] === "'") {
|
|
57
|
+
let j = i + 1;
|
|
58
|
+
while (j < sql.length) {
|
|
59
|
+
if (sql[j] === "'" && sql[j + 1] === "'") { j += 2; continue; }
|
|
60
|
+
if (sql[j] === "'") { j++; break; }
|
|
61
|
+
j++;
|
|
62
|
+
}
|
|
63
|
+
tokens.push({ type: 'string', value: sql.slice(i, j) });
|
|
64
|
+
i = j;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Double-quoted identifier
|
|
68
|
+
if (sql[i] === '"') {
|
|
69
|
+
let j = i + 1;
|
|
70
|
+
while (j < sql.length) {
|
|
71
|
+
if (sql[j] === '"' && sql[j + 1] === '"') { j += 2; continue; }
|
|
72
|
+
if (sql[j] === '"') { j++; break; }
|
|
73
|
+
j++;
|
|
74
|
+
}
|
|
75
|
+
tokens.push({ type: 'string', value: sql.slice(i, j) });
|
|
76
|
+
i = j;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Backtick identifier
|
|
80
|
+
if (sql[i] === '`') {
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
while (j < sql.length && sql[j] !== '`') j++;
|
|
83
|
+
tokens.push({ type: 'string', value: sql.slice(i, j + 1) });
|
|
84
|
+
i = j + 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Line comment
|
|
88
|
+
if (sql[i] === '-' && sql[i + 1] === '-') {
|
|
89
|
+
let j = i;
|
|
90
|
+
while (j < sql.length && sql[j] !== '\n') j++;
|
|
91
|
+
tokens.push({ type: 'comment_line', value: sql.slice(i, j) });
|
|
92
|
+
i = j;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Block comment
|
|
96
|
+
if (sql[i] === '/' && sql[i + 1] === '*') {
|
|
97
|
+
let j = i + 2;
|
|
98
|
+
while (j < sql.length && !(sql[j] === '*' && sql[j + 1] === '/')) j++;
|
|
99
|
+
tokens.push({ type: 'comment_block', value: sql.slice(i, j + 2) });
|
|
100
|
+
i = j + 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Parens
|
|
104
|
+
if (sql[i] === '(') { tokens.push({ type: 'paren_open', value: '(' }); i++; continue; }
|
|
105
|
+
if (sql[i] === ')') { tokens.push({ type: 'paren_close', value: ')' }); i++; continue; }
|
|
106
|
+
// Comma
|
|
107
|
+
if (sql[i] === ',') { tokens.push({ type: 'comma', value: ',' }); i++; continue; }
|
|
108
|
+
// Semicolon
|
|
109
|
+
if (sql[i] === ';') { tokens.push({ type: 'semicolon', value: ';' }); i++; continue; }
|
|
110
|
+
// Whitespace
|
|
111
|
+
if (/\s/.test(sql[i])) {
|
|
112
|
+
let j = i;
|
|
113
|
+
while (j < sql.length && /\s/.test(sql[j])) j++;
|
|
114
|
+
tokens.push({ type: 'whitespace', value: sql.slice(i, j) });
|
|
115
|
+
i = j;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Word / symbol
|
|
119
|
+
let j = i;
|
|
120
|
+
while (j < sql.length && !/[\s(),;'"` ]/.test(sql[j])) j++;
|
|
121
|
+
if (j === i) j++; // consume at least one char
|
|
122
|
+
tokens.push({ type: 'word', value: sql.slice(i, j) });
|
|
123
|
+
i = j;
|
|
124
|
+
}
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Uppercase SQL keywords in a flat token stream (preserves strings/comments).
|
|
130
|
+
* Returns modified tokens.
|
|
131
|
+
*/
|
|
132
|
+
function uppercaseKeywords(tokens) {
|
|
133
|
+
// Build a flat text version of non-string tokens so we can pattern-match
|
|
134
|
+
// multi-word keywords that may be split across token boundaries.
|
|
135
|
+
// Strategy: join adjacent word/whitespace tokens, then do keyword replacement.
|
|
136
|
+
const result = [];
|
|
137
|
+
let i = 0;
|
|
138
|
+
while (i < tokens.length) {
|
|
139
|
+
const tok = tokens[i];
|
|
140
|
+
// Preserve strings and comments verbatim
|
|
141
|
+
if (tok.type === 'string' || tok.type === 'comment_line' || tok.type === 'comment_block') {
|
|
142
|
+
result.push(tok);
|
|
143
|
+
i++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (tok.type === 'word') {
|
|
147
|
+
// Try to match a multi-word keyword starting here
|
|
148
|
+
let matched = false;
|
|
149
|
+
for (const kw of KEYWORDS) {
|
|
150
|
+
const parts = kw.split(' ');
|
|
151
|
+
if (parts.length === 1) {
|
|
152
|
+
if (tok.value.toUpperCase() === kw) {
|
|
153
|
+
result.push({ type: 'word', value: kw });
|
|
154
|
+
i++;
|
|
155
|
+
matched = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Multi-word: gather upcoming word tokens (skip single whitespace between)
|
|
160
|
+
let j = i;
|
|
161
|
+
let p = 0;
|
|
162
|
+
const consumed = [];
|
|
163
|
+
while (p < parts.length && j < tokens.length) {
|
|
164
|
+
// Skip whitespace tokens between words
|
|
165
|
+
if (tokens[j].type === 'whitespace' && p > 0) {
|
|
166
|
+
consumed.push(tokens[j]);
|
|
167
|
+
j++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (tokens[j].type === 'word' && tokens[j].value.toUpperCase() === parts[p]) {
|
|
171
|
+
consumed.push(tokens[j]);
|
|
172
|
+
j++;
|
|
173
|
+
p++;
|
|
174
|
+
} else {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (p === parts.length) {
|
|
179
|
+
// Replace with a single uppercase keyword token (spaces normalised)
|
|
180
|
+
result.push({ type: 'word', value: kw });
|
|
181
|
+
i = j;
|
|
182
|
+
matched = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!matched) {
|
|
188
|
+
result.push(tok);
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
result.push(tok);
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Rebuild a formatted SQL string from the token stream.
|
|
201
|
+
* - Top-level clause keywords start on a new line (depth === 0)
|
|
202
|
+
* - Content inside parentheses is indented by 2 spaces per depth level
|
|
203
|
+
* - Commas at top level produce a newline + indent continuation
|
|
204
|
+
*/
|
|
205
|
+
function format(tokens, indentStr = ' ') {
|
|
206
|
+
let depth = 0;
|
|
207
|
+
let output = '';
|
|
208
|
+
let lineIsEmpty = true;
|
|
209
|
+
|
|
210
|
+
// Helper: current indent prefix for depth
|
|
211
|
+
const indent = (d) => indentStr.repeat(d);
|
|
212
|
+
|
|
213
|
+
// We'll do a second pass to insert newlines before clause keywords at depth 0
|
|
214
|
+
// First reconstruct a compact (single-space) version of the token values
|
|
215
|
+
const parts = [];
|
|
216
|
+
let i = 0;
|
|
217
|
+
while (i < tokens.length) {
|
|
218
|
+
const tok = tokens[i];
|
|
219
|
+
if (tok.type === 'whitespace') { parts.push({ type: 'ws' }); i++; continue; }
|
|
220
|
+
parts.push(tok);
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Now emit formatted output
|
|
225
|
+
let out = '';
|
|
226
|
+
let col = 0; // approximate column position
|
|
227
|
+
let prevNonWs = null;
|
|
228
|
+
|
|
229
|
+
function emit(str) {
|
|
230
|
+
out += str;
|
|
231
|
+
const lastNl = str.lastIndexOf('\n');
|
|
232
|
+
if (lastNl >= 0) col = str.length - lastNl - 1;
|
|
233
|
+
else col += str.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function newline(d) {
|
|
237
|
+
out += '\n' + indent(d);
|
|
238
|
+
col = indent(d).length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Trim leading/trailing whitespace tokens
|
|
242
|
+
let start = 0;
|
|
243
|
+
let end = parts.length - 1;
|
|
244
|
+
while (start <= end && parts[start].type === 'ws') start++;
|
|
245
|
+
while (end >= start && parts[end].type === 'ws') end--;
|
|
246
|
+
|
|
247
|
+
for (let idx = start; idx <= end; idx++) {
|
|
248
|
+
const p = parts[idx];
|
|
249
|
+
|
|
250
|
+
if (p.type === 'ws') {
|
|
251
|
+
// We'll handle spacing contextually; skip raw whitespace
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (p.type === 'paren_open') {
|
|
256
|
+
emit('(');
|
|
257
|
+
depth++;
|
|
258
|
+
// Look ahead: if next non-ws is a clause keyword, newline+indent
|
|
259
|
+
let next = idx + 1;
|
|
260
|
+
while (next <= end && parts[next].type === 'ws') next++;
|
|
261
|
+
if (next <= end && parts[next].type === 'word') {
|
|
262
|
+
const nv = parts[next].value;
|
|
263
|
+
if (CLAUSE_KEYWORDS.includes(nv)) {
|
|
264
|
+
newline(depth);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
prevNonWs = p;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (p.type === 'paren_close') {
|
|
272
|
+
depth = Math.max(0, depth - 1);
|
|
273
|
+
// If previous output doesn't end in a newline+indent, just close
|
|
274
|
+
if (out.length > 0 && out[out.length - 1] !== '\n' && col > 0) {
|
|
275
|
+
// Check if the open-paren content had newlines (subquery style)
|
|
276
|
+
const lastNl = out.lastIndexOf('\n');
|
|
277
|
+
if (lastNl >= 0 && out.slice(lastNl + 1).trim() === '') {
|
|
278
|
+
// Already on its own line — just close
|
|
279
|
+
out = out.slice(0, lastNl + 1) + indent(depth) + ')';
|
|
280
|
+
col = indent(depth).length + 1;
|
|
281
|
+
} else {
|
|
282
|
+
emit(')');
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
emit(')');
|
|
286
|
+
}
|
|
287
|
+
prevNonWs = p;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (p.type === 'comma') {
|
|
292
|
+
emit(',');
|
|
293
|
+
if (depth === 0) {
|
|
294
|
+
newline(0);
|
|
295
|
+
// Emit an indent to align with SELECT columns
|
|
296
|
+
emit(' '); // 7 spaces aligns under "SELECT "
|
|
297
|
+
} else {
|
|
298
|
+
// Inside parens: comma + space
|
|
299
|
+
emit(' ');
|
|
300
|
+
}
|
|
301
|
+
prevNonWs = p;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (p.type === 'semicolon') {
|
|
306
|
+
emit(';');
|
|
307
|
+
if (idx < end) {
|
|
308
|
+
out += '\n\n';
|
|
309
|
+
col = 0;
|
|
310
|
+
depth = 0;
|
|
311
|
+
}
|
|
312
|
+
prevNonWs = p;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (p.type === 'word') {
|
|
317
|
+
// Check if it's a clause keyword at depth 0
|
|
318
|
+
if (depth === 0 && CLAUSE_KEYWORDS.includes(p.value)) {
|
|
319
|
+
const isFirst = (prevNonWs === null);
|
|
320
|
+
if (!isFirst) {
|
|
321
|
+
newline(0);
|
|
322
|
+
}
|
|
323
|
+
emit(p.value);
|
|
324
|
+
emit(' ');
|
|
325
|
+
prevNonWs = p;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// Otherwise emit with space separation
|
|
329
|
+
if (prevNonWs !== null &&
|
|
330
|
+
prevNonWs.type !== 'paren_open' &&
|
|
331
|
+
prevNonWs.type !== 'comma' &&
|
|
332
|
+
!(prevNonWs.type === 'word' && out.endsWith(' '))) {
|
|
333
|
+
// Don't double-space
|
|
334
|
+
if (!out.endsWith(' ') && !out.endsWith('\n')) emit(' ');
|
|
335
|
+
}
|
|
336
|
+
emit(p.value);
|
|
337
|
+
prevNonWs = p;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// string / comment
|
|
342
|
+
if (prevNonWs !== null &&
|
|
343
|
+
prevNonWs.type !== 'paren_open' &&
|
|
344
|
+
prevNonWs.type !== 'comma' &&
|
|
345
|
+
!out.endsWith(' ') && !out.endsWith('\n')) {
|
|
346
|
+
emit(' ');
|
|
347
|
+
}
|
|
348
|
+
emit(p.value);
|
|
349
|
+
prevNonWs = p;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Trim trailing whitespace on each line
|
|
353
|
+
out = out.split('\n').map(l => l.trimEnd()).join('\n');
|
|
354
|
+
// Ensure single trailing newline
|
|
355
|
+
out = out.trimEnd() + '\n';
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function formatSQL(sql) {
|
|
360
|
+
const tokens = tokenize(sql);
|
|
361
|
+
const upped = uppercaseKeywords(tokens);
|
|
362
|
+
return format(upped);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
const args = process.argv.slice(2);
|
|
368
|
+
const checkMode = args.includes('--check');
|
|
369
|
+
const fileArgs = args.filter(a => !a.startsWith('--'));
|
|
370
|
+
|
|
371
|
+
function run(sql) {
|
|
372
|
+
const formatted = formatSQL(sql);
|
|
373
|
+
if (checkMode) {
|
|
374
|
+
const normalIn = formatSQL(sql);
|
|
375
|
+
if (normalIn === formatted) {
|
|
376
|
+
process.stdout.write('SQL is already formatted.\n');
|
|
377
|
+
process.exit(0);
|
|
378
|
+
} else {
|
|
379
|
+
process.stderr.write('SQL is not formatted. Run sqlfmt to fix.\n');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
process.stdout.write(formatted);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (fileArgs.length > 0) {
|
|
387
|
+
// Read from file(s)
|
|
388
|
+
fileArgs.forEach(filePath => {
|
|
389
|
+
const abs = path.resolve(filePath);
|
|
390
|
+
if (!fs.existsSync(abs)) {
|
|
391
|
+
process.stderr.write(`sqlfmt: file not found: ${filePath}\n`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const sql = fs.readFileSync(abs, 'utf8');
|
|
395
|
+
run(sql);
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
// Read from stdin
|
|
399
|
+
let input = '';
|
|
400
|
+
process.stdin.setEncoding('utf8');
|
|
401
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
402
|
+
process.stdin.on('end', () => {
|
|
403
|
+
if (!input.trim()) {
|
|
404
|
+
process.stderr.write('sqlfmt: no input. Usage: sqlfmt query.sql or echo "SELECT ..." | sqlfmt\n');
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
run(input);
|
|
408
|
+
});
|
|
409
|
+
process.stdin.resume();
|
|
410
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sql-fmt-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Format SQL queries from stdin or file — uppercase keywords, one clause per line, indent subqueries",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sqlfmt": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"select id,name from users where active=1 order by name\" | node index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"sql",
|
|
14
|
+
"format",
|
|
15
|
+
"prettier",
|
|
16
|
+
"cli",
|
|
17
|
+
"database",
|
|
18
|
+
"formatter",
|
|
19
|
+
"sql-formatter",
|
|
20
|
+
"query",
|
|
21
|
+
"mysql",
|
|
22
|
+
"postgres",
|
|
23
|
+
"sqlite"
|
|
24
|
+
],
|
|
25
|
+
"author": "chengyixu",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/chengyixu/sql-fmt-cli"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/chengyixu/sql-fmt-cli#readme",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=12.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|