sam-dm-core 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/.autotest +26 -0
  2. data/CONTRIBUTING +51 -0
  3. data/FAQ +92 -0
  4. data/History.txt +145 -0
  5. data/MIT-LICENSE +22 -0
  6. data/Manifest.txt +125 -0
  7. data/QUICKLINKS +12 -0
  8. data/README.txt +143 -0
  9. data/Rakefile +30 -0
  10. data/SPECS +63 -0
  11. data/TODO +1 -0
  12. data/lib/dm-core.rb +224 -0
  13. data/lib/dm-core/adapters.rb +4 -0
  14. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  15. data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
  16. data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
  17. data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
  18. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  19. data/lib/dm-core/associations.rb +199 -0
  20. data/lib/dm-core/associations/many_to_many.rb +147 -0
  21. data/lib/dm-core/associations/many_to_one.rb +107 -0
  22. data/lib/dm-core/associations/one_to_many.rb +309 -0
  23. data/lib/dm-core/associations/one_to_one.rb +61 -0
  24. data/lib/dm-core/associations/relationship.rb +218 -0
  25. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  26. data/lib/dm-core/auto_migrations.rb +113 -0
  27. data/lib/dm-core/collection.rb +638 -0
  28. data/lib/dm-core/dependency_queue.rb +31 -0
  29. data/lib/dm-core/hook.rb +11 -0
  30. data/lib/dm-core/identity_map.rb +45 -0
  31. data/lib/dm-core/is.rb +16 -0
  32. data/lib/dm-core/logger.rb +232 -0
  33. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  34. data/lib/dm-core/migrator.rb +29 -0
  35. data/lib/dm-core/model.rb +471 -0
  36. data/lib/dm-core/naming_conventions.rb +84 -0
  37. data/lib/dm-core/property.rb +673 -0
  38. data/lib/dm-core/property_set.rb +162 -0
  39. data/lib/dm-core/query.rb +625 -0
  40. data/lib/dm-core/repository.rb +159 -0
  41. data/lib/dm-core/resource.rb +637 -0
  42. data/lib/dm-core/scope.rb +58 -0
  43. data/lib/dm-core/support.rb +7 -0
  44. data/lib/dm-core/support/array.rb +13 -0
  45. data/lib/dm-core/support/assertions.rb +8 -0
  46. data/lib/dm-core/support/errors.rb +23 -0
  47. data/lib/dm-core/support/kernel.rb +7 -0
  48. data/lib/dm-core/support/symbol.rb +41 -0
  49. data/lib/dm-core/transaction.rb +267 -0
  50. data/lib/dm-core/type.rb +160 -0
  51. data/lib/dm-core/type_map.rb +80 -0
  52. data/lib/dm-core/types.rb +19 -0
  53. data/lib/dm-core/types/boolean.rb +7 -0
  54. data/lib/dm-core/types/discriminator.rb +34 -0
  55. data/lib/dm-core/types/object.rb +24 -0
  56. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  57. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  58. data/lib/dm-core/types/serial.rb +9 -0
  59. data/lib/dm-core/types/text.rb +10 -0
  60. data/lib/dm-core/version.rb +3 -0
  61. data/script/all +5 -0
  62. data/script/performance.rb +203 -0
  63. data/script/profile.rb +87 -0
  64. data/spec/integration/association_spec.rb +1371 -0
  65. data/spec/integration/association_through_spec.rb +203 -0
  66. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  67. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  68. data/spec/integration/associations/one_to_many_spec.rb +151 -0
  69. data/spec/integration/auto_migrations_spec.rb +398 -0
  70. data/spec/integration/collection_spec.rb +1069 -0
  71. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  72. data/spec/integration/dependency_queue_spec.rb +58 -0
  73. data/spec/integration/model_spec.rb +127 -0
  74. data/spec/integration/mysql_adapter_spec.rb +85 -0
  75. data/spec/integration/postgres_adapter_spec.rb +731 -0
  76. data/spec/integration/property_spec.rb +233 -0
  77. data/spec/integration/query_spec.rb +506 -0
  78. data/spec/integration/repository_spec.rb +57 -0
  79. data/spec/integration/resource_spec.rb +475 -0
  80. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  81. data/spec/integration/sti_spec.rb +208 -0
  82. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  83. data/spec/integration/transaction_spec.rb +75 -0
  84. data/spec/integration/type_spec.rb +271 -0
  85. data/spec/lib/logging_helper.rb +18 -0
  86. data/spec/lib/mock_adapter.rb +27 -0
  87. data/spec/lib/model_loader.rb +91 -0
  88. data/spec/lib/publicize_methods.rb +28 -0
  89. data/spec/models/vehicles.rb +34 -0
  90. data/spec/models/zoo.rb +47 -0
  91. data/spec/spec.opts +3 -0
  92. data/spec/spec_helper.rb +86 -0
  93. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  94. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  95. data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
  96. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  97. data/spec/unit/associations/many_to_many_spec.rb +17 -0
  98. data/spec/unit/associations/many_to_one_spec.rb +152 -0
  99. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  100. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  101. data/spec/unit/associations/relationship_spec.rb +71 -0
  102. data/spec/unit/associations_spec.rb +242 -0
  103. data/spec/unit/auto_migrations_spec.rb +111 -0
  104. data/spec/unit/collection_spec.rb +182 -0
  105. data/spec/unit/data_mapper_spec.rb +35 -0
  106. data/spec/unit/identity_map_spec.rb +126 -0
  107. data/spec/unit/is_spec.rb +80 -0
  108. data/spec/unit/migrator_spec.rb +33 -0
  109. data/spec/unit/model_spec.rb +339 -0
  110. data/spec/unit/naming_conventions_spec.rb +36 -0
  111. data/spec/unit/property_set_spec.rb +83 -0
  112. data/spec/unit/property_spec.rb +753 -0
  113. data/spec/unit/query_spec.rb +530 -0
  114. data/spec/unit/repository_spec.rb +93 -0
  115. data/spec/unit/resource_spec.rb +626 -0
  116. data/spec/unit/scope_spec.rb +142 -0
  117. data/spec/unit/transaction_spec.rb +493 -0
  118. data/spec/unit/type_map_spec.rb +114 -0
  119. data/spec/unit/type_spec.rb +119 -0
  120. data/tasks/ci.rb +68 -0
  121. data/tasks/dm.rb +63 -0
  122. data/tasks/doc.rb +20 -0
  123. data/tasks/gemspec.rb +23 -0
  124. data/tasks/hoe.rb +46 -0
  125. data/tasks/install.rb +20 -0
  126. metadata +216 -0
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if [ HAS_SQLITE3, HAS_MYSQL, HAS_POSTGRES ].include?(ADAPTER)
4
+ describe DataMapper::Adapters::DataObjectsAdapter, "with #{ADAPTER}" do
5
+ describe 'a connection' do
6
+ before do
7
+ @adapter = DataMapper::Repository.adapters[ADAPTER]
8
+ @transaction = DataMapper::Transaction.new(@adapter)
9
+
10
+ @command = mock('command', :execute_non_query => nil)
11
+ @connection = mock('connection', :create_command => @command)
12
+ DataObjects::Connection.stub!(:new).and_return(@connection)
13
+ end
14
+
15
+ it 'should close automatically when no longer needed' do
16
+ @connection.should_receive(:close)
17
+ @adapter.execute('SELECT 1')
18
+ end
19
+
20
+ it 'should not close when a current transaction is active' do
21
+ @connection.should_receive(:create_command).with('SELECT 1').twice.and_return(@command)
22
+ @connection.should_not_receive(:close)
23
+
24
+ @transaction.begin
25
+ @transaction.within do
26
+ @adapter.execute('SELECT 1')
27
+ @adapter.execute('SELECT 1')
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe "DataMapper::DependencyQueue" do
4
+ before :each do
5
+ @q = DataMapper::DependencyQueue.new
6
+ @dependencies = @q.instance_variable_get("@dependencies")
7
+ end
8
+
9
+ describe "#initialize" do
10
+ describe "@dependencies" do
11
+ it "should be a hash after initialize" do
12
+ @dependencies.should be_a_kind_of(Hash)
13
+ end
14
+
15
+ it "should set value to [] when new key is accessed" do
16
+ @dependencies['New Key'].should == []
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "#add" do
22
+ it "should store the supplied callback in @dependencies" do
23
+ @q.add('MissingConstant') { true }
24
+ @dependencies['MissingConstant'].first.call.should == true
25
+ end
26
+ end
27
+
28
+ describe "#resolve!" do
29
+ describe "(when dependency is not defined)" do
30
+ it "should not alter @dependencies" do
31
+ @q.add('MissingConstant') { true }
32
+ old_dependencies = @dependencies.dup
33
+ @q.resolve!
34
+ old_dependencies.should == @dependencies
35
+ end
36
+ end
37
+
38
+ describe "(when dependency is defined)" do
39
+ before :each do
40
+ @q.add('MissingConstant') { |klass| klass.instance_variable_set("@resolved", true) } # add before MissingConstant is loaded
41
+
42
+ class MissingConstant
43
+ end
44
+ end
45
+
46
+ it "should execute stored callbacks" do
47
+ @q.resolve!
48
+ MissingConstant.instance_variable_get("@resolved").should == true
49
+ end
50
+
51
+ it "should clear @dependencies" do
52
+ @q.resolve!
53
+ @dependencies['MissingConstant'].should be_empty
54
+ end
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,127 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if ADAPTER
4
+ module ModelSpec
5
+ class STI
6
+ include DataMapper::Resource
7
+
8
+ def self.default_repository_name
9
+ ADAPTER
10
+ end
11
+
12
+ property :id, Serial
13
+ property :name, String
14
+ property :type, Discriminator
15
+ end
16
+
17
+ class STIDescendant < STI
18
+ end
19
+ end
20
+
21
+ describe "DataMapper::Model with #{ADAPTER}" do
22
+ before do
23
+ repository(ADAPTER) do
24
+ ModelSpec::STI.auto_migrate!
25
+ end
26
+
27
+ @planet = DataMapper::Model.new('planet') do
28
+ def self.default_repository_name; ADAPTER end
29
+ property :name, String, :key => true
30
+ property :distance, Integer
31
+ end
32
+
33
+ @moon = DataMapper::Model.new('moon') do
34
+ def self.default_repository_name; ADAPTER end
35
+ property :id, DM::Serial
36
+ property :name, String
37
+ end
38
+
39
+ @planet.auto_migrate!(ADAPTER)
40
+ @moon.auto_migrate!(ADAPTER)
41
+
42
+ repository(ADAPTER) do
43
+ @moon.create(:name => "Charon")
44
+ @moon.create(:name => "Phobos")
45
+ end
46
+ end
47
+
48
+ describe '.new' do
49
+ it 'should be able to persist' do
50
+ repository(ADAPTER) do
51
+ pluto = @planet.new
52
+ pluto.name = 'Pluto'
53
+ pluto.distance = 1_000_000
54
+ pluto.save
55
+
56
+ clone = @planet.get!('Pluto')
57
+ clone.name.should == 'Pluto'
58
+ clone.distance.should == 1_000_000
59
+ end
60
+ end
61
+ end
62
+
63
+ describe ".get" do
64
+ include LoggingHelper
65
+
66
+ it "should typecast key" do
67
+ resource = nil
68
+ lambda {
69
+ repository(ADAPTER) do
70
+ resource = @moon.get("1")
71
+ end
72
+ }.should_not raise_error
73
+ resource.should be_kind_of(DataMapper::Resource)
74
+ end
75
+
76
+ it "should use the identity map within a repository block" do
77
+ logger do |log|
78
+ repository(ADAPTER) do
79
+ @moon.get("1")
80
+ @moon.get(1)
81
+ end
82
+ log.readlines.size.should == 1
83
+ end
84
+ end
85
+
86
+ it "should not use the identity map outside a repository block" do
87
+ logger do |log|
88
+ @moon.get(1)
89
+ @moon.get(1)
90
+ log.readlines.size.should == 2
91
+ end
92
+ end
93
+ end
94
+
95
+ describe ".base_model" do
96
+ describe "(when called on base model)" do
97
+ it "should refer to itself" do
98
+ ModelSpec::STI.base_model.should == ModelSpec::STI
99
+ end
100
+ end
101
+ describe "(when called on descendant model)" do
102
+ it "should refer to the base model" do
103
+ ModelSpec::STIDescendant.base_model.should == ModelSpec::STI.base_model
104
+ end
105
+ end
106
+ end
107
+
108
+ it 'should provide #load' do
109
+ ModelSpec::STI.should respond_to(:load)
110
+ end
111
+
112
+ describe '#load' do
113
+ it 'should load resources with nil discriminator fields' do
114
+ resource = ModelSpec::STI.create(:name => 'resource')
115
+ query = ModelSpec::STI.all.query
116
+ fields = query.fields
117
+
118
+ fields.should == ModelSpec::STI.properties(ADAPTER).slice(:id, :name, :type)
119
+
120
+ # would blow up prior to fix
121
+ lambda {
122
+ ModelSpec::STI.load([ resource.id, resource.name, nil ], query)
123
+ }.should_not raise_error(NoMethodError)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,85 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if HAS_MYSQL
4
+ describe DataMapper::Adapters::MysqlAdapter do
5
+ before :all do
6
+ @adapter = repository(:mysql).adapter
7
+ end
8
+
9
+ before :all do
10
+ class Sputnik
11
+ include DataMapper::Resource
12
+
13
+ property :id, Serial
14
+ property :name, DM::Text
15
+ property :object, Object
16
+
17
+ auto_migrate!(:mysql)
18
+ end
19
+ end
20
+
21
+ it "should handle Object type" do
22
+ time = Time.now
23
+ repository(:mysql) do
24
+ Sputnik.create(:name => "Sputnik", :object => time)
25
+ Sputnik.first.object.should == time
26
+ end
27
+ end
28
+
29
+ describe "auto migrating" do
30
+ it "#upgrade_model should work" do
31
+ @adapter.destroy_model_storage(repository(:mysql), Sputnik)
32
+ @adapter.storage_exists?("sputniks").should == false
33
+ Sputnik.auto_migrate!(:mysql)
34
+ @adapter.storage_exists?("sputniks").should == true
35
+ @adapter.field_exists?("sputniks", "new_prop").should == false
36
+ Sputnik.property :new_prop, Integer
37
+ Sputnik.auto_upgrade!(:mysql)
38
+ @adapter.field_exists?("sputniks", "new_prop").should == true
39
+ end
40
+ end
41
+
42
+ describe "querying metadata" do
43
+ it "#storage_exists? should return true for tables that exist" do
44
+ @adapter.storage_exists?("sputniks").should == true
45
+ end
46
+
47
+ it "#storage_exists? should return false for tables that don't exist" do
48
+ @adapter.storage_exists?("space turds").should == false
49
+ end
50
+
51
+ it "#field_exists? should return true for columns that exist" do
52
+ @adapter.field_exists?("sputniks", "name").should == true
53
+ end
54
+
55
+ it "#storage_exists? should return false for tables that don't exist" do
56
+ @adapter.field_exists?("sputniks", "plur").should == false
57
+ end
58
+ end
59
+
60
+ describe "handling transactions" do
61
+ before do
62
+ @transaction = DataMapper::Transaction.new(@adapter)
63
+ end
64
+
65
+ it "should rollback changes when #rollback_transaction is called" do
66
+ repository(:mysql) do
67
+ @transaction.commit do |trans|
68
+ Sputnik.create(:name => 'my pretty sputnik')
69
+ trans.rollback
70
+ end
71
+ Sputnik.all(:name => 'my pretty sputnik').should be_empty
72
+ end
73
+ end
74
+
75
+ it "should commit changes when #commit_transaction is called" do
76
+ repository(:mysql) do
77
+ @transaction.commit do
78
+ Sputnik.create(:name => 'my pretty sputnik')
79
+ end
80
+ Sputnik.all(:name => 'my pretty sputnik').size.should == 1
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,731 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if HAS_POSTGRES
4
+ describe DataMapper::Adapters::PostgresAdapter do
5
+ before :all do
6
+ @adapter = repository(:postgres).adapter
7
+ end
8
+
9
+ describe "auto migrating" do
10
+ before :all do
11
+ class Sputnik
12
+ include DataMapper::Resource
13
+
14
+ property :id, Serial
15
+ property :name, DM::Text
16
+ end
17
+ end
18
+
19
+ it "#upgrade_model should work" do
20
+ @adapter.destroy_model_storage(repository(:postgres), Sputnik)
21
+ @adapter.storage_exists?("sputniks").should be_false
22
+ Sputnik.auto_migrate!(:postgres)
23
+ @adapter.storage_exists?("sputniks").should be_true
24
+ @adapter.field_exists?("sputniks", "new_prop").should be_false
25
+ Sputnik.property :new_prop, DM::Serial
26
+ @adapter.send(:drop_sequence, repository(:postgres), Sputnik.new_prop)
27
+ Sputnik.auto_upgrade!(:postgres)
28
+ @adapter.field_exists?("sputniks", "new_prop").should == true
29
+ end
30
+ end
31
+
32
+ describe '#312' do
33
+ it "should behave sanely for time fields" do
34
+
35
+ class Thing
36
+ include DataMapper::Resource
37
+ property :id, Integer, :serial => true
38
+ property :created_at, Time
39
+ end
40
+
41
+ Thing.auto_migrate!(:postgres)
42
+
43
+ repository(:postgres) do
44
+ time_now = Time.now
45
+
46
+ t = Thing.new
47
+ t.created_at = time_now
48
+
49
+ t.save
50
+
51
+ t1 = Thing.first
52
+ t1.created_at.should == time_now
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ describe "querying metadata" do
59
+ before :all do
60
+ class Sputnik
61
+ include DataMapper::Resource
62
+
63
+ property :id, Serial
64
+ property :name, DM::Text
65
+ end
66
+
67
+ Sputnik.auto_migrate!(:postgres)
68
+ end
69
+
70
+ it "#storage_exists? should return true for tables that exist" do
71
+ @adapter.storage_exists?("sputniks").should == true
72
+ end
73
+
74
+ it "#storage_exists? should return false for tables that don't exist" do
75
+ @adapter.storage_exists?("space turds").should == false
76
+ end
77
+
78
+ it "#field_exists? should return true for columns that exist" do
79
+ @adapter.field_exists?("sputniks", "name").should == true
80
+ end
81
+
82
+ it "#field_exists? should return false for columns that don't exist" do
83
+ @adapter.field_exists?("sputniks", "plur").should == false
84
+ end
85
+ end
86
+
87
+ describe "handling transactions" do
88
+ before :all do
89
+ class Sputnik
90
+ include DataMapper::Resource
91
+
92
+ property :id, Serial
93
+ property :name, DM::Text
94
+ end
95
+
96
+ Sputnik.auto_migrate!(:postgres)
97
+ end
98
+
99
+ before do
100
+ @transaction = DataMapper::Transaction.new(@adapter)
101
+ end
102
+
103
+ it "should rollback changes when #rollback_transaction is called" do
104
+ @transaction.commit do |trans|
105
+ @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')")
106
+ trans.rollback
107
+ end
108
+ @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").empty?.should == true
109
+ end
110
+
111
+ it "should commit changes when #commit_transaction is called" do
112
+ @transaction.commit do
113
+ @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')")
114
+ end
115
+ @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").size.should == 1
116
+ end
117
+ end
118
+
119
+ describe "reading & writing a database" do
120
+ before :all do
121
+ class User
122
+ include DataMapper::Resource
123
+
124
+ property :id, Serial
125
+ property :name, DM::Text
126
+ end
127
+
128
+ class Voyager
129
+ include DataMapper::Resource
130
+ storage_names[:postgres] = 'sattellites.voyagers'
131
+
132
+ property :id, Serial
133
+ property :age, Integer
134
+ end
135
+
136
+ # Voyager.auto_migrate!(:postgres)
137
+ end
138
+
139
+ before do
140
+ User.auto_migrate!(:postgres)
141
+
142
+ @adapter.execute("INSERT INTO users (name) VALUES ('Paul')")
143
+ end
144
+
145
+ it "should be able to specify a schema name as part of the storage name" do
146
+ pending "This works, but no create-schema support in PostgresAdapter to easily test with"
147
+ lambda do
148
+ repository(:postgres) do
149
+ Voyager.create(:age => 1_000)
150
+ end
151
+ end.should_not raise_error
152
+ end
153
+
154
+ it 'should be able to #execute an arbitrary query' do
155
+ result = @adapter.execute("INSERT INTO users (name) VALUES ('Sam')")
156
+
157
+ result.affected_rows.should == 1
158
+ end
159
+
160
+ it 'should be able to #query' do
161
+ result = @adapter.query("SELECT * FROM users")
162
+
163
+ result.should be_kind_of(Array)
164
+ row = result.first
165
+ row.should be_kind_of(Struct)
166
+ row.members.should == %w{id name}
167
+
168
+ row.id.should == 1
169
+ row.name.should == 'Paul'
170
+ end
171
+
172
+ it 'should return an empty array if #query found no rows' do
173
+ @adapter.execute("DELETE FROM users")
174
+
175
+ result = nil
176
+ lambda { result = @adapter.query("SELECT * FROM users") }.should_not raise_error
177
+
178
+ result.should be_kind_of(Array)
179
+ result.size.should == 0
180
+ end
181
+ end
182
+
183
+ describe "CRUD for serial Key" do
184
+ before :all do
185
+ class VideoGame
186
+ include DataMapper::Resource
187
+
188
+ property :id, Serial
189
+ property :name, String
190
+ property :object, Object
191
+ end
192
+ end
193
+
194
+ before do
195
+ VideoGame.auto_migrate!(:postgres)
196
+ end
197
+
198
+ it 'should be able to create a record' do
199
+ time = Time.now
200
+ game = VideoGame.new(:name => 'System Shock', :object => time)
201
+ repository(:postgres) do
202
+ game.save
203
+ game.should_not be_a_new_record
204
+ game.should_not be_dirty
205
+
206
+ saved = VideoGame.first(:name => game.name)
207
+ saved.id.should == game.id
208
+ saved.object.should == time
209
+ end
210
+ end
211
+
212
+ it 'should be able to read a record' do
213
+ name = 'Wing Commander: Privateer'
214
+ id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?) RETURNING id', name).insert_id
215
+
216
+ game = repository(:postgres) do
217
+ VideoGame.get(id)
218
+ end
219
+
220
+ game.name.should == name
221
+ game.should_not be_dirty
222
+ game.should_not be_a_new_record
223
+ end
224
+
225
+ it 'should be able to update a record' do
226
+ name = 'Resistance: Fall of Mon'
227
+ id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?) RETURNING id', name).insert_id
228
+
229
+ game = repository(:postgres) do
230
+ VideoGame.get(id)
231
+ end
232
+
233
+ game.should_not be_a_new_record
234
+
235
+ game.should_not be_dirty
236
+ game.name = game.name.sub(/Mon/, 'Man')
237
+ game.should be_dirty
238
+
239
+ repository(:postgres) do
240
+ game.save
241
+ end
242
+
243
+ game.should_not be_dirty
244
+
245
+ clone = repository(:postgres) do
246
+ VideoGame.get(id)
247
+ end
248
+
249
+ clone.name.should == game.name
250
+ end
251
+
252
+ it 'should be able to delete a record' do
253
+ name = 'Zelda'
254
+ id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?) RETURNING id', name).insert_id
255
+
256
+ game = repository(:postgres) do
257
+ VideoGame.get(id)
258
+ end
259
+
260
+ game.name.should == name
261
+
262
+ repository(:postgres) do
263
+ game.destroy.should be_true
264
+ end
265
+
266
+ game.should be_a_new_record
267
+ game.should be_dirty
268
+ end
269
+
270
+ it 'should respond to Resource#get' do
271
+ name = 'Contra'
272
+ id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?) RETURNING id', name).insert_id
273
+
274
+ contra = repository(:postgres) { VideoGame.get(id) }
275
+
276
+ contra.should_not be_nil
277
+ contra.should_not be_dirty
278
+ contra.should_not be_a_new_record
279
+ contra.id.should == id
280
+ end
281
+ end
282
+
283
+ describe "CRUD for Composite Key" do
284
+ before :all do
285
+ class BankCustomer
286
+ include DataMapper::Resource
287
+
288
+ property :bank, String, :key => true
289
+ property :account_number, String, :key => true
290
+ property :name, String
291
+ end
292
+ end
293
+
294
+ before do
295
+ BankCustomer.auto_migrate!(:postgres)
296
+ end
297
+
298
+ it 'should be able to create a record' do
299
+ customer = BankCustomer.new(:bank => 'Community Bank', :account_number => '123456', :name => 'David Hasselhoff')
300
+ repository(:postgres) do
301
+ customer.save
302
+ end
303
+
304
+ customer.should_not be_a_new_record
305
+ customer.should_not be_dirty
306
+
307
+ row = @adapter.query('SELECT "bank", "account_number" FROM "bank_customers" WHERE "name" = ?', customer.name).first
308
+ row.bank.should == customer.bank
309
+ row.account_number.should == customer.account_number
310
+ end
311
+
312
+ it 'should be able to read a record' do
313
+ bank, account_number, name = 'Chase', '4321', 'Super Wonderful'
314
+ @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name)
315
+
316
+ repository(:postgres) do
317
+ BankCustomer.get(bank, account_number).name.should == name
318
+ end
319
+ end
320
+
321
+ it 'should be able to update a record' do
322
+ bank, account_number, name = 'Wells Fargo', '00101001', 'Spider Pig'
323
+ @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name)
324
+
325
+ customer = repository(:postgres) do
326
+ BankCustomer.get(bank, account_number)
327
+ end
328
+
329
+ customer.name = 'Bat-Pig'
330
+
331
+ customer.should_not be_a_new_record
332
+ customer.should be_dirty
333
+
334
+ customer.save
335
+
336
+ customer.should_not be_dirty
337
+
338
+ clone = repository(:postgres) do
339
+ BankCustomer.get(bank, account_number)
340
+ end
341
+
342
+ clone.name.should == customer.name
343
+ end
344
+
345
+ it 'should be able to delete a record' do
346
+ bank, account_number, name = 'Megacorp', 'ABC', 'Flash Gordon'
347
+ @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name)
348
+
349
+ customer = repository(:postgres) do
350
+ BankCustomer.get(bank, account_number)
351
+ end
352
+
353
+ customer.name.should == name
354
+
355
+ repository(:postgres) do
356
+ customer.destroy.should be_true
357
+ end
358
+
359
+ customer.should be_a_new_record
360
+ customer.should be_dirty
361
+ end
362
+
363
+ it 'should respond to Resource#get' do
364
+ bank, account_number, name = 'Conchords', '1100101', 'Robo Boogie'
365
+ @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name)
366
+
367
+ robots = repository(:postgres) { BankCustomer.get(bank, account_number) }
368
+
369
+ robots.should_not be_nil
370
+ robots.should_not be_dirty
371
+ robots.should_not be_a_new_record
372
+ robots.bank.should == bank
373
+ robots.account_number.should == account_number
374
+ end
375
+ end
376
+
377
+ describe "Ordering a Query" do
378
+ before :all do
379
+ class SailBoat
380
+ include DataMapper::Resource
381
+ property :id, Serial
382
+ property :name, String
383
+ property :port, String
384
+ end
385
+ end
386
+
387
+ before do
388
+ SailBoat.auto_migrate!(:postgres)
389
+
390
+ repository(:postgres) do
391
+ SailBoat.create(:id => 1, :name => "A", :port => "C")
392
+ SailBoat.create(:id => 2, :name => "B", :port => "B")
393
+ SailBoat.create(:id => 3, :name => "C", :port => "A")
394
+ end
395
+ end
396
+
397
+ it "should order results" do
398
+ repository(:postgres) do
399
+ result = SailBoat.all(:order => [
400
+ DataMapper::Query::Direction.new(SailBoat.properties[:name], :asc)
401
+ ])
402
+ result[0].id.should == 1
403
+
404
+ result = SailBoat.all(:order => [
405
+ DataMapper::Query::Direction.new(SailBoat.properties[:port], :asc)
406
+ ])
407
+ result[0].id.should == 3
408
+
409
+ result = SailBoat.all(:order => [
410
+ DataMapper::Query::Direction.new(SailBoat.properties[:name], :asc),
411
+ DataMapper::Query::Direction.new(SailBoat.properties[:port], :asc)
412
+ ])
413
+ result[0].id.should == 1
414
+
415
+ result = SailBoat.all(:order => [
416
+ SailBoat.properties[:name],
417
+ DataMapper::Query::Direction.new(SailBoat.properties[:port], :asc)
418
+ ])
419
+ result[0].id.should == 1
420
+ end
421
+ end
422
+ end
423
+
424
+ describe "Lazy Loaded Properties" do
425
+ before :all do
426
+ class SailBoat
427
+ include DataMapper::Resource
428
+ property :id, Serial
429
+ property :notes, String, :lazy => [:notes]
430
+ property :trip_report, String, :lazy => [:notes,:trip]
431
+ property :miles, Integer, :lazy => [:trip]
432
+ end
433
+ end
434
+
435
+ before do
436
+ SailBoat.auto_migrate!(:postgres)
437
+
438
+ repository(:postgres) do
439
+ SailBoat.create(:id => 1, :notes=>'Note',:trip_report=>'Report',:miles=>23)
440
+ SailBoat.create(:id => 2, :notes=>'Note',:trip_report=>'Report',:miles=>23)
441
+ SailBoat.create(:id => 3, :notes=>'Note',:trip_report=>'Report',:miles=>23)
442
+ end
443
+ end
444
+
445
+ it "should lazy load" do
446
+ result = repository(:postgres) { SailBoat.all.to_a }
447
+
448
+ result[0].attribute_loaded?(:notes).should be_false
449
+ result[0].attribute_loaded?(:trip_report).should be_false
450
+ result[1].attribute_loaded?(:notes).should be_false
451
+
452
+ result[1].notes.should_not be_nil
453
+
454
+ result[1].attribute_loaded?(:notes).should be_true
455
+ result[1].attribute_loaded?(:trip_report).should be_true
456
+ result[1].attribute_loaded?(:miles).should be_false
457
+
458
+ result = repository(:postgres) { SailBoat.all.to_a }
459
+
460
+ result[0].attribute_loaded?(:trip_report).should be_false
461
+ result[0].attribute_loaded?(:miles).should be_false
462
+
463
+ result[1].trip_report.should_not be_nil
464
+ result[2].attribute_loaded?(:miles).should be_true
465
+ end
466
+ end
467
+
468
+ describe "finders" do
469
+ before :all do
470
+ class SerialFinderSpec
471
+ include DataMapper::Resource
472
+
473
+ property :id, Serial
474
+ property :sample, String
475
+ end
476
+ end
477
+
478
+ before do
479
+ SerialFinderSpec.auto_migrate!(:postgres)
480
+
481
+ repository(:postgres) do
482
+ 100.times do
483
+ SerialFinderSpec.create(:sample => rand.to_s)
484
+ end
485
+ end
486
+ end
487
+
488
+ it "should return all available rows" do
489
+ repository(:postgres) do
490
+ SerialFinderSpec.all.should have(100).entries
491
+ end
492
+ end
493
+
494
+ it "should allow limit and offset" do
495
+ repository(:postgres) do
496
+ SerialFinderSpec.all(:limit => 50).should have(50).entries
497
+
498
+ SerialFinderSpec.all(:limit => 20, :offset => 40).map { |entry| entry.id }.should == SerialFinderSpec.all[40...60].map { |entry| entry.id }
499
+ end
500
+ end
501
+
502
+ it "should lazy-load missing attributes" do
503
+ sfs = repository(:postgres) do
504
+ SerialFinderSpec.first(:fields => [ :id ])
505
+ end
506
+
507
+ sfs.should be_a_kind_of(SerialFinderSpec)
508
+ sfs.should_not be_a_new_record
509
+
510
+ sfs.attribute_loaded?(:sample).should be_false
511
+ sfs.sample
512
+ sfs.attribute_loaded?(:sample).should be_true
513
+ end
514
+
515
+ it "should translate an Array to an IN clause" do
516
+ ids = repository(:postgres) do
517
+ SerialFinderSpec.all(:limit => 10).map { |entry| entry.id }
518
+ end
519
+
520
+ results = repository(:postgres) do
521
+ SerialFinderSpec.all(:id => ids)
522
+ end
523
+
524
+ results.size.should == 10
525
+ results.map { |entry| entry.id }.should == ids
526
+ end
527
+ end
528
+
529
+ describe "belongs_to associations" do
530
+ before :all do
531
+ class Engine
532
+ include DataMapper::Resource
533
+ def self.default_repository_name; :postgres end
534
+
535
+ property :id, Serial
536
+ property :name, String
537
+ end
538
+
539
+ class Yard
540
+ include DataMapper::Resource
541
+ def self.default_repository_name; :postgres end
542
+
543
+ property :id, Serial
544
+ property :name, String
545
+ property :engine_id, Integer
546
+
547
+ belongs_to :engine
548
+ end
549
+ end
550
+
551
+ before do
552
+ Engine.auto_migrate!(:postgres)
553
+
554
+ @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 1, 'engine1')
555
+ @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 2, 'engine2')
556
+
557
+ Yard.auto_migrate!(:postgres)
558
+
559
+ @adapter.execute('INSERT INTO "yards" ("id", "name", "engine_id") values (?, ?, ?)', 1, 'yard1', 1)
560
+ end
561
+
562
+ it "should load without the parent"
563
+
564
+ it 'should allow substituting the parent' do
565
+ repository(:postgres) do
566
+ y = Yard.first(:id => 1)
567
+ e = Engine.first(:id => 2)
568
+ y.engine = e
569
+ y.save
570
+ end
571
+
572
+ repository(:postgres) do
573
+ Yard.first(:id => 1).engine_id.should == 2
574
+ end
575
+ end
576
+
577
+ it "#belongs_to" do
578
+ yard = Yard.new
579
+ yard.should respond_to(:engine)
580
+ yard.should respond_to(:engine=)
581
+ end
582
+
583
+ it "should load the associated instance" do
584
+ y = repository(:postgres) do
585
+ Yard.first(:id => 1)
586
+ end
587
+ y.engine.should_not be_nil
588
+ y.engine.id.should == 1
589
+ y.engine.name.should == "engine1"
590
+ end
591
+
592
+ it 'should save the association key in the child' do
593
+ repository(:postgres) do
594
+ e = Engine.first(:id => 2)
595
+ Yard.create(:id => 2, :name => 'yard2', :engine => e)
596
+ end
597
+
598
+ repository(:postgres) do
599
+ Yard.first(:id => 2).engine_id.should == 2
600
+ end
601
+ end
602
+
603
+ it 'should save the parent upon saving of child' do
604
+ repository(:postgres) do
605
+ e = Engine.new(:id => 10, :name => "engine10")
606
+ y = Yard.new(:id => 10, :name => "Yard10", :engine => e)
607
+ y.save
608
+
609
+ y.engine_id.should == 10
610
+ end
611
+
612
+ repository(:postgres) do
613
+ Engine.first(:id => 10).should_not be_nil
614
+ end
615
+ end
616
+ end
617
+
618
+ describe "has n associations" do
619
+ before :all do
620
+ class Host
621
+ include DataMapper::Resource
622
+ def self.default_repository_name; :postgres end
623
+
624
+ property :id, Serial
625
+ property :name, String
626
+
627
+ has n, :slices
628
+ end
629
+
630
+ class Slice
631
+ include DataMapper::Resource
632
+ def self.default_repository_name; :postgres end
633
+
634
+ property :id, Serial
635
+ property :name, String
636
+ property :host_id, Integer
637
+
638
+ belongs_to :host
639
+ end
640
+ end
641
+
642
+ before do
643
+ Host.auto_migrate!(:postgres)
644
+ Slice.auto_migrate!(:postgres)
645
+
646
+ @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 1, 'host1')
647
+ @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 2, 'host2')
648
+
649
+ @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 1, 'slice1', 1)
650
+ @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 2, 'slice2', 1)
651
+ end
652
+
653
+ it "#has n" do
654
+ h = Host.new
655
+ h.should respond_to(:slices)
656
+ end
657
+
658
+ it "should allow removal of a child through a loaded association" do
659
+ h = repository(:postgres) do
660
+ Host.first(:id => 1)
661
+ end
662
+
663
+ s = h.slices.first
664
+
665
+ h.slices.delete(s)
666
+ h.slices.size.should == 1
667
+ h.save
668
+
669
+ s = repository(:postgres) do
670
+ Slice.first(:id => s.id)
671
+ end
672
+
673
+ s.host.should be_nil
674
+ s.host_id.should be_nil
675
+ end
676
+
677
+ it "should load the associated instances" do
678
+ h = repository(:postgres) do
679
+ Host.first(:id => 1)
680
+ end
681
+
682
+ h.slices.should_not be_nil
683
+ h.slices.size.should == 2
684
+ h.slices.first.id.should == 1
685
+ h.slices.last.id.should == 2
686
+ end
687
+
688
+ it "should add and save the associated instance" do
689
+ repository(:postgres) do
690
+ h = Host.first(:id => 1)
691
+
692
+ h.slices << Slice.new(:id => 3, :name => 'slice3')
693
+ h.save
694
+
695
+ s = repository(:postgres) do
696
+ Slice.first(:id => 3)
697
+ end
698
+
699
+ s.host.id.should == 1
700
+ end
701
+ end
702
+
703
+ it "should not save the associated instance if the parent is not saved" do
704
+ repository(:postgres) do
705
+ h = Host.new(:id => 10, :name => "host10")
706
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
707
+ end
708
+
709
+ repository(:postgres) do
710
+ Slice.first(:id => 10).should be_nil
711
+ end
712
+ end
713
+
714
+ it "should save the associated instance upon saving of parent" do
715
+ repository(:postgres) do
716
+ h = Host.new(:id => 10, :name => "host10")
717
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
718
+ h.save
719
+ end
720
+
721
+ s = repository(:postgres) do
722
+ Slice.first(:id => 10)
723
+ end
724
+
725
+ s.should_not be_nil
726
+ s.host.should_not be_nil
727
+ s.host.id.should == 10
728
+ end
729
+ end
730
+ end
731
+ end