activerecord 2.2.3 → 2.3.2

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 (120) hide show
  1. data/CHANGELOG +438 -396
  2. data/Rakefile +4 -2
  3. data/lib/active_record.rb +46 -43
  4. data/lib/active_record/association_preload.rb +34 -19
  5. data/lib/active_record/associations.rb +193 -251
  6. data/lib/active_record/associations/association_collection.rb +38 -21
  7. data/lib/active_record/associations/association_proxy.rb +11 -4
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
  9. data/lib/active_record/associations/has_many_association.rb +2 -2
  10. data/lib/active_record/associations/has_many_through_association.rb +8 -8
  11. data/lib/active_record/associations/has_one_association.rb +11 -2
  12. data/lib/active_record/attribute_methods.rb +1 -0
  13. data/lib/active_record/autosave_association.rb +349 -0
  14. data/lib/active_record/base.rb +292 -106
  15. data/lib/active_record/batches.rb +73 -0
  16. data/lib/active_record/calculations.rb +34 -16
  17. data/lib/active_record/callbacks.rb +37 -8
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +16 -0
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -0
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +103 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -6
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +28 -25
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -5
  24. data/lib/active_record/connection_adapters/mysql_adapter.rb +50 -21
  25. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -41
  26. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  27. data/lib/active_record/connection_adapters/sqlite_adapter.rb +41 -21
  28. data/lib/active_record/dirty.rb +1 -1
  29. data/lib/active_record/dynamic_scope_match.rb +25 -0
  30. data/lib/active_record/fixtures.rb +193 -198
  31. data/lib/active_record/locale/en.yml +1 -1
  32. data/lib/active_record/locking/optimistic.rb +33 -0
  33. data/lib/active_record/migration.rb +8 -2
  34. data/lib/active_record/named_scope.rb +13 -6
  35. data/lib/active_record/nested_attributes.rb +329 -0
  36. data/lib/active_record/query_cache.rb +25 -13
  37. data/lib/active_record/reflection.rb +6 -1
  38. data/lib/active_record/schema_dumper.rb +2 -0
  39. data/lib/active_record/serialization.rb +3 -1
  40. data/lib/active_record/serializers/json_serializer.rb +19 -0
  41. data/lib/active_record/serializers/xml_serializer.rb +28 -13
  42. data/lib/active_record/session_store.rb +318 -0
  43. data/lib/active_record/test_case.rb +15 -9
  44. data/lib/active_record/timestamp.rb +2 -2
  45. data/lib/active_record/transactions.rb +58 -8
  46. data/lib/active_record/validations.rb +29 -24
  47. data/lib/active_record/version.rb +2 -2
  48. data/test/cases/ar_schema_test.rb +0 -1
  49. data/test/cases/associations/belongs_to_associations_test.rb +35 -131
  50. data/test/cases/associations/cascaded_eager_loading_test.rb +8 -0
  51. data/test/cases/associations/eager_load_nested_include_test.rb +29 -0
  52. data/test/cases/associations/eager_test.rb +137 -7
  53. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +45 -7
  54. data/test/cases/associations/has_many_associations_test.rb +110 -149
  55. data/test/cases/associations/has_many_through_associations_test.rb +39 -7
  56. data/test/cases/associations/has_one_associations_test.rb +39 -92
  57. data/test/cases/associations/has_one_through_associations_test.rb +34 -3
  58. data/test/cases/associations/inner_join_association_test.rb +0 -5
  59. data/test/cases/associations/join_model_test.rb +5 -7
  60. data/test/cases/attribute_methods_test.rb +13 -1
  61. data/test/cases/autosave_association_test.rb +901 -0
  62. data/test/cases/base_test.rb +41 -21
  63. data/test/cases/batches_test.rb +61 -0
  64. data/test/cases/calculations_test.rb +37 -17
  65. data/test/cases/callbacks_test.rb +43 -5
  66. data/test/cases/connection_pool_test.rb +25 -0
  67. data/test/cases/copy_table_test_sqlite.rb +11 -0
  68. data/test/cases/datatype_test_postgresql.rb +1 -0
  69. data/test/cases/defaults_test.rb +37 -26
  70. data/test/cases/dirty_test.rb +26 -2
  71. data/test/cases/finder_test.rb +79 -44
  72. data/test/cases/fixtures_test.rb +15 -19
  73. data/test/cases/helper.rb +26 -19
  74. data/test/cases/inheritance_test.rb +2 -2
  75. data/test/cases/json_serialization_test.rb +1 -1
  76. data/test/cases/locking_test.rb +23 -5
  77. data/test/cases/method_scoping_test.rb +126 -3
  78. data/test/cases/migration_test.rb +253 -237
  79. data/test/cases/named_scope_test.rb +73 -3
  80. data/test/cases/nested_attributes_test.rb +509 -0
  81. data/test/cases/query_cache_test.rb +0 -4
  82. data/test/cases/reflection_test.rb +13 -3
  83. data/test/cases/reload_models_test.rb +3 -1
  84. data/test/cases/repair_helper.rb +50 -0
  85. data/test/cases/schema_dumper_test.rb +0 -1
  86. data/test/cases/transactions_test.rb +177 -12
  87. data/test/cases/validations_i18n_test.rb +288 -294
  88. data/test/cases/validations_test.rb +230 -180
  89. data/test/cases/xml_serialization_test.rb +19 -1
  90. data/test/fixtures/fixture_database.sqlite3 +0 -0
  91. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  92. data/test/fixtures/member_types.yml +6 -0
  93. data/test/fixtures/members.yml +3 -1
  94. data/test/fixtures/people.yml +10 -1
  95. data/test/fixtures/toys.yml +4 -0
  96. data/test/models/author.rb +1 -2
  97. data/test/models/bird.rb +3 -0
  98. data/test/models/category.rb +1 -0
  99. data/test/models/company.rb +3 -0
  100. data/test/models/developer.rb +12 -0
  101. data/test/models/event.rb +3 -0
  102. data/test/models/member.rb +1 -0
  103. data/test/models/member_detail.rb +1 -0
  104. data/test/models/member_type.rb +3 -0
  105. data/test/models/owner.rb +2 -1
  106. data/test/models/parrot.rb +2 -0
  107. data/test/models/person.rb +6 -0
  108. data/test/models/pet.rb +2 -1
  109. data/test/models/pirate.rb +55 -1
  110. data/test/models/post.rb +6 -0
  111. data/test/models/project.rb +1 -0
  112. data/test/models/reply.rb +6 -0
  113. data/test/models/ship.rb +8 -1
  114. data/test/models/ship_part.rb +5 -0
  115. data/test/models/topic.rb +13 -1
  116. data/test/models/toy.rb +4 -0
  117. data/test/schema/schema.rb +35 -2
  118. metadata +70 -9
  119. data/test/fixtures/fixture_database.sqlite +0 -0
  120. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -1,20 +1,32 @@
