activerecord 2.0.5 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (289) hide show
  1. data/CHANGELOG +168 -6
  2. data/README +27 -22
  3. data/RUNNING_UNIT_TESTS +7 -4
  4. data/Rakefile +22 -25
  5. data/lib/active_record.rb +8 -2
  6. data/lib/active_record/aggregations.rb +21 -12
  7. data/lib/active_record/association_preload.rb +277 -0
  8. data/lib/active_record/associations.rb +481 -295
  9. data/lib/active_record/associations/association_collection.rb +162 -37
  10. data/lib/active_record/associations/association_proxy.rb +71 -7
  11. data/lib/active_record/associations/belongs_to_association.rb +5 -3
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +5 -6
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -64
  14. data/lib/active_record/associations/has_many_association.rb +8 -73
  15. data/lib/active_record/associations/has_many_through_association.rb +68 -117
  16. data/lib/active_record/associations/has_one_association.rb +7 -5
  17. data/lib/active_record/associations/has_one_through_association.rb +28 -0
  18. data/lib/active_record/attribute_methods.rb +69 -19
  19. data/lib/active_record/base.rb +496 -275
  20. data/lib/active_record/calculations.rb +28 -21
  21. data/lib/active_record/callbacks.rb +9 -38
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -2
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +2 -2
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +232 -45
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +141 -27
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +9 -13
  28. data/lib/active_record/connection_adapters/mysql_adapter.rb +57 -24
  29. data/lib/active_record/connection_adapters/postgresql_adapter.rb +143 -42
  30. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  31. data/lib/active_record/connection_adapters/sqlite_adapter.rb +18 -10
  32. data/lib/active_record/dirty.rb +158 -0
  33. data/lib/active_record/fixtures.rb +121 -156
  34. data/lib/active_record/locking/optimistic.rb +14 -11
  35. data/lib/active_record/locking/pessimistic.rb +2 -2
  36. data/lib/active_record/migration.rb +157 -77
  37. data/lib/active_record/named_scope.rb +163 -0
  38. data/lib/active_record/observer.rb +19 -5
  39. data/lib/active_record/reflection.rb +34 -14
  40. data/lib/active_record/schema.rb +7 -14
  41. data/lib/active_record/schema_dumper.rb +4 -4
  42. data/lib/active_record/serialization.rb +5 -5
  43. data/lib/active_record/serializers/json_serializer.rb +37 -28
  44. data/lib/active_record/serializers/xml_serializer.rb +52 -29
  45. data/lib/active_record/test_case.rb +36 -0
  46. data/lib/active_record/timestamp.rb +4 -4
  47. data/lib/active_record/transactions.rb +3 -3
  48. data/lib/active_record/validations.rb +182 -248
  49. data/lib/active_record/version.rb +2 -2
  50. data/test/{fixtures → assets}/example.log +0 -0
  51. data/test/{fixtures → assets}/flowers.jpg +0 -0
  52. data/test/cases/aaa_create_tables_test.rb +24 -0
  53. data/test/cases/active_schema_test_mysql.rb +95 -0
  54. data/test/cases/active_schema_test_postgresql.rb +24 -0
  55. data/test/{adapter_test.rb → cases/adapter_test.rb} +15 -14
  56. data/test/{adapter_test_sqlserver.rb → cases/adapter_test_sqlserver.rb} +95 -95
  57. data/test/{aggregations_test.rb → cases/aggregations_test.rb} +20 -20
  58. data/test/{ar_schema_test.rb → cases/ar_schema_test.rb} +6 -6
  59. data/test/cases/associations/belongs_to_associations_test.rb +412 -0
  60. data/test/{associations → cases/associations}/callbacks_test.rb +24 -10
  61. data/test/{associations → cases/associations}/cascaded_eager_loading_test.rb +18 -17
  62. data/test/cases/associations/eager_load_nested_include_test.rb +83 -0
  63. data/test/{associations → cases/associations}/eager_singularization_test.rb +5 -5
  64. data/test/{associations → cases/associations}/eager_test.rb +216 -51
  65. data/test/{associations → cases/associations}/extension_test.rb +8 -8
  66. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +684 -0
  67. data/test/cases/associations/has_many_associations_test.rb +932 -0
  68. data/test/cases/associations/has_many_through_associations_test.rb +190 -0
  69. data/test/cases/associations/has_one_associations_test.rb +323 -0
  70. data/test/cases/associations/has_one_through_associations_test.rb +74 -0
  71. data/test/{associations → cases/associations}/inner_join_association_test.rb +20 -20
  72. data/test/{associations → cases/associations}/join_model_test.rb +175 -35
  73. data/test/cases/associations_test.rb +262 -0
  74. data/test/{attribute_methods_test.rb → cases/attribute_methods_test.rb} +103 -11
  75. data/test/{base_test.rb → cases/base_test.rb} +338 -191
  76. data/test/{binary_test.rb → cases/binary_test.rb} +6 -4
  77. data/test/{calculations_test.rb → cases/calculations_test.rb} +35 -23
  78. data/test/{callbacks_test.rb → cases/callbacks_test.rb} +7 -7
  79. data/test/{class_inheritable_attributes_test.rb → cases/class_inheritable_attributes_test.rb} +3 -3
  80. data/test/{column_alias_test.rb → cases/column_alias_test.rb} +3 -3
  81. data/test/{connection_test_firebird.rb → cases/connection_test_firebird.rb} +2 -2
  82. data/test/{connection_test_mysql.rb → cases/connection_test_mysql.rb} +2 -2
  83. data/test/{copy_table_test_sqlite.rb → cases/copy_table_test_sqlite.rb} +13 -13
  84. data/test/{datatype_test_postgresql.rb → cases/datatype_test_postgresql.rb} +8 -8
  85. data/test/{date_time_test.rb → cases/date_time_test.rb} +5 -5
  86. data/test/{default_test_firebird.rb → cases/default_test_firebird.rb} +3 -3
  87. data/test/{defaults_test.rb → cases/defaults_test.rb} +8 -6
  88. data/test/{deprecated_finder_test.rb → cases/deprecated_finder_test.rb} +3 -3
  89. data/test/cases/dirty_test.rb +163 -0
  90. data/test/cases/finder_respond_to_test.rb +76 -0
  91. data/test/{finder_test.rb → cases/finder_test.rb} +266 -33
  92. data/test/{fixtures_test.rb → cases/fixtures_test.rb} +88 -72
  93. data/test/cases/helper.rb +47 -0
  94. data/test/{inheritance_test.rb → cases/inheritance_test.rb} +61 -17
  95. data/test/cases/invalid_date_test.rb +24 -0
  96. data/test/{json_serialization_test.rb → cases/json_serialization_test.rb} +36 -11
  97. data/test/{lifecycle_test.rb → cases/lifecycle_test.rb} +16 -13
  98. data/test/{locking_test.rb → cases/locking_test.rb} +17 -10
  99. data/test/{method_scoping_test.rb → cases/method_scoping_test.rb} +75 -39
  100. data/test/{migration_test.rb → cases/migration_test.rb} +420 -80
  101. data/test/{migration_test_firebird.rb → cases/migration_test_firebird.rb} +3 -3
  102. data/test/{mixin_test.rb → cases/mixin_test.rb} +7 -6
  103. data/test/{modules_test.rb → cases/modules_test.rb} +11 -6
  104. data/test/{multiple_db_test.rb → cases/multiple_db_test.rb} +5 -5
  105. data/test/cases/named_scope_test.rb +157 -0
  106. data/test/{pk_test.rb → cases/pk_test.rb} +10 -10
  107. data/test/{query_cache_test.rb → cases/query_cache_test.rb} +12 -10
  108. data/test/{readonly_test.rb → cases/readonly_test.rb} +11 -11
  109. data/test/{reflection_test.rb → cases/reflection_test.rb} +15 -14
  110. data/test/{reserved_word_test_mysql.rb → cases/reserved_word_test_mysql.rb} +4 -5
  111. data/test/{schema_authorization_test_postgresql.rb → cases/schema_authorization_test_postgresql.rb} +5 -5
  112. data/test/cases/schema_dumper_test.rb +138 -0
  113. data/test/cases/schema_test_postgresql.rb +102 -0
  114. data/test/{serialization_test.rb → cases/serialization_test.rb} +7 -7
  115. data/test/{synonym_test_oracle.rb → cases/synonym_test_oracle.rb} +5 -5
  116. data/test/{table_name_test_sqlserver.rb → cases/table_name_test_sqlserver.rb} +3 -3
  117. data/test/{threaded_connections_test.rb → cases/threaded_connections_test.rb} +7 -7
  118. data/test/{transactions_test.rb → cases/transactions_test.rb} +31 -5
  119. data/test/{unconnected_test.rb → cases/unconnected_test.rb} +2 -2
  120. data/test/{validations_test.rb → cases/validations_test.rb} +141 -39
  121. data/test/{xml_serialization_test.rb → cases/xml_serialization_test.rb} +12 -12
  122. data/test/config.rb +5 -0
  123. data/test/connections/native_db2/connection.rb +1 -1
  124. data/test/connections/native_firebird/connection.rb +1 -1
  125. data/test/connections/native_frontbase/connection.rb +1 -1
  126. data/test/connections/native_mysql/connection.rb +1 -1
  127. data/test/connections/native_openbase/connection.rb +1 -1
  128. data/test/connections/native_oracle/connection.rb +1 -1
  129. data/test/connections/native_postgresql/connection.rb +1 -3
  130. data/test/connections/native_sqlite/connection.rb +2 -2
  131. data/test/connections/native_sqlite3/connection.rb +2 -2
  132. data/test/connections/native_sqlite3/in_memory_connection.rb +3 -3
  133. data/test/connections/native_sybase/connection.rb +1 -1
  134. data/test/fixtures/author_addresses.yml +5 -0
  135. data/test/fixtures/authors.yml +2 -0
  136. data/test/fixtures/clubs.yml +6 -0
  137. data/test/fixtures/jobs.yml +7 -0
  138. data/test/fixtures/members.yml +4 -0
  139. data/test/fixtures/memberships.yml +20 -0
  140. data/test/fixtures/owners.yml +7 -0
  141. data/test/fixtures/people.yml +4 -1
  142. data/test/fixtures/pets.yml +14 -0
  143. data/test/fixtures/posts.yml +1 -0
  144. data/test/fixtures/price_estimates.yml +7 -0
  145. data/test/fixtures/readers.yml +5 -0
  146. data/test/fixtures/references.yml +17 -0
  147. data/test/fixtures/sponsors.yml +9 -0
  148. data/test/fixtures/subscribers.yml +7 -0
  149. data/test/fixtures/subscriptions.yml +12 -0
  150. data/test/fixtures/taggings.yml +4 -1
  151. data/test/fixtures/topics.yml +22 -2
  152. data/test/fixtures/warehouse-things.yml +3 -0
  153. data/test/{fixtures/migrations_with_decimal → migrations/decimal}/1_give_me_big_numbers.rb +0 -0
  154. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/1_people_have_last_names.rb +1 -1
  155. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/2_we_need_reminders.rb +1 -1
  156. data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/3_foo.rb +0 -0
  157. data/test/{fixtures/migrations → migrations/duplicate}/3_innocent_jointable.rb +0 -0
  158. data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
  159. data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
  160. data/test/{fixtures/migrations_with_duplicate → migrations/interleaved/pass_1}/3_innocent_jointable.rb +0 -0
  161. data/test/{fixtures/migrations → migrations/interleaved/pass_2}/1_people_have_last_names.rb +1 -1
  162. data/test/{fixtures/migrations_with_missing_versions/4_innocent_jointable.rb → migrations/interleaved/pass_2/3_innocent_jointable.rb} +0 -0
  163. data/test/{fixtures/migrations_with_missing_versions → migrations/interleaved/pass_3}/1_people_have_last_names.rb +1 -1
  164. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
  165. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
  166. data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/1000_people_have_middle_names.rb +1 -1
  167. data/test/migrations/missing/1_people_have_last_names.rb +9 -0
  168. data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/3_we_need_reminders.rb +1 -1
  169. data/test/migrations/missing/4_innocent_jointable.rb +12 -0
  170. data/test/migrations/valid/1_people_have_last_names.rb +9 -0
  171. data/test/{fixtures/migrations → migrations/valid}/2_we_need_reminders.rb +1 -1
  172. data/test/migrations/valid/3_innocent_jointable.rb +12 -0
  173. data/test/{fixtures → models}/author.rb +28 -4
  174. data/test/{fixtures → models}/auto_id.rb +0 -0
  175. data/test/{fixtures → models}/binary.rb +0 -0
  176. data/test/{fixtures → models}/book.rb +0 -0
  177. data/test/{fixtures → models}/categorization.rb +0 -0
  178. data/test/{fixtures → models}/category.rb +8 -5
  179. data/test/{fixtures → models}/citation.rb +0 -0
  180. data/test/models/club.rb +7 -0
  181. data/test/{fixtures → models}/column_name.rb +0 -0
  182. data/test/{fixtures → models}/comment.rb +5 -3
  183. data/test/{fixtures → models}/company.rb +15 -6
  184. data/test/{fixtures → models}/company_in_module.rb +5 -3
  185. data/test/{fixtures → models}/computer.rb +0 -1
  186. data/test/{fixtures → models}/contact.rb +1 -1
  187. data/test/{fixtures → models}/course.rb +0 -0
  188. data/test/{fixtures → models}/customer.rb +8 -8
  189. data/test/{fixtures → models}/default.rb +0 -0
  190. data/test/{fixtures → models}/developer.rb +14 -10
  191. data/test/{fixtures → models}/edge.rb +0 -0
  192. data/test/{fixtures → models}/entrant.rb +0 -0
  193. data/test/models/guid.rb +2 -0
  194. data/test/{fixtures → models}/item.rb +0 -0
  195. data/test/models/job.rb +5 -0
  196. data/test/{fixtures → models}/joke.rb +0 -0
  197. data/test/{fixtures → models}/keyboard.rb +0 -0
  198. data/test/{fixtures → models}/legacy_thing.rb +0 -0
  199. data/test/{fixtures → models}/matey.rb +0 -0
  200. data/test/models/member.rb +9 -0
  201. data/test/models/membership.rb +9 -0
  202. data/test/{fixtures → models}/minimalistic.rb +0 -0
  203. data/test/{fixtures → models}/mixed_case_monkey.rb +0 -0
  204. data/test/{fixtures → models}/movie.rb +0 -0
  205. data/test/{fixtures → models}/order.rb +2 -2
  206. data/test/models/owner.rb +4 -0
  207. data/test/{fixtures → models}/parrot.rb +0 -0
  208. data/test/models/person.rb +10 -0
  209. data/test/models/pet.rb +4 -0
  210. data/test/models/pirate.rb +9 -0
  211. data/test/{fixtures → models}/post.rb +23 -2
  212. data/test/models/price_estimate.rb +3 -0
  213. data/test/{fixtures → models}/project.rb +1 -0
  214. data/test/{fixtures → models}/reader.rb +0 -0
  215. data/test/models/reference.rb +4 -0
  216. data/test/{fixtures → models}/reply.rb +7 -5
  217. data/test/{fixtures → models}/ship.rb +0 -0
  218. data/test/models/sponsor.rb +4 -0
  219. data/test/{fixtures → models}/subject.rb +0 -0
  220. data/test/{fixtures → models}/subscriber.rb +2 -0
  221. data/test/models/subscription.rb +4 -0
  222. data/test/{fixtures → models}/tag.rb +0 -0
  223. data/test/{fixtures → models}/tagging.rb +0 -0
  224. data/test/{fixtures → models}/task.rb +0 -0
  225. data/test/{fixtures → models}/topic.rb +32 -4
  226. data/test/{fixtures → models}/treasure.rb +2 -0
  227. data/test/{fixtures → models}/vertex.rb +0 -0
  228. data/test/models/warehouse_thing.rb +5 -0
  229. data/test/schema/mysql_specific_schema.rb +12 -0
  230. data/test/schema/postgresql_specific_schema.rb +103 -0
  231. data/test/schema/schema.rb +421 -0
  232. data/test/schema/schema2.rb +6 -0
  233. data/test/schema/sqlite_specific_schema.rb +25 -0
  234. data/test/schema/sqlserver_specific_schema.rb +5 -0
  235. metadata +192 -176
  236. data/test/aaa_create_tables_test.rb +0 -72
  237. data/test/abstract_unit.rb +0 -84
  238. data/test/active_schema_test_mysql.rb +0 -46
  239. data/test/all.sh +0 -8
  240. data/test/association_inheritance_reload.rb +0 -14
  241. data/test/associations_test.rb +0 -2177
  242. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +0 -1
  243. data/test/fixtures/bad_fixtures/attr_with_spaces +0 -1
  244. data/test/fixtures/bad_fixtures/blank_line +0 -3
  245. data/test/fixtures/bad_fixtures/duplicate_attributes +0 -3
  246. data/test/fixtures/bad_fixtures/missing_value +0 -1
  247. data/test/fixtures/db_definitions/db2.drop.sql +0 -33
  248. data/test/fixtures/db_definitions/db2.sql +0 -235
  249. data/test/fixtures/db_definitions/db22.drop.sql +0 -2
  250. data/test/fixtures/db_definitions/db22.sql +0 -5
  251. data/test/fixtures/db_definitions/firebird.drop.sql +0 -65
  252. data/test/fixtures/db_definitions/firebird.sql +0 -310
  253. data/test/fixtures/db_definitions/firebird2.drop.sql +0 -2
  254. data/test/fixtures/db_definitions/firebird2.sql +0 -6
  255. data/test/fixtures/db_definitions/frontbase.drop.sql +0 -33
  256. data/test/fixtures/db_definitions/frontbase.sql +0 -273
  257. data/test/fixtures/db_definitions/frontbase2.drop.sql +0 -1
  258. data/test/fixtures/db_definitions/frontbase2.sql +0 -4
  259. data/test/fixtures/db_definitions/openbase.drop.sql +0 -2
  260. data/test/fixtures/db_definitions/openbase.sql +0 -318
  261. data/test/fixtures/db_definitions/openbase2.drop.sql +0 -2
  262. data/test/fixtures/db_definitions/openbase2.sql +0 -7
  263. data/test/fixtures/db_definitions/oracle.drop.sql +0 -67
  264. data/test/fixtures/db_definitions/oracle.sql +0 -330
  265. data/test/fixtures/db_definitions/oracle2.drop.sql +0 -2
  266. data/test/fixtures/db_definitions/oracle2.sql +0 -6
  267. data/test/fixtures/db_definitions/postgresql.drop.sql +0 -44
  268. data/test/fixtures/db_definitions/postgresql.sql +0 -292
  269. data/test/fixtures/db_definitions/postgresql2.drop.sql +0 -2
  270. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  271. data/test/fixtures/db_definitions/schema.rb +0 -354
  272. data/test/fixtures/db_definitions/schema2.rb +0 -11
  273. data/test/fixtures/db_definitions/sqlite.drop.sql +0 -33
  274. data/test/fixtures/db_definitions/sqlite.sql +0 -219
  275. data/test/fixtures/db_definitions/sqlite2.drop.sql +0 -2
  276. data/test/fixtures/db_definitions/sqlite2.sql +0 -5
  277. data/test/fixtures/db_definitions/sybase.drop.sql +0 -35
  278. data/test/fixtures/db_definitions/sybase.sql +0 -222
  279. data/test/fixtures/db_definitions/sybase2.drop.sql +0 -4
  280. data/test/fixtures/db_definitions/sybase2.sql +0 -5
  281. data/test/fixtures/developers_projects/david_action_controller +0 -3
  282. data/test/fixtures/developers_projects/david_active_record +0 -3
  283. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  284. data/test/fixtures/person.rb +0 -4
  285. data/test/fixtures/pirate.rb +0 -5
  286. data/test/fixtures/subscribers/first +0 -2
  287. data/test/fixtures/subscribers/second +0 -2
  288. data/test/schema_dumper_test.rb +0 -131
  289. data/test/schema_test_postgresql.rb +0 -64
