├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── LICENSE
├── README.org
├── binary
└── sql-formatter-1.0.0-jar-with-dependencies.jar
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── github
│ │ └── sambatriste
│ │ ├── CustomFormatter.java
│ │ └── SqlFormatter.java
└── script
│ └── sql-formatter.groovy
└── test
└── java
└── com
└── github
└── sambatriste
└── SqlFormatterTest.java
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
3 |
4 | name: Java CI with Maven
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up JDK 1.8
20 | uses: actions/setup-java@v1
21 | with:
22 | java-version: 1.8
23 | - name: Build with Maven
24 | run: mvn -B package --file pom.xml
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Maven template
3 | target/
4 | pom.xml.tag
5 | pom.xml.releaseBackup
6 | pom.xml.versionsBackup
7 | pom.xml.next
8 | release.properties
9 | dependency-reduced-pom.xml
10 | buildNumber.properties
11 | .mvn/timing.properties
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 KAWASAKI Tsuyoshi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | #+TITLE: SQL formatter
2 |
3 | ** これは何?
4 |
5 | SQL文のフォーマットを行います。
6 | 標準入力から受け取ったSQL文をフォーマットして、標準出力に出力します。
7 |
8 | *** 使用例
9 | #+BEGIN_SRC sh
10 | echo "SELECT * FROM HOGE WHERE HOGE.FUGA = :fuga" | java -jar target/sql-formatter-1.0.1-jar-with-dependencies.jar
11 |
12 | #+END_SRC
13 |
14 | *** 出力結果
15 |
16 | #+BEGIN_EXAMPLE
17 | SELECT
18 | *
19 | FROM
20 | HOGE
21 | WHERE
22 | HOGE.FUGA = :fuga
23 | #+END_EXAMPLE
24 |
25 | *** 依存ライブラリ
26 |
27 | Hibernateのorg.hibernate.engine.jdbc.internal.BasicFormatterImplを使用しています。
28 |
29 | *** 参考URL
30 |
31 | [[http://stackoverflow.com/questions/312552/looking-for-an-embeddable-sql-beautifier-or-reformatter][- Looking for an embeddable SQL beautifier or reformatter]]
32 |
33 |
34 | ** バイナリ
35 |
36 | ビルドが面倒な場合は、binaryディレクトリ配下のjarをダウンロードしてください。
37 |
38 | ** Emacsとの連携
39 |
40 | Emacsでは、リージョン内の内容を外部コマンドに渡して、その結果でバッファを書き換えることができます。
41 | これを利用してSQL文の整形をEmacs上で行います。
42 |
43 | #+BEGIN_SRC lisp
44 | ;;; SQL文の整形をする設定
45 | ;; 実行する外部コマンド
46 | (setq sql-format-external-command
47 | (concat "java -jar " (expand-file-name "~/.emacs.d/lib/sql-formatter-1.0.0-jar-with-dependencies.jar")))
48 |
49 | ;; SQL文をフォーマットする関数
50 | (defun my-format-sql ()
51 | "バッファまたはリージョン内のSQL文を整形する。"
52 | (interactive)
53 | (let (begin end)
54 | (cond (mark-active
55 | (setq begin (region-beginning))
56 | (setq end (region-end)))
57 | (t
58 | (setq begin (point-min))
59 | (setq end (point-max))))
60 | (save-excursion
61 | (shell-command-on-region
62 | begin
63 | end
64 | sql-format-external-command
65 | nil
66 | t ; replace buffer
67 | ))))
68 |
69 | ;; キーバインド設定
70 | (with-eval-after-load "sql"
71 | (define-key sql-mode-map (kbd "C-S-f") 'my-format-sql))
72 | #+END_SRC
73 |
--------------------------------------------------------------------------------
/binary/sql-formatter-1.0.0-jar-with-dependencies.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sambatriste/sql-formatter/8674a8be78262b3817810cc4150b32361e5536ea/binary/sql-formatter-1.0.0-jar-with-dependencies.jar
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | com.github.sambatriste
5 | sql-formatter
6 | jar
7 | 1.0.1
8 | SQL formatter
9 | https://github.com/sambatriste/sql-formatter
10 |
11 |
12 | UTF-8
13 | 1.7
14 | 1.7
15 |
16 |
17 |
18 |
19 | org.hibernate
20 | hibernate-core
21 | 5.4.28.Final
22 |
23 |
24 | junit
25 | junit
26 | 4.13.1
27 | test
28 |
29 |
30 |
31 |
32 |
33 |
34 | org.apache.maven.plugins
35 | maven-compiler-plugin
36 | 3.6.2
37 |
38 |
39 | org.apache.maven.plugins
40 | maven-assembly-plugin
41 | 2.5.1
42 |
43 |
44 | make-assembly
45 | package
46 |
47 | single
48 |
49 |
50 |
51 |
52 |
53 | jar-with-dependencies
54 |
55 |
56 |
57 | com.github.sambatriste.SqlFormatter
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sambatriste/CustomFormatter.java:
--------------------------------------------------------------------------------
1 | package com.github.sambatriste;
2 |
3 |
4 | import org.hibernate.engine.jdbc.internal.Formatter;
5 |
6 | import java.util.HashSet;
7 | import java.util.LinkedList;
8 | import java.util.Locale;
9 | import java.util.Set;
10 | import java.util.StringTokenizer;
11 |
12 | import org.hibernate.internal.util.StringHelper;
13 |
14 | /**
15 | * {@link org.hibernate.engine.jdbc.internal.BasicFormatterImpl}をカスタマイズしたフォーマッタ。
16 | *
17 | *
18 | * | 項目 | BasicFormatterImpl | CustomFormatter |
19 | * |----------------|--------------------|-----------------|
20 | * | インデント | 4スペース | 2スペース |
21 | * | 初期インデント | 1 | 0 |
22 | * | 開始行 | 改行あり | 改行なし |
23 | *
24 | *
25 | * @see org.hibernate.engine.jdbc.internal.BasicFormatterImpl
26 | */
27 | public class CustomFormatter implements Formatter {
28 |
29 | private static final Set BEGIN_CLAUSES = new HashSet();
30 | private static final Set END_CLAUSES = new HashSet();
31 | private static final Set LOGICAL = new HashSet();
32 | private static final Set QUANTIFIERS = new HashSet();
33 | private static final Set DML = new HashSet();
34 | private static final Set MISC = new HashSet();
35 |
36 | static {
37 | BEGIN_CLAUSES.add( "left" );
38 | BEGIN_CLAUSES.add( "right" );
39 | BEGIN_CLAUSES.add( "inner" );
40 | BEGIN_CLAUSES.add( "outer" );
41 | BEGIN_CLAUSES.add( "group" );
42 | BEGIN_CLAUSES.add( "order" );
43 |
44 | END_CLAUSES.add( "where" );
45 | END_CLAUSES.add( "set" );
46 | END_CLAUSES.add( "having" );
47 | END_CLAUSES.add( "from" );
48 | END_CLAUSES.add( "by" );
49 | END_CLAUSES.add( "join" );
50 | END_CLAUSES.add( "into" );
51 | END_CLAUSES.add( "union" );
52 |
53 | LOGICAL.add( "and" );
54 | LOGICAL.add( "or" );
55 | LOGICAL.add( "when" );
56 | LOGICAL.add( "else" );
57 | LOGICAL.add( "end" );
58 |
59 | QUANTIFIERS.add( "in" );
60 | QUANTIFIERS.add( "all" );
61 | QUANTIFIERS.add( "exists" );
62 | QUANTIFIERS.add( "some" );
63 | QUANTIFIERS.add( "any" );
64 |
65 | DML.add( "insert" );
66 | DML.add( "update" );
67 | DML.add( "delete" );
68 |
69 | MISC.add( "select" );
70 | MISC.add( "on" );
71 | }
72 |
73 | //private static final String INDENT_STRING = " ";
74 | private static final String INDENT_STRING = " ";
75 | //private static final String INITIAL = System.lineSeparator() + INDENT_STRING;
76 | private static final String INITIAL = "";
77 |
78 | @Override
79 | public String format(String source) {
80 | return new FormatProcess( source ).perform();
81 | }
82 |
83 | private static class FormatProcess {
84 | boolean beginLine = true;
85 | boolean afterBeginBeforeEnd;
86 | boolean afterByOrSetOrFromOrSelect;
87 | boolean afterOn;
88 | boolean afterBetween;
89 | boolean afterInsert;
90 | int inFunction;
91 | int parensSinceSelect;
92 | private LinkedList parenCounts = new LinkedList();
93 | private LinkedList afterByOrFromOrSelects = new LinkedList();
94 |
95 | //int indent = 1;
96 | int indent = 0;
97 |
98 | StringBuilder result = new StringBuilder();
99 | StringTokenizer tokens;
100 | String lastToken;
101 | String token;
102 | String lcToken;
103 |
104 | public FormatProcess(String sql) {
105 | tokens = new StringTokenizer(
106 | sql,
107 | "()+*/-=<>'`\"[]," + StringHelper.WHITESPACE,
108 | true
109 | );
110 | }
111 |
112 | public String perform() {
113 |
114 | result.append( INITIAL );
115 |
116 | while ( tokens.hasMoreTokens() ) {
117 | token = tokens.nextToken();
118 | lcToken = token.toLowerCase(Locale.ROOT);
119 |
120 | if ( "'".equals( token ) ) {
121 | String t;
122 | do {
123 | t = tokens.nextToken();
124 | token += t;
125 | }
126 | // cannot handle single quotes
127 | while ( !"'".equals( t ) && tokens.hasMoreTokens() );
128 | }
129 | else if ( "\"".equals( token ) ) {
130 | String t;
131 | do {
132 | t = tokens.nextToken();
133 | token += t;
134 | }
135 | while ( !"\"".equals( t ) && tokens.hasMoreTokens() );
136 | }
137 | // SQL Server uses "[" and "]" to escape reserved words
138 | // see SQLServerDialect.openQuote and SQLServerDialect.closeQuote
139 | else if ( "[".equals( token ) ) {
140 | String t;
141 | do {
142 | t = tokens.nextToken();
143 | token += t;
144 | }
145 | while ( !"]".equals( t ) && tokens.hasMoreTokens());
146 | }
147 |
148 | if ( afterByOrSetOrFromOrSelect && ",".equals( token ) ) {
149 | commaAfterByOrFromOrSelect();
150 | }
151 | else if ( afterOn && ",".equals( token ) ) {
152 | commaAfterOn();
153 | }
154 |
155 | else if ( "(".equals( token ) ) {
156 | openParen();
157 | }
158 | else if ( ")".equals( token ) ) {
159 | closeParen();
160 | }
161 |
162 | else if ( BEGIN_CLAUSES.contains( lcToken ) ) {
163 | beginNewClause();
164 | }
165 |
166 | else if ( END_CLAUSES.contains( lcToken ) ) {
167 | endNewClause();
168 | }
169 |
170 | else if ( "select".equals( lcToken ) ) {
171 | select();
172 | }
173 |
174 | else if ( DML.contains( lcToken ) ) {
175 | updateOrInsertOrDelete();
176 | }
177 |
178 | else if ( "values".equals( lcToken ) ) {
179 | values();
180 | }
181 |
182 | else if ( "on".equals( lcToken ) ) {
183 | on();
184 | }
185 |
186 | else if ( afterBetween && lcToken.equals( "and" ) ) {
187 | misc();
188 | afterBetween = false;
189 | }
190 |
191 | else if ( LOGICAL.contains( lcToken ) ) {
192 | logical();
193 | }
194 |
195 | else if ( isWhitespace( token ) ) {
196 | white();
197 | }
198 |
199 | else {
200 | misc();
201 | }
202 |
203 | if ( !isWhitespace( token ) ) {
204 | lastToken = lcToken;
205 | }
206 |
207 | }
208 | return result.toString();
209 | }
210 |
211 | private void commaAfterOn() {
212 | out();
213 | indent--;
214 | newline();
215 | afterOn = false;
216 | afterByOrSetOrFromOrSelect = true;
217 | }
218 |
219 | private void commaAfterByOrFromOrSelect() {
220 | out();
221 | newline();
222 | }
223 |
224 | private void logical() {
225 | if ( "end".equals( lcToken ) ) {
226 | indent--;
227 | }
228 | newline();
229 | out();
230 | beginLine = false;
231 | }
232 |
233 | private void on() {
234 | indent++;
235 | afterOn = true;
236 | newline();
237 | out();
238 | beginLine = false;
239 | }
240 |
241 | private void misc() {
242 | out();
243 | if ( "between".equals( lcToken ) ) {
244 | afterBetween = true;
245 | }
246 | if ( afterInsert ) {
247 | newline();
248 | afterInsert = false;
249 | }
250 | else {
251 | beginLine = false;
252 | if ( "case".equals( lcToken ) ) {
253 | indent++;
254 | }
255 | }
256 | }
257 |
258 | private void white() {
259 | if ( !beginLine ) {
260 | result.append( " " );
261 | }
262 | }
263 |
264 | private void updateOrInsertOrDelete() {
265 | out();
266 | indent++;
267 | beginLine = false;
268 | if ( "update".equals( lcToken ) ) {
269 | newline();
270 | }
271 | if ( "insert".equals( lcToken ) ) {
272 | afterInsert = true;
273 | }
274 | }
275 |
276 | private void select() {
277 | out();
278 | indent++;
279 | newline();
280 | parenCounts.addLast( parensSinceSelect );
281 | afterByOrFromOrSelects.addLast( afterByOrSetOrFromOrSelect );
282 | parensSinceSelect = 0;
283 | afterByOrSetOrFromOrSelect = true;
284 | }
285 |
286 | private void out() {
287 | result.append( token );
288 | }
289 |
290 | private void endNewClause() {
291 | if ( !afterBeginBeforeEnd ) {
292 | indent--;
293 | if ( afterOn ) {
294 | indent--;
295 | afterOn = false;
296 | }
297 | newline();
298 | }
299 | out();
300 | if ( !"union".equals( lcToken ) ) {
301 | indent++;
302 | }
303 | newline();
304 | afterBeginBeforeEnd = false;
305 | afterByOrSetOrFromOrSelect = "by".equals( lcToken )
306 | || "set".equals( lcToken )
307 | || "from".equals( lcToken );
308 | }
309 |
310 | private void beginNewClause() {
311 | if ( !afterBeginBeforeEnd ) {
312 | if ( afterOn ) {
313 | indent--;
314 | afterOn = false;
315 | }
316 | indent--;
317 | newline();
318 | }
319 | out();
320 | beginLine = false;
321 | afterBeginBeforeEnd = true;
322 | }
323 |
324 | private void values() {
325 | indent--;
326 | newline();
327 | out();
328 | indent++;
329 | newline();
330 | }
331 |
332 | private void closeParen() {
333 | parensSinceSelect--;
334 | if ( parensSinceSelect < 0 ) {
335 | indent--;
336 | parensSinceSelect = parenCounts.removeLast();
337 | afterByOrSetOrFromOrSelect = afterByOrFromOrSelects.removeLast();
338 | }
339 | if ( inFunction > 0 ) {
340 | inFunction--;
341 | out();
342 | }
343 | else {
344 | if ( !afterByOrSetOrFromOrSelect ) {
345 | indent--;
346 | newline();
347 | }
348 | out();
349 | }
350 | beginLine = false;
351 | }
352 |
353 | private void openParen() {
354 | if ( isFunctionName( lastToken ) || inFunction > 0 ) {
355 | inFunction++;
356 | }
357 | beginLine = false;
358 | if ( inFunction > 0 ) {
359 | out();
360 | }
361 | else {
362 | out();
363 | if ( !afterByOrSetOrFromOrSelect ) {
364 | indent++;
365 | newline();
366 | beginLine = true;
367 | }
368 | }
369 | parensSinceSelect++;
370 | }
371 |
372 | private static boolean isFunctionName(String tok) {
373 | if ( tok == null || tok.length() == 0 ) {
374 | return false;
375 | }
376 |
377 | final char begin = tok.charAt( 0 );
378 | final boolean isIdentifier = Character.isJavaIdentifierStart( begin ) || '"' == begin;
379 | return isIdentifier &&
380 | !LOGICAL.contains( tok ) &&
381 | !END_CLAUSES.contains( tok ) &&
382 | !QUANTIFIERS.contains( tok ) &&
383 | !DML.contains( tok ) &&
384 | !MISC.contains( tok );
385 | }
386 |
387 | private static boolean isWhitespace(String token) {
388 | return StringHelper.WHITESPACE.contains( token );
389 | }
390 |
391 | private void newline() {
392 | result.append( System.lineSeparator() );
393 | for ( int i = 0; i < indent; i++ ) {
394 | result.append( INDENT_STRING );
395 | }
396 | beginLine = true;
397 | }
398 | }
399 | }
400 |
--------------------------------------------------------------------------------
/src/main/java/com/github/sambatriste/SqlFormatter.java:
--------------------------------------------------------------------------------
1 | package com.github.sambatriste;
2 |
3 | import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
4 | import org.hibernate.engine.jdbc.internal.Formatter;
5 |
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.io.InputStreamReader;
9 | import java.io.OutputStream;
10 | import java.io.OutputStreamWriter;
11 | import java.io.Reader;
12 | import java.io.Writer;
13 |
14 | public class SqlFormatter {
15 |
16 | private final Reader reader;
17 |
18 | private final Writer writer;
19 |
20 | public static void main(String[] args) throws IOException {
21 | SqlFormatter sqlFormatter = new SqlFormatter(System.in, System.out);
22 | sqlFormatter.format();
23 | }
24 |
25 | public SqlFormatter(Reader reader, Writer writer) {
26 | this.reader = reader;
27 | this.writer = writer;
28 | }
29 |
30 | public SqlFormatter(InputStream in, OutputStream out) {
31 | this(new InputStreamReader(in), new OutputStreamWriter(out));
32 | }
33 |
34 | public void format() throws IOException {
35 | String original = readSql();
36 | String formatted = newFormatter().format(original);
37 | writer.write(formatted);
38 | writer.flush();
39 | }
40 |
41 | private String readSql() throws IOException {
42 | int c;
43 | StringBuilder sql = new StringBuilder(1024);
44 | while ((c = reader.read()) != -1) {
45 | sql.append((char) c);
46 | }
47 | return sql.toString();
48 |
49 | }
50 |
51 | private Formatter newFormatter() {
52 | return new CustomFormatter();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/script/sql-formatter.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * 標準入力からSQL文を読み取り、整形したSQL文を標準出力に出力します。
3 | */
4 |
5 | @Grab(group='org.hibernate', module='hibernate-core', version='5.2.3.Final')
6 | import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
7 |
8 | def formatter = new BasicFormatterImpl()
9 |
10 | String original = System.in.text
11 | String formatted = formatter.format(original)
12 | println formatted
--------------------------------------------------------------------------------
/src/test/java/com/github/sambatriste/SqlFormatterTest.java:
--------------------------------------------------------------------------------
1 | package com.github.sambatriste;
2 |
3 | import org.junit.Test;
4 |
5 | import java.io.IOException;
6 | import java.io.StringReader;
7 | import java.io.StringWriter;
8 |
9 | import static org.hamcrest.CoreMatchers.is;
10 | import static org.junit.Assert.assertThat;
11 |
12 | public class SqlFormatterTest {
13 |
14 | @Test
15 | public void test() throws IOException {
16 | String original = "SELECT * FROM HOGE WHERE HOGE.FUGA = :fuga";
17 | String expected =
18 | "SELECT\n" +
19 | " * \n" +
20 | "FROM\n" +
21 | " HOGE \n" +
22 | "WHERE\n" +
23 | " HOGE.FUGA = :fuga";
24 | String formatted = format(original);
25 | assertThat(formatted, is(expected));
26 |
27 | }
28 |
29 | private String format(String sql) throws IOException {
30 | StringReader reader = new StringReader(sql);
31 | StringWriter writer = new StringWriter();
32 | SqlFormatter sut = new SqlFormatter(reader, writer);
33 | sut.format();
34 | return writer.toString();
35 |
36 | }
37 |
38 | }
--------------------------------------------------------------------------------