sql_beautifier 0.10.2 → 0.10.3

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: 918b0bcf08b9f13dabc26250b303f2b35244cf996877939ac6c31ef7d263dd24
4
- data.tar.gz: b8daaffc147aaf1556823c3bbc9ca95efdf5070a0747638b2dcc2af14b090e0e
3
+ metadata.gz: 35c33bf6ef1f1c6095643a9e6661aa16a93a29ff44534aab07fddd0befec5b43
4
+ data.tar.gz: aab36afe287416dc61b4633f3d4c07975736d6111a427118bf975be2742325a6
5
5
  SHA512:
6
- metadata.gz: 3e9188e453a6055bbd79f7c1b4eb0b9aed091a7123d3ce83613e64a82629898022c090845fa36f6a68ff7a51cb692ad97b07dae55b3f1ec5927d196daa267ed7
7
- data.tar.gz: 89a38090f7fd926e77a54d46d340a50d7b45bd461f49eda0fda865d6801f62ad8ccac2c751f4535cffd6418f1e2f9146f43fd9573e398bcb5113eb01ab022a5e
6
+ metadata.gz: 58f7b763cc122816a919b2caa1ba0281efb5fa8dc1bfbd4e97f27c6838728e8a8db2a6018d819d1f2314359851f1cd138f28bb0f0fb21986defa4a1e11ad724a
7
+ data.tar.gz: db2e7de085a689a9331e831432f5ed31f8a58304c9f20f4cca2376acf68681da9ba84741c18bf0fb9b9002c85fb4f1d87c3e8bf92fdf31a384455caba9cccb23
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [X.X.X] - YYYY-MM-DD
4
4
 
5
+ ## [0.10.3] - 2026-03-30
6
+
7
+ - Add `DROP TABLE` formatting — `DROP TABLE [IF EXISTS] table_name` statements are now recognized and rendered with proper keyword casing and PascalCase table names instead of passing through as normalized text
8
+ - Add `CREATE TABLE` (DDL) formatting — `CREATE [TEMP|TEMPORARY|UNLOGGED|LOCAL] TABLE [IF NOT EXISTS] table_name (column_defs)` statements with column definitions are now recognized and rendered with proper keyword casing and PascalCase table names
9
+ - Fix `INSERT INTO` with a single column rendering the column list on multiple lines — single-column lists now render inline (e.g. `insert into Table (id)` instead of expanding to three lines)
10
+ - Reject `DROP TABLE` statements with trailing text (e.g. `CASCADE`, `RESTRICT`, multiple table names) — these now return `nil` instead of silently dropping the trailing text
11
+ - Reject `CREATE TABLE` (DDL) statements with trailing clauses after column definitions (e.g. `WITH (...)`, `TABLESPACE ...`) — these now return `nil` instead of silently dropping the trailing text
12
+
5
13
  ## [0.10.2] - 2026-03-30
6
14
 
7
15
  - Fix `INSERT INTO ... (columns) (SELECT ...)` not being recognized — `InsertQuery.parse_body` now unwraps parenthesized SELECT subqueries, supporting PostgreSQL's valid `INSERT INTO table (cols) (SELECT ...)` syntax
data/README.md CHANGED
@@ -411,6 +411,36 @@ where users.account_id = accounts.id
411
411
  returning users.id;