@@ -1,11 +1,25 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
2
 
3
+ begin
4
+ require_library_or_gem 'pg'
5
+ rescue LoadError => e
6
+ begin
7
+ require_library_or_gem 'postgres'
8
+ class PGresult
9
+ alias_method :nfields, :num_fields unless self.method_defined?(:nfields)
10
+ alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples)
11
+ alias_method :ftype, :type unless self.method_defined?(:ftype)
12
+ alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples)
13
+ end
14
+ rescue LoadError
15
+ raise e
16
+ end
17
+ end
18
+
3
19
  module ActiveRecord
4
20
  class Base
5
21
  # Establishes a connection to the database that's used by all Active Record objects
6
22
  def self.postgresql_connection(config) # :nodoc:
7
- require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
8
-
9
23
  config = config.symbolize_keys
10
24
  host = config[:host]
11
25
  port = config[:port] || 5432
@@ -214,15 +228,15 @@ module ActiveRecord
214
228
  #
215
229
  # Options:
216
230
  #
217
- # * <tt>:host</tt> -- Defaults to localhost
218
- # * <tt>:port</tt> -- Defaults to 5432
219
- # * <tt>:username</tt> -- Defaults to nothing
220
- # * <tt>:password</tt> -- Defaults to nothing
221
- # * <tt>:database</tt> -- The name of the database. No default, must be provided.
222
- # * <tt>:schema_search_path</tt> -- An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the :schema_order option.
223
- # * <tt>:encoding</tt> -- An optional client encoding that is used in a SET client_encoding TO <encoding> call on the connection.
224
- # * <tt>:min_messages</tt> -- An optional client min messages that is used in a SET client_min_messages TO <min_messages> call on the connection.
225
- # * <tt>:allow_concurrency</tt> -- If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
231
+ # * <tt>:host</tt> - Defaults to "localhost".
232
+ # * <tt>:port</tt> - Defaults to 5432.
233
+ # * <tt>:username</tt> - Defaults to nothing.
234
+ # * <tt>:password</tt> - Defaults to nothing.
235
+ # * <tt>:database</tt> - The name of the database. No default, must be provided.
236
+ # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
237
+ # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO <encoding></tt> call on the connection.
238
+ # * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
239
+ # * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
226
240
  class PostgreSQLAdapter < AbstractAdapter
