bigrecord 0.0.5

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 (104) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +44 -0
  3. data/Rakefile +17 -0
  4. data/VERSION +1 -0
  5. data/doc/bigrecord_specs.rdoc +36 -0
  6. data/doc/getting_started.rdoc +157 -0
  7. data/examples/bigrecord.yml +25 -0
  8. data/generators/bigrecord/bigrecord_generator.rb +17 -0
  9. data/generators/bigrecord/templates/bigrecord.rake +47 -0
  10. data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
  11. data/generators/bigrecord_migration/templates/migration.rb +9 -0
  12. data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
  13. data/generators/bigrecord_model/templates/migration.rb +13 -0
  14. data/generators/bigrecord_model/templates/model.rb +7 -0
  15. data/generators/bigrecord_model/templates/model_spec.rb +12 -0
  16. data/init.rb +9 -0
  17. data/install.rb +22 -0
  18. data/lib/big_record/abstract_base.rb +1088 -0
  19. data/lib/big_record/action_view_extensions.rb +266 -0
  20. data/lib/big_record/ar_associations/association_collection.rb +194 -0
  21. data/lib/big_record/ar_associations/association_proxy.rb +158 -0
  22. data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
  23. data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
  24. data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
  25. data/lib/big_record/ar_associations/has_many_association.rb +191 -0
  26. data/lib/big_record/ar_associations/has_one_association.rb +80 -0
  27. data/lib/big_record/ar_associations.rb +1608 -0
  28. data/lib/big_record/ar_reflection.rb +223 -0
  29. data/lib/big_record/attribute_methods.rb +75 -0
  30. data/lib/big_record/base.rb +618 -0
  31. data/lib/big_record/br_associations/association_collection.rb +194 -0
  32. data/lib/big_record/br_associations/association_proxy.rb +153 -0
  33. data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
  34. data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
  35. data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
  36. data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
  37. data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
  38. data/lib/big_record/br_associations/has_one_association.rb +80 -0
  39. data/lib/big_record/br_associations.rb +978 -0
  40. data/lib/big_record/br_reflection.rb +151 -0
  41. data/lib/big_record/callbacks.rb +367 -0
  42. data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
  43. data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
  44. data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
  45. data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
  46. data/lib/big_record/connection_adapters/column.rb +491 -0
  47. data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
  48. data/lib/big_record/connection_adapters/view.rb +27 -0
  49. data/lib/big_record/connection_adapters.rb +10 -0
  50. data/lib/big_record/deletion.rb +73 -0
  51. data/lib/big_record/dynamic_schema.rb +92 -0
  52. data/lib/big_record/embedded.rb +71 -0
  53. data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
  54. data/lib/big_record/family_span_columns.rb +89 -0
  55. data/lib/big_record/fixtures.rb +1025 -0
  56. data/lib/big_record/migration.rb +380 -0
  57. data/lib/big_record/routing_ext.rb +65 -0
  58. data/lib/big_record/timestamp.rb +51 -0
  59. data/lib/big_record/validations.rb +830 -0
  60. data/lib/big_record.rb +125 -0
  61. data/lib/bigrecord.rb +1 -0
  62. data/rails/init.rb +9 -0
  63. data/spec/connections/bigrecord.yml +13 -0
  64. data/spec/connections/cassandra/connection.rb +2 -0
  65. data/spec/connections/hbase/connection.rb +2 -0
  66. data/spec/debug.log +281 -0
  67. data/spec/integration/br_associations_spec.rb +80 -0
  68. data/spec/lib/animal.rb +12 -0
  69. data/spec/lib/book.rb +10 -0
  70. data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
  71. data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
  72. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
  73. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
  74. data/spec/lib/company.rb +14 -0
  75. data/spec/lib/embedded/web_link.rb +12 -0
  76. data/spec/lib/employee.rb +33 -0
  77. data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
  78. data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
  79. data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
  80. data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
  81. data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
  82. data/spec/lib/novel.rb +5 -0
  83. data/spec/lib/zoo.rb +17 -0
  84. data/spec/spec.opts +4 -0
  85. data/spec/spec_helper.rb +55 -0
  86. data/spec/unit/abstract_base_spec.rb +287 -0
  87. data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
  88. data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
  89. data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
  90. data/spec/unit/ar_associations_spec.rb +8 -0
  91. data/spec/unit/base_spec.rb +6 -0
  92. data/spec/unit/br_associations_spec.rb +58 -0
  93. data/spec/unit/embedded_spec.rb +43 -0
  94. data/spec/unit/find_spec.rb +34 -0
  95. data/spec/unit/hash_helper_spec.rb +44 -0
  96. data/spec/unit/migration_spec.rb +144 -0
  97. data/spec/unit/model_spec.rb +315 -0
  98. data/spec/unit/validations_spec.rb +182 -0
  99. data/tasks/bigrecord_tasks.rake +47 -0
  100. data/tasks/data_store.rb +46 -0
  101. data/tasks/gem.rb +22 -0
  102. data/tasks/rdoc.rb +8 -0
  103. data/tasks/spec.rb +34 -0
  104. metadata +189 -0
