sql_beautifier 0.3.0 → 0.4.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: 6df1f55c9242793101e9f562329763541d56aa4f17f7e3d81a12038ab5b53cca
4
- data.tar.gz: 43fea825ac0c4bded07098814a2ed67e5503e1829f3d47cc99a7c989ec0e69d1
3
+ metadata.gz: 928801124a1241f4b12dc2e7e7966cd95dcba5754cc6ee88a08c9f641f2e32e4
4
+ data.tar.gz: eb1db005fb6923f5f99aaacc44caeeed14b6c7f6e4d8cd174fb3fcff92cf944e
5
5
  SHA512:
6
- metadata.gz: d56c72c27d4b71781be0ee83ba44fb3a4e466567c5edbde267c2cfbeb245c81e78bc52a309d27e7dae69a2a7e3cbf1bb3ce55926a266a6c914e822140b6650b7
7
- data.tar.gz: 9e2e5118b5b12e52551300e2c0219c6cc42907fdb87fd56a8e6e80e193b212ae36ff87ee48d2a0135dc0140121d145c3f104622a0efbd0a3bbeffe10af378cea
6
+ metadata.gz: c0ee8ca9569e7409f989cce52070112bb61af2c064dec679a96e7f26bb74837b8f2ae207617f619280140548fb5cc8804a1996464265415efc96d0e0fafc7085
7
+ data.tar.gz: b1cff6570a5137fd7ff26fd11eed662d7002c9d242ca03ea303c0c1711f4a44f12dbdc3958ce3657b0988742abd351792476ab0fc7171ccafebe1c28a70be595
data/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [X.X.X] - YYYY-MM-DD
4
4
 
5
+ ## [0.4.0] - 2026-03-27
6
+
5
7
  ## [0.3.0] - 2026-03-27
6
8
 
