dynamoid 0.4.1 → 0.5.0

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.
@@ -48,7 +48,7 @@ module Dynamoid #:nodoc:
48
48
  #
49
49
  # @since 0.2.0
50
50
  def table_name
51
- "#{Dynamoid::Config.namespace}_index_#{source.to_s.downcase}_#{name.collect(&:to_s).collect(&:pluralize).join('_and_')}"
51
+ "#{Dynamoid::Config.namespace}_index_" + source.table_name.sub("#{Dynamoid::Config.namespace}_", '').singularize + "_#{name.collect(&:to_s).collect(&:pluralize).join('_and_')}"
52
52
  end
53
53
 
54
54
  # Given either an object or a list of attributes, generate a hash key and a range key for the index. Optionally pass in
@@ -0,0 +1,16 @@
1
+ module Dynamoid
2
+ module Middleware
3
+ class IdentityMap
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Dynamoid::IdentityMap.clear
10
+ @app.call(env)
11
+ ensure
12
+ Dynamoid::IdentityMap.clear
13
+ end
14
+ end
15
+ end
16
+ end
@@ -32,8 +32,7 @@ module Dynamoid
32
32
  # @since 0.4.0
33
33
  def create_table(options = {})
34
34
  if self.range_key
35
- range_key_type = [:integer, :float].include?(attributes[range_key][:type]) ? :number : attributes[range_key][:type]
36
- range_key_hash = { range_key => range_key_type}
35
+ range_key_hash = { range_key => dynamo_type(attributes[range_key][:type]) }
37
36
  else
38
37
  range_key_hash = nil
39
38
  end
@@ -57,6 +56,10 @@ module Dynamoid
57
56
  Dynamoid::Adapter.tables.include?(table_name)
58
57
  end
59
58
 
59
+ def from_database(attrs = {})
60
+ new(attrs).tap { |r| r.new_record = false }
61
+ end
62
+
60
63
  # Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
61
64
  #
62
65
  # @since 0.2.0
@@ -75,7 +78,11 @@ module Dynamoid
75
78
  #
76
79
  # @since 0.2.0
77
80
  def undump_field(value, options)
78
- return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
81
+ if value.nil? && (default_value = options[:default])
82
+ value = default_value.respond_to?(:call) ? default_value.call : default_value
83
+ else
84
+ return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
85
+ end
79
86
 
80
87
  case options[:type]
81
88
  when :string
@@ -105,6 +112,26 @@ module Dynamoid
105
112
  end
106
113
  end
107
114
 
115
+ def dynamo_type(type)
116
+ case type
117
+ when :integer, :float, :datetime
118
+ :number
119
+ when :string, :serialized
120
+ :string
121
+ else
122
+ raise 'unknown type'
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ # Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
129
+ #
130
+ def touch(name = nil)
131
+ now = DateTime.now
132
+ self.updated_at = now
133
+ attributes[name] = now if name
134
+ save
108
135
  end
109
136
 
110
137
  # Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
@@ -120,8 +147,6 @@ module Dynamoid
120
147
  def save(options = {})
121
148
  self.class.create_table
122
149
 
123
- @previously_changed = changes
124
-
125
150
  if new_record?
126
151
  run_callbacks(:create) { persist }
127
152
  else
@@ -131,6 +156,19 @@ module Dynamoid
131
156
  self
132
157
  end
133
158
 
159
+ def update!(conditions = {}, &block)
160
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
161
+ new_attrs = Dynamoid::Adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions), &block)
162
+ load(new_attrs)
163
+ end
164
+
165
+ def update(conditions = {}, &block)
166
+ update!(conditions, &block)
167
+ true
168
+ rescue Dynamoid::Errors::ConditionalCheckFailedException
169
+ false
170
+ end
171
+
134
172
  # Delete this object, but only after running callbacks for it.
135
173
  #
136
174
  # @since 0.2.0
@@ -146,7 +184,8 @@ module Dynamoid
146
184
  # @since 0.2.0
147
185
  def delete
148
186
  delete_indexes
149
- Dynamoid::Adapter.delete(self.class.table_name, self.id)
187
+ options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
188
+ Dynamoid::Adapter.delete(self.class.table_name, self.hash_key, options)
150
189
  end
151
190
 
152
191
  # Dump this object's attributes into hash form, fit to be persisted into the datastore.
