From 2f5cc9b4b264103f0bad5e9d99fd388de854de7d Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Thu, 5 Nov 2015 15:47:22 -0500 Subject: [PATCH 1/7] FilteredProperties, a class for filtering hashes --- lib/neo4j.rb | 1 + .../shared/property/filtered_properties.rb | 78 +++++++++++++++++++ .../property/filtered_properties_spec.rb | 72 +++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 lib/neo4j/shared/property/filtered_properties.rb create mode 100644 spec/unit/shared/property/filtered_properties_spec.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index 7714f1719..97d96a0de 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -35,6 +35,7 @@ require 'neo4j/shared/declared_property' require 'neo4j/shared/declared_properties' require 'neo4j/shared/property' +require 'neo4j/shared/property/filtered_properties' require 'neo4j/shared/persistence' require 'neo4j/shared/validations' require 'neo4j/shared/identity' diff --git a/lib/neo4j/shared/property/filtered_properties.rb b/lib/neo4j/shared/property/filtered_properties.rb new file mode 100644 index 000000000..bdfcef8b3 --- /dev/null +++ b/lib/neo4j/shared/property/filtered_properties.rb @@ -0,0 +1,78 @@ +module Neo4j::Shared::Property + class FilteredProperties + class InvalidPropertyFilterType < Neo4j::Neo4jrbError; end + VALID_SYMBOL_INSTRUCTIONS = [:all, :none] + VALID_HASH_INSTRUCTIONS = [:on, :except] + + attr_reader :properties, :instructions, :instructions_type + + def initialize(properties, instructions) + @properties = properties + @instructions = instructions + @instructions_type = instructions.class + validate_instructions!(instructions) + end + + def filtered_properties + case instructions + when Symbol + filtered_properties_by_symbol + when Hash + filtered_properties_by_hash + end + end + + private + + def filtered_properties_by_symbol + case instructions + when :all + [properties, {}] + when :none + [{}, properties] + end + end + + def filtered_properties_by_hash + behavior_key = instructions.keys.first + filter_keys = keys_array(behavior_key) + base = [filter(filter_keys, :with), filter(filter_keys, :without)] + behavior_key == :on ? base : base.reverse + end + + def key?(filter_keys, key) + filter_keys.include?(key) + end + + def filter(filter_keys, key) + filtering = key == :with + properties.select { |k, _v| key?(filter_keys, k) == filtering } + end + + def keys_array(key) + instructions[key].is_a?(Array) ? instructions[key] : [instructions[key]] + end + + def validate_instructions!(instructions) + clazz = instructions_type.name.downcase + return if send(:"valid_#{clazz}_instructions?", instructions) + fail InvalidPropertyFilterType, "Invalid instructions #{instructions}, valid options for #{clazz}: #{send(:"valid_#{clazz}_instructions")}" + end + + def valid_symbol_instructions?(instructions) + valid_symbol_instructions.include?(instructions) + end + + def valid_hash_instructions?(instructions) + valid_hash_instructions.include?(instructions.keys.first) + end + + def valid_symbol_instructions + VALID_SYMBOL_INSTRUCTIONS + end + + def valid_hash_instructions + VALID_HASH_INSTRUCTIONS + end + end +end diff --git a/spec/unit/shared/property/filtered_properties_spec.rb b/spec/unit/shared/property/filtered_properties_spec.rb new file mode 100644 index 000000000..1900789fa --- /dev/null +++ b/spec/unit/shared/property/filtered_properties_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +module Neo4j::Shared::Property + describe FilteredProperties do + let(:properties) { {first: :foo, second: :bar, third: :baz, fourth: :buzz} } + let(:instructions) { :all } + let(:filtered_props) { described_class.new(properties, instructions) } + + describe '#initialize' do + it 'takes a hash of properties and an instructions argument' do + expect { filtered_props }.not_to raise_error + end + end + + describe 'accessors' do + subject { described_class.new(properties, instructions) } + + it { expect(subject.properties).to eq properties } + it { expect(subject.instructions).to eq instructions } + end + + describe 'instructions' do + describe 'symbols' do + it 'raise unless :all or :none' do + expect { FilteredProperties.new(properties, :all) }.not_to raise_error + expect { FilteredProperties.new(properties, :none) }.not_to raise_error + expect { FilteredProperties.new(properties, :foo) }.to raise_error FilteredProperties::InvalidPropertyFilterType + end + + describe 'filtering' do + context ':all' do + let(:instructions) { :all } + it 'returns [original_hash, empty_hash]' do + expect(filtered_props.filtered_properties).to eq([properties, {}]) + end + end + + context ':none' do + let(:instructions) { :none } + it 'returns [empty_hash, original_hash]' do + expect(filtered_props.filtered_properties).to eq([{}, properties]) + end + end + end + end + + describe 'hash' do + it 'raises unless first key is :on or :except' do + expect { FilteredProperties.new(properties, on: :foo) }.not_to raise_error + expect { FilteredProperties.new(properties, except: :foo) }.not_to raise_error + expect { FilteredProperties.new(properties, foo: :foo) }.to raise_error FilteredProperties::InvalidPropertyFilterType + end + + describe 'filtering' do + context 'on:' do + let(:instructions) { {on: [:second, :fourth]} } + it 'returns [hash with keys specified, hash with remaining key' do + expect(filtered_props.filtered_properties).to eq([{second: :bar, fourth: :buzz}, {first: :foo, third: :baz}]) + end + end + + context 'except:' do + let(:instructions) { {except: [:second, :fourth]} } + it 'returns [hash without keys specified, hash with keys specified' do + expect(filtered_props.filtered_properties).to eq([{first: :foo, third: :baz}, {second: :bar, fourth: :buzz}]) + end + end + end + end + end + end +end From da6df0293e71481dc2cc7d319044f0fc6571a459 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Thu, 5 Nov 2015 16:31:55 -0500 Subject: [PATCH 2/7] ActiveRel#creates_unique demands an argument to set behavior --- lib/neo4j/active_rel/property.rb | 10 ++-- lib/neo4j/shared/query_factory.rb | 12 ++++- spec/e2e/active_rel/unpersisted_spec.rb | 16 +++++++ spec/e2e/active_rel_spec.rb | 63 ++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/lib/neo4j/active_rel/property.rb b/lib/neo4j/active_rel/property.rb index 9e6a21df6..cf7d02c2b 100644 --- a/lib/neo4j/active_rel/property.rb +++ b/lib/neo4j/active_rel/property.rb @@ -63,8 +63,12 @@ def load_entity(id) Neo4j::Node.load(id) end - def creates_unique - @creates_unique = true + def creates_unique(option) + @creates_unique = option + end + + def creates_unique_option + @creates_unique || :none end def creates_unique_rel @@ -75,7 +79,7 @@ def creates_unique_rel ActiveSupport::Deprecation.warn(warning, caller) - creates_unique + creates_unique(:none) end def creates_unique? diff --git a/lib/neo4j/shared/query_factory.rb b/lib/neo4j/shared/query_factory.rb index 8377b8488..62aa12090 100644 --- a/lib/neo4j/shared/query_factory.rb +++ b/lib/neo4j/shared/query_factory.rb @@ -79,13 +79,21 @@ def match_string def create_query return match_query if graph_object.persisted? - base_query.send(graph_object.create_method, query_string).params(identifier_params.to_sym => graph_object.props_for_create) + create_props, set_props = filtered_props + + base_query.send(graph_object.create_method, query_string).break + .set(identifier => set_props) + .params(:"#{identifier}_create_props" => create_props) end private + def filtered_props + Neo4j::Shared::Property::FilteredProperties.new(graph_object.props_for_create, graph_object.class.creates_unique_option).filtered_properties + end + def query_string - "#{graph_object.from_node_identifier}-[#{identifier}:#{graph_object.type} {#{identifier_params}}]->#{graph_object.to_node_identifier}" + "#{graph_object.from_node_identifier}-[#{identifier}:#{graph_object.type} {#{identifier}_create_props}]->#{graph_object.to_node_identifier}" end end end diff --git a/spec/e2e/active_rel/unpersisted_spec.rb b/spec/e2e/active_rel/unpersisted_spec.rb index 7abec15d6..b30806659 100644 --- a/spec/e2e/active_rel/unpersisted_spec.rb +++ b/spec/e2e/active_rel/unpersisted_spec.rb @@ -12,6 +12,7 @@ stub_active_node_class('FromClass') do before_create :log_before after_create :log_after + property :name property :created_at, type: Integer property :updated_at, type: Integer property :before_run, type: ActiveAttr::Typecasting::Boolean @@ -96,6 +97,21 @@ def log_after expect { rel.save }.to change { [from_node, to_node].all?(&:persisted?) }.from(false).to true expect(from_node.uuid).not_to be_nil end + + context 'with creates_unique set' do + before { MyRelClass.creates_unique(:none) } + + it 'will create duplicate nodes' do + from_node.name = 'Chris' + from_node.save + expect { MyRelClass.create(FromClass.new(name: 'Chris'), ToClass.new) }.to change { FromClass.count }.by(1) + end + + it 'will not create duplicate rels' do + expect { MyRelClass.create(from_node, to_node) }.to change { from_node.others.count }.by(1) + expect { MyRelClass.create(from_node, to_node) }.not_to change { from_node.others.count } + end + end end end diff --git a/spec/e2e/active_rel_spec.rb b/spec/e2e/active_rel_spec.rb index 74315b523..2c7e0f843 100644 --- a/spec/e2e/active_rel_spec.rb +++ b/spec/e2e/active_rel_spec.rb @@ -164,10 +164,71 @@ def is_persisted_with_nodes(rel) expect(from_node.others.count).to eq 0 MyRelClass.create(from_node: from_node, to_node: to_node) expect(from_node.others.count).to eq 1 - MyRelClass.creates_unique + MyRelClass.creates_unique :none MyRelClass.create(from_node: from_node, to_node: to_node) expect(from_node.others.count).to eq 1 end + + describe 'property filtering' do + let(:nodes) { {from_node: from_node, to_node: to_node} } + let(:first_props) { {score: 900} } + let(:second_props) { {score: 1000} } + let(:changed_props_create) { proc { MyRelClass.create(nodes.merge(second_props)) } } + before do + MyRelClass.creates_unique(:none) + MyRelClass.create(nodes.merge(first_props)) + end + + context 'with :none open' do + it 'does not create additional rels, even when properties change' do + expect do + changed_props_create.call + end.not_to change { from_node.others.count } + end + end + + context 'with `:all` option' do + before { MyRelClass.creates_unique :all } + + it 'creates additional rels when properties change' do + expect { changed_props_create.call }.to change { from_node.others.count } + end + end + + context 'with {on: *keys} option' do + before { MyRelClass.creates_unique(on: :score) } + + context 'and a listed property changes' do + it 'creates a new rel' do + expect { changed_props_create.call }.to change { from_node.others.count } + end + end + + context 'and an unlisted property changes' do + it 'does not create a new rel' do + expect do + MyRelClass.create(nodes.merge(default: 'some other value')) + end.not_to change { from_node.others.count } + end + end + end + + context 'with {except: *keys} option' do + before { MyRelClass.creates_unique(except: :score) } + + context 'and a listed property changes' do + it 'does not create a new rel' do + expect { changed_props_create.call }.not_to change { from_node.others.count } + end + end + + context 'and an unlisted property changes' do + it 'creates a new rel' do + expect { MyRelClass.create(nodes.merge(default: 'some other value')) }.to change { from_node.others.count } + end + end + end + end end describe 'type checking' do From 87b89603836bccc7f3701ac05b9b35315ebfbb58 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Fri, 6 Nov 2015 11:20:20 -0500 Subject: [PATCH 3/7] We are filtering hashes, not properties, so name should match --- lib/neo4j.rb | 2 +- ...iltered_properties.rb => filtered_hash.rb} | 30 ++++++++--------- lib/neo4j/shared/query_factory.rb | 2 +- ...operties_spec.rb => filtered_hash_spec.rb} | 32 +++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) rename lib/neo4j/shared/{property/filtered_properties.rb => filtered_hash.rb} (66%) rename spec/unit/shared/{property/filtered_properties_spec.rb => filtered_hash_spec.rb} (50%) diff --git a/lib/neo4j.rb b/lib/neo4j.rb index 97d96a0de..67bcc59c4 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -31,11 +31,11 @@ require 'neo4j/timestamps' require 'neo4j/shared/callbacks' +require 'neo4j/shared/filtered_hash' require 'neo4j/shared/declared_property/index' require 'neo4j/shared/declared_property' require 'neo4j/shared/declared_properties' require 'neo4j/shared/property' -require 'neo4j/shared/property/filtered_properties' require 'neo4j/shared/persistence' require 'neo4j/shared/validations' require 'neo4j/shared/identity' diff --git a/lib/neo4j/shared/property/filtered_properties.rb b/lib/neo4j/shared/filtered_hash.rb similarity index 66% rename from lib/neo4j/shared/property/filtered_properties.rb rename to lib/neo4j/shared/filtered_hash.rb index bdfcef8b3..386a39c77 100644 --- a/lib/neo4j/shared/property/filtered_properties.rb +++ b/lib/neo4j/shared/filtered_hash.rb @@ -1,39 +1,39 @@ -module Neo4j::Shared::Property - class FilteredProperties - class InvalidPropertyFilterType < Neo4j::Neo4jrbError; end +module Neo4j::Shared + class FilteredHash + class InvalidHashFilterType < Neo4j::Neo4jrbError; end VALID_SYMBOL_INSTRUCTIONS = [:all, :none] VALID_HASH_INSTRUCTIONS = [:on, :except] - attr_reader :properties, :instructions, :instructions_type + attr_reader :base, :instructions, :instructions_type - def initialize(properties, instructions) - @properties = properties + def initialize(base, instructions) + @base = base @instructions = instructions @instructions_type = instructions.class validate_instructions!(instructions) end - def filtered_properties + def filtered_base case instructions when Symbol - filtered_properties_by_symbol + filtered_base_by_symbol when Hash - filtered_properties_by_hash + filtered_base_by_hash end end private - def filtered_properties_by_symbol + def filtered_base_by_symbol case instructions when :all - [properties, {}] + [base, {}] when :none - [{}, properties] + [{}, base] end end - def filtered_properties_by_hash + def filtered_base_by_hash behavior_key = instructions.keys.first filter_keys = keys_array(behavior_key) base = [filter(filter_keys, :with), filter(filter_keys, :without)] @@ -46,7 +46,7 @@ def key?(filter_keys, key) def filter(filter_keys, key) filtering = key == :with - properties.select { |k, _v| key?(filter_keys, k) == filtering } + base.select { |k, _v| key?(filter_keys, k) == filtering } end def keys_array(key) @@ -56,7 +56,7 @@ def keys_array(key) def validate_instructions!(instructions) clazz = instructions_type.name.downcase return if send(:"valid_#{clazz}_instructions?", instructions) - fail InvalidPropertyFilterType, "Invalid instructions #{instructions}, valid options for #{clazz}: #{send(:"valid_#{clazz}_instructions")}" + fail InvalidHashFilterType, "Invalid instructions #{instructions}, valid options for #{clazz}: #{send(:"valid_#{clazz}_instructions")}" end def valid_symbol_instructions?(instructions) diff --git a/lib/neo4j/shared/query_factory.rb b/lib/neo4j/shared/query_factory.rb index 62aa12090..cb06609a5 100644 --- a/lib/neo4j/shared/query_factory.rb +++ b/lib/neo4j/shared/query_factory.rb @@ -89,7 +89,7 @@ def create_query private def filtered_props - Neo4j::Shared::Property::FilteredProperties.new(graph_object.props_for_create, graph_object.class.creates_unique_option).filtered_properties + Neo4j::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.class.creates_unique_option).filtered_base end def query_string diff --git a/spec/unit/shared/property/filtered_properties_spec.rb b/spec/unit/shared/filtered_hash_spec.rb similarity index 50% rename from spec/unit/shared/property/filtered_properties_spec.rb rename to spec/unit/shared/filtered_hash_spec.rb index 1900789fa..320e3216e 100644 --- a/spec/unit/shared/property/filtered_properties_spec.rb +++ b/spec/unit/shared/filtered_hash_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -module Neo4j::Shared::Property - describe FilteredProperties do - let(:properties) { {first: :foo, second: :bar, third: :baz, fourth: :buzz} } +module Neo4j::Shared + describe FilteredHash do + let(:base) { {first: :foo, second: :bar, third: :baz, fourth: :buzz} } let(:instructions) { :all } - let(:filtered_props) { described_class.new(properties, instructions) } + let(:filtered_props) { described_class.new(base, instructions) } describe '#initialize' do it 'takes a hash of properties and an instructions argument' do @@ -13,32 +13,32 @@ module Neo4j::Shared::Property end describe 'accessors' do - subject { described_class.new(properties, instructions) } + subject { described_class.new(base, instructions) } - it { expect(subject.properties).to eq properties } + it { expect(subject.base).to eq base } it { expect(subject.instructions).to eq instructions } end describe 'instructions' do describe 'symbols' do it 'raise unless :all or :none' do - expect { FilteredProperties.new(properties, :all) }.not_to raise_error - expect { FilteredProperties.new(properties, :none) }.not_to raise_error - expect { FilteredProperties.new(properties, :foo) }.to raise_error FilteredProperties::InvalidPropertyFilterType + expect { FilteredHash.new(base, :all) }.not_to raise_error + expect { FilteredHash.new(base, :none) }.not_to raise_error + expect { FilteredHash.new(base, :foo) }.to raise_error FilteredHash::InvalidHashFilterType end describe 'filtering' do context ':all' do let(:instructions) { :all } it 'returns [original_hash, empty_hash]' do - expect(filtered_props.filtered_properties).to eq([properties, {}]) + expect(filtered_props.filtered_base).to eq([base, {}]) end end context ':none' do let(:instructions) { :none } it 'returns [empty_hash, original_hash]' do - expect(filtered_props.filtered_properties).to eq([{}, properties]) + expect(filtered_props.filtered_base).to eq([{}, base]) end end end @@ -46,23 +46,23 @@ module Neo4j::Shared::Property describe 'hash' do it 'raises unless first key is :on or :except' do - expect { FilteredProperties.new(properties, on: :foo) }.not_to raise_error - expect { FilteredProperties.new(properties, except: :foo) }.not_to raise_error - expect { FilteredProperties.new(properties, foo: :foo) }.to raise_error FilteredProperties::InvalidPropertyFilterType + expect { FilteredHash.new(base, on: :foo) }.not_to raise_error + expect { FilteredHash.new(base, except: :foo) }.not_to raise_error + expect { FilteredHash.new(base, foo: :foo) }.to raise_error FilteredHash::InvalidHashFilterType end describe 'filtering' do context 'on:' do let(:instructions) { {on: [:second, :fourth]} } it 'returns [hash with keys specified, hash with remaining key' do - expect(filtered_props.filtered_properties).to eq([{second: :bar, fourth: :buzz}, {first: :foo, third: :baz}]) + expect(filtered_props.filtered_base).to eq([{second: :bar, fourth: :buzz}, {first: :foo, third: :baz}]) end end context 'except:' do let(:instructions) { {except: [:second, :fourth]} } it 'returns [hash without keys specified, hash with keys specified' do - expect(filtered_props.filtered_properties).to eq([{first: :foo, third: :baz}, {second: :bar, fourth: :buzz}]) + expect(filtered_props.filtered_base).to eq([{first: :foo, third: :baz}, {second: :bar, fourth: :buzz}]) end end end From 4e592f90a7dbba36f41bd1b7763224c9bffd9fca Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Sat, 7 Nov 2015 18:12:58 -0500 Subject: [PATCH 4/7] QueryProxy#<< uses RelQueryFactory, same unique: options as ActiveRel --- lib/neo4j.rb | 2 + lib/neo4j/active_node/has_n/association.rb | 4 + .../has_n/association/rel_wrapper.rb | 23 ++++++ lib/neo4j/active_node/query/query_proxy.rb | 37 ++++++--- lib/neo4j/active_rel/persistence.rb | 11 ++- .../active_rel/persistence/query_factory.rb | 2 +- lib/neo4j/active_rel/property.rb | 30 ++----- lib/neo4j/shared/cypher.rb | 36 +++++++++ lib/neo4j/shared/filtered_hash.rb | 2 + lib/neo4j/shared/query_factory.rb | 3 +- spec/e2e/active_rel_spec.rb | 4 +- spec/e2e/has_many_spec.rb | 4 +- spec/e2e/query_spec.rb | 66 ++++++++++++++++ .../has_n/association/rel_wrapper_spec.rb | 78 +++++++++++++++++++ 14 files changed, 260 insertions(+), 42 deletions(-) create mode 100644 lib/neo4j/active_node/has_n/association/rel_wrapper.rb create mode 100644 lib/neo4j/shared/cypher.rb create mode 100644 spec/unit/active_node/has_n/association/rel_wrapper_spec.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index 67bcc59c4..a2ad47411 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -43,6 +43,7 @@ require 'neo4j/shared/typecaster' require 'neo4j/shared/initialize' require 'neo4j/shared/query_factory' +require 'neo4j/shared/cypher' require 'neo4j/shared' require 'neo4j/active_rel/callbacks' @@ -81,6 +82,7 @@ require 'neo4j/active_node/unpersisted' require 'neo4j/active_node/has_n' require 'neo4j/active_node/has_n/association_cypher_methods' +require 'neo4j/active_node/has_n/association/rel_wrapper' require 'neo4j/active_node/has_n/association' require 'neo4j/active_node/query/query_proxy' require 'neo4j/active_node/query' diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index e099562f5..51ffe995e 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -133,6 +133,10 @@ def unique? @origin ? origin_association.unique? : !!@unique end + def creates_unique_option + @unique || :none + end + def create_method unique? ? :create_unique : :create end diff --git a/lib/neo4j/active_node/has_n/association/rel_wrapper.rb b/lib/neo4j/active_node/has_n/association/rel_wrapper.rb new file mode 100644 index 000000000..c6ae1ffff --- /dev/null +++ b/lib/neo4j/active_node/has_n/association/rel_wrapper.rb @@ -0,0 +1,23 @@ +class Neo4j::ActiveNode::HasN::Association + # Provides the interface needed to interact with the ActiveRel query factory. + class RelWrapper + include Neo4j::Shared::Cypher::RelIdentifiers + include Neo4j::Shared::Cypher::CreateMethod + + attr_reader :type, :association + attr_accessor :properties + private :association + alias_method :props_for_create, :properties + + def initialize(association, properties = {}) + @association = association + @properties = properties + @type = association.relationship_type(true) + creates_unique(association.creates_unique_option) if association.unique? + end + + def persisted? + false + end + end +end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 6ffeb648c..8d6d0450e 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -206,23 +206,42 @@ def _nodeify!(*args) end def _create_relationship(other_node_or_nodes, properties) - if association.relationship_class - _create_relationship_with_rel_class(other_node_or_nodes, properties) - else - _session.query(context: @options[:context]) - .match(:start, :end).match_nodes(start: @start_object, end: other_node_or_nodes) - .send(association.create_method, "start#{_association_arrow(properties, true)}end").exec - end + creator = association.relationship_class ? :rel_class : :factory + send(:"_create_relationship_with_#{creator}", other_node_or_nodes, properties) end def _create_relationship_with_rel_class(other_node_or_nodes, properties) Array(other_node_or_nodes).each do |other_node| - node_props = (association.direction == :in) ? {from_node: other_node, to_node: @start_object} : {from_node: @start_object, to_node: other_node} - + node_props = _nodes_for_create(other_node, :from_node, :to_node) association.relationship_class.create(properties.merge(node_props)) end end + def _create_relationship_with_factory(other_node_or_nodes, properties) + Array(other_node_or_nodes).each do |other_node| + wrapper = _rel_wrapper(properties) + base = _match_query(other_node, wrapper) + factory = Neo4j::Shared::RelQueryFactory.new(wrapper, wrapper.rel_identifier) + factory.base_query = base + factory.query.exec + end + end + + def _match_query(other_node, wrapper) + nodes = _nodes_for_create(other_node, wrapper.from_node_identifier, wrapper.to_node_identifier) + Neo4j::Session.current.query.match_nodes(nodes) + end + + def _nodes_for_create(other_node, from_node_id, to_node_id) + nodes = [@start_object, other_node] + nodes.reverse! if association.direction == :in + {from_node_id => nodes[0], to_node_id => nodes[1]} + end + + def _rel_wrapper(properties) + Neo4j::ActiveNode::HasN::Association::RelWrapper.new(association, properties) + end + def read_attribute_for_serialization(*args) to_a.map { |o| o.read_attribute_for_serialization(*args) } end diff --git a/lib/neo4j/active_rel/persistence.rb b/lib/neo4j/active_rel/persistence.rb index e8838b841..6fa5b6f86 100644 --- a/lib/neo4j/active_rel/persistence.rb +++ b/lib/neo4j/active_rel/persistence.rb @@ -1,10 +1,9 @@ module Neo4j::ActiveRel module Persistence extend ActiveSupport::Concern + include Neo4j::Shared::Cypher::RelIdentifiers include Neo4j::Shared::Persistence - attr_writer :from_node_identifier, :to_node_identifier - class RelInvalidError < RuntimeError; end class ModelClassInvalidError < RuntimeError; end class RelCreateFailedError < RuntimeError; end @@ -17,6 +16,14 @@ def to_node_identifier @to_node_identifier || :to_node end + def from_node_identifier=(id) + @from_node_identifier = id.to_sym + end + + def to_node_identifier=(id) + @to_node_identifier = id.to_sym + end + def cypher_identifier @cypher_identifier || :rel end diff --git a/lib/neo4j/active_rel/persistence/query_factory.rb b/lib/neo4j/active_rel/persistence/query_factory.rb index ea4a01287..0e564e387 100644 --- a/lib/neo4j/active_rel/persistence/query_factory.rb +++ b/lib/neo4j/active_rel/persistence/query_factory.rb @@ -29,7 +29,7 @@ def build! private def rel_id - @rel_id ||= rel.cypher_identifier + @rel_id ||= rel.rel_identifier end # Node callbacks only need to be executed if the node is not persisted. We let the `conditional_callback` method do the work, diff --git a/lib/neo4j/active_rel/property.rb b/lib/neo4j/active_rel/property.rb index cf7d02c2b..103f691c3 100644 --- a/lib/neo4j/active_rel/property.rb +++ b/lib/neo4j/active_rel/property.rb @@ -31,7 +31,13 @@ def initialize(attributes = nil) send_props(@relationship_props) unless @relationship_props.nil? end + def creates_unique_option + self.class.creates_unique_option + end + module ClassMethods + include Neo4j::Shared::Cypher::CreateMethod + # Extracts keys from attributes hash which are relationships of the model # TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save? def extract_association_attributes!(attributes) @@ -62,30 +68,6 @@ def id_property_name def load_entity(id) Neo4j::Node.load(id) end - - def creates_unique(option) - @creates_unique = option - end - - def creates_unique_option - @creates_unique || :none - end - - def creates_unique_rel - warning = <<-WARNING -creates_unique_rel() is deprecated and will be removed from future releases, -use creates_unique() instead. -WARNING - - ActiveSupport::Deprecation.warn(warning, caller) - - creates_unique(:none) - end - - def creates_unique? - !!@creates_unique - end - alias_method :unique?, :creates_unique? end private diff --git a/lib/neo4j/shared/cypher.rb b/lib/neo4j/shared/cypher.rb new file mode 100644 index 000000000..91e875f8e --- /dev/null +++ b/lib/neo4j/shared/cypher.rb @@ -0,0 +1,36 @@ +module Neo4j::Shared + module Cypher + module CreateMethod + def create_method + creates_unique? ? :create_unique : :create + end + + def creates_unique(option) + @creates_unique = option + end + + def creates_unique_option + @creates_unique || :none + end + + def creates_unique? + !!@creates_unique + end + alias_method :unique?, :creates_unique? + end + + module RelIdentifiers + extend ActiveSupport::Concern + + [:from_node, :to_node, :rel].each do |element| + define_method("#{element}_identifier") do + instance_variable_get(:"@#{element}_identifier") || element + end + + define_method("#{element}_identifier=") do |id| + instance_variable_set(:"@#{element}_identifier", id.to_sym) + end + end + end + end +end diff --git a/lib/neo4j/shared/filtered_hash.rb b/lib/neo4j/shared/filtered_hash.rb index 386a39c77..e83107967 100644 --- a/lib/neo4j/shared/filtered_hash.rb +++ b/lib/neo4j/shared/filtered_hash.rb @@ -3,6 +3,7 @@ class FilteredHash class InvalidHashFilterType < Neo4j::Neo4jrbError; end VALID_SYMBOL_INSTRUCTIONS = [:all, :none] VALID_HASH_INSTRUCTIONS = [:on, :except] + VALID_INSTRUCTIONS_TYPES = [Hash, Symbol] attr_reader :base, :instructions, :instructions_type @@ -54,6 +55,7 @@ def keys_array(key) end def validate_instructions!(instructions) + fail InvalidHashFilterType, "Filtering instructions #{instructions} are invalid" unless VALID_INSTRUCTIONS_TYPES.include?(instructions.class) clazz = instructions_type.name.downcase return if send(:"valid_#{clazz}_instructions?", instructions) fail InvalidHashFilterType, "Invalid instructions #{instructions}, valid options for #{clazz}: #{send(:"valid_#{clazz}_instructions")}" diff --git a/lib/neo4j/shared/query_factory.rb b/lib/neo4j/shared/query_factory.rb index cb06609a5..9d3ee459c 100644 --- a/lib/neo4j/shared/query_factory.rb +++ b/lib/neo4j/shared/query_factory.rb @@ -80,7 +80,6 @@ def match_string def create_query return match_query if graph_object.persisted? create_props, set_props = filtered_props - base_query.send(graph_object.create_method, query_string).break .set(identifier => set_props) .params(:"#{identifier}_create_props" => create_props) @@ -89,7 +88,7 @@ def create_query private def filtered_props - Neo4j::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.class.creates_unique_option).filtered_base + Neo4j::Shared::FilteredHash.new(graph_object.props_for_create, graph_object.creates_unique_option).filtered_base end def query_string diff --git a/spec/e2e/active_rel_spec.rb b/spec/e2e/active_rel_spec.rb index 2c7e0f843..4960ed087 100644 --- a/spec/e2e/active_rel_spec.rb +++ b/spec/e2e/active_rel_spec.rb @@ -195,7 +195,7 @@ def is_persisted_with_nodes(rel) end end - context 'with {on: *keys} option' do + context 'with {on: [keys]} option' do before { MyRelClass.creates_unique(on: :score) } context 'and a listed property changes' do @@ -213,7 +213,7 @@ def is_persisted_with_nodes(rel) end end - context 'with {except: *keys} option' do + context 'with {except: [keys]} option' do before { MyRelClass.creates_unique(except: :score) } context 'and a listed property changes' do diff --git a/spec/e2e/has_many_spec.rb b/spec/e2e/has_many_spec.rb index 7a2acf872..30136888b 100644 --- a/spec/e2e/has_many_spec.rb +++ b/spec/e2e/has_many_spec.rb @@ -43,8 +43,8 @@ end end - describe 'unique: true' do - before { Person.reflect_on_association(:knows).association.instance_variable_set(:@unique, true) } + describe 'unique: :none' do + before { Person.reflect_on_association(:knows).association.instance_variable_set(:@unique, :none) } after do Person.reflect_on_association(:knows).association.instance_variable_set(:@unique, false) [friend1, friend2].each(&:destroy) diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index dff09fe44..2572212e9 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -705,6 +705,72 @@ def custom_prop_method end end + describe 'Associations with `unique` set' do + let(:from_node) { Student.create } + let(:to_node) { Interest.create } + let(:first_props) { {score: 900} } + let(:second_props) { {score: 1000} } + let(:changed_props_create) { proc { from_node.interests.create(to_node, second_props) } } + + before do + Student.has_many :out, :interests, options + from_node.interests.create(to_node, first_props) + expect(from_node.interests.count).to eq 1 + end + + context 'with :none open' do + let(:options) { {type: nil, unique: :none} } + + it 'does not create additional rels, even when properties change' do + expect do + changed_props_create.call + end.not_to change { from_node.interests.count } + end + end + + context 'with `:all` option' do + let(:options) { {type: nil, unique: :all} } + + it 'creates additional rels when properties change' do + expect { changed_props_create.call }.to change { from_node.interests.count } + end + end + + context 'with {on: [keys]} option' do + let(:options) { {type: nil, unique: {on: :score}} } + + context 'and a listed property changes' do + it 'creates a new rel' do + expect { changed_props_create.call }.to change { from_node.interests.count } + end + end + + context 'and an unlisted property changes' do + it 'does not create a new rel' do + expect do + from_node.interests.create(to_node, first_props.merge(default: 'some other value')) + end.not_to change { from_node.interests.count } + end + end + end + + context 'with {except: [keys]} option' do + let(:options) { {type: nil, unique: {except: :score}} } + + context 'and a listed property changes' do + it 'does not create a new rel' do + expect { changed_props_create.call }.not_to change { from_node.interests.count } + end + end + + context 'and an unlisted property changes' do + it 'creates a new rel' do + expect { from_node.interests.create(to_node, first_props.merge(default: 'some other value')) }.to change { from_node.interests.count } + end + end + end + end + describe 'rel_methods' do before do student = Student.create diff --git a/spec/unit/active_node/has_n/association/rel_wrapper_spec.rb b/spec/unit/active_node/has_n/association/rel_wrapper_spec.rb new file mode 100644 index 000000000..8df9ddaa5 --- /dev/null +++ b/spec/unit/active_node/has_n/association/rel_wrapper_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +class Neo4j::ActiveNode::HasN::Association + describe RelWrapper do + let(:type) { :FRIENDS_WITH } + let(:identifier) { :r } + let(:unique_props) { {} } + let(:assoc_props) { {type: 'FRIENDS_WITH'}.merge(unique_props) } + let(:association) { Neo4j::ActiveNode::HasN::Association.new(:has_many, :out, :friends, assoc_props) } + let(:props) { {first: :foo, second: :bar, third: :baz, fourth: :buzz} } + let(:wrapper) { described_class.new(association, props) } + + describe '#initialize' do + it 'requires an Association and properties' do + expect { wrapper }.not_to raise_error + expect(wrapper.type).to eq type + expect(wrapper.properties).to eq props + end + end + + describe 'identifiers' do + it 'have defaults' do + expect(wrapper.from_node_identifier).to eq :from_node + expect(wrapper.to_node_identifier).to eq :to_node + expect(wrapper.rel_identifier).to eq :rel + end + + it 'can be redefined' do + expect { wrapper.from_node_identifier = :from }.to change { wrapper.from_node_identifier }.to :from + expect { wrapper.to_node_identifier = :to }.to change { wrapper.to_node_identifier }.to :to + expect { wrapper.rel_identifier = :rel_id }.to change { wrapper.rel_identifier }.to :rel_id + end + end + + describe '#persisted?' do + it { expect(wrapper.persisted?).to eq false } + end + + describe 'properties' do + let(:new_props) { {foo: :bar} } + + it 'can be reset' do + expect { wrapper.properties = new_props }.to change { wrapper.properties }.to(new_props) + end + end + + describe '#props_for_create' do + before { wrapper.properties = props } + + it 'returns the current properties' do + expect(wrapper.props_for_create).to eq props + end + end + + describe '#create_method' do + it 'defaults to :create' do + expect(wrapper.create_method).to eq :create + end + + it 'changes through #creates_unique' do + expect do + expect { wrapper.creates_unique(:none) }.to change { wrapper.create_method }.from(:create).to(:create_unique) + end.to change { wrapper.creates_unique? }.from(false).to(true) + + expect do + expect { wrapper.creates_unique(false) }.to change { wrapper.create_method }.from(:create_unique).to(:create) + end.to change { wrapper.creates_unique? }.from(true).to(false) + end + end + + describe '#creates_unique_options' do + let(:unique_props) { {unique: {on: [:foo, :bar, :baz]}} } + it 'corresponds with the setting on the association' do + expect(wrapper.creates_unique_option).to eq unique_props[:unique] + end + end + end +end From 27f395f14d32bdf153b34aa5e5b25c7aded4d358 Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Sat, 7 Nov 2015 19:18:57 -0500 Subject: [PATCH 5/7] Association knows there is a class that knows how to make relationships... --- lib/neo4j.rb | 1 + lib/neo4j/active_node/has_n/association.rb | 4 ++ .../has_n/association/rel_factory.rb | 61 +++++++++++++++++++ lib/neo4j/active_node/query/query_proxy.rb | 35 +---------- 4 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 lib/neo4j/active_node/has_n/association/rel_factory.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index a2ad47411..e61e5fd51 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -83,6 +83,7 @@ require 'neo4j/active_node/has_n' require 'neo4j/active_node/has_n/association_cypher_methods' require 'neo4j/active_node/has_n/association/rel_wrapper' +require 'neo4j/active_node/has_n/association/rel_factory' require 'neo4j/active_node/has_n/association' require 'neo4j/active_node/query/query_proxy' require 'neo4j/active_node/query' diff --git a/lib/neo4j/active_node/has_n/association.rb b/lib/neo4j/active_node/has_n/association.rb index 51ffe995e..159c301d9 100644 --- a/lib/neo4j/active_node/has_n/association.rb +++ b/lib/neo4j/active_node/has_n/association.rb @@ -141,6 +141,10 @@ def create_method unique? ? :create_unique : :create end + def _create_relationship(start_object, node_or_nodes, properties) + RelFactory.create(start_object, node_or_nodes, properties, self) + end + def relationship_class? !!relationship_class end diff --git a/lib/neo4j/active_node/has_n/association/rel_factory.rb b/lib/neo4j/active_node/has_n/association/rel_factory.rb new file mode 100644 index 000000000..1141b9c72 --- /dev/null +++ b/lib/neo4j/active_node/has_n/association/rel_factory.rb @@ -0,0 +1,61 @@ +module Neo4j::ActiveNode::HasN + class Association + class RelFactory + [:start_object, :other_node_or_nodes, :properties, :association].tap do |accessors| + attr_reader(*accessors) + private(*accessors) + end + + def self.create(start_object, other_node_or_nodes, properties, association) + factory = new(start_object, other_node_or_nodes, properties, association) + factory._create_relationship + end + + def _create_relationship + creator = association.relationship_class ? :rel_class : :factory + send(:"_create_relationship_with_#{creator}") + end + + private + + def initialize(start_object, other_node_or_nodes, properties, association) + @start_object = start_object + @other_node_or_nodes = other_node_or_nodes + @properties = properties + @association = association + end + + def _create_relationship_with_rel_class + Array(other_node_or_nodes).each do |other_node| + node_props = _nodes_for_create(other_node, :from_node, :to_node) + association.relationship_class.create(properties.merge(node_props)) + end + end + + def _create_relationship_with_factory + Array(other_node_or_nodes).each do |other_node| + wrapper = _rel_wrapper(properties) + base = _match_query(other_node, wrapper) + factory = Neo4j::Shared::RelQueryFactory.new(wrapper, wrapper.rel_identifier) + factory.base_query = base + factory.query.exec + end + end + + def _match_query(other_node, wrapper) + nodes = _nodes_for_create(other_node, wrapper.from_node_identifier, wrapper.to_node_identifier) + Neo4j::Session.current.query.match_nodes(nodes) + end + + def _nodes_for_create(other_node, from_node_id, to_node_id) + nodes = [@start_object, other_node] + nodes.reverse! if association.direction == :in + {from_node_id => nodes[0], to_node_id => nodes[1]} + end + + def _rel_wrapper(properties) + Neo4j::ActiveNode::HasN::Association::RelWrapper.new(association, properties) + end + end + end +end diff --git a/lib/neo4j/active_node/query/query_proxy.rb b/lib/neo4j/active_node/query/query_proxy.rb index 8d6d0450e..3b038f793 100644 --- a/lib/neo4j/active_node/query/query_proxy.rb +++ b/lib/neo4j/active_node/query/query_proxy.rb @@ -206,40 +206,7 @@ def _nodeify!(*args) end def _create_relationship(other_node_or_nodes, properties) - creator = association.relationship_class ? :rel_class : :factory - send(:"_create_relationship_with_#{creator}", other_node_or_nodes, properties) - end - - def _create_relationship_with_rel_class(other_node_or_nodes, properties) - Array(other_node_or_nodes).each do |other_node| - node_props = _nodes_for_create(other_node, :from_node, :to_node) - association.relationship_class.create(properties.merge(node_props)) - end - end - - def _create_relationship_with_factory(other_node_or_nodes, properties) - Array(other_node_or_nodes).each do |other_node| - wrapper = _rel_wrapper(properties) - base = _match_query(other_node, wrapper) - factory = Neo4j::Shared::RelQueryFactory.new(wrapper, wrapper.rel_identifier) - factory.base_query = base - factory.query.exec - end - end - - def _match_query(other_node, wrapper) - nodes = _nodes_for_create(other_node, wrapper.from_node_identifier, wrapper.to_node_identifier) - Neo4j::Session.current.query.match_nodes(nodes) - end - - def _nodes_for_create(other_node, from_node_id, to_node_id) - nodes = [@start_object, other_node] - nodes.reverse! if association.direction == :in - {from_node_id => nodes[0], to_node_id => nodes[1]} - end - - def _rel_wrapper(properties) - Neo4j::ActiveNode::HasN::Association::RelWrapper.new(association, properties) + association._create_relationship(@start_object, other_node_or_nodes, properties) end def read_attribute_for_serialization(*args) From c66f218c459744c25ca9779c9540e6fdf2a1580d Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Tue, 10 Nov 2015 12:56:09 -0500 Subject: [PATCH 6/7] {except: [keys]} no longer a valid FilteredHash option --- lib/neo4j/shared/filtered_hash.rb | 5 ++--- spec/e2e/active_rel_spec.rb | 16 ---------------- spec/e2e/query_spec.rb | 16 ---------------- spec/unit/shared/filtered_hash_spec.rb | 10 +--------- 4 files changed, 3 insertions(+), 44 deletions(-) diff --git a/lib/neo4j/shared/filtered_hash.rb b/lib/neo4j/shared/filtered_hash.rb index e83107967..787f1513c 100644 --- a/lib/neo4j/shared/filtered_hash.rb +++ b/lib/neo4j/shared/filtered_hash.rb @@ -2,7 +2,7 @@ module Neo4j::Shared class FilteredHash class InvalidHashFilterType < Neo4j::Neo4jrbError; end VALID_SYMBOL_INSTRUCTIONS = [:all, :none] - VALID_HASH_INSTRUCTIONS = [:on, :except] + VALID_HASH_INSTRUCTIONS = [:on] VALID_INSTRUCTIONS_TYPES = [Hash, Symbol] attr_reader :base, :instructions, :instructions_type @@ -37,8 +37,7 @@ def filtered_base_by_symbol def filtered_base_by_hash behavior_key = instructions.keys.first filter_keys = keys_array(behavior_key) - base = [filter(filter_keys, :with), filter(filter_keys, :without)] - behavior_key == :on ? base : base.reverse + [filter(filter_keys, :with), filter(filter_keys, :without)] end def key?(filter_keys, key) diff --git a/spec/e2e/active_rel_spec.rb b/spec/e2e/active_rel_spec.rb index 4960ed087..6fa4a4cab 100644 --- a/spec/e2e/active_rel_spec.rb +++ b/spec/e2e/active_rel_spec.rb @@ -212,22 +212,6 @@ def is_persisted_with_nodes(rel) end end end - - context 'with {except: [keys]} option' do - before { MyRelClass.creates_unique(except: :score) } - - context 'and a listed property changes' do - it 'does not create a new rel' do - expect { changed_props_create.call }.not_to change { from_node.others.count } - end - end - - context 'and an unlisted property changes' do - it 'creates a new rel' do - expect { MyRelClass.create(nodes.merge(default: 'some other value')) }.to change { from_node.others.count } - end - end - end end end diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 2572212e9..6c61fb255 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -753,22 +753,6 @@ def custom_prop_method end end end - - context 'with {except: [keys]} option' do - let(:options) { {type: nil, unique: {except: :score}} } - - context 'and a listed property changes' do - it 'does not create a new rel' do - expect { changed_props_create.call }.not_to change { from_node.interests.count } - end - end - - context 'and an unlisted property changes' do - it 'creates a new rel' do - expect { from_node.interests.create(to_node, first_props.merge(default: 'some other value')) }.to change { from_node.interests.count } - end - end - end end describe 'rel_methods' do diff --git a/spec/unit/shared/filtered_hash_spec.rb b/spec/unit/shared/filtered_hash_spec.rb index 320e3216e..4db4bab6b 100644 --- a/spec/unit/shared/filtered_hash_spec.rb +++ b/spec/unit/shared/filtered_hash_spec.rb @@ -45,9 +45,8 @@ module Neo4j::Shared end describe 'hash' do - it 'raises unless first key is :on or :except' do + it 'raises unless first key is :on' do expect { FilteredHash.new(base, on: :foo) }.not_to raise_error - expect { FilteredHash.new(base, except: :foo) }.not_to raise_error expect { FilteredHash.new(base, foo: :foo) }.to raise_error FilteredHash::InvalidHashFilterType end @@ -58,13 +57,6 @@ module Neo4j::Shared expect(filtered_props.filtered_base).to eq([{second: :bar, fourth: :buzz}, {first: :foo, third: :baz}]) end end - - context 'except:' do - let(:instructions) { {except: [:second, :fourth]} } - it 'returns [hash without keys specified, hash with keys specified' do - expect(filtered_props.filtered_base).to eq([{first: :foo, third: :baz}, {second: :bar, fourth: :buzz}]) - end - end end end end From 0b8411b9dbf9afd2f8fa4bfd30dba4001b3529db Mon Sep 17 00:00:00 2001 From: Chris Grigg Date: Wed, 11 Nov 2015 10:16:44 -0500 Subject: [PATCH 7/7] default ActiveRel.creates_unique arg to :none, allow Association unique: true --- lib/neo4j/shared/cypher.rb | 3 ++- spec/e2e/active_rel_spec.rb | 24 +++++++++++++++++++----- spec/e2e/query_spec.rb | 8 ++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/neo4j/shared/cypher.rb b/lib/neo4j/shared/cypher.rb index 91e875f8e..3d298705b 100644 --- a/lib/neo4j/shared/cypher.rb +++ b/lib/neo4j/shared/cypher.rb @@ -5,7 +5,8 @@ def create_method creates_unique? ? :create_unique : :create end - def creates_unique(option) + def creates_unique(option = :none) + option = :none if option == true @creates_unique = option end diff --git a/spec/e2e/active_rel_spec.rb b/spec/e2e/active_rel_spec.rb index 6fa4a4cab..0d6c4e39e 100644 --- a/spec/e2e/active_rel_spec.rb +++ b/spec/e2e/active_rel_spec.rb @@ -160,6 +160,7 @@ def is_persisted_with_nodes(rel) [from_node, to_node].each(&:destroy) end + it 'creates a unique relationship between to nodes' do expect(from_node.others.count).to eq 0 MyRelClass.create(from_node: from_node, to_node: to_node) @@ -174,12 +175,22 @@ def is_persisted_with_nodes(rel) let(:first_props) { {score: 900} } let(:second_props) { {score: 1000} } let(:changed_props_create) { proc { MyRelClass.create(nodes.merge(second_props)) } } - before do - MyRelClass.creates_unique(:none) - MyRelClass.create(nodes.merge(first_props)) + + context 'with no arguments' do + before { MyRelClass.creates_unique } + + it 'defaults to :none' do + expect(Neo4j::Shared::FilteredHash).to receive(:new).with(instance_of(Hash), :none).and_call_original + MyRelClass.create(nodes.merge(first_props)) + end end - context 'with :none open' do + context 'with :none option' do + before do + MyRelClass.creates_unique(:none) + MyRelClass.create(nodes.merge(first_props)) + end + it 'does not create additional rels, even when properties change' do expect do changed_props_create.call @@ -196,7 +207,10 @@ def is_persisted_with_nodes(rel) end context 'with {on: [keys]} option' do - before { MyRelClass.creates_unique(on: :score) } + before do + MyRelClass.creates_unique(on: :score) + MyRelClass.create(nodes.merge(first_props)) + end context 'and a listed property changes' do it 'creates a new rel' do diff --git a/spec/e2e/query_spec.rb b/spec/e2e/query_spec.rb index 6c61fb255..c489ab452 100644 --- a/spec/e2e/query_spec.rb +++ b/spec/e2e/query_spec.rb @@ -718,6 +718,14 @@ def custom_prop_method expect(from_node.interests.count).to eq 1 end + context 'with `true` option' do + let(:options) { {type: nil, unique: true} } + it 'becomes :none' do + expect(Neo4j::Shared::FilteredHash).to receive(:new).with(instance_of(Hash), :none).and_call_original + changed_props_create.call + end + end + context 'with :none open' do let(:options) { {type: nil, unique: :none} }