neo4j 7.2.3 → 8.0.0.alpha.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -46
  3. data/Gemfile +15 -14
  4. data/README.md +21 -14
  5. data/bin/neo4j-jars +1 -1
  6. data/lib/neo4j.rb +12 -1
  7. data/lib/neo4j/active_base.rb +68 -0
  8. data/lib/neo4j/active_base/session_registry.rb +12 -0
  9. data/lib/neo4j/active_node.rb +13 -21
  10. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +6 -6
  11. data/lib/neo4j/active_node/enum.rb +3 -6
  12. data/lib/neo4j/active_node/has_n.rb +24 -19
  13. data/lib/neo4j/active_node/has_n/association.rb +6 -2
  14. data/lib/neo4j/active_node/has_n/association/rel_factory.rb +1 -1
  15. data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +1 -1
  16. data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +1 -1
  17. data/lib/neo4j/active_node/id_property.rb +52 -15
  18. data/lib/neo4j/active_node/labels.rb +32 -10
  19. data/lib/neo4j/active_node/labels/index.rb +5 -55
  20. data/lib/neo4j/active_node/node_list_formatter.rb +13 -0
  21. data/lib/neo4j/active_node/node_wrapper.rb +39 -37
  22. data/lib/neo4j/active_node/persistence.rb +27 -13
  23. data/lib/neo4j/active_node/query/query_proxy.rb +11 -9
  24. data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +4 -4
  25. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +1 -0
  26. data/lib/neo4j/active_node/query/query_proxy_link.rb +13 -9
  27. data/lib/neo4j/active_node/query/query_proxy_methods.rb +76 -8
  28. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +1 -1
  29. data/lib/neo4j/active_node/query_methods.rb +3 -3
  30. data/lib/neo4j/active_node/scope.rb +24 -7
  31. data/lib/neo4j/active_rel.rb +21 -3
  32. data/lib/neo4j/active_rel/initialize.rb +2 -2
  33. data/lib/neo4j/active_rel/persistence.rb +32 -6
  34. data/lib/neo4j/active_rel/persistence/query_factory.rb +3 -3
  35. data/lib/neo4j/active_rel/property.rb +9 -9
  36. data/lib/neo4j/active_rel/query.rb +6 -4
  37. data/lib/neo4j/active_rel/rel_wrapper.rb +24 -16
  38. data/lib/neo4j/active_rel/related_node.rb +5 -1
  39. data/lib/neo4j/active_rel/types.rb +2 -2
  40. data/lib/neo4j/config.rb +0 -1
  41. data/lib/neo4j/errors.rb +3 -0
  42. data/lib/neo4j/migration.rb +90 -71
  43. data/lib/neo4j/migrations.rb +10 -0
  44. data/lib/neo4j/migrations/base.rb +44 -0
  45. data/lib/neo4j/migrations/helpers.rb +101 -0
  46. data/lib/neo4j/migrations/helpers/id_property.rb +75 -0
  47. data/lib/neo4j/migrations/helpers/relationships.rb +66 -0
  48. data/lib/neo4j/migrations/helpers/schema.rb +53 -0
  49. data/lib/neo4j/migrations/migration_file.rb +24 -0
  50. data/lib/neo4j/migrations/runner.rb +110 -0
  51. data/lib/neo4j/migrations/schema_migration.rb +9 -0
  52. data/lib/neo4j/model_schema.rb +100 -0
  53. data/lib/neo4j/railtie.rb +29 -110
  54. data/lib/neo4j/schema/operation.rb +24 -13
  55. data/lib/neo4j/session_manager.rb +137 -0
  56. data/lib/neo4j/shared.rb +20 -11
  57. data/lib/neo4j/shared/attributes.rb +10 -16
  58. data/lib/neo4j/shared/callbacks.rb +3 -3
  59. data/lib/neo4j/shared/cypher.rb +1 -1
  60. data/lib/neo4j/shared/declared_properties.rb +1 -1
  61. data/lib/neo4j/shared/declared_property.rb +1 -1
  62. data/lib/neo4j/shared/enum.rb +6 -18
  63. data/lib/neo4j/shared/identity.rb +27 -21
  64. data/lib/neo4j/shared/persistence.rb +26 -17
  65. data/lib/neo4j/shared/property.rb +5 -2
  66. data/lib/neo4j/shared/query_factory.rb +4 -5
  67. data/lib/neo4j/shared/type_converters.rb +8 -9
  68. data/lib/neo4j/shared/validations.rb +1 -5
  69. data/lib/neo4j/tasks/migration.rake +83 -2
  70. data/lib/neo4j/version.rb +1 -1
  71. data/lib/rails/generators/neo4j/migration/migration_generator.rb +14 -0
  72. data/lib/rails/generators/neo4j/migration/templates/migration.erb +9 -0
  73. data/lib/rails/generators/neo4j/model/model_generator.rb +1 -3
  74. data/lib/rails/generators/neo4j_generator.rb +1 -0
  75. data/neo4j.gemspec +3 -3
  76. metadata +58 -65
  77. data/bin/rake +0 -17
  78. data/lib/neo4j/shared/permitted_attributes.rb +0 -28
