grumlin 0.17.0 → 0.19.0
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/.gitignore +2 -0
- data/.rubocop.yml +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +14 -4
- data/README.md +25 -9
- data/grumlin.gemspec +1 -0
- data/lib/async/channel.rb +11 -0
- data/lib/grumlin/action.rb +0 -6
- data/lib/grumlin/repository/error_handling_strategy.rb +44 -0
- data/lib/grumlin/repository/instance_methods.rb +135 -0
- data/lib/grumlin/repository.rb +2 -12
- data/lib/grumlin/request_dispatcher.rb +47 -22
- data/lib/grumlin/shortcuts/properties.rb +5 -7
- data/lib/grumlin/shortcuts/upserts.rb +28 -0
- data/lib/grumlin/shortcuts_applyer.rb +27 -31
- data/lib/grumlin/step_data.rb +6 -4
- data/lib/grumlin/steps.rb +7 -7
- data/lib/grumlin/steps_serializers/bytecode.rb +7 -2
- data/lib/grumlin/steps_serializers/human_readable_bytecode.rb +4 -10
- data/lib/grumlin/steps_serializers/string.rb +10 -9
- data/lib/grumlin/traversal_start.rb +1 -1
- data/lib/grumlin/typing.rb +1 -1
- data/lib/grumlin/version.rb +1 -1
- data/lib/grumlin.rb +23 -0
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f899b66f4f483334c271ca60f6ade26d5b403bc10552e03700595e983e0268cb
|
4
|
+
data.tar.gz: ea689ad161f0b220e37af0fd5aa32adaa50990ebdcdc6d8e8cef7b12b53067e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9d3c770966a9f44efc25c7f7ad3a33203daafc0e1f3887db22ef6cd9c37415073c72cb90dd7cb7658c12a5e9ac2aa587c48d590ff417a7ba4c26b1f2586214d
|
7
|
+
data.tar.gz: 5a9ed3d9e7ff5c717083b5d36b8de17a4ef8b3ed4310c67b35b8ff56d5f7b49e2d74770e47cd1aa96f3544549f9fad9ddba5bf5695ca4bf9e23c04bb4d15f90b
|
data/.gitignore
CHANGED
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
data/Gemfile.lock
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
grumlin (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.
|
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.
|
80
|
-
protocol-http1 (0.14.
|
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.
|
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
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
data/lib/grumlin/action.rb
CHANGED
@@ -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
|
data/lib/grumlin/repository.rb
CHANGED
@@ -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(
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
request[:
|
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
|
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
|
9
|
-
|
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
|
-
|
12
|
-
tt.property(prop, value)
|
12
|
+
tt
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
shortcut :hasAll do
|
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
|
-
|
8
|
-
end
|
9
|
-
end
|
7
|
+
return steps unless steps.uses_shortcuts?
|
10
8
|
|
11
|
-
|
12
|
-
return steps unless steps.uses_shortcuts?
|
9
|
+
shortcuts = steps.shortcuts
|
13
10
|
|
14
|
-
|
11
|
+
configuration_steps = process_steps(steps.configuration_steps, shortcuts)
|
12
|
+
regular_steps = process_steps(steps.steps, shortcuts)
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/grumlin/step_data.rb
CHANGED
@@ -2,17 +2,19 @@
|
|
2
2
|
|
3
3
|
module Grumlin
|
4
4
|
class StepData
|
5
|
-
attr_reader :name, :
|
5
|
+
attr_reader :name, :args, :params
|
6
6
|
|
7
|
-
def initialize(name,
|
7
|
+
def initialize(name, args: [], params: {})
|
8
8
|
@name = name
|
9
|
-
@
|
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
|
-
@
|
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.
|
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,
|
35
|
-
return add_configuration_step(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(
|
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.
|
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,
|
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(
|
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
|
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 <
|
5
|
+
class HumanReadableBytecode < Bytecode
|
6
6
|
def serialize
|
7
7
|
steps = ShortcutsApplyer.call(@steps)
|
8
|
-
[
|
9
|
-
|
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
|
-
|
14
|
-
|
13
|
+
steps = [steps.configuration_steps, steps.steps].map do |stps|
|
14
|
+
stps.map { |step| serialize_step(step) }
|
15
|
+
end
|
15
16
|
|
16
|
-
"#{prefix}.#{(
|
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
|
data/lib/grumlin/typing.rb
CHANGED
@@ -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
|
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
|
data/lib/grumlin/version.rb
CHANGED
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.
|
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
|
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.
|
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.
|