@@ -0,0 +1,9 @@
1
+ class Message
2
+ include Dynamoid::Document
3
+
4
+ table name: :messages, key: :message_id, read_capacity: 200, write_capacity: 200
5
+
6
+ range :time, :datetime
7
+
8
+ field :text
9
+ end
@@ -6,4 +6,7 @@ class Tweet
6
6
  range :group, :string
7
7
 
8
8
  field :msg
9
+ field :count, :integer
10
+ field :tags, :set
11
+ field :user_name
9
12
  end
@@ -2,20 +2,20 @@ require 'dynamoid/adapter/aws_sdk'
2
2
  require File.expand_path(File.dirname(__FILE__) + '../../../spec_helper')
3
3
 
4
4
  describe Dynamoid::Adapter::AwsSdk do
5
-
5
+
6
6
  if ENV['ACCESS_KEY'] && ENV['SECRET_KEY']
7
-
7
+
8
8
  context 'without a preexisting table' do
9
9
  # CreateTable and DeleteTable
10
10
  it 'performs CreateTable and DeleteTable' do
11
11
  table = Dynamoid::Adapter.create_table('CreateTable', :id, :range_key => { :created_at => :number })
12
-
12
+
13
13
  Dynamoid::Adapter.connection.tables.collect{|t| t.name}.should include 'CreateTable'
14
-
14
+
15
15
  Dynamoid::Adapter.delete_table('CreateTable')
16
16
  end
17
17
  end
18
-
18
+
19
19
  context 'with a preexisting table' do
20
20
  before(:all) do
21
21
  Dynamoid::Adapter.create_table('dynamoid_tests_TestTable1', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable1')
@@ -34,20 +34,20 @@ describe Dynamoid::Adapter::AwsSdk do
34
34
  Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:name => 'Josh', :id => '1'}
35
35
 
36
36
  Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
37
-
37
+
38
38
  Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
39
39
  end
40
-
40
+
41
41
  it 'performs GetItem for an item that does exist with a range key' do
42
42
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :name => 'Josh', :range => 2.0})
43
43
 
44
44
  Dynamoid::Adapter.get_item('dynamoid_tests_TestTable3', '1', :range_key => 2.0).should == {:name => 'Josh', :id => '1', :range => 2.0}
45
45
 
46
46
  Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable3', '1', :range_key => 2.0)
47
-
48
- Dynamoid::Adapter.get_item('dynamoid_tests_TestTable3', '1', :range_key => 2.0).should be_nil
47
+
48
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable3', '1', :range_key => 2.0).should be_nil
49
49
  end
50
-
50
+
51
51
  it 'performs DeleteItem for an item that does not exist' do
52
52
  Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
53
53
 
@@ -59,53 +59,53 @@ describe Dynamoid::Adapter::AwsSdk do
59
59
 
60
60
  Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:id => '1', :name => 'Josh'}
61
61
  end
62
-
62
+
63
63
  # BatchGetItem
64
64
  it "performs BatchGetItem with singular keys" do
65
65
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
66
66
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable2', {:id => '1', :name => 'Justin'})
67
-
67
+
68
68
  results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => '1', 'dynamoid_tests_TestTable2' => '1')
69
69
  results.size.should == 2
70
70
  results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
71
71
  results['dynamoid_tests_TestTable2'].should include({:name => 'Justin', :id => '1'})
72
72
  end
73
-
73
+
74
74
  it "performs BatchGetItem with multiple keys" do
75
75
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
76
76
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
77
-
77
+
78
78
  results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => ['1', '2'])
79
79
  results.size.should == 1
80
80
  results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
81
81
  results['dynamoid_tests_TestTable1'].should include({:name => 'Justin', :id => '2'})
82
82
  end
83
-
83
+
84
84
  it 'performs BatchGetItem with one ranged key' do
85
85
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :name => 'Josh', :range => 1.0})
86
86
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '2', :name => 'Justin', :range => 2.0})
87
-
87
+
88
88
  results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable3' => [['1', 1.0]])
89
89
  results.size.should == 1
90
90
  results['dynamoid_tests_TestTable3'].should include({:name => 'Josh', :id => '1', :range => 1.0})
91
91
  end
92
-
92
+
93
93
  it 'performs BatchGetItem with multiple ranged keys' do
94
94
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :name => 'Josh', :range => 1.0})
95
95
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '2', :name => 'Justin', :range => 2.0})
96
-
96
+
97
97
  results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable3' => [['1', 1.0],['2', 2.0]])
98
98
  results.size.should == 1
99
99
  results['dynamoid_tests_TestTable3'].should include({:name => 'Josh', :id => '1', :range => 1.0})
