sql_beautifier 0.10.3 → 0.10.4
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 +5 -0
- data/README.md +35 -0
- data/lib/sql_beautifier/clauses/condition_clause.rb +5 -1
- data/lib/sql_beautifier/condition.rb +25 -2
- data/lib/sql_beautifier/dml_rendering.rb +2 -1
- data/lib/sql_beautifier/in_list.rb +107 -0
- 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: 7c92d6b96851b3e1d24b983a6cf8d3d01ee643f4418d7c66cdc92bdc32baa132
|
|
4
|
+
data.tar.gz: 8cf024876d629ff3a2beaae6604c4cc860a6652582b90bf1f3c6c4001acc3517
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0252dddc816463054f4bcd225485cadb58eac11d964f6fd1f18e1d5fa3b23c2619caa0dc38d8f3395f7616fcf5e0e7d5d52e60bfca4f7d96b83fbfec7d345f19
|
|
7
|
+
data.tar.gz: 6dfa3284884d2c4df58d93dab99b31dcebce6e07f00a71d499a71bd37f0030a680f905ca15565b23e97f860b53a108feb23d5a3d004e2967c328eace9ff51bfe
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [X.X.X] - YYYY-MM-DD
|
|
4
4
|
|
|
5
|
+
## [0.10.4] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
- Add `IN` list multiline formatting — `WHERE x IN (1, 2, 3)` value lists with 2+ items are now expanded to one item per line with proper indentation; single-item lists and `IN (SELECT ...)` subqueries are left unchanged
|
|
8
|
+
- Fix redundant parentheses after `NOT` not being stripped — `NOT ((a = 1 OR b = 2))` is now reduced to `NOT (a = 1 OR b = 2)`, removing unnecessary doubled parentheses while preserving semantically required grouping
|
|
9
|
+
|
|
5
10
|
## [0.10.3] - 2026-03-30
|
|
6
11
|
|
|
7
12
|
- Add `DROP TABLE` formatting — `DROP TABLE [IF EXISTS] table_name` statements are now recognized and rendered with proper keyword casing and PascalCase table names instead of passing through as normalized text
|
data/README.md
CHANGED
|
@@ -188,6 +188,41 @@ where active = true
|
|
|
188
188
|
);
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
`IN` value lists with multiple items are expanded to one item per line. Single-item lists and `IN (SELECT ...)` subqueries are left inline:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
SqlBeautifier.call("SELECT id FROM users WHERE status IN ('active', 'pending', 'banned')")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Produces:
|
|
198
|
+
|
|
199
|
+
```sql
|
|
200
|
+
select id
|
|
201
|
+
from Users u
|
|
202
|
+
where status in (
|
|
203
|
+
'active',
|
|
204
|
+
'pending',
|
|
205
|
+
'banned'
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Redundant parentheses are removed, including after `NOT`:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
SqlBeautifier.call("SELECT id FROM users WHERE NOT ((active = true OR role = 'guest')) AND verified = true")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Produces:
|
|
216
|
+
|
|
217
|
+
```sql
|
|
218
|
+
select id
|
|
219
|
+
|
|
220
|
+
from Users u
|
|
221
|
+
|
|
222
|
+
where not (active = true or role = 'guest')
|
|
223
|
+
and verified = true;
|
|
224
|
+
```
|
|
225
|
+
|
|
191
226
|
### GROUP BY and HAVING
|
|
192
227
|
|
|
193
228
|
```ruby
|
|
@@ -7,8 +7,12 @@ module SqlBeautifier
|
|
|
7
7
|
keyword_width = SqlBeautifier.config_for(:keyword_column_width)
|
|
8
8
|
formatted_value = CaseExpression.format_in_text(@value, base_indent: keyword_width)
|
|
9
9
|
unwrapped_value = strip_wrapping_parentheses(formatted_value)
|
|
10
|
+
unwrapped_value = Condition.unwrap_not_prefix_parens(unwrapped_value)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
unless multiple_conditions?(unwrapped_value)
|
|
13
|
+
formatted_leaf = InList.format_in_text(unwrapped_value, base_indent: keyword_width)
|
|
14
|
+
return "#{keyword_prefix}#{formatted_leaf}"
|
|
15
|
+
end
|
|
12
16
|
|
|
13
17
|
formatted_conditions = Condition.format(unwrapped_value, indent_width: keyword_width)
|
|
14
18
|
formatted_conditions.sub(continuation_indent, keyword_prefix)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
class Condition < Base
|
|
5
|
+
NOT_PREFIX_PATTERN = %r{\Anot([[:space:]]+)}i
|
|
6
|
+
|
|
5
7
|
option :conjunction, default: -> {}
|
|
6
8
|
option :expression, default: -> {}
|
|
7
9
|
option :children, default: -> {}
|
|
@@ -52,7 +54,7 @@ module SqlBeautifier
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def render(indent_width:)
|
|
55
|
-
return @expression if leaf?
|
|
57
|
+
return InList.format_in_text(@expression, base_indent: indent_width) if leaf?
|
|
56
58
|
|
|
57
59
|
inline_version = render_inline
|
|
58
60
|
return inline_version if inline_version.length <= SqlBeautifier.config_for(:inline_group_threshold)
|
|
@@ -89,7 +91,28 @@ module SqlBeautifier
|
|
|
89
91
|
output = inner_content
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
output
|
|
94
|
+
unwrap_not_prefix_parens(output)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.unwrap_not_prefix_parens(text)
|
|
98
|
+
prefix_match = text.match(NOT_PREFIX_PATTERN)
|
|
99
|
+
return text unless prefix_match
|
|
100
|
+
|
|
101
|
+
remainder = text[prefix_match[0].length..]
|
|
102
|
+
|
|
103
|
+
unwrapped_remainder = remainder
|
|
104
|
+
|
|
105
|
+
while Tokenizer.outer_parentheses_wrap_all?(unwrapped_remainder)
|
|
106
|
+
inner_content = Util.strip_outer_parentheses(unwrapped_remainder)
|
|
107
|
+
inner_conditions = Tokenizer.split_top_level_conditions(inner_content)
|
|
108
|
+
break if inner_conditions.length > 1
|
|
109
|
+
|
|
110
|
+
unwrapped_remainder = inner_content
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return text if unwrapped_remainder == remainder
|
|
114
|
+
|
|
115
|
+
"not #{unwrapped_remainder}"
|
|
93
116
|
end
|
|
94
117
|
|
|
95
118
|
def self.parse_condition_group(condition_text)
|
|
@@ -10,7 +10,8 @@ module SqlBeautifier
|
|
|
10
10
|
|
|
11
11
|
formatted_where_text = begin
|
|
12
12
|
if conditions.length <= 1 && conditions.first&.leaf?
|
|
13
|
-
|
|
13
|
+
formatted_expression = InList.format_in_text(conditions.first.expression, base_indent: keyword_column_width)
|
|
14
|
+
"\n#{Util.keyword_padding('where')}#{formatted_expression}"
|
|
14
15
|
else
|
|
15
16
|
formatted_conditions = Condition.render_all(conditions, indent_width: keyword_column_width)
|
|
16
17
|
"\n#{formatted_conditions.sub(Util.continuation_padding, Util.keyword_padding('where'))}"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
module InList
|
|
5
|
+
IN_KEYWORD = "in"
|
|
6
|
+
NOT_IN_KEYWORD = "not in"
|
|
7
|
+
SUBQUERY_PATTERN = %r{\A[[:space:]]*select(?:[[:space:]]|\()}i
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def format_in_text(text, base_indent: 0)
|
|
12
|
+
output = +""
|
|
13
|
+
scanner = Scanner.new(text)
|
|
14
|
+
|
|
15
|
+
while scanner.position < text.length
|
|
16
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
17
|
+
if consumed
|
|
18
|
+
output << consumed
|
|
19
|
+
next
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
23
|
+
scanner.increment_depth!
|
|
24
|
+
output << scanner.current_char
|
|
25
|
+
scanner.advance!
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
30
|
+
scanner.decrement_depth!
|
|
31
|
+
output << scanner.current_char
|
|
32
|
+
scanner.advance!
|
|
33
|
+
next
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if scanner.parenthesis_depth.zero? && in_keyword_at?(scanner)
|
|
37
|
+
keyword_length = in_keyword_length_at(scanner)
|
|
38
|
+
parenthesis_position = find_opening_parenthesis(text, scanner.position + keyword_length)
|
|
39
|
+
|
|
40
|
+
if parenthesis_position
|
|
41
|
+
expansion = format_in_list(text, keyword_position: scanner.position, parenthesis_position: parenthesis_position, base_indent: base_indent)
|
|
42
|
+
|
|
43
|
+
if expansion
|
|
44
|
+
output << expansion[:text]
|
|
45
|
+
scanner.advance!(expansion[:consumed] - scanner.position)
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
output << scanner.current_char
|
|
52
|
+
scanner.advance!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
output
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def in_keyword_at?(scanner)
|
|
59
|
+
scanner.keyword_at?(NOT_IN_KEYWORD) || scanner.keyword_at?(IN_KEYWORD)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def in_keyword_length_at(scanner)
|
|
63
|
+
scanner.keyword_at?(NOT_IN_KEYWORD) ? NOT_IN_KEYWORD.length : IN_KEYWORD.length
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def find_opening_parenthesis(text, from_position)
|
|
67
|
+
position = from_position
|
|
68
|
+
|
|
69
|
+
while position < text.length
|
|
70
|
+
return position if text[position] == Constants::OPEN_PARENTHESIS
|
|
71
|
+
return nil unless text[position] =~ Constants::WHITESPACE_CHARACTER_REGEX
|
|
72
|
+
|
|
73
|
+
position += 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def format_in_list(text, keyword_position:, parenthesis_position:, base_indent:)
|
|
80
|
+
closing_position = Scanner.new(text).find_matching_parenthesis(parenthesis_position)
|
|
81
|
+
return nil unless closing_position
|
|
82
|
+
|
|
83
|
+
inner_text = text[(parenthesis_position + 1)...closing_position]
|
|
84
|
+
return nil if inner_text.match?(SUBQUERY_PATTERN)
|
|
85
|
+
|
|
86
|
+
items = Tokenizer.split_by_top_level_commas(inner_text)
|
|
87
|
+
return nil if items.length <= 1
|
|
88
|
+
|
|
89
|
+
keyword_text = text[keyword_position...(parenthesis_position + 1)]
|
|
90
|
+
indent_spaces = SqlBeautifier.config_for(:indent_spaces)
|
|
91
|
+
item_indent = Util.whitespace(base_indent + indent_spaces)
|
|
92
|
+
closing_indent = Util.whitespace(base_indent)
|
|
93
|
+
|
|
94
|
+
formatted_items = items.map.with_index do |item, index|
|
|
95
|
+
trailing_comma = index < items.length - 1 ? "," : ""
|
|
96
|
+
"#{item_indent}#{item}#{trailing_comma}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
text: "#{keyword_text}\n#{formatted_items.join("\n")}\n#{closing_indent})",
|
|
101
|
+
consumed: closing_position + 1,
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private_class_method :in_keyword_at?, :in_keyword_length_at, :find_opening_parenthesis, :format_in_list
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/sql_beautifier.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "sql_beautifier/table_reference"
|
|
|
19
19
|
require_relative "sql_beautifier/table_registry"
|
|
20
20
|
require_relative "sql_beautifier/join"
|
|
21
21
|
require_relative "sql_beautifier/case_expression"
|
|
22
|
+
require_relative "sql_beautifier/in_list"
|
|
22
23
|
require_relative "sql_beautifier/expression"
|
|
23
24
|
require_relative "sql_beautifier/sort_expression"
|
|
24
25
|
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.10.
|
|
4
|
+
version: 0.10.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kinnell Shah
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- lib/sql_beautifier/drop_table.rb
|
|
93
93
|
- lib/sql_beautifier/expression.rb
|
|
94
94
|
- lib/sql_beautifier/formatter.rb
|
|
95
|
+
- lib/sql_beautifier/in_list.rb
|
|
95
96
|
- lib/sql_beautifier/insert_query.rb
|
|
96
97
|
- lib/sql_beautifier/join.rb
|
|
97
98
|
- lib/sql_beautifier/normalizer.rb
|