sql_beautifier 0.8.0 → 0.9.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: 4a22a4c99d49622a7631196a0622d66ebcfb81144bbd85238e854cf5fb7ad6d3
4
- data.tar.gz: 685310c29776884c4164feeb272d6fdac9fb4e640df7272170ba6b0530ef10b4
3
+ metadata.gz: c4aaee15f7f4dc7c46c933893b8a02931c5fa83771707ccd22c092ec34ab859d
4
+ data.tar.gz: c059346ebc6f54814f8a7c9611ce768f022d8279631e0ca7f3234925f83a2f97
5
5
  SHA512:
6
- metadata.gz: a4df9d20c0c4c92c6fe59ffbaa270ab82ed7356ffdb545d2055cae874d35c1f64347dd67971ac1260bfced3bf41cfd9dbf83bc77472ed5c1664b18772e4210c4
7
- data.tar.gz: 26aebcef3cd11bc88dd898faee9d693bc72458e130ec814370c6163a903bbabb33c9cedd10cb40aadd5b7ea2f6ebc1edb42c0e8a21dc5471ca2a5c7ec0412c37
6
+ metadata.gz: b3717a14b1589c8aa1c8f14ffd046f68f385501c60f18ddb2b1330cde847189844d908600531b95b27a160dfebb4d0bb36b9044e25885e590f646a07e00b9f30
7
+ data.tar.gz: 52430c788c2ff7ebaf9da5e19c6cd5b1b73431e295fee2fed4b077ee36fabd9334bbbba6f8431c2020e6598bce6883f475cf3be624f898e9ff356b6aa1dff8ad
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [X.X.X] - YYYY-MM-DD
4
4
 
5
+ ## [0.9.0] - 2026-03-29
6
+
7
+ - Add DML statement formatting for `INSERT`, `UPDATE`, and `DELETE` — each statement type is routed to a dedicated entity class (`InsertQuery`, `UpdateQuery`, `DeleteQuery`) following the `Base` + `parse`/`render` pattern
8
+ - Add `INSERT INTO ... VALUES` formatting with aligned column lists and multi-row value support
9
+ - Add `INSERT INTO ... SELECT` formatting with automatic delegation of the SELECT portion to the existing formatter pipeline
10
+ - Add `INSERT ... ON CONFLICT` and `INSERT ... RETURNING` clause support
11
+ - Add `UPDATE ... SET` formatting with comma-separated assignment alignment and optional `FROM` and `WHERE` clauses
12
+ - Add `DELETE FROM` formatting with optional `USING`, `WHERE`, and `RETURNING` clauses
13
+ - Extract shared `render_where` and `render_returning` methods into `DmlRendering` module, included by `InsertQuery`, `UpdateQuery`, and `DeleteQuery`
14
+ - Fix `InsertQuery` accepting malformed `VALUES` clause with no value tuples — empty rows from `scan_value_rows` are now treated as a parse failure
15
+ - Fix `DeleteQuery` silently dropping table aliases (e.g. `DELETE FROM users u WHERE u.id = 1` rendered without the `u` alias, producing invalid SQL) — aliases (with or without `AS`) are now captured and included in the formatted output
16
+ - Fix `DeleteQuery` mis-parsing `DELETE FROM ONLY <table>` — `ONLY` was read as the table name; the parser now bails out for unsupported modifiers
17
+ - Fix `InsertQuery` silently dropping unrecognized trailing text after VALUES tuples (e.g. `VALUES (1) foo` would drop `foo`) — remaining text must start with `ON CONFLICT` or `RETURNING`, otherwise the parser bails out
18
+ - Fix `DeleteQuery` silently dropping unrecognized text between table/alias and clause keywords — the parser now bails out when remaining text doesn't match any known clause
19
+
5
20
  ## [0.8.0] - 2026-03-29
6
21
 
7
22
  - Add compound query support for set operators (`UNION`, `UNION ALL`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, `EXCEPT ALL`) — top-level set operator boundaries are detected via `Scanner`, each segment is independently formatted through the `Formatter` pipeline, and operators appear on their own line with blank-line separation
