neo4j_bolt 0.1.6 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e754e8e69d3794520280d568a879834a6ceb91bad419385fb68318959f8168c2
4
- data.tar.gz: 48ac0bab0ae453aa6d41ce6832271db2798b99f49baaa0307c0795a7d932264d
3
+ metadata.gz: 1ac7e327b2ece44cd23a4408c81c0a9a41d52faf277c24b56280a15f1b79aeec
4
+ data.tar.gz: a24414f311a40ff6cb79d18a18497200f79ed7e2a58e070b5e321a4dc31cc757
5
5
  SHA512:
6
- metadata.gz: dfabf88d8ec3a58298c87ab3a1662bcf43abe83a5bc6d2d948992389704a34c70f2fb5f21877adf7d2e826f39e1da5936764369de3ee1942e4466e6d54ea1acc
7
- data.tar.gz: f21d0f51b0a90083a79d5df951b76d8acf5c3bd55386fa3a10039f0ae107115927eea9bbe56ee3d0763b9e42e04f86181b4806f43d8f72f392b82f3538c01e29
6
+ metadata.gz: f716cd2380decb532dbe6954030e4ead5ea0a1edde1868922b81d2105801d7ed6bb9e5b35e3eb22f79ca9413408582bca6da44a9fa767ad6fa554b929df84436
7
+ data.tar.gz: 35f82570c1bcf2b56a61a17d9bbf189f2055386efd1c6f83cd3c60313069d26380ce050394665ede60ef9ea4eb27626bf7438b43a4070e361a2b9ed626c51893
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
7
  gem "rspec", "~> 3.0"
8
+ gem "gli"
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- neo4j_bolt (0.1.5)
4
+ neo4j_bolt (0.1.8)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  diff-lcs (1.5.0)
10
+ gli (2.21.0)
10
11
  rake (12.3.3)
11
12
  rspec (3.11.0)
12
13
  rspec-core (~> 3.11.0)
@@ -26,6 +27,7 @@ PLATFORMS
26
27
  ruby
27
28
 
28
29
  DEPENDENCIES
30
+ gli
29
31
  neo4j_bolt!
30
32
  rake (~> 12.0)
31
33
  rspec (~> 3.0)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Neo4jBolt
2
2
 
3
- A Neo4j/Bolt driver written in pure Ruby. Currently only supporting Neo4j 4.4.
3
+ A Neo4j/Bolt driver written in pure Ruby. Currently only supporting Neo4j 4.4. Caution: This gem is not feature complete, and also the documention is not complete yet.
4
4
 
5
5
  ## Installation
6
6
 
data/bin/neo4j_bolt ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "neo4j_bolt"
5
+ require "gli"
6
+
7
+ include Neo4jBolt
8
+
9
+ class App
10
+ extend GLI::App
11
+
12
+ program_desc 'run various Neo4j housekeeping tasks'
13
+ version Neo4jBolt::VERSION
14
+
15
+ flag [:v, :verbosity], :default_value => 0
16
+ flag [:h, :host], :default_value => 'localhost:7687'
17
+
18
+ pre do |global_options, command, options, args|
19
+ host = global_options[:host]
20
+ Neo4jBolt.bolt_host = host.split(':').first
21
+ Neo4jBolt.bolt_port = host.split(':').last.to_i
22
+ Neo4jBolt.bolt_verbosity = global_options[:verbosity].to_i
23
+ true
24
+ end
25
+
26
+ # --------------------------------------------
27
+
28
+ desc 'Console'
29
+ command :console do |c|
30
+ c.action do
31
+ require "irb"
32
+ IRB.start(__FILE__)
33
+ end
34
+ end
35
+
36
+ # --------------------------------------------
37
+
38
+ desc 'Dump database'
39
+ long_desc 'Dump all nodes and relationships.'
40
+ command :dump do |c|
41
+ c.flag [:o, :out_file], :default_value => '/dev/stdout'
42
+ c.action do |global_options, options|
43
+ File.open(options[:out_file], 'w') do |f|
44
+ dump_database(f)
45
+ end
46
+ end
47
+ end
48
+
49
+ # --------------------------------------------
50
+
51
+ desc 'Load database dump'
52
+ long_desc 'Load nodes and relationships from a database dump.'
53
+ command :load do |c|
54
+ # c.flag [:i, :in_file], :desc => 'input path', :required => true
55
+ c.switch [:f, :force], :default_value => false, :desc => 'force appending nodes even if the database is not empty'
56
+ c.action do |global_options, options, args|
57
+ help_now!('input path is required') if args.empty?
58
+ path = args.shift
59
+ File.open(path, 'r') do |f|
60
+ load_database_dump(f, force_append: options[:force])
61
+ end
62
+ end
63
+ end
64
+
65
+ # --------------------------------------------
66
+
67
+ desc 'Clear database'
68
+ long_desc 'Clear all nodes and relationships'
69
+ command :clear do |c|
70
+ c.switch [:srsly], :required => true, :negatable => false, :desc => 'Specify --srsly to really clear the database'
71
+ c.action do |global_options, options, args|
72
+ if options[:srsly]
73
+ neo4j_query("MATCH (n) DETACH DELETE n;")
74
+ else
75
+ STDERR.puts "Doing nothing unless you specify --srsly."
76
+ end
77
+ end
78
+ end
79
+
80
+ # --------------------------------------------
81
+
82
+ desc 'Visualize database'
83
+ long_desc 'Generate a GraphViz-formatted visual representation of the database'
84
+ command :visualize do |c|
85
+ c.flag [:o, :out_file], :default_value => '/dev/stdout'
86
+ c.switch [:p, :properties], :default_value => false, :desc => 'include properties'
87
+ c.action do |global_options, options|
88
+ File.open(options[:out_file], 'w') do |f|
89
+ all_labels = Set.new()
90
+
91
+ TR = {'String' => 'string',
92
+ 'Array' => 'list',
93
+ 'Hash' => 'hash',
94
+ 'TrueClass' => 'true',
95
+ 'FalseClass' => 'false',
96
+ 'NilClass' => 'null',
97
+ 'Integer' => 'int',
98
+ 'Float' => 'float'
99
+ }
100
+
101
+ neo4j_query("MATCH (n) RETURN DISTINCT labels(n) AS labels") do |entry|
102
+ labels = entry['labels']
103
+ if labels.size != 1
104
+ raise "multiple labels per node not supported yet: #{labels.join(' ')}"
105
+ end
106
+ all_labels << labels.first
107
+ end
108
+
109
+ all_relationships = Set.new()
110
+
111
+ neo4j_query("MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS la, type(r) AS t, labels(b) AS lb;") do |entry|
112
+ la = entry['la'].first
113
+ t = entry['t']
114
+ lb = entry['lb'].first
115
+ all_relationships << "#{la}/#{t}/#{lb}"
116
+ end
117
+
118
+ properties_for_label = {}
119
+ counts_for_label = {}
120
+
121
+ all_labels.to_a.sort.each do |label|
122
+ properties_for_label[label] ||= {}
123
+ if options[:properties]
124
+ neo4j_query("MATCH (n:#{label}) RETURN n") do |entry|
125
+ counts_for_label[label] ||= 0
126
+ counts_for_label[label] += 1
127
+ node = entry['n']
128
+ node.each_pair do |key, value|
129
+ properties_for_label[label][key] ||= {:classes => Set.new()}
130
+ properties_for_label[label][key][:classes] << value.class
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ all_relationships.each do |s|
137
+ properties_for_label[s] ||= {}
138
+ parts = s.split('/')
139
+ la = parts[0]
140
+ type = parts[1]
141
+ lb = parts[2]
142
+ if options[:properties]
143
+ neo4j_query("MATCH (a:#{la})-[r:#{type}]->(b:#{lb}) RETURN r") do |entry|
144
+ counts_for_label[s] ||= 0
145
+ counts_for_label[s] += 1
146
+ rel = entry['r']
147
+ rel.each_pair do |key, value|
148
+ properties_for_label[s][key] ||= {:classes => Set.new()}
149
+ properties_for_label[s][key][:classes] << value.class
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ dot = StringIO.open do |io|
156
+ io.puts "digraph {"
157
+ io.puts "graph [fontname = Helvetica, fontsize = 10, nodesep = 0.2, ranksep = 0.3];"
158
+ io.puts "node [fontname = Helvetica, fontsize = 10, shape = none, margin = 0];"
159
+ io.puts "edge [fontname = Helvetica, fontsize = 10, arrowsize = 0.6, color = \"#000000\"];"
160
+ io.puts 'rankdir=LR;'
161
+ io.puts 'splines=true;'
162
+ properties_for_label.keys.sort.each do |lbl|
163
+ label = "<<table valign='top' align='left' border='0' cellborder='0' cellspacing='0' cellpadding='4'>"
164
+ label += "<tr><td border='1' bgcolor='#fce94f' valign='top' align='left' colspan='2'><b>#{lbl}</b>"
165
+ if options[:properties]
166
+ label += " <i>(#{counts_for_label[lbl]})</i>"
167
+ end
168
+ label += "</td></tr>"
169
+ properties_for_label[lbl].keys.sort.each do |key|
170
+ label += "<tr>"
171
+ label += "<td border='1' valign='top' align='left' colspan='1'>#{key}</td>"
172
+ label += "<td border='1' valign='top' align='left' colspan='1'>#{properties_for_label[lbl][key][:classes].to_a.map { |x| TR[x.to_s] || x.to_s }.sort.join(' / ')}</td>"
173
+ label += "</tr>"
174
+ end
175
+ label += "</table>>"
176
+ io.puts "\"#{lbl}\" [label = #{label}, pencolor = \"#000000\"];"
177
+ end
178
+ all_relationships.each do |s|
179
+ parts = s.split('/')
180
+ la = parts[0]
181
+ type = parts[1]
182
+ lb = parts[2]
183
+
184
+ label = "<<table valign='top' align='left' border='0' cellborder='0' cellspacing='0' cellpadding='4'>"
185
+ label += "<tr><td border='1' bgcolor='#d3d7cf' valign='top' align='left' colspan='2'><b>#{type}</b>"
186
+ if options[:properties]
187
+ label += " <i>(#{counts_for_label[s]})</i>"
188
+ end
189
+ label += "</td></tr>"
190
+ (properties_for_label[s] || {}).keys.sort.each do |key|
191
+ label += "<tr>"
192
+ label += "<td border='1' valign='top' align='left' colspan='1'>#{key}</td>"
193
+ label += "<td border='1' valign='top' align='left' colspan='1'>#{properties_for_label[s][key][:classes].to_a.map { |x| TR[x.to_s] || x.to_s }.sort.join(' / ')}</td>"
194
+ label += "</tr>"
195
+ end
196
+ label += "</table>>"
197
+ io.puts "\"#{s}\" [label = #{label}, pencolor = \"#000000\"];"
198
+
199
+ io.puts "\"#{la}\" -> \"#{s}\";"
200
+ io.puts "\"#{s}\" -> \"#{lb}\";"
201
+ end
202
+
203
+ io.puts "}"
204
+ io.string
205
+ end
206
+ f.puts dot
207
+ end
208
+ end
209
+ end
210
+
211
+ end
212
+
213
+ exit App.run(ARGV)
@@ -1,3 +1,3 @@
1
1
  module Neo4jBolt
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.8"
3
3
  end
