Skip to content

Instantly share code, notes, and snippets.

@snusnu
Created December 11, 2012 04:00
Show Gist options
  • Save snusnu/4255792 to your computer and use it in GitHub Desktop.
Save snusnu/4255792 to your computer and use it in GitHub Desktop.
minimal aliasing strategies for dm2
require 'equalizer'
require 'abstract_type'
module DataMapper
module Relation
class Graph
class Node
class Aliases
include Enumerable
include Equalizer.new(:index)
attr_reader :index
attr_reader :header
protected :index
def initialize(index, aliases = {})
@index = index
@aliases = aliases
@header = @index.header
end
def each(&block)
return to_enum unless block_given?
@aliases.each(&block)
self
end
def join(other, join_definition)
joined_index = index.join(other.index, join_definition)
self.class.new(joined_index, index.aliases(joined_index))
end
def rename(aliases)
self.class.new(index.rename(aliases), aliases)
end
class Index
include Equalizer.new(:entries)
attr_reader :entries
attr_reader :header
def initialize(entries, strategy)
@entries = entries
@inverted = @entries.invert
@header = @entries.values.to_set
@strategy = strategy.new(self)
end
def join(*args)
@strategy.join(*args)
end
def rename(aliases)
self.class.new(renamed_entries(aliases), @strategy.class)
end
def aliases(other)
entries.each_with_object({}) { |(key, name), aliases|
other_name = other[key]
aliases[name] = other_name if name != other_name
}
end
def renamed_join_key_entries(join_definition)
entries.each_with_object({}) { |(key, name), renamed|
join_definition.each do |left_key, right_key|
renamed[key] = right_key if name == left_key
end
}
end
def renamed_clashing_entries(other, join_definition)
entries.each_with_object({}) { |(key, name), renamed|
next if !other.include?(name) || join_definition.key?(name)
renamed[key] = key
}
end
def [](key)
entries[key]
end
def include?(name)
entries.value?(name)
end
private
def renamed_entries(aliases)
aliases.each_with_object(entries.dup) { |(from, to), renamed|
renamed[@inverted.fetch(from)] = to
}
end
end # class Index
class Strategy
include AbstractType
def initialize(index)
@index = index
end
def join(index, join_definition)
index.class.new(index_entries(index, join_definition), self.class)
end
abstract_method :index_entries
private :index_entries
private
attr_reader :index
def join_key_entries(*args)
index.renamed_join_key_entries(*args)
end
def clashing_entries(*args)
index.renamed_clashing_entries(*args)
end
class InnerJoin < self
private
def index_entries(other_index, join_definition)
index.entries.dup.
update(clashing_entries(other_index, join_definition)).
update(other_index.entries)
end
end
class NaturalJoin < self
private
def index_entries(other_index, join_definition)
index.entries.dup.
update(join_key_entries(join_definition)).
update(clashing_entries(other_index, join_definition)).
update(other_index.entries)
end
end # class NaturalJoin
end # class Strategy
end # class Aliases
end # class Node
end # class Graph
end # module Relation
end # module DataMapper
require 'rspec'
describe DataMapper::Relation::Graph::Node::Aliases, '#rename' do
subject { object.rename(aliases) }
let(:object) { described_class.new(songs_index) }
let(:songs_index) { described_class::Index.new(songs_entries, strategy) }
let(:strategy) { described_class::Strategy::NaturalJoin }
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
}}
let(:aliases) {{
:id => :foo_id,
:title => :foo_title
}}
let(:expected_index) { described_class::Index.new(expected_entries, strategy) }
let(:expected_entries) {{
:songs_id => :foo_id,
:songs_title => :foo_title,
}}
it { should be_instance_of(object.class) }
its(:index) { should eql(expected_index) }
end
shared_examples_for 'a command method' do
it 'returns self' do
should equal(object)
end
end
shared_examples_for 'an #each method' do
it_should_behave_like 'a command method'
context 'with no block' do
subject { object.each }
it { should be_instance_of(to_enum.class) }
it 'yields the expected values' do
subject.to_a.should eql(object.to_a)
end
end
end
describe DataMapper::Relation::Graph::Node::Aliases, '#each' do
subject { object.each { |field, aliased_field| yields[field] = aliased_field } }
let(:yields) { {} }
before do
object.should be_instance_of(described_class)
end
context 'with a block' do
context "using Aliases::Strategy::NaturalJoin" do
let(:strategy) { described_class::Strategy::NaturalJoin }
context "when no join has been performed" do
let(:object) { described_class.new(songs_index) }
let(:songs_index) { described_class::Index.new(songs_entries, strategy) }
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to_not change { yields.dup }
end
end
context "when a join has been performed" do
let(:object) { songs.join(song_tags, join_definition) }
let(:songs) { described_class.new(songs_index) }
let(:song_tags) { described_class.new(song_tags_index) }
let(:songs_index) { described_class::Index.new(songs_entries, strategy) }
let(:song_tags_index) { described_class::Index.new(song_tags_entries, strategy) }
let(:join_definition) {{
:id => :song_id
}}
context "with unique attribute names across both relations" do
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
}}
let(:song_tags_entries) {{
:song_tags_song_id => :song_id,
:song_tags_tag_id => :tag_id,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to change { yields.dup }.
from({}).
to(:id => :song_id)
end
end
context "and the left join key has been renamed before already" do
let(:object) { songs_X_song_tags.join(song_comments, other_join_definition) }
let(:other_join_definition) {{
:song_id => :song_id
}}
let(:songs_X_song_tags) { songs.join(song_tags, join_definition) }
let(:song_comments) { described_class.new(song_comments_index) }
let(:song_comments_index) { described_class::Index.new(song_comments_entries, strategy) }
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
}}
let(:song_tags_entries) {{
:song_tags_song_id => :song_id,
:song_tags_tag_id => :tag_id,
}}
let(:song_comments_entries) {{
:song_comments_song_id => :song_id,
:song_comments_comment_id => :comment_id,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to_not change { yields.dup }
end
end
context "with clashing attribute names" do
context "only before renaming join keys" do
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
}}
let(:song_tags_entries) {{
:song_tags_id => :id,
:song_tags_song_id => :song_id,
:song_tags_tag_id => :tag_id,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to change { yields.dup }.
from({}).
to(:id => :song_id)
end
end
context "before and after renaming join keys" do
context "and the clashing attribute is not part of the join keys" do
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
:songs_created_at => :created_at
}}
let(:song_tags_entries) {{
:song_tags_song_id => :song_id,
:song_tags_tag_id => :tag_id,
:song_tags_created_at => :created_at,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to change { yields.dup }.
from({}).
to(
:id => :song_id,
:created_at => :songs_created_at
)
end
end
context "and the clashing attribute matches a join key" do
let(:songs_entries) {{
:songs_id => :id,
:songs_title => :title,
:songs_song_id => :song_id,
}}
let(:song_tags_entries) {{
:song_tags_id => :id,
:song_tags_song_id => :song_id,
:song_tags_tag_id => :tag_id,
}}
it_should_behave_like 'an #each method'
it 'yields correct aliases' do
expect { subject }.to change { yields.dup }.
from({}).
to(
:id => :song_id,
:song_id => :songs_song_id
)
end
end
end
end
end
end
end
end
describe DataMapper::Relation::Graph::Node::Aliases do
subject { object.new(index) }
let(:object) { described_class }
let(:index) { mock('index', :header => mock) }
before do
subject.should be_instance_of(object)
end
it { should be_kind_of(Enumerable) }
it 'case matches Enumerable' do
(Enumerable === subject).should be(true)
end
end
@dkubb
Copy link

dkubb commented Dec 11, 2012

@snusnu can Index#keys be written as:

def keys(name)
  entries.each_with_object([]) do |(key, value), keys|
    keys << key if value == name
  end
end

It's not exactly elegant, but it creates less temporary objects than the original.

@dkubb
Copy link

dkubb commented Dec 11, 2012

@snusnu I think you can remove Index#update_name and Index#keys and replace them with:

def initialize(entries, strategy)
  @entries  = entries
  @inverted = entries.invert
  @strategy = strategy.new(self)
end

# ...

def renamed_entries(aliases)
  aliases.each_with_object(@entries.dup) do |(from, to), renamed_entries|
    renamed_entries[@inverted.fetch(from)] = to
  end
end

@dkubb
Copy link

dkubb commented Dec 11, 2012

@snusnu here's my current best attempt at refactoring/simplification: https://gist.github.com/4256126

@snusnu
Copy link
Author

snusnu commented Dec 11, 2012

@dkubb i've updated the gist to match your latest code. i also use attr_reader's now when feasible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment