grumlin 0.17.0 → 0.19.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: 2d63d3923f895d20e89fd1647495d99bb3a97888689c8f25c34973355d7c80e1
4
- data.tar.gz: 878546a65736522f3c1f00f42741b6b207e992a1983321205d05406e0a74ed6b
3
+ metadata.gz: f899b66f4f483334c271ca60f6ade26d5b403bc10552e03700595e983e0268cb
4
+ data.tar.gz: ea689ad161f0b220e37af0fd5aa32adaa50990ebdcdc6d8e8cef7b12b53067e5
5
5
  SHA512:
6
- metadata.gz: 60bf726b74019b36bb0437c78b97ea1ba78703058ba2a619805e0081d49158f30bb67cc7918b959cf0375c37a5600632a1df805563edccb0266b71c9ab70f1de
7
- data.tar.gz: cc7dc9ecfd45c67b28301593d3ba60e471e7a9993460ce39dcb912bbed5fb0aa01ed25bcf52f49634f704d6732ba563d849fb8d492276f19fe83e5e8531a8181
6
+ metadata.gz: f9d3c770966a9f44efc25c7f7ad3a33203daafc0e1f3887db22ef6cd9c37415073c72cb90dd7cb7658c12a5e9ac2aa587c48d590ff417a7ba4c26b1f2586214d
7
+ data.tar.gz: 5a9ed3d9e7ff5c717083b5d36b8de17a4ef8b3ed4310c67b35b8ff56d5f7b49e2d74770e47cd1aa96f3544549f9fad9ddba5bf5695ca4bf9e23c04bb4d15f90b
data/.gitignore CHANGED
@@ -10,3 +10,5 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+ profile.flat.txt
14
+ profile.graph.html
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ Layout/LineLength:
15
15
  Metrics/BlockLength:
16
16
  Exclude:
17
17
  - spec/**/*_spec.rb
18
+ - "*.gemspec"
18
19
 
19
20
  Metrics/MethodLength:
20
21
  Max: 20
@@ -44,6 +45,7 @@ Naming/MethodParameterName:
44
45
  - outV
45
46
  - inVLabel
46
47
  - outVLabel
48
+ - to
47
49
 
48
50
  RSpec/NamedSubject:
49
51
  Enabled: false
data/Gemfile CHANGED
@@ -17,3 +17,7 @@ gem "overcommit"
17
17
  gem "rake"
18
18
  gem "rspec"
19
19
  gem "simplecov"
20
+
21
+ gem "benchmark-ips"
22
+ gem "memory_profiler"
23
+ gem "ruby-prof"
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.17.0)
4
+ grumlin (0.19.0)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
  oj (~> 3.12)
8
+ retryable (~> 3.0)
8
9
  zeitwerk (~> 2.4)
9
10
 
10
11
  GEM
@@ -21,13 +22,14 @@ GEM
21
22
  console (~> 1.10)
22
23
  nio4r (~> 2.3)
23
24
  timers (~> 4.1)
24
- async-http (0.56.5)
25
+ async-http (0.56.6)
25
26
  async (>= 1.25)
26
27
  async-io (>= 1.28)
27
28
  async-pool (>= 0.2)
28
29
  protocol-http (~> 0.22.0)
29
30
  protocol-http1 (~> 0.14.0)
30
31
  protocol-http2 (~> 0.14.0)
32
+ traces (~> 0.4.0)
31
33
  async-io (1.33.0)
32
34
  async
33
35
  async-pool (0.3.9)
@@ -42,6 +44,7 @@ GEM
42
44
  protocol-websocket (~> 0.7.0)
43
45
  backport (1.2.0)
44
46
  benchmark (0.1.1)
47
+ benchmark-ips (2.10.0)
45
48
  childprocess (4.0.0)
46
49
  concurrent-ruby (1.1.8)
47
50
  console (1.15.0)
@@ -60,6 +63,7 @@ GEM
60
63
  rexml
61
64
  kramdown-parser-gfm (1.1.0)
62
65
  kramdown (~> 2.0)
66
+ memory_profiler (1.0.0)
63
67
  mini_portile2 (2.7.1)
64
68
  minitest (5.14.4)
65
69
  nio4r (2.5.8)
@@ -76,8 +80,8 @@ GEM
76
80
  parser (3.0.3.2)
77
81
  ast (~> 2.4.1)
78
82
  protocol-hpack (1.4.2)
79
- protocol-http (0.22.5)
80
- protocol-http1 (0.14.2)
83
+ protocol-http (0.22.6)
84
+ protocol-http1 (0.14.4)
81
85
  protocol-http (~> 0.22)