1
1
  module ActiveRecord
2
- module QueryCache
3
- # Enable the query cache within the block if Active Record is configured.
4
- def cache(&block)
5
- if ActiveRecord::Base.configurations.blank?
6
- yield
7
- else
8
- connection.cache(&block)
2
+ class QueryCache
3
+ module ClassMethods
4
+ # Enable the query cache within the block if Active Record is configured.
5
+ def cache(&block)
6
+ if ActiveRecord::Base.configurations.blank?
7
+ yield
8
+ else
9
+ connection.cache(&block)
10
+ end
9
11
  end
12
+
13
+ # Disable the query cache within the block if Active Record is configured.
14
+ def uncached(&block)
15
+ if ActiveRecord::Base.configurations.blank?
16
+ yield
17
+ else
18
+ connection.uncached(&block)
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(app)
24
+ @app = app
10
25
  end
11
26
 
12
- # Disable the query cache within the block if Active Record is configured.
13
- def uncached(&block)
14
- if ActiveRecord::Base.configurations.blank?
15
- yield
16
- else
17
- connection.uncached(&block)
27
+ def call(env)
28
+ ActiveRecord::Base.cache do
29
+ @app.call(env)
18
30
  end
19
31
  end
20
32
  end
@@ -65,6 +65,11 @@ module ActiveRecord
65
65
  def reflect_on_association(association)
