activegraph 11.1.0.beta.1 → 11.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4aade7db7557b1222b78e048550606354b0707a60c9375359a4f945b702763c2
4
- data.tar.gz: a22b396cdc491118ecbaf947ac9e00d9ea088fe0df5afa51a3a38b7f085db9ff
3
+ metadata.gz: 4b0ec846225c753d7a2dc2bc3923c95f52dec582324559df182f23df633f564c
4
+ data.tar.gz: 1205c8eaf374112629a173f55e2c9a5fae42324073b2f540815eb984cb87e324
5
5
  SHA512:
6
- metadata.gz: 663bc1323338cc7261cb2281b5ad6a86d794baed1fd2dc82a08f8d1b5eae593a151927529330425d8ffdc71adb17e8a7c70aa3872c63641cd2d77dcfe46016cc
7
- data.tar.gz: c894eff95679b5982e418dccff5deb160ff41a46e661bee2f8278b230ab59fe20befc8aed83b8144dd112485df98d61c6fa1f60d226f63a1df3db6cd34b4a9a4
6
+ metadata.gz: '07091d4bf26b0b2bfb4d6313668e358eb78f8eb6afb7244063a2293a3596963687debf7fe6e72177ba388ac008a9452a8bd580fb48064575354e0e208765f475'
7
+ data.tar.gz: '09dc2f6eaf0126d26b76aba057b9b7e77e0b633f0b13ab99964ed64fd64e7421f22eaa470d9abd0fca4bd896e122ca938814683bc0c618e7d37d2ded400c00de'
data/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file.
3
3
  This file should follow the standards specified on [http://keepachangelog.com/]
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
 
6
+ ## [11.2.0] 2023-02-06
7
+
8
+ ## Added
9
+
10
+ - support for neo4j:schema:dump and :load for neo4j 4 and 5. No automatic migration of schema on major neo4j upgrades. Schema must be regenerated with neo4j:schema:dump or manually adjusted to new syntax on those upgrades.
11
+
12
+ ## [11.1.0] 2023-01-10
13
+
14
+ ## Added
15
+
16
+ - support for pure ruby driver, neo4j-ruby-driver 4.4
17
+ - support for neo4j 5
18
+
19
+ ## Fixed
20
+
21
+ - moved migrations into their own transactions
22
+ - Uniqueness within a scope defined by a proc
23
+ - Fixed lost @rel_var when branching QueryProxy
24
+
6
25
  ## [11.0.2] 2021-11-05
7
26
 
8
27
  ## Fixed
data/README.md CHANGED
@@ -45,21 +45,22 @@ Neo4j.rb v4.1.0 was released in January of 2015. Its changes are outlined [here]
45
45
 
46
46
  ## Neo4j version support
47
47
 
48
- | **Neo4j Version** | v2.x | v3.x | >= v4.x | >= 7.0.3 | activegraph 10 | activegraph 11 (jRuby only) |
49
- |-------------------|------|-------|---------|----------|------------------|-----------------------------|
50
- | 1.9.x | Yes | No | No | No | No | No
51
- | 2.0.x | No | Yes | No | No | No | No
52
- | 2.1.x | No | Yes | Yes * | Yes | No | No
53
- | 2.2.x | No | No | Yes | Yes | No | No
54
- | 2.3.x | No | No | Yes | Yes | No | No
55
- | 3.0, 3.1, 3.3 | No | No | No | Yes | No | No
56
- | 3.4 | No | No | No | Yes | Yes | No
57
- | 3.5 | No | No | No | Yes | Yes | Yes
58
- | 4.0 | No | No | No | No | Yes | Yes
59
- | 4.1 | No | No | No | No | No | Yes
60
- | 4.2 | No | No | No | No | No | Yes
61
- | 4.3 | No | No | No | No | No | Yes
62
- | 4.4 | No | No | No | No | No | Yes
48
+ | **Neo4j Version** | v2.x | v3.x | >= v4.x | >= 7.0.3 | activegraph 10 | activegraph 11.1 |
49
+ |-------------------|------|-------|---------|----------|------------------|------------------|
50
+ | 1.9.x | Yes | No | No | No | No | No
51
+ | 2.0.x | No | Yes | No | No | No | No
52
+ | 2.1.x | No | Yes | Yes * | Yes | No | No
53
+ | 2.2.x | No | No | Yes | Yes | No | No
54
+ | 2.3.x | No | No | Yes | Yes | No | No
55
+ | 3.0, 3.1, 3.3 | No | No | No | Yes | No | No
56
+ | 3.4 | No | No | No | Yes | Yes | No
57
+ | 3.5 | No | No | No | Yes | Yes | Yes
58
+ | 4.0 | No | No | No | No | Yes | Yes
59
+ | 4.1 | No | No | No | No | No | Yes
60
+ | 4.2 | No | No | No | No | No | Yes
61
+ | 4.3 | No | No | No | No | No | Yes
62
+ | 4.4 | No | No | No | No | No | Yes
63
+ | 5.x | No | No | No | No | No | Yes
63
64
 
64
65
  `*` Neo4j.rb >= 4.x doesn't support Neo4j versions before 2.1.5. To use 2.1.x you should upgrade to a version >= 2.1.5
65
66
 
data/activegraph.gemspec CHANGED
@@ -33,7 +33,7 @@ DESCRIPTION
33
33
  s.add_dependency('activemodel', '>= 4.0')
34
34
  s.add_dependency('activesupport', '>= 4.0')
35
35
  s.add_dependency('i18n', '!= 1.8.8') # https://github.com/jruby/jruby/issues/6547
36
- s.add_dependency('neo4j-ruby-driver', '>= 4.4.0.alpha.7')
36
+ s.add_dependency('neo4j-ruby-driver', '>= 4.4.1')
37
37
  s.add_dependency('orm_adapter', '~> 0.5.0')
38
38
  s.add_dependency('sorted_set')
39
39
  s.add_development_dependency('guard')
@@ -2,20 +2,33 @@ module ActiveGraph
2
2
  module Core
3
3
  class Label
4
4
  attr_reader :name
5
+ delegate :version?, to: ActiveGraph::Base
5
6
 
6
7
  def initialize(name)
7
8
  @name = name
8
9
  end
9
10
 
10
- def create_index(property, options = {})
11
+ def create_index(*properties, **options)
11
12
  validate_index_options!(options)
12
- properties = property.is_a?(Array) ? property.join(',') : property
13
- schema_query("CREATE INDEX ON :`#{@name}`(#{properties})")
13
+ if version?('>=4.4')
14
+ properties = properties.map { |p| "l.#{p}" }
15
+ fragment = "FOR (l:`#{@name}`) ON"
16
+ else
17
+ fragment = "ON :`#{@name}`"
18
+ end
19
+ schema_query("CREATE INDEX #{fragment} (#{properties.join('.')})")
14
20
  end
15
21
 
16
22
  def drop_index(property, options = {})
17
23
  validate_index_options!(options)
18
- schema_query("DROP INDEX ON :`#{@name}`(#{property})")
24
+ if version?('<4.3')
25
+ schema_query("DROP INDEX ON :`#{@name}`(#{property})")
26
+ else
27
+ schema_query("SHOW INDEXES YIELD * WHERE labelsOrTypes = $labels AND properties = $properties",
28
+ labels: [@name], properties: [property]).each do |record|
29
+ schema_query("DROP INDEX #{record[:name]}")
30
+ end
31
+ end
19
32
  end
20
33
 
21
34
  # Creates a neo4j constraint on a property
@@ -27,7 +40,8 @@ module ActiveGraph
27
40
  def create_constraint(property, constraints)
28
41
  cypher = case constraints[:type]
29
42
  when :unique, :uniqueness
30
- "CREATE CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
43
+ _for, _require = version?('>=4.4') ? %w[FOR REQUIRE] : %w[ON ASSERT]
44
+ "CREATE CONSTRAINT #{_for} (n:`#{name}`) #{_require} n.`#{property}` IS UNIQUE"
31
45
  else
32
46
  fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)"
