diff options
Diffstat (limited to 'activerecord')
8 files changed, 3 insertions, 583 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c5ef39b9d2..18b10f1d81 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,8 @@ ## Rails 4.0.0 (unreleased) ## +* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` + gem. Please read the `README.md` file on the gem for the usage. *Prem Sichanugrist* + * Fix `reset_counters` when there are multiple `belongs_to` association with the same foreign key and one of them have a counter cache. Fixes #5200. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index fa94f6a941..1675127ab0 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -53,17 +53,6 @@ module ActiveRecord autoload :ReadonlyAttributes autoload :Reflection autoload :Sanitization - - # ActiveRecord::SessionStore depends on the abstract store in Action Pack. - # Eager loading this class would break client code that eager loads Active - # Record standalone. - # - # Note that the Rails application generator creates an initializer specific - # for setting the session store. Thus, albeit in theory this autoload would - # not be thread-safe, in practice it is because if the application uses this - # session store its autoload happens at boot time. - autoload :SessionStore - autoload :Schema autoload :SchemaDumper autoload :SchemaMigration diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 4e5ec4f739..ae24542521 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -408,21 +408,6 @@ db_namespace = namespace :db do end end end - - namespace :sessions do - # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => [:environment, :load_config] do - raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? - Rails.application.load_generators - require 'rails/generators/rails/session_migration/session_migration_generator' - Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ] - end - - # desc "Clear the sessions table" - task :clear => [:environment, :load_config] do - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}" - end - end end namespace :railties do diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb deleted file mode 100644 index 58e1dab508..0000000000 --- a/activerecord/lib/active_record/session_store.rb +++ /dev/null @@ -1,365 +0,0 @@ -require 'action_dispatch/middleware/session/abstract_store' - -module ActiveRecord - # = Active Record Session Store - # - # A session store backed by an Active Record class. A default class is - # provided, but any object duck-typing to an Active Record Session class - # with text +session_id+ and +data+ attributes is sufficient. - # - # The default assumes a +sessions+ tables with columns: - # +id+ (numeric primary key), - # +session_id+ (string, usually varchar; maximum length is 255), and - # +data+ (text or longtext; careful if your session data exceeds 65KB). - # - # The +session_id+ column should always be indexed for speedy lookups. - # Session data is marshaled to the +data+ column in Base64 format. - # If the data you write is larger than the column's size limit, - # ActionController::SessionOverflowError will be raised. - # - # You may configure the table name, primary key, and data column. - # For example, at the end of <tt>config/application.rb</tt>: - # - # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' - # ActiveRecord::SessionStore::Session.primary_key = 'session_id' - # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' - # - # Note that setting the primary key to the +session_id+ frees you from - # having a separate +id+ column if you don't want it. However, you must - # set <tt>session.model.id = session.session_id</tt> by hand! A before filter - # on ApplicationController is a good place. - # - # Since the default class is a simple Active Record, you get timestamps - # for free if you add +created_at+ and +updated_at+ datetime columns to - # the +sessions+ table, making periodic session expiration a snap. - # - # You may provide your own session class implementation, whether a - # feature-packed Active Record or a bare-metal high-performance SQL - # store, by setting - # - # ActiveRecord::SessionStore.session_class = MySessionClass - # - # You must implement these methods: - # - # self.find_by_session_id(session_id) - # initialize(hash_of_session_id_and_data, options_hash = {}) - # attr_reader :session_id - # attr_accessor :data - # save - # destroy - # - # The example SqlBypass class is a generic SQL session store. You may - # use it as a basis for high-performance database-specific stores. - class SessionStore < ActionDispatch::Session::AbstractStore - module ClassMethods # :nodoc: - def marshal(data) - ::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(::Base64.decode64(data)) if data - end - - def drop_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.drop_table table_name - end - - def create_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.create_table(table_name) do |t| - t.string session_id_column, :limit => 255 - t.text data_column_name - end - connection.add_index table_name, session_id_column, :unique => true - end - end - - # The default Active Record class. - class Session < ActiveRecord::Base - extend ClassMethods - - ## - # :singleton-method: - # Customizable data column name. Defaults to 'data'. - cattr_accessor :data_column_name - self.data_column_name = 'data' - - attr_accessible :session_id, :data, :marshaled_data - - before_save :marshal_data! - before_save :raise_on_session_data_overflow! - - class << self - def data_column_size_limit - @data_column_size_limit ||= columns_hash[data_column_name].limit - end - - # Hook to set up sessid compatibility. - def find_by_session_id(session_id) - setup_sessid_compatibility! - find_by_session_id(session_id) - end - - private - def session_id_column - 'session_id' - end - - # Compatibility with tables using sessid instead of session_id. - def setup_sessid_compatibility! - # Reset column info since it may be stale. - reset_column_information - if columns_hash['sessid'] - def self.find_by_session_id(*args) - find_by_sessid(*args) - end - - define_method(:session_id) { sessid } - define_method(:session_id=) { |session_id| self.sessid = session_id } - else - class << self; remove_possible_method :find_by_session_id; end - - def self.find_by_session_id(session_id) - where(session_id: session_id).first - end - end - end - end - - def initialize(attributes = nil, options = {}) - @data = nil - super - end - - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} - end - - attr_writer :data - - # Has the session been loaded yet? - def loaded? - @data - end - - private - def marshal_data! - return false unless loaded? - write_attribute(@@data_column_name, self.class.marshal(data)) - end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - def raise_on_session_data_overflow! - return false unless loaded? - limit = self.class.data_column_size_limit - if limit and read_attribute(@@data_column_name).size > limit - raise ActionController::SessionOverflowError - end - end - end - - # A barebones session store which duck-types with the default session - # store but bypasses Active Record and issues SQL directly. This is - # an example session model class meant as a basis for your own classes. - # - # The database connection, table name, and session id and data columns - # are configurable class attributes. Marshaling and unmarshaling - # are implemented as class methods that you may override. By default, - # marshaling data is - # - # ::Base64.encode64(Marshal.dump(data)) - # - # and unmarshaling data is - # - # Marshal.load(::Base64.decode64(data)) - # - # This marshaling behavior is intended to store the widest range of - # binary session data in a +text+ column. For higher performance, - # store in a +blob+ column instead and forgo the Base64 encoding. - class SqlBypass - extend ClassMethods - - ## - # :singleton-method: - # The table name defaults to 'sessions'. - cattr_accessor :table_name - @@table_name = 'sessions' - - ## - # :singleton-method: - # The session id field defaults to 'session_id'. - cattr_accessor :session_id_column - @@session_id_column = 'session_id' - - ## - # :singleton-method: - # The data field defaults to 'data'. - cattr_accessor :data_column - @@data_column = 'data' - - class << self - alias :data_column_name :data_column - - # Use the ActiveRecord::Base.connection by default. - attr_writer :connection - - # Use the ActiveRecord::Base.connection_pool by default. - attr_writer :connection_pool - - def connection - @connection ||= ActiveRecord::Base.connection - end - - def connection_pool - @connection_pool ||= ActiveRecord::Base.connection_pool - end - - # Look up a session by id and unmarshal its data if found. - def find_by_session_id(session_id) - if record = connection.select_one("SELECT #{connection.quote_column_name(data_column)} AS data FROM #{@@table_name} WHERE #{connection.quote_column_name(@@session_id_column)}=#{connection.quote(session_id.to_s)}") - new(:session_id => session_id, :marshaled_data => record['data']) - end - end - end - - delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self - - attr_reader :session_id, :new_record - alias :new_record? :new_record - - attr_writer :data - - # Look for normal and marshaled data, self.find_by_session_id's way of - # telling us to postpone unmarshaling until the data is requested. - # We need to handle a normal data attribute in case of a new record. - def initialize(attributes) - @session_id = attributes[:session_id] - @data = attributes[:data] - @marshaled_data = attributes[:marshaled_data] - @new_record = @marshaled_data.nil? - end - - # Returns true if the record is persisted, i.e. it's not a new record - def persisted? - !@new_record - end - - # Lazy-unmarshal session state. - def data - unless @data - if @marshaled_data - @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil - else - @data = {} - end - end - @data - end - - def loaded? - @data - end - - def save - return false unless loaded? - marshaled_data = self.class.marshal(data) - connect = connection - - if @new_record - @new_record = false - connect.update <<-end_sql, 'Create session' - INSERT INTO #{table_name} ( - #{connect.quote_column_name(session_id_column)}, - #{connect.quote_column_name(data_column)} ) - VALUES ( - #{connect.quote(session_id)}, - #{connect.quote(marshaled_data)} ) - end_sql - else - connect.update <<-end_sql, 'Update session' - UPDATE #{table_name} - SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} - end_sql - end - end - - def destroy - return if @new_record - - connect = connection - connect.delete <<-end_sql, 'Destroy session' - DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} - end_sql - end - end - - # The class used for session storage. Defaults to - # ActiveRecord::SessionStore::Session - cattr_accessor :session_class - self.session_class = Session - - SESSION_RECORD_KEY = 'rack.session.record' - ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY - - private - def get_session(env, sid) - Base.silence do - unless sid and session = @@session_class.find_by_session_id(sid) - # If the sid was nil or if there is no pre-existing session under the sid, - # force the generation of a new sid and associate a new session associated with the new sid - sid = generate_sid - session = @@session_class.new(:session_id => sid, :data => {}) - end - env[SESSION_RECORD_KEY] = session - [sid, session.data] - end - end - - def set_session(env, sid, session_data, options) - Base.silence do - record = get_session_model(env, sid) - record.data = session_data - return false unless record.save - - session_data = record.data - if session_data && session_data.respond_to?(:each_value) - session_data.each_value do |obj| - obj.clear_association_cache if obj.respond_to?(:clear_association_cache) - end - end - end - - sid - end - - def destroy_session(env, session_id, options) - if sid = current_session_id(env) - Base.silence do - get_session_model(env, sid).destroy - env[SESSION_RECORD_KEY] = nil - end - end - - generate_sid unless options[:drop] - end - - def get_session_model(env, sid) - if env[ENV_SESSION_OPTIONS_KEY][:id].nil? - env[SESSION_RECORD_KEY] = find_session(sid) - else - env[SESSION_RECORD_KEY] ||= find_session(sid) - end - end - - def find_session(id) - @@session_class.find_by_session_id(id) || - @@session_class.new(:session_id => id, :data => {}) - end - end -end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb deleted file mode 100644 index 75aee4f408..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rails/generators/active_record' - -module ActiveRecord - module Generators - class SessionMigrationGenerator < Base - argument :name, :type => :string, :default => "add_sessions_table" - - def create_migration_file - migration_template "migration.rb", "db/migrate/#{file_name}.rb" - end - - protected - - def session_table_name - current_table_name = ActiveRecord::SessionStore::Session.table_name - if current_table_name == 'session' || current_table_name == 'sessions' - current_table_name = ActiveRecord::Base.pluralize_table_names ? 'sessions' : 'session' - end - current_table_name - end - - end - end -end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb deleted file mode 100644 index 9ea3248513..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ /dev/null @@ -1,12 +0,0 @@ -class <%= migration_class_name %> < ActiveRecord::Migration - def change - create_table :<%= session_table_name %> do |t| - t.string :session_id, :null => false - t.text :data - t.timestamps - end - - add_index :<%= session_table_name %>, :session_id - add_index :<%= session_table_name %>, :updated_at - end -end diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb deleted file mode 100644 index a3b8ab74d9..0000000000 --- a/activerecord/test/cases/session_store/session_test.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'cases/helper' -require 'action_dispatch' -require 'active_record/session_store' - -module ActiveRecord - class SessionStore - class SessionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - attr_reader :session_klass - - def setup - super - ActiveRecord::Base.connection.schema_cache.clear! - Session.drop_table! if Session.table_exists? - @session_klass = Class.new(Session) - end - - def test_data_column_name - # default column name is 'data' - assert_equal 'data', Session.data_column_name - end - - def test_table_name - assert_equal 'sessions', Session.table_name - end - - def test_accessible_attributes - assert Session.accessible_attributes.include?(:session_id) - assert Session.accessible_attributes.include?(:data) - assert Session.accessible_attributes.include?(:marshaled_data) - end - - def test_create_table! - assert !Session.table_exists? - Session.create_table! - assert Session.table_exists? - Session.drop_table! - assert !Session.table_exists? - end - - def test_find_by_sess_id_compat - Session.reset_column_information - klass = Class.new(Session) do - def self.session_id_column - 'sessid' - end - end - klass.create_table! - - assert klass.columns_hash['sessid'], 'sessid column exists' - session = klass.new(:data => 'hello') - session.sessid = "100" - session.save! - - found = klass.find_by_session_id("100") - assert_equal session, found - assert_equal session.sessid, found.session_id - ensure - klass.drop_table! - Session.reset_column_information - end - - def test_find_by_session_id - Session.create_table! - session_id = "10" - s = session_klass.create!(:data => 'world', :session_id => session_id) - t = session_klass.find_by_session_id(session_id) - assert_equal s, t - assert_equal s.data, t.data - Session.drop_table! - end - - def test_loaded? - Session.create_table! - s = Session.new - assert !s.loaded?, 'session is not loaded' - end - end - end -end diff --git a/activerecord/test/cases/session_store/sql_bypass_test.rb b/activerecord/test/cases/session_store/sql_bypass_test.rb deleted file mode 100644 index b8cf4cf2cc..0000000000 --- a/activerecord/test/cases/session_store/sql_bypass_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'cases/helper' -require 'action_dispatch' -require 'active_record/session_store' - -module ActiveRecord - class SessionStore - class SqlBypassTest < ActiveRecord::TestCase - def setup - super - Session.drop_table! if Session.table_exists? - end - - def test_create_table - assert !Session.table_exists? - SqlBypass.create_table! - assert Session.table_exists? - SqlBypass.drop_table! - assert !Session.table_exists? - end - - def test_new_record? - s = SqlBypass.new :data => 'foo', :session_id => 10 - assert s.new_record?, 'this is a new record!' - end - - def test_persisted? - s = SqlBypass.new :data => 'foo', :session_id => 10 - assert !s.persisted?, 'this is a new record!' - end - - def test_not_loaded? - s = SqlBypass.new({}) - assert !s.loaded?, 'it is not loaded' - end - - def test_loaded? - s = SqlBypass.new :data => 'hello' - assert s.loaded?, 'it is loaded' - end - - def test_save - SqlBypass.create_table! unless Session.table_exists? - session_id = 20 - s = SqlBypass.new :data => 'hello', :session_id => session_id - s.save - t = SqlBypass.find_by_session_id session_id - assert_equal s.session_id, t.session_id - assert_equal s.data, t.data - end - - def test_destroy - SqlBypass.create_table! unless Session.table_exists? - session_id = 20 - s = SqlBypass.new :data => 'hello', :session_id => session_id - s.save - s.destroy - assert_nil SqlBypass.find_by_session_id session_id - end - - def test_data_column - SqlBypass.drop_table! if exists = Session.table_exists? - old, SqlBypass.data_column = SqlBypass.data_column, 'foo' - SqlBypass.create_table! - - session_id = 20 - SqlBypass.new(:data => 'hello', :session_id => session_id).save - assert_equal 'hello', SqlBypass.find_by_session_id(session_id).data - ensure - SqlBypass.drop_table! - SqlBypass.data_column = old - SqlBypass.create_table! if exists - end - end - end -end |