227
241
  # Returns 'PostgreSQL' as adapter name for identification purposes.
228
242
  def adapter_name
@@ -300,7 +314,7 @@ module ActiveRecord
300
314
  # postgres-pr does not raise an exception when client_min_messages is set higher
301
315
  # than error and "SHOW standard_conforming_strings" fails, but returns an empty
302
316
  # PGresult instead.
303
- has_support = execute('SHOW standard_conforming_strings')[0][0] rescue false
317
+ has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
304
318
  self.client_min_messages = client_min_messages_old
305
319
  has_support
306
320
  end
@@ -346,7 +360,7 @@ module ActiveRecord
346
360
  # There are some incorrectly compiled postgres drivers out there
347
361
  # that don't define PGconn.escape.
348
362
  self.class.instance_eval do
349
- remove_method(:quote_string)
363
+ undef_method(:quote_string)
350
364
  end
351
365
  end
352
366
  quote_string(s)
@@ -369,11 +383,22 @@ module ActiveRecord
369
383
 
370
384
  # REFERENTIAL INTEGRITY ====================================
371
385
 
386
+ def supports_disable_referential_integrity?() #:nodoc:
387
+ version = query("SHOW server_version")[0][0].split('.')
388
+ (version[0].to_i >= 8 && version[1].to_i >= 1) ? true : false
389
+ rescue
390
+ return false
391
+ end
392
+
372
393
  def disable_referential_integrity(&block) #:nodoc:
373
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
394
+ if supports_disable_referential_integrity?() then
395
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
396
+ end
374
397
  yield
375
398
  ensure
376
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
399
+ if supports_disable_referential_integrity?() then
400
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
401
+ end
377
402
  end
378
403
 
379
404
  # DATABASE STATEMENTS ======================================
@@ -390,14 +415,28 @@ module ActiveRecord
390
415
  super || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk))
391
416
  end
392
417
 
393
- # Queries the database and returns the results in an Array or nil otherwise.
418
+ # create a 2D array representing the result set
419
+ def result_as_array(res) #:nodoc:
420
+ ary = []
421
+ for i in 0...res.ntuples do
422
+ ary << []
423
+ for j in 0...res.nfields do
424
+ ary[i] << res.getvalue(i,j)
425
+ end
426
+ end
427
+ return ary
428
+ end
429
+
430
+
431
+ # Queries the database and returns the results in an Array-like object
394
432
  def query(sql, name = nil) #:nodoc:
395
433
  log(sql, name) do
396
434
  if @async
397
- @connection.async_query(sql)
435
+ res = @connection.async_exec(sql)
398
436
  else
399
- @connection.query(sql)
437
+ res = @connection.exec(sql)
400
438
  end
439
+ return result_as_array(res)
401
440
  end
