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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +2 -2
  4. data/lib/sql_beautifier/base.rb +9 -0
  5. data/lib/sql_beautifier/clauses/base.rb +2 -2
  6. data/lib/sql_beautifier/clauses/condition_clause.rb +1 -1
  7. data/lib/sql_beautifier/clauses/from.rb +15 -69
  8. data/lib/sql_beautifier/clauses/order_by.rb +12 -1
  9. data/lib/sql_beautifier/clauses/select.rb +28 -15
  10. data/lib/sql_beautifier/comment.rb +23 -0
  11. data/lib/sql_beautifier/{comment_stripper.rb → comment_parser.rb} +67 -24
  12. data/lib/sql_beautifier/condition.rb +162 -0
  13. data/lib/sql_beautifier/configuration.rb +4 -15
  14. data/lib/sql_beautifier/create_table_as.rb +127 -0
  15. data/lib/sql_beautifier/cte_definition.rb +41 -0
  16. data/lib/sql_beautifier/cte_query.rb +129 -0
  17. data/lib/sql_beautifier/expression.rb +54 -0
  18. data/lib/sql_beautifier/formatter.rb +13 -80
  19. data/lib/sql_beautifier/join.rb +69 -0
  20. data/lib/sql_beautifier/normalizer.rb +33 -59
  21. data/lib/sql_beautifier/query.rb +185 -0
  22. data/lib/sql_beautifier/scanner.rb +420 -0
  23. data/lib/sql_beautifier/sort_expression.rb +39 -0
  24. data/lib/sql_beautifier/statement_assembler.rb +4 -4
  25. data/lib/sql_beautifier/statement_splitter.rb +35 -143
  26. data/lib/sql_beautifier/table_reference.rb +52 -0
  27. data/lib/sql_beautifier/table_registry.rb +50 -124
  28. data/lib/sql_beautifier/tokenizer.rb +47 -278
  29. data/lib/sql_beautifier/types.rb +9 -0
  30. data/lib/sql_beautifier/version.rb +1 -1
  31. data/lib/sql_beautifier.rb +14 -6
  32. metadata +43 -7
  33. data/lib/sql_beautifier/comment_restorer.rb +0 -62
  34. data/lib/sql_beautifier/condition_formatter.rb +0 -127
  35. data/lib/sql_beautifier/create_table_as_formatter.rb +0 -177
  36. data/lib/sql_beautifier/cte_formatter.rb +0 -192
  37. 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(value)
8
- new(value).call
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
- while @position < @source.length
29
- case current_character
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 << current_character.downcase
49
- @position += 1
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
- @position += 1
83
- @position += 1 while @position < @source.length && @source[@position] =~ Constants::WHITESPACE_CHARACTER_REGEX
57
+ @scanner.advance!
58
+ @scanner.skip_whitespace!
84
59
  end
85
60
 
86
61
  def consume_string_literal!
87
- @output << current_character
88
- @position += 1
62
+ @output << @scanner.current_char
63
+ @scanner.advance!
89
64
 
90
- while @position < @source.length
91
- character = current_character
65
+ until @scanner.finished?
66
+ character = @scanner.current_char
92
67
  @output << character
93
68
 
94
- if character == Constants::SINGLE_QUOTE && @source[@position + 1] == Constants::SINGLE_QUOTE
95
- @position += 1
96
- @output << current_character
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
- @position += 1
73
+ @scanner.advance!
99
74
  return
100
75
  end
101
76
 
102
- @position += 1
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
- @position += 1
84
+ @scanner.advance!
110
85
 
111
- while @position < @source.length
112
- character = current_character
86
+ until @scanner.finished?
87
+ character = @scanner.current_char
113
88
 
114
- if character == Constants::DOUBLE_QUOTE && @source[@position + 1] == Constants::DOUBLE_QUOTE
89
+ if character == Constants::DOUBLE_QUOTE && @scanner.peek == Constants::DOUBLE_QUOTE
115
90
  identifier << Constants::DOUBLE_QUOTE
116
- @position += 2
91
+ @scanner.advance!(2)
117
92
  elsif character == Constants::DOUBLE_QUOTE
118
- @position += 1
93
+ @scanner.advance!
119
94
  @output << format_identifier(identifier)
120
95
  return
121
96
  else
122
97
  identifier << character
123
- @position += 1
98
+ @scanner.advance!
124
99
  end
125
100
  end
126
101
 
127
- @position = start_position
128
- @output << current_character.downcase
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