82
86
  protocol-http2 (0.14.2)
83
87
  protocol-hpack (~> 1.4)
@@ -89,6 +93,7 @@ GEM
89
93
  rainbow (3.0.0)
90
94
  rake (13.0.3)
91
95
  regexp_parser (2.2.0)
96
+ retryable (3.0.5)
92
97
  reverse_markdown (2.0.0)
93
98
  nokogiri
94
99
  rexml (3.2.5)
@@ -125,6 +130,7 @@ GEM
125
130
  rubocop-ast (>= 0.4.0)
126
131
  rubocop-rspec (2.6.0)
127
132
  rubocop (~> 1.19)
133
+ ruby-prof (1.4.3)
128
134
  ruby-progressbar (1.11.0)
129
135
  simplecov (0.21.2)
130
136
  docile (~> 1.1)
@@ -150,6 +156,7 @@ GEM
150
156
  thor (1.1.0)
151
157
  tilt (2.0.10)
152
158
  timers (4.3.3)
159
+ traces (0.4.1)
153
160
  tzinfo (2.0.4)
154
161
  concurrent-ruby (~> 1.0)
155
162
  unicode-display_width (2.1.0)
@@ -162,8 +169,10 @@ PLATFORMS
162
169
 
163
170
  DEPENDENCIES
164
171
  async-rspec
172
+ benchmark-ips
165
173
  factory_bot
166
174
  grumlin!
175
+ memory_profiler
167
176
  nokogiri
168
177
  overcommit
169
178
  rake
@@ -171,6 +180,7 @@ DEPENDENCIES
171
180
  rubocop
172
181
  rubocop-performance
173
182
  rubocop-rspec
183
+ ruby-prof
174
184
  simplecov
175
185
  solargraph
176
186
 
data/README.md CHANGED
@@ -77,8 +77,7 @@ end
77
77
 
78
78
  **Shortcuts** is a way to share and organize gremlin code. They let developers define their own steps consisting of
79
79
  sequences of standard gremlin steps, other shortcuts and even add new initially unsupported by Grumlin steps.
80
- Remember ActiveRecord scopes? Shortcuts are very similar. `Grumlin::Shortcuts#with_shortcuts` wraps a given object into
81
- a proxy object that simply proxies all methods existing in the wrapped object to it and handles shortcuts.
80
+ Remember ActiveRecord scopes? Shortcuts are very similar.
82
81
 
83
82
  **Important**: if a shortcut's name matches a name of a method defined on the wrapped object, this shortcut will be
84
83
  be ignored because methods have higher priority. You cannot override supported by Grumlin steps with shortcuts,
@@ -128,17 +127,18 @@ class MyRepository
128
127
 
129
128
  # Wrapping a traversal
130
129
  def red_triangles
131
- with_shortcuts(g).V.hasLabel(:triangle)
132
- .hasColor("red")
133
- .toList
130
+ g(self.class.shortcuts).V.hasLabel(:triangle)
131
+ .hasColor("red")
132
+ .toList
134
133
  end
135
134
 
136
135
  # Wrapping _
137
136
  def something_else
138
- with_shortcuts(g).V.hasColor("red")
139
- .repeat(with_shortcuts(__)
140
- .out(:has)
141
- .hasColor("blue")).toList
137
+ g(self.class.shortcuts).V.hasColor("red")
138
+ .repeat(__(self.class.shortcuts))
139
+ .out(:has)
140
+ .hasColor("blue")
141
+ .toList
142
142
  end
143
143
  end
