sql_beautifier 0.2.0 → 0.3.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: d52933edcc70d3aaa5ed46f2c5d3bc6ac0a285294d84b25aa95c4e397fcb1d79
4
- data.tar.gz: 5411d9f654c2d73fad75289e15433f47c34a45f463eae15fb1d832306fb62bd6
3
+ metadata.gz: 6df1f55c9242793101e9f562329763541d56aa4f17f7e3d81a12038ab5b53cca
4
+ data.tar.gz: 43fea825ac0c4bded07098814a2ed67e5503e1829f3d47cc99a7c989ec0e69d1
5
5
  SHA512:
6
- metadata.gz: bbb3edc9cbe4e663ee167fda3632b12ffff04e5eddc6f225728abad772be643c7c6c97755ca4662210b380489ab5c2acf39968457fbc49042079e39eec5b120a
7
- data.tar.gz: 5822d3514ad54619848b8e90cce8d5c4989cd2d367a20df00d51d3c03bf07a34b55166757ae72694970583a28db840a3234d7a309520673d2afc8e0d6a98e6f8
6
+ metadata.gz: d56c72c27d4b71781be0ee83ba44fb3a4e466567c5edbde267c2cfbeb245c81e78bc52a309d27e7dae69a2a7e3cbf1bb3ce55926a266a6c914e822140b6650b7
7
+ data.tar.gz: 9e2e5118b5b12e52551300e2c0219c6cc42907fdb87fd56a8e6e80e193b212ae36ff87ee48d2a0135dc0140121d145c3f104622a0efbd0a3bbeffe10af378cea
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [X.X.X] - YYYY-MM-DD
4
4
 
5
+ ## [0.3.0] - 2026-03-27
6
+
7
+ - Add configuration system with `SqlBeautifier.configure` block and `SqlBeautifier.reset_configuration!`
8
+ - Add configurable keyword case (`:lower` / `:upper`), keyword column width, indent spaces, table name format (`:pascal_case` / `:lowercase`), inline group threshold, and alias strategy (`:initials` / `:none` / callable)
9
+ - Add semicolon stripping in normalizer (trailing `;` removed before formatting)
10
+ - Add comment stripping in normalizer (`--` line comments and `/* */` block comments, string-aware)
11
+ - Add subquery formatting with recursive indentation (`(select ...)` expanded to multiline)
12
+
5
13
  ## [0.2.0] - 2026-03-27
6
14
 
7
15
  - Add JOIN support (inner, left, right, full outer, cross) with formatted continuation lines
data/README.md CHANGED
@@ -48,16 +48,16 @@ where active = true
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. Table names are PascalCased and automatically aliased.
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. Clause spacing is compact by default for simple one-column / one-table / one-condition queries, and otherwise uses blank lines between top-level clauses. Multi-column SELECT lists place each column on its own line with continuation indentation. Table names are PascalCased and automatically aliased.
52
52
 
53
53
  ### Table Aliasing
54
54
 
55
55
  Tables are automatically aliased using their initials. Underscore-separated table names use the first letter of each segment:
56
56
 
57
- | Table Name | PascalCase | Alias |
58
- | --- | --- | --- |
59
- | `users` | `Users` | `u` |
60
- | `active_storage_blobs` | `Active_Storage_Blobs` | `asb` |
57
+ | Table Name | PascalCase | Alias |
58
+ | -------------------------- | -------------------------- | ----- |
59
+ | `users` | `Users` | `u` |
60
+ | `active_storage_blobs` | `Active_Storage_Blobs` | `asb` |
61
61
  | `person_event_invitations` | `Person_Event_Invitations` | `pei` |
62
62
 
63
63
  All `table.column` references throughout the query are replaced with `alias.column`:
@@ -219,11 +219,8 @@ Produces:
219
219
 
220
220
  ```sql
221
221
  select id
222
-
223
222
  from Users u
224
-
225
223
  order by created_at desc
226
-
227
224
  limit 25
228
225
  ```
229
226
 
@@ -263,6 +260,87 @@ select user_id,
263
260
  from Users u
