neo4j 4.1.5 → 5.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +584 -0
- data/CONTRIBUTORS +7 -28
- data/Gemfile +6 -1
- data/README.md +54 -8
- data/lib/neo4j.rb +5 -0
- data/lib/neo4j/active_node.rb +1 -0
- data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
- data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
- data/lib/neo4j/active_node/has_n.rb +377 -132
- data/lib/neo4j/active_node/has_n/association.rb +77 -38
- data/lib/neo4j/active_node/id_property.rb +46 -28
- data/lib/neo4j/active_node/initialize.rb +18 -6
- data/lib/neo4j/active_node/labels.rb +69 -35
- data/lib/neo4j/active_node/node_wrapper.rb +37 -30
- data/lib/neo4j/active_node/orm_adapter.rb +5 -4
- data/lib/neo4j/active_node/persistence.rb +53 -10
- data/lib/neo4j/active_node/property.rb +13 -5
- data/lib/neo4j/active_node/query.rb +11 -10
- data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
- data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
- data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
- data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
- data/lib/neo4j/active_node/query_methods.rb +3 -1
- data/lib/neo4j/active_node/scope.rb +17 -21
- data/lib/neo4j/active_node/validations.rb +8 -2
- data/lib/neo4j/active_rel/initialize.rb +1 -2
- data/lib/neo4j/active_rel/persistence.rb +21 -33
- data/lib/neo4j/active_rel/property.rb +4 -2
- data/lib/neo4j/active_rel/types.rb +20 -8
- data/lib/neo4j/config.rb +16 -6
- data/lib/neo4j/core/query.rb +2 -2
- data/lib/neo4j/errors.rb +10 -0
- data/lib/neo4j/migration.rb +57 -46
- data/lib/neo4j/paginated.rb +3 -1
- data/lib/neo4j/railtie.rb +26 -14
- data/lib/neo4j/shared.rb +7 -1
- data/lib/neo4j/shared/declared_property.rb +62 -0
- data/lib/neo4j/shared/declared_property_manager.rb +150 -0
- data/lib/neo4j/shared/persistence.rb +15 -8
- data/lib/neo4j/shared/property.rb +64 -49
- data/lib/neo4j/shared/rel_type_converters.rb +13 -12
- data/lib/neo4j/shared/serialized_properties.rb +0 -15
- data/lib/neo4j/shared/type_converters.rb +53 -47
- data/lib/neo4j/shared/typecaster.rb +49 -0
- data/lib/neo4j/version.rb +1 -1
- data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
- data/lib/rails/generators/neo4j_generator.rb +5 -12
- data/neo4j.gemspec +4 -3
- metadata +30 -11
- data/CHANGELOG +0 -545
@@ -29,8 +29,10 @@ module Neo4j::ActiveRel
|
|
29
29
|
# Extracts keys from attributes hash which are relationships of the model
|
30
30
|
# TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
|
31
31
|
def extract_association_attributes!(attributes)
|
32
|
-
|
33
|
-
|
32
|
+
{}.tap do |relationship_props|
|
33
|
+
attributes.each_key do |key|
|
34
|
+
relationship_props[key] = attributes.delete(key) if [:from_node, :to_node].include?(key)
|
35
|
+
end
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
@@ -21,25 +21,37 @@ module Neo4j
|
|
21
21
|
WRAPPED_CLASSES = {}
|
22
22
|
|
23
23
|
included do
|
24
|
-
type self.
|
24
|
+
type self.namespaced_model_name, true
|
25
25
|
end
|
26
26
|
|
27
27
|
module ClassMethods
|
28
28
|
include Neo4j::Shared::RelTypeConverters
|
29
29
|
|
30
|
-
def inherited(
|
31
|
-
|
30
|
+
def inherited(subclass)
|
31
|
+
subclass.type subclass.namespaced_model_name, true
|
32
32
|
end
|
33
33
|
|
34
34
|
# @param type [String] sets the relationship type when creating relationships via this class
|
35
|
-
def type(given_type =
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
def type(given_type = namespaced_model_name, auto = false)
|
36
|
+
@rel_type = (auto ? decorated_rel_type(given_type) : given_type).tap do |type|
|
37
|
+
add_wrapped_class type unless uses_classname?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def namespaced_model_name
|
42
|
+
case Neo4j::Config[:module_handling]
|
43
|
+
when :demodulize
|
44
|
+
self.name.demodulize
|
45
|
+
when Proc
|
46
|
+
Neo4j::Config[:module_handling].call(self.name)
|
47
|
+
else
|
48
|
+
self.name
|
49
|
+
end
|
39
50
|
end
|
40
51
|
|
41
|
-
# @return [String] a string representing the relationship type that will be created
|
42
52
|
attr_reader :rel_type
|
53
|
+
# @return [String] a string representing the relationship type that will be created
|
54
|
+
# attr_reader :rel_type
|
43
55
|
alias_method :_type, :rel_type # Should be deprecated
|
44
56
|
|
45
57
|
def add_wrapped_class(type)
|
data/lib/neo4j/config.rb
CHANGED
@@ -5,6 +5,7 @@ module Neo4j
|
|
5
5
|
#
|
6
6
|
class Config
|
7
7
|
DEFAULT_FILE = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'config', 'neo4j', 'config.yml'))
|
8
|
+
CLASS_NAME_PROPERTY_KEY = 'class_name_property'
|
8
9
|
|
9
10
|
class << self
|
10
11
|
# @return [Fixnum] The location of the default configuration file.
|
@@ -53,7 +54,6 @@ module Neo4j
|
|
53
54
|
nil
|
54
55
|
end
|
55
56
|
|
56
|
-
|
57
57
|
# Sets the value of a config entry.
|
58
58
|
#
|
59
59
|
# @param [Symbol] key the key to set the parameter for
|
@@ -62,14 +62,12 @@ module Neo4j
|
|
62
62
|
configuration[key.to_s] = val
|
63
63
|
end
|
64
64
|
|
65
|
-
|
66
65
|
# @param [Symbol] key The key of the config entry value we want
|
67
66
|
# @return the the value of a config entry
|
68
67
|
def [](key)
|
69
68
|
configuration[key.to_s]
|
70
69
|
end
|
71
70
|
|
72
|
-
|
73
71
|
# Remove the value of a config entry.
|
74
72
|
#
|
75
73
|
# @param [Symbol] key the key of the configuration entry to delete
|
@@ -78,7 +76,6 @@ module Neo4j
|
|
78
76
|
configuration.delete(key)
|
79
77
|
end
|
80
78
|
|
81
|
-
|
82
79
|
# Remove all configuration. This can be useful for testing purpose.
|
83
80
|
#
|
84
81
|
# @return nil
|
@@ -86,7 +83,6 @@ module Neo4j
|
|
86
83
|
@configuration = nil
|
87
84
|
end
|
88
85
|
|
89
|
-
|
90
86
|
# @return [Hash] The config as a hash.
|
91
87
|
def to_hash
|
92
88
|
configuration.to_hash
|
@@ -98,13 +94,27 @@ module Neo4j
|
|
98
94
|
end
|
99
95
|
|
100
96
|
def class_name_property
|
101
|
-
Neo4j::Config[
|
97
|
+
@_class_name_property = Neo4j::Config[CLASS_NAME_PROPERTY_KEY] || :_classname
|
102
98
|
end
|
103
99
|
|
104
100
|
def include_root_in_json
|
105
101
|
# we use ternary because a simple || will always evaluate true
|
106
102
|
Neo4j::Config[:include_root_in_json].nil? ? true : Neo4j::Config[:include_root_in_json]
|
107
103
|
end
|
104
|
+
|
105
|
+
def module_handling
|
106
|
+
Neo4j::Config[:module_handling] || :none
|
107
|
+
end
|
108
|
+
|
109
|
+
def association_model_namespace
|
110
|
+
Neo4j::Config[:association_model_namespace] || nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def association_model_namespace_string
|
114
|
+
namespace = Neo4j::Config[:association_model_namespace]
|
115
|
+
return nil if namespace.nil?
|
116
|
+
"::#{namespace}"
|
117
|
+
end
|
108
118
|
end
|
109
119
|
end
|
110
120
|
end
|
data/lib/neo4j/core/query.rb
CHANGED
@@ -8,14 +8,14 @@ module Neo4j::Core
|
|
8
8
|
# @return [Neo4j::ActiveNode::Query::QueryProxy] A QueryProxy object.
|
9
9
|
def proxy_as(model, var, optional = false)
|
10
10
|
# TODO: Discuss whether it's necessary to call `break` on the query or if this should be left to the user.
|
11
|
-
Neo4j::ActiveNode::Query::QueryProxy.new(model, nil,
|
11
|
+
Neo4j::ActiveNode::Query::QueryProxy.new(model, nil, node: var, optional: optional, starting_query: self, chain_level: @proxy_chain_level)
|
12
12
|
end
|
13
13
|
|
14
14
|
# Calls proxy_as with `optional` set true. This doesn't offer anything different from calling `proxy_as` directly but it may be more readable.
|
15
15
|
def proxy_as_optional(model, var)
|
16
16
|
proxy_as(model, var, true)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
# For instances where you turn a QueryProxy into a Query and then back to a QueryProxy with `#proxy_as`
|
20
20
|
attr_accessor :proxy_chain_level
|
21
21
|
end
|
data/lib/neo4j/errors.rb
ADDED
data/lib/neo4j/migration.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
1
3
|
module Neo4j
|
2
4
|
class Migration
|
3
5
|
def migrate
|
@@ -17,7 +19,7 @@ module Neo4j
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def joined_path(path)
|
20
|
-
File.join(path, 'db', 'neo4j-migrate')
|
22
|
+
File.join(path.to_s, 'db', 'neo4j-migrate')
|
21
23
|
end
|
22
24
|
|
23
25
|
class AddIdProperty < Neo4j::Migration
|
@@ -41,64 +43,72 @@ module Neo4j
|
|
41
43
|
|
42
44
|
def setup
|
43
45
|
FileUtils.mkdir_p('db/neo4j-migrate')
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
|
47
|
+
return if File.file?(models_filename)
|
48
|
+
|
49
|
+
File.open(models_filename, 'w') do |file|
|
50
|
+
message = <<MESSAGE
|
51
|
+
# Provide models to which IDs should be added.
|
52
|
+
# # It will only modify nodes that do not have IDs. There is no danger of overwriting data.
|
53
|
+
# # models: [Student,Lesson,Teacher,Exam]\nmodels: []
|
54
|
+
MESSAGE
|
55
|
+
file.write(message)
|
48
56
|
end
|
49
57
|
end
|
50
58
|
|
51
59
|
private
|
52
60
|
|
53
61
|
def add_ids_to(model)
|
54
|
-
require 'benchmark'
|
55
|
-
|
56
62
|
max_per_batch = (ENV['MAX_PER_BATCH'] || default_max_per_batch).to_i
|
57
63
|
|
58
64
|
label = model.mapped_label_name
|
59
|
-
property = model.primary_key
|
60
|
-
nodes_left = 1
|
61
65
|
last_time_taken = nil
|
62
66
|
|
63
|
-
until nodes_left == 0
|
64
|
-
|
67
|
+
until (nodes_left = idless_count(label, model.primary_key)) == 0
|
68
|
+
print_status(last_time_taken, max_per_batch, nodes_left)
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
eta_seconds = (nodes_left * time_per_node).round
|
70
|
-
print_output "#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)\r"
|
70
|
+
count = [nodes_left, max_per_batch].min
|
71
|
+
last_time_taken = Benchmark.realtime do
|
72
|
+
max_per_batch = id_batch_set(label, model.primary_key, count.times.map { new_id_for(model) }, count)
|
71
73
|
end
|
74
|
+
end
|
75
|
+
end
|
72
76
|
|
73
|
-
|
74
|
-
|
77
|
+
def idless_count(label, id_property)
|
78
|
+
Neo4j::Session.query.match(n: label).where("NOT has(n.#{id_property})").pluck('COUNT(n) AS ids').first
|
79
|
+
end
|
75
80
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
81
|
+
def print_status(last_time_taken, max_per_batch, nodes_left)
|
82
|
+
time_per_node = last_time_taken / max_per_batch if last_time_taken
|
83
|
+
message = if time_per_node
|
84
|
+
eta_seconds = (nodes_left * time_per_node).round
|
85
|
+
"#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)\r"
|
86
|
+
else
|
87
|
+
"Running first batch...\r"
|
88
|
+
end
|
89
|
+
|
90
|
+
print_output message
|
85
91
|
end
|
86
92
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
93
|
+
|
94
|
+
def id_batch_set(label, id_property, new_ids, count)
|
95
|
+
tx = Neo4j::Transaction.new
|
96
|
+
|
97
|
+
Neo4j::Session.query("MATCH (n:`#{label}`) WHERE NOT has(n.#{id_property})
|
98
|
+
with COLLECT(n) as nodes, #{new_ids} as ids
|
99
|
+
FOREACH(i in range(0,#{count - 1})|
|
100
|
+
FOREACH(node in [nodes[i]]|
|
101
|
+
SET node.#{id_property} = ids[i]))
|
102
|
+
RETURN distinct(true)
|
103
|
+
LIMIT #{count}")
|
104
|
+
|
105
|
+
count
|
106
|
+
rescue Neo4j::Server::CypherResponse::ResponseError, Faraday::TimeoutError
|
107
|
+
new_max_per_batch = (max_per_batch * 0.8).round
|
108
|
+
output "Error querying #{max_per_batch} nodes. Trying #{new_max_per_batch}"
|
109
|
+
new_max_per_batch
|
110
|
+
ensure
|
111
|
+
tx.close
|
102
112
|
end
|
103
113
|
|
104
114
|
def default_max_per_batch
|
@@ -133,10 +143,11 @@ module Neo4j
|
|
133
143
|
def setup
|
134
144
|
output "Creating file #{classnames_filepath}. Please use this as the migration guide."
|
135
145
|
FileUtils.mkdir_p('db/neo4j-migrate')
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
146
|
+
|
147
|
+
return if File.file?(classnames_filepath)
|
148
|
+
|
149
|
+
source = File.join(File.dirname(__FILE__), '..', '..', 'config', 'neo4j', classnames_filename)
|
150
|
+
FileUtils.copy_file(source, classnames_filepath)
|
140
151
|
end
|
141
152
|
|
142
153
|
private
|
data/lib/neo4j/paginated.rb
CHANGED
@@ -4,7 +4,9 @@ module Neo4j
|
|
4
4
|
attr_reader :items, :total, :current_page
|
5
5
|
|
6
6
|
def initialize(items, total, current_page)
|
7
|
-
@items
|
7
|
+
@items = items
|
8
|
+
@total = total
|
9
|
+
@current_page = current_page
|
8
10
|
end
|
9
11
|
|
10
12
|
def self.create_from(source, page, per_page, order = nil)
|
data/lib/neo4j/railtie.rb
CHANGED
@@ -21,19 +21,24 @@ module Neo4j
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def setup_default_session(cfg)
|
24
|
+
setup_config_defaults!(cfg)
|
25
|
+
|
26
|
+
return if !cfg.sessions.empty?
|
27
|
+
|
28
|
+
cfg.sessions << {type: cfg.session_type, path: cfg.session_path, options: cfg.session_options}
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_config_defaults!(cfg)
|
24
32
|
cfg.session_type ||= :server_db
|
25
33
|
cfg.session_path ||= 'http://localhost:7474'
|
26
34
|
cfg.session_options ||= {}
|
27
35
|
cfg.sessions ||= []
|
28
36
|
|
29
|
-
|
30
|
-
|
31
|
-
cfg.session_path = cfg.session_path.gsub("#{uri.user}:#{uri.password}@", '')
|
32
|
-
end
|
37
|
+
uri = URI(cfg.session_path)
|
38
|
+
return if uri.user.blank?
|
33
39
|
|
34
|
-
|
35
|
-
|
36
|
-
end
|
40
|
+
cfg.session_options.reverse_merge!(basic_auth: {username: uri.user, password: uri.password})
|
41
|
+
cfg.session_path = cfg.session_path.gsub("#{uri.user}:#{uri.password}@", '')
|
37
42
|
end
|
38
43
|
|
39
44
|
|
@@ -63,6 +68,16 @@ module Neo4j
|
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
71
|
+
def register_neo4j_cypher_logging
|
72
|
+
return if @neo4j_cypher_logging_registered
|
73
|
+
|
74
|
+
Neo4j::Server::CypherSession.log_with do |message|
|
75
|
+
Rails.logger.info message
|
76
|
+
end
|
77
|
+
|
78
|
+
@neo4j_cypher_logging_registered = true
|
79
|
+
end
|
80
|
+
|
66
81
|
# Starting Neo after :load_config_initializers allows apps to
|
67
82
|
# register migrations in config/initializers
|
68
83
|
initializer 'neo4j.start', after: :load_config_initializers do |app|
|
@@ -75,14 +90,11 @@ module Neo4j
|
|
75
90
|
end
|
76
91
|
Neo4j::Config.configuration.merge!(cfg.to_hash)
|
77
92
|
|
78
|
-
|
79
|
-
|
80
|
-
cyan = "\e[36m"
|
93
|
+
register_neo4j_cypher_logging
|
94
|
+
end
|
81
95
|
|
82
|
-
|
83
|
-
|
84
|
-
Rails.logger.info " #{cyan}#{payload[:context]}#{clear} #{yellow}#{ms.round}ms#{clear} #{payload[:cypher]}" + (payload[:params].size > 0 ? ' | ' + payload[:params].inspect : '')
|
85
|
-
end
|
96
|
+
console do
|
97
|
+
register_neo4j_cypher_logging
|
86
98
|
end
|
87
99
|
end
|
88
100
|
end
|
data/lib/neo4j/shared.rb
CHANGED
@@ -18,7 +18,8 @@ module Neo4j
|
|
18
18
|
|
19
19
|
def neo4j_session
|
20
20
|
if @neo4j_session_name
|
21
|
-
Neo4j::Session.named(@neo4j_session_name) ||
|
21
|
+
Neo4j::Session.named(@neo4j_session_name) ||
|
22
|
+
fail("#{self.name} is configured to use a neo4j session named #{@neo4j_session_name}, but no such session is registered with Neo4j::Session")
|
22
23
|
else
|
23
24
|
Neo4j::Session.current!
|
24
25
|
end
|
@@ -27,10 +28,15 @@ module Neo4j
|
|
27
28
|
|
28
29
|
included do
|
29
30
|
self.include_root_in_json = Neo4j::Config.include_root_in_json
|
31
|
+
@_declared_property_manager ||= Neo4j::Shared::DeclaredPropertyManager.new(self)
|
30
32
|
|
31
33
|
def self.i18n_scope
|
32
34
|
:neo4j
|
33
35
|
end
|
34
36
|
end
|
37
|
+
|
38
|
+
def declared_property_manager
|
39
|
+
self.class.declared_property_manager
|
40
|
+
end
|
35
41
|
end
|
36
42
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Neo4j::Shared
|
2
|
+
# Contains methods related to the management
|
3
|
+
class DeclaredProperty
|
4
|
+
class IllegalPropertyError < StandardError; end
|
5
|
+
|
6
|
+
ILLEGAL_PROPS = %w(from_node to_node start_node end_node)
|
7
|
+
attr_reader :name, :name_string, :name_sym, :options, :magic_typecaster
|
8
|
+
|
9
|
+
def initialize(name, options = {})
|
10
|
+
fail IllegalPropertyError, "#{name} is an illegal property" if ILLEGAL_PROPS.include?(name.to_s)
|
11
|
+
@name = @name_sym = name
|
12
|
+
@name_string = name.to_s
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def register
|
17
|
+
register_magic_properties
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
options[:type]
|
22
|
+
end
|
23
|
+
|
24
|
+
def typecaster
|
25
|
+
options[:typecaster]
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_value
|
29
|
+
options[:default]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Tweaks properties
|
35
|
+
def register_magic_properties
|
36
|
+
options[:type] ||= DateTime if name.to_sym == :created_at || name.to_sym == :updated_at
|
37
|
+
# TODO: Custom typecaster to fix the stuff below
|
38
|
+
# ActiveAttr does not handle "Time", Rails and Neo4j.rb 2.3 did
|
39
|
+
# Convert it to DateTime in the interest of consistency
|
40
|
+
options[:type] = DateTime if options[:type] == Time
|
41
|
+
|
42
|
+
register_magic_typecaster
|
43
|
+
register_type_converter
|
44
|
+
end
|
45
|
+
|
46
|
+
def register_magic_typecaster
|
47
|
+
found_typecaster = Neo4j::Shared::TypeConverters.typecaster_for(options[:type])
|
48
|
+
return unless found_typecaster && found_typecaster.respond_to?(:primitive_type)
|
49
|
+
options[:typecaster] = found_typecaster
|
50
|
+
@magic_typecaster = options[:type]
|
51
|
+
options[:type] = found_typecaster.primitive_type
|
52
|
+
end
|
53
|
+
|
54
|
+
def register_type_converter
|
55
|
+
converter = options[:serializer]
|
56
|
+
return unless converter
|
57
|
+
options[:type] = converter.convert_type
|
58
|
+
options[:typecaster] = ActiveAttr::Typecasting::ObjectTypecaster.new
|
59
|
+
Neo4j::Shared::TypeConverters.register_converter(converter)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|