sql_beautifier 0.1.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cd89341ca715477774f461f2f819550e28aea5b10da268f4cb662079b6d6577
4
- data.tar.gz: f9e1cb24ddfcae2290117366a969c14865cb938edb88574cd4e0fd5be1aee439
3
+ metadata.gz: d52933edcc70d3aaa5ed46f2c5d3bc6ac0a285294d84b25aa95c4e397fcb1d79
4
+ data.tar.gz: 5411d9f654c2d73fad75289e15433f47c34a45f463eae15fb1d832306fb62bd6
5
5
  SHA512:
6
- metadata.gz: bd61f519fd40401a02d1a8a235a4c4957754f07a0c8862bbfccf464706c0417397c9284215c9c02dadfb21a38e7bd7dc36a0302abf1042fe2931adcf940ae859
7
- data.tar.gz: d76dfa235ca58a94733582e52f991c4997dfe5dee336a8dc7adc4937fc89891c76de6e2bb6d06449c241bdcd416ecdb8f137b31fd9a6513cc490a478c890f8b5
6
+ metadata.gz: bbb3edc9cbe4e663ee167fda3632b12ffff04e5eddc6f225728abad772be643c7c6c97755ca4662210b380489ab5c2acf39968457fbc49042079e39eec5b120a
7
+ data.tar.gz: 5822d3514ad54619848b8e90cce8d5c4989cd2d367a20df00d51d3c03bf07a34b55166757ae72694970583a28db840a3234d7a309520673d2afc8e0d6a98e6f8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [X.X.X] - YYYY-MM-DD
4
4
 
5
+ ## [0.2.0] - 2026-03-27
6
+
7
+ - Add JOIN support (inner, left, right, full outer, cross) with formatted continuation lines
8
+ - Add automatic table aliasing using initials (e.g. `users` → `u`, `active_storage_blobs` → `asb`)
9
+ - Add PascalCase table name formatting (e.g. `users` → `Users`, `user_sessions` → `User_Sessions`)
10
+ - Add `table.column` → `alias.column` replacement across full output
11
+ - Add DISTINCT and DISTINCT ON support in SELECT clause
12
+ - Add AND/OR condition formatting in WHERE and HAVING clauses with per-line indentation
13
+ - Add parenthesized condition group handling (inline when short, expanded when long)
14
+
5
15
  ## [0.1.4] - 2026-03-26
6
16
 
7
17
  - Update `bin/ci` and `bin/release` to use new formatting functions
data/README.md CHANGED
@@ -41,14 +41,149 @@ select id,
41
41
  name,
42
42
  email
43
43
 
44
- from users
44
+ from Users u
45
45
 
46
46
  where active = true
47
47
 
48
48
  order by name
