dynamoid 1.3.3 → 1.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +136 -11
- data/Vagrantfile +27 -0
- data/dynamoid.gemspec +1 -1
- data/lib/dynamoid.rb +0 -2
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +125 -25
- data/lib/dynamoid/associations.rb +1 -1
- data/lib/dynamoid/config.rb +1 -3
- data/lib/dynamoid/criteria.rb +5 -5
- data/lib/dynamoid/criteria/chain.rb +164 -26
- data/lib/dynamoid/document.rb +5 -1
- data/lib/dynamoid/fields.rb +35 -21
- data/lib/dynamoid/indexes.rb +2 -2
- data/lib/dynamoid/persistence.rb +28 -7
- data/lib/dynamoid/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 563c512230d5a7f2485b6cf226eff77accfa2a3e
|
4
|
+
data.tar.gz: e2f88e774032ddf472bc81a2b9e2a5844b61933a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a9dba2ea759a74d83f27ef984e3ca963230ddebde577084e89324eb69dd08c65c4c19690891e4086bc9cacadb17eaed5c460c03944dcc9db1eb109044ceef91
|
7
|
+
data.tar.gz: 314ffb14350619c950bd6615d46ccb3ca32383116f1a1dfe0aa39891694575e89d3376051bfac53b4e4a0f584257c0315a334365e5715727bcc767a31c2daa5e
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -2,10 +2,10 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- ruby-2.0.0-p648
|
4
4
|
- ruby-2.1.10
|
5
|
-
- ruby-2.2.
|
6
|
-
- ruby-2.3.
|
5
|
+
- ruby-2.2.7
|
6
|
+
- ruby-2.3.4
|
7
7
|
- ruby-2.4.1
|
8
|
-
- jruby-9.1.
|
8
|
+
- jruby-9.1.9.0
|
9
9
|
gemfile:
|
10
10
|
- gemfiles/rails_4_0.gemfile
|
11
11
|
- gemfiles/rails_4_1.gemfile
|
@@ -21,7 +21,7 @@ matrix:
|
|
21
21
|
gemfile: gemfiles/rails_4_0.gemfile
|
22
22
|
- rvm: ruby-2.4.1
|
23
23
|
gemfile: gemfiles/rails_4_1.gemfile
|
24
|
-
before_install: gem install bundler -v 1.
|
24
|
+
before_install: gem install bundler -v 1.15.4
|
25
25
|
install:
|
26
26
|
- wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.zip --quiet -O spec/dynamodb_temp.zip
|
27
27
|
- unzip -qq spec/dynamodb_temp.zip -d spec/DynamoDBLocal-latest
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
# HEAD
|
2
2
|
|
3
|
+
# 1.3.4
|
4
|
+
|
5
|
+
Improving
|
6
|
+
|
7
|
+
* Added `Chain#last` method (@andrykonchin)
|
8
|
+
* Added `date` field type (@andrykonchin)
|
9
|
+
* Added `application_timezone` config option (@andrykonchin)
|
10
|
+
* Allow consistent reading for Scan request (@andrykonchin)
|
11
|
+
* Use Query instead of Scan if there are no conditions for sort (range) key in where clause (@andrykonchin)
|
12
|
+
* Support condition operators for non-key fields for Query request (@andrykonchin)
|
13
|
+
* Support condition operators for Scan request (@andrykonchin)
|
14
|
+
* Support additional operators `in`, `contains`, `not_contains` (@andrykonchin)
|
15
|
+
* Support type casting in `where` clause (@andrykonchin)
|
16
|
+
* Rename `Chain#eval_limit` to `#record_limit` (@richardhsu)
|
17
|
+
* Add `Chain#scan_limit` (@richardhsu)
|
18
|
+
* Support batch loading for Query requests (@richardhsu)
|
19
|
+
* Support querying Global/Local Secondary Indices in `where` clause (@richardhsu)
|
20
|
+
* Only query on GSI if projects all attributes in `where` clause (@richardhsu)
|
21
|
+
|
22
|
+
Fixes
|
23
|
+
|
24
|
+
* Fix incorrect applying of default field value (#36 and #117, @andrykonchin)
|
25
|
+
* Fix sync table creation/deletion (#160, @mirokuxy)
|
26
|
+
* Allow to override document timestamps (@andrykonchin)
|
27
|
+
* Fix storing empty array as nil (#8, @andrykonchin)
|
28
|
+
* Fix `limit` handling for Query requests (#85, @richardhsu)
|
29
|
+
* Fix `limit` handling for Scan requests (#85, @richardhsu)
|
30
|
+
* Fix paginating for Query requests (@richardhsu)
|
31
|
+
* Fix paginating for Scan requests (@richardhsu)
|
32
|
+
* Fix `batch_get_item` method call for integer partition key (@mudasirraza)
|
33
|
+
|
3
34
|
# 1.3.3
|
4
35
|
|
5
36
|
* Allow configuration of the Dynamoid models directory, as not everyone keeps non AR models in app/models
|
data/README.md
CHANGED
@@ -70,11 +70,7 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
|
|
70
70
|
|
71
71
|
```ruby
|
72
72
|
Dynamoid.configure do |config|
|
73
|
-
config.adapter = 'aws_sdk_v2' # This adapter establishes a connection to the DynamoDB servers using Amazon's own AWS gem.
|
74
73
|
config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. Set to nil to avoid namespacing.
|
75
|
-
config.warn_on_scan = true # Output a warning to the logger when you perform a scan rather than a query on a table.
|
76
|
-
config.read_capacity = 5 # Read capacity for your tables
|
77
|
-
config.write_capacity = 5 # Write capacity for your tables
|
78
74
|
config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html).
|
79
75
|
end
|
80
76
|
|
@@ -123,7 +119,8 @@ These fields will not change an existing table: so specifying a new read_capacit
|
|
123
119
|
You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects.
|
124
120
|
|
125
121
|
By default, fields are assumed to be of type ```:string```. Other built-in types are
|
126
|
-
```:integer```, ```:number```, ```:set```, ```:array```, ```:datetime```, ```:boolean```, and ```:serialized```.
|
122
|
+
```:integer```, ```:number```, ```:set```, ```:array```, ```:datetime```, ```date```, ```:boolean```, ```:raw``` and ```:serialized```.
|
123
|
+
```raw``` type means you can store Ruby Array, Hash, String and numbers.
|
127
124
|
If built-in types do not suit you, you can use a custom field type represented by an arbitrary class, provided that the class supports a compatible serialization interface.
|
128
125
|
The primary use case for using a custom field type is to represent your business logic with high-level types, while ensuring portability or backward-compatibility of the serialized representation.
|
129
126
|
|
@@ -257,6 +254,12 @@ end
|
|
257
254
|
|
258
255
|
To see more usage and examples of ActiveModel validations, check out the [ActiveModel validation documentation](http://api.rubyonrails.org/classes/ActiveModel/Validations.html).
|
259
256
|
|
257
|
+
If you want to bypass model validation, pass `validate: false` to `save` call:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
model.save(validate: false)
|
261
|
+
```
|
262
|
+
|
260
263
|
### Callbacks
|
261
264
|
|
262
265
|
Dynamoid also employs ActiveModel callbacks. Right now, callbacks are defined on ```save```, ```update```, ```destroy```, which allows you to do ```before_``` or ```after_``` any of those.
|
@@ -273,6 +276,29 @@ class User
|
|
273
276
|
end
|
274
277
|
```
|
275
278
|
|
279
|
+
### STI
|
280
|
+
|
281
|
+
Dynamoid supports STI (Single Table Inheritance) like Active Record does. You need just specify `type` field in a base class. Example:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
class Animal
|
285
|
+
include Dynamoid::Document
|
286
|
+
|
287
|
+
field :name
|
288
|
+
field :type
|
289
|
+
end
|
290
|
+
|
291
|
+
class Cat < Animal
|
292
|
+
field :lives, :integer
|
293
|
+
end
|
294
|
+
|
295
|
+
cat = Cat.create(name: 'Morgan')
|
296
|
+
animal = Animal.find(cat.id)
|
297
|
+
animal.class
|
298
|
+
#=> Cat
|
299
|
+
|
300
|
+
```
|
301
|
+
|
276
302
|
## Usage
|
277
303
|
|
278
304
|
### Object Creation
|
@@ -317,18 +343,64 @@ u.addresses.where(:city => 'Chicago').all
|
|
317
343
|
|
318
344
|
But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines.
|
319
345
|
|
320
|
-
|
346
|
+
#### Limits
|
347
|
+
|
348
|
+
There are three types of limits that you can query with:
|
349
|
+
|
350
|
+
1. `record_limit` - The number of evaluated records that are returned by the query.
|
351
|
+
2. `scan_limit` - The number of scanned records that DynamoDB will look at before returning.
|
352
|
+
3. `batch_size` - The number of records requested to DynamoDB per underlying request, good for large queries!
|
353
|
+
|
354
|
+
Using these in various combinations results in the underlying requests to be made in the smallest size possible and
|
355
|
+
the query returns once `record_limit` or `scan_limit` is satisfied. It will attempt to batch whenever possible.
|
356
|
+
|
357
|
+
You can thus limit the number of evaluated records, or select a record from which to start, to support pagination:
|
321
358
|
|
322
359
|
```ruby
|
323
|
-
Address.
|
360
|
+
Address.record_limit(5).start(address) # Only 5 addresses starting at `address`
|
361
|
+
```
|
362
|
+
|
363
|
+
If you are potentially running over a large data set and this is especially true when using certain filters, you may
|
364
|
+
want to consider limiting the number of scanned records (the number of records DynamoDB infrastructure looks through
|
365
|
+
when evaluating data to return):
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
Address.scan_limit(5).start(address) # Only scan at most 5 records and return what's found starting from `address`
|
324
369
|
```
|
325
370
|
|
326
371
|
For large queries that return many rows, Dynamoid can use AWS' support for requesting documents in batches:
|
327
372
|
|
328
373
|
```ruby
|
329
|
-
#Do some maintenance on the entire table without flooding DynamoDB
|
374
|
+
# Do some maintenance on the entire table without flooding DynamoDB
|
330
375
|
Address.all(batch_size: 100).each { |address| address.do_some_work; sleep(0.01) }
|
331
|
-
Address.
|
376
|
+
Address.record_limit(10_000).batch(100). each { … } # Batch specified as part of a chain
|
377
|
+
```
|
378
|
+
|
379
|
+
The implication of batches is that the underlying requests are done in the batch sizes to make the request and responses
|
380
|
+
more manageable. Note that this batching is for `Query` and `Scans` and not `BatchGetItem` commands.
|
381
|
+
|
382
|
+
#### Sort Conditions and Filters
|
383
|
+
|
384
|
+
You are able to optimize query with condition for sort key. Following operators are available: `gt`, `lt`, `gte`, `lte`,
|
385
|
+
`begins_with`, `between` as well as equality:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
Address.where(latitude: 10212)
|
389
|
+
Address.where('latitude.gt': 10212)
|
390
|
+
Address.where('latitude.lt': 10212)
|
391
|
+
Address.where('latitude.gte': 10212)
|
392
|
+
Address.where('latitude.lte': 10212)
|
393
|
+
Address.where('city.begins_with': 'Lon')
|
394
|
+
Address.where('latitude.between': [10212, 20000])
|
395
|
+
```
|
396
|
+
|
397
|
+
You are able to filter results on the DynamoDB side and specify conditions for non-key fields.
|
398
|
+
Following operators are available: `in`, `contains`, `not_contains`:
|
399
|
+
|
400
|
+
```ruby
|
401
|
+
Address.where('city.in': ['London', 'Edenburg', 'Birmingham'])
|
402
|
+
Address.where('city.contains': [on])
|
403
|
+
Address.where('city.not_contains': [ing])
|
332
404
|
```
|
333
405
|
|
334
406
|
### Consistent Reads
|
@@ -353,7 +425,12 @@ It also supports .gte and .lte. Turning those into symbols and allowing a Rails
|
|
353
425
|
|
354
426
|
### Global Secondary Indexes
|
355
427
|
|
356
|
-
|
428
|
+
There are two ways to query Global Secondary Indexes (GSI).
|
429
|
+
|
430
|
+
#### Explicit
|
431
|
+
|
432
|
+
The first way explicitly uses your GSI and utilizes the `find_all_by_secondary_index` method which will lookup a valid
|
433
|
+
GSI to use based on the inputs, you MUST provide the correct keys to match the GSI you want:
|
357
434
|
|
358
435
|
```ruby
|
359
436
|
find_all_by_secondary_index(
|
@@ -368,7 +445,8 @@ find_all_by_secondary_index(
|
|
368
445
|
:scan_index_forward => false # or true
|
369
446
|
)
|
370
447
|
```
|
371
|
-
|
448
|
+
|
449
|
+
Where the range modifier is one of `Dynamoid::Finders::RANGE_MAP.keys`, where the `RANGE_MAP` is:
|
372
450
|
|
373
451
|
```ruby
|
374
452
|
RANGE_MAP = {
|
@@ -384,6 +462,53 @@ RANGE_MAP = {
|
|
384
462
|
|
385
463
|
Most range searches, like `eq`, need a single value, and searches like `between`, need an array with two values.
|
386
464
|
|
465
|
+
#### Implicit
|
466
|
+
|
467
|
+
The second way implicitly uses your GSI through the `where` clauses and deduces the index based on the query fields
|
468
|
+
provided. Another added benefit is that it is built into query chaining so you can use all the methods used in normal
|
469
|
+
querying. The explicit way from above would be rewritten as follows:
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
where(dynamo_primary_key_column_name => dynamo_primary_key_value,
|
473
|
+
"#{range_column}.#{range_modifier}" => range_value)
|
474
|
+
.scan_index_forward(false)
|
475
|
+
```
|
476
|
+
|
477
|
+
The only caveat with this method is that because it is also used for general querying, it WILL NOT use a GSI unless it
|
478
|
+
explicitly has defined `projected_attributes: :all` on the GSI in your model. This is because GSIs that do not have all
|
479
|
+
attributes projected will only contain the index keys and therefore will not return objects with fully resolved field
|
480
|
+
values. It currently opts to provide the complete results rather than partial results unless you've explicitly looked up
|
481
|
+
the data.
|
482
|
+
|
483
|
+
*Future TODO could involve implementing `select` in chaining as well as resolving the fields with a second query against
|
484
|
+
the table since a query against GSI then a query on base table is still likely faster than scan on the base table*
|
485
|
+
|
486
|
+
## Configuration
|
487
|
+
|
488
|
+
There are listed all the configuration options:
|
489
|
+
|
490
|
+
* `adapter` - usefull only for the gem developers to switch to a new adapter. Default and the only available value is `aws_sdk_v2`
|
491
|
+
* `namespace` - prefix for table names, default is `dynamoid_#{application_name}_#{environment}` for Rails application and `dynamoid` otherwise
|
492
|
+
* `logger` - by default it's a `Rails.logger` in Rails application and `stdout` otherwise. You can disable logging by setting `nil` or `false` values. Set `true` value to use defaults
|
493
|
+
* `access_key` - DynamoDb custom credentials for AWS, override global AWS credentials if they present
|
494
|
+
* `secret_key` - DynamoDb custom credentials for AWS, override global AWS credentials if they present
|
495
|
+
* `region` - DynamoDb custom credentials for AWS, override global AWS credentials if they present
|
496
|
+
* `batch_size` - when you try to load multiple items at once with `batch_get_item` call Dynamoid loads them not with one api call but piece by piece. Default is 100 items
|
497
|
+
* `read_capacity` - is used at table or indices creation. Default is 100 (units)
|
498
|
+
* `write_capacity` - is used at table or indices creation. Default is 20 (units)
|
499
|
+
* `warn_on_scan` - log warnings when scan table. Default is `true`
|
500
|
+
* `endpoint` - if provided, it communicates with the DynamoDB listening at the endpoint. This is useful for testing with [Amazon Local DB]
|
501
|
+
* `identity_map` - ensures that each object gets loaded only once by keeping every loaded object in a map. Looks up objects using the map when referring to them. Isn't thread safe. Default is `false`.
|
502
|
+
`Use Dynamoid::Middleware::IdentityMap` to clear identity map for each HTTP request
|
503
|
+
* `timestamps` - by default Dynamoid sets `created_at` and `updated_at` fields for model at creation and updating. You can disable this behavior by setting `false` value
|
504
|
+
* `sync_retry_max_times` - when Dynamoid creates or deletes table synchronously it checks for completion specified times. Default is 60 (times). It's a bit over 2 minutes by default
|
505
|
+
* `sync_retry_wait_seconds` - time to wait between retries. Default is 2 (seconds)
|
506
|
+
* `convert_big_decimal` - if `true` then Dynamoid converts numbers stored in `Hash` in `raw` field to float. Default is `false`
|
507
|
+
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `app/models`. In Rails application you should set `./app/models` value
|
508
|
+
* `application_timezone` - Dynamoid converts all `datetime` fields to specified time zone when loads data from the storage.
|
509
|
+
Acceptable values - `utc`, `local` (to use system time zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is `local`
|
510
|
+
|
511
|
+
|
387
512
|
## Concurrency
|
388
513
|
|
389
514
|
Dynamoid supports basic, ActiveRecord-like optimistic locking on save operations. Simply add a `lock_version` column to your table like so:
|
data/Vagrantfile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Vagrant.configure('2') do |config|
|
2
|
+
# Choose base box
|
3
|
+
config.vm.box = 'bento/ubuntu-16.04'
|
4
|
+
|
5
|
+
config.vm.provider 'virtualbox' do |vb|
|
6
|
+
# Prevent clock skew when host goes to sleep while VM is running
|
7
|
+
vb.customize ['guestproperty', 'set', :id, '/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold', 10_000]
|
8
|
+
|
9
|
+
vb.cpus = 2
|
10
|
+
vb.memory = 2048
|
11
|
+
end
|
12
|
+
|
13
|
+
# Defaults
|
14
|
+
config.vm.provision :salt do |salt|
|
15
|
+
salt.masterless = true
|
16
|
+
salt.minion_config = '.dev/vagrant/minion'
|
17
|
+
|
18
|
+
# Pillars
|
19
|
+
salt.pillar({
|
20
|
+
'ruby' => {
|
21
|
+
'version' => '2.3.3',
|
22
|
+
}
|
23
|
+
})
|
24
|
+
|
25
|
+
salt.run_highstate = true
|
26
|
+
end
|
27
|
+
end
|
data/dynamoid.gemspec
CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
"LICENSE.txt",
|
32
32
|
"README.md"
|
33
33
|
]
|
34
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features)/}) }
|
34
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features|.dev|Vagrantfile)/}) }
|
35
35
|
spec.homepage = "http://github.com/Dynamoid/Dynamoid"
|
36
36
|
spec.licenses = ["MIT"]
|
37
37
|
spec.bindir = "exe"
|
data/lib/dynamoid.rb
CHANGED
data/lib/dynamoid/adapter.rb
CHANGED
@@ -13,6 +13,21 @@ module Dynamoid
|
|
13
13
|
range_between: 'BETWEEN',
|
14
14
|
range_eq: 'EQ'
|
15
15
|
}
|
16
|
+
|
17
|
+
# Don't implement NULL and NOT_NULL because it doesn't make seanse -
|
18
|
+
# we declare schema in models
|
19
|
+
FIELD_MAP = {
|
20
|
+
eq: 'EQ',
|
21
|
+
gt: 'GT',
|
22
|
+
lt: 'LT',
|
23
|
+
gte: 'GE',
|
24
|
+
lte: 'LE',
|
25
|
+
begins_with: 'BEGINS_WITH',
|
26
|
+
between: 'BETWEEN',
|
27
|
+
in: 'IN',
|
28
|
+
contains: 'CONTAINS',
|
29
|
+
not_contains: 'NOT_CONTAINS'
|
30
|
+
}
|
16
31
|
HASH_KEY = "HASH".freeze
|
17
32
|
RANGE_KEY = "RANGE".freeze
|
18
33
|
STRING_TYPE = "S".freeze
|
@@ -110,12 +125,12 @@ module Dynamoid
|
|
110
125
|
# @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
111
126
|
def batch_get_item(table_ids, options = {})
|
112
127
|
request_items = Hash.new{|h, k| h[k] = []}
|
113
|
-
return request_items if table_ids.all?{|k, v| v.
|
128
|
+
return request_items if table_ids.all?{|k, v| v.blank?}
|
114
129
|
|
115
130
|
ret = Hash.new([].freeze) # Default for tables where no rows are returned
|
116
131
|
|
117
132
|
table_ids.each do |t, ids|
|
118
|
-
next if ids.
|
133
|
+
next if ids.blank?
|
119
134
|
tbl = describe_table(t)
|
120
135
|
hk = tbl.hash_key.to_s
|
121
136
|
rng = tbl.range_key.to_s
|
@@ -230,9 +245,9 @@ module Dynamoid
|
|
230
245
|
end
|
231
246
|
resp = client.create_table(client_opts)
|
232
247
|
options[:sync] = true if !options.has_key?(:sync) && ls_indexes.present? || gs_indexes.present?
|
233
|
-
until_past_table_status(table_name) if options[:sync] &&
|
248
|
+
until_past_table_status(table_name, :creating) if options[:sync] &&
|
234
249
|
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
235
|
-
status
|
250
|
+
status == TABLE_STATUSES[:creating]
|
236
251
|
# Response to original create_table, which, if options[:sync]
|
237
252
|
# may have an outdated table_description.table_status of "CREATING"
|
238
253
|
resp
|
@@ -295,7 +310,7 @@ module Dynamoid
|
|
295
310
|
resp = client.delete_table(table_name: table_name)
|
296
311
|
until_past_table_status(table_name, :deleting) if options[:sync] &&
|
297
312
|
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
298
|
-
status
|
313
|
+
status == TABLE_STATUSES[:deleting]
|
299
314
|
table_cache.delete(table_name)
|
300
315
|
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
301
316
|
Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
|
@@ -382,7 +397,7 @@ module Dynamoid
|
|
382
397
|
options ||= {}
|
383
398
|
|
384
399
|
object.each do |k, v|
|
385
|
-
next if v.nil? || (v.
|
400
|
+
next if v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
386
401
|
item[k.to_s] = v
|
387
402
|
end
|
388
403
|
|
@@ -419,28 +434,37 @@ module Dynamoid
|
|
419
434
|
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
420
435
|
def query(table_name, opts = {})
|
421
436
|
table = describe_table(table_name)
|
422
|
-
hk = (opts[:hash_key].present? ? opts
|
423
|
-
rng = (opts[:range_key].present? ? opts
|
437
|
+
hk = (opts[:hash_key].present? ? opts.delete(:hash_key) : table.hash_key).to_s
|
438
|
+
rng = (opts[:range_key].present? ? opts.delete(:range_key) : table.range_key).to_s
|
424
439
|
q = opts.slice(
|
425
440
|
:consistent_read,
|
426
441
|
:scan_index_forward,
|
427
|
-
:limit,
|
428
442
|
:select,
|
429
443
|
:index_name
|
430
444
|
)
|
431
445
|
|
432
446
|
opts.delete(:consistent_read)
|
433
447
|
opts.delete(:scan_index_forward)
|
434
|
-
opts.delete(:limit)
|
435
448
|
opts.delete(:select)
|
436
449
|
opts.delete(:index_name)
|
437
450
|
|
451
|
+
# Deal with various limits and batching
|
452
|
+
record_limit = opts.delete(:record_limit)
|
453
|
+
scan_limit = opts.delete(:scan_limit)
|
454
|
+
batch_size = opts.delete(:batch_size)
|
455
|
+
limit = [record_limit, scan_limit, batch_size].compact.min
|
456
|
+
q[:limit] = limit if limit
|
457
|
+
|
438
458
|
opts.delete(:next_token).tap do |token|
|
439
459
|
break unless token
|
440
460
|
q[:exclusive_start_key] = {
|
441
461
|
hk => token[:hash_key_element],
|
442
462
|
rng => token[:range_key_element]
|
443
463
|
}
|
464
|
+
# For secondary indices the start key must contain the indices composite key
|
465
|
+
# but also the table's composite keys
|
466
|
+
q[:exclusive_start_key][table.hash_key] = token[:table_hash_key_element] if token[:table_hash_key_element]
|
467
|
+
q[:exclusive_start_key][table.range_key] = token[:table_range_key_element] if token[:table_range_key_element]
|
444
468
|
end
|
445
469
|
|
446
470
|
key_conditions = {
|
@@ -464,14 +488,55 @@ module Dynamoid
|
|
464
488
|
}
|
465
489
|
end
|
466
490
|
|
491
|
+
query_filter = {}
|
492
|
+
opts.reject {|k,_| k.in? RANGE_MAP.keys}.each do |attr, hash|
|
493
|
+
query_filter[attr] = {
|
494
|
+
comparison_operator: FIELD_MAP[hash.keys[0]],
|
495
|
+
attribute_value_list: [
|
496
|
+
hash.values[0].freeze
|
497
|
+
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
498
|
+
}
|
499
|
+
end
|
500
|
+
|
467
501
|
q[:table_name] = table_name
|
468
502
|
q[:key_conditions] = key_conditions
|
503
|
+
q[:query_filter] = query_filter
|
469
504
|
|
470
505
|
Enumerator.new { |y|
|
506
|
+
record_count = 0
|
507
|
+
scan_count = 0
|
471
508
|
loop do
|
509
|
+
# Adjust the limit down if the remaining record and/or scan limit are
|
510
|
+
# lower to obey limits. We can assume the difference won't be
|
511
|
+
# negative due to break statements below but choose smaller limit
|
512
|
+
# which is why we have 2 separate if statements.
|
513
|
+
# NOTE: Adjusting based on record_limit can cause many HTTP requests
|
514
|
+
# being made. We may want to change this behavior, but it affects
|
515
|
+
# filtering on data with potentially large gaps.
|
516
|
+
# Example:
|
517
|
+
# User.where('created_at.gte' => 1.day.ago).record_limit(1000)
|
518
|
+
# Records 1-999 User's that fit criteria
|
519
|
+
# Records 1000-2000 Users's that do not fit criteria
|
520
|
+
# Record 2001 fits criteria
|
521
|
+
# The underlying implementation will have 1 page for records 1-999
|
522
|
+
# then will request with limit 1 for records 1000-2000 (making 1000
|
523
|
+
# requests of limit 1) until hit record 2001.
|
524
|
+
if q[:limit] && record_limit && record_limit - record_count < q[:limit]
|
525
|
+
q[:limit] = record_limit - record_count
|
526
|
+
end
|
527
|
+
if q[:limit] && scan_limit && scan_limit - scan_count < q[:limit]
|
528
|
+
q[:limit] = scan_limit - scan_count
|
529
|
+
end
|
530
|
+
|
472
531
|
results = client.query(q)
|
473
532
|
results.items.each { |row| y << result_item_to_hash(row) }
|
474
533
|
|
534
|
+
record_count += results.items.size
|
535
|
+
break if record_limit && record_count >= record_limit
|
536
|
+
|
537
|
+
scan_count += results.scanned_count
|
538
|
+
break if scan_limit && scan_count >= scan_limit
|
539
|
+
|
475
540
|
if(lk = results.last_evaluated_key)
|
476
541
|
q[:exclusive_start_key] = lk
|
477
542
|
else
|
@@ -493,28 +558,63 @@ module Dynamoid
|
|
493
558
|
#
|
494
559
|
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
495
560
|
def scan(table_name, scan_hash, select_opts = {})
|
496
|
-
limit = select_opts.delete(:limit)
|
497
|
-
batch = select_opts.delete(:batch_size)
|
498
|
-
|
499
561
|
request = { table_name: table_name }
|
500
|
-
request[:
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
562
|
+
request[:consistent_read] = true if select_opts.delete(:consistent_read)
|
563
|
+
|
564
|
+
# Deal with various limits and batching
|
565
|
+
record_limit = select_opts.delete(:record_limit)
|
566
|
+
scan_limit = select_opts.delete(:scan_limit)
|
567
|
+
batch_size = select_opts.delete(:batch_size)
|
568
|
+
request_limit = [record_limit, scan_limit, batch_size].compact.min
|
569
|
+
request[:limit] = request_limit if request_limit
|
570
|
+
|
571
|
+
if scan_hash.present?
|
572
|
+
request[:scan_filter] = scan_hash.reduce({}) do |memo, (attr, cond)|
|
573
|
+
# Flatten as BETWEEN operator specifies array of two elements
|
574
|
+
memo.merge(attr.to_s => {
|
575
|
+
comparison_operator: FIELD_MAP[cond.keys[0]],
|
576
|
+
attribute_value_list: [cond.values[0].freeze].flatten
|
577
|
+
})
|
578
|
+
end
|
579
|
+
end
|
509
580
|
|
510
581
|
Enumerator.new do |y|
|
511
|
-
|
582
|
+
record_count = 0
|
583
|
+
scan_count = 0
|
512
584
|
loop do
|
585
|
+
# Adjust the limit down if the remaining record and/or scan limit are
|
586
|
+
# lower to obey limits. We can assume the difference won't be
|
587
|
+
# negative due to break statements below but choose smaller limit
|
588
|
+
# which is why we have 2 separate if statements.
|
589
|
+
# NOTE: Adjusting based on record_limit can cause many HTTP requests
|
590
|
+
# being made. We may want to change this behavior, but it affects
|
591
|
+
# filtering on data with potentially large gaps.
|
592
|
+
# Example:
|
593
|
+
# User.where('created_at.gte' => 1.day.ago).record_limit(1000)
|
594
|
+
# Records 1-999 User's that fit criteria
|
595
|
+
# Records 1000-2000 Users's that do not fit criteria
|
596
|
+
# Record 2001 fits criteria
|
597
|
+
# The underlying implementation will have 1 page for records 1-999
|
598
|
+
# then will request with limit 1 for records 1000-2000 (making 1000
|
599
|
+
# requests of limit 1) until hit record 2001.
|
600
|
+
if request[:limit] && record_limit && record_limit - record_count < request[:limit]
|
601
|
+
request[:limit] = record_limit - record_count
|
602
|
+
end
|
603
|
+
if request[:limit] && scan_limit && scan_limit - scan_count < request[:limit]
|
604
|
+
request[:limit] = scan_limit - scan_count
|
605
|
+
end
|
606
|
+
|
513
607
|
results = client.scan(request)
|
608
|
+
results.items.each { |row| y << result_item_to_hash(row) }
|
609
|
+
|
610
|
+
record_count += results.items.size
|
611
|
+
break if record_limit && record_count >= record_limit
|
514
612
|
|
515
|
-
results.
|
613
|
+
scan_count += results.scanned_count
|
614
|
+
break if scan_limit && scan_count >= scan_limit
|
516
615
|
|
517
|
-
if
|
616
|
+
# Keep pulling if we haven't finished paging in all data
|
617
|
+
if(lk = results[:last_evaluated_key])
|
518
618
|
request[:exclusive_start_key] = lk
|
519
619
|
else
|
520
620
|
break
|
data/lib/dynamoid/config.rb
CHANGED
@@ -13,7 +13,6 @@ module Dynamoid
|
|
13
13
|
# All the default options.
|
14
14
|
option :adapter, :default => 'aws_sdk_v2'
|
15
15
|
option :namespace, :default => defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : "dynamoid"
|
16
|
-
option :logger, :default => defined?(Rails)
|
17
16
|
option :access_key, :default => nil
|
18
17
|
option :secret_key, :default => nil
|
19
18
|
option :region, :default => nil
|
@@ -22,14 +21,13 @@ module Dynamoid
|
|
22
21
|
option :write_capacity, :default => 20
|
23
22
|
option :warn_on_scan, :default => true
|
24
23
|
option :endpoint, :default => nil
|
25
|
-
option :use_ssl, :default => true
|
26
|
-
option :port, :default => '443'
|
27
24
|
option :identity_map, :default => false
|
28
25
|
option :timestamps, :default => true
|
29
26
|
option :sync_retry_max_times, :default => 60 # a bit over 2 minutes
|
30
27
|
option :sync_retry_wait_seconds, :default => 2
|
31
28
|
option :convert_big_decimal, :default => false
|
32
29
|
option :models_dir, :default => "app/models" # perhaps you keep your dynamoid models in a different directory?
|
30
|
+
option :application_timezone, default: :local # available values - :utc, :local, time zone names
|
33
31
|
|
34
32
|
# The default logger for Dynamoid: either the Rails logger or just stdout.
|
35
33
|
#
|
data/lib/dynamoid/criteria.rb
CHANGED
@@ -6,11 +6,11 @@ module Dynamoid
|
|
6
6
|
# Allows classes to be queried by where, all, first, and each and return criteria chains.
|
7
7
|
module Criteria
|
8
8
|
extend ActiveSupport::Concern
|
9
|
-
|
9
|
+
|
10
10
|
module ClassMethods
|
11
|
-
|
12
|
-
[:where, :all, :first, :each, :
|
13
|
-
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
11
|
+
|
12
|
+
[:where, :all, :first, :last, :each, :record_limit, :scan_limit, :batch, :start, :scan_index_forward].each do |meth|
|
13
|
+
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
14
14
|
# see Dynamoid::Criteria::Chain.
|
15
15
|
#
|
16
16
|
# @since 0.2.0
|
@@ -25,5 +25,5 @@ module Dynamoid
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
end
|
@@ -6,8 +6,9 @@ module Dynamoid #:nodoc:
|
|
6
6
|
# chain to relation). It is a chainable object that builds up a query and eventually executes it by a Query or Scan.
|
7
7
|
class Chain
|
8
8
|
# TODO: Should we transform any other types of query values?
|
9
|
-
TYPES_TO_DUMP_FOR_QUERY = [:string, :integer, :boolean]
|
9
|
+
TYPES_TO_DUMP_FOR_QUERY = [:string, :integer, :boolean, :serialized]
|
10
10
|
attr_accessor :query, :source, :values, :consistent_read
|
11
|
+
attr_reader :hash_key, :range_key, :index_name
|
11
12
|
include Enumerable
|
12
13
|
# Create a new criteria chain.
|
13
14
|
#
|
@@ -54,6 +55,12 @@ module Dynamoid #:nodoc:
|
|
54
55
|
records
|
55
56
|
end
|
56
57
|
|
58
|
+
# Returns the last fetched record matched the criteria
|
59
|
+
#
|
60
|
+
def last
|
61
|
+
all.last
|
62
|
+
end
|
63
|
+
|
57
64
|
# Destroys all the records matching the criteria.
|
58
65
|
#
|
59
66
|
def destroy_all
|
@@ -76,8 +83,19 @@ module Dynamoid #:nodoc:
|
|
76
83
|
end
|
77
84
|
end
|
78
85
|
|
79
|
-
|
80
|
-
|
86
|
+
# The record limit is the limit of evaluated records returned by the
|
87
|
+
# query or scan.
|
88
|
+
def record_limit(limit)
|
89
|
+
@record_limit = limit
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
# The scan limit which is the limit of records that DynamoDB will
|
94
|
+
# internally query or scan. This is different from the record limit
|
95
|
+
# as with filtering DynamoDB may look at N scanned records but return 0
|
96
|
+
# records if none pass the filter.
|
97
|
+
def scan_limit(limit)
|
98
|
+
@scan_limit = limit
|
81
99
|
self
|
82
100
|
end
|
83
101
|
|
@@ -139,26 +157,21 @@ module Dynamoid #:nodoc:
|
|
139
157
|
def records_via_scan
|
140
158
|
if Dynamoid::Config.warn_on_scan
|
141
159
|
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
142
|
-
Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{
|
143
|
-
end
|
144
|
-
|
145
|
-
if @consistent_read
|
146
|
-
raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
|
160
|
+
Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}]"
|
147
161
|
end
|
148
162
|
|
149
163
|
Enumerator.new do |yielder|
|
150
|
-
Dynamoid.adapter.scan(source.table_name,
|
164
|
+
Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |hash|
|
151
165
|
yielder.yield source.from_database(hash)
|
152
166
|
end
|
153
167
|
end
|
154
168
|
end
|
155
169
|
|
156
170
|
def range_hash(key)
|
157
|
-
|
158
|
-
|
159
|
-
return { :range_value => query[key] } if query[key].is_a?(Range)
|
171
|
+
name, operation = key.to_s.split('.')
|
172
|
+
val = type_cast_condition_parameter(name, query[key])
|
160
173
|
|
161
|
-
case
|
174
|
+
case operation
|
162
175
|
when 'gt'
|
163
176
|
{ :range_greater_than => val }
|
164
177
|
when 'lt'
|
@@ -174,45 +187,170 @@ module Dynamoid #:nodoc:
|
|
174
187
|
end
|
175
188
|
end
|
176
189
|
|
190
|
+
def field_hash(key)
|
191
|
+
name, operation = key.to_s.split('.')
|
192
|
+
val = type_cast_condition_parameter(name, query[key])
|
193
|
+
|
194
|
+
hash = case operation
|
195
|
+
when 'gt'
|
196
|
+
{ gt: val }
|
197
|
+
when 'lt'
|
198
|
+
{ lt: val }
|
199
|
+
when 'gte'
|
200
|
+
{ gte: val }
|
201
|
+
when 'lte'
|
202
|
+
{ lte: val }
|
203
|
+
when 'between'
|
204
|
+
{ between: val }
|
205
|
+
when 'begins_with'
|
206
|
+
{ begins_with: val }
|
207
|
+
when 'in'
|
208
|
+
{ in: val }
|
209
|
+
when 'contains'
|
210
|
+
{ contains: val }
|
211
|
+
when 'not_contains'
|
212
|
+
{ not_contains: val }
|
213
|
+
end
|
214
|
+
|
215
|
+
return { name.to_sym => hash }
|
216
|
+
end
|
217
|
+
|
177
218
|
def range_query
|
178
|
-
opts = {
|
179
|
-
|
180
|
-
|
219
|
+
opts = {}
|
220
|
+
|
221
|
+
# Add hash key
|
222
|
+
opts[:hash_key] = @hash_key
|
223
|
+
opts[:hash_value] = type_cast_condition_parameter(@hash_key, query[@hash_key])
|
224
|
+
|
225
|
+
# Add range key
|
226
|
+
if @range_key
|
227
|
+
opts[:range_key] = @range_key
|
228
|
+
if query[@range_key].present?
|
229
|
+
value = type_cast_condition_parameter(@range_key, query[@range_key])
|
230
|
+
opts.update(:range_eq => value)
|
231
|
+
end
|
232
|
+
|
233
|
+
query.keys.select { |k| k.to_s =~ /^#{@range_key}\./ }.each do |key|
|
234
|
+
opts.merge!(range_hash(key))
|
235
|
+
end
|
181
236
|
end
|
237
|
+
|
238
|
+
(query.keys.map(&:to_sym) - [@hash_key.to_sym, @range_key.try(:to_sym)])
|
239
|
+
.reject { |k, _| k.to_s =~ /^#{@range_key}\./ }
|
240
|
+
.each do |key|
|
241
|
+
if key.to_s.include?('.')
|
242
|
+
opts.update(field_hash(key))
|
243
|
+
else
|
244
|
+
value = type_cast_condition_parameter(key, query[key])
|
245
|
+
opts[key] = {eq: value}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
182
249
|
opts.merge(query_opts).merge(consistent_opts)
|
183
250
|
end
|
184
251
|
|
185
|
-
def
|
186
|
-
|
252
|
+
def type_cast_condition_parameter(key, value)
|
253
|
+
if !value.respond_to?(:to_ary)
|
254
|
+
source.dump_field(value, source.attributes[key.to_sym])
|
255
|
+
else
|
256
|
+
value.to_ary.map { |el| source.dump_field(el, source.attributes[key.to_sym]) }
|
257
|
+
end
|
187
258
|
end
|
188
259
|
|
189
|
-
# [hash_key] or [hash_key, range_key] is specified in query keys.
|
190
260
|
def key_present?
|
191
|
-
query_keys
|
261
|
+
query_keys = query.keys.collect { |k| k.to_s.split('.').first }
|
262
|
+
|
263
|
+
# See if querying based on table hash key
|
264
|
+
if query_keys.include?(source.hash_key.to_s)
|
265
|
+
@hash_key = source.hash_key
|
266
|
+
|
267
|
+
# Use table's default range key
|
268
|
+
if query_keys.include?(source.range_key.to_s)
|
269
|
+
@range_key = source.range_key
|
270
|
+
return true
|
271
|
+
end
|
272
|
+
|
273
|
+
# See if can use any local secondary index range key
|
274
|
+
# Chooses the first LSI found that can be utilized for the query
|
275
|
+
source.local_secondary_indexes.each do |_, lsi|
|
276
|
+
next unless query_keys.include?(lsi.range_key.to_s)
|
277
|
+
@range_key = lsi.range_key
|
278
|
+
@index_name = lsi.name
|
279
|
+
end
|
280
|
+
|
281
|
+
return true
|
282
|
+
end
|
283
|
+
|
284
|
+
# See if can use any global secondary index
|
285
|
+
# Chooses the first GSI found that can be utilized for the query
|
286
|
+
# But only do so if projects ALL attributes otherwise we won't
|
287
|
+
# get back full data
|
288
|
+
source.global_secondary_indexes.each do |_, gsi|
|
289
|
+
next unless query_keys.include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
|
290
|
+
@hash_key = gsi.hash_key
|
291
|
+
@range_key = gsi.range_key
|
292
|
+
@index_name = gsi.name
|
293
|
+
return true
|
294
|
+
end
|
295
|
+
|
296
|
+
# Could not utilize any indices so we'll have to scan
|
297
|
+
false
|
192
298
|
end
|
193
299
|
|
300
|
+
# Start key needs to be set up based on the index utilized
|
301
|
+
# If using a secondary index then we must include the index's composite key
|
302
|
+
# as well as the tables composite key.
|
194
303
|
def start_key
|
195
|
-
|
196
|
-
|
197
|
-
|
304
|
+
hash_key = @hash_key || source.hash_key
|
305
|
+
range_key = @range_key || source.range_key
|
306
|
+
|
307
|
+
key = {}
|
308
|
+
key[:hash_key_element] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
|
309
|
+
key[:range_key_element] = type_cast_condition_parameter(range_key, @start.send(range_key)) if range_key
|
310
|
+
|
311
|
+
# Add table composite keys if differ from secondary index used composite key
|
312
|
+
if hash_key != source.hash_key
|
313
|
+
key[:table_hash_key_element] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
|
198
314
|
end
|
315
|
+
if source.range_key && range_key != source.range_key
|
316
|
+
key[:table_range_key_element] = type_cast_condition_parameter(source.range_key, @start.range_value)
|
317
|
+
end
|
318
|
+
|
199
319
|
key
|
200
320
|
end
|
201
321
|
|
202
322
|
def query_opts
|
203
323
|
opts = {}
|
324
|
+
opts[:index_name] = @index_name if @index_name
|
204
325
|
opts[:select] = 'ALL_ATTRIBUTES'
|
205
|
-
opts[:
|
326
|
+
opts[:record_limit] = @record_limit if @record_limit
|
327
|
+
opts[:scan_limit] = @scan_limit if @scan_limit
|
328
|
+
opts[:batch_size] = @batch_size if @batch_size
|
206
329
|
opts[:next_token] = start_key if @start
|
207
330
|
opts[:scan_index_forward] = @scan_index_forward
|
208
331
|
opts
|
209
332
|
end
|
210
333
|
|
334
|
+
def scan_query
|
335
|
+
{}.tap do |opts|
|
336
|
+
query.keys.map(&:to_sym).each do |key|
|
337
|
+
if key.to_s.include?('.')
|
338
|
+
opts.update(field_hash(key))
|
339
|
+
else
|
340
|
+
value = type_cast_condition_parameter(key, query[key])
|
341
|
+
opts[key] = {eq: value}
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
211
347
|
def scan_opts
|
212
348
|
opts = {}
|
213
|
-
opts[:
|
214
|
-
opts[:
|
349
|
+
opts[:record_limit] = @record_limit if @record_limit
|
350
|
+
opts[:scan_limit] = @scan_limit if @scan_limit
|
215
351
|
opts[:batch_size] = @batch_size if @batch_size
|
352
|
+
opts[:next_token] = start_key if @start
|
353
|
+
opts[:consistent_read] = true if @consistent_read
|
216
354
|
opts
|
217
355
|
end
|
218
356
|
end
|
data/lib/dynamoid/document.rb
CHANGED
@@ -8,7 +8,7 @@ module Dynamoid #:nodoc:
|
|
8
8
|
include Dynamoid::Components
|
9
9
|
|
10
10
|
included do
|
11
|
-
class_attribute :options, :read_only_attributes, :base_class
|
11
|
+
class_attribute :options, :read_only_attributes, :base_class, instance_accessor: false
|
12
12
|
self.options = {}
|
13
13
|
self.read_only_attributes = []
|
14
14
|
self.base_class = self
|
@@ -120,6 +120,10 @@ module Dynamoid #:nodoc:
|
|
120
120
|
#
|
121
121
|
# @since 0.2.0
|
122
122
|
def initialize(attrs = {})
|
123
|
+
# we need this hack for Rails 4.0 only
|
124
|
+
# because `run_callbacks` calls `attributes` getter while it is still nil
|
125
|
+
@attributes = {}
|
126
|
+
|
123
127
|
run_callbacks :initialize do
|
124
128
|
@new_record = true
|
125
129
|
@attributes ||= {}
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -10,12 +10,13 @@ module Dynamoid #:nodoc:
|
|
10
10
|
:number,
|
11
11
|
:integer,
|
12
12
|
:string,
|
13
|
-
:datetime
|
13
|
+
:datetime,
|
14
|
+
:serialized,
|
14
15
|
]
|
15
16
|
|
16
17
|
# Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
|
17
18
|
included do
|
18
|
-
class_attribute :attributes
|
19
|
+
class_attribute :attributes, instance_accessor: false
|
19
20
|
class_attribute :range_key
|
20
21
|
|
21
22
|
self.attributes = {}
|
@@ -30,7 +31,7 @@ module Dynamoid #:nodoc:
|
|
30
31
|
# Specify a field for a document.
|
31
32
|
#
|
32
33
|
# Its type determines how it is coerced when read in and out of the datastore.
|
33
|
-
# You can specify :integer, :number, :set, :array, :datetime, and :serialized,
|
34
|
+
# You can specify :integer, :number, :set, :array, :datetime, :date and :serialized,
|
34
35
|
# or specify a class that defines a serialization strategy.
|
35
36
|
#
|
36
37
|
# If you specify a class for field type, Dynamoid will serialize using
|
@@ -51,17 +52,19 @@ module Dynamoid #:nodoc:
|
|
51
52
|
end
|
52
53
|
self.attributes = attributes.merge(name => {:type => type}.merge(options))
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
55
|
+
generated_methods.module_eval do
|
56
|
+
define_method(named) { read_attribute(named) }
|
57
|
+
define_method("#{named}?") do
|
58
|
+
value = read_attribute(named)
|
59
|
+
case value
|
60
|
+
when true then true
|
61
|
+
when false, nil then false
|
62
|
+
else
|
63
|
+
!value.nil?
|
64
|
+
end
|
62
65
|
end
|
66
|
+
define_method("#{named}=") {|value| write_attribute(named, value) }
|
63
67
|
end
|
64
|
-
define_method("#{named}=") {|value| write_attribute(named, value) }
|
65
68
|
end
|
66
69
|
|
67
70
|
def range(name, type = :string)
|
@@ -80,9 +83,22 @@ module Dynamoid #:nodoc:
|
|
80
83
|
def remove_field(field)
|
81
84
|
field = field.to_sym
|
82
85
|
attributes.delete(field) or raise "No such field"
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
+
|
87
|
+
generated_methods.module_eval do
|
88
|
+
remove_method field
|
89
|
+
remove_method :"#{field}="
|
90
|
+
remove_method :"#{field}?"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def generated_methods
|
97
|
+
@generated_methods ||= begin
|
98
|
+
Module.new.tap do |mod|
|
99
|
+
include(mod)
|
100
|
+
end
|
101
|
+
end
|
86
102
|
end
|
87
103
|
end
|
88
104
|
|
@@ -97,10 +113,6 @@ module Dynamoid #:nodoc:
|
|
97
113
|
#
|
98
114
|
# @since 0.2.0
|
99
115
|
def write_attribute(name, value)
|
100
|
-
if (size = value.to_s.size) > MAX_ITEM_SIZE
|
101
|
-
Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
|
102
|
-
end
|
103
|
-
|
104
116
|
if association = @associations[name]
|
105
117
|
association.reset
|
106
118
|
end
|
@@ -146,14 +158,16 @@ module Dynamoid #:nodoc:
|
|
146
158
|
#
|
147
159
|
# @since 0.2.0
|
148
160
|
def set_created_at
|
149
|
-
self.created_at
|
161
|
+
self.created_at ||= DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps
|
150
162
|
end
|
151
163
|
|
152
164
|
# Automatically called during the save callback to set the updated_at time.
|
153
165
|
#
|
154
166
|
# @since 0.2.0
|
155
167
|
def set_updated_at
|
156
|
-
|
168
|
+
if Dynamoid::Config.timestamps && !self.updated_at_changed?
|
169
|
+
self.updated_at = DateTime.now.in_time_zone(Time.zone)
|
170
|
+
end
|
157
171
|
end
|
158
172
|
|
159
173
|
def set_type
|
data/lib/dynamoid/indexes.rb
CHANGED
@@ -3,8 +3,8 @@ module Dynamoid
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
class_attribute :local_secondary_indexes
|
7
|
-
class_attribute :global_secondary_indexes
|
6
|
+
class_attribute :local_secondary_indexes, instance_accessor: false
|
7
|
+
class_attribute :global_secondary_indexes, instance_accessor: false
|
8
8
|
self.local_secondary_indexes = {}
|
9
9
|
self.global_secondary_indexes = {}
|
10
10
|
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -13,6 +13,8 @@ module Dynamoid
|
|
13
13
|
attr_accessor :new_record
|
14
14
|
alias :new_record? :new_record
|
15
15
|
|
16
|
+
UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
|
17
|
+
|
16
18
|
module ClassMethods
|
17
19
|
|
18
20
|
def table_name
|
@@ -69,7 +71,15 @@ module Dynamoid
|
|
69
71
|
incoming = (incoming || {}).symbolize_keys
|
70
72
|
Hash.new.tap do |hash|
|
71
73
|
self.attributes.each do |attribute, options|
|
72
|
-
|
74
|
+
if incoming.has_key?(attribute)
|
75
|
+
hash[attribute] = undump_field(incoming[attribute], options)
|
76
|
+
elsif options.has_key?(:default)
|
77
|
+
default_value = options[:default]
|
78
|
+
value = default_value.respond_to?(:call) ? default_value.call : default_value.dup
|
79
|
+
hash[attribute] = value
|
80
|
+
else
|
81
|
+
hash[attribute] = nil
|
82
|
+
end
|
73
83
|
end
|
74
84
|
incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
|
75
85
|
end
|
@@ -92,10 +102,6 @@ module Dynamoid
|
|
92
102
|
value
|
93
103
|
end
|
94
104
|
else
|
95
|
-
if value.nil? && (default_value = options[:default])
|
96
|
-
value = default_value.respond_to?(:call) ? default_value.call : default_value.dup
|
97
|
-
end
|
98
|
-
|
99
105
|
unless value.nil?
|
100
106
|
case options[:type]
|
101
107
|
when :string
|
@@ -118,7 +124,20 @@ module Dynamoid
|
|
118
124
|
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
119
125
|
value
|
120
126
|
else
|
121
|
-
|
127
|
+
case Dynamoid::Config.application_timezone
|
128
|
+
when :utc
|
129
|
+
ActiveSupport::TimeZone['UTC'].at(value).to_datetime
|
130
|
+
when :local
|
131
|
+
Time.at(value).to_datetime
|
132
|
+
when String
|
133
|
+
ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
|
134
|
+
end
|
135
|
+
end
|
136
|
+
when :date
|
137
|
+
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
138
|
+
value.to_date
|
139
|
+
else
|
140
|
+
UNIX_EPOCH_DATE + value.to_i
|
122
141
|
end
|
123
142
|
when :boolean
|
124
143
|
# persisted as 't', but because undump is called during initialize it can come in as true
|
@@ -159,6 +178,8 @@ module Dynamoid
|
|
159
178
|
!value.nil? ? value : nil
|
160
179
|
when :datetime
|
161
180
|
!value.nil? ? value.to_time.to_f : nil
|
181
|
+
when :date
|
182
|
+
!value.nil? ? (value.to_date - UNIX_EPOCH_DATE).to_i : nil
|
162
183
|
when :serialized
|
163
184
|
options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
|
164
185
|
when :raw
|
@@ -176,7 +197,7 @@ module Dynamoid
|
|
176
197
|
type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
|
177
198
|
else
|
178
199
|
case type
|
179
|
-
when :integer, :number, :datetime
|
200
|
+
when :integer, :number, :datetime, :date
|
180
201
|
:number
|
181
202
|
when :string, :serialized
|
182
203
|
:string
|
data/lib/dynamoid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Symonds
|
@@ -20,7 +20,7 @@ authors:
|
|
20
20
|
autorequire:
|
21
21
|
bindir: exe
|
22
22
|
cert_chain: []
|
23
|
-
date: 2017-
|
23
|
+
date: 2017-09-06 00:00:00.000000000 Z
|
24
24
|
dependencies:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activemodel
|
@@ -226,6 +226,7 @@ files:
|
|
226
226
|
- LICENSE.txt
|
227
227
|
- README.md
|
228
228
|
- Rakefile
|
229
|
+
- Vagrantfile
|
229
230
|
- dynamoid.gemspec
|
230
231
|
- gemfiles/rails_4_0.gemfile
|
231
232
|
- gemfiles/rails_4_0.gemfile.lock
|
@@ -285,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
285
286
|
version: '0'
|
286
287
|
requirements: []
|
287
288
|
rubyforge_project:
|
288
|
-
rubygems_version: 2.6.
|
289
|
+
rubygems_version: 2.6.12
|
289
290
|
signing_key:
|
290
291
|
specification_version: 4
|
291
292
|
summary: Dynamoid is an ORM for Amazon's DynamoDB
|