activerecord_csi 2.3.5.p6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +5858 -0
- data/README +351 -0
- data/RUNNING_UNIT_TESTS +36 -0
- data/Rakefile +270 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +162 -0
- data/install.rb +30 -0
- data/lib/active_record/aggregations.rb +261 -0
- data/lib/active_record/association_preload.rb +389 -0
- data/lib/active_record/associations/association_collection.rb +475 -0
- data/lib/active_record/associations/association_proxy.rb +278 -0
- data/lib/active_record/associations/belongs_to_association.rb +76 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +53 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
- data/lib/active_record/associations/has_many_association.rb +122 -0
- data/lib/active_record/associations/has_many_through_association.rb +266 -0
- data/lib/active_record/associations/has_one_association.rb +133 -0
- data/lib/active_record/associations/has_one_through_association.rb +37 -0
- data/lib/active_record/associations.rb +2241 -0
- data/lib/active_record/attribute_methods.rb +388 -0
- data/lib/active_record/autosave_association.rb +364 -0
- data/lib/active_record/base.rb +3171 -0
- data/lib/active_record/batches.rb +81 -0
- data/lib/active_record/calculations.rb +311 -0
- data/lib/active_record/callbacks.rb +360 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +371 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +139 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +289 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +94 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +722 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +434 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +241 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +630 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1113 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +453 -0
- data/lib/active_record/dirty.rb +183 -0
- data/lib/active_record/dynamic_finder_match.rb +41 -0
- data/lib/active_record/dynamic_scope_match.rb +25 -0
- data/lib/active_record/fixtures.rb +996 -0
- data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
- data/lib/active_record/locale/en.yml +58 -0
- data/lib/active_record/locking/optimistic.rb +148 -0
- data/lib/active_record/locking/pessimistic.rb +55 -0
- data/lib/active_record/migration.rb +566 -0
- data/lib/active_record/named_scope.rb +192 -0
- data/lib/active_record/nested_attributes.rb +392 -0
- data/lib/active_record/observer.rb +197 -0
- data/lib/active_record/query_cache.rb +33 -0
- data/lib/active_record/reflection.rb +320 -0
- data/lib/active_record/schema.rb +51 -0
- data/lib/active_record/schema_dumper.rb +182 -0
- data/lib/active_record/serialization.rb +101 -0
- data/lib/active_record/serializers/json_serializer.rb +91 -0
- data/lib/active_record/serializers/xml_serializer.rb +357 -0
- data/lib/active_record/session_store.rb +326 -0
- data/lib/active_record/test_case.rb +66 -0
- data/lib/active_record/timestamp.rb +71 -0
- data/lib/active_record/transactions.rb +235 -0
- data/lib/active_record/validations.rb +1135 -0
- data/lib/active_record/version.rb +9 -0
- data/lib/active_record.rb +84 -0
- data/lib/activerecord.rb +2 -0
- data/test/assets/example.log +1 -0
- data/test/assets/flowers.jpg +0 -0
- data/test/cases/aaa_create_tables_test.rb +24 -0
- data/test/cases/active_schema_test_mysql.rb +100 -0
- data/test/cases/active_schema_test_postgresql.rb +24 -0
- data/test/cases/adapter_test.rb +145 -0
- data/test/cases/aggregations_test.rb +167 -0
- data/test/cases/ar_schema_test.rb +32 -0
- data/test/cases/associations/belongs_to_associations_test.rb +425 -0
- data/test/cases/associations/callbacks_test.rb +161 -0
- data/test/cases/associations/cascaded_eager_loading_test.rb +131 -0
- data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +36 -0
- data/test/cases/associations/eager_load_nested_include_test.rb +130 -0
- data/test/cases/associations/eager_singularization_test.rb +145 -0
- data/test/cases/associations/eager_test.rb +834 -0
- data/test/cases/associations/extension_test.rb +62 -0
- data/test/cases/associations/habtm_join_table_test.rb +56 -0
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +822 -0
- data/test/cases/associations/has_many_associations_test.rb +1134 -0
- data/test/cases/associations/has_many_through_associations_test.rb +346 -0
- data/test/cases/associations/has_one_associations_test.rb +330 -0
- data/test/cases/associations/has_one_through_associations_test.rb +209 -0
- data/test/cases/associations/inner_join_association_test.rb +93 -0
- data/test/cases/associations/join_model_test.rb +712 -0
- data/test/cases/associations_test.rb +262 -0
- data/test/cases/attribute_methods_test.rb +305 -0
- data/test/cases/autosave_association_test.rb +1142 -0
- data/test/cases/base_test.rb +2154 -0
- data/test/cases/batches_test.rb +61 -0
- data/test/cases/binary_test.rb +30 -0
- data/test/cases/calculations_test.rb +348 -0
- data/test/cases/callbacks_observers_test.rb +38 -0
- data/test/cases/callbacks_test.rb +438 -0
- data/test/cases/class_inheritable_attributes_test.rb +32 -0
- data/test/cases/column_alias_test.rb +17 -0
- data/test/cases/column_definition_test.rb +70 -0
- data/test/cases/connection_pool_test.rb +25 -0
- data/test/cases/connection_test_firebird.rb +8 -0
- data/test/cases/connection_test_mysql.rb +64 -0
- data/test/cases/copy_table_test_sqlite.rb +80 -0
- data/test/cases/database_statements_test.rb +12 -0
- data/test/cases/datatype_test_postgresql.rb +204 -0
- data/test/cases/date_time_test.rb +37 -0
- data/test/cases/default_test_firebird.rb +16 -0
- data/test/cases/defaults_test.rb +111 -0
- data/test/cases/deprecated_finder_test.rb +30 -0
- data/test/cases/dirty_test.rb +316 -0
- data/test/cases/finder_respond_to_test.rb +76 -0
- data/test/cases/finder_test.rb +1066 -0
- data/test/cases/fixtures_test.rb +656 -0
- data/test/cases/helper.rb +68 -0
- data/test/cases/i18n_test.rb +46 -0
- data/test/cases/inheritance_test.rb +262 -0
- data/test/cases/invalid_date_test.rb +24 -0
- data/test/cases/json_serialization_test.rb +205 -0
- data/test/cases/lifecycle_test.rb +193 -0
- data/test/cases/locking_test.rb +304 -0
- data/test/cases/method_scoping_test.rb +704 -0
- data/test/cases/migration_test.rb +1523 -0
- data/test/cases/migration_test_firebird.rb +124 -0
- data/test/cases/mixin_test.rb +96 -0
- data/test/cases/modules_test.rb +81 -0
- data/test/cases/multiple_db_test.rb +85 -0
- data/test/cases/named_scope_test.rb +361 -0
- data/test/cases/nested_attributes_test.rb +581 -0
- data/test/cases/pk_test.rb +119 -0
- data/test/cases/pooled_connections_test.rb +103 -0
- data/test/cases/query_cache_test.rb +123 -0
- data/test/cases/readonly_test.rb +107 -0
- data/test/cases/reflection_test.rb +194 -0
- data/test/cases/reload_models_test.rb +22 -0
- data/test/cases/repair_helper.rb +50 -0
- data/test/cases/reserved_word_test_mysql.rb +176 -0
- data/test/cases/sanitize_test.rb +25 -0
- data/test/cases/schema_authorization_test_postgresql.rb +75 -0
- data/test/cases/schema_dumper_test.rb +211 -0
- data/test/cases/schema_test_postgresql.rb +178 -0
- data/test/cases/serialization_test.rb +47 -0
- data/test/cases/synonym_test_oracle.rb +17 -0
- data/test/cases/timestamp_test.rb +75 -0
- data/test/cases/transactions_test.rb +522 -0
- data/test/cases/unconnected_test.rb +32 -0
- data/test/cases/validations_i18n_test.rb +955 -0
- data/test/cases/validations_test.rb +1640 -0
- data/test/cases/xml_serialization_test.rb +240 -0
- data/test/config.rb +5 -0
- data/test/connections/jdbc_jdbcderby/connection.rb +18 -0
- data/test/connections/jdbc_jdbch2/connection.rb +18 -0
- data/test/connections/jdbc_jdbchsqldb/connection.rb +18 -0
- data/test/connections/jdbc_jdbcmysql/connection.rb +26 -0
- data/test/connections/jdbc_jdbcpostgresql/connection.rb +26 -0
- data/test/connections/jdbc_jdbcsqlite3/connection.rb +25 -0
- data/test/connections/native_db2/connection.rb +25 -0
- data/test/connections/native_firebird/connection.rb +26 -0
- data/test/connections/native_frontbase/connection.rb +27 -0
- data/test/connections/native_mysql/connection.rb +25 -0
- data/test/connections/native_openbase/connection.rb +21 -0
- data/test/connections/native_oracle/connection.rb +27 -0
- data/test/connections/native_postgresql/connection.rb +25 -0
- data/test/connections/native_sqlite/connection.rb +25 -0
- data/test/connections/native_sqlite3/connection.rb +25 -0
- data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
- data/test/connections/native_sybase/connection.rb +23 -0
- data/test/fixtures/accounts.yml +29 -0
- 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 +5 -0
- data/test/fixtures/author_favorites.yml +4 -0
- data/test/fixtures/authors.yml +9 -0
- data/test/fixtures/binaries.yml +132 -0
- data/test/fixtures/books.yml +7 -0
- data/test/fixtures/categories/special_categories.yml +9 -0
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/fixtures/categories.yml +14 -0
- data/test/fixtures/categories_ordered.yml +7 -0
- data/test/fixtures/categories_posts.yml +23 -0
- data/test/fixtures/categorizations.yml +17 -0
- data/test/fixtures/clubs.yml +6 -0
- data/test/fixtures/comments.yml +59 -0
- data/test/fixtures/companies.yml +56 -0
- data/test/fixtures/computers.yml +4 -0
- data/test/fixtures/courses.yml +7 -0
- data/test/fixtures/customers.yml +26 -0
- data/test/fixtures/developers.yml +21 -0
- data/test/fixtures/developers_projects.yml +17 -0
- data/test/fixtures/edges.yml +6 -0
- data/test/fixtures/entrants.yml +14 -0
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/fixtures/fk_test_has_fk.yml +3 -0
- data/test/fixtures/fk_test_has_pk.yml +2 -0
- data/test/fixtures/funny_jokes.yml +10 -0
- data/test/fixtures/items.yml +4 -0
- data/test/fixtures/jobs.yml +7 -0
- data/test/fixtures/legacy_things.yml +3 -0
- data/test/fixtures/mateys.yml +4 -0
- data/test/fixtures/member_types.yml +6 -0
- data/test/fixtures/members.yml +6 -0
- data/test/fixtures/memberships.yml +20 -0
- data/test/fixtures/minimalistics.yml +2 -0
- data/test/fixtures/mixed_case_monkeys.yml +6 -0
- data/test/fixtures/mixins.yml +29 -0
- data/test/fixtures/movies.yml +7 -0
- data/test/fixtures/naked/csv/accounts.csv +1 -0
- data/test/fixtures/naked/yml/accounts.yml +1 -0
- data/test/fixtures/naked/yml/companies.yml +1 -0
- data/test/fixtures/naked/yml/courses.yml +1 -0
- data/test/fixtures/organizations.yml +5 -0
- data/test/fixtures/owners.yml +7 -0
- data/test/fixtures/parrots.yml +27 -0
- data/test/fixtures/parrots_pirates.yml +7 -0
- data/test/fixtures/people.yml +15 -0
- data/test/fixtures/pets.yml +14 -0
- data/test/fixtures/pirates.yml +9 -0
- data/test/fixtures/posts.yml +52 -0
- data/test/fixtures/price_estimates.yml +7 -0
- data/test/fixtures/projects.yml +7 -0
- data/test/fixtures/readers.yml +9 -0
- data/test/fixtures/references.yml +17 -0
- data/test/fixtures/reserved_words/distinct.yml +5 -0
- data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
- data/test/fixtures/reserved_words/group.yml +14 -0
- data/test/fixtures/reserved_words/select.yml +8 -0
- data/test/fixtures/reserved_words/values.yml +7 -0
- data/test/fixtures/ships.yml +5 -0
- data/test/fixtures/sponsors.yml +9 -0
- data/test/fixtures/subscribers.yml +7 -0
- data/test/fixtures/subscriptions.yml +12 -0
- data/test/fixtures/taggings.yml +28 -0
- data/test/fixtures/tags.yml +7 -0
- data/test/fixtures/tasks.yml +7 -0
- data/test/fixtures/topics.yml +42 -0
- data/test/fixtures/toys.yml +4 -0
- data/test/fixtures/treasures.yml +10 -0
- data/test/fixtures/vertices.yml +4 -0
- data/test/fixtures/warehouse-things.yml +3 -0
- data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
- data/test/migrations/decimal/1_give_me_big_numbers.rb +15 -0
- data/test/migrations/duplicate/1_people_have_last_names.rb +9 -0
- data/test/migrations/duplicate/2_we_need_reminders.rb +12 -0
- data/test/migrations/duplicate/3_foo.rb +7 -0
- data/test/migrations/duplicate/3_innocent_jointable.rb +12 -0
- data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
- data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
- data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +12 -0
- data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +9 -0
- data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +12 -0
- data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +9 -0
- data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
- data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
- data/test/migrations/missing/1000_people_have_middle_names.rb +9 -0
- data/test/migrations/missing/1_people_have_last_names.rb +9 -0
- data/test/migrations/missing/3_we_need_reminders.rb +12 -0
- data/test/migrations/missing/4_innocent_jointable.rb +12 -0
- data/test/migrations/valid/1_people_have_last_names.rb +9 -0
- data/test/migrations/valid/2_we_need_reminders.rb +12 -0
- data/test/migrations/valid/3_innocent_jointable.rb +12 -0
- data/test/models/author.rb +146 -0
- data/test/models/auto_id.rb +4 -0
- data/test/models/binary.rb +2 -0
- data/test/models/bird.rb +3 -0
- data/test/models/book.rb +4 -0
- data/test/models/categorization.rb +5 -0
- data/test/models/category.rb +34 -0
- data/test/models/citation.rb +6 -0
- data/test/models/club.rb +13 -0
- data/test/models/column_name.rb +3 -0
- data/test/models/comment.rb +29 -0
- data/test/models/company.rb +171 -0
- data/test/models/company_in_module.rb +61 -0
- data/test/models/computer.rb +3 -0
- data/test/models/contact.rb +16 -0
- data/test/models/contract.rb +5 -0
- data/test/models/course.rb +3 -0
- data/test/models/customer.rb +73 -0
- data/test/models/default.rb +2 -0
- data/test/models/developer.rb +101 -0
- data/test/models/edge.rb +5 -0
- data/test/models/entrant.rb +3 -0
- data/test/models/essay.rb +3 -0
- data/test/models/event.rb +3 -0
- data/test/models/guid.rb +2 -0
- data/test/models/item.rb +7 -0
- data/test/models/job.rb +5 -0
- data/test/models/joke.rb +3 -0
- data/test/models/keyboard.rb +3 -0
- data/test/models/legacy_thing.rb +3 -0
- data/test/models/matey.rb +4 -0
- data/test/models/member.rb +12 -0
- data/test/models/member_detail.rb +5 -0
- data/test/models/member_type.rb +3 -0
- data/test/models/membership.rb +9 -0
- data/test/models/minimalistic.rb +2 -0
- data/test/models/mixed_case_monkey.rb +3 -0
- data/test/models/movie.rb +5 -0
- data/test/models/order.rb +4 -0
- data/test/models/organization.rb +6 -0
- data/test/models/owner.rb +5 -0
- data/test/models/parrot.rb +16 -0
- data/test/models/person.rb +16 -0
- data/test/models/pet.rb +5 -0
- data/test/models/pirate.rb +70 -0
- data/test/models/post.rb +100 -0
- data/test/models/price_estimate.rb +3 -0
- data/test/models/project.rb +30 -0
- data/test/models/reader.rb +4 -0
- data/test/models/reference.rb +4 -0
- data/test/models/reply.rb +46 -0
- data/test/models/ship.rb +10 -0
- data/test/models/ship_part.rb +5 -0
- data/test/models/sponsor.rb +4 -0
- data/test/models/subject.rb +4 -0
- data/test/models/subscriber.rb +8 -0
- data/test/models/subscription.rb +4 -0
- data/test/models/tag.rb +7 -0
- data/test/models/tagging.rb +10 -0
- data/test/models/task.rb +3 -0
- data/test/models/topic.rb +80 -0
- data/test/models/toy.rb +6 -0
- data/test/models/treasure.rb +8 -0
- data/test/models/vertex.rb +9 -0
- data/test/models/warehouse_thing.rb +5 -0
- data/test/schema/mysql_specific_schema.rb +24 -0
- data/test/schema/postgresql_specific_schema.rb +114 -0
- data/test/schema/schema.rb +493 -0
- data/test/schema/schema2.rb +6 -0
- data/test/schema/sqlite_specific_schema.rb +25 -0
- metadata +420 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'active_record/connection_adapters/sqlite_adapter'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
class Base
|
|
5
|
+
# sqlite3 adapter reuses sqlite_connection.
|
|
6
|
+
def self.sqlite3_connection(config) # :nodoc:
|
|
7
|
+
parse_sqlite_config!(config)
|
|
8
|
+
|
|
9
|
+
unless self.class.const_defined?(:SQLite3)
|
|
10
|
+
require_library_or_gem(config[:adapter])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
db = SQLite3::Database.new(
|
|
14
|
+
config[:database],
|
|
15
|
+
:results_as_hash => true,
|
|
16
|
+
:type_translation => false
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
|
|
20
|
+
|
|
21
|
+
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module ConnectionAdapters #:nodoc:
|
|
26
|
+
class SQLite3Adapter < SQLiteAdapter # :nodoc:
|
|
27
|
+
def table_structure(table_name)
|
|
28
|
+
returning structure = @connection.table_info(quote_table_name(table_name)) do
|
|
29
|
+
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# encoding: binary
|
|
2
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
|
3
|
+
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
|
8
|
+
def sqlite_connection(config) # :nodoc:
|
|
9
|
+
parse_sqlite_config!(config)
|
|
10
|
+
|
|
11
|
+
unless self.class.const_defined?(:SQLite)
|
|
12
|
+
require_library_or_gem(config[:adapter])
|
|
13
|
+
|
|
14
|
+
db = SQLite::Database.new(config[:database], 0)
|
|
15
|
+
db.show_datatypes = "ON" if !defined? SQLite::Version
|
|
16
|
+
db.results_as_hash = true if defined? SQLite::Version
|
|
17
|
+
db.type_translation = false
|
|
18
|
+
|
|
19
|
+
message = "Support for SQLite2Adapter and DeprecatedSQLiteAdapter has been removed from Rails 3. "
|
|
20
|
+
message << "You should migrate to SQLite 3+ or use the plugin from git://github.com/rails/sqlite2_adapter.git with Rails 3."
|
|
21
|
+
ActiveSupport::Deprecation.warn(message)
|
|
22
|
+
|
|
23
|
+
# "Downgrade" deprecated sqlite API
|
|
24
|
+
if SQLite.const_defined?(:Version)
|
|
25
|
+
ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
|
|
26
|
+
else
|
|
27
|
+
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def parse_sqlite_config!(config)
|
|
34
|
+
if config.include?(:dbfile)
|
|
35
|
+
ActiveSupport::Deprecation.warn "Please update config/database.yml to use 'database' instead of 'dbfile'"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
config[:database] ||= config[:dbfile]
|
|
39
|
+
# Require database.
|
|
40
|
+
unless config[:database]
|
|
41
|
+
raise ArgumentError, "No database file specified. Missing argument: database"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Allow database path relative to RAILS_ROOT, but only if
|
|
45
|
+
# the database path is not the special path that tells
|
|
46
|
+
# Sqlite to build a database only in memory.
|
|
47
|
+
if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database]
|
|
48
|
+
config[:database] = File.expand_path(config[:database], RAILS_ROOT)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module ConnectionAdapters #:nodoc:
|
|
55
|
+
class SQLiteColumn < Column #:nodoc:
|
|
56
|
+
class << self
|
|
57
|
+
def string_to_binary(value)
|
|
58
|
+
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
|
59
|
+
value.gsub(/\0|\%/n) do |b|
|
|
60
|
+
case b
|
|
61
|
+
when "\0" then "%00"
|
|
62
|
+
when "%" then "%25"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def binary_to_string(value)
|
|
68
|
+
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
|
69
|
+
value.gsub(/%00|%25/n) do |b|
|
|
70
|
+
case b
|
|
71
|
+
when "%00" then "\0"
|
|
72
|
+
when "%25" then "%"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and
|
|
80
|
+
# from http://rubyforge.org/projects/sqlite-ruby/).
|
|
81
|
+
#
|
|
82
|
+
# Options:
|
|
83
|
+
#
|
|
84
|
+
# * <tt>:database</tt> - Path to the database file.
|
|
85
|
+
class SQLiteAdapter < AbstractAdapter
|
|
86
|
+
class Version
|
|
87
|
+
include Comparable
|
|
88
|
+
|
|
89
|
+
def initialize(version_string)
|
|
90
|
+
@version = version_string.split('.').map(&:to_i)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def <=>(version_string)
|
|
94
|
+
@version <=> version_string.split('.').map(&:to_i)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def initialize(connection, logger, config)
|
|
99
|
+
super(connection, logger)
|
|
100
|
+
@config = config
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def adapter_name #:nodoc:
|
|
104
|
+
'SQLite'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def supports_ddl_transactions?
|
|
108
|
+
sqlite_version >= '2.0.0'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def supports_migrations? #:nodoc:
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def supports_primary_key? #:nodoc:
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def requires_reloading?
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def supports_add_column?
|
|
124
|
+
sqlite_version >= '3.1.6'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def disconnect!
|
|
128
|
+
super
|
|
129
|
+
@connection.close rescue nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def supports_count_distinct? #:nodoc:
|
|
133
|
+
sqlite_version >= '3.2.6'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def supports_autoincrement? #:nodoc:
|
|
137
|
+
sqlite_version >= '3.1.0'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def native_database_types #:nodoc:
|
|
141
|
+
{
|
|
142
|
+
:primary_key => default_primary_key_type,
|
|
143
|
+
:string => { :name => "varchar", :limit => 255 },
|
|
144
|
+
:text => { :name => "text" },
|
|
145
|
+
:integer => { :name => "integer" },
|
|
146
|
+
:float => { :name => "float" },
|
|
147
|
+
:decimal => { :name => "decimal" },
|
|
148
|
+
:datetime => { :name => "datetime" },
|
|
149
|
+
:timestamp => { :name => "datetime" },
|
|
150
|
+
:time => { :name => "time" },
|
|
151
|
+
:date => { :name => "date" },
|
|
152
|
+
:binary => { :name => "blob" },
|
|
153
|
+
:boolean => { :name => "boolean" }
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# QUOTING ==================================================
|
|
159
|
+
|
|
160
|
+
def quote_string(s) #:nodoc:
|
|
161
|
+
@connection.class.quote(s)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def quote_column_name(name) #:nodoc:
|
|
165
|
+
%Q("#{name}")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# DATABASE STATEMENTS ======================================
|
|
170
|
+
|
|
171
|
+
def execute(sql, name = nil) #:nodoc:
|
|
172
|
+
catch_schema_changes { log(sql, name) { @connection.execute(sql) } }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def update_sql(sql, name = nil) #:nodoc:
|
|
176
|
+
super
|
|
177
|
+
@connection.changes
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def delete_sql(sql, name = nil) #:nodoc:
|
|
181
|
+
sql += " WHERE 1=1" unless sql =~ /WHERE/i
|
|
182
|
+
super sql, name
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
|
186
|
+
super || @connection.last_insert_row_id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def select_rows(sql, name = nil)
|
|
190
|
+
execute(sql, name).map do |row|
|
|
191
|
+
(0...(row.size / 2)).map { |i| row[i] }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def begin_db_transaction #:nodoc:
|
|
196
|
+
catch_schema_changes { @connection.transaction }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def commit_db_transaction #:nodoc:
|
|
200
|
+
catch_schema_changes { @connection.commit }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def rollback_db_transaction #:nodoc:
|
|
204
|
+
catch_schema_changes { @connection.rollback }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# SELECT ... FOR UPDATE is redundant since the table is locked.
|
|
208
|
+
def add_lock!(sql, options) #:nodoc:
|
|
209
|
+
sql
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# SCHEMA STATEMENTS ========================================
|
|
214
|
+
|
|
215
|
+
def tables(name = nil) #:nodoc:
|
|
216
|
+
sql = <<-SQL
|
|
217
|
+
SELECT name
|
|
218
|
+
FROM sqlite_master
|
|
219
|
+
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
|
|
220
|
+
SQL
|
|
221
|
+
|
|
222
|
+
execute(sql, name).map do |row|
|
|
223
|
+
row[0]
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def columns(table_name, name = nil) #:nodoc:
|
|
228
|
+
table_structure(table_name).map do |field|
|
|
229
|
+
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def indexes(table_name, name = nil) #:nodoc:
|
|
234
|
+
execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
|
|
235
|
+
index = IndexDefinition.new(table_name, row['name'])
|
|
236
|
+
index.unique = row['unique'] != '0'
|
|
237
|
+
index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
|
|
238
|
+
index
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def primary_key(table_name) #:nodoc:
|
|
243
|
+
column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
|
|
244
|
+
column ? column['name'] : nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def remove_index(table_name, options={}) #:nodoc:
|
|
248
|
+
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def rename_table(name, new_name)
|
|
252
|
+
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# See: http://www.sqlite.org/lang_altertable.html
|
|
256
|
+
# SQLite has an additional restriction on the ALTER TABLE statement
|
|
257
|
+
def valid_alter_table_options( type, options)
|
|
258
|
+
type.to_sym != :primary_key
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def add_column(table_name, column_name, type, options = {}) #:nodoc:
|
|
262
|
+
if supports_add_column? && valid_alter_table_options( type, options )
|
|
263
|
+
super(table_name, column_name, type, options)
|
|
264
|
+
else
|
|
265
|
+
alter_table(table_name) do |definition|
|
|
266
|
+
definition.column(column_name, type, options)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def remove_column(table_name, *column_names) #:nodoc:
|
|
272
|
+
column_names.flatten.each do |column_name|
|
|
273
|
+
alter_table(table_name) do |definition|
|
|
274
|
+
definition.columns.delete(definition[column_name])
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
alias :remove_columns :remove_column
|
|
279
|
+
|
|
280
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
|
281
|
+
alter_table(table_name) do |definition|
|
|
282
|
+
definition[column_name].default = default
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
|
287
|
+
unless null || default.nil?
|
|
288
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
|
289
|
+
end
|
|
290
|
+
alter_table(table_name) do |definition|
|
|
291
|
+
definition[column_name].null = null
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
|
296
|
+
alter_table(table_name) do |definition|
|
|
297
|
+
include_default = options_include_default?(options)
|
|
298
|
+
definition[column_name].instance_eval do
|
|
299
|
+
self.type = type
|
|
300
|
+
self.limit = options[:limit] if options.include?(:limit)
|
|
301
|
+
self.default = options[:default] if include_default
|
|
302
|
+
self.null = options[:null] if options.include?(:null)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
|
308
|
+
unless columns(table_name).detect{|c| c.name == column_name.to_s }
|
|
309
|
+
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
|
|
310
|
+
end
|
|
311
|
+
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def empty_insert_statement(table_name)
|
|
315
|
+
"INSERT INTO #{table_name} VALUES(NULL)"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
protected
|
|
319
|
+
def select(sql, name = nil) #:nodoc:
|
|
320
|
+
execute(sql, name).map do |row|
|
|
321
|
+
record = {}
|
|
322
|
+
row.each_key do |key|
|
|
323
|
+
if key.is_a?(String)
|
|
324
|
+
record[key.sub(/^"?\w+"?\./, '')] = row[key]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
record
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def table_structure(table_name)
|
|
332
|
+
returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
|
|
333
|
+
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def alter_table(table_name, options = {}) #:nodoc:
|
|
338
|
+
altered_table_name = "altered_#{table_name}"
|
|
339
|
+
caller = lambda {|definition| yield definition if block_given?}
|
|
340
|
+
|
|
341
|
+
transaction do
|
|
342
|
+
move_table(table_name, altered_table_name,
|
|
343
|
+
options.merge(:temporary => true))
|
|
344
|
+
move_table(altered_table_name, table_name, &caller)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def move_table(from, to, options = {}, &block) #:nodoc:
|
|
349
|
+
copy_table(from, to, options, &block)
|
|
350
|
+
drop_table(from)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def copy_table(from, to, options = {}) #:nodoc:
|
|
354
|
+
options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
|
|
355
|
+
create_table(to, options) do |definition|
|
|
356
|
+
@definition = definition
|
|
357
|
+
columns(from).each do |column|
|
|
358
|
+
column_name = options[:rename] ?
|
|
359
|
+
(options[:rename][column.name] ||
|
|
360
|
+
options[:rename][column.name.to_sym] ||
|
|
361
|
+
column.name) : column.name
|
|
362
|
+
|
|
363
|
+
@definition.column(column_name, column.type,
|
|
364
|
+
:limit => column.limit, :default => column.default,
|
|
365
|
+
:null => column.null)
|
|
366
|
+
end
|
|
367
|
+
@definition.primary_key(primary_key(from)) if primary_key(from)
|
|
368
|
+
yield @definition if block_given?
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
copy_table_indexes(from, to, options[:rename] || {})
|
|
372
|
+
copy_table_contents(from, to,
|
|
373
|
+
@definition.columns.map {|column| column.name},
|
|
374
|
+
options[:rename] || {})
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def copy_table_indexes(from, to, rename = {}) #:nodoc:
|
|
378
|
+
indexes(from).each do |index|
|
|
379
|
+
name = index.name
|
|
380
|
+
if to == "altered_#{from}"
|
|
381
|
+
name = "temp_#{name}"
|
|
382
|
+
elsif from == "altered_#{to}"
|
|
383
|
+
name = name[5..-1]
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
to_column_names = columns(to).map(&:name)
|
|
387
|
+
columns = index.columns.map {|c| rename[c] || c }.select do |column|
|
|
388
|
+
to_column_names.include?(column)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
unless columns.empty?
|
|
392
|
+
# index name can't be the same
|
|
393
|
+
opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
|
|
394
|
+
opts[:unique] = true if index.unique
|
|
395
|
+
add_index(to, columns, opts)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
|
|
401
|
+
column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
|
|
402
|
+
rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
|
|
403
|
+
from_columns = columns(from).collect {|col| col.name}
|
|
404
|
+
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
|
|
405
|
+
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
|
|
406
|
+
|
|
407
|
+
quoted_to = quote_table_name(to)
|
|
408
|
+
@connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
|
|
409
|
+
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
|
|
410
|
+
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
|
|
411
|
+
sql << ')'
|
|
412
|
+
@connection.execute sql
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def catch_schema_changes
|
|
417
|
+
return yield
|
|
418
|
+
rescue ActiveRecord::StatementInvalid => exception
|
|
419
|
+
if exception.message =~ /database schema has changed/
|
|
420
|
+
reconnect!
|
|
421
|
+
retry
|
|
422
|
+
else
|
|
423
|
+
raise
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def sqlite_version
|
|
428
|
+
@sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def default_primary_key_type
|
|
432
|
+
if supports_autoincrement?
|
|
433
|
+
'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
|
|
434
|
+
else
|
|
435
|
+
'INTEGER PRIMARY KEY NOT NULL'.freeze
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
class SQLite2Adapter < SQLiteAdapter # :nodoc:
|
|
441
|
+
def rename_table(name, new_name)
|
|
442
|
+
move_table(name, new_name)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
|
|
447
|
+
def insert(sql, name = nil, pk = nil, id_value = nil)
|
|
448
|
+
execute(sql, name = nil)
|
|
449
|
+
id_value || @connection.last_insert_rowid
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
# Track unsaved attribute changes.
|
|
3
|
+
#
|
|
4
|
+
# A newly instantiated object is unchanged:
|
|
5
|
+
# person = Person.find_by_name('uncle bob')
|
|
6
|
+
# person.changed? # => false
|
|
7
|
+
#
|
|
8
|
+
# Change the name:
|
|
9
|
+
# person.name = 'Bob'
|
|
10
|
+
# person.changed? # => true
|
|
11
|
+
# person.name_changed? # => true
|
|
12
|
+
# person.name_was # => 'uncle bob'
|
|
13
|
+
# person.name_change # => ['uncle bob', 'Bob']
|
|
14
|
+
# person.name = 'Bill'
|
|
15
|
+
# person.name_change # => ['uncle bob', 'Bill']
|
|
16
|
+
#
|
|
17
|
+
# Save the changes:
|
|
18
|
+
# person.save
|
|
19
|
+
# person.changed? # => false
|
|
20
|
+
# person.name_changed? # => false
|
|
21
|
+
#
|
|
22
|
+
# Assigning the same value leaves the attribute unchanged:
|
|
23
|
+
# person.name = 'Bill'
|
|
24
|
+
# person.name_changed? # => false
|
|
25
|
+
# person.name_change # => nil
|
|
26
|
+
#
|
|
27
|
+
# Which attributes have changed?
|
|
28
|
+
# person.name = 'bob'
|
|
29
|
+
# person.changed # => ['name']
|
|
30
|
+
# person.changes # => { 'name' => ['Bill', 'bob'] }
|
|
31
|
+
#
|
|
32
|
+
# Before modifying an attribute in-place:
|
|
33
|
+
# person.name_will_change!
|
|
34
|
+
# person.name << 'by'
|
|
35
|
+
# person.name_change # => ['uncle bob', 'uncle bobby']
|
|
36
|
+
module Dirty
|
|
37
|
+
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
|
38
|
+
|
|
39
|
+
def self.included(base)
|
|
40
|
+
base.attribute_method_suffix *DIRTY_SUFFIXES
|
|
41
|
+
base.alias_method_chain :write_attribute, :dirty
|
|
42
|
+
base.alias_method_chain :save, :dirty
|
|
43
|
+
base.alias_method_chain :save!, :dirty
|
|
44
|
+
base.alias_method_chain :update, :dirty
|
|
45
|
+
base.alias_method_chain :reload, :dirty
|
|
46
|
+
|
|
47
|
+
base.superclass_delegating_accessor :partial_updates
|
|
48
|
+
base.partial_updates = true
|
|
49
|
+
|
|
50
|
+
base.send(:extend, ClassMethods)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Do any attributes have unsaved changes?
|
|
54
|
+
# person.changed? # => false
|
|
55
|
+
# person.name = 'bob'
|
|
56
|
+
# person.changed? # => true
|
|
57
|
+
def changed?
|
|
58
|
+
!changed_attributes.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# List of attributes with unsaved changes.
|
|
62
|
+
# person.changed # => []
|
|
63
|
+
# person.name = 'bob'
|
|
64
|
+
# person.changed # => ['name']
|
|
65
|
+
def changed
|
|
66
|
+
changed_attributes.keys
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Map of changed attrs => [original value, new value].
|
|
70
|
+
# person.changes # => {}
|
|
71
|
+
# person.name = 'bob'
|
|
72
|
+
# person.changes # => { 'name' => ['bill', 'bob'] }
|
|
73
|
+
def changes
|
|
74
|
+
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Attempts to +save+ the record and clears changed attributes if successful.
|
|
78
|
+
def save_with_dirty(*args) #:nodoc:
|
|
79
|
+
if status = save_without_dirty(*args)
|
|
80
|
+
changed_attributes.clear
|
|
81
|
+
end
|
|
82
|
+
status
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
|
86
|
+
def save_with_dirty!(*args) #:nodoc:
|
|
87
|
+
status = save_without_dirty!(*args)
|
|
88
|
+
changed_attributes.clear
|
|
89
|
+
status
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# <tt>reload</tt> the record and clears changed attributes.
|
|
93
|
+
def reload_with_dirty(*args) #:nodoc:
|
|
94
|
+
record = reload_without_dirty(*args)
|
|
95
|
+
changed_attributes.clear
|
|
96
|
+
record
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
# Map of change <tt>attr => original value</tt>.
|
|
101
|
+
def changed_attributes
|
|
102
|
+
@changed_attributes ||= {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Handle <tt>*_changed?</tt> for +method_missing+.
|
|
106
|
+
def attribute_changed?(attr)
|
|
107
|
+
changed_attributes.include?(attr)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Handle <tt>*_change</tt> for +method_missing+.
|
|
111
|
+
def attribute_change(attr)
|
|
112
|
+
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Handle <tt>*_was</tt> for +method_missing+.
|
|
116
|
+
def attribute_was(attr)
|
|
117
|
+
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
|
121
|
+
def attribute_will_change!(attr)
|
|
122
|
+
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Wrap write_attribute to remember original attribute value.
|
|
126
|
+
def write_attribute_with_dirty(attr, value)
|
|
127
|
+
attr = attr.to_s
|
|
128
|
+
|
|
129
|
+
# The attribute already has an unsaved change.
|
|
130
|
+
if changed_attributes.include?(attr)
|
|
131
|
+
old = changed_attributes[attr]
|
|
132
|
+
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
|
133
|
+
else
|
|
134
|
+
old = clone_attribute_value(:read_attribute, attr)
|
|
135
|
+
changed_attributes[attr] = old if field_changed?(attr, old, value)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Carry on.
|
|
139
|
+
write_attribute_without_dirty(attr, value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def update_with_dirty
|
|
143
|
+
if partial_updates?
|
|
144
|
+
# Serialized attributes should always be written in case they've been
|
|
145
|
+
# changed in place.
|
|
146
|
+
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
|
|
147
|
+
else
|
|
148
|
+
update_without_dirty
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def field_changed?(attr, old, value)
|
|
153
|
+
if column = column_for_attribute(attr)
|
|
154
|
+
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
|
155
|
+
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
|
156
|
+
# Hence we don't record it as a change if the value changes from nil to ''.
|
|
157
|
+
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
|
158
|
+
# be typecast back to 0 (''.to_i => 0)
|
|
159
|
+
value = nil
|
|
160
|
+
else
|
|
161
|
+
value = column.type_cast(value)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
old != value
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
module ClassMethods
|
|
169
|
+
def self.extended(base)
|
|
170
|
+
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def alias_attribute_with_dirty(new_name, old_name)
|
|
174
|
+
alias_attribute_without_dirty(new_name, old_name)
|
|
175
|
+
DIRTY_SUFFIXES.each do |suffix|
|
|
176
|
+
module_eval <<-STR, __FILE__, __LINE__+1
|
|
177
|
+
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
|
178
|
+
STR
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
class DynamicFinderMatch
|
|
3
|
+
def self.match(method)
|
|
4
|
+
df_match = self.new(method)
|
|
5
|
+
df_match.finder ? df_match : nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(method)
|
|
9
|
+
@finder = :first
|
|
10
|
+
case method.to_s
|
|
11
|
+
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
|
12
|
+
@finder = :last if $1 == 'last_by'
|
|
13
|
+
@finder = :all if $1 == 'all_by'
|
|
14
|
+
names = $2
|
|
15
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
|
16
|
+
@bang = true
|
|
17
|
+
names = $1
|
|
18
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
|
19
|
+
@instantiator = $1 == 'initialize' ? :new : :create
|
|
20
|
+
names = $2
|
|
21
|
+
else
|
|
22
|
+
@finder = nil
|
|
23
|
+
end
|
|
24
|
+
@attribute_names = names && names.split('_and_')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :finder, :attribute_names, :instantiator
|
|
28
|
+
|
|
29
|
+
def finder?
|
|
30
|
+
!@finder.nil? && @instantiator.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def instantiator?
|
|
34
|
+
@finder == :first && !@instantiator.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bang?
|
|
38
|
+
@bang
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|