33
47
  end
@@ -46,15 +60,20 @@ module ActiveGraph
46
60
  # label.drop_constraint(:name, {type: :unique})
47
61
  #
48
62
  def drop_constraint(property, constraint)
49
- cypher = case constraint[:type]
50
- when :unique, :uniqueness
51
- "n.`#{property}` IS UNIQUE"
52
- when :exists
53
- "exists(n.`#{property}`)"
54
- else
55
- fail "Not supported constraint #{constraint.inspect}"
56
- end
57
- schema_query("DROP CONSTRAINT ON (n:`#{name}`) ASSERT #{cypher}")
63
+ return drop_constraint42(property, constraint) if version?('<4.3')
64
+ type = case constraint[:type]
65
+ when :unique, :uniqueness
66
+ 'UNIQUENESS'
67
+ when :exists
68
+ 'NODE_PROPERTY_EXISTENCE'
69
+ else
70
+ fail "Not supported constraint #{constraint.inspect}"
71
+ end
72
+ schema_query(
73
+ 'SHOW CONSTRAINTS YIELD * WHERE type = $type AND labelsOrTypes = $labels AND properties = $properties',
74
+ type: type, labels: [name], properties: [property]).first[:name].tap do |constraint_name|
75
+ schema_query("DROP CONSTRAINT #{constraint_name}")
76
+ end
58
77
  end