144
144
  ```
@@ -183,6 +183,22 @@ Each `return_mode` is mapped to a particular termination step:
183
183
  - `:none` - `iterate`
184
184
  - `:traversal` - do not execute the query and return the traversal as is
185
185
 
186
+ `Grumlin::Repository` also provides a set of generic CRUD operations:
187
+ - `add_vertex(label, id = nil, **properties)`
188
+ - `add_edge(label, id = nil, from:, to:, **properties)`
189
+ - `drop_vertex(id)`
190
+ - `drop_edge(id = nil, from: nil, to: nil, label: nil)`
191
+
192
+ and a few methods that emulate upserts:
193
+ - `upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
194
+ - `upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params)`
195
+ - `upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)`
196
+ - `upsert_vertices(edges, batch_size: 100, on_failure: :retry, **params)`
197
+
198
+ All of them support 3 different modes for error handling: `:retry`, `:ignore` and `:raise`. Retry mode is implemented
199
+ with [retryable](https://github.com/nfedyashev/retryable). **params will be merged to the default config for upserts
200
+ and passed to `Retryable.retryable`. In case if you want to modify retryable behaviour you are to do so.
201
+
186
202
  **Usage**
187
203
 
188
204
  To execute the query defined in a query block one simply needs to call a method with the same name:
data/grumlin.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency "async-pool", "~> 0.3"
31
31
  spec.add_dependency "async-websocket", "~> 0.19"
32
32
  spec.add_dependency "oj", "~> 3.12"
33
+ spec.add_dependency "retryable", "~> 3.0"
33
34
  spec.add_dependency "zeitwerk", "~> 2.4"
34
35
  spec.metadata = {
35
36
  "rubygems_mfa_required" => "true"
data/lib/async/channel.rb CHANGED
@@ -18,6 +18,10 @@ module Async
18
18
  @closed
19
19
  end
20
20
 
21
+ def open?
22
+ !@closed
23
+ end
24
+
21
25
  # Methods for a publisher
22
26
  def <<(payload)
23
27
  raise(ChannelClosedError, "Cannot send to a closed channel") if @closed
@@ -38,6 +42,13 @@ module Async
38
42
  @closed = true
39
43
  end
40
44
 
45
+ def close!
46
+ return if closed?
47
+
48
+ exception(ChannelClosedError.new("Channel was forcefully closed"))
49
+ close
50
+ end
51
+
41
52
  # Methods for a subscriber
42
53
  def dequeue
43
54
  each do |payload| # rubocop:disable Lint/UnreachableLoop this is intended
@@ -49,12 +49,6 @@ module Grumlin
49
49
  @shortcuts.key?(@name)
50
50
  end
51
51
 
52
- def arguments
53
- @arguments ||= [*@args].tap do |args|
54
- args << @params if @params.any?
55
- end
56
- end
57
-
58
52
  def method_missing(name, *args, **params)
59
53
  return step(name, *args, **params) if @shortcuts.key?(name)
60
54
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Repository
5
+ class ErrorHandlingStrategy
6
+ def initialize(mode: :retry, **params)
7
+ @mode = mode
8
+ @params = params
9
+ @on_exceptions = params[:on]
10
+ end
11
+
12
+ def raise?
13
+ @mode == :raise
14
+ end
15
+
16
+ def ignore?
17
+ @mode == :ignore
18
+ end
19
+
20
+ def retry?
21
+ @mode == :retry
22
+ end
23
+
24
+ def apply!(&block)
25
+ return yield if raise?
26
+ return ignore_errors!(&block) if ignore?
27
+
28
+ retry_errors!(&block)
29
+ end
30
+
31
+ private
32
+
33
+ def ignore_errors!
34
+ yield
35
+ rescue *@on_exceptions
36
+ # ignore errors
37
+ end
38
+
39
+ def retry_errors!(&block)
40
+ Retryable.retryable(**@params, &block)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Repository
5
+ module InstanceMethods
6
+ include Grumlin::Expressions
7
+
8
+ UPSERT_RETRY_PARAMS = {
9
+ on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentInsertFailedError],
10
+ sleep_method: ->(n) { Async::Task.current.sleep(n) },
11
+ tries: 3,
12
+ sleep: ->(n) { (n**2) + 1 + rand }
13
+ }.freeze
14
+
15
+ DEFAULT_ERROR_HANDLING_STRATEGY = ErrorHandlingStrategy.new(mode: :retry, **UPSERT_RETRY_PARAMS)
16
+
17
+ def __
18
+ @__ ||= TraversalStart.new(self.class.shortcuts)
19
+ end
20
+
21
+ def g
22
+ @g ||= TraversalStart.new(self.class.shortcuts)
23
+ end
24
+
25
+ def drop_vertex(id)
26
+ g.V(id).drop.iterate
27
+ end
28
+
29
+ def drop_edge(id = nil, from: nil, to: nil, label: nil) # rubocop:disable Metrics/AbcSize
30
+ raise ArgumentError, "either id or from:, to: and label: must be passed" if [id, from, to, label].all?(&:nil?)
31
+ return g.E(id).drop.iterate unless id.nil?
32
+
33
+ raise ArgumentError, "from:, to: and label: must be passed" if [from, to, label].any?(&:nil?)
34
+
35
+ g.V(from).outE(label).where(__.inV.hasId(to)).limit(1).drop.iterate
36
+ end
37
+
38
+ def add_vertex(label, id = nil, **properties)
39
+ id ||= properties[T.id]
40
+ properties = except(properties, T.id)
41
+
42
+ t = g.addV(label)
43
+ t = t.props(T.id => id) unless id.nil?
44
+ t.props(**properties).next
45
+ end
46
+
47
+ def add_edge(label, id = nil, from:, to:, **properties)
48
+ id ||= properties[T.id]
49
+ properties = except(properties, T.label)
50
+ properties[T.id] = id
51
+
52
+ g.addE(label).from(__.V(from)).to(__.V(to)).props(**properties).next
53
+ end
54
+
55
+ def upsert_vertex(label, id, create_properties: {}, update_properties: {}, on_failure: :retry, **params)
56
+ with_upsert_error_handling(on_failure, params) do
57
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties)
58
+
59
+ g.upsertV(label, id, create_properties, update_properties).next
60
+ end
61
+ end
62
+
63
+ # vertices:
64
+ # [["label", "id", {create: :properties}, {update: properties}]]
65
+ # params can override Retryable config from UPSERT_RETRY_PARAMS
66
+ def upsert_vertices(vertices, batch_size: 100, on_failure: :retry, **params)
67
+ with_upsert_error_handling(on_failure, params) do
68
+ vertices.each_slice(batch_size) do |slice|
69
+ slice.reduce(g) do |t, (label, id, create_properties, update_properties)|
70
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties)
71
+
72
+ t.upsertV(label, id, create_properties, update_properties)
73
+ end.iterate
74
+ end
75
+ end
76
+ end
77
+
78
+ # Only from and to are used to find the existing edge, if one wants to assign an id to a created edge,
79
+ # it must be passed as T.id in create_properties.
80
+ def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params) # rubocop:disable Metrics/ParameterLists
81
+ with_upsert_error_handling(on_failure, params) do
82
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
83
+ g.upsertE(label, from, to, create_properties, update_properties).next
84
+ end
85
+ end
86
+
87
+ # edges:
88
+ # [["label", "id", {create: :properties}, {update: properties}]]
89
+ # params can override Retryable config from UPSERT_RETRY_PARAMS
90
+ def upsert_edges(edges, batch_size: 100, on_failure: :retry, **params)
91
+ with_upsert_error_handling(on_failure, params) do
92
+ edges.each_slice(batch_size) do |slice|
93
+ slice.reduce(g) do |t, (label, from, to, create_properties, update_properties)|
94
+ create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
95
+
96
+ t.upsertE(label, from, to, create_properties, update_properties)
97
+ end.iterate
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def with_upsert_error_handling(on_failure, params, &block)
105
+ if params.any?
106
+ ErrorHandlingStrategy.new(mode: on_failure, **UPSERT_RETRY_PARAMS.merge(params))
107
+ else
108
+ DEFAULT_ERROR_HANDLING_STRATEGY
109
+ end.apply!(&block)
110
+ end
111
+
112
+ def with_upsert_retry(retry_params, &block)
113
+ retry_params = UPSERT_RETRY_PARAMS.merge((retry_params))
114
+ Retryable.retryable(**retry_params, &block)
115
+ end
116
+
117
+ # A polyfill for Hash#except for ruby 2.x environments without ActiveSupport
118
+ # TODO: delete and use native Hash#except after ruby 2.7 is deprecated.
119
+ def except(hash, *keys)
120
+ return hash.except(*keys) if hash.respond_to?(:except)
121
+
122
+ hash.each_with_object({}) do |(k, v), res|
123
+ res[k] = v unless keys.include?(k)
124
+ end
125
+ end
126
+
127
+ def cleanup_properties(create_properties, update_properties, *props_to_cleanup)
128
+ props_to_cleanup = [T.id, T.label] if props_to_cleanup.empty?
129
+ [create_properties, update_properties].map do |props|
130
+ except(props, props_to_cleanup)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -9,22 +9,12 @@ module Grumlin
9
9
  traversal: :nil
