neo4j 4.1.5 → 5.0.0.rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|