sql_beautifier 0.9.1 → 0.9.2
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 +8 -0
- data/README.md +17 -0
- data/lib/sql_beautifier/clauses/from.rb +3 -37
- data/lib/sql_beautifier/join.rb +5 -3
- data/lib/sql_beautifier/query.rb +2 -2
- data/lib/sql_beautifier/table_reference.rb +60 -3
- data/lib/sql_beautifier/table_registry.rb +20 -5
- data/lib/sql_beautifier/tokenizer.rb +35 -0
- data/lib/sql_beautifier/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcf549a9025b3f1baa8e3e6586154ce0e4b9529985f8e54b49ff241e4a24cd19
|
|
4
|
+
data.tar.gz: ce6eb31a10ad63436442bf0a27d4054857540a03d2b799ecf40db46246ac0852
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2bed68bdfff3e132819a44756f57da8b8e8445738d85e967ea98d108510afe4c2840f0f4a2ef592bf0cbbccac8facfaf247b31e2f8810c6e22d65c8ecee551d
|
|
7
|
+
data.tar.gz: 595b15be6f7f1a6b7541580d679ef0bde692bca70e4cffec251ea4f995aa3f54c17305b28b2a62ee0172ed40ca1137a013f4041d5ecf2624bcd22a7563a52672
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [X.X.X] - YYYY-MM-DD
|
|
4
4
|
|
|
5
|
+
## [0.9.2] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
- 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
|
|
8
|
+
- Add derived table support to `TableReference` — segments starting with `(` are parsed as derived tables, preserving the full expression and extracting the alias from text after the closing `)`
|
|
9
|
+
- Extract `find_all_top_level_join_positions` and `find_earliest_top_level_join_keyword` into `Tokenizer` for shared use by `Clauses::From` and `TableRegistry`
|
|
10
|
+
- Improve subquery indentation for `FROM`-line subqueries — derived tables now align with keyword column width, matching the existing behavior for `WHERE`-line subqueries
|
|
11
|
+
- Fix aliasless derived tables in `FROM` clauses raising `NoMethodError` or receiving malformed auto-generated aliases — lookup now falls back to the full derived-table expression when no alias is present and alias assignment skips aliasless derived tables
|
|
12
|
+
|
|
5
13
|
## [0.9.1] - 2026-03-29
|
|
6
14
|
|
|
7
15
|
## [0.9.0] - 2026-03-29
|
data/README.md
CHANGED
|
@@ -469,6 +469,23 @@ where id in (
|
|
|
469
469
|
|
|
470
470
|
Nested subqueries increase indentation at each level.
|
|
471
471
|
|
|
472
|
+
Derived tables (subqueries in `FROM` clauses) are also supported — the subquery content is recursively formatted and the alias is preserved:
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
SqlBeautifier.call("SELECT active_users.id FROM (SELECT id FROM users WHERE active = true) AS active_users")
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Produces:
|
|
479
|
+
|
|
480
|
+
```sql
|
|
481
|
+
select active_users.id
|
|
482
|
+
from (
|
|
483
|
+
select id
|
|
484
|
+
from Users u
|
|
485
|
+
where active = true
|
|
486
|
+
) active_users;
|
|
487
|
+
```
|
|
488
|
+
|
|
472
489
|
### Trailing Semicolons
|
|
473
490
|
|
|
474
491
|
By default, each formatted statement ends with a `;`:
|
|
@@ -18,7 +18,8 @@ module SqlBeautifier
|
|
|
18
18
|
def call
|
|
19
19
|
join_parts = split_join_parts
|
|
20
20
|
primary_table_text = join_parts.shift.strip
|
|
21
|
-
|
|
21
|
+
primary_lookup_name = TableReference.derived_table_lookup_name_from(primary_table_text) || Util.first_word(primary_table_text)
|
|
22
|
+
primary_reference = @table_registry.reference_for(primary_lookup_name)
|
|
22
23
|
trailing_sentinels = Join.extract_trailing_sentinels(primary_table_text)
|
|
23
24
|
|
|
24
25
|
lines = []
|
|
@@ -42,7 +43,7 @@ module SqlBeautifier
|
|
|
42
43
|
|
|
43
44
|
def split_join_parts
|
|
44
45
|
from_content = @value.strip
|
|
45
|
-
join_keyword_positions =
|
|
46
|
+
join_keyword_positions = Tokenizer.find_all_top_level_join_positions(from_content)
|
|
46
47
|
|
|
47
48
|
return [from_content] if join_keyword_positions.empty?
|
|
48
49
|
|
|
@@ -62,41 +63,6 @@ module SqlBeautifier
|
|
|
62
63
|
|
|
63
64
|
parts
|
|
64
65
|
end
|
|
65
|
-
|
|
66
|
-
def find_all_join_keyword_positions(text)
|
|
67
|
-
positions = []
|
|
68
|
-
search_offset = 0
|
|
69
|
-
|
|
70
|
-
while search_offset < text.length
|
|
71
|
-
earliest_match = find_earliest_join_keyword(text, search_offset)
|
|
72
|
-
break unless earliest_match
|
|
73
|
-
|
|
74
|
-
positions << earliest_match
|
|
75
|
-
search_offset = earliest_match[:position] + earliest_match[:keyword].length
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
positions
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def find_earliest_join_keyword(text, search_offset)
|
|
82
|
-
earliest_match = nil
|
|
83
|
-
|
|
84
|
-
Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
|
|
85
|
-
remaining_text = text[search_offset..]
|
|
86
|
-
keyword_position = Tokenizer.find_top_level_keyword(remaining_text, keyword)
|
|
87
|
-
next unless keyword_position
|
|
88
|
-
|
|
89
|
-
absolute_position = search_offset + keyword_position
|
|
90
|
-
next if earliest_match && absolute_position >= earliest_match[:position]
|
|
91
|
-
|
|
92
|
-
earliest_match = {
|
|
93
|
-
position: absolute_position,
|
|
94
|
-
keyword: keyword,
|
|
95
|
-
}
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
earliest_match
|
|
99
|
-
end
|
|
100
66
|
end
|
|
101
67
|
end
|
|
102
68
|
end
|
data/lib/sql_beautifier/join.rb
CHANGED
|
@@ -22,11 +22,13 @@ module SqlBeautifier
|
|
|
22
22
|
conditions = []
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
table_lookup_name = TableReference.derived_table_lookup_name_from(table_text) || Util.first_word(table_text)
|
|
26
|
+
table_reference = table_registry.reference_for(table_lookup_name)
|
|
27
|
+
return unless table_reference
|
|
28
|
+
|
|
27
29
|
trailing_sentinels = extract_trailing_sentinels(table_text)
|
|
28
30
|
|
|
29
|
-
new(keyword: keyword, table_reference:
|
|
31
|
+
new(keyword: keyword, table_reference: table_reference, trailing_sentinels: trailing_sentinels, conditions: conditions)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def self.extract_keyword(join_text)
|
data/lib/sql_beautifier/query.rb
CHANGED
|
@@ -11,7 +11,7 @@ module SqlBeautifier
|
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
13
|
LEADING_WHITESPACE_PATTERN = %r{\A[[:space:]]*}
|
|
14
|
-
|
|
14
|
+
CLAUSE_KEYWORD_PREFIX_PATTERN = %r{\A(?:where|from)(?:[[:space:]]|$)}i
|
|
15
15
|
|
|
16
16
|
attr_reader :clauses
|
|
17
17
|
attr_reader :depth
|
|
@@ -88,7 +88,7 @@ module SqlBeautifier
|
|
|
88
88
|
line_before_subquery = text[line_start_position...subquery_position]
|
|
89
89
|
line_leading_spaces = line_before_subquery[LEADING_WHITESPACE_PATTERN].to_s.length
|
|
90
90
|
|
|
91
|
-
return default_base_indent unless line_before_subquery.lstrip.match?(
|
|
91
|
+
return default_base_indent unless line_before_subquery.lstrip.match?(CLAUSE_KEYWORD_PREFIX_PATTERN)
|
|
92
92
|
|
|
93
93
|
default_base_indent + line_leading_spaces + SqlBeautifier.config_for(:keyword_column_width)
|
|
94
94
|
end
|
|
@@ -5,13 +5,33 @@ module SqlBeautifier
|
|
|
5
5
|
option :name
|
|
6
6
|
option :explicit_alias, default: -> {}
|
|
7
7
|
option :assigned_alias, default: -> {}
|
|
8
|
+
option :derived_table_expression, default: -> {}
|
|
8
9
|
|
|
9
10
|
def self.parse(segment_text)
|
|
10
11
|
table_specification = table_specification_text(segment_text)
|
|
11
|
-
|
|
12
|
+
stripped_specification = table_specification.strip
|
|
13
|
+
|
|
14
|
+
return parse_derived_table(stripped_specification) if stripped_specification.start_with?(Constants::OPEN_PARENTHESIS)
|
|
15
|
+
|
|
16
|
+
table_name = Util.first_word(stripped_specification)
|
|
12
17
|
return unless table_name
|
|
13
18
|
|
|
14
|
-
new(name: table_name, explicit_alias: extract_explicit_alias(
|
|
19
|
+
new(name: table_name, explicit_alias: extract_explicit_alias(stripped_specification))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.parse_derived_table(table_specification)
|
|
23
|
+
closing_position = Scanner.new(table_specification).find_matching_parenthesis(0)
|
|
24
|
+
return unless closing_position
|
|
25
|
+
|
|
26
|
+
expression = table_specification[0..closing_position]
|
|
27
|
+
remaining = table_specification[(closing_position + 1)..].strip
|
|
28
|
+
derived_alias = extract_derived_table_alias(remaining)
|
|
29
|
+
|
|
30
|
+
new(
|
|
31
|
+
name: derived_alias || expression,
|
|
32
|
+
explicit_alias: derived_alias,
|
|
33
|
+
derived_table_expression: expression
|
|
34
|
+
)
|
|
15
35
|
end
|
|
16
36
|
|
|
17
37
|
def self.table_specification_text(segment_text)
|
|
@@ -32,6 +52,36 @@ module SqlBeautifier
|
|
|
32
52
|
end
|
|
33
53
|
end
|
|
34
54
|
|
|
55
|
+
def self.derived_table_lookup_name_from(text)
|
|
56
|
+
stripped_text = text.strip
|
|
57
|
+
return unless stripped_text.start_with?(Constants::OPEN_PARENTHESIS)
|
|
58
|
+
|
|
59
|
+
closing_position = Scanner.new(stripped_text).find_matching_parenthesis(0)
|
|
60
|
+
return unless closing_position
|
|
61
|
+
|
|
62
|
+
expression = stripped_text[0..closing_position]
|
|
63
|
+
remaining_text = stripped_text[(closing_position + 1)..].strip
|
|
64
|
+
derived_alias = extract_derived_table_alias(remaining_text)
|
|
65
|
+
|
|
66
|
+
derived_alias || expression
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.extract_derived_table_alias(remaining_text)
|
|
70
|
+
return if remaining_text.empty?
|
|
71
|
+
|
|
72
|
+
words = remaining_text.split(Constants::WHITESPACE_REGEX)
|
|
73
|
+
|
|
74
|
+
if words.first&.downcase == "as"
|
|
75
|
+
words[1]
|
|
76
|
+
else
|
|
77
|
+
words.first
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def derived_table?
|
|
82
|
+
@derived_table_expression.present?
|
|
83
|
+
end
|
|
84
|
+
|
|
35
85
|
def formatted_name
|
|
36
86
|
Util.format_table_name(@name)
|
|
37
87
|
end
|
|
@@ -45,7 +95,14 @@ module SqlBeautifier
|
|
|
45
95
|
end
|
|
46
96
|
|
|
47
97
|
def render(trailing_sentinels: nil)
|
|
48
|
-
formatted =
|
|
98
|
+
formatted = begin
|
|
99
|
+
if derived_table?
|
|
100
|
+
alias_name ? "#{@derived_table_expression} #{alias_name}" : @derived_table_expression
|
|
101
|
+
else
|
|
102
|
+
alias_name ? "#{formatted_name} #{alias_name}" : formatted_name
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
49
106
|
trailing_sentinels&.any? ? "#{formatted} #{trailing_sentinels.join(' ')}" : formatted
|
|
50
107
|
end
|
|
51
108
|
end
|
|
@@ -79,6 +79,11 @@ module SqlBeautifier
|
|
|
79
79
|
next
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
if reference.derived_table?
|
|
83
|
+
@references_by_name[reference.name] = reference
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
82
87
|
initials = table_initials(reference.name)
|
|
83
88
|
duplicate_initials_counts[initials] += 1 if initials_occurrence_counts[initials] > 1
|
|
84
89
|
|
|
@@ -109,7 +114,7 @@ module SqlBeautifier
|
|
|
109
114
|
occurrence_counts = Hash.new(0)
|
|
110
115
|
|
|
111
116
|
@references.each do |reference|
|
|
112
|
-
next if reference.explicit_alias
|
|
117
|
+
next if reference.explicit_alias || reference.derived_table?
|
|
113
118
|
|
|
114
119
|
occurrence_counts[table_initials(reference.name)] += 1
|
|
115
120
|
end
|
|
@@ -136,16 +141,26 @@ module SqlBeautifier
|
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
def parse_references(from_content)
|
|
139
|
-
|
|
144
|
+
from_text = from_content.strip
|
|
145
|
+
join_positions = Tokenizer.find_all_top_level_join_positions(from_text)
|
|
140
146
|
|
|
141
147
|
references = []
|
|
142
148
|
|
|
143
|
-
|
|
149
|
+
primary_end = join_positions.any? ? join_positions.first[:position] : from_text.length
|
|
150
|
+
primary_segment = from_text[0...primary_end].strip
|
|
144
151
|
references << TableReference.parse(primary_segment)
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
join_positions.each_with_index do |join_info, index|
|
|
154
|
+
content_start = join_info[:position] + join_info[:keyword].length
|
|
155
|
+
content_end = begin
|
|
156
|
+
if index + 1 < join_positions.length
|
|
157
|
+
join_positions[index + 1][:position]
|
|
158
|
+
else
|
|
159
|
+
from_text.length
|
|
160
|
+
end
|
|
161
|
+
end
|
|
148
162
|
|
|
163
|
+
join_content = from_text[content_start...content_end].strip
|
|
149
164
|
references << TableReference.parse(join_content)
|
|
150
165
|
end
|
|
151
166
|
|
|
@@ -173,6 +173,41 @@ module SqlBeautifier
|
|
|
173
173
|
scanner.top_level?
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def find_all_top_level_join_positions(text)
|
|
177
|
+
positions = []
|
|
178
|
+
search_offset = 0
|
|
179
|
+
|
|
180
|
+
while search_offset < text.length
|
|
181
|
+
earliest_match = find_earliest_top_level_join_keyword(text, search_offset)
|
|
182
|
+
break unless earliest_match
|
|
183
|
+
|
|
184
|
+
positions << earliest_match
|
|
185
|
+
search_offset = earliest_match[:position] + earliest_match[:keyword].length
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
positions
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def find_earliest_top_level_join_keyword(text, search_offset)
|
|
192
|
+
earliest_match = nil
|
|
193
|
+
|
|
194
|
+
Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
|
|
195
|
+
remaining_text = text[search_offset..]
|
|
196
|
+
keyword_position = find_top_level_keyword(remaining_text, keyword)
|
|
197
|
+
next unless keyword_position
|
|
198
|
+
|
|
199
|
+
absolute_position = search_offset + keyword_position
|
|
200
|
+
next if earliest_match && absolute_position >= earliest_match[:position]
|
|
201
|
+
|
|
202
|
+
earliest_match = {
|
|
203
|
+
position: absolute_position,
|
|
204
|
+
keyword: keyword,
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
earliest_match
|
|
209
|
+
end
|
|
210
|
+
|
|
176
211
|
def scan_top_level_conjunctions(text)
|
|
177
212
|
scanner = Scanner.new(text)
|
|
178
213
|
conjunction_boundaries = []
|