402
441
  end
403
442
 
@@ -415,7 +454,7 @@ module ActiveRecord
415
454
 
416
455
  # Executes an UPDATE query and returns the number of affected tuples.
417
456
  def update_sql(sql, name = nil)
418
- super.cmdtuples
457
+ super.cmd_tuples
419
458
  end
420
459
 
421
460
  # Begins a transaction.
@@ -435,6 +474,50 @@ module ActiveRecord
435
474
 
436
475
  # SCHEMA STATEMENTS ========================================
437
476
 
477
+ def recreate_database(name) #:nodoc:
478
+ drop_database(name)
479
+ create_database(name)
480
+ end
481
+
482
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
483
+ # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
484
+ # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
485
+ #
486
+ # Example:
487
+ # create_database config[:database], config
488
+ # create_database 'foo_development', :encoding => 'unicode'
489
+ def create_database(name, options = {})
490
+ options = options.reverse_merge(:encoding => "utf8")
491
+
492
+ option_string = options.symbolize_keys.sum do |key, value|
493
+ case key
494
+ when :owner
495
+ " OWNER = '#{value}'"
496
+ when :template
497
+ " TEMPLATE = #{value}"
498
+ when :encoding
499
+ " ENCODING = '#{value}'"
500
+ when :tablespace
501
+ " TABLESPACE = #{value}"
502
+ when :connection_limit
503
+ " CONNECTION LIMIT = #{value}"
504
+ else
505
+ ""
506
+ end
507
+ end
508
+
509
+ execute "CREATE DATABASE #{name}#{option_string}"
510
+ end
511
+
512
+ # Drops a PostgreSQL database
513
+ #
514
+ # Example:
515
+ # drop_database 'matt_development'
516
+ def drop_database(name) #:nodoc:
517
+ execute "DROP DATABASE IF EXISTS #{name}"
518
+ end
519
+
520
+
438
521
  # Returns the list of all tables in the schema search path or a specified schema.
