diff --git a/.rubocop_schema.49.yml b/.rubocop_schema.49.yml deleted file mode 100644 index 8996d87..0000000 --- a/.rubocop_schema.49.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration for Rubocop >= 0.49.0 - -Layout/AlignHash: - EnforcedColonStyle: 'key' - EnforcedHashRocketStyle: 'key' - -Layout/ExtraSpacing: - # When true, allows most uses of extra spacing if the intent is to align - # things with the previous or next line, not counting empty lines or comment - # lines. - AllowForAlignment: false - -Layout/SpaceBeforeFirstArg: - Enabled: true - -Style/NumericLiterals: - Enabled: false - -Metrics/BlockNesting: - Max: 2 - -Style/WordArray: - Enabled: false - -Style/TrailingCommaInLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: 'comma' - -Style/HashSyntax: - EnforcedStyle: 'ruby19' - -Style/StringLiterals: - EnforcedStyle: double_quotes diff --git a/.rubocop_schema.53.yml b/.rubocop_schema.53.yml deleted file mode 100644 index 7d6be78..0000000 --- a/.rubocop_schema.53.yml +++ /dev/null @@ -1,38 +0,0 @@ -# Configuration for Rubocop >= 0.53.0 - -Layout/AlignHash: - EnforcedColonStyle: 'key' - EnforcedHashRocketStyle: 'key' - -Layout/ExtraSpacing: - # When true, allows most uses of extra spacing if the intent is to align - # things with the previous or next line, not counting empty lines or comment - # lines. - AllowForAlignment: false - -Layout/SpaceBeforeFirstArg: - Enabled: true - -Style/NumericLiterals: - Enabled: false - -Metrics/BlockNesting: - Max: 2 - -Style/WordArray: - Enabled: false - -Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: 'comma' - -Style/HashSyntax: - EnforcedStyle: 'ruby19' - -Style/StringLiterals: - EnforcedStyle: double_quotes diff --git a/.rubocop_schema.77.yml b/.rubocop_schema.77.yml deleted file mode 100644 index d4b549c..0000000 --- a/.rubocop_schema.77.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Configuration for Rubocop >= 0.77.0 - -Layout/ExtraSpacing: - # When true, allows most uses of extra spacing if the intent is to align - # things with the previous or next line, not counting empty lines or comment - # lines. - AllowForAlignment: false - -Layout/HashAlignment: - EnforcedColonStyle: 'key' - EnforcedHashRocketStyle: 'key' - -Layout/LineLength: - Enabled: false - -Layout/SpaceBeforeFirstArg: - Enabled: true - -Style/NumericLiterals: - Enabled: false - -Metrics/BlockNesting: - Max: 2 - -Style/WordArray: - Enabled: false - -Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: 'comma' - -Style/HashSyntax: - EnforcedStyle: 'ruby19' - -Style/StringLiterals: - EnforcedStyle: double_quotes diff --git a/.rubocop_schema.yml b/.rubocop_schema.yml deleted file mode 100644 index 09dc449..0000000 --- a/.rubocop_schema.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration for Rubocop >= 0.38.0, < 0.49.0 - -Style/ExtraSpacing: - # When true, allows most uses of extra spacing if the intent is to align - # things with the previous or next line, not counting empty lines or comment - # lines. - AllowForAlignment: false - -Style/NumericLiterals: - Enabled: false - -Style/SpaceBeforeFirstArg: - Enabled: true - -Metrics/BlockNesting: - Max: 2 - -Style/AlignHash: - EnforcedColonStyle: 'key' - EnforcedHashRocketStyle: 'key' - -Style/WordArray: - Enabled: false - -Style/TrailingCommaInLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/TrailingCommaInArguments: - EnforcedStyleForMultiline: 'comma' - -Style/HashSyntax: - EnforcedStyle: 'ruby19' - -Style/StringLiterals: - EnforcedStyle: double_quotes diff --git a/Gemfile b/Gemfile index af743cd..d67c727 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,4 @@ source 'https://rubygems.org' # Specify your gem's dependencies in fix-db-schema-conflicts.gemspec gemspec +gem "pg" diff --git a/fix-db-schema-conflicts.gemspec b/fix-db-schema-conflicts.gemspec index 739a058..f39ecb9 100644 --- a/fix-db-schema-conflicts.gemspec +++ b/fix-db-schema-conflicts.gemspec @@ -15,16 +15,10 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' - spec.add_development_dependency 'rspec', '~>3.4.0' spec.add_development_dependency 'rails', '~> 4.2.0' - spec.add_development_dependency 'sqlite3', '~> 1.3.0' - - spec.add_dependency 'rubocop', '>= 0.38.0' - spec.required_ruby_version = '>= 2.0.0', '< 4' end diff --git a/lib/fix_db_schema_conflicts/autocorrect_configuration.rb b/lib/fix_db_schema_conflicts/autocorrect_configuration.rb deleted file mode 100644 index f183d89..0000000 --- a/lib/fix_db_schema_conflicts/autocorrect_configuration.rb +++ /dev/null @@ -1,25 +0,0 @@ -module FixDBSchemaConflicts - class AutocorrectConfiguration - def self.load - new.load - end - - def load - if less_than_rubocop?(49) - '.rubocop_schema.yml' - elsif less_than_rubocop?(53) - '.rubocop_schema.49.yml' - elsif less_than_rubocop?(77) - '.rubocop_schema.53.yml' - else - '.rubocop_schema.77.yml' - end - end - - private - - def less_than_rubocop?(ver) - Gem.loaded_specs['rubocop'].version < Gem::Version.new("0.#{ver}.0") - end - end -end diff --git a/lib/fix_db_schema_conflicts/postgres_details_extractor.rb b/lib/fix_db_schema_conflicts/postgres_details_extractor.rb new file mode 100644 index 0000000..cd7baaa --- /dev/null +++ b/lib/fix_db_schema_conflicts/postgres_details_extractor.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +module FixDBSchemaConflicts + class PostgresDetailsExtractor + def initialize(connection) + @connection = connection + @pg_enum_types = @connection.fetch_enum_types + @pg_functions = @connection.pg_functions + @pg_fts_configurations = @connection.fts_configurations + @pg_aggregates = @connection.all_aggregates + files_verif_and_reset + end + + def create_types(stream) + @pg_enum_types.each do |enum| + exttracted_values = extract_types_components(enum) + types_in_file(exttracted_values) + end + + if @pg_composite_types.present? + @pg_composite_types.each do |composite| + exttracted_values = extract_types_components(composite) + types_in_file(exttracted_values) + end + end + stream.puts("\tfile = Rails.root.join('db', 'others', 'types.sql')") + stream.puts("\tfile_content = File.read(file)") + stream.puts("\texecute file_content") + end + + def create_aggregates(stream) + return if @pg_aggregates.empty? + + @pg_aggregates.each do |aggregate| + aggregates_in_file(aggregate.definition) + end + stream.puts("\taggregate_file = Rails.root.join('db', 'others', 'aggregates.sql')") + stream.puts("\taggregate_content = File.read(aggregate_file)") + stream.puts("\texecute aggregate_content") + end + + def create_functions(stream) + @pg_functions.each do |row| + function_content, function_name = extract_function_components(row) + functions_in_file(function_name, function_content) + end + + stream.puts("\tfunction_files_path = Rails.root.join('db', 'functions')") + stream.puts("\tsql_files = Dir.glob(File.join(function_files_path, '*.sql')).sort") + stream.puts("\tsql_files.sort.each do |file|") + stream.puts("\t sql = File.read(file)") + stream.puts("\t execute sql") + stream.puts("\tend") + end + + def create_fts_configurations(stream) + unless @pg_fts_configurations.empty? + @pg_fts_configurations.each do |fts_configuration| + sql_code = "DO $$ BEGIN " \ + "IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = '#{fts_configuration.name}') THEN " \ + "CREATE TEXT SEARCH CONFIGURATION #{fts_configuration.name} (COPY = simple); " \ + "END IF; " \ + "END $$;" + extract_fts_configurations(sql_code) + end + stream.puts("\tfts_file = Rails.root.join('db', 'others', 'fts.sql')") + stream.puts("\tfts_content = File.read(fts_file)") + stream.puts("\texecute fts_content") + end + end + + private + + def files_verif_and_reset + type_file_path = Rails.root.join('db', 'others') + file_name = type_file_path.join("types.sql") + FileUtils.mkdir_p type_file_path unless type_file_path.exist? + File.open(file_name, "w") {} + fts_file_path = Rails.root.join('db', 'others', 'fts.sql') + File.open(fts_file_path, "w") {} + aggreagte_file_path = Rails.root.join('db', 'others', 'aggregates.sql') + File.open(aggreagte_file_path, "w") {} + function_files = Rails.root.join('db', 'functions') + FileUtils.rm_rf(Dir.glob("#{function_files}/*")) if function_files.present? + end + + def extract_function_components(row) + # Extract function components + function_name = row.name + arguments = row.arguments + return_type = row.return_type + language_name = row.language_name + + # Ensure function body doesn't have trailing semicolons + function_body = row.body.strip.sub(/;+\z/, '') + + # Check if the function body starts with 'BEGIN' + starts_with_begin = function_body.match?(/^\s*BEGIN/i) + + if starts_with_begin + function_content = <<~SQL + CREATE OR REPLACE FUNCTION #{function_name}(#{arguments}) + RETURNS #{return_type} AS $$ + #{function_body}; + $$ LANGUAGE #{language_name} #{row.volatility}; + SQL + else + function_content = <<~SQL + CREATE OR REPLACE FUNCTION #{function_name}(#{arguments}) + RETURNS #{return_type} AS $$ + BEGIN + #{function_body}; + END; + $$ LANGUAGE plpgsql #{row.volatility}; + SQL + end + [function_content, function_name] + end + + def extract_types_components(row) + <<~SQL + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '#{row.name}') THEN + CREATE TYPE #{row.name} AS (#{row.attributes}); + END IF; + END + $$; + SQL + end + + def functions_in_file(file_name, function_content) + function_file_path = Rails.root.join('db', 'functions') + file_name = function_file_path.join("#{file_name}.sql") + FileUtils.mkdir_p function_file_path unless function_file_path.exist? + File.open(file_name, "w") {} unless File.exist?(file_name) + File.open(file_name, 'a') do |file| + function_content = function_content.gsub(/\n+/, "\n") + .gsub(/^\s+/m, '') + .strip + formatted_sql = function_content.gsub(/(?=\b(BEGIN|END|LANGUAGE|SET|WHERE)\b)/, "\n") + file.write(formatted_sql + ";") + file.puts + file.puts + end + end + + def types_in_file(type_content) + file_name = Rails.root.join('db', 'others', "types.sql") + File.open(file_name, 'a') do |file| + type_content = type_content.gsub(/\n+/, "\n") + .gsub(/^\s+/m, '') + .strip + formatted_sql = type_content.gsub(/(?=\b(AS|BEGIN|END|LANGUAGE|CREATE)\b)/, "\n") + file.write(formatted_sql + ";") + file.puts + file.puts + end + end + + def extract_fts_configurations(fts_content) + file_name = Rails.root.join('db', 'others', "fts.sql") + File.open(file_name, 'a') do |file| + fts_content = fts_content.strip + formatted_sql = fts_content.gsub(/(?=\b(AS|BEGIN|END|LANGUAGE|CREATE)\b)/, "\n") + file.write(formatted_sql + ";") + file.puts + file.puts + end + end + + def aggregates_in_file(aggregate_content) + file_name = Rails.root.join('db', 'others', "aggregates.sql") + File.open(file_name, 'a') do |file| + aggregate_content = sanitize_aggregate_definition(aggregate_content) + formatted_sql = aggregate_content.gsub(/(?=\b(AS|BEGIN|END|LANGUAGE|CREATE)\b)/, "\n") + file.write(formatted_sql + ";") + file.puts + file.puts + end + end + + def sanitize_aggregate_definition(sql) + # Remove invalid or empty clauses completely + sql = sql.gsub(/,\s*FINALFUNC\s*=\s*[^,\)\n]*/, '') + .gsub(/,\s*FINALFUNC_MODIFY\s*=\s*[^,\)\n]*/, '') + .gsub(/,\s*MFINALFUNC_MODIFY\s*=\s*[^,\)\n]*/, '') + .gsub(/,\s*COMBINEFUNC\s*=\s*[^,\)\n]*/, '') + .gsub(/,\s*SERIALFUNC\s*=\s*[^,\)\n]*/, '') + .gsub(/,\s*DESERIALFUNC\s*=\s*[^,\)\n]*/, '') + + # Remove dangling empty clauses + sql = sql.gsub(/\b(FINALFUNC|FINALFUNC_MODIFY|MFINALFUNC_MODIFY|COMBINEFUNC|SERIALFUNC|DESERIALFUNC)\s*=\s*[^,\)\n]*/, '') + + # Add missing commas between SFUNC and STYPE + sql.gsub!(/(SFUNC\s*=\s*[^\s,]+)\s+(STYPE\s*=\s*[^\s,]+)/, '\1, \2') + + # Remove trailing commas before closing parentheses + sql.gsub!(/,\s*\)/, ')') + + # Remove unnecessary semicolons + sql.gsub!(/;;$/, ';') + + # Ensure proper formatting by removing extra spaces + sql.gsub!(/\s{2,}/, ' ') # Replace multiple spaces with a single space + + # Final cleanup + sql.strip + end + end +end diff --git a/lib/fix_db_schema_conflicts/postgres_schema_info_extractor.rb b/lib/fix_db_schema_conflicts/postgres_schema_info_extractor.rb new file mode 100644 index 0000000..e54b44f --- /dev/null +++ b/lib/fix_db_schema_conflicts/postgres_schema_info_extractor.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true +require 'delegate' +require 'fix-db-schema-conflicts' +require 'rails' + +module FixDBSchemaConflicts + class PostgresSchemaInfoExtractor + def initialize(connection) + @connection = connection + end + + def pg_functions + query = <<-SQL + SELECT + proname AS function_name, + pg_get_function_arguments(pg_proc.oid) AS arguments, + pg_get_function_result(pg_proc.oid) AS return_type, + CASE + WHEN prosrc IS NOT NULL THEN prosrc + ELSE 'Invalid Function Body' + END AS function_body, + CASE provolatile + WHEN 'i' THEN 'IMMUTABLE' + WHEN 's' THEN 'STABLE' + WHEN 'v' THEN 'VOLATILE' + ELSE 'UNKNOWN' + END AS volatility, + pg_language.lanname AS language_name + FROM pg_proc + JOIN pg_namespace ON pg_namespace.oid = pg_proc.pronamespace + JOIN pg_language ON pg_language.oid = pg_proc.prolang + WHERE pg_language.lanname IN ('sql', 'plpgsql') + AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema') + AND proname NOT LIKE 'pg_%' + ORDER BY function_name; + SQL + + @connection.execute(query).map do |row| + OpenStruct.new( + name: row['function_name'], + arguments: row['arguments'], + return_type: row['return_type'], + body: row['function_body'], + volatility: row['volatility'], + language_name: row['language_name'] + ) + end + end + + def fetch_enum_types + query = <<-SQL + SELECT t.typname AS type_name, + string_agg(quote_literal(e.enumlabel), ', ') AS enum_values + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + WHERE t.typcategory = 'E' + GROUP BY t.typname; + SQL + + @connection.execute(query).map do |row| + OpenStruct.new( + name: row['type_name'], + enum_values: row['enum_values'] + ) + end + end + + def fts_configurations + query = <<-SQL + SELECT + nspname AS schema_name, + cfgname AS configuration_name + FROM + pg_ts_config + JOIN + pg_namespace ON pg_ts_config.cfgnamespace = pg_namespace.oid + ORDER BY + schema_name, configuration_name; + SQL + + @connection.execute(query).map do |row| + OpenStruct.new(schema: row['schema_name'], name: row['configuration_name']) + end + end + + def aggregates + query = <<-SQL + SELECT + p.proname AS name, + format_type(a.aggtranstype, NULL) AS state_type, + (SELECT proname FROM pg_proc WHERE oid = a.aggtransfn) AS transition_function, + (SELECT proname FROM pg_proc WHERE oid = a.aggfinalfn) AS final_function, + (SELECT proname FROM pg_proc WHERE oid = a.aggcombinefn) AS combine_function, + (SELECT proname FROM pg_proc WHERE oid = a.aggserialfn) AS serialize_function, + (SELECT proname FROM pg_proc WHERE oid = a.aggdeserialfn) AS deserialize_function, + a.agginitval AS initial_value, + a.aggfinalmodify AS finalfunc_modify, + a.aggmfinalmodify AS mfinalfunc_modify, + a.aggkind AS aggregate_kind + FROM + pg_aggregate a + JOIN + pg_proc p ON a.aggfnoid = p.oid + JOIN + pg_namespace n ON p.pronamespace = n.oid + WHERE + n.nspname = 'wizville' + ORDER BY + p.proname; + SQL + + @connection.execute(query).map do |row| + # Start building the SQL string dynamically + sql_parts = [] + sql_parts << "CREATE AGGREGATE public.#{row['name']} (#{row['state_type']}) (" + sql_parts << "SFUNC = #{row['transition_function']}" + sql_parts << "STYPE = #{row['state_type']}" + + # Add optional clauses only if valid + sql_parts << "FINALFUNC = #{row['final_function']}" unless row['final_function'] == "-" + sql_parts << "FINALFUNC_MODIFY = #{row['finalfunc_modify']}" if row['finalfunc_modify'] + sql_parts << "MFINALFUNC_MODIFY = #{row['mfinalfunc_modify']}" if row['mfinalfunc_modify'] + sql_parts << "COMBINEFUNC = #{row['combine_function']}" unless row['combine_function'] == "-" + sql_parts << "SERIALFUNC = #{row['serialize_function']}" unless row['serialize_function'] == "-" + sql_parts << "DESERIALFUNC = #{row['deserialize_function']}" unless row['deserialize_function'] == "-" + sql_parts << "INITCOND = '#{row['initial_value']}'" if row['initial_value'] + + # Close the SQL statement + sql_parts << ");" + + # Join all parts into a single string + create_aggregate_sql = sql_parts.join("\n ") + + # Sanitize or validate the SQL if needed + sanitized_sql = sanitize_aggregate_definition(create_aggregate_sql) + + OpenStruct.new(name: row['name'], definition: sanitized_sql) + end + end + + def sanitize_aggregate_definition(sql) + # Remove invalid or empty clauses + sql = sql.gsub(/, FINALFUNC =\s*\w*/, '') + .gsub(/, FINALFUNC_MODIFY =\s*\w*/, '') + .gsub(/, MFINALFUNC_MODIFY =\s*\w*/, '') + .gsub(/, COMBINEFUNC =\s*\w*/, '') + .gsub(/, SERIALFUNC =\s*\w*/, '') + .gsub(/, DESERIALFUNC =\s*\w*/, '') + + # Remove trailing commas and clean up the SQL + sql.gsub!(/,\s*\)/, ')') + sql.strip + end + end +end diff --git a/lib/fix_db_schema_conflicts/schema_dumper.rb b/lib/fix_db_schema_conflicts/schema_dumper.rb index 1bcd458..666d7a9 100644 --- a/lib/fix_db_schema_conflicts/schema_dumper.rb +++ b/lib/fix_db_schema_conflicts/schema_dumper.rb @@ -1,8 +1,16 @@ require 'delegate' +require_relative 'postgres_schema_info_extractor' +require 'fix_db_schema_conflicts/triggers_operations' +require 'fix_db_schema_conflicts/postgres_details_extractor' module FixDBSchemaConflicts module SchemaDumper class ConnectionWithSorting < SimpleDelegator + def initialize(connection) + super(connection) + @schema_info_extractor = FixDBSchemaConflicts::PostgresSchemaInfoExtractor.new(__getobj__) + end + def extensions __getobj__.extensions.sort end @@ -16,7 +24,42 @@ def indexes(table) end def foreign_keys(table) - __getobj__.indexes(table).sort_by(&:name) + __getobj__.foreign_keys(table).sort_by(&:name) + end + + def pg_functions + @schema_info_extractor.pg_functions + end + + def fetch_enum_types + @schema_info_extractor.fetch_enum_types + end + + def triggers(table) + query = <<-SQL + SELECT tgname AS name, pg_get_triggerdef(oid) AS definition + FROM pg_trigger + WHERE tgrelid = '#{table}'::regclass AND NOT tgisinternal; + SQL + + __getobj__.execute(query).map do |row| + sanitized_sql = sanitize_trigger_definition(row['definition']) + OpenStruct.new(name: row['name'], definition: sanitized_sql) + end + end + + def sanitize_trigger_definition(definition) + new_definition = definition.gsub(/ON\s+(\w+\.)?(\w+\.)?/, 'ON ') + new_definition.gsub!(/\bwizville\./, 'public.') + new_definition + end + + def fts_configurations + @schema_info_extractor.fts_configurations + end + + def all_aggregates + @schema_info_extractor.aggregates end end @@ -26,17 +69,53 @@ def extensions(*args) end end - def table(*args) + def tables(stream) + specific = ENV['SCHEMA'] || if defined? ActiveRecord::Tasks::DatabaseTasks + File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'specific.rb') + else + "#{Rails.root}/db/specific.rb" + end + + if File.exist?(specific) + stream.puts("\t" + File.read(specific).gsub("\n", "\n\t") + "\n\n") + end + + aggreagates_fetched = false with_sorting do - super(*args) + @details_operations = FixDBSchemaConflicts::PostgresDetailsExtractor.new(@connection) + @details_operations.create_types(stream) + @details_operations.create_functions(stream) + @details_operations.create_fts_configurations(stream) + @details_operations.create_aggregates(stream) unless aggreagates_fetched + end + super(stream) + end + + def table(table_name, stream, *args) + with_sorting do + super(table_name, stream, *args) + + if @triggers_operations.nil? + @triggers_operations = FixDBSchemaConflicts::TriggersOperations.new(@connection) + @triggers_operations.reset_triggers_folder + end + @triggers_operations.triggers_creation(table_name, stream) end end def with_sorting - old, @connection = @connection, ConnectionWithSorting.new(@connection) - result = yield - @connection = old - result + old_connection = @connection + @connection = ConnectionWithSorting.new(old_connection) + begin + yield + ensure + @connection = old_connection + end + end + + def sanitize_aggregate_definition(definition) + # Sanitize or format the definition as needed + definition.strip end end end diff --git a/lib/fix_db_schema_conflicts/tasks/db.rake b/lib/fix_db_schema_conflicts/tasks/db.rake index 5b33648..dbb7a7d 100644 --- a/lib/fix_db_schema_conflicts/tasks/db.rake +++ b/lib/fix_db_schema_conflicts/tasks/db.rake @@ -1,25 +1,13 @@ require 'shellwords' -require_relative '../autocorrect_configuration' namespace :db do namespace :schema do task :dump do - puts "Dumping database schema with fix-db-schema-conflicts gem" - filename = ENV['SCHEMA'] || if defined? ActiveRecord::Tasks::DatabaseTasks File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') else "#{Rails.root}/db/schema.rb" end - autocorrect_config = FixDBSchemaConflicts::AutocorrectConfiguration.load - rubocop_yml = File.expand_path("../../../../#{autocorrect_config}", __FILE__) - auto_correct_arg = if Gem.loaded_specs['rubocop'].version >= Gem::Version.new('1.30') - 'autocorrect' - else - 'auto-correct' - end - - `bundle exec rubocop --#{auto_correct_arg} --config #{rubocop_yml} #{filename.shellescape}` end end end diff --git a/lib/fix_db_schema_conflicts/triggers_operations.rb b/lib/fix_db_schema_conflicts/triggers_operations.rb new file mode 100644 index 0000000..4a2495c --- /dev/null +++ b/lib/fix_db_schema_conflicts/triggers_operations.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require 'delegate' + +module FixDBSchemaConflicts + class TriggersOperations + def initialize(connection) + @connection = connection + @reset_done = false + end + + def triggers_creation(table_name, stream, *args) + triggers = @connection.triggers(table_name) + unless triggers.empty? + triggers.each do |trigger| + add_triggers_in_file(table_name, trigger.definition) + end + stream.puts("\ttrigger_file = Rails.root.join('db', 'triggers', \"#{table_name}.sql\")") + stream.puts("\tfile_content = File.read(trigger_file)") + stream.puts("\texecute file_content") + end + end + + def reset_triggers_folder + return if @reset_done + + dir_path = Rails.root.join('app', 'triggers') + FileUtils.rm_rf Dir.glob("#{dir_path}/*") if dir_path.present? + @reset_done = true + end + + private + + def add_triggers_in_file(file_name, trigger_content) + triggers_path = Rails.root.join('db', 'triggers') + FileUtils.mkdir_p triggers_path unless triggers_path.exist? + file_path = triggers_path.join("#{file_name}.sql") + File.open(file_path, "w") {} + File.open(file_path, 'a') do |file| + formatted_sql = format_trigger(trigger_content) + file.write(formatted_sql + ";") + file.puts + file.puts + end + end + + def format_trigger(trigger_content) + trigger_content = trigger_content.gsub(/\n+/, "\n") # Remove extra blank lines + .gsub(/^\s+/m, '') # Trim leading spaces for each line + .strip # Remove leading/trailing blank lines + trigger_content.gsub(/(?=\b(AFTER|BEFORE|FOR EACH ROW|WHEN|EXECUTE FUNCTION)\b)/, "\n") + end + end +end diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb deleted file mode 100644 index 20f1885..0000000 --- a/spec/integration/integration_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -RSpec.describe 'Fix DB Schema Conflicts' do - - let(:expected_lines) { reference_db_schema.lines } - - it 'generates a sorted schema with no extra spacing' do - - `cd spec/test-app && rm -f db/schema.rb && rake db:migrate` - - generated_lines = File.readlines('spec/test-app/db/schema.rb') - - generated_lines.zip(expected_lines).each do |generated, expected| - next if expected.nil? - next if expected.start_with?('ActiveRecord::Schema.define') - expect(generated).to eq(expected) - end - end -end - -def reference_db_schema - <<-RUBY -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20160322223258) do - create_table "companies", force: :cascade do |t| - t.string "addr1" - t.string "addr2" - t.string "city" - t.datetime "created_at", null: false - t.string "name" - t.string "phone" - t.string "state" - t.datetime "updated_at", null: false - t.string "zip" - end - - add_index "companies", ["city"], name: "index_companies_on_city" - add_index "companies", ["name"], name: "index_companies_on_name" - add_index "companies", ["state"], name: "index_companies_on_state" - - create_table "people", force: :cascade do |t| - t.integer "age" - t.date "birthdate" - t.datetime "created_at", null: false - t.string "first_name" - t.string "last_name" - t.datetime "updated_at", null: false - end - RUBY -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index ce9e119..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,95 +0,0 @@ -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# The `.rspec` file also contains a few flags that are not defaults but that -# users commonly want. -# -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - - # These two settings work together to allow you to limit a spec run - # to individual examples or groups you care about by tagging them with - # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # get run. - config.filter_run :focus - config.run_all_when_everything_filtered = true - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - # config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - # config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -end diff --git a/spec/test-app/.gitignore b/spec/test-app/.gitignore deleted file mode 100644 index 050c9d9..0000000 --- a/spec/test-app/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. -# -# If you find yourself ignoring temporary files generated by your text editor -# or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' - -# Ignore bundler config. -/.bundle - -# Ignore the default SQLite database. -/db/*.sqlite3 -/db/*.sqlite3-journal - -# Ignore all logfiles and tempfiles. -/log/* -!/log/.keep -/tmp diff --git a/spec/test-app/Gemfile b/spec/test-app/Gemfile deleted file mode 100644 index 7ab0618..0000000 --- a/spec/test-app/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -source 'https://rubygems.org' - -# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.5.2' -# Use sqlite3 as the database for Active Record -gem 'sqlite3' - -gem 'fix-db-schema-conflicts', path: '../..' diff --git a/spec/test-app/Gemfile.lock b/spec/test-app/Gemfile.lock deleted file mode 100644 index c0941b9..0000000 --- a/spec/test-app/Gemfile.lock +++ /dev/null @@ -1,125 +0,0 @@ -PATH - remote: ../.. - specs: - fix-db-schema-conflicts (1.2.0) - rubocop (>= 0.36.0) - -GEM - remote: https://rubygems.org/ - specs: - actionmailer (4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5.2) - actionview (= 4.2.5.2) - activesupport (= 4.2.5.2) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5.2) - activesupport (= 4.2.5.2) - builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5.2) - activesupport (= 4.2.5.2) - globalid (>= 0.3.0) - activemodel (4.2.5.2) - activesupport (= 4.2.5.2) - builder (~> 3.1) - activerecord (4.2.5.2) - activemodel (= 4.2.5.2) - activesupport (= 4.2.5.2) - arel (~> 6.0) - activesupport (4.2.5.2) - i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - arel (6.0.3) - ast (2.2.0) - builder (3.2.2) - concurrent-ruby (1.0.1) - erubis (2.7.0) - globalid (0.3.6) - activesupport (>= 4.1.0) - i18n (0.7.0) - json (1.8.3) - loofah (2.0.3) - nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) - mime-types (2.99.1) - mini_portile2 (2.0.0) - minitest (5.8.4) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) - parser (2.3.0.6) - ast (~> 2.2) - powerpack (0.1.1) - rack (1.6.4) - rack-test (0.6.3) - rack (>= 1.0) - rails (4.2.5.2) - actionmailer (= 4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) - activemodel (= 4.2.5.2) - activerecord (= 4.2.5.2) - activesupport (= 4.2.5.2) - bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5.2) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) - activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) - rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) - railties (4.2.5.2) - actionpack (= 4.2.5.2) - activesupport (= 4.2.5.2) - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) - rake (11.1.1) - rubocop (0.38.0) - parser (>= 2.3.0.6, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.7.5) - sprockets (3.5.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.0.4) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - sqlite3 (1.3.11) - thor (0.19.1) - thread_safe (0.3.5) - tzinfo (1.2.2) - thread_safe (~> 0.1) - unicode-display_width (1.0.2) - -PLATFORMS - ruby - -DEPENDENCIES - fix-db-schema-conflicts! - rails (= 4.2.5.2) - sqlite3 - -BUNDLED WITH - 1.10.6 diff --git a/spec/test-app/README.rdoc b/spec/test-app/README.rdoc deleted file mode 100644 index dd4e97e..0000000 --- a/spec/test-app/README.rdoc +++ /dev/null @@ -1,28 +0,0 @@ -== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -rake doc:app. diff --git a/spec/test-app/Rakefile b/spec/test-app/Rakefile deleted file mode 100644 index ba6b733..0000000 --- a/spec/test-app/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require File.expand_path('../config/application', __FILE__) - -Rails.application.load_tasks diff --git a/spec/test-app/app/assets/config/manifest.js b/spec/test-app/app/assets/config/manifest.js deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/assets/images/.keep b/spec/test-app/app/assets/images/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/assets/javascripts/application.js b/spec/test-app/app/assets/javascripts/application.js deleted file mode 100644 index e07c5a8..0000000 --- a/spec/test-app/app/assets/javascripts/application.js +++ /dev/null @@ -1,16 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery -//= require jquery_ujs -//= require turbolinks -//= require_tree . diff --git a/spec/test-app/app/assets/stylesheets/application.css b/spec/test-app/app/assets/stylesheets/application.css deleted file mode 100644 index f9cd5b3..0000000 --- a/spec/test-app/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any styles - * defined in the other CSS/SCSS files in this directory. It is generally better to create a new - * file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/spec/test-app/app/controllers/application_controller.rb b/spec/test-app/app/controllers/application_controller.rb deleted file mode 100644 index d83690e..0000000 --- a/spec/test-app/app/controllers/application_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ApplicationController < ActionController::Base - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception -end diff --git a/spec/test-app/app/controllers/concerns/.keep b/spec/test-app/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/helpers/application_helper.rb b/spec/test-app/app/helpers/application_helper.rb deleted file mode 100644 index de6be79..0000000 --- a/spec/test-app/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/spec/test-app/app/mailers/.keep b/spec/test-app/app/mailers/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/models/.keep b/spec/test-app/app/models/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/models/company.rb b/spec/test-app/app/models/company.rb deleted file mode 100644 index 4ac174e..0000000 --- a/spec/test-app/app/models/company.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Company < ActiveRecord::Base -end diff --git a/spec/test-app/app/models/concerns/.keep b/spec/test-app/app/models/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/test-app/app/models/person.rb b/spec/test-app/app/models/person.rb deleted file mode 100644 index 2f2e286..0000000 --- a/spec/test-app/app/models/person.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Person < ActiveRecord::Base -end diff --git a/spec/test-app/app/views/layouts/application.html.erb b/spec/test-app/app/views/layouts/application.html.erb deleted file mode 100644 index 797902d..0000000 --- a/spec/test-app/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - -
-You may have mistyped the address or the page may have moved.
-If you are the application owner check the logs for more information.
-Maybe you tried to change something you didn't have access to.
-If you are the application owner check the logs for more information.
-If you are the application owner check the logs for more information.
-