@@ -0,0 +1,1025 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'csv'
4
+
5
+ module YAML #:nodoc:
6
+ class Omap #:nodoc:
7
+ def keys; map { |k, v| k } end
8
+ def values; map { |k, v| v } end
9
+ end
10
+ end
11
+
12
+ if defined? BigRecord
13
+ class BigRecordFixtureClassNotFound < BigRecord::BigRecordError #:nodoc:
14
+ end
15
+ else
16
+ class BigRecordFixtureClassNotFound < StandardError #:nodoc:
17
+ end
18
+ end
19
+
20
+ # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
21
+ #
22
+ # 1. YAML fixtures
23
+ # 2. CSV fixtures
24
+ # 3. Single-file fixtures
25
+ #
26
+ # = YAML fixtures
27
+ #
28
+ # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
29
+ # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
30
+ #
31
+ # Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
32
+ # by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
33
+ # put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
34
+ # "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
35
+ #
36
+ # rubyonrails:
37
+ # id: 1
38
+ # name: Ruby on Rails
39
+ # url: http://www.rubyonrails.org
40
+ #
41
+ # google:
42
+ # id: 2
43
+ # name: Google
44
+ # url: http://www.google.com
45
+ #
46
+ # This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an
47
+ # indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
48
+ # pleasure.
49
+ #
50
+ # Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html
51
+ # for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
52
+ # This is commonly needed for tree structures. Example:
53
+ #
54
+ # --- !omap
55
+ # - parent:
56
+ # id: 1
57
+ # parent_id: NULL
58
+ # title: Parent
59
+ # - child:
60
+ # id: 2
61
+ # parent_id: 1
62
+ # title: Child
63
+ #
64
+ # = CSV fixtures
65
+ #
66
+ # Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
67
+ # in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
68
+ #
69
+ # The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
70
+ # humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised
71
+ # of the actual data (1 per line). Here's an example:
72
+ #
73
+ # id, name, url
74
+ # 1, Ruby On Rails, http://www.rubyonrails.org
75
+ # 2, Google, http://www.google.com
76
+ #
77
+ # Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you
78
+ # need to use a double quote character, you must escape it with another double quote.
79
+ #
80
+ # Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the
81
+ # fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
82
+ # number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
83
+ # "web_site_2".
84
+ #
85
+ # Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
86
+ # have existing data somewhere already.
87
+ #
88
+ # = Single-file fixtures
89
+ #
90
+ # This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
91
+ # Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
92
+ # appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
93
+ # put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
94
+ # model).
95
+ #
96
+ # Each text file placed in this directory represents a "record". Usually these types of fixtures are named without
97
+ # extensions, but if you are on a Windows machine, you might consider adding .txt as the extension. Here's what the
98
+ # above example might look like:
99
+ #
100
+ # web_sites/google
101
+ # web_sites/yahoo.txt
102
+ # web_sites/ruby-on-rails
103
+ #
104
+ # The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax
105
+ # of "name => value". Here's an example of the ruby-on-rails fixture above:
106
+ #
107
+ # id => 1
108
+ # name => Ruby on Rails
109
+ # url => http://www.rubyonrails.org
110
+ #
111
+ # = Using Fixtures
112
+ #
113
+ # Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
114
+ # fixtures, but first let's take a look at a sample unit test:
115
+ #
116
+ # require 'web_site'
117
+ #
118
+ # class WebSiteTest < Test::Unit::TestCase
119
+ # def test_web_site_count
120
+ # assert_equal 2, WebSite.count
121
+ # end
122
+ # end
123
+ #
124
+ # As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the
125
+ # easiest way to add fixtures to the database:
126
+ #
127
+ # ...
128
+ # class WebSiteTest < Test::Unit::TestCase
129
+ # fixtures :web_sites # add more by separating the symbols with commas
130
+ # ...
131
+ #
132
+ # By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
133
+ # the testing environment to automatically load the appropriate fixtures into the database before each test.
134
+ # To ensure consistent data, the environment deletes the fixtures before running the load.
135
+ #
136
+ # In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
137
+ # of the test case. It is named after the symbol... so, in our example, there would be a hash available called
138
+ # @web_sites. This is where the "fixture name" comes into play.
139
+ #
140
+ # On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
141
+ # So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
142
+ #
143
+ # # test if the object created from the fixture data has the same attributes as the data itself
144
+ # def test_find
145
+ # assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
146
+ # end
147
+ #
148
+ # As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
149
+ # "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
150
+ # from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
151
+ # fixtures available as instance variables @web_site_1 and @web_site_2.
152
+ #
153
+ # If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
154
+ #
155
+ # - to completely disable instantiated fixtures:
156
+ # self.use_instantiated_fixtures = false
157
+ #
158
+ # - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
159
+ # self.use_instantiated_fixtures = :no_instances
160
+ #
161
+ # Even if auto-instantiated fixtures are disabled, you can still access them
162
+ # by name via special dynamic methods. Each method has the same name as the
163
+ # model, and accepts the name of the fixture to instantiate:
164
+ #
165
+ # fixtures :web_sites
166
+ #
167
+ # def test_find
168
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
169
+ # end
170
+ #
171
+ # = Dynamic fixtures with ERb
172
+ #
173
+ # Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
174
+ # mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
175
+ #
176
+ # <% for i in 1..1000 %>
177
+ # fix_<%= i %>:
178
+ # id: <%= i %>
179
+ # name: guy_<%= 1 %>
180
+ # <% end %>
181
+ #
182
+ # This will create 1000 very simple YAML fixtures.
183
+ #
184
+ # Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
185
+ # This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
186
+ # sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
187
+ # is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
188
+ #
189
+ # = Transactional fixtures
190
+ #
191
+ # TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
192
+ # They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
193
+ #
194
+ # class FooTest < Test::Unit::TestCase
195
+ # self.use_transactional_fixtures = true
196
+ # self.use_instantiated_fixtures = false
197
+ #
198
+ # fixtures :foos
199
+ #
200
+ # def test_godzilla
201
+ # assert !Foo.find(:all).empty?
202
+ # Foo.destroy_all
203
+ # assert Foo.find(:all).empty?
204
+ # end
205
+ #
206
+ # def test_godzilla_aftermath
207
+ # assert !Foo.find(:all).empty?
208
+ # end
209
+ # end
210
+ #
211
+ # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
212
+ # then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
213
+ #
214
+ # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
215
+ # access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
216
+ #
217
+ # When *not* to use transactional fixtures:
218
+ # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
219
+ # particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
220
+ # the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
221
+ # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
222
+ # Use InnoDB, MaxDB, or NDB instead.
223
+ #
224
+ # = Advanced YAML Fixtures
225
+ #
226
+ # YAML fixtures that don't specify an ID get some extra features:
227
+ #
228
+ # * Stable, autogenerated ID's
229
+ # * Label references for associations (belongs_to, has_one, has_many)
230
+ # * HABTM associations as inline lists
231
+ # * Autofilled timestamp columns
232
+ # * Fixture label interpolation
233
+ # * Support for YAML defaults
234
+ #
235
+ # == Stable, autogenerated ID's
236
+ #
237
+ # Here, have a monkey fixture:
238
+ #
239
+ # george:
240
+ # id: 1
241
+ # name: George the Monkey
242
+ #
243
+ # reginald:
244
+ # id: 2
245
+ # name: Reginald the Pirate
246
+ #
247
+ # Each of these fixtures has two unique identifiers: one for the database
248
+ # and one for the humans. Why don't we generate the primary key instead?
249
+ # Hashing each fixture's label yields a consistent ID:
250
+ #
251
+ # george: # generated id: 503576764
252
+ # name: George the Monkey
253
+ #
254
+ # reginald: # generated id: 324201669
255
+ # name: Reginald the Pirate
256
+ #
257
+ # ActiveRecord looks at the fixture's model class, discovers the correct
258
+ # primary key, and generates it right before inserting the fixture
259
+ # into the database.
260
+ #
261
+ # The generated ID for a given label is constant, so we can discover
262
+ # any fixture's ID without loading anything, as long as we know the label.
263
+ #
264
+ # == Label references for associations (belongs_to, has_one, has_many)
265
+ #
266
+ # Specifying foreign keys in fixtures can be very fragile, not to
267
+ # mention difficult to read. Since ActiveRecord can figure out the ID of
268
+ # any fixture from its label, you can specify FK's by label instead of ID.
269
+ #
270
+ # === belongs_to
271
+ #
272
+ # Let's break out some more monkeys and pirates.
273
+ #
274
+ # ### in pirates.yml
275
+ #
276
+ # reginald:
277
+ # id: 1
278
+ # name: Reginald the Pirate
279
+ # monkey_id: 1
280
+ #
281
+ # ### in monkeys.yml
282
+ #
283
+ # george:
284
+ # id: 1
285
+ # name: George the Monkey
286
+ # pirate_id: 1
287
+ #
288
+ # Add a few more monkeys and pirates and break this into multiple files,
289
+ # and it gets pretty hard to keep track of what's going on. Let's
290
+ # use labels instead of ID's:
291
+ #
292
+ # ### in pirates.yml
293
+ #
294
+ # reginald:
295
+ # name: Reginald the Pirate
296
+ # monkey: george
297
+ #
298
+ # ### in monkeys.yml
299
+ #
300
+ # george:
301
+ # name: George the Monkey
302
+ # pirate: reginald
303
+ #
304
+ # Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
305
+ # finds all the +belongs_to+ associations, and allows you to specify
306
+ # a target *label* for the *association* (monkey: george) rather than
307
+ # a target *id* for the *FK* (monkey_id: 1).
308
+ #
309
+ # ==== Polymorphic belongs_to
310
+ #
311
+ # Supporting polymorphic relationships is a little bit more complicated, since
312
+ # ActiveRecord needs to know what type your association is pointing at. Something
313
+ # like this should look familiar:
314
+ #
315
+ # ### in fruit.rb
316
+ #
317
+ # belongs_to :eater, :polymorphic => true
318
+ #
319
+ # ### in fruits.yml
320
+ #
321
+ # apple:
322
+ # id: 1
323
+ # name: apple
324
+ # eater_id: 1
325
+ # eater_type: Monkey
326
+ #
327
+ # Can we do better? You bet!
328
+ #
329
+ # apple:
330
+ # eater: george (Monkey)
331
+ #
332
+ # Just provide the polymorphic target type and ActiveRecord will take care of the rest.
333
+ #
334
+ # === has_and_belongs_to_many
335
+ #
336
+ # Time to give our monkey some fruit.
337
+ #
338
+ # ### in monkeys.yml
339
+ #
340
+ # george:
341
+ # id: 1
342
+ # name: George the Monkey
343
+ # pirate_id: 1
344
+ #
345
+ # ### in fruits.yml
346
+ #
347
+ # apple:
348
+ # id: 1
349
+ # name: apple
350
+ #
351
+ # orange:
352
+ # id: 2
353
+ # name: orange
354
+ #
355
+ # grape:
356
+ # id: 3
357
+ # name: grape
358
+ #
359
+ # ### in fruits_monkeys.yml
360
+ #
361
+ # apple_george:
362
+ # fruit_id: 1
363
+ # monkey_id: 1
364
+ #
365
+ # orange_george:
366
+ # fruit_id: 2
367
+ # monkey_id: 1
368
+ #
369
+ # grape_george:
370
+ # fruit_id: 3
371
+ # monkey_id: 1
372
+ #
373
+ # Let's make the HABTM fixture go away.
374
+ #
375
+ # ### in monkeys.yml
376
+ #
377
+ # george:
378
+ # name: George the Monkey
379
+ # pirate: reginald
380
+ # fruits: apple, orange, grape
381
+ #
382
+ # ### in fruits.yml
383
+ #
384
+ # apple:
385
+ # name: apple
386
+ #
387
+ # orange:
388
+ # name: orange
389
+ #
390
+ # grape:
391
+ # name: grape
392
+ #
393
+ # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
394
+ # on George's fixture, but we could've just as easily specified a list
395
+ # of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
396
+ # the fixture's model class and discovers the +has_and_belongs_to_many+
397
+ # associations.
398
+ #
399
+ # == Autofilled timestamp columns
400
+ #
401
+ # If your table/model specifies any of ActiveRecord's
402
+ # standard timestamp columns (created_at, created_on, updated_at, updated_on),
403
+ # they will automatically be set to Time.now.
404
+ #
405
+ # If you've set specific values, they'll be left alone.
406
+ #
407
+ # == Fixture label interpolation
408
+ #
409
+ # The label of the current fixture is always available as a column value:
410
+ #
411
+ # geeksomnia:
412
+ # name: Geeksomnia's Account
413
+ # subdomain: $LABEL
414
+ #
415
+ # Also, sometimes (like when porting older join table fixtures) you'll need
416
+ # to be able to get ahold of the identifier for a given label. ERB
417
+ # to the rescue:
418
+ #
419
+ # george_reginald:
420
+ # monkey_id: <%= Fixtures.identify(:reginald) %>
421
+ # pirate_id: <%= Fixtures.identify(:george) %>
422
+ #
423
+ # == Support for YAML defaults
424
+ #
425
+ # You probably already know how to use YAML to set and reuse defaults in
426
+ # your +database.yml+ file,. You can use the same technique in your fixtures:
427
+ #
428
+ # DEFAULTS: &DEFAULTS
429
+ # created_on: <%= 3.weeks.ago.to_s(:db) %>
430
+ #
431
+ # first:
432
+ # name: Smurf
433
+ # <<: *DEFAULTS
434
+ #
435
+ # second:
436
+ # name: Fraggle
437
+ # <<: *DEFAULTS
438
+ #
439
+ # Any fixture labeled "DEFAULTS" is safely ignored.
440
+
441
+ class BigRecordFixtures < YAML::Omap
442
+ DEFAULT_FILTER_RE = /\.ya?ml$/
443
+
444
+ @@all_cached_big_record_fixtures = {}
445
+
446
+ def self.reset_cache(connection = nil)
447
+ connection ||= BigRecord::Base.connection
448
+ @@all_cached_big_record_fixtures[connection.object_id] = {}
449
+ end
450
+
451
+ def self.cache_for_connection(connection)
452
+ @@all_cached_big_record_fixtures[connection.object_id] ||= {}
453
+ @@all_cached_big_record_fixtures[connection.object_id]
454
+ end
455
+
456
+ def self.fixture_is_cached?(connection, table_name)
457
+ cache_for_connection(connection)[table_name]
458
+ end
459
+
460
+ def self.cached_fixtures(connection, keys_to_fetch = nil)
461
+ if keys_to_fetch
462
+ fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
463
+ else
464
+ fixtures = cache_for_connection(connection).values
465
+ end
466
+ fixtures.size > 1 ? fixtures : fixtures.first
467
+ end
468
+
469
+ def self.cache_fixtures(connection, fixtures)
470
+ cache_for_connection(connection).update(fixtures.index_by(&:table_name))
471
+ end
472
+
473
+ def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
474
+ object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
475
+ if load_instances
476
+ BigRecord::Base.silence do
477
+ fixtures.each do |name, fixture|
478
+ begin
479
+ object.instance_variable_set "@#{name}", fixture.find
480
+ rescue BigRecordFixtureClassNotFound
481
+ nil
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ def self.instantiate_all_loaded_fixtures(object, load_instances = true)
489
+ all_loaded_fixtures.each do |table_name, fixtures|
490
+ BigRecordFixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
491
+ end
492
+ end
493
+
494
+ cattr_accessor :all_loaded_fixtures
495
+ self.all_loaded_fixtures = {}
496
+
497
+ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
498
+ table_names = [table_names].flatten.map { |n| n.to_s }
499
+ connection = block_given? ? yield : BigRecord::Base.connection
500
+
501
+ table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
502
+
503
+ unless table_names_to_fetch.empty?
504
+ BigRecord::Base.silence do
505
+ connection.disable_referential_integrity do
506
+ fixtures_map = {}
507
+
508
+ fixtures = table_names_to_fetch.map do |table_name|
509
+ fixtures_map[table_name] = BigRecordFixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
510
+ end
511
+
512
+ all_loaded_fixtures.update(fixtures_map)
513
+
514
+ # connection.transaction(Thread.current['open_transactions'].to_i == 0) do
515
+ fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
516
+ fixtures.each { |fixture| fixture.insert_fixtures }
517
+
518
+ # Cap primary key sequences to max(pk).
519
+ if connection.respond_to?(:reset_pk_sequence!)
520
+ table_names.each do |table_name|
521
+ connection.reset_pk_sequence!(table_name)
522
+ end
523
+ end
524
+ # end
525
+
526
+ cache_fixtures(connection, fixtures)
527
+ end
528
+ end
529
+ end
530
+ cached_fixtures(connection, table_names)
531
+ end
532
+
533
+ # Returns a consistent identifier for +label+. This will always
534
+ # be a positive integer, and will always be the same for a given
535
+ # label, assuming the same OS, platform, and version of Ruby.
536
+ def self.identify(label)
537
+ label.to_s.hash.abs
538
+ end
539
+
540
+ attr_reader :table_name
541
+
542
+ def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
543
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
544
+ @class_name = class_name ||
545
+ (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
546
+ @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
547
+ @table_name = class_name.table_name if class_name.respond_to?(:table_name)
548
+ @connection = class_name.connection if class_name.respond_to?(:connection)
549
+ read_fixture_files
550
+ end
551
+
552
+ def delete_existing_fixtures
553
+ @connection.get_consecutive_rows(table_name, nil, nil, ["attribute:"]).each do |row|
554
+ @connection.delete(table_name, row['id'])
555
+ end
556
+ end
557
+
558
+ def insert_fixtures
559
+ now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
560
+ now = now.to_s(:db)
561
+
562
+ # allow a standard key to be used for doing defaults in YAML
563
+ delete(assoc("DEFAULTS"))
564
+
565
+ # track any join tables we need to insert later
566
+ habtm_fixtures = Hash.new do |h, habtm|
567
+ h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
568
+ end
569
+
570
+ each do |label, fixture|
571
+ row = fixture.to_hash
572
+
573
+ if model_class && model_class < ActiveRecord::Base
574
+ # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
575
+ if model_class.record_timestamps
576
+ timestamp_column_names.each do |name|
577
+ row[name] = now unless row.key?(name)
578
+ end
579
+ end
580
+
581
+ # interpolate the fixture label
582
+ row.each do |key, value|
583
+ row[key] = label if value == "$LABEL"
584
+ end
585
+
586
+ # generate a primary key if necessary
587
+ if has_primary_key_column? && !row.include?(primary_key_name)
588
+ row[primary_key_name] = BigRecordFixtures.identify(label)
589
+ end
590
+
591
+ # If STI is used, find the correct subclass for association reflection
592
+ reflection_class =
593
+ if row.include?(inheritance_column_name)
594
+ row[inheritance_column_name].constantize rescue model_class
595
+ else
596
+ model_class
597
+ end
598
+
599
+ reflection_class.reflect_on_all_associations.each do |association|
600
+ case association.macro
601
+ when :belongs_to
602
+ # Do not replace association name with association foreign key if they are named the same
603
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
604
+
605
+ if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
606
+ if association.options[:polymorphic]
607
+ if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
608
+ target_type = $1
609
+ target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
610
+
611
+ # support polymorphic belongs_to as "label (Type)"
612
+ row[target_type_name] = target_type
613
+ end
614
+ end
615
+
616
+ row[fk_name] = BigRecordFixtures.identify(value)
617
+ end
618
+ when :has_and_belongs_to_many
619
+ if (targets = row.delete(association.name.to_s))
620
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
621
+ join_fixtures = habtm_fixtures[association]
622
+
623
+ targets.each do |target|
624
+ join_fixtures["#{label}_#{target}"] = BigRecordFixture.new(
625
+ { association.primary_key_name => row[primary_key_name],
626
+ association.association_foreign_key => BigRecordFixtures.identify(target) }, nil)
627
+ end
628
+ end
629
+ end
630
+ end
631
+ end
632
+
633
+ @connection.insert_fixture(fixture, @table_name)
634
+ end
635
+
636
+ # insert any HABTM join tables we discovered
637
+ habtm_fixtures.values.each do |fixture|
638
+ fixture.delete_existing_fixtures
639
+ fixture.insert_fixtures
640
+ end
641
+ end
642
+
643
+ private
644
+ class HabtmFixtures < ::BigRecordFixtures #:nodoc:
645
+ def read_fixture_files; end
646
+ end
647
+
648
+ def model_class
649
+ @model_class ||= @class_name.is_a?(Class) ?
650
+ @class_name : @class_name.constantize rescue nil
651
+ end
652
+
653
+ def primary_key_name
654
+ @primary_key_name ||= model_class && model_class.primary_key
655
+ end
656
+
657
+ def has_primary_key_column?
658
+ @has_primary_key_column ||= model_class && primary_key_name &&
659
+ model_class.columns.find { |c| c.name == primary_key_name }
660
+ end
661
+
662
+ def timestamp_column_names
663
+ @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
664
+ column_names.include?(name)
665
+ end
666
+ end
667
+
668
+ def inheritance_column_name
669
+ @inheritance_column_name ||= model_class && model_class.inheritance_column
670
+ end
671
+
672
+ def column_names
673
+ @column_names ||= @connection.columns(@table_name).collect(&:name)
674
+ end
675
+
676
+ def read_fixture_files
677
+ if File.file?(yaml_file_path)
678
+ read_yaml_fixture_files
679
+ elsif File.file?(csv_file_path)
680
+ read_csv_fixture_files
681
+ else
682
+ # Standard fixtures
683
+ Dir.entries(@fixture_path).each do |file|
684
+ path = File.join(@fixture_path, file)
685
+ if File.file?(path) and file !~ @file_filter
686
+ self[file] = BigRecordFixture.new(path, @class_name)
687
+ end
688
+ end
689
+ end
690
+ end
691
+
692
+ def read_yaml_fixture_files
693
+ yaml_string = ""
694
+ Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
695
+ yaml_string << IO.read(subfixture_path)
696
+ end
697
+ yaml_string << IO.read(yaml_file_path)
698
+
699
+ if yaml = parse_yaml_string(yaml_string)
700
+ # If the file is an ordered map, extract its children.
701
+ yaml_value =
702
+ if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
703
+ yaml.value
704
+ else
705
+ [yaml]
706
+ end
707
+
708
+ yaml_value.each do |fixture|
709
+ fixture.each do |name, data|
710
+ unless data
711
+ raise BigRecordFixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
712
+ end
713
+
714
+ self[name] = BigRecordFixture.new(data, @class_name)
715
+ end
716
+ end
717
+ end
718
+ end
719
+
720
+ def read_csv_fixture_files
721
+ reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
722
+ header = reader.shift
723
+ i = 0
724
+ reader.each do |row|
725
+ data = {}
726
+ row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
727
+ self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= BigRecordFixture.new(data, @class_name)
728
+ end
729
+ end
730
+
731
+ def yaml_file_path
732
+ "#{@fixture_path}.yml"
733
+ end
734
+
735
+ def csv_file_path
736
+ @fixture_path + ".csv"
737
+ end
738
+
739
+ def yaml_fixtures_key(path)
740
+ File.basename(@fixture_path).split(".").first
741
+ end
742
+
743
+ def parse_yaml_string(fixture_content)
744
+ YAML::load(erb_render(fixture_content))
745
+ rescue => error
746
+ raise BigRecordFixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}"
747
+ end
748
+
749
+ #FIXME: Turn ERB render back on, conditionnaly...
750
+ def erb_render(fixture_content)
751
+ fixture_content
752
+ #ERB.new(fixture_content).result
753
+ end
754
+ end
755
+
756
+ class BigRecordFixture #:nodoc:
757
+ include Enumerable
758
+
759
+ class BigRecordFixtureError < StandardError #:nodoc:
760
+ end
761
+
762
+ class BigRecordFixtureFormatError < BigRecordFixtureError #:nodoc:
763
+ end
764
+
765
+ attr_reader :class_name
766
+
767
+ def initialize(fixture, class_name)
768
+ case fixture
769
+ when Hash, YAML::Omap
770
+ @fixture = fixture
771
+ when String
772
+ @fixture = read_fixture_file(fixture)
773
+ else
774
+ raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
775
+ end
776
+
777
+ @class_name = class_name
778
+ end
779
+
780
+ def each
781
+ @fixture.each { |item| yield item }
782
+ end
783
+
784
+ def [](key)
785
+ @fixture[key]
786
+ end
787
+
788
+ def to_hash
789
+ @fixture
790
+ end
791
+
792
+ def key_list
793
+ columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
794
+ columns.join(", ")
795
+ end
796
+
797
+ def value_list
798
+ klass = @class_name.constantize rescue nil
799
+
800
+ list = @fixture.inject([]) do |fixtures, (key, value)|
801
+ col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
802
+ fixtures << BigRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
803
+ end
804
+ list * ', '
805
+ end
806
+
807
+ def find
808
+ klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
809
+ if klass
810
+ klass.find(self[klass.primary_key])
811
+ else
812
+ raise BigRecordFixtureClassNotFound, "The class #{@class_name.inspect} was not found."
813
+ end
814
+ end
815
+
816
+ private
817
+ def read_fixture_file(fixture_file_path)
818
+ IO.readlines(fixture_file_path).inject({}) do |fixture, line|
819
+ # Mercifully skip empty lines.
820
+ next if line =~ /^\s*$/
821
+
822
+ # Use the same regular expression for attributes as Active Record.
823
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
824
+ raise FormatError, "#{fixture_file_path}: fixture format error at '#{line}'. Expecting 'key => value'."
825
+ end
826
+ key, value = md.captures
827
+
828
+ # Disallow duplicate keys to catch typos.
829
+ raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
830
+ fixture[key] = value.strip
831
+ fixture
832
+ end
833
+ end
834
+ end
835
+
836
+ module BigRecord
837
+ module TestFixtures
838
+ def self.included(base)
839
+ base.class_eval do
840
+ setup :setup_bigrecord_fixtures
841
+ teardown :teardown_bigrecord_fixtures
842
+
843
+ superclass_delegating_accessor :bigrecord_fixture_table_names
844
+ superclass_delegating_accessor :bigrecord_fixture_class_names
845
+ superclass_delegating_accessor :bigrecord_use_transactional_fixtures
846
+ superclass_delegating_accessor :bigrecord_use_instantiated_fixtures # true, false, or :no_instances
847
+ superclass_delegating_accessor :bigrecord_pre_loaded_fixtures
848
+
849
+ self.bigrecord_fixture_table_names = []
850
+ self.bigrecord_use_transactional_fixtures = false
851
+ self.bigrecord_use_instantiated_fixtures = true
852
+ self.bigrecord_pre_loaded_fixtures = false
853
+
854
+ @@already_loaded_bigrecord_fixtures = {}
855
+ self.bigrecord_fixture_class_names = {}
856
+ end
857
+
858
+ base.extend ClassMethods
859
+ end
860
+
861
+ module ClassMethods
862
+
863
+ def bigrecord_set_fixture_class(class_names = {})
864
+ self.bigrecord_fixture_class_names = self.bigrecord_fixture_class_names.merge(class_names)
865
+ end
866
+
867
+ def bigrecord_fixtures(*bigrecord_table_names)
868
+ if bigrecord_table_names.first == :all
869
+ bigrecord_table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
870
+ bigrecord_table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
871
+ else
872
+ bigrecord_table_names = bigrecord_table_names.flatten.map { |n| n.to_s }
873
+ end
874
+
875
+ self.bigrecord_fixture_table_names |= bigrecord_table_names
876
+ require_bigrecord_fixture_classes(bigrecord_table_names)
877
+ setup_bigrecord_fixture_accessors(bigrecord_table_names)
878
+ end
879
+
880
+ def require_bigrecord_fixture_classes(table_names = nil)
881
+ (table_names || fixture_table_names).each do |table_name|
882
+ file_name = table_name.to_s
883
+ file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
884
+ begin
885
+ require_dependency file_name
886
+ rescue LoadError
887
+ # Let's hope the developer has included it himself
888
+ end
889
+ end
890
+ end
891
+
892
+ def setup_bigrecord_fixture_accessors(table_names = nil)
893
+ (table_names || fixture_table_names).each do |table_name|
894
+ table_name = table_name.to_s.tr('.', '_')
895
+
896
+ define_method(table_name) do |*fixtures|
897
+ force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
898
+ @bigrecord_fixture_cache[table_name] ||= {}
899
+
900
+ instances = fixtures.map do |fixture|
901
+ @bigrecord_fixture_cache[table_name].delete(fixture) if force_reload
902
+
903
+ if @loaded_bigrecord_fixtures[table_name][fixture.to_s]
904
+ @bigrecord_fixture_cache[table_name][fixture] ||= @loaded_bigrecord_fixtures[table_name][fixture.to_s].find
905
+ else
906
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
907
+ end
908
+ end
909
+
910
+ instances.size == 1 ? instances.first : instances
911
+ end
912
+ end
913
+ end
914
+
915
+ def uses_transaction(*methods)
916
+ @bigrecord_uses_transaction = [] unless defined?(@bigrecord_uses_transaction)
917
+ @bigrecord_uses_transaction.concat methods.map(&:to_s)
918
+ end
919
+
920
+ def uses_transaction?(method)
921
+ @uses_transaction = [] unless defined?(@uses_transaction)
922
+ @uses_transaction.include?(method.to_s)
923
+ end
924
+ end
925
+
926
+ def run_in_transaction?
927
+ use_transactional_fixtures &&
928
+ !self.class.uses_transaction?(method_name)
929
+ end
930
+
931
+ def setup_bigrecord_fixtures
932
+ return if @bigrecord_fixtures_setup
933
+ @bigrecord_fixtures_setup = true
934
+ return unless defined?(BigRecord::Base) && !BigRecord::Base.configurations.blank?
935
+
936
+ if pre_loaded_fixtures && !use_transactional_fixtures
937
+ raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
938
+ end
939
+
940
+ @bigrecord_fixture_cache = {}
941
+
942
+ BigRecordFixtures.reset_cache
943
+ @@already_loaded_bigrecord_fixtures[self.class] = nil
944
+ load_bigrecord_fixtures
945
+
946
+ # Instantiate fixtures for every test if requested.
947
+ instantiate_fixtures if use_instantiated_fixtures
948
+ end
949
+
950
+ def teardown_bigrecord_fixtures
951
+ return if @bigrecord_fixtures_teardown
952
+ @bigrecord_fixtures_teardown = true
953
+ return unless defined?(BigRecord::Base) && !BigRecord::Base.configurations.blank?
954
+
955
+ unless use_transactional_fixtures?
956
+ BigRecordFixtures.reset_cache
957
+ end
958
+
959
+ BigRecord::Base.verify_active_connections!
960
+ end
961
+
962
+ def self.method_added(method)
963
+ return if @__disable_method_added__
964
+ @__disable_method_added__ = true
965
+
966
+ case method.to_s
967
+ when 'setup'
968
+ undef_method :setup_without_fixtures if method_defined?(:setup_without_fixtures)
969
+ alias_method :setup_without_fixtures, :setup
970
+ define_method(:full_setup) do
971
+ setup_with_bigrecord_fixtures
972
+ setup_without_fixtures
973
+ end
974
+ alias_method :setup, :full_setup
975
+ when 'teardown'
976
+ undef_method :teardown_without_fixtures if method_defined?(:teardown_without_fixtures)
977
+ alias_method :teardown_without_fixtures, :teardown
978
+ define_method(:full_teardown) do
979
+ teardown_fixtures
980
+ teardown_with_bigrecord_fixtures
981
+ end
982
+ alias_method :teardown, :full_teardown
983
+ end
984
+
985
+ @__disable_method_added__ = false
986
+ end
987
+
988
+ private
989
+ def load_bigrecord_fixtures
990
+ @loaded_bigrecord_fixtures = {}
991
+ fixtures = BigRecordFixtures.create_fixtures(fixture_path, bigrecord_fixture_table_names, bigrecord_fixture_class_names)
992
+
993
+ unless fixtures.nil?
994
+ if fixtures.instance_of?(BigRecordFixtures)
995
+ @loaded_bigrecord_fixtures[fixtures.table_name] = fixtures
996
+ else
997
+ fixtures.each { |f| @loaded_bigrecord_fixtures[f.table_name] = f }
998
+ end
999
+ end
1000
+ end
1001
+
1002
+ # for pre_loaded_fixtures, only require the classes once. huge speed improvement
1003
+ @@required_bigrecord_fixture_classes = false
1004
+
1005
+ def instantiate_bigrecord_fixtures
1006
+ if pre_loaded_fixtures
1007
+ raise RuntimeError, 'Load fixtures before instantiating them.' if BigRecordFixtures.all_loaded_fixtures.empty?
1008
+ unless @@required_bigrecord_fixture_classes
1009
+ self.class.require_fixture_classes BigRecordFixtures.all_loaded_fixtures.keys
1010
+ @@required_bigrecord_fixture_classes = true
1011
+ end
1012
+ BigRecordFixtures.instantiate_all_loaded_fixtures(self, load_instances?)
1013
+ else
1014
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_bigrecord_fixtures.nil?
1015
+ @loaded_bigrecord_fixtures.each do |table_name, fixtures|
1016
+ BigRecordFixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
1017
+ end
1018
+ end
1019
+ end
1020
+
1021
+ def load_bigrecord_instances?
1022
+ use_instantiated_fixtures != :no_instances
1023
+ end
1024
+ end
1025
+ end