toy-dynamo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +70 -0
- data/Rakefile +1 -0
- data/lib/toy/dynamo/adapter.rb +58 -0
- data/lib/toy/dynamo/attributes.rb +29 -0
- data/lib/toy/dynamo/extensions/array.rb +14 -0
- data/lib/toy/dynamo/extensions/boolean.rb +14 -0
- data/lib/toy/dynamo/extensions/date.rb +21 -0
- data/lib/toy/dynamo/extensions/hash.rb +13 -0
- data/lib/toy/dynamo/extensions/set.rb +17 -0
- data/lib/toy/dynamo/extensions/symbol.rb +11 -0
- data/lib/toy/dynamo/extensions/time.rb +25 -0
- data/lib/toy/dynamo/persistence.rb +41 -0
- data/lib/toy/dynamo/querying.rb +82 -0
- data/lib/toy/dynamo/response.rb +38 -0
- data/lib/toy/dynamo/schema.rb +212 -0
- data/lib/toy/dynamo/table.rb +359 -0
- data/lib/toy/dynamo/version.rb +5 -0
- data/lib/toy/dynamo.rb +26 -0
- data/toy-dynamo.gemspec +26 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 8a7c12efaf31f18e82b7f7eb008ef7e78c564551
|
|
4
|
+
data.tar.gz: 6a756b5cd20bee61f2e1e9b9b56ce79b4f5e6b88
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f2047d2af54e2e2697bd146f29dda06da93f86c4309306313700225425ae367daec3039e10be63a705633e614ccb235df38653a7de4be2b049139ee3dc6f2488
|
|
7
|
+
data.tar.gz: 7642b6a3cf88648a184b5e1aff30658fb760f479cc34950e06be65d44c278e92b9dab30842f67dbd7d097b321be7b5f4c62dd1ef5e030dc4839f87d83bccff1d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2013 Cary Dunn
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
## Install
|
|
2
|
+
* toystore gem
|
|
3
|
+
* originally [https://github.com/jnunemaker/toystore](https://github.com/jnunemaker/toystore)
|
|
4
|
+
* Rails 4 compat: [https://github.com/cdunn/toystore](https://github.com/cdunn/toystore) (until merge)
|
|
5
|
+
* Official [aws-sdk](http://aws.amazon.com/sdkforruby/) gem
|
|
6
|
+
|
|
7
|
+
## Config
|
|
8
|
+
In ActiveModel model:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
dynamo_table do
|
|
12
|
+
adapter :dynamo, AWS::DynamoDB::ClientV2.new, {:model => self}
|
|
13
|
+
hash_key :thread_guid
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
* **Other options:**
|
|
17
|
+
* range_key :comment_id
|
|
18
|
+
* table_name "user_dynamo_table"
|
|
19
|
+
* read_provision 20
|
|
20
|
+
* write_provision 10
|
|
21
|
+
* local_secondary_index :created_at
|
|
22
|
+
* :projection => :keys_only **[default]**
|
|
23
|
+
* :projection => :all
|
|
24
|
+
* Can also specify an Array of attributes to project besides the primary keys ex:
|
|
25
|
+
* :projection => [:subject, :commenter_name]
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
* **Read Hash Key Only**
|
|
29
|
+
* Example:
|
|
30
|
+
* Model.read("xyz")
|
|
31
|
+
* Model.read("xyz", :consistent_read => true)
|
|
32
|
+
* **Read Hash + Range**
|
|
33
|
+
* Example:
|
|
34
|
+
* Model.read("xyz", :range_value => "123")
|
|
35
|
+
* Model.read("xyz", :range_value => "123", :consistent_read => true)
|
|
36
|
+
* **Read Multiple Hash Key Only**
|
|
37
|
+
* Example:
|
|
38
|
+
* Model.read_multiple(["abc", "xyz"])
|
|
39
|
+
* **Returns** Hash of Hash Keys => Object
|
|
40
|
+
* **Read Multiple Hash + Range**
|
|
41
|
+
* Example:
|
|
42
|
+
* Model.read_multiple([{:hash_value => "xyz", :range_value => "123"}, {:hash_value => "xyz", :range_value => "456"}], :consistent_read => true)
|
|
43
|
+
* Assumes all the same table
|
|
44
|
+
* Runs queries using the batch_get_item API call
|
|
45
|
+
* Limited to 100 items max (or 1MB result size)
|
|
46
|
+
* **Returns** Hash of Hash Keys => Hash of Range keys => Object
|
|
47
|
+
* **Read Range**
|
|
48
|
+
* Example:
|
|
49
|
+
* Model.read_range("xyz", :range => {:comment_id.eq => "123"})
|
|
50
|
+
* Model.read_range("xyz", :range => {:comment_id.gte => "123"})
|
|
51
|
+
* Model.read_range("xyz") (any range)
|
|
52
|
+
* If expecting lots of results, use batch and limit
|
|
53
|
+
* Model.read_range("xyz", :batch => 10, :limit => 100)
|
|
54
|
+
* Read 10 at a time up to a max of 100 items
|
|
55
|
+
* Model.read_range("xyz", :limit => 10)
|
|
56
|
+
* Read max 10 items in one request
|
|
57
|
+
* **Count Range**
|
|
58
|
+
* Example:
|
|
59
|
+
* Model.count_range("xyz", :range => {:comment_id.eq => "123"})
|
|
60
|
+
* Returns the number of total results (no attributes)
|
|
61
|
+
|
|
62
|
+
## Compatibility
|
|
63
|
+
* Tested with
|
|
64
|
+
* Rails 4
|
|
65
|
+
* Ruby 2.0.0p0
|
|
66
|
+
|
|
67
|
+
## TODO
|
|
68
|
+
* raise error if trying to use an attribute that wasn't 'select'ed (defaulting to selecting all attributes which loads everything with an extra read)
|
|
69
|
+
* while loop for situation where batch_get_item returns batched results
|
|
70
|
+
* error out on mismatch of table schema from dynamo_table schema (changed?)
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'adapter'
|
|
2
|
+
|
|
3
|
+
module Toy
|
|
4
|
+
module Dynamo
|
|
5
|
+
module Adapter
|
|
6
|
+
def read(key, options=nil)
|
|
7
|
+
options ||= {}
|
|
8
|
+
attrs = nil
|
|
9
|
+
if @options[:model].dynamo_table.range_keys.present?
|
|
10
|
+
raise ArgumentError, "Expected :range_value option" unless options[:range_value].present?
|
|
11
|
+
result = @options[:model].dynamo_table.query(key, options.merge(
|
|
12
|
+
:range => {
|
|
13
|
+
"#{@options[:model].dynamo_table.range_keys.find{|k| k[:primary_range_key] }[:attribute_name]}".to_sym.eq => options[:range_value]
|
|
14
|
+
}
|
|
15
|
+
))
|
|
16
|
+
attrs = (result[:member].empty? ? nil : Response.strip_attr_types(result[:member].first))
|
|
17
|
+
else
|
|
18
|
+
result = @options[:model].dynamo_table.get_item(key, options)
|
|
19
|
+
attrs = (result[:item].empty? ? nil : Response.strip_attr_types(result[:item]))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attrs
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def batch_read(keys, options=nil)
|
|
26
|
+
options ||= {}
|
|
27
|
+
@options[:model].dynamo_table.batch_get_item(keys, options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write(key, attributes, options=nil)
|
|
31
|
+
options ||= {}
|
|
32
|
+
@options[:model].dynamo_table.write(key, attributes, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(key, options=nil)
|
|
36
|
+
options ||= {}
|
|
37
|
+
@options[:model].dynamo_table.delete_item(key, options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clear(options=nil)
|
|
41
|
+
@options[:model].dynamo_table.delete
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def attributes_from_result(result)
|
|
47
|
+
attrs = {}
|
|
48
|
+
result.each_pair do |k,v|
|
|
49
|
+
attrs[k] = v.values.first
|
|
50
|
+
end
|
|
51
|
+
attrs
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Adapter.define(:dynamo, Toy::Dynamo::Adapter)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Attributes
|
|
3
|
+
|
|
4
|
+
# [OVERRIDE] 'write_attribute' to account for setting hash_key and id to same value
|
|
5
|
+
# u.id = 1
|
|
6
|
+
# * set id to 1
|
|
7
|
+
# * set hash_key to 1
|
|
8
|
+
# u.hash_key = 2
|
|
9
|
+
# * set hash_key to 2
|
|
10
|
+
# * set id to 2
|
|
11
|
+
def write_attribute(key, value)
|
|
12
|
+
key = key.to_s
|
|
13
|
+
attribute = attribute_instance(key)
|
|
14
|
+
|
|
15
|
+
if self.class.dynamo_table.hash_key[:attribute_name] != "id" # If primary hash_key is not the standard `id`
|
|
16
|
+
if key == self.class.dynamo_table.hash_key[:attribute_name]
|
|
17
|
+
@attributes[key] = attribute_instance(key).from_store(value)
|
|
18
|
+
return @attributes["id"] = attribute_instance("id").from_store(value)
|
|
19
|
+
elsif key == "id"
|
|
20
|
+
@attributes["id"] = attribute_instance("id").from_store(value)
|
|
21
|
+
return @attributes[self.class.dynamo_table.hash_key[:attribute_name]] = attribute_instance(self.class.dynamo_table.hash_key[:attribute_name]).from_store(value)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@attributes[key] = attribute.from_store(value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Extensions
|
|
3
|
+
module Array
|
|
4
|
+
def to_store(value, *)
|
|
5
|
+
value = value.respond_to?(:lines) ? value.lines : value
|
|
6
|
+
Marshal.dump(value.to_a)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def from_store(value, *)
|
|
10
|
+
value.nil? ? store_default : (value.class.is_a?(Array) ? value : Marshal.load(value))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Extensions
|
|
3
|
+
module Date
|
|
4
|
+
def to_store(value, *)
|
|
5
|
+
if value.nil? || value == ''
|
|
6
|
+
nil
|
|
7
|
+
else
|
|
8
|
+
date = value.is_a?(::Date) || value.is_a?(::Time) ? value : ::Date.parse(value.to_s)
|
|
9
|
+
#::Time.utc(date.year, date.month, date.day)
|
|
10
|
+
date.to_i
|
|
11
|
+
end
|
|
12
|
+
rescue
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def from_store(value, *)
|
|
17
|
+
Time.at(value.to_i).to_date if value.present?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Extensions
|
|
3
|
+
module Set
|
|
4
|
+
def store_default
|
|
5
|
+
[].to_set
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_store(value, *)
|
|
9
|
+
Marshal.dump(value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def from_store(value, *)
|
|
13
|
+
value.nil? ? store_default : Marshal.load(value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Extensions
|
|
3
|
+
module Time
|
|
4
|
+
def to_store(value, *)
|
|
5
|
+
if value.nil? || value == ''
|
|
6
|
+
nil
|
|
7
|
+
else
|
|
8
|
+
time_class = ::Time.try(:zone).present? ? ::Time.zone : ::Time
|
|
9
|
+
time = value.is_a?(::Time) ? value : time_class.parse(value.to_s)
|
|
10
|
+
# strip milliseconds as Ruby does micro and bson does milli and rounding rounded wrong
|
|
11
|
+
time.to_i if time
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def from_store(value, *)
|
|
16
|
+
value = ::Time.at(value.to_i)
|
|
17
|
+
if ::Time.try(:zone).present? && value.present?
|
|
18
|
+
value.in_time_zone(::Time.zone)
|
|
19
|
+
else
|
|
20
|
+
value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Persistence
|
|
3
|
+
|
|
4
|
+
def persisted_attributes
|
|
5
|
+
attributes_with_values = {}
|
|
6
|
+
attributes_to_persist = []
|
|
7
|
+
|
|
8
|
+
if self.new_record?
|
|
9
|
+
attributes_to_persist = self.class.persisted_attributes
|
|
10
|
+
else
|
|
11
|
+
attributes_to_persist = self.class.persisted_attributes.select { |a|
|
|
12
|
+
# Persist changed attributes and always the range key if applicable (for lookup)
|
|
13
|
+
self.changed_attributes.keys.include?(a.name) || (self.class.dynamo_table.range_keys && self.class.dynamo_table.range_keys.find{|k| k[:primary_range_key]}[:attribute_name] == a.name)
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attributes_to_persist.each do |attribute|
|
|
18
|
+
if (value = attribute.to_store(read_attribute(attribute.name)))
|
|
19
|
+
attributes_with_values[attribute.persisted_name] = value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attributes_with_values
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def persist
|
|
27
|
+
adapter.write(persisted_id, persisted_attributes, {:update_item => !self.new_record?})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete
|
|
31
|
+
@_destroyed = true
|
|
32
|
+
options = {}
|
|
33
|
+
if self.class.dynamo_table.range_keys
|
|
34
|
+
range_key = self.class.dynamo_table.range_keys.find{|k| k[:primary_range_key]}
|
|
35
|
+
options[:range_value] = read_attribute(range_key[:attribute_name])
|
|
36
|
+
end
|
|
37
|
+
adapter.delete(persisted_id, options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Dynamo
|
|
3
|
+
module Querying
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
# Failsafe
|
|
7
|
+
MAX_BATCH_ITERATIONS = 100
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
|
|
11
|
+
# Read results up to the limit
|
|
12
|
+
# read_range("1", :range_value => "2", :limit => 10)
|
|
13
|
+
# Loop results in given batch size until limit is hit or no more results
|
|
14
|
+
# read_range("1", :range_value => "2", :batch => 10, :limit => 1000)
|
|
15
|
+
def read_range(hash_value, options={})
|
|
16
|
+
raise ArgumentError, "no range_key specified for this table" if dynamo_table.range_keys.blank?
|
|
17
|
+
aggregated_results = []
|
|
18
|
+
|
|
19
|
+
if (batch_size = options.delete(:batch))
|
|
20
|
+
max_results_limit = options[:limit]
|
|
21
|
+
if options[:limit] && options[:limit] > batch_size
|
|
22
|
+
options.merge!(:limit => batch_size)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
results = dynamo_table.query(hash_value, options)
|
|
26
|
+
response = Response.new(results)
|
|
27
|
+
|
|
28
|
+
results[:member].each do |result|
|
|
29
|
+
attrs = Response.strip_attr_types(result)
|
|
30
|
+
aggregated_results << load(attrs[dynamo_table.hash_key[:attribute_name]], attrs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if batch_size
|
|
34
|
+
results_returned = response.count
|
|
35
|
+
batch_iteration = 0
|
|
36
|
+
while response.more_results? && batch_iteration < MAX_BATCH_ITERATIONS
|
|
37
|
+
if max_results_limit && (delta_results_limit = (max_results_limit-results_returned)) < batch_size
|
|
38
|
+
break if delta_results_limit == 0
|
|
39
|
+
options.merge!(:limit => delta_results_limit)
|
|
40
|
+
else
|
|
41
|
+
options.merge!(:limit => batch_size)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
results = dynamo_table.query(hash_value, options.merge(:exclusive_start_key => response.last_evaluated_key))
|
|
45
|
+
response = Response.new(results)
|
|
46
|
+
results[:member].each do |result|
|
|
47
|
+
attrs = Response.strip_attr_types(result)
|
|
48
|
+
aggregated_results << load(attrs[dynamo_table.hash_key[:attribute_name]], attrs)
|
|
49
|
+
end
|
|
50
|
+
results_returned += response.count
|
|
51
|
+
batch_iteration += 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
aggregated_results
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def count_range(hash_value, options={})
|
|
59
|
+
raise ArgumentError, "no range_key specified for this table" if dynamo_table.range_keys.blank?
|
|
60
|
+
results = dynamo_table.query(hash_value, options.merge(:select => :count))
|
|
61
|
+
Response.new(results).count
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_multiple(keys, options=nil)
|
|
65
|
+
results_map = {}
|
|
66
|
+
results = adapter.batch_read(keys, options)
|
|
67
|
+
results[:responses][dynamo_table.table_schema[:table_name]].each do |result|
|
|
68
|
+
attrs = Response.strip_attr_types(result)
|
|
69
|
+
if dynamo_table.range_keys.present?
|
|
70
|
+
(results_map[attrs[dynamo_table.hash_key[:attribute_name]]] ||= {})[attrs[dynamo_table.range_keys.find{|rk| rk[:primary_range_key] }[:attribute_name]]] = load(attrs[dynamo_table.hash_key[:attribute_name]], attrs)
|
|
71
|
+
else
|
|
72
|
+
results_map[attrs[dynamo_table.hash_key[:attribute_name]]] = load(attrs[dynamo_table.hash_key[:attribute_name]], attrs)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
results_map
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end # ClassMethods
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Dynamo
|
|
3
|
+
class Response
|
|
4
|
+
|
|
5
|
+
def initialize(response)
|
|
6
|
+
raise ArgumentError, "response should be an AWS::Core::Response" unless response.is_a?(AWS::Core::Response)
|
|
7
|
+
@raw_response = response
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
#def values_from_response_hash(options = {})
|
|
11
|
+
#@raw_response.inject({}) do |h, (key, value_hash)|
|
|
12
|
+
#h.update(key => value_hash.to_a.last)
|
|
13
|
+
#end
|
|
14
|
+
#end
|
|
15
|
+
|
|
16
|
+
def count
|
|
17
|
+
@raw_response[:count]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def last_evaluated_key
|
|
21
|
+
@raw_response[:last_evaluated_key]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def more_results?
|
|
25
|
+
@raw_response.has_key?(:last_evaluated_key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.strip_attr_types(hash)
|
|
29
|
+
attrs = {}
|
|
30
|
+
hash.each_pair do |k,v|
|
|
31
|
+
attrs[k] = v.values.first
|
|
32
|
+
end
|
|
33
|
+
attrs
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Dynamo
|
|
3
|
+
module Schema
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
|
|
8
|
+
KEY_TYPE = {
|
|
9
|
+
:hash => "HASH",
|
|
10
|
+
:range => "RANGE"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
PROJECTION_TYPE = {
|
|
14
|
+
:keys_only => "KEYS_ONLY",
|
|
15
|
+
:all => "ALL",
|
|
16
|
+
:include => "INCLUDE"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def dynamo_table(&block)
|
|
20
|
+
if block
|
|
21
|
+
@dynamo_table_config_block ||= block
|
|
22
|
+
else
|
|
23
|
+
@dynamo_table_config_block.call unless @dynamo_table_configged
|
|
24
|
+
|
|
25
|
+
@dynamo_table ||= Table.new(table_schema, self.adapter.client)
|
|
26
|
+
@dynamo_table_configged = true
|
|
27
|
+
@dynamo_table
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def table_schema
|
|
32
|
+
schema = {
|
|
33
|
+
:table_name => table_name,
|
|
34
|
+
:provisioned_throughput => {
|
|
35
|
+
:read_capacity_units => read_provision,
|
|
36
|
+
:write_capacity_units => write_provision
|
|
37
|
+
},
|
|
38
|
+
:key_schema => key_schema,
|
|
39
|
+
:attribute_definitions => attribute_definitions
|
|
40
|
+
}
|
|
41
|
+
schema.merge!(:local_secondary_indexes => local_secondary_indexes) unless local_secondary_indexes.blank?
|
|
42
|
+
schema
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def table_name(val=nil)
|
|
46
|
+
if val
|
|
47
|
+
raise(ArgumentError, "Invalid table name") unless val
|
|
48
|
+
@dynamo_table_name = val
|
|
49
|
+
else
|
|
50
|
+
@dynamo_table_name ||= "#{Rails.application.class.parent_name.to_s.underscore.dasherize}-#{self.to_s.underscore.dasherize.pluralize}-#{Rails.env}"
|
|
51
|
+
@dynamo_table_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_provision(val=nil)
|
|
56
|
+
if val
|
|
57
|
+
raise(ArgumentError, "Invalid read provision") unless val.to_i >= 1
|
|
58
|
+
@dynamo_read_provision = val.to_i
|
|
59
|
+
else
|
|
60
|
+
@dynamo_read_provision || 10
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write_provision(val=nil)
|
|
65
|
+
if val
|
|
66
|
+
raise(ArgumentError, "Invalid write provision") unless val.to_i >= 1
|
|
67
|
+
@dynamo_write_provision = val.to_i
|
|
68
|
+
else
|
|
69
|
+
@dynamo_write_provision || 10
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# TODO - need to add projections?
|
|
74
|
+
def attribute_definitions
|
|
75
|
+
# Keys for hash/range/secondary
|
|
76
|
+
# S | N | B
|
|
77
|
+
#{:attribute_name => , :attribute_type => }
|
|
78
|
+
|
|
79
|
+
keys = []
|
|
80
|
+
keys << hash_key[:attribute_name]
|
|
81
|
+
keys << range_key[:attribute_name] if range_key
|
|
82
|
+
local_secondary_indexes.each do |lsi|
|
|
83
|
+
keys << lsi[:key_schema].select{|h| h[:key_type] == "RANGE"}.first[:attribute_name]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
definitions = keys.uniq.collect do |k|
|
|
87
|
+
attr = self.attributes[k.to_s]
|
|
88
|
+
{
|
|
89
|
+
:attribute_name => attr.name,
|
|
90
|
+
:attribute_type => attribute_type_indicator(attr.type)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def attribute_type_indicator(type)
|
|
96
|
+
if type == Array
|
|
97
|
+
"S"
|
|
98
|
+
elsif type == Boolean
|
|
99
|
+
"S"
|
|
100
|
+
elsif type == Date
|
|
101
|
+
"N"
|
|
102
|
+
elsif type == Float
|
|
103
|
+
"N"
|
|
104
|
+
elsif type == Hash
|
|
105
|
+
"S"
|
|
106
|
+
elsif type == Integer
|
|
107
|
+
"N"
|
|
108
|
+
elsif type == Object
|
|
109
|
+
"S"
|
|
110
|
+
elsif type == Set
|
|
111
|
+
"S"
|
|
112
|
+
elsif type == String
|
|
113
|
+
"S"
|
|
114
|
+
elsif type == Time
|
|
115
|
+
"N"
|
|
116
|
+
elsif type == SimpleUUID::UUID
|
|
117
|
+
"S"
|
|
118
|
+
else
|
|
119
|
+
raise "unsupported attribute type #{type}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def key_schema
|
|
124
|
+
raise(ArgumentError, 'hash_key was not set for this table') if @dynamo_hash_key.blank?
|
|
125
|
+
schema = [hash_key]
|
|
126
|
+
schema << range_key if range_key
|
|
127
|
+
schema
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def hash_key(hash_key_key=nil)
|
|
131
|
+
if hash_key_key
|
|
132
|
+
hash_key_attribute = self.attributes[hash_key_key.to_s]
|
|
133
|
+
raise(ArgumentError, "Could not find attribute definition for hash_key #{hash_key_key}") unless hash_key_attribute
|
|
134
|
+
raise(ArgumentError, "Cannot use virtual attributes for hash_key") if hash_key_attribute.virtual?
|
|
135
|
+
@dynamo_hash_key = {
|
|
136
|
+
:attribute_name => hash_key_attribute.name,
|
|
137
|
+
:key_type => KEY_TYPE[:hash]
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
@dynamo_hash_key
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def range_key(range_key_key=nil)
|
|
145
|
+
if range_key_key
|
|
146
|
+
range_key_attribute = self.attributes[range_key_key.to_s]
|
|
147
|
+
raise(ArgumentError, "Could not find attribute definition for range_key #{range_key_key}") unless range_key_attribute
|
|
148
|
+
raise(ArgumentError, "Cannot use virtual attributes for range_key") if range_key_attribute.virtual?
|
|
149
|
+
|
|
150
|
+
validates_presence_of range_key_attribute.name.to_sym
|
|
151
|
+
|
|
152
|
+
@dynamo_range_key = {
|
|
153
|
+
:attribute_name => range_key_attribute.name,
|
|
154
|
+
:key_type => KEY_TYPE[:range]
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
@dynamo_range_key
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def local_secondary_indexes
|
|
162
|
+
@local_secondary_indexes ||= []
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @param [Symbol] index_attr the attribute to index secondary
|
|
166
|
+
# @param [Hash] options
|
|
167
|
+
# @option options [Symbol, Array<String>] :projection
|
|
168
|
+
# * `:all`
|
|
169
|
+
# * `:keys_only`
|
|
170
|
+
# * [:attributes to project]
|
|
171
|
+
def local_secondary_index(range_key_attr, options={})
|
|
172
|
+
options[:projection] ||= :keys_only
|
|
173
|
+
local_secondary_index_hash = {
|
|
174
|
+
:projection => {}
|
|
175
|
+
}
|
|
176
|
+
if options[:projection].is_a?(Array) && options[:projection].size > 0
|
|
177
|
+
options[:projection].each do |non_key_attr|
|
|
178
|
+
attr = self.attributes[non_key_attr.to_s]
|
|
179
|
+
raise(ArgumentError, "Could not find attribute definition for projection on #{non_key_attr}") unless attr
|
|
180
|
+
(local_secondary_index_hash[:projection][:non_key_attributes] ||= []) << attr.name
|
|
181
|
+
end
|
|
182
|
+
local_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[:include]
|
|
183
|
+
else
|
|
184
|
+
raise(ArgumentError, 'projection must be :all, :keys_only, Array (or attrs)') unless options[:projection] == :keys_only || options[:projection] == :all
|
|
185
|
+
local_secondary_index_hash[:projection][:projection_type] = PROJECTION_TYPE[options[:projection]]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
range_attr = self.attributes[range_key_attr.to_s]
|
|
189
|
+
raise(ArgumentError, "Could not find attribute definition for local secondary index on #{range_key_attr}") unless range_attr
|
|
190
|
+
local_secondary_index_hash[:index_name] = (options[:name] || "#{range_attr.name}_index".camelcase)
|
|
191
|
+
|
|
192
|
+
hash_key_attr = self.attributes[hash_key[:attribute_name].to_s]
|
|
193
|
+
raise(ArgumentError, "Could not find attribute definition for hash_key") unless hash_key_attr
|
|
194
|
+
|
|
195
|
+
local_secondary_index_hash[:key_schema] = [
|
|
196
|
+
{
|
|
197
|
+
:attribute_name => hash_key_attr.name,
|
|
198
|
+
:key_type => KEY_TYPE[:hash]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
:attribute_name => range_attr.name,
|
|
202
|
+
:key_type => KEY_TYPE[:range]
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
(@local_secondary_indexes ||= []) << local_secondary_index_hash
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
end # ClassMethods
|
|
209
|
+
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
module Toy
|
|
2
|
+
module Dynamo
|
|
3
|
+
class Table
|
|
4
|
+
|
|
5
|
+
attr_reader :table_schema, :client, :schema_loaded, :hash_key, :range_keys
|
|
6
|
+
|
|
7
|
+
RETURNED_CONSUMED_CAPACITY = {
|
|
8
|
+
:none => "NONE",
|
|
9
|
+
:total => "TOTAL"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
TYPE_INDICATOR = {
|
|
13
|
+
:b => "B",
|
|
14
|
+
:n => "N",
|
|
15
|
+
:s => "S",
|
|
16
|
+
:ss => "SS",
|
|
17
|
+
:ns => "NS"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
QUERY_SELECT = {
|
|
21
|
+
:all => "ALL_ATTRIBUTES",
|
|
22
|
+
:projected => "ALL_PROJECTED_ATTRIBUTES",
|
|
23
|
+
:count => "COUNT",
|
|
24
|
+
:specific => "SPECIFIC_ATTRIBUTES"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
COMPARISON_OPERATOR = {
|
|
28
|
+
:eq => "EQ",
|
|
29
|
+
:le => "LE",
|
|
30
|
+
:lt => "LT",
|
|
31
|
+
:ge => "GE",
|
|
32
|
+
:gt => "GT",
|
|
33
|
+
:begins_with => "BEGINS_WITH",
|
|
34
|
+
:between => "BETWEEN"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def initialize(table_schema, client)
|
|
38
|
+
@table_schema = table_schema
|
|
39
|
+
@client = client
|
|
40
|
+
begin
|
|
41
|
+
self.load_schema
|
|
42
|
+
rescue AWS::DynamoDB::Errors::ResourceNotFoundException => e
|
|
43
|
+
puts "No table found! Creating..."
|
|
44
|
+
self.create
|
|
45
|
+
self.load_schema
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_schema
|
|
50
|
+
@schema_loaded = @client.describe_table(:table_name => @table_schema[:table_name])
|
|
51
|
+
|
|
52
|
+
@schema_loaded[:table][:key_schema].each do |key|
|
|
53
|
+
key_schema_attr = {
|
|
54
|
+
:attribute_name => key[:attribute_name],
|
|
55
|
+
:attribute_type => @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == key[:attribute_name]}[:attribute_type]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if key[:key_type] == "HASH"
|
|
59
|
+
@hash_key = key_schema_attr
|
|
60
|
+
else
|
|
61
|
+
(@range_keys ||= []) << key_schema_attr.merge(:primary_range_key => true)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if @schema_loaded[:table][:local_secondary_indexes]
|
|
66
|
+
@schema_loaded[:table][:local_secondary_indexes].each do |key|
|
|
67
|
+
lsi_range_key = key[:key_schema].find{|h| h[:key_type] == "RANGE" }
|
|
68
|
+
(@range_keys ||= []) << {
|
|
69
|
+
:attribute_name => lsi_range_key[:attribute_name],
|
|
70
|
+
:attribute_type => @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == lsi_range_key[:attribute_name]}[:attribute_type],
|
|
71
|
+
:index_name => key[:index_name]
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@schema_loaded
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def hash_key_item_param(value)
|
|
80
|
+
hash_key = @table_schema[:key_schema].find{|h| h[:key_type] == "HASH"}[:attribute_name]
|
|
81
|
+
hash_key_type = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == hash_key}[:attribute_type]
|
|
82
|
+
{ hash_key => { hash_key_type => value } }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def hash_key_condition_param(value)
|
|
86
|
+
hash_key = @table_schema[:key_schema].find{|h| h[:key_type] == "HASH"}[:attribute_name]
|
|
87
|
+
hash_key_type = @table_schema[:attribute_definitions].find{|h| h[:attribute_name] == hash_key}[:attribute_type]
|
|
88
|
+
{
|
|
89
|
+
hash_key => {
|
|
90
|
+
:attribute_value_list => [hash_key_type => value],
|
|
91
|
+
:comparison_operator => COMPARISON_OPERATOR[:eq]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def attr_with_type(attr_name, value)
|
|
97
|
+
{ attr_name => { TYPE_INDICATOR[type_from_value(value)] => value.to_s } }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def get_item(hash_key, options={})
|
|
101
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
|
102
|
+
options[:return_consumed_capacity] ||= :none # "NONE" # || "TOTAL"
|
|
103
|
+
options[:select] ||= []
|
|
104
|
+
|
|
105
|
+
get_item_request = {
|
|
106
|
+
:table_name => options[:table_name] || @table_schema[:table_name],
|
|
107
|
+
:key => hash_key_item_param(hash_key),
|
|
108
|
+
:consistent_read => options[:consistent_read],
|
|
109
|
+
:return_consumed_capacity => RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
|
110
|
+
}
|
|
111
|
+
get_item_request.merge!( :attributes_to_get => [options[:select]].flatten ) unless options[:select].blank?
|
|
112
|
+
@client.get_item(get_item_request)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# == options
|
|
116
|
+
# * consistent_read
|
|
117
|
+
# * return_consumed_capacity
|
|
118
|
+
# * order
|
|
119
|
+
# * select
|
|
120
|
+
# * range
|
|
121
|
+
def query(hash_key_value, options={})
|
|
122
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
|
123
|
+
options[:return_consumed_capacity] ||= :none # "NONE" # || "TOTAL"
|
|
124
|
+
options[:order] ||= :desc
|
|
125
|
+
#options[:index_name] ||= :none
|
|
126
|
+
#AWS::DynamoDB::Errors::ValidationException: ALL_PROJECTED_ATTRIBUTES can be used only when Querying using an IndexName
|
|
127
|
+
#options[:limit] ||= 10
|
|
128
|
+
#options[:exclusive_start_key]
|
|
129
|
+
|
|
130
|
+
key_conditions = {}
|
|
131
|
+
key_conditions.merge!(hash_key_condition_param(hash_key_value))
|
|
132
|
+
|
|
133
|
+
query_request = {
|
|
134
|
+
:table_name => options[:table_name] || @table_schema[:table_name],
|
|
135
|
+
:key_conditions => key_conditions,
|
|
136
|
+
:consistent_read => options[:consistent_read],
|
|
137
|
+
:return_consumed_capacity => RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
|
|
138
|
+
:scan_index_forward => (options[:order] == :asc)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if options[:range]
|
|
142
|
+
raise ArgumentError, "Expected a 1 element Hash for :range (ex {:age.gt => 13})" unless options[:range].is_a?(Hash) && options[:range].keys.size == 1
|
|
143
|
+
range_key_name, comparison_operator = options[:range].keys.first.split(".")
|
|
144
|
+
raise ArgumentError, "Comparison operator must be one of (#{COMPARISON_OPERATOR.keys.join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
|
|
145
|
+
range_key = @range_keys.find{|k| k[:attribute_name] == range_key_name}
|
|
146
|
+
raise ArgumentError, ":range key must be a valid Range attribute" unless range_key
|
|
147
|
+
raise ArgumentError, ":range key must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !options[:range].values.first.is_a?(Range)
|
|
148
|
+
|
|
149
|
+
if range_key.has_key?(:index_name) # Local Secondary Index
|
|
150
|
+
#options[:select] = :projected unless options[:select].present?
|
|
151
|
+
query_request.merge!(:index_name => range_key[:index_name])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
range_value = options[:range].values.first
|
|
155
|
+
range_attribute_list = []
|
|
156
|
+
if comparison_operator == "between"
|
|
157
|
+
range_attribute_list << { range_key[:attribute_type] => range_value.min }
|
|
158
|
+
range_attribute_list << { range_key[:attribute_type] => range_value.max }
|
|
159
|
+
else
|
|
160
|
+
# TODO - support Binary?
|
|
161
|
+
range_attribute_list = [{ range_key[:attribute_type] => range_value.to_s }]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
key_conditions.merge!({
|
|
165
|
+
range_key[:attribute_name] => {
|
|
166
|
+
:attribute_value_list => range_attribute_list,
|
|
167
|
+
:comparison_operator => COMPARISON_OPERATOR[comparison_operator.to_sym]
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Default if not already set
|
|
173
|
+
options[:select] ||= :all # :all, :projected, :count, []
|
|
174
|
+
if options[:select].is_a?(Array)
|
|
175
|
+
attrs_to_select = [options[:select].map(&:to_s)].flatten
|
|
176
|
+
attrs_to_select << @hash_key[:attribute_name]
|
|
177
|
+
attrs_to_select << @range_keys.find{|k| k[:primary_range_key] }[:attribute_name] if @range_keys
|
|
178
|
+
query_request.merge!({
|
|
179
|
+
:select => QUERY_SELECT[:specific],
|
|
180
|
+
:attributes_to_get => attrs_to_select.uniq
|
|
181
|
+
})
|
|
182
|
+
else
|
|
183
|
+
query_request.merge!({ :select => QUERY_SELECT[options[:select]] })
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
query_request.merge!({ :limit => options[:limit].to_i }) if options.has_key?(:limit)
|
|
187
|
+
query_request.merge!({ :exclusive_start_key => options[:exclusive_start_key] }) if options[:exclusive_start_key]
|
|
188
|
+
|
|
189
|
+
@client.query(query_request)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def batch_get_item(keys, options={})
|
|
193
|
+
options[:return_consumed_capacity] ||= :none
|
|
194
|
+
options[:select] ||= []
|
|
195
|
+
options[:consistent_read] = false unless options[:consistent_read]
|
|
196
|
+
|
|
197
|
+
raise ArgumentError, "must include between 1 - 100 keys" if keys.size == 0 || keys.size > 100
|
|
198
|
+
keys_request = []
|
|
199
|
+
keys.each do |k|
|
|
200
|
+
key_request = {}
|
|
201
|
+
if @range_keys.present?
|
|
202
|
+
hash_value = k[:hash_value]
|
|
203
|
+
else
|
|
204
|
+
raise ArgumentError, "expected keys to be in the form of ['hash key here'] for table with no range keys" if hash_value.is_a?(Hash)
|
|
205
|
+
hash_value = k
|
|
206
|
+
end
|
|
207
|
+
raise ArgumentError, "every key must include a :hash_value" if hash_value.blank?
|
|
208
|
+
key_request[@hash_key[:attribute_name]] = { @hash_key[:attribute_type] => hash_value.to_s }
|
|
209
|
+
if @range_keys.present?
|
|
210
|
+
range_value = k[:range_value]
|
|
211
|
+
raise ArgumentError, "every key must include a :range_value" if range_value.blank?
|
|
212
|
+
range_key = @range_keys.find{|k| k[:primary_range_key] }
|
|
213
|
+
key_request[range_key[:attribute_name]] = { range_key[:attribute_type] => range_value.to_s }
|
|
214
|
+
end
|
|
215
|
+
keys_request << key_request
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
request_items_request = {}
|
|
219
|
+
request_items_request.merge!( :keys => keys_request )
|
|
220
|
+
request_items_request.merge!( :attributes_to_get => [options[:select]].flatten ) unless options[:select].blank?
|
|
221
|
+
request_items_request.merge!( :consistent_read => options[:consistent_read] ) if options[:consistent_read]
|
|
222
|
+
batch_get_item_request = {
|
|
223
|
+
:request_items => { (options[:table_name] || @table_schema[:table_name]) => request_items_request },
|
|
224
|
+
:return_consumed_capacity => RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
|
225
|
+
}
|
|
226
|
+
@client.batch_get_item(batch_get_item_request)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def write(hash_key_value, attributes, options={})
|
|
230
|
+
options[:return_consumed_capacity] ||= :none
|
|
231
|
+
options[:update_item] = false unless options[:update_item]
|
|
232
|
+
|
|
233
|
+
if options[:update_item]
|
|
234
|
+
# UpdateItem
|
|
235
|
+
key_request = {
|
|
236
|
+
@hash_key[:attribute_name] => {
|
|
237
|
+
@hash_key[:attribute_type] => hash_key_value.to_s
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if @range_keys
|
|
241
|
+
range_key = @range_keys.find{|k| k[:primary_range_key]}
|
|
242
|
+
range_key_value = attributes[range_key[:attribute_name]]
|
|
243
|
+
raise ArgumentError, "range_key was not provided to the write command" if range_key_value.blank?
|
|
244
|
+
key_request.merge!({
|
|
245
|
+
range_key[:attribute_name] => {
|
|
246
|
+
range_key[:attribute_type] => range_key_value.to_s
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
end
|
|
250
|
+
attrs_to_update = {}
|
|
251
|
+
attributes.each_pair do |k,v|
|
|
252
|
+
next if @range_keys && k == range_key[:attribute_name]
|
|
253
|
+
attrs_to_update.merge!({
|
|
254
|
+
k => {
|
|
255
|
+
:value => attr_with_type(k,v).values.last,
|
|
256
|
+
:action => "PUT"
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
end
|
|
260
|
+
update_item_request = {
|
|
261
|
+
:table_name => options[:table_name] || @table_schema[:table_name],
|
|
262
|
+
:key => key_request,
|
|
263
|
+
:attribute_updates => attrs_to_update,
|
|
264
|
+
:return_consumed_capacity => RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
|
265
|
+
}
|
|
266
|
+
@client.update_item(update_item_request)
|
|
267
|
+
else
|
|
268
|
+
# PutItem
|
|
269
|
+
items = {}
|
|
270
|
+
attributes.each_pair do |k,v|
|
|
271
|
+
items.merge!(attr_with_type(k,v))
|
|
272
|
+
end
|
|
273
|
+
items.merge!(hash_key_item_param(hash_key_value))
|
|
274
|
+
put_item_request = {
|
|
275
|
+
:table_name => options[:table_name] || @table_schema[:table_name],
|
|
276
|
+
:item => items,
|
|
277
|
+
:return_consumed_capacity => RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
|
|
278
|
+
}
|
|
279
|
+
@client.put_item(put_item_request)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def delete_item(hash_key_value, options={})
|
|
284
|
+
key_request = {
|
|
285
|
+
@hash_key[:attribute_name] => {
|
|
286
|
+
@hash_key[:attribute_type] => hash_key_value.to_s
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if @range_keys
|
|
290
|
+
range_key = @range_keys.find{|k| k[:primary_range_key]}
|
|
291
|
+
raise ArgumentError, "range_key was not provided to the delete_item command" if options[:range_value].blank?
|
|
292
|
+
key_request.merge!({
|
|
293
|
+
range_key[:attribute_name] => {
|
|
294
|
+
range_key[:attribute_type] => options[:range_value].to_s
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
end
|
|
298
|
+
delete_item_request = {
|
|
299
|
+
:table_name => options[:table_name] || @table_schema[:table_name],
|
|
300
|
+
:key => key_request
|
|
301
|
+
}
|
|
302
|
+
@client.delete_item(delete_item_request)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def type_from_value(value)
|
|
306
|
+
case
|
|
307
|
+
when value.kind_of?(AWS::DynamoDB::Binary) then :b
|
|
308
|
+
when value.respond_to?(:to_str) then :s
|
|
309
|
+
when value.kind_of?(Numeric) then :n
|
|
310
|
+
when value.respond_to?(:each)
|
|
311
|
+
indicator = nil
|
|
312
|
+
value.each do |v|
|
|
313
|
+
member_indicator = type_indicator(v)
|
|
314
|
+
raise ArgumentError, "nested collections" if member_indicator.to_s.size > 1
|
|
315
|
+
raise ArgumentError, "mixed types" if indicator and member_indicator != indicator
|
|
316
|
+
indicator = member_indicator
|
|
317
|
+
end
|
|
318
|
+
indicator ||= :s
|
|
319
|
+
:"#{indicator}s"
|
|
320
|
+
else
|
|
321
|
+
raise ArgumentError, "unsupported attribute type #{value.class}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def create(options={})
|
|
326
|
+
if @client.list_tables[:table_names].include?(options[:table_name] || @table_schema[:table_name])
|
|
327
|
+
raise "Table #{options[:table_name] || @table_schema[:table_name]} already exists!"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
@client.create_table(@table_schema.merge({
|
|
331
|
+
:table_name => options[:table_name] || @table_schema[:table_name]
|
|
332
|
+
}))
|
|
333
|
+
|
|
334
|
+
while (table_metadata = self.describe)[:table][:table_status] == "CREATING"
|
|
335
|
+
sleep 1
|
|
336
|
+
end
|
|
337
|
+
table_metadata
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def describe
|
|
341
|
+
@client.describe_table(:table_name => @table_schema[:table_name])
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def delete(options={})
|
|
345
|
+
return false unless @client.list_tables[:table_names].include?(options[:table_name] || @table_schema[:table_name])
|
|
346
|
+
@client.delete_table(:table_name => options[:table_name] || @table_schema[:table_name])
|
|
347
|
+
begin
|
|
348
|
+
while (table_metadata = self.describe) && table_metadata[:table][:table_status] == "DELETING"
|
|
349
|
+
sleep 1
|
|
350
|
+
end
|
|
351
|
+
rescue AWS::DynamoDB::Errors::ResourceNotFoundException => e
|
|
352
|
+
puts "Table deleted!"
|
|
353
|
+
end
|
|
354
|
+
true
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
data/lib/toy/dynamo.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "toy/dynamo/version"
|
|
2
|
+
require "toy/dynamo/adapter"
|
|
3
|
+
require "toy/dynamo/schema"
|
|
4
|
+
require "toy/dynamo/table"
|
|
5
|
+
require "toy/dynamo/querying"
|
|
6
|
+
require "toy/dynamo/response"
|
|
7
|
+
require "toy/dynamo/persistence"
|
|
8
|
+
# Override 'write_attribute' for hash_key == id
|
|
9
|
+
require "toy/dynamo/attributes"
|
|
10
|
+
require "toy/dynamo/extensions/array"
|
|
11
|
+
require "toy/dynamo/extensions/boolean"
|
|
12
|
+
require "toy/dynamo/extensions/date"
|
|
13
|
+
require "toy/dynamo/extensions/hash"
|
|
14
|
+
require "toy/dynamo/extensions/set"
|
|
15
|
+
require "toy/dynamo/extensions/time"
|
|
16
|
+
require "toy/dynamo/extensions/symbol"
|
|
17
|
+
|
|
18
|
+
module Toy
|
|
19
|
+
module Dynamo
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
include Toy::Store
|
|
22
|
+
|
|
23
|
+
include Schema
|
|
24
|
+
include Querying
|
|
25
|
+
end
|
|
26
|
+
end
|
data/toy-dynamo.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'toy/dynamo/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "toy-dynamo"
|
|
8
|
+
spec.version = Toy::Dynamo::VERSION
|
|
9
|
+
spec.authors = ["Cary Dunn"]
|
|
10
|
+
spec.email = ["cary.dunn@gmail.com"]
|
|
11
|
+
spec.description = %q{DynamoDB ORM - extension to toystore}
|
|
12
|
+
spec.summary = %q{DynamoDB ORM - extension to toystore}
|
|
13
|
+
spec.homepage = ""
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files`.split($/)
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
|
22
|
+
spec.add_development_dependency "rake"
|
|
23
|
+
|
|
24
|
+
spec.add_dependency 'adapter', '~> 0.7.0'
|
|
25
|
+
spec.add_dependency 'aws-sdk', '~> 1.9'
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toy-dynamo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cary Dunn
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2013-05-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ~>
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ~>
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '>='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: adapter
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ~>
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.7.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ~>
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.7.0
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: aws-sdk
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ~>
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.9'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ~>
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.9'
|
|
69
|
+
description: DynamoDB ORM - extension to toystore
|
|
70
|
+
email:
|
|
71
|
+
- cary.dunn@gmail.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- .gitignore
|
|
77
|
+
- Gemfile
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- README.md
|
|
80
|
+
- Rakefile
|
|
81
|
+
- lib/toy/dynamo.rb
|
|
82
|
+
- lib/toy/dynamo/adapter.rb
|
|
83
|
+
- lib/toy/dynamo/attributes.rb
|
|
84
|
+
- lib/toy/dynamo/extensions/array.rb
|
|
85
|
+
- lib/toy/dynamo/extensions/boolean.rb
|
|
86
|
+
- lib/toy/dynamo/extensions/date.rb
|
|
87
|
+
- lib/toy/dynamo/extensions/hash.rb
|
|
88
|
+
- lib/toy/dynamo/extensions/set.rb
|
|
89
|
+
- lib/toy/dynamo/extensions/symbol.rb
|
|
90
|
+
- lib/toy/dynamo/extensions/time.rb
|
|
91
|
+
- lib/toy/dynamo/persistence.rb
|
|
92
|
+
- lib/toy/dynamo/querying.rb
|
|
93
|
+
- lib/toy/dynamo/response.rb
|
|
94
|
+
- lib/toy/dynamo/schema.rb
|
|
95
|
+
- lib/toy/dynamo/table.rb
|
|
96
|
+
- lib/toy/dynamo/version.rb
|
|
97
|
+
- toy-dynamo.gemspec
|
|
98
|
+
homepage: ''
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata: {}
|
|
102
|
+
post_install_message:
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - '>='
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - '>='
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubyforge_project:
|
|
118
|
+
rubygems_version: 2.0.0
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: DynamoDB ORM - extension to toystore
|
|
122
|
+
test_files: []
|