439
522
  def tables(name = nil)
440
523
  schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
@@ -447,14 +530,16 @@ module ActiveRecord
447
530
 
448
531
  # Returns the list of all indexes for a table.
449
532
  def indexes(table_name, name = nil)
450
- result = query(<<-SQL, name)
451
- SELECT i.relname, d.indisunique, a.attname
452
- FROM pg_class t, pg_class i, pg_index d, pg_attribute a
533
+ schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
534
+ result = query(<<-SQL, name)
535
+ SELECT distinct i.relname, d.indisunique, a.attname
536
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a
453
537
  WHERE i.relkind = 'i'
454
538
  AND d.indexrelid = i.oid
455
539
  AND d.indisprimary = 'f'
456
540
  AND t.oid = d.indrelid
457
541
  AND t.relname = '#{table_name}'
542
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
458
543
  AND a.attrelid = t.oid
459
544
  AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
460
545
  OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
@@ -529,8 +614,10 @@ module ActiveRecord
529
614
  end
530
615
  if pk
531
616
  if sequence
617
+ quoted_sequence = quote_column_name(sequence)
618
+
532
619
  select_value <<-end_sql, 'Reset sequence'
533
- SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
620
+ SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
534
621
  end_sql
535
622
  else
536
623
  @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
@@ -564,7 +651,13 @@ module ActiveRecord
564
651
  # Support the 7.x and 8.0 nextval('foo'::text) as well as
565
652
  # the 8.1+ nextval('foo'::regclass).
566
653
  result = query(<<-end_sql, 'PK and custom sequence')[0]
