From 1b05ebef8c8012523f0e19b370c7dd2756f9fd96 Mon Sep 17 00:00:00 2001 From: Serhii Bodnaruk Date: Tue, 30 Jun 2026 16:55:51 +0300 Subject: [PATCH] Wrap long column annotation lines; raise MAX_LINE to 120; multi-adapter triggers - ColumnFormatter: wrap options onto a continuation line when the assembled column comment exceeds WrapHelper::MAX_LINE (120 chars), fixing Rubocop Layout/LineLength offenses in annotated model files - WrapHelper: raise MAX_LINE from 100 to 120 to match the standard Rubocop default; affects column wrapping, index WHERE wrapping, and check constraint wrapping consistently - TriggerFormatter: dispatch on adapter_name to support PostgreSQL (pg_trigger), MySQL/Trilogy (information_schema.TRIGGERS), and SQLite (sqlite_master); unsupported adapters return [] silently - Update specs: CheckConstraintFormatter wrap points updated for 120, TriggerFormatter covers all three adapters + unsupported adapter, ColumnFormatter covers the wrapping behaviour --- Gemfile.lock | 2 +- lib/annotato/column_formatter.rb | 16 +++-- lib/annotato/trigger_formatter.rb | 27 ++++++-- lib/annotato/version.rb | 2 +- lib/annotato/wrap_helper.rb | 2 +- spec/annotato/annotation_builder_spec.rb | 1 + .../check_constraint_formatter_spec.rb | 2 +- spec/annotato/column_formatter_spec.rb | 22 ++++++- spec/annotato/trigger_formatter_spec.rb | 66 +++++++++++++------ 9 files changed, 106 insertions(+), 34 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 37d1563..5c92b5a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - annotato (0.2.1) + annotato (0.2.2) rails (>= 6.0) GEM diff --git a/lib/annotato/column_formatter.rb b/lib/annotato/column_formatter.rb index fd9cc41..bc9823f 100644 --- a/lib/annotato/column_formatter.rb +++ b/lib/annotato/column_formatter.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true require "json" +require_relative "wrap_helper" module Annotato class ColumnFormatter + extend WrapHelper + # Main entry: returns an array of comment lines per column, flattened. def self.format(model, connection) table_name = model.table_name @@ -57,10 +60,15 @@ def self.format(model, connection) lines << closing lines else - # single-line comment - line = left - line += " #{opts.join(', ')}" unless opts.empty? - [line.rstrip] + # single-line comment; wrap options onto continuation line if too long + opts_str = opts.join(", ") + line = opts_str.empty? ? left : "#{left} #{opts_str}" + if line.length > WrapHelper::MAX_LINE && !opts_str.empty? + cont_indent = " " * (left.length + 1) + [left, "# #{cont_indent}#{opts_str}"] + else + [line.rstrip] + end end end end diff --git a/lib/annotato/trigger_formatter.rb b/lib/annotato/trigger_formatter.rb index 1a7f7fa..8b30c88 100644 --- a/lib/annotato/trigger_formatter.rb +++ b/lib/annotato/trigger_formatter.rb @@ -3,11 +3,28 @@ module Annotato class TriggerFormatter def self.format(conn, table_name) - conn.exec_query( - "SELECT tgname FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal", - "SQL", - [table_name] - ).map { |r| "# #{r['tgname']}" } + rows = case conn.adapter_name.downcase + when "postgresql" + conn.exec_query( + "SELECT tgname AS name FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal", + "SQL", [table_name] + ) + when "mysql2", "mysql", "trilogy" + conn.exec_query( + "SELECT TRIGGER_NAME AS name FROM information_schema.TRIGGERS " \ + "WHERE EVENT_OBJECT_SCHEMA = DATABASE() AND EVENT_OBJECT_TABLE = ?", + "SQL", [table_name] + ) + when "sqlite3" + conn.exec_query( + "SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?", + "SQL", [table_name] + ) + else + return [] + end + + rows.map { |r| "# #{r['name']}" } end end end diff --git a/lib/annotato/version.rb b/lib/annotato/version.rb index a97965f..8f3ae77 100644 --- a/lib/annotato/version.rb +++ b/lib/annotato/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Annotato - VERSION = "0.2.1" + VERSION = "0.2.2" end diff --git a/lib/annotato/wrap_helper.rb b/lib/annotato/wrap_helper.rb index 8fd0fb1..6a5d46e 100644 --- a/lib/annotato/wrap_helper.rb +++ b/lib/annotato/wrap_helper.rb @@ -2,7 +2,7 @@ module Annotato module WrapHelper - MAX_LINE = 100 + MAX_LINE = 120 # Produces one or more lines, each already prefixed with "# " # First line: "# where (..." diff --git a/spec/annotato/annotation_builder_spec.rb b/spec/annotato/annotation_builder_spec.rb index 9ddf69e..c516a86 100644 --- a/spec/annotato/annotation_builder_spec.rb +++ b/spec/annotato/annotation_builder_spec.rb @@ -19,6 +19,7 @@ class << self before do allow(model).to receive(:table_name).and_return(table_name) + allow(connection).to receive(:adapter_name).and_return("PostgreSQL") ActiveRecord::Base.connection = connection end diff --git a/spec/annotato/check_constraint_formatter_spec.rb b/spec/annotato/check_constraint_formatter_spec.rb index d340fe4..88c1253 100644 --- a/spec/annotato/check_constraint_formatter_spec.rb +++ b/spec/annotato/check_constraint_formatter_spec.rb @@ -16,6 +16,6 @@ result = described_class.format(connection, "users") expect(result).to include("# chk_positive_age\n# (age > 0)") - expect(result).to include("# chk_valid_status\n# (((((status)::text = 'deleted'::text) AND\n# (deleted_at IS NOT NULL)) OR (((status)::text <> 'deleted'::text) AND\n# (deleted_at IS NULL) AND (active = true))))") + expect(result).to include("# chk_valid_status\n# (((((status)::text = 'deleted'::text) AND (deleted_at IS NOT NULL)) OR (((status)::text <> 'deleted'::text) AND\n# (deleted_at IS NULL) AND (active = true))))") end end diff --git a/spec/annotato/column_formatter_spec.rb b/spec/annotato/column_formatter_spec.rb index ef56c19..fa3ba4a 100644 --- a/spec/annotato/column_formatter_spec.rb +++ b/spec/annotato/column_formatter_spec.rb @@ -102,6 +102,26 @@ # Closing line should be correctly indented closing_line = multiline_lines.find { |line| line.include?("]),") } - expect(closing_line).to match(/^#{"#" + ' ' * (indent_start-1)}\]\),/) + expect(closing_line).to match(/^#{"#" + ' ' * (indent_start - 1)}\]\),/) + end + + it "wraps options onto a continuation line when the column line exceeds MAX_LINE" do + # left = "# " (3) + name (79) + " :" (2) + type (22) = 106 chars + # 106 + " not null, unique" (17) = 123 > MAX_LINE (120) → must wrap + long_name = "a_very_long_column_name_that_will_definitely_push_the_line_over_the_rubocop_limit" + long_type = "character varying(255)" + col = double("Column", name: long_name, sql_type: long_type, default: nil, null: false) + allow(col).to receive(:respond_to?).with(:comment).and_return(false) + allow(connection).to receive(:columns).with("users").and_return([col]) + allow(connection).to receive(:indexes).with("users").and_return([ + double(name: "idx", columns: [long_name], unique: true) + ]) + + result = described_class.format(model, connection) + + expect(result.size).to eq(2) + expect(result[0]).not_to include("not null") + expect(result[0].length).to be <= Annotato::WrapHelper::MAX_LINE + expect(result[1]).to include("not null, unique") end end diff --git a/spec/annotato/trigger_formatter_spec.rb b/spec/annotato/trigger_formatter_spec.rb index 7929785..066ab7e 100644 --- a/spec/annotato/trigger_formatter_spec.rb +++ b/spec/annotato/trigger_formatter_spec.rb @@ -6,28 +6,54 @@ RSpec.describe Annotato::TriggerFormatter do let(:connection) { double("Connection") } - it "formats triggers correctly" do - result_set = [ - { "tgname" => "audit_trigger_row" }, - { "tgname" => "update_timestamp" } - ] - allow(connection).to receive(:exec_query) - .with(/pg_trigger/, "SQL", ["users"]) - .and_return(result_set) - - result = described_class.format(connection, "users") - expect(result).to include("# audit_trigger_row") - expect(result).to include("# update_timestamp") + shared_examples "formats trigger names" do |adapter, sql_pattern, bind_params| + before { allow(connection).to receive(:adapter_name).and_return(adapter) } + + it "queries with correct SQL and formats results" do + allow(connection).to receive(:exec_query) + .with(sql_pattern, "SQL", bind_params) + .and_return([{ "name" => "audit_trigger" }, { "name" => "update_timestamp" }]) + + result = described_class.format(connection, "users") + expect(result).to eq(["# audit_trigger", "# update_timestamp"]) + end + + it "uses a parameterized query (no string interpolation)" do + expect(connection).to receive(:exec_query) + .with(sql_pattern, "SQL", bind_params) + .and_return([]) + + described_class.format(connection, "users") + end + end + + context "PostgreSQL adapter" do + include_examples "formats trigger names", + "PostgreSQL", + "SELECT tgname AS name FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal", + ["users"] + end + + context "MySQL2 adapter" do + include_examples "formats trigger names", + "Mysql2", + /EVENT_OBJECT_TABLE/, + ["users"] + end + + context "SQLite3 adapter" do + include_examples "formats trigger names", + "SQLite3", + "SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?", + ["users"] end - it "uses a parameterized query (no string interpolation)" do - expect(connection).to receive(:exec_query) - .with( - "SELECT tgname FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal", - "SQL", - ["orders"] - ).and_return([]) + context "unsupported adapter" do + before { allow(connection).to receive(:adapter_name).and_return("OracleEnhanced") } - described_class.format(connection, "orders") + it "returns empty array without querying" do + expect(connection).not_to receive(:exec_query) + expect(described_class.format(connection, "users")).to eq([]) + end end end