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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +129 -0
- data/lib/sql_beautifier/delete_query.rb +119 -0
- data/lib/sql_beautifier/dml_rendering.rb +22 -0
- data/lib/sql_beautifier/formatter.rb +9 -0
- data/lib/sql_beautifier/insert_query.rb +220 -0
- data/lib/sql_beautifier/update_query.rb +111 -0
- data/lib/sql_beautifier/version.rb +1 -1
- data/lib/sql_beautifier.rb +4 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4aaee15f7f4dc7c46c933893b8a02931c5fa83771707ccd22c092ec34ab859d
|
|
4
|
+
data.tar.gz: c059346ebc6f54814f8a7c9611ce768f022d8279631e0ca7f3234925f83a2f97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/sql_beautifier.rb
CHANGED
|
@@ -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.
|
|
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
|