sql_beautifier 0.9.2 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +58 -3
- data/lib/sql_beautifier/case_expression.rb +405 -0
- data/lib/sql_beautifier/clauses/condition_clause.rb +14 -4
- data/lib/sql_beautifier/clauses/select.rb +4 -1
- data/lib/sql_beautifier/condition.rb +1 -1
- data/lib/sql_beautifier/constants.rb +2 -0
- data/lib/sql_beautifier/create_table_as.rb +7 -1
- data/lib/sql_beautifier/join.rb +7 -3
- data/lib/sql_beautifier/statement_splitter.rb +22 -3
- data/lib/sql_beautifier/table_reference.rb +12 -2
- data/lib/sql_beautifier/table_registry.rb +1 -0
- data/lib/sql_beautifier/tokenizer.rb +29 -14
- data/lib/sql_beautifier/update_query.rb +5 -2
- data/lib/sql_beautifier/version.rb +1 -1
- data/lib/sql_beautifier.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9557199079511cec5188c694dd32e2d114aaedebb896b90dc4ebe9e95308f116
|
|
4
|
+
data.tar.gz: da7fe2c4ff35032c6908a8ea10b5e8c4482cb607a78ac3c241b7d4f2093bbc88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d363570c1c0add348461b8b9069eb4f7075036140f6c7b86f03130c88903f5fcf17613123841a28ca28b55eeb6b3f16905572017222904a58595586dbb782134
|
|
7
|
+
data.tar.gz: da33c33ada19314d1421d7a1cab384b9f6fa25b456909f2086b662bf8c235089443a8513b95049bbebe7db70d4535d32cefec664de3927a0d5ca7a0f9da10c87
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [X.X.X] - YYYY-MM-DD
|
|
4
4
|
|
|
5
|
+
## [0.10.0] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
- Add CASE expression formatting — searched CASE (`CASE WHEN ... THEN ... ELSE ... END`) and simple CASE (`CASE expr WHEN value THEN ... END`) are detected and formatted with consistent indentation of `when`/`else`/`end` lines relative to the `case` keyword
|
|
8
|
+
- Add CASE integration into SELECT columns, WHERE/HAVING conditions, and UPDATE SET assignments via `CaseExpression.format_in_text`
|
|
9
|
+
- Add inline vs expanded rendering for CASE expressions controlled by the existing `inline_group_threshold` configuration — short CASE expressions remain on a single line when below the threshold
|
|
10
|
+
- Add nested CASE support — inner CASE blocks within WHEN/THEN/ELSE values are recursively formatted with increased indentation
|
|
11
|
+
- Add string-literal and sentinel safety — CASE keywords inside quoted strings, double-quoted identifiers, and comment sentinels are not mistakenly parsed
|
|
12
|
+
- Preserve CASE expressions inside parenthesized function calls (e.g. `COALESCE(CASE ... END, 0)`) without top-level expansion
|
|
13
|
+
- Fix `AND`/`OR` inside CASE expressions being misidentified as top-level conjunctions in WHERE/HAVING clauses — conjunction scanning now tracks CASE/END depth
|
|
14
|
+
- Use `indent_spaces` configuration for CASE body indentation instead of a hard-coded value
|
|
15
|
+
- Add `INNER JOIN LATERAL` and `LEFT JOIN LATERAL` support — `LATERAL` is recognized as a join modifier rather than a table name, preserving correct derived table parsing and alias resolution for lateral subqueries
|
|
16
|
+
- Strip redundant outer parentheses from WHERE and HAVING clause bodies — `WHERE (active = true)`, `WHERE ((active = true))`, and `WHERE ((a = true) AND (b = true))` now correctly unwrap before formatting
|
|
17
|
+
- Fix `Condition.format` early return for single leaf conditions to use the unwrapped expression instead of the original text
|
|
18
|
+
|
|
5
19
|
## [0.9.2] - 2026-03-30
|
|
6
20
|
|
|
7
21
|
- Fix derived tables (subqueries in `FROM` clauses) losing their content during formatting — `TableRegistry#parse_references` used a regex split that did not respect parenthesis depth, causing JOIN keywords inside derived table subqueries to be treated as top-level boundaries
|
data/README.md
CHANGED
|
@@ -111,7 +111,7 @@ where u.active = true
|
|
|
111
111
|
order by o.total desc;
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Supported join types: `inner join`, `left join`, `right join`, `full join`, `left outer join`, `right outer join`, `full outer join`, `cross join`.
|
|
114
|
+
Supported join types: `inner join`, `left join`, `right join`, `full join`, `left outer join`, `right outer join`, `full outer join`, `cross join`. The `LATERAL` modifier is supported with `inner join lateral` and `left join lateral` for lateral subqueries.
|
|
115
115
|
|
|
116
116
|
### DISTINCT and DISTINCT ON
|
|
117
117
|
|
|
@@ -212,6 +212,61 @@ group by status
|
|
|
212
212
|
having count(*) > 5;
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
### CASE Expressions
|
|
216
|
+
|
|
217
|
+
Both searched (`CASE WHEN ... THEN ... END`) and simple (`CASE expr WHEN value THEN ... END`) forms are formatted with multiline indentation. Inner `when`/`else`/`end` lines are indented relative to the `case` keyword:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
SqlBeautifier.call("SELECT id, CASE WHEN status = 'active' THEN 'Active' WHEN status = 'pending' THEN 'Pending' ELSE 'Unknown' END AS status_label, name FROM users")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Produces:
|
|
224
|
+
|
|
225
|
+
```sql
|
|
226
|
+
select id,
|
|
227
|
+
case
|
|
228
|
+
when status = 'active' then 'Active'
|
|
229
|
+
when status = 'pending' then 'Pending'
|
|
230
|
+
else 'Unknown'
|
|
231
|
+
end as status_label,
|
|
232
|
+
name
|
|
233
|
+
|
|
234
|
+
from Users u;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Simple CASE places the operand on the `case` line:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
SqlBeautifier.call("SELECT CASE u.role WHEN 'admin' THEN 'Administrator' WHEN 'user' THEN 'Standard User' ELSE 'Guest' END AS role_label FROM users")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Produces:
|
|
244
|
+
|
|
245
|
+
```sql
|
|
246
|
+
select case u.role
|
|
247
|
+
when 'admin' then 'Administrator'
|
|
248
|
+
when 'user' then 'Standard User'
|
|
249
|
+
else 'Guest'
|
|
250
|
+
end as role_label
|
|
251
|
+
|
|
252
|
+
from Users u;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
CASE expressions inside parenthesized function calls are preserved inline:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
SqlBeautifier.call("SELECT COALESCE(CASE WHEN x > 0 THEN x ELSE NULL END, 0) AS safe_x FROM users")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Produces:
|
|
262
|
+
|
|
263
|
+
```sql
|
|
264
|
+
select coalesce(case when x > 0 then x else null end, 0) as safe_x
|
|
265
|
+
from Users u;
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
CASE expressions also work in WHERE/HAVING conditions and UPDATE SET assignments. Nested CASE blocks are recursively formatted with increased indentation. Short CASE expressions can remain inline when below the `inline_group_threshold`.
|
|
269
|
+
|
|
215
270
|
### LIMIT
|
|
216
271
|
|
|
217
272
|
```ruby
|
|
@@ -616,9 +671,9 @@ Controls how table names are formatted in the output. Default: `:pascal_case`.
|
|
|
616
671
|
|
|
617
672
|
#### `inline_group_threshold`
|
|
618
673
|
|
|
619
|
-
Maximum character length for a parenthesized condition group to remain on a single line. Groups whose inline representation exceeds this length are expanded to multiple lines with indented
|
|
674
|
+
Maximum character length for a parenthesized condition group or CASE expression to remain on a single line. Groups and CASE expressions whose inline representation exceeds this length are expanded to multiple lines with indented contents. Default: `0` (always expand).
|
|
620
675
|
|
|
621
|
-
Set to a positive integer to allow short groups to stay inline. For example, with a threshold of `80`, the group `(role = 'admin' or role = 'moderator')` would stay on one line since
|
|
676
|
+
Set to a positive integer to allow short groups and CASE expressions to stay inline. For example, with a threshold of `80`, the group `(role = 'admin' or role = 'moderator')` and a short CASE like `case when x = 1 then 'yes' else 'no' end` would stay on one line since they're under 80 characters.
|
|
622
677
|
|
|
623
678
|
#### `trailing_semicolon`
|
|
624
679
|
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class CaseExpression < Base
|
|
5
|
+
CASE_KEYWORD = "case"
|
|
6
|
+
WHEN_KEYWORD = "when"
|
|
7
|
+
THEN_KEYWORD = "then"
|
|
8
|
+
ELSE_KEYWORD = "else"
|
|
9
|
+
END_KEYWORD = "end"
|
|
10
|
+
|
|
11
|
+
option :operand, default: -> {}
|
|
12
|
+
option :when_clauses
|
|
13
|
+
option :else_value, default: -> {}
|
|
14
|
+
option :base_indent, default: -> { 0 }
|
|
15
|
+
|
|
16
|
+
def self.format_in_text(text, base_indent: 0)
|
|
17
|
+
output = +""
|
|
18
|
+
scanner = Scanner.new(text)
|
|
19
|
+
|
|
20
|
+
while scanner.position < text.length
|
|
21
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
22
|
+
if consumed
|
|
23
|
+
output << consumed
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
28
|
+
scanner.increment_depth!
|
|
29
|
+
output << scanner.current_char
|
|
30
|
+
scanner.advance!
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
35
|
+
scanner.decrement_depth!
|
|
36
|
+
output << scanner.current_char
|
|
37
|
+
scanner.advance!
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if scanner.parenthesis_depth.zero? && scanner.keyword_at?(CASE_KEYWORD)
|
|
42
|
+
case_start = scanner.position
|
|
43
|
+
end_position = find_matching_end(text, case_start)
|
|
44
|
+
|
|
45
|
+
unless end_position
|
|
46
|
+
output << scanner.current_char
|
|
47
|
+
scanner.advance!
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
case_text = text[case_start...(end_position + END_KEYWORD.length)]
|
|
52
|
+
parsed = parse(case_text, base_indent: base_indent)
|
|
53
|
+
output << (parsed ? parsed.render : case_text)
|
|
54
|
+
|
|
55
|
+
scanner.advance!(end_position + END_KEYWORD.length - scanner.position)
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
output << scanner.current_char
|
|
60
|
+
scanner.advance!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
output
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.parse(text, base_indent: 0)
|
|
67
|
+
stripped = text.strip
|
|
68
|
+
scanner = Scanner.new(stripped)
|
|
69
|
+
return nil unless scanner.keyword_at?(CASE_KEYWORD)
|
|
70
|
+
|
|
71
|
+
scanner.skip_past_keyword!(CASE_KEYWORD)
|
|
72
|
+
|
|
73
|
+
operand = extract_operand(stripped, scanner)
|
|
74
|
+
when_clauses = []
|
|
75
|
+
else_value = nil
|
|
76
|
+
case_depth = 1
|
|
77
|
+
|
|
78
|
+
until scanner.finished?
|
|
79
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
80
|
+
next if consumed
|
|
81
|
+
|
|
82
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
83
|
+
scanner.increment_depth!
|
|
84
|
+
scanner.advance!
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
89
|
+
scanner.decrement_depth!
|
|
90
|
+
scanner.advance!
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if scanner.parenthesis_depth.zero?
|
|
95
|
+
if scanner.keyword_at?(CASE_KEYWORD)
|
|
96
|
+
case_depth += 1
|
|
97
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if scanner.keyword_at?(END_KEYWORD)
|
|
102
|
+
case_depth -= 1
|
|
103
|
+
|
|
104
|
+
if case_depth.zero?
|
|
105
|
+
return new(
|
|
106
|
+
operand: operand,
|
|
107
|
+
when_clauses: when_clauses,
|
|
108
|
+
else_value: else_value,
|
|
109
|
+
base_indent: base_indent
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
scanner.advance!(END_KEYWORD.length)
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if case_depth == 1
|
|
118
|
+
if scanner.keyword_at?(WHEN_KEYWORD)
|
|
119
|
+
parsed_clause = parse_when_clause(stripped, scanner)
|
|
120
|
+
return nil unless parsed_clause
|
|
121
|
+
|
|
122
|
+
when_clauses << parsed_clause
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if scanner.keyword_at?(ELSE_KEYWORD)
|
|
127
|
+
else_value = parse_else_value(stripped, scanner)
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
scanner.advance!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def render
|
|
140
|
+
inline_version = render_inline
|
|
141
|
+
threshold = SqlBeautifier.config_for(:inline_group_threshold)
|
|
142
|
+
|
|
143
|
+
return inline_version if inline_version.length <= threshold
|
|
144
|
+
|
|
145
|
+
render_expanded
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def render_inline
|
|
151
|
+
parts = +"case"
|
|
152
|
+
parts << " #{@operand}" if @operand
|
|
153
|
+
|
|
154
|
+
@when_clauses.each do |clause|
|
|
155
|
+
parts << " when #{clause[:condition]} then #{clause[:result]}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
parts << " else #{@else_value}" if @else_value
|
|
159
|
+
parts << " end"
|
|
160
|
+
|
|
161
|
+
parts
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def render_expanded
|
|
165
|
+
indent_spaces = SqlBeautifier.config_for(:indent_spaces)
|
|
166
|
+
body_indent = Util.whitespace(@base_indent + indent_spaces)
|
|
167
|
+
closing_indent = Util.whitespace(@base_indent)
|
|
168
|
+
nested_base_indent = @base_indent + indent_spaces
|
|
169
|
+
|
|
170
|
+
lines = []
|
|
171
|
+
|
|
172
|
+
case_line = +"case"
|
|
173
|
+
case_line << " #{@operand}" if @operand
|
|
174
|
+
lines << case_line
|
|
175
|
+
|
|
176
|
+
@when_clauses.each do |clause|
|
|
177
|
+
formatted_condition = CaseExpression.format_in_text(clause[:condition], base_indent: nested_base_indent)
|
|
178
|
+
formatted_result = CaseExpression.format_in_text(clause[:result], base_indent: nested_base_indent)
|
|
179
|
+
lines << "#{body_indent}when #{formatted_condition} then #{formatted_result}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if @else_value
|
|
183
|
+
formatted_else = CaseExpression.format_in_text(@else_value, base_indent: nested_base_indent)
|
|
184
|
+
lines << "#{body_indent}else #{formatted_else}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
lines << "#{closing_indent}end"
|
|
188
|
+
|
|
189
|
+
lines.join("\n")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.extract_operand(text, scanner)
|
|
193
|
+
start_position = scanner.position
|
|
194
|
+
operand_scanner = Scanner.new(text, position: start_position)
|
|
195
|
+
|
|
196
|
+
until operand_scanner.finished?
|
|
197
|
+
consumed = operand_scanner.scan_quoted_or_sentinel!
|
|
198
|
+
next if consumed
|
|
199
|
+
|
|
200
|
+
if operand_scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
201
|
+
operand_scanner.increment_depth!
|
|
202
|
+
operand_scanner.advance!
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if operand_scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
207
|
+
operand_scanner.decrement_depth!
|
|
208
|
+
operand_scanner.advance!
|
|
209
|
+
next
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if operand_scanner.parenthesis_depth.zero? && operand_scanner.keyword_at?(WHEN_KEYWORD)
|
|
213
|
+
operand_text = text[start_position...operand_scanner.position].strip
|
|
214
|
+
scanner.advance!(operand_scanner.position - scanner.position)
|
|
215
|
+
|
|
216
|
+
return operand_text.empty? ? nil : operand_text
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
operand_scanner.advance!
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
scanner.advance!(operand_scanner.position - scanner.position)
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def self.parse_when_clause(text, scanner)
|
|
227
|
+
scanner.skip_past_keyword!(WHEN_KEYWORD)
|
|
228
|
+
condition_start = scanner.position
|
|
229
|
+
case_depth = 0
|
|
230
|
+
|
|
231
|
+
until scanner.finished?
|
|
232
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
233
|
+
next if consumed
|
|
234
|
+
|
|
235
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
236
|
+
scanner.increment_depth!
|
|
237
|
+
scanner.advance!
|
|
238
|
+
next
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
242
|
+
scanner.decrement_depth!
|
|
243
|
+
scanner.advance!
|
|
244
|
+
next
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if scanner.parenthesis_depth.zero?
|
|
248
|
+
if scanner.keyword_at?(CASE_KEYWORD)
|
|
249
|
+
case_depth += 1
|
|
250
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
251
|
+
next
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if scanner.keyword_at?(END_KEYWORD) && case_depth.positive?
|
|
255
|
+
case_depth -= 1
|
|
256
|
+
scanner.advance!(END_KEYWORD.length)
|
|
257
|
+
next
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if case_depth.zero? && scanner.keyword_at?(THEN_KEYWORD)
|
|
261
|
+
condition = text[condition_start...scanner.position].strip
|
|
262
|
+
scanner.skip_past_keyword!(THEN_KEYWORD)
|
|
263
|
+
then_result = parse_then_result(text, scanner)
|
|
264
|
+
|
|
265
|
+
return { condition: condition, result: then_result }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
scanner.advance!
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def self.parse_then_result(text, scanner)
|
|
276
|
+
result_start = scanner.position
|
|
277
|
+
case_depth = 0
|
|
278
|
+
|
|
279
|
+
until scanner.finished?
|
|
280
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
281
|
+
next if consumed
|
|
282
|
+
|
|
283
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
284
|
+
scanner.increment_depth!
|
|
285
|
+
scanner.advance!
|
|
286
|
+
next
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
290
|
+
scanner.decrement_depth!
|
|
291
|
+
scanner.advance!
|
|
292
|
+
next
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if scanner.parenthesis_depth.zero?
|
|
296
|
+
if scanner.keyword_at?(CASE_KEYWORD)
|
|
297
|
+
case_depth += 1
|
|
298
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
299
|
+
next
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if scanner.keyword_at?(END_KEYWORD) && case_depth.positive?
|
|
303
|
+
case_depth -= 1
|
|
304
|
+
scanner.advance!(END_KEYWORD.length)
|
|
305
|
+
next
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
return text[result_start...scanner.position].strip if case_depth.zero? && (scanner.keyword_at?(WHEN_KEYWORD) || scanner.keyword_at?(ELSE_KEYWORD) || scanner.keyword_at?(END_KEYWORD))
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
scanner.advance!
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
text[result_start...scanner.position].strip
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def self.parse_else_value(text, scanner)
|
|
318
|
+
scanner.skip_past_keyword!(ELSE_KEYWORD)
|
|
319
|
+
else_start = scanner.position
|
|
320
|
+
case_depth = 0
|
|
321
|
+
|
|
322
|
+
until scanner.finished?
|
|
323
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
324
|
+
next if consumed
|
|
325
|
+
|
|
326
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
327
|
+
scanner.increment_depth!
|
|
328
|
+
scanner.advance!
|
|
329
|
+
next
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
333
|
+
scanner.decrement_depth!
|
|
334
|
+
scanner.advance!
|
|
335
|
+
next
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if scanner.parenthesis_depth.zero?
|
|
339
|
+
if scanner.keyword_at?(CASE_KEYWORD)
|
|
340
|
+
case_depth += 1
|
|
341
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
342
|
+
next
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if scanner.keyword_at?(END_KEYWORD) && case_depth.positive?
|
|
346
|
+
case_depth -= 1
|
|
347
|
+
scanner.advance!(END_KEYWORD.length)
|
|
348
|
+
next
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
return text[else_start...scanner.position].strip if case_depth.zero? && scanner.keyword_at?(END_KEYWORD)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
scanner.advance!
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
text[else_start...scanner.position].strip
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def self.find_matching_end(text, case_start)
|
|
361
|
+
scanner = Scanner.new(text, position: case_start)
|
|
362
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
363
|
+
case_depth = 1
|
|
364
|
+
|
|
365
|
+
until scanner.finished?
|
|
366
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
367
|
+
next if consumed
|
|
368
|
+
|
|
369
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
370
|
+
scanner.increment_depth!
|
|
371
|
+
scanner.advance!
|
|
372
|
+
next
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
376
|
+
scanner.decrement_depth!
|
|
377
|
+
scanner.advance!
|
|
378
|
+
next
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
if scanner.parenthesis_depth.zero?
|
|
382
|
+
if scanner.keyword_at?(CASE_KEYWORD)
|
|
383
|
+
case_depth += 1
|
|
384
|
+
scanner.advance!(CASE_KEYWORD.length)
|
|
385
|
+
next
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if scanner.keyword_at?(END_KEYWORD)
|
|
389
|
+
case_depth -= 1
|
|
390
|
+
return scanner.position if case_depth.zero?
|
|
391
|
+
|
|
392
|
+
scanner.advance!(END_KEYWORD.length)
|
|
393
|
+
next
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
scanner.advance!
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
private_class_method :extract_operand, :parse_when_clause, :parse_then_result, :parse_else_value, :find_matching_end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
@@ -4,16 +4,26 @@ module SqlBeautifier
|
|
|
4
4
|
module Clauses
|
|
5
5
|
class ConditionClause < Base
|
|
6
6
|
def call
|
|
7
|
-
|
|
7
|
+
keyword_width = SqlBeautifier.config_for(:keyword_column_width)
|
|
8
|
+
formatted_value = CaseExpression.format_in_text(@value, base_indent: keyword_width)
|
|
9
|
+
unwrapped_value = strip_wrapping_parentheses(formatted_value)
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
return "#{keyword_prefix}#{unwrapped_value}" unless multiple_conditions?(unwrapped_value)
|
|
12
|
+
|
|
13
|
+
formatted_conditions = Condition.format(unwrapped_value, indent_width: keyword_width)
|
|
10
14
|
formatted_conditions.sub(continuation_indent, keyword_prefix)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
private
|
|
14
18
|
|
|
15
|
-
def
|
|
16
|
-
|
|
19
|
+
def strip_wrapping_parentheses(text)
|
|
20
|
+
output = text.strip
|
|
21
|
+
output = Util.strip_outer_parentheses(output) while Tokenizer.outer_parentheses_wrap_all?(output)
|
|
22
|
+
output
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def multiple_conditions?(text)
|
|
26
|
+
Tokenizer.split_top_level_conditions(text).length > 1
|
|
17
27
|
end
|
|
18
28
|
end
|
|
19
29
|
end
|
|
@@ -21,8 +21,11 @@ module SqlBeautifier
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def parse_expressions(value)
|
|
24
|
+
keyword_width = SqlBeautifier.config_for(:keyword_column_width)
|
|
25
|
+
|
|
24
26
|
Tokenizer.split_by_top_level_commas(value).map do |column|
|
|
25
|
-
|
|
27
|
+
formatted_column = CaseExpression.format_in_text(column, base_indent: keyword_width)
|
|
28
|
+
Expression.parse(formatted_column)
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -8,7 +8,7 @@ module SqlBeautifier
|
|
|
8
8
|
|
|
9
9
|
def self.format(text, indent_width: 0)
|
|
10
10
|
conditions = parse_all(text)
|
|
11
|
-
return
|
|
11
|
+
return conditions.first.expression if conditions.length <= 1 && conditions.first&.leaf?
|
|
12
12
|
|
|
13
13
|
render_all(conditions, indent_width: indent_width)
|
|
14
14
|
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
class CreateTableAs < Base
|
|
5
|
-
MODIFIERS = %w[
|
|
5
|
+
MODIFIERS = %w[
|
|
6
|
+
temp
|
|
7
|
+
temporary
|
|
8
|
+
unlogged
|
|
9
|
+
local
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
6
12
|
WITH_DATA_SUFFIX_REGEX = %r{\s+(with\s+(?:no\s+)?data)\s*\z}i
|
|
7
13
|
|
|
8
14
|
option :modifier, default: -> {}
|
data/lib/sql_beautifier/join.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
class Join < Base
|
|
5
5
|
option :keyword
|
|
6
|
+
option :lateral, default: -> { false }
|
|
6
7
|
option :table_reference
|
|
7
8
|
option :trailing_sentinels, default: -> {}
|
|
8
9
|
option :conditions, default: -> { [] }
|
|
@@ -11,6 +12,8 @@ module SqlBeautifier
|
|
|
11
12
|
keyword, remaining_content = extract_keyword(join_text)
|
|
12
13
|
return unless keyword && remaining_content
|
|
13
14
|
|
|
15
|
+
remaining_content, lateral = TableReference.strip_lateral_prefix(remaining_content)
|
|
16
|
+
|
|
14
17
|
on_keyword_position = Tokenizer.find_top_level_keyword(remaining_content, "on")
|
|
15
18
|
|
|
16
19
|
if on_keyword_position
|
|
@@ -28,7 +31,7 @@ module SqlBeautifier
|
|
|
28
31
|
|
|
29
32
|
trailing_sentinels = extract_trailing_sentinels(table_text)
|
|
30
33
|
|
|
31
|
-
new(keyword: keyword, table_reference: table_reference, trailing_sentinels: trailing_sentinels, conditions: conditions)
|
|
34
|
+
new(keyword: keyword, lateral: lateral, table_reference: table_reference, trailing_sentinels: trailing_sentinels, conditions: conditions)
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def self.extract_keyword(join_text)
|
|
@@ -52,17 +55,18 @@ module SqlBeautifier
|
|
|
52
55
|
|
|
53
56
|
def render(continuation_indent:, condition_indent:)
|
|
54
57
|
rendered_table = @table_reference.render(trailing_sentinels: @trailing_sentinels)
|
|
58
|
+
lateral_prefix = @lateral ? "lateral " : ""
|
|
55
59
|
lines = []
|
|
56
60
|
|
|
57
61
|
if @conditions.any?
|
|
58
62
|
first_condition = @conditions.first[1]
|
|
59
|
-
lines << "#{continuation_indent}#{@keyword} #{rendered_table} on #{first_condition}"
|
|
63
|
+
lines << "#{continuation_indent}#{@keyword} #{lateral_prefix}#{rendered_table} on #{first_condition}"
|
|
60
64
|
|
|
61
65
|
@conditions.drop(1).each do |conjunction, condition|
|
|
62
66
|
lines << "#{condition_indent}#{conjunction} #{condition}"
|
|
63
67
|
end
|
|
64
68
|
else
|
|
65
|
-
lines << "#{continuation_indent}#{@keyword} #{rendered_table}"
|
|
69
|
+
lines << "#{continuation_indent}#{@keyword} #{lateral_prefix}#{rendered_table}"
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
lines.join("\n")
|
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
module StatementSplitter
|
|
5
|
-
STATEMENT_KEYWORDS = %w[
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
STATEMENT_KEYWORDS = %w[
|
|
6
|
+
select
|
|
7
|
+
with
|
|
8
|
+
create
|
|
9
|
+
insert
|
|
10
|
+
update
|
|
11
|
+
delete
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
BOUNDARY_KEYWORDS = %w[
|
|
15
|
+
from
|
|
16
|
+
where
|
|
17
|
+
having
|
|
18
|
+
limit
|
|
19
|
+
into
|
|
20
|
+
set
|
|
21
|
+
values
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
CONTINUATION_PAIRS = {
|
|
25
|
+
"insert" => "select",
|
|
26
|
+
}.freeze
|
|
8
27
|
|
|
9
28
|
module_function
|
|
10
29
|
|
|
@@ -9,7 +9,7 @@ module SqlBeautifier
|
|
|
9
9
|
|
|
10
10
|
def self.parse(segment_text)
|
|
11
11
|
table_specification = table_specification_text(segment_text)
|
|
12
|
-
stripped_specification = table_specification.strip
|
|
12
|
+
stripped_specification, _lateral = strip_lateral_prefix(table_specification.strip)
|
|
13
13
|
|
|
14
14
|
return parse_derived_table(stripped_specification) if stripped_specification.start_with?(Constants::OPEN_PARENTHESIS)
|
|
15
15
|
|
|
@@ -52,8 +52,18 @@ module SqlBeautifier
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def self.strip_lateral_prefix(text)
|
|
56
|
+
stripped = text.strip
|
|
57
|
+
|
|
58
|
+
if stripped.match?(Constants::LATERAL_PREFIX_PATTERN)
|
|
59
|
+
[stripped.sub(Constants::LATERAL_PREFIX_PATTERN, ""), true]
|
|
60
|
+
else
|
|
61
|
+
[stripped, false]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
def self.derived_table_lookup_name_from(text)
|
|
56
|
-
stripped_text = text.strip
|
|
66
|
+
stripped_text, _lateral = strip_lateral_prefix(text.strip)
|
|
57
67
|
return unless stripped_text.start_with?(Constants::OPEN_PARENTHESIS)
|
|
58
68
|
|
|
59
69
|
closing_position = Scanner.new(stripped_text).find_matching_parenthesis(0)
|
|
@@ -212,6 +212,7 @@ module SqlBeautifier
|
|
|
212
212
|
scanner = Scanner.new(text)
|
|
213
213
|
conjunction_boundaries = []
|
|
214
214
|
inside_between = false
|
|
215
|
+
case_depth = 0
|
|
215
216
|
|
|
216
217
|
until scanner.finished?
|
|
217
218
|
next if scanner.skip_quoted_or_sentinel!
|
|
@@ -229,23 +230,37 @@ module SqlBeautifier
|
|
|
229
230
|
scanner.advance!
|
|
230
231
|
else
|
|
231
232
|
if scanner.parenthesis_depth.zero?
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if matched_conjunction == "and" && inside_between
|
|
238
|
-
inside_between = false
|
|
239
|
-
else
|
|
240
|
-
conjunction_boundaries << {
|
|
241
|
-
conjunction: matched_conjunction,
|
|
242
|
-
position: scanner.position,
|
|
243
|
-
}
|
|
244
|
-
end
|
|
233
|
+
if scanner.keyword_at?("case")
|
|
234
|
+
case_depth += 1
|
|
235
|
+
scanner.advance!("case".length)
|
|
236
|
+
next
|
|
237
|
+
end
|
|
245
238
|
|
|
246
|
-
|
|
239
|
+
if scanner.keyword_at?("end") && case_depth.positive?
|
|
240
|
+
case_depth -= 1
|
|
241
|
+
scanner.advance!("end".length)
|
|
247
242
|
next
|
|
248
243
|
end
|
|
244
|
+
|
|
245
|
+
if case_depth.zero?
|
|
246
|
+
inside_between = true if scanner.keyword_at?(Constants::BETWEEN_KEYWORD)
|
|
247
|
+
|
|
248
|
+
matched_conjunction = scanner.detect_conjunction_at
|
|
249
|
+
|
|
250
|
+
if matched_conjunction
|
|
251
|
+
if matched_conjunction == "and" && inside_between
|
|
252
|
+
inside_between = false
|
|
253
|
+
else
|
|
254
|
+
conjunction_boundaries << {
|
|
255
|
+
conjunction: matched_conjunction,
|
|
256
|
+
position: scanner.position,
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
scanner.advance!(matched_conjunction.length)
|
|
261
|
+
next
|
|
262
|
+
end
|
|
263
|
+
end
|
|
249
264
|
end
|
|
250
265
|
|
|
251
266
|
scanner.advance!
|
|
@@ -92,12 +92,15 @@ module SqlBeautifier
|
|
|
92
92
|
def render_assignments
|
|
93
93
|
items = Tokenizer.split_by_top_level_commas(@assignments)
|
|
94
94
|
continuation = Util.continuation_padding
|
|
95
|
+
keyword_width = SqlBeautifier.config_for(:keyword_column_width)
|
|
95
96
|
|
|
96
97
|
formatted_items = items.map.with_index do |item, index|
|
|
98
|
+
formatted_item = CaseExpression.format_in_text(item.strip, base_indent: keyword_width)
|
|
99
|
+
|
|
97
100
|
if index.zero?
|
|
98
|
-
"\n#{Util.keyword_padding('set')}#{
|
|
101
|
+
"\n#{Util.keyword_padding('set')}#{formatted_item}"
|
|
99
102
|
else
|
|
100
|
-
"\n#{continuation}#{
|
|
103
|
+
"\n#{continuation}#{formatted_item}"
|
|
101
104
|
end
|
|
102
105
|
end
|
|
103
106
|
|
data/lib/sql_beautifier.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "sql_beautifier/statement_splitter"
|
|
|
18
18
|
require_relative "sql_beautifier/table_reference"
|
|
19
19
|
require_relative "sql_beautifier/table_registry"
|
|
20
20
|
require_relative "sql_beautifier/join"
|
|
21
|
+
require_relative "sql_beautifier/case_expression"
|
|
21
22
|
require_relative "sql_beautifier/expression"
|
|
22
23
|
require_relative "sql_beautifier/sort_expression"
|
|
23
24
|
require_relative "sql_beautifier/condition"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sql_beautifier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kinnell Shah
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- README.md
|
|
67
67
|
- lib/sql_beautifier.rb
|
|
68
68
|
- lib/sql_beautifier/base.rb
|
|
69
|
+
- lib/sql_beautifier/case_expression.rb
|
|
69
70
|
- lib/sql_beautifier/clauses/base.rb
|
|
70
71
|
- lib/sql_beautifier/clauses/condition_clause.rb
|
|
71
72
|
- lib/sql_beautifier/clauses/from.rb
|