activerecord 1.13.2 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +452 -10
- data/RUNNING_UNIT_TESTS +1 -1
- data/lib/active_record.rb +5 -2
- data/lib/active_record/acts/list.rb +1 -1
- data/lib/active_record/acts/tree.rb +29 -25
- data/lib/active_record/aggregations.rb +3 -2
- data/lib/active_record/associations.rb +783 -337
- data/lib/active_record/associations/association_collection.rb +7 -12
- data/lib/active_record/associations/association_proxy.rb +62 -24
- data/lib/active_record/associations/belongs_to_association.rb +27 -46
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
- data/lib/active_record/associations/has_many_association.rb +61 -56
- data/lib/active_record/associations/has_many_through_association.rb +144 -0
- data/lib/active_record/associations/has_one_association.rb +22 -16
- data/lib/active_record/base.rb +482 -182
- data/lib/active_record/calculations.rb +225 -0
- data/lib/active_record/callbacks.rb +7 -7
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
- data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
- data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
- data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
- data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
- data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
- data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
- data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
- data/lib/active_record/fixtures.rb +42 -17
- data/lib/active_record/locking.rb +36 -15
- data/lib/active_record/migration.rb +111 -8
- data/lib/active_record/observer.rb +25 -1
- data/lib/active_record/reflection.rb +103 -41
- data/lib/active_record/schema.rb +2 -2
- data/lib/active_record/schema_dumper.rb +55 -18
- data/lib/active_record/timestamp.rb +6 -6
- data/lib/active_record/validations.rb +65 -40
- data/lib/active_record/vendor/db2.rb +10 -5
- data/lib/active_record/vendor/simple.rb +693 -702
- data/lib/active_record/version.rb +2 -2
- data/rakefile +4 -4
- data/test/aaa_create_tables_test.rb +25 -6
- data/test/abstract_unit.rb +39 -1
- data/test/adapter_test.rb +31 -4
- data/test/associations_cascaded_eager_loading_test.rb +106 -0
- data/test/associations_go_eager_test.rb +85 -16
- data/test/associations_join_model_test.rb +338 -0
- data/test/associations_test.rb +129 -50
- data/test/base_test.rb +204 -49
- data/test/binary_test.rb +1 -1
- data/test/calculations_test.rb +169 -0
- data/test/callbacks_test.rb +5 -23
- data/test/class_inheritable_attributes_test.rb +1 -1
- data/test/column_alias_test.rb +1 -1
- data/test/connections/native_mysql/connection.rb +1 -0
- data/test/connections/native_openbase/connection.rb +22 -0
- data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
- data/test/connections/native_sqlite/connection.rb +1 -1
- data/test/connections/native_sqlite3/connection.rb +1 -0
- data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
- data/test/connections/native_sybase/connection.rb +24 -0
- data/test/defaults_test.rb +18 -0
- data/test/deprecated_associations_test.rb +2 -2
- data/test/deprecated_finder_test.rb +0 -6
- data/test/finder_test.rb +26 -23
- data/test/fixtures/accounts.yml +10 -0
- data/test/fixtures/author.rb +31 -6
- data/test/fixtures/author_favorites.yml +4 -0
- data/test/fixtures/categories/special_categories.yml +9 -0
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/fixtures/categories_posts.yml +4 -0
- data/test/fixtures/categorization.rb +5 -0
- data/test/fixtures/categorizations.yml +11 -0
- data/test/fixtures/category.rb +6 -0
- data/test/fixtures/company.rb +17 -5
- data/test/fixtures/company_in_module.rb +19 -5
- data/test/fixtures/db_definitions/db2.drop.sql +3 -0
- data/test/fixtures/db_definitions/db2.sql +121 -100
- data/test/fixtures/db_definitions/db22.sql +2 -2
- data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
- data/test/fixtures/db_definitions/firebird.sql +26 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
- data/test/fixtures/db_definitions/mysql.sql +21 -1
- data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase.sql +282 -0
- data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase2.sql +7 -0
- data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
- data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
- data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
- data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
- data/test/fixtures/db_definitions/postgresql.sql +22 -1
- data/test/fixtures/db_definitions/schema.rb +32 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
- data/test/fixtures/db_definitions/sqlite.sql +18 -0
- data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
- data/test/fixtures/db_definitions/sqlserver.sql +23 -3
- data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
- data/test/fixtures/db_definitions/sybase.sql +204 -0
- data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
- data/test/fixtures/db_definitions/sybase2.sql +5 -0
- data/test/fixtures/developers.yml +6 -1
- data/test/fixtures/developers_projects.yml +4 -0
- data/test/fixtures/funny_jokes.yml +14 -0
- data/test/fixtures/joke.rb +6 -0
- data/test/fixtures/legacy_thing.rb +3 -0
- data/test/fixtures/legacy_things.yml +3 -0
- data/test/fixtures/mixin.rb +1 -1
- data/test/fixtures/person.rb +4 -1
- data/test/fixtures/post.rb +26 -1
- data/test/fixtures/project.rb +1 -0
- data/test/fixtures/reader.rb +4 -0
- data/test/fixtures/readers.yml +4 -0
- data/test/fixtures/reply.rb +2 -1
- data/test/fixtures/tag.rb +5 -0
- data/test/fixtures/tagging.rb +6 -0
- data/test/fixtures/taggings.yml +18 -0
- data/test/fixtures/tags.yml +7 -0
- data/test/fixtures/tasks.yml +2 -2
- data/test/fixtures/topic.rb +2 -2
- data/test/fixtures/topics.yml +1 -0
- data/test/fixtures_test.rb +47 -13
- data/test/inheritance_test.rb +2 -2
- data/test/locking_test.rb +15 -1
- data/test/method_scoping_test.rb +248 -13
- data/test/migration_test.rb +68 -11
- data/test/mixin_nested_set_test.rb +1 -1
- data/test/modules_test.rb +6 -1
- data/test/readonly_test.rb +1 -1
- data/test/reflection_test.rb +63 -9
- data/test/schema_dumper_test.rb +41 -0
- data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
- data/test/threaded_connections_test.rb +10 -0
- data/test/unconnected_test.rb +12 -5
- data/test/validations_test.rb +197 -10
- metadata +295 -260
- data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
- data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
data/lib/active_record/base.rb
CHANGED
@@ -21,6 +21,8 @@ module ActiveRecord #:nodoc:
|
|
21
21
|
end
|
22
22
|
class RecordNotFound < ActiveRecordError #:nodoc:
|
23
23
|
end
|
24
|
+
class RecordNotSaved < ActiveRecordError #:nodoc:
|
25
|
+
end
|
24
26
|
class StatementInvalid < ActiveRecordError #:nodoc:
|
25
27
|
end
|
26
28
|
class PreparedStatementInvalid < ActiveRecordError #:nodoc:
|
@@ -242,22 +244,16 @@ module ActiveRecord #:nodoc:
|
|
242
244
|
# Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
|
243
245
|
# on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
|
244
246
|
cattr_accessor :logger
|
245
|
-
|
247
|
+
|
248
|
+
include Reloadable::Subclasses
|
249
|
+
|
246
250
|
def self.inherited(child) #:nodoc:
|
247
251
|
@@subclasses[self] ||= []
|
248
252
|
@@subclasses[self] << child
|
249
253
|
super
|
250
254
|
end
|
251
255
|
|
252
|
-
|
253
|
-
# explicitly decline the honor. USE WITH CAUTION. Only AR subclasses kept
|
254
|
-
# in the framework should use the flag, so #reset_subclasses and so forth
|
255
|
-
# leave it alone.
|
256
|
-
def self.reloadable? #:nodoc:
|
257
|
-
true
|
258
|
-
end
|
259
|
-
|
260
|
-
def self.reset_subclasses
|
256
|
+
def self.reset_subclasses #:nodoc:
|
261
257
|
nonreloadables = []
|
262
258
|
subclasses.each do |klass|
|
263
259
|
unless klass.reloadable?
|
@@ -310,12 +306,12 @@ module ActiveRecord #:nodoc:
|
|
310
306
|
# This is set to :local by default.
|
311
307
|
cattr_accessor :default_timezone
|
312
308
|
@@default_timezone = :local
|
313
|
-
|
309
|
+
|
314
310
|
# Determines whether or not to use a connection for each thread, or a single shared connection for all threads.
|
315
|
-
# Defaults to true
|
311
|
+
# Defaults to false. Set to true if you're writing a threaded application.
|
316
312
|
cattr_accessor :allow_concurrency
|
317
|
-
@@allow_concurrency =
|
318
|
-
|
313
|
+
@@allow_concurrency = false
|
314
|
+
|
319
315
|
# Determines whether to speed up access by generating optimized reader
|
320
316
|
# methods to avoid expensive calls to method_missing when accessing
|
321
317
|
# attributes by name. You might want to set this to false in development
|
@@ -330,7 +326,7 @@ module ActiveRecord #:nodoc:
|
|
330
326
|
# supports migrations. Use :ruby if you want to have different database
|
331
327
|
# adapters for, e.g., your development and test environments.
|
332
328
|
cattr_accessor :schema_format
|
333
|
-
@@schema_format = :
|
329
|
+
@@schema_format = :ruby
|
334
330
|
|
335
331
|
class << self # Class methods
|
336
332
|
# Find operates with three different retrieval approaches:
|
@@ -377,53 +373,16 @@ module ActiveRecord #:nodoc:
|
|
377
373
|
# Person.find(:all, :group => "category")
|
378
374
|
def find(*args)
|
379
375
|
options = extract_options_from_args!(args)
|
380
|
-
|
381
|
-
|
382
|
-
# if :joins is not blank then :readonly defaults to true.
|
383
|
-
unless options.has_key?(:readonly)
|
384
|
-
if scoped?(:find, :readonly)
|
385
|
-
options[:readonly] = scope(:find, :readonly)
|
386
|
-
elsif !options[:joins].blank?
|
387
|
-
options[:readonly] = true
|
388
|
-
end
|
389
|
-
end
|
376
|
+
validate_find_options(options)
|
377
|
+
set_readonly_option!(options)
|
390
378
|
|
391
379
|
case args.first
|
392
|
-
when :first
|
393
|
-
|
394
|
-
|
395
|
-
records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options))
|
396
|
-
records.each { |record| record.readonly! } if options[:readonly]
|
397
|
-
records
|
398
|
-
else
|
399
|
-
return args.first if args.first.kind_of?(Array) && args.first.empty?
|
400
|
-
expects_array = args.first.kind_of?(Array)
|
401
|
-
|
402
|
-
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
403
|
-
|
404
|
-
ids = args.flatten.compact.uniq
|
405
|
-
case ids.size
|
406
|
-
when 0
|
407
|
-
raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
|
408
|
-
when 1
|
409
|
-
if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" }))
|
410
|
-
return expects_array ? [ result ] : result
|
411
|
-
else
|
412
|
-
raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
|
413
|
-
end
|
414
|
-
else
|
415
|
-
# Find multiple ids
|
416
|
-
ids_list = ids.map { |id| sanitize(id) }.join(',')
|
417
|
-
result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"}))
|
418
|
-
if result.size == ids.size
|
419
|
-
return result
|
420
|
-
else
|
421
|
-
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
|
422
|
-
end
|
423
|
-
end
|
380
|
+
when :first then find_initial(options)
|
381
|
+
when :all then find_every(options)
|
382
|
+
else find_from_ids(args, options)
|
424
383
|
end
|
425
384
|
end
|
426
|
-
|
385
|
+
|
427
386
|
# Works like find(:all), but requires a complete SQL string. Examples:
|
428
387
|
# Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
|
429
388
|
# Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
|
@@ -444,9 +403,8 @@ module ActiveRecord #:nodoc:
|
|
444
403
|
if attributes.is_a?(Array)
|
445
404
|
attributes.collect { |attr| create(attr) }
|
446
405
|
else
|
447
|
-
attributes.reverse_merge!(scope(:create)) if scoped?(:create)
|
448
|
-
|
449
406
|
object = new(attributes)
|
407
|
+
scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create)
|
450
408
|
object.save
|
451
409
|
object
|
452
410
|
end
|
@@ -454,6 +412,16 @@ module ActiveRecord #:nodoc:
|
|
454
412
|
|
455
413
|
# Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
|
456
414
|
# and returns it. If the save fails under validations, the unsaved object is still returned.
|
415
|
+
#
|
416
|
+
# The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
|
417
|
+
# +attributes+ and an array of objects is returned.
|
418
|
+
#
|
419
|
+
# Example of updating one record:
|
420
|
+
# Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
|
421
|
+
#
|
422
|
+
# Example of updating multiple records:
|
423
|
+
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
|
424
|
+
# Person.update(people.keys, people.values)
|
457
425
|
def update(id, attributes)
|
458
426
|
if id.is_a?(Array)
|
459
427
|
idx = -1
|
@@ -482,7 +450,7 @@ module ActiveRecord #:nodoc:
|
|
482
450
|
# Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
|
483
451
|
def update_all(updates, conditions = nil)
|
484
452
|
sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} "
|
485
|
-
add_conditions!(sql, conditions)
|
453
|
+
add_conditions!(sql, conditions, scope(:find))
|
486
454
|
connection.update(sql, "#{name} Update")
|
487
455
|
end
|
488
456
|
|
@@ -498,19 +466,10 @@ module ActiveRecord #:nodoc:
|
|
498
466
|
# Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
|
499
467
|
def delete_all(conditions = nil)
|
500
468
|
sql = "DELETE FROM #{table_name} "
|
501
|
-
add_conditions!(sql, conditions)
|
469
|
+
add_conditions!(sql, conditions, scope(:find))
|
502
470
|
connection.delete(sql, "#{name} Delete all")
|
503
471
|
end
|
504
472
|
|
505
|
-
# Returns the number of records that meet the +conditions+. Zero is returned if no records match. Example:
|
506
|
-
# Product.count "sales > 1"
|
507
|
-
def count(conditions = nil, joins = nil)
|
508
|
-
sql = "SELECT COUNT(*) FROM #{table_name} "
|
509
|
-
sql << " #{joins} " if joins
|
510
|
-
add_conditions!(sql, conditions)
|
511
|
-
count_by_sql(sql)
|
512
|
-
end
|
513
|
-
|
514
473
|
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
|
515
474
|
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
|
516
475
|
def count_by_sql(sql)
|
@@ -532,6 +491,7 @@ module ActiveRecord #:nodoc:
|
|
532
491
|
update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote(id)}"
|
533
492
|
end
|
534
493
|
|
494
|
+
|
535
495
|
# Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
|
536
496
|
# <tt>attributes=(attributes)</tt>. Their assignment will simply be ignored. Instead, you can use the direct writer
|
537
497
|
# methods to do assignment. This is meant to protect sensitive attributes from being overwritten by URL/form hackers. Example:
|
@@ -569,6 +529,7 @@ module ActiveRecord #:nodoc:
|
|
569
529
|
read_inheritable_attribute("attr_accessible")
|
570
530
|
end
|
571
531
|
|
532
|
+
|
572
533
|
# Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
|
573
534
|
# after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
|
574
535
|
# object must be of that class on retrieval or +SerializationTypeMismatch+ will be raised.
|
@@ -581,6 +542,7 @@ module ActiveRecord #:nodoc:
|
|
581
542
|
read_inheritable_attribute("attr_serialized") or write_inheritable_attribute("attr_serialized", {})
|
582
543
|
end
|
583
544
|
|
545
|
+
|
584
546
|
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
|
585
547
|
# directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
|
586
548
|
# to guess the table name from even when called on Reply. The rules used to do the guess are handled by the Inflector class
|
@@ -599,9 +561,9 @@ module ActiveRecord #:nodoc:
|
|
599
561
|
reset_table_name
|
600
562
|
end
|
601
563
|
|
602
|
-
def reset_table_name
|
603
|
-
name = "#{table_name_prefix}#{undecorated_table_name(
|
604
|
-
set_table_name
|
564
|
+
def reset_table_name #:nodoc:
|
565
|
+
name = "#{table_name_prefix}#{undecorated_table_name(base_class.name)}#{table_name_suffix}"
|
566
|
+
set_table_name(name)
|
605
567
|
name
|
606
568
|
end
|
607
569
|
|
@@ -611,13 +573,13 @@ module ActiveRecord #:nodoc:
|
|
611
573
|
reset_primary_key
|
612
574
|
end
|
613
575
|
|
614
|
-
def reset_primary_key
|
576
|
+
def reset_primary_key #:nodoc:
|
615
577
|
key = 'id'
|
616
578
|
case primary_key_prefix_type
|
617
579
|
when :table_name
|
618
|
-
key = Inflector.foreign_key(
|
580
|
+
key = Inflector.foreign_key(base_class.name, false)
|
619
581
|
when :table_name_with_underscore
|
620
|
-
key = Inflector.foreign_key(
|
582
|
+
key = Inflector.foreign_key(base_class.name)
|
621
583
|
end
|
622
584
|
set_primary_key(key)
|
623
585
|
key
|
@@ -630,11 +592,11 @@ module ActiveRecord #:nodoc:
|
|
630
592
|
|
631
593
|
# Lazy-set the sequence name to the connection's default. This method
|
632
594
|
# is only ever called once since set_sequence_name overrides it.
|
633
|
-
def sequence_name
|
595
|
+
def sequence_name #:nodoc:
|
634
596
|
reset_sequence_name
|
635
597
|
end
|
636
598
|
|
637
|
-
def reset_sequence_name
|
599
|
+
def reset_sequence_name #:nodoc:
|
638
600
|
default = connection.default_sequence_name(table_name, primary_key)
|
639
601
|
set_sequence_name(default)
|
640
602
|
default
|
@@ -648,7 +610,7 @@ module ActiveRecord #:nodoc:
|
|
648
610
|
# class Project < ActiveRecord::Base
|
649
611
|
# set_table_name "project"
|
650
612
|
# end
|
651
|
-
def set_table_name(
|
613
|
+
def set_table_name(value = nil, &block)
|
652
614
|
define_attr_method :table_name, value, &block
|
653
615
|
end
|
654
616
|
alias :table_name= :set_table_name
|
@@ -662,7 +624,7 @@ module ActiveRecord #:nodoc:
|
|
662
624
|
# class Project < ActiveRecord::Base
|
663
625
|
# set_primary_key "sysid"
|
664
626
|
# end
|
665
|
-
def set_primary_key(
|
627
|
+
def set_primary_key(value = nil, &block)
|
666
628
|
define_attr_method :primary_key, value, &block
|
667
629
|
end
|
668
630
|
alias :primary_key= :set_primary_key
|
@@ -678,7 +640,7 @@ module ActiveRecord #:nodoc:
|
|
678
640
|
# original_inheritance_column + "_id"
|
679
641
|
# end
|
680
642
|
# end
|
681
|
-
def set_inheritance_column(
|
643
|
+
def set_inheritance_column(value = nil, &block)
|
682
644
|
define_attr_method :inheritance_column, value, &block
|
683
645
|
end
|
684
646
|
alias :inheritance_column= :set_inheritance_column
|
@@ -699,7 +661,7 @@ module ActiveRecord #:nodoc:
|
|
699
661
|
# class Project < ActiveRecord::Base
|
700
662
|
# set_sequence_name "projectseq" # default would have been "project_seq"
|
701
663
|
# end
|
702
|
-
def set_sequence_name(
|
664
|
+
def set_sequence_name(value = nil, &block)
|
703
665
|
define_attr_method :sequence_name, value, &block
|
704
666
|
end
|
705
667
|
alias :sequence_name= :set_sequence_name
|
@@ -742,6 +704,7 @@ module ActiveRecord #:nodoc:
|
|
742
704
|
@columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
|
743
705
|
end
|
744
706
|
|
707
|
+
# Returns an array of column names as strings.
|
745
708
|
def column_names
|
746
709
|
@column_names ||= columns.map { |column| column.name }
|
747
710
|
end
|
@@ -755,7 +718,7 @@ module ActiveRecord #:nodoc:
|
|
755
718
|
# Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
|
756
719
|
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
|
757
720
|
# is available.
|
758
|
-
def column_methods_hash
|
721
|
+
def column_methods_hash #:nodoc:
|
759
722
|
@dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
|
760
723
|
attr_name = attr.to_s
|
761
724
|
methods[attr.to_sym] = attr_name
|
@@ -767,7 +730,7 @@ module ActiveRecord #:nodoc:
|
|
767
730
|
end
|
768
731
|
|
769
732
|
# Contains the names of the generated reader methods.
|
770
|
-
def read_methods
|
733
|
+
def read_methods #:nodoc:
|
771
734
|
@read_methods ||= Set.new
|
772
735
|
end
|
773
736
|
|
@@ -834,35 +797,88 @@ module ActiveRecord #:nodoc:
|
|
834
797
|
end
|
835
798
|
|
836
799
|
# Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
|
837
|
-
# method_name may be :find or :create.
|
838
|
-
#
|
839
|
-
# <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options.
|
840
|
-
# :create parameters are an attributes hash.
|
800
|
+
# method_name may be :find or :create. :find parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
|
801
|
+
# <tt>:include</tt>, <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options. :create parameters are an attributes hash.
|
841
802
|
#
|
842
803
|
# Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do
|
843
804
|
# Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
|
844
805
|
# a = Article.create(1)
|
845
|
-
# a.blog_id
|
806
|
+
# a.blog_id # => 1
|
846
807
|
# end
|
847
|
-
|
808
|
+
#
|
809
|
+
# In nested scopings, all previous parameters are overwritten by inner rule
|
810
|
+
# except :conditions in :find, that are merged as hash.
|
811
|
+
#
|
812
|
+
# Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do
|
813
|
+
# Article.with_scope(:find => { :limit => 10})
|
814
|
+
# Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
|
815
|
+
# end
|
816
|
+
# Article.with_scope(:find => { :conditions => "author_id = 3" })
|
817
|
+
# Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
|
818
|
+
# end
|
819
|
+
# end
|
820
|
+
#
|
821
|
+
# You can ignore any previous scopings by using <tt>with_exclusive_scope</tt> method.
|
822
|
+
#
|
823
|
+
# Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do
|
824
|
+
# Article.with_exclusive_scope(:find => { :limit => 10 })
|
825
|
+
# Article.find(:all) # => SELECT * from articles LIMIT 10
|
826
|
+
# end
|
827
|
+
# end
|
828
|
+
def with_scope(method_scoping = {}, action = :merge, &block)
|
829
|
+
method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
|
830
|
+
|
848
831
|
# Dup first and second level of hash (method and params).
|
849
832
|
method_scoping = method_scoping.inject({}) do |hash, (method, params)|
|
850
|
-
hash[method] = params.dup
|
833
|
+
hash[method] = (params == true) ? params : params.dup
|
851
834
|
hash
|
852
835
|
end
|
853
836
|
|
854
|
-
method_scoping.assert_valid_keys
|
837
|
+
method_scoping.assert_valid_keys([ :find, :create ])
|
838
|
+
|
855
839
|
if f = method_scoping[:find]
|
856
|
-
f.assert_valid_keys
|
840
|
+
f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :readonly ])
|
857
841
|
f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
|
858
842
|
end
|
859
843
|
|
860
|
-
|
844
|
+
# Merge scopings
|
845
|
+
if action == :merge && current_scoped_methods
|
846
|
+
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
|
847
|
+
case hash[method]
|
848
|
+
when Hash
|
849
|
+
if method == :find
|
850
|
+
(hash[method].keys + params.keys).uniq.each do |key|
|
851
|
+
merge = hash[method][key] && params[key] # merge if both scopes have the same key
|
852
|
+
if key == :conditions && merge
|
853
|
+
hash[method][key] = [params[key], hash[method][key]].collect{|sql| "( %s )" % sanitize_sql(sql)}.join(" AND ")
|
854
|
+
elsif key == :include && merge
|
855
|
+
hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
|
856
|
+
else
|
857
|
+
hash[method][key] = hash[method][key] || params[key]
|
858
|
+
end
|
859
|
+
end
|
860
|
+
else
|
861
|
+
hash[method] = params.merge(hash[method])
|
862
|
+
end
|
863
|
+
else
|
864
|
+
hash[method] = params
|
865
|
+
end
|
866
|
+
hash
|
867
|
+
end
|
868
|
+
end
|
869
|
+
|
870
|
+
self.scoped_methods << method_scoping
|
861
871
|
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
872
|
+
begin
|
873
|
+
yield
|
874
|
+
ensure
|
875
|
+
self.scoped_methods.pop
|
876
|
+
end
|
877
|
+
end
|
878
|
+
|
879
|
+
# Works like with_scope, but discards any nested properties.
|
880
|
+
def with_exclusive_scope(method_scoping = {}, &block)
|
881
|
+
with_scope(method_scoping, :overwrite, &block)
|
866
882
|
end
|
867
883
|
|
868
884
|
# Overwrite the default class equality method to provide support for association proxies.
|
@@ -871,17 +887,89 @@ module ActiveRecord #:nodoc:
|
|
871
887
|
end
|
872
888
|
|
873
889
|
# Deprecated
|
874
|
-
def threaded_connections
|
890
|
+
def threaded_connections #:nodoc:
|
875
891
|
allow_concurrency
|
876
892
|
end
|
877
893
|
|
878
894
|
# Deprecated
|
879
|
-
def threaded_connections=(value)
|
895
|
+
def threaded_connections=(value) #:nodoc:
|
880
896
|
self.allow_concurrency = value
|
881
897
|
end
|
882
898
|
|
883
|
-
|
899
|
+
# Returns the base AR subclass that this class descends from. If A
|
900
|
+
# extends AR::Base, A.base_class will return A. If B descends from A
|
901
|
+
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
902
|
+
def base_class
|
903
|
+
class_of_active_record_descendant(self)
|
904
|
+
end
|
905
|
+
|
906
|
+
# Set this to true if this is an abstract class (see #abstract_class?).
|
907
|
+
attr_accessor :abstract_class
|
908
|
+
|
909
|
+
# Returns whether this class is a base AR class. If A is a base class and
|
910
|
+
# B descends from A, then B.base_class will return B.
|
911
|
+
def abstract_class?
|
912
|
+
abstract_class == true
|
913
|
+
end
|
914
|
+
|
884
915
|
private
|
916
|
+
def find_initial(options)
|
917
|
+
options.update(:limit => 1) unless options[:include]
|
918
|
+
find_every(options).first
|
919
|
+
end
|
920
|
+
|
921
|
+
def find_every(options)
|
922
|
+
records = scoped?(:find, :include) || options[:include] ?
|
923
|
+
find_with_associations(options) :
|
924
|
+
find_by_sql(construct_finder_sql(options))
|
925
|
+
|
926
|
+
records.each { |record| record.readonly! } if options[:readonly]
|
927
|
+
|
928
|
+
records
|
929
|
+
end
|
930
|
+
|
931
|
+
def find_from_ids(ids, options)
|
932
|
+
expects_array = ids.first.kind_of?(Array)
|
933
|
+
return ids.first if expects_array && ids.first.empty?
|
934
|
+
|
935
|
+
ids = ids.flatten.compact.uniq
|
936
|
+
|
937
|
+
case ids.size
|
938
|
+
when 0
|
939
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
940
|
+
when 1
|
941
|
+
result = find_one(ids.first, options)
|
942
|
+
expects_array ? [ result ] : result
|
943
|
+
else
|
944
|
+
find_some(ids, options)
|
945
|
+
end
|
946
|
+
end
|
947
|
+
|
948
|
+
def find_one(id, options)
|
949
|
+
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
950
|
+
options = options.merge :conditions => "#{table_name}.#{primary_key} = #{sanitize(id)}#{conditions}"
|
951
|
+
|
952
|
+
if result = find_initial(options)
|
953
|
+
result
|
954
|
+
else
|
955
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
|
956
|
+
end
|
957
|
+
end
|
958
|
+
|
959
|
+
def find_some(ids, options)
|
960
|
+
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
961
|
+
ids_list = ids.map { |id| sanitize(id) }.join(',')
|
962
|
+
options = options.merge :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"
|
963
|
+
|
964
|
+
result = find_every(options)
|
965
|
+
|
966
|
+
if result.size == ids.size
|
967
|
+
result
|
968
|
+
else
|
969
|
+
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
|
970
|
+
end
|
971
|
+
end
|
972
|
+
|
885
973
|
# Finder methods must instantiate through this method to work with the single-table inheritance model
|
886
974
|
# that makes it possible to create objects of different types from the same table.
|
887
975
|
def instantiate(record)
|
@@ -909,36 +997,62 @@ module ActiveRecord #:nodoc:
|
|
909
997
|
object
|
910
998
|
end
|
911
999
|
|
912
|
-
#
|
913
|
-
# MyApp::Business::
|
1000
|
+
# Nest the type name in the same module as this class.
|
1001
|
+
# Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo
|
914
1002
|
def type_name_with_module(type_name)
|
915
|
-
self.name
|
1003
|
+
"#{self.name.sub(/(::)?[^:]+$/, '')}#{$1}#{type_name}"
|
916
1004
|
end
|
917
1005
|
|
918
1006
|
def construct_finder_sql(options)
|
919
|
-
|
920
|
-
|
921
|
-
|
1007
|
+
scope = scope(:find)
|
1008
|
+
sql = "SELECT #{(scope && scope[:select]) || options[:select] || '*'} "
|
1009
|
+
sql << "FROM #{(scope && scope[:from]) || options[:from] || table_name} "
|
1010
|
+
|
1011
|
+
add_joins!(sql, options, scope)
|
1012
|
+
add_conditions!(sql, options[:conditions], scope)
|
1013
|
+
|
922
1014
|
sql << " GROUP BY #{options[:group]} " if options[:group]
|
923
1015
|
sql << " ORDER BY #{options[:order]} " if options[:order]
|
924
|
-
|
1016
|
+
|
1017
|
+
add_limit!(sql, options, scope)
|
1018
|
+
|
925
1019
|
sql
|
926
1020
|
end
|
927
1021
|
|
928
|
-
|
929
|
-
|
930
|
-
|
1022
|
+
# Merges includes so that the result is a valid +include+
|
1023
|
+
def merge_includes(first, second)
|
1024
|
+
safe_to_array(first) + safe_to_array(second)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# Object#to_a is deprecated, though it does have the desired behaviour
|
1028
|
+
def safe_to_array(o)
|
1029
|
+
case o
|
1030
|
+
when NilClass
|
1031
|
+
[]
|
1032
|
+
when Array
|
1033
|
+
o
|
1034
|
+
else
|
1035
|
+
[o]
|
1036
|
+
end
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
def add_limit!(sql, options, scope)
|
1040
|
+
if scope
|
1041
|
+
options[:limit] ||= scope[:limit]
|
1042
|
+
options[:offset] ||= scope[:offset]
|
1043
|
+
end
|
931
1044
|
connection.add_limit_offset!(sql, options)
|
932
1045
|
end
|
933
1046
|
|
934
|
-
def add_joins!(sql, options)
|
935
|
-
join = scope
|
1047
|
+
def add_joins!(sql, options, scope)
|
1048
|
+
join = (scope && scope[:joins]) || options[:joins]
|
936
1049
|
sql << " #{join} " if join
|
937
1050
|
end
|
938
1051
|
|
939
1052
|
# Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed.
|
940
|
-
def add_conditions!(sql, conditions)
|
941
|
-
segments = [
|
1053
|
+
def add_conditions!(sql, conditions, scope)
|
1054
|
+
segments = []
|
1055
|
+
segments << sanitize_sql(scope[:conditions]) if scope && scope[:conditions]
|
942
1056
|
segments << sanitize_sql(conditions) unless conditions.nil?
|
943
1057
|
segments << type_condition unless descends_from_active_record?
|
944
1058
|
segments.compact!
|
@@ -955,7 +1069,7 @@ module ActiveRecord #:nodoc:
|
|
955
1069
|
end
|
956
1070
|
|
957
1071
|
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
958
|
-
def undecorated_table_name(class_name =
|
1072
|
+
def undecorated_table_name(class_name = base_class.name)
|
959
1073
|
table_name = Inflector.underscore(Inflector.demodulize(class_name))
|
960
1074
|
table_name = Inflector.pluralize(table_name) if pluralize_table_names
|
961
1075
|
table_name
|
@@ -969,31 +1083,53 @@ module ActiveRecord #:nodoc:
|
|
969
1083
|
# is actually find_all_by_amount(amount, options).
|
970
1084
|
def method_missing(method_id, *arguments)
|
971
1085
|
if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
972
|
-
finder = determine_finder(match)
|
1086
|
+
finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
|
973
1087
|
|
974
1088
|
attribute_names = extract_attribute_names_from_match(match)
|
975
1089
|
super unless all_attributes_exists?(attribute_names)
|
976
1090
|
|
977
1091
|
conditions = construct_conditions_from_arguments(attribute_names, arguments)
|
978
1092
|
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
1093
|
+
case extra_options = arguments[attribute_names.size]
|
1094
|
+
when nil
|
1095
|
+
options = { :conditions => conditions }
|
1096
|
+
set_readonly_option!(options)
|
1097
|
+
send(finder, options)
|
1098
|
+
|
1099
|
+
when Hash
|
1100
|
+
finder_options = extra_options.merge(:conditions => conditions)
|
1101
|
+
validate_find_options(finder_options)
|
1102
|
+
set_readonly_option!(finder_options)
|
1103
|
+
|
1104
|
+
if extra_options[:conditions]
|
1105
|
+
with_scope(:find => { :conditions => extra_options[:conditions] }) do
|
1106
|
+
send(finder, finder_options)
|
1107
|
+
end
|
1108
|
+
else
|
1109
|
+
send(finder, finder_options)
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
else
|
1113
|
+
send(deprecated_finder, conditions, *arguments[attribute_names.length..-1]) # deprecated API
|
983
1114
|
end
|
984
1115
|
elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
985
1116
|
attribute_names = extract_attribute_names_from_match(match)
|
986
1117
|
super unless all_attributes_exists?(attribute_names)
|
987
1118
|
|
988
|
-
|
989
|
-
|
1119
|
+
options = { :conditions => construct_conditions_from_arguments(attribute_names, arguments) }
|
1120
|
+
set_readonly_option!(options)
|
1121
|
+
find_initial(options) || create(construct_attributes_from_arguments(attribute_names, arguments))
|
990
1122
|
else
|
991
1123
|
super
|
992
1124
|
end
|
993
1125
|
end
|
994
1126
|
|
995
1127
|
def determine_finder(match)
|
996
|
-
match.captures.first == 'all_by' ? :
|
1128
|
+
match.captures.first == 'all_by' ? :find_every : :find_initial
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
def determine_deprecated_finder(match)
|
1132
|
+
match.captures.first == 'all_by' ? :find_all : :find_first
|
997
1133
|
end
|
998
1134
|
|
999
1135
|
def extract_attribute_names_from_match(match)
|
@@ -1055,60 +1191,74 @@ module ActiveRecord #:nodoc:
|
|
1055
1191
|
end
|
1056
1192
|
|
1057
1193
|
protected
|
1058
|
-
def subclasses
|
1194
|
+
def subclasses #:nodoc:
|
1059
1195
|
@@subclasses[self] ||= []
|
1060
1196
|
@@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
|
1061
1197
|
end
|
1062
1198
|
|
1063
1199
|
# Test whether the given method and optional key are scoped.
|
1064
|
-
def scoped?(method, key = nil)
|
1065
|
-
|
1200
|
+
def scoped?(method, key = nil) #:nodoc:
|
1201
|
+
if current_scoped_methods && (scope = current_scoped_methods[method])
|
1202
|
+
!key || scope.has_key?(key)
|
1203
|
+
end
|
1066
1204
|
end
|
1067
1205
|
|
1068
1206
|
# Retrieve the scope for the given method and optional key.
|
1069
|
-
def scope(method, key = nil)
|
1070
|
-
if
|
1207
|
+
def scope(method, key = nil) #:nodoc:
|
1208
|
+
if current_scoped_methods && (scope = current_scoped_methods[method])
|
1071
1209
|
key ? scope[key] : scope
|
1072
1210
|
end
|
1073
1211
|
end
|
1074
1212
|
|
1075
|
-
def
|
1076
|
-
|
1077
|
-
|
1078
|
-
Thread.current[:scoped_methods][self] ||= nil
|
1079
|
-
else
|
1080
|
-
@scoped_methods ||= nil
|
1081
|
-
end
|
1213
|
+
def thread_safe_scoped_methods #:nodoc:
|
1214
|
+
scoped_methods = (Thread.current[:scoped_methods] ||= {})
|
1215
|
+
scoped_methods[self] ||= []
|
1082
1216
|
end
|
1083
|
-
|
1084
|
-
def
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1217
|
+
|
1218
|
+
def single_threaded_scoped_methods #:nodoc:
|
1219
|
+
@scoped_methods ||= []
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
# pick up the correct scoped_methods version from @@allow_concurrency
|
1223
|
+
if @@allow_concurrency
|
1224
|
+
alias_method :scoped_methods, :thread_safe_scoped_methods
|
1225
|
+
else
|
1226
|
+
alias_method :scoped_methods, :single_threaded_scoped_methods
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
def current_scoped_methods #:nodoc:
|
1230
|
+
scoped_methods.last
|
1091
1231
|
end
|
1092
1232
|
|
1093
1233
|
# Returns the class type of the record using the current module as a prefix. So descendents of
|
1094
1234
|
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
1095
1235
|
def compute_type(type_name)
|
1096
|
-
type_name_with_module(type_name)
|
1097
|
-
|
1236
|
+
modularized_name = type_name_with_module(type_name)
|
1237
|
+
begin
|
1238
|
+
instance_eval(modularized_name)
|
1239
|
+
rescue NameError => e
|
1240
|
+
first_module = modularized_name.split("::").first
|
1241
|
+
raise unless e.to_s.include? first_module
|
1242
|
+
instance_eval(type_name)
|
1098
1243
|
end
|
1099
1244
|
end
|
1100
1245
|
|
1101
|
-
# Returns the
|
1102
|
-
def
|
1103
|
-
if klass.superclass == Base
|
1104
|
-
klass
|
1246
|
+
# Returns the class descending directly from ActiveRecord in the inheritance hierarchy.
|
1247
|
+
def class_of_active_record_descendant(klass)
|
1248
|
+
if klass.superclass == Base || klass.superclass.abstract_class?
|
1249
|
+
klass
|
1105
1250
|
elsif klass.superclass.nil?
|
1106
1251
|
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
|
1107
1252
|
else
|
1108
|
-
|
1253
|
+
class_of_active_record_descendant(klass.superclass)
|
1109
1254
|
end
|
1110
1255
|
end
|
1111
1256
|
|
1257
|
+
# Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
|
1258
|
+
def class_name_of_active_record_descendant(klass) #:nodoc:
|
1259
|
+
klass.base_class.name
|
1260
|
+
end
|
1261
|
+
|
1112
1262
|
# Accepts an array or string. The string is returned untouched, but the array has each value
|
1113
1263
|
# sanitized and interpolated into the sql statement.
|
1114
1264
|
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
|
@@ -1127,14 +1277,13 @@ module ActiveRecord #:nodoc:
|
|
1127
1277
|
|
1128
1278
|
alias_method :sanitize_conditions, :sanitize_sql
|
1129
1279
|
|
1130
|
-
def replace_bind_variables(statement, values)
|
1280
|
+
def replace_bind_variables(statement, values) #:nodoc:
|
1131
1281
|
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
|
1132
1282
|
bound = values.dup
|
1133
1283
|
statement.gsub('?') { quote_bound_value(bound.shift) }
|
1134
1284
|
end
|
1135
1285
|
|
1136
|
-
def replace_named_bind_variables(statement, bind_vars)
|
1137
|
-
raise_if_bind_arity_mismatch(statement, statement.scan(/:(\w+)/).uniq.size, bind_vars.size)
|
1286
|
+
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
|
1138
1287
|
statement.gsub(/:(\w+)/) do
|
1139
1288
|
match = $1.to_sym
|
1140
1289
|
if bind_vars.include?(match)
|
@@ -1145,7 +1294,7 @@ module ActiveRecord #:nodoc:
|
|
1145
1294
|
end
|
1146
1295
|
end
|
1147
1296
|
|
1148
|
-
def quote_bound_value(value)
|
1297
|
+
def quote_bound_value(value) #:nodoc:
|
1149
1298
|
if (value.respond_to?(:map) && !value.is_a?(String))
|
1150
1299
|
value.map { |v| connection.quote(v) }.join(',')
|
1151
1300
|
else
|
@@ -1153,23 +1302,36 @@ module ActiveRecord #:nodoc:
|
|
1153
1302
|
end
|
1154
1303
|
end
|
1155
1304
|
|
1156
|
-
def raise_if_bind_arity_mismatch(statement, expected, provided)
|
1305
|
+
def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
|
1157
1306
|
unless expected == provided
|
1158
1307
|
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
|
1159
1308
|
end
|
1160
1309
|
end
|
1161
1310
|
|
1162
|
-
def extract_options_from_args!(args)
|
1163
|
-
|
1164
|
-
validate_find_options(options)
|
1165
|
-
options
|
1311
|
+
def extract_options_from_args!(args) #:nodoc:
|
1312
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
1166
1313
|
end
|
1167
1314
|
|
1168
|
-
|
1169
|
-
|
1315
|
+
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
1316
|
+
:order, :select, :readonly, :group, :from ]
|
1317
|
+
|
1318
|
+
def validate_find_options(options) #:nodoc:
|
1319
|
+
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
def set_readonly_option!(options) #:nodoc:
|
1323
|
+
# Inherit :readonly from finder scope if set. Otherwise,
|
1324
|
+
# if :joins is not blank then :readonly defaults to true.
|
1325
|
+
unless options.has_key?(:readonly)
|
1326
|
+
if scoped?(:find, :readonly)
|
1327
|
+
options[:readonly] = scope(:find, :readonly)
|
1328
|
+
elsif !options[:joins].blank?
|
1329
|
+
options[:readonly] = true
|
1330
|
+
end
|
1331
|
+
end
|
1170
1332
|
end
|
1171
1333
|
|
1172
|
-
def encode_quoted_value(value)
|
1334
|
+
def encode_quoted_value(value) #:nodoc:
|
1173
1335
|
quoted_value = connection.quote(value)
|
1174
1336
|
quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
|
1175
1337
|
quoted_value
|
@@ -1222,9 +1384,15 @@ module ActiveRecord #:nodoc:
|
|
1222
1384
|
# * No record exists: Creates a new record with values matching those of the object attributes.
|
1223
1385
|
# * A record does exist: Updates the record with values matching those of the object attributes.
|
1224
1386
|
def save
|
1225
|
-
raise
|
1387
|
+
raise ReadOnlyRecord if readonly?
|
1226
1388
|
create_or_update
|
1227
1389
|
end
|
1390
|
+
|
1391
|
+
# Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
|
1392
|
+
# RecordNotSaved exception
|
1393
|
+
def save!
|
1394
|
+
save || raise(RecordNotSaved)
|
1395
|
+
end
|
1228
1396
|
|
1229
1397
|
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
1230
1398
|
# be made (since they can't be persisted).
|
@@ -1328,20 +1496,39 @@ module ActiveRecord #:nodoc:
|
|
1328
1496
|
# from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
|
1329
1497
|
# specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
|
1330
1498
|
# attributes not included in that won't be allowed to be mass-assigned.
|
1331
|
-
def attributes=(
|
1332
|
-
return if
|
1499
|
+
def attributes=(new_attributes)
|
1500
|
+
return if new_attributes.nil?
|
1501
|
+
attributes = new_attributes.dup
|
1333
1502
|
attributes.stringify_keys!
|
1334
1503
|
|
1335
1504
|
multi_parameter_attributes = []
|
1336
1505
|
remove_attributes_protected_from_mass_assignment(attributes).each do |k, v|
|
1337
1506
|
k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
|
1338
1507
|
end
|
1508
|
+
|
1339
1509
|
assign_multiparameter_attributes(multi_parameter_attributes)
|
1340
1510
|
end
|
1341
1511
|
|
1512
|
+
|
1342
1513
|
# Returns a hash of all the attributes with their names as keys and clones of their objects as values.
|
1343
|
-
def attributes
|
1344
|
-
clone_attributes :read_attribute
|
1514
|
+
def attributes(options = nil)
|
1515
|
+
attributes = clone_attributes :read_attribute
|
1516
|
+
|
1517
|
+
if options.nil?
|
1518
|
+
attributes
|
1519
|
+
else
|
1520
|
+
if except = options[:except]
|
1521
|
+
except = Array(except).collect { |attribute| attribute.to_s }
|
1522
|
+
except.each { |attribute_name| attributes.delete(attribute_name) }
|
1523
|
+
attributes
|
1524
|
+
elsif only = options[:only]
|
1525
|
+
only = Array(only).collect { |attribute| attribute.to_s }
|
1526
|
+
attributes.delete_if { |key, value| !only.include?(key) }
|
1527
|
+
attributes
|
1528
|
+
else
|
1529
|
+
raise ArgumentError, "Options does not specify :except or :only (#{options.keys.inspect})"
|
1530
|
+
end
|
1531
|
+
end
|
1345
1532
|
end
|
1346
1533
|
|
1347
1534
|
# Returns a hash of cloned attributes before typecasting and deserialization.
|
@@ -1418,14 +1605,108 @@ module ActiveRecord #:nodoc:
|
|
1418
1605
|
@attributes.frozen?
|
1419
1606
|
end
|
1420
1607
|
|
1608
|
+
# Records loaded through joins with piggy-back attributes will be marked as read only as they cannot be saved and return true to this query.
|
1421
1609
|
def readonly?
|
1422
1610
|
@readonly == true
|
1423
1611
|
end
|
1424
1612
|
|
1425
|
-
def readonly!
|
1613
|
+
def readonly! #:nodoc:
|
1426
1614
|
@readonly = true
|
1427
1615
|
end
|
1428
1616
|
|
1617
|
+
# Builds an XML document to represent the model. Some configuration is
|
1618
|
+
# availble through +options+, however more complicated cases should use
|
1619
|
+
# Builder.
|
1620
|
+
#
|
1621
|
+
# By default the generated XML document will include the processing
|
1622
|
+
# instruction and all object's attributes. For example:
|
1623
|
+
#
|
1624
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
1625
|
+
# <topic>
|
1626
|
+
# <title>The First Topic</title>
|
1627
|
+
# <author-name>David</author-name>
|
1628
|
+
# <id type="integer">1</id>
|
1629
|
+
# <approved type="boolean">false</approved>
|
1630
|
+
# <replies-count type="integer">0</replies-count>
|
1631
|
+
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
|
1632
|
+
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
|
1633
|
+
# <content>Have a nice day</content>
|
1634
|
+
# <author-email-address>david@loudthinking.com</author-email-address>
|
1635
|
+
# <parent-id></parent-id>
|
1636
|
+
# <last-read type="date">2004-04-15</last-read>
|
1637
|
+
# </topic>
|
1638
|
+
#
|
1639
|
+
# This behaviour can be controlled with :skip_attributes and :skip_instruct
|
1640
|
+
# for instance:
|
1641
|
+
#
|
1642
|
+
# topic.to_xml(:skip_instruct => true, :skip_attributes => [ :id, bonus_time, :written_on, replies_count ])
|
1643
|
+
#
|
1644
|
+
# <topic>
|
1645
|
+
# <title>The First Topic</title>
|
1646
|
+
# <author-name>David</author-name>
|
1647
|
+
# <approved type="boolean">false</approved>
|
1648
|
+
# <content>Have a nice day</content>
|
1649
|
+
# <author-email-address>david@loudthinking.com</author-email-address>
|
1650
|
+
# <parent-id></parent-id>
|
1651
|
+
# <last-read type="date">2004-04-15</last-read>
|
1652
|
+
# </topic>
|
1653
|
+
#
|
1654
|
+
# To include first level associations use :include
|
1655
|
+
#
|
1656
|
+
# firm.to_xml :include => [ :account, :clients ]
|
1657
|
+
#
|
1658
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
1659
|
+
# <firm>
|
1660
|
+
# <id type="integer">1</id>
|
1661
|
+
# <rating type="integer">1</rating>
|
1662
|
+
# <name>37signals</name>
|
1663
|
+
# <clients>
|
1664
|
+
# <client>
|
1665
|
+
# <rating type="integer">1</rating>
|
1666
|
+
# <name>Summit</name>
|
1667
|
+
# </client>
|
1668
|
+
# <client>
|
1669
|
+
# <rating type="integer">1</rating>
|
1670
|
+
# <name>Microsoft</name>
|
1671
|
+
# </client>
|
1672
|
+
# </clients>
|
1673
|
+
# <account>
|
1674
|
+
# <id type="integer">1</id>
|
1675
|
+
# <credit-limit type="integer">50</credit-limit>
|
1676
|
+
# </account>
|
1677
|
+
# </firm>
|
1678
|
+
def to_xml(options = {})
|
1679
|
+
options[:root] ||= self.class.to_s.underscore
|
1680
|
+
options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column
|
1681
|
+
root_only_or_except = { :only => options[:only], :except => options[:except] }
|
1682
|
+
|
1683
|
+
attributes_for_xml = attributes(root_only_or_except)
|
1684
|
+
|
1685
|
+
if include_associations = options.delete(:include)
|
1686
|
+
include_has_options = include_associations.is_a?(Hash)
|
1687
|
+
|
1688
|
+
for association in include_has_options ? include_associations.keys : Array(include_associations)
|
1689
|
+
association_options = include_has_options ? include_associations[association] : root_only_or_except
|
1690
|
+
|
1691
|
+
case self.class.reflect_on_association(association).macro
|
1692
|
+
when :has_many, :has_and_belongs_to_many
|
1693
|
+
records = send(association).to_a
|
1694
|
+
unless records.empty?
|
1695
|
+
attributes_for_xml[association] = records.collect do |record|
|
1696
|
+
record.attributes(association_options)
|
1697
|
+
end
|
1698
|
+
end
|
1699
|
+
when :has_one, :belongs_to
|
1700
|
+
if record = send(association)
|
1701
|
+
attributes_for_xml[association] = record.attributes(association_options)
|
1702
|
+
end
|
1703
|
+
end
|
1704
|
+
end
|
1705
|
+
end
|
1706
|
+
|
1707
|
+
attributes_for_xml.to_xml(options)
|
1708
|
+
end
|
1709
|
+
|
1429
1710
|
private
|
1430
1711
|
def create_or_update
|
1431
1712
|
if new_record? then create else update end
|
@@ -1439,11 +1720,13 @@ module ActiveRecord #:nodoc:
|
|
1439
1720
|
"WHERE #{self.class.primary_key} = #{quote(id)}",
|
1440
1721
|
"#{self.class.name} Update"
|
1441
1722
|
)
|
1723
|
+
|
1724
|
+
return true
|
1442
1725
|
end
|
1443
1726
|
|
1444
1727
|
# Creates a new record with values matching those of the instance attributes.
|
1445
1728
|
def create
|
1446
|
-
if self.id.nil?
|
1729
|
+
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
|
1447
1730
|
self.id = connection.next_sequence_value(self.class.sequence_name)
|
1448
1731
|
end
|
1449
1732
|
|
@@ -1456,6 +1739,8 @@ module ActiveRecord #:nodoc:
|
|
1456
1739
|
)
|
1457
1740
|
|
1458
1741
|
@new_record = false
|
1742
|
+
|
1743
|
+
return true
|
1459
1744
|
end
|
1460
1745
|
|
1461
1746
|
# Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendent.
|
@@ -1478,19 +1763,19 @@ module ActiveRecord #:nodoc:
|
|
1478
1763
|
# table with a master_id foreign key can instantiate master through Client#master.
|
1479
1764
|
def method_missing(method_id, *args, &block)
|
1480
1765
|
method_name = method_id.to_s
|
1481
|
-
if @attributes.include?(method_name)
|
1766
|
+
if @attributes.include?(method_name) or
|
1767
|
+
(md = /\?$/.match(method_name) and
|
1768
|
+
@attributes.include?(method_name = md.pre_match))
|
1482
1769
|
define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
|
1483
|
-
read_attribute(method_name)
|
1770
|
+
md ? query_attribute(method_name) : read_attribute(method_name)
|
1484
1771
|
elsif self.class.primary_key.to_s == method_name
|
1485
1772
|
id
|
1486
|
-
elsif md = /(
|
1773
|
+
elsif md = /(=|_before_type_cast)$/.match(method_name)
|
1487
1774
|
attribute_name, method_type = md.pre_match, md.to_s
|
1488
1775
|
if @attributes.include?(attribute_name)
|
1489
1776
|
case method_type
|
1490
1777
|
when '='
|
1491
1778
|
write_attribute(attribute_name, args.first)
|
1492
|
-
when '?'
|
1493
|
-
query_attribute(attribute_name)
|
1494
1779
|
when '_before_type_cast'
|
1495
1780
|
read_attribute_before_type_cast(attribute_name)
|
1496
1781
|
end
|
@@ -1530,8 +1815,9 @@ module ActiveRecord #:nodoc:
|
|
1530
1815
|
# ActiveRecord::Base.generate_read_methods is set to true.
|
1531
1816
|
def define_read_methods
|
1532
1817
|
self.class.columns_hash.each do |name, column|
|
1533
|
-
unless self.class.serialized_attributes[name]
|
1534
|
-
define_read_method(name.to_sym, name, column)
|
1818
|
+
unless self.class.serialized_attributes[name]
|
1819
|
+
define_read_method(name.to_sym, name, column) unless respond_to_without_attributes?(name)
|
1820
|
+
define_question_method(name) unless respond_to_without_attributes?("#{name}?")
|
1535
1821
|
end
|
1536
1822
|
end
|
1537
1823
|
end
|
@@ -1540,14 +1826,28 @@ module ActiveRecord #:nodoc:
|
|
1540
1826
|
def define_read_method(symbol, attr_name, column)
|
1541
1827
|
cast_code = column.type_cast_code('v') if column
|
1542
1828
|
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
1543
|
-
|
1829
|
+
|
1544
1830
|
unless attr_name.to_s == self.class.primary_key.to_s
|
1545
1831
|
access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
|
1546
1832
|
self.class.read_methods << attr_name
|
1547
1833
|
end
|
1548
|
-
|
1834
|
+
|
1835
|
+
evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
|
1836
|
+
end
|
1837
|
+
|
1838
|
+
# Define an attribute ? method.
|
1839
|
+
def define_question_method(attr_name)
|
1840
|
+
unless attr_name.to_s == self.class.primary_key.to_s
|
1841
|
+
self.class.read_methods << "#{attr_name}?"
|
1842
|
+
end
|
1843
|
+
|
1844
|
+
evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end"
|
1845
|
+
end
|
1846
|
+
|
1847
|
+
# Evaluate the definition for an attribute reader or ? method
|
1848
|
+
def evaluate_read_method(attr_name, method_definition)
|
1549
1849
|
begin
|
1550
|
-
self.class.class_eval(
|
1850
|
+
self.class.class_eval(method_definition)
|
1551
1851
|
rescue SyntaxError => err
|
1552
1852
|
self.class.read_methods.delete(attr_name)
|
1553
1853
|
if logger
|
@@ -1764,4 +2064,4 @@ module ActiveRecord #:nodoc:
|
|
1764
2064
|
value
|
1765
2065
|
end
|
1766
2066
|
end
|
1767
|
-
end
|
2067
|
+
end
|