10
10
  }.freeze
11
11
 
12
- module InstanceMethods
13
- def __
14
- TraversalStart.new(self.class.shortcuts)
15
- end
16
-
17
- def g
18
- TraversalStart.new(self.class.shortcuts)
19
- end
20
- end
21
-
22
12
  def self.extended(base)
23
13
  base.extend(Grumlin::Shortcuts)
24
- base.include(Grumlin::Expressions)
25
- base.include(InstanceMethods)
14
+ base.include(Repository::InstanceMethods)
26
15
 
27
16
  base.shortcuts_from(Grumlin::Shortcuts::Properties)
17
+ base.shortcuts_from(Grumlin::Shortcuts::Upserts)
28
18
  end
29
19
 
30
20
  def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -22,6 +22,11 @@ module Grumlin
22
22
  498 => ClientSideError
23
23
  }.freeze
24
24
 
25
+ VERTEX_ALREADY_EXISTS = "Vertex with id already exists:"
26
+ EDGE_ALREADY_EXISTS = "Edge with id already exists:"
27
+ CONCURRENT_VERTEX_INSERT_FAILED = "Failed to complete Insert operation for a Vertex due to conflicting concurrent"
28
+ CONCURRENT_EDGE_INSERT_FAILED = "Failed to complete Insert operation for an Edge due to conflicting concurrent"
29
+
25
30
  class DispatcherError < Grumlin::Error; end