7
9
  - Add configuration system with `SqlBeautifier.configure` block and `SqlBeautifier.reset_configuration!`
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module CteFormatter
5
+ module_function
6
+
7
+ def format(normalized_sql, depth: 0)
8
+ return nil unless cte_query?(normalized_sql)
9
+
10
+ recursive, definitions, main_query_sql = parse(normalized_sql)
11
+ return nil unless definitions.any? && main_query_sql.present?
12
+
13
+ format_cte_statement(recursive, definitions, main_query_sql, depth)
14
+ end
15
+
16
+ def cte_query?(sql)
17
+ Tokenizer.keyword_at?(sql, 0, "with")
18
+ end
19
+
20
+ def parse(sql)
21
+ position = skip_past_keyword(sql, 0, "with")
22
+
23
+ recursive = Tokenizer.keyword_at?(sql, position, "recursive")
24
+ position = skip_past_keyword(sql, position, "recursive") if recursive
25
+
26
+ definitions = []
27
+
28
+ loop do
29
+ definition, new_position = parse_definition(sql, position)
30
+ break unless definition
31
+
32
+ definitions << definition
33
+ position = skip_whitespace(sql, new_position)
34
+
35
+ break unless position < sql.length && sql[position] == Constants::COMMA
36
+
37
+ position = skip_whitespace(sql, position + 1)
38
+ end
39
+
40
+ main_query_sql = sql[position..].strip
41
+
42
+ [recursive, definitions, main_query_sql]
43
+ end
44
+
45
+ def parse_definition(sql, position)
46
+ name, position = read_identifier(sql, position)
47
+ return nil unless name
48
+
49
+ position = skip_whitespace(sql, position)
50
+
51
+ column_list = parse_column_list(sql, position)
52
+ position = column_list[:next_position] if column_list
53
+
54
+ return nil unless Tokenizer.keyword_at?(sql, position, "as")
55
+
56
+ position = skip_past_keyword(sql, position, "as")
57
+ materialization, position = parse_materialization(sql, position)
58
+
59
+ return nil unless position < sql.length && sql[position] == Constants::OPEN_PARENTHESIS
60
+
61
+ closing = Tokenizer.find_matching_parenthesis(sql, position)
62
+ return nil unless closing
63
+
64
+ body_sql = sql[(position + 1)...closing].strip
65
+ definition = { name: name, body: body_sql }
66
+ definition[:column_list] = column_list[:text] if column_list
67
+ definition[:materialization] = materialization if materialization
68
+
69
+ [definition, closing + 1]
70
+ end
71
+
72
+ def parse_column_list(sql, position)
73
+ return nil unless position < sql.length && sql[position] == Constants::OPEN_PARENTHESIS
74
+
75
+ closing = Tokenizer.find_matching_parenthesis(sql, position)
76
+ return nil unless closing
77
+
78
+ after_paren = skip_whitespace(sql, closing + 1)
79
+ return nil unless Tokenizer.keyword_at?(sql, after_paren, "as")
80
+
81
+ { text: sql[(position + 1)...closing].strip, next_position: after_paren }
82
+ end
83
+
84
+ def format_cte_statement(recursive, definitions, main_query_sql, depth)
85
+ keyword_width = SqlBeautifier.config_for(:keyword_column_width)
86
+ cte_name_column = keyword_width
87
+ continuation_indent = Util.continuation_padding
88
+
89
+ output = +""
90
+
91
+ definitions.each_with_index do |definition, index|
92
+ if index.zero?
93
+ output << Util.keyword_padding("with")
94
+ output << "#{Util.format_keyword('recursive')} " if recursive
95
+ else
96
+ output << continuation_indent
97
+ end
98
+
99
+ output << definition_header(definition)
100
+ output << format_body(definition[:body], cte_name_column)
101
+ output << (index < definitions.length - 1 ? ",\n" : "\n\n")
102
+ end
103
+
104
+ formatted_main = Formatter.new(main_query_sql, depth: depth).call
105
+ output << formatted_main if formatted_main
106
+
107
+ output
108
+ end
109
+
110
+ def definition_header(definition)
111
+ header = +definition[:name].to_s
112
+ header << " (#{definition[:column_list]})" if definition[:column_list]
113
+ header << " #{Util.format_keyword('as')}"
114
+ header << " #{format_materialization(definition[:materialization])}" if definition[:materialization]
115
+ header << " "
116
+ header
117
+ end
118
+
119
+ def parse_materialization(sql, position)
120
+ position = skip_whitespace(sql, position)
121
+ return ["materialized", skip_past_keyword(sql, position, "materialized")] if Tokenizer.keyword_at?(sql, position, "materialized")
122
+ return [nil, position] unless Tokenizer.keyword_at?(sql, position, "not")
123
+
124
+ materialized_position = skip_past_keyword(sql, position, "not")
125
+ return [nil, position] unless Tokenizer.keyword_at?(sql, materialized_position, "materialized")
126
+
127
+ ["not materialized", skip_past_keyword(sql, materialized_position, "materialized")]
128
+ end
129
+
130
+ def format_materialization(materialization)
131
+ return Util.format_keyword("materialized") if materialization == "materialized"
132
+
133
+ [Util.format_keyword("not"), Util.format_keyword("materialized")].join(" ")
134
+ end
135
+
136
+ def format_body(body_sql, base_indent)
137
+ indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
138
+ body_indent = base_indent + indent_spaces
139
+ formatted = Formatter.new(body_sql, depth: 0).call
140
+ return "(#{body_sql})" unless formatted
141
+
142
+ indentation = Util.whitespace(body_indent)
143
+ indented_lines = formatted.chomp.lines.map { |line| line.strip.empty? ? "\n" : "#{indentation}#{line}" }.join
144
+
145
+ "(\n#{indented_lines}\n#{Util.whitespace(base_indent)})"
146
+ end
147
+
148
+ def read_identifier(sql, position)
149
+ position = skip_whitespace(sql, position)
150
+ return nil if position >= sql.length
151
+
152
+ if sql[position] == Constants::DOUBLE_QUOTE
153
+ start = position
154
+ position += 1
155
+
156
+ while position < sql.length
157
+ if sql[position] == Constants::DOUBLE_QUOTE
158
+ if position + 1 < sql.length && sql[position + 1] == Constants::DOUBLE_QUOTE
159
+ position += 2
160
+ next
161
+ end
162
+
163
+ position += 1
164
+ break
165
+ end
166
+
167
+ position += 1
168
+ end
169
+
170
+ return nil unless position <= sql.length && sql[position - 1] == Constants::DOUBLE_QUOTE
171
+
172
+ return [sql[start...position], position]
173
+ end
174
+
175
+ start = position
176
+ position += 1 while position < sql.length && sql[position] =~ Tokenizer::IDENTIFIER_CHARACTER
177
+ return nil if position == start
178
+
179
+ [sql[start...position], position]
180
+ end
181
+
182
+ def skip_whitespace(sql, position)
183
+ position += 1 while position < sql.length && sql[position] =~ Constants::WHITESPACE_CHARACTER_REGEX
184
+ position
185
+ end
186
+
187
+ def skip_past_keyword(sql, position, keyword)
188
+ skip_whitespace(sql, position + keyword.length)
189
+ end
190
+ end
191
+ end
@@ -17,6 +17,9 @@ module SqlBeautifier
17
17
  @normalized_value = Normalizer.call(@value)
18
18
  return unless @normalized_value.present?
19
19
 
20
+ cte_result = CteFormatter.format(@normalized_value, depth: @depth)
21
+ return cte_result if cte_result
22
+
20
23
  first_clause_position = Tokenizer.first_clause_position(@normalized_value)
21
24
  return "#{@normalized_value}\n" if first_clause_position.nil? || first_clause_position.positive?
22
25
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SqlBeautifier
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -12,6 +12,7 @@ require_relative "sql_beautifier/tokenizer"
12
12
  require_relative "sql_beautifier/table_registry"
13
13
  require_relative "sql_beautifier/condition_formatter"
14
14
  require_relative "sql_beautifier/subquery_formatter"
15
+ require_relative "sql_beautifier/cte_formatter"
15
16
  require_relative "sql_beautifier/clauses/base"
16
17
  require_relative "sql_beautifier/clauses/condition_clause"
17
18
  require_relative "sql_beautifier/clauses/select"
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinnell Shah
@@ -49,6 +49,7 @@ files:
49
49
  - lib/sql_beautifier/condition_formatter.rb
50
50
  - lib/sql_beautifier/configuration.rb
51
51
  - lib/sql_beautifier/constants.rb
52
+ - lib/sql_beautifier/cte_formatter.rb
52
53
  - lib/sql_beautifier/formatter.rb
53
54
  - lib/sql_beautifier/normalizer.rb
54
55
  - lib/sql_beautifier/subquery_formatter.rb