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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
|
-
class Configuration
|
|
4
|
+
class Configuration < Base
|
|
5
5
|
DEFAULTS = {
|
|
6
6
|
keyword_case: :lower,
|
|
7
7
|
keyword_column_width: 8,
|
|
@@ -14,20 +14,9 @@ module SqlBeautifier
|
|
|
14
14
|
removable_comment_types: :none,
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
attr_accessor :keyword_column_width
|
|
21
|
-
attr_accessor :indent_spaces
|
|
22
|
-
attr_accessor :clause_spacing_mode
|
|
23
|
-
attr_accessor :table_name_format
|
|
24
|
-
attr_accessor :inline_group_threshold
|
|
25
|
-
attr_accessor :alias_strategy
|
|
26
|
-
attr_accessor :trailing_semicolon
|
|
27
|
-
attr_accessor :removable_comment_types
|
|
28
|
-
|
|
29
|
-
def initialize
|
|
30
|
-
reset!
|
|
17
|
+
DEFAULTS.each_key do |key|
|
|
18
|
+
option key, default: -> { DEFAULTS[key] }
|
|
19
|
+
attr_writer key
|
|
31
20
|
end
|
|
32
21
|
|
|
33
22
|
def reset!
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class CreateTableAs < Base
|
|
5
|
+
MODIFIERS = %w[temp temporary unlogged local].freeze
|
|
6
|
+
WITH_DATA_SUFFIX_REGEX = %r{\s+(with\s+(?:no\s+)?data)\s*\z}i
|
|
7
|
+
|
|
8
|
+
option :modifier, default: -> {}
|
|
9
|
+
option :if_not_exists, type: Types::Bool
|
|
10
|
+
option :table_name
|
|
11
|
+
option :body_sql
|
|
12
|
+
option :suffix, default: -> {}
|
|
13
|
+
option :depth, default: -> { 0 }
|
|
14
|
+
|
|
15
|
+
def self.parse(normalized_sql, depth: 0)
|
|
16
|
+
scanner = Scanner.new(normalized_sql)
|
|
17
|
+
return nil unless scanner.keyword_at?("create")
|
|
18
|
+
|
|
19
|
+
scanner.skip_past_keyword!("create")
|
|
20
|
+
modifier = detect_modifier(scanner)
|
|
21
|
+
scanner.skip_past_keyword!(modifier) if modifier
|
|
22
|
+
|
|
23
|
+
return nil unless scanner.keyword_at?("table")
|
|
24
|
+
|
|
25
|
+
scanner.skip_past_keyword!("table")
|
|
26
|
+
|
|
27
|
+
if_not_exists = detect_if_not_exists?(scanner)
|
|
28
|
+
skip_past_if_not_exists!(scanner) if if_not_exists
|
|
29
|
+
|
|
30
|
+
table_name = scanner.read_identifier!
|
|
31
|
+
return nil unless table_name
|
|
32
|
+
|
|
33
|
+
scanner.skip_whitespace!
|
|
34
|
+
return nil unless scanner.keyword_at?("as")
|
|
35
|
+
|
|
36
|
+
scanner.skip_past_keyword!("as")
|
|
37
|
+
|
|
38
|
+
body_sql, suffix = extract_body(normalized_sql, scanner.position)
|
|
39
|
+
return nil unless body_sql
|
|
40
|
+
|
|
41
|
+
new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, body_sql: body_sql, suffix: suffix, depth: depth)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.detect_modifier(scanner)
|
|
45
|
+
MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.detect_if_not_exists?(scanner)
|
|
49
|
+
return false unless scanner.keyword_at?("if")
|
|
50
|
+
|
|
51
|
+
probe = Scanner.new(scanner.source, position: scanner.position)
|
|
52
|
+
probe.skip_past_keyword!("if")
|
|
53
|
+
return false unless probe.keyword_at?("not")
|
|
54
|
+
|
|
55
|
+
probe.skip_past_keyword!("not")
|
|
56
|
+
probe.keyword_at?("exists")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.skip_past_if_not_exists!(scanner)
|
|
60
|
+
scanner.skip_past_keyword!("if")
|
|
61
|
+
scanner.skip_past_keyword!("not")
|
|
62
|
+
scanner.skip_past_keyword!("exists")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.extract_body(sql, position)
|
|
66
|
+
scanner = Scanner.new(sql, position: position)
|
|
67
|
+
scanner.skip_whitespace!
|
|
68
|
+
return nil if scanner.finished?
|
|
69
|
+
|
|
70
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
71
|
+
closing = scanner.find_matching_parenthesis(scanner.position)
|
|
72
|
+
return nil unless closing
|
|
73
|
+
|
|
74
|
+
body = sql[(scanner.position + 1)...closing].strip
|
|
75
|
+
suffix = sql[(closing + 1)..].strip.presence
|
|
76
|
+
[body, suffix]
|
|
77
|
+
else
|
|
78
|
+
extract_unparenthesized_body(sql[scanner.position..].strip)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.extract_unparenthesized_body(raw_body)
|
|
83
|
+
return nil unless raw_body.present?
|
|
84
|
+
|
|
85
|
+
match = raw_body.match(WITH_DATA_SUFFIX_REGEX)
|
|
86
|
+
|
|
87
|
+
if match
|
|
88
|
+
body = raw_body[0...match.begin(0)].strip
|
|
89
|
+
return nil unless body.present?
|
|
90
|
+
|
|
91
|
+
[body, match[1]]
|
|
92
|
+
else
|
|
93
|
+
[raw_body, nil]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render
|
|
98
|
+
indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
|
|
99
|
+
formatted = Formatter.new(@body_sql, depth: 0).call
|
|
100
|
+
return "#{preamble}\n" unless formatted
|
|
101
|
+
|
|
102
|
+
indentation = Util.whitespace(indent_spaces)
|
|
103
|
+
indented_lines = formatted.chomp.lines.map do |line|
|
|
104
|
+
line.strip.empty? ? "\n" : "#{indentation}#{line}"
|
|
105
|
+
end.join
|
|
106
|
+
|
|
107
|
+
formatted_suffix = @suffix ? " #{format_suffix}" : ""
|
|
108
|
+
"#{preamble} (\n#{indented_lines}\n)#{formatted_suffix}\n"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def preamble
|
|
114
|
+
parts = [Util.format_keyword("create")]
|
|
115
|
+
parts << Util.format_keyword(@modifier) if @modifier
|
|
116
|
+
parts << Util.format_keyword("table")
|
|
117
|
+
parts << "#{Util.format_keyword('if')} #{Util.format_keyword('not')} #{Util.format_keyword('exists')}" if @if_not_exists
|
|
118
|
+
parts << Util.format_table_name(@table_name)
|
|
119
|
+
parts << Util.format_keyword("as")
|
|
120
|
+
parts.join(" ")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def format_suffix
|
|
124
|
+
@suffix.strip.split(Constants::WHITESPACE_REGEX).map { |word| Util.format_keyword(word) }.join(" ")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class CteDefinition < Base
|
|
5
|
+
option :name
|
|
6
|
+
option :body_sql
|
|
7
|
+
option :column_list, default: -> {}
|
|
8
|
+
option :materialization, default: -> {}
|
|
9
|
+
|
|
10
|
+
def render_header
|
|
11
|
+
header = +@name.to_s
|
|
12
|
+
header << " (#{@column_list})" if @column_list
|
|
13
|
+
header << " #{Util.format_keyword('as')}"
|
|
14
|
+
header << " #{format_materialization}" if @materialization
|
|
15
|
+
header << " "
|
|
16
|
+
header
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render_body(base_indent)
|
|
20
|
+
indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
|
|
21
|
+
body_indent = base_indent + indent_spaces
|
|
22
|
+
formatted = Formatter.new(@body_sql, depth: 0).call
|
|
23
|
+
return "(#{@body_sql})" unless formatted
|
|
24
|
+
|
|
25
|
+
indentation = Util.whitespace(body_indent)
|
|
26
|
+
indented_lines = formatted.chomp.lines.map do |line|
|
|
27
|
+
line.strip.empty? ? "\n" : "#{indentation}#{line}"
|
|
28
|
+
end.join
|
|
29
|
+
|
|
30
|
+
"(\n#{indented_lines}\n#{Util.whitespace(base_indent)})"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def format_materialization
|
|
36
|
+
return Util.format_keyword("materialized") if @materialization == "materialized"
|
|
37
|
+
|
|
38
|
+
[Util.format_keyword("not"), Util.format_keyword("materialized")].join(" ")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class CteQuery < Base
|
|
5
|
+
option :recursive, type: Types::Bool
|
|
6
|
+
option :definitions
|
|
7
|
+
option :main_query_sql
|
|
8
|
+
option :depth, default: -> { 0 }
|
|
9
|
+
|
|
10
|
+
def self.parse(normalized_sql, depth: 0)
|
|
11
|
+
scanner = Scanner.new(normalized_sql)
|
|
12
|
+
return nil unless scanner.keyword_at?("with")
|
|
13
|
+
|
|
14
|
+
scanner.skip_past_keyword!("with")
|
|
15
|
+
|
|
16
|
+
recursive = scanner.keyword_at?("recursive")
|
|
17
|
+
scanner.skip_past_keyword!("recursive") if recursive
|
|
18
|
+
|
|
19
|
+
definitions = []
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
definition, new_position = parse_raw_definition(normalized_sql, scanner.position)
|
|
23
|
+
break unless definition
|
|
24
|
+
|
|
25
|
+
definitions << CteDefinition.new(**definition)
|
|
26
|
+
scanner.advance!(new_position - scanner.position)
|
|
27
|
+
scanner.skip_whitespace!
|
|
28
|
+
|
|
29
|
+
break unless scanner.position < normalized_sql.length && scanner.current_char == Constants::COMMA
|
|
30
|
+
|
|
31
|
+
scanner.advance!
|
|
32
|
+
scanner.skip_whitespace!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
main_query_sql = normalized_sql[scanner.position..].strip
|
|
36
|
+
return nil unless definitions.any? && main_query_sql.present?
|
|
37
|
+
|
|
38
|
+
new(recursive: recursive, definitions: definitions, main_query_sql: main_query_sql, depth: depth)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.parse_raw_definition(sql, position)
|
|
42
|
+
scanner = Scanner.new(sql, position: position)
|
|
43
|
+
|
|
44
|
+
name = scanner.read_identifier!
|
|
45
|
+
return nil unless name
|
|
46
|
+
|
|
47
|
+
scanner.skip_whitespace!
|
|
48
|
+
|
|
49
|
+
column_list = parse_column_list(sql, scanner.position)
|
|
50
|
+
scanner.advance!(column_list[:next_position] - scanner.position) if column_list
|
|
51
|
+
|
|
52
|
+
return nil unless scanner.keyword_at?("as")
|
|
53
|
+
|
|
54
|
+
scanner.skip_past_keyword!("as")
|
|
55
|
+
materialization, materialization_end_position = parse_materialization(sql, scanner.position)
|
|
56
|
+
scanner.advance!(materialization_end_position - scanner.position)
|
|
57
|
+
|
|
58
|
+
return nil unless scanner.position < sql.length && scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
59
|
+
|
|
60
|
+
closing = scanner.find_matching_parenthesis(scanner.position)
|
|
61
|
+
return nil unless closing
|
|
62
|
+
|
|
63
|
+
body_sql = sql[(scanner.position + 1)...closing].strip
|
|
64
|
+
result = { name: name, body_sql: body_sql }
|
|
65
|
+
result[:column_list] = column_list[:text] if column_list
|
|
66
|
+
result[:materialization] = materialization if materialization
|
|
67
|
+
|
|
68
|
+
[result, closing + 1]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.parse_column_list(sql, position)
|
|
72
|
+
return nil unless position < sql.length && sql[position] == Constants::OPEN_PARENTHESIS
|
|
73
|
+
|
|
74
|
+
scanner = Scanner.new(sql)
|
|
75
|
+
closing = scanner.find_matching_parenthesis(position)
|
|
76
|
+
return nil unless closing
|
|
77
|
+
|
|
78
|
+
after_paren_scanner = Scanner.new(sql, position: closing + 1)
|
|
79
|
+
after_paren_scanner.skip_whitespace!
|
|
80
|
+
return nil unless after_paren_scanner.keyword_at?("as")
|
|
81
|
+
|
|
82
|
+
{ text: sql[(position + 1)...closing].strip, next_position: after_paren_scanner.position }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.parse_materialization(sql, position)
|
|
86
|
+
scanner = Scanner.new(sql, position: position)
|
|
87
|
+
scanner.skip_whitespace!
|
|
88
|
+
|
|
89
|
+
if scanner.keyword_at?("materialized")
|
|
90
|
+
scanner.skip_past_keyword!("materialized")
|
|
91
|
+
return ["materialized", scanner.position]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return [nil, scanner.position] unless scanner.keyword_at?("not")
|
|
95
|
+
|
|
96
|
+
scanner.skip_past_keyword!("not")
|
|
97
|
+
|
|
98
|
+
return [nil, position] unless scanner.keyword_at?("materialized")
|
|
99
|
+
|
|
100
|
+
scanner.skip_past_keyword!("materialized")
|
|
101
|
+
["not materialized", scanner.position]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def render
|
|
105
|
+
keyword_width = SqlBeautifier.config_for(:keyword_column_width)
|
|
106
|
+
continuation_indent = Util.continuation_padding
|
|
107
|
+
|
|
108
|
+
output = +""
|
|
109
|
+
|
|
110
|
+
@definitions.each_with_index do |definition, index|
|
|
111
|
+
if index.zero?
|
|
112
|
+
output << Util.keyword_padding("with")
|
|
113
|
+
output << "#{Util.format_keyword('recursive')} " if @recursive
|
|
114
|
+
else
|
|
115
|
+
output << continuation_indent
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
output << definition.render_header
|
|
119
|
+
output << definition.render_body(keyword_width)
|
|
120
|
+
output << (index < @definitions.length - 1 ? ",\n" : "\n\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
formatted_main = Formatter.new(@main_query_sql, depth: @depth).call
|
|
124
|
+
output << formatted_main if formatted_main
|
|
125
|
+
|
|
126
|
+
output
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Expression < Base
|
|
5
|
+
option :definition
|
|
6
|
+
option :alias_name, default: -> {}
|
|
7
|
+
|
|
8
|
+
def self.parse(text)
|
|
9
|
+
stripped = text.strip
|
|
10
|
+
alias_position = find_top_level_as(stripped)
|
|
11
|
+
|
|
12
|
+
if alias_position
|
|
13
|
+
definition = stripped[0...alias_position].strip
|
|
14
|
+
alias_name = stripped[(alias_position + 3)..].strip
|
|
15
|
+
new(definition: definition, alias_name: alias_name)
|
|
16
|
+
else
|
|
17
|
+
new(definition: stripped)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.find_top_level_as(text)
|
|
22
|
+
scanner = Scanner.new(text)
|
|
23
|
+
|
|
24
|
+
until scanner.finished?
|
|
25
|
+
consumed = scanner.scan_quoted_or_sentinel!
|
|
26
|
+
next if consumed
|
|
27
|
+
|
|
28
|
+
if scanner.current_char == Constants::OPEN_PARENTHESIS
|
|
29
|
+
scanner.increment_depth!
|
|
30
|
+
scanner.advance!
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if scanner.current_char == Constants::CLOSE_PARENTHESIS
|
|
35
|
+
scanner.decrement_depth!
|
|
36
|
+
scanner.advance!
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return scanner.position if scanner.top_level? && scanner.keyword_at?("as")
|
|
41
|
+
|
|
42
|
+
scanner.advance!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render
|
|
49
|
+
return @definition unless @alias_name
|
|
50
|
+
|
|
51
|
+
"#{@definition} as #{@alias_name}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SqlBeautifier
|
|
4
4
|
class Formatter
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
LEADING_SENTINEL_PATTERN = %r{\A#{CommentParser::SENTINEL_PATTERN}[[:space:]]*}
|
|
6
|
+
LEADING_SENTINEL_CAPTURE = %r{\A(#{CommentParser::SENTINEL_PATTERN}[[:space:]]*)}
|
|
7
|
+
|
|
8
|
+
def self.call(...)
|
|
9
|
+
new(...).call
|
|
7
10
|
end
|
|
8
11
|
|
|
9
12
|
def initialize(value, depth: 0)
|
|
@@ -20,33 +23,20 @@ module SqlBeautifier
|
|
|
20
23
|
@leading_sentinels = extract_leading_sentinels!
|
|
21
24
|
return unless @normalized_value.present?
|
|
22
25
|
|
|
23
|
-
cte_result =
|
|
26
|
+
cte_result = CteQuery.parse(@normalized_value, depth: @depth)&.render
|
|
24
27
|
return prepend_sentinels(cte_result) if cte_result
|
|
25
28
|
|
|
26
|
-
create_table_as_result =
|
|
29
|
+
create_table_as_result = CreateTableAs.parse(@normalized_value, depth: @depth)&.render
|
|
27
30
|
return prepend_sentinels(create_table_as_result) if create_table_as_result
|
|
28
31
|
|
|
29
32
|
first_clause_position = Tokenizer.first_clause_position(@normalized_value)
|
|
30
33
|
return prepend_sentinels("#{@normalized_value}\n") if first_clause_position.nil? || first_clause_position.positive?
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
append_clause!(:select, Clauses::Select)
|
|
37
|
-
append_from_clause!
|
|
38
|
-
append_clause!(:where, Clauses::Where)
|
|
39
|
-
append_clause!(:group_by, Clauses::GroupBy)
|
|
40
|
-
append_clause!(:having, Clauses::Having)
|
|
41
|
-
append_clause!(:order_by, Clauses::OrderBy)
|
|
42
|
-
append_clause!(:limit, Clauses::Limit)
|
|
35
|
+
query = Query.parse(@normalized_value, depth: @depth)
|
|
36
|
+
result = query.render
|
|
37
|
+
return prepend_sentinels("#{@normalized_value}\n") unless result
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
return prepend_sentinels("#{@normalized_value}\n") if output.empty?
|
|
46
|
-
|
|
47
|
-
output = SubqueryFormatter.format(output, @depth)
|
|
48
|
-
output = @table_registry.apply_aliases(output) if @table_registry
|
|
49
|
-
prepend_sentinels("#{output}\n")
|
|
39
|
+
prepend_sentinels(result)
|
|
50
40
|
end
|
|
51
41
|
|
|
52
42
|
private
|
|
@@ -55,8 +45,8 @@ module SqlBeautifier
|
|
|
55
45
|
leading_sentinel_text = +""
|
|
56
46
|
remaining_value = @normalized_value
|
|
57
47
|
|
|
58
|
-
while remaining_value.match?(
|
|
59
|
-
match = remaining_value.match(
|
|
48
|
+
while remaining_value.match?(LEADING_SENTINEL_PATTERN)
|
|
49
|
+
match = remaining_value.match(LEADING_SENTINEL_CAPTURE)
|
|
60
50
|
leading_sentinel_text << match[1]
|
|
61
51
|
remaining_value = remaining_value[match[1].length..]
|
|
62
52
|
end
|
|
@@ -71,62 +61,5 @@ module SqlBeautifier
|
|
|
71
61
|
|
|
72
62
|
"#{@leading_sentinels}#{output}"
|
|
73
63
|
end
|
|
74
|
-
|
|
75
|
-
def append_clause!(clause_key, formatter_class)
|
|
76
|
-
value = @clauses[clause_key]
|
|
77
|
-
return unless value.present?
|
|
78
|
-
|
|
79
|
-
@parts << formatter_class.call(value)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def append_from_clause!
|
|
83
|
-
value = @clauses[:from]
|
|
84
|
-
return unless value.present?
|
|
85
|
-
|
|
86
|
-
@parts << Clauses::From.call(value, table_registry: @table_registry)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def clause_separator
|
|
90
|
-
return "\n\n" if SqlBeautifier.config_for(:clause_spacing_mode) == :spacious
|
|
91
|
-
return "\n\n" unless compact_query?
|
|
92
|
-
|
|
93
|
-
"\n"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def compact_query?
|
|
97
|
-
compact_clause_set? && single_select_column? && single_from_table? && one_or_fewer_conditions?
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def compact_clause_set?
|
|
101
|
-
clause_keys = @clauses.keys
|
|
102
|
-
allowed_keys = %i[select from where order_by limit]
|
|
103
|
-
|
|
104
|
-
clause_keys.all? { |key| allowed_keys.include?(key) } && clause_keys.include?(:select) && clause_keys.include?(:from)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def single_select_column?
|
|
108
|
-
select_value = @clauses[:select]
|
|
109
|
-
return false unless select_value.present?
|
|
110
|
-
|
|
111
|
-
formatted_select = Clauses::Select.call(select_value)
|
|
112
|
-
formatted_select.lines.length == 1
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def single_from_table?
|
|
116
|
-
from_value = @clauses[:from]
|
|
117
|
-
return false unless from_value.present?
|
|
118
|
-
|
|
119
|
-
join_keywords = Constants::JOIN_KEYWORDS_BY_LENGTH.any? { |keyword| Tokenizer.find_top_level_keyword(from_value, keyword) }
|
|
120
|
-
return false if join_keywords
|
|
121
|
-
|
|
122
|
-
!from_value.match?(%r{,})
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def one_or_fewer_conditions?
|
|
126
|
-
where_value = @clauses[:where]
|
|
127
|
-
return true unless where_value.present?
|
|
128
|
-
|
|
129
|
-
Tokenizer.split_top_level_conditions(where_value).length <= 1
|
|
130
|
-
end
|
|
131
64
|
end
|
|
132
65
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqlBeautifier
|
|
4
|
+
class Join < Base
|
|
5
|
+
option :keyword
|
|
6
|
+
option :table_reference
|
|
7
|
+
option :trailing_sentinels, default: -> {}
|
|
8
|
+
option :conditions, default: -> { [] }
|
|
9
|
+
|
|
10
|
+
def self.parse(join_text, table_registry:)
|
|
11
|
+
keyword, remaining_content = extract_keyword(join_text)
|
|
12
|
+
return unless keyword && remaining_content
|
|
13
|
+
|
|
14
|
+
on_keyword_position = Tokenizer.find_top_level_keyword(remaining_content, "on")
|
|
15
|
+
|
|
16
|
+
if on_keyword_position
|
|
17
|
+
table_text = remaining_content[0...on_keyword_position].strip
|
|
18
|
+
condition_text = remaining_content[on_keyword_position..].delete_prefix("on").strip
|
|
19
|
+
conditions = Tokenizer.split_top_level_conditions(condition_text)
|
|
20
|
+
else
|
|
21
|
+
table_text = remaining_content
|
|
22
|
+
conditions = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
table_name = Util.first_word(table_text)
|
|
26
|
+
table_ref = table_registry.reference_for(table_name)
|
|
27
|
+
trailing_sentinels = extract_trailing_sentinels(table_text)
|
|
28
|
+
|
|
29
|
+
new(keyword: keyword, table_reference: table_ref, trailing_sentinels: trailing_sentinels, conditions: conditions)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.extract_keyword(join_text)
|
|
33
|
+
trimmed = join_text.strip
|
|
34
|
+
|
|
35
|
+
Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
|
|
36
|
+
next unless trimmed.downcase.start_with?(keyword)
|
|
37
|
+
|
|
38
|
+
remaining = trimmed[keyword.length..].strip
|
|
39
|
+
return [keyword, remaining]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
[nil, nil]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.extract_trailing_sentinels(text)
|
|
46
|
+
text.scan(CommentParser::SENTINEL_PATTERN).map do |match|
|
|
47
|
+
"#{CommentParser::SENTINEL_PREFIX}#{match[0]}#{CommentParser::SENTINEL_SUFFIX}"
|
|
48
|
+
end.presence
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render(continuation_indent:, condition_indent:)
|
|
52
|
+
rendered_table = @table_reference.render(trailing_sentinels: @trailing_sentinels)
|
|
53
|
+
lines = []
|
|
54
|
+
|
|
55
|
+
if @conditions.any?
|
|
56
|
+
first_condition = @conditions.first[1]
|
|
57
|
+
lines << "#{continuation_indent}#{@keyword} #{rendered_table} on #{first_condition}"
|
|
58
|
+
|
|
59
|
+
@conditions.drop(1).each do |conjunction, condition|
|
|
60
|
+
lines << "#{condition_indent}#{conjunction} #{condition}"
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
lines << "#{continuation_indent}#{@keyword} #{rendered_table}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|