26
31
 
27
32
  class RequestAlreadyAddedError < DispatcherError; end
@@ -46,29 +51,25 @@ module Grumlin
46
51
  request_id = response[:requestId]
47
52
  raise UnknownRequestError unless ongoing_request?(request_id)
48
53
 
49
- request = @requests[request_id]
50
-
51
- check_errors!(response[:status], request[:request])
52
-
53
- case SUCCESS[response.dig(:status, :code)]
54
- when :success
55
- request[:channel] << [*request[:result], response.dig(:result, :data)]
56
- close_request(request_id)
57
- when :partial_content then request[:result] << response.dig(:result, :data)
58
- when :no_content
59
- request[:channel] << []
54
+ begin
55
+ request = @requests[request_id]
56
+
57
+ check_errors!(response[:status], request[:request])
58
+
59
+ case SUCCESS[response.dig(:status, :code)]
60
+ when :success
61
+ request[:result] << response.dig(:result, :data)
62
+ request[:channel] << request[:result]
63
+ close_request(request_id)
64
+ when :partial_content then request[:result] << response.dig(:result, :data)
65
+ when :no_content
66
+ request[:channel] << []
67
+ close_request(request_id)
68
+ end
69
+ rescue StandardError => e
70
+ request[:channel].exception(e)
60
71
  close_request(request_id)
61
72
  end
62
- rescue StandardError => e
63
- request[:channel].exception(e)
64
- close_request(request_id)
65
- end
66
-
67
- def close_request(request_id)
68
- raise UnknownRequestError unless ongoing_request?(request_id)
69
-
70
- request = @requests.delete(request_id)
71
- request[:channel].close
72
73
  end
73
74
 
74
75
  def ongoing_request?(request_id)
@@ -76,19 +77,43 @@ module Grumlin
76
77
  end
77
78
 
78
79
  def clear
80
+ @requests.each do |_id, request|
81
+ request[:channel].close!
82
+ end
79
83
  @requests.clear
80
84
  end
81
85
 
82
86
  private
83
87
 
88
+ def close_request(request_id)
89
+ raise UnknownRequestError unless ongoing_request?(request_id)
90
+
91
+ request = @requests.delete(request_id)
92
+ request[:channel].close
93
+ end
94
+
84
95
  def check_errors!(status, query)
85
96
  if (error = ERRORS[status[:code]])
86
- raise error.new(status, query)
97
+ raise (
98
+ already_exists_error(status) ||
99
+ concurrent_insert_error(status) ||
100
+ error
101
+ ).new(status, query)
87
102
  end
88
103
 
89
104
  return unless SUCCESS[status[:code]].nil?
90
105
 
91
106
  raise(UnknownResponseStatus, status)
92
107
  end
108
+
109
+ def already_exists_error(status)
110
+ return VertexAlreadyExistsError if status[:message].include?(VERTEX_ALREADY_EXISTS)
111
+ return EdgeAlreadyExistsError if status[:message].include?(EDGE_ALREADY_EXISTS)
112
+ end
113
+
114
+ def concurrent_insert_error(status)
115
+ return ConcurrentVertexInsertFailedError if status[:message].include?(CONCURRENT_VERTEX_INSERT_FAILED)
116
+ return ConcurrentEdgeInsertFailedError if status[:message].include?(CONCURRENT_EDGE_INSERT_FAILED)
117
+ end
93
118
  end
94
119
  end
@@ -5,17 +5,15 @@ module Grumlin
5
5
  module Properties
6
6
  extend Grumlin::Shortcuts
7
7
 
