grumlin 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1f9f9d0ec31cd18d6852dc7920ee15de0d4799d229ed7bfa92ee1dccfe48d9d
4
- data.tar.gz: 0fb3bd378cb520ef310c4926c914fd2ddb4dbc1d674ef245a727d4bb776c5164
3
+ metadata.gz: 95963424bb38728bb21c2d3746a93be60daf303b2c2897b24d99684fd1fb7bff
4
+ data.tar.gz: 809b4b589b2bc5fd8687f0f63a6f73f17aab82060990b3a5942164c2ac92ef83
5
5
  SHA512:
6
- metadata.gz: aaf7fcc34cb5c14705e72fea99cf013bbebcec4c06b2c5bd39d863a97ca13a14ff3c87ec70f209752f9e09c5964f49b4447bb66c5420405b9b6a61660d807f81
7
- data.tar.gz: 5626a23f9279d2989ce781141a388ac97e6f1f1cedcfe9b7b9f7b91e5455dc4c8f98c913575901ee3d94cecfce11c437abd28918cdde9544eb9564fa175fb450
6
+ metadata.gz: b095f3958ebf4b70577e607a0b10e27fc073055fe08d6fe55787c71fd104e77cf78cdcd0a54d06e7881c9183cab479ef6e92e6117a1c1b418945d1ebb417655c
7
+ data.tar.gz: 0477a8ce8ccfc5ea1cc3b95b7b48501b05387207918478adc4eecb8e286e8495910b6bd718e1d5c00ab63e61e02ca6e89212913ec35d8df49b267cb75281e9eb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grumlin (0.20.1)
4
+ grumlin (0.21.1)
5
5
  async-pool (~> 0.3)
6
6
  async-websocket (~> 0.19)
7
7
  oj (~> 3.13)
@@ -21,11 +21,11 @@ GEM
21
21
  console (~> 1.10)
22
22
  nio4r (~> 2.3)
23
23
  timers (~> 4.1)
24
- async-http (0.56.6)
24
+ async-http (0.57.0)
25
25
  async (>= 1.25)
26
26
  async-io (>= 1.28)
27
27
  async-pool (>= 0.2)
28
- protocol-http (~> 0.22.0)
28
+ protocol-http (~> 0.23.1)
29
29
  protocol-http1 (~> 0.14.0)
30
30
  protocol-http2 (~> 0.14.0)
31
31
  traces (~> 0.4.0)
@@ -37,7 +37,7 @@ GEM
37
37
  rspec (~> 3.0)
38
38
  rspec-files (~> 1.0)
39
39
  rspec-memory (~> 1.0)
40
- async-websocket (0.19.0)
40
+ async-websocket (0.19.2)
41
41
  async-http (~> 0.54)
42
42
  async-io (~> 1.23)
43
43
  protocol-websocket (~> 0.7.0)
@@ -72,7 +72,7 @@ GEM
72
72
  racc (~> 1.4)
73
73
  nokogiri (1.13.6-x86_64-linux)
74
74
  racc (~> 1.4)
75
- oj (3.13.17)
75
+ oj (3.13.20)
76
76
  overcommit (0.59.1)
77
77
  childprocess (>= 0.6.3, < 5)
78
78
  iniparse (~> 1.4)
@@ -81,7 +81,7 @@ GEM
81
81
  parser (3.1.2.0)
82
82
  ast (~> 2.4.1)
83
83
  protocol-hpack (1.4.2)
84
- protocol-http (0.22.6)
84
+ protocol-http (0.23.1)
85
85
  protocol-http1 (0.14.4)
86
86
  protocol-http (~> 0.22)
87
87
  protocol-http2 (0.14.2)
data/README.md CHANGED
@@ -46,9 +46,33 @@ Or install it yourself as:
46
46
  ```ruby
47
47
  Grumlin.configure do |config|
48
48
  config.url = "ws://localhost:8182/gremlin"
49
+
50
+ # make sure you select right provider for better compatibility
51
+ config.provider = :tinkergraph
49
52
  end
50
53
  ```
51
54
 