data/lib/neo4j_bolt.rb CHANGED
@@ -5,12 +5,13 @@ require 'yaml'
5
5
 
6
6
  module Neo4jBolt
7
7
  class << self
8
- attr_accessor :bolt_host, :bolt_port
8
+ attr_accessor :bolt_host, :bolt_port, :bolt_verbosity
9
9
  end
10
10
  self.bolt_host = 'localhost'
11
11
  self.bolt_port = 7687
12
+ self.bolt_verbosity = 0
12
13
 
13
- NEO4J_DEBUG = 0
14
+ CONSTRAINT_INDEX_PREFIX = 'neo4j_bolt_'
14
15
 
15
16
  module ServerState
16
17
  DISCONNECTED = 0
@@ -114,7 +115,7 @@ module Neo4jBolt
114
115
  end
115
116
  chunk = @socket.read(length).unpack('C*')
116
117
  @data += chunk
117
- if NEO4J_DEBUG >= 3
118
+ if Neo4jBolt.bolt_verbosity >= 3
118
119
  dump()
119
120
  end
120
121
  end
@@ -752,7 +753,7 @@ module Neo4jBolt
752
753
  end
753
754
 
754
755
  def run_query(query, data = {}, &block)
755
- if NEO4J_DEBUG >= 1
756
+ if Neo4jBolt.bolt_verbosity >= 1
756
757
  STDERR.puts query
757
758
  STDERR.puts data.to_json
758
759
  STDERR.puts '-' * 40
@@ -790,7 +791,7 @@ module Neo4jBolt
790
791
  keys.each.with_index do |key, i|
791
792
  entry[key] = fix_value(data[:data][i])