49
49
  ```
50
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.
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. Table names are PascalCased and automatically aliased.
52
+
53
+ ### Table Aliasing
54
+
55
+ Tables are automatically aliased using their initials. Underscore-separated table names use the first letter of each segment:
56
+
57
+ | Table Name | PascalCase | Alias |
58
+ | --- | --- | --- |
59
+ | `users` | `Users` | `u` |
60
+ | `active_storage_blobs` | `Active_Storage_Blobs` | `asb` |
61
+ | `person_event_invitations` | `Person_Event_Invitations` | `pei` |
62
+
63
+ All `table.column` references throughout the query are replaced with `alias.column`:
64
+
65
+ ```ruby
66
+ SqlBeautifier.call("SELECT users.id, users.name FROM users WHERE users.active = true")
67
+ ```
68
+
69
+ Produces:
70
+
71
+ ```sql
72
+ select u.id,
73
+ u.name
74
+
75
+ from Users u
76
+
77
+ where u.active = true
78
+ ```
79
+
80
+ When two tables produce the same initials, a counter is appended for disambiguation (e.g. `u1`, `u2`).
81
+
82
+ ### JOINs
83
+
84
+ JOIN clauses are formatted on continuation-indented lines with PascalCase table names and aliases. Multi-condition JOINs place additional conditions on further-indented lines:
85
+
86
+ ```ruby
87
+ SqlBeautifier.call(<<~SQL)
88
+ SELECT users.id, orders.total, products.name
89
+ FROM users
90
+ INNER JOIN orders ON orders.user_id = users.id
91
+ INNER JOIN products ON products.id = orders.product_id
92
+ WHERE users.active = true AND orders.total > 100
93
+ ORDER BY orders.total DESC
94
+ SQL
95
+ ```
96
+
97
+ Produces:
98
+
99
+ ```sql
100
+ select u.id,
101
+ o.total,
102
+ p.name
103
+
104
+ from Users u
105
+ inner join Orders o on o.user_id = u.id
106
+ inner join Products p on p.id = o.product_id
107
+
108
+ where u.active = true
109
+ and o.total > 100
110
+
111
+ order by o.total desc
112
+ ```
113
+
114
+ Supported join types: `inner join`, `left join`, `right join`, `full join`, `left outer join`, `right outer join`, `full outer join`, `cross join`.
115
+
116
+ ### DISTINCT and DISTINCT ON
117
+
118
+ `DISTINCT` is placed on the `select` line as a modifier, with columns on continuation lines:
119
+
120
+ ```ruby
121
+ SqlBeautifier.call("SELECT DISTINCT id, name, email FROM users")
122
+ ```
123
+
124
+ Produces:
125
+
126
+ ```sql
127
+ select distinct
128
+ id,
129
+ name,
130
+ email
131
+
132
+ from Users u
133
+ ```
134
+
135
+ `DISTINCT ON` preserves the full expression:
136
+
137
+ ```ruby
138
+ SqlBeautifier.call("SELECT DISTINCT ON (user_id) id, name FROM events")
139
+ ```
140
+
141
+ Produces:
142
+
143
+ ```sql
144
+ select distinct on (user_id)
145
+ id,
146
+ name
147
+
148
+ from Events e
149
+ ```
150
+
151
+ ### WHERE and HAVING Conditions
152
+
153
+ Multiple conditions in WHERE and HAVING clauses are formatted with each condition on its own line:
154
+
155
+ ```ruby
156
+ SqlBeautifier.call("SELECT * FROM users WHERE active = true AND role = 'admin' AND created_at > '2024-01-01'")
157
+ ```
158
+
159
+ Produces:
160
+
161
+ ```sql
162
+ select *
163
+
164
+ from Users u
165
+
166
+ where active = true
167
+ and role = 'admin'
168
+ and created_at > '2024-01-01'
169
+ ```
170
+
171
+ Short parenthesized groups stay inline:
172
+
173
+ ```ruby
174
+ SqlBeautifier.call("SELECT * FROM users WHERE active = true AND (role = 'admin' OR role = 'moderator')")
175
+ ```
176
+
177
+ Produces:
178
+
179
+ ```sql
180
+ select *
181
+
182
+ from Users u
183
+
184
+ where active = true
185
+ and (role = 'admin' or role = 'moderator')
186
+ ```
52
187
 
53
188
  ### GROUP BY and HAVING
54
189
 
@@ -67,7 +202,7 @@ Produces:
67
202
  select status,
68
203
  count(*)
69
204
 
70
- from users
205
+ from Users u
71
206
 
72
207
  group by status
73
208
 
@@ -85,7 +220,7 @@ Produces:
85
220
  ```sql
86
221
  select id
87
222
 
88
- from users
223
+ from Users u
89
224
 
90
225
  order by created_at desc
91
226
 
@@ -105,9 +240,10 @@ Produces:
105
240
  ```sql
106
241
  select *
107
242
 
108
- from users
243
+ from Users u
109
244
 
110
- where name = 'O''Brien' and status = 'Active'
245
+ where name = 'O''Brien'
246
+ and status = 'Active'
111
247
  ```
112
248
 
113
249
  ### Double-Quoted Identifiers
@@ -124,7 +260,7 @@ Produces:
124
260
  select user_id,
125
261
  full_name
126
262
 
