Skip to content

Commit

Permalink
new method includes_filtered
Browse files Browse the repository at this point in the history
  • Loading branch information
subvertallchris committed Jan 29, 2015
1 parent fd4438e commit 8405b25
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 26 deletions.
1 change: 1 addition & 0 deletions lib/neo4j.rb
Expand Up @@ -64,6 +64,7 @@
require 'neo4j/active_node/has_n/association'
require 'neo4j/active_node/query/query_proxy'
require 'neo4j/active_node/query/query_proxy_preloader'
require 'neo4j/active_node/query/query_proxy_filtered_preloader'
require 'neo4j/active_node/query'
require 'neo4j/active_node/scope'
require 'neo4j/active_node'
Expand Down
12 changes: 12 additions & 0 deletions lib/neo4j/active_node/query/query_proxy_filtered_preloader.rb
@@ -0,0 +1,12 @@
module Neo4j
module ActiveNode
module Query
class QueryProxyFilteredPreloader < QueryProxyPreloader

def method_missing(method_name, *args, &block)
caller.send(method_name, *args, &block)
end
end
end
end
end
23 changes: 17 additions & 6 deletions lib/neo4j/active_node/query/query_proxy_methods.rb
Expand Up @@ -124,16 +124,27 @@ def optional(association, node_id = nil)
self.query.proxy_as(model, var, true)
end

def includes(association_name, given_parent_id = nil, given_rel_id = nil, given_child_id = nil)
starting_id = given_parent_id || identity
child_id = given_child_id || :"#{identity}_child"
query.proxy_as_optional(model, starting_id).send(association_name, child_id, given_rel_id).prepopulate(association_name, starting_id, child_id)
def includes(association_name, given_child_id = nil, given_rel_id = nil)
data = { association: association_name, rel_id: given_rel_id, child_id: given_child_id }
includes_builder(false, data)
end

def includes_filtered(association_name, given_child_id = nil, given_rel_id = nil)
data = { association: association_name, rel_id: given_rel_id, child_id: given_child_id }
includes_builder(true, data)
end

protected

def prepopulate(association_name, target_identifier, child_identifier)
@preloader ||= Neo4j::ActiveNode::Query::QueryProxyPreloader.new(self, target_identifier, child_identifier)
def includes_builder(filtered, params)
starting_id = identity
child_id = params[:child_id] || :"#{identity}_child"
query.proxy_as_optional(model, starting_id).send(params[:association], child_id, params[:rel_id]).prepopulate(filtered, params[:association], starting_id, child_id)
end

def prepopulate(filtered, association_name, target_identifier, child_identifier)
preloader_class = filtered ? Neo4j::ActiveNode::Query::QueryProxyFilteredPreloader : Neo4j::ActiveNode::Query::QueryProxyPreloader
@preloader ||= preloader_class.new(self, target_identifier, child_identifier)
preloader.queue(association_name)
end

Expand Down
5 changes: 1 addition & 4 deletions lib/neo4j/active_node/query/query_proxy_preloader.rb
Expand Up @@ -3,6 +3,7 @@ module ActiveNode
module Query
class QueryProxyPreloader
attr_reader :queued_methods, :caller, :target_id, :child_id
delegate :each, :each_with_rel, :each_rel, :to_a, :first, :last, :to_cypher, to: :caller

def initialize(query_proxy, target_id, child_id)
@caller = query_proxy
Expand All @@ -22,10 +23,6 @@ def replay(returned_node, child)
cypher_string = @chained_node.to_cypher_with_params([@chained_node.identity])
returned_node.association_instance_set(cypher_string, child, returned_node.class.associations[queued_methods.keys.first])
end

def method_missing(method_name, *args, &block)
caller.send(method_name, *args, &block)
end
end
end
end
Expand Down
71 changes: 55 additions & 16 deletions spec/e2e/association_includes_spec.rb
Expand Up @@ -3,7 +3,7 @@
# The `includes` method works by preloading a node's association cache with the results of a query.
# In these tests, we demonstrate that the association cache is prepopulated with the expected results.
# We do not test the association cache itself here because it has its own tests and we trust they are working properly.
describe 'association .includes method' do
describe 'association inclusion' do
class AISBand
include Neo4j::ActiveNode
property :name
Expand Down Expand Up @@ -40,24 +40,53 @@ class AISInstrument

