dynomite 1.2.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +18 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +45 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +178 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +12 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- data/lib/dynomite/migration/executor.rb +0 -38
@@ -0,0 +1,21 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
class ComparisionExpression
|
3
|
+
def initialize(where_group, comparisions)
|
4
|
+
@where_group, @comparisions = where_group, comparisions
|
5
|
+
end
|
6
|
+
|
7
|
+
def or?
|
8
|
+
@where_group.or?
|
9
|
+
end
|
10
|
+
|
11
|
+
def build
|
12
|
+
# join @comparisions with AND if there are more than one
|
13
|
+
expression = []
|
14
|
+
expression << 'NOT' if @where_group.not?
|
15
|
+
expression << '(' if @comparisions.size > 1
|
16
|
+
expression << @comparisions.join(' AND ') # always AND within a group
|
17
|
+
expression << ')' if @comparisions.size > 1
|
18
|
+
expression.join(' ')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
module ComparisionMap
|
3
|
+
COMPARISION_MAP = {
|
4
|
+
'eq' => '=',
|
5
|
+
'gt' => '>',
|
6
|
+
'gte' => '>=',
|
7
|
+
'lt' => '<',
|
8
|
+
'lte' => '<=',
|
9
|
+
}
|
10
|
+
|
11
|
+
def comparision_for(operator)
|
12
|
+
COMPARISION_MAP[operator] || operator
|
13
|
+
end
|
14
|
+
|
15
|
+
def comparision_operators
|
16
|
+
COMPARISION_MAP.keys + COMPARISION_MAP.values
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
module Delete
|
3
|
+
def delete_all
|
4
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
|
5
|
+
# A single call to BatchWriteItem can transmit up to 16MB of data over the network,
|
6
|
+
# consisting of up to 25 item put or delete operations.
|
7
|
+
batch_limit = 25 # max batch size for batch_write_item
|
8
|
+
each_page.each do |page|
|
9
|
+
page.each_slice(batch_limit) do |slice|
|
10
|
+
primary_keys = slice.map(&:primary_key)
|
11
|
+
delete_requests = primary_keys.map do |primary_key|
|
12
|
+
{
|
13
|
+
delete_request: {
|
14
|
+
key: primary_key,
|
15
|
+
},
|
16
|
+
}
|
17
|
+
end
|
18
|
+
request_items = { @source.table_name => delete_requests }
|
19
|
+
client.batch_write_item(request_items: request_items)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def destroy_all
|
25
|
+
each(&:destroy)
|
26
|
+
end
|
27
|
+
|
28
|
+
# require args
|
29
|
+
def delete_by(args)
|
30
|
+
where(args).delete_all
|
31
|
+
end
|
32
|
+
|
33
|
+
# require args
|
34
|
+
def destroy_by(args)
|
35
|
+
where(args).destroy_all
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
module Ids
|
3
|
+
def pluck(*names)
|
4
|
+
project(*names)
|
5
|
+
super # provided by Ruby Enumerable
|
6
|
+
end
|
7
|
+
|
8
|
+
def ids
|
9
|
+
project(:id).each.map(&:id).to_a
|
10
|
+
end
|
11
|
+
|
12
|
+
def exists?(args={})
|
13
|
+
!!limit(1).first
|
14
|
+
end
|
15
|
+
|
16
|
+
# Surprisingly, Enumberable does not provide empty?
|
17
|
+
def empty?
|
18
|
+
!exists?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
module Math
|
3
|
+
def average(field)
|
4
|
+
map(&field).sum.to_f / count
|
5
|
+
end
|
6
|
+
|
7
|
+
def min(field)
|
8
|
+
map(&field).min
|
9
|
+
end
|
10
|
+
|
11
|
+
def max(field)
|
12
|
+
map(&field).max
|
13
|
+
end
|
14
|
+
|
15
|
+
def sum(field)
|
16
|
+
map(&field).sum
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
class WhereField
|
3
|
+
attr_reader :full_field, :value, :index
|
4
|
+
def initialize(full_field, value, index)
|
5
|
+
@full_field, @value, @index = full_field.to_s, value, index
|
6
|
+
end
|
7
|
+
|
8
|
+
def field
|
9
|
+
@full_field.split('.').first
|
10
|
+
end
|
11
|
+
|
12
|
+
def operator
|
13
|
+
if raw_operator
|
14
|
+
not? ? raw_operator[4..-1] : raw_operator
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def not?
|
19
|
+
raw_operator.match(/^not_/) if raw_operator
|
20
|
+
end
|
21
|
+
|
22
|
+
def raw_operator
|
23
|
+
_, operator = @full_field.split('.')
|
24
|
+
operator.downcase if operator
|
25
|
+
end
|
26
|
+
|
27
|
+
# Example: price_1, price_2
|
28
|
+
def reference
|
29
|
+
"#{field}_#{@index}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Dynomite::Item::Query::Relation
|
2
|
+
class WhereGroup
|
3
|
+
delegate :keys, :size, to: :hash
|
4
|
+
include ComparisionMap
|
5
|
+
|
6
|
+
attr_accessor :hash, :meta
|
7
|
+
def initialize(relation, hash={}, meta={})
|
8
|
+
@relation = relation
|
9
|
+
@hash = hash
|
10
|
+
@meta = meta
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#<#{self.class.name} @hash=#{@hash} @meta=#{@meta}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
def each
|
18
|
+
@hash.each do |full_field, value|
|
19
|
+
where_field = WhereField.new(full_field, value, @relation.index)
|
20
|
+
yield where_field
|
21
|
+
@relation.index += 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def fields
|
26
|
+
@hash.map do |full_field, value|
|
27
|
+
full_field.to_s.split('.').first
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def not?
|
32
|
+
@meta[:not]
|
33
|
+
end
|
34
|
+
|
35
|
+
def or?
|
36
|
+
@meta[:or]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Method helps remove duplication and DRY up building of compare expression.
|
40
|
+
# It a bit confusing but unsure how to make it clearer.
|
41
|
+
def build_compare_expression_if
|
42
|
+
comparisions = []
|
43
|
+
each do |where_field|
|
44
|
+
field = where_field.field
|
45
|
+
next unless yield(field) # only build if condition is true
|
46
|
+
comparisions << build_compare(where_field)
|
47
|
+
end
|
48
|
+
unless comparisions.empty?
|
49
|
+
ComparisionExpression.new(self, comparisions) # to pass in where_group for or? and not?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_compare(where_field)
|
54
|
+
reference = where_field.reference
|
55
|
+
operator = where_field.operator
|
56
|
+
expression = case operator
|
57
|
+
when 'in'
|
58
|
+
# ProductStatus in (:avail, :back, :disc)
|
59
|
+
array = Array(where_field.value)
|
60
|
+
list = array.map.with_index { |v, i| ":#{reference}_#{i}" }.join(', ') # values
|
61
|
+
"##{reference} in (#{list})"
|
62
|
+
when 'between'
|
63
|
+
# sortKeyName between :sortkeyval1 AND :sortkeyval2
|
64
|
+
"##{reference} between :#{reference}_0 AND :#{reference}_1"
|
65
|
+
when 'begins_with'
|
66
|
+
# begins_with ( sortKeyName, :sortkeyval )
|
67
|
+
"begins_with(##{reference}, :#{reference})"
|
68
|
+
when *comparision_operators # eq, gt, gte, lt, lte, =, >, >=, <, <=
|
69
|
+
comparision = comparision_for(operator)
|
70
|
+
"##{reference} #{comparision} :#{reference}"
|
71
|
+
else
|
72
|
+
"##{reference} = :#{reference}"
|
73
|
+
end
|
74
|
+
expression = "NOT (#{expression})" if where_field.not?
|
75
|
+
expression
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Dynomite::Item::Query
|
2
|
+
# Builds up the query with methods like where and eventually executes Query or Scan.
|
3
|
+
class Relation
|
4
|
+
extend Memoist
|
5
|
+
include Dynomite::Client
|
6
|
+
include Enumerable
|
7
|
+
include Chain
|
8
|
+
include Math
|
9
|
+
include Ids
|
10
|
+
include Delete
|
11
|
+
|
12
|
+
attr_accessor :index, :query, :source
|
13
|
+
def initialize(source)
|
14
|
+
@source = source # source is the model class. IE: Post User etc
|
15
|
+
@query = {
|
16
|
+
where: [],
|
17
|
+
attribute_exists: [],
|
18
|
+
attribute_not_exists: [],
|
19
|
+
attribute_type: [],
|
20
|
+
begins_with: [],
|
21
|
+
contains: [],
|
22
|
+
size_fn: [],
|
23
|
+
}
|
24
|
+
@index = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
# Enumerable provides .to_a, add force so it's more like lazy Enumerable.
|
28
|
+
# Also, not using: `alias load to_a` because load is Enumerable private method
|
29
|
+
# Instead user should use force or eager. Docs:
|
30
|
+
# https://docs.ruby-lang.org/en/master/Enumerator/Lazy.html
|
31
|
+
# https://www.rubydoc.info/stdlib/core/Enumerator/Lazy
|
32
|
+
alias size count
|
33
|
+
alias force to_a
|
34
|
+
|
35
|
+
def each(&block)
|
36
|
+
items.each(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def items
|
40
|
+
pages.flat_map { |i| i } # flat_map flattens the Lazy Enumerator since yielding pages
|
41
|
+
end
|
42
|
+
private :items
|
43
|
+
|
44
|
+
def pages
|
45
|
+
raw_pages.map do |raw_page|
|
46
|
+
raw_page.items.map.map(&method(:build_item))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
alias each_page pages
|
50
|
+
|
51
|
+
def raw_pages
|
52
|
+
params = to_params
|
53
|
+
# total_limit is the total limit across all pages
|
54
|
+
# For the AWS API call itself use the default limit and allow AWS to scan 1MB for page
|
55
|
+
total_limit = params.delete(:limit)
|
56
|
+
total_count = 0
|
57
|
+
Enumerator.new do |y|
|
58
|
+
last_evaluated_key = :start
|
59
|
+
while last_evaluated_key
|
60
|
+
if last_evaluated_key && last_evaluated_key != :start
|
61
|
+
params[:exclusive_start_key] = last_evaluated_key
|
62
|
+
end
|
63
|
+
|
64
|
+
meth = params[:key_condition_expression] ? :query : :scan
|
65
|
+
log_debug(params)
|
66
|
+
raw_warn_scan if meth == :scan
|
67
|
+
response = client.send(meth, params) # scan or query
|
68
|
+
records = response.items.map { |i| build_item(i, run_callback: false) }
|
69
|
+
y.yield(response, records)
|
70
|
+
|
71
|
+
# Track total_count across pages. If limit is set, then stop when we reach it.
|
72
|
+
# Since limit can be greater than each API response paged size.
|
73
|
+
total_count += response.items.size
|
74
|
+
break if total_limit && total_count >= total_limit
|
75
|
+
|
76
|
+
last_evaluated_key = response.last_evaluated_key
|
77
|
+
end
|
78
|
+
end.lazy
|
79
|
+
end
|
80
|
+
alias each_raw_page raw_pages
|
81
|
+
|
82
|
+
def build_item(i, run_callback: true)
|
83
|
+
item = @source.new(i) # IE: Post.new(i)
|
84
|
+
item.new_record = false
|
85
|
+
item.run_callbacks :find if run_callback
|
86
|
+
item
|
87
|
+
end
|
88
|
+
|
89
|
+
# Enumerable provides .first but does not provide .last
|
90
|
+
# Note, cannot use:
|
91
|
+
# scan_index_forward(false).limit(1).first
|
92
|
+
# Since that will not work for queries that do not have a sort key.
|
93
|
+
# Users need to use query directly if they want to find the last item more efficiently.
|
94
|
+
def last
|
95
|
+
warn_scan <<~EOL
|
96
|
+
WARN: Dynomite::Item::Query::Relation#last is slow.
|
97
|
+
Consider using query directly if you have a primary key that as a sort key.
|
98
|
+
EOL
|
99
|
+
to_a.last # force load of lazy enumerator. slow
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_params
|
103
|
+
Params.new(self, @source).to_h
|
104
|
+
end
|
105
|
+
|
106
|
+
# Allows all to chain itself. This allows.
|
107
|
+
#
|
108
|
+
# Post.where(category: "Electronics").all
|
109
|
+
# Post.limit(1).all
|
110
|
+
# Post.all.all # also works, side effect
|
111
|
+
#
|
112
|
+
def all
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def raw_warn_scan
|
117
|
+
warn_on_scan = @warn_on_scan.nil? ? Dynomite.config.warn_on_scan : @warn_on_scan
|
118
|
+
return unless warn_on_scan
|
119
|
+
warn_scan <<~EOL
|
120
|
+
WARN: Scanning detected. It's recommended to not use scan. It can be slow.
|
121
|
+
Scanning table: #{@source.table_name}
|
122
|
+
Try creating a LSI or GSI index so dynomite can use query instead.
|
123
|
+
Docs: https://rubyonjets.com/docs/database/dynamodb/indexing/
|
124
|
+
EOL
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Dynomite::Item::Read
|
2
|
+
module Find
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Note: options are merged into get_item params.
|
6
|
+
def find(id, options={})
|
7
|
+
self.class.find(id, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def find_by(attrs, options={})
|
12
|
+
# Note: ActionController::Parameters does not have .any? Unsure if should use .to_h on it
|
13
|
+
# A blank attrs will break find_by since DynamoDB doesnt support blank attribute values
|
14
|
+
# Guard blank attrs like and return new return so validation error surfaces in a standard CRUD scaffold
|
15
|
+
attrs = attrs.to_h
|
16
|
+
return nil if attrs.any? { |k,v| v.blank? }
|
17
|
+
|
18
|
+
primary_key_attrs = attrs.stringify_keys.slice(partition_key_field, sort_key_field).symbolize_keys
|
19
|
+
if primary_key_attrs.size == primary_key_fields.size
|
20
|
+
find(primary_key_attrs, options.merge(raise_error: false))
|
21
|
+
else
|
22
|
+
where(attrs).first # possible scan
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Examples with out the args are received.
|
27
|
+
#
|
28
|
+
# Post.find("ae3ae")
|
29
|
+
# args ["ae3ae"]
|
30
|
+
# Post.find(id: "ae3ae")
|
31
|
+
# args [{:id=>"ae3ae"}]
|
32
|
+
# Post.find("ae3ae", consistent_read: true)
|
33
|
+
# args ["ae3ae", {:consistent_read=>true}]
|
34
|
+
# Post.find({id: "ae3ae"}, consistent_read: true)
|
35
|
+
# args [{:id=>"ae3ae"}, {:consistent_read=>true}]
|
36
|
+
# Product.find(category: "Electronics", product_id: 101)
|
37
|
+
# args [{:category=>"Electronics", :product_id=>101}]
|
38
|
+
# Product.find({category: "Electronics", product_id: 101}, consistent_read: true)
|
39
|
+
# args [{:category=>"Electronics", :product_id=>101}, {:consistent_read=>true}]
|
40
|
+
#
|
41
|
+
# Product.find(id1,id2,id3)
|
42
|
+
# args [id1,id2,id3]
|
43
|
+
# Product.find([id1,id2,id3])
|
44
|
+
# args [[id1,id2,id3]]
|
45
|
+
# Product.find([id1,id2,id3], consistent_read: true)
|
46
|
+
# args [[id1,id2,id3], {:consistent_read=>true}]
|
47
|
+
#
|
48
|
+
# Note: options are merged into get_item params.
|
49
|
+
def find(*args)
|
50
|
+
options = {}
|
51
|
+
key_schema = if args.size == 1
|
52
|
+
# args is an array of one element:
|
53
|
+
# ["f74de472"]
|
54
|
+
# [{:id=>"f74de472"}]
|
55
|
+
# [{:category=>"Electronics", :product_id=>101}]
|
56
|
+
# [[id1, id2, id3]]
|
57
|
+
get_key_schema_from_one_arg(args.first)
|
58
|
+
else
|
59
|
+
# ["f74de472", {:consistent_read=>true}]
|
60
|
+
# [{:id=>"f74de472"}, {:consistent_read=>true}]
|
61
|
+
# [{:category=>"Electronics", :product_id=>101}, {:consistent_read=>true}]
|
62
|
+
# [[id1, id2, id3], {:consistent_read=>true}] # HERE
|
63
|
+
options = args.extract_options!
|
64
|
+
if args.size >= 2 # still at least 2 after extracting options
|
65
|
+
get_key_schema_from_one_arg(args) # [id1, id2, id3]
|
66
|
+
else
|
67
|
+
get_key_schema_from_one_arg(args.first)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
raise_error = options.delete(:raise_error) # clean for params
|
72
|
+
raise_error = raise_error.nil? ? true : raise_error
|
73
|
+
|
74
|
+
# Early return if ids the provided arg. find(ids)
|
75
|
+
# List of ids: IE: [id1, id2, id3]
|
76
|
+
if key_schema.is_a?(Array)
|
77
|
+
keys = key_schema # [{"id"=>"post-1"}, {"id"=>"post-2"}]
|
78
|
+
ids = keys.map(&:values).flatten # ["post-1", "post-2"]
|
79
|
+
items = batch_get_items(keys, options)
|
80
|
+
result = items
|
81
|
+
result = nil if items.size != keys.size # some missing items
|
82
|
+
|
83
|
+
if result.nil?
|
84
|
+
if raise_error
|
85
|
+
looking, found = ids, items.map(&:id)
|
86
|
+
missing = ids - found
|
87
|
+
message = "Couldn't find all #{self.name.pluralize} with '#{partition_key_field}': (#{looking.join(', ')}) (found #{found.size} results, but was looking for #{looking.size})"
|
88
|
+
message << ". Missing: #{missing.sort.join(', ')}" if found.size > 0
|
89
|
+
raise Dynomite::Error::RecordNotFound.new(message)
|
90
|
+
else
|
91
|
+
return items # return early
|
92
|
+
end
|
93
|
+
else
|
94
|
+
return items
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
params = {
|
99
|
+
table_name: table_name,
|
100
|
+
key: key_schema,
|
101
|
+
}
|
102
|
+
params.merge!(options)
|
103
|
+
|
104
|
+
log_debug(params)
|
105
|
+
attrs = client.get_item(params).item # unwraps the item's attrs
|
106
|
+
|
107
|
+
# Mimic ActiveRecord::RecordNotFound behavior
|
108
|
+
raise Dynomite::Error::RecordNotFound if attrs.nil? && raise_error != false
|
109
|
+
|
110
|
+
build_item(attrs)
|
111
|
+
end
|
112
|
+
|
113
|
+
def batch_get_items(*args)
|
114
|
+
options = args.extract_options!
|
115
|
+
retries = options.delete(:retries)|| 0
|
116
|
+
keys = args.flatten
|
117
|
+
items = []
|
118
|
+
unprocessed_keys = []
|
119
|
+
|
120
|
+
# exponential backoff to handle unprocessed keys
|
121
|
+
delay = 2 ** retries
|
122
|
+
if retries > 0
|
123
|
+
logger.debug "batch_get_items: sleeping for #{delay} seconds and will retry. retries: #{retries}"
|
124
|
+
sleep(delay)
|
125
|
+
if retries >= 3 # 2 + 4 + 8 = 14s total of retries
|
126
|
+
raise "ERROR: Exceeded max retries: #{retries}. Unable to batch_get_items for keys: #{keys.inspect}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
max_batch_size = 100
|
131
|
+
keys.each_slice(max_batch_size).each do |slice|
|
132
|
+
# Note: client.batch_get_items will silently not return items if any of the
|
133
|
+
# keys are not found. So we do not have to do a compact and remove nils.
|
134
|
+
# Merge options to allow passing in consistent_read: true
|
135
|
+
params = options.merge(keys: slice)
|
136
|
+
resp = client.batch_get_item(
|
137
|
+
request_items: {
|
138
|
+
table_name => params
|
139
|
+
}
|
140
|
+
)
|
141
|
+
resp[:responses][table_name].each do |item|
|
142
|
+
items << build_item(item)
|
143
|
+
end
|
144
|
+
unprocessed_keys += resp[:unprocessed_keys][table_name] if resp[:unprocessed_keys][table_name]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Recursively call batch_get_items if there are unprocessed_keys
|
148
|
+
# Increase the retries by 1 for exponential backoff
|
149
|
+
items += batch_get_items(unprocessed_keys, options.merge(retries: retries+1)) if unprocessed_keys.any?
|
150
|
+
|
151
|
+
items
|
152
|
+
end
|
153
|
+
|
154
|
+
def build_item(attrs)
|
155
|
+
return unless attrs # is nil when no item found: client.get_item(params).item
|
156
|
+
item = self.new(attrs)
|
157
|
+
item.new_record = false
|
158
|
+
item.run_callbacks :find # find and find_by leads to build_item. so we can run callbacks here
|
159
|
+
item
|
160
|
+
end
|
161
|
+
|
162
|
+
# Examples of id:
|
163
|
+
#
|
164
|
+
# "f74de472"
|
165
|
+
# {:id=>"f74de472"}
|
166
|
+
# {:category=>"Electronics", :product_id=>101}
|
167
|
+
# [id1, id2, id3]
|
168
|
+
#
|
169
|
+
# Returns hash of key_schema
|
170
|
+
#
|
171
|
+
# { id: "f74de472" }
|
172
|
+
# { category: "Electronics", product_id: 101 }
|
173
|
+
#
|
174
|
+
def get_key_schema_from_one_arg(id)
|
175
|
+
# standardize key structure
|
176
|
+
case id
|
177
|
+
when Integer, String, Symbol # "f74de472"
|
178
|
+
if id.is_a?(String) && id.starts_with?(id_prefix)
|
179
|
+
{ id: id }
|
180
|
+
else
|
181
|
+
if sort_key_field
|
182
|
+
raise "ERROR: You must provide both partition and sort key for class: #{self.name} partition_key_field: #{partition_key_field} sort_key_field: #{sort_key_field}"
|
183
|
+
end
|
184
|
+
{ partition_key_field => id }
|
185
|
+
end
|
186
|
+
when Hash # {:id=>"f74de472"} or {:category=>"Electronics", :product_id=>101}
|
187
|
+
id # User needs to provide both partition and sort key
|
188
|
+
when Array # [id1, id2, id3]
|
189
|
+
id.map do |i|
|
190
|
+
i.is_a?(Hash) ? i : { partition_key_field => i }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Dynomite::Item::Read
|
2
|
+
module FindWithEvent
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def find_all_with_stream_event(event, options={})
|
7
|
+
# For event payload structure see
|
8
|
+
# https://rubyonjets.com/docs/events/dynamodb/#event-payload
|
9
|
+
# Keys structure:
|
10
|
+
# {
|
11
|
+
# "Records": [
|
12
|
+
# "dynamodb": {
|
13
|
+
# "Keys": {
|
14
|
+
# "id": {
|
15
|
+
# "S": "post-1"
|
16
|
+
# }
|
17
|
+
# },
|
18
|
+
event = JSON.load(event) if event.is_a?(String)
|
19
|
+
event = event.deep_symbolize_keys
|
20
|
+
# raw_keys: { "id": { "S": "post-1" } }
|
21
|
+
raw_keys = event[:Records].map do |record|
|
22
|
+
record[:dynamodb][:Keys]
|
23
|
+
end
|
24
|
+
# keys: { id: "post-1" }
|
25
|
+
keys = get_key_schema_from_raw_keys(raw_keys)
|
26
|
+
items = find(keys, options) # find can return single item or Array of items
|
27
|
+
Array(items) # ensure Array is returned
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_key_schema_from_raw_keys(raw_keys)
|
31
|
+
# raw_keys: { "id": { "S": "post-1" } }
|
32
|
+
# keys: { id: "post-1" }
|
33
|
+
# Note: raw_keys can have duplicates.
|
34
|
+
# IE: [{ "id": { "S": "post-1" }, "id": { "S": "post-1" } }]
|
35
|
+
keys = raw_keys.uniq.map do |hash|
|
36
|
+
hash.transform_values { |value| value.values.first }
|
37
|
+
end
|
38
|
+
get_key_schema_from_one_arg(keys)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|