sql_beautifier 0.6.0 → 0.7.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 +12 -0
- data/README.md +2 -2
- data/lib/sql_beautifier/base.rb +9 -0
- data/lib/sql_beautifier/clauses/base.rb +2 -2
- data/lib/sql_beautifier/clauses/condition_clause.rb +1 -1
- data/lib/sql_beautifier/clauses/from.rb +15 -69
- data/lib/sql_beautifier/clauses/order_by.rb +12 -1
- data/lib/sql_beautifier/clauses/select.rb +28 -15
- data/lib/sql_beautifier/comment.rb +23 -0
- data/lib/sql_beautifier/{comment_stripper.rb → comment_parser.rb} +67 -24
- data/lib/sql_beautifier/condition.rb +162 -0
- data/lib/sql_beautifier/configuration.rb +4 -15
- data/lib/sql_beautifier/create_table_as.rb +127 -0
- data/lib/sql_beautifier/cte_definition.rb +41 -0
- data/lib/sql_beautifier/cte_query.rb +129 -0
- data/lib/sql_beautifier/expression.rb +54 -0
- data/lib/sql_beautifier/formatter.rb +13 -80
- data/lib/sql_beautifier/join.rb +69 -0
- data/lib/sql_beautifier/normalizer.rb +33 -59
- data/lib/sql_beautifier/query.rb +185 -0
- data/lib/sql_beautifier/scanner.rb +420 -0
- data/lib/sql_beautifier/sort_expression.rb +39 -0
- data/lib/sql_beautifier/statement_assembler.rb +4 -4
- data/lib/sql_beautifier/statement_splitter.rb +35 -143
- data/lib/sql_beautifier/table_reference.rb +52 -0
- data/lib/sql_beautifier/table_registry.rb +50 -124
- data/lib/sql_beautifier/tokenizer.rb +47 -278
- data/lib/sql_beautifier/types.rb +9 -0
- data/lib/sql_beautifier/version.rb +1 -1
- data/lib/sql_beautifier.rb +14 -6
- metadata +43 -7
- data/lib/sql_beautifier/comment_restorer.rb +0 -62
- data/lib/sql_beautifier/condition_formatter.rb +0 -127
- data/lib/sql_beautifier/create_table_as_formatter.rb +0 -177
- data/lib/sql_beautifier/cte_formatter.rb +0 -192
- data/lib/sql_beautifier/subquery_formatter.rb +0 -113
|
@@ -4,8 +4,8 @@ module SqlBeautifier
|
|
|
4
4
|
class Normalizer
|
|
5
5
|
SAFE_UNQUOTED_IDENTIFIER = %r{\A[[:lower:]_][[:lower:][:digit:]_]*\z}
|
|
6
6
|
|
|
7
|
-
def self.call(
|
|
8
|
-
new(
|
|
7
|
+
def self.call(...)
|
|
8
|
+
new(...).call
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def initialize(value)
|
|
@@ -22,31 +22,28 @@ module SqlBeautifier
|
|
|
22
22
|
@source = @source.strip
|
|
23
23
|
return unless @source.present?
|
|
24
24
|
|
|
25
|
+
@scanner = Scanner.new(@source)
|
|
25
26
|
@output = +""
|
|
26
|
-
@position = 0
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
until @scanner.finished?
|
|
29
|
+
if @scanner.sentinel_at?
|
|
30
|
+
@output << @scanner.consume_sentinel!
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
case @scanner.current_char
|
|
30
35
|
when Constants::SINGLE_QUOTE
|
|
31
36
|
consume_string_literal!
|
|
32
37
|
|
|
33
38
|
when Constants::DOUBLE_QUOTE
|
|
34
39
|
consume_quoted_identifier!
|
|
35
40
|
|
|
36
|
-
when "/"
|
|
37
|
-
if sentinel_at_position?
|
|
38
|
-
consume_sentinel!
|
|
39
|
-
else
|
|
40
|
-
@output << current_character.downcase
|
|
41
|
-
@position += 1
|
|
42
|
-
end
|
|
43
|
-
|
|
44
41
|
when Constants::WHITESPACE_CHARACTER_REGEX
|
|
45
42
|
collapse_whitespace!
|
|
46
43
|
|
|
47
44
|
else
|
|
48
|
-
@output <<
|
|
49
|
-
@
|
|
45
|
+
@output << @scanner.current_char.downcase
|
|
46
|
+
@scanner.advance!
|
|
50
47
|
end
|
|
51
48
|
end
|
|
52
49
|
|
|
@@ -55,78 +52,55 @@ module SqlBeautifier
|
|
|
55
52
|
|
|
56
53
|
private
|
|
57
54
|
|
|
58
|
-
def current_character
|
|
59
|
-
@source[@position]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def sentinel_at_position?
|
|
63
|
-
@source[@position, CommentStripper::SENTINEL_PREFIX.length] == CommentStripper::SENTINEL_PREFIX
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def consume_sentinel!
|
|
67
|
-
sentinel_end = @source.index("*/", @position + CommentStripper::SENTINEL_PREFIX.length)
|
|
68
|
-
|
|
69
|
-
unless sentinel_end
|
|
70
|
-
@output << current_character.downcase
|
|
71
|
-
@position += 1
|
|
72
|
-
return
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
end_position = sentinel_end + 2
|
|
76
|
-
@output << @source[@position...end_position]
|
|
77
|
-
@position = end_position
|
|
78
|
-
end
|
|
79
|
-
|
|
80
55
|
def collapse_whitespace!
|
|
81
56
|
@output << " "
|
|
82
|
-
@
|
|
83
|
-
@
|
|
57
|
+
@scanner.advance!
|
|
58
|
+
@scanner.skip_whitespace!
|
|
84
59
|
end
|
|
85
60
|
|
|
86
61
|
def consume_string_literal!
|
|
87
|
-
@output <<
|
|
88
|
-
@
|
|
62
|
+
@output << @scanner.current_char
|
|
63
|
+
@scanner.advance!
|
|
89
64
|
|
|
90
|
-
|
|
91
|
-
character =
|
|
65
|
+
until @scanner.finished?
|
|
66
|
+
character = @scanner.current_char
|
|
92
67
|
@output << character
|
|
93
68
|
|
|
94
|
-
if character == Constants::SINGLE_QUOTE && @
|
|
95
|
-
@
|
|
96
|
-
@output <<
|
|
69
|
+
if character == Constants::SINGLE_QUOTE && @scanner.peek == Constants::SINGLE_QUOTE
|
|
70
|
+
@scanner.advance!
|
|
71
|
+
@output << @scanner.current_char
|
|
97
72
|
elsif character == Constants::SINGLE_QUOTE
|
|
98
|
-
@
|
|
73
|
+
@scanner.advance!
|
|
99
74
|
return
|
|
100
75
|
end
|
|
101
76
|
|
|
102
|
-
@
|
|
77
|
+
@scanner.advance!
|
|
103
78
|
end
|
|
104
79
|
end
|
|
105
80
|
|
|
106
81
|
def consume_quoted_identifier!
|
|
107
|
-
start_position = @position
|
|
82
|
+
start_position = @scanner.position
|
|
108
83
|
identifier = +""
|
|
109
|
-
@
|
|
84
|
+
@scanner.advance!
|
|
110
85
|
|
|
111
|
-
|
|
112
|
-
character =
|
|
86
|
+
until @scanner.finished?
|
|
87
|
+
character = @scanner.current_char
|
|
113
88
|
|
|
114
|
-
if character == Constants::DOUBLE_QUOTE && @
|
|
89
|
+
if character == Constants::DOUBLE_QUOTE && @scanner.peek == Constants::DOUBLE_QUOTE
|
|
115
90
|
identifier << Constants::DOUBLE_QUOTE
|
|
116
|
-
@
|
|
91
|
+
@scanner.advance!(2)
|
|
117
92
|
elsif character == Constants::DOUBLE_QUOTE
|
|
118
|
-
@
|
|
93
|
+
@scanner.advance!
|
|
119
94
|
@output << format_identifier(identifier)
|
|
120
95
|
return
|
|
121
96
|
else
|
|
122
97
|
identifier << character
|
|
123
|
-
@
|
|
98
|
+
@scanner.advance!
|
|
124
99
|
end
|
|
125
100
|
end
|
|
126
101
|
|
|
127
|
-
@position
|
|
128
|
-
@output <<
|
|
129
|
-
@position += 1
|
|
102
|
+
@scanner.advance!(start_position - @scanner.position + 1) if start_position != @scanner.position
|
|
103
|
+
@output << @source[start_position].downcase
|
|
130
104
|
end
|
|
131
105
|
|
|
132
106
|
def format_identifier(identifier)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Query
|
|
5
|
+
COMPACT_CLAUSE_KEYS = %i[
|
|
6
|
+
select
|
|
7
|
+
from
|
|
8
|
+
where
|
|
9
|
+
order_by
|
|
10
|
+
limit
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
LEADING_WHITESPACE_PATTERN = %r{\A[[:space:]]*}
|
|
14
|
+
WHERE_PREFIX_PATTERN = %r{\Awhere(?:[[:space:]]|$)}i
|
|
15
|
+
|
|
16
|
+
attr_reader :clauses
|
|
17
|
+
attr_reader :depth
|
|
18
|
+
attr_reader :table_registry
|
|
19
|
+
|
|
20
|
+
def self.parse(normalized_sql, depth: 0)
|
|
21
|
+
clauses = Tokenizer.split_into_clauses(normalized_sql)
|
|
22
|
+
|
|
23
|
+
new(clauses: clauses, depth: depth)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.format_as_subquery(inner_sql, base_indent:)
|
|
27
|
+
indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
|
|
28
|
+
subquery_indent = base_indent + indent_spaces
|
|
29
|
+
formatted = Formatter.new(inner_sql, depth: subquery_indent).call
|
|
30
|
+
return "(#{inner_sql})" unless formatted
|
|
31
|
+
|
|
32
|
+
indentation = Util.whitespace(subquery_indent)
|
|
33
|
+
indented_lines = formatted.chomp.lines.map do |line|
|
|
34
|
+
line.strip.empty? ? "\n" : "#{indentation}#{line}"
|
|
35
|
+
end.join
|
|
36
|
+
|
|
37
|
+
"(\n#{indented_lines}\n#{Util.whitespace(base_indent)})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.format_subqueries_in_text(text, depth:)
|
|
41
|
+
output = +""
|
|
42
|
+
position = 0
|
|
43
|
+
|
|
44
|
+
while position < text.length
|
|
45
|
+
subquery_position = find_top_level_subquery(text, position)
|
|
46
|
+
|
|
47
|
+
unless subquery_position
|
|
48
|
+
output << text[position..]
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
output << text[position...subquery_position]
|
|
53
|
+
|
|
54
|
+
closing_position = Scanner.new(text).find_matching_parenthesis(subquery_position)
|
|
55
|
+
|
|
56
|
+
unless closing_position
|
|
57
|
+
output << text[subquery_position..]
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
inner_sql = text[(subquery_position + 1)...closing_position].strip
|
|
62
|
+
base_indent = subquery_base_indent_for(text, subquery_position, depth)
|
|
63
|
+
output << format_as_subquery(inner_sql, base_indent: base_indent)
|
|
64
|
+
position = closing_position + 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
output
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.find_top_level_subquery(text, start_position)
|
|
71
|
+
scanner = Scanner.new(text, position: start_position)
|
|
72
|
+
|
|
73
|
+
until scanner.finished?
|
|
74
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
75
|
+
next if consumed
|
|
76
|
+
|
|
77
|
+
return scanner.position if scanner.current_char == Constants::OPEN_PARENTHESIS && select_follows?(text, scanner.position)
|
|
78
|
+
|
|
79
|
+
scanner.advance!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.subquery_base_indent_for(text, subquery_position, default_base_indent)
|
|
86
|
+
line_start_position = text.rindex("\n", subquery_position - 1)
|
|
87
|
+
line_start_position = line_start_position ? line_start_position + 1 : 0
|
|
88
|
+
line_before_subquery = text[line_start_position...subquery_position]
|
|
89
|
+
line_leading_spaces = line_before_subquery[LEADING_WHITESPACE_PATTERN].to_s.length
|
|
90
|
+
|
|
91
|
+
return default_base_indent unless line_before_subquery.lstrip.match?(WHERE_PREFIX_PATTERN)
|
|
92
|
+
|
|
93
|
+
default_base_indent + line_leading_spaces + SqlBeautifier.config_for(:keyword_column_width)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.select_follows?(text, position)
|
|
97
|
+
remaining_text = text[(position + 1)..]
|
|
98
|
+
return false unless remaining_text
|
|
99
|
+
|
|
100
|
+
remaining_text.match?(%r{\A[[:space:]]*select(?:[[:space:]]|\()}i)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def initialize(clauses:, depth: 0)
|
|
104
|
+
@clauses = clauses
|
|
105
|
+
@depth = depth
|
|
106
|
+
@table_registry = TableRegistry.new(@clauses[:from]) if @clauses[:from].present?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
append_clause!(parts, :select, Clauses::Select)
|
|
113
|
+
append_from_clause!(parts)
|
|
114
|
+
append_clause!(parts, :where, Clauses::Where)
|
|
115
|
+
append_clause!(parts, :group_by, Clauses::GroupBy)
|
|
116
|
+
append_clause!(parts, :having, Clauses::Having)
|
|
117
|
+
append_clause!(parts, :order_by, Clauses::OrderBy)
|
|
118
|
+
append_clause!(parts, :limit, Clauses::Limit)
|
|
119
|
+
|
|
120
|
+
output = parts.join(clause_separator)
|
|
121
|
+
return nil if output.empty?
|
|
122
|
+
|
|
123
|
+
output = self.class.format_subqueries_in_text(output, depth: @depth)
|
|
124
|
+
output = @table_registry.apply_aliases(output) if @table_registry
|
|
125
|
+
"#{output}\n"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def compact?
|
|
129
|
+
compact_clause_set? && single_select_column? && single_from_table? && one_or_fewer_conditions?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def append_clause!(parts, clause_key, formatter_class)
|
|
135
|
+
value = @clauses[clause_key]
|
|
136
|
+
return unless value.present?
|
|
137
|
+
|
|
138
|
+
parts << formatter_class.call(value)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def append_from_clause!(parts)
|
|
142
|
+
value = @clauses[:from]
|
|
143
|
+
return unless value.present?
|
|
144
|
+
|
|
145
|
+
parts << Clauses::From.call(value, table_registry: @table_registry)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def clause_separator
|
|
149
|
+
return "\n\n" if SqlBeautifier.config_for(:clause_spacing_mode) == :spacious
|
|
150
|
+
return "\n\n" unless compact?
|
|
151
|
+
|
|
152
|
+
"\n"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def compact_clause_set?
|
|
156
|
+
clause_keys = @clauses.keys
|
|
157
|
+
clause_keys.all? { |key| COMPACT_CLAUSE_KEYS.include?(key) } && clause_keys.include?(:select) && clause_keys.include?(:from)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def single_select_column?
|
|
161
|
+
select_value = @clauses[:select]
|
|
162
|
+
return false unless select_value.present?
|
|
163
|
+
|
|
164
|
+
formatted_select = Clauses::Select.call(select_value)
|
|
165
|
+
formatted_select.lines.length == 1
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def single_from_table?
|
|
169
|
+
from_value = @clauses[:from]
|
|
170
|
+
return false unless from_value.present?
|
|
171
|
+
|
|
172
|
+
join_keywords = Constants::JOIN_KEYWORDS_BY_LENGTH.any? { |keyword| Tokenizer.find_top_level_keyword(from_value, keyword) }
|
|
173
|
+
return false if join_keywords
|
|
174
|
+
|
|
175
|
+
!from_value.match?(%r{,})
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def one_or_fewer_conditions?
|
|
179
|
+
where_value = @clauses[:where]
|
|
180
|
+
return true unless where_value.present?
|
|
181
|
+
|
|
182
|
+
Tokenizer.split_top_level_conditions(where_value).length <= 1
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|