diff --git a/lib/activeuuid.rb b/lib/activeuuid.rb index 0877a7b..1e5a940 100644 --- a/lib/activeuuid.rb +++ b/lib/activeuuid.rb @@ -1,6 +1,8 @@ require "activeuuid/version" require 'activeuuid/patches' +require 'activeuuid/association' require 'activeuuid/uuid' +require 'activeuuid/schema_dumper' require 'activeuuid/railtie' if defined?(Rails::Railtie) module ActiveUUID diff --git a/lib/activeuuid/association.rb b/lib/activeuuid/association.rb new file mode 100644 index 0000000..87f62ef --- /dev/null +++ b/lib/activeuuid/association.rb @@ -0,0 +1,45 @@ +require 'active_record' +require 'active_record/associations/preloader/association' + +module ActiveRecord::Associations + class Preloader + class Association + def owners_by_key + @owners_by_key ||= owners.group_by do |owner| + key = owner[owner_key_name] + # NOTE: key.to_s screws up the literal in the `WHERE ... IN (...)` clause + # in AR::Base.includes... so we patch and comment it out. + key # && key.to_s + end + end + + def associated_records_by_owner + owners_map = owners_by_key + owner_keys = owners_map.keys.compact + + if klass.nil? || owner_keys.empty? + records = [] + else + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # Make several smaller queries if necessary or make one query if the adapter supports it + sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) + records = sliced.map { |slice| records_for(slice) }.flatten + end + + # Each record may have multiple owners, and vice-versa + records_by_owner = Hash[owners.map { |owner| [owner, []] }] + records.each do |record| + owner_key = record[association_key_name].to_s + # NOTE: #to_s screws up the literal in the `WHERE ... IN (...)` clause + # in AR::Base.includes... so we patch and comment it out. + owner_key = record[association_key_name] # .to_s + + owners_map[owner_key].each do |owner| + records_by_owner[owner] << record + end + end + records_by_owner + end + end + end +end \ No newline at end of file diff --git a/lib/activeuuid/patches.rb b/lib/activeuuid/patches.rb index 5f89dc3..7c3507f 100644 --- a/lib/activeuuid/patches.rb +++ b/lib/activeuuid/patches.rb @@ -8,10 +8,13 @@ module Migrations def uuid(*column_names) options = column_names.extract_options! column_names.each do |name| - type = @base.adapter_name.downcase == 'postgresql' ? 'uuid' : 'binary(16)' + type = adapter_name.downcase == 'postgresql' ? 'uuid' : 'binary(16)' column(name, "#{type}#{' PRIMARY KEY' if options.delete(:primary_key)}", options) end end + def adapter_name + defined?(@base) ? @base.adapter_name : ActiveRecord::Base.connection.adapter_name + end end module Column @@ -29,7 +32,7 @@ def type_cast_code_with_uuid(var_name) end def simplified_type_with_uuid(field_type) - return :uuid if field_type == 'binary(16)' || field_type == 'binary(16,0)' + return :uuid if field_type == 'binary(16)' || field_type == 'binary(16,0)' || field_type == 'tinyblob' simplified_type_without_uuid(field_type) end diff --git a/lib/activeuuid/schema_dumper.rb b/lib/activeuuid/schema_dumper.rb new file mode 100644 index 0000000..8486228 --- /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 \ No newline at end of file diff --git a/lib/activeuuid/uuid.rb b/lib/activeuuid/uuid.rb index dcff691..472ca11 100644 --- a/lib/activeuuid/uuid.rb +++ b/lib/activeuuid/uuid.rb @@ -18,7 +18,7 @@ def quoted_id end def as_json(options = nil) - hexdigest.upcase + hexdigest ? hexdigest.upcase : to_s end def to_param @@ -54,25 +54,25 @@ def self.parse_string(str) module Arel module Visitors class DepthFirst < Arel::Visitors::Visitor - def visit_UUIDTools_UUID(o) + def visit_UUIDTools_UUID(o, a) o.quoted_id end end class MySQL < Arel::Visitors::ToSql - def visit_UUIDTools_UUID(o) + def visit_UUIDTools_UUID(o, a) o.quoted_id end end class SQLite < Arel::Visitors::ToSql - def visit_UUIDTools_UUID(o) + def visit_UUIDTools_UUID(o, a) o.quoted_id end end class PostgreSQL < Arel::Visitors::ToSql - def visit_UUIDTools_UUID(o) + def visit_UUIDTools_UUID(o, a) "'#{o.to_s}'" end end @@ -90,7 +90,7 @@ module UUID self._uuid_generator = :random singleton_class.alias_method_chain :instantiate, :uuid - before_create :generate_uuids_if_needed + after_initialize :generate_uuids_if_needed end module ClassMethods @@ -114,7 +114,7 @@ def uuids(*attributes) EOS end - def instantiate_with_uuid(record) + def instantiate_with_uuid(record, column_types={}) uuid_columns.each do |uuid_column| record[uuid_column] = UUIDTools::UUID.serialize(record[uuid_column]).to_s if record[uuid_column] end @@ -143,7 +143,7 @@ def create_uuid def generate_uuids_if_needed primary_key = self.class.primary_key - if self.class.columns_hash[primary_key].type == :uuid + if primary_key && self.class.columns_hash[primary_key].type == :uuid send("#{primary_key}=", create_uuid) unless send("#{primary_key}?") end end diff --git a/spec/fabricators/tag_fabricator.rb b/spec/fabricators/tag_fabricator.rb new file mode 100644 index 0000000..134a88b --- /dev/null +++ b/spec/fabricators/tag_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:tag) do + name { Forgery::LoremIpsum.word } +end \ No newline at end of file diff --git a/spec/fabricators/uuid_article_fabricator.rb b/spec/fabricators/uuid_article_fabricator.rb index 71acb18..23f65bf 100644 --- a/spec/fabricators/uuid_article_fabricator.rb +++ b/spec/fabricators/uuid_article_fabricator.rb @@ -1,4 +1,5 @@ Fabricator(:uuid_article) do title { Forgery::LoremIpsum.word } body { Forgery::LoremIpsum.sentence } + tags(count: 5) end diff --git a/spec/lib/activerecord_spec.rb b/spec/lib/activerecord_spec.rb index 69b6f7e..7b06c76 100644 --- a/spec/lib/activerecord_spec.rb +++ b/spec/lib/activerecord_spec.rb @@ -43,7 +43,7 @@ spec_for_adapter do |adapters| adapters.sqlite3 { connection.change_column table_name, column_name, :uuid } adapters.mysql2 { connection.change_column table_name, column_name, :uuid } - # adapters.postgresql { connection.change_column table_name, column_name, :uuid } + adapters.postgresql { connection.change_column table_name, column_name, :uuid } end end @@ -130,6 +130,11 @@ specify { model.where(id: id.raw).first.should == article } end + context '.includes' do + specify { model.includes(:tags).first.tags.count.should == 5 } + specify { model.includes(:tags).first.tags.first.uuid_article.should == article } + end + context '#destroy' do subject { article } its(:delete) { should be_true } 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 diff --git a/spec/support/migrate/20130808112105_create_tags.rb b/spec/support/migrate/20130808112105_create_tags.rb new file mode 100644 index 0000000..d4974ca --- /dev/null +++ b/spec/support/migrate/20130808112105_create_tags.rb @@ -0,0 +1,10 @@ +class CreateTags < ActiveRecord::Migration + def change + create_table :tags, :id => false do |t| + t.uuid :id, :primary_key => true + t.string :name + t.uuid :uuid_article_id + t.timestamps + end + end +end \ No newline at end of file diff --git a/spec/support/models/tag.rb b/spec/support/models/tag.rb new file mode 100644 index 0000000..bef1335 --- /dev/null +++ b/spec/support/models/tag.rb @@ -0,0 +1,4 @@ +class Tag < ActiveRecord::Base + include ActiveUUID::UUID + belongs_to :uuid_article +end \ No newline at end of file diff --git a/spec/support/models/uuid_article.rb b/spec/support/models/uuid_article.rb index 0292bb9..10a159d 100644 --- a/spec/support/models/uuid_article.rb +++ b/spec/support/models/uuid_article.rb @@ -1,3 +1,5 @@ class UuidArticle < ActiveRecord::Base include ActiveUUID::UUID + + has_many :tags end