dynamoid 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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