792
793
  end
793
- if NEO4J_DEBUG >= 1
794
+ if Neo4jBolt.bolt_verbosity >= 1
794
795
  STDERR.puts ">>> #{entry.to_json}"
795
796
  STDERR.puts '-' * 40
796
797
  end
@@ -830,6 +831,50 @@ module Neo4jBolt
830
831
  end
831
832
  rows.first
832
833
  end
834
+
835
+ def setup_constraints_and_indexes(constraints, indexes)
836
+ wanted_constraints = Set.new()
837
+ wanted_indexes = Set.new()
838
+ # STDERR.puts "Setting up constraints and indexes..."
839
+ constraints.each do |constraint|
840
+ unless constraint =~ /\w+\/\w+/
841
+ raise "Unexpected constraint format: #{constraint}"
842
+ end
843
+ constraint_name = "#{CONSTRAINT_INDEX_PREFIX}#{constraint.gsub('/', '_')}"
844
+ wanted_constraints << constraint_name
845
+ label = constraint.split('/').first
846
+ property = constraint.split('/').last
847
+ query = "CREATE CONSTRAINT #{constraint_name} IF NOT EXISTS FOR (n:#{label}) REQUIRE n.#{property} IS UNIQUE"
848
+ # STDERR.puts query
849
+ neo4j_query(query)
850
+ end
851
+ indexes.each do |index|
852
+ unless index =~ /\w+\/\w+/
853
+ raise "Unexpected index format: #{index}"
854
+ end
855
+ index_name = "#{CONSTRAINT_INDEX_PREFIX}#{index.gsub('/', '_')}"
856
+ wanted_indexes << index_name
857
+ label = index.split('/').first
858
+ property = index.split('/').last
859
+ query = "CREATE INDEX #{index_name} IF NOT EXISTS FOR (n:#{label}) ON (n.#{property})"
860
+ # STDERR.puts query
861
+ neo4j_query(query)
862
+ end
863
+ neo4j_query("SHOW ALL CONSTRAINTS").each do |row|
864
+ next unless row['name'].index(CONSTRAINT_INDEX_PREFIX) == 0
865
+ next if wanted_constraints.include?(row['name'])
866
+ query = "DROP CONSTRAINT #{row['name']}"
867
+ # STDERR.puts query
868
+ neo4j_query(query)
869
+ end
870
+ neo4j_query("SHOW ALL INDEXES").each do |row|
871
+ next unless row['name'].index(CONSTRAINT_INDEX_PREFIX) == 0
872
+ next if wanted_indexes.include?(row['name']) || wanted_constraints.include?(row['name'])
873
+ query = "DROP INDEX #{row['name']}"
874
+ # STDERR.puts query
875
+ neo4j_query(query)
876
+ end
877
+ end
833
878
  end
834
879
 
835
880
  def transaction(&block)
@@ -864,7 +909,7 @@ module Neo4jBolt
864
909
  end
865
910
  end
866
911
 
867
- def dump_database(&block)
912
+ def dump_database(io)
868
913
  tr_id = {}
869
914
  id = 0
870
915
  neo4j_query("MATCH (n) RETURN n ORDER BY ID(n);") do |row|
@@ -874,7 +919,7 @@ module Neo4jBolt
874
919
  :labels => row['n'].labels,
875
920
  :properties => row['n']
876
921
  }
877
- yield "n #{node.to_json}"
922
+ io.puts "n #{node.to_json}"
878
923
  id += 1
879
924
  end
880
925
  neo4j_query("MATCH ()-[r]->() RETURN r;") do |row|
@@ -884,14 +929,99 @@ module Neo4jBolt
884
929
  :type => row['r'].type,
885
930
  :properties => row['r']
886
931
  }
887
- yield "r #{rel.to_json}"
932
+ io.puts "r #{rel.to_json}"
888
933
  end
889
934
  end
890
935
 
