sql_beautifier 0.1.4 → 0.3.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 +18 -0
- data/README.md +224 -10
- data/lib/sql_beautifier/clauses/base.rb +10 -0
- data/lib/sql_beautifier/clauses/condition_clause.rb +20 -0
- data/lib/sql_beautifier/clauses/from.rb +139 -2
- data/lib/sql_beautifier/clauses/group_by.rb +2 -2
- data/lib/sql_beautifier/clauses/having.rb +2 -6
- data/lib/sql_beautifier/clauses/limit.rb +8 -2
- data/lib/sql_beautifier/clauses/order_by.rb +2 -2
- data/lib/sql_beautifier/clauses/select.rb +60 -6
- data/lib/sql_beautifier/clauses/where.rb +2 -6
- data/lib/sql_beautifier/condition_formatter.rb +126 -0
- data/lib/sql_beautifier/configuration.rb +33 -0
- data/lib/sql_beautifier/constants.rb +27 -0
- data/lib/sql_beautifier/formatter.rb +57 -3
- data/lib/sql_beautifier/normalizer.rb +84 -15
- data/lib/sql_beautifier/subquery_formatter.rb +113 -0
- data/lib/sql_beautifier/table_registry.rb +229 -0
- data/lib/sql_beautifier/tokenizer.rb +223 -20
- data/lib/sql_beautifier/util.rb +67 -0
- data/lib/sql_beautifier/version.rb +1 -1
- data/lib/sql_beautifier.rb +23 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6df1f55c9242793101e9f562329763541d56aa4f17f7e3d81a12038ab5b53cca
|
|
4
|
+
data.tar.gz: 43fea825ac0c4bded07098814a2ed67e5503e1829f3d47cc99a7c989ec0e69d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d56c72c27d4b71781be0ee83ba44fb3a4e466567c5edbde267c2cfbeb245c81e78bc52a309d27e7dae69a2a7e3cbf1bb3ce55926a266a6c914e822140b6650b7
|
|
7
|
+
data.tar.gz: 9e2e5118b5b12e52551300e2c0219c6cc42907fdb87fd56a8e6e80e193b212ae36ff87ee48d2a0135dc0140121d145c3f104622a0efbd0a3bbeffe10af378cea
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [X.X.X] - YYYY-MM-DD
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
- Add configuration system with `SqlBeautifier.configure` block and `SqlBeautifier.reset_configuration!`
|
|
8
|
+
- Add configurable keyword case (`:lower` / `:upper`), keyword column width, indent spaces, table name format (`:pascal_case` / `:lowercase`), inline group threshold, and alias strategy (`:initials` / `:none` / callable)
|
|
9
|
+
- Add semicolon stripping in normalizer (trailing `;` removed before formatting)
|
|
10
|
+
- Add comment stripping in normalizer (`--` line comments and `/* */` block comments, string-aware)
|
|
11
|
+
- Add subquery formatting with recursive indentation (`(select ...)` expanded to multiline)
|
|
12
|
+
|
|
13
|
+
## [0.2.0] - 2026-03-27
|
|
14
|
+
|
|
15
|
+
- Add JOIN support (inner, left, right, full outer, cross) with formatted continuation lines
|
|
16
|
+
- Add automatic table aliasing using initials (e.g. `users` → `u`, `active_storage_blobs` → `asb`)
|
|
17
|
+
- Add PascalCase table name formatting (e.g. `users` → `Users`, `user_sessions` → `User_Sessions`)
|
|
18
|
+
- Add `table.column` → `alias.column` replacement across full output
|
|
19
|
+
- Add DISTINCT and DISTINCT ON support in SELECT clause
|
|
20
|
+
- Add AND/OR condition formatting in WHERE and HAVING clauses with per-line indentation
|
|
21
|
+
- Add parenthesized condition group handling (inline when short, expanded when long)
|
|
22
|
+
|
|
5
23
|
## [0.1.4] - 2026-03-26
|
|
6
24
|
|
|
7
25
|
- Update `bin/ci` and `bin/release` to use new formatting functions
|
data/README.md
CHANGED
|
@@ -41,14 +41,149 @@ select id,
|
|
|
41
41
|
name,
|
|
42
42
|
email
|
|
43
43
|
|
|
44
|
-
from
|
|
44
|
+
from Users u
|
|
45
45
|
|
|
46
46
|
where active = true
|
|
47
47
|
|
|
48
48
|
order by name
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Single-word keywords are lowercased and padded so their clause bodies start at an 8-character column. Multi-word clauses such as `order by` and `group by`, and short clauses like `limit`, use a single space between the keyword and the clause body instead of padding.
|
|
51
|
+
Single-word keywords are lowercased and padded so their clause bodies start at an 8-character column. Multi-word clauses such as `order by` and `group by`, and short clauses like `limit`, use a single space between the keyword and the clause body instead of padding. Clause spacing is compact by default for simple one-column / one-table / one-condition queries, and otherwise uses blank lines between top-level clauses. Multi-column SELECT lists place each column on its own line with continuation indentation. Table names are PascalCased and automatically aliased.
|
|
52
|
+
|
|
53
|
+
### Table Aliasing
|
|
54
|
+
|
|
55
|
+
Tables are automatically aliased using their initials. Underscore-separated table names use the first letter of each segment:
|
|
56
|
+
|
|
57
|
+
| Table Name | PascalCase | Alias |
|
|
58
|
+
| -------------------------- | -------------------------- | ----- |
|
|
59
|
+
| `users` | `Users` | `u` |
|
|
60
|
+
| `active_storage_blobs` | `Active_Storage_Blobs` | `asb` |
|
|
61
|
+
| `person_event_invitations` | `Person_Event_Invitations` | `pei` |
|
|
62
|
+
|
|
63
|
+
All `table.column` references throughout the query are replaced with `alias.column`:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
SqlBeautifier.call("SELECT users.id, users.name FROM users WHERE users.active = true")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Produces:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
select u.id,
|
|
73
|
+
u.name
|
|
74
|
+
|
|
75
|
+
from Users u
|
|
76
|
+
|
|
77
|
+
where u.active = true
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
When two tables produce the same initials, a counter is appended for disambiguation (e.g. `u1`, `u2`).
|
|
81
|
+
|
|
82
|
+
### JOINs
|
|
83
|
+
|
|
84
|
+
JOIN clauses are formatted on continuation-indented lines with PascalCase table names and aliases. Multi-condition JOINs place additional conditions on further-indented lines:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
SqlBeautifier.call(<<~SQL)
|
|
88
|
+
SELECT users.id, orders.total, products.name
|
|
89
|
+
FROM users
|
|
90
|
+
INNER JOIN orders ON orders.user_id = users.id
|
|
91
|
+
INNER JOIN products ON products.id = orders.product_id
|
|
92
|
+
WHERE users.active = true AND orders.total > 100
|
|
93
|
+
ORDER BY orders.total DESC
|
|
94
|
+
SQL
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Produces:
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
select u.id,
|
|
101
|
+
o.total,
|
|
102
|
+
p.name
|
|
103
|
+
|
|
104
|
+
from Users u
|
|
105
|
+
inner join Orders o on o.user_id = u.id
|
|
106
|
+
inner join Products p on p.id = o.product_id
|
|
107
|
+
|
|
108
|
+
where u.active = true
|
|
109
|
+
and o.total > 100
|
|
110
|
+
|
|
111
|
+
order by o.total desc
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Supported join types: `inner join`, `left join`, `right join`, `full join`, `left outer join`, `right outer join`, `full outer join`, `cross join`.
|
|
115
|
+
|
|
116
|
+
### DISTINCT and DISTINCT ON
|
|
117
|
+
|
|
118
|
+
`DISTINCT` is placed on the `select` line as a modifier, with columns on continuation lines:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
SqlBeautifier.call("SELECT DISTINCT id, name, email FROM users")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Produces:
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
select distinct
|
|
128
|
+
id,
|
|
129
|
+
name,
|
|
130
|
+
email
|
|
131
|
+
|
|
132
|
+
from Users u
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`DISTINCT ON` preserves the full expression:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
SqlBeautifier.call("SELECT DISTINCT ON (user_id) id, name FROM events")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Produces:
|
|
142
|
+
|
|
143
|
+
```sql
|
|
144
|
+
select distinct on (user_id)
|
|
145
|
+
id,
|
|
146
|
+
name
|
|
147
|
+
|
|
148
|
+
from Events e
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### WHERE and HAVING Conditions
|
|
152
|
+
|
|
153
|
+
Multiple conditions in WHERE and HAVING clauses are formatted with each condition on its own line:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
SqlBeautifier.call("SELECT * FROM users WHERE active = true AND role = 'admin' AND created_at > '2024-01-01'")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Produces:
|
|
160
|
+
|
|
161
|
+
```sql
|
|
162
|
+
select *
|
|
163
|
+
|
|
164
|
+
from Users u
|
|
165
|
+
|
|
166
|
+
where active = true
|
|
167
|
+
and role = 'admin'
|
|
168
|
+
and created_at > '2024-01-01'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Short parenthesized groups stay inline:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
SqlBeautifier.call("SELECT * FROM users WHERE active = true AND (role = 'admin' OR role = 'moderator')")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Produces:
|
|
178
|
+
|
|
179
|
+
```sql
|
|
180
|
+
select *
|
|
181
|
+
|
|
182
|
+
from Users u
|
|
183
|
+
|
|
184
|
+
where active = true
|
|
185
|
+
and (role = 'admin' or role = 'moderator')
|
|
186
|
+
```
|
|
52
187
|
|
|
53
188
|
### GROUP BY and HAVING
|
|
54
189
|
|
|
@@ -67,7 +202,7 @@ Produces:
|
|
|
67
202
|
select status,
|
|
68
203
|
count(*)
|
|
69
204
|
|
|
70
|
-
from
|
|
205
|
+
from Users u
|
|
71
206
|
|
|
72
207
|
group by status
|
|
73
208
|
|
|
@@ -84,11 +219,8 @@ Produces:
|
|
|
84
219
|
|
|
85
220
|
```sql
|
|
86
221
|
select id
|
|
87
|
-
|
|
88
|
-
from users
|
|
89
|
-
|
|
222
|
+
from Users u
|
|
90
223
|
order by created_at desc
|
|
91
|
-
|
|
92
224
|
limit 25
|
|
93
225
|
```
|
|
94
226
|
|
|
@@ -105,9 +237,10 @@ Produces:
|
|
|
105
237
|
```sql
|
|
106
238
|
select *
|
|
107
239
|
|
|
108
|
-
from
|
|
240
|
+
from Users u
|
|
109
241
|
|
|
110
|
-
where name = 'O''Brien'
|
|
242
|
+
where name = 'O''Brien'
|
|
243
|
+
and status = 'Active'
|
|
111
244
|
```
|
|
112
245
|
|
|
113
246
|
### Double-Quoted Identifiers
|
|
@@ -124,7 +257,88 @@ Produces:
|
|
|
124
257
|
select user_id,
|
|
125
258
|
full_name
|
|
126
259
|
|
|
127
|
-
from
|
|
260
|
+
from Users u
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Subqueries
|
|
264
|
+
|
|
265
|
+
Subqueries are automatically detected and recursively formatted with indentation:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
SqlBeautifier.call("SELECT id FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100)")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Produces:
|
|
272
|
+
|
|
273
|
+
```sql
|
|
274
|
+
select id
|
|
275
|
+
from Users u
|
|
276
|
+
where id in (
|
|
277
|
+
select user_id
|
|
278
|
+
from Orders o
|
|
279
|
+
where total > 100
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Nested subqueries increase indentation at each level.
|
|
284
|
+
|
|
285
|
+
### Comments and Semicolons
|
|
286
|
+
|
|
287
|
+
SQL comments (`--` line comments and `/* */` block comments) and trailing semicolons are automatically stripped during normalization. Comments inside string literals are preserved:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
SqlBeautifier.call("SELECT id /* primary key */ FROM users -- main table\nWHERE active = true;")
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Produces:
|
|
294
|
+
|
|
295
|
+
```sql
|
|
296
|
+
select id
|
|
297
|
+
from Users u
|
|
298
|
+
where active = true
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Configuration
|
|
302
|
+
|
|
303
|
+
Customize formatting behavior with `SqlBeautifier.configure`:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
SqlBeautifier.configure do |config|
|
|
307
|
+
config.keyword_case = :upper # :lower (default), :upper
|
|
308
|
+
config.keyword_column_width = 10 # default: 8
|
|
309
|
+
config.indent_spaces = 4 # default: 4
|
|
310
|
+
config.clause_spacing_mode = :spacious # :compact (default), :spacious
|
|
311
|
+
config.table_name_format = :lowercase # :pascal_case (default), :lowercase
|
|
312
|
+
config.inline_group_threshold = 80 # default: 100
|
|
313
|
+
config.alias_strategy = :none # :initials (default), :none, or a callable
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Clause Spacing Modes
|
|
318
|
+
|
|
319
|
+
- `:compact` (default) keeps top-level clauses on single newlines only when the query is simple:
|
|
320
|
+
- exactly one SELECT column
|
|
321
|
+
- exactly one FROM table (no JOINs)
|
|
322
|
+
- zero or one top-level WHERE condition
|
|
323
|
+
- only `select`, `from`, optional `where`, optional `order by`, and optional `limit`
|
|
324
|
+
- `:spacious` always separates top-level clauses with blank lines
|
|
325
|
+
|
|
326
|
+
Reset to defaults:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
SqlBeautifier.reset_configuration!
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### Alias Strategies
|
|
333
|
+
|
|
334
|
+
- `:initials` (default) — automatic aliases using table initials (`users` → `u`, `active_storage_blobs` → `asb`)
|
|
335
|
+
- `:none` — no automatic aliases (explicit aliases in the SQL are still preserved)
|
|
336
|
+
- Callable — provide a proc/lambda for custom alias generation:
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
SqlBeautifier.configure do |config|
|
|
340
|
+
config.alias_strategy = ->(table_name) { "t_#{table_name[0..2]}" }
|
|
341
|
+
end
|
|
128
342
|
```
|
|
129
343
|
|
|
130
344
|
### Callable Interface
|
|
@@ -10,6 +10,16 @@ module SqlBeautifier
|
|
|
10
10
|
def initialize(value)
|
|
11
11
|
@value = value
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def keyword_prefix
|
|
17
|
+
Util.keyword_padding(self.class::KEYWORD)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def continuation_indent
|
|
21
|
+
Util.continuation_padding
|
|
22
|
+
end
|
|
13
23
|
end
|
|
14
24
|
end
|
|
15
25
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
module Clauses
|
|
5
|
+
class ConditionClause < Base
|
|
6
|
+
def call
|
|
7
|
+
return "#{keyword_prefix}#{@value.strip}" unless multiple_conditions?
|
|
8
|
+
|
|
9
|
+
formatted_conditions = ConditionFormatter.format(@value, indent_width: SqlBeautifier.config_for(:keyword_column_width))
|
|
10
|
+
formatted_conditions.sub(continuation_indent, keyword_prefix)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def multiple_conditions?
|
|
16
|
+
Tokenizer.split_top_level_conditions(@value).length > 1
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -3,10 +3,147 @@
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
module Clauses
|
|
5
5
|
class From < Base
|
|
6
|
-
|
|
6
|
+
KEYWORD = "from"
|
|
7
|
+
|
|
8
|
+
def self.call(value, table_registry:)
|
|
9
|
+
new(value, table_registry: table_registry).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(value, table_registry:)
|
|
13
|
+
super(value)
|
|
14
|
+
@table_registry = table_registry
|
|
15
|
+
end
|
|
7
16
|
|
|
8
17
|
def call
|
|
9
|
-
|
|
18
|
+
@lines = []
|
|
19
|
+
|
|
20
|
+
join_parts = split_join_parts
|
|
21
|
+
primary_table_text = join_parts.shift.strip
|
|
22
|
+
formatted_primary_table_name = format_table_with_alias(primary_table_text)
|
|
23
|
+
add_line!("#{keyword_prefix}#{formatted_primary_table_name}")
|
|
24
|
+
|
|
25
|
+
join_parts.each { |join_part| format_join_part(join_part) }
|
|
26
|
+
|
|
27
|
+
@lines.join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def add_line!(line)
|
|
33
|
+
@lines << line
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def join_condition_indentation
|
|
37
|
+
Util.whitespace(SqlBeautifier.config_for(:keyword_column_width) + 4)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_join_part(join_part)
|
|
41
|
+
join_keyword, remaining_join_content = extract_join_keyword(join_part)
|
|
42
|
+
return unless join_keyword && remaining_join_content
|
|
43
|
+
|
|
44
|
+
on_keyword_position = Tokenizer.find_top_level_keyword(remaining_join_content, "on")
|
|
45
|
+
|
|
46
|
+
if on_keyword_position
|
|
47
|
+
format_join_with_conditions(join_keyword, remaining_join_content, on_keyword_position)
|
|
48
|
+
else
|
|
49
|
+
formatted_table_name = format_table_with_alias(remaining_join_content)
|
|
50
|
+
add_line!("#{continuation_indent}#{join_keyword} #{formatted_table_name}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_join_with_conditions(join_keyword, join_content, on_keyword_position)
|
|
55
|
+
table_text = join_content[0...on_keyword_position].strip
|
|
56
|
+
condition_text = join_content[on_keyword_position..].delete_prefix("on").strip
|
|
57
|
+
on_conditions = Tokenizer.split_top_level_conditions(condition_text)
|
|
58
|
+
|
|
59
|
+
formatted_table_name = format_table_with_alias(table_text)
|
|
60
|
+
first_condition = on_conditions.first[1]
|
|
61
|
+
add_line!("#{continuation_indent}#{join_keyword} #{formatted_table_name} on #{first_condition}")
|
|
62
|
+
|
|
63
|
+
on_conditions.drop(1).each do |conjunction, additional_condition|
|
|
64
|
+
add_line!("#{join_condition_indentation}#{conjunction} #{additional_condition}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def split_join_parts
|
|
69
|
+
from_content = @value.strip
|
|
70
|
+
join_keyword_positions = find_all_join_keyword_positions(from_content)
|
|
71
|
+
|
|
72
|
+
return [from_content] if join_keyword_positions.empty?
|
|
73
|
+
|
|
74
|
+
parts = [from_content[0...join_keyword_positions.first[:position]]]
|
|
75
|
+
|
|
76
|
+
join_keyword_positions.each_with_index do |join_info, index|
|
|
77
|
+
end_position = begin
|
|
78
|
+
if index + 1 < join_keyword_positions.length
|
|
79
|
+
join_keyword_positions[index + 1][:position]
|
|
80
|
+
else
|
|
81
|
+
from_content.length
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
parts << from_content[join_info[:position]...end_position]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
parts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def find_all_join_keyword_positions(text)
|
|
92
|
+
positions = []
|
|
93
|
+
search_offset = 0
|
|
94
|
+
|
|
95
|
+
while search_offset < text.length
|
|
96
|
+
earliest_match = find_earliest_join_keyword(text, search_offset)
|
|
97
|
+
break unless earliest_match
|
|
98
|
+
|
|
99
|
+
positions << earliest_match
|
|
100
|
+
search_offset = earliest_match[:position] + earliest_match[:keyword].length
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
positions
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def find_earliest_join_keyword(text, search_offset)
|
|
107
|
+
earliest_match = nil
|
|
108
|
+
|
|
109
|
+
Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
|
|
110
|
+
remaining_text = text[search_offset..]
|
|
111
|
+
keyword_position = Tokenizer.find_top_level_keyword(remaining_text, keyword)
|
|
112
|
+
next unless keyword_position
|
|
113
|
+
|
|
114
|
+
absolute_position = search_offset + keyword_position
|
|
115
|
+
next if earliest_match && absolute_position >= earliest_match[:position]
|
|
116
|
+
|
|
117
|
+
earliest_match = {
|
|
118
|
+
position: absolute_position,
|
|
119
|
+
keyword: keyword,
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
earliest_match
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def extract_join_keyword(join_part)
|
|
127
|
+
trimmed_join_text = join_part.strip
|
|
128
|
+
|
|
129
|
+
Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
|
|
130
|
+
next unless trimmed_join_text.downcase.start_with?(keyword)
|
|
131
|
+
|
|
132
|
+
remaining_join_content = trimmed_join_text[keyword.length..].strip
|
|
133
|
+
|
|
134
|
+
return [keyword, remaining_join_content]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
[nil, nil]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def format_table_with_alias(table_text)
|
|
141
|
+
table_name = Util.first_word(table_text)
|
|
142
|
+
formatted_table_name = Util.format_table_name(table_name)
|
|
143
|
+
table_alias = @table_registry.alias_for(table_name)
|
|
144
|
+
return formatted_table_name unless table_alias
|
|
145
|
+
|
|
146
|
+
"#{formatted_table_name} #{table_alias}"
|
|
10
147
|
end
|
|
11
148
|
end
|
|
12
149
|
end
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
module Clauses
|
|
5
5
|
class Limit < Base
|
|
6
|
-
|
|
6
|
+
KEYWORD = "limit"
|
|
7
7
|
|
|
8
8
|
def call
|
|
9
|
-
"#{
|
|
9
|
+
"#{keyword_prefix}#{@value.strip}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def keyword_prefix
|
|
15
|
+
"#{Util.format_keyword(KEYWORD)} "
|
|
10
16
|
end
|
|
11
17
|
end
|
|
12
18
|
end
|
|
@@ -3,18 +3,72 @@
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
module Clauses
|
|
5
5
|
class Select < Base
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
KEYWORD = "select"
|
|
7
|
+
DISTINCT_ON_PARENTHESIS_PATTERN = %r{distinct on\s*\(}
|
|
8
|
+
DISTINCT_ON_PATTERN = %r{distinct on }
|
|
9
|
+
LEADING_COMMA_PATTERN = %r{\A,\s*}
|
|
8
10
|
|
|
9
11
|
def call
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
prefix, remaining_columns = extract_prefix
|
|
13
|
+
columns = Tokenizer.split_by_top_level_commas(remaining_columns)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
return format_with_prefix(prefix, columns) if prefix
|
|
16
|
+
return keyword_line(columns.first) if columns.length == 1
|
|
17
|
+
|
|
18
|
+
format_columns_list(columns)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def keyword_line(column)
|
|
24
|
+
"#{keyword_prefix}#{column.strip}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def continuation_line(column)
|
|
28
|
+
"#{continuation_indent}#{column.strip}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def format_with_prefix(prefix, columns)
|
|
32
|
+
first_line = "#{keyword_prefix}#{prefix}"
|
|
33
|
+
column_lines = columns.map { |column| continuation_line(column) }
|
|
34
|
+
|
|
35
|
+
"#{first_line}\n#{column_lines.join(",\n")}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def format_columns_list(columns)
|
|
39
|
+
column_lines = columns.map { |column| continuation_line(column) }
|
|
40
|
+
column_lines[0] = keyword_line(columns.first)
|
|
15
41
|
|
|
16
42
|
column_lines.join(",\n")
|
|
17
43
|
end
|
|
44
|
+
|
|
45
|
+
def extract_prefix
|
|
46
|
+
stripped_value = @value.strip
|
|
47
|
+
|
|
48
|
+
if stripped_value.start_with?("distinct on ")
|
|
49
|
+
extract_distinct_on_prefix(stripped_value)
|
|
50
|
+
elsif stripped_value.start_with?("distinct ")
|
|
51
|
+
remaining_columns = stripped_value.delete_prefix("distinct ").strip
|
|
52
|
+
|
|
53
|
+
["distinct", remaining_columns]
|
|
54
|
+
else
|
|
55
|
+
[nil, stripped_value]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_distinct_on_prefix(stripped_value)
|
|
60
|
+
distinct_on_position = stripped_value.index(DISTINCT_ON_PARENTHESIS_PATTERN) || stripped_value.index(DISTINCT_ON_PATTERN)
|
|
61
|
+
opening_parenthesis_position = stripped_value.index(Constants::OPEN_PARENTHESIS, distinct_on_position)
|
|
62
|
+
return [nil, stripped_value] unless opening_parenthesis_position
|
|
63
|
+
|
|
64
|
+
closing_parenthesis_position = Tokenizer.find_matching_parenthesis(stripped_value, opening_parenthesis_position)
|
|
65
|
+
return [nil, stripped_value] unless closing_parenthesis_position
|
|
66
|
+
|
|
67
|
+
prefix = stripped_value[0..closing_parenthesis_position]
|
|
68
|
+
remaining_columns = stripped_value[(closing_parenthesis_position + 1)..].strip.sub(LEADING_COMMA_PATTERN, "")
|
|
69
|
+
|
|
70
|
+
[prefix, remaining_columns]
|
|
71
|
+
end
|
|
18
72
|
end
|
|
19
73
|
end
|
|
20
74
|
end
|