127
- from users
263
+ from Users u
128
264
  ```
129
265
 
130
266
  ### Callable Interface
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module Clauses
5
+ class ConditionClause < Base
6
+ def call
7
+ return "#{self.class::KEYWORD_PREFIX}#{@value.strip}" unless multiple_conditions?
8
+
9
+ formatted_conditions = ConditionFormatter.format(@value, indent_width: Constants::KEYWORD_COLUMN_WIDTH)
10
+ formatted_conditions.sub(Constants::LEADING_KEYWORD_INDENT_PATTERN, self.class::KEYWORD_PREFIX)
11
+ end
12
+
13
+ private
14
+
15
+ def multiple_conditions?
16
+ Tokenizer.split_top_level_conditions(@value).length > 1
17
+ end
18
+ end
19
+ end
20
+ end
@@ -4,9 +4,144 @@ module SqlBeautifier
4
4
  module Clauses
5
5
  class From < Base
6
6
  KEYWORD_PREFIX = "from "
7
+ CONTINUATION_INDENTATION = " "
8
+ JOIN_CONDITION_INDENTATION = " "
9
+
10
+ def self.call(value, table_registry:)
11
+ new(value, table_registry: table_registry).call
12
+ end
13
+
14
+ def initialize(value, table_registry:)
15
+ super(value)
16
+ @table_registry = table_registry
17
+ end
7
18
 
8
19
  def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
20
+ @lines = []
21
+
22
+ join_parts = split_join_parts
23
+ primary_table_text = join_parts.shift.strip
24
+ formatted_primary_table_name = format_table_with_alias(primary_table_text)
25
+ add_line!("#{KEYWORD_PREFIX}#{formatted_primary_table_name}")
26
+
27
+ join_parts.each { |join_part| format_join_part(join_part) }
28
+
29
+ @lines.join("\n")
30
+ end
31
+
32
+ private
33
+
34
+ def add_line!(line)
35
+ @lines << line
36
+ end
37
+
38
+ def format_join_part(join_part)
39
+ join_keyword, remaining_join_content = extract_join_keyword(join_part)
40
+ return unless join_keyword && remaining_join_content
41
+
42
+ on_keyword_position = Tokenizer.find_top_level_keyword(remaining_join_content, "on")
43
+
44
+ if on_keyword_position
45
+ format_join_with_conditions(join_keyword, remaining_join_content, on_keyword_position)
46
+ else
47
+ formatted_table_name = format_table_with_alias(remaining_join_content)
48
+ add_line!("#{CONTINUATION_INDENTATION}#{join_keyword} #{formatted_table_name}")
49
+ end
50
+ end
51
+
52
+ def format_join_with_conditions(join_keyword, join_content, on_keyword_position)
53
+ table_text = join_content[0...on_keyword_position].strip
54
+ condition_text = join_content[on_keyword_position..].delete_prefix("on").strip
55
+ on_conditions = Tokenizer.split_top_level_conditions(condition_text)
56
+
57
+ formatted_table_name = format_table_with_alias(table_text)
58
+ first_condition = on_conditions.first[1]
59
+ add_line!("#{CONTINUATION_INDENTATION}#{join_keyword} #{formatted_table_name} on #{first_condition}")
60
+
61
+ on_conditions.drop(1).each do |conjunction, additional_condition|
62
+ add_line!("#{JOIN_CONDITION_INDENTATION}#{conjunction} #{additional_condition}")
63
+ end
64
+ end
65
+
66
+ def split_join_parts
67
+ from_content = @value.strip
68
+ join_keyword_positions = find_all_join_keyword_positions(from_content)
69
+
70
+ return [from_content] if join_keyword_positions.empty?
71
+
72
+ parts = [from_content[0...join_keyword_positions.first[:position]]]
73
+
74
+ join_keyword_positions.each_with_index do |join_info, index|
75
+ end_position = begin
76
+ if index + 1 < join_keyword_positions.length
77
+ join_keyword_positions[index + 1][:position]
78
+ else
79
+ from_content.length
80
+ end
81
+ end
82
+
83
+ parts << from_content[join_info[:position]...end_position]
84
+ end
85
+
86
+ parts
87
+ end
88
+
89
+ def find_all_join_keyword_positions(text)
90
+ positions = []
91
+ search_offset = 0
92
+
93
+ while search_offset < text.length
94
+ earliest_match = find_earliest_join_keyword(text, search_offset)
95
+ break unless earliest_match
96
+
97
+ positions << earliest_match
98
+ search_offset = earliest_match[:position] + earliest_match[:keyword].length
99
+ end
100
+
101
+ positions
102
+ end
103
+
104
+ def find_earliest_join_keyword(text, search_offset)
105
+ earliest_match = nil
106
+
107
+ Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
108
+ remaining_text = text[search_offset..]
109
+ keyword_position = Tokenizer.find_top_level_keyword(remaining_text, keyword)
110
+ next unless keyword_position
111
+
112
+ absolute_position = search_offset + keyword_position
113
+ next if earliest_match && absolute_position >= earliest_match[:position]
114
+
115
+ earliest_match = {
116
+ position: absolute_position,
117
+ keyword: keyword,
118
+ }
119
+ end
120
+
121
+ earliest_match
122
+ end
123
+
124
+ def extract_join_keyword(join_part)
125
+ trimmed_join_text = join_part.strip
126
+
127
+ Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
128
+ next unless trimmed_join_text.downcase.start_with?(keyword)
129
+
130
+ remaining_join_content = trimmed_join_text[keyword.length..].strip
131
+
132
+ return [keyword, remaining_join_content]
133
+ end
134
+
135
+ [nil, nil]
136
+ end
137
+
138
+ def format_table_with_alias(table_text)
139
+ table_name = Util.first_word(table_text)
140
+ formatted_table_name = Util.upper_pascal_case(table_name)
141
+ table_alias = @table_registry.alias_for(table_name)
142
+ return formatted_table_name unless table_alias
143
+
144
+ "#{formatted_table_name} #{table_alias}"
10
145
  end
11
146
  end
12
147
  end
@@ -2,12 +2,8 @@
2
2
 
3
3
  module SqlBeautifier
4
4
  module Clauses
5
- class Having < Base
5
+ class Having < ConditionClause
6
6
  KEYWORD_PREFIX = "having "
7
-
8
- def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
10
- end
11
7
  end
12
8
  end
13
9
  end
@@ -4,17 +4,72 @@ module SqlBeautifier
4
4
  module Clauses
5
5
  class Select < Base
6
6
  KEYWORD_PREFIX = "select "
7
- CONTINUATION_INDENT = " "
7
+ CONTINUATION_INDENTATION = " "
8
+ DISTINCT_ON_PARENTHESIS_PATTERN = %r{distinct on\s*\(}
9
+ DISTINCT_ON_PATTERN = %r{distinct on }
10
+ LEADING_COMMA_PATTERN = %r{\A,\s*}
8
11
 
9
12
  def call
10
- columns = Tokenizer.split_by_top_level_commas(@value)
11
- return "#{KEYWORD_PREFIX}#{columns.first.strip}" if columns.length == 1
13
+ prefix, remaining_columns = extract_prefix
14
+ columns = Tokenizer.split_by_top_level_commas(remaining_columns)
12
15
 
13
- column_lines = columns.map { |column| "#{CONTINUATION_INDENT}#{column.strip}" }
14
- column_lines[0] = "#{KEYWORD_PREFIX}#{columns.first.strip}"
16
+ return format_with_prefix(prefix, columns) if prefix
17
+ return keyword_line(columns.first) if columns.length == 1
18
+
19
+ format_columns_list(columns)
20
+ end
21
+
22
+ private
23
+
24
+ def keyword_line(column)
25
+ "#{KEYWORD_PREFIX}#{column.strip}"
26
+ end
27
+
28
+ def continuation_line(column)
29
+ "#{CONTINUATION_INDENTATION}#{column.strip}"
30
+ end
31
+
32
+ def format_with_prefix(prefix, columns)
33
+ first_line = "#{KEYWORD_PREFIX}#{prefix}"
34
+ column_lines = columns.map { |column| continuation_line(column) }
35
+
36
+ "#{first_line}\n#{column_lines.join(",\n")}"
37
+ end
38
+
39
+ def format_columns_list(columns)
40
+ column_lines = columns.map { |column| continuation_line(column) }
41
+ column_lines[0] = keyword_line(columns.first)
15
42
 
16
43
  column_lines.join(",\n")
17
44
  end
45
+
46
+ def extract_prefix
47
+ stripped_value = @value.strip
48
+
49
+ if stripped_value.start_with?("distinct on ")
50
+ extract_distinct_on_prefix(stripped_value)
51
+ elsif stripped_value.start_with?("distinct ")
52
+ remaining_columns = stripped_value.delete_prefix("distinct ").strip
53
+
54
+ ["distinct", remaining_columns]
55
+ else
56
+ [nil, stripped_value]
57
+ end
58
+ end
59
+
60
+ def extract_distinct_on_prefix(stripped_value)
61
+ distinct_on_position = stripped_value.index(DISTINCT_ON_PARENTHESIS_PATTERN) || stripped_value.index(DISTINCT_ON_PATTERN)
62
+ opening_parenthesis_position = stripped_value.index(Constants::OPEN_PARENTHESIS, distinct_on_position)
63
+ return [nil, stripped_value] unless opening_parenthesis_position
64
+
65
+ closing_parenthesis_position = Tokenizer.find_matching_parenthesis(stripped_value, opening_parenthesis_position)
66
+ return [nil, stripped_value] unless closing_parenthesis_position
67
+
68
+ prefix = stripped_value[0..closing_parenthesis_position]
69
+ remaining_columns = stripped_value[(closing_parenthesis_position + 1)..].strip.sub(LEADING_COMMA_PATTERN, "")
70
+
71
+ [prefix, remaining_columns]
72
+ end
18
73
  end
19
74
  end
20
75
  end
@@ -2,12 +2,8 @@
2
2
 
3
3
  module SqlBeautifier
4
4
  module Clauses
5
- class Where < Base
5
+ class Where < ConditionClause
6
6
  KEYWORD_PREFIX = "where "
7
-
8
- def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
10
- end
11
7
  end
12
8
  end
13
9
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module ConditionFormatter
5
+ module_function
6
+
7
+ def format(text, indent_width:)
8
+ conditions = Tokenizer.split_top_level_conditions(text)
9
+ return text.strip if conditions.length <= 1 && !parse_condition_group(conditions.dig(0, 1))
10
+
11
+ conditions = flatten_same_conjunction_groups(conditions)
12
+ indentation = " " * indent_width
13
+ lines = []
14
+
15
+ conditions.each_with_index do |(conjunction, condition_text), index|
16
+ unwrapped_condition = unwrap_single_condition(condition_text)
17
+ formatted_condition_text = format_single_condition(unwrapped_condition, indent_width: indent_width)
18
+
19
+ line = begin
20
+ if index.zero?
21
+ "#{indentation}#{formatted_condition_text}"
22
+ else
23
+ "#{indentation}#{conjunction} #{formatted_condition_text}"
24
+ end
25
+ end
26
+
27
+ lines << line
28
+ end
29
+
30
+ lines.join("\n")
31
+ end
32
+
33
+ def flatten_same_conjunction_groups(conditions)
34
+ return conditions if conditions.length <= 1
35
+
36
+ outer_conjunction = conditions[1]&.first
37
+ return conditions unless outer_conjunction
38
+ return conditions unless conditions.drop(1).all? { |pair| pair[0] == outer_conjunction }
39
+
40
+ flattened_conditions = []
41
+
42
+ conditions.each do |conjunction, condition_text|
43
+ inner_conditions = parse_condition_group(condition_text)
44
+
45
+ if inner_conditions && flattenable_into_conjunction?(inner_conditions, outer_conjunction)
46
+ flatten_inner_conditions_into!(flattened_conditions, inner_conditions, conjunction, outer_conjunction)
47
+ else
48
+ flattened_conditions << [conjunction, condition_text]
49
+ end
50
+ end
51
+
52
+ flattened_conditions
53
+ end
54
+
55
+ def rebuild_inline(inner_conditions)
56
+ parts = inner_conditions.map.with_index do |(conjunction, condition_text), index|
57
+ index.zero? ? condition_text : "#{conjunction} #{condition_text}"
58
+ end
59
+
60
+ "(#{parts.join(' ')})"
61
+ end
62
+
63
+ def unwrap_single_condition(condition)
64
+ output = condition.strip
65
+
66
+ while Tokenizer.outer_parentheses_wrap_all?(output)
67
+ inner_content = Util.strip_outer_parentheses(output)
68
+ inner_conditions = Tokenizer.split_top_level_conditions(inner_content)
69
+ break if inner_conditions.length > 1
70
+
71
+ output = inner_content
72
+ end
73
+
74
+ output
75
+ end
76
+
77
+ def parse_condition_group(condition_text)
78
+ return unless condition_text
79
+
80
+ trimmed_condition = condition_text.strip
81
+ return unless Tokenizer.outer_parentheses_wrap_all?(trimmed_condition)
82
+
83
+ inner_content = Util.strip_outer_parentheses(trimmed_condition)
84
+ inner_conditions = Tokenizer.split_top_level_conditions(inner_content)
85
+ return unless inner_conditions.length > 1
86
+
87
+ inner_conditions
88
+ end
89
+
90
+ def format_single_condition(condition_text, indent_width:)
91
+ inner_conditions = parse_condition_group(condition_text)
92
+ return condition_text unless inner_conditions
93
+
94
+ inline_version = rebuild_inline(inner_conditions)
95
+ return inline_version if inline_version.length <= Constants::INLINE_GROUP_THRESHOLD
96
+
97
+ inner_content = Util.strip_outer_parentheses(condition_text.strip)
98
+ formatted_inner_content = format(inner_content, indent_width: indent_width + 4)
99
+ indentation = " " * indent_width
100
+
101
+ "(\n#{formatted_inner_content}\n#{indentation})"
102
+ end
103
+
104
+ def flattenable_into_conjunction?(inner_conditions, outer_conjunction)
105
+ inner_conjunction = inner_conditions[1]&.first
106
+
107
+ inner_conjunction == outer_conjunction && inner_conditions.drop(1).all? { |pair| pair[0] == outer_conjunction }
108
+ end
109
+
110
+ def flatten_inner_conditions_into!(flattened_conditions, inner_conditions, conjunction, outer_conjunction)
111
+ inner_conditions.each_with_index do |inner_pair, inner_index|
112
+ condition_pair = begin
113
+ if flattened_conditions.empty?
114
+ [nil, inner_pair[1]]
115
+ elsif inner_index.zero?
116
+ [conjunction || outer_conjunction, inner_pair[1]]
117
+ else
118
+ [outer_conjunction, inner_pair[1]]
119
+ end
120
+ end
121
+
122
+ flattened_conditions << condition_pair
123
+ end
124
+ end
125
+ end
126
+ end
@@ -11,5 +11,37 @@ module SqlBeautifier
11
11
  "order by",
12
12
  "limit",
13
13
  ].freeze
14
+
15
+ JOIN_KEYWORDS = [
16
+ "inner join",
17
+ "left outer join",
18
+ "right outer join",
19
+ "full outer join",
20
+ "left join",
21
+ "right join",
22
+ "full join",
23
+ "cross join",
24
+ ].freeze
25
+
26
+ JOIN_KEYWORDS_BY_LENGTH = JOIN_KEYWORDS.sort_by { |keyword| -keyword.length }.freeze
27
+ JOIN_KEYWORD_PATTERN = %r{\b(#{JOIN_KEYWORDS.map { |keyword| Regexp.escape(keyword) }.join('|')})\b}i
28
+
29
+ CONJUNCTIONS = %w[and or].freeze
30
+ BETWEEN_KEYWORD = "between"
31
+
32
+ INLINE_GROUP_THRESHOLD = 100
33
+ KEYWORD_COLUMN_WIDTH = 8
34
+
35
+ LEADING_KEYWORD_INDENT_PATTERN = %r{\A#{' ' * KEYWORD_COLUMN_WIDTH}}
36
+
37
+ OPEN_PARENTHESIS = "("
38
+ CLOSE_PARENTHESIS = ")"
39
+ COMMA = ","
40
+
41
+ WHITESPACE_REGEX = %r{\s+}
42
+ WHITESPACE_CHARACTER_REGEX = %r{\s}
43
+ SINGLE_QUOTE = "'"
44
+ DOUBLE_QUOTE = '"'
45
+ ESCAPED_DOUBLE_QUOTE = '""'
14
46
  end
15
47
  end
@@ -20,10 +20,11 @@ module SqlBeautifier
20
20
  return "#{@normalized_value}\n" if first_clause_position.nil? || first_clause_position.positive?
21
21
 
22
22
  @clauses = Tokenizer.split_into_clauses(@normalized_value)
23
+ @table_registry = TableRegistry.new(@clauses[:from]) if @clauses[:from].present?
23
24
  @parts = []
24
25
 
25
26
  append_clause!(:select, Clauses::Select)
26
- append_clause!(:from, Clauses::From)
27
+ append_from_clause!
27
28
  append_clause!(:where, Clauses::Where)
28
29
  append_clause!(:group_by, Clauses::GroupBy)
29
30
  append_clause!(:having, Clauses::Having)
@@ -33,6 +34,7 @@ module SqlBeautifier
33
34
  output = @parts.join("\n\n")
34
35
  return "#{@normalized_value}\n" if output.empty?
35
36
 
37
+ output = @table_registry.apply_aliases(output) if @table_registry
36
38
  "#{output}\n"
37
39
  end
38
40
 
@@ -44,5 +46,12 @@ module SqlBeautifier
44
46
 
45
47
  @parts << formatter_class.call(value)
46
48
  end
49
+
50
+ def append_from_clause!
51
+ value = @clauses[:from]
52
+ return unless value.present?
53
+
54
+ @parts << Clauses::From.call(value, table_registry: @table_registry)
55
+ end
47
56
  end
48
57
  end