66
66
  reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
67
67
  end
68
+
69
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
70
+ def reflect_on_all_autosave_associations
71
+ reflections.values.select { |reflection| reflection.options[:autosave] }
72
+ end
68
73
  end
69
74
 
70
75
 
@@ -192,7 +197,7 @@ module ActiveRecord
192
197
 
193
198
  def counter_cache_column
194
199
  if options[:counter_cache] == true
195
- "#{active_record.name.underscore.pluralize}_count"
200
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
196
201
  elsif options[:counter_cache]
197
202
  options[:counter_cache]
198
203
  end
@@ -7,6 +7,8 @@ module ActiveRecord
7
7
  class SchemaDumper #:nodoc:
8
8
  private_class_method :new
9
9
 
10
+ ##
11
+ # :singleton-method:
10
12
  # A list of tables which should not be dumped to the schema.
11
13
  # Acceptable values are strings as well as regexp.
12
14
  # This setting is only used if ActiveRecord::Base.schema_format == :ruby
@@ -1,3 +1,5 @@
1
+ require 'active_support/json'
2
+
1
3
  module ActiveRecord #:nodoc:
2
4
  module Serialization
3
5
  class Serializer #:nodoc:
@@ -95,4 +97,4 @@ module ActiveRecord #:nodoc:
95
97
  end
96
98
 
97
99
  require 'active_record/serializers/xml_serializer'
98
- require 'active_record/serializers/json_serializer'
100
+ require 'active_record/serializers/json_serializer'
@@ -8,6 +8,25 @@ module ActiveRecord #:nodoc:
8
8
  # Returns a JSON string representing the model. Some configuration is
9
9
  # available through +options+.
10
10
  #
11
+ # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
12
+ # top-level behavior of to_json. In a new Rails application, it is set to
13
+ # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
14
+ # to_json will emit a single root node named after the object's type. For example:
15
+ #
16
+ # konata = User.find(1)
17
+ # ActiveRecord::Base.include_root_in_json = true
18
+ # konata.to_json
19
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
20
+ # "created_at": "2006/08/01", "awesome": true} }
21
+ #
22
+ # ActiveRecord::Base.include_root_in_json = false
23
+ # konata.to_json
24
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
25
+ # "created_at": "2006/08/01", "awesome": true}
26
+ #
27
+ # The remainder of the examples in this section assume include_root_in_json is set to
28
+ # <tt>false</tt>.
29
+ #
11
30
  # Without any +options+, the returned JSON string will include all
12
31
  # the model's attributes. For example:
13
32
  #
@@ -23,11 +23,12 @@ module ActiveRecord #:nodoc:
23
23
  # </topic>
24
24
  #
25
25
  # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
26
- # <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>.
26
+ # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
27
27
  # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
28
28
  # +attributes+ method. The default is to dasherize all column names, but you
29
- # can disable this setting <tt>:dasherize</tt> to +false+. To not have the
30
- # column type included in the XML output set <tt>:skip_types</tt> to +true+.
29
+ # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
30
+ # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
31
+ # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
31
32
  #
32
33
  # For instance:
33
34
  #
@@ -178,13 +179,22 @@ module ActiveRecord #:nodoc:
178
179
 
179
180
  def root
180
181
  root = (options[:root] || @record.class.to_s.underscore).to_s
181
- dasherize? ? root.dasherize : root
182
+ reformat_name(root)
182
183
  end
183
184
 
184
185
  def dasherize?
185
186
  !options.has_key?(:dasherize) || options[:dasherize]
186
187
  end
187
188
 
189
+ def camelize?
190
+ options.has_key?(:camelize) && options[:camelize]
191
+ end
192
+
193
+ def reformat_name(name)
194
+ name = name.camelize if camelize?
195
+ dasherize? ? name.dasherize : name
196
+ end
197
+
188
198
  def serializable_attributes
189
199
  serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
190
200
  end