264
261
  ```
265
262
 
263
+ ### Subqueries
264
+
265
+ Subqueries are automatically detected and recursively formatted with indentation:
266
+
267
+ ```ruby
268
+ SqlBeautifier.call("SELECT id FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100)")
269
+ ```
270
+
271
+ Produces:
272
+
273
+ ```sql
274
+ select id
275
+ from Users u
276
+ where id in (
277
+ select user_id
278
+ from Orders o
279
+ where total > 100
280
+ )
281
+ ```
282
+
283
+ Nested subqueries increase indentation at each level.
284
+
285
+ ### Comments and Semicolons
286
+
287
+ SQL comments (`--` line comments and `/* */` block comments) and trailing semicolons are automatically stripped during normalization. Comments inside string literals are preserved:
288
+
289
+ ```ruby
290
+ SqlBeautifier.call("SELECT id /* primary key */ FROM users -- main table\nWHERE active = true;")
291
+ ```
292
+
293
+ Produces:
294
+
295
+ ```sql
296
+ select id
297
+ from Users u
298
+ where active = true
299
+ ```
300
+
301
+ ### Configuration
302
+
303
+ Customize formatting behavior with `SqlBeautifier.configure`:
304
+
305
+ ```ruby
306
+ SqlBeautifier.configure do |config|
307
+ config.keyword_case = :upper # :lower (default), :upper
308
+ config.keyword_column_width = 10 # default: 8
309
+ config.indent_spaces = 4 # default: 4
310
+ config.clause_spacing_mode = :spacious # :compact (default), :spacious
311
+ config.table_name_format = :lowercase # :pascal_case (default), :lowercase
312
+ config.inline_group_threshold = 80 # default: 100
313
+ config.alias_strategy = :none # :initials (default), :none, or a callable
314
+ end
315
+ ```
316
+
317
+ #### Clause Spacing Modes
318
+
319
+ - `:compact` (default) keeps top-level clauses on single newlines only when the query is simple:
320
+ - exactly one SELECT column
321
+ - exactly one FROM table (no JOINs)
322
+ - zero or one top-level WHERE condition
323
+ - only `select`, `from`, optional `where`, optional `order by`, and optional `limit`
324
+ - `:spacious` always separates top-level clauses with blank lines
325
+
326
+ Reset to defaults:
327
+
328
+ ```ruby
329
+ SqlBeautifier.reset_configuration!
330
+ ```
331
+
332
+ #### Alias Strategies
333
+
334
+ - `:initials` (default) — automatic aliases using table initials (`users` → `u`, `active_storage_blobs` → `asb`)
335
+ - `:none` — no automatic aliases (explicit aliases in the SQL are still preserved)
336
+ - Callable — provide a proc/lambda for custom alias generation:
337
+
338
+ ```ruby
339
+ SqlBeautifier.configure do |config|
340
+ config.alias_strategy = ->(table_name) { "t_#{table_name[0..2]}" }
341
+ end
342
+ ```
343
+
266
344
  ### Callable Interface
267
345
 
268
346
  `SqlBeautifier.call` is the public API, making it a valid callable for Rails `normalizes` and anywhere a proc-like object is expected:
@@ -10,6 +10,16 @@ module SqlBeautifier
10
10
  def initialize(value)
11
11
  @value = value
12
12
  end
13
+
14
+ private
15
+
16
+ def keyword_prefix
17
+ Util.keyword_padding(self.class::KEYWORD)
18
+ end
19
+
20
+ def continuation_indent
21
+ Util.continuation_padding
22
+ end
13
23
  end
14
24
  end
15
25
  end
@@ -4,10 +4,10 @@ module SqlBeautifier
4
4
  module Clauses
5
5
  class ConditionClause < Base
6
6
  def call
7
- return "#{self.class::KEYWORD_PREFIX}#{@value.strip}" unless multiple_conditions?
7
+ return "#{keyword_prefix}#{@value.strip}" unless multiple_conditions?
8
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)
9
+ formatted_conditions = ConditionFormatter.format(@value, indent_width: SqlBeautifier.config_for(:keyword_column_width))
10
+ formatted_conditions.sub(continuation_indent, keyword_prefix)
11
11
  end
12
12
 
13
13
  private
@@ -3,9 +3,7 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class From < Base
6
- KEYWORD_PREFIX = "from "
7
- CONTINUATION_INDENTATION = " "
8
- JOIN_CONDITION_INDENTATION = " "
6
+ KEYWORD = "from"
9
7
 
10
8
  def self.call(value, table_registry:)
11
9
  new(value, table_registry: table_registry).call
@@ -22,7 +20,7 @@ module SqlBeautifier
22
20
  join_parts = split_join_parts
23
21
  primary_table_text = join_parts.shift.strip
24
22
  formatted_primary_table_name = format_table_with_alias(primary_table_text)
25
- add_line!("#{KEYWORD_PREFIX}#{formatted_primary_table_name}")
23
+ add_line!("#{keyword_prefix}#{formatted_primary_table_name}")
26
24
 
27
25
  join_parts.each { |join_part| format_join_part(join_part) }
28
26
 
@@ -35,6 +33,10 @@ module SqlBeautifier
35
33
  @lines << line
36
34
  end
37
35
 
36
+ def join_condition_indentation
37
+ Util.whitespace(SqlBeautifier.config_for(:keyword_column_width) + 4)
38
+ end
39
+
38
40
  def format_join_part(join_part)
39
41
  join_keyword, remaining_join_content = extract_join_keyword(join_part)
40
42
  return unless join_keyword && remaining_join_content
@@ -45,7 +47,7 @@ module SqlBeautifier
45
47
  format_join_with_conditions(join_keyword, remaining_join_content, on_keyword_position)
46
48
  else
47
49
  formatted_table_name = format_table_with_alias(remaining_join_content)
48
- add_line!("#{CONTINUATION_INDENTATION}#{join_keyword} #{formatted_table_name}")
50
+ add_line!("#{continuation_indent}#{join_keyword} #{formatted_table_name}")
49
51
  end
50
52
  end
51
53
 
@@ -56,10 +58,10 @@ module SqlBeautifier
56
58
 
57
59
  formatted_table_name = format_table_with_alias(table_text)
58
60
  first_condition = on_conditions.first[1]
59
- add_line!("#{CONTINUATION_INDENTATION}#{join_keyword} #{formatted_table_name} on #{first_condition}")
61
+ add_line!("#{continuation_indent}#{join_keyword} #{formatted_table_name} on #{first_condition}")
60
62
 
61
63
  on_conditions.drop(1).each do |conjunction, additional_condition|
62
- add_line!("#{JOIN_CONDITION_INDENTATION}#{conjunction} #{additional_condition}")
64
+ add_line!("#{join_condition_indentation}#{conjunction} #{additional_condition}")
63
65
  end
64
66
  end
65
67
 
@@ -137,7 +139,7 @@ module SqlBeautifier
137
139
 
138
140
  def format_table_with_alias(table_text)
139
141
  table_name = Util.first_word(table_text)
140
- formatted_table_name = Util.upper_pascal_case(table_name)
142
+ formatted_table_name = Util.format_table_name(table_name)
141
143
  table_alias = @table_registry.alias_for(table_name)
142
144
  return formatted_table_name unless table_alias
143
145
 
@@ -3,10 +3,10 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class GroupBy < Base
6
- KEYWORD_PREFIX = "group by "
6
+ KEYWORD = "group by"
7
7
 
8
8
  def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
9
+ "#{keyword_prefix}#{@value.strip}"
10
10
  end
11
11
  end
12
12
  end
@@ -3,7 +3,7 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class Having < ConditionClause
6
- KEYWORD_PREFIX = "having "
6
+ KEYWORD = "having"
7
7
  end
8
8
  end
9
9
  end
@@ -3,10 +3,16 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class Limit < Base
6
- KEYWORD_PREFIX = "limit "
6
+ KEYWORD = "limit"
7
7
 
8
8
  def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
9
+ "#{keyword_prefix}#{@value.strip}"
10
+ end
11
+
12
+ private
13
+
14
+ def keyword_prefix
15
+ "#{Util.format_keyword(KEYWORD)} "
10
16
  end
11
17
  end
12
18
  end
@@ -3,10 +3,10 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class OrderBy < Base
6
- KEYWORD_PREFIX = "order by "
6
+ KEYWORD = "order by"
7
7
 
8
8
  def call
9
- "#{KEYWORD_PREFIX}#{@value.strip}"
9
+ "#{keyword_prefix}#{@value.strip}"
10
10
  end
11
11
  end
12
12
  end
@@ -3,8 +3,7 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class Select < Base
6
- KEYWORD_PREFIX = "select "
7
- CONTINUATION_INDENTATION = " "
6
+ KEYWORD = "select"
8
7
  DISTINCT_ON_PARENTHESIS_PATTERN = %r{distinct on\s*\(}
9
8
  DISTINCT_ON_PATTERN = %r{distinct on }
10
9
  LEADING_COMMA_PATTERN = %r{\A,\s*}
@@ -22,15 +21,15 @@ module SqlBeautifier
22
21
  private
23
22
 
24
23
  def keyword_line(column)
25
- "#{KEYWORD_PREFIX}#{column.strip}"
24
+ "#{keyword_prefix}#{column.strip}"
26
25
  end
27
26
 
28
27
  def continuation_line(column)
29
- "#{CONTINUATION_INDENTATION}#{column.strip}"
28
+ "#{continuation_indent}#{column.strip}"
30
29
  end
31
30
 
32
31
  def format_with_prefix(prefix, columns)
33
- first_line = "#{KEYWORD_PREFIX}#{prefix}"
32
+ first_line = "#{keyword_prefix}#{prefix}"
34
33
  column_lines = columns.map { |column| continuation_line(column) }
35
34
 
36
35
  "#{first_line}\n#{column_lines.join(",\n")}"
@@ -3,7 +3,7 @@
3
3
  module SqlBeautifier
4
4
  module Clauses
5
5
  class Where < ConditionClause
6
- KEYWORD_PREFIX = "where "
6
+ KEYWORD = "where"
7
7
  end
8
8
  end
9
9
  end
@@ -9,7 +9,7 @@ module SqlBeautifier
9
9
  return text.strip if conditions.length <= 1 && !parse_condition_group(conditions.dig(0, 1))
10
10
 
11
11
  conditions = flatten_same_conjunction_groups(conditions)
12
- indentation = " " * indent_width
12
+ indentation = Util.whitespace(indent_width)
13
13
  lines = []
14
14
 
15
15
  conditions.each_with_index do |(conjunction, condition_text), index|
@@ -92,11 +92,11 @@ module SqlBeautifier
92
92
  return condition_text unless inner_conditions
93
93
 
94
94
  inline_version = rebuild_inline(inner_conditions)
95
- return inline_version if inline_version.length <= Constants::INLINE_GROUP_THRESHOLD
95
+ return inline_version if inline_version.length <= SqlBeautifier.config_for(:inline_group_threshold)
96
96
 
97
97
  inner_content = Util.strip_outer_parentheses(condition_text.strip)
98
98
  formatted_inner_content = format(inner_content, indent_width: indent_width + 4)
99
- indentation = " " * indent_width
99
+ indentation = Util.whitespace(indent_width)
100
100
 
101
101
  "(\n#{formatted_inner_content}\n#{indentation})"
102
102
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class Configuration
5
+ DEFAULTS = {
6
+ keyword_case: :lower,
7
+ keyword_column_width: 8,
8
+ indent_spaces: 4,
9
+ clause_spacing_mode: :compact,
10
+ table_name_format: :pascal_case,
11
+ inline_group_threshold: 100,
12
+ alias_strategy: :initials,
13
+ }.freeze
14
+
15
+ attr_accessor :keyword_case
16
+ attr_accessor :keyword_column_width
17
+ attr_accessor :indent_spaces
18
+ attr_accessor :clause_spacing_mode
19
+ attr_accessor :table_name_format
20
+ attr_accessor :inline_group_threshold
21
+ attr_accessor :alias_strategy
22
+
23
+ def initialize
24
+ reset!
25
+ end
26
+
27
+ def reset!
28
+ DEFAULTS.each do |key, value|
29
+ public_send(:"#{key}=", value)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -29,11 +29,6 @@ module SqlBeautifier
29
29
  CONJUNCTIONS = %w[and or].freeze
30
30
  BETWEEN_KEYWORD = "between"
31
31
 
32
- INLINE_GROUP_THRESHOLD = 100
33
- KEYWORD_COLUMN_WIDTH = 8
34
-
35
- LEADING_KEYWORD_INDENT_PATTERN = %r{\A#{' ' * KEYWORD_COLUMN_WIDTH}}
36
-
37
32
  OPEN_PARENTHESIS = "("
38
33
  CLOSE_PARENTHESIS = ")"
39
34
  COMMA = ","
@@ -6,8 +6,9 @@ module SqlBeautifier
6
6
  new(value).call
7
7
  end
8
8
 
9
- def initialize(value)
9
+ def initialize(value, depth: 0)
10
10
  @value = value
11
+ @depth = depth
11
12
  end
12
13
 
13
14
  def call
@@ -31,9 +32,10 @@ module SqlBeautifier
31
32
  append_clause!(:order_by, Clauses::OrderBy)
32
33
  append_clause!(:limit, Clauses::Limit)
33
34
 
34
- output = @parts.join("\n\n")
35
+ output = @parts.join(clause_separator)
35
36
  return "#{@normalized_value}\n" if output.empty?
36
37
 
38
+ output = SubqueryFormatter.format(output, @depth)
37
39
  output = @table_registry.apply_aliases(output) if @table_registry
38
40
  "#{output}\n"
39
41
  end
@@ -53,5 +55,48 @@ module SqlBeautifier
53
55
 
54
56
  @parts << Clauses::From.call(value, table_registry: @table_registry)
55
57
  end
58
+
59
+ def clause_separator
60
+ return "\n\n" if SqlBeautifier.config_for(:clause_spacing_mode) == :spacious
61
+ return "\n\n" unless compact_query?
62
+
63
+ "\n"
64
+ end
65
+
66
+ def compact_query?
67
+ compact_clause_set? && single_select_column? && single_from_table? && one_or_fewer_conditions?
68
+ end
69
+
70
+ def compact_clause_set?
71
+ clause_keys = @clauses.keys
72
+ allowed_keys = %i[select from where order_by limit]
73
+
74
+ clause_keys.all? { |key| allowed_keys.include?(key) } && clause_keys.include?(:select) && clause_keys.include?(:from)
75
+ end
76
+
77
+ def single_select_column?
78
+ select_value = @clauses[:select]
79
+ return false unless select_value.present?
80
+
81
+ formatted_select = Clauses::Select.call(select_value)
82
+ formatted_select.lines.length == 1
83
+ end
84
+
85
+ def single_from_table?
86
+ from_value = @clauses[:from]
87
+ return false unless from_value.present?
88
+
89
+ join_keywords = Constants::JOIN_KEYWORDS_BY_LENGTH.any? { |keyword| Tokenizer.find_top_level_keyword(from_value, keyword) }
90
+ return false if join_keywords
91
+
92
+ !from_value.match?(%r{,})
93
+ end
94
+
95
+ def one_or_fewer_conditions?
96
+ where_value = @clauses[:where]
97
+ return true unless where_value.present?
98
+
99
+ Tokenizer.split_top_level_conditions(where_value).length <= 1
100
+ end
56
101
  end
57
102
  end
@@ -18,6 +18,11 @@ module SqlBeautifier
18
18
  @source = @value.strip
19
19
  return unless @source.present?
20
20
 
21
+ @source = strip_comments(@source)
22
+ @source = strip_trailing_semicolons(@source)
23
+ @source = @source.strip
24
+ return unless @source.present?
25
+
21
26
  @output = +""
22
27
  @position = 0
23
28
 
@@ -110,5 +115,71 @@ module SqlBeautifier
110
115
  def requires_quoting?(identifier)
111
116
  identifier !~ SAFE_UNQUOTED_IDENTIFIER
112
117
  end
118
+
119
+ def strip_trailing_semicolons(sql)
120
+ sql.sub(%r{;[[:space:]]*\z}, "")
121
+ end
122
+
123
+ def strip_comments(sql)
124
+ output = +""
125
+ position = 0
126
+ in_single_quoted_string = false
127
+ in_double_quoted_identifier = false
128
+
129
+ while position < sql.length
130
+ character = sql[position]
131
+
132
+ if in_single_quoted_string
133
+ output << character
134
+
135
+ if character == Constants::SINGLE_QUOTE && sql[position + 1] == Constants::SINGLE_QUOTE
136
+ position += 1
137
+ output << sql[position]
138
+ elsif character == Constants::SINGLE_QUOTE
139
+ in_single_quoted_string = false
140
+ end
141
+
142
+ position += 1
143
+ next
144
+ end
145
+
146
+ if in_double_quoted_identifier
147
+ output << character
148
+
149
+ if character == Constants::DOUBLE_QUOTE && sql[position + 1] == Constants::DOUBLE_QUOTE
150
+ position += 1
151
+ output << sql[position]
152
+ elsif character == Constants::DOUBLE_QUOTE
153
+ in_double_quoted_identifier = false
154
+ end
155
+
156
+ position += 1
157
+ next
158
+ end
159
+
160
+ if character == Constants::SINGLE_QUOTE
161
+ in_single_quoted_string = true
162
+ output << character
163
+ position += 1
164
+ elsif character == Constants::DOUBLE_QUOTE
165
+ in_double_quoted_identifier = true
166
+ output << character
167
+ position += 1
168
+ elsif character == "-" && sql[position + 1] == "-"
169
+ position += 2
170
+ position += 1 while position < sql.length && sql[position] != "\n"
171
+ elsif character == "/" && sql[position + 1] == "*"
172
+ output << " " unless output.empty? || output[-1] =~ Constants::WHITESPACE_CHARACTER_REGEX
173
+ position += 2
174
+ position += 1 while position < sql.length && !(sql[position] == "*" && sql[position + 1] == "/")
175
+ position += 2 if position < sql.length
176
+ else
177
+ output << character
178
+ position += 1
179
+ end
180
+ end
181
+
182
+ output
183
+ end
113
184
  end
114
185
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module SubqueryFormatter
5
+ module_function
6
+
7
+ def format(text, base_indent)
8
+ output = +""
9
+ position = 0
10
+
11
+ while position < text.length
12
+ subquery_position = find_top_level_subquery(text, position)
13
+
14
+ unless subquery_position
15
+ output << text[position..]
16
+ break
17
+ end
18
+
19
+ output << text[position...subquery_position]
20
+
21
+ closing_position = Tokenizer.find_matching_parenthesis(text, subquery_position)
22
+
23
+ unless closing_position
24
+ output << text[subquery_position..]
25
+ break
26
+ end
27
+
28
+ inner_sql = text[(subquery_position + 1)...closing_position].strip
29
+ subquery_base_indent = subquery_base_indent_for(text, subquery_position, base_indent)
30
+ output << format_subquery(inner_sql, subquery_base_indent)
31
+ position = closing_position + 1
32
+ end
33
+
34
+ output
35
+ end
36
+
37
+ def find_top_level_subquery(text, start_position)
38
+ position = start_position
39
+ in_single_quoted_string = false
40
+ in_double_quoted_identifier = false
41
+ while position < text.length
42
+ character = text[position]
43
+
44
+ if in_single_quoted_string
45
+ if character == Constants::SINGLE_QUOTE && text[position + 1] == Constants::SINGLE_QUOTE
46
+ position += 2
47
+ elsif character == Constants::SINGLE_QUOTE
48
+ in_single_quoted_string = false
49
+ position += 1
50
+ else
51
+ position += 1
52
+ end
53
+ next
54
+ end
55
+
56
+ if in_double_quoted_identifier
57
+ if character == Constants::DOUBLE_QUOTE && text[position + 1] == Constants::DOUBLE_QUOTE
58
+ position += 2
59
+ elsif character == Constants::DOUBLE_QUOTE
60
+ in_double_quoted_identifier = false
61
+ position += 1
62
+ else
63
+ position += 1
64
+ end
65
+ next
66
+ end
67
+
68
+ case character
69
+ when Constants::SINGLE_QUOTE
70
+ in_single_quoted_string = true
71
+ when Constants::DOUBLE_QUOTE
72
+ in_double_quoted_identifier = true
73
+ when Constants::OPEN_PARENTHESIS
74
+ return position if select_follows?(text, position)
75
+ end
76
+
77
+ position += 1
78
+ end
79
+
80
+ nil
81
+ end
82
+
83
+ def format_subquery(inner_sql, base_indent)
84
+ indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
85
+ subquery_indent = base_indent + indent_spaces
86
+ formatted = Formatter.new(inner_sql, depth: subquery_indent).call
87
+ return "(#{inner_sql})" unless formatted
88
+
89
+ indentation = Util.whitespace(subquery_indent)
90
+ indented_lines = formatted.chomp.lines.map { |line| line.strip.empty? ? "\n" : "#{indentation}#{line}" }.join
91
+
92
+ "(\n#{indented_lines}\n#{Util.whitespace(base_indent)})"
93
+ end
94
+
95
+ def subquery_base_indent_for(text, subquery_position, default_base_indent)
96
+ line_start_position = text.rindex("\n", subquery_position - 1)
97
+ line_start_position = line_start_position ? line_start_position + 1 : 0
98
+ line_before_subquery = text[line_start_position...subquery_position]
99
+ line_leading_spaces = line_before_subquery[%r{\A[[:space:]]*}].to_s.length
100
+
101
+ return default_base_indent unless line_before_subquery.lstrip.match?(%r{\Awhere(?:[[:space:]]|$)}i)
102
+
103
+ default_base_indent + line_leading_spaces + SqlBeautifier.config_for(:keyword_column_width)
104
+ end
105
+
106
+ def select_follows?(text, position)
107
+ remaining_text = text[(position + 1)..]
108
+ return false unless remaining_text
109
+
110
+ remaining_text.match?(%r{\A[[:space:]]*select(?:[[:space:]]|\()}i)
111
+ end
112
+ end
113
+ end
@@ -7,6 +7,7 @@ module SqlBeautifier
7
7
  def initialize(from_content)
8
8
  @from_content = from_content
9
9
  @table_map = {}
10
+ @alias_strategy = SqlBeautifier.config_for(:alias_strategy)
10
11
  build!
11
12
  end
12
13
 
@@ -15,6 +16,8 @@ module SqlBeautifier
15
16
  end
16
17
 
17
18
  def apply_aliases(text)
19
+ return text if @table_map.empty?
20
+
18
21
  output = +""
19
22
  position = 0
20
23
 
@@ -48,10 +51,19 @@ module SqlBeautifier
48
51
 
49
52
  def build!
50
53
  table_entries = extract_table_entries(@from_content)
51
- initials_occurrence_counts = count_initials_occurrences(table_entries)
52
- used_aliases = []
53
54
 
54
- assign_aliases!(table_entries, initials_occurrence_counts, used_aliases)
55
+ if @alias_strategy == :none
56
+ table_entries.each do |table_entry|
57
+ next unless table_entry[:explicit_alias]
58
+
59
+ @table_map[table_entry[:table_name]] = table_entry[:explicit_alias]
60
+ end
61
+ else
62
+ initials_occurrence_counts = count_initials_occurrences(table_entries)
63
+ used_aliases = []
64
+ assign_aliases!(table_entries, initials_occurrence_counts, used_aliases)
65
+ end
66
+
55
67
  @tables_by_descending_length = @table_map.keys.sort_by { |name| -name.length }.freeze
56
68
  end
57
69
 
@@ -163,6 +175,8 @@ module SqlBeautifier
163
175
  end
164
176
 
165
177
  def table_initials(table_name)
178
+ return @alias_strategy.call(table_name) if @alias_strategy.respond_to?(:call)
179
+
166
180
  table_name.split("_").map { |segment| segment[0] }.join
167
181
  end
168
182
 
@@ -4,6 +4,10 @@ module SqlBeautifier
4
4
  module Util
5
5
  module_function
6
6
 
7
+ def whitespace(length)
8
+ " " * length
9
+ end
10
+
7
11
  def upper_pascal_case(name)
8
12
  name.split("_").map(&:capitalize).join("_")
9
13
  end
@@ -30,5 +34,34 @@ module SqlBeautifier
30
34
 
31
35
  value.gsub(Constants::DOUBLE_QUOTE, Constants::ESCAPED_DOUBLE_QUOTE)
32
36
  end
37
+
38
+ def keyword_padding(keyword)
39
+ formatted_keyword = format_keyword(keyword)
40
+ padding_width = [SqlBeautifier.config_for(:keyword_column_width) - formatted_keyword.length, 1].max
41
+
42
+ "#{formatted_keyword}#{whitespace(padding_width)}"
43
+ end
44
+
45
+ def continuation_padding
46
+ whitespace(SqlBeautifier.config_for(:keyword_column_width))
47
+ end
48
+
49
+ def format_keyword(keyword)
50
+ case SqlBeautifier.config_for(:keyword_case)
51
+ when :upper
52
+ keyword.upcase
53
+ else
54
+ keyword.downcase
55
+ end
56
+ end
57
+
58
+ def format_table_name(name)
59
+ case SqlBeautifier.config_for(:table_name_format)
60
+ when :lowercase
61
+ name.downcase
62
+ else
63
+ upper_pascal_case(name)
64
+ end
65
+ end
33
66
  end
34
67
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SqlBeautifier
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -5,11 +5,13 @@ require "active_support/core_ext/object/blank"
5
5
  require_relative "sql_beautifier/version"
6
6
  require_relative "sql_beautifier/constants"
7
7
  require_relative "sql_beautifier/util"
8
+ require_relative "sql_beautifier/configuration"
8
9
 
9
10
  require_relative "sql_beautifier/normalizer"
10
11
  require_relative "sql_beautifier/tokenizer"
11
12
  require_relative "sql_beautifier/table_registry"
12
13
  require_relative "sql_beautifier/condition_formatter"
14
+ require_relative "sql_beautifier/subquery_formatter"
13
15
  require_relative "sql_beautifier/clauses/base"
14
16
  require_relative "sql_beautifier/clauses/condition_clause"
15
17
  require_relative "sql_beautifier/clauses/select"
@@ -31,4 +33,20 @@ module SqlBeautifier
31
33
 
32
34
  Formatter.call(value)
33
35
  end
36
+
37
+ def configuration
38
+ @configuration ||= Configuration.new
39
+ end
40
+
41
+ def configure
42
+ yield configuration
43
+ end
44
+
45
+ def config_for(key)
46
+ configuration.public_send(key)
47
+ end
48
+
49
+ def reset_configuration!
50
+ @configuration = Configuration.new
51
+ end
34
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sql_beautifier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinnell Shah
@@ -47,9 +47,11 @@ files:
47
47
  - lib/sql_beautifier/clauses/select.rb
48
48
  - lib/sql_beautifier/clauses/where.rb
49
49
  - lib/sql_beautifier/condition_formatter.rb
50
+ - lib/sql_beautifier/configuration.rb
50
51
  - lib/sql_beautifier/constants.rb
51
52
  - lib/sql_beautifier/formatter.rb
52
53
  - lib/sql_beautifier/normalizer.rb
54
+ - lib/sql_beautifier/subquery_formatter.rb
53
55
  - lib/sql_beautifier/table_registry.rb
54
56
  - lib/sql_beautifier/tokenizer.rb
55
57
  - lib/sql_beautifier/util.rb