sql_beautifier 0.1.1
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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/lib/sql_beautifier/clauses/base.rb +15 -0
- data/lib/sql_beautifier/clauses/from.rb +13 -0
- data/lib/sql_beautifier/clauses/group_by.rb +13 -0
- data/lib/sql_beautifier/clauses/having.rb +13 -0
- data/lib/sql_beautifier/clauses/limit.rb +13 -0
- data/lib/sql_beautifier/clauses/order_by.rb +13 -0
- data/lib/sql_beautifier/clauses/select.rb +20 -0
- data/lib/sql_beautifier/clauses/where.rb +13 -0
- data/lib/sql_beautifier/constants.rb +15 -0
- data/lib/sql_beautifier/formatter.rb +48 -0
- data/lib/sql_beautifier/normalizer.rb +116 -0
- data/lib/sql_beautifier/tokenizer.rb +189 -0
- data/lib/sql_beautifier/version.rb +5 -0
- data/lib/sql_beautifier.rb +29 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d9b1fa5cd125f5c255888fe73fa0eb382a711452e0dde36e72d194eede7909d8
|
|
4
|
+
data.tar.gz: 613ec14ba2aaa8e276e284195924ad955bd82634ce76598c822c523048939a7d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 554c19234224a8b2f96fe62499944ebb3f4d50f24e2ead9d38c5b340b983a4b1231b0feb395e978bf489a75cb390811254acbaa7239e3560d7c66a4520f41556
|
|
7
|
+
data.tar.gz: 04fcd9d23787c9e8a2e82a480ee545839666fc5e0b2b509ada502356d46547166cbd32ec7531662bfaaaeb12be39eeb18318f76f268595e324f5b19ccf6ef862
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [X.X.X] - YYYY-MM-DD
|
|
4
|
+
|
|
5
|
+
## [0.1.1] - 2026-03-26
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-03-26
|
|
8
|
+
|
|
9
|
+
- Initial release
|
|
10
|
+
- Formats SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT clauses
|
|
11
|
+
- Lowercase keywords with 8-character column alignment
|
|
12
|
+
- Multi-column SELECT with continuation indentation
|
|
13
|
+
- Parenthesis and string-literal aware tokenization
|
|
14
|
+
- Normalizes whitespace and handles quoted identifiers
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kinnell Shah
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# SqlBeautifier
|
|
2
|
+
|
|
3
|
+
Opinionated PostgreSQL SQL formatter.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby >= 3.2.0
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "sql_beautifier"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install it directly:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install sql_beautifier
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Basic Formatting
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
SqlBeautifier.call("SELECT id, name, email FROM users WHERE active = true ORDER BY name")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Produces:
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
select id,
|
|
41
|
+
name,
|
|
42
|
+
email
|
|
43
|
+
|
|
44
|
+
from users
|
|
45
|
+
|
|
46
|
+
where active = true
|
|
47
|
+
|
|
48
|
+
order by name
|
|
49
|
+
```
|
|
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. Each clause is separated by a blank line. Multi-column SELECT lists place each column on its own line with continuation indentation.
|
|
52
|
+
|
|
53
|
+
### GROUP BY and HAVING
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
SqlBeautifier.call(<<~SQL)
|
|
57
|
+
SELECT status, count(*)
|
|
58
|
+
FROM users
|
|
59
|
+
GROUP BY status
|
|
60
|
+
HAVING count(*) > 5
|
|
61
|
+
SQL
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Produces:
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
select status,
|
|
68
|
+
count(*)
|
|
69
|
+
|
|
70
|
+
from users
|
|
71
|
+
|
|
72
|
+
group by status
|
|
73
|
+
|
|
74
|
+
having count(*) > 5
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### LIMIT
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
SqlBeautifier.call("SELECT id FROM users ORDER BY created_at DESC LIMIT 25")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Produces:
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
select id
|
|
87
|
+
|
|
88
|
+
from users
|
|
89
|
+
|
|
90
|
+
order by created_at desc
|
|
91
|
+
|
|
92
|
+
limit 25
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### String Literals
|
|
96
|
+
|
|
97
|
+
Case is preserved inside single-quoted string literals, and escaped quotes (`''`) are handled correctly:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
SqlBeautifier.call("SELECT * FROM users WHERE name = 'O''Brien' AND status = 'Active'")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Produces:
|
|
104
|
+
|
|
105
|
+
```sql
|
|
106
|
+
select *
|
|
107
|
+
|
|
108
|
+
from users
|
|
109
|
+
|
|
110
|
+
where name = 'O''Brien' and status = 'Active'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Double-Quoted Identifiers
|
|
114
|
+
|
|
115
|
+
Double-quoted PostgreSQL identifiers are normalized by lowercasing their contents. If the resulting identifier can be safely represented as an unquoted PostgreSQL identifier, the surrounding quotes are removed; otherwise, the quotes are preserved and only the contents are lowercased:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
SqlBeautifier.call('SELECT "User_Id", "Full_Name" FROM "Users"')
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Produces:
|
|
122
|
+
|
|
123
|
+
```sql
|
|
124
|
+
select user_id,
|
|
125
|
+
full_name
|
|
126
|
+
|
|
127
|
+
from users
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Callable Interface
|
|
131
|
+
|
|
132
|
+
`SqlBeautifier.call` is the public API, making it a valid callable for Rails `normalizes` and anywhere a proc-like object is expected:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class Query < ApplicationRecord
|
|
136
|
+
normalizes :sql, with: SqlBeautifier
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
After checking out the repo, run:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bin/setup
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run the test suite:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
rake test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Run the linter:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
rake lint
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Run the full CI suite (tests + linting):
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
rake
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Start an interactive console with the gem loaded:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
bin/console
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kinnell/sql_beautifier.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
module Clauses
|
|
5
|
+
class Select < Base
|
|
6
|
+
KEYWORD_PREFIX = "select "
|
|
7
|
+
CONTINUATION_INDENT = " "
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
columns = Tokenizer.split_by_top_level_commas(@value)
|
|
11
|
+
return "#{KEYWORD_PREFIX}#{columns.first.strip}" if columns.length == 1
|
|
12
|
+
|
|
13
|
+
column_lines = columns.map { |column| "#{CONTINUATION_INDENT}#{column.strip}" }
|
|
14
|
+
column_lines[0] = "#{KEYWORD_PREFIX}#{columns.first.strip}"
|
|
15
|
+
|
|
16
|
+
column_lines.join(",\n")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Formatter
|
|
5
|
+
def self.call(value)
|
|
6
|
+
new(value).call
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(value)
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
return unless @value.present?
|
|
15
|
+
|
|
16
|
+
@normalized_value = Normalizer.call(@value)
|
|
17
|
+
return unless @normalized_value.present?
|
|
18
|
+
|
|
19
|
+
first_clause_position = Tokenizer.first_clause_position(@normalized_value)
|
|
20
|
+
return "#{@normalized_value}\n" if first_clause_position.nil? || first_clause_position.positive?
|
|
21
|
+
|
|
22
|
+
@clauses = Tokenizer.split_into_clauses(@normalized_value)
|
|
23
|
+
@parts = []
|
|
24
|
+
|
|
25
|
+
append_clause!(:select, Clauses::Select)
|
|
26
|
+
append_clause!(:from, Clauses::From)
|
|
27
|
+
append_clause!(:where, Clauses::Where)
|
|
28
|
+
append_clause!(:group_by, Clauses::GroupBy)
|
|
29
|
+
append_clause!(:having, Clauses::Having)
|
|
30
|
+
append_clause!(:order_by, Clauses::OrderBy)
|
|
31
|
+
append_clause!(:limit, Clauses::Limit)
|
|
32
|
+
|
|
33
|
+
output = @parts.join("\n\n")
|
|
34
|
+
return "#{@normalized_value}\n" if output.empty?
|
|
35
|
+
|
|
36
|
+
"#{output}\n"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def append_clause!(clause_key, formatter_class)
|
|
42
|
+
value = @clauses[clause_key]
|
|
43
|
+
return unless value.present?
|
|
44
|
+
|
|
45
|
+
@parts << formatter_class.call(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Normalizer
|
|
5
|
+
SAFE_UNQUOTED_IDENTIFIER = %r{\A[[:lower:]_][[:lower:][:digit:]_]*\z}
|
|
6
|
+
|
|
7
|
+
def self.call(value)
|
|
8
|
+
new(value).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(value)
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
return unless @value.present?
|
|
17
|
+
|
|
18
|
+
@source = @value.strip
|
|
19
|
+
return unless @source.present?
|
|
20
|
+
|
|
21
|
+
@output = +""
|
|
22
|
+
@position = 0
|
|
23
|
+
|
|
24
|
+
while @position < @source.length
|
|
25
|
+
case current_character
|
|
26
|
+
when "'"
|
|
27
|
+
consume_string_literal!
|
|
28
|
+
|
|
29
|
+
when '"'
|
|
30
|
+
consume_quoted_identifier!
|
|
31
|
+
|
|
32
|
+
when %r{\s}
|
|
33
|
+
collapse_whitespace!
|
|
34
|
+
|
|
35
|
+
else
|
|
36
|
+
@output << current_character.downcase
|
|
37
|
+
@position += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@output
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def current_character
|
|
47
|
+
@source[@position]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collapse_whitespace!
|
|
51
|
+
@output << " "
|
|
52
|
+
@position += 1
|
|
53
|
+
@position += 1 while @position < @source.length && @source[@position] =~ %r{\s}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def consume_string_literal!
|
|
57
|
+
@output << current_character
|
|
58
|
+
@position += 1
|
|
59
|
+
|
|
60
|
+
while @position < @source.length
|
|
61
|
+
character = current_character
|
|
62
|
+
@output << character
|
|
63
|
+
|
|
64
|
+
if character == "'" && @source[@position + 1] == "'"
|
|
65
|
+
@position += 1
|
|
66
|
+
@output << current_character
|
|
67
|
+
elsif character == "'"
|
|
68
|
+
@position += 1
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@position += 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def consume_quoted_identifier!
|
|
77
|
+
start_position = @position
|
|
78
|
+
identifier = +""
|
|
79
|
+
@position += 1
|
|
80
|
+
|
|
81
|
+
while @position < @source.length
|
|
82
|
+
character = current_character
|
|
83
|
+
|
|
84
|
+
if character == '"' && @source[@position + 1] == '"'
|
|
85
|
+
identifier << '"'
|
|
86
|
+
@position += 2
|
|
87
|
+
elsif character == '"'
|
|
88
|
+
@position += 1
|
|
89
|
+
@output << format_identifier(identifier)
|
|
90
|
+
return
|
|
91
|
+
else
|
|
92
|
+
identifier << character
|
|
93
|
+
@position += 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@position = start_position
|
|
98
|
+
@output << current_character.downcase
|
|
99
|
+
@position += 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def format_identifier(identifier)
|
|
103
|
+
lowercased = identifier.downcase
|
|
104
|
+
|
|
105
|
+
if requires_quoting?(lowercased)
|
|
106
|
+
"\"#{lowercased.gsub('"', '""')}\""
|
|
107
|
+
else
|
|
108
|
+
lowercased
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def requires_quoting?(identifier)
|
|
113
|
+
identifier !~ SAFE_UNQUOTED_IDENTIFIER
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
module Tokenizer
|
|
5
|
+
IDENTIFIER_CHARACTER = %r{[[:alnum:]_$]}
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def find_top_level_keyword(sql, keyword)
|
|
10
|
+
keyword_pattern = %r{#{Regexp.escape(keyword)}}i
|
|
11
|
+
search_position = 0
|
|
12
|
+
|
|
13
|
+
while search_position < sql.length
|
|
14
|
+
match = sql.match(keyword_pattern, search_position)
|
|
15
|
+
return nil unless match
|
|
16
|
+
|
|
17
|
+
match_position = match.begin(0)
|
|
18
|
+
|
|
19
|
+
previous_character = match_position.zero? ? nil : sql[match_position - 1]
|
|
20
|
+
next_character = match_position + keyword.length >= sql.length ? nil : sql[match_position + keyword.length]
|
|
21
|
+
|
|
22
|
+
return match_position if word_boundary?(previous_character) && word_boundary?(next_character) && top_level?(sql, match_position)
|
|
23
|
+
|
|
24
|
+
search_position = match_position + 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def first_clause_position(sql)
|
|
31
|
+
Constants::CLAUSE_KEYWORDS.filter_map { |keyword| find_top_level_keyword(sql, keyword) }.min
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def split_into_clauses(sql)
|
|
35
|
+
boundaries = Constants::CLAUSE_KEYWORDS.filter_map do |keyword|
|
|
36
|
+
keyword_position = find_top_level_keyword(sql, keyword)
|
|
37
|
+
next unless keyword_position
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
keyword: keyword,
|
|
41
|
+
position: keyword_position,
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
boundaries.sort_by! { |boundary| boundary[:position] }
|
|
46
|
+
|
|
47
|
+
clauses = {}
|
|
48
|
+
|
|
49
|
+
boundaries.each_with_index do |boundary, boundary_index|
|
|
50
|
+
content_start = boundary[:position] + boundary[:keyword].length
|
|
51
|
+
content_end = boundary_index + 1 < boundaries.length ? boundaries[boundary_index + 1][:position] : sql.length
|
|
52
|
+
clause_symbol = boundary[:keyword].tr(" ", "_").to_sym
|
|
53
|
+
|
|
54
|
+
clauses[clause_symbol] = sql[content_start...content_end].strip
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
clauses
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def split_by_top_level_commas(text)
|
|
61
|
+
segments = []
|
|
62
|
+
current_segment = +""
|
|
63
|
+
parenthesis_depth = 0
|
|
64
|
+
inside_string_literal = false
|
|
65
|
+
inside_quoted_identifier = false
|
|
66
|
+
position = 0
|
|
67
|
+
|
|
68
|
+
while position < text.length
|
|
69
|
+
character = text[position]
|
|
70
|
+
|
|
71
|
+
if inside_string_literal
|
|
72
|
+
current_segment << character
|
|
73
|
+
|
|
74
|
+
if character == "'" && text[position + 1] == "'"
|
|
75
|
+
position += 1
|
|
76
|
+
current_segment << text[position]
|
|
77
|
+
elsif character == "'"
|
|
78
|
+
inside_string_literal = false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
position += 1
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if inside_quoted_identifier
|
|
86
|
+
current_segment << character
|
|
87
|
+
|
|
88
|
+
if character == '"' && text[position + 1] == '"'
|
|
89
|
+
position += 1
|
|
90
|
+
current_segment << text[position]
|
|
91
|
+
elsif character == '"'
|
|
92
|
+
inside_quoted_identifier = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
position += 1
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
case character
|
|
100
|
+
when "'"
|
|
101
|
+
inside_string_literal = true
|
|
102
|
+
current_segment << character
|
|
103
|
+
|
|
104
|
+
when '"'
|
|
105
|
+
inside_quoted_identifier = true
|
|
106
|
+
current_segment << character
|
|
107
|
+
|
|
108
|
+
when "("
|
|
109
|
+
parenthesis_depth += 1
|
|
110
|
+
current_segment << character
|
|
111
|
+
|
|
112
|
+
when ")"
|
|
113
|
+
parenthesis_depth = [parenthesis_depth - 1, 0].max
|
|
114
|
+
current_segment << character
|
|
115
|
+
|
|
116
|
+
when ","
|
|
117
|
+
if parenthesis_depth.zero?
|
|
118
|
+
segments << current_segment.strip
|
|
119
|
+
current_segment = +""
|
|
120
|
+
else
|
|
121
|
+
current_segment << character
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
else
|
|
125
|
+
current_segment << character
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
position += 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
segments << current_segment.strip unless current_segment.strip.empty?
|
|
132
|
+
segments
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def word_boundary?(character)
|
|
136
|
+
character.nil? || character !~ IDENTIFIER_CHARACTER
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def top_level?(sql, target_position)
|
|
140
|
+
parenthesis_depth = 0
|
|
141
|
+
inside_string_literal = false
|
|
142
|
+
inside_quoted_identifier = false
|
|
143
|
+
position = 0
|
|
144
|
+
|
|
145
|
+
while position < target_position
|
|
146
|
+
character = sql[position]
|
|
147
|
+
|
|
148
|
+
if inside_string_literal
|
|
149
|
+
if character == "'" && sql[position + 1] == "'"
|
|
150
|
+
position += 2
|
|
151
|
+
next
|
|
152
|
+
elsif character == "'"
|
|
153
|
+
inside_string_literal = false
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
position += 1
|
|
157
|
+
next
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if inside_quoted_identifier
|
|
161
|
+
if character == '"' && sql[position + 1] == '"'
|
|
162
|
+
position += 2
|
|
163
|
+
next
|
|
164
|
+
elsif character == '"'
|
|
165
|
+
inside_quoted_identifier = false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
position += 1
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
case character
|
|
173
|
+
when "'"
|
|
174
|
+
inside_string_literal = true
|
|
175
|
+
when '"'
|
|
176
|
+
inside_quoted_identifier = true
|
|
177
|
+
when "("
|
|
178
|
+
parenthesis_depth += 1
|
|
179
|
+
when ")"
|
|
180
|
+
parenthesis_depth = [parenthesis_depth - 1, 0].max
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
position += 1
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
parenthesis_depth.zero? && !inside_string_literal && !inside_quoted_identifier
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
|
|
5
|
+
require_relative "sql_beautifier/version"
|
|
6
|
+
require_relative "sql_beautifier/constants"
|
|
7
|
+
require_relative "sql_beautifier/normalizer"
|
|
8
|
+
require_relative "sql_beautifier/tokenizer"
|
|
9
|
+
require_relative "sql_beautifier/clauses/base"
|
|
10
|
+
require_relative "sql_beautifier/clauses/select"
|
|
11
|
+
require_relative "sql_beautifier/clauses/from"
|
|
12
|
+
require_relative "sql_beautifier/clauses/where"
|
|
13
|
+
require_relative "sql_beautifier/clauses/group_by"
|
|
14
|
+
require_relative "sql_beautifier/clauses/order_by"
|
|
15
|
+
require_relative "sql_beautifier/clauses/having"
|
|
16
|
+
require_relative "sql_beautifier/clauses/limit"
|
|
17
|
+
require_relative "sql_beautifier/formatter"
|
|
18
|
+
|
|
19
|
+
module SqlBeautifier
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def call(value)
|
|
25
|
+
return unless value.present?
|
|
26
|
+
|
|
27
|
+
Formatter.call(value)
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sql_beautifier
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kinnell Shah
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
description: 'Formats raw SQL into a clean, consistent style with lowercase keywords,
|
|
27
|
+
padded keyword alignment, and vertically separated clauses.
|
|
28
|
+
|
|
29
|
+
'
|
|
30
|
+
email:
|
|
31
|
+
- kinnell@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
- lib/sql_beautifier.rb
|
|
40
|
+
- lib/sql_beautifier/clauses/base.rb
|
|
41
|
+
- lib/sql_beautifier/clauses/from.rb
|
|
42
|
+
- lib/sql_beautifier/clauses/group_by.rb
|
|
43
|
+
- lib/sql_beautifier/clauses/having.rb
|
|
44
|
+
- lib/sql_beautifier/clauses/limit.rb
|
|
45
|
+
- lib/sql_beautifier/clauses/order_by.rb
|
|
46
|
+
- lib/sql_beautifier/clauses/select.rb
|
|
47
|
+
- lib/sql_beautifier/clauses/where.rb
|
|
48
|
+
- lib/sql_beautifier/constants.rb
|
|
49
|
+
- lib/sql_beautifier/formatter.rb
|
|
50
|
+
- lib/sql_beautifier/normalizer.rb
|
|
51
|
+
- lib/sql_beautifier/tokenizer.rb
|
|
52
|
+
- lib/sql_beautifier/version.rb
|
|
53
|
+
homepage: https://github.com/kinnell/sql_beautifier
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
homepage_uri: https://github.com/kinnell/sql_beautifier
|
|
58
|
+
github_repo: https://github.com/kinnell/sql_beautifier
|
|
59
|
+
source_code_uri: https://github.com/kinnell/sql_beautifier
|
|
60
|
+
changelog_uri: https://github.com/kinnell/sql_beautifier/blob/main/CHANGELOG.md
|
|
61
|
+
bug_tracker_uri: https://github.com/kinnell/sql_beautifier/issues
|
|
62
|
+
rubygems_mfa_required: 'true'
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 4.0.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Opinionated PostgreSQL SQL formatter
|
|
80
|
+
test_files: []
|