@@ -212,7 +222,7 @@ module ActiveRecord #:nodoc:
212
222
 
213
223
  def add_tag(attribute)
214
224
  builder.tag!(
215
- dasherize? ? attribute.name.dasherize : attribute.name,
225
+ reformat_name(attribute.name),
216
226
  attribute.value.to_s,
217
227
  attribute.decorations(!options[:skip_types])
218
228
  )
@@ -220,18 +230,23 @@ module ActiveRecord #:nodoc:
220
230
 
221
231
  def add_associations(association, records, opts)
222
232
  if records.is_a?(Enumerable)
223
- tag = association.to_s
224
- tag = tag.dasherize if dasherize?
233
+ tag = reformat_name(association.to_s)
234
+ type = options[:skip_types] ? {} : {:type => "array"}
235
+
225
236
  if records.empty?
226
- builder.tag!(tag, :type => :array)
237
+ builder.tag!(tag, type)
227
238
  else
228
- builder.tag!(tag, :type => :array) do
239
+ builder.tag!(tag, type) do
229
240
  association_name = association.to_s.singularize
230
241
  records.each do |record|
231
- record.to_xml opts.merge(
232
- :root => association_name,
233
- :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
234
- )
242
+ if options[:skip_types]
243
+ record_type = {}
244
+ else
245
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
246
+ record_type = {:type => record_class}
247
+ end
248
+
249
+ record.to_xml opts.merge(:root => association_name).merge(record_type)
235
250
  end
236
251
  end
237
252
  end
