From 03b44a152f4c5ed14a16fea754b37fec2a5b9d4b Mon Sep 17 00:00:00 2001 From: Alexey Lapitsky Date: Tue, 5 Aug 2014 20:29:14 +0200 Subject: [PATCH] schema dumper This is adapted from @agios' and @Altonymous' pull requests --- lib/activeuuid.rb | 1 + lib/activeuuid/schema_dumper.rb | 119 ++++++++++++++++++++++++++++++++ spec/lib/schema_dumper_spec.rb | 40 +++++++++++ 3 files changed, 160 insertions(+) create mode 100644 lib/activeuuid/schema_dumper.rb create mode 100644 spec/lib/schema_dumper_spec.rb diff --git a/lib/activeuuid.rb b/lib/activeuuid.rb index 0877a7b..f826215 100644 --- a/lib/activeuuid.rb +++ b/lib/activeuuid.rb @@ -1,6 +1,7 @@ require "activeuuid/version" require 'activeuuid/patches' require 'activeuuid/uuid' +require 'activeuuid/schema_dumper' require 'activeuuid/railtie' if defined?(Rails::Railtie) module ActiveUUID diff --git a/lib/activeuuid/schema_dumper.rb b/lib/activeuuid/schema_dumper.rb new file mode 100644 index 0000000..89fa663 --- /dev/null +++ b/lib/activeuuid/schema_dumper.rb @@ -0,0 +1,119 @@ +require 'active_record/schema_dumper' + +class ActiveRecord::SchemaDumper + private + def default_string(value) + case value + when BigDecimal + value.to_s + when Date, DateTime, Time + "'" + value.to_s(:db) + "'" + else + value.inspect + end + end + + def spec_for_column(column) + spec = {} + spec[:name] = column.name.inspect + + # AR has an optimization which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = case column.type + when :integer + column.sql_type =~ /^(numeric|decimal)/ ? 'decimal' : 'integer' + when :string + column.sql_type == 'uuid' ? 'uuid' : 'string' + else + column.type.to_s + end + spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null + spec[:default] = default_string(column.default) if column.has_default? + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} + spec + end + + # Adapted from rails 3.2 code + def table(table, stream) + columns = @connection.columns(table) + begin + tbl = StringIO.new + + # first dump primary key column + if @connection.respond_to?(:primary_key) + pk = @connection.primary_key(table) + elsif @connection.respond_to?(:pk_and_sequence_for) + pk, _ = @connection.pk_and_sequence_for(table) + end + + tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" + if columns.detect { |c| c.name == pk && c.type == :integer} + if pk != 'id' + tbl.print %Q(, :primary_key => "#{pk}") + end + else + tbl.print ", :id => false" + end + tbl.print ", :force => true" + tbl.puts " do |t|" + + # then dump all non-primary key columns + column_specs = columns.map do |column| + raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? + next if column.name == pk + spec_for_column column + end.compact + + # explicitly prepend non-integer primary key + if col = columns.detect { |c| c.name == pk && c.type != :integer} + pk_spec = spec_for_column col + pk_spec.delete(:null) + pk_spec[:primary_key] = ":primary_key => true" + column_specs.unshift pk_spec + end + + # find all migration keys used in this table + keys = [:name, :limit, :precision, :scale, :default, :null, :primary_key] & column_specs.map{ |k| k.keys }.flatten + + # figure out the lengths for each column based on above keys + lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } + + # the string we're going to sprintf our values against, with standardized column widths + format_string = lengths.map{ |len| "%-#{len}s" } + + # find the max length for the 'type' column, which is special + type_length = column_specs.map{ |column| column[:type].length }.max + + # add column type definition to our format string + format_string.unshift " t.%-#{type_length}s " + + format_string *= '' + + column_specs.each do |colspec| + values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } + values.unshift colspec[:type] + tbl.print((format_string % values).gsub(/,\s*$/, '')) + tbl.puts + end + + tbl.puts " end" + tbl.puts + + indexes(table, tbl) + + tbl.rewind + stream.print tbl.read + rescue => e + stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" + stream.puts "# #{e.message}" + stream.puts + puts e + puts e.backtrace + end + + stream + end +end diff --git a/spec/lib/schema_dumper_spec.rb b/spec/lib/schema_dumper_spec.rb new file mode 100644 index 0000000..89011c5 --- /dev/null +++ b/spec/lib/schema_dumper_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe ActiveRecord::SchemaDumper do + let!(:connection) { ActiveRecord::Base.connection } + let(:table_name) { :test_uuid_pk_dump } + + before do + connection.drop_table(table_name) if connection.table_exists?(table_name) + connection.create_table table_name, :id => false do |t| + t.uuid :id, :primary_key => true + end + end + + after do + connection.drop_table table_name + end + + context 'dump' do + let(:dump) do + stream = StringIO.new + ActiveRecord::SchemaDumper::dump(connection, stream) + stream.string.split("\n") + end + it 'should generate a dump' do + dump.should be_a_kind_of(Array) + end + context 'schema definition' do + let(:create_table_line) { dump.index{|l| /create_table "#{table_name}", :id =\> false/ =~ l} } + it 'should have a table creation statement' do + create_table_line.should be_a_kind_of(Numeric) + end + context 'table' do + it 'should have a uuid primary key' do + create_table_line.should_not be_nil + dump[create_table_line+1].should match(/t.uuid *"id", *:primary_key =\> true/) + end + end + end + end +end \ No newline at end of file