567
- SELECT attr.attname, split_part(def.adsrc, '''', 2)
654
+ SELECT attr.attname,
655
+ CASE
656
+ WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
657
+ substr(split_part(def.adsrc, '''', 2),
658
+ strpos(split_part(def.adsrc, '''', 2), '.')+1)
659
+ ELSE split_part(def.adsrc, '''', 2)
660
+ END
568
661
  FROM pg_class t
569
662
  JOIN pg_attribute attr ON (t.oid = attrelid)
570
663
  JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
@@ -574,6 +667,7 @@ module ActiveRecord
574
667
  AND def.adsrc ~* 'nextval'
575
668
  end_sql
576
669
  end
670
+
577
671
  # [primary_key, sequence]
578
672
  [result.first, result.last]
579
673
  rescue
@@ -585,13 +679,14 @@ module ActiveRecord
585
679
  execute "ALTER TABLE #{name} RENAME TO #{new_name}"
586
680
  end
587
681
 
588
- # Adds a column to a table.
682
+ # Adds a new column to the named table.
683
+ # See TableDefinition#column for details of the options you can use.
589
684
  def add_column(table_name, column_name, type, options = {})
590
685
  default = options[:default]
591
686
  notnull = options[:null] == false
592
687
 
593
688
  # Add the column.
594
- execute("ALTER TABLE #{table_name} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}")
689
+ execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
595
690
 
596
691
  change_column_default(table_name, column_name, default) if options_include_default?(options)
597
692
  change_column_null(table_name, column_name, false, default) if notnull
@@ -599,17 +694,23 @@ module ActiveRecord
599
694
 
600
695
  # Changes the column of a table.
601
696
  def change_column(table_name, column_name, type, options = {})
697
+ quoted_table_name = quote_table_name(table_name)
698
+
602
699
  begin
603
- execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
700
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
604
701
  rescue ActiveRecord::StatementInvalid
605
702
  # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
606
- begin_db_transaction
607
- tmp_column_name = "#{column_name}_ar_tmp"
608
- add_column(table_name, tmp_column_name, type, options)
609
- execute "UPDATE #{table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
610
- remove_column(table_name, column_name)
611
- rename_column(table_name, tmp_column_name, column_name)
612
- commit_db_transaction
703
+ begin
704
+ begin_db_transaction
705
+ tmp_column_name = "#{column_name}_ar_tmp"
706
+ add_column(table_name, tmp_column_name, type, options)
707
+ execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
708
+ remove_column(table_name, column_name)
709
+ rename_column(table_name, tmp_column_name, column_name)
710
+ commit_db_transaction
711
+ rescue
712
+ rollback_db_transaction
713
+ end
613
714
  end
614
715
 
615
716
  change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
@@ -618,19 +719,19 @@ module ActiveRecord
618
719
 
619
720
  # Changes the default value of a table column.
620
721
  def change_column_default(table_name, column_name, default)
621
- execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
722
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
622
723
  end
623
724
 
624
725
  def change_column_null(table_name, column_name, null, default = nil)
625
726
  unless null || default.nil?
626
- execute("UPDATE #{table_name} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
727
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
627
728
  end
628
- execute("ALTER TABLE #{table_name} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
729
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
629
730
  end
630
731
 
631
732
  # Renames a column in a table.
632
733
  def rename_column(table_name, column_name, new_column_name)
633
- execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
734
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
634
735
  end
635
736
 
636
737
  # Drops an index from a table.
@@ -675,7 +776,7 @@ module ActiveRecord
675
776
  # Returns an ORDER BY clause for the passed order option.
676
777
  #
677
778
  # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
678
- # by wrapping the sql as a sub-select and ordering in that query.
779
+ # by wrapping the +sql+ string as a sub-select and ordering in that query.
679
780
  def add_order_by_for_association_limiting!(sql, options) #:nodoc:
680
781
  return sql if options[:order].blank?
681
782
 
@@ -780,10 +881,10 @@ module ActiveRecord
780
881
 
781
882
  def select_raw(sql, name = nil)
782
883
  res = execute(sql, name)
783
- results = res.result
884
+ results = result_as_array(res)
784
885
  fields = []
785
886
  rows = []
786
- if results.length > 0
887
+ if res.ntuples > 0
787
888
  fields = res.fields
788
889
  results.each do |row|
789
890
  hashed_row = {}
@@ -792,7 +893,7 @@ module ActiveRecord
792
893
  # then strip them off. Indeed it would be prettier to do this in
793
894
  # PostgreSQLColumn.string_to_decimal but would break form input
794
895
  # fields that call value_before_type_cast.
795
- if res.type(cell_index) == MONEY_COLUMN_TYPE_OID
896
+ if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID
796
897
  # Because money output is formatted according to the locale, there are two
797
898
  # cases to consider (note the decimal separators):
798
899
  # (1) $12,345,678.12
@@ -25,7 +25,7 @@ module ActiveRecord
25
25
  module ConnectionAdapters #:nodoc:
26
26
  class SQLite3Adapter < SQLiteAdapter # :nodoc:
27
27
  def table_structure(table_name)
28
- returning structure = @connection.table_info(table_name) do
28
+ returning structure = @connection.table_info(quote_table_name(table_name)) do
29
29
  raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
30
30
  end
31
31
  end
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
  #
71
71
  # Options:
72
72
  #
73
- # * <tt>:database</tt> -- Path to the database file.
73
+ # * <tt>:database</tt> - Path to the database file.
74
74
  class SQLiteAdapter < AbstractAdapter
75
75
  def adapter_name #:nodoc:
76
76
  'SQLite'
@@ -107,7 +107,7 @@ module ActiveRecord
107
107
  :decimal => { :name => "decimal" },
108
108
  :datetime => { :name => "datetime" },
109
109
  :timestamp => { :name => "datetime" },
110
- :time => { :name => "datetime" },
110
+ :time => { :name => "time" },
111
111
  :date => { :name => "date" },
112
112
  :binary => { :name => "blob" },
113
113
  :boolean => { :name => "boolean" }
@@ -192,7 +192,7 @@ module ActiveRecord
192
192
  end
193
193
 
194
194
  def indexes(table_name, name = nil) #:nodoc:
195
- execute("PRAGMA index_list(#{table_name})", name).map do |row|
195
+ execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
196
196
  index = IndexDefinition.new(table_name, row['name'])
197
197
  index.unique = row['unique'] != '0'
198
198
  index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
@@ -214,16 +214,23 @@ module ActiveRecord
214
214
  end
215
215
 
216
216
  def add_column(table_name, column_name, type, options = {}) #:nodoc:
217
+ if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
218
+ raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
219
+ end
220
+
217
221
  super(table_name, column_name, type, options)
218
222
  # See last paragraph on http://www.sqlite.org/lang_altertable.html
219
223
  execute "VACUUM"
220
224
  end
221
225
 
222
- def remove_column(table_name, column_name) #:nodoc:
223
- alter_table(table_name) do |definition|
224
- definition.columns.delete(definition[column_name])
226
+ def remove_column(table_name, *column_names) #:nodoc:
227
+ column_names.flatten.each do |column_name|
228
+ alter_table(table_name) do |definition|
229
+ definition.columns.delete(definition[column_name])
230
+ end
225
231
  end
226
232
  end
233
+ alias :remove_columns :remove_column
227
234
 
228
235
  def change_column_default(table_name, column_name, default) #:nodoc:
229
236
  alter_table(table_name) do |definition|
@@ -257,7 +264,7 @@ module ActiveRecord
257
264
  record = {}
258
265
  row.each_key do |key|
259
266
  if key.is_a?(String)
260
- record[key.sub(/^\w+\./, '')] = row[key]
267
+ record[key.sub(/^"?\w+"?\./, '')] = row[key]
261
268
  end
262
269
  end
263
270
  record
@@ -265,7 +272,7 @@ module ActiveRecord
265
272
  end
266
273
 
267
274
  def table_structure(table_name)
268
- returning structure = execute("PRAGMA table_info(#{table_name})") do
275
+ returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
269
276
  raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
270
277
  end
271
278
  end
@@ -340,8 +347,9 @@ module ActiveRecord
340
347
  columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
341
348
  quoted_columns = columns.map { |col| quote_column_name(col) } * ','
342
349
 
343
- @connection.execute "SELECT * FROM #{from}" do |row|
344
- sql = "INSERT INTO #{to} (#{quoted_columns}) VALUES ("
350
+ quoted_to = quote_table_name(to)
351
+ @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
352
+ sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
345
353
  sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
346
354
  sql << ')'
347
355
  @connection.execute sql
@@ -0,0 +1,158 @@
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
+ def self.included(base)
38
+ base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
39
+ base.alias_method_chain :write_attribute, :dirty
40
+ base.alias_method_chain :save, :dirty
41
+ base.alias_method_chain :save!, :dirty
42
+ base.alias_method_chain :update, :dirty
43
+ base.alias_method_chain :reload, :dirty
44
+
45
+ base.superclass_delegating_accessor :partial_updates
46
+ base.partial_updates = true
47
+ end
48
+
49
+ # Do any attributes have unsaved changes?
50
+ # person.changed? # => false
51
+ # person.name = 'bob'
52
+ # person.changed? # => true
53
+ def changed?
54
+ !changed_attributes.empty?
55
+ end
56
+
57
+ # List of attributes with unsaved changes.
58
+ # person.changed # => []
59
+ # person.name = 'bob'
60
+ # person.changed # => ['name']
61
+ def changed
62
+ changed_attributes.keys
63
+ end
64
+
65
+ # Map of changed attrs => [original value, new value]
66
+ # person.changes # => {}
67
+ # person.name = 'bob'
68
+ # person.changes # => { 'name' => ['bill', 'bob'] }
69
+ def changes
70
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
71
+ end
72
+
73
+ # Attempts to +save+ the record and clears changed attributes if successful.
74
+ def save_with_dirty(*args) #:nodoc:
75
+ if status = save_without_dirty(*args)
76
+ changed_attributes.clear
77
+ end
78
+ status
79
+ end
80
+
81
+ # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
82
+ def save_with_dirty!(*args) #:nodoc:
83
+ status = save_without_dirty!(*args)
84
+ changed_attributes.clear
85
+ status
86
+ end
87
+
88
+ # <tt>reload</tt> the record and clears changed attributes.
89
+ def reload_with_dirty(*args) #:nodoc:
90
+ record = reload_without_dirty(*args)
91
+ changed_attributes.clear
92
+ record
93
+ end
94
+
95
+ private
96
+ # Map of change attr => original value.
97
+ def changed_attributes
98
+ @changed_attributes ||= {}
99
+ end
100
+
101
+ # Handle *_changed? for method_missing.
102
+ def attribute_changed?(attr)
103
+ changed_attributes.include?(attr)
104
+ end
105
+
106
+ # Handle *_change for method_missing.
107
+ def attribute_change(attr)
108
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
109
+ end
110
+
111
+ # Handle *_was for method_missing.
112
+ def attribute_was(attr)
113
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
114
+ end
115
+
116
+ # Handle *_will_change! for method_missing.
117
+ def attribute_will_change!(attr)
118
+ changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
119
+ end
120
+
121
+ # Wrap write_attribute to remember original attribute value.
122
+ def write_attribute_with_dirty(attr, value)
123
+ attr = attr.to_s
124
+
125
+ # The attribute already has an unsaved change.
126
+ unless changed_attributes.include?(attr)
127
+ old = clone_attribute_value(:read_attribute, attr)
128
+ changed_attributes[attr] = old if field_changed?(attr, old, value)
129
+ end
130
+
131
+ # Carry on.
132
+ write_attribute_without_dirty(attr, value)
133
+ end
134
+
135
+ def update_with_dirty
136
+ if partial_updates?
137
+ update_without_dirty(changed)
138
+ else
139
+ update_without_dirty
140
+ end
141
+ end
142
+
143
+ def field_changed?(attr, old, value)
144
+ if column = column_for_attribute(attr)
145
+ if column.type == :integer && column.null && old.nil?
146
+ # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
147
+ # Hence we don't record it as a change if the value changes from nil to ''.
148
+ value = nil if value.blank?
149
+ else
150
+ value = column.type_cast(value)
151
+ end
152
+ end
153
+
154
+ old != value
155
+ end
156
+
157
+ end
158
+ end