@@ -0,0 +1,318 @@
1
+ module ActiveRecord
2
+ # A session store backed by an Active Record class. A default class is
3
+ # provided, but any object duck-typing to an Active Record Session class
4
+ # with text +session_id+ and +data+ attributes is sufficient.
5
+ #
6
+ # The default assumes a +sessions+ tables with columns:
7
+ # +id+ (numeric primary key),
8
+ # +session_id+ (text, or longtext if your session data exceeds 65K), and
9
+ # +data+ (text or longtext; careful if your session data exceeds 65KB).
10
+ # The +session_id+ column should always be indexed for speedy lookups.
11
+ # Session data is marshaled to the +data+ column in Base64 format.
12
+ # If the data you write is larger than the column's size limit,
13
+ # ActionController::SessionOverflowError will be raised.
14
+ #
15
+ # You may configure the table name, primary key, and data column.
16
+ # For example, at the end of <tt>config/environment.rb</tt>:
17
+ # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
18
+ # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
19
+ # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
20
+ # Note that setting the primary key to the +session_id+ frees you from
21
+ # having a separate +id+ column if you don't want it. However, you must
22
+ # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
23
+ # on ApplicationController is a good place.
24
+ #
25
+ # Since the default class is a simple Active Record, you get timestamps
26
+ # for free if you add +created_at+ and +updated_at+ datetime columns to
27
+ # the +sessions+ table, making periodic session expiration a snap.
28
+ #
29
+ # You may provide your own session class implementation, whether a
30
+ # feature-packed Active Record or a bare-metal high-performance SQL
31
+ # store, by setting
32
+ # ActiveRecord::SessionStore.session_class = MySessionClass
33
+ # You must implement these methods:
34
+ # self.find_by_session_id(session_id)
35
+ # initialize(hash_of_session_id_and_data)
36
+ # attr_reader :session_id
37
+ # attr_accessor :data
38
+ # save
39
+ # destroy
40
+ #
41
+ # The example SqlBypass class is a generic SQL session store. You may
42
+ # use it as a basis for high-performance database-specific stores.
43
+ class SessionStore < ActionController::Session::AbstractStore
44
+ # The default Active Record class.
45
+ class Session < ActiveRecord::Base
46
+ ##
47
+ # :singleton-method:
48
+ # Customizable data column name. Defaults to 'data'.
49
+ cattr_accessor :data_column_name
50
+ self.data_column_name = 'data'
51
+
52
+ before_save :marshal_data!
53
+ before_save :raise_on_session_data_overflow!
54
+
55
+ class << self
56
+ def data_column_size_limit
57
+ @data_column_size_limit ||= columns_hash[@@data_column_name].limit
58
+ end
59
+
60
+ # Hook to set up sessid compatibility.
61
+ def find_by_session_id(session_id)
62
+ setup_sessid_compatibility!
63
+ find_by_session_id(session_id)
64
+ end
65
+
66
+ def marshal(data)
67
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
68
+ end
69
+
70
+ def unmarshal(data)
71
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
72
+ end
73
+
74
+ def create_table!
75
+ connection.execute <<-end_sql
76
+ CREATE TABLE #{table_name} (
77
+ id INTEGER PRIMARY KEY,
78
+ #{connection.quote_column_name('session_id')} TEXT UNIQUE,
79
+ #{connection.quote_column_name(@@data_column_name)} TEXT(255)
80
+ )
81
+ end_sql
82
+ end
83
+
84
+ def drop_table!
85
+ connection.execute "DROP TABLE #{table_name}"
86
+ end
87
+
88
+ private
89
+ # Compatibility with tables using sessid instead of session_id.
90
+ def setup_sessid_compatibility!
91
+ # Reset column info since it may be stale.
92
+ reset_column_information
93
+ if columns_hash['sessid']
94
+ def self.find_by_session_id(*args)
95
+ find_by_sessid(*args)
96
+ end
97
+
98
+ define_method(:session_id) { sessid }
99
+ define_method(:session_id=) { |session_id| self.sessid = session_id }
100
+ else
101
+ def self.find_by_session_id(session_id)
102
+ find :first, :conditions => {:session_id=>session_id}
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Lazy-unmarshal session state.
109
+ def data
110
+ @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
111
+ end
112
+
113
+ attr_writer :data
114
+
115
+ # Has the session been loaded yet?
116
+ def loaded?
117
+ !!@data
118
+ end
119
+
120
+ private
121
+ def marshal_data!
122
+ return false if !loaded?
123
+ write_attribute(@@data_column_name, self.class.marshal(self.data))
124
+ end
125
+
126
+ # Ensures that the data about to be stored in the database is not
127
+ # larger than the data storage column. Raises
128
+ # ActionController::SessionOverflowError.
129
+ def raise_on_session_data_overflow!
130
+ return false if !loaded?
131
+ limit = self.class.data_column_size_limit
132
+ if loaded? and limit and read_attribute(@@data_column_name).size > limit
133
+ raise ActionController::SessionOverflowError
134
+ end
135
+ end
136
+ end
137
+
138
+ # A barebones session store which duck-types with the default session
139
+ # store but bypasses Active Record and issues SQL directly. This is
140
+ # an example session model class meant as a basis for your own classes.
141
+ #
142
+ # The database connection, table name, and session id and data columns
143
+ # are configurable class attributes. Marshaling and unmarshaling
144
+ # are implemented as class methods that you may override. By default,
145
+ # marshaling data is
146
+ #
147
+ # ActiveSupport::Base64.encode64(Marshal.dump(data))
148
+ #
149
+ # and unmarshaling data is
150
+ #
151
+ # Marshal.load(ActiveSupport::Base64.decode64(data))
152
+ #
153
+ # This marshaling behavior is intended to store the widest range of
154
+ # binary session data in a +text+ column. For higher performance,
155
+ # store in a +blob+ column instead and forgo the Base64 encoding.
156
+ class SqlBypass
157
+ ##
158
+ # :singleton-method:
159
+ # Use the ActiveRecord::Base.connection by default.
160
+ cattr_accessor :connection
161
+
162
+ ##
163
+ # :singleton-method:
164
+ # The table name defaults to 'sessions'.
165
+ cattr_accessor :table_name
166
+ @@table_name = 'sessions'
167
+
168
+ ##
169
+ # :singleton-method:
170
+ # The session id field defaults to 'session_id'.
171
+ cattr_accessor :session_id_column
172
+ @@session_id_column = 'session_id'
173
+
174
+ ##
175
+ # :singleton-method:
176
+ # The data field defaults to 'data'.
177
+ cattr_accessor :data_column
178
+ @@data_column = 'data'
179
+
180
+ class << self
181
+ def connection
182
+ @@connection ||= ActiveRecord::Base.connection
183
+ end
184
+
185
+ # Look up a session by id and unmarshal its data if found.
186
+ def find_by_session_id(session_id)
187
+ if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
188
+ new(:session_id => session_id, :marshaled_data => record['data'])
189
+ end
190
+ end
191
+
192
+ def marshal(data)
193
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
194
+ end
195
+
196
+ def unmarshal(data)
197
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
198
+ end
199
+
200
+ def create_table!
201
+ @@connection.execute <<-end_sql
202
+ CREATE TABLE #{table_name} (
203
+ id INTEGER PRIMARY KEY,
204
+ #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
205
+ #{@@connection.quote_column_name(data_column)} TEXT
206
+ )
207
+ end_sql
208
+ end
209
+
210
+ def drop_table!
211
+ @@connection.execute "DROP TABLE #{table_name}"
212
+ end
213
+ end
214
+
215
+ attr_reader :session_id
216
+ attr_writer :data
217
+
218
+ # Look for normal and marshaled data, self.find_by_session_id's way of
219
+ # telling us to postpone unmarshaling until the data is requested.
220
+ # We need to handle a normal data attribute in case of a new record.
221
+ def initialize(attributes)
222
+ @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
223
+ @new_record = @marshaled_data.nil?
224
+ end
225
+
226
+ def new_record?
227
+ @new_record
228
+ end
229
+
230
+ # Lazy-unmarshal session state.
231
+ def data
232
+ unless @data
233
+ if @marshaled_data
234
+ @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
235
+ else
236
+ @data = {}
237
+ end
238
+ end
239
+ @data
240
+ end
241
+
242
+ def loaded?
243
+ !!@data
244
+ end
245
+
246
+ def save
247
+ return false if !loaded?
248
+ marshaled_data = self.class.marshal(data)
249
+
250
+ if @new_record
251
+ @new_record = false
252
+ @@connection.update <<-end_sql, 'Create session'
253
+ INSERT INTO #{@@table_name} (
254
+ #{@@connection.quote_column_name(@@session_id_column)},
255
+ #{@@connection.quote_column_name(@@data_column)} )
256
+ VALUES (
257
+ #{@@connection.quote(session_id)},
258
+ #{@@connection.quote(marshaled_data)} )
259
+ end_sql
260
+ else
261
+ @@connection.update <<-end_sql, 'Update session'
262
+ UPDATE #{@@table_name}
263
+ SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
264
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
265
+ end_sql
266
+ end
267
+ end
268
+
269
+ def destroy
270
+ unless @new_record
271
+ @@connection.delete <<-end_sql, 'Destroy session'
272
+ DELETE FROM #{@@table_name}
273
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
274
+ end_sql
275
+ end
276
+ end
277
+ end
278
+
279
+ # The class used for session storage. Defaults to
280
+ # ActiveRecord::SessionStore::Session
281
+ cattr_accessor :session_class
282
+ self.session_class = Session
283
+
284
+ SESSION_RECORD_KEY = 'rack.session.record'.freeze
285
+
286
+ private
287
+ def get_session(env, sid)
288
+ Base.silence do
289
+ sid ||= generate_sid
290
+ session = find_session(sid)
291
+ env[SESSION_RECORD_KEY] = session
292
+ [sid, session.data]
293
+ end
294
+ end
295
+
296
+ def set_session(env, sid, session_data)
297
+ Base.silence do
298
+ record = env[SESSION_RECORD_KEY] ||= find_session(sid)
299
+ record.data = session_data
300
+ return false unless record.save
301
+
302
+ session_data = record.data
303
+ if session_data && session_data.respond_to?(:each_value)
304
+ session_data.each_value do |obj|
305
+ obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
306
+ end
307
+ end
308
+ end
309
+
310
+ return true
311
+ end
312
+
313
+ def find_session(id)
314
+ @@session_class.find_by_session_id(id) ||
315
+ @@session_class.new(:session_id => id, :data => {})
316
+ end
317
+ end
318
+ end