From af4f0ddaea30340249366f1682e95a13611ebadd Mon Sep 17 00:00:00 2001 From: Kinnell Shah Date: Mon, 30 Mar 2026 10:34:22 -0400 Subject: [PATCH] wip --- CHANGELOG.md | 6 + README.md | 30 ++ lib/sql_beautifier.rb | 3 + lib/sql_beautifier/constants.rb | 7 + lib/sql_beautifier/create_table.rb | 65 +++++ lib/sql_beautifier/create_table_as.rb | 28 +- lib/sql_beautifier/create_table_parsing.rb | 32 +++ lib/sql_beautifier/drop_table.rb | 56 ++++ lib/sql_beautifier/formatter.rb | 6 + lib/sql_beautifier/insert_query.rb | 3 +- spec/readme.spec.rb | 80 ++++++ spec/sql_beautifier.spec.rb | 17 ++ spec/sql_beautifier/create_table.spec.rb | 305 +++++++++++++++++++++ spec/sql_beautifier/drop_table.spec.rb | 221 +++++++++++++++ spec/sql_beautifier/formatter.spec.rb | 63 ++++- spec/sql_beautifier/insert_query.spec.rb | 66 ++++- 16 files changed, 954 insertions(+), 34 deletions(-) create mode 100644 lib/sql_beautifier/create_table.rb create mode 100644 lib/sql_beautifier/create_table_parsing.rb create mode 100644 lib/sql_beautifier/drop_table.rb create mode 100644 spec/sql_beautifier/create_table.spec.rb create mode 100644 spec/sql_beautifier/drop_table.spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb390b..6d14786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [X.X.X] - YYYY-MM-DD +- 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 +- 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 +- 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) +- 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 +- 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 + ## [0.10.2] - 2026-03-30 - 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 diff --git a/README.md b/README.md index 226c8e4..1bba795 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,36 @@ where users.account_id = accounts.id returning users.id; ``` +### DROP TABLE + +`DROP TABLE` statements are recognized and formatted with proper keyword casing and table name formatting: + +```ruby +SqlBeautifier.call("DROP TABLE IF EXISTS persons") +``` + +Produces: + +```sql +drop table if exists Persons; +``` + +### CREATE TABLE (DDL) + +`CREATE TABLE` statements with column definitions are recognized and formatted with proper keyword casing and table name formatting. Column definitions are preserved as-is: + +```ruby +SqlBeautifier.call("CREATE TEMPORARY TABLE persons (id bigint)") +``` + +Produces: + +```sql +create temporary table Persons (id bigint); +``` + +Modifiers (`TEMP`, `TEMPORARY`, `UNLOGGED`, `LOCAL`) and `IF NOT EXISTS` are supported. + ### Set Operators (UNION, INTERSECT, EXCEPT) 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: diff --git a/lib/sql_beautifier.rb b/lib/sql_beautifier.rb index 58e4930..66d152d 100644 --- a/lib/sql_beautifier.rb +++ b/lib/sql_beautifier.rb @@ -24,6 +24,9 @@ require_relative "sql_beautifier/condition" require_relative "sql_beautifier/cte_definition" require_relative "sql_beautifier/cte_query" +require_relative "sql_beautifier/drop_table" +require_relative "sql_beautifier/create_table_parsing" +require_relative "sql_beautifier/create_table" require_relative "sql_beautifier/create_table_as" require_relative "sql_beautifier/compound_query" require_relative "sql_beautifier/dml_rendering" diff --git a/lib/sql_beautifier/constants.rb b/lib/sql_beautifier/constants.rb index 1d7a272..018e594 100644 --- a/lib/sql_beautifier/constants.rb +++ b/lib/sql_beautifier/constants.rb @@ -42,6 +42,13 @@ module Constants CLOSE_PARENTHESIS = ")" COMMA = "," + TABLE_MODIFIERS = %w[ + temp + temporary + unlogged + local + ].freeze + LATERAL_PREFIX_PATTERN = %r{\Alateral\s+}i WHITESPACE_REGEX = %r{\s+} diff --git a/lib/sql_beautifier/create_table.rb b/lib/sql_beautifier/create_table.rb new file mode 100644 index 0000000..6a21a0a --- /dev/null +++ b/lib/sql_beautifier/create_table.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module SqlBeautifier + class CreateTable < Base + extend CreateTableParsing + + option :modifier, default: -> {} + option :if_not_exists, type: Types::Bool + option :table_name + option :column_definitions + + def self.parse(normalized_sql, **) + scanner = Scanner.new(normalized_sql) + return nil unless scanner.keyword_at?("create") + + scanner.skip_past_keyword!("create") + modifier = detect_modifier(scanner) + scanner.skip_past_keyword!(modifier) if modifier + + return nil unless scanner.keyword_at?("table") + + scanner.skip_past_keyword!("table") + + if_not_exists = detect_if_not_exists?(scanner) + skip_past_if_not_exists!(scanner) if if_not_exists + + table_name = scanner.read_identifier! + return nil unless table_name + + scanner.skip_whitespace! + return nil if scanner.finished? + return nil unless scanner.current_char == Constants::OPEN_PARENTHESIS + + column_definitions = extract_column_definitions(normalized_sql, scanner) + return nil unless column_definitions + + new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, column_definitions: column_definitions) + end + + def self.extract_column_definitions(normalized_sql, scanner) + closing = scanner.find_matching_parenthesis(scanner.position) + return nil unless closing + + inner_text = normalized_sql[(scanner.position + 1)...closing].strip + return nil if inner_text.empty? + + trailing_text = normalized_sql[(closing + 1)..].strip + return nil unless trailing_text.empty? + + inner_text + end + + private_class_method :extract_column_definitions + + def render + parts = [Util.format_keyword("create")] + parts << Util.format_keyword(@modifier) if @modifier + parts << Util.format_keyword("table") + parts << "#{Util.format_keyword('if')} #{Util.format_keyword('not')} #{Util.format_keyword('exists')}" if @if_not_exists + parts << Util.format_table_name(@table_name) + + "#{parts.join(' ')} (#{@column_definitions})\n" + end + end +end diff --git a/lib/sql_beautifier/create_table_as.rb b/lib/sql_beautifier/create_table_as.rb index d0f0214..44ec723 100644 --- a/lib/sql_beautifier/create_table_as.rb +++ b/lib/sql_beautifier/create_table_as.rb @@ -2,12 +2,7 @@ module SqlBeautifier class CreateTableAs < Base - MODIFIERS = %w[ - temp - temporary - unlogged - local - ].freeze + extend CreateTableParsing WITH_DATA_SUFFIX_REGEX = %r{\s+(with\s+(?:no\s+)?data)\s*\z}i @@ -47,27 +42,6 @@ def self.parse(normalized_sql, depth: 0) new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, body_sql: body_sql, suffix: suffix, depth: depth) end - def self.detect_modifier(scanner) - MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) } - end - - def self.detect_if_not_exists?(scanner) - return false unless scanner.keyword_at?("if") - - probe = Scanner.new(scanner.source, position: scanner.position) - probe.skip_past_keyword!("if") - return false unless probe.keyword_at?("not") - - probe.skip_past_keyword!("not") - probe.keyword_at?("exists") - end - - def self.skip_past_if_not_exists!(scanner) - scanner.skip_past_keyword!("if") - scanner.skip_past_keyword!("not") - scanner.skip_past_keyword!("exists") - end - def self.extract_body(sql, position) scanner = Scanner.new(sql, position: position) scanner.skip_whitespace! diff --git a/lib/sql_beautifier/create_table_parsing.rb b/lib/sql_beautifier/create_table_parsing.rb new file mode 100644 index 0000000..fa60c22 --- /dev/null +++ b/lib/sql_beautifier/create_table_parsing.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module SqlBeautifier + module CreateTableParsing + def self.extended(base) + base.private_class_method :detect_modifier + base.private_class_method :detect_if_not_exists? + base.private_class_method :skip_past_if_not_exists! + end + + def detect_modifier(scanner) + Constants::TABLE_MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) } + end + + def detect_if_not_exists?(scanner) + return false unless scanner.keyword_at?("if") + + probe = Scanner.new(scanner.source, position: scanner.position) + probe.skip_past_keyword!("if") + return false unless probe.keyword_at?("not") + + probe.skip_past_keyword!("not") + probe.keyword_at?("exists") + end + + def skip_past_if_not_exists!(scanner) + scanner.skip_past_keyword!("if") + scanner.skip_past_keyword!("not") + scanner.skip_past_keyword!("exists") + end + end +end diff --git a/lib/sql_beautifier/drop_table.rb b/lib/sql_beautifier/drop_table.rb new file mode 100644 index 0000000..cf0c833 --- /dev/null +++ b/lib/sql_beautifier/drop_table.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module SqlBeautifier + class DropTable < Base + option :table_name + option :if_exists, type: Types::Bool + + def self.parse(normalized_sql, **) + scanner = Scanner.new(normalized_sql) + return nil unless scanner.keyword_at?("drop") + + scanner.skip_past_keyword!("drop") + return nil unless scanner.keyword_at?("table") + + scanner.skip_past_keyword!("table") + + if_exists = false + + if scanner.keyword_at?("if") + return nil unless detect_if_exists?(scanner) + + skip_past_if_exists!(scanner) + if_exists = true + end + + table_name = scanner.read_identifier! + return nil unless table_name + + scanner.skip_whitespace! + return nil unless scanner.finished? + + new(table_name: table_name, if_exists: if_exists) + end + + def self.detect_if_exists?(scanner) + probe = Scanner.new(scanner.source, position: scanner.position) + probe.skip_past_keyword!("if") + probe.keyword_at?("exists") + end + + def self.skip_past_if_exists!(scanner) + scanner.skip_past_keyword!("if") + scanner.skip_past_keyword!("exists") + end + + private_class_method :detect_if_exists?, :skip_past_if_exists! + + def render + parts = [Util.format_keyword("drop"), Util.format_keyword("table")] + parts << "#{Util.format_keyword('if')} #{Util.format_keyword('exists')}" if @if_exists + parts << Util.format_table_name(@table_name) + + "#{parts.join(' ')}\n" + end + end +end diff --git a/lib/sql_beautifier/formatter.rb b/lib/sql_beautifier/formatter.rb index 4181eab..8dcdcc5 100644 --- a/lib/sql_beautifier/formatter.rb +++ b/lib/sql_beautifier/formatter.rb @@ -23,12 +23,18 @@ def call @leading_sentinels = extract_leading_sentinels! return unless @normalized_value.present? + drop_table_result = DropTable.parse(@normalized_value)&.render + return prepend_sentinels(drop_table_result) if drop_table_result + cte_result = CteQuery.parse(@normalized_value, depth: @depth)&.render return prepend_sentinels(cte_result) if cte_result create_table_as_result = CreateTableAs.parse(@normalized_value, depth: @depth)&.render return prepend_sentinels(create_table_as_result) if create_table_as_result + create_table_result = CreateTable.parse(@normalized_value)&.render + return prepend_sentinels(create_table_result) if create_table_result + compound_result = CompoundQuery.parse(@normalized_value, depth: @depth)&.render return prepend_sentinels(compound_result) if compound_result diff --git a/lib/sql_beautifier/insert_query.rb b/lib/sql_beautifier/insert_query.rb index a5ee40e..7d97c31 100644 --- a/lib/sql_beautifier/insert_query.rb +++ b/lib/sql_beautifier/insert_query.rb @@ -199,8 +199,9 @@ def render_insert_into def render_column_list columns = Tokenizer.split_by_top_level_commas(@column_list) - indent = Util.whitespace(SqlBeautifier.config_for(:indent_spaces) || 4) + return " (#{columns.first.strip})" if columns.length == 1 + indent = Util.whitespace(SqlBeautifier.config_for(:indent_spaces) || 4) formatted_columns = columns.map { |column| "#{indent}#{column.strip}" }.join(",\n") " (\n#{formatted_columns}\n)" diff --git a/spec/readme.spec.rb b/spec/readme.spec.rb index 020925b..3bb8b7a 100644 --- a/spec/readme.spec.rb +++ b/spec/readme.spec.rb @@ -137,6 +137,51 @@ end end + context "searched CASE in SELECT" do + let(:value) { "SELECT id, CASE WHEN status = 'active' THEN 'Active' WHEN status = 'pending' THEN 'Pending' ELSE 'Unknown' END AS status_label, name FROM users" } + + it "formats with expanded indentation" do + expect(output).to match_formatted_text(<<~SQL) + select id, + case + when status = 'active' then 'Active' + when status = 'pending' then 'Pending' + else 'Unknown' + end as status_label, + name + + from Users u; + SQL + end + end + + context "simple CASE with operand" do + let(:value) { "SELECT CASE u.role WHEN 'admin' THEN 'Administrator' WHEN 'user' THEN 'Standard User' ELSE 'Guest' END AS role_label FROM users" } + + it "places the operand on the case line" do + expect(output).to match_formatted_text(<<~SQL) + select case u.role + when 'admin' then 'Administrator' + when 'user' then 'Standard User' + else 'Guest' + end as role_label + + from Users u; + SQL + end + end + + context "CASE inside parenthesized function call" do + let(:value) { "SELECT COALESCE(CASE WHEN x > 0 THEN x ELSE NULL END, 0) AS safe_x FROM users" } + + it "preserves the CASE inline" do + expect(output).to match_formatted_text(<<~SQL) + select coalesce(case when x > 0 then x else null end, 0) as safe_x + from Users u; + SQL + end + end + context "LIMIT with compact spacing" do let(:value) { "SELECT id FROM users ORDER BY created_at DESC LIMIT 25" } @@ -255,6 +300,26 @@ end end + context "DROP TABLE IF EXISTS" do + let(:value) { "DROP TABLE IF EXISTS persons" } + + it "formats with keyword casing and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Persons; + SQL + end + end + + context "CREATE TEMPORARY TABLE with column definitions" do + let(:value) { "CREATE TEMPORARY TABLE persons (id bigint)" } + + it "formats with keyword casing and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + create temporary table Persons (id bigint); + SQL + end + end + context "set operators with UNION ALL" do let(:value) { "SELECT id, name FROM users WHERE active = true UNION ALL SELECT id, name FROM admins WHERE role = 'super'" } @@ -342,6 +407,21 @@ end end + context "derived table in FROM" do + let(:value) { "SELECT active_users.id FROM (SELECT id FROM users WHERE active = true) AS active_users" } + + it "formats the derived table with indentation and preserves the alias" do + expect(output).to match_formatted_text(<<~SQL) + select active_users.id + from ( + select id + from Users u + where active = true + ) active_users; + SQL + end + end + context "trailing semicolons" do let(:value) { "SELECT id FROM users WHERE active = true" } diff --git a/spec/sql_beautifier.spec.rb b/spec/sql_beautifier.spec.rb index 210bcfc..5dd42d4 100644 --- a/spec/sql_beautifier.spec.rb +++ b/spec/sql_beautifier.spec.rb @@ -84,6 +84,23 @@ end end + context "with DROP TABLE, CREATE TABLE, and INSERT INTO as multiple statements" do + let(:value) { "drop table if exists temp_export_constituents; create temporary table temp_export_constituents (id bigint); insert into temp_export_constituents (id) select id from users" } + + it "formats each statement independently" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Temp_Export_Constituents; + + create temporary table Temp_Export_Constituents (id bigint); + + insert into Temp_Export_Constituents (id) + + select id + from Users u; + SQL + end + end + context "with trailing_semicolon disabled and multiple statements" do let(:value) { "SELECT id FROM constituents; SELECT id FROM departments" } diff --git a/spec/sql_beautifier/create_table.spec.rb b/spec/sql_beautifier/create_table.spec.rb new file mode 100644 index 0000000..75ea32e --- /dev/null +++ b/spec/sql_beautifier/create_table.spec.rb @@ -0,0 +1,305 @@ +# frozen_string_literal: true + +RSpec.describe SqlBeautifier::CreateTable do + describe ".parse" do + let(:result) { described_class.parse(value) } + + context "with a non-CREATE statement" do + let(:value) { "select id from users" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with a CREATE TABLE AS query" do + let(:value) { "create table foo as (select 1)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with an identifier starting with 'create'" do + let(:value) { "create_backup(100)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "without the TABLE keyword" do + let(:value) { "create index idx_users_name on users (name)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "without a table name" do + let(:value) { "create table (id bigint)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "without column definitions" do + let(:value) { "create table users" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with a simple CREATE TABLE" do + let(:value) { "create table users (id bigint)" } + + it "returns a CreateTable" do + expect(result).to be_a(described_class) + end + + it "parses the table name" do + expect(result.table_name).to eq("users") + end + + it "parses the column definitions" do + expect(result.column_definitions).to eq("id bigint") + end + + it "has no modifier" do + expect(result.modifier).to be_nil + end + + it "does not set :if_not_exists" do + expect(result.if_not_exists).to be false + end + end + + context "with CREATE TEMPORARY TABLE" do + let(:value) { "create temporary table temp_export_constituents (id bigint)" } + + it "returns a CreateTable" do + expect(result).to be_a(described_class) + end + + it "parses the table name" do + expect(result.table_name).to eq("temp_export_constituents") + end + + it "extracts the modifier" do + expect(result.modifier).to eq("temporary") + end + + it "parses the column definitions" do + expect(result.column_definitions).to eq("id bigint") + end + end + + context "with CREATE TEMP TABLE" do + let(:value) { "create temp table foo (id bigint)" } + + it "extracts the modifier" do + expect(result.modifier).to eq("temp") + end + end + + context "with CREATE UNLOGGED TABLE" do + let(:value) { "create unlogged table foo (id bigint)" } + + it "extracts the modifier" do + expect(result.modifier).to eq("unlogged") + end + end + + context "with CREATE LOCAL TABLE" do + let(:value) { "create local table foo (id bigint)" } + + it "extracts the modifier" do + expect(result.modifier).to eq("local") + end + end + + context "with IF NOT EXISTS" do + let(:value) { "create table if not exists users (id bigint)" } + + it "sets :if_not_exists" do + expect(result.if_not_exists).to be true + end + end + + context "with a modifier and IF NOT EXISTS" do + let(:value) { "create temp table if not exists foo (id bigint)" } + + it "extracts the modifier" do + expect(result.modifier).to eq("temp") + end + + it "sets :if_not_exists" do + expect(result.if_not_exists).to be true + end + end + + context "with multiple column definitions" do + let(:value) { "create table users (id bigint, name text, email varchar(255))" } + + it "parses all column definitions" do + expect(result.column_definitions).to eq("id bigint, name text, email varchar(255)") + end + end + + context "with a quoted table name" do + let(:result) { described_class.parse('create table "my table" (id bigint)') } + + it "preserves the quoted identifier" do + expect(result.table_name).to eq('"my table"') + end + end + + context "with uppercase input" do + let(:value) { "CREATE TEMPORARY TABLE TEMP_EXPORT_CONSTITUENTS (ID BIGINT)" } + + it "returns a CreateTable" do + expect(result).to be_a(described_class) + end + end + + context "with unclosed parentheses" do + let(:value) { "create table users (id bigint" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with empty parentheses" do + let(:value) { "create table users ()" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with trailing WITH clause" do + let(:value) { "create table users (id bigint) with (fillfactor = 70)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with trailing TABLESPACE clause" do + let(:value) { "create table users (id bigint) tablespace fast_storage" } + + it "returns nil" do + expect(result).to be_nil + end + end + end + + describe "#render" do + let(:output) { described_class.parse(value).render } + + context "with a simple CREATE TABLE" do + let(:value) { "create table users (id bigint)" } + + it "formats with PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + create table Users (id bigint) + SQL + end + end + + context "with CREATE TEMPORARY TABLE" do + let(:value) { "create temporary table temp_export_constituents (id bigint)" } + + it "formats with modifier and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + create temporary table Temp_Export_Constituents (id bigint) + SQL + end + end + + context "with CREATE TEMP TABLE" do + let(:value) { "create temp table foo (id bigint)" } + + it "formats with the temp modifier" do + expect(output).to match_formatted_text(<<~SQL) + create temp table Foo (id bigint) + SQL + end + end + + context "with IF NOT EXISTS" do + let(:value) { "create temp table if not exists foo (id bigint)" } + + it "includes if not exists in the preamble" do + expect(output).to match_formatted_text(<<~SQL) + create temp table if not exists Foo (id bigint) + SQL + end + end + + context "with multiple column definitions" do + let(:value) { "create table users (id bigint, name text, email varchar(255))" } + + it "preserves column definitions" do + expect(output).to match_formatted_text(<<~SQL) + create table Users (id bigint, name text, email varchar(255)) + SQL + end + end + + context "with an underscore table name" do + let(:value) { "create temp table tmp_export_ids (id bigint)" } + + it "formats the table name with PascalCase" do + expect(output).to match_formatted_text(<<~SQL) + create temp table Tmp_Export_Ids (id bigint) + SQL + end + end + + context "with a quoted table name" do + let(:output) { described_class.parse('create table "my table" (id bigint)').render } + + it "preserves the quoted identifier" do + expect(output).to match_formatted_text(<<~SQL) + create table "my table" (id bigint) + SQL + end + end + + context "with :keyword_case set to :upper" do + let(:value) { "create temporary table temp_export_constituents (id bigint)" } + + before do + SqlBeautifier.configure do |config| + config.keyword_case = :upper + end + end + + it "uppercases the keywords" do + expect(output).to match_formatted_text(<<~SQL) + CREATE TEMPORARY TABLE Temp_Export_Constituents (id bigint) + SQL + end + end + + context "with :table_name_format set to :lowercase" do + let(:value) { "create temporary table temp_export_constituents (id bigint)" } + + before do + SqlBeautifier.configure do |config| + config.table_name_format = :lowercase + end + end + + it "uses lowercase table name" do + expect(output).to match_formatted_text(<<~SQL) + create temporary table temp_export_constituents (id bigint) + SQL + end + end + end +end diff --git a/spec/sql_beautifier/drop_table.spec.rb b/spec/sql_beautifier/drop_table.spec.rb new file mode 100644 index 0000000..c3115b9 --- /dev/null +++ b/spec/sql_beautifier/drop_table.spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +RSpec.describe SqlBeautifier::DropTable do + describe ".parse" do + let(:result) { described_class.parse(value) } + + context "with a non-DROP statement" do + let(:value) { "select id from users" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with DROP without TABLE" do + let(:value) { "drop index idx_users_name" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with an identifier starting with 'drop'" do + let(:value) { "drop_backup(100)" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with DROP TABLE" do + let(:value) { "drop table users" } + + it "returns a DropTable" do + expect(result).to be_a(described_class) + end + + it "parses the table name" do + expect(result.table_name).to eq("users") + end + + it "does not set :if_exists" do + expect(result.if_exists).to be false + end + end + + context "with DROP TABLE IF EXISTS" do + let(:value) { "drop table if exists users" } + + it "returns a DropTable" do + expect(result).to be_a(described_class) + end + + it "parses the table name" do + expect(result.table_name).to eq("users") + end + + it "sets :if_exists" do + expect(result.if_exists).to be true + end + end + + context "with uppercase DROP TABLE IF EXISTS" do + let(:value) { "DROP TABLE IF EXISTS USERS" } + + it "returns a DropTable" do + expect(result).to be_a(described_class) + end + end + + context "with an underscore table name" do + let(:value) { "drop table if exists temp_export_constituents" } + + it "preserves the raw table name" do + expect(result.table_name).to eq("temp_export_constituents") + end + end + + context "with a quoted table name" do + let(:result) { described_class.parse('drop table "my table"') } + + it "preserves the quoted identifier" do + expect(result.table_name).to eq('"my table"') + end + end + + context "with DROP TABLE IF but not EXISTS" do + let(:value) { "drop table if users" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "without a table name" do + let(:value) { "drop table" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "without a table name after IF EXISTS" do + let(:value) { "drop table if exists" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with multiple table names" do + let(:value) { "drop table users, admins" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with CASCADE suffix" do + let(:value) { "drop table users cascade" } + + it "returns nil" do + expect(result).to be_nil + end + end + + context "with RESTRICT suffix" do + let(:value) { "drop table users restrict" } + + it "returns nil" do + expect(result).to be_nil + end + end + end + + describe "#render" do + let(:output) { described_class.parse(value).render } + + context "with DROP TABLE" do + let(:value) { "drop table users" } + + it "formats with PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table Users + SQL + end + end + + context "with DROP TABLE IF EXISTS" do + let(:value) { "drop table if exists users" } + + it "formats with IF EXISTS and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Users + SQL + end + end + + context "with an underscore table name" do + let(:value) { "drop table if exists temp_export_constituents" } + + it "formats the table name with PascalCase" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Temp_Export_Constituents + SQL + end + end + + context "with a quoted table name" do + let(:output) { described_class.parse('drop table "my table"').render } + + it "preserves the quoted identifier" do + expect(output).to match_formatted_text(<<~SQL) + drop table "my table" + SQL + end + end + + context "with uppercase input" do + let(:value) { "DROP TABLE IF EXISTS TEMP_EXPORT_CONSTITUENTS" } + + it "formats with lowercase keywords and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Temp_Export_Constituents + SQL + end + end + + context "with :keyword_case set to :upper" do + let(:value) { "drop table if exists users" } + + before do + SqlBeautifier.configure do |config| + config.keyword_case = :upper + end + end + + it "uppercases the keywords" do + expect(output).to match_formatted_text(<<~SQL) + DROP TABLE IF EXISTS Users + SQL + end + end + + context "with :table_name_format set to :lowercase" do + let(:value) { "drop table if exists temp_export_constituents" } + + before do + SqlBeautifier.configure do |config| + config.table_name_format = :lowercase + end + end + + it "uses lowercase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists temp_export_constituents + SQL + end + end + end +end diff --git a/spec/sql_beautifier/formatter.spec.rb b/spec/sql_beautifier/formatter.spec.rb index 04454f0..81ed218 100644 --- a/spec/sql_beautifier/formatter.spec.rb +++ b/spec/sql_beautifier/formatter.spec.rb @@ -824,6 +824,54 @@ end end + ############################################################################ + ## DROP TABLE Integration + ############################################################################ + + context "with DROP TABLE" do + let(:value) { "DROP TABLE users" } + + it "formats with PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table Users + SQL + end + end + + context "with DROP TABLE IF EXISTS" do + let(:value) { "DROP TABLE IF EXISTS temp_export_constituents" } + + it "formats with IF EXISTS and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + drop table if exists Temp_Export_Constituents + SQL + end + end + + ############################################################################ + ## CREATE TABLE (DDL) Integration + ############################################################################ + + context "with CREATE TEMPORARY TABLE with column definitions" do + let(:value) { "CREATE TEMPORARY TABLE temp_export_constituents (id bigint)" } + + it "formats with modifier and PascalCase table name" do + expect(output).to match_formatted_text(<<~SQL) + create temporary table Temp_Export_Constituents (id bigint) + SQL + end + end + + context "with CREATE TABLE with multiple column definitions" do + let(:value) { "CREATE TABLE users (id bigint, name text, email varchar(255))" } + + it "formats with PascalCase table name and preserves column definitions" do + expect(output).to match_formatted_text(<<~SQL) + create table Users (id bigint, name text, email varchar(255)) + SQL + end + end + ############################################################################ ## CREATE TABLE AS Integration ############################################################################ @@ -1252,6 +1300,17 @@ end end + context "with INSERT with a single column" do + let(:value) { "INSERT INTO temp_export_constituents (id) VALUES (1)" } + + it "renders the column list inline" do + expect(output).to match_formatted_text(<<~SQL) + insert into Temp_Export_Constituents (id) + values (1) + SQL + end + end + context "with INSERT without column list" do let(:value) { "INSERT INTO users VALUES (1, 'Alice', 'alice@example.com')" } @@ -1314,9 +1373,7 @@ it "formats the INSERT and delegates the parenthesized SELECT" do expect(output).to match_formatted_text(<<~SQL) - insert into Export_Table ( - id - ) + insert into Export_Table (id) select distinct u.id diff --git a/spec/sql_beautifier/insert_query.spec.rb b/spec/sql_beautifier/insert_query.spec.rb index 3e9cfcb..a87f868 100644 --- a/spec/sql_beautifier/insert_query.spec.rb +++ b/spec/sql_beautifier/insert_query.spec.rb @@ -249,9 +249,7 @@ it "formats the SELECT without wrapping parentheses" do expect(output).to match_formatted_text(<<~SQL) - insert into Export_Table ( - id - ) + insert into Export_Table (id) select distinct u.id @@ -335,5 +333,67 @@ SQL end end + + context "with a single column" do + let(:output) { described_class.parse("insert into users (id) values (1)").render } + + it "renders the column list inline" do + expect(output).to match_formatted_text(<<~SQL) + insert into Users (id) + values (1) + SQL + end + end + + context "with a single column and INSERT...SELECT" do + let(:output) { described_class.parse("insert into temp_export_constituents (id) select id from users").render } + + it "renders the column list inline" do + expect(output).to match_formatted_text(<<~SQL) + insert into Temp_Export_Constituents (id) + + select id + from Users u + SQL + end + end + + context "with a single column and RETURNING" do + let(:output) { described_class.parse("insert into users (id) values (1) returning id").render } + + it "renders the column list inline" do + expect(output).to match_formatted_text(<<~SQL) + insert into Users (id) + values (1) + returning id + SQL + end + end + + context "with a single column and ON CONFLICT" do + let(:output) { described_class.parse("insert into users (id) values (1) on conflict (id) do nothing").render } + + it "renders the column list inline" do + expect(output).to match_formatted_text(<<~SQL) + insert into Users (id) + values (1) + on conflict (id) do nothing + SQL + end + end + + context "with two columns" do + let(:output) { described_class.parse("insert into users (id, name) values (1, 'Alice')").render } + + it "renders the column list on multiple lines" do + expect(output).to match_formatted_text(<<~SQL) + insert into Users ( + id, + name + ) + values (1, 'Alice') + SQL + end + end end end