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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: decc1a7abee20de61693c51cdc89d39dc5a37340
4
- data.tar.gz: 33dc108ac41878f619092ca53b3e0865b73aa775
3
+ metadata.gz: 563c512230d5a7f2485b6cf226eff77accfa2a3e
4
+ data.tar.gz: e2f88e774032ddf472bc81a2b9e2a5844b61933a
5
5
  SHA512:
6
- metadata.gz: 21b9a070f8cb22b2c0fb21803e57fca01647c4b0091b98a08f00e84939fe20ed565e3703f78dbd726a62b203da8a36dfa2ded2274186aae43846513b5a64f092
7
- data.tar.gz: acf6e06bc0df07455152b26af7830d57cf3b9a13cb670fbe68d3a88de3097cd47fd86e10864dfc6101617d8e0afb65e05e4487c1b3ab1aff6c349a4c4e2d3fcf
6
+ metadata.gz: 4a9dba2ea759a74d83f27ef984e3ca963230ddebde577084e89324eb69dd08c65c4c19690891e4086bc9cacadb17eaed5c460c03944dcc9db1eb109044ceef91
7
+ data.tar.gz: 314ffb14350619c950bd6615d46ccb3ca32383116f1a1dfe0aa39891694575e89d3376051bfac53b4e4a0f584257c0315a334365e5715727bcc767a31c2daa5e
data/.gitignore CHANGED
@@ -66,3 +66,6 @@ Gemfile.lock
66
66
  /tmp/
67
67
  /spec/DynamoDBLocal-latest/
68
68
  /vendor/
69
+
70
+ # For vagrant
71
+ .vagrant
@@ -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
6
- - ruby-2.3.3
5
+ - ruby-2.2.7
6
+ - ruby-2.3.4
7
7
  - ruby-2.4.1
8
- - jruby-9.1.8.0
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.14.6
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
@@ -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
- You can also limit the number of evaluated records, or select a record from which to start, to support pagination:
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.eval_limit(5).start(address) # Only 5 addresses.
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.limit(10_000).batch(100). each { … } #batch specified as part of a chain
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
- The query I use is as follows, but I really do not know a lot about Dynamoid, and got this working by reading through other Amazon Dynamo code bases and the documentation form Amazon.
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
- where the range modifier is one of Dynamoid::Finders::RANGE_MAP.keys, where the RANGE_MAP is:
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:
@@ -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
@@ -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"
@@ -36,8 +36,6 @@ end
36
36
  module Dynamoid
37
37
  extend self
38
38
 
39
- MAX_ITEM_SIZE = 65_536
40
-
41
39
  def configure
42
40
  block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
43
41
  end
@@ -145,7 +145,7 @@ module Dynamoid
145
145
  #
146
146
  # @since 0.2.0
147
147
  define_method(m) do |*args|
148
- benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
148
+ benchmark("#{m.to_s}", *args) {adapter.send(m, *args)}
149
149
  end
150
150
  end
151
151
 
@@ -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.empty?}
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.empty?
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 != TABLE_STATUSES[:creating]
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 != TABLE_STATUSES[:deleting]
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.respond_to?(:empty?) && v.empty?)
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[:hash_key] : table.hash_key).to_s
423
- rng = (opts[:range_key].present? ? opts[:range_key] : table.range_key).to_s
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[:limit] = batch || limit if batch || limit
501
- request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp|
502
- memo[kvp[0].to_s] = {
503
- attribute_value_list: [kvp[1]],
504
- # TODO: Provide support for all comparison operators
505
- comparison_operator: EQ
506
- }
507
- memo
508
- end if scan_hash.present?
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
- # Batch loop, pulls multiple requests until done using the start_key
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.data[:items].each { |row| y << result_item_to_hash(row) }
613
+ scan_count += results.scanned_count
614
+ break if scan_limit && scan_count >= scan_limit
516
615
 
517
- if((lk = results[:last_evaluated_key]) && batch)
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
@@ -19,7 +19,7 @@ module Dynamoid
19
19
 
20
20
  # Create the association tracking attribute and initialize it to an empty hash.
21
21
  included do
22
- class_attribute :associations
22
+ class_attribute :associations, instance_accessor: false
23
23
 
24
24
  self.associations = {}
25
25
  end
@@ -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
  #
@@ -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, :eval_limit, :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,
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
- def eval_limit(limit)
80
- @eval_limit = limit
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 [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
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, query, scan_opts).each do |hash|
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
- val = query[key]
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 key.to_s.split('.').last
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 = { :hash_value => query[source.hash_key] }
179
- query.keys.select { |k| k.to_s.include?('.') }.each do |key|
180
- opts.merge!(range_hash(key))
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 query_keys
186
- query.keys.collect{|k| k.to_s.split('.').first}
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 == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
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
- key = { :hash_key_element => @start.hash_key }
196
- if range_key = @start.class.range_key
197
- key.merge!({:range_key_element => @start.send(range_key) })
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[:limit] = @eval_limit if @eval_limit
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[:limit] = @eval_limit if @eval_limit
214
- opts[:next_token] = start_key if @start
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
@@ -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 ||= {}
@@ -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
- define_method(named) { read_attribute(named) }
55
- define_method("#{named}?") do
56
- value = read_attribute(named)
57
- case value
58
- when true then true
59
- when false, nil then false
60
- else
61
- !value.nil?
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
- remove_method field
84
- remove_method :"#{field}="
85
- remove_method :"#{field}?"
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 = DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps
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
- self.updated_at = DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps
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
@@ -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
@@ -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
- hash[attribute] = undump_field(incoming[attribute], options)
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
- Time.at(value).to_datetime
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
@@ -1,3 +1,3 @@
1
1
  module Dynamoid
2
- VERSION = "1.3.3"
2
+ VERSION = "1.3.4"
3
3
  end
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.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-05-15 00:00:00.000000000 Z
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.8
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