@@ -0,0 +1,10 @@
1
+ module Neo4j
2
+ module Migrations
3
+ extend ActiveSupport::Autoload
4
+ autoload :Helpers
5
+ autoload :MigrationFile
6
+ autoload :Base
7
+ autoload :Runner
8
+ autoload :SchemaMigration
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ module Neo4j
2
+ module Migrations
3
+ class Base < ::Neo4j::Migration
4
+ include Neo4j::Migrations::Helpers
5
+ include Neo4j::Migrations::Helpers::Schema
6
+ include Neo4j::Migrations::Helpers::IdProperty
7
+ include Neo4j::Migrations::Helpers::Relationships
8
+
9
+ def initialize(migration_id)
10
+ @migration_id = migration_id
11
+ end
12
+
13
+ def migrate(method)
14
+ ensure_schema_migration_constraint
15
+ Benchmark.realtime do
16
+ ActiveBase.run_transaction(transactions?) do
17
+ if method == :up
18
+ up
19
+ SchemaMigration.create!(migration_id: @migration_id)
20
+ else
21
+ down
22
+ SchemaMigration.find_by!(migration_id: @migration_id).destroy
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def up
29
+ fail NotImplementedError
30
+ end
31
+
32
+ def down
33
+ fail NotImplementedError
34
+ end
35
+
36
+ private
37
+
38
+ def ensure_schema_migration_constraint
39
+ SchemaMigration.first
40
+ Neo4j::Core::Label.wait_for_schema_changes(ActiveBase.current_session)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,101 @@
1
+ require 'benchmark'
2
+
3
+ module Neo4j
4
+ module Migrations
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :Schema
10
+ autoload :IdProperty
11
+ autoload :Relationships
12
+
13
+ PROPERTY_ALREADY_DEFINED = 'Property `%{new_property}` is already defined in `%{label}`. '\
14
+ 'To overwrite, call `remove_property(:%{label}, :%{new_property})` before this method.'.freeze
15
+
16
+ def remove_property(label, property)
17
+ by_label(label).remove(n: property).exec
18
+ end
19
+
20
+ def rename_property(label, old_property, new_property)
21
+ fail Neo4j::MigrationError, format(PROPERTY_ALREADY_DEFINED, new_property: new_property, label: label) if property_exists?(label, new_property)
22
+ by_label(label).set("n.#{new_property} = n.#{old_property}")
23
+ .remove("n.#{old_property}").exec
24
+ end
25
+
26
+ def drop_nodes(label)
27
+ query.match(n: label)
28
+ .optional_match('(n)-[r]-()')
29
+ .delete(:r, :n).exec
30
+ end
31
+
32
+ def add_labels(label, new_labels)
33
+ by_label(label).set(n: new_labels).exec
34
+ end
35
+
36
+ def add_label(label, new_label)
37
+ add_labels(label, [new_label])
38
+ end
39
+
40
+ def remove_labels(label, labels_to_remove)
41
+ by_label(label).remove(n: labels_to_remove).exec
42
+ end
43
+
44
+ def remove_label(label, label_to_remove)
45
+ remove_labels(label, [label_to_remove])
46
+ end
47
+
48
+ def rename_label(old_label, new_label)
49
+ by_label(old_label).set(n: new_label).remove(n: old_label).exec
50
+ end
51
+
52
+ def execute(string, params = {})
53
+ ActiveBase.query(string, params).to_a
54
+ end
55
+
56
+ def say_with_time(message)
57
+ say(message)
58
+ result = nil
59
+ time = Benchmark.measure { result = yield }
60
+ say format('%.4fs', time.real), :subitem
61
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
62
+ result
63
+ end
64
+
65
+ def say(message, subitem = false)
66
+ output "#{subitem ? ' ->' : '--'} #{message}"
67
+ end
68
+
69
+ def query(*args)
70
+ ActiveBase.new_query(*args)
71
+ end
72
+
73
+ protected
74
+
75
+ def transactions?
76
+ self.class.transaction?
77
+ end
78
+
79
+ private
80
+
81
+ def property_exists?(label, property)
82
+ by_label(label).where("EXISTS(n.#{property})").return(:n).any?
83
+ end
84
+
85
+ def by_label(label, options = {})
86
+ symbol = options[:symbol] || :n
87
+ query.match(symbol => label)
88
+ end
89
+
90
+ module ClassMethods
91
+ def disable_transactions!
92
+ @disable_transactions = true
93
+ end
94
+
95
+ def transaction?
96
+ !@disable_transactions
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,75 @@
1
+ module Neo4j
2
+ module Migrations
3
+ module Helpers
4
+ module IdProperty
5
+ extend ActiveSupport::Concern
6
+
7
+ def populate_id_property(label)
8
+ model = label.to_s.constantize
9
+ max_per_batch = (ENV['MAX_PER_BATCH'] || default_max_per_batch).to_i
10
+
11
+ last_time_taken = nil
12
+
13
+ until (nodes_left = idless_count(label, model.primary_key)) == 0
14
+ print_status(last_time_taken, max_per_batch, nodes_left)
15
+
16
+ count = [nodes_left, max_per_batch].min
17
+ last_time_taken = Benchmark.realtime do
18
+ max_per_batch = id_batch_set(label, model.primary_key, Array.new(count) { new_id_for(model) }, count)
19
+ end
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def idless_count(label, id_property)
26
+ query.match(n: label).where("NOT EXISTS(n.#{id_property})").pluck('COUNT(n) AS ids').first
27
+ end
28
+
29
+ def id_batch_set(label, id_property, new_ids, count)
30
+ tx = ActiveBase.new_transaction
31
+
32
+ execute("MATCH (n:`#{label}`) WHERE NOT EXISTS(n.#{id_property})
33
+ with COLLECT(n) as nodes, #{new_ids} as ids
34
+ FOREACH(i in range(0,#{count - 1})|
35
+ FOREACH(node in [nodes[i]]|
36
+ SET node.#{id_property} = ids[i]))
37
+ RETURN distinct(true)
38
+ LIMIT #{count}")
39
+
40
+ count
41
+ rescue Neo4j::Server::CypherResponse::ResponseError, Faraday::TimeoutError
42
+ new_max_per_batch = (max_per_batch * 0.8).round
43
+ output "Error querying #{max_per_batch} nodes. Trying #{new_max_per_batch}"
44
+ new_max_per_batch
45
+ ensure
46
+ tx.close
47
+ end
48
+
49
+ def print_status(last_time_taken, max_per_batch, nodes_left)
50
+ time_per_node = last_time_taken / max_per_batch if last_time_taken
51
+ message = if time_per_node
52
+ eta_seconds = (nodes_left * time_per_node).round
53
+ "#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)"
54
+ else
55
+ 'Running first batch...'
56
+ end
57
+
58
+ output message
59
+ end
60
+
61
+ def default_max_per_batch
62
+ 900
63
+ end
64
+
65
+ def new_id_for(model)
66
+ if model.id_property_info[:type][:auto]
67
+ SecureRandom.uuid
68
+ else
69
+ model.new.send(model.id_property_info[:type][:on])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,66 @@
1
+ module Neo4j
2
+ module Migrations
3
+ module Helpers
4
+ module Relationships
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_MAX_PER_BATCH = 1000
8
+
9
+ def change_relations_style(relationships, old_style, new_style, params = {})
10
+ relationships.each do |rel|
11
+ relabel_relation(relationship_style(rel, old_style), relationship_style(rel, new_style), params)
12
+ end
13
+ end
14
+
15
+ def relabel_relation(old_name, new_name, params = {})
16
+ relation_query = match_relation(old_name, params)
17
+
18
+ max_per_batch = (ENV['MAX_PER_BATCH'] || DEFAULT_MAX_PER_BATCH).to_i
19
+
20
+ count = count_relations(relation_query)
21
+ output "Indexing #{count} #{old_name}s into #{new_name}..."
22
+ while count > 0
23
+ relation_query.create("(a)-[r2:`#{new_name}`]->(b)").set('r2 = r').with(:r).limit(max_per_batch).delete(:r).exec
24
+ count = count_relations(relation_query)
25
+ output "... #{count} #{old_name}'s left to go.." if count > 0
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def match_relation(label, params = {})
32
+ from = params[:from] ? "(a:`#{params[:from]}`)" : '(a)'
33
+ to = params[:to] ? "(b:`#{params[:to]}`)" : '(b)'
34
+ relation = arrow_cypher(label, params[:direction])
35
+
36
+ query.match("#{from}#{relation}#{to}")
37
+ end
38
+
39
+ def arrow_cypher(label, direction)
40
+ case direction
41
+ when :in
42
+ "<-[r:`#{label}`]-"
43
+ when :both
44
+ "<-[r:`#{label}`]->"
45
+ else
46
+ "-[r:`#{label}`]->"
47
+ end
48
+ end
49
+
50
+ def count_relations(query)
51
+ query.pluck('COUNT(r)').first
52
+ end
53
+
54
+ def relationship_style(relationship, format)
55
+ case format.to_s
56
+ when 'lower_hashtag' then "##{relationship.downcase}"
57
+ when 'lower' then relationship.downcase
58
+ when 'upper' then relationship.upcase
59
+ else
60
+ fail("Invalid relationship type style `#{format}`.")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ module Neo4j
2
+ module Migrations
3
+ module Helpers
4
+ module Schema
5
+ extend ActiveSupport::Concern
6
+ MISSING_CONSTRAINT_OR_INDEX = 'No such %{type} for %{label}#%{property}'.freeze
7
+ DUPLICATE_CONSTRAINT_OR_INDEX = 'Duplicate %{type} for %{label}#%{property}'.freeze
8
+
9
+ def add_constraint(label, property, options = {})
10
+ force = options[:force] || false
11
+ label_object = ActiveBase.label_object(label)
12
+ fail_duplicate_constraint_or_index!(:constraint, label, property) if !force && label_object.constraint?(property)
13
+ label_object.create_constraint(property, type: :uniqueness)
14
+ end
15
+
16
+ def add_index(label, property, options = {})
17
+ force = options[:force] || false
18
+ label_object = ActiveBase.label_object(label)
19
+ fail_duplicate_constraint_or_index!(:index, label, property) if !force && label_object.index?(property)
20
+ label_object.create_index(property)
21
+ end
22
+
23
+ def force_add_index(label, property)
24
+ add_index(label, property)
25
+ end
26
+
27
+ def drop_constraint(label, property)
28
+ label_object = ActiveBase.label_object(label)
29
+ fail_missing_constraint_or_index!(:constraint, label, property) if !label_object.constraint?(property)
30
+ label_object.drop_constraint(property, type: :uniqueness)
31
+ end
32
+
33
+ def drop_index(label, property)
34
+ label_object = ActiveBase.label_object(label)
35
+ fail_missing_constraint_or_index!(:index, label, property) if !label_object.index?(property)
36
+ label_object.drop_index(property)
37
+ end
38
+
39
+ protected
40
+
41
+ def fail_missing_constraint_or_index!(type, label, property)
42
+ fail Neo4j::MigrationError,
43
+ format(MISSING_CONSTRAINT_OR_INDEX, type: type, label: label, property: property)
44
+ end
45
+
46
+ def fail_duplicate_constraint_or_index!(type, label, property)
47
+ fail Neo4j::MigrationError,
48
+ format(DUPLICATE_CONSTRAINT_OR_INDEX, type: type, label: label, property: property)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ module Neo4j
2
+ module Migrations
3
+ class MigrationFile
4
+ attr_reader :file_name, :symbol_name, :class_name, :version
5
+
6
+ def initialize(file_name)
7
+ @file_name = file_name
8
+ extract_data!
9
+ end
10
+
11
+ def create
12
+ require @file_name
13
+ class_name.constantize.new(@version)
14
+ end
15
+
16
+ private
17
+
18
+ def extract_data!
19
+ @version, @symbol_name = File.basename(@file_name, '.rb').split('_', 2)
20
+ @class_name = @symbol_name.camelize
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,110 @@
1
+ module Neo4j
2
+ module Migrations
3
+ class Runner
4
+ STATUS_TABLE_FORMAT = '%-10s %-20s %s'.freeze
5
+ SEPARATOR = '--------------------------------------------------'.freeze
6
+ FILE_MISSING = '**** file missing ****'.freeze
7
+ STATUS_TABLE_HEADER = ['Status'.freeze, 'Migration ID'.freeze, 'Migration Name'.freeze].freeze
8
+ UP_MESSAGE = 'up'.freeze
9
+ DOWN_MESSAGE = 'down'.freeze
10
+ MIGRATION_RUNNING = {up: 'running'.freeze, down: 'reverting'.freeze}.freeze
11
+ MIGRATION_DONE = {up: 'migrated'.freeze, down: 'reverted'.freeze}.freeze
12
+
13
+ def initialize
14
+ SchemaMigration.mapped_label.create_constraint(:migration_id, type: :unique)
15
+ @up_versions = SortedSet.new(SchemaMigration.all.pluck(:migration_id))
16
+ end
17
+
18
+ def all
19
+ migration_files.each do |migration_file|
20
+ next if up?(migration_file.version)
21
+ migrate(:up, migration_file)
22
+ end
23
+ end
24
+
25
+ def up(version)
26
+ migration_file = find_by_version!(version)
27
+ return if up?(version)
28
+ migrate(:up, migration_file)
29
+ end
30
+
31
+ def down(version)
32
+ migration_file = find_by_version!(version)
33
+ return unless up?(version)
34
+ migrate(:down, migration_file)
35
+ end
36
+
37
+ def rollback(steps)
38
+ @up_versions.to_a.reverse.first(steps).each do |version|
39
+ down(version)
40
+ end
41
+ end
42
+
43
+ def status
44
+ output STATUS_TABLE_FORMAT, *STATUS_TABLE_HEADER
45
+ output SEPARATOR
46
+ all_migrations.each do |version|
47
+ status = up?(version) ? UP_MESSAGE : DOWN_MESSAGE
48
+ migration_file = find_by_version(version)
49
+ migration_name = migration_file ? migration_file.class_name : FILE_MISSING
50
+ output STATUS_TABLE_FORMAT, status, version, migration_name
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def up?(version)
57
+ @up_versions.include?(version)
58
+ end
59
+
60
+ def migrate(direction, migration_file)
61
+ migration_message(direction, migration_file) do
62
+ migration = migration_file.create
63
+ migration.migrate(direction)
64
+ end
65
+ end
66
+
67
+ def migration_message(direction, migration)
68
+ output "== #{migration.version} #{migration.class_name}: #{MIGRATION_RUNNING[direction]}... ========="
69
+ yield
70
+ output "== #{migration.version} #{migration.class_name}: #{MIGRATION_DONE[direction]} ========="
71
+ end
72
+
73
+ def output(*string_format)
74
+ puts format(*string_format) unless !!ENV['MIGRATIONS_SILENCED']
75
+ end
76
+
77
+ def find_by_version!(version)
78
+ find_by_version(version) || fail(UnknownMigrationVersionError, "No such migration #{version}")
79
+ end
80
+
81
+ def find_by_version(version)
82
+ migration_files.find { |file| file.version == version }
83
+ end
84
+
85
+ def all_migrations
86
+ @up_versions + files_versions
87
+ end
88
+
89
+ def files_versions
90
+ migration_files.map(&:version)
91
+ end
92
+
93
+ def migration_files
94
+ files.map { |file_path| MigrationFile.new(file_path) }
95
+ end
96
+
97
+ def files
98
+ Dir[files_path].sort
99
+ end
100
+
101
+ def files_path
102
+ app_root.join('db', 'neo4j', 'migrate', '*.rb')
103
+ end
104
+
105
+ def app_root
106
+ defined?(Rails) ? Rails.root : Pathname.new('.')
107
+ end
108
+ end
109
+ end
110
+ end