data/README.md CHANGED
@@ -227,6 +227,135 @@ order by created_at desc
227
227
  limit 25;
228
228
  ```
229
229
 
230
+ ### INSERT
231
+
232
+ `INSERT INTO ... VALUES` statements format with an indented column list and aligned value rows:
233
+
234
+ ```ruby
235
+ SqlBeautifier.call(<<~SQL)
236
+ INSERT INTO users (id, name, email)
237
+ VALUES (1, 'Alice', 'alice@example.com'),
238
+ (2, 'Bob', 'bob@example.com')
239
+ SQL
240
+ ```
241
+
242
+ Produces:
243
+
244
+ ```sql
245
+ insert into Users (
246
+ id,
247
+ name,
248
+ email
249
+ )
250
+ values (1, 'Alice', 'alice@example.com'),
251
+ (2, 'Bob', 'bob@example.com');
252
+ ```
253
+
254
+ `INSERT INTO ... SELECT` delegates the SELECT portion to the full formatter pipeline:
255
+
256
+ ```ruby
257
+ SqlBeautifier.call("INSERT INTO users (id, name) SELECT id, name FROM temp_users WHERE active = true")
258
+ ```
259
+
260
+ Produces:
261
+
262
+ ```sql
263
+ insert into Users (
264
+ id,
265
+ name
266
+ )
267
+
268
+ select id,
269
+ name
270
+
271
+ from Temp_Users tu
272
+
273
+ where active = true;
274
+ ```
275
+
276
+ PostgreSQL `ON CONFLICT` and `RETURNING` clauses are supported:
277
+
278
+ ```ruby
279
+ SqlBeautifier.call("INSERT INTO users (id, name) VALUES (1, 'Alice') ON CONFLICT (id) DO NOTHING RETURNING id")
280
+ ```
281
+
282
+ Produces:
283
+
284
+ ```sql
285
+ insert into Users (
286
+ id,
287
+ name
288
+ )
289
+ values (1, 'Alice')
290
+ on conflict (id) do nothing
291
+ returning id;
292
+ ```
293
+
294
+ ### UPDATE
295
+
296
+ `UPDATE ... SET` formats with aligned assignments and optional `FROM` and `WHERE` clauses:
297
+
298
+ ```ruby
299
+ SqlBeautifier.call("UPDATE users SET name = 'Alice', email = 'alice@example.com' WHERE id = 1")
300
+ ```
301
+
302
+ Produces:
303
+
304
+ ```sql
305
+ update Users
306
+ set name = 'Alice',
307
+ email = 'alice@example.com'
308
+ where id = 1;
309
+ ```
310
+
311
+ PostgreSQL join-style `UPDATE ... FROM ... WHERE` is supported:
312
+
313
+ ```ruby
314
+ SqlBeautifier.call("UPDATE users SET name = accounts.name FROM accounts WHERE users.account_id = accounts.id")
315
+ ```
316
+
317
+ Produces:
318
+
319
+ ```sql
320
+ update Users
321
+ set name = accounts.name
322
+ from accounts
323
+ where users.account_id = accounts.id;
324
+ ```
325
+
326
+ ### DELETE
327
+
328
+ `DELETE FROM` formats with standard clause layout:
329
+
330
+ ```ruby
331
+ SqlBeautifier.call("DELETE FROM users WHERE status = 'inactive' AND last_login < '2024-01-01'")
332
+ ```
333
+
334
+ Produces:
335
+
336
+ ```sql
337
+ delete
338
+ from Users
339
+ where status = 'inactive'
340
+ and last_login < '2024-01-01';
341
+ ```
342
+
343
+ PostgreSQL `USING` and `RETURNING` clauses are supported:
344
+
345
+ ```ruby
346
+ SqlBeautifier.call("DELETE FROM users USING accounts WHERE users.account_id = accounts.id RETURNING users.id")
347
+ ```
348
+
349
+ Produces:
350
+
351
+ ```sql
352
+ delete
353
+ from Users
354
+ using accounts
355
+ where users.account_id = accounts.id
356
+ returning users.id;
357
+ ```
358
+
230
359
  ### Set Operators (UNION, INTERSECT, EXCEPT)
231
360
 
232
361
  Compound queries joined by set operators are detected and each segment is formatted independently. The operator keyword appears on its own line with blank-line separation:
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class DeleteQuery < Base
5
+ include DmlRendering
6
+
7
+ option :table_name
8
+ option :table_alias, default: -> {}
9
+ option :using_clause, default: -> {}
10
+ option :where_clause, default: -> {}
11
+ option :returning_clause, default: -> {}
12
+ option :depth, default: -> { 0 }
13
+
14
+ def self.parse(normalized_sql, depth: 0)
15
+ scanner = Scanner.new(normalized_sql)
16
+ return nil unless scanner.keyword_at?("delete")
17
+
18
+ scanner.skip_past_keyword!("delete")
19
+
20
+ return nil unless scanner.keyword_at?("from")
21
+
22
+ scanner.skip_past_keyword!("from")
23
+ return nil if scanner.keyword_at?("only")
24
+
25
+ table_name = scanner.read_identifier!
26
+ return nil unless table_name
27
+
28
+ scanner.skip_whitespace!
29
+
30
+ table_alias = parse_table_alias(scanner)
31
+
32
+ remaining_text = normalized_sql[scanner.position..].strip
33
+ clauses = split_delete_clauses(normalized_sql, scanner.position)
34
+ return nil if remaining_text.present? && clauses.values.all?(&:nil?)
35
+
36
+ new(
37
+ table_name: table_name,
38
+ table_alias: table_alias,
39
+ using_clause: clauses[:using_clause],
40
+ where_clause: clauses[:where_clause],
41
+ returning_clause: clauses[:returning_clause],
42
+ depth: depth
43
+ )
44
+ end
45
+
46
+ def render
47
+ output = +""
48
+ output << render_delete
49
+ output << render_from
50
+ output << render_using if @using_clause
51
+ output << render_where if @where_clause
52
+ output << render_returning if @returning_clause
53
+
54
+ "#{output}\n"
55
+ end
56
+
57
+ def self.parse_table_alias(scanner)
58
+ return nil if scanner.finished?
59
+
60
+ next_keywords = %w[using where returning]
61
+ return nil if next_keywords.any? { |keyword| scanner.keyword_at?(keyword) }
62
+
63
+ scanner.skip_past_keyword!("as") if scanner.keyword_at?("as")
64
+
65
+ alias_name = scanner.read_identifier!
66
+ scanner.skip_whitespace! if alias_name
67
+
68
+ alias_name
69
+ end
70
+
71
+ def self.split_delete_clauses(normalized_sql, after_table_position)
72
+ remaining = normalized_sql[after_table_position..]
73
+ return {} unless remaining
74
+
75
+ using_position = Tokenizer.find_top_level_keyword(remaining, "using")
76
+ where_position = Tokenizer.find_top_level_keyword(remaining, "where")
77
+ returning_position = Tokenizer.find_top_level_keyword(remaining, "returning")
78
+
79
+ using_clause = nil
80
+ if using_position
81
+ using_end = [where_position, returning_position].compact.min || remaining.length
82
+ using_clause = remaining[(using_position + "using".length)...using_end].strip
83
+ end
84
+
85
+ where_clause = nil
86
+ if where_position
87
+ where_end = returning_position || remaining.length
88
+ where_clause = remaining[(where_position + "where".length)...where_end].strip
89
+ end
90
+
91
+ returning_clause = remaining[(returning_position + "returning".length)..].strip if returning_position
92
+
93
+ {
94
+ using_clause: using_clause.presence,
95
+ where_clause: where_clause.presence,
96
+ returning_clause: returning_clause.presence,
97
+ }
98
+ end
99
+
100
+ private_class_method :parse_table_alias, :split_delete_clauses
101
+
102
+ private
103
+
104
+ def render_delete
105
+ Util.format_keyword("delete")
106
+ end
107
+
108
+ def render_from
109
+ table_reference = Util.format_table_name(@table_name)
110
+ table_reference = "#{table_reference} #{@table_alias}" if @table_alias
111
+
112
+ "\n#{Util.keyword_padding('from')}#{table_reference}"
113
+ end
114
+
115
+ def render_using
116
+ "\n#{Util.keyword_padding('using')}#{@using_clause}"
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module DmlRendering
5
+ private
6
+
7
+ def render_where
8
+ keyword_column_width = SqlBeautifier.config_for(:keyword_column_width)
9
+ conditions = Condition.parse_all(@where_clause)
10
+
11
+ return "\n#{Util.keyword_padding('where')}#{@where_clause.strip}" if conditions.length <= 1 && conditions.first&.leaf?
12
+
13
+ formatted_conditions = Condition.render_all(conditions, indent_width: keyword_column_width)
14
+
15
+ "\n#{formatted_conditions.sub(Util.continuation_padding, Util.keyword_padding('where'))}"
16
+ end
17
+
18
+ def render_returning
19
+ "\n#{Util.keyword_padding('returning')}#{@returning_clause}"
20
+ end
21
+ end
22
+ end
@@ -32,6 +32,15 @@ module SqlBeautifier
32
32
  compound_result = CompoundQuery.parse(@normalized_value, depth: @depth)&.render
33
33
  return prepend_sentinels(compound_result) if compound_result
34
34
 
35
+ insert_result = InsertQuery.parse(@normalized_value, depth: @depth)&.render
36
+ return prepend_sentinels(insert_result) if insert_result
37
+
38
+ update_result = UpdateQuery.parse(@normalized_value, depth: @depth)&.render
39
+ return prepend_sentinels(update_result) if update_result
40
+
41
+ delete_result = DeleteQuery.parse(@normalized_value, depth: @depth)&.render
42
+ return prepend_sentinels(delete_result) if delete_result
43
+
35
44
  first_clause_position = Tokenizer.first_clause_position(@normalized_value)
36
45
  return prepend_sentinels("#{@normalized_value}\n") if first_clause_position.nil? || first_clause_position.positive?
37
46
 
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class InsertQuery < Base
5
+ include DmlRendering
6
+
7
+ option :table_name
8
+ option :column_list, default: -> {}
9
+ option :values_rows, default: -> {}
10
+ option :select_sql, default: -> {}
11
+ option :on_conflict_clause, default: -> {}
12
+ option :returning_clause, 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?("insert")
18
+
19
+ scanner.skip_past_keyword!("insert")
20
+ return nil unless scanner.keyword_at?("into")
21
+
22
+ scanner.skip_past_keyword!("into")
23
+
24
+ table_name = scanner.read_identifier!
25
+ return nil unless table_name
26
+
27
+ scanner.skip_whitespace!
28
+
29
+ column_list = parse_column_list(normalized_sql, scanner)
30
+ values_rows, select_sql, on_conflict_clause, returning_clause = parse_body(normalized_sql, scanner)
31
+
32
+ return nil unless values_rows || select_sql
33
+
34
+ new(
35
+ table_name: table_name,
36
+ column_list: column_list,
37
+ values_rows: values_rows,
38
+ select_sql: select_sql,
39
+ on_conflict_clause: on_conflict_clause,
40
+ returning_clause: returning_clause,
41
+ depth: depth
42
+ )
43
+ end
44
+
45
+ def render
46
+ output = +""
47
+ output << render_insert_into
48
+ output << render_column_list if @column_list
49
+ output << render_values if @values_rows
50
+ output << render_select if @select_sql
51
+ output << render_on_conflict if @on_conflict_clause
52
+ output << render_returning if @returning_clause
53
+
54
+ "#{output}\n"
55
+ end
56
+
57
+ def self.parse_column_list(normalized_sql, scanner)
58
+ return nil unless scanner.position < normalized_sql.length && scanner.current_char == Constants::OPEN_PARENTHESIS
59
+
60
+ closing = scanner.find_matching_parenthesis(scanner.position)
61
+ return nil unless closing
62
+
63
+ inner_text = normalized_sql[(scanner.position + 1)...closing].strip
64
+ scanner.advance!(closing + 1 - scanner.position)
65
+ scanner.skip_whitespace!
66
+
67
+ return nil if inner_text.empty?
68
+
69
+ inner_text
70
+ end
71
+
72
+ def self.parse_body(normalized_sql, scanner)
73
+ remaining = normalized_sql[scanner.position..].strip
74
+ scanner.advance!(normalized_sql.length - scanner.position)
75
+
76
+ values_rows = nil
77
+ select_sql = nil
78
+ on_conflict_clause = nil
79
+ returning_clause = nil
80
+
81
+ remaining_scanner = Scanner.new(remaining)
82
+
83
+ if remaining_scanner.keyword_at?("values")
84
+ remaining_scanner.skip_past_keyword!("values")
85
+ values_text = remaining[remaining_scanner.position..].strip
86
+ values_rows, on_conflict_clause, returning_clause = split_values_tail(values_text)
87
+ elsif remaining_scanner.keyword_at?("select")
88
+ select_sql, on_conflict_clause, returning_clause = split_select_tail(remaining)
89
+ end
90
+
91
+ [values_rows, select_sql, on_conflict_clause, returning_clause]
92
+ end
93
+
94
+ def self.split_values_tail(values_text)
95
+ rows, remaining_text = scan_value_rows(values_text)
96
+ return [nil, nil, nil] if rows.empty?
97
+
98
+ on_conflict_clause = nil
99
+ returning_clause = nil
100
+
101
+ if remaining_text.present?
102
+ normalized_remaining = remaining_text.lstrip.downcase
103
+ return [nil, nil, nil] unless normalized_remaining.start_with?("on conflict", "returning")
104
+
105
+ on_conflict_clause, returning_clause = split_on_conflict_and_returning(remaining_text)
106
+ end
107
+
108
+ [rows, on_conflict_clause, returning_clause]
109
+ end
110
+
111
+ def self.scan_value_rows(values_text)
112
+ scanner = Scanner.new(values_text)
113
+ rows = []
114
+
115
+ until scanner.finished?
116
+ scanner.skip_whitespace!
117
+ break if scanner.finished?
118
+ break unless scanner.current_char == Constants::OPEN_PARENTHESIS
119
+
120
+ closing = scanner.find_matching_parenthesis(scanner.position)
121
+ break unless closing
122
+
123
+ rows << values_text[scanner.position..(closing)]
124
+ scanner.advance!(closing + 1 - scanner.position)
125
+ scanner.skip_whitespace!
126
+
127
+ break unless !scanner.finished? && scanner.current_char == Constants::COMMA
128
+
129
+ scanner.advance!
130
+ end
131
+
132
+ remaining_text = values_text[scanner.position..].strip
133
+
134
+ [rows, remaining_text]
135
+ end
136
+
137
+ def self.split_select_tail(remaining)
138
+ on_conflict_position = find_top_level_keyword_position(remaining, "on conflict")
139
+ returning_position = find_top_level_keyword_position(remaining, "returning")
140
+
141
+ end_of_select = [on_conflict_position, returning_position].compact.min || remaining.length
142
+ select_sql = remaining[0...end_of_select].strip
143
+
144
+ on_conflict_clause = nil
145
+ returning_clause = nil
146
+
147
+ if on_conflict_position
148
+ on_conflict_end = returning_position || remaining.length
149
+ on_conflict_clause = remaining[on_conflict_position...on_conflict_end].strip
150
+ end
151
+
152
+ returning_clause = remaining[(returning_position + "returning".length)..].strip if returning_position
153
+
154
+ [select_sql, on_conflict_clause, returning_clause]
155
+ end
156
+
157
+ def self.split_on_conflict_and_returning(text)
158
+ on_conflict_position = find_top_level_keyword_position(text, "on conflict")
159
+ returning_position = find_top_level_keyword_position(text, "returning")
160
+
161
+ on_conflict_clause = nil
162
+ returning_clause = nil
163
+
164
+ if on_conflict_position
165
+ on_conflict_end = returning_position || text.length
166
+ on_conflict_clause = text[on_conflict_position...on_conflict_end].strip
167
+ end
168
+
169
+ returning_clause = text[(returning_position + "returning".length)..].strip if returning_position
170
+
171
+ [on_conflict_clause, returning_clause]
172
+ end
173
+
174
+ def self.find_top_level_keyword_position(text, keyword)
175
+ Tokenizer.find_top_level_keyword(text, keyword)
176
+ end
177
+
178
+ private_class_method :parse_column_list, :parse_body, :split_values_tail, :scan_value_rows, :split_select_tail, :split_on_conflict_and_returning, :find_top_level_keyword_position
179
+
180
+ private
181
+
182
+ def render_insert_into
183
+ "#{Util.keyword_padding('insert into')}#{Util.format_table_name(@table_name)}"
184
+ end
185
+
186
+ def render_column_list
187
+ columns = Tokenizer.split_by_top_level_commas(@column_list)
188
+ indent = Util.whitespace(SqlBeautifier.config_for(:indent_spaces) || 4)
189
+
190
+ formatted_columns = columns.map { |column| "#{indent}#{column.strip}" }.join(",\n")
191
+
192
+ " (\n#{formatted_columns}\n)"
193
+ end
194
+
195
+ def render_values
196
+ continuation = Util.continuation_padding
197
+
198
+ formatted_rows = @values_rows.map.with_index do |row, index|
199
+ if index.zero?
200
+ "#{Util.keyword_padding('values')}#{row}"
201
+ else
202
+ "#{continuation}#{row}"
203
+ end
204
+ end
205
+
206
+ "\n#{formatted_rows.join(",\n")}"
207
+ end
208
+
209
+ def render_select
210
+ formatted_select = Formatter.new(@select_sql, depth: @depth).call
211
+ return "" unless formatted_select
212
+
213
+ "\n\n#{formatted_select.chomp}"
214
+ end
215
+
216
+ def render_on_conflict
217
+ "\n#{@on_conflict_clause}"
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class UpdateQuery < Base
5
+ include DmlRendering
6
+
7
+ option :table_name
8
+ option :assignments
9
+ option :from_clause, default: -> {}
10
+ option :where_clause, default: -> {}
11
+ option :returning_clause, default: -> {}
12
+ option :depth, default: -> { 0 }
13
+
14
+ def self.parse(normalized_sql, depth: 0)
15
+ scanner = Scanner.new(normalized_sql)
16
+ return nil unless scanner.keyword_at?("update")
17
+
18
+ scanner.skip_past_keyword!("update")
19
+
20
+ table_name = scanner.read_identifier!
21
+ return nil unless table_name
22
+
23
+ scanner.skip_whitespace!
24
+ return nil unless scanner.keyword_at?("set")
25
+
26
+ scanner.skip_past_keyword!("set")
27
+
28
+ clauses = split_update_clauses(normalized_sql, scanner.position)
29
+ return nil unless clauses[:assignments]
30
+
31
+ new(
32
+ table_name: table_name,
33
+ assignments: clauses[:assignments],
34
+ from_clause: clauses[:from_clause],
35
+ where_clause: clauses[:where_clause],
36
+ returning_clause: clauses[:returning_clause],
37
+ depth: depth
38
+ )
39
+ end
40
+
41
+ def render
42
+ output = +""
43
+ output << render_update
44
+ output << render_assignments
45
+ output << render_from if @from_clause
46
+ output << render_where if @where_clause
47
+ output << render_returning if @returning_clause
48
+
49
+ "#{output}\n"
50
+ end
51
+
52
+ def self.split_update_clauses(normalized_sql, set_content_start)
53
+ remaining = normalized_sql[set_content_start..]
54
+
55
+ from_position = Tokenizer.find_top_level_keyword(remaining, "from")
56
+ where_position = Tokenizer.find_top_level_keyword(remaining, "where")
57
+ returning_position = Tokenizer.find_top_level_keyword(remaining, "returning")
58
+
59
+ assignments_end = [from_position, where_position, returning_position].compact.min || remaining.length
60
+ assignments_text = remaining[0...assignments_end].strip
61
+
62
+ from_clause = nil
63
+ if from_position
64
+ from_end = [where_position, returning_position].compact.min || remaining.length
65
+ from_clause = remaining[(from_position + "from".length)...from_end].strip
66
+ end
67
+
68
+ where_clause = nil
69
+ if where_position
70
+ where_end = returning_position || remaining.length
71
+ where_clause = remaining[(where_position + "where".length)...where_end].strip
72
+ end
73
+
74
+ returning_clause = remaining[(returning_position + "returning".length)..].strip if returning_position
75
+
76
+ {
77
+ assignments: assignments_text.presence,
78
+ from_clause: from_clause.presence,
79
+ where_clause: where_clause.presence,
80
+ returning_clause: returning_clause.presence,
81
+ }
82
+ end
83
+
84
+ private_class_method :split_update_clauses
85
+
86
+ private
87
+
88
+ def render_update
89
+ "#{Util.keyword_padding('update')}#{Util.format_table_name(@table_name)}"
90
+ end
91
+
92
+ def render_assignments
93
+ items = Tokenizer.split_by_top_level_commas(@assignments)
94
+ continuation = Util.continuation_padding
95
+
96
+ formatted_items = items.map.with_index do |item, index|
97
+ if index.zero?
98
+ "\n#{Util.keyword_padding('set')}#{item.strip}"
99
+ else
100
+ "\n#{continuation}#{item.strip}"
101
+ end
102
+ end
103
+
104
+ formatted_items.join(",")
105
+ end
106
+
107
+ def render_from
108
+ "\n#{Util.keyword_padding('from')}#{@from_clause}"
109
+ end
110
+ end
111
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SqlBeautifier
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -25,6 +25,10 @@ require_relative "sql_beautifier/cte_definition"
25
25
  require_relative "sql_beautifier/cte_query"
26
26
  require_relative "sql_beautifier/create_table_as"
27
27
  require_relative "sql_beautifier/compound_query"
28
+ require_relative "sql_beautifier/dml_rendering"
29
+ require_relative "sql_beautifier/insert_query"
30
+ require_relative "sql_beautifier/update_query"
31
+ require_relative "sql_beautifier/delete_query"
28
32
  require_relative "sql_beautifier/clauses/base"
29
33
  require_relative "sql_beautifier/clauses/condition_clause"
30
34
  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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinnell Shah
@@ -84,8 +84,11 @@ files:
84
84
  - lib/sql_beautifier/create_table_as.rb
85
85
  - lib/sql_beautifier/cte_definition.rb
86
86
  - lib/sql_beautifier/cte_query.rb
87
+ - lib/sql_beautifier/delete_query.rb
88
+ - lib/sql_beautifier/dml_rendering.rb
87
89
  - lib/sql_beautifier/expression.rb
88
90
  - lib/sql_beautifier/formatter.rb
91
+ - lib/sql_beautifier/insert_query.rb
89
92
  - lib/sql_beautifier/join.rb
90
93
  - lib/sql_beautifier/normalizer.rb
91
94
  - lib/sql_beautifier/query.rb
@@ -97,6 +100,7 @@ files:
97
100
  - lib/sql_beautifier/table_registry.rb
98
101
  - lib/sql_beautifier/tokenizer.rb
99
102
  - lib/sql_beautifier/types.rb
103
+ - lib/sql_beautifier/update_query.rb
100
104
  - lib/sql_beautifier/util.rb
101
105
  - lib/sql_beautifier/version.rb
102
106
  homepage: https://github.com/kinnell/sql_beautifier