412
412
  ```
413
413
 
414
+ ### DROP TABLE
415
+
416
+ `DROP TABLE` statements are recognized and formatted with proper keyword casing and table name formatting:
417
+
418
+ ```ruby
419
+ SqlBeautifier.call("DROP TABLE IF EXISTS persons")
420
+ ```
421
+
422
+ Produces:
423
+
424
+ ```sql
425
+ drop table if exists Persons;
426
+ ```
427
+
428
+ ### CREATE TABLE (DDL)
429
+
430
+ `CREATE TABLE` statements with column definitions are recognized and formatted with proper keyword casing and table name formatting. Column definitions are preserved as-is:
431
+
432
+ ```ruby
433
+ SqlBeautifier.call("CREATE TEMPORARY TABLE persons (id bigint)")
434
+ ```
435
+
436
+ Produces:
437
+
438
+ ```sql
439
+ create temporary table Persons (id bigint);
440
+ ```
441
+
442
+ Modifiers (`TEMP`, `TEMPORARY`, `UNLOGGED`, `LOCAL`) and `IF NOT EXISTS` are supported.
443
+
414
444
  ### Set Operators (UNION, INTERSECT, EXCEPT)
415
445
 
416
446
  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:
@@ -42,6 +42,13 @@ module SqlBeautifier
42
42
  CLOSE_PARENTHESIS = ")"
43
43
  COMMA = ","
44
44
 
45
+ TABLE_MODIFIERS = %w[
46
+ temp
47
+ temporary
48
+ unlogged
49
+ local
50
+ ].freeze
51
+
45
52
  LATERAL_PREFIX_PATTERN = %r{\Alateral\s+}i
46
53
 
47
54
  WHITESPACE_REGEX = %r{\s+}
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class CreateTable < Base
5
+ extend CreateTableParsing
6
+
7
+ option :modifier, default: -> {}
8
+ option :if_not_exists, type: Types::Bool
9
+ option :table_name
10
+ option :column_definitions
11
+
12
+ def self.parse(normalized_sql, **)
13
+ scanner = Scanner.new(normalized_sql)
14
+ return nil unless scanner.keyword_at?("create")
15
+
16
+ scanner.skip_past_keyword!("create")
17
+ modifier = detect_modifier(scanner)
18
+ scanner.skip_past_keyword!(modifier) if modifier
19
+
20
+ return nil unless scanner.keyword_at?("table")
21
+
22
+ scanner.skip_past_keyword!("table")
23
+
24
+ if_not_exists = detect_if_not_exists?(scanner)
25
+ skip_past_if_not_exists!(scanner) if if_not_exists
26
+
27
+ table_name = scanner.read_identifier!
28
+ return nil unless table_name
29
+
30
+ scanner.skip_whitespace!
31
+ return nil if scanner.finished?
32
+ return nil unless scanner.current_char == Constants::OPEN_PARENTHESIS
33
+
34
+ column_definitions = extract_column_definitions(normalized_sql, scanner)
35
+ return nil unless column_definitions
36
+
37
+ new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, column_definitions: column_definitions)
38
+ end
39
+
40
+ def self.extract_column_definitions(normalized_sql, scanner)
41
+ closing = scanner.find_matching_parenthesis(scanner.position)
42
+ return nil unless closing
43
+
44
+ inner_text = normalized_sql[(scanner.position + 1)...closing].strip
45
+ return nil if inner_text.empty?
46
+
47
+ trailing_text = normalized_sql[(closing + 1)..].strip
48
+ return nil unless trailing_text.empty?
49
+
50
+ inner_text
51
+ end
52
+
53
+ private_class_method :extract_column_definitions
54
+
55
+ def render
56
+ parts = [Util.format_keyword("create")]
57
+ parts << Util.format_keyword(@modifier) if @modifier
58
+ parts << Util.format_keyword("table")
59
+ parts << "#{Util.format_keyword('if')} #{Util.format_keyword('not')} #{Util.format_keyword('exists')}" if @if_not_exists
60
+ parts << Util.format_table_name(@table_name)
61
+
62
+ "#{parts.join(' ')} (#{@column_definitions})\n"
63
+ end
64
+ end
65
+ end
@@ -2,12 +2,7 @@
2
2
 
3
3
  module SqlBeautifier
4
4
  class CreateTableAs < Base
5
- MODIFIERS = %w[
6
- temp
7
- temporary
8
- unlogged
9
- local
10
- ].freeze
5
+ extend CreateTableParsing
11
6
 
12
7
  WITH_DATA_SUFFIX_REGEX = %r{\s+(with\s+(?:no\s+)?data)\s*\z}i
13
8
 
@@ -47,27 +42,6 @@ module SqlBeautifier
47
42
  new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, body_sql: body_sql, suffix: suffix, depth: depth)
48
43
  end
49
44
 
50
- def self.detect_modifier(scanner)
51
- MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) }
52
- end
53
-
54
- def self.detect_if_not_exists?(scanner)
55
- return false unless scanner.keyword_at?("if")
56
-
57
- probe = Scanner.new(scanner.source, position: scanner.position)
58
- probe.skip_past_keyword!("if")
59
- return false unless probe.keyword_at?("not")
60
-
61
- probe.skip_past_keyword!("not")
62
- probe.keyword_at?("exists")
63
- end
64
-
65
- def self.skip_past_if_not_exists!(scanner)
66
- scanner.skip_past_keyword!("if")
67
- scanner.skip_past_keyword!("not")
68
- scanner.skip_past_keyword!("exists")
69
- end
70
-
71
45
  def self.extract_body(sql, position)
72
46
  scanner = Scanner.new(sql, position: position)
73
47
  scanner.skip_whitespace!
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ module CreateTableParsing
5
+ def self.extended(base)
6
+ base.private_class_method :detect_modifier
7
+ base.private_class_method :detect_if_not_exists?
8
+ base.private_class_method :skip_past_if_not_exists!
9
+ end
10
+
11
+ def detect_modifier(scanner)
12
+ Constants::TABLE_MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) }
13
+ end
14
+
15
+ def detect_if_not_exists?(scanner)
16
+ return false unless scanner.keyword_at?("if")
17
+
18
+ probe = Scanner.new(scanner.source, position: scanner.position)
19
+ probe.skip_past_keyword!("if")
20
+ return false unless probe.keyword_at?("not")
21
+
22
+ probe.skip_past_keyword!("not")
23
+ probe.keyword_at?("exists")
24
+ end
25
+
26
+ def skip_past_if_not_exists!(scanner)
27
+ scanner.skip_past_keyword!("if")
28
+ scanner.skip_past_keyword!("not")
29
+ scanner.skip_past_keyword!("exists")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class DropTable < Base
5
+ option :table_name
6
+ option :if_exists, type: Types::Bool
7
+
8
+ def self.parse(normalized_sql, **)
9
+ scanner = Scanner.new(normalized_sql)
10
+ return nil unless scanner.keyword_at?("drop")
11
+
12
+ scanner.skip_past_keyword!("drop")
13
+ return nil unless scanner.keyword_at?("table")
14
+
15
+ scanner.skip_past_keyword!("table")
16
+
17
+ if_exists = false
18
+
19
+ if scanner.keyword_at?("if")
20
+ return nil unless detect_if_exists?(scanner)
21
+
22
+ skip_past_if_exists!(scanner)
23
+ if_exists = true
24
+ end
25
+
26
+ table_name = scanner.read_identifier!
27
+ return nil unless table_name
28
+
29
+ scanner.skip_whitespace!
30
+ return nil unless scanner.finished?
31
+
32
+ new(table_name: table_name, if_exists: if_exists)
33
+ end
34
+
35
+ def self.detect_if_exists?(scanner)
36
+ probe = Scanner.new(scanner.source, position: scanner.position)
37
+ probe.skip_past_keyword!("if")
38
+ probe.keyword_at?("exists")
39
+ end
40
+
41
+ def self.skip_past_if_exists!(scanner)
42
+ scanner.skip_past_keyword!("if")
43
+ scanner.skip_past_keyword!("exists")
44
+ end
45
+
46
+ private_class_method :detect_if_exists?, :skip_past_if_exists!
47
+
48
+ def render
49
+ parts = [Util.format_keyword("drop"), Util.format_keyword("table")]
50
+ parts << "#{Util.format_keyword('if')} #{Util.format_keyword('exists')}" if @if_exists
51
+ parts << Util.format_table_name(@table_name)
52
+
53
+ "#{parts.join(' ')}\n"
54
+ end
55
+ end
56
+ end
@@ -23,12 +23,18 @@ module SqlBeautifier
23
23
  @leading_sentinels = extract_leading_sentinels!
24
24
  return unless @normalized_value.present?
25
25
 
26
+ drop_table_result = DropTable.parse(@normalized_value)&.render
27
+ return prepend_sentinels(drop_table_result) if drop_table_result
28
+
26
29
  cte_result = CteQuery.parse(@normalized_value, depth: @depth)&.render
27
30
  return prepend_sentinels(cte_result) if cte_result
28
31
 
29
32
  create_table_as_result = CreateTableAs.parse(@normalized_value, depth: @depth)&.render
30
33
  return prepend_sentinels(create_table_as_result) if create_table_as_result
31
34
 
35
+ create_table_result = CreateTable.parse(@normalized_value)&.render
36
+ return prepend_sentinels(create_table_result) if create_table_result
37
+
32
38
  compound_result = CompoundQuery.parse(@normalized_value, depth: @depth)&.render
33
39
  return prepend_sentinels(compound_result) if compound_result
34
40
 
@@ -199,8 +199,9 @@ module SqlBeautifier
199
199
 
200
200
  def render_column_list
201
201
  columns = Tokenizer.split_by_top_level_commas(@column_list)
202
- indent = Util.whitespace(SqlBeautifier.config_for(:indent_spaces) || 4)
202
+ return " (#{columns.first.strip})" if columns.length == 1
203
203
 
204
+ indent = Util.whitespace(SqlBeautifier.config_for(:indent_spaces) || 4)
204
205
  formatted_columns = columns.map { |column| "#{indent}#{column.strip}" }.join(",\n")
205
206
 
206
207
  " (\n#{formatted_columns}\n)"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SqlBeautifier
4
- VERSION = "0.10.2"
4
+ VERSION = "0.10.3"
5
5
  end
@@ -24,6 +24,9 @@ require_relative "sql_beautifier/sort_expression"
24
24
  require_relative "sql_beautifier/condition"
25
25
  require_relative "sql_beautifier/cte_definition"
26
26
  require_relative "sql_beautifier/cte_query"
27
+ require_relative "sql_beautifier/drop_table"
28
+ require_relative "sql_beautifier/create_table_parsing"
29
+ require_relative "sql_beautifier/create_table"
27
30
  require_relative "sql_beautifier/create_table_as"
28
31
  require_relative "sql_beautifier/compound_query"
29
32
  require_relative "sql_beautifier/dml_rendering"
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.10.2
4
+ version: 0.10.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinnell Shah
@@ -82,11 +82,14 @@ files:
82
82
  - lib/sql_beautifier/condition.rb
83
83
  - lib/sql_beautifier/configuration.rb
84
84
  - lib/sql_beautifier/constants.rb
85
+ - lib/sql_beautifier/create_table.rb
85
86
  - lib/sql_beautifier/create_table_as.rb
87
+ - lib/sql_beautifier/create_table_parsing.rb
86
88
  - lib/sql_beautifier/cte_definition.rb
87
89
  - lib/sql_beautifier/cte_query.rb
88
90
  - lib/sql_beautifier/delete_query.rb
89
91
  - lib/sql_beautifier/dml_rendering.rb
92
+ - lib/sql_beautifier/drop_table.rb
90
93
  - lib/sql_beautifier/expression.rb
91
94
  - lib/sql_beautifier/formatter.rb
92
95
  - lib/sql_beautifier/insert_query.rb