55
+ #### Providers
56
+
57
+ Currently `Grumlin` supports 2 providers:
58
+ - tinkergraph (default)
59
+ - neptune
60
+
61
+ As different providers may have or may have not support for specific features it's recommended to
62
+ explicitly specify the provider you use.
63
+
64
+ #### Provider features
65
+
66
+ Every provider is described by a set of features. In the future `Grumlin` may decide to disable or enable
67
+ some parts of it's functionality to comply provider's supported features. Currently there is no difference
68
+ in behaviour when working with different providers.
69
+
70
+ To check current providers supported features use
71
+
72
+ ```ruby
73
+ Grumlin.features
74
+ ```
75
+
52
76
  ### Traversing graphs
53
77
 
54
78
  **Warning**: Not all steps and expressions defined in the reference documentation are supported.
@@ -231,6 +255,10 @@ All of them support 3 different modes for error handling: `:retry`, `:ignore` an
231
255
  with [retryable](https://github.com/nfedyashev/retryable). **params will be merged to the default config for upserts
232
256
  and passed to `Retryable.retryable`. In case if you want to modify retryable behaviour you are to do so.
233
257
 
258
+ If you don't want to define you own repository, simply use
259
+
260
+ `Grumlin::Repository.new` returns an instance of an anonymous class extending `Grumlin::Repository`.
261
+
234
262
  **Usage**
235
263
 
236
264
  To execute the query defined in a query block one simply needs to call a method with the same name:
@@ -1,4 +1,4 @@
1
- FROM tinkerpop/gremlin-server
1
+ FROM tinkerpop/gremlin-server:3.5.3
2
2
 
3
3
  RUN rm -rf /opt/gremlin-server/ext/tinkergraph-gremlin
4
4
 
data/grumlin.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.metadata["homepage_uri"] = spec.homepage
22
22
  spec.metadata["source_code_uri"] = "https://github.com/zhulik/grumlin"
23
- spec.metadata["changelog_uri"] = "https://github.com/zhulik/grumlin/blob/master/CHANGELOG.md"
23
+ spec.metadata["changelog_uri"] = "https://github.com/babbel/grumlin/releases"
24
24
  spec.metadata["rubygems_mfa_required"] = "true"
25
25
 
26
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
data/lib/definitions.yml CHANGED
@@ -82,6 +82,7 @@ steps:
82
82
  configuration:
83
83
  - withSack
84
84
  - withSideEffect
85
+ - withStrategies
85
86
  expressions:
86
87
  cardinality:
87
88
  - list
@@ -4,14 +4,15 @@ module Grumlin
4
4
  class Action < Steppable
5
5
  attr_reader :name, :args, :params, :next_step, :configuration_steps, :previous_step, :shortcut
6
6
 
7
- def initialize(name, args: [], params: {}, previous_step: nil, pool: nil)
8
- super()
7
+ # client is only used when a traversal is a part of transaction
8
+ def initialize(name, args: [], params: {}, previous_step: nil, pool: Grumlin.default_pool, session_id: nil)
9
+ super(pool: pool, session_id: session_id)
10
+
9
11
  @name = name.to_sym
10
12
  @args = args # TODO: add recursive validation: only json types or Action
11
13
  @params = params # TODO: add recursive validation: only json types
12
14
  @previous_step = previous_step
13
15
  @shortcut = shortcuts[@name]
14
- @pool = pool || Grumlin.default_pool
15
16
  end
16
17
 
17
18
  def configuration_step?
@@ -73,14 +74,18 @@ module Grumlin
73
74
  end
74
75
 
75
76
  def toList
76
- @pool.acquire do |client|
77
- client.write(bytecode)
78
- end
77
+ client_write(bytecode)
79
78
  end
80
79
 
81
80
  def iterate
81
+ client_write(bytecode(no_return: true))
82
+ end
83
+
84
+ private
85
+
86
+ def client_write(payload)
82
87
  @pool.acquire do |client|
83
- client.write(bytecode(no_return: true))
88
+ client.write(payload, session_id: @session_id)
84
89
  end
85
90
  end
86
91
  end
@@ -24,8 +24,14 @@ module Grumlin
24
24
  @client.close
25
25
  end
26
26
 
27
- def write(bytecode)
28
- @client.write(bytecode)
27
+ def write(bytecode, session_id: nil)
28
+ @client.write(bytecode, session_id: session_id)
29
+ ensure
30
+ @count += 1
31
+ end
32
+
33
+ def finalize_tx(action, session_id)
34
+ @client.finalize_tx(action, session_id)
29
35
  ensure
30
36
  @count += 1
31
37
  end
@@ -94,19 +100,19 @@ module Grumlin
94
100
  end
95
101
 
96
102
  # TODO: support yielding
97
- def write(bytecode)
103
+ def write(bytecode, session_id: nil)
98
104
  raise NotConnectedError unless connected?
99
105
 
100
- request = to_query(bytecode)
101
- channel = @request_dispatcher.add_request(request)
102
- @transport.write(request)
106
+ request = to_query(bytecode, session_id: session_id)
107
+ submit_request(request)
108
+ end
103
109
 
104
- begin
105
- channel.dequeue.flat_map { |item| Typing.cast(item) }
106
- rescue Async::Stop, Async::TimeoutError
107
- close(check_requests: false)
108
- raise
109
- end
110
+ def finalize_tx(action, session_id)
111
+ raise NotConnectedError unless connected?
112
+ raise ArgumentError, "session_id cannot be nil" if session_id.nil?
113
+
114
+ request = finalize_tx_query(action, session_id)
115
+ submit_request(request)
110
116
  end
111
117
 
112
118
  def inspect
@@ -119,20 +125,52 @@ module Grumlin
119
125
 
120
126
  private
121
127
 
128
+ def submit_request(request)
129
+ channel = @request_dispatcher.add_request(request)
130
+ @transport.write(request)
131
+
132
+ begin
133
+ channel.dequeue.flat_map { |item| Typing.cast(item) }
134
+ rescue Async::Stop, Async::TimeoutError
135
+ close(check_requests: false)
136
+ raise
137
+ end
138
+ end
139
+
122
140
  # This might be overridden in successors
123
141
  def build_transport
124
142
  Transport.new(@url, parent: @parent, **@client_options)
125
143
  end
126
144
 
127
- def to_query(bytecode)
145
+ def to_query(bytecode, session_id:)
146
+ {
147
+ requestId: SecureRandom.uuid,
148
+ op: :bytecode,
149
+ processor: session_id ? :session : :traversal,
150
+ args: {
151
+ gremlin: {
152
+ :@type => "g:Bytecode",
153
+ :@value => bytecode.serialize
154
+ },
155
+ aliases: { g: :g },
156
+ session: session_id
157
+ }.compact
158
+ }
159
+ end
160
+
161
+ def finalize_tx_query(action, session_id)
128
162
  {
129
163
  requestId: SecureRandom.uuid,
130
- op: "bytecode",
131
- processor: "traversal",
164
+ op: :bytecode,
165
+ processor: session_id ? :session : :traversal,
132
166
  args: {
133
- gremlin: { :@type => "g:Bytecode", :@value => bytecode.serialize },
134
- aliases: { g: :g }
135
- }
167
+ gremlin: {
168
+ :@type => "g:Bytecode",
169
+ :@value => { source: [[:tx, action]] }
170
+ },
171
+ aliases: { g: :g },
172
+ session: session_id
173
+ }.compact
136
174
  }
