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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 189e4fb1ebb3e61fa40afa896d3ac83897714fb3236f3ef27a1a97da03c82fea
|
|
4
|
+
data.tar.gz: 9143a35ed8be4d33372d1d82bb05ec52d8acf3eb2ce34ea5880e0b7e5e53b2f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 991c1470b7beb1db6c405b709d5ee96f9d7562c27882e5a4f8269a2f3680d50a3787c326df87bd04c7331cab0b87f7c5b87867f03a094f5628f39cfaf0376bd9
|
|
7
|
+
data.tar.gz: a8cc4f9eed82b92905be0961adea425731ad5741e941b5eb6aa669341dbc1bb97ba6540721f9734497009823075769257fccae7ae4416b84ba76fa172b5d6694
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [X.X.X] - YYYY-MM-DD
|
|
4
4
|
|
|
5
|
+
## [0.7.0] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
- Introduce `Query` entity encapsulating parsed clauses, depth, table registry, compact detection, and subquery formatting — `Formatter` delegates clause assembly and rendering to `Query`, and `SubqueryFormatter` is eliminated
|
|
8
|
+
- Introduce `CteQuery` and `CteDefinition` entities replacing `CteFormatter` — CTE parsing produces structured objects that render themselves
|
|
9
|
+
- Introduce `CreateTableAs` entity replacing `CreateTableAsFormatter` — structured object with modifier, if-not-exists, table name, body query, and suffix
|
|
10
|
+
- Introduce `Condition` tree model replacing flat `[conjunction, text]` pairs — parsed into leaf and group nodes with recursive rendering; eliminates `ConditionFormatter`
|
|
11
|
+
- Extract `Scanner` class consolidating duplicated character-by-character scanning logic across seven modules
|
|
12
|
+
- Consolidate `CommentStripper` and `CommentRestorer` into `CommentParser`
|
|
13
|
+
- Introduce `TableReference` and `Join` entities — `Clauses::From` delegates join rendering to `Join#render`; `TableRegistry` holds `TableReference` objects instead of raw hashes
|
|
14
|
+
- Introduce `Expression` entity for SELECT list items and `SortExpression` entity for ORDER BY items
|
|
15
|
+
- Introduce `Comment` entity with `content`, `type`, and `renderable` attributes
|
|
16
|
+
|
|
5
17
|
## [0.6.0] - 2026-03-28
|
|
6
18
|
|
|
7
19
|
- **Breaking**: comments are now preserved by default. Set `removable_comment_types = :all` to restore previous behavior of stripping all comments
|
data/README.md
CHANGED
|
@@ -445,12 +445,12 @@ end
|
|
|
445
445
|
Controls which SQL comment types are stripped during formatting. Default: `:none`.
|
|
446
446
|
|
|
447
447
|
- `:none` — preserves all comments in the formatted output
|
|
448
|
-
- `:all` — strips all comments (equivalent to `[:inline, :
|
|
448
|
+
- `:all` — strips all comments (equivalent to `[:inline, :line, :blocks]`)
|
|
449
449
|
- Array of specific types — strips only the listed types, preserving the rest
|
|
450
450
|
|
|
451
451
|
The three comment types:
|
|
452
452
|
|
|
453
|
-
- `:
|
|
453
|
+
- `:line` — `--` comments on their own line (only whitespace before `--`), including banner-style dividers
|
|
454
454
|
- `:inline` — `--` comments at the end of a line that contains SQL
|
|
455
455
|
- `:blocks` — `/* ... */` block comments (single or multi-line)
|
|
456
456
|
|
|
@@ -6,7 +6,7 @@ module SqlBeautifier
|
|
|
6
6
|
def call
|
|
7
7
|
return "#{keyword_prefix}#{@value.strip}" unless multiple_conditions?
|
|
8
8
|
|
|
9
|
-
formatted_conditions =
|
|
9
|
+
formatted_conditions = Condition.format(@value, indent_width: SqlBeautifier.config_for(:keyword_column_width))
|
|
10
10
|
formatted_conditions.sub(continuation_indent, keyword_prefix)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -5,66 +5,41 @@ module SqlBeautifier
|
|
|
5
5
|
class From < Base
|
|
6
6
|
KEYWORD = "from"
|
|
7
7
|
|
|
8
|
-
def self.call(
|
|
9
|
-
new(
|
|
8
|
+
def self.call(...)
|
|
9
|
+
new(...).call
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(value, table_registry:)
|
|
13
13
|
super(value)
|
|
14
|
+
|
|
14
15
|
@table_registry = table_registry
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def call
|
|
18
|
-
@lines = []
|
|
19
|
-
|
|
20
19
|
join_parts = split_join_parts
|
|
21
20
|
primary_table_text = join_parts.shift.strip
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
primary_reference = @table_registry.reference_for(Util.first_word(primary_table_text))
|
|
22
|
+
trailing_sentinels = Join.extract_trailing_sentinels(primary_table_text)
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
lines = []
|
|
25
|
+
lines << "#{keyword_prefix}#{primary_reference.render(trailing_sentinels: trailing_sentinels)}"
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
join_parts.each do |join_text|
|
|
28
|
+
join = Join.parse(join_text, table_registry: @table_registry)
|
|
29
|
+
next unless join
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
lines << join.render(continuation_indent: continuation_indent, condition_indent: join_condition_indentation)
|
|
32
|
+
end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
@lines << line
|
|
34
|
+
lines.join("\n")
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
private
|
|
38
|
+
|
|
36
39
|
def join_condition_indentation
|
|
37
40
|
Util.whitespace(SqlBeautifier.config_for(:keyword_column_width) + 4)
|
|
38
41
|
end
|
|
39
42
|
|
|
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
43
|
def split_join_parts
|
|
69
44
|
from_content = @value.strip
|
|
70
45
|
join_keyword_positions = find_all_join_keyword_positions(from_content)
|
|
@@ -122,35 +97,6 @@ module SqlBeautifier
|
|
|
122
97
|
|
|
123
98
|
earliest_match
|
|
124
99
|
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
|
-
trailing_sentinels = extract_trailing_sentinels(table_text)
|
|
145
|
-
|
|
146
|
-
formatted = table_alias ? "#{formatted_table_name} #{table_alias}" : formatted_table_name
|
|
147
|
-
trailing_sentinels.empty? ? formatted : "#{formatted} #{trailing_sentinels}"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def extract_trailing_sentinels(text)
|
|
151
|
-
sentinels = text.scan(CommentStripper::SENTINEL_PATTERN).map { |match| "#{CommentStripper::SENTINEL_PREFIX}#{match[0]}#{CommentStripper::SENTINEL_SUFFIX}" }
|
|
152
|
-
sentinels.join(" ")
|
|
153
|
-
end
|
|
154
100
|
end
|
|
155
101
|
end
|
|
156
102
|
end
|
|
@@ -6,7 +6,18 @@ module SqlBeautifier
|
|
|
6
6
|
KEYWORD = "order by"
|
|
7
7
|
|
|
8
8
|
def call
|
|
9
|
-
|
|
9
|
+
expressions = parse_expressions(@value)
|
|
10
|
+
expressions_output = expressions.map(&:render).join(", ")
|
|
11
|
+
|
|
12
|
+
"#{keyword_prefix}#{expressions_output}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def parse_expressions(value)
|
|
18
|
+
Tokenizer.split_by_top_level_commas(value).map do |item|
|
|
19
|
+
SortExpression.parse(item)
|
|
20
|
+
end
|
|
10
21
|
end
|
|
11
22
|
end
|
|
12
23
|
end
|
|
@@ -10,34 +10,45 @@ module SqlBeautifier
|
|
|
10
10
|
|
|
11
11
|
def call
|
|
12
12
|
prefix, remaining_columns = extract_prefix
|
|
13
|
-
|
|
13
|
+
@expressions = parse_expressions(remaining_columns)
|
|
14
14
|
|
|
15
|
-
return format_with_prefix(prefix
|
|
16
|
-
return keyword_line(
|
|
15
|
+
return format_with_prefix(prefix) if prefix
|
|
16
|
+
return keyword_line(@expressions.first.render) if @expressions.length == 1
|
|
17
17
|
|
|
18
|
-
format_columns_list
|
|
18
|
+
format_columns_list
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
|
|
23
|
+
def parse_expressions(value)
|
|
24
|
+
Tokenizer.split_by_top_level_commas(value).map do |column|
|
|
25
|
+
Expression.parse(column)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def keyword_line(text)
|
|
30
|
+
"#{keyword_prefix}#{text.strip}"
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
def continuation_line(
|
|
28
|
-
"#{continuation_indent}#{
|
|
33
|
+
def continuation_line(text)
|
|
34
|
+
"#{continuation_indent}#{text.strip}"
|
|
29
35
|
end
|
|
30
36
|
|
|
31
|
-
def format_with_prefix(prefix
|
|
37
|
+
def format_with_prefix(prefix)
|
|
32
38
|
first_line = "#{keyword_prefix}#{prefix}"
|
|
33
|
-
column_lines =
|
|
39
|
+
column_lines = @expressions.map do |expression|
|
|
40
|
+
continuation_line(expression.render)
|
|
41
|
+
end
|
|
34
42
|
|
|
35
43
|
"#{first_line}\n#{column_lines.join(",\n")}"
|
|
36
44
|
end
|
|
37
45
|
|
|
38
|
-
def format_columns_list
|
|
39
|
-
column_lines =
|
|
40
|
-
|
|
46
|
+
def format_columns_list
|
|
47
|
+
column_lines = @expressions.map do |expression|
|
|
48
|
+
continuation_line(expression.render)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
column_lines[0] = keyword_line(@expressions.first.render)
|
|
41
52
|
|
|
42
53
|
column_lines.join(",\n")
|
|
43
54
|
end
|
|
@@ -61,11 +72,13 @@ module SqlBeautifier
|
|
|
61
72
|
opening_parenthesis_position = stripped_value.index(Constants::OPEN_PARENTHESIS, distinct_on_position)
|
|
62
73
|
return [nil, stripped_value] unless opening_parenthesis_position
|
|
63
74
|
|
|
64
|
-
closing_parenthesis_position =
|
|
75
|
+
closing_parenthesis_position = Scanner.new(stripped_value).find_matching_parenthesis(opening_parenthesis_position)
|
|
65
76
|
return [nil, stripped_value] unless closing_parenthesis_position
|
|
66
77
|
|
|
67
78
|
prefix = stripped_value[0..closing_parenthesis_position]
|
|
68
|
-
|
|
79
|
+
columns_text = stripped_value[(closing_parenthesis_position + 1)..]
|
|
80
|
+
stripped_columns_text = columns_text.strip
|
|
81
|
+
remaining_columns = stripped_columns_text.sub(LEADING_COMMA_PATTERN, "")
|
|
69
82
|
|
|
70
83
|
[prefix, remaining_columns]
|
|
71
84
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Comment < Base
|
|
5
|
+
TYPES = %i[
|
|
6
|
+
inline
|
|
7
|
+
line
|
|
8
|
+
blocks
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
param :content
|
|
12
|
+
option :type, type: Types::Coercible::Symbol.enum(*TYPES), default: -> { :line }
|
|
13
|
+
option :renderable, type: Types::Bool, optional: true, default: -> { true }
|
|
14
|
+
|
|
15
|
+
def renderable?
|
|
16
|
+
@renderable
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render
|
|
20
|
+
@content
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,25 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
|
-
class
|
|
4
|
+
class CommentParser
|
|
5
5
|
SENTINEL_PREFIX = "/*__sqlb_"
|
|
6
6
|
SENTINEL_SUFFIX = "__*/"
|
|
7
7
|
SENTINEL_PATTERN = %r{/\*__sqlb_(\d+)__\*/}
|
|
8
8
|
|
|
9
|
-
Result = Struct.new(:stripped_sql, :comment_map)
|
|
9
|
+
Result = Struct.new(:stripped_sql, :comment_map, :comments)
|
|
10
10
|
|
|
11
11
|
def self.call(sql, removable_types)
|
|
12
12
|
new(sql, removable_types).call
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def self.restore(formatted_sql, comment_map)
|
|
16
|
+
return formatted_sql if comment_map.empty?
|
|
17
|
+
|
|
18
|
+
result = formatted_sql
|
|
19
|
+
|
|
20
|
+
comment_map.each do |index, entry|
|
|
21
|
+
sentinel = "#{SENTINEL_PREFIX}#{index}#{SENTINEL_SUFFIX}"
|
|
22
|
+
|
|
23
|
+
result = begin
|
|
24
|
+
case entry[:type]
|
|
25
|
+
when :blocks
|
|
26
|
+
result.sub(sentinel, entry[:text])
|
|
27
|
+
when :line
|
|
28
|
+
result.sub(%r{#{Regexp.escape(sentinel)}[ \n]?}, "#{entry[:text]}\n")
|
|
29
|
+
when :inline
|
|
30
|
+
restore_inline_comment(result, sentinel, entry[:text])
|
|
31
|
+
else
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.restore_inline_comment(sql, sentinel, comment_text)
|
|
41
|
+
pattern = %r{ ?#{Regexp.escape(sentinel)}([^\n]*)}
|
|
42
|
+
sql.sub(pattern) do
|
|
43
|
+
trailing_content = Regexp.last_match(1)
|
|
44
|
+
|
|
45
|
+
if trailing_content.strip.empty?
|
|
46
|
+
" #{comment_text}"
|
|
47
|
+
else
|
|
48
|
+
"#{trailing_content.strip} #{comment_text}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
15
53
|
def initialize(sql, removable_types)
|
|
16
54
|
@sql = sql
|
|
17
55
|
@removal_set = resolve_removal_set(removable_types)
|
|
18
56
|
@output = +""
|
|
19
57
|
@comment_map = {}
|
|
58
|
+
@comments = []
|
|
20
59
|
@sentinel_index = 0
|
|
21
60
|
@position = 0
|
|
22
|
-
@
|
|
61
|
+
@pending_line_comments = []
|
|
23
62
|
end
|
|
24
63
|
|
|
25
64
|
def call
|
|
@@ -31,30 +70,30 @@ module SqlBeautifier
|
|
|
31
70
|
elsif @in_double_quoted_identifier
|
|
32
71
|
consume_double_quoted_character!(character)
|
|
33
72
|
elsif character == Constants::SINGLE_QUOTE
|
|
34
|
-
|
|
73
|
+
flush_pending_line_comments!
|
|
35
74
|
@in_single_quoted_string = true
|
|
36
75
|
@output << character
|
|
37
76
|
@position += 1
|
|
38
77
|
elsif character == Constants::DOUBLE_QUOTE
|
|
39
|
-
|
|
78
|
+
flush_pending_line_comments!
|
|
40
79
|
@in_double_quoted_identifier = true
|
|
41
80
|
@output << character
|
|
42
81
|
@position += 1
|
|
43
82
|
elsif line_comment_start?
|
|
44
83
|
handle_line_comment!
|
|
45
84
|
elsif block_comment_start?
|
|
46
|
-
|
|
85
|
+
flush_pending_line_comments!
|
|
47
86
|
handle_block_comment!
|
|
48
87
|
else
|
|
49
|
-
|
|
88
|
+
flush_pending_line_comments! unless character == "\n"
|
|
50
89
|
@output << character
|
|
51
90
|
@position += 1
|
|
52
91
|
end
|
|
53
92
|
end
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
flush_pending_line_comments!
|
|
56
95
|
|
|
57
|
-
Result.new(@output, @comment_map)
|
|
96
|
+
Result.new(@output, @comment_map, @comments)
|
|
58
97
|
end
|
|
59
98
|
|
|
60
99
|
private
|
|
@@ -64,16 +103,16 @@ module SqlBeautifier
|
|
|
64
103
|
when :none
|
|
65
104
|
[]
|
|
66
105
|
when :all
|
|
67
|
-
|
|
106
|
+
Comment::TYPES.dup
|
|
68
107
|
when Array
|
|
69
|
-
invalid_types = removable_types -
|
|
70
|
-
raise ArgumentError, "Unsupported removable_types entries: #{invalid_types.inspect}. Expected elements of #{
|
|
108
|
+
invalid_types = removable_types - Comment::TYPES
|
|
109
|
+
raise ArgumentError, "Unsupported removable_types entries: #{invalid_types.inspect}. Expected elements of #{Comment::TYPES.inspect}" if invalid_types.any?
|
|
71
110
|
|
|
72
111
|
removable_types
|
|
73
|
-
when *
|
|
112
|
+
when *Comment::TYPES
|
|
74
113
|
[removable_types]
|
|
75
114
|
else
|
|
76
|
-
raise ArgumentError, "Unsupported removable_types: #{removable_types.inspect}. Expected :none, :all, an Array, or one of #{
|
|
115
|
+
raise ArgumentError, "Unsupported removable_types: #{removable_types.inspect}. Expected :none, :all, an Array, or one of #{Comment::TYPES.inspect}"
|
|
77
116
|
end
|
|
78
117
|
end
|
|
79
118
|
|
|
@@ -112,9 +151,11 @@ module SqlBeautifier
|
|
|
112
151
|
end
|
|
113
152
|
|
|
114
153
|
def handle_line_comment!
|
|
115
|
-
comment_type =
|
|
154
|
+
comment_type = line_comment? ? :line : :inline
|
|
116
155
|
comment_text = extract_line_comment_text
|
|
117
156
|
|
|
157
|
+
@comments << Comment.new(comment_text, type: comment_type, renderable: !removable?(comment_type))
|
|
158
|
+
|
|
118
159
|
if removable?(comment_type)
|
|
119
160
|
strip_line_comment!
|
|
120
161
|
else
|
|
@@ -122,7 +163,7 @@ module SqlBeautifier
|
|
|
122
163
|
end
|
|
123
164
|
end
|
|
124
165
|
|
|
125
|
-
def
|
|
166
|
+
def line_comment?
|
|
126
167
|
line_start = @output.rindex("\n")
|
|
127
168
|
preceding_content = begin
|
|
128
169
|
if line_start
|
|
@@ -147,11 +188,11 @@ module SqlBeautifier
|
|
|
147
188
|
def strip_line_comment!; end
|
|
148
189
|
|
|
149
190
|
def preserve_line_comment!(comment_type, comment_text)
|
|
150
|
-
if comment_type == :
|
|
151
|
-
@
|
|
191
|
+
if comment_type == :line
|
|
192
|
+
@pending_line_comments << comment_text
|
|
152
193
|
@position += 1 if @position < @sql.length && @sql[@position] == "\n"
|
|
153
194
|
else
|
|
154
|
-
|
|
195
|
+
flush_pending_line_comments!
|
|
155
196
|
sentinel = build_sentinel(comment_type, comment_text)
|
|
156
197
|
@output << sentinel
|
|
157
198
|
end
|
|
@@ -160,6 +201,8 @@ module SqlBeautifier
|
|
|
160
201
|
def handle_block_comment!
|
|
161
202
|
comment_text = extract_block_comment_text
|
|
162
203
|
|
|
204
|
+
@comments << Comment.new(comment_text, type: :blocks, renderable: !removable?(:blocks))
|
|
205
|
+
|
|
163
206
|
if removable?(:blocks)
|
|
164
207
|
strip_block_comment!
|
|
165
208
|
else
|
|
@@ -192,16 +235,16 @@ module SqlBeautifier
|
|
|
192
235
|
@output << " " unless @output.empty? || @output[-1] =~ Constants::WHITESPACE_CHARACTER_REGEX
|
|
193
236
|
end
|
|
194
237
|
|
|
195
|
-
def
|
|
196
|
-
return if @
|
|
238
|
+
def flush_pending_line_comments!
|
|
239
|
+
return if @pending_line_comments.empty?
|
|
197
240
|
|
|
198
|
-
grouped_text = @
|
|
199
|
-
sentinel = build_sentinel(:
|
|
241
|
+
grouped_text = @pending_line_comments.join("\n")
|
|
242
|
+
sentinel = build_sentinel(:line, grouped_text)
|
|
200
243
|
|
|
201
244
|
@output << sentinel
|
|
202
245
|
@output << "\n"
|
|
203
246
|
|
|
204
|
-
@
|
|
247
|
+
@pending_line_comments.clear
|
|
205
248
|
end
|
|
206
249
|
|
|
207
250
|
def build_sentinel(comment_type, comment_text)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Condition < Base
|
|
5
|
+
option :conjunction, default: -> {}
|
|
6
|
+
option :expression, default: -> {}
|
|
7
|
+
option :children, default: -> {}
|
|
8
|
+
|
|
9
|
+
def self.format(text, indent_width: 0)
|
|
10
|
+
conditions = parse_all(text)
|
|
11
|
+
return text.strip if conditions.length <= 1 && conditions.first&.leaf?
|
|
12
|
+
|
|
13
|
+
render_all(conditions, indent_width: indent_width)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.parse_all(text)
|
|
17
|
+
raw_pairs = Tokenizer.split_top_level_conditions(text)
|
|
18
|
+
conditions = raw_pairs.map do |conjunction, condition_text|
|
|
19
|
+
build(conjunction, condition_text)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
flatten_same_conjunction_groups(conditions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.render_all(conditions, indent_width:)
|
|
26
|
+
indentation = Util.whitespace(indent_width)
|
|
27
|
+
lines = []
|
|
28
|
+
|
|
29
|
+
conditions.each_with_index do |condition, index|
|
|
30
|
+
rendered = condition.render(indent_width: indent_width)
|
|
31
|
+
|
|
32
|
+
line = begin
|
|
33
|
+
if index.zero?
|
|
34
|
+
"#{indentation}#{rendered}"
|
|
35
|
+
else
|
|
36
|
+
"#{indentation}#{condition.conjunction} #{rendered}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
lines << line
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
lines.join("\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def leaf?
|
|
47
|
+
@children.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def group?
|
|
51
|
+
!leaf?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render(indent_width:)
|
|
55
|
+
return @expression if leaf?
|
|
56
|
+
|
|
57
|
+
inline_version = render_inline
|
|
58
|
+
return inline_version if inline_version.length <= SqlBeautifier.config_for(:inline_group_threshold)
|
|
59
|
+
|
|
60
|
+
inner_output = self.class.render_all(@children, indent_width: indent_width + 4)
|
|
61
|
+
closing_indentation = Util.whitespace(indent_width)
|
|
62
|
+
|
|
63
|
+
"(\n#{inner_output}\n#{closing_indentation})"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.build(conjunction, condition_text)
|
|
67
|
+
unwrapped = unwrap_single_condition(condition_text)
|
|
68
|
+
inner_conditions = parse_condition_group(unwrapped)
|
|
69
|
+
|
|
70
|
+
if inner_conditions
|
|
71
|
+
children = inner_conditions.map do |inner_conjunction, inner_text|
|
|
72
|
+
build(inner_conjunction, inner_text)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
new(conjunction: conjunction, children: children)
|
|
76
|
+
else
|
|
77
|
+
new(conjunction: conjunction, expression: unwrapped)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.unwrap_single_condition(condition_text)
|
|
82
|
+
output = condition_text.strip
|
|
83
|
+
|
|
84
|
+
while Tokenizer.outer_parentheses_wrap_all?(output)
|
|
85
|
+
inner_content = Util.strip_outer_parentheses(output)
|
|
86
|
+
inner_conditions = Tokenizer.split_top_level_conditions(inner_content)
|
|
87
|
+
break if inner_conditions.length > 1
|
|
88
|
+
|
|
89
|
+
output = inner_content
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
output
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.parse_condition_group(condition_text)
|
|
96
|
+
return unless condition_text
|
|
97
|
+
|
|
98
|
+
trimmed = condition_text.strip
|
|
99
|
+
return unless Tokenizer.outer_parentheses_wrap_all?(trimmed)
|
|
100
|
+
|
|
101
|
+
inner_content = Util.strip_outer_parentheses(trimmed)
|
|
102
|
+
inner_conditions = Tokenizer.split_top_level_conditions(inner_content)
|
|
103
|
+
return unless inner_conditions.length > 1
|
|
104
|
+
|
|
105
|
+
inner_conditions
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.flatten_same_conjunction_groups(conditions)
|
|
109
|
+
return conditions if conditions.length <= 1
|
|
110
|
+
|
|
111
|
+
outer_conjunction = conditions[1]&.conjunction
|
|
112
|
+
return conditions unless outer_conjunction
|
|
113
|
+
return conditions unless conditions.drop(1).all? { |condition| condition.conjunction == outer_conjunction }
|
|
114
|
+
|
|
115
|
+
flattened = []
|
|
116
|
+
|
|
117
|
+
conditions.each do |condition|
|
|
118
|
+
if condition.group? && flattenable_into_conjunction?(condition, outer_conjunction)
|
|
119
|
+
flatten_group_into!(flattened, condition, outer_conjunction)
|
|
120
|
+
else
|
|
121
|
+
flattened << condition
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
flattened
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.flattenable_into_conjunction?(condition, outer_conjunction)
|
|
129
|
+
return false unless condition.group?
|
|
130
|
+
|
|
131
|
+
inner_conjunction = condition.children[1]&.conjunction
|
|
132
|
+
inner_conjunction == outer_conjunction && condition.children.drop(1).all? { |child| child.conjunction == outer_conjunction }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.flatten_group_into!(flattened, group_condition, outer_conjunction)
|
|
136
|
+
group_condition.children.each_with_index do |child, inner_index|
|
|
137
|
+
new_conjunction = begin
|
|
138
|
+
if flattened.empty?
|
|
139
|
+
nil
|
|
140
|
+
elsif inner_index.zero?
|
|
141
|
+
group_condition.conjunction || outer_conjunction
|
|
142
|
+
else
|
|
143
|
+
outer_conjunction
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
flattened << new(conjunction: new_conjunction, expression: child.expression, children: child.children)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
protected
|
|
152
|
+
|
|
153
|
+
def render_inline
|
|
154
|
+
parts = @children.map.with_index do |child, index|
|
|
155
|
+
rendered = child.leaf? ? child.expression : child.render_inline
|
|
156
|
+
index.zero? ? rendered : "#{child.conjunction} #{rendered}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
"(#{parts.join(' ')})"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|