after { [AISBand, AISMember, AISInstrument].each(&:delete_all) }

it 'preloads the association cache' do
band = AISBand.where(name: 'Tool').includes(:members).to_a.first
expect(band.association_cache[:members]).not_to be_empty
expect(band.association_cache[:members].values.first).to include(maynard, adam, danny)
describe 'preloading association by name' do
context '.includes' do
it 'preloads the association cache' do
band = AISBand.where(name: 'Tool').includes(:members).to_a.first
expect(band.association_cache[:members]).not_to be_empty
expect(band.association_cache[:members].values.first).to include(maynard, adam, danny)
end
end

context '.includes_filtered' do
it 'preloads the association cache' do
band = AISBand.where(name: 'Tool').includes_filtered(:members).to_a.first
expect(band.association_cache[:members]).not_to be_empty
expect(band.association_cache[:members].values.first).to include(maynard, adam, danny)
end
end
end

# This is the only difference between `includes` and `includes_filtered`
describe 'filtering included association' do
context 'includes' do
it 'cannot filter matches' do
expect { AISBand.where(name: 'Tool').includes(:members).where(name: 'Maynard') }.to raise_error NoMethodError
end
end

context '.includes_filtered' do
it 'can filter the match' do
band = AISBand.where(name: 'Tool').includes_filtered(:members).where(name: 'Maynard').to_a.first
expect(band.association_cache[:members].values.first.count).to eq 1
expect(band.association_cache[:members].values.first.first).to eq(maynard)
end
end
end

it 'can filter the match' do
band = AISBand.where(name: 'Tool').includes(:members).where(name: 'Maynard').to_a.first
expect(band.association_cache[:members].values.first.count).to eq 1
expect(band.association_cache[:members].values.first.first).to eq(maynard)
it 'accepts an id representing the child node' do
q = AISBand.as(:a).where(name: 'Tool').includes(:members, :b)
expect(q.to_cypher).to include 'OPTIONAL MATCH (a:`AISBand`), (b:`AISMember`)'
end

it 'accepts a symbol for node_id'
it 'accepts a symbol for child_id'
it 'accepts a symbol for the rel between the node and child'
it 'accepts a symbol for the rel between the parent and child' do
q = AISBand.as(:a).where(name: 'Tool').includes(:members, nil, :included_foo_rel)
expect(q.to_cypher).to include '-[included_foo_rel:`MEMBERS`]->'
end

it 'works on instances' do
members = tool.members.includes(:instruments).to_a
members = tool.members.includes_filtered(:instruments).to_a
expect(members.first.association_cache[:instruments]).not_to be_empty
members.each do |member|
case member
Expand All @@ -75,14 +104,24 @@ class AISInstrument

describe 'first' do
it 'returns the first match and populates its association cache' do
result = tool.members.where(name: 'Maynard').includes(:instruments).where(name: 'Vocals').first
result = tool.members.where(name: 'Maynard').includes(:instruments).first
expect(result).to eq maynard
# require 'pry'; binding.pry
# This might seem odd but I'm tired and it's a way to get to the contents of the association cache.
# TODO: Fix it. In the meantime...
# association_cache = { instruments: { long_cypher_integer: [preloaded_node(s)] }}]
# association_cache = { instruments: { long_cypher_integer: [preloaded_nodes] }}]
# In this case, the preloaded node == vocals
expect(result.association_cache[:instruments].first.last.first).to eq vocals
end
end

# describe 'each_with_rel' do
# it 'preloads rels' do
# result = tool.members.where(name: 'Maynard').includes(:instruments).each_with_rel do |member, rel|
# expect(member).to be_a(AISMember)
# expect(rel).to be_a(Neo4j::Server::CypherRelationship)
# end
# # require 'pry'; binding.pry
# # result
# end
# end
end
Empty file.

0 comments on commit 8405b25

Please sign in to comment.