dm-core 0.9.2

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