100
100
  results['dynamoid_tests_TestTable3'].should include({:name => 'Justin', :id => '2', :range => 2.0})
101
101
  end
102
-
102
+
103
103
  # ListTables
104
104
  it 'performs ListTables' do
105
105
  Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable1'
106
106
  Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable2'
107
107
  end
108
-
108
+
109
109
  # Query
110
110
  it 'performs query on a table and returns items' do
111
111
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
@@ -119,30 +119,30 @@ describe Dynamoid::Adapter::AwsSdk do
119
119
 
120
120
  Dynamoid::Adapter.query('dynamoid_tests_TestTable1', :hash_value => '1').should == { :id=> '1', :name=>"Josh" }
121
121
  end
122
-
122
+
123
123
  context 'range queries' do
124
124
  before do
125
125
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :range => 1.0})
126
126
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable3', {:id => '1', :range => 3.0})
127
127
  end
128
128
 
129
- it 'performs query on a table with a range and selects items in a range' do
129
+ it 'performs query on a table with a range and selects items in a range' do
130
130
  Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_value => 0.0..3.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
131
131
  end
132
132
 
133
- it 'performs query on a table with a range and selects items greater than' do
133
+ it 'performs query on a table with a range and selects items greater than' do
134
134
  Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_greater_than => 1.0).should =~ [{:id => '1', :range => BigDecimal.new(3)}]
135
135
  end
136
136
 
137
- it 'performs query on a table with a range and selects items less than' do
137
+ it 'performs query on a table with a range and selects items less than' do
138
138
  Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_less_than => 2.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}]
139
139
  end
140
140
 
141
- it 'performs query on a table with a range and selects items gte' do
141
+ it 'performs query on a table with a range and selects items gte' do
142
142
  Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_gte => 1.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
143
143
  end
144
144
 
145
- it 'performs query on a table with a range and selects items lte' do
145
+ it 'performs query on a table with a range and selects items lte' do
146
146
  Dynamoid::Adapter.query('dynamoid_tests_TestTable3', :hash_value => '1', :range_lte => 3.0).should =~ [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}]
147
147
  end
148
148
  end
@@ -165,22 +165,22 @@ describe Dynamoid::Adapter::AwsSdk do
165
165
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
166
166
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Josh'})
167
167
 
168
- Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}]
168
+ Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"})
169
169
  end
170
-
170
+
171
171
  it 'performs scan on a table and returns all items if no criteria are specified' do
172
172
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
173
173
  Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Josh'})
174
174
 
175
- Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', {}).should == [{:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}]
176
- end
175
+ Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', {}).should include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"})
176
+ end
177
177
  end
178
-
178
+
179
179
  # DescribeTable
180
-
180
+
181
181
  # UpdateItem
182
-
182
+
183
183
  # UpdateTable
184
-
184
+
185
185
  end
186
186
  end
@@ -73,6 +73,14 @@ describe "Dynamoid::Associations::Chain" do
73
73
  @chain.send(:records_without_index).should == [@user]
74
74
  end
75
75
 
76
+ it "doesn't crash if it finds a nil id in the index" do
77
+ @chain.query = {:name => 'Josh', "created_at.gt" => @time - 1.day}
78
+ Dynamoid::Adapter.expects(:query).
79
+ with("dynamoid_tests_index_user_created_ats_and_names", kind_of(Hash)).
80
+ returns([{ids: nil}, {ids: Set.new([42])}])
81
+ @chain.send(:ids_from_index).should == Set.new([42])
82
+ end
83
+
76
84
  it 'defines each' do
77
85
  @chain.query = {:name => 'Josh'}
78
86
  @chain.each {|u| u.update_attribute(:name, 'Justin')}
@@ -88,10 +96,10 @@ describe "Dynamoid::Associations::Chain" do
88
96
 
89
97
  it 'finds range querys' do
90
98
  @chain = Dynamoid::Criteria::Chain.new(Tweet)
91
- @chain.query = { :id => 'test' }
99
+ @chain.query = { :tweet_id => 'test' }
92
100
  @chain.send(:range?).should be_true
93
101
 
94
- @chain.query = {:id => 'test', :group => 'xx'}
102
+ @chain.query = {:tweet_id => 'test', :group => 'xx'}
95
103
  @chain.send(:range?).should be_true
96
104
 
97
105
  @chain.query = { :group => 'xx' }
@@ -100,32 +108,32 @@ describe "Dynamoid::Associations::Chain" do
100
108
  @chain.query = { :group => 'xx', :msg => 'hai' }