59
78
 
60
79
  def drop_uniqueness_constraint(property, options = {})
@@ -105,7 +124,12 @@ module ActiveGraph
105
124
  def drop_indexes
106
125
  indexes.each do |definition|
107
126
  begin
108
- ActiveGraph::Base.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
127
+ ActiveGraph::Base.query(
128
+ if definition[:name]
129
+ "DROP INDEX #{definition[:name]}"
130
+ else
131
+ "DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})"
132
+ end)
109
133
  rescue Neo4j::Driver::Exceptions::DatabaseException
110
134
  # This will error on each constraint. Ignore and continue.
111
135
  next
@@ -114,7 +138,9 @@ module ActiveGraph
114
138
  end
115
139
 
116
140
  def drop_constraints
117
- result = ActiveGraph::Base.read_transaction { |tx| tx.run('CALL db.constraints').to_a }
141
+ result = ActiveGraph::Base.read_transaction do |tx|
142
+ tx.run(ActiveGraph::Base.version?('<4.3') ? 'CALL db.constraints' : 'SHOW CONSTRAINTS YIELD *').to_a
143
+ end
118
144
  ActiveGraph::Base.write_transaction do |tx|
119
145
  result.each do |record|
120
146
  tx.run("DROP #{record.keys.include?(:name) ? "CONSTRAINT #{record[:name]}" : record[:description]}")
@@ -123,14 +149,26 @@ module ActiveGraph
123
149
  end
124
150
  end
125
151
 
126
- def schema_query(cypher)
127
- ActiveGraph::Base.query(cypher, {})
152
+ def schema_query(cypher, **params)
153
+ ActiveGraph::Base.query(cypher, params)
128
154
  end
129
155
 
130
156
  def validate_index_options!(options)
131
157
  return unless options[:type] && options[:type] != :exact
132
158
  fail "Type #{options[:type]} is not supported"
133
159
  end
160
+
161
+ def drop_constraint42(property, constraint)
162
+ cypher = case constraint[:type]
163
+ when :unique, :uniqueness
164
+ "n.`#{property}` IS UNIQUE"
165
+ when :exists
166
+ "exists(n.`#{property}`)"
167
+ else
168
+ fail "Not supported constraint #{constraint.inspect}"
169
+ end
170
+ schema_query("DROP CONSTRAINT ON (n:`#{name}`) ASSERT #{cypher}")
171
+ end
134
172
  end
135
173
  end
136
174
  end
@@ -1,63 +1,92 @@
1
1
  module ActiveGraph
2
2
  module Core
3
3
  module Schema
4
+ FILTER = {
5
+ 3 => [:type, 'node_unique_property'],
6
+ 4 => [:uniqueness, 'UNIQUE'],
7
+ }
8
+
4
9
  def version
5
- read_transaction do
10
+ @version ||= read_transaction do
6
11
  # BTW: community / enterprise could be retrieved via `result.first.edition`
7
12
  query('CALL dbms.components()', {}, skip_instrumentation: true).first[:versions][0]
13
+ .then(&Gem::Version.method(:new))
8
14
  end
9
15
  end
10
16
 
17
+ def version?(requirement)
18
+ Gem::Requirement.create(requirement).satisfied_by?(Gem::Version.new(version))
19
+ end
20
+
11
21
  def indexes
12
- raw_indexes do |keys, result|
13
- result.map do |row|
14
- { type: row[:type].to_sym, label: label(keys, row), properties: properties(row),
15
- state: row[:state].to_sym }
16
- end
22
+ normalize(raw_indexes, *%i[type state])
23
+ end
24
+
25
+ def normalize(result, *extra)
26
+ result.map do |row|
27
+ definition(row, version?('<4') ? :index_cypher_v3 : :index_cypher)
28
+ .merge(extra.to_h { |key| [key, row[key].to_sym] })
17
29
  end
18
30
  end
19
31
 
20
32
  def constraints
21
- raw_indexes do |keys, result|
22
- result.select(&method(v4?(keys) ? :v4_filter : :v3_filter)).map do |row|
23
- { type: :uniqueness, label: label(keys, row), properties: properties(row) }
24
- end
33
+ if version?('<4.3')
34
+ raw_indexes.select(&method(:constraint_owned?))
35
+ else
36
+ raw_constraints.select(&method(:constraint_filter))
37
+ end.map { |row| definition(row, :constraint_cypher).merge(type: :uniqueness) }
38
+ end
39
+
40
+ private def raw_constraints
41
+ read_transaction do
42
+ query('SHOW CONSTRAINTS YIELD *', {}, skip_instrumentation: true).to_a
25
43
  end
26
44
  end
27
45
 
28
46
  def raw_indexes
29
47
  read_transaction do
30
- result = query('CALL db.indexes()', {}, skip_instrumentation: true)
31
- yield result.keys, result.reject { |row| row[:type] == 'LOOKUP' }
48
+ query(version?('<4.3') ? 'CALL db.indexes()' : 'SHOW INDEXES YIELD *', {}, skip_instrumentation: true)
49
+ .reject { |row| row[:type] == 'LOOKUP' }
32
50
  end
33
51
  end
34
52
 
53
+ def constraint_owned?(record)
54
+ FILTER[major]&.then { |(key, value)| record[key] == value } || record[:owningConstraint]
55
+ end
56
+
35
57
  private
36
58
 
37
- def v4_filter(row)
38
- row[:uniqueness] == 'UNIQUE'
59
+ def major
60
+ @major ||= version.segments.first
39
61
  end
40
62
 
41
- def v3_filter(row)
42
- row[:type] == 'node_unique_property'
63
+ def constraint_filter(record)
64
+ %w[UNIQUENESS RELATIONSHIP_PROPERTY_EXISTENCE NODE_PROPERTY_EXISTENCE NODE_KEY].include?(record[:type])
43
65
  end
44
66
 
45
- def label(keys, row)
46
- if v34?(keys)
47
- row[:label]
48
- else
49
- (v4?(keys) ? row[:labelsOrTypes] : row[:tokenNames]).first
50
- end.to_sym
67
+ def index_cypher_v3(label, properties)
68
+ "INDEX ON :#{label}#{com_sep(properties, nil)}"
69
+ end
70
+
71
+ def index_cypher(label, properties)
72
+ "INDEX FOR (n:#{label}) ON #{com_sep(properties)}"
73
+ end
74
+
75
+ def constraint_cypher(label, properties)
76
+ "CONSTRAINT ON (n:#{label}) ASSERT #{com_sep(properties)} IS UNIQUE"
77
+ end
78
+
79
+ def com_sep(properties, prefix = 'n.')
80
+ "(#{properties.map { |prop| "#{prefix}#{prop}" }.join(', ')})"
51
81
  end
52
82
 
53
- def v4?(keys)
54
- return @v4 unless @v4.nil?
55
- @v4 = keys.include?(:labelsOrTypes)
83
+ def definition(row, template)
84
+ { label: label(row), properties: properties(row), name: row[:name],
85
+ create_statement: row[:createStatement] || send(template,label(row), row[:properties]) }
56
86
  end
57
87
 
58
- def v34?(keys)
59
- return @v34 unless @v34.nil?
60
- @v34 = keys.include?(:label)
88
+ def label(row)
89
+ row[version?('>=4') ? :labelsOrTypes : :tokenNames].first.to_sym
61
90
  end
62
91
 
63
92
  def properties(row)
@@ -5,7 +5,7 @@ require 'active_graph/core/query'
5
5
  require 'active_graph/core/record'
6
6
  require 'active_graph/core/wrappable'
7
7
  require 'active_graph/transaction'
8
- require 'neo4j_ruby_driver'
8
+ require 'neo4j/driver'
9
9
 
10
10
  Neo4j::Driver::Types::Entity.include ActiveGraph::Core::Wrappable
11
11
  Neo4j::Driver::Types::Entity.prepend ActiveGraph::Core::Entity
@@ -23,12 +23,12 @@ module ActiveGraph
23
23
  protected
24
24
 
25
25
  def idless_count(label, id_property)
26
- query.match(n: label).where("NOT EXISTS(n.#{id_property})").pluck('COUNT(n) AS ids').first
26
+ query.match(n: label).where("n.#{id_property} IS NULL").pluck('COUNT(n) AS ids').first
27
27
  end
28
28
 
29
29
  def id_batch_set(label, id_property, new_ids, count)
30
30
  ActiveGraph::Base.transaction do
31
- execute("MATCH (n:`#{label}`) WHERE NOT EXISTS(n.#{id_property})
31
+ execute("MATCH (n:`#{label}`) WHERE n.#{id_property} IS NULL
32
32
  with COLLECT(n) as nodes, #{new_ids} as ids
33
33
  FOREACH(i in range(0,#{count - 1})|
34
34
  FOREACH(node in [nodes[i]]|
@@ -83,7 +83,7 @@ module ActiveGraph
83
83
  private
84
84
 
85
85
  def property_exists?(label, property)
86
- by_label(label).where("EXISTS(n.#{property})").return(:n).any?
86
+ by_label(label).where("n.#{property} IS NOT NULL").return(:n).any?
87
87
  end
88
88
 
89
89
  def by_label(label, options = {})
@@ -3,15 +3,15 @@ module ActiveGraph
3
3
  module Schema
4
4
  class << self
5
5
  def fetch_schema_data
6
- { constraints: fetch_constraint_descriptions.sort, indexes: fetch_index_descriptions.sort }
6
+ %i[constraints indexes].to_h { |schema_elem| [schema_elem, send("fetch_#{schema_elem}_descriptions").keys] }
7
7
  end
8
8
 
9
9
  def synchronize_schema_data(schema_data, remove_missing)
10
- queries = []
11
- ActiveGraph::Base.read_transaction do
12
- queries += drop_and_create_queries(fetch_constraint_descriptions, schema_data[:constraints], remove_missing)
13
- queries += drop_and_create_queries(fetch_index_descriptions, schema_data[:indexes], remove_missing)
14
- end
10
+ queries =
11
+ ActiveGraph::Base.read_transaction do
12
+ drop_and_create_queries(fetch_constraints_descriptions, schema_data[:constraints], 'CONSTRAINT', remove_missing) +
13
+ drop_and_create_queries(fetch_indexes_descriptions, schema_data[:indexes], 'INDEX', remove_missing)
14
+ end
15
15
  ActiveGraph::Base.write_transaction do
16
16
  queries.each(&ActiveGraph::Base.method(:query))
17
17
  end
@@ -19,46 +19,30 @@ module ActiveGraph
19
19
 
20
20
  private
21
21
 
22
- def fetch_constraint_descriptions
23
- ActiveGraph::Base.query('CALL db.constraints() YIELD description').map(&:first)
22
+ def fetch_indexes_descriptions
23
+ ActiveGraph::Base.raw_indexes.reject(&ActiveGraph::Base.method(:constraint_owned?))
24
+ .then(&ActiveGraph::Base.method(:normalize)).then(&method(:fetch_descriptions))
24
25
  end
25
26
 
26
- def fetch_index_descriptions
27
- ActiveGraph::Base.raw_indexes do |keys, result|
28
- if keys.include?(:description)
29
- v3_indexes(result)
30
- else
31
- v4_indexes(result)
32
- end
33
- end
27
+ def fetch_constraints_descriptions
28
+ fetch_descriptions(ActiveGraph::Base.constraints)
34
29
  end
35
30
 
36
- def v3_indexes(result)
37
- result.reject do |row|
38
- # These indexes are created automagically when the corresponding constraints are created
39
- row[:type] == 'node_unique_property'
40
- end.map { |row| row[:description] }
31
+ def fetch_descriptions(results)
32
+ results.map { |definition| definition.values_at(:create_statement, :name) }.sort.to_h
41
33
  end
42
34
 
43
- def v4_indexes(result)
44
- result.reject do |row|
45
- # These indexes are created automagically when the corresponding constraints are created
46
- row[:uniqueness] == 'UNIQUE'
47
- end.map(&method(:description))
35
+ def drop_and_create_queries(existing, specified, schema_elem, remove_missing)
36
+ (remove_missing ? existing.except(*specified).map { |stmt, name| drop_statement(schema_elem, stmt, name) } : []) +
37
+ (specified - existing.keys).map(&method(:create_statement))
48
38
  end
49
39
 
50
- def description(row)
51
- "INDEX FOR (n:#{row[:labelsOrTypes].first}) ON (#{row[:properties].map { |prop| "n.#{prop}" }.join(', ')})"
40
+ def drop_statement(schema_elem, create_statement, name)
41
+ "DROP #{name&.then { |name| "#{schema_elem} #{name}" } || create_statement}"
52
42
  end
53
43
 
54
- def drop_and_create_queries(existing, specified, remove_missing)
55
- [].tap do |queries|
56
- if remove_missing
57
- (existing - specified).each { |description| queries << "DROP #{description}" }
58
- end
59
-
60
- (specified - existing).each { |description| queries << "CREATE #{description}" }
61
- end
44
+ def create_statement(stmt)
45
+ stmt.start_with?('CREATE ') ? stmt : "CREATE #{stmt}"
62
46
  end
63
47
  end
64
48
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveGraph
2
- VERSION = '11.1.0.beta.1'
2
+ VERSION = '11.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activegraph
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.1.0.beta.1
4
+ version: 11.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Ronge, Brian Underwood, Chris Grigg, Heinrich Klobuczek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-23 00:00:00.000000000 Z
11
+ date: 2023-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 4.4.0.alpha.7
61
+ version: 4.4.1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 4.4.0.alpha.7
68
+ version: 4.4.1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: orm_adapter
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -440,11 +440,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
440
440
  version: '2.6'
441
441
  required_rubygems_version: !ruby/object:Gem::Requirement
442
442
  requirements:
443
- - - ">"
443
+ - - ">="
444
444
  - !ruby/object:Gem::Version
445
- version: 1.3.1
445
+ version: '0'
446
446
  requirements: []
447
- rubygems_version: 3.3.3
447
+ rubygems_version: 3.4.1
448
448
  signing_key:
449
449
  specification_version: 4
450
450
  summary: A graph database for Ruby