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,398 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+ require 'ostruct'
4
+
5
+ TODAY = Date.today
6
+ NOW = DateTime.now
7
+
8
+ TIME_STRING_1 = '2007-04-21 04:14:12'
9
+ TIME_STRING_2 = '2007-04-21 04:14:12.1'
10
+ TIME_STRING_3 = '2007-04-21 04:14:12.01'
11
+ TIME_STRING_4 = '2007-04-21 04:14:12.123456'
12
+
13
+ TIME_1 = Time.parse(TIME_STRING_1)
14
+ TIME_2 = Time.parse(TIME_STRING_2)
15
+ TIME_3 = Time.parse(TIME_STRING_3)
16
+ TIME_4 = Time.parse(TIME_STRING_4)
17
+
18
+ class EveryType
19
+ include DataMapper::Resource
20
+
21
+ property :serial, Serial
22
+ property :fixnum, Integer, :nullable => false, :default => 1
23
+ property :string, String, :nullable => false, :default => 'default'
24
+ property :empty, String, :nullable => false, :default => ''
25
+ property :date, Date, :nullable => false, :default => TODAY, :index => :date_date_time, :unique_index => :date_float
26
+ property :true_class, TrueClass, :nullable => false, :default => true
27
+ property :false_class, TrueClass, :nullable => false, :default => false
28
+ property :text, DM::Text, :nullable => false, :default => 'text'
29
+ # property :class, Class, :nullable => false, :default => Class # FIXME: Class types cause infinite recursions in Resource
30
+ property :big_decimal, BigDecimal, :nullable => false, :default => BigDecimal('1.1'), :precision => 2, :scale => 1
31
+ property :float, Float, :nullable => false, :default => 1.1, :precision => 2, :scale => 1, :unique_index => :date_float
32
+ property :date_time, DateTime, :nullable => false, :default => NOW, :index => [:date_date_time, true]
33
+ property :time_1, Time, :nullable => false, :default => TIME_1, :unique_index => true
34
+ property :time_2, Time, :nullable => false, :default => TIME_2
35
+ property :time_3, Time, :nullable => false, :default => TIME_3
36
+ property :time_4, Time, :nullable => false, :default => TIME_4
37
+ property :object, Object, :nullable => true # FIXME: cannot supply a default for Object
38
+ property :discriminator, DM::Discriminator
39
+ end
40
+
41
+ module Publications
42
+ class ShortStoryCollection
43
+ include DataMapper::Resource
44
+ property :serial, Serial
45
+ property :date, Date, :nullable => false, :default => TODAY, :index => :date_date_time
46
+ end
47
+ end
48
+
49
+ if HAS_SQLITE3
50
+ describe DataMapper::AutoMigrations, '.auto_migrate! with sqlite3' do
51
+ before :all do
52
+ @adapter = repository(:sqlite3).adapter
53
+
54
+ DataMapper::Resource.descendants.clear
55
+
56
+ @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
57
+ end
58
+
59
+ after :all do
60
+ DataMapper::Resource.descendants.clear
61
+ end
62
+
63
+ describe 'with sqlite3' do
64
+ before :all do
65
+ EveryType.auto_migrate!(:sqlite3).should be_true
66
+
67
+ @table_set = @adapter.query('PRAGMA table_info(?)', 'every_types').inject({}) do |ts,column|
68
+ default = if 'NULL' == column.dflt_value || column.dflt_value.nil?
69
+ nil
70
+ else
71
+ /^(['"]?)(.*)\1$/.match(column.dflt_value)[2]
72
+ end
73
+
74
+ property = @property_class.new(
75
+ column.name,
76
+ column.type.upcase,
77
+ column.notnull == 0,
78
+ default,
79
+ column.pk == 1 # in SQLite3 the serial key is also primary
80
+ )
81
+
82
+ ts.update(property.name => property)
83
+ end
84
+
85
+ @index_list = @adapter.query('PRAGMA index_list(?)', 'every_types')
86
+
87
+ # bypass DM to create the record using only the column default values
88
+ @adapter.execute('INSERT INTO "every_types" ("serial", "discriminator") VALUES (?, ?)', 1, EveryType)
89
+
90
+ @book = repository(:sqlite3) { EveryType.first }
91
+ end
92
+
93
+ types = {
94
+ :serial => [ Integer, 'INTEGER', false, nil, 1, true ],
95
+ :fixnum => [ Integer, 'INTEGER', false, '1', 1, false ],
96
+ :string => [ String, 'VARCHAR(50)', false, 'default', 'default', false ],
97
+ :empty => [ String, 'VARCHAR(50)', false, '', '' , false ],
98
+ :date => [ Date, 'DATE', false, TODAY.strftime('%Y-%m-%d'), TODAY, false ],
99
+ :true_class => [ TrueClass, 'BOOLEAN', false, 't', true, false ],
100
+ :false_class => [ TrueClass, 'BOOLEAN', false, 'f', false, false ],
101
+ :text => [ DM::Text, 'TEXT', false, 'text', 'text', false ],
102
+ # :class => [ Class, 'VARCHAR(50)', false, 'Class', 'Class', false ],
103
+ :big_decimal => [ BigDecimal, 'DECIMAL(2,1)', false, '1.1', BigDecimal('1.1'), false ],
104
+ :float => [ Float, 'FLOAT(2,1)', false, '1.1', 1.1, false ],
105
+ :date_time => [ DateTime, 'DATETIME', false, NOW.strftime('%Y-%m-%d %H:%M:%S'), NOW, false ],
106
+ :time_1 => [ Time, 'TIMESTAMP', false, TIME_STRING_1, TIME_1, false ],
107
+ #SQLite pads out the microseconds to the full 6 digits no matter what the value is - we simply pad up the zeros needed
108
+ :time_2 => [ Time, 'TIMESTAMP', false, TIME_STRING_2.dup << '00000', TIME_2, false ],
109
+ :time_3 => [ Time, 'TIMESTAMP', false, TIME_STRING_3.dup << '0000', TIME_3, false ],
110
+ :time_4 => [ Time, 'TIMESTAMP', false, TIME_STRING_4, TIME_4, false ],
111
+ :object => [ Object, 'TEXT', true, nil, nil, false ],
112
+ :discriminator => [ DM::Discriminator, 'VARCHAR(50)', false, nil, EveryType, false ],
113
+ }
114
+
115
+ types.each do |name,(klass,type,nullable,default,key)|
116
+ describe "a #{klass} property" do
117
+ it "should be created as a #{type}" do
118
+ @table_set[name.to_s].type.should == type
119
+ end
120
+
121
+ it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
122
+ @table_set[name.to_s].nullable.should == nullable
123
+ end
124
+
125
+ it "should have a default value #{default.inspect}" do
126
+ @table_set[name.to_s].default.should == default
127
+ end
128
+
129
+ expected_value = types[name][4]
130
+ it 'should properly typecast value' do
131
+ if DateTime == klass
132
+ @book.send(name).to_s.should == expected_value.to_s
133
+ else
134
+ @book.send(name).should == expected_value
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
141
+ @index_list.size.should == 4
142
+
143
+ expected_indices = {
144
+ "unique_index_every_types_date_float" => 1,
145
+ "unique_index_every_types_time_1" => 1,
146
+ "index_every_types_date_date_time" => 0,
147
+ "index_every_types_date_time" => 0
148
+ }
149
+
150
+ @index_list.each do |index|
151
+ expected_indices.should have_key(index.name)
152
+ expected_indices[index.name].should == index.unique
153
+ end
154
+ end
155
+
156
+ it 'should escape a namespaced model' do
157
+ Publications::ShortStoryCollection.auto_migrate!(:sqlite3).should be_true
158
+ @adapter.query('SELECT "name" FROM "sqlite_master" WHERE type = ?', 'table').should include('publications_short_story_collections')
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ if HAS_MYSQL
165
+ describe DataMapper::AutoMigrations, '.auto_migrate! with mysql' do
166
+ before :all do
167
+ @adapter = repository(:mysql).adapter
168
+
169
+ DataMapper::Resource.descendants.clear
170
+
171
+ @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
172
+ end
173
+
174
+ after :all do
175
+ DataMapper::Resource.descendants.clear
176
+ end
177
+
178
+ describe 'with mysql' do#
179
+ before :all do
180
+ EveryType.auto_migrate!(:mysql).should be_true
181
+
182
+ @table_set = @adapter.query('DESCRIBE `every_types`').inject({}) do |ts,column|
183
+ property = @property_class.new(
184
+ column.field,
185
+ column.type.upcase,
186
+ column.null == 'YES',
187
+ column.type.upcase == 'TEXT' ? nil : column.default,
188
+ column.extra.split.include?('auto_increment')
189
+ )
190
+
191
+ ts.update(property.name => property)
192
+ end
193
+
194
+ @index_list = @adapter.query('SHOW INDEX FROM `every_types`')
195
+
196
+ # bypass DM to create the record using only the column default values
197
+ @adapter.execute('INSERT INTO `every_types` (`serial`, `text`, `discriminator`) VALUES (?, ?, ?)', 1, 'text', EveryType)
198
+
199
+ @book = repository(:mysql) { EveryType.first }
200
+ end
201
+
202
+ types = {
203
+ :serial => [ Integer, 'INT(11)', false, nil, 1, true ],
204
+ :fixnum => [ Integer, 'INT(11)', false, '1', 1, false ],
205
+ :string => [ String, 'VARCHAR(50)', false, 'default', 'default', false ],
206
+ :empty => [ String, 'VARCHAR(50)', false, '', '', false ],
207
+ :date => [ Date, 'DATE', false, TODAY.strftime('%Y-%m-%d'), TODAY, false ],
208
+ :true_class => [ TrueClass, 'TINYINT(1)', false, '1', true, false ],
209
+ :false_class => [ TrueClass, 'TINYINT(1)', false, '0', false, false ],
210
+ :text => [ DM::Text, 'TEXT', false, nil, 'text', false ],
211
+ # :class => [ Class, 'VARCHAR(50)', false, 'Class', 'Class', false ],
212
+ :big_decimal => [ BigDecimal, 'DECIMAL(2,1)', false, '1.1', BigDecimal('1.1'), false ],
213
+ :float => [ Float, 'FLOAT(2,1)', false, '1.1', 1.1, false ],
214
+ :date_time => [ DateTime, 'DATETIME', false, NOW.strftime('%Y-%m-%d %H:%M:%S'), NOW, false ],
215
+ :time_1 => [ Time, 'TIMESTAMP', false, TIME_1.strftime('%Y-%m-%d %H:%M:%S'), TIME_1, false ],
216
+ :time_2 => [ Time, 'TIMESTAMP', false, TIME_2.strftime('%Y-%m-%d %H:%M:%S'), TIME_2, false ],
217
+ :time_3 => [ Time, 'TIMESTAMP', false, TIME_3.strftime('%Y-%m-%d %H:%M:%S'), TIME_3 , false ],
218
+ :time_4 => [ Time, 'TIMESTAMP', false, TIME_4.strftime('%Y-%m-%d %H:%M:%S'), TIME_4 , false ],
219
+ :object => [ Object, 'TEXT', true, nil, nil, false ],
220
+ :discriminator => [ DM::Discriminator, 'VARCHAR(50)', false, nil, EveryType, false ],
221
+ }
222
+
223
+ types.each do |name,(klass,type,nullable,default,key)|
224
+ describe "a #{klass} property" do
225
+ it "should be created as a #{type}" do
226
+ @table_set[name.to_s].type.should == type
227
+ end
228
+
229
+ it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
230
+ @table_set[name.to_s].nullable.should == nullable
231
+ end
232
+
233
+ it "should have a default value #{default.inspect}" do
234
+ @table_set[name.to_s].default.should == default
235
+ end
236
+
237
+ expected_value = types[name][4]
238
+ it 'should properly typecast value' do
239
+ if DateTime == klass || Time == klass # mysql doesn't support microsecond
240
+ @book.send(name).to_s.should == expected_value.to_s
241
+ else
242
+ @book.send(name).should == expected_value
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
249
+ pending do
250
+ # TODO
251
+ @index_list[0].Key_name.should == 'unique_index_every_types_date_float'
252
+ @index_list[0].Non_unique.should == 0
253
+ @index_list[1].Key_name.should == 'unique_index_every_types_time_1'
254
+ @index_list[1].Non_unique.should == 0
255
+ @index_list[2].Key_name.should == 'index_every_types_date_date_time'
256
+ @index_list[2].Non_unique.should == 1
257
+ @index_list[3].Key_name.should == 'index_every_types_date_time'
258
+ @index_list[3].Non_unique.should == 1
259
+ end
260
+ end
261
+
262
+ it 'should escape a namespaced model' do
263
+ Publications::ShortStoryCollection.auto_migrate!(:mysql).should be_true
264
+ @adapter.query('SHOW TABLES').should include('publications_short_story_collections')
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ if HAS_POSTGRES
271
+ describe DataMapper::AutoMigrations, '.auto_migrate! with postgres' do
272
+ before :all do
273
+ @adapter = repository(:postgres).adapter
274
+
275
+ DataMapper::Resource.descendants.clear
276
+
277
+ @property_class = Struct.new(:name, :type, :nullable, :default, :serial)
278
+ end
279
+
280
+ after :all do
281
+ DataMapper::Resource.descendants.clear
282
+ end
283
+
284
+ describe 'with postgres' do
285
+ before :all do
286
+ EveryType.auto_migrate!(:postgres).should be_true
287
+
288
+ query = <<-EOS
289
+ SELECT
290
+ -- Field
291
+ "pg_attribute"."attname" AS "Field",
292
+ -- Type
293
+ CASE "pg_type"."typname"
294
+ WHEN 'varchar' THEN 'varchar'
295
+ ELSE "pg_type"."typname"
296
+ END AS "Type",
297
+ -- Null
298
+ CASE WHEN "pg_attribute"."attnotnull" THEN ''
299
+ ELSE 'YES'
300
+ END AS "Null",
301
+ -- Default
302
+ "pg_attrdef"."adsrc" AS "Default"
303
+ FROM "pg_class"
304
+ INNER JOIN "pg_attribute"
305
+ ON ("pg_class"."oid" = "pg_attribute"."attrelid")
306
+ INNER JOIN pg_type
307
+ ON ("pg_attribute"."atttypid" = "pg_type"."oid")
308
+ LEFT JOIN "pg_attrdef"
309
+ ON ("pg_class"."oid" = "pg_attrdef"."adrelid" AND "pg_attribute"."attnum" = "pg_attrdef"."adnum")
310
+ WHERE "pg_class"."relname" = ? AND "pg_attribute"."attnum" >= ? AND NOT "pg_attribute"."attisdropped"
311
+ ORDER BY "pg_attribute"."attnum"
312
+ EOS
313
+
314
+ @table_set = @adapter.query(query, 'every_types', 1).inject({}) do |ts,column|
315
+ default = column.default
316
+ serial = false
317
+
318
+ if column.default == "nextval('every_types_serial_seq'::regclass)"
319
+ default = nil
320
+ serial = true
321
+ end
322
+
323
+ property = @property_class.new(
324
+ column.field,
325
+ column.type.upcase,
326
+ column.null == 'YES',
327
+ default,
328
+ serial
329
+ )
330
+
331
+ ts.update(property.name => property)
332
+ end
333
+
334
+ # bypass DM to create the record using only the column default values
335
+ @adapter.execute('INSERT INTO "every_types" ("serial", "discriminator") VALUES (?, ?)', 1, EveryType)
336
+
337
+ @book = repository(:postgres) { EveryType.first }
338
+ end
339
+
340
+ types = {
341
+ :serial => [ Integer, 'INT4', false, nil, 1, true ],
342
+ :fixnum => [ Integer, 'INT4', false, '1', 1, false ],
343
+ :string => [ String, 'VARCHAR', false, "'default'::character varying", 'default', false ],
344
+ :empty => [ String, 'VARCHAR', false, "''::character varying", '', false ],
345
+ :date => [ Date, 'DATE', false, "'#{TODAY.strftime('%Y-%m-%d')}'::date", TODAY, false ],
346
+ :true_class => [ TrueClass, 'BOOL', false, 'true', true, false ],
347
+ :false_class => [ TrueClass, 'BOOL', false, 'false', false, false ],
348
+ :text => [ DM::Text, 'TEXT', false, "'text'::text", 'text', false ],
349
+ # :class => [ Class, 'VARCHAR(50)', false, 'Class', 'Class', false ],
350
+ :big_decimal => [ BigDecimal, 'NUMERIC', false, '1.1', BigDecimal('1.1'), false ],
351
+ :float => [ Float, 'FLOAT8', false, '1.1', 1.1, false ],
352
+ :date_time => [ DateTime, 'TIMESTAMP', false, "'#{NOW.strftime('%Y-%m-%d %H:%M:%S')}'::timestamp without time zone", NOW, false ],
353
+ :time_1 => [ Time, 'TIMESTAMP', false, "'" << TIME_STRING_1.dup << "'::timestamp without time zone", TIME_1, false ],
354
+ #The weird zero here is simply because postgresql seems to want to store .10 instead of .1 for this one
355
+ #affects anything with an exact tenth of a second (i.e. .1, .2, .3, ...)
356
+ :time_2 => [ Time, 'TIMESTAMP', false, "'" << TIME_STRING_2.dup << "0'::timestamp without time zone", TIME_2, false ],
357
+ :time_3 => [ Time, 'TIMESTAMP', false, "'" << TIME_STRING_3.dup << "'::timestamp without time zone", TIME_3, false ],
358
+ :time_4 => [ Time, 'TIMESTAMP', false, "'" << TIME_STRING_4.dup << "'::timestamp without time zone", TIME_4, false ],
359
+ :object => [ Object, 'TEXT', true, nil, nil, false ],
360
+ :discriminator => [ DM::Discriminator, 'VARCHAR', false, nil, EveryType, false ],
361
+ }
362
+
363
+ types.each do |name,(klass,type,nullable,default,key)|
364
+ describe "a #{Extlib::Inflection.classify(name.to_s)} property" do
365
+ it "should be created as a #{type}" do
366
+ @table_set[name.to_s].type.should == type
367
+ end
368
+
369
+ it "should #{!nullable && 'not'} be nullable".squeeze(' ') do
370
+ @table_set[name.to_s].nullable.should == nullable
371
+ end
372
+
373
+ it "should have a default value #{default.inspect}" do
374
+ @table_set[name.to_s].default.should == default
375
+ end
376
+
377
+ expected_value = types[name][4]
378
+ it 'should properly typecast value' do
379
+ if DateTime == klass
380
+ @book.send(name).to_s.should == expected_value.to_s
381
+ else
382
+ @book.send(name).should == expected_value
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ it 'should have 4 indexes: 2 non-unique index, 2 unique index' do
389
+ pending 'TODO'
390
+ end
391
+
392
+ it 'should escape a namespaced model' do
393
+ Publications::ShortStoryCollection.auto_migrate!(:postgres).should be_true
394
+ @adapter.query('SELECT "tablename" FROM "pg_tables" WHERE "tablename" NOT LIKE ?', 'pg_%').should include('publications_short_story_collections')
395
+ end
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,1015 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ if ADAPTER
4
+ class Zebra
5
+ include DataMapper::Resource
6
+
7
+ def self.default_repository_name
8
+ ADAPTER
9
+ end
10
+
11
+ property :id, Serial
12
+ property :name, String
13
+ property :age, Integer
14
+ property :notes, Text
15
+
16
+ has n, :stripes
17
+ end
18
+
19
+ class Stripe
20
+ include DataMapper::Resource
21
+
22
+ def self.default_repository_name
23
+ ADAPTER
24
+ end
25
+
26
+ property :id, Serial
27
+ property :name, String
28
+ property :age, Integer
29
+ property :zebra_id, Integer
30
+
31
+ belongs_to :zebra
32
+ end
33
+
34
+ class CollectionSpecParty
35
+ include DataMapper::Resource
36
+
37
+ def self.default_repository_name
38
+ ADAPTER
39
+ end
40
+
41
+ property :name, String, :key => true
42
+ property :type, Discriminator
43
+ end
44
+
45
+ class CollectionSpecUser < CollectionSpecParty
46
+ def self.default_repository_name
47
+ ADAPTER
48
+ end
49
+
50
+ property :username, String
51
+ property :password, String
52
+ end
53
+
54
+ module CollectionSpecHelper
55
+ def setup
56
+ Zebra.auto_migrate!(ADAPTER)
57
+ Stripe.auto_migrate!(ADAPTER)
58
+
59
+ repository(ADAPTER) do
60
+ @nancy = Zebra.create(:name => 'Nancy', :age => 11, :notes => 'Spotted!')
61
+ @bessie = Zebra.create(:name => 'Bessie', :age => 10, :notes => 'Striped!')
62
+ @steve = Zebra.create(:name => 'Steve', :age => 8, :notes => 'Bald!')
63
+
64
+ @babe = Stripe.create(:name => 'Babe')
65
+ @snowball = Stripe.create(:name => 'snowball')
66
+
67
+ @nancy.stripes << @babe
68
+ @nancy.stripes << @snowball
69
+ @nancy.save
70
+ end
71
+ end
72
+ end
73
+
74
+ describe 'association proxying' do
75
+ include CollectionSpecHelper
76
+
77
+ before do
78
+ setup
79
+ end
80
+
81
+ it "should provide a Query" do
82
+ repository(ADAPTER) do
83
+ zebras = Zebra.all(:order => [ :name ])
84
+ zebras.query.order.should == [DataMapper::Query::Direction.new(Zebra.properties(ADAPTER)[:name])]
85
+ end
86
+ end
87
+
88
+ it "should proxy the relationships of the model" do
89
+ repository(ADAPTER) do
90
+ zebras = Zebra.all
91
+ zebras.should have(3).entries
92
+ zebras.find { |zebra| zebra.name == 'Nancy' }.stripes.should have(2).entries
93
+ zebras.stripes.should == [@babe, @snowball]
94
+ end
95
+ end
96
+
97
+ it "should preserve it's order on reload" do
98
+ repository(ADAPTER) do |r|
99
+ zebras = Zebra.all(:order => [ :name ])
100
+
101
+ order = %w{ Bessie Nancy Steve }
102
+
103
+ zebras.map { |z| z.name }.should == order
104
+
105
+ # Force a lazy-load call:
106
+ zebras.first.notes
107
+
108
+ # The order should be unaffected.
109
+ zebras.map { |z| z.name }.should == order
110
+ end
111
+ end
112
+ end
113
+
114
+ describe DataMapper::Collection do
115
+ include CollectionSpecHelper
116
+
117
+ before do
118
+ setup
119
+ end
120
+
121
+ before do
122
+ @repository = repository(ADAPTER)
123
+ @model = Zebra
124
+ @query = DataMapper::Query.new(@repository, @model, :order => [ :id ])
125
+ @collection = @repository.read_many(@query)
126
+ @other = @repository.read_many(@query.merge(:limit => 2))
127
+ end
128
+
129
+ it "should return the correct repository" do
130
+ repository = repository(:legacy)
131
+ query = DataMapper::Query.new(repository, @model)
132
+ DataMapper::Collection.new(query){}.repository.object_id.should == repository.object_id
133
+ end
134
+
135
+ it "should be able to add arbitrary objects" do
136
+ properties = @model.properties(:default)
137
+
138
+ collection = DataMapper::Collection.new(@query) do |c|
139
+ c.load([ 4, 'Bob', 10 ])
140
+ c.load([ 5, 'Nancy', 11 ])
141
+ end
142
+
143
+ collection.should respond_to(:reload)
144
+
145
+ results = collection.entries
146
+ results.should have(2).entries
147
+
148
+ results.each do |cow|
149
+ cow.attribute_loaded?(:name).should == true
150
+ cow.attribute_loaded?(:age).should == true
151
+ end
152
+
153
+ bob, nancy = results[0], results[1]
154
+
155
+ bob.name.should eql('Bob')
156
+ bob.age.should eql(10)
157
+ bob.should_not be_a_new_record
158
+
159
+ nancy.name.should eql('Nancy')
160
+ nancy.age.should eql(11)
161
+ nancy.should_not be_a_new_record
162
+
163
+ results.first.should == bob
164
+ end
165
+
166
+ describe '.new' do
167
+ describe 'with non-index keys' do
168
+ it 'should instantiate read-only resources' do
169
+ @collection = DataMapper::Collection.new(DataMapper::Query.new(@repository, @model, :fields => [ :age ])) do |c|
170
+ c.load([ 1 ])
171
+ end
172
+
173
+ @collection.size.should == 1
174
+
175
+ resource = @collection.entries[0]
176
+
177
+ resource.should be_kind_of(@model)
178
+ resource.collection.object_id.should == @collection.object_id
179
+ resource.should_not be_new_record
180
+ resource.should be_readonly
181
+ resource.age.should == 1
182
+ end
183
+ end
184
+
185
+ describe 'with inheritance property' do
186
+ before do
187
+ CollectionSpecUser.auto_migrate!
188
+ CollectionSpecUser.create(:name => 'John')
189
+
190
+ properties = CollectionSpecParty.properties(:default)
191
+ end
192
+
193
+ it 'should instantiate resources using the inheritance property class' do
194
+ query = DataMapper::Query.new(@repository, CollectionSpecParty)
195
+ collection = @repository.read_many(query)
196
+ collection.should have(1).entries
197
+ collection.first.model.should == CollectionSpecUser
198
+ end
199
+ end
200
+ end
201
+
202
+ [ true, false ].each do |loaded|
203
+ describe " (#{loaded ? '' : 'not '}loaded) " do
204
+ if loaded
205
+ before do
206
+ @collection.to_a
207
+ end
208
+ end
209
+
210
+ describe '#<<' do
211
+ it 'should relate each new resource to the collection' do
212
+ # resource is orphaned
213
+ @nancy.collection.object_id.should_not == @collection.object_id
214
+
215
+ @collection << @nancy
216
+
217
+ # resource is related
218
+ @nancy.collection.object_id.should == @collection.object_id
219
+ end
220
+
221
+ it 'should return self' do
222
+ @collection.<<(@steve).object_id.should == @collection.object_id
223
+ end
224
+ end
225
+
226
+ describe '#all' do
227
+ describe 'with no arguments' do
228
+ it 'should return self' do
229
+ @collection.all.object_id.should == @collection.object_id
230
+ end
231
+ end
232
+
233
+ describe 'with query arguments' do
234
+ describe 'should return a Collection' do
235
+ before do
236
+ @query.update(:offset => 10, :limit => 10)
237
+ query = DataMapper::Query.new(@repository, @model)
238
+ @unlimited = DataMapper::Collection.new(query) {}
239
+ end
240
+
241
+ it 'has an offset equal to 10' do
242
+ @collection.all.query.offset.should == 10
243
+ end
244
+
245
+ it 'has a cumulative offset equal to 11 when passed an offset of 1' do
246
+ @collection.all(:offset => 1).query.offset.should == 11
247
+ end
248
+
249
+ it 'has a cumulative offset equal to 19 when passed an offset of 9' do
250
+ @collection.all(:offset => 9).query.offset.should == 19
251
+ end
252
+
253
+ it 'is empty when passed an offset that is out of range' do
254
+ pending do
255
+ empty_collection = @collection.all(:offset => 10)
256
+ empty_collection.should == []
257
+ empty_collection.should be_loaded
258
+ end
259
+ end
260
+
261
+ it 'has an limit equal to 10' do
262
+ @collection.all.query.limit.should == 10
263
+ end
264
+
265
+ it 'has a limit equal to 5' do
266
+ @collection.all(:limit => 5).query.limit.should == 5
267
+ end
268
+
269
+ it 'has a limit equal to 10 if passed a limit greater than 10' do
270
+ @collection.all(:limit => 11).query.limit.should == 10
271
+ end
272
+
273
+ it 'has no limit' do
274
+ @unlimited.all.query.limit.should be_nil
275
+ end
276
+
277
+ it 'has a limit equal to 1000 when passed a limit of 1000' do
278
+ @unlimited.all(:limit => 1000).query.limit.should == 1000
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ describe '#at' do
285
+ it 'should return a Resource' do
286
+ resource_at = @collection.at(1)
287
+ resource_at.should be_kind_of(DataMapper::Resource)
288
+ resource_at.id.should == @bessie.id
289
+ end
290
+
291
+ it 'should return a Resource when using a negative index' do
292
+ resource_at = @collection.at(-1)
293
+ resource_at.should be_kind_of(DataMapper::Resource)
294
+ resource_at.id.should == @steve.id
295
+ end
296
+ end
297
+
298
+ describe '#clear' do
299
+ it 'should orphan the resource from the collection' do
300
+ entries = @collection.entries
301
+
302
+ # resources are related
303
+ entries.each { |r| r.collection.object_id.should == @collection.object_id }
304
+
305
+ @collection.should have(3).entries
306
+ @collection.clear
307
+ @collection.should be_empty
308
+
309
+ # resources are orphaned
310
+ entries.each { |r| r.collection.object_id.should_not == @collection.object_id }
311
+ end
312
+
313
+ it 'should return self' do
314
+ @collection.clear.object_id.should == @collection.object_id
315
+ end
316
+ end
317
+
318
+ describe '#collect!' do
319
+ it 'should return self' do
320
+ @collection.collect! { |resource| resource }.object_id.should == @collection.object_id
321
+ end
322
+ end
323
+
324
+ describe '#concat' do
325
+ it 'should return self' do
326
+ @collection.concat(@other).object_id.should == @collection.object_id
327
+ end
328
+ end
329
+
330
+ describe '#create' do
331
+ it 'should create a new resource' do
332
+ resource = @collection.create(:name => 'John')
333
+ resource.should be_kind_of(@model)
334
+ resource.should_not be_new_record
335
+ end
336
+
337
+ it 'should append the new resource to the collection' do
338
+ resource = @collection.create(:name => 'John')
339
+ resource.should_not be_new_record
340
+ resource.collection.object_id.should == @collection.object_id
341
+ @collection.should include(resource)
342
+ end
343
+
344
+ it 'should not append the resource if it was not saved' do
345
+ @repository.should_receive(:create).and_return(false)
346
+ Zebra.should_receive(:repository).at_least(:once).and_return(@repository)
347
+
348
+ resource = @collection.create(:name => 'John')
349
+ resource.should be_new_record
350
+
351
+ resource.collection.object_id.should_not == @collection.object_id
352
+ @collection.should_not include(resource)
353
+ end
354
+
355
+ it 'should use the query conditions to set default values' do
356
+ resource = @collection.create
357
+ resource.should_not be_new_record
358
+ resource.name.should be_nil
359
+
360
+ @collection.query.update(:name => 'John')
361
+
362
+ resource = @collection.create
363
+ resource.name.should == 'John'
364
+ end
365
+ end
366
+
367
+ describe '#delete' do
368
+ it 'should orphan the resource from the collection' do
369
+ collection = @nancy.collection
370
+
371
+ # resource is related
372
+ @nancy.collection.object_id.should == collection.object_id
373
+
374
+ collection.should have(1).entries
375
+ collection.delete(@nancy)
376
+ collection.should be_empty
377
+
378
+ # resource is orphaned
379
+ @nancy.collection.object_id.should_not == collection.object_id
380
+ end
381
+
382
+ it 'should return a Resource' do
383
+ collection = @nancy.collection
384
+
385
+ resource = collection.delete(@nancy)
386
+
387
+ resource.should be_kind_of(DataMapper::Resource)
388
+ resource.object_id.should == @nancy.object_id
389
+ end
390
+ end
391
+
392
+ describe '#delete_at' do
393
+ it 'should orphan the resource from the collection' do
394
+ collection = @nancy.collection
395
+
396
+ # resource is related
397
+ @nancy.collection.object_id.should == collection.object_id
398
+
399
+ collection.should have(1).entries
400
+ collection.delete_at(0).object_id.should == @nancy.object_id
401
+ collection.should be_empty
402
+
403
+ # resource is orphaned
404
+ @nancy.collection.object_id.should_not == collection.object_id
405
+ end
406
+
407
+ it 'should return a Resource' do
408
+ collection = @nancy.collection
409
+
410
+ resource = collection.delete_at(0)
411
+
412
+ resource.should be_kind_of(DataMapper::Resource)
413
+ resource.object_id.should == @nancy.object_id
414
+ end
415
+ end
416
+
417
+ describe '#destroy!' do
418
+ before do
419
+ @ids = [ @nancy.id, @bessie.id, @steve.id ]
420
+ end
421
+
422
+ it 'should destroy the resources in the collection' do
423
+ @collection.map { |r| r.id }.should == @ids
424
+ @collection.destroy!.should == true
425
+ @model.all(:id => @ids).should == []
426
+ @collection.reload.should == []
427
+ end
428
+
429
+ it 'should clear the collection' do
430
+ @collection.map { |r| r.id }.should == @ids
431
+ @collection.destroy!.should == true
432
+ @collection.should == []
433
+ end
434
+ end
435
+
436
+ describe '#each' do
437
+ it 'should return self' do
438
+ @collection.each { |resource| }.object_id.should == @collection.object_id
439
+ end
440
+ end
441
+
442
+ describe '#each_index' do
443
+ it 'should return self' do
444
+ @collection.each_index { |resource| }.object_id.should == @collection.object_id
445
+ end
446
+ end
447
+
448
+ describe '#eql?' do
449
+ it 'should return true if for the same collection' do
450
+ @collection.object_id.should == @collection.object_id
451
+ @collection.should be_eql(@collection)
452
+ end
453
+
454
+ it 'should return true for duplicate collections' do
455
+ dup = @collection.dup
456
+ dup.should be_kind_of(DataMapper::Collection)
457
+ dup.object_id.should_not == @collection.object_id
458
+ dup.entries.should == @collection.entries
459
+ dup.should be_eql(@collection)
460
+ end
461
+
462
+ it 'should return false for different collections' do
463
+ @collection.should_not be_eql(@other)
464
+ end
465
+ end
466
+
467
+ describe '#fetch' do
468
+ it 'should return a Resource' do
469
+ @collection.fetch(0).should be_kind_of(DataMapper::Resource)
470
+ end
471
+ end
472
+
473
+ describe '#first' do
474
+ describe 'with no arguments' do
475
+ it 'should return a Resource' do
476
+ first = @collection.first
477
+ first.should_not be_nil
478
+ first.should be_kind_of(DataMapper::Resource)
479
+ first.id.should == @nancy.id
480
+ end
481
+ end
482
+
483
+ describe 'with limit specified' do
484
+ it 'should return a Collection' do
485
+ collection = @collection.first(2)
486
+
487
+ collection.should be_kind_of(DataMapper::Collection)
488
+ collection.object_id.should_not == @collection.object_id
489
+
490
+ collection.query.order.size.should == 1
491
+ collection.query.order.first.property.should == @model.properties[:id]
492
+ collection.query.order.first.direction.should == :asc
493
+
494
+ collection.query.offset.should == 0
495
+ collection.query.limit.should == 2
496
+
497
+ collection.length.should == 2
498
+
499
+ collection.entries.map { |r| r.id }.should == [ @nancy.id, @bessie.id ]
500
+ end
501
+
502
+ it 'should return a Collection if limit is 1' do
503
+ collection = @collection.first(1)
504
+
505
+ collection.should be_kind_of(DataMapper::Collection)
506
+ collection.object_id.should_not == @collection.object_id
507
+ end
508
+ end
509
+ end
510
+
511
+ describe '#get' do
512
+ it 'should find a resource in a collection by key' do
513
+ resource = @collection.get(*@nancy.key)
514
+ resource.should be_kind_of(DataMapper::Resource)
515
+ resource.id.should == @nancy.id
516
+ end
517
+
518
+ it 'should not find a resource not in the collection' do
519
+ @query.update(:offset => 0, :limit => 3)
520
+ @david = Zebra.create!(:name => 'David', :age => 15, :notes => 'Albino')
521
+ @collection.get(@david.key).should be_nil
522
+ end
523
+ end
524
+
525
+ describe '#get!' do
526
+ it 'should find a resource in a collection by key' do
527
+ resource = @collection.get!(*@nancy.key)
528
+ resource.should be_kind_of(DataMapper::Resource)
529
+ resource.id.should == @nancy.id
530
+ end
531
+
532
+ it 'should raise an exception if the resource is not found' do
533
+ @query.update(:offset => 0, :limit => 3)
534
+ @david = Zebra.create!(:name => 'David', :age => 15, :notes => 'Albino')
535
+ lambda {
536
+ @collection.get!(@david.key)
537
+ }.should raise_error(DataMapper::ObjectNotFoundError)
538
+ end
539
+ end
540
+
541
+ describe '#insert' do
542
+ it 'should return self' do
543
+ @collection.insert(1, @steve).object_id.should == @collection.object_id
544
+ end
545
+ end
546
+
547
+ describe '#last' do
548
+ describe 'with no arguments' do
549
+ it 'should return a Resource' do
550
+ last = @collection.last
551
+ last.should_not be_nil
552
+ last.should be_kind_of(DataMapper::Resource)
553
+ last.id.should == @steve.id
554
+ end
555
+ end
556
+
557
+ describe 'with limit specified' do
558
+ it 'should return a Collection' do
559
+ collection = @collection.last(2)
560
+
561
+ collection.should be_kind_of(DataMapper::Collection)
562
+ collection.object_id.should_not == @collection.object_id
563
+
564
+ collection.query.order.size.should == 1
565
+ collection.query.order.first.property.should == @model.properties[:id]
566
+ collection.query.order.first.direction.should == :desc
567
+
568
+ collection.query.offset.should == 0
569
+ collection.query.limit.should == 2
570
+
571
+ collection.length.should == 2
572
+
573
+ collection.entries.map { |r| r.id }.should == [ @bessie.id, @steve.id ]
574
+ end
575
+
576
+ it 'should return a Collection if limit is 1' do
577
+ collection = @collection.last(1)
578
+
579
+ collection.class.should == DataMapper::Collection # should be_kind_of(DataMapper::Collection)
580
+ collection.object_id.should_not == @collection.object_id
581
+ end
582
+ end
583
+ end
584
+
585
+ describe '#load' do
586
+ it 'should load resources from the identity map when possible' do
587
+ @steve.collection = nil
588
+ @repository.identity_map(@model).should_receive(:get).with([ @steve.id ]).and_return(@steve)
589
+
590
+ collection = @repository.read_many(@query.merge(:id => @steve.id))
591
+
592
+ collection.size.should == 1
593
+ collection.map { |r| r.object_id }.should == [ @steve.object_id ]
594
+
595
+ @steve.collection.object_id.should == collection.object_id
596
+ end
597
+
598
+ it 'should return a Resource' do
599
+ @collection.load([ @steve.id, @steve.name, @steve.age ]).should be_kind_of(DataMapper::Resource)
600
+ end
601
+ end
602
+
603
+ describe '#loaded?' do
604
+ if loaded
605
+ it 'should return true for an initialized collection' do
606
+ @collection.should be_loaded
607
+ end
608
+ else
609
+ it 'should return false for an uninitialized collection' do
610
+ @collection.should_not be_loaded
611
+ @collection.to_a # load collection
612
+ @collection.should be_loaded
613
+ end
614
+ end
615
+ end
616
+
617
+ describe '#pop' do
618
+ it 'should orphan the resource from the collection' do
619
+ collection = @steve.collection
620
+
621
+ # resource is related
622
+ @steve.collection.object_id.should == collection.object_id
623
+
624
+ collection.should have(1).entries
625
+ collection.pop.object_id.should == @steve.object_id
626
+ collection.should be_empty
627
+
628
+ # resource is orphaned
629
+ @steve.collection.object_id.should_not == collection.object_id
630
+ end
631
+
632
+ it 'should return a Resource' do
633
+ @collection.pop.key.should == @steve.key
634
+ end
635
+ end
636
+
637
+ describe '#properties' do
638
+ it 'should return a PropertySet' do
639
+ @collection.properties.should be_kind_of(DataMapper::PropertySet)
640
+ end
641
+
642
+ it 'should contain same properties as query.fields' do
643
+ properties = @collection.properties
644
+ properties.entries.should == @collection.query.fields
645
+ end
646
+ end
647
+
648
+ describe '#push' do
649
+ it 'should relate each new resource to the collection' do
650
+ # resource is orphaned
651
+ @nancy.collection.object_id.should_not == @collection.object_id
652
+
653
+ @collection.push(@nancy)
654
+
655
+ # resource is related
656
+ @nancy.collection.object_id.should == @collection.object_id
657
+ end
658
+
659
+ it 'should return self' do
660
+ @collection.push(@steve).object_id.should == @collection.object_id
661
+ end
662
+ end
663
+
664
+ describe '#relationships' do
665
+ it 'should return a Hash' do
666
+ @collection.relationships.should be_kind_of(Hash)
667
+ end
668
+
669
+ it 'should contain same properties as query.model.relationships' do
670
+ relationships = @collection.relationships
671
+ relationships.should == @collection.query.model.relationships
672
+ end
673
+ end
674
+
675
+ describe '#reject' do
676
+ it 'should return a Collection with resources that did not match the block' do
677
+ rejected = @collection.reject { |resource| false }
678
+ rejected.class.should == Array
679
+ rejected.should == [ @nancy, @bessie, @steve ]
680
+ end
681
+
682
+ it 'should return an empty Array if resources matched the block' do
683
+ rejected = @collection.reject { |resource| true }
684
+ rejected.class.should == Array
685
+ rejected.should == []
686
+ end
687
+ end
688
+
689
+ describe '#reject!' do
690
+ it 'should return self if resources matched the block' do
691
+ @collection.reject! { |resource| true }.object_id.should == @collection.object_id
692
+ end
693
+
694
+ it 'should return nil if no resources matched the block' do
695
+ @collection.reject! { |resource| false }.should be_nil
696
+ end
697
+ end
698
+
699
+ describe '#reload' do
700
+ it 'should return self' do
701
+ @collection.reload.object_id.should == @collection.object_id
702
+ end
703
+
704
+ it 'should replace the collection' do
705
+ original = @collection.dup
706
+ @collection.reload.should == @collection
707
+ @collection.should == original
708
+ end
709
+
710
+ it 'should reload lazily initialized fields' do
711
+ pending 'Move to unit specs'
712
+
713
+ @repository.should_receive(:all) do |model,query|
714
+ model.should == @model
715
+
716
+ query.should be_instance_of(DataMapper::Query)
717
+ query.reload.should == true
718
+ query.offset.should == 0
719
+ query.limit.should == 10
720
+ query.order.should == []
721
+ query.fields.should == @model.properties.defaults
722
+ query.links.should == []
723
+ query.includes.should == []
724
+ query.conditions.should == [ [ :eql, @model.properties[:id], [ 1, 2, 3 ] ] ]
725
+
726
+ @collection
727
+ end
728
+
729
+ @collection.reload
730
+ end
731
+ end
732
+
733
+ describe '#replace' do
734
+ it "should orphan each existing resource from the collection if loaded?" do
735
+ entries = @collection.entries
736
+
737
+ # resources are related
738
+ entries.each { |r| r.collection.object_id.should == @collection.object_id }
739
+
740
+ @collection.should have(3).entries
741
+ @collection.replace([]).object_id.should == @collection.object_id
742
+ @collection.should be_empty
743
+
744
+ # resources are orphaned
745
+ entries.each { |r| r.collection.object_id.should_not == @collection.object_id }
746
+ end
747
+
748
+ it 'should relate each new resource to the collection' do
749
+ # resource is orphaned
750
+ @nancy.collection.object_id.should_not == @collection.object_id
751
+
752
+ @collection.replace([ @nancy ])
753
+
754
+ # resource is related
755
+ @nancy.collection.object_id.should == @collection.object_id
756
+ end
757
+
758
+ it 'should replace the contents of the collection' do
759
+ other = [ @nancy ]
760
+ @collection.should_not == other
761
+ @collection.replace(other)
762
+ @collection.should == other
763
+ @collection.object_id.should_not == @other.object_id
764
+ end
765
+ end
766
+
767
+ describe '#reverse' do
768
+ [ true, false ].each do |loaded|
769
+ describe "on a collection where loaded? == #{loaded}" do
770
+ before do
771
+ @collection.to_a if loaded
772
+ end
773
+
774
+ it 'should return a Collection with reversed entries' do
775
+ reversed = @collection.reverse
776
+ reversed.should be_kind_of(DataMapper::Collection)
777
+ reversed.object_id.should_not == @collection.object_id
778
+ reversed.entries.should == @collection.entries.reverse
779
+
780
+ reversed.query.order.size.should == 1
781
+ reversed.query.order.first.property.should == @model.properties[:id]
782
+ reversed.query.order.first.direction.should == :desc
783
+ end
784
+ end
785
+ end
786
+ end
787
+
788
+ describe '#reverse!' do
789
+ it 'should return self' do
790
+ @collection.reverse!.object_id.should == @collection.object_id
791
+ end
792
+ end
793
+
794
+ describe '#reverse_each' do
795
+ it 'should return self' do
796
+ @collection.reverse_each { |resource| }.object_id.should == @collection.object_id
797
+ end
798
+ end
799
+
800
+ describe '#select' do
801
+ it 'should return an Array with resources that matched the block' do
802
+ selected = @collection.select { |resource| true }
803
+ selected.class.should == Array
804
+ selected.should == @collection
805
+ end
806
+
807
+ it 'should return an empty Array if no resources matched the block' do
808
+ selected = @collection.select { |resource| false }
809
+ selected.class.should == Array
810
+ selected.should == []
811
+ end
812
+ end
813
+
814
+ describe '#shift' do
815
+ it 'should orphan the resource from the collection' do
816
+ collection = @nancy.collection
817
+
818
+ # resource is related
819
+ @nancy.collection.object_id.should == collection.object_id
820
+
821
+ collection.should have(1).entries
822
+ collection.shift.object_id.should == @nancy.object_id
823
+ collection.should be_empty
824
+
825
+ # resource is orphaned
826
+ @nancy.collection.object_id.should_not == collection.object_id
827
+ end
828
+
829
+ it 'should return a Resource' do
830
+ @collection.shift.key.should == @nancy.key
831
+ end
832
+ end
833
+
834
+ [ :slice, :[] ].each do |method|
835
+ describe '#slice' do
836
+ describe 'with an index' do
837
+ it 'should return a Resource' do
838
+ resource = @collection.send(method, 0)
839
+ resource.should be_kind_of(DataMapper::Resource)
840
+ resource.id.should == @nancy.id
841
+ end
842
+ end
843
+
844
+ describe 'with a start and length' do
845
+ it 'should return a Collection' do
846
+ sliced = @collection.send(method, 0, 1)
847
+ sliced.should be_kind_of(DataMapper::Collection)
848
+ sliced.object_id.should_not == @collection.object_id
849
+ sliced.length.should == 1
850
+ sliced.map { |r| r.id }.should == [ @nancy.id ]
851
+ end
852
+ end
853
+
854
+ describe 'with a Range' do
855
+ it 'should return a Collection' do
856
+ sliced = @collection.send(method, 0..1)
857
+ sliced.should be_kind_of(DataMapper::Collection)
858
+ sliced.object_id.should_not == @collection.object_id
859
+ sliced.length.should == 2
860
+ sliced.map { |r| r.id }.should == [ @nancy.id, @bessie.id ]
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ describe '#slice!' do
867
+ describe 'with an index' do
868
+ it 'should return a Resource' do
869
+ resource = @collection.slice!(0)
870
+ resource.should be_kind_of(DataMapper::Resource)
871
+ end
872
+ end
873
+
874
+ describe 'with a start and length' do
875
+ it 'should return an Array' do
876
+ sliced = @collection.slice!(0, 1)
877
+ sliced.class.should == Array
878
+ sliced.map { |r| r.id }.should == [ @nancy.id ]
879
+ end
880
+ end
881
+
882
+ describe 'with a Range' do
883
+ it 'should return a Collection' do
884
+ sliced = @collection.slice(0..1)
885
+ sliced.should be_kind_of(DataMapper::Collection)
886
+ sliced.object_id.should_not == @collection.object_id
887
+ sliced.length.should == 2
888
+ sliced[0].id.should == @nancy.id
889
+ sliced[1].id.should == @bessie.id
890
+ end
891
+ end
892
+ end
893
+
894
+ describe '#sort' do
895
+ it 'should return an Array' do
896
+ sorted = @collection.sort { |a,b| a.age <=> b.age }
897
+ sorted.class.should == Array
898
+ end
899
+ end
900
+
901
+ describe '#sort!' do
902
+ it 'should return self' do
903
+ @collection.sort! { |a,b| 0 }.object_id.should == @collection.object_id
904
+ end
905
+ end
906
+
907
+ describe '#unshift' do
908
+ it 'should relate each new resource to the collection' do
909
+ # resource is orphaned
910
+ @nancy.collection.object_id.should_not == @collection.object_id
911
+
912
+ @collection.unshift(@nancy)
913
+
914
+ # resource is related
915
+ @nancy.collection.object_id.should == @collection.object_id
916
+ end
917
+
918
+ it 'should return self' do
919
+ @collection.unshift(@steve).object_id.should == @collection.object_id
920
+ end
921
+ end
922
+
923
+ describe '#update!' do
924
+ it 'should update the resources in the collection' do
925
+ pending do
926
+ # this will not pass with new update!
927
+ # update! should never loop through and set attributes
928
+ # even if it is loaded, and it will not reload the
929
+ # changed objects (even with reload=true, as objects
930
+ # are created is not in any identity map)
931
+ names = [ @nancy.name, @bessie.name, @steve.name ]
932
+ @collection.map { |r| r.name }.should == names
933
+ @collection.update!(:name => 'John')
934
+ @collection.map { |r| r.name }.should_not == names
935
+ @collection.map { |r| r.name }.should == %w[ John ] * 3
936
+ end
937
+ end
938
+
939
+ it 'should not update loaded resources unless forced' do
940
+ repository(ADAPTER) do
941
+ nancy = Zebra.first
942
+ nancy.name.should == "Nancy"
943
+
944
+ collection = Zebra.all(:name => ["Nancy","Bessie"])
945
+ collection.update!(:name => "Stevie")
946
+
947
+ nancy.name.should == "Nancy"
948
+ end
949
+ end
950
+
951
+ it 'should update loaded resources if forced' do
952
+ repository(ADAPTER) do
953
+ nancy = Zebra.first
954
+ nancy.name.should == "Nancy"
955
+
956
+ collection = Zebra.all(:name => ["Nancy","Bessie"])
957
+ collection.update!({:name => "Stevie"},true)
958
+
959
+ nancy.name.should == "Stevie"
960
+ end
961
+ end
962
+
963
+ it 'should update collection-query when updating' do
964
+ repository(ADAPTER) do
965
+ collection = Zebra.all(:name => ["Nancy","Bessie"])
966
+ collection.query.conditions.first[2].should == ["Nancy","Bessie"]
967
+ collection.length.should == 2
968
+ collection.update!(:name => "Stevie")
969
+ collection.length.should == 2
970
+ collection.query.conditions.first[2].should == "Stevie"
971
+ end
972
+ end
973
+ end
974
+
975
+ describe '#keys' do
976
+ it 'should return a hash of keys' do
977
+ keys = @collection.send(:keys)
978
+ keys.length.should == 1
979
+ keys.each{|property,values| values.should == [1,2,3]}
980
+ end
981
+
982
+ it 'should return an empty hash if collection is empty' do
983
+ keys = Zebra.all(:id.gt => 10000).send(:keys)
984
+ keys.should == {}
985
+ end
986
+ end
987
+
988
+ describe '#values_at' do
989
+ it 'should return an Array' do
990
+ values = @collection.values_at(0)
991
+ values.class.should == Array
992
+ end
993
+
994
+ it 'should return an Array of the resources at the index' do
995
+ @collection.values_at(0).entries.map { |r| r.id }.should == [ @nancy.id ]
996
+ end
997
+ end
998
+
999
+ describe 'with lazy loading' do
1000
+ it "should take a materialization block" do
1001
+ collection = DataMapper::Collection.new(@query) do |c|
1002
+ c.should == []
1003
+ c.load([ 1, 'Bob', 10 ])
1004
+ c.load([ 2, 'Nancy', 11 ])
1005
+ end
1006
+
1007
+ collection.should_not be_loaded
1008
+ collection.length.should == 2
1009
+ collection.should be_loaded
1010
+ end
1011
+ end
1012
+ end
1013
+ end
1014
+ end
1015
+ end