8
- shortcut :props do |props|
9
- next if props.nil? # TODO: fixme, add proper support for **params
8
+ shortcut :props do |**props|
9
+ props.reduce(self) do |tt, (prop, value)| # rubocop:disable Style/EachWithObject
10
+ next tt.property(prop, value) unless value.nil? # nils are not supported
10
11
 
11
- props.reduce(self) do |tt, (prop, value)|
12
- tt.property(prop, value)
12
+ tt
13
13
  end
14
14
  end
15
15
 
16
- shortcut :hasAll do |props|
17
- next if props.nil? # TODO: fixme, add proper support for **params
18
-
16
+ shortcut :hasAll do |**props|
19
17
  props.reduce(self) do |tt, (prop, value)|
20
18
  tt.has(prop, value)
21
19
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Shortcuts
5
+ module Upserts
6
+ extend Grumlin::Shortcuts
7
+
8
+ shortcut :upsertV do |label, id, create_properties, update_properties|
9
+ self.V(id)
10
+ .fold
11
+ .coalesce( # TODO: extract upsert pattern to a shortcut
12
+ __.unfold,
13
+ __.addV(label).props(**create_properties.merge(T.id => id))
14
+ ).props(**update_properties)
15
+ end
16
+
17
+ shortcut :upsertE do |label, from, to, create_properties, update_properties|
18
+ self.V(from)
19
+ .outE(label).where(__.inV.hasId(to))
20
+ .fold
21
+ .coalesce(
22
+ __.unfold,
23
+ __.addE(label).from(__.V(from)).to(__.V(to)).props(**create_properties)
24
+ ).props(**update_properties)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,43 +4,39 @@ module Grumlin
4
4
  class ShortcutsApplyer
5
5
  class << self
6
6
  def call(steps)
7
- new.call(steps)
8
- end
9
- end
7
+ return steps unless steps.uses_shortcuts?
10
8
 
11
- def call(steps)
12
- return steps unless steps.uses_shortcuts?
9
+ shortcuts = steps.shortcuts
13
10
 
14
- shortcuts = steps.shortcuts
11
+ configuration_steps = process_steps(steps.configuration_steps, shortcuts)
12
+ regular_steps = process_steps(steps.steps, shortcuts)
15
13
 
16
- configuration_steps = process_steps(steps.configuration_steps, shortcuts)
17
- regular_steps = process_steps(steps.steps, shortcuts)
18
-
19
- Steps.new(shortcuts).tap do |processed_steps|
20
- (configuration_steps + regular_steps).each do |step|
21
- processed_steps.add(step.name, step.arguments)
14
+ Steps.new(shortcuts).tap do |processed_steps|
15
+ (configuration_steps + regular_steps).each do |step|
16
+ processed_steps.add(step.name, args: step.args, params: step.params)
17
+ end
22
18
  end
23
19
  end
24
- end
25
-
26
- private
27
-
28
- def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
29
- steps.each_with_object([]) do |step, result|
30
- arguments = step.arguments.map do |arg|
31
- arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
32
- end
33
-
34
- if shortcuts.include?(step.name)
35
- t = TraversalStart.new(shortcuts)
36
- action = shortcuts[step.name].apply(t, *arguments)
37
- next if action.nil? || action == t # Shortcut did not add any steps
38
20
 
39
- new_steps = ShortcutsApplyer.call(Steps.from(action))
40
- result.concat(new_steps.configuration_steps)
41
- result.concat(new_steps.steps)
42
- else
43
- result << StepData.new(step.name, arguments)
21
+ private
22
+
23
+ def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
24
+ steps.each_with_object([]) do |step, result|
25
+ args = step.args.map do |arg|
26
+ arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
27
+ end
28
+
29
+ if shortcuts.include?(step.name)
30
+ t = TraversalStart.new(shortcuts)
31
+ action = shortcuts[step.name].apply(t, *args, **step.params)
32
+ next if action.nil? || action == t # Shortcut did not add any steps
33
+
34
+ new_steps = ShortcutsApplyer.call(Steps.from(action))
35
+ result.concat(new_steps.configuration_steps)
36
+ result.concat(new_steps.steps)
37
+ else
38
+ result << StepData.new(step.name, args: args, params: step.params)
39
+ end
44
40
  end
45
41
  end
46
42
  end
@@ -2,17 +2,19 @@
2
2
 
3
3
  module Grumlin
4
4
  class StepData
5
- attr_reader :name, :arguments
5
+ attr_reader :name, :args, :params
6
6
 
7
- def initialize(name, arguments)
7
+ def initialize(name, args: [], params: {})
8
8
  @name = name
9
- @arguments = arguments
9
+ @args = args
10
+ @params = params
10
11
  end
