google-cloud-datastore 0.20.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.
@@ -0,0 +1,171 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/datastore/credentials"
17
+ require "google/datastore/v1/datastore_services_pb"
18
+ require "google/cloud/core/grpc_backoff"
19
+
20
+ module Google
21
+ module Cloud
22
+ module Datastore
23
+ ##
24
+ # @private Represents the gRPC Datastore service, including all the API
25
+ # methods.
26
+ class Service
27
+ attr_accessor :project, :credentials, :host, :retries, :timeout
28
+
29
+ ##
30
+ # Creates a new Service instance.
31
+ def initialize project, credentials, host: nil, retries: nil,
32
+ timeout: nil
33
+ @project = project
34
+ @credentials = credentials
35
+ @host = host || "datastore.googleapis.com"
36
+ @retries = retries
37
+ @timeout = timeout
38
+ end
39
+
40
+ def creds
41
+ return credentials if insecure?
42
+ GRPC::Core::ChannelCredentials.new.compose \
43
+ GRPC::Core::CallCredentials.new credentials.client.updater_proc
44
+ end
45
+
46
+ def datastore
47
+ return mocked_datastore if mocked_datastore
48
+ @datastore ||= Google::Datastore::V1::Datastore::Stub.new(
49
+ host, creds, timeout: timeout)
50
+ end
51
+ attr_accessor :mocked_datastore
52
+
53
+ def insecure?
54
+ credentials == :this_channel_is_insecure
55
+ end
56
+
57
+ ##
58
+ # Allocate IDs for incomplete keys.
59
+ # (This is useful for referencing an entity before it is inserted.)
60
+ def allocate_ids *incomplete_keys
61
+ allocate_req = Google::Datastore::V1::AllocateIdsRequest.new(
62
+ project_id: project,
63
+ keys: incomplete_keys
64
+ )
65
+
66
+ execute { datastore.allocate_ids allocate_req }
67
+ end
68
+
69
+ ##
70
+ # Look up entities by keys.
71
+ def lookup *keys, consistency: nil, transaction: nil
72
+ lookup_req = Google::Datastore::V1::LookupRequest.new(
73
+ project_id: project,
74
+ keys: keys
75
+ )
76
+ lookup_req.read_options = generate_read_options consistency,
77
+ transaction
78
+
79
+ execute { datastore.lookup lookup_req }
80
+ end
81
+
82
+ # Query for entities.
83
+ def run_query query, namespace = nil, consistency: nil, transaction: nil
84
+ run_req = Google::Datastore::V1::RunQueryRequest.new(
85
+ project_id: project)
86
+ if query.is_a? Google::Datastore::V1::Query
87
+ run_req["query"] = query
88
+ elsif query.is_a? Google::Datastore::V1::GqlQuery
89
+ run_req["gql_query"] = query
90
+ else
91
+ fail ArgumentError, "Unable to query with a #{query.class} object."
92
+ end
93
+ run_req.read_options = generate_read_options consistency, transaction
94
+
95
+ run_req.partition_id = Google::Datastore::V1::PartitionId.new(
96
+ namespace_id: namespace) if namespace
97
+
98
+ execute { datastore.run_query run_req }
99
+ end
100
+
101
+ ##
102
+ # Begin a new transaction.
103
+ def begin_transaction
104
+ tx_req = Google::Datastore::V1::BeginTransactionRequest.new(
105
+ project_id: project
106
+ )
107
+
108
+ execute { datastore.begin_transaction tx_req }
109
+ end
110
+
111
+ ##
112
+ # Commit a transaction, optionally creating, deleting or modifying
113
+ # some entities.
114
+ def commit mutations, transaction: nil
115
+ commit_req = Google::Datastore::V1::CommitRequest.new(
116
+ project_id: project,
117
+ mode: :NON_TRANSACTIONAL,
118
+ mutations: mutations
119
+ )
120
+ if transaction
121
+ commit_req.mode = :TRANSACTIONAL
122
+ commit_req.transaction = transaction
123
+ end
124
+
125
+ execute { datastore.commit commit_req }
126
+ end
127
+
128
+ ##
129
+ # Roll back a transaction.
130
+ def rollback transaction
131
+ rb_req = Google::Datastore::V1::RollbackRequest.new(
132
+ project_id: project,
133
+ transaction: transaction
134
+ )
135
+
136
+ execute { datastore.rollback rb_req }
137
+ end
138
+
139
+ def inspect
140
+ "#{self.class}(#{@project})"
141
+ end
142
+
143
+ ##
144
+ # Performs backoff and error handling
145
+ def execute
146
+ Google::Cloud::Core::GrpcBackoff.new(retries: retries).execute do
147
+ yield
148
+ end
149
+ rescue GRPC::BadStatus => e
150
+ raise Google::Cloud::Error.from_error(e)
151
+ end
152
+
153
+ protected
154
+
155
+ def generate_read_options consistency, transaction
156
+ if consistency == :eventual
157
+ return Google::Datastore::V1::ReadOptions.new(
158
+ read_consistency: :EVENTUAL)
159
+ elsif consistency == :strong
160
+ return Google::Datastore::V1::ReadOptions.new(
161
+ read_consistency: :STRONG)
162
+ elsif transaction
163
+ return Google::Datastore::V1::ReadOptions.new(
164
+ transaction: transaction)
165
+ end
166
+ nil
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,365 @@
1
+ # Copyright 2014 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Datastore
19
+ ##
20
+ # # Transaction
21
+ #
22
+ # Special Connection instance for running transactions.
23
+ #
24
+ # See {Google::Cloud::Datastore::Dataset#transaction}
25
+ #
26
+ # @see https://cloud.google.com/datastore/docs/concepts/transactions
27
+ # Transactions
28
+ #
29
+ # @example Transactional update:
30
+ # def transfer_funds from_key, to_key, amount
31
+ # datastore.transaction do |tx|
32
+ # from = tx.find from_key
33
+ # from["balance"] -= amount
34
+ # to = tx.find to_key
35
+ # to["balance"] += amount
36
+ # tx.save from, to
37
+ # end
38
+ # end
39
+ #
40
+ # @example Retry logic using the transactional update example above:
41
+ # (1..5).each do |i|
42
+ # begin
43
+ # return transfer_funds from_key, to_key, amount
44
+ # rescue Google::Cloud::Error => e
45
+ # raise e if i == 5
46
+ # end
47
+ # end
48
+ #
49
+ # @example Transactional read:
50
+ # task_list_key = datastore.key "TaskList", "default"
51
+ # datastore.transaction do |tx|
52
+ # task_list = tx.find task_list_key
53
+ # query = tx.query("Task").ancestor(task_list)
54
+ # tasks_in_list = tx.run query
55
+ # end
56
+ #
57
+ class Transaction < Dataset
58
+ attr_reader :id
59
+
60
+ ##
61
+ # @private Creates a new Transaction instance.
62
+ # Takes a Connection and Service instead of project and Credentials.
63
+ def initialize service
64
+ @service = service
65
+ reset!
66
+ start
67
+ end
68
+
69
+ ##
70
+ # Persist entities in a transaction.
71
+ #
72
+ # @example Transactional get or create:
73
+ # task_key = datastore.key "Task", "sampleTask"
74
+ #
75
+ # task = nil
76
+ # datastore.transaction do |tx|
77
+ # task = tx.find task_key
78
+ # if task.nil?
79
+ # task = datastore.entity task_key do |t|
80
+ # t["type"] = "Personal"
81
+ # t["done"] = false
82
+ # t["priority"] = 4
83
+ # t["description"] = "Learn Cloud Datastore"
84
+ # end
85
+ # tx.save task
86
+ # end
87
+ # end
88
+ #
89
+ def save *entities
90
+ @commit.save(*entities)
91
+ # Do not save yet
92
+ entities
93
+ end
94
+ alias_method :upsert, :save
95
+
96
+ ##
97
+ # Insert entities in a transaction. An InvalidArgumentError will raised
98
+ # if the entities cannot be inserted.
99
+ #
100
+ # @example Transactional insert:
101
+ # task_key = datastore.key "Task", "sampleTask"
102
+ #
103
+ # task = nil
104
+ # datastore.transaction do |tx|
105
+ # task = tx.find task_key
106
+ # if task.nil?
107
+ # task = datastore.entity task_key do |t|
108
+ # t["type"] = "Personal"
109
+ # t["done"] = false
110
+ # t["priority"] = 4
111
+ # t["description"] = "Learn Cloud Datastore"
112
+ # end
113
+ # tx.insert task
114
+ # end
115
+ # end
116
+ #
117
+ def insert *entities
118
+ @commit.insert(*entities)
119
+ # Do not insert yet
120
+ entities
121
+ end
122
+
123
+ ##
124
+ # Update entities in a transaction. An InvalidArgumentError will raised
125
+ # if the entities cannot be updated.
126
+ #
127
+ # @example Transactional update:
128
+ # task_key = datastore.key "Task", "sampleTask"
129
+ #
130
+ # task = nil
131
+ # datastore.transaction do |tx|
132
+ # task = tx.find task_key
133
+ # if task
134
+ # task["done"] = true
135
+ # tx.update task
136
+ # end
137
+ # end
138
+ #
139
+ def update *entities
140
+ @commit.update(*entities)
141
+ # Do not update yet
142
+ entities
143
+ end
144
+
145
+ ##
146
+ # Remove entities in a transaction.
147
+ #
148
+ # @example
149
+ # datastore.transaction do |tx|
150
+ # if tx.find(task_list.key).nil?
151
+ # tx.delete task1, task2
152
+ # end
153
+ # end
154
+ #
155
+ def delete *entities_or_keys
156
+ @commit.delete(*entities_or_keys)
157
+ # Do not delete yet
158
+ true
159
+ end
160
+
161
+ ##
162
+ # Retrieve an entity by providing key information. The lookup is run
163
+ # within the transaction.
164
+ #
165
+ # @param [Key, String] key_or_kind A Key object or `kind` string value.
166
+ #
167
+ # @return [Google::Cloud::Datastore::Entity, nil]
168
+ #
169
+ # @example Finding an entity with a key:
170
+ # task_key = datastore.key "Task", "sampleTask"
171
+ # task = datastore.find task_key
172
+ #
173
+ # @example Finding an entity with a `kind` and `id`/`name`:
174
+ # task = datastore.find "Task", "sampleTask"
175
+ #
176
+ def find key_or_kind, id_or_name = nil
177
+ key = key_or_kind
178
+ unless key.is_a? Google::Cloud::Datastore::Key
179
+ key = Key.new key_or_kind, id_or_name
180
+ end
181
+ find_all(key).first
182
+ end
183
+ alias_method :get, :find
184
+
185
+ ##
186
+ # Retrieve the entities for the provided keys. The lookup is run within
187
+ # the transaction.
188
+ #
189
+ # @param [Key] keys One or more Key objects to find records for.
190
+ #
191
+ # @return [Google::Cloud::Datastore::Dataset::LookupResults]
192
+ #
193
+ # @example
194
+ # gcloud = Google::Cloud.new
195
+ # datastore = gcloud.datastore
196
+ # task_key1 = datastore.key "Task", 123456
197
+ # task_key2 = datastore.key "Task", 987654
198
+ # tasks = datastore.find_all task_key1, task_key2
199
+ #
200
+ def find_all *keys
201
+ ensure_service!
202
+ lookup_res = service.lookup(*Array(keys).flatten.map(&:to_grpc),
203
+ transaction: @id)
204
+ LookupResults.from_grpc lookup_res, service, nil, @id
205
+ end
206
+ alias_method :lookup, :find_all
207
+
208
+ ##
209
+ # Retrieve entities specified by a Query. The query is run within the
210
+ # transaction.
211
+ #
212
+ # @param [Query] query The Query object with the search criteria.
213
+ # @param [String] namespace The namespace the query is to run within.
214
+ #
215
+ # @return [Google::Cloud::Datastore::Dataset::QueryResults]
216
+ #
217
+ # @example
218
+ # query = datastore.query("Task").
219
+ # where("done", "=", false)
220
+ # datastore.transaction do |tx|
221
+ # tasks = tx.run query
222
+ # end
223
+ #
224
+ # @example Run the query within a namespace with the `namespace` option:
225
+ # query = Google::Cloud::Datastore::Query.new.kind("Task").
226
+ # where("done", "=", false)
227
+ # datastore.transaction do |tx|
228
+ # tasks = tx.run query, namespace: "ns~todo-project"
229
+ # end
230
+ #
231
+ def run query, namespace: nil
232
+ ensure_service!
233
+ unless query.is_a?(Query) || query.is_a?(GqlQuery)
234
+ fail ArgumentError, "Cannot run a #{query.class} object."
235
+ end
236
+ query_res = service.run_query query.to_grpc, namespace,
237
+ transaction: @id
238
+ QueryResults.from_grpc query_res, service, namespace,
239
+ query.to_grpc.dup
240
+ end
241
+ alias_method :run_query, :run
242
+
243
+ ##
244
+ # Begins a transaction.
245
+ # This method is run when a new Transaction is created.
246
+ def start
247
+ fail TransactionError, "Transaction already opened." unless @id.nil?
248
+
249
+ ensure_service!
250
+ tx_res = service.begin_transaction
251
+ @id = tx_res.transaction
252
+ end
253
+ alias_method :begin_transaction, :start
254
+
255
+ ##
256
+ # Commits a transaction.
257
+ #
258
+ # @yield [commit] an optional block for making changes
259
+ # @yieldparam [Commit] commit The object that changes are made on
260
+ #
261
+ # @example
262
+ # require "google/cloud"
263
+ #
264
+ # gcloud = Google::Cloud.new
265
+ # datastore = gcloud.datastore
266
+ #
267
+ # task = datastore.entity "Task" do |t|
268
+ # t["type"] = "Personal"
269
+ # t["done"] = false
270
+ # t["priority"] = 4
271
+ # t["description"] = "Learn Cloud Datastore"
272
+ # end
273
+ #
274
+ # tx = datastore.transaction
275
+ # begin
276
+ # if tx.find(task.key).nil?
277
+ # tx.save task
278
+ # end
279
+ # tx.commit
280
+ # rescue
281
+ # tx.rollback
282
+ # end
283
+ #
284
+ # @example Commit can be passed a block, same as {Dataset#commit}:
285
+ # require "google/cloud"
286
+ #
287
+ # gcloud = Google::Cloud.new
288
+ # datastore = gcloud.datastore
289
+ #
290
+ # tx = datastore.transaction
291
+ # begin
292
+ # tx.commit do |c|
293
+ # c.save task1, task2
294
+ # c.delete entity1, entity2
295
+ # end
296
+ # rescue
297
+ # tx.rollback
298
+ # end
299
+ #
300
+ def commit
301
+ fail TransactionError,
302
+ "Cannot commit when not in a transaction." if @id.nil?
303
+
304
+ yield @commit if block_given?
305
+
306
+ ensure_service!
307
+
308
+ commit_res = service.commit @commit.mutations, transaction: @id
309
+ entities = @commit.entities
310
+ returned_keys = commit_res.mutation_results.map(&:key)
311
+ returned_keys.each_with_index do |key, index|
312
+ next if entities[index].nil?
313
+ entities[index].key = Key.from_grpc(key) unless key.nil?
314
+ end
315
+ # Make sure all entity keys are frozen so all show as persisted
316
+ entities.each { |e| e.key.freeze unless e.persisted? }
317
+ true
318
+ end
319
+
320
+ ##
321
+ # Rolls a transaction back.
322
+ #
323
+ # @example
324
+ # require "google/cloud"
325
+ #
326
+ # gcloud = Google::Cloud.new
327
+ # datastore = gcloud.datastore
328
+ #
329
+ # task = datastore.entity "Task" do |t|
330
+ # t["type"] = "Personal"
331
+ # t["done"] = false
332
+ # t["priority"] = 4
333
+ # t["description"] = "Learn Cloud Datastore"
334
+ # end
335
+ #
336
+ # tx = datastore.transaction
337
+ # begin
338
+ # if tx.find(task.key).nil?
339
+ # tx.save task
340
+ # end
341
+ # tx.commit
342
+ # rescue
343
+ # tx.rollback
344
+ # end
345
+ def rollback
346
+ if @id.nil?
347
+ fail TransactionError, "Cannot rollback when not in a transaction."
348
+ end
349
+
350
+ ensure_service!
351
+ service.rollback @id
352
+ true
353
+ end
354
+
355
+ ##
356
+ # Reset the transaction.
357
+ # {Transaction#start} must be called afterwards.
358
+ def reset!
359
+ @id = nil
360
+ @commit = Commit.new
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end