dynamoid 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +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
|