11
12
 
12
13
  def ==(other)
13
14
  self.class == other.class &&
14
15
  @name == other.name &&
15
- @arguments == other.arguments
16
+ @args == other.args &&
17
+ @params == other.params
16
18
  end
17
19
  end
18
20
  end
data/lib/grumlin/steps.rb CHANGED
@@ -18,7 +18,7 @@ module Grumlin
18
18
 
19
19
  new(shortcuts).tap do |chain|
20
20
  actions.each do |act|
21
- chain.add(act.name, act.arguments)
21
+ chain.add(act.name, args: act.args, params: act.params)
22
22
  end
23
23
  end
24
24
  end
@@ -31,10 +31,10 @@ module Grumlin
31
31
  @steps = steps
32
32
  end
33
33
 
34
- def add(name, arguments)
35
- return add_configuration_step(name, arguments) if CONFIGURATION_STEPS.include?(name)
34
+ def add(name, args: [], params: {})
35
+ return add_configuration_step(name, args: args, params: params) if CONFIGURATION_STEPS.include?(name)
36
36
 
37
- StepData.new(name, cast_arguments(arguments)).tap do |step|
37
+ StepData.new(name, args: cast_arguments(args), params: params).tap do |step|
38
38
  @steps << step
39
39
  end
40
40
  end
@@ -56,16 +56,16 @@ module Grumlin
56
56
 
57
57
  def shortcuts?(steps_ary)
58
58
  steps_ary.any? do |step|
59
- @shortcuts.include?(step.name) || step.arguments.any? do |arg|
59
+ @shortcuts.include?(step.name) || step.args.any? do |arg|
60
60
  arg.is_a?(Steps) ? arg.uses_shortcuts? : false
61
61
  end
62
62
  end
63
63
  end
64
64
 
65
- def add_configuration_step(name, arguments)
65
+ def add_configuration_step(name, args: [], params: {})
66
66
  raise ArgumentError, "cannot use configuration steps after start step was used" unless @steps.empty?
67
67
 
68
- StepData.new(name, cast_arguments(arguments)).tap do |step|
68
+ StepData.new(name, args: cast_arguments(args), params: params).tap do |step|
69
69
  @configuration_steps << step
70
70
  end
71
71
  end
@@ -6,7 +6,7 @@ module Grumlin
6
6
  # constructor params: no_return: true|false, default false
7
7
  # TODO: add pretty
8
8
 
9
- NONE_STEP = StepData.new("none", [])
9
+ NONE_STEP = StepData.new("none")
10
10
 
11
11
  def serialize
12
12
  steps = ShortcutsApplyer.call(@steps)
@@ -22,7 +22,12 @@ module Grumlin
22
22
  private
23
23
 
24
24
  def serialize_step(step)
25
- [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
25
+ [step.name].tap do |result|
26
+ step.args.each do |arg|
27
+ result << serialize_arg(arg)
28
+ end
29
+ result << step.params if step.params.any?
30
+ end
26
31
  end
27
32
 
28
33
  def serialize_arg(arg)
@@ -2,22 +2,16 @@
2
2
 
3
3
  module Grumlin
4
4
  module StepsSerializers
5
- class HumanReadableBytecode < Serializer
5
+ class HumanReadableBytecode < Bytecode
6
6
  def serialize
7
7
  steps = ShortcutsApplyer.call(@steps)
8
- [serialize_steps(steps.configuration_steps), serialize_steps(steps.steps)]
9
- end
10
-
11
- def serialize_steps(steps)
12
- steps.map { |s| serialize_step(s) }
8
+ [steps.configuration_steps, steps.steps].map do |stps|
9
+ stps.map { |s| serialize_step(s) }
10
+ end
13
11
  end
14
12
 
15
13
  private
16
14
 
17
- def serialize_step(step)
18
- [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
19
- end
20
-
21
15
  def serialize_arg(arg)
22
16
  return arg.to_s if arg.is_a?(TypedValue)
23
17
  return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
@@ -10,14 +10,21 @@ module Grumlin
10
10
  def serialize
11
11
  steps = @params[:apply_shortcuts] ? ShortcutsApplyer.call(@steps) : @steps
12
12
 
13
- configuration_steps = serialize_steps(steps.configuration_steps)
14
- regular_steps = serialize_steps(steps.steps)
13
+ steps = [steps.configuration_steps, steps.steps].map do |stps|
14
+ stps.map { |step| serialize_step(step) }
15
+ end
15
16
 
16
- "#{prefix}.#{(configuration_steps + regular_steps).join(".")}"
17
+ "#{prefix}.#{(steps[0] + steps[1]).join(".")}"
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ def serialize_step(step)
23
+ "#{step.name}(#{(step.args + [step.params.any? ? step.params : nil].compact).map do |a|
24
+ serialize_arg(a)
25
+ end.join(", ")})"
26
+ end
27
+
21
28
  def prefix
