lib/dyna_model/table.rb in dyna_model-0.0.7 vs lib/dyna_model/table.rb in dyna_model-0.0.8
- old
+ new
@@ -16,10 +16,24 @@
s: "S",
ss: "SS",
ns: "NS"
}
+ RETURN_VALUES = {
+ none: "NONE",
+ all_old: "ALL_OLD",
+ updated_old: "UPDATED_OLD",
+ all_new: "ALL_NEW",
+ updated_new: "UPDATED_NEW"
+ }
+
+ RETURN_VALUES_UPDATE_ONLY = [
+ :updated_old,
+ :all_new,
+ :updated_new
+ ]
+
QUERY_SELECT = {
all: "ALL_ATTRIBUTES",
projected: "ALL_PROJECTED_ATTRIBUTES",
count: "COUNT",
specific: "SPECIFIC_ATTRIBUTES"
@@ -40,38 +54,38 @@
contains: "CONTAINS",
not_contains: "NOT_CONTAINS",
in: "IN"
}
+ CONDITIONAL_OPERATOR = {
+ and: "AND",
+ or: "OR"
+ }
+
COMPARISON_OPERATOR_SCAN_ONLY = [
:ne,
:not_null,
:null,
:contains,
:not_contains,
:in
]
- class << self
+ def self.type_from_value(value)
+ case
+ when value.kind_of?(AWS::DynamoDB::Binary) then :b
+ when value.respond_to?(:to_str) then :s
+ when value.kind_of?(Numeric) then :n
+ else
+ raise ArgumentError, "unsupported attribute type #{value.class}"
+ end
+ end
-
+ def self.attr_with_type(attr_name, value)
+ { attr_name => { TYPE_INDICATOR[type_from_value(value)] => value.to_s } }
end
- def self.type_from_value(value)
- case
- when value.kind_of?(AWS::DynamoDB::Binary) then :b
- when value.respond_to?(:to_str) then :s
- when value.kind_of?(Numeric) then :n
- else
- raise ArgumentError, "unsupported attribute type #{value.class}"
- end
- end
-
- def self.attr_with_type(attr_name, value)
- { attr_name => { TYPE_INDICATOR[type_from_value(value)] => value.to_s } }
- end
-
def initialize(model)
@model = model
@table_schema = model.table_schema
self.load_schema
self.validate_key_schema
@@ -197,10 +211,11 @@
options[:order] ||= :desc
#options[:index_name] ||= :none
#AWS::DynamoDB::Errors::ValidationException: ALL_PROJECTED_ATTRIBUTES can be used only when Querying using an IndexName
#options[:limit] ||= 10
#options[:exclusive_start_key]
+ #options[:query_filter]
key_conditions = {}
gsi = nil
if options[:global_secondary_index]
gsi = @table_schema[:global_secondary_indexes].select{ |gsi| gsi[:index_name].to_s == options[:global_secondary_index].to_s}.first
@@ -219,39 +234,37 @@
return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
scan_index_forward: (options[:order] == :asc)
}
if options[:range]
- raise ArgumentError, "Expected a 2 element Hash for :range (ex {:age.gt => 13})" unless options[:range].is_a?(Hash) && options[:range].keys.size == 1 && options[:range].keys.first.is_a?(String)
- range_key_name, comparison_operator = options[:range].keys.first.split(".")
- raise ArgumentError, "Comparison operator must be one of (#{(COMPARISON_OPERATOR.keys - COMPARISON_OPERATOR_SCAN_ONLY).join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
- range_key = @range_keys.find{|k| k[:attribute_name] == range_key_name}
+ raise ArgumentError, "Table does not use a range key in its schema!" if @range_keys.blank?
+ attr_with_condition_hash = self.attr_with_condition(options[:range])
+ range_key = @range_keys.find{|k| k[:attribute_name] == attr_with_condition_hash.keys.first}
raise ArgumentError, ":range key must be a valid Range attribute" unless range_key
- raise ArgumentError, ":range key must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !options[:range].values.first.is_a?(Range)
if range_key.has_key?(:index_name) # Local/Global Secondary Index
options[:index_name] = range_key[:index_name]
query_request[:index_name] = range_key[:index_name]
end
- range_value = options[:range].values.first
- range_attribute_list = []
- if comparison_operator == "between"
- range_attribute_list << { range_key[:attribute_type] => range_value.min }
- range_attribute_list << { range_key[:attribute_type] => range_value.max }
- else
- # TODO - support Binary?
- range_attribute_list = [{ range_key[:attribute_type] => range_value.to_s }]
- end
+ key_conditions.merge!(attr_with_condition_hash)
+ end
- key_conditions.merge!({
- range_key[:attribute_name] => {
- attribute_value_list: range_attribute_list,
- comparison_operator: COMPARISON_OPERATOR[comparison_operator.to_sym]
- }
- })
+ query_filter = {}
+ conditional_operator = nil
+ if options[:query_filter]
+ raise ArgumentError, ":query_filter must be a hash" unless options[:query_filter].is_a?(Hash)
+ options[:query_filter].each_pair do |k,v|
+ query_filter.merge!(self.attr_with_condition({ k => v}))
+ end
+ if options[:conditional_operator]
+ raise ArgumentError, ":condition_operator invalid! Must be one of (#{CONDITIONAL_OPERATOR.keys.join(", ")})" unless CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ conditional_operator = CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ end
end
+ query_request.merge!(query_filter: query_filter) unless query_filter.blank?
+ query_request.merge!(conditional_operator: conditional_operator) unless conditional_operator.blank? || query_filter.blank?
if options[:global_secondary_index] # Override index_name if using GSI
# You can only select projected attributes from a GSI
options[:select] = :projected #if options[:select].blank?
options[:index_name] = gsi[:index_name]
@@ -313,12 +326,26 @@
@model.dynamo_db_client.batch_get_item(batch_get_item_request)
end
def write(attributes, options={})
options[:return_consumed_capacity] ||= :none
+ options[:return_values] ||= :none
options[:update_item] = false unless options[:update_item]
+ expected = {}
+ conditional_operator = nil
+ if options[:expected]
+ raise ArgumentError, ":expected must be a hash" unless options[:expected].is_a?(Hash)
+ options[:expected].each_pair do |k,v|
+ expected.merge!(self.attr_with_condition({ k => v}))
+ end
+ if options[:conditional_operator]
+ raise ArgumentError, ":condition_operator invalid! Must be one of (#{CONDITIONAL_OPERATOR.keys.join(", ")})" unless CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ conditional_operator = CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ end
+ end
+
if options[:update_item]
# UpdateItem
key_request = {
@hash_key[:attribute_name] => {
@hash_key[:attribute_type] => options[:update_item][:hash_value].to_s,
@@ -344,52 +371,82 @@
action: "PUT"
}
})
end
end
+ raise ArgumentError, ":return_values must be one of (#{RETURN_VALUES.keys.join(", ")})" unless RETURN_VALUES[options[:return_values]]
update_item_request = {
table_name: @model.dynamo_db_table_name(options[:shard_name]),
key: key_request,
attribute_updates: attrs_to_update,
- return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
+ return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
+ return_values: RETURN_VALUES[options[:return_values]]
}
+ update_item_request.merge!(expected: expected) unless expected.blank?
+ update_item_request.merge!(conditional_operator: conditional_operator) unless conditional_operator.blank? || expected.blank?
@model.dynamo_db_client.update_item(update_item_request)
else
# PutItem
items = {}
attributes.each_pair do |k,v|
next if v.blank? # If empty string or nil, skip...
items.merge!(self.class.attr_with_type(k,v))
end
+ raise ArgumentError, ":return_values must be one of (#{(RETURN_VALUES.keys - RETURN_VALUES_UPDATE_ONLY).join(", ")})" unless RETURN_VALUES[options[:return_values]] && !RETURN_VALUES_UPDATE_ONLY.include?(options[:return_values])
put_item_request = {
table_name: @model.dynamo_db_table_name(options[:shard_name]),
item: items,
- return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]]
+ return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
+ return_values: RETURN_VALUES[options[:return_values]]
}
+ put_item_request.merge!(expected: expected) unless expected.blank?
+ put_item_request.merge!(conditional_operator: conditional_operator) unless conditional_operator.blank? || expected.blank?
@model.dynamo_db_client.put_item(put_item_request)
end
end
def delete_item(options={})
raise ":delete_item => {...key_values...} required" unless options[:delete_item].present?
+ options[:return_consumed_capacity] ||= :none
+ options[:return_values] ||= :none
+ raise ArgumentError, ":return_values must be one of (#{(RETURN_VALUES.keys - RETURN_VALUES_UPDATE_ONLY).join(", ")})" unless RETURN_VALUES[options[:return_values]] && !RETURN_VALUES_UPDATE_ONLY.include?(options[:return_values])
key_request = {
@hash_key[:attribute_name] => {
@hash_key[:attribute_type] => options[:delete_item][:hash_value].to_s
}
}
+
if @primary_range_key
raise ArgumentError, "range_key was not provided to the delete_item command" if options[:delete_item][:range_value].blank?
key_request.merge!({
@primary_range_key[:attribute_name] => {
@primary_range_key[:attribute_type] => options[:delete_item][:range_value].to_s
}
})
end
+
+ expected = {}
+ conditional_operator = nil
+ if options[:expected]
+ raise ArgumentError, ":expected must be a hash" unless options[:expected].is_a?(Hash)
+ options[:expected].each_pair do |k,v|
+ expected.merge!(self.attr_with_condition({ k => v}))
+ end
+ if options[:conditional_operator]
+ raise ArgumentError, ":condition_operator invalid! Must be one of (#{CONDITIONAL_OPERATOR.keys.join(", ")})" unless CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ conditional_operator = CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ end
+ end
+
delete_item_request = {
table_name: @model.dynamo_db_table_name(options[:shard_name]),
- key: key_request
+ key: key_request,
+ return_consumed_capacity: RETURNED_CONSUMED_CAPACITY[options[:return_consumed_capacity]],
+ return_values: RETURN_VALUES[options[:return_values]]
}
+ delete_item_request.merge!(expected: expected) unless expected.blank?
+ delete_item_request.merge!(conditional_operator: conditional_operator) unless conditional_operator.blank? || expected.blank?
@model.dynamo_db_client.delete_item(delete_item_request)
end
# Perform a table scan
# http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html
@@ -418,42 +475,70 @@
scan_request.merge!({ select: QUERY_SELECT[options[:select]] })
end
# :scan_filter => { :name.begins_with => "a" }
scan_filter = {}
+ conditional_operator = nil
if options[:scan_filter].present?
options[:scan_filter].each_pair.each do |k,v|
- # Hard to validate attribute types here, so infer by type sent and assume the user knows their own attrs
- key_name, comparison_operator = k.split(".")
- raise ArgumentError, "Comparison operator must be one of (#{COMPARISON_OPERATOR.keys.join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
- raise ArgumentError, "scan_filter value must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !v.is_a?(Range)
- raise ArgumentError, "scan_filter value must be a Array if using the operator IN" if comparison_operator == "in" && !v.is_a?(Array)
+ scan_filter.merge!(self.attr_with_condition({ k => v}))
+ end
+ end
+ if options[:conditional_operator]
+ raise ArgumentError, ":condition_operator invalid! Must be one of (#{CONDITIONAL_OPERATOR.keys.join(", ")})" unless CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ conditional_operator = CONDITIONAL_OPERATOR[options[:conditional_operator]]
+ end
+ scan_request.merge!(scan_filter: scan_filter) unless scan_filter.blank?
+ scan_request.merge!(conditional_operator: conditional_operator) unless conditional_operator.blank? || scan_filter.blank?
+ scan_request.merge!(segment: options[:segment].to_i) if options[:segment].present?
+ scan_request.merge!(total_segments: options[:total_segments].to_i) if options[:total_segments].present?
- attribute_value_list = []
- if comparison_operator == "in"
- v.each do |in_v|
- attribute_value_list << self.class.attr_with_type(key_name, in_v).values.last
- end
- elsif comparison_operator == "between"
- attribute_value_list << self.class.attr_with_type(key_name, v.min).values.last
- attribute_value_list << self.class.attr_with_type(key_name, v.max).values.last
- else
- attribute_value_list << self.class.attr_with_type(key_name, v).values.last
- end
- scan_filter.merge!({
- key_name => {
- comparison_operator: COMPARISON_OPERATOR[comparison_operator.to_sym],
- attribute_value_list: attribute_value_list
- }
- })
+ @model.dynamo_db_client.scan(scan_request)
+ end
+
+ protected
+
+ # {:name.eq => "cary"}
+ #
+ # return:
+ # {
+ # "name" => {
+ # attribute_value_list: [
+ # "S" => "cary"
+ # ],
+ # comparison_operator: "EQ"
+ # }
+ # }
+ def attr_with_condition(attr_conditional)
+ raise ArgumentError, "Expected a 2 element Hash for each :query_filter (ex {:age.gt => 13})" unless attr_conditional.is_a?(Hash) && attr_conditional.keys.size == 1 && attr_conditional.keys.first.is_a?(String)
+ attr_name, comparison_operator = attr_conditional.keys.first.split(".")
+ raise ArgumentError, "Comparison operator must be one of (#{(COMPARISON_OPERATOR.keys - COMPARISON_OPERATOR_SCAN_ONLY).join(", ")})" unless COMPARISON_OPERATOR.keys.include?(comparison_operator.to_sym)
+ attr_key = @model.attributes[attr_name]
+ raise ArgumentError, "#{attr_name} not a valid attribute" unless attr_key
+ attr_type = @model.attribute_type_indicator(attr_key)
+ raise ArgumentError, "#{attr_name} key must be a Range if using the operator BETWEEN" if comparison_operator == "between" && !attr_conditional.values.first.is_a?(Range)
+ raise ArgumentError, ":query_filter value must be an Array if using the operator IN" if comparison_operator == "in" && !attr_conditional.values.first.is_a?(Array)
+
+ attr_value = attr_conditional.values.first
+
+ attribute_value_list = []
+ if comparison_operator == "in"
+ attr_conditional.values.first.each do |in_v|
+ attribute_value_list << { attr_type => in_v.to_s }
end
- scan_request.merge!(scan_filter: scan_filter)
+ elsif comparison_operator == "between"
+ attribute_value_list << { attr_type => attr_value.min.to_s }
+ attribute_value_list << { attr_type => attr_value.max.to_s }
+ else
+ attribute_value_list = [{ attr_type => attr_value.to_s }]
end
- scan_request.merge!({ segment: options[:segment].to_i }) if options[:segment].present?
- scan_request.merge!({ total_segments: options[:total_segments].to_i }) if options[:total_segments].present?
+ attribute_comparison_hash = {
+ comparison_operator: COMPARISON_OPERATOR[comparison_operator.to_sym]
+ }
+ attribute_comparison_hash.merge!(attribute_value_list: attribute_value_list) unless %w(null not_null).include?(comparison_operator)
- @model.dynamo_db_client.scan(scan_request)
+ { attr_name => attribute_comparison_hash }
end
end
end