activerecord 2.3.18 → 3.2.22
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1014 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +222 -0
- data/examples/performance.rb +100 -126
- data/examples/simple.rb +14 -0
- data/lib/active_record/aggregations.rb +93 -99
- data/lib/active_record/associations/alias_tracker.rb +76 -0
- data/lib/active_record/associations/association.rb +247 -0
- data/lib/active_record/associations/association_scope.rb +134 -0
- data/lib/active_record/associations/belongs_to_association.rb +54 -61
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +17 -59
- data/lib/active_record/associations/builder/association.rb +55 -0
- data/lib/active_record/associations/builder/belongs_to.rb +88 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
- data/lib/active_record/associations/builder/has_many.rb +71 -0
- data/lib/active_record/associations/builder/has_one.rb +62 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +580 -0
- data/lib/active_record/associations/collection_proxy.rb +133 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +39 -119
- data/lib/active_record/associations/has_many_association.rb +60 -79
- data/lib/active_record/associations/has_many_through_association.rb +127 -206
- data/lib/active_record/associations/has_one_association.rb +55 -114
- data/lib/active_record/associations/has_one_through_association.rb +25 -26
- data/lib/active_record/associations/join_dependency/join_association.rb +159 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_dependency.rb +214 -0
- data/lib/active_record/associations/join_helper.rb +55 -0
- data/lib/active_record/associations/preloader/association.rb +125 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/preloader.rb +181 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +87 -0
- data/lib/active_record/associations.rb +693 -1337
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +111 -0
- data/lib/active_record/attribute_methods/primary_key.rb +114 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +136 -0
- data/lib/active_record/attribute_methods/serialization.rb +120 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +65 -0
- data/lib/active_record/attribute_methods/write.rb +70 -0
- data/lib/active_record/attribute_methods.rb +211 -339
- data/lib/active_record/autosave_association.rb +179 -149
- data/lib/active_record/base.rb +401 -2907
- data/lib/active_record/callbacks.rb +91 -176
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +236 -119
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +110 -58
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +175 -74
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +31 -35
- data/lib/active_record/connection_adapters/abstract/quoting.rb +71 -21
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +81 -311
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +194 -78
- data/lib/active_record/connection_adapters/abstract_adapter.rb +130 -83
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
- data/lib/active_record/connection_adapters/column.rb +296 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +280 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +272 -493
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +650 -405
- data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +30 -9
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +276 -147
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/counter_cache.rb +123 -0
- data/lib/active_record/dynamic_finder_match.rb +41 -14
- data/lib/active_record/dynamic_matchers.rb +84 -0
- data/lib/active_record/dynamic_scope_match.rb +13 -15
- data/lib/active_record/errors.rb +195 -0
- data/lib/active_record/explain.rb +86 -0
- data/lib/active_record/explain_subscriber.rb +25 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/fixtures.rb +695 -770
- data/lib/active_record/identity_map.rb +162 -0
- data/lib/active_record/inheritance.rb +174 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locale/en.yml +9 -27
- data/lib/active_record/locking/optimistic.rb +76 -73
- data/lib/active_record/locking/pessimistic.rb +32 -10
- data/lib/active_record/log_subscriber.rb +72 -0
- data/lib/active_record/migration/command_recorder.rb +105 -0
- data/lib/active_record/migration.rb +415 -205
- data/lib/active_record/model_schema.rb +368 -0
- data/lib/active_record/nested_attributes.rb +153 -63
- data/lib/active_record/observer.rb +27 -103
- data/lib/active_record/persistence.rb +376 -0
- data/lib/active_record/query_cache.rb +49 -8
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +131 -0
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/controller_runtime.rb +49 -0
- data/lib/active_record/railties/databases.rake +659 -0
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +269 -120
- data/lib/active_record/relation/batches.rb +90 -0
- data/lib/active_record/relation/calculations.rb +372 -0
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +402 -0
- data/lib/active_record/relation/predicate_builder.rb +63 -0
- data/lib/active_record/relation/query_methods.rb +417 -0
- data/lib/active_record/relation/spawn_methods.rb +180 -0
- data/lib/active_record/relation.rb +537 -0
- data/lib/active_record/result.rb +40 -0
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema.rb +9 -6
- data/lib/active_record/schema_dumper.rb +55 -32
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +200 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/serialization.rb +8 -91
- data/lib/active_record/serializers/xml_serializer.rb +43 -197
- data/lib/active_record/session_store.rb +129 -103
- data/lib/active_record/store.rb +52 -0
- data/lib/active_record/test_case.rb +30 -23
- data/lib/active_record/timestamp.rb +95 -52
- data/lib/active_record/transactions.rb +212 -66
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +43 -0
- data/lib/active_record/validations/uniqueness.rb +180 -0
- data/lib/active_record/validations.rb +43 -1106
- data/lib/active_record/version.rb +5 -4
- data/lib/active_record.rb +121 -48
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +34 -0
- data/lib/rails/generators/active_record/migration.rb +15 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +47 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +12 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
- data/lib/rails/generators/active_record.rb +25 -0
- metadata +187 -363
- data/CHANGELOG +0 -5904
- data/README +0 -351
- data/RUNNING_UNIT_TESTS +0 -36
- data/Rakefile +0 -268
- data/install.rb +0 -30
- data/lib/active_record/association_preload.rb +0 -406
- data/lib/active_record/associations/association_collection.rb +0 -533
- data/lib/active_record/associations/association_proxy.rb +0 -288
- data/lib/active_record/batches.rb +0 -85
- data/lib/active_record/calculations.rb +0 -321
- data/lib/active_record/dirty.rb +0 -183
- data/lib/active_record/named_scope.rb +0 -197
- data/lib/active_record/serializers/json_serializer.rb +0 -91
- data/lib/activerecord.rb +0 -2
- data/test/assets/example.log +0 -1
- data/test/assets/flowers.jpg +0 -0
- data/test/cases/aaa_create_tables_test.rb +0 -24
- data/test/cases/active_schema_test_mysql.rb +0 -122
- data/test/cases/active_schema_test_postgresql.rb +0 -24
- data/test/cases/adapter_test.rb +0 -144
- data/test/cases/aggregations_test.rb +0 -167
- data/test/cases/ar_schema_test.rb +0 -32
- data/test/cases/associations/belongs_to_associations_test.rb +0 -438
- data/test/cases/associations/callbacks_test.rb +0 -161
- data/test/cases/associations/cascaded_eager_loading_test.rb +0 -131
- data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +0 -36
- data/test/cases/associations/eager_load_nested_include_test.rb +0 -131
- data/test/cases/associations/eager_load_nested_polymorphic_include.rb +0 -19
- data/test/cases/associations/eager_singularization_test.rb +0 -145
- data/test/cases/associations/eager_test.rb +0 -852
- data/test/cases/associations/extension_test.rb +0 -62
- data/test/cases/associations/habtm_join_table_test.rb +0 -56
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +0 -827
- data/test/cases/associations/has_many_associations_test.rb +0 -1273
- data/test/cases/associations/has_many_through_associations_test.rb +0 -360
- data/test/cases/associations/has_one_associations_test.rb +0 -330
- data/test/cases/associations/has_one_through_associations_test.rb +0 -209
- data/test/cases/associations/inner_join_association_test.rb +0 -93
- data/test/cases/associations/inverse_associations_test.rb +0 -566
- data/test/cases/associations/join_model_test.rb +0 -712
- data/test/cases/associations_test.rb +0 -282
- data/test/cases/attribute_methods_test.rb +0 -305
- data/test/cases/autosave_association_test.rb +0 -1218
- data/test/cases/base_test.rb +0 -2166
- data/test/cases/batches_test.rb +0 -81
- data/test/cases/binary_test.rb +0 -30
- data/test/cases/calculations_test.rb +0 -360
- data/test/cases/callbacks_observers_test.rb +0 -38
- data/test/cases/callbacks_test.rb +0 -438
- data/test/cases/class_inheritable_attributes_test.rb +0 -32
- data/test/cases/column_alias_test.rb +0 -17
- data/test/cases/column_definition_test.rb +0 -70
- data/test/cases/connection_pool_test.rb +0 -25
- data/test/cases/connection_test_firebird.rb +0 -8
- data/test/cases/connection_test_mysql.rb +0 -65
- data/test/cases/copy_table_test_sqlite.rb +0 -80
- data/test/cases/counter_cache_test.rb +0 -84
- data/test/cases/database_statements_test.rb +0 -12
- data/test/cases/datatype_test_postgresql.rb +0 -204
- data/test/cases/date_time_test.rb +0 -37
- data/test/cases/default_test_firebird.rb +0 -16
- data/test/cases/defaults_test.rb +0 -111
- data/test/cases/deprecated_finder_test.rb +0 -30
- data/test/cases/dirty_test.rb +0 -316
- data/test/cases/finder_respond_to_test.rb +0 -76
- data/test/cases/finder_test.rb +0 -1098
- data/test/cases/fixtures_test.rb +0 -661
- data/test/cases/helper.rb +0 -68
- data/test/cases/i18n_test.rb +0 -46
- data/test/cases/inheritance_test.rb +0 -262
- data/test/cases/invalid_date_test.rb +0 -24
- data/test/cases/json_serialization_test.rb +0 -219
- data/test/cases/lifecycle_test.rb +0 -193
- data/test/cases/locking_test.rb +0 -350
- data/test/cases/method_scoping_test.rb +0 -704
- data/test/cases/migration_test.rb +0 -1649
- data/test/cases/migration_test_firebird.rb +0 -124
- data/test/cases/mixin_test.rb +0 -96
- data/test/cases/modules_test.rb +0 -109
- data/test/cases/multiple_db_test.rb +0 -85
- data/test/cases/named_scope_test.rb +0 -372
- data/test/cases/nested_attributes_test.rb +0 -840
- data/test/cases/pk_test.rb +0 -119
- data/test/cases/pooled_connections_test.rb +0 -103
- data/test/cases/query_cache_test.rb +0 -129
- data/test/cases/readonly_test.rb +0 -107
- data/test/cases/reflection_test.rb +0 -234
- data/test/cases/reload_models_test.rb +0 -22
- data/test/cases/repair_helper.rb +0 -50
- data/test/cases/reserved_word_test_mysql.rb +0 -176
- data/test/cases/sanitize_test.rb +0 -25
- data/test/cases/schema_authorization_test_postgresql.rb +0 -75
- data/test/cases/schema_dumper_test.rb +0 -211
- data/test/cases/schema_test_postgresql.rb +0 -178
- data/test/cases/serialization_test.rb +0 -47
- data/test/cases/sp_test_mysql.rb +0 -16
- data/test/cases/synonym_test_oracle.rb +0 -17
- data/test/cases/timestamp_test.rb +0 -75
- data/test/cases/transactions_test.rb +0 -543
- data/test/cases/unconnected_test.rb +0 -32
- data/test/cases/validations_i18n_test.rb +0 -925
- data/test/cases/validations_test.rb +0 -1684
- data/test/cases/xml_serialization_test.rb +0 -240
- data/test/cases/yaml_serialization_test.rb +0 -11
- data/test/config.rb +0 -5
- data/test/connections/jdbc_jdbcderby/connection.rb +0 -18
- data/test/connections/jdbc_jdbch2/connection.rb +0 -18
- data/test/connections/jdbc_jdbchsqldb/connection.rb +0 -18
- data/test/connections/jdbc_jdbcmysql/connection.rb +0 -26
- data/test/connections/jdbc_jdbcpostgresql/connection.rb +0 -26
- data/test/connections/jdbc_jdbcsqlite3/connection.rb +0 -25
- data/test/connections/native_db2/connection.rb +0 -25
- data/test/connections/native_firebird/connection.rb +0 -26
- data/test/connections/native_frontbase/connection.rb +0 -27
- data/test/connections/native_mysql/connection.rb +0 -25
- data/test/connections/native_openbase/connection.rb +0 -21
- data/test/connections/native_oracle/connection.rb +0 -27
- data/test/connections/native_postgresql/connection.rb +0 -21
- data/test/connections/native_sqlite/connection.rb +0 -25
- data/test/connections/native_sqlite3/connection.rb +0 -25
- data/test/connections/native_sqlite3/in_memory_connection.rb +0 -18
- data/test/connections/native_sybase/connection.rb +0 -23
- data/test/fixtures/accounts.yml +0 -29
- data/test/fixtures/all/developers.yml +0 -0
- data/test/fixtures/all/people.csv +0 -0
- data/test/fixtures/all/tasks.yml +0 -0
- data/test/fixtures/author_addresses.yml +0 -5
- data/test/fixtures/author_favorites.yml +0 -4
- data/test/fixtures/authors.yml +0 -9
- data/test/fixtures/binaries.yml +0 -132
- data/test/fixtures/books.yml +0 -7
- data/test/fixtures/categories/special_categories.yml +0 -9
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +0 -4
- data/test/fixtures/categories.yml +0 -14
- data/test/fixtures/categories_ordered.yml +0 -7
- data/test/fixtures/categories_posts.yml +0 -23
- data/test/fixtures/categorizations.yml +0 -17
- data/test/fixtures/clubs.yml +0 -6
- data/test/fixtures/comments.yml +0 -59
- data/test/fixtures/companies.yml +0 -56
- data/test/fixtures/computers.yml +0 -4
- data/test/fixtures/courses.yml +0 -7
- data/test/fixtures/customers.yml +0 -26
- data/test/fixtures/developers.yml +0 -21
- data/test/fixtures/developers_projects.yml +0 -17
- data/test/fixtures/edges.yml +0 -6
- data/test/fixtures/entrants.yml +0 -14
- data/test/fixtures/faces.yml +0 -11
- data/test/fixtures/fk_test_has_fk.yml +0 -3
- data/test/fixtures/fk_test_has_pk.yml +0 -2
- data/test/fixtures/funny_jokes.yml +0 -10
- data/test/fixtures/interests.yml +0 -33
- data/test/fixtures/items.yml +0 -4
- data/test/fixtures/jobs.yml +0 -7
- data/test/fixtures/legacy_things.yml +0 -3
- data/test/fixtures/mateys.yml +0 -4
- data/test/fixtures/member_types.yml +0 -6
- data/test/fixtures/members.yml +0 -6
- data/test/fixtures/memberships.yml +0 -20
- data/test/fixtures/men.yml +0 -5
- data/test/fixtures/minimalistics.yml +0 -2
- data/test/fixtures/mixed_case_monkeys.yml +0 -6
- data/test/fixtures/mixins.yml +0 -29
- data/test/fixtures/movies.yml +0 -7
- data/test/fixtures/naked/csv/accounts.csv +0 -1
- data/test/fixtures/naked/yml/accounts.yml +0 -1
- data/test/fixtures/naked/yml/companies.yml +0 -1
- data/test/fixtures/naked/yml/courses.yml +0 -1
- data/test/fixtures/organizations.yml +0 -5
- data/test/fixtures/owners.yml +0 -7
- data/test/fixtures/parrots.yml +0 -27
- data/test/fixtures/parrots_pirates.yml +0 -7
- data/test/fixtures/people.yml +0 -15
- data/test/fixtures/pets.yml +0 -14
- data/test/fixtures/pirates.yml +0 -9
- data/test/fixtures/polymorphic_designs.yml +0 -19
- data/test/fixtures/polymorphic_prices.yml +0 -19
- data/test/fixtures/posts.yml +0 -52
- data/test/fixtures/price_estimates.yml +0 -7
- data/test/fixtures/projects.yml +0 -7
- data/test/fixtures/readers.yml +0 -9
- data/test/fixtures/references.yml +0 -17
- data/test/fixtures/reserved_words/distinct.yml +0 -5
- data/test/fixtures/reserved_words/distincts_selects.yml +0 -11
- data/test/fixtures/reserved_words/group.yml +0 -14
- data/test/fixtures/reserved_words/select.yml +0 -8
- data/test/fixtures/reserved_words/values.yml +0 -7
- data/test/fixtures/ships.yml +0 -5
- data/test/fixtures/sponsors.yml +0 -9
- data/test/fixtures/subscribers.yml +0 -7
- data/test/fixtures/subscriptions.yml +0 -12
- data/test/fixtures/taggings.yml +0 -28
- data/test/fixtures/tags.yml +0 -7
- data/test/fixtures/tasks.yml +0 -7
- data/test/fixtures/tees.yml +0 -4
- data/test/fixtures/ties.yml +0 -4
- data/test/fixtures/topics.yml +0 -42
- data/test/fixtures/toys.yml +0 -4
- data/test/fixtures/treasures.yml +0 -10
- data/test/fixtures/vertices.yml +0 -4
- data/test/fixtures/warehouse-things.yml +0 -3
- data/test/fixtures/zines.yml +0 -5
- data/test/migrations/broken/100_migration_that_raises_exception.rb +0 -10
- data/test/migrations/decimal/1_give_me_big_numbers.rb +0 -15
- data/test/migrations/duplicate/1_people_have_last_names.rb +0 -9
- data/test/migrations/duplicate/2_we_need_reminders.rb +0 -12
- data/test/migrations/duplicate/3_foo.rb +0 -7
- data/test/migrations/duplicate/3_innocent_jointable.rb +0 -12
- data/test/migrations/duplicate_names/20080507052938_chunky.rb +0 -7
- data/test/migrations/duplicate_names/20080507053028_chunky.rb +0 -7
- data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +0 -12
- data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +0 -9
- data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +0 -12
- data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +0 -9
- data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +0 -8
- data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +0 -12
- data/test/migrations/missing/1000_people_have_middle_names.rb +0 -9
- data/test/migrations/missing/1_people_have_last_names.rb +0 -9
- data/test/migrations/missing/3_we_need_reminders.rb +0 -12
- data/test/migrations/missing/4_innocent_jointable.rb +0 -12
- data/test/migrations/valid/1_people_have_last_names.rb +0 -9
- data/test/migrations/valid/2_we_need_reminders.rb +0 -12
- data/test/migrations/valid/3_innocent_jointable.rb +0 -12
- data/test/models/author.rb +0 -151
- data/test/models/auto_id.rb +0 -4
- data/test/models/binary.rb +0 -2
- data/test/models/bird.rb +0 -9
- data/test/models/book.rb +0 -4
- data/test/models/categorization.rb +0 -5
- data/test/models/category.rb +0 -34
- data/test/models/citation.rb +0 -6
- data/test/models/club.rb +0 -13
- data/test/models/column_name.rb +0 -3
- data/test/models/comment.rb +0 -29
- data/test/models/company.rb +0 -173
- data/test/models/company_in_module.rb +0 -78
- data/test/models/computer.rb +0 -3
- data/test/models/contact.rb +0 -16
- data/test/models/contract.rb +0 -5
- data/test/models/course.rb +0 -3
- data/test/models/customer.rb +0 -73
- data/test/models/default.rb +0 -2
- data/test/models/developer.rb +0 -101
- data/test/models/edge.rb +0 -5
- data/test/models/entrant.rb +0 -3
- data/test/models/essay.rb +0 -3
- data/test/models/event.rb +0 -3
- data/test/models/event_author.rb +0 -8
- data/test/models/face.rb +0 -7
- data/test/models/guid.rb +0 -2
- data/test/models/interest.rb +0 -5
- data/test/models/invoice.rb +0 -4
- data/test/models/item.rb +0 -7
- data/test/models/job.rb +0 -5
- data/test/models/joke.rb +0 -3
- data/test/models/keyboard.rb +0 -3
- data/test/models/legacy_thing.rb +0 -3
- data/test/models/line_item.rb +0 -3
- data/test/models/man.rb +0 -9
- data/test/models/matey.rb +0 -4
- data/test/models/member.rb +0 -12
- data/test/models/member_detail.rb +0 -5
- data/test/models/member_type.rb +0 -3
- data/test/models/membership.rb +0 -9
- data/test/models/minimalistic.rb +0 -2
- data/test/models/mixed_case_monkey.rb +0 -3
- data/test/models/movie.rb +0 -5
- data/test/models/order.rb +0 -4
- data/test/models/organization.rb +0 -6
- data/test/models/owner.rb +0 -5
- data/test/models/parrot.rb +0 -22
- data/test/models/person.rb +0 -16
- data/test/models/pet.rb +0 -5
- data/test/models/pirate.rb +0 -80
- data/test/models/polymorphic_design.rb +0 -3
- data/test/models/polymorphic_price.rb +0 -3
- data/test/models/post.rb +0 -102
- data/test/models/price_estimate.rb +0 -3
- data/test/models/project.rb +0 -30
- data/test/models/reader.rb +0 -4
- data/test/models/reference.rb +0 -4
- data/test/models/reply.rb +0 -46
- data/test/models/ship.rb +0 -19
- data/test/models/ship_part.rb +0 -7
- data/test/models/sponsor.rb +0 -4
- data/test/models/subject.rb +0 -4
- data/test/models/subscriber.rb +0 -8
- data/test/models/subscription.rb +0 -4
- data/test/models/tag.rb +0 -7
- data/test/models/tagging.rb +0 -10
- data/test/models/task.rb +0 -3
- data/test/models/tee.rb +0 -4
- data/test/models/tie.rb +0 -4
- data/test/models/topic.rb +0 -80
- data/test/models/toy.rb +0 -6
- data/test/models/treasure.rb +0 -8
- data/test/models/vertex.rb +0 -9
- data/test/models/warehouse_thing.rb +0 -5
- data/test/models/zine.rb +0 -3
- data/test/schema/mysql_specific_schema.rb +0 -31
- data/test/schema/postgresql_specific_schema.rb +0 -114
- data/test/schema/schema.rb +0 -550
- data/test/schema/schema2.rb +0 -6
- data/test/schema/sqlite_specific_schema.rb +0 -25
@@ -0,0 +1,580 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Associations
|
5
|
+
# = Active Record Association Collection
|
6
|
+
#
|
7
|
+
# CollectionAssociation is an abstract class that provides common stuff to
|
8
|
+
# ease the implementation of association proxies that represent
|
9
|
+
# collections. See the class hierarchy in AssociationProxy.
|
10
|
+
#
|
11
|
+
# You need to be careful with assumptions regarding the target: The proxy
|
12
|
+
# does not fetch records from the database until it needs them, but new
|
13
|
+
# ones created with +build+ are added to the target. So, the target may be
|
14
|
+
# non-empty and still lack children waiting to be read from the database.
|
15
|
+
# If you look directly to the database you cannot assume that's the entire
|
16
|
+
# collection because new records may have been added to the target, etc.
|
17
|
+
#
|
18
|
+
# If you need to work on all current children, new and existing records,
|
19
|
+
# +load_target+ and the +loaded+ flag are your friends.
|
20
|
+
class CollectionAssociation < Association #:nodoc:
|
21
|
+
attr_reader :proxy
|
22
|
+
|
23
|
+
def initialize(owner, reflection)
|
24
|
+
super
|
25
|
+
@proxy = CollectionProxy.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Implements the reader method, e.g. foo.items for Foo.has_many :items
|
29
|
+
def reader(force_reload = false)
|
30
|
+
if force_reload
|
31
|
+
klass.uncached { reload }
|
32
|
+
elsif stale_target?
|
33
|
+
reload
|
34
|
+
end
|
35
|
+
|
36
|
+
proxy
|
37
|
+
end
|
38
|
+
|
39
|
+
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
|
40
|
+
def writer(records)
|
41
|
+
replace(records)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
|
45
|
+
def ids_reader
|
46
|
+
if owner.new_record? || loaded? || options[:finder_sql]
|
47
|
+
load_target.map do |record|
|
48
|
+
record.send(reflection.association_primary_key)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
|
52
|
+
relation = scoped
|
53
|
+
|
54
|
+
including = (relation.eager_load_values + relation.includes_values).uniq
|
55
|
+
|
56
|
+
if including.any?
|
57
|
+
join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
|
58
|
+
relation = join_dependency.join_associations.inject(relation) do |r, association|
|
59
|
+
association.join_relation(r)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
relation.pluck(column)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
|
68
|
+
def ids_writer(ids)
|
69
|
+
pk_column = reflection.primary_key_column
|
70
|
+
ids = Array.wrap(ids).reject { |id| id.blank? }
|
71
|
+
ids.map! { |i| pk_column.type_cast(i) }
|
72
|
+
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset
|
76
|
+
@loaded = false
|
77
|
+
@target = []
|
78
|
+
end
|
79
|
+
|
80
|
+
def select(select = nil)
|
81
|
+
if block_given?
|
82
|
+
load_target.select.each { |e| yield e }
|
83
|
+
else
|
84
|
+
scoped.select(select)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def find(*args)
|
89
|
+
if block_given?
|
90
|
+
load_target.find(*args) { |*block_args| yield(*block_args) }
|
91
|
+
else
|
92
|
+
if options[:finder_sql]
|
93
|
+
find_by_scan(*args)
|
94
|
+
else
|
95
|
+
scoped.find(*args)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def first(*args)
|
101
|
+
first_or_last(:first, *args)
|
102
|
+
end
|
103
|
+
|
104
|
+
def last(*args)
|
105
|
+
first_or_last(:last, *args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def build(attributes = {}, options = {}, &block)
|
109
|
+
if attributes.is_a?(Array)
|
110
|
+
attributes.collect { |attr| build(attr, options, &block) }
|
111
|
+
else
|
112
|
+
add_to_target(build_record(attributes, options)) do |record|
|
113
|
+
yield(record) if block_given?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def create(attributes = {}, options = {}, &block)
|
119
|
+
create_record(attributes, options, &block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def create!(attributes = {}, options = {}, &block)
|
123
|
+
create_record(attributes, options, true, &block)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
127
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
128
|
+
def concat(*records)
|
129
|
+
load_target if owner.new_record?
|
130
|
+
|
131
|
+
if owner.new_record?
|
132
|
+
concat_records(records)
|
133
|
+
else
|
134
|
+
transaction { concat_records(records) }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Starts a transaction in the association class's database connection.
|
139
|
+
#
|
140
|
+
# class Author < ActiveRecord::Base
|
141
|
+
# has_many :books
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# Author.first.books.transaction do
|
145
|
+
# # same effect as calling Book.transaction
|
146
|
+
# end
|
147
|
+
def transaction(*args)
|
148
|
+
reflection.klass.transaction(*args) do
|
149
|
+
yield
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Remove all records from this association
|
154
|
+
#
|
155
|
+
# See delete for more info.
|
156
|
+
def delete_all
|
157
|
+
delete(load_target).tap do
|
158
|
+
reset
|
159
|
+
loaded!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Called when the association is declared as :dependent => :delete_all. This is
|
164
|
+
# an optimised version which avoids loading the records into memory. Not really
|
165
|
+
# for public consumption.
|
166
|
+
def delete_all_on_destroy
|
167
|
+
scoped.delete_all
|
168
|
+
end
|
169
|
+
|
170
|
+
# Destroy all the records from this association.
|
171
|
+
#
|
172
|
+
# See destroy for more info.
|
173
|
+
def destroy_all
|
174
|
+
destroy(load_target).tap do
|
175
|
+
reset
|
176
|
+
loaded!
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Calculate sum using SQL, not Enumerable
|
181
|
+
def sum(*args)
|
182
|
+
if block_given?
|
183
|
+
scoped.sum(*args) { |*block_args| yield(*block_args) }
|
184
|
+
else
|
185
|
+
scoped.sum(*args)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
|
190
|
+
# association, it will be used for the query. Otherwise, construct options and pass them with
|
191
|
+
# scope to the target class's +count+.
|
192
|
+
def count(column_name = nil, count_options = {})
|
193
|
+
return 0 if owner.new_record?
|
194
|
+
|
195
|
+
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
|
196
|
+
|
197
|
+
if options[:counter_sql] || options[:finder_sql]
|
198
|
+
unless count_options.blank?
|
199
|
+
raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
|
200
|
+
end
|
201
|
+
|
202
|
+
reflection.klass.count_by_sql(custom_counter_sql)
|
203
|
+
else
|
204
|
+
if options[:uniq]
|
205
|
+
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
|
206
|
+
column_name ||= reflection.klass.primary_key
|
207
|
+
count_options.merge!(:distinct => true)
|
208
|
+
end
|
209
|
+
|
210
|
+
value = scoped.count(column_name, count_options)
|
211
|
+
|
212
|
+
limit = options[:limit]
|
213
|
+
offset = options[:offset]
|
214
|
+
|
215
|
+
if limit || offset
|
216
|
+
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
217
|
+
else
|
218
|
+
value
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Removes +records+ from this association calling +before_remove+ and
|
224
|
+
# +after_remove+ callbacks.
|
225
|
+
#
|
226
|
+
# This method is abstract in the sense that +delete_records+ has to be
|
227
|
+
# provided by descendants. Note this method does not imply the records
|
228
|
+
# are actually removed from the database, that depends precisely on
|
229
|
+
# +delete_records+. They are in any case removed from the collection.
|
230
|
+
def delete(*records)
|
231
|
+
delete_or_destroy(records, options[:dependent])
|
232
|
+
end
|
233
|
+
|
234
|
+
# Destroy +records+ and remove them from this association calling
|
235
|
+
# +before_remove+ and +after_remove+ callbacks.
|
236
|
+
#
|
237
|
+
# Note that this method will _always_ remove records from the database
|
238
|
+
# ignoring the +:dependent+ option.
|
239
|
+
def destroy(*records)
|
240
|
+
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
|
241
|
+
delete_or_destroy(records, :destroy)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the size of the collection by executing a SELECT COUNT(*)
|
245
|
+
# query if the collection hasn't been loaded, and calling
|
246
|
+
# <tt>collection.size</tt> if it has.
|
247
|
+
#
|
248
|
+
# If the collection has been already loaded +size+ and +length+ are
|
249
|
+
# equivalent. If not and you are going to need the records anyway
|
250
|
+
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
251
|
+
#
|
252
|
+
# This method is abstract in the sense that it relies on
|
253
|
+
# +count_records+, which is a method descendants have to provide.
|
254
|
+
def size
|
255
|
+
if !find_target? || (loaded? && !options[:uniq])
|
256
|
+
target.size
|
257
|
+
elsif !loaded? && options[:group]
|
258
|
+
load_target.size
|
259
|
+
elsif !loaded? && !options[:uniq] && target.is_a?(Array)
|
260
|
+
unsaved_records = target.select { |r| r.new_record? }
|
261
|
+
unsaved_records.size + count_records
|
262
|
+
else
|
263
|
+
count_records
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns the size of the collection calling +size+ on the target.
|
268
|
+
#
|
269
|
+
# If the collection has been already loaded +length+ and +size+ are
|
270
|
+
# equivalent. If not and you are going to need the records anyway this
|
271
|
+
# method will take one less query. Otherwise +size+ is more efficient.
|
272
|
+
def length
|
273
|
+
load_target.size
|
274
|
+
end
|
275
|
+
|
276
|
+
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
|
277
|
+
# not been already loaded and you are going to fetch the records anyway
|
278
|
+
# it is better to check <tt>collection.length.zero?</tt>.
|
279
|
+
def empty?
|
280
|
+
size.zero?
|
281
|
+
end
|
282
|
+
|
283
|
+
def any?
|
284
|
+
if block_given?
|
285
|
+
load_target.any? { |*block_args| yield(*block_args) }
|
286
|
+
else
|
287
|
+
!empty?
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
|
292
|
+
def many?
|
293
|
+
if block_given?
|
294
|
+
load_target.many? { |*block_args| yield(*block_args) }
|
295
|
+
else
|
296
|
+
size > 1
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def uniq(collection = load_target)
|
301
|
+
seen = {}
|
302
|
+
collection.find_all do |record|
|
303
|
+
seen[record.id] = true unless seen.key?(record.id)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Replace this collection with +other_array+
|
308
|
+
# This will perform a diff and delete/add only records that have changed.
|
309
|
+
def replace(other_array)
|
310
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
311
|
+
original_target = load_target.dup
|
312
|
+
|
313
|
+
if owner.new_record?
|
314
|
+
replace_records(other_array, original_target)
|
315
|
+
else
|
316
|
+
transaction { replace_records(other_array, original_target) }
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def include?(record)
|
321
|
+
if record.is_a?(reflection.klass)
|
322
|
+
if record.new_record?
|
323
|
+
include_in_memory?(record)
|
324
|
+
else
|
325
|
+
load_target if options[:finder_sql]
|
326
|
+
loaded? ? target.include?(record) : scoped.exists?(record)
|
327
|
+
end
|
328
|
+
else
|
329
|
+
false
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def load_target
|
334
|
+
if find_target?
|
335
|
+
@target = merge_target_lists(find_target, target)
|
336
|
+
end
|
337
|
+
|
338
|
+
loaded!
|
339
|
+
target
|
340
|
+
end
|
341
|
+
|
342
|
+
def add_to_target(record)
|
343
|
+
callback(:before_add, record)
|
344
|
+
yield(record) if block_given?
|
345
|
+
|
346
|
+
if options[:uniq] && index = @target.index(record)
|
347
|
+
@target[index] = record
|
348
|
+
else
|
349
|
+
@target << record
|
350
|
+
end
|
351
|
+
|
352
|
+
callback(:after_add, record)
|
353
|
+
set_inverse_instance(record)
|
354
|
+
|
355
|
+
record
|
356
|
+
end
|
357
|
+
|
358
|
+
private
|
359
|
+
|
360
|
+
def custom_counter_sql
|
361
|
+
if options[:counter_sql]
|
362
|
+
interpolate(options[:counter_sql])
|
363
|
+
else
|
364
|
+
# replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
|
365
|
+
interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
|
366
|
+
count_with = $2.to_s
|
367
|
+
count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
|
368
|
+
"SELECT #{$1}COUNT(#{count_with}) FROM"
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def custom_finder_sql
|
374
|
+
interpolate(options[:finder_sql])
|
375
|
+
end
|
376
|
+
|
377
|
+
def find_target
|
378
|
+
records =
|
379
|
+
if options[:finder_sql]
|
380
|
+
reflection.klass.find_by_sql(custom_finder_sql)
|
381
|
+
else
|
382
|
+
scoped.all
|
383
|
+
end
|
384
|
+
|
385
|
+
records = options[:uniq] ? uniq(records) : records
|
386
|
+
records.each { |record| set_inverse_instance(record) }
|
387
|
+
records
|
388
|
+
end
|
389
|
+
|
390
|
+
# We have some records loaded from the database (persisted) and some that are
|
391
|
+
# in-memory (memory). The same record may be represented in the persisted array
|
392
|
+
# and in the memory array.
|
393
|
+
#
|
394
|
+
# So the task of this method is to merge them according to the following rules:
|
395
|
+
#
|
396
|
+
# * The final array must not have duplicates
|
397
|
+
# * The order of the persisted array is to be preserved
|
398
|
+
# * Any changes made to attributes on objects in the memory array are to be preserved
|
399
|
+
# * Otherwise, attributes should have the value found in the database
|
400
|
+
def merge_target_lists(persisted, memory)
|
401
|
+
return persisted if memory.empty?
|
402
|
+
return memory if persisted.empty?
|
403
|
+
|
404
|
+
persisted.map! do |record|
|
405
|
+
# Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns
|
406
|
+
# record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9.
|
407
|
+
mem_index = memory.index(record)
|
408
|
+
|
409
|
+
if mem_index
|
410
|
+
mem_record = memory.delete_at(mem_index)
|
411
|
+
|
412
|
+
((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
|
413
|
+
mem_record[name] = record[name]
|
414
|
+
end
|
415
|
+
|
416
|
+
mem_record
|
417
|
+
else
|
418
|
+
record
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
persisted + memory
|
423
|
+
end
|
424
|
+
|
425
|
+
def create_record(attributes, options, raise = false, &block)
|
426
|
+
unless owner.persisted?
|
427
|
+
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
|
428
|
+
end
|
429
|
+
|
430
|
+
if attributes.is_a?(Array)
|
431
|
+
attributes.collect { |attr| create_record(attr, options, raise, &block) }
|
432
|
+
else
|
433
|
+
transaction do
|
434
|
+
add_to_target(build_record(attributes, options)) do |record|
|
435
|
+
yield(record) if block_given?
|
436
|
+
insert_record(record, true, raise)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Do the relevant stuff to insert the given record into the association collection.
|
443
|
+
def insert_record(record, validate = true, raise = false)
|
444
|
+
raise NotImplementedError
|
445
|
+
end
|
446
|
+
|
447
|
+
def create_scope
|
448
|
+
scoped.scope_for_create.stringify_keys
|
449
|
+
end
|
450
|
+
|
451
|
+
def delete_or_destroy(records, method)
|
452
|
+
records = records.flatten
|
453
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
454
|
+
existing_records = records.reject { |r| r.new_record? }
|
455
|
+
|
456
|
+
if existing_records.empty?
|
457
|
+
remove_records(existing_records, records, method)
|
458
|
+
else
|
459
|
+
transaction { remove_records(existing_records, records, method) }
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def remove_records(existing_records, records, method)
|
464
|
+
records.each { |record| callback(:before_remove, record) }
|
465
|
+
|
466
|
+
delete_records(existing_records, method) if existing_records.any?
|
467
|
+
records.each { |record| target.delete(record) }
|
468
|
+
|
469
|
+
records.each { |record| callback(:after_remove, record) }
|
470
|
+
end
|
471
|
+
|
472
|
+
# Delete the given records from the association, using one of the methods :destroy,
|
473
|
+
# :delete_all or :nullify (or nil, in which case a default is used).
|
474
|
+
def delete_records(records, method)
|
475
|
+
raise NotImplementedError
|
476
|
+
end
|
477
|
+
|
478
|
+
def replace_records(new_target, original_target)
|
479
|
+
delete(target - new_target)
|
480
|
+
|
481
|
+
unless concat(new_target - target)
|
482
|
+
@target = original_target
|
483
|
+
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
|
484
|
+
"new records could not be saved."
|
485
|
+
end
|
486
|
+
|
487
|
+
target
|
488
|
+
end
|
489
|
+
|
490
|
+
def concat_records(records)
|
491
|
+
result = true
|
492
|
+
|
493
|
+
records.flatten.each do |record|
|
494
|
+
raise_on_type_mismatch(record)
|
495
|
+
add_to_target(record) do |r|
|
496
|
+
result &&= insert_record(record) unless owner.new_record?
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
result && records
|
501
|
+
end
|
502
|
+
|
503
|
+
def callback(method, record)
|
504
|
+
callbacks_for(method).each do |callback|
|
505
|
+
case callback
|
506
|
+
when Symbol
|
507
|
+
owner.send(callback, record)
|
508
|
+
when Proc
|
509
|
+
callback.call(owner, record)
|
510
|
+
else
|
511
|
+
callback.send(method, owner, record)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
def callbacks_for(callback_name)
|
517
|
+
full_callback_name = "#{callback_name}_for_#{reflection.name}"
|
518
|
+
owner.class.send(full_callback_name.to_sym) || []
|
519
|
+
end
|
520
|
+
|
521
|
+
# Should we deal with assoc.first or assoc.last by issuing an independent query to
|
522
|
+
# the database, or by getting the target, and then taking the first/last item from that?
|
523
|
+
#
|
524
|
+
# If the args is just a non-empty options hash, go to the database.
|
525
|
+
#
|
526
|
+
# Otherwise, go to the database only if none of the following are true:
|
527
|
+
# * target already loaded
|
528
|
+
# * owner is new record
|
529
|
+
# * custom :finder_sql exists
|
530
|
+
# * target contains new or changed record(s)
|
531
|
+
# * the first arg is an integer (which indicates the number of records to be returned)
|
532
|
+
def fetch_first_or_last_using_find?(args)
|
533
|
+
if args.first.is_a?(Hash)
|
534
|
+
true
|
535
|
+
else
|
536
|
+
!(loaded? ||
|
537
|
+
owner.new_record? ||
|
538
|
+
options[:finder_sql] ||
|
539
|
+
target.any? { |record| record.new_record? || record.changed? } ||
|
540
|
+
args.first.kind_of?(Integer))
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
def include_in_memory?(record)
|
545
|
+
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
546
|
+
owner.send(reflection.through_reflection.name).any? { |source|
|
547
|
+
target = source.send(reflection.source_reflection.name)
|
548
|
+
target.respond_to?(:include?) ? target.include?(record) : target == record
|
549
|
+
} || target.include?(record)
|
550
|
+
else
|
551
|
+
target.include?(record)
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# If using a custom finder_sql, #find scans the entire collection.
|
556
|
+
def find_by_scan(*args)
|
557
|
+
expects_array = args.first.kind_of?(Array)
|
558
|
+
ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
|
559
|
+
|
560
|
+
if ids.size == 1
|
561
|
+
id = ids.first
|
562
|
+
record = load_target.detect { |r| id == r.id }
|
563
|
+
expects_array ? [ record ] : record
|
564
|
+
else
|
565
|
+
load_target.select { |r| ids.include?(r.id) }
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
# Fetches the first/last using SQL if possible, otherwise from the target array.
|
570
|
+
def first_or_last(type, *args)
|
571
|
+
args.shift if args.first.is_a?(Hash) && args.first.empty?
|
572
|
+
|
573
|
+
collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
|
574
|
+
collection.send(type, *args).tap do |record|
|
575
|
+
set_inverse_instance record if record.is_a? ActiveRecord::Base
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
# Association proxies in Active Record are middlemen between the object that
|
4
|
+
# holds the association, known as the <tt>@owner</tt>, and the actual associated
|
5
|
+
# object, known as the <tt>@target</tt>. The kind of association any proxy is
|
6
|
+
# about is available in <tt>@reflection</tt>. That's an instance of the class
|
7
|
+
# ActiveRecord::Reflection::AssociationReflection.
|
8
|
+
#
|
9
|
+
# For example, given
|
10
|
+
#
|
11
|
+
# class Blog < ActiveRecord::Base
|
12
|
+
# has_many :posts
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# blog = Blog.first
|
16
|
+
#
|
17
|
+
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
|
18
|
+
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
|
19
|
+
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
|
20
|
+
#
|
21
|
+
# This class has most of the basic instance methods removed, and delegates
|
22
|
+
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
|
23
|
+
# corner case, it even removes the +class+ method and that's why you get
|
24
|
+
#
|
25
|
+
# blog.posts.class # => Array
|
26
|
+
#
|
27
|
+
# though the object behind <tt>blog.posts</tt> is not an Array, but an
|
28
|
+
# ActiveRecord::Associations::HasManyAssociation.
|
29
|
+
#
|
30
|
+
# The <tt>@target</tt> object is not \loaded until needed. For example,
|
31
|
+
#
|
32
|
+
# blog.posts.count
|
33
|
+
#
|
34
|
+
# is computed directly through SQL and does not trigger by itself the
|
35
|
+
# instantiation of the actual post records.
|
36
|
+
class CollectionProxy # :nodoc:
|
37
|
+
alias :proxy_extend :extend
|
38
|
+
|
39
|
+
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
|
40
|
+
|
41
|
+
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
|
42
|
+
:lock, :readonly, :having, :pluck, :to => :scoped
|
43
|
+
|
44
|
+
delegate :target, :load_target, :loaded?, :to => :@association
|
45
|
+
|
46
|
+
delegate :select, :find, :first, :last,
|
47
|
+
:build, :create, :create!,
|
48
|
+
:concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
|
49
|
+
:sum, :count, :size, :length, :empty?,
|
50
|
+
:any?, :many?, :include?,
|
51
|
+
:to => :@association
|
52
|
+
|
53
|
+
def initialize(association)
|
54
|
+
@association = association
|
55
|
+
Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :new, :build
|
59
|
+
|
60
|
+
def proxy_association
|
61
|
+
@association
|
62
|
+
end
|
63
|
+
|
64
|
+
def scoped
|
65
|
+
association = @association
|
66
|
+
association.scoped.extending do
|
67
|
+
define_method(:proxy_association) { association }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def respond_to?(name, include_private = false)
|
72
|
+
super ||
|
73
|
+
(load_target && target.respond_to?(name, include_private)) ||
|
74
|
+
proxy_association.klass.respond_to?(name, include_private)
|
75
|
+
end
|
76
|
+
|
77
|
+
def method_missing(method, *args, &block)
|
78
|
+
match = DynamicFinderMatch.match(method)
|
79
|
+
if match && match.instantiator?
|
80
|
+
send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |record|
|
81
|
+
proxy_association.send :set_owner_attributes, record
|
82
|
+
proxy_association.send :add_to_target, record
|
83
|
+
yield(record) if block_given?
|
84
|
+
end.tap do |record|
|
85
|
+
proxy_association.send :set_inverse_instance, record
|
86
|
+
end
|
87
|
+
|
88
|
+
elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
|
89
|
+
if load_target
|
90
|
+
if target.respond_to?(method)
|
91
|
+
target.send(method, *args, &block)
|
92
|
+
else
|
93
|
+
begin
|
94
|
+
super
|
95
|
+
rescue NoMethodError => e
|
96
|
+
raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
else
|
102
|
+
scoped.readonly(nil).send(method, *args, &block)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Forwards <tt>===</tt> explicitly to the \target because the instance method
|
107
|
+
# removal above doesn't catch it. Loads the \target if needed.
|
108
|
+
def ===(other)
|
109
|
+
other === load_target
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_ary
|
113
|
+
load_target.dup
|
114
|
+
end
|
115
|
+
alias_method :to_a, :to_ary
|
116
|
+
|
117
|
+
def <<(*records)
|
118
|
+
proxy_association.concat(records) && self
|
119
|
+
end
|
120
|
+
alias_method :push, :<<
|
121
|
+
|
122
|
+
def clear
|
123
|
+
delete_all
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
def reload
|
128
|
+
proxy_association.reload
|
129
|
+
self
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|