dynamoid 0.1.1 → 0.1.2
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.
- data/Dynamoid.gemspec +2 -6
- data/README.markdown +28 -4
- data/VERSION +1 -1
- data/lib/dynamoid.rb +4 -1
- data/lib/dynamoid/adapter.rb +79 -6
- data/lib/dynamoid/adapter/aws_sdk.rb +9 -7
- data/lib/dynamoid/adapter/local.rb +1 -1
- data/lib/dynamoid/associations.rb +1 -1
- data/lib/dynamoid/associations/association.rb +26 -4
- data/lib/dynamoid/associations/belongs_to.rb +8 -0
- data/lib/dynamoid/components.rb +3 -1
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +1 -1
- data/lib/dynamoid/document.rb +18 -15
- data/lib/dynamoid/fields.rb +39 -4
- data/lib/dynamoid/finders.rb +3 -3
- data/lib/dynamoid/indexes.rb +10 -17
- data/lib/dynamoid/persistence.rb +92 -16
- data/spec/app/models/magazine.rb +2 -0
- data/spec/app/models/subscription.rb +2 -0
- data/spec/dynamoid/adapter_spec.rb +76 -0
- data/spec/dynamoid/associations/association_spec.rb +22 -0
- data/spec/dynamoid/associations/belongs_to_spec.rb +8 -0
- data/spec/dynamoid/criteria_spec.rb +3 -0
- data/spec/dynamoid/document_spec.rb +11 -2
- data/spec/dynamoid/fields_spec.rb +57 -0
- data/spec/dynamoid/indexes_spec.rb +7 -15
- data/spec/dynamoid/persistence_spec.rb +34 -4
- data/spec/spec_helper.rb +5 -5
- metadata +21 -25
- data/lib/dynamoid/attributes.rb +0 -37
- data/lib/dynamoid/relations.rb +0 -21
- data/spec/dynamoid/attributes_spec.rb +0 -46
- data/spec/dynamoid/relations_spec.rb +0 -6
data/lib/dynamoid/finders.rb
CHANGED
@@ -12,14 +12,14 @@ module Dynamoid #:nodoc:
|
|
12
12
|
if id.count == 1
|
13
13
|
self.find_by_id(id.first)
|
14
14
|
else
|
15
|
-
items = Dynamoid::Adapter.
|
15
|
+
items = Dynamoid::Adapter.read(self.table_name, id)
|
16
16
|
items[self.table_name].collect{|i| o = self.build(i); o.new_record = false; o}
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
def find_by_id(id)
|
21
|
-
if item = Dynamoid::Adapter.
|
22
|
-
obj = self.new(
|
21
|
+
if item = Dynamoid::Adapter.read(self.table_name, id)
|
22
|
+
obj = self.new(item)
|
23
23
|
obj.new_record = false
|
24
24
|
return obj
|
25
25
|
else
|
data/lib/dynamoid/indexes.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'digest/sha2'
|
2
|
-
|
3
1
|
# encoding: utf-8
|
4
2
|
module Dynamoid #:nodoc:
|
5
3
|
|
@@ -16,30 +14,23 @@ module Dynamoid #:nodoc:
|
|
16
14
|
module ClassMethods
|
17
15
|
def index(name, options = {})
|
18
16
|
name = Array(name).collect(&:to_s).sort.collect(&:to_sym)
|
19
|
-
raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless name.all?{|n| self.
|
17
|
+
raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless name.all?{|n| self.attributes.include?(n)}
|
20
18
|
self.indexes << name
|
21
19
|
create_indexes
|
22
20
|
end
|
23
21
|
|
24
22
|
def create_indexes
|
25
23
|
self.indexes.each do |index|
|
26
|
-
self.create_table(index_table_name(index),
|
24
|
+
self.create_table(index_table_name(index), :id) unless self.table_exists?(index_table_name(index))
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
30
28
|
def index_table_name(index)
|
31
|
-
"#{Dynamoid::Config.namespace}_index_#{
|
32
|
-
end
|
33
|
-
|
34
|
-
def index_key_name(index)
|
35
|
-
"#{self.to_s.downcase}_#{index.collect(&:to_s).collect(&:pluralize).join('_and_')}"
|
29
|
+
"#{Dynamoid::Config.namespace}_index_#{self.to_s.downcase}_#{index.collect(&:to_s).collect(&:pluralize).join('_and_')}"
|
36
30
|
end
|
37
31
|
|
38
32
|
def key_for_index(index, values = [])
|
39
|
-
values = values.collect(&:to_s).sort
|
40
|
-
Digest::SHA2.new.tap do |sha|
|
41
|
-
index.each_with_index {|i, index| sha << values[index] if values[index]}
|
42
|
-
end.to_s
|
33
|
+
values = values.collect(&:to_s).sort.join('.')
|
43
34
|
end
|
44
35
|
end
|
45
36
|
|
@@ -49,17 +40,19 @@ module Dynamoid #:nodoc:
|
|
49
40
|
|
50
41
|
def save_indexes
|
51
42
|
self.class.indexes.each do |index|
|
52
|
-
|
43
|
+
next if self.key_for_index(index).blank?
|
44
|
+
existing = Dynamoid::Adapter.read(self.class.index_table_name(index), self.key_for_index(index))
|
53
45
|
ids = existing ? existing[:ids] : Set.new
|
54
|
-
Dynamoid::Adapter.
|
46
|
+
Dynamoid::Adapter.write(self.class.index_table_name(index), {:id => self.key_for_index(index), :ids => ids.merge([self.id])})
|
55
47
|
end
|
56
48
|
end
|
57
49
|
|
58
50
|
def delete_indexes
|
59
51
|
self.class.indexes.each do |index|
|
60
|
-
|
52
|
+
next if self.key_for_index(index).blank?
|
53
|
+
existing = Dynamoid::Adapter.read(self.class.index_table_name(index), self.key_for_index(index))
|
61
54
|
next unless existing && existing[:ids]
|
62
|
-
Dynamoid::Adapter.
|
55
|
+
Dynamoid::Adapter.write(self.class.index_table_name(index), {:id => self.key_for_index(index), :ids => existing[:ids] - [self.id]})
|
63
56
|
end
|
64
57
|
end
|
65
58
|
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -7,42 +7,118 @@ module Dynamoid #:nodoc:
|
|
7
7
|
module Persistence
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
|
+
attr_accessor :new_record
|
11
|
+
alias :new_record? :new_record
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def table_name
|
15
|
+
"#{Dynamoid::Config.namespace}_#{self.to_s.downcase.pluralize}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_table(table_name, id = :id)
|
19
|
+
Dynamoid::Adapter.tables << table_name if Dynamoid::Adapter.create_table(table_name, id.to_sym)
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_exists?(table_name)
|
23
|
+
Dynamoid::Adapter.tables.include?(table_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def undump(incoming = {})
|
27
|
+
incoming.symbolize_keys!
|
28
|
+
Hash.new.tap do |hash|
|
29
|
+
self.attributes.each do |attribute, options|
|
30
|
+
value = incoming[attribute]
|
31
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
32
|
+
case options[:type]
|
33
|
+
when :string
|
34
|
+
hash[attribute] = value.to_s
|
35
|
+
when :integer
|
36
|
+
hash[attribute] = value.to_i
|
37
|
+
when :float
|
38
|
+
hash[attribute] = value.to_f
|
39
|
+
when :set, :array
|
40
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
41
|
+
hash[attribute] = value
|
42
|
+
else
|
43
|
+
hash[attribute] = Set[value]
|
44
|
+
end
|
45
|
+
when :datetime
|
46
|
+
hash[attribute] = Time.at(value).to_datetime
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
10
54
|
included do
|
11
55
|
self.create_table(self.table_name) unless self.table_exists?(self.table_name)
|
12
56
|
end
|
13
57
|
|
58
|
+
def persisted?
|
59
|
+
!new_record?
|
60
|
+
end
|
61
|
+
|
14
62
|
def save
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
63
|
+
if self.new_record?
|
64
|
+
run_callbacks(:create) do
|
65
|
+
run_callbacks(:save) do
|
66
|
+
persist
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
run_callbacks(:save) do
|
71
|
+
persist
|
72
|
+
end
|
19
73
|
end
|
74
|
+
self
|
20
75
|
end
|
21
76
|
|
22
77
|
def destroy
|
23
78
|
run_callbacks(:destroy) do
|
24
79
|
self.delete
|
25
80
|
end
|
81
|
+
self
|
26
82
|
end
|
27
83
|
|
28
84
|
def delete
|
29
85
|
delete_indexes
|
30
|
-
Dynamoid::Adapter.
|
86
|
+
Dynamoid::Adapter.delete(self.class.table_name, self.id)
|
31
87
|
end
|
32
88
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
89
|
+
def dump
|
90
|
+
Hash.new.tap do |hash|
|
91
|
+
self.class.attributes.each do |attribute, options|
|
92
|
+
value = self.read_attribute(attribute)
|
93
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
94
|
+
case options[:type]
|
95
|
+
when :string
|
96
|
+
hash[attribute] = value.to_s
|
97
|
+
when :integer
|
98
|
+
hash[attribute] = value.to_i
|
99
|
+
when :float
|
100
|
+
hash[attribute] = value.to_f
|
101
|
+
when :set, :array
|
102
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
103
|
+
hash[attribute] = value
|
104
|
+
else
|
105
|
+
hash[attribute] = Set[value]
|
106
|
+
end
|
107
|
+
when :datetime
|
108
|
+
hash[attribute] = value.to_time.to_f
|
109
|
+
end
|
110
|
+
end
|
44
111
|
end
|
45
112
|
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def persist
|
117
|
+
self.id = SecureRandom.uuid if self.id.nil? || self.id.blank?
|
118
|
+
Dynamoid::Adapter.write(self.class.table_name, self.dump)
|
119
|
+
save_indexes
|
120
|
+
end
|
121
|
+
|
46
122
|
end
|
47
123
|
|
48
124
|
end
|
data/spec/app/models/magazine.rb
CHANGED
@@ -2,6 +2,10 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
2
2
|
|
3
3
|
describe "Dynamoid::Adapter" do
|
4
4
|
|
5
|
+
before(:all) do
|
6
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable')
|
7
|
+
end
|
8
|
+
|
5
9
|
it 'extends itself automatically' do
|
6
10
|
lambda {Dynamoid::Adapter.list_tables}.should_not raise_error
|
7
11
|
end
|
@@ -9,5 +13,77 @@ describe "Dynamoid::Adapter" do
|
|
9
13
|
it 'raises nomethod if we try a method that is not on the child' do
|
10
14
|
lambda {Dynamoid::Adapter.foobar}.should raise_error
|
11
15
|
end
|
16
|
+
|
17
|
+
context 'without partioning' do
|
18
|
+
before(:all) do
|
19
|
+
@previous_value = Dynamoid::Config.partitioning
|
20
|
+
Dynamoid::Config.partitioning = false
|
21
|
+
end
|
22
|
+
|
23
|
+
after(:all) do
|
24
|
+
Dynamoid::Config.partitioning = @previous_value
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'writes through the adapter' do
|
28
|
+
Dynamoid::Adapter.expects(:put_item).with('dynamoid_tests_TestTable', {:id => '123'}).returns(true)
|
29
|
+
|
30
|
+
Dynamoid::Adapter.write('dynamoid_tests_TestTable', {:id => '123'})
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'reads through the adapter for one ID' do
|
34
|
+
Dynamoid::Adapter.expects(:get_item).with('dynamoid_tests_TestTable', '123').returns(true)
|
35
|
+
|
36
|
+
Dynamoid::Adapter.read('dynamoid_tests_TestTable', '123')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'reads through the adapter for many IDs' do
|
40
|
+
Dynamoid::Adapter.expects(:batch_get_item).with({'dynamoid_tests_TestTable' => ['1', '2']}).returns(true)
|
41
|
+
|
42
|
+
Dynamoid::Adapter.read('dynamoid_tests_TestTable', ['1', '2'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'with partitioning' do
|
47
|
+
before(:all) do
|
48
|
+
@previous_value = Dynamoid::Config.partitioning
|
49
|
+
Dynamoid::Config.partitioning = true
|
50
|
+
end
|
51
|
+
|
52
|
+
after(:all) do
|
53
|
+
Dynamoid::Config.partitioning = @previous_value
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'writes through the adapter' do
|
57
|
+
Random.expects(:rand).with(Dynamoid::Config.partition_size).once.returns(0)
|
58
|
+
Dynamoid::Adapter.write('dynamoid_tests_TestTable', {:id => 'testid'})
|
59
|
+
|
60
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable', 'testid.0')[:id].should == 'testid.0'
|
61
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable', 'testid.0')[:updated_at].should_not be_nil
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'reads through the adapter for one ID' do
|
65
|
+
Dynamoid::Adapter.expects(:batch_get_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| "123.#{n}"}).returns({})
|
66
|
+
|
67
|
+
Dynamoid::Adapter.read('dynamoid_tests_TestTable', '123')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'reads through the adapter for many IDs' do
|
71
|
+
Dynamoid::Adapter.expects(:batch_get_item).with('dynamoid_tests_TestTable' => (0...Dynamoid::Config.partition_size).collect{|n| "1.#{n}"} + (0...Dynamoid::Config.partition_size).collect{|n| "2.#{n}"}).returns({})
|
72
|
+
|
73
|
+
Dynamoid::Adapter.read('dynamoid_tests_TestTable', ['1', '2'])
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'returns an ID with all partitions' do
|
77
|
+
Dynamoid::Adapter.id_with_partitions('1').should =~ (0...Dynamoid::Config.partition_size).collect{|n| "1.#{n}"}
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'returns a result for one partitioned element' do
|
81
|
+
@time = DateTime.now
|
82
|
+
@array =[{:id => '1.0', :updated_at => @time - 6.hours}, {:id => '1.1', :updated_at => @time - 3.hours}, {:id => '1.2', :updated_at => @time - 1.hour}, {:id => '1.3', :updated_at => @time - 6.hours}, {:id => '2.0', :updated_at => @time}]
|
83
|
+
|
84
|
+
Dynamoid::Adapter.result_for_partition(@array).should =~ [{:id => '1', :updated_at => @time - 1.hour}, {:id => '2', :updated_at => @time}]
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
12
88
|
|
13
89
|
end
|
@@ -78,5 +78,27 @@ describe "Dynamoid::Associations::Association" do
|
|
78
78
|
@magazine.subscriptions = nil
|
79
79
|
@magazine.subscriptions.size.should == 0
|
80
80
|
end
|
81
|
+
|
82
|
+
it 'uses where inside an association and returns a result' do
|
83
|
+
@included_subscription = @magazine.subscriptions.create(:length => 10)
|
84
|
+
@unincldued_subscription = @magazine.subscriptions.create(:length => 8)
|
85
|
+
|
86
|
+
@magazine.subscriptions.where(:length => 10).all.should == [@included_subscription]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'uses where inside an association and returns an empty set' do
|
90
|
+
@included_subscription = @magazine.subscriptions.create(:length => 10)
|
91
|
+
@unincldued_subscription = @magazine.subscriptions.create(:length => 8)
|
92
|
+
|
93
|
+
@magazine.subscriptions.where(:length => 6).all.should be_empty
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'includes enumerable' do
|
97
|
+
@subscription1 = @magazine.subscriptions.create
|
98
|
+
@subscription2 = @magazine.subscriptions.create
|
99
|
+
@subscription3 = @magazine.subscriptions.create
|
100
|
+
|
101
|
+
@magazine.subscriptions.collect(&:id).should =~ [@subscription1.id, @subscription2.id, @subscription3.id]
|
102
|
+
end
|
81
103
|
|
82
104
|
end
|
@@ -23,6 +23,14 @@ describe "Dynamoid::Associations::BelongsTo" do
|
|
23
23
|
@magazine.subscriptions.size.should == 1
|
24
24
|
@magazine.subscriptions.should include @subscription
|
25
25
|
end
|
26
|
+
|
27
|
+
it 'behaves like the object it is trying to be' do
|
28
|
+
@magazine = @subscription.magazine.create
|
29
|
+
|
30
|
+
@subscription.magazine.update_attribute(:title, 'Test Title')
|
31
|
+
|
32
|
+
Magazine.first.title.should == 'Test Title'
|
33
|
+
end
|
26
34
|
end
|
27
35
|
|
28
36
|
context 'has one' do
|
@@ -6,7 +6,7 @@ describe "Dynamoid::Document" do
|
|
6
6
|
@address = Address.new
|
7
7
|
|
8
8
|
@address.new_record.should be_true
|
9
|
-
@address.attributes.should == {:id => nil, :city
|
9
|
+
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>nil}
|
10
10
|
end
|
11
11
|
|
12
12
|
it 'initializes a new document with attributes' do
|
@@ -14,7 +14,7 @@ describe "Dynamoid::Document" do
|
|
14
14
|
|
15
15
|
@address.new_record.should be_true
|
16
16
|
|
17
|
-
@address.attributes.should == {:id => nil, :city
|
17
|
+
@address.attributes.should == {:id=>nil, :created_at=>nil, :updated_at=>nil, :city=>"Chicago"}
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'creates a new document' do
|
@@ -45,4 +45,13 @@ describe "Dynamoid::Document" do
|
|
45
45
|
@address.errors.should be_empty
|
46
46
|
@address.errors.full_messages.should be_empty
|
47
47
|
end
|
48
|
+
|
49
|
+
it 'reloads itself and sees persisted changes' do
|
50
|
+
@address = Address.create
|
51
|
+
|
52
|
+
Address.first.update_attributes(:city => 'Chicago')
|
53
|
+
|
54
|
+
@address.city.should be_nil
|
55
|
+
@address.reload.city.should == 'Chicago'
|
56
|
+
end
|
48
57
|
end
|
@@ -23,4 +23,61 @@ describe "Dynamoid::Fields" do
|
|
23
23
|
@address.city?.should be_true
|
24
24
|
end
|
25
25
|
|
26
|
+
it 'automatically declares id' do
|
27
|
+
lambda {@address.id}.should_not raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'automatically declares and fills in created_at and updated_at' do
|
31
|
+
@address.save
|
32
|
+
|
33
|
+
@address = @address.reload
|
34
|
+
@address.created_at.should_not be_nil
|
35
|
+
@address.created_at.class.should == DateTime
|
36
|
+
@address.updated_at.should_not be_nil
|
37
|
+
@address.updated_at.class.should == DateTime
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with a saved address' do
|
41
|
+
before do
|
42
|
+
@address = Address.create
|
43
|
+
@original_id = @address.id
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should write an attribute correctly' do
|
47
|
+
@address.write_attribute(:city, 'Chicago')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should write an attribute with an alias' do
|
51
|
+
@address[:city] = 'Chicago'
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should read a written attribute' do
|
55
|
+
@address.write_attribute(:city, 'Chicago')
|
56
|
+
@address.read_attribute(:city).should == 'Chicago'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should read a written attribute with the alias' do
|
60
|
+
@address.write_attribute(:city, 'Chicago')
|
61
|
+
@address[:city].should == 'Chicago'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should update all attributes' do
|
65
|
+
@address.expects(:save).once.returns(true)
|
66
|
+
@address.update_attributes(:city => 'Chicago')
|
67
|
+
@address[:city].should == 'Chicago'
|
68
|
+
@address.id.should == @original_id
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should update one attribute' do
|
72
|
+
@address.expects(:save).once.returns(true)
|
73
|
+
@address.update_attribute(:city, 'Chicago')
|
74
|
+
@address[:city].should == 'Chicago'
|
75
|
+
@address.id.should == @original_id
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'returns all attributes' do
|
79
|
+
Address.attributes.should == {:id=>{:type=>:string}, :created_at=>{:type=>:datetime}, :updated_at=>{:type=>:datetime}, :city=>{:type=>:string}}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
26
83
|
end
|