22
29
  @prefix ||= @params[:anonymous] ? "__" : "g"
23
30
  end
@@ -31,12 +38,6 @@ module Grumlin
31
38
 
32
39
  StepsSerializers::String.new(arg, anonymous: true, **@params).serialize
33
40
  end
34
-
35
- def serialize_steps(steps)
36
- steps.map do |step|
37
- "#{step.name}(#{step.arguments.map { |a| serialize_arg(a) }.join(", ")})"
38
- end
39
- end
40
41
  end
41
42
  end
42
43
  end
@@ -31,7 +31,7 @@ module Grumlin
31
31
  end
32
32
 
33
33
  def __
34
- TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
34
+ @__ ||= TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
35
35
  end
36
36
 
37
37
  def to_s(*)
@@ -18,7 +18,7 @@ module Grumlin
18
18
  # "g:VertexProperty"=> ->(value) { value }, # TODO: implement me
19
19
  "g:TraversalMetrics" => ->(value) { cast_map(value[:@value]) },
20
20
  "g:Metrics" => ->(value) { cast_map(value[:@value]) },
21
- "g:T" => ->(value) { value.to_sym }
21
+ "g:T" => ->(value) { Grumlin::Expressions::T.public_send(value) }
22
22
  }.freeze
23
23
 
24
24
  CASTABLE_TYPES = [Hash, String, Integer, TrueClass, FalseClass].freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.17.0"
4
+ VERSION = "0.19.0"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -17,6 +17,8 @@ require "async/barrier"
17
17
  require "async/http/endpoint"
18
18
  require "async/websocket/client"
19
19
 
20
+ require "retryable"
21
+
20
22
  require "zeitwerk"
21
23
 
22
24
  loader = Zeitwerk::Loader.for_gem
@@ -80,6 +82,27 @@ module Grumlin
80
82
 
81
83
  class ServerError < ServerSideError; end
82
84
 
85
+ class AlreadyExistsError < ServerError
86
+ attr_reader :id
87
+
88
+ def initialize(status, query)
89
+ super
90
+ id = status[:message].split(":").last.strip
91
+ @id = id == "" ? nil : id
92
+ end
93
+
94
+ # TODO: parse message and assign @id
95
+ # NOTE: Neptune does not return id.
96
+ end
97
+
98
+ class VertexAlreadyExistsError < AlreadyExistsError; end
99
+ class EdgeAlreadyExistsError < AlreadyExistsError; end
100
+
101
+ class ConcurrentInsertFailedError < ServerError; end
102
+
103
+ class ConcurrentVertexInsertFailedError < ConcurrentInsertFailedError; end
104
+ class ConcurrentEdgeInsertFailedError < ConcurrentInsertFailedError; end
105
+
83
106
  class ServerSerializationError < ServerSideError; end
84
107
 
85
108
  class ServerTimeoutError < ServerSideError; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-30 00:00:00.000000000 Z
11
+ date: 2022-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: retryable
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: zeitwerk
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -113,10 +127,13 @@ files:
113
127
  - lib/grumlin/path.rb
114
128
  - lib/grumlin/property.rb
115
129
  - lib/grumlin/repository.rb
130
+ - lib/grumlin/repository/error_handling_strategy.rb
131
+ - lib/grumlin/repository/instance_methods.rb
116
132
  - lib/grumlin/request_dispatcher.rb
117
133
  - lib/grumlin/shortcut.rb
118
134
  - lib/grumlin/shortcuts.rb
119
135
  - lib/grumlin/shortcuts/properties.rb
136
+ - lib/grumlin/shortcuts/upserts.rb
120
137
  - lib/grumlin/shortcuts_applyer.rb
121
138
  - lib/grumlin/step_data.rb
122
139
  - lib/grumlin/steps.rb
@@ -155,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
172
  - !ruby/object:Gem::Version
156
173
  version: '0'
157
174
  requirements: []
158
- rubygems_version: 3.2.32
175
+ rubygems_version: 3.2.33
159
176
  signing_key:
160
177
  specification_version: 4
161
178
  summary: Gremlin graph traversal language DSL and client for Ruby.