dm-core 0.9.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.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -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,68 @@
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
+ end
17
+
18
+ describe "DataMapper::Model with #{ADAPTER}" do
19
+ before do
20
+ repository(ADAPTER) do
21
+ ModelSpec::STI.auto_migrate!
22
+ end
23
+ end
24
+
25
+ describe '.new' do
26
+ before :all do
27
+ @planet = DataMapper::Model.new('planet') do
28
+ property :name, String, :key => true
29
+ property :distance, Integer
30
+ end
31
+
32
+ @planet.auto_migrate!(ADAPTER)
33
+ end
34
+
35
+ it 'should be able to persist' do
36
+ repository(ADAPTER) do
37
+ pluto = @planet.new
38
+ pluto.name = 'Pluto'
39
+ pluto.distance = 1_000_000
40
+ pluto.save
41
+
42
+ clone = @planet.get!('Pluto')
43
+ clone.name.should == 'Pluto'
44
+ clone.distance.should == 1_000_000
45
+ end
46
+ end
47
+ end
48
+
49
+ it 'should provide #load' do
50
+ ModelSpec::STI.should respond_to(:load)
51
+ end
52
+
53
+ describe '#load' do
54
+ it 'should load resources with nil discriminator fields' do
55
+ resource = ModelSpec::STI.create(:name => 'resource')
56
+ query = ModelSpec::STI.all.query
57
+ fields = query.fields
58
+
59
+ fields.should == ModelSpec::STI.properties(ADAPTER).slice(:id, :name, :type)
60
+
61
+ # would blow up prior to fix
62
+ lambda {
63
+ ModelSpec::STI.load([ resource.id, resource.name, nil ], query)
64
+ }.should_not raise_error(NoMethodError)
65
+ end
66
+ end
67
+ end
68
+ 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,732 @@
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.instance_variables.should_not include('@sample')
511
+ sfs.sample.should_not be_nil
512
+ end
513
+
514
+ it "should translate an Array to an IN clause" do
515
+ ids = repository(:postgres) do
516
+ SerialFinderSpec.all(:limit => 10).map { |entry| entry.id }
517
+ end
518
+
519
+ results = repository(:postgres) do
520
+ SerialFinderSpec.all(:id => ids)
521
+ end
522
+
523
+ results.size.should == 10
524
+ results.map { |entry| entry.id }.should == ids
525
+ end
526
+ end
527
+
528
+ describe "belongs_to associations" do
529
+ before :all do
530
+ class Engine
531
+ include DataMapper::Resource
532
+
533
+ property :id, Serial
534
+ property :name, String
535
+ end
536
+
537
+ class Yard
538
+ include DataMapper::Resource
539
+
540
+ property :id, Serial
541
+ property :name, String
542
+ property :engine_id, Integer
543
+
544
+ repository(:postgres) do
545
+ belongs_to :engine
546
+ end
547
+ end
548
+ end
549
+
550
+ before do
551
+ Engine.auto_migrate!(:postgres)
552
+
553
+ @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 1, 'engine1')
554
+ @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 2, 'engine2')
555
+
556
+ Yard.auto_migrate!(:postgres)
557
+
558
+ @adapter.execute('INSERT INTO "yards" ("id", "name", "engine_id") values (?, ?, ?)', 1, 'yard1', 1)
559
+ end
560
+
561
+ it "should load without the parent"
562
+
563
+ it 'should allow substituting the parent' do
564
+ repository(:postgres) do
565
+ y = Yard.first(:id => 1)
566
+ e = Engine.first(:id => 2)
567
+ y.engine = e
568
+ y.save
569
+ end
570
+
571
+ repository(:postgres) do
572
+ Yard.first(:id => 1).engine_id.should == 2
573
+ end
574
+ end
575
+
576
+ it "#belongs_to" do
577
+ yard = Yard.new
578
+ yard.should respond_to(:engine)
579
+ yard.should respond_to(:engine=)
580
+ end
581
+
582
+ it "should load the associated instance" do
583
+ y = repository(:postgres) do
584
+ Yard.first(:id => 1)
585
+ end
586
+ y.engine.should_not be_nil
587
+ y.engine.id.should == 1
588
+ y.engine.name.should == "engine1"
589
+ end
590
+
591
+ it 'should save the association key in the child' do
592
+ repository(:postgres) do
593
+ e = Engine.first(:id => 2)
594
+ Yard.create!(:id => 2, :name => 'yard2', :engine => e)
595
+ end
596
+
597
+ repository(:postgres) do
598
+ Yard.first(:id => 2).engine_id.should == 2
599
+ end
600
+ end
601
+
602
+ it 'should save the parent upon saving of child' do
603
+ repository(:postgres) do
604
+ e = Engine.new(:id => 10, :name => "engine10")
605
+ y = Yard.new(:id => 10, :name => "Yard10", :engine => e)
606
+ y.save
607
+
608
+ y.engine_id.should == 10
609
+ end
610
+
611
+ repository(:postgres) do
612
+ Engine.first(:id => 10).should_not be_nil
613
+ end
614
+ end
615
+ end
616
+
617
+ describe "has n associations" do
618
+ before :all do
619
+ class Host
620
+ include DataMapper::Resource
621
+
622
+ property :id, Serial
623
+ property :name, String
624
+
625
+ repository(:postgres) do
626
+ has n, :slices
627
+ end
628
+ end
629
+
630
+ class Slice
631
+ include DataMapper::Resource
632
+
633
+ property :id, Serial
634
+ property :name, String
635
+ property :host_id, Integer
636
+
637
+ repository(:postgres) do
638
+ belongs_to :host
639
+ end
640
+ end
641
+ end
642
+
643
+ before do
644
+ Host.auto_migrate!(:postgres)
645
+ Slice.auto_migrate!(:postgres)
646
+
647
+ @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 1, 'host1')
648
+ @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 2, 'host2')
649
+
650
+ @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 1, 'slice1', 1)
651
+ @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 2, 'slice2', 1)
652
+ end
653
+
654
+ it "#has n" do
655
+ h = Host.new
656
+ h.should respond_to(:slices)
657
+ end
658
+
659
+ it "should allow removal of a child through a loaded association" do
660
+ h = repository(:postgres) do
661
+ Host.first(:id => 1)
662
+ end
663
+
664
+ s = h.slices.first
665
+
666
+ h.slices.delete(s)
667
+ h.slices.size.should == 1
668
+ h.save
669
+
670
+ s = repository(:postgres) do
671
+ Slice.first(:id => s.id)
672
+ end
673
+
674
+ s.host.should be_nil
675
+ s.host_id.should be_nil
676
+ end
677
+
678
+ it "should load the associated instances" do
679
+ h = repository(:postgres) do
680
+ Host.first(:id => 1)
681
+ end
682
+
683
+ h.slices.should_not be_nil
684
+ h.slices.size.should == 2
685
+ h.slices.first.id.should == 1
686
+ h.slices.last.id.should == 2
687
+ end
688
+
689
+ it "should add and save the associated instance" do
690
+ repository(:postgres) do
691
+ h = Host.first(:id => 1)
692
+
693
+ h.slices << Slice.new(:id => 3, :name => 'slice3')
694
+ h.save
695
+
696
+ s = repository(:postgres) do
697
+ Slice.first(:id => 3)
698
+ end
699
+
700
+ s.host.id.should == 1
701
+ end
702
+ end
703
+
704
+ it "should not save the associated instance if the parent is not saved" do
705
+ repository(:postgres) do
706
+ h = Host.new(:id => 10, :name => "host10")
707
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
708
+ end
709
+
710
+ repository(:postgres) do
711
+ Slice.first(:id => 10).should be_nil
712
+ end
713
+ end
714
+
715
+ it "should save the associated instance upon saving of parent" do
716
+ repository(:postgres) do
717
+ h = Host.new(:id => 10, :name => "host10")
718
+ h.slices << Slice.new(:id => 10, :name => 'slice10')
719
+ h.save
720
+ end
721
+
722
+ s = repository(:postgres) do
723
+ Slice.first(:id => 10)
724
+ end
725
+
726
+ s.should_not be_nil
727
+ s.host.should_not be_nil
728
+ s.host.id.should == 10
729
+ end
730
+ end
731
+ end
732
+ end