bigrecord 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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,380 @@
1
+ # require 'active_support/core_ext/object/metaclass'
2
+
3
+ # Instead of requiring the activesupport class (metaclass)
4
+ class Object
5
+ # Get object's meta (ghost, eigenclass, singleton) class
6
+ def metaclass
7
+ class << self
8
+ self
9
+ end
10
+ end
11
+
12
+ # If class_eval is called on an object, add those methods to its metaclass
13
+ def class_eval(*args, &block)
14
+ metaclass.class_eval(*args, &block)
15
+ end
16
+ end
17
+
18
+ module BigRecord
19
+ class IrreversibleMigration < BigRecordError#:nodoc:
20
+ end
21
+
22
+ class DuplicateMigrationVersionError < BigRecordError#:nodoc:
23
+ def initialize(version)
24
+ super("Multiple migrations have the version number #{version}")
25
+ end
26
+ end
27
+
28
+ class DuplicateMigrationNameError < BigRecordError#:nodoc:
29
+ def initialize(name)
30
+ super("Multiple migrations have the name #{name}")
31
+ end
32
+ end
33
+
34
+ class UnknownMigrationVersionError < BigRecordError #:nodoc:
35
+ def initialize(version)
36
+ super("No migration with version number #{version}")
37
+ end
38
+ end
39
+
40
+ class IllegalMigrationNameError < BigRecordError#:nodoc:
41
+ def initialize(name)
42
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
43
+ end
44
+ end
45
+
46
+ # Although column-oriented databases are generally schema-less, certain ones (like Hbase)
47
+ # require the creation of tables and column families ahead of time. The individual columns,
48
+ # however, are defined in the model itself and can be modified dynamically without the need for migrations.
49
+ #
50
+ # Unless you're familiar with column families, the majority of use cases work perfectly fine within one
51
+ # column family. When you generate a bigrecord_model, it will default to creating the :attribute column family.
52
+ #
53
+ # The following is a standard migration file that creates a table called "Books" with the default
54
+ # column family :attribute that has the following option of 100 versions and uses the 'lzo' compression scheme.
55
+ # Leave any options blank for the default value.
56
+ #
57
+ # class CreateBooks < BigRecord::Migration
58
+ # def self.up
59
+ # create_table :books, :force => true do |t|
60
+ # t.family :attribute, :versions => 100, :compression => 'lzo'
61
+ # end
62
+ # end
63
+ #
64
+ # def self.down
65
+ # drop_table :books
66
+ # end
67
+ # end
68
+ #
69
+ class Migration
70
+ @@verbose = true
71
+ cattr_accessor :verbose
72
+
73
+ class << self
74
+ def up_with_benchmarks #:nodoc:
75
+ migrate(:up)
76
+ end
77
+
78
+ def down_with_benchmarks #:nodoc:
79
+ migrate(:down)
80
+ end
81
+
82
+ # Execute this migration in the named direction
83
+ def migrate(direction)
84
+ return unless respond_to?(direction)
85
+
86
+ case direction
87
+ when :up then announce "migrating"
88
+ when :down then announce "reverting"
89
+ end
90
+
91
+ result = nil
92
+ time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
93
+
94
+ case direction
95
+ when :up then announce "migrated (%.4fs)" % time.real; write
96
+ when :down then announce "reverted (%.4fs)" % time.real; write
97
+ end
98
+
99
+ result
100
+ end
101
+
102
+ # Because the method added may do an alias_method, it can be invoked
103
+ # recursively. We use @ignore_new_methods as a guard to indicate whether
104
+ # it is safe for the call to proceed.
105
+ def singleton_method_added(sym) #:nodoc:
106
+ return if defined?(@ignore_new_methods) && @ignore_new_methods
107
+
108
+ begin
109
+ @ignore_new_methods = true
110
+
111
+ case sym
112
+ when :up, :down
113
+ metaclass.send(:alias_method_chain, sym, "benchmarks")
114
+ end
115
+ ensure
116
+ @ignore_new_methods = false
117
+ end
118
+ end
119
+
120
+ def write(text="")
121
+ puts(text) if verbose
122
+ end
123
+
124
+ def announce(message)
125
+ text = "#{@version} #{name}: #{message}"
126
+ length = [0, 75 - text.length].max
127
+ write "== %s %s" % [text, "=" * length]
128
+ end
129
+
130
+ def say(message, subitem=false)
131
+ write "#{subitem ? " ->" : "--"} #{message}"
132
+ end
133
+
134
+ def say_with_time(message)
135
+ say(message)
136
+ result = nil
137
+ time = Benchmark.measure { result = yield }
138
+ say "%.4fs" % time.real, :subitem
139
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
140
+ result
141
+ end
142
+
143
+ def suppress_messages
144
+ save, self.verbose = verbose, false
145
+ yield
146
+ ensure
147
+ self.verbose = save
148
+ end
149
+
150
+ def connection
151
+ BigRecord::Base.connection
152
+ end
153
+
154
+ def method_missing(method, *arguments, &block)
155
+ arg_list = arguments.map(&:inspect) * ', '
156
+
157
+ say_with_time "#{method}(#{arg_list})" do
158
+ unless arguments.empty? || method == :execute
159
+ arguments[0] = Migrator.proper_table_name(arguments.first)
160
+ end
161
+ connection.send(method, *arguments, &block)
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # MigrationProxy is used to defer loading of the actual migration classes
168
+ # until they are needed
169
+ class MigrationProxy
170
+
171
+ attr_accessor :name, :version, :filename
172
+
173
+ delegate :migrate, :announce, :write, :to=>:migration
174
+
175
+ private
176
+
177
+ def migration
178
+ @migration ||= load_migration
179
+ end
180
+
181
+ def load_migration
182
+ load(filename)
183
+ name.constantize
184
+ end
185
+
186
+ end
187
+
188
+ class Migrator#:nodoc:
189
+ class << self
190
+ def migrate(migrations_path, target_version = nil)
191
+ case
192
+ when target_version.nil? then up(migrations_path, target_version)
193
+ when current_version > target_version then down(migrations_path, target_version)
194
+ else up(migrations_path, target_version)
195
+ end
196
+ end
197
+
198
+ def rollback(migrations_path, steps=1)
199
+ migrator = self.new(:down, migrations_path)
200
+ start_index = migrator.migrations.index(migrator.current_migration)
201
+
202
+ return unless start_index
203
+
204
+ finish = migrator.migrations[start_index + steps]
205
+ down(migrations_path, finish ? finish.version : 0)
206
+ end
207
+
208
+ def up(migrations_path, target_version = nil)
209
+ self.new(:up, migrations_path, target_version).migrate
210
+ end
211
+
212
+ def down(migrations_path, target_version = nil)
213
+ self.new(:down, migrations_path, target_version).migrate
214
+ end
215
+
216
+ def run(direction, migrations_path, target_version)
217
+ self.new(direction, migrations_path, target_version).run
218
+ end
219
+
220
+ def schema_migrations_table_name
221
+ Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
222
+ end
223
+
224
+ def get_all_versions
225
+ Base.connection.get_all_schema_versions.sort
226
+ end
227
+
228
+ def current_version
229
+ sm_table = schema_migrations_table_name
230
+ if Base.connection.table_exists?(sm_table)
231
+ get_all_versions.max || 0
232
+ else
233
+ 0
234
+ end
235
+ end
236
+
237
+ def proper_table_name(name)
238
+ # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
239
+ name.table_name rescue "#{BigRecord::Base.table_name_prefix}#{name}#{BigRecord::Base.table_name_suffix}"
240
+ end
241
+ end
242
+
243
+ def initialize(direction, migrations_path, target_version = nil)
244
+ raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
245
+ Base.connection.initialize_schema_migrations_table
246
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
247
+ end
248
+
249
+ def current_version
250
+ migrated.last || 0
251
+ end
252
+
253
+ def current_migration
254
+ migrations.detect { |m| m.version == current_version }
255
+ end
256
+
257
+ def run
258
+ target = migrations.detect { |m| m.version == @target_version }
259
+ raise UnknownMigrationVersionError.new(@target_version) if target.nil?
260
+ unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
261
+ target.migrate(@direction)
262
+ record_version_state_after_migrating(target.version)
263
+ end
264
+ end
265
+
266
+ def migrate
267
+ current = migrations.detect { |m| m.version == current_version }
268
+ target = migrations.detect { |m| m.version == @target_version }
269
+
270
+ if target.nil? && !@target_version.nil? && @target_version > 0
271
+ raise UnknownMigrationVersionError.new(@target_version)
272
+ end
273
+
274
+ start = up? ? 0 : (migrations.index(current) || 0)
275
+ finish = migrations.index(target) || migrations.size - 1
276
+ runnable = migrations[start..finish]
277
+
278
+ # skip the last migration if we're headed down, but not ALL the way down
279
+ runnable.pop if down? && !target.nil?
280
+
281
+ runnable.each do |migration|
282
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
283
+
284
+ # On our way up, we skip migrating the ones we've already migrated
285
+ next if up? && migrated.include?(migration.version.to_i)
286
+
287
+ # On our way down, we skip reverting the ones we've never migrated
288
+ if down? && !migrated.include?(migration.version.to_i)
289
+ migration.announce 'never migrated, skipping'; migration.write
290
+ next
291
+ end
292
+
293
+ begin
294
+ ddl_transaction do
295
+ migration.migrate(@direction)
296
+ record_version_state_after_migrating(migration.version)
297
+ end
298
+ rescue => e
299
+ canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
300
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
301
+ end
302
+ end
303
+ end
304
+
305
+ def migrations
306
+ @migrations ||= begin
307
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
308
+
309
+ migrations = files.inject([]) do |klasses, file|
310
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
311
+
312
+ raise IllegalMigrationNameError.new(file) unless version
313
+ version = version.to_i
314
+
315
+ if klasses.detect { |m| m.version == version }
316
+ raise DuplicateMigrationVersionError.new(version)
317
+ end
318
+
319
+ if klasses.detect { |m| m.name == name.camelize }
320
+ raise DuplicateMigrationNameError.new(name.camelize)
321
+ end
322
+
323
+ migration = MigrationProxy.new
324
+ migration.name = name.camelize
325
+ migration.version = version
326
+ migration.filename = file
327
+ klasses << migration
328
+ end
329
+
330
+ migrations = migrations.sort_by(&:version)
331
+ down? ? migrations.reverse : migrations
332
+ end
333
+ end
334
+
335
+ def pending_migrations
336
+ already_migrated = migrated
337
+ migrations.reject { |m| already_migrated.include?(m.version.to_i) }
338
+ end
339
+
340
+ def migrated
341
+ @migrated_versions ||= self.class.get_all_versions
342
+ end
343
+
344
+ private
345
+ def record_version_state_after_migrating(version)
346
+ sm_table = self.class.schema_migrations_table_name
347
+
348
+ @migrated_versions ||= []
349
+ if down?
350
+ @migrated_versions.delete(version.to_i)
351
+
352
+ # TODO: Consider putting this in the adapter instead
353
+ Base.connection.delete(sm_table, version.to_s)
354
+ else
355
+ @migrated_versions.push(version.to_i).sort!
356
+
357
+ timestamp = Time.parse(version.to_s).to_bigrecord_timestamp
358
+ # TODO: Consider putting this in the adapter instead
359
+ Base.connection.update(sm_table, version.to_s, {'attribute:version' => version}, timestamp)
360
+ end
361
+ end
362
+
363
+ def up?
364
+ @direction == :up
365
+ end
366
+
367
+ def down?
368
+ @direction == :down
369
+ end
370
+
371
+ # Wrap the migration in a transaction only if supported by the adapter.
372
+ def ddl_transaction(&block)
373
+ if Base.connection.supports_ddl_transactions?
374
+ Base.transaction { block.call }
375
+ else
376
+ block.call
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,65 @@
1
+ # The following overrides are required because we use strings as ids and it's
2
+ # possible that these strings are not clean. The default implementation
3
+ # escapes them with URI.escape() but its' not good. e.g. '&' becomes &amp; instead of %26
4
+ module ActionController
5
+ module Routing
6
+ class PathSegment
7
+ def interpolation_chunk(value_code = "#{local_name}")
8
+ "\#{CGI.escape(#{value_code}.to_s)}"
9
+ end
10
+
11
+ class Result
12
+ def self.new_escaped(strings)
13
+ new strings.collect {|str| CGI.unescape(str)}
14
+ end
15
+ end
16
+ end
17
+
18
+ class DynamicSegment
19
+ def interpolation_chunk(value_code = "#{local_name}")
20
+ "\#{CGI.escape(#{value_code}.to_s)}"
21
+ end
22
+
23
+ def match_extraction(next_capture)
24
+ # All non code-related keys (such as :id, :slug) are URI-unescaped as
25
+ # path parameters.
26
+ default_value = default ? default.inspect : nil
27
+ %[
28
+ value = if (m = match[#{next_capture}])
29
+ CGI.unescape(m)
30
+ else
31
+ #{default_value}
32
+ end
33
+ params[:#{key}] = value if value
34
+ ]
35
+ end
36
+ end
37
+ end
38
+
39
+ # TODO: Remove this monkey patch once we migrate to rails 2.3. This patch
40
+ # add support for the :as option in map.resources.
41
+ module Resources
42
+ class Resource
43
+
44
+ attr_reader :path_segment
45
+
46
+ def initialize(entities, options)
47
+ @plural ||= entities
48
+ @singular ||= options[:singular] || plural.to_s.singularize
49
+ @path_segment = options.delete(:as) || @plural
50
+
51
+ @options = options
52
+
53
+ arrange_actions
54
+ add_default_actions
55
+ set_prefixes
56
+ end
57
+
58
+ def path
59
+ @path ||= "#{path_prefix}/#{path_segment}"
60
+ end
61
+
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,51 @@
1
+ module BigRecord
2
+ # Active Record automatically timestamps create and update if the table has fields
3
+ # created_at/created_on or updated_at/updated_on.
4
+ #
5
+ # Timestamping can be turned off by setting
6
+ # <tt>ActiveRecord::Base.record_timestamps = false</tt>
7
+ #
8
+ # Keep in mind that, via inheritance, you can turn off timestamps on a per
9
+ # model basis by setting <tt>record_timestamps</tt> to false in the desired
10
+ # models.
11
+ #
12
+ # class Feed < ActiveRecord::Base
13
+ # self.record_timestamps = false
14
+ # # ...
15
+ # end
16
+ #
17
+ # Timestamps are in the local timezone by default but can use UTC by setting
18
+ # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
19
+ module Timestamp
20
+ def self.included(base) #:nodoc:
21
+ super
22
+
23
+ base.alias_method_chain :create, :timestamps
24
+ base.alias_method_chain :update, :timestamps
25
+
26
+ base.cattr_accessor :record_timestamps, :instance_writer => false
27
+ base.record_timestamps = true
28
+ end
29
+
30
+ def create_with_timestamps #:nodoc:
31
+ if record_timestamps
32
+ t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
33
+ self.send(:created_at=, t) if respond_to?(:created_at) && created_at.nil?
34
+ self.send(:created_on=, t) if respond_to?(:created_on) && created_on.nil?
35
+
36
+ self.send(:updated_at=, t) if respond_to?(:updated_at)
37
+ self.send(:updated_on=, t) if respond_to?(:updated_on)
38
+ end
39
+ create_without_timestamps
40
+ end
41
+
42
+ def update_with_timestamps #:nodoc:
43
+ if record_timestamps
44
+ t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
45
+ self.send(:updated_at=, t) if respond_to?(:updated_at)
46
+ self.send(:updated_on=, t) if respond_to?(:updated_on)
47
+ end
48
+ update_without_timestamps
49
+ end
50
+ end
51
+ end