diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0a2e7a127c..ab8c54e4cb 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,13 @@ ## Rails 4.0.0 (unreleased) ## +* Session variables can be set for the `mysql`, `mysql2`, and `postgresql` adapters + in the `variables: ` parameter in `database.yml`. The key-value pairs of this + hash will be sent in a `SET key = value` query on new database connections. See also: + http://dev.mysql.com/doc/refman/5.0/en/set-statement.html + http://www.postgresql.org/docs/8.3/static/sql-set.html + + *Aaron Stone* + * Allow `Relation#where` with no arguments to be chained with new `not` query method. Example: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 84e73e6f0f..d37e489f5c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -704,6 +704,45 @@ def column_for(table_name, column_name) end column end + + def configure_connection + variables = @config[:variables] || {} + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variables[:sql_auto_is_null] = 0 + + # Increase timeout so the server doesn't disconnect us. + wait_timeout = @config[:wait_timeout] + wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) + variables[:wait_timeout] = wait_timeout + + # Make MySQL reject illegal values rather than truncating or blanking them, see + # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables + # If the user has provided another value for sql_mode, don't replace it. + if strict_mode? && !variables.has_key?(:sql_mode) + variables[:sql_mode] = 'STRICT_ALL_TABLES' + end + + # NAMES does not have an equals sign, see + # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 + # (trailing comma because variable_assignments will always have content) + encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding] + + # Gather up all of the SET variables... + variable_assignments = variables.map do |k, v| + if v == ':default' || v == :default + "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default + elsif !v.nil? + "@@SESSION.#{k.to_s} = #{quote(v)}" + end + # or else nil; compact to clear nils out + end.compact.join(', ') + + # ...and send them all in one query + execute("SET #{encoding} #{variable_assignments}", :skip_logging) + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index f55d19393c..a6013f754a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -251,27 +251,7 @@ def connect def configure_connection @connection.query_options.merge!(:as => :array) - - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variable_assignments = ['SQL_AUTO_IS_NULL=0'] - - # Make MySQL reject illegal values rather than truncating or - # blanking them. See - # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables - variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" if strict_mode? - - encoding = @config[:encoding] - - # make sure we set the encoding - variable_assignments << "NAMES '#{encoding}'" if encoding - - # increase timeout so mysql server doesn't disconnect us - wait_timeout = @config[:wait_timeout] - wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variable_assignments << "@@wait_timeout = #{wait_timeout}" - - execute("SET #{variable_assignments.join(', ')}", :skip_logging) + super end def version diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e9677415cc..631f646f58 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -51,7 +51,8 @@ module ConnectionAdapters # * :database - The name of the database. No default, must be provided. # * :encoding - (Optional) Sets the client encoding by executing "SET NAMES " after connection. # * :reconnect - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * :strict - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) + # * :strict - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) + # * :variables - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). # * :sslca - Necessary to use MySQL with an SSL connection. # * :sslkey - Necessary to use MySQL with an SSL connection. # * :sslcert - Necessary to use MySQL with an SSL connection. @@ -535,18 +536,10 @@ def connect configure_connection end + # Many Rails applications monkey-patch a replacement of the configure_connection method + # and don't call 'super', so leave this here even though it looks superfluous. def configure_connection - encoding = @config[:encoding] - execute("SET NAMES '#{encoding}'", :skip_logging) if encoding - - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) - - # Make MySQL reject illegal values rather than truncating or - # blanking them. See - # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables - execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) if strict_mode? + super end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e18464fa35..e24ee1efdd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -24,7 +24,7 @@ def postgresql_connection(config) # :nodoc: # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, :schema_order, :adapter, :pool, :checkout_timeout, :template, - :reaping_frequency, :insert_returning].each do |key| + :reaping_frequency, :insert_returning, :variables].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -238,6 +238,8 @@ def simplified_type(field_type) # call on the connection. # * :min_messages - An optional client min messages that is used in a # SET client_min_messages TO call on the connection. + # * :variables - An optional hash of additional parameters that + # will be used in SET SESSION key = val calls on the connection. # * :insert_returning - An optional boolean to control the use or RETURNING for INSERT statements # defaults to true. # @@ -706,11 +708,24 @@ def configure_connection # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. + # (SET TIME ZONE does not use an equals sign like other SET variables) if ActiveRecord::Base.default_timezone == :utc execute("SET time zone 'UTC'", 'SCHEMA') elsif @local_tz execute("SET time zone '#{@local_tz}'", 'SCHEMA') end + + # SET statements from :variables config hash + # http://www.postgresql.org/docs/8.3/static/sql-set.html + variables = @config[:variables] || {} + variables.map do |k, v| + if v == ':default' || v == :default + # Sets the value to the global or compile default + execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA') + elsif !v.nil? + execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA') + end + end end # Returns the current ID of a table's sequence. diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 534dc2c2df..ffd6904aec 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -137,6 +137,23 @@ def test_mysql_strict_mode_disabled_dont_override_global_sql_mode end end + def test_mysql_set_session_variable + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal 3, session_mode.rows.first.first.to_i + end + end + + def test_mysql_set_session_variable_to_default + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) + global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal global_mode.rows, session_mode.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 14c22d2519..1265cb927e 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -53,6 +53,23 @@ def test_mysql_strict_mode_disabled_dont_override_global_sql_mode end end + def test_mysql_set_session_variable + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal 3, session_mode.rows.first.first.to_i + end + end + + def test_mysql_set_session_variable_to_default + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) + global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal global_mode.rows, session_mode.rows + end + end + def test_logs_name_structure_dump @connection.structure_dump assert_equal "SCHEMA", @connection.logged[0][1] diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 1ff307c735..fa8f339f00 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -154,5 +154,46 @@ def test_reconnection_after_actual_disconnection_with_verify end end + def test_set_session_variable_true + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => true}})) + set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" + assert_equal set_true.rows, [["on"]] + end + end + + def test_set_session_variable_false + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => false}})) + set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" + assert_equal set_false.rows, [["off"]] + end + end + + def test_set_session_variable_nil + run_without_connection do |orig_connection| + # This should be a no-op that does not raise an error + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => nil}})) + end + end + + def test_set_session_variable_default + run_without_connection do |orig_connection| + # This should execute a query that does not raise an error + ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}})) + end + end + + private + + def run_without_connection + original_connection = ActiveRecord::Base.remove_connection + begin + yield original_connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + end + end end