synamoid 1.2.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/.document +5 -0
- data/.gitignore +67 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +443 -0
- data/Rakefile +64 -0
- data/dynamoid.gemspec +53 -0
- data/lib/dynamoid.rb +53 -0
- data/lib/dynamoid/adapter.rb +190 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +892 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/associations/association.rb +116 -0
- data/lib/dynamoid/associations/belongs_to.rb +44 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
- data/lib/dynamoid/associations/has_many.rb +39 -0
- data/lib/dynamoid/associations/has_one.rb +39 -0
- data/lib/dynamoid/associations/many_association.rb +193 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config.rb +58 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/criteria/chain.rb +214 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +201 -0
- data/lib/dynamoid/errors.rb +66 -0
- data/lib/dynamoid/fields.rb +164 -0
- data/lib/dynamoid/finders.rb +199 -0
- data/lib/dynamoid/identity_map.rb +92 -0
- data/lib/dynamoid/indexes.rb +273 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +359 -0
- data/lib/dynamoid/validations.rb +63 -0
- data/lib/dynamoid/version.rb +3 -0
- metadata +266 -0
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
require "rake"
|
12
|
+
require "rspec/core/rake_task"
|
13
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
14
|
+
spec.pattern = FileList["spec/**/*_spec.rb"]
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
18
|
+
spec.pattern = "spec/**/*_spec.rb"
|
19
|
+
spec.rcov = true
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Start DynamoDBLocal, run tests, clean up"
|
23
|
+
task :unattended_spec do |t|
|
24
|
+
|
25
|
+
if system("bin/start_dynamodblocal")
|
26
|
+
puts "DynamoDBLocal started; proceeding with specs."
|
27
|
+
else
|
28
|
+
raise "Unable to start DynamoDBLocal. Cannot run unattended specs."
|
29
|
+
end
|
30
|
+
|
31
|
+
#Cleanup
|
32
|
+
at_exit do
|
33
|
+
unless system("bin/stop_dynamodblocal")
|
34
|
+
$stderr.puts "Unable to cleanly stop DynamoDBLocal."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::Task["spec"].invoke
|
39
|
+
end
|
40
|
+
|
41
|
+
require "yard"
|
42
|
+
YARD::Rake::YardocTask.new do |t|
|
43
|
+
t.files = ["lib/**/*.rb", "README", "LICENSE"] # optional
|
44
|
+
t.options = ["-m", "markdown"] # optional
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Publish documentation to gh-pages"
|
48
|
+
task :publish do
|
49
|
+
Rake::Task["yard"].invoke
|
50
|
+
`git add .`
|
51
|
+
`git commit -m 'Regenerated documentation'`
|
52
|
+
`git checkout gh-pages`
|
53
|
+
`git clean -fdx`
|
54
|
+
`git checkout master -- doc`
|
55
|
+
`cp -R doc/* .`
|
56
|
+
`git rm -rf doc/`
|
57
|
+
`git add .`
|
58
|
+
`git commit -m 'Regenerated documentation'`
|
59
|
+
`git pull`
|
60
|
+
`git push`
|
61
|
+
`git checkout master`
|
62
|
+
end
|
63
|
+
|
64
|
+
task :default => :spec
|
data/dynamoid.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "dynamoid/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "synamoid"
|
8
|
+
spec.version = Dynamoid::VERSION
|
9
|
+
|
10
|
+
# Keep in sync with README
|
11
|
+
spec.authors = [
|
12
|
+
"Josh Symonds",
|
13
|
+
"Logan Bowers",
|
14
|
+
"Craig Heneveld",
|
15
|
+
"Anatha Kumaran",
|
16
|
+
"Jason Dew",
|
17
|
+
"Luis Arias",
|
18
|
+
"Stefan Neculai",
|
19
|
+
"Philip White",
|
20
|
+
"Peeyush Kumar",
|
21
|
+
"Sumanth Ravipati",
|
22
|
+
"Pascal Corpet",
|
23
|
+
"Brian Glusman",
|
24
|
+
"Peter Boling"
|
25
|
+
]
|
26
|
+
spec.email = ["peter.boling@gmail.com", "brian@stellaservice.com"]
|
27
|
+
|
28
|
+
spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
|
29
|
+
spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
|
30
|
+
spec.extra_rdoc_files = [
|
31
|
+
"LICENSE.txt",
|
32
|
+
"README.md"
|
33
|
+
]
|
34
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features)/}) }
|
35
|
+
spec.homepage = "http://github.com/Dynamoid/Dynamoid"
|
36
|
+
spec.licenses = ["MIT"]
|
37
|
+
spec.bindir = "exe"
|
38
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
39
|
+
spec.require_paths = ["lib"]
|
40
|
+
|
41
|
+
spec.add_runtime_dependency(%q<activemodel>, ["~> 5"])
|
42
|
+
spec.add_runtime_dependency(%q<activemodel-serializers-xml>, ["~> 1"])
|
43
|
+
spec.add_runtime_dependency(%q<aws-sdk-resources>, ["~> 2"])
|
44
|
+
spec.add_runtime_dependency(%q<concurrent-ruby>, [">= 1.0"])
|
45
|
+
spec.add_development_dependency(%q<rake>, [">= 10"])
|
46
|
+
spec.add_development_dependency(%q<bundler>, ["~> 1.12"])
|
47
|
+
spec.add_development_dependency(%q<rspec>, [">= 3"])
|
48
|
+
spec.add_development_dependency(%q<yard>, [">= 0"])
|
49
|
+
spec.add_development_dependency(%q<github-markup>, [">= 0"])
|
50
|
+
spec.add_development_dependency(%q<pry>, [">= 0"])
|
51
|
+
spec.add_development_dependency(%q<coveralls>, [">= 0"])
|
52
|
+
spec.add_development_dependency(%q<rspec-retry>, [">= 0"])
|
53
|
+
end
|
data/lib/dynamoid.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "delegate"
|
2
|
+
require "time"
|
3
|
+
require "securerandom"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/core_ext"
|
6
|
+
require "active_support/json"
|
7
|
+
require "active_support/inflector"
|
8
|
+
require "active_support/lazy_load_hooks"
|
9
|
+
require "active_support/time_with_zone"
|
10
|
+
require "active_model"
|
11
|
+
require 'activemodel-serializers-xml'
|
12
|
+
#gem 'active_model_serializers'
|
13
|
+
|
14
|
+
require "dynamoid/version"
|
15
|
+
require "dynamoid/errors"
|
16
|
+
require "dynamoid/fields"
|
17
|
+
require "dynamoid/indexes"
|
18
|
+
require "dynamoid/associations"
|
19
|
+
require "dynamoid/persistence"
|
20
|
+
require "dynamoid/dirty"
|
21
|
+
require "dynamoid/validations"
|
22
|
+
require "dynamoid/criteria"
|
23
|
+
require "dynamoid/finders"
|
24
|
+
require "dynamoid/identity_map"
|
25
|
+
require "dynamoid/config"
|
26
|
+
require "dynamoid/components"
|
27
|
+
require "dynamoid/document"
|
28
|
+
require "dynamoid/adapter"
|
29
|
+
|
30
|
+
require "dynamoid/middleware/identity_map"
|
31
|
+
|
32
|
+
module Dynamoid
|
33
|
+
extend self
|
34
|
+
|
35
|
+
MAX_ITEM_SIZE = 65_536
|
36
|
+
|
37
|
+
def configure
|
38
|
+
block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
|
39
|
+
end
|
40
|
+
alias :config :configure
|
41
|
+
|
42
|
+
def logger
|
43
|
+
Dynamoid::Config.logger
|
44
|
+
end
|
45
|
+
|
46
|
+
def included_models
|
47
|
+
@included_models ||= []
|
48
|
+
end
|
49
|
+
|
50
|
+
def adapter
|
51
|
+
@adapter ||= Adapter.new
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# require only 'concurrent/atom' once this issue is resolved:
|
2
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/pull/377
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
# encoding: utf-8
|
6
|
+
module Dynamoid
|
7
|
+
|
8
|
+
# Adapter's value-add:
|
9
|
+
# 1) For the rest of Dynamoid, the gateway to DynamoDB.
|
10
|
+
# 2) Allows switching `config.adapter` to ease development of a new adapter.
|
11
|
+
# 3) Caches the list of tables Dynamoid knows about.
|
12
|
+
class Adapter
|
13
|
+
def initialize
|
14
|
+
@adapter_ = Concurrent::Atom.new(nil)
|
15
|
+
@tables_ = Concurrent::Atom.new(nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def tables
|
19
|
+
if !@tables_.value
|
20
|
+
@tables_.swap{|value, args| benchmark('Cache Tables') { list_tables } }
|
21
|
+
end
|
22
|
+
@tables_.value
|
23
|
+
end
|
24
|
+
|
25
|
+
# The actual adapter currently in use.
|
26
|
+
#
|
27
|
+
# @since 0.2.0
|
28
|
+
def adapter
|
29
|
+
if !@adapter_.value
|
30
|
+
adapter = self.class.adapter_plugin_class.new
|
31
|
+
adapter.connect! if adapter.respond_to?(:connect!)
|
32
|
+
@adapter_.compare_and_set(nil, adapter)
|
33
|
+
clear_cache!
|
34
|
+
end
|
35
|
+
@adapter_.value
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear_cache!
|
39
|
+
@tables_.swap{|value, args| nil}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Shows how long it takes a method to run on the adapter. Useful for generating logged output.
|
43
|
+
#
|
44
|
+
# @param [Symbol] method the name of the method to appear in the log
|
45
|
+
# @param [Array] args the arguments to the method to appear in the log
|
46
|
+
# @yield the actual code to benchmark
|
47
|
+
#
|
48
|
+
# @return the result of the yield
|
49
|
+
#
|
50
|
+
# @since 0.2.0
|
51
|
+
def benchmark(method, *args)
|
52
|
+
start = Time.now
|
53
|
+
result = yield
|
54
|
+
Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
|
55
|
+
return result
|
56
|
+
end
|
57
|
+
|
58
|
+
# Write an object to the adapter.
|
59
|
+
#
|
60
|
+
# @param [String] table the name of the table to write the object to
|
61
|
+
# @param [Object] object the object itself
|
62
|
+
# @param [Hash] options Options that are passed to the put_item call
|
63
|
+
#
|
64
|
+
# @return [Object] the persisted object
|
65
|
+
#
|
66
|
+
# @since 0.2.0
|
67
|
+
def write(table, object, options = nil)
|
68
|
+
put_item(table, object, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Read one or many keys from the selected table.
|
72
|
+
# This method intelligently calls batch_get or get on the underlying adapter
|
73
|
+
# depending on whether ids is a range or a single key.
|
74
|
+
# If a range key is present, it will also interpolate that into the ids so
|
75
|
+
# that the batch get will acquire the correct record.
|
76
|
+
#
|
77
|
+
# @param [String] table the name of the table to write the object to
|
78
|
+
# @param [Array] ids to fetch, can also be a string of just one id
|
79
|
+
# @param [Hash] options: Passed to the underlying query. The :range_key option is required whenever the table has a range key,
|
80
|
+
# unless multiple ids are passed in.
|
81
|
+
#
|
82
|
+
# @since 0.2.0
|
83
|
+
def read(table, ids, options = {})
|
84
|
+
range_key = options.delete(:range_key)
|
85
|
+
|
86
|
+
if ids.respond_to?(:each)
|
87
|
+
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
88
|
+
batch_get_item({table => ids}, options)
|
89
|
+
else
|
90
|
+
options[:range_key] = range_key if range_key
|
91
|
+
get_item(table, ids, options)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Delete an item from a table.
|
96
|
+
#
|
97
|
+
# @param [String] table the name of the table to write the object to
|
98
|
+
# @param [Array] ids to delete, can also be a string of just one id
|
99
|
+
# @param [Array] range_key of the record to delete, can also be a string of just one range_key
|
100
|
+
#
|
101
|
+
def delete(table, ids, options = {})
|
102
|
+
range_key = options[:range_key] #array of range keys that matches the ids passed in
|
103
|
+
if ids.respond_to?(:each)
|
104
|
+
if range_key.respond_to?(:each)
|
105
|
+
#turn ids into array of arrays each element being hash_key, range_key
|
106
|
+
ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
|
107
|
+
else
|
108
|
+
ids = range_key ? [[ids, range_key]] : ids
|
109
|
+
end
|
110
|
+
|
111
|
+
batch_delete_item(table => ids)
|
112
|
+
else
|
113
|
+
delete_item(table, ids, options)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Scans a table. Generally quite slow; try to avoid using scan if at all possible.
|
118
|
+
#
|
119
|
+
# @param [String] table the name of the table to write the object to
|
120
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
121
|
+
#
|
122
|
+
# @since 0.2.0
|
123
|
+
def scan(table, query, opts = {})
|
124
|
+
benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_table(table_name, key, options = {})
|
128
|
+
if !tables.include?(table_name)
|
129
|
+
benchmark('Create Table') { adapter.create_table(table_name, key, options) }
|
130
|
+
tables << table_name
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @since 0.2.0
|
135
|
+
def delete_table(table_name, options = {})
|
136
|
+
if tables.include?(table_name)
|
137
|
+
benchmark('Delete Table') { adapter.delete_table(table_name, options) }
|
138
|
+
idx = tables.index(table_name)
|
139
|
+
tables.delete_at(idx)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
[:batch_get_item, :delete_item, :get_item, :list_tables, :put_item, :truncate, :batch_write_item, :batch_delete_item].each do |m|
|
144
|
+
# Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
|
145
|
+
#
|
146
|
+
# @since 0.2.0
|
147
|
+
define_method(m) do |*args|
|
148
|
+
benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Delegate all methods that aren't defind here to the underlying adapter.
|
153
|
+
#
|
154
|
+
# @since 0.2.0
|
155
|
+
def method_missing(method, *args, &block)
|
156
|
+
return benchmark(method, *args) {adapter.send(method, *args, &block)} if adapter.respond_to?(method)
|
157
|
+
super
|
158
|
+
end
|
159
|
+
|
160
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
161
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
162
|
+
# one range key to the hash.
|
163
|
+
#
|
164
|
+
# @param [String] table_name the name of the table
|
165
|
+
# @param [Hash] opts the options to query the table with
|
166
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
167
|
+
# @option opts [Range] :range_value find the range key within this range
|
168
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
169
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
170
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
171
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
172
|
+
#
|
173
|
+
# @return [Array] an array of all matching items
|
174
|
+
#
|
175
|
+
def query(table_name, opts = {})
|
176
|
+
adapter.query(table_name, opts)
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def self.adapter_plugin_class
|
182
|
+
unless Dynamoid.const_defined?(:AdapterPlugin) && Dynamoid::AdapterPlugin.const_defined?(Dynamoid::Config.adapter.camelcase)
|
183
|
+
require "dynamoid/adapter_plugin/#{Dynamoid::Config.adapter}"
|
184
|
+
end
|
185
|
+
|
186
|
+
Dynamoid::AdapterPlugin.const_get(Dynamoid::Config.adapter.camelcase)
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,892 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module AdapterPlugin
|
3
|
+
|
4
|
+
# The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby.
|
5
|
+
class AwsSdkV2
|
6
|
+
EQ = "EQ".freeze
|
7
|
+
RANGE_MAP = {
|
8
|
+
range_greater_than: 'GT',
|
9
|
+
range_less_than: 'LT',
|
10
|
+
range_gte: 'GE',
|
11
|
+
range_lte: 'LE',
|
12
|
+
range_begins_with: 'BEGINS_WITH',
|
13
|
+
range_between: 'BETWEEN',
|
14
|
+
range_eq: 'EQ'
|
15
|
+
}
|
16
|
+
HASH_KEY = "HASH".freeze
|
17
|
+
RANGE_KEY = "RANGE".freeze
|
18
|
+
STRING_TYPE = "S".freeze
|
19
|
+
NUM_TYPE = "N".freeze
|
20
|
+
BINARY_TYPE = "B".freeze
|
21
|
+
TABLE_STATUSES = {
|
22
|
+
creating: "CREATING",
|
23
|
+
updating: "UPDATING",
|
24
|
+
deleting: "DELETING",
|
25
|
+
active: "ACTIVE"
|
26
|
+
}.freeze
|
27
|
+
PARSE_TABLE_STATUS = ->(resp, lookup = :table) {
|
28
|
+
# lookup is table for describe_table API
|
29
|
+
# lookup is table_description for create_table API
|
30
|
+
# because Amazon, damnit.
|
31
|
+
resp.send(lookup).table_status
|
32
|
+
}
|
33
|
+
attr_reader :table_cache
|
34
|
+
|
35
|
+
# Establish the connection to DynamoDB.
|
36
|
+
#
|
37
|
+
# @return [Aws::DynamoDB::Client] the DynamoDB connection
|
38
|
+
def connect!
|
39
|
+
@client = if Dynamoid::Config.endpoint?
|
40
|
+
Aws::DynamoDB::Client.new(endpoint: Dynamoid::Config.endpoint)
|
41
|
+
else
|
42
|
+
Aws::DynamoDB::Client.new
|
43
|
+
end
|
44
|
+
@table_cache = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the client object.
|
48
|
+
#
|
49
|
+
# @since 1.0.0
|
50
|
+
def client
|
51
|
+
@client
|
52
|
+
end
|
53
|
+
|
54
|
+
# Puts or deletes multiple items in one or more tables
|
55
|
+
#
|
56
|
+
# @param [String] table_name the name of the table
|
57
|
+
# @param [Array] items to be processed
|
58
|
+
# @param [Hash] additional options
|
59
|
+
#
|
60
|
+
#See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
|
61
|
+
def batch_write_item table_name, objects, options = {}
|
62
|
+
request_items = []
|
63
|
+
options ||= {}
|
64
|
+
objects.each do |o|
|
65
|
+
request_items << { "put_request" => { item: o } }
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
client.batch_write_item(
|
70
|
+
{
|
71
|
+
request_items: {
|
72
|
+
table_name => request_items,
|
73
|
+
},
|
74
|
+
return_consumed_capacity: "TOTAL",
|
75
|
+
return_item_collection_metrics: "SIZE"
|
76
|
+
}.merge!(options)
|
77
|
+
)
|
78
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
79
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
84
|
+
#
|
85
|
+
# @example Retrieve IDs 1 and 2 from the table testtable
|
86
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item({'table1' => ['1', '2']})
|
87
|
+
#
|
88
|
+
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
89
|
+
# @param [Hash] options to be passed to underlying BatchGet call
|
90
|
+
#
|
91
|
+
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
92
|
+
#
|
93
|
+
# @since 1.0.0
|
94
|
+
#
|
95
|
+
# @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
|
96
|
+
def batch_get_item(table_ids, options = {})
|
97
|
+
request_items = Hash.new{|h, k| h[k] = []}
|
98
|
+
return request_items if table_ids.all?{|k, v| v.empty?}
|
99
|
+
|
100
|
+
table_ids.each do |t, ids|
|
101
|
+
next if ids.empty?
|
102
|
+
tbl = describe_table(t)
|
103
|
+
hk = tbl.hash_key.to_s
|
104
|
+
rng = tbl.range_key.to_s
|
105
|
+
|
106
|
+
keys = if rng.present?
|
107
|
+
Array(ids).map do |h,r|
|
108
|
+
{ hk => h, rng => r }
|
109
|
+
end
|
110
|
+
else
|
111
|
+
Array(ids).map do |id|
|
112
|
+
{ hk => id }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
request_items[t] = {
|
117
|
+
keys: keys
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
results = client.batch_get_item(
|
122
|
+
request_items: request_items
|
123
|
+
)
|
124
|
+
|
125
|
+
ret = Hash.new([].freeze) # Default for tables where no rows are returned
|
126
|
+
results.data[:responses].each do |table, rows|
|
127
|
+
ret[table] = rows.collect { |r| result_item_to_hash(r) }
|
128
|
+
end
|
129
|
+
ret
|
130
|
+
end
|
131
|
+
|
132
|
+
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
133
|
+
#
|
134
|
+
# @example Delete IDs 1 and 2 from the table testtable
|
135
|
+
# Dynamoid::AdapterPlugin::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
136
|
+
#or
|
137
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
138
|
+
#
|
139
|
+
# @param [Hash] options the hash of tables and IDs to delete
|
140
|
+
#
|
141
|
+
# @return nil
|
142
|
+
#
|
143
|
+
# @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
144
|
+
def batch_delete_item(options)
|
145
|
+
options.each_pair do |table_name, ids|
|
146
|
+
table = describe_table(table_name)
|
147
|
+
ids.each do |id|
|
148
|
+
client.delete_item(table_name: table_name, key: key_stanza(table, *id))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create a table on DynamoDB. This usually takes a long time to complete.
|
155
|
+
#
|
156
|
+
# @param [String] table_name the name of the table to create
|
157
|
+
# @param [Symbol] key the table's primary key (defaults to :id)
|
158
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
159
|
+
# @option options [Array<Dynamoid::Indexes::Index>] local_secondary_indexes
|
160
|
+
# @option options [Array<Dynamoid::Indexes::Index>] global_secondary_indexes
|
161
|
+
# @option options [Symbol] hash_key_type The type of the hash key
|
162
|
+
# @option options [Boolean] sync Wait for table status to be ACTIVE?
|
163
|
+
# @since 1.0.0
|
164
|
+
def create_table(table_name, key = :id, options = {})
|
165
|
+
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
166
|
+
read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
|
167
|
+
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
168
|
+
|
169
|
+
secondary_indexes = options.slice(
|
170
|
+
:local_secondary_indexes,
|
171
|
+
:global_secondary_indexes
|
172
|
+
)
|
173
|
+
ls_indexes = options[:local_secondary_indexes]
|
174
|
+
gs_indexes = options[:global_secondary_indexes]
|
175
|
+
|
176
|
+
key_schema = {
|
177
|
+
:hash_key_schema => { key => (options[:hash_key_type] || :string) },
|
178
|
+
:range_key_schema => options[:range_key]
|
179
|
+
}
|
180
|
+
attribute_definitions = build_all_attribute_definitions(
|
181
|
+
key_schema,
|
182
|
+
secondary_indexes
|
183
|
+
)
|
184
|
+
key_schema = aws_key_schema(
|
185
|
+
key_schema[:hash_key_schema],
|
186
|
+
key_schema[:range_key_schema]
|
187
|
+
)
|
188
|
+
|
189
|
+
client_opts = {
|
190
|
+
table_name: table_name,
|
191
|
+
provisioned_throughput: {
|
192
|
+
read_capacity_units: read_capacity,
|
193
|
+
write_capacity_units: write_capacity
|
194
|
+
},
|
195
|
+
key_schema: key_schema,
|
196
|
+
attribute_definitions: attribute_definitions
|
197
|
+
}
|
198
|
+
|
199
|
+
if ls_indexes.present?
|
200
|
+
client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
|
201
|
+
index_to_aws_hash(index)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
if gs_indexes.present?
|
206
|
+
client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
|
207
|
+
index_to_aws_hash(index)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
resp = client.create_table(client_opts)
|
211
|
+
options[:sync] = true if !options.has_key?(:sync) && ls_indexes.present? || gs_indexes.present?
|
212
|
+
until_past_table_status(table_name) if options[:sync] &&
|
213
|
+
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
214
|
+
status != TABLE_STATUSES[:creating]
|
215
|
+
# Response to original create_table, which, if options[:sync]
|
216
|
+
# may have an outdated table_description.table_status of "CREATING"
|
217
|
+
resp
|
218
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
219
|
+
Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
|
220
|
+
end
|
221
|
+
|
222
|
+
# Create a table on DynamoDB *synchronously*.
|
223
|
+
# This usually takes a long time to complete.
|
224
|
+
# CreateTable is normally an asynchronous operation.
|
225
|
+
# You can optionally define secondary indexes on the new table,
|
226
|
+
# as part of the CreateTable operation.
|
227
|
+
# If you want to create multiple tables with secondary indexes on them,
|
228
|
+
# you must create the tables sequentially.
|
229
|
+
# Only one table with secondary indexes can be
|
230
|
+
# in the CREATING state at any given time.
|
231
|
+
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#create_table-instance_method
|
232
|
+
#
|
233
|
+
# @param [String] table_name the name of the table to create
|
234
|
+
# @param [Symbol] key the table's primary key (defaults to :id)
|
235
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
236
|
+
# @option options [Array<Dynamoid::Indexes::Index>] local_secondary_indexes
|
237
|
+
# @option options [Array<Dynamoid::Indexes::Index>] global_secondary_indexes
|
238
|
+
# @option options [Symbol] hash_key_type The type of the hash key
|
239
|
+
# @since 1.2.0
|
240
|
+
def create_table_synchronously(table_name, key = :id, options = {})
|
241
|
+
create_table(table_name, key, options.merge(sync: true))
|
242
|
+
end
|
243
|
+
|
244
|
+
# Removes an item from DynamoDB.
|
245
|
+
#
|
246
|
+
# @param [String] table_name the name of the table
|
247
|
+
# @param [String] key the hash key of the item to delete
|
248
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
249
|
+
#
|
250
|
+
# @since 1.0.0
|
251
|
+
#
|
252
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
253
|
+
def delete_item(table_name, key, options = {})
|
254
|
+
options ||= {}
|
255
|
+
range_key = options[:range_key]
|
256
|
+
conditions = options[:conditions]
|
257
|
+
table = describe_table(table_name)
|
258
|
+
client.delete_item(
|
259
|
+
table_name: table_name,
|
260
|
+
key: key_stanza(table, key, range_key),
|
261
|
+
expected: expected_stanza(conditions)
|
262
|
+
)
|
263
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
264
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
265
|
+
end
|
266
|
+
|
267
|
+
# Deletes an entire table from DynamoDB.
|
268
|
+
#
|
269
|
+
# @param [String] table_name the name of the table to destroy
|
270
|
+
# @option options [Boolean] sync Wait for table status check to raise ResourceNotFoundException
|
271
|
+
#
|
272
|
+
# @since 1.0.0
|
273
|
+
def delete_table(table_name, options = {})
|
274
|
+
resp = client.delete_table(table_name: table_name)
|
275
|
+
until_past_table_status(table_name, :deleting) if options[:sync] &&
|
276
|
+
(status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
|
277
|
+
status != TABLE_STATUSES[:deleting]
|
278
|
+
table_cache.delete(table_name)
|
279
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
280
|
+
Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
|
281
|
+
raise e
|
282
|
+
end
|
283
|
+
|
284
|
+
def delete_table_synchronously(table_name, options = {})
|
285
|
+
delete_table(table_name, options.merge(sync: true))
|
286
|
+
end
|
287
|
+
|
288
|
+
# @todo Add a DescribeTable method.
|
289
|
+
|
290
|
+
# Fetches an item from DynamoDB.
|
291
|
+
#
|
292
|
+
# @param [String] table_name the name of the table
|
293
|
+
# @param [String] key the hash key of the item to find
|
294
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
295
|
+
#
|
296
|
+
# @return [Hash] a hash representing the raw item in DynamoDB
|
297
|
+
#
|
298
|
+
# @since 1.0.0
|
299
|
+
#
|
300
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method
|
301
|
+
def get_item(table_name, key, options = {})
|
302
|
+
options ||= {}
|
303
|
+
table = describe_table(table_name)
|
304
|
+
range_key = options.delete(:range_key)
|
305
|
+
|
306
|
+
item = client.get_item(table_name: table_name,
|
307
|
+
key: key_stanza(table, key, range_key)
|
308
|
+
)[:item]
|
309
|
+
item ? result_item_to_hash(item) : nil
|
310
|
+
end
|
311
|
+
|
312
|
+
# Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values
|
313
|
+
#
|
314
|
+
# @param [String] table_name the name of the table
|
315
|
+
# @param [String] key the hash key of the item to find
|
316
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
317
|
+
#
|
318
|
+
# @return new attributes for the record
|
319
|
+
#
|
320
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
321
|
+
def update_item(table_name, key, options = {})
|
322
|
+
range_key = options.delete(:range_key)
|
323
|
+
conditions = options.delete(:conditions)
|
324
|
+
table = describe_table(table_name)
|
325
|
+
|
326
|
+
yield(iu = ItemUpdater.new(table, key, range_key))
|
327
|
+
|
328
|
+
raise "non-empty options: #{options}" unless options.empty?
|
329
|
+
begin
|
330
|
+
result = client.update_item(table_name: table_name,
|
331
|
+
key: key_stanza(table, key, range_key),
|
332
|
+
attribute_updates: iu.to_h,
|
333
|
+
expected: expected_stanza(conditions),
|
334
|
+
return_values: "ALL_NEW"
|
335
|
+
)
|
336
|
+
result_item_to_hash(result[:attributes])
|
337
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
338
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# List all tables on DynamoDB.
|
343
|
+
#
|
344
|
+
# @since 1.0.0
|
345
|
+
#
|
346
|
+
# @todo Provide limit support http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
347
|
+
def list_tables
|
348
|
+
client.list_tables[:table_names]
|
349
|
+
end
|
350
|
+
|
351
|
+
# Persists an item on DynamoDB.
|
352
|
+
#
|
353
|
+
# @param [String] table_name the name of the table
|
354
|
+
# @param [Object] object a hash or Dynamoid object to persist
|
355
|
+
#
|
356
|
+
# @since 1.0.0
|
357
|
+
#
|
358
|
+
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
359
|
+
def put_item(table_name, object, options = {})
|
360
|
+
item = {}
|
361
|
+
options ||= {}
|
362
|
+
|
363
|
+
object.each do |k, v|
|
364
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
365
|
+
item[k.to_s] = v
|
366
|
+
end
|
367
|
+
|
368
|
+
begin
|
369
|
+
client.put_item(
|
370
|
+
{
|
371
|
+
table_name: table_name,
|
372
|
+
item: item,
|
373
|
+
expected: expected_stanza(options)
|
374
|
+
}.merge!(options)
|
375
|
+
)
|
376
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
377
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
382
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
383
|
+
# one range key to the hash.
|
384
|
+
#
|
385
|
+
# @param [String] table_name the name of the table
|
386
|
+
# @param [Hash] opts the options to query the table with
|
387
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
388
|
+
# @option opts [Number, Number] :range_between find the range key within this range
|
389
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
390
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
391
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
392
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
393
|
+
#
|
394
|
+
# @return [Enumerable] matching items
|
395
|
+
#
|
396
|
+
# @since 1.0.0
|
397
|
+
#
|
398
|
+
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
399
|
+
def query(table_name, opts = {})
|
400
|
+
table = describe_table(table_name)
|
401
|
+
hk = (opts[:hash_key].present? ? opts[:hash_key] : table.hash_key).to_s
|
402
|
+
rng = (opts[:range_key].present? ? opts[:range_key] : table.range_key).to_s
|
403
|
+
q = opts.slice(
|
404
|
+
:consistent_read,
|
405
|
+
:scan_index_forward,
|
406
|
+
:limit,
|
407
|
+
:select,
|
408
|
+
:index_name
|
409
|
+
)
|
410
|
+
|
411
|
+
opts.delete(:consistent_read)
|
412
|
+
opts.delete(:scan_index_forward)
|
413
|
+
opts.delete(:limit)
|
414
|
+
opts.delete(:select)
|
415
|
+
opts.delete(:index_name)
|
416
|
+
|
417
|
+
opts.delete(:next_token).tap do |token|
|
418
|
+
break unless token
|
419
|
+
q[:exclusive_start_key] = {
|
420
|
+
hk => token[:hash_key_element],
|
421
|
+
rng => token[:range_key_element]
|
422
|
+
}
|
423
|
+
end
|
424
|
+
|
425
|
+
key_conditions = {
|
426
|
+
hk => {
|
427
|
+
# TODO: Provide option for other operators like NE, IN, LE, etc
|
428
|
+
comparison_operator: EQ,
|
429
|
+
attribute_value_list: [
|
430
|
+
opts.delete(:hash_value).freeze
|
431
|
+
]
|
432
|
+
}
|
433
|
+
}
|
434
|
+
|
435
|
+
opts.each_pair do |k, v|
|
436
|
+
# TODO: ATM, only few comparison operators are supported, provide support for all operators
|
437
|
+
next unless(op = RANGE_MAP[k])
|
438
|
+
key_conditions[rng] = {
|
439
|
+
comparison_operator: op,
|
440
|
+
attribute_value_list: [
|
441
|
+
opts.delete(k).freeze
|
442
|
+
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
443
|
+
}
|
444
|
+
end
|
445
|
+
|
446
|
+
q[:table_name] = table_name
|
447
|
+
q[:key_conditions] = key_conditions
|
448
|
+
|
449
|
+
Enumerator.new { |y|
|
450
|
+
loop do
|
451
|
+
results = client.query(q)
|
452
|
+
results.items.each { |row| y << result_item_to_hash(row) }
|
453
|
+
|
454
|
+
if(lk = results.last_evaluated_key)
|
455
|
+
q[:exclusive_start_key] = lk
|
456
|
+
else
|
457
|
+
break
|
458
|
+
end
|
459
|
+
end
|
460
|
+
}
|
461
|
+
end
|
462
|
+
|
463
|
+
# Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
|
464
|
+
# the DynamoDB servers.
|
465
|
+
#
|
466
|
+
# @param [String] table_name the name of the table
|
467
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
468
|
+
#
|
469
|
+
# @return [Enumerable] matching items
|
470
|
+
#
|
471
|
+
# @since 1.0.0
|
472
|
+
#
|
473
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
474
|
+
def scan(table_name, scan_hash, select_opts = {})
|
475
|
+
limit = select_opts.delete(:limit)
|
476
|
+
batch = select_opts.delete(:batch_size)
|
477
|
+
|
478
|
+
request = { table_name: table_name }
|
479
|
+
request[:limit] = batch || limit if batch || limit
|
480
|
+
request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp|
|
481
|
+
memo[kvp[0].to_s] = {
|
482
|
+
attribute_value_list: [kvp[1]],
|
483
|
+
# TODO: Provide support for all comparison operators
|
484
|
+
comparison_operator: EQ
|
485
|
+
}
|
486
|
+
memo
|
487
|
+
end if scan_hash.present?
|
488
|
+
|
489
|
+
Enumerator.new do |y|
|
490
|
+
# Batch loop, pulls multiple requests until done using the start_key
|
491
|
+
loop do
|
492
|
+
results = client.scan(request)
|
493
|
+
|
494
|
+
results.data[:items].each { |row| y << result_item_to_hash(row) }
|
495
|
+
|
496
|
+
if((lk = results[:last_evaluated_key]) && batch)
|
497
|
+
request[:exclusive_start_key] = lk
|
498
|
+
else
|
499
|
+
break
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
#
|
506
|
+
# Truncates all records in the given table
|
507
|
+
#
|
508
|
+
# @param [String] table_name the name of the table
|
509
|
+
#
|
510
|
+
# @since 1.0.0
|
511
|
+
def truncate(table_name)
|
512
|
+
table = describe_table(table_name)
|
513
|
+
hk = table.hash_key
|
514
|
+
rk = table.range_key
|
515
|
+
|
516
|
+
scan(table_name, {}, {}).each do |attributes|
|
517
|
+
opts = {}
|
518
|
+
opts[:range_key] = attributes[rk.to_sym] if rk
|
519
|
+
delete_item(table_name, attributes[hk], opts)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def count(table_name)
|
524
|
+
describe_table(table_name, true).item_count
|
525
|
+
end
|
526
|
+
|
527
|
+
protected
|
528
|
+
|
529
|
+
def check_table_status?(counter, resp, expect_status)
|
530
|
+
status = PARSE_TABLE_STATUS.call(resp)
|
531
|
+
again = counter < Dynamoid::Config.sync_retry_max_times &&
|
532
|
+
status == TABLE_STATUSES[expect_status]
|
533
|
+
{again: again, status: status, counter: counter}
|
534
|
+
end
|
535
|
+
|
536
|
+
def until_past_table_status(table_name, status = :creating)
|
537
|
+
counter = 0
|
538
|
+
resp = nil
|
539
|
+
begin
|
540
|
+
check = {again: true}
|
541
|
+
while check[:again]
|
542
|
+
sleep Dynamoid::Config.sync_retry_wait_seconds
|
543
|
+
resp = client.describe_table({ table_name: table_name })
|
544
|
+
check = check_table_status?(counter, resp, status)
|
545
|
+
Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})"
|
546
|
+
counter += 1
|
547
|
+
end
|
548
|
+
# If you issue a DescribeTable request immediately after a CreateTable
|
549
|
+
# request, DynamoDB might return a ResourceNotFoundException.
|
550
|
+
# This is because DescribeTable uses an eventually consistent query,
|
551
|
+
# and the metadata for your table might not be available at that moment.
|
552
|
+
# Wait for a few seconds, and then try the DescribeTable request again.
|
553
|
+
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
|
554
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
|
555
|
+
case status
|
556
|
+
when :creating then
|
557
|
+
if counter >= Dynamoid::Config.sync_retry_max_times
|
558
|
+
Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})"
|
559
|
+
retry # start over at first line of begin, does not reset counter
|
560
|
+
else
|
561
|
+
Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})"
|
562
|
+
raise e
|
563
|
+
end
|
564
|
+
else
|
565
|
+
# When deleting a table, "not found" is the goal.
|
566
|
+
Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})"
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
#Converts from symbol to the API string for the given data type
|
572
|
+
# E.g. :number -> 'N'
|
573
|
+
def api_type(type)
|
574
|
+
case(type)
|
575
|
+
when :string then STRING_TYPE
|
576
|
+
when :number then NUM_TYPE
|
577
|
+
when :binary then BINARY_TYPE
|
578
|
+
else raise "Unknown type: #{type}"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
#
|
583
|
+
# The key hash passed on get_item, put_item, delete_item, update_item, etc
|
584
|
+
#
|
585
|
+
def key_stanza(table, hash_key, range_key = nil)
|
586
|
+
key = { table.hash_key.to_s => hash_key }
|
587
|
+
key[table.range_key.to_s] = range_key if range_key
|
588
|
+
key
|
589
|
+
end
|
590
|
+
|
591
|
+
#
|
592
|
+
# @param [Hash] conditions Conditions to enforce on operation (e.g. { :if => { :count => 5 }, :unless_exists => ['id']})
|
593
|
+
# @return an Expected stanza for the given conditions hash
|
594
|
+
#
|
595
|
+
def expected_stanza(conditions = nil)
|
596
|
+
expected = Hash.new { |h,k| h[k] = {} }
|
597
|
+
return expected unless conditions
|
598
|
+
|
599
|
+
conditions.delete(:unless_exists).try(:each) do |col|
|
600
|
+
expected[col.to_s][:exists] = false
|
601
|
+
end
|
602
|
+
conditions.delete(:if).try(:each) do |col,val|
|
603
|
+
expected[col.to_s][:value] = val
|
604
|
+
end
|
605
|
+
|
606
|
+
expected
|
607
|
+
end
|
608
|
+
|
609
|
+
#
|
610
|
+
# New, semi-arbitrary API to get data on the table
|
611
|
+
#
|
612
|
+
def describe_table(table_name, reload = false)
|
613
|
+
(!reload && table_cache[table_name]) || begin
|
614
|
+
table_cache[table_name] = Table.new(client.describe_table(table_name: table_name).data)
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
#
|
619
|
+
# Converts a hash returned by get_item, scan, etc. into a key-value hash
|
620
|
+
#
|
621
|
+
def result_item_to_hash(item)
|
622
|
+
{}.tap do |r|
|
623
|
+
item.each { |k,v| r[k.to_sym] = v }
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
# Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
|
628
|
+
# This resulting hash is of the form:
|
629
|
+
#
|
630
|
+
# {
|
631
|
+
# index_name: String
|
632
|
+
# keys: {
|
633
|
+
# hash_key: aws_key_schema (hash)
|
634
|
+
# range_key: aws_key_schema (hash)
|
635
|
+
# }
|
636
|
+
# projection: {
|
637
|
+
# projection_type: (ALL, KEYS_ONLY, INCLUDE) String
|
638
|
+
# non_key_attributes: (optional) Array
|
639
|
+
# }
|
640
|
+
# provisioned_throughput: {
|
641
|
+
# read_capacity_units: Integer
|
642
|
+
# write_capacity_units: Integer
|
643
|
+
# }
|
644
|
+
# }
|
645
|
+
#
|
646
|
+
# @param [Dynamoid::Indexes::Index] index the index.
|
647
|
+
# @return [Hash] hash representing an AWS Index definition.
|
648
|
+
def index_to_aws_hash(index)
|
649
|
+
key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)
|
650
|
+
|
651
|
+
hash = {
|
652
|
+
:index_name => index.name,
|
653
|
+
:key_schema => key_schema,
|
654
|
+
:projection => {
|
655
|
+
:projection_type => index.projection_type.to_s.upcase
|
656
|
+
}
|
657
|
+
}
|
658
|
+
|
659
|
+
# If the projection type is include, specify the non key attributes
|
660
|
+
if index.projection_type == :include
|
661
|
+
hash[:projection][:non_key_attributes] = index.projected_attributes
|
662
|
+
end
|
663
|
+
|
664
|
+
# Only global secondary indexes have a separate throughput.
|
665
|
+
if index.type == :global_secondary
|
666
|
+
hash[:provisioned_throughput] = {
|
667
|
+
:read_capacity_units => index.read_capacity,
|
668
|
+
:write_capacity_units => index.write_capacity
|
669
|
+
}
|
670
|
+
end
|
671
|
+
hash
|
672
|
+
end
|
673
|
+
|
674
|
+
# Converts hash_key_schema and range_key_schema to aws_key_schema
|
675
|
+
# @param [Hash] hash_key_schema eg: {:id => :string}
|
676
|
+
# @param [Hash] range_key_schema eg: {:created_at => :number}
|
677
|
+
# @return [Array]
|
678
|
+
def aws_key_schema(hash_key_schema, range_key_schema)
|
679
|
+
schema = [{
|
680
|
+
attribute_name: hash_key_schema.keys.first.to_s,
|
681
|
+
key_type: HASH_KEY
|
682
|
+
}]
|
683
|
+
|
684
|
+
if range_key_schema.present?
|
685
|
+
schema << {
|
686
|
+
attribute_name: range_key_schema.keys.first.to_s,
|
687
|
+
key_type: RANGE_KEY
|
688
|
+
}
|
689
|
+
end
|
690
|
+
schema
|
691
|
+
end
|
692
|
+
|
693
|
+
# Builds aws attributes definitions based off of primary hash/range and
|
694
|
+
# secondary indexes
|
695
|
+
#
|
696
|
+
# @param key_data
|
697
|
+
# @option key_data [Hash] hash_key_schema - eg: {:id => :string}
|
698
|
+
# @option key_data [Hash] range_key_schema - eg: {:created_at => :number}
|
699
|
+
# @param [Hash] secondary_indexes
|
700
|
+
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
|
701
|
+
# @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
|
702
|
+
def build_all_attribute_definitions(key_schema, secondary_indexes = {})
|
703
|
+
ls_indexes = secondary_indexes[:local_secondary_indexes]
|
704
|
+
gs_indexes = secondary_indexes[:global_secondary_indexes]
|
705
|
+
|
706
|
+
attribute_definitions = []
|
707
|
+
|
708
|
+
attribute_definitions << build_attribute_definitions(
|
709
|
+
key_schema[:hash_key_schema],
|
710
|
+
key_schema[:range_key_schema]
|
711
|
+
)
|
712
|
+
|
713
|
+
if ls_indexes.present?
|
714
|
+
ls_indexes.map do |index|
|
715
|
+
attribute_definitions << build_attribute_definitions(
|
716
|
+
index.hash_key_schema,
|
717
|
+
index.range_key_schema
|
718
|
+
)
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
if gs_indexes.present?
|
723
|
+
gs_indexes.map do |index|
|
724
|
+
attribute_definitions << build_attribute_definitions(
|
725
|
+
index.hash_key_schema,
|
726
|
+
index.range_key_schema
|
727
|
+
)
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
attribute_definitions.flatten!
|
732
|
+
# uniq these definitions because range keys might be common between
|
733
|
+
# primary and secondary indexes
|
734
|
+
attribute_definitions.uniq!
|
735
|
+
attribute_definitions
|
736
|
+
end
|
737
|
+
|
738
|
+
|
739
|
+
# Builds an attribute definitions based on hash key and range key
|
740
|
+
# @params [Hash] hash_key_schema - eg: {:id => :string}
|
741
|
+
# @params [Hash] range_key_schema - eg: {:created_at => :datetime}
|
742
|
+
# @return [Array]
|
743
|
+
def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
|
744
|
+
attrs = []
|
745
|
+
|
746
|
+
attrs << attribute_definition_element(
|
747
|
+
hash_key_schema.keys.first,
|
748
|
+
hash_key_schema.values.first
|
749
|
+
)
|
750
|
+
|
751
|
+
if range_key_schema.present?
|
752
|
+
attrs << attribute_definition_element(
|
753
|
+
range_key_schema.keys.first,
|
754
|
+
range_key_schema.values.first
|
755
|
+
)
|
756
|
+
end
|
757
|
+
|
758
|
+
attrs
|
759
|
+
end
|
760
|
+
|
761
|
+
# Builds an aws attribute definition based on name and dynamoid type
|
762
|
+
# @params [Symbol] name - eg: :id
|
763
|
+
# @params [Symbol] dynamoid_type - eg: :string
|
764
|
+
# @return [Hash]
|
765
|
+
def attribute_definition_element(name, dynamoid_type)
|
766
|
+
aws_type = api_type(dynamoid_type)
|
767
|
+
|
768
|
+
{
|
769
|
+
:attribute_name => name.to_s,
|
770
|
+
:attribute_type => aws_type
|
771
|
+
}
|
772
|
+
end
|
773
|
+
|
774
|
+
#
|
775
|
+
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
776
|
+
# provides methods for coercing values to the proper types based on the table's schema data
|
777
|
+
#
|
778
|
+
class Table
|
779
|
+
attr_reader :schema
|
780
|
+
|
781
|
+
#
|
782
|
+
# @param [Hash] schema Data returns from a "DescribeTable" call
|
783
|
+
#
|
784
|
+
def initialize(schema)
|
785
|
+
@schema = schema[:table]
|
786
|
+
end
|
787
|
+
|
788
|
+
def range_key
|
789
|
+
@range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
|
790
|
+
end
|
791
|
+
|
792
|
+
def range_type
|
793
|
+
range_type ||= schema[:attribute_definitions].find { |d|
|
794
|
+
d[:attribute_name] == range_key
|
795
|
+
}.try(:fetch,:attribute_type, nil)
|
796
|
+
end
|
797
|
+
|
798
|
+
def hash_key
|
799
|
+
@hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
|
800
|
+
end
|
801
|
+
|
802
|
+
#
|
803
|
+
# Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
|
804
|
+
# nil otherwise
|
805
|
+
#
|
806
|
+
def col_type(col)
|
807
|
+
col = col.to_s
|
808
|
+
col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
|
809
|
+
col_def && col_def[:attribute_type]
|
810
|
+
end
|
811
|
+
|
812
|
+
def item_count
|
813
|
+
schema[:item_count]
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
#
|
818
|
+
# Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
|
819
|
+
#
|
820
|
+
class ItemUpdater
|
821
|
+
attr_reader :table, :key, :range_key
|
822
|
+
|
823
|
+
def initialize(table, key, range_key = nil)
|
824
|
+
@table = table; @key = key, @range_key = range_key
|
825
|
+
@additions = {}
|
826
|
+
@deletions = {}
|
827
|
+
@updates = {}
|
828
|
+
end
|
829
|
+
|
830
|
+
#
|
831
|
+
# Adds the given values to the values already stored in the corresponding columns.
|
832
|
+
# The column must contain a Set or a number.
|
833
|
+
#
|
834
|
+
# @param [Hash] vals keys of the hash are the columns to update, vals are the values to
|
835
|
+
# add. values must be a Set, Array, or Numeric
|
836
|
+
#
|
837
|
+
def add(values)
|
838
|
+
@additions.merge!(values)
|
839
|
+
end
|
840
|
+
|
841
|
+
#
|
842
|
+
# Removes values from the sets of the given columns
|
843
|
+
#
|
844
|
+
# @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
|
845
|
+
# to remove
|
846
|
+
#
|
847
|
+
def delete(values)
|
848
|
+
@deletions.merge!(values)
|
849
|
+
end
|
850
|
+
|
851
|
+
#
|
852
|
+
# Replaces the values of one or more attributes
|
853
|
+
#
|
854
|
+
def set(values)
|
855
|
+
@updates.merge!(values)
|
856
|
+
end
|
857
|
+
|
858
|
+
#
|
859
|
+
# Returns an AttributeUpdates hash suitable for passing to the V2 Client API
|
860
|
+
#
|
861
|
+
def to_h
|
862
|
+
ret = {}
|
863
|
+
|
864
|
+
@additions.each do |k,v|
|
865
|
+
ret[k.to_s] = {
|
866
|
+
action: ADD,
|
867
|
+
value: v
|
868
|
+
}
|
869
|
+
end
|
870
|
+
@deletions.each do |k,v|
|
871
|
+
ret[k.to_s] = {
|
872
|
+
action: DELETE,
|
873
|
+
value: v
|
874
|
+
}
|
875
|
+
end
|
876
|
+
@updates.each do |k,v|
|
877
|
+
ret[k.to_s] = {
|
878
|
+
action: PUT,
|
879
|
+
value: v
|
880
|
+
}
|
881
|
+
end
|
882
|
+
|
883
|
+
ret
|
884
|
+
end
|
885
|
+
|
886
|
+
ADD = "ADD".freeze
|
887
|
+
DELETE = "DELETE".freeze
|
888
|
+
PUT = "PUT".freeze
|
889
|
+
end
|
890
|
+
end
|
891
|
+
end
|
892
|
+
end
|