101
109
  @chain.send(:range?).should be_false
102
110
  end
103
-
111
+
104
112
  context 'range queries' do
105
113
  before do
106
114
  @tweet1 = Tweet.create(:tweet_id => "x", :group => "one")
107
115
  @tweet2 = Tweet.create(:tweet_id => "x", :group => "two")
108
- @tweet3 = Tweet.create(:tweet_id => "xx", :group => "two")
116
+ @tweet3 = Tweet.create(:tweet_id => "xx", :group => "two")
109
117
  @chain = Dynamoid::Criteria::Chain.new(Tweet)
110
118
  end
111
-
119
+
112
120
  it 'finds tweets with a simple range query' do
113
121
  @chain.query = { :tweet_id => "x" }
114
122
  @chain.send(:records_with_range).size.should == 2
115
123
  @chain.all.size.should == 2
116
- @chain.limit(1).size.should == 1
124
+ @chain.limit(1).size.should == 1
117
125
  end
118
-
126
+
119
127
  it 'finds tweets with a start' do
120
128
  @chain.query = { :tweet_id => "x" }
121
129
  @chain.start(@tweet1)
122
130
  @chain.all.should =~ [@tweet2]
123
131
  end
124
-
132
+
125
133
  it 'finds one specific tweet' do
126
134
  @chain = Dynamoid::Criteria::Chain.new(Tweet)
127
135
  @chain.query = { :tweet_id => "xx", :group => "two" }
128
- @chain.send(:records_with_range).should == [@tweet3]
136
+ @chain.send(:records_with_range).should == [@tweet3]
129
137
  end
130
138
  end
131
139
 
@@ -57,10 +57,16 @@ describe "Dynamoid::Criteria" do
57
57
  User.where(:name => 'x').consistent.first
58
58
 
59
59
  Dynamoid::Adapter.expects(:query).with { |table_name, options| options[:consistent_read] == true }.returns([])
60
- Tweet.where(:id => 'xx', :group => 'two').consistent.all
60
+ Tweet.where(:tweet_id => 'xx', :group => 'two').consistent.all
61
61
 
62
62
  Dynamoid::Adapter.expects(:query).with { |table_name, options| options[:consistent_read] == false }.returns([])
63
- Tweet.where(:id => 'xx', :group => 'two').all
63
+ Tweet.where(:tweet_id => 'xx', :group => 'two').all
64
+ end
65
+
66
+ it 'raises exception when consistent_read is used with scan' do
67
+ expect do
68
+ User.where(:password => 'password').consistent.first
69
+ end.to raise_error(Dynamoid::Errors::InvalidQuery)
64
70
  end
65
71
 
66
72
  end
@@ -0,0 +1,49 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe 'Dynamoid::Dirty' do
4
+
5
+ context 'changes' do
6
+ it 'should be empty' do
7
+ tweet = Tweet.new
8
+ tweet.msg_changed?.should be_false
9
+ end
10
+
11
+ it 'should not be empty' do
12
+ tweet = Tweet.new(:tweet_id => "1", :group => 'abc')
13
+ tweet.changed?.should be_true
14
+ tweet.group_was.should be_nil
15
+ end
16
+
17
+ it 'should be empty when loaded from database' do
18
+ Tweet.create!(:tweet_id => "1", :group => 'abc')
19
+ tweet = Tweet.where(:tweet_id => "1", :group => 'abc').first
20
+ tweet.changed?.should be_false
21
+ tweet.group = 'abc'
22
+ tweet.reload
23
+ tweet.changed?.should be_false
24
+ end
25
+
26
+ it 'track changes after saves' do
27
+ tweet = Tweet.new(:tweet_id => "1", :group => 'abc')
28
+ tweet.save!
29
+ tweet.changed?.should be_false
30
+
31
+ tweet.user_name = 'xyz'
32
+ tweet.user_name_changed?.should be_true
33
+ tweet.user_name_was.should be_nil
34
+ tweet.save!
35
+
36
+ tweet.user_name_changed?.should be_false
37
+ tweet.user_name = 'abc'
38
+ tweet.user_name_was.should == 'xyz'
39
+ end
40
+
41
+ it 'clear changes on save' do
42
+ tweet = Tweet.new(:tweet_id => "1", :group => 'abc')
43
+ tweet.group = 'xyz'
44
+ tweet.group_changed?.should be_true
45
+ tweet.save!
46
+ tweet.group_changed?.should be_false
47
+ end
48
+ end
49
+ end