936
+ def load_database_dump(io, force_append: false)
937
+ unless force_append
938
+ transaction do
939
+ node_count = neo4j_query_expect_one('MATCH (n) RETURN COUNT(n) as count;')['count']
940
+ unless node_count == 0
941
+ raise "There are nodes in this database, exiting now."
942
+ end
943
+ end
944
+ end
945
+ n_count = 0
946
+ r_count = 0
947
+ node_tr = {}
948
+ node_batch_by_label = {}
949
+ relationship_batch_by_type = {}
950
+ io.each_line do |line|
951
+ line.strip!
952
+ next if line.empty?
953
+ if line[0] == 'n'
954
+ line = line[2, line.size - 2]
955
+ node = JSON.parse(line)
956
+ label_key = node['labels'].sort.join('/')
957
+ node_batch_by_label[label_key] ||= []
958
+ node_batch_by_label[label_key] << node
959
+ elsif line[0] == 'r'
960
+ line = line[2, line.size - 2]
961
+ relationship = JSON.parse(line)
962
+ relationship_batch_by_type[relationship['type']] ||= []
963
+ relationship_batch_by_type[relationship['type']] << relationship
964
+ else
965
+ STDERR.puts "Invalid entry: #{line}"
966
+ exit(1)
967
+ end
968
+ end
969
+ node_batch_by_label.each_pair do |label_key, batch|
970
+ while !batch.empty? do
971
+ slice = []
972
+ json_size = 0
973
+ while (!batch.empty?) && json_size < 0x20000 && slice.size < 256
974
+ x = batch.shift
975
+ slice << x
976
+ json_size += x.to_json.size
977
+ end
978
+ ids = neo4j_query(<<~END_OF_QUERY, {:properties => slice.map { |x| x['properties']}})
979
+ UNWIND $properties AS props
980
+ CREATE (n:#{slice.first['labels'].join(':')})
981
+ SET n = props
982
+ RETURN ID(n) AS id;
983
+ END_OF_QUERY
984
+ slice.each.with_index do |node, i|
985
+ node_tr[node['id']] = ids[i]['id']
986
+ end
987
+ n_count += slice.size
988
+ STDERR.print "\rLoaded #{n_count} nodes, #{r_count} relationships..."
989
+ end
990
+ end
991
+ relationship_batch_by_type.each_pair do |rel_type, batch|
992
+ batch.each_slice(256) do |slice|
993
+ slice.map! do |rel|
994
+ rel['from'] = node_tr[rel['from']]
995
+ rel['to'] = node_tr[rel['to']]
996
+ rel
997
+ end
998
+ count = neo4j_query_expect_one(<<~END_OF_QUERY, {:slice => slice})['count_r']
999
+ UNWIND $slice AS props
1000
+ MATCH (from), (to) WHERE ID(from) = props.from AND ID(to) = props.to
1001
+ CREATE (from)-[r:#{rel_type}]->(to)
1002
+ SET r = props.properties
1003
+ RETURN COUNT(r) AS count_r, COUNT(from) AS count_from, COUNT(to) AS count_to;
1004
+ END_OF_QUERY
1005
+ if count != slice.size
1006
+ raise "Ooops... expected #{slice.size} relationships, got #{count}."
1007
+ end
1008
+ r_count += slice.size
1009
+ STDERR.print "\rLoaded #{n_count} nodes, #{r_count} relationships..."
1010
+ end
1011
+ end
1012
+
1013
+ STDERR.puts
1014
+ end
1015
+
891
1016
  def cleanup_neo4j
892
1017
  if @bolt_socket
893
1018
  @bolt_socket.disconnect()
894
1019
  @bolt_socket = nil
895
1020
  end
896
1021
  end
1022
+
1023
+ def setup_constraints_and_indexes(constraints, indexes)
1024
+ @bolt_socket ||= BoltSocket.new()
1025
+ @bolt_socket.setup_constraints_and_indexes(constraints, indexes)
1026
+ end
897
1027
  end
data/neo4j_bolt.gemspec CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
23
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
24
  end
25
- spec.bindir = "exe"
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.bindir = "bin"
26
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.add_development_dependency "rspec", "~> 3.2"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neo4j_bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Specht
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-28 00:00:00.000000000 Z
11
+ date: 2022-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -27,7 +27,10 @@ dependencies:
27
27
  description:
28
28
  email:
29
29
  - micha.specht@gmail.com
30
- executables: []
30
+ executables:
31
+ - console
32
+ - neo4j_bolt
33
+ - setup
31
34
  extensions: []
32
35
  extra_rdoc_files: []
33
36
  files:
@@ -40,6 +43,7 @@ files:
40
43
  - README.md
41
44
  - Rakefile
42
45
  - bin/console
46
+ - bin/neo4j_bolt
43
47
  - bin/setup
44
48
  - lib/neo4j_bolt.rb
45
49
  - lib/neo4j_bolt/version.rb