137
175
  end
138
176
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Config
5
+ attr_accessor :url, :pool_size, :client_concurrency, :client_factory, :provider
6
+
7
+ SUPPORTED_PROVIDERS = %i[neptune tinkergraph].freeze
8
+
9
+ class ConfigurationError < Grumlin::Error; end
10
+
11
+ class UnknownProviderError < ConfigurationError; end
12
+
13
+ def initialize
14
+ @pool_size = 10
15
+ @client_concurrency = 5
16
+ @provider = :tinkergraph
17
+ @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
18
+ end
19
+
20
+ def validate!
21
+ return if SUPPORTED_PROVIDERS.include?(provider.to_sym)
22
+
23
+ raise UnknownProviderError, "provider '#{provider}' is unknown. Supported providers: #{SUPPORTED_PROVIDERS}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class FeatureList
6
+ def user_supplied_ids?
7
+ raise(NotImplementedError) if @user_supplied_ids.nil?
8
+
9
+ @user_supplied_ids
10
+ end
11
+
12
+ def supports_transactions?
13
+ raise(NotImplementedError) if @supports_transactions.nil?
14
+
15
+ @supports_transactions
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class NeptuneFeatures < FeatureList
6
+ def initialize
7
+ super
8
+ @user_supplied_ids = true
9
+ @supports_transactions = true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class TinkergraphFeatures < FeatureList
6
+ def initialize
7
+ super
8
+ @user_supplied_ids = true
9
+ @supports_transactions = false
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module Features
5
+ class << self
6
+ FEATURES = {
7
+ neptune: NeptuneFeatures.new,
8
+ tinkergraph: TinkergraphFeatures.new
9
+ }.freeze
10
+
11
+ def for(provider)
12
+ FEATURES[provider]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -8,7 +8,7 @@ module Grumlin
8
8
  extend Forwardable
9
9
 
10
10
  UPSERT_RETRY_PARAMS = {
11
- on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentInsertFailedError],
11
+ on: [Grumlin::AlreadyExistsError, Grumlin::ConcurrentModificationError],
12
12
  sleep_method: ->(n) { Async::Task.current.sleep(n) },
13
13
  tries: 3,
14
14
  sleep: ->(n) { (n**2) + 1 + rand }
@@ -56,7 +56,7 @@ module Grumlin
56
56
  with_upsert_error_handling(on_failure, params) do
57
57
  create_properties, update_properties = cleanup_properties(create_properties, update_properties)
58
58
 
59
- g.upsertV(label, id, create_properties, update_properties).next
59
+ g.upsertV(label, id, create_properties, update_properties).id.next
60
60
  end
61
61
  end
62
62
 
@@ -70,7 +70,7 @@ module Grumlin
70
70
  create_properties, update_properties = cleanup_properties(create_properties, update_properties)
71
71
 
72
72
  t.upsertV(label, id, create_properties, update_properties)
73
- end.iterate
73
+ end.id.iterate
74
74
  end
75
75
  end
76
76
  end
@@ -80,7 +80,7 @@ module Grumlin
80
80
  def upsert_edge(label, from:, to:, create_properties: {}, update_properties: {}, on_failure: :retry, **params) # rubocop:disable Metrics/ParameterLists
81
81
  with_upsert_error_handling(on_failure, params) do
82
82
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
83
- g.upsertE(label, from, to, create_properties, update_properties).next
83
+ g.upsertE(label, from, to, create_properties, update_properties).id.next
84
84
  end
85
85
  end
86
86
 
@@ -94,7 +94,7 @@ module Grumlin
94
94
  create_properties, update_properties = cleanup_properties(create_properties, update_properties, T.label)
95
95
 
96
96
  t.upsertE(label, from, to, create_properties, update_properties)
97
- end.iterate
97
+ end.id.iterate
98
98
  end
99
99
  end
100
100
  end
@@ -10,6 +10,7 @@ module Grumlin
10
10
  }.freeze
11
11
 
12
12
  def self.extended(base)
13
+ super
13
14
  base.extend(Grumlin::Shortcuts)
14
15
  base.include(Repository::InstanceMethods)
15
16
 
@@ -17,6 +18,12 @@ module Grumlin
17
18
  base.shortcuts_from(Grumlin::Shortcuts::Upserts)
18
19
  end
19
20
 
21
+ def self.new
22
+ @repository ||= Class.new do # rubocop:disable Naming/MemoizedInstanceVariableName
23
+ extend Grumlin::Repository
24
+ end.new
25
+ end
26
+
20
27
  def query(name, return_mode: :list, postprocess_with: nil, &query_block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
21
28
  return_mode = validate_return_mode!(return_mode)
22
29
  postprocess_with = validate_postprocess_with!(postprocess_with)
@@ -10,23 +10,6 @@ module Grumlin
10
10
  206 => :partial_content
11
11
  }.freeze
12
12
 
13
- ERRORS = {
14
- 499 => InvalidRequestArgumentsError,
15
- 500 => ServerError,
16
- 597 => ScriptEvaluationError,
17
- 599 => ServerSerializationError,
18
- 598 => ServerTimeoutError,
19
-
20
- 401 => ClientSideError,
21
- 407 => ClientSideError,
22
- 498 => ClientSideError
23
- }.freeze
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
-
30
13
  class DispatcherError < Grumlin::Error; end
31
14
 
32
15
  class RequestAlreadyAddedError < DispatcherError; end
@@ -47,14 +30,16 @@ module Grumlin
47
30
 
48
31
  # builds a response object, when it's ready sends it to the client via a channel
49
32
  # TODO: sometimes response does not include requestID, no idea how to handle it so far.
50
- def add_response(response) # rubocop:disable Metrics/AbcSize
33
+ def add_response(response) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
51
34
  request_id = response[:requestId]
52
35
  raise UnknownRequestError unless ongoing_request?(request_id)
53
36
 
54
37
  begin
55
38
  request = @requests[request_id]
56
39
 
57
- check_errors!(response[:status], request[:request])
40
+ RequestErrorFactory.build(request, response).tap do |err|
41
+ raise err unless err.nil?
42
+ end
58
43
 
59
44
  case SUCCESS[response.dig(:status, :code)]
60
45
  when :success
@@ -91,29 +76,5 @@ module Grumlin
91
76
  request = @requests.delete(request_id)
92
77
  request[:channel].close
93
78
  end
94
-
95
- def check_errors!(status, query)
96
- if (error = ERRORS[status[:code]])
97
- raise (
98
- already_exists_error(status) ||
99
- concurrent_insert_error(status) ||
100
- error
101
- ).new(status, query)
102
- end
103
-
104
- return unless SUCCESS[status[:code]].nil?
105
-
106
- raise(UnknownResponseStatus, status)
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
118
79
  end
119
80
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class RequestErrorFactory
5
+ ERRORS = {
6
+ 499 => InvalidRequestArgumentsError,
7
+ 500 => ServerError,
8
+ 597 => ScriptEvaluationError,
9
+ 599 => ServerSerializationError,
10
+ 598 => ServerTimeoutError,
11
+
12
+ 401 => ClientSideError,
13
+ 407 => ClientSideError,
14
+ 498 => ClientSideError
15
+ }.freeze
16
+
17
+ # Neptune presumably returns message as a JSON string of format
18
+ # {"detailedMessage":"",
19
+ # "requestId":"UUID",
20
+ # "code":"ConcurrentModificationException"}
21
+ # Currencly we simply search for substings to identify the exact error
22
+ # TODO: parse json and use `code` instead
23
+ VERTEX_ALREADY_EXISTS = "Vertex with id already exists:"
24
+ EDGE_ALREADY_EXISTS = "Edge with id already exists:"
25
+ CONCURRENT_VERTEX_INSERT_FAILED = "Failed to complete Insert operation for a Vertex due to conflicting concurrent"
26
+ CONCURRENT_EDGE_INSERT_FAILED = "Failed to complete Insert operation for an Edge due to conflicting concurrent"
27
+ CONCURRENCT_MODIFICATION_FAILED = "Failed to complete operation due to conflicting concurrent"
28
+
29
+ class << self
30
+ def build(request, response)
31
+ status = response[:status]
32
+ query = request[:request]
33
+
34
+ if (error = ERRORS[status[:code]])
35
+ return (
36
+ already_exists_error(status) ||
37
+ concurrent_modification_error(status) ||
38
+ error
39
+ ).new(status, query)
40
+ end
41
+
42
+ return unless RequestDispatcher::SUCCESS[status[:code]].nil?
43
+
44
+ UnknownResponseStatus.new(status)
45
+ end
46
+
47
+ def already_exists_error(status)
48
+ return VertexAlreadyExistsError if status[:message]&.include?(VERTEX_ALREADY_EXISTS)
49
+ return EdgeAlreadyExistsError if status[:message]&.include?(EDGE_ALREADY_EXISTS)
50
+ end
51
+
52
+ def concurrent_modification_error(status)
53
+ return ConcurrentVertexInsertFailedError if status[:message]&.include?(CONCURRENT_VERTEX_INSERT_FAILED)
54
+ return ConcurrentEdgeInsertFailedError if status[:message]&.include?(CONCURRENT_EDGE_INSERT_FAILED)
55
+ return ConcurrentModificationError if status[:message]&.include?(CONCURRENCT_MODIFICATION_FAILED)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,11 +5,12 @@ module Grumlin
5
5
  module Properties
6
6
  extend Grumlin::Shortcuts
7
7
 
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
8
+ shortcut :props do |cardinality = nil, **props|
9
+ props.reduce(self) do |tt, (prop, value)|
10
+ next tt if value.nil? # nils are not supported
11
+ next tt.property(prop, value) if cardinality.nil?
11
12
 
12
- tt
13
+ tt.property(cardinality, prop, value)
13
14
  end
14
15
  end
15
16
 
@@ -10,8 +10,8 @@ module Grumlin
10
10
  .fold
11
11
  .coalesce(
12
12
  __.unfold,
13
- __.addV(label).props(**create_properties.merge(T.id => id))
14
- ).props(**update_properties)
13
+ __.addV(label).props(Cardinality.single, **create_properties.merge(T.id => id))
14
+ ).props(Cardinality.single, **update_properties)
15
15
  end
16
16
 
17
17
  shortcut :upsertE do |label, from, to, create_properties = {}, update_properties = {}|
@@ -4,27 +4,31 @@ module Grumlin
4
4
  class Steppable
5
5
  extend Forwardable
6
6
 
7
+ attr_reader :session_id
8
+
7
9
  START_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
8
10
  REGULAR_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
9
11
  CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
10
12
 
11
13
  ALL_STEPS = START_STEPS + CONFIGURATION_STEPS + REGULAR_STEPS
12
14
 
13
- def initialize
15
+ def initialize(pool: Grumlin.default_pool, session_id: nil)
16
+ @pool = pool
17
+ @session_id = session_id
18
+
14
19
  return if respond_to?(:shortcuts)
15
20
 
16
- raise RuntimerError,
17
- "steppable must not be initialized directly, use Grumlin::Shortcuts::Storage#g or #__ instead"
21
+ raise "steppable must not be initialized directly, use Grumlin::Shortcuts::Storage#g or #__ instead"
18
22
  end
19
23
 
20
24
  ALL_STEPS.each do |step|
21
25
  define_method step do |*args, **params|
22
- shortcuts.action_class.new(step, args: args, params: params, previous_step: self)
26
+ shortcuts.action_class.new(step, args: args, params: params, previous_step: self, session_id: @session_id)
23
27
  end
24
28
  end
25
29
 
26
30
  def step(name, *args, **params)
27
- shortcuts.action_class.new(name, args: args, params: params, previous_step: self)
31
+ shortcuts.action_class.new(name, args: args, params: params, previous_step: self, session_id: @session_id)
28
32
  end
29
33
 
30
34
  def_delegator :shortcuts, :__
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Transaction
5
+ attr_reader :uuid
6
+
7
+ include Console
8
+
9
+ def initialize(traversal_start_class, pool: Grumlin.default_pool)
10
+ @traversal_start_class = traversal_start_class
11
+ @pool = pool
12
+
13
+ if supported?
14
+ @uuid = SecureRandom.uuid
15
+ return
16
+ end
17
+
18
+ logger.info(self) do
19
+ "#{Grumlin.config.provider} does not support transactions. commit and rollback are ignored, data will be saved"
20
+ end
21
+ end
22
+
23
+ def supported?
24
+ Grumlin.features.supports_transactions?
25
+ end
26
+
27
+ def begin
28
+ @traversal_start_class.new(session_id: @uuid)
29
+ end
30
+
31
+ def commit
32
+ return unless supported?
33
+
34
+ finalize(:commit)
35
+ end
36
+
37
+ def rollback
38
+ return unless supported?
39
+
40
+ finalize(:rollback)
41
+ end
42
+
43
+ private
44
+
45
+ def finalize(action)
46
+ @pool.acquire do |client|
47
+ client.finalize_tx(action, @uuid)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,17 @@
2
2
 
3
3
  module Grumlin
4
4
  class TraversalStart < Steppable
5
+ include WithExtension
6
+
7
+ class TraversalError < Grumlin::Error; end
8
+ class AlreadyBoundToTransationError < TraversalError; end
9
+
10
+ def tx
11
+ raise AlreadyBoundToTransationError if @session_id
12
+
13
+ Transaction.new(self.class, pool: @pool)
14
+ end
15
+
5
16
  def to_s(*)
6
17
  self.class.to_s
7
18
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module TraversalStrategies
5
+ class OptionsStrategy < TypedValue
6
+ def initialize(value)
7
+ super(type: "OptionsStrategy", value: value)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.20.1"
4
+ VERSION = "0.21.1"
5
5
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module WithExtension
5
+ def with(name, value)
6
+ prev = self
7
+ strategy = if is_a?(with_action_class)
8
+ prev = previous_step
9
+ TraversalStrategies::OptionsStrategy.new(args.first.value.merge(name => value))
10
+ else
11
+ TraversalStrategies::OptionsStrategy.new({ name => value })
12
+ end
13
+ with_action_class.new(:withStrategies, args: [strategy], previous_step: prev)
14
+ end
15
+
16
+ private
17
+
18
+ def with_action_class
19
+ @with_action_class ||= Class.new(shortcuts.action_class) do
20
+ include WithExtension
21
+
22
+ def with_action_class
23
+ self.class
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/grumlin.rb CHANGED
@@ -98,7 +98,8 @@ module Grumlin
98
98
  class VertexAlreadyExistsError < AlreadyExistsError; end
99
99
  class EdgeAlreadyExistsError < AlreadyExistsError; end
100
100
 
101
- class ConcurrentInsertFailedError < ServerError; end
101
+ class ConcurrentModificationError < ServerError; end
102
+ class ConcurrentInsertFailedError < ConcurrentModificationError; end
102
103
 
103
104
  class ConcurrentVertexInsertFailedError < ConcurrentInsertFailedError; end
104
105
  class ConcurrentEdgeInsertFailedError < ConcurrentInsertFailedError; end
@@ -127,27 +128,26 @@ module Grumlin
127
128
 
128
129
  class WrongQueryResult < RepositoryError; end
129
130
 
130
- class Config
131
- attr_accessor :url, :pool_size, :client_concurrency, :client_factory
132
-
133
- def initialize
134
- @pool_size = 10
135
- @client_concurrency = 5
136
- @client_factory = ->(url, parent) { Grumlin::Client.new(url, parent: parent) }
137
- end
138
- end
139
-
140
131
  @pool_mutex = Mutex.new
141
132
 
142
133
  class << self
143
134
  def configure
144
135
  yield config
136
+
137
+ config.validate!
145
138
  end
146
139
 
147
140
  def config
148
141
  @config ||= Config.new
149
142
  end
150
143
 
144
+ # returns a subset of features for currently configured backend.
145
+ # The features lists are hardcoded as there is no way to get them
146
+ # from the remote server.
147
+ def features
148
+ Features.for(config.provider) # no memoization as provider may be changed
149
+ end
150
+
151
151
  def default_pool
152
152
  if Thread.current.thread_variable_get(:grumlin_default_pool)
153
153
  return Thread.current.thread_variable_get(:grumlin_default_pool)
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.20.1
4
+ version: 0.21.1
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-07-20 00:00:00.000000000 Z
11
+ date: 2022-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -96,7 +96,6 @@ files:
96
96
  - ".rspec"
97
97
  - ".rubocop.yml"
98
98
  - ".tool-versions"
99
- - CHANGELOG.md
100
99
  - CODE_OF_CONDUCT.md
101
100
  - Gemfile
102
101
  - Gemfile.lock
@@ -117,6 +116,7 @@ files:
117
116
  - lib/grumlin/action.rb
118
117
  - lib/grumlin/benchmark/repository.rb
119
118
  - lib/grumlin/client.rb
119
+ - lib/grumlin/config.rb
120
120
  - lib/grumlin/edge.rb
121
121
  - lib/grumlin/expressions/cardinality.rb
122
122
  - lib/grumlin/expressions/column.rb
@@ -129,12 +129,17 @@ files:
129
129
  - lib/grumlin/expressions/t.rb
130
130
  - lib/grumlin/expressions/text_p.rb
131
131
  - lib/grumlin/expressions/with_options.rb
132
+ - lib/grumlin/features.rb
133
+ - lib/grumlin/features/feature_list.rb
134
+ - lib/grumlin/features/neptune_features.rb
135
+ - lib/grumlin/features/tinkergraph_features.rb
132
136
  - lib/grumlin/path.rb
133
137
  - lib/grumlin/property.rb
134
138
  - lib/grumlin/repository.rb
135
139
  - lib/grumlin/repository/error_handling_strategy.rb
136
140
  - lib/grumlin/repository/instance_methods.rb
137
141
  - lib/grumlin/request_dispatcher.rb
142
+ - lib/grumlin/request_error_factory.rb
138
143
  - lib/grumlin/shortcut.rb
139
144
  - lib/grumlin/shortcuts.rb
140
145
  - lib/grumlin/shortcuts/properties.rb
@@ -152,13 +157,16 @@ files:
152
157
  - lib/grumlin/test/rspec.rb
153
158
  - lib/grumlin/test/rspec/db_cleaner_context.rb
154
159
  - lib/grumlin/test/rspec/gremlin_context.rb
160
+ - lib/grumlin/transaction.rb
155
161
  - lib/grumlin/transport.rb
156
162
  - lib/grumlin/traversal_start.rb
163
+ - lib/grumlin/traversal_strategies/options_strategy.rb
157
164
  - lib/grumlin/traverser.rb
158
165
  - lib/grumlin/typed_value.rb
159
166
  - lib/grumlin/typing.rb
160
167
  - lib/grumlin/version.rb
161
168
  - lib/grumlin/vertex.rb
169
+ - lib/grumlin/with_extension.rb
162
170
  - lib/tasks/benchmark.rake
163
171
  homepage: https://github.com/zhulik/grumlin
164
172
  licenses:
@@ -166,7 +174,7 @@ licenses:
166
174
  metadata:
167
175
  homepage_uri: https://github.com/zhulik/grumlin
168
176
  source_code_uri: https://github.com/zhulik/grumlin
169
- changelog_uri: https://github.com/zhulik/grumlin/blob/master/CHANGELOG.md
177
+ changelog_uri: https://github.com/babbel/grumlin/releases
170
178
  rubygems_mfa_required: 'true'
171
179
  post_install_message:
172
180
  rdoc_options: []
data/CHANGELOG.md DELETED
@@ -1,71 +0,0 @@
1
- ## [0.16.0] - 2022-03-11
2
-
3
- - Query building is rewritten from scratch. No public APIs were changed. [Details](https://github.com/babbel/grumlin/pull/64)
4
- - Add support for [TextP](https://tinkerpop.apache.org/javadocs/current/core/org/apache/tinkerpop/gremlin/process/traversal/TextP.html)
5
-
6
- ## [0.15.4] - 2022-01-20
7
-
8
- - Move step and expression definitions to a yaml file for better diffs
9
- - Add `definitions:format` rake task
10
-
11
- ## [0.15.3] - 2022-01-18
12
-
13
- - Fix passing nils as step arguments. Even if they are not supported by the server, they should not be omitted.
14
-
15
- ## [0.15.2] - 2022-01-17
16
-
17
- - New steps: `map` and `identity`
18
-
19
- ## [0.15.1] - 2022-01-17
20
-
21
- - Fix passing arrays as step arguments
22
-
23
- ## [0.15.0] - 2022-01-11
24
-
25
- - Add `properties` step
26
- - Add proper support for bulked results
27
- - Add support for `Property` objects
28
-
29
- ## [0.14.5] - 2021-12-27
30
-
31
- - Fix params handling
32
- - Add `aggregate` step
33
- - Add `Order.shuffle`
34
-
35
- ## [0.14.4] - 2021-12-17
36
-
37
- - `Grumlin::Repository.shorcuts_from` do not raise `ArgumentError` when importing an already existing shortcut
38
- pointing to the same block. This fixes importing shortcuts from another repository.
39
-
40
- ## [0.14.2] - 2021-12-13
41
-
42
- - Fix `Module` bloating
43
- - Add `Operator` expressions
44
- - Add `__.coalesce` and `__.constant`
45
- - Add steps: `sum`, `sack`
46
- - Add configuration steps: `withSack`
47
- - Rename `Grumlin::Expressions::Tool` to `Grumlin::Expressions::Expression`
48
-
49
-
50
- ## [0.14.2] - 2021-12-12
51
-
52
- - Better exceptions
53
- - Add `choose` step
54
- - Add `__.hasNot`, `__.is`, `__.select`
55
-
56
- ## [0.14.0] - 2021-12-07
57
-
58
- - Add initial support for [configuration steps](https://tinkerpop.apache.org/docs/current/reference/#configuration-steps)
59
- - Add the `withSideEffect` configuration step
60
- - Fix passing keyword arguments to regular steps
61
- - *Drop support for ruby 2.6*
62
-
63
- ## [0.13.0] - 2021-12-03
64
-
65
- - Add `Shortcuts` and `Repository`
66
- - Allow executing any gremlin steps by name using `Grumlin::AnonymousStep#step`
67
- - Rename `Grumlin::Tools` to `Grumlin::Expressions`
68
-
69
- ## [0.1.0] - 2021-05-25
70
-
71
- - Initial release