aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb18
-rw-r--r--activerecord/lib/active_record/autosave_association.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb34
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb130
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/column.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb16
-rw-r--r--activerecord/lib/active_record/enum.rb2
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb47
-rw-r--r--activerecord/lib/active_record/migration.rb54
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb5
-rw-r--r--activerecord/lib/active_record/model_schema.rb13
-rw-r--r--activerecord/lib/active_record/railtie.rb7
-rw-r--r--activerecord/lib/active_record/railties/databases.rake18
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb49
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb26
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb10
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb2
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb1
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/lib/active_record/schema.rb3
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/schema_migration.rb2
-rw-r--r--activerecord/lib/active_record/secure_token.rb25
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb18
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb9
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb8
40 files changed, 483 insertions, 275 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 264f869c68..ab3846ae65 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2016 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -40,6 +40,7 @@ module ActiveRecord
autoload :CounterCache
autoload :DynamicMatchers
autoload :Enum
+ autoload :InternalMetadata
autoload :Explain
autoload :Inheritance
autoload :Integration
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 45d2c855a5..061628725d 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -9,9 +9,9 @@ module ActiveRecord
end
def cast(value)
- if value.is_a?(Array)
- value.map { |v| cast(v) }
- elsif value.is_a?(Hash)
+ return if value.nil?
+
+ if value.is_a?(Hash)
set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
@@ -19,18 +19,22 @@ module ActiveRecord
rescue ArgumentError
nil
end
+ else
+ map(super) { |t| cast(t) }
end
end
private
def convert_time_to_time_zone(value)
- if value.is_a?(Array)
- value.map { |v| convert_time_to_time_zone(v) }
- elsif value.acts_like?(:time)
+ return if value.nil?
+
+ if value.acts_like?(:time)
value.in_time_zone
- else
+ elsif value.is_a?(::Float)
value
+ else
+ map(value) { |v| convert_time_to_time_zone(v) }
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index fc12c3f45a..bac5a38a5d 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -331,15 +331,30 @@ module ActiveRecord
validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
unless valid = record.valid?(validation_context)
if reflection.options[:autosave]
+ indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)
+
record.errors.each do |attribute, message|
- if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors)
- attribute = "#{reflection.name}.#{attribute}"
- else
+ if indexed_attribute
attribute = "#{reflection.name}[#{index}].#{attribute}"
+ else
+ attribute = "#{reflection.name}.#{attribute}"
end
errors[attribute] << message
errors[attribute].uniq!
end
+
+ record.errors.details.each_key do |attribute|
+ if indexed_attribute
+ reflection_attribute = "#{reflection.name}[#{index}].#{attribute}"
+ else
+ reflection_attribute = "#{reflection.name}.#{attribute}"
+ end
+
+ record.errors.details[attribute].each do |error|
+ errors.details[reflection_attribute] << error
+ errors.details[reflection_attribute].uniq!
+ end
+ end
else
errors.add(reflection.name)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index d3bc378bea..0ac5e80119 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -58,8 +58,8 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
- def select_values(arel, name = nil)
- arel, binds = binds_from_relation arel, []
+ def select_values(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
select_rows(to_sql(arel, binds), name, binds).map(&:first)
end
@@ -115,20 +115,21 @@ module ActiveRecord
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
- sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
- value = exec_insert(sql, name, binds, pk, sequence_name)
- id_value || last_inserted_id(value)
+ insert_sql(to_sql(arel, binds), name, pk, id_value, sequence_name, binds)
end
+ alias create insert
# Executes the update statement and returns the number of rows affected.
def update(arel, name = nil, binds = [])
exec_update(to_sql(arel, binds), name, binds)
end
+ alias update_sql update
# Executes the delete statement and returns the number of rows affected.
def delete(arel, name = nil, binds = [])
exec_delete(to_sql(arel, binds), name, binds)
end
+ alias delete_sql delete
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
@@ -351,6 +352,13 @@ module ActiveRecord
end
alias join_to_delete join_to_update
+ # Executes an INSERT query and returns the new record's ID
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
+ sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ value = exec_insert(sql, name, binds, pk, sequence_name)
+ id_value || last_inserted_id(value)
+ end
+
protected
# Returns a subquery for the given key using the join information.
@@ -369,22 +377,6 @@ module ActiveRecord
exec_query(sql, name, binds, prepare: true)
end
- # Returns the last auto-generated ID from the affected table.
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- execute(sql, name)
- id_value
- end
-
- # Executes the update statement and returns the number of rows affected.
- def update_sql(sql, name = nil)
- execute(sql, name)
- end
-
- # Executes the delete statement and returns the number of rows affected.
- def delete_sql(sql, name = nil)
- update_sql(sql, name)
- end
-
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
[sql, binds]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 7bf548fcba..70868ebd03 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -968,6 +968,10 @@ module ActiveRecord
ActiveRecord::SchemaMigration.create_table
end
+ def initialize_internal_metadata_table
+ ActiveRecord::InternalMetadata.create_table
+ end
+
def assume_migrated_upto_version(version, migrations_paths)
migrations_paths = Array(migrations_paths)
version = version.to_i
@@ -1028,11 +1032,12 @@ module ActiveRecord
end
# Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they
# require the order columns appear in the SELECT.
#
# columns_for_distinct("posts.id", ["posts.created_at desc"])
- def columns_for_distinct(columns, orders) #:nodoc:
+ #
+ def columns_for_distinct(columns, orders) # :nodoc:
columns
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 295a7bed87..14d04a6388 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -33,6 +33,7 @@ module ActiveRecord
class NullTransaction #:nodoc:
def initialize; end
+ def state; end
def closed?; true; end
def open?; false; end
def joinable?; false; end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 3b8d1c1a9f..dce34a208f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -23,6 +23,7 @@ module ActiveRecord
autoload :TableDefinition
autoload :Table
autoload :AlterTable
+ autoload :ReferenceDefinition
end
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
@@ -391,19 +392,17 @@ module ActiveRecord
def release_savepoint(name = nil)
end
- def case_sensitive_modifier(node, table_attribute)
- node
- end
-
def case_sensitive_comparison(table, attribute, column, value)
- table_attr = table[attribute]
- value = case_sensitive_modifier(value, table_attr) unless value.nil?
- table_attr.eq(value)
+ if value.nil?
+ table[attribute].eq(value)
+ else
+ table[attribute].eq(Arel::Nodes::BindParam.new)
+ end
end
def case_insensitive_comparison(table, attribute, column, value)
if can_perform_case_insensitive_comparison_for?(column)
- table[attribute].lower.eq(table.lower(value))
+ table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new))
else
case_sensitive_comparison(table, attribute, column, value)
end
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 37e4eb24a8..d435d91102 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,7 +1,9 @@
require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/mysql/column'
require 'active_record/connection_adapters/mysql/schema_creation'
require 'active_record/connection_adapters/mysql/schema_definitions'
require 'active_record/connection_adapters/mysql/schema_dumper'
+require 'active_record/connection_adapters/mysql/type_metadata'
require 'active_support/core_ext/string/strip'
@@ -19,78 +21,6 @@ module ActiveRecord
MySQL::SchemaCreation.new(self)
end
- class Column < ConnectionAdapters::Column # :nodoc:
- delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
-
- def initialize(*)
- super
- assert_valid_default(default)
- extract_default
- end
-
- def extract_default
- if blob_or_text_column?
- @default = null || strict ? nil : ''
- end
- end
-
- def has_default?
- return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
- super
- end
-
- def blob_or_text_column?
- sql_type =~ /blob/i || type == :text
- end
-
- def unsigned?
- /unsigned/ === sql_type
- end
-
- def case_sensitive?
- collation && !collation.match(/_ci$/)
- end
-
- def auto_increment?
- extra == 'auto_increment'
- end
-
- private
-
- def assert_valid_default(default)
- if blob_or_text_column? && default.present?
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- end
- end
-
- class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
- attr_reader :extra, :strict
-
- def initialize(type_metadata, extra: "", strict: false)
- super(type_metadata)
- @type_metadata = type_metadata
- @extra = extra
- @strict = strict
- end
-
- def ==(other)
- other.is_a?(MysqlTypeMetadata) &&
- attributes_for_hash == other.attributes_for_hash
- end
- alias eql? ==
-
- def hash
- attributes_for_hash.hash
- end
-
- protected
-
- def attributes_for_hash
- [self.class, @type_metadata, extra, strict]
- end
- end
-
##
# :singleton-method:
# By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt>
@@ -234,7 +164,7 @@ module ActiveRecord
end
def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- Column.new(field, default, sql_type_metadata, null, default_function, collation)
+ MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -393,11 +323,6 @@ module ActiveRecord
yield execute(sql, name)
end
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.affected_rows
- end
-
def begin_db_transaction
execute "BEGIN"
end
@@ -507,6 +432,7 @@ module ActiveRecord
end
def table_exists?(table_name)
+ # Update lib/active_record/internal_metadata.rb when this gets removed
ActiveSupport::Deprecation.warn(<<-MSG.squish)
#table_exists? currently checks both tables and views.
This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
@@ -623,6 +549,7 @@ module ActiveRecord
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
+ create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -751,16 +678,11 @@ module ActiveRecord
SQL
end
- def case_sensitive_modifier(node, table_attribute)
- node = Arel::Nodes.build_quoted node, table_attribute
- Arel::Nodes::Bin.new(node)
- end
-
def case_sensitive_comparison(table, attribute, column, value)
- if column.case_sensitive?
- table[attribute].eq(value)
- else
+ if value.nil? || column.case_sensitive?
super
+ else
+ table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
end
end
@@ -768,10 +690,25 @@ module ActiveRecord
if column.case_sensitive?
super
else
- table[attribute].eq(value)
+ table[attribute].eq(Arel::Nodes::BindParam.new)
end
end
+ # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use
+ # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for
+ # distinct queries, and requires that the ORDER BY include the distinct column.
+ # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
+ def columns_for_distinct(columns, orders) # :nodoc:
+ order_columns = orders.reject(&:blank?).map { |s|
+ # Convert Arel node to string
+ s = s.to_sql unless s.is_a?(String)
+ # Remove any ASC/DESC modifiers
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+
+ [super, *order_columns].join(', ')
+ end
+
def strict_mode?
self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
@@ -824,7 +761,7 @@ module ActiveRecord
def register_integer_type(mapping, key, options) # :nodoc:
mapping.register_type(key) do |sql_type|
- if /unsigned/i =~ sql_type
+ if /\bunsigned\z/ === sql_type
Type::UnsignedInteger.new(options)
else
Type::Integer.new(options)
@@ -841,7 +778,7 @@ module ActiveRecord
end
def fetch_type_metadata(sql_type, extra = "")
- MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
+ MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
end
def add_index_length(option_strings, column_names, options = {})
@@ -951,11 +888,13 @@ module ActiveRecord
subsubselect = select.clone
subsubselect.projections = [key]
+ # Materialize subquery by adding distinct
+ # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
+ subsubselect.distinct unless select.limit || select.offset || select.orders.any?
+
subselect = Arel::SelectManager.new(select.engine)
subselect.project Arel.sql(key.name)
- # Materialized subquery by adding distinct
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
- subselect.from subsubselect.distinct.as('__active_record_temp')
+ subselect.from subsubselect.as('__active_record_temp')
end
def mariadb?
@@ -1018,9 +957,12 @@ module ActiveRecord
end
end
+ def create_table_info_cache # :nodoc:
+ @create_table_info_cache ||= {}
+ end
+
def create_table_info(table_name) # :nodoc:
- @create_table_info_cache = {}
- @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
end
def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
new file mode 100644
index 0000000000..9c45fdd44a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class Column < ConnectionAdapters::Column # :nodoc:
+ delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
+
+ def initialize(*)
+ super
+ assert_valid_default
+ extract_default
+ end
+
+ def has_default?
+ return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
+ super
+ end
+
+ def blob_or_text_column?
+ /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text
+ end
+
+ def unsigned?
+ /\bunsigned\z/ === sql_type
+ end
+
+ def case_sensitive?
+ collation && collation !~ /_ci\z/
+ end
+
+ def auto_increment?
+ extra == 'auto_increment'
+ end
+
+ private
+
+ def extract_default
+ if blob_or_text_column?
+ @default = null || strict ? nil : ''
+ end
+ end
+
+ def assert_valid_default
+ if blob_or_text_column? && default.present?
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index ca7dfda80d..157e75dbf7 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -11,6 +11,30 @@ module ActiveRecord
args.each { |name| column(name, :blob, options) }
end
+ def tinyblob(*args, **options)
+ args.each { |name| column(name, :tinyblob, options) }
+ end
+
+ def mediumblob(*args, **options)
+ args.each { |name| column(name, :mediumblob, options) }
+ end
+
+ def longblob(*args, **options)
+ args.each { |name| column(name, :longblob, options) }
+ end
+
+ def tinytext(*args, **options)
+ args.each { |name| column(name, :tinytext, options) }
+ end
+
+ def mediumtext(*args, **options)
+ args.each { |name| column(name, :mediumtext, options) }
+ end
+
+ def longtext(*args, **options)
+ args.each { |name| column(name, :longtext, options) }
+ end
+
def json(*args, **options)
args.each { |name| column(name, :json, options) }
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
new file mode 100644
index 0000000000..e1e3f7b472
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
+ attr_reader :extra, :strict
+
+ def initialize(type_metadata, extra: "", strict: false)
+ super(type_metadata)
+ @type_metadata = type_metadata
+ @extra = extra
+ @strict = strict
+ end
+
+ def ==(other)
+ other.is_a?(MySQL::TypeMetadata) &&
+ attributes_for_hash == other.attributes_for_hash
+ end
+ alias eql? ==
+
+ def hash
+ attributes_for_hash.hash
+ end
+
+ protected
+
+ def attributes_for_hash
+ [self.class, @type_metadata, extra, strict]
+ end
+ 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 f4686b680c..c3c5b660fd 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -99,33 +99,15 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
- # FIXME: re-enable the following once a "better" query_cache solution is in core
- #
- # The overrides below perform much better than the originals in AbstractAdapter
- # because we're able to take advantage of mysql2's lazy-loading capabilities
- #
- # # Returns a record hash with the column names as keys and column values
- # # as values.
- # def select_one(sql, name = nil)
- # result = execute(sql, name)
- # result.each(as: :hash) do |r|
- # return r
- # end
- # end
- #
- # # Returns a single value from a record
- # def select_value(sql, name = nil)
- # result = execute(sql, name)
- # if first = result.first
- # first.first
- # end
- # end
- #
- # # Returns an array of the values of the first column in a select:
- # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
- # def select_values(sql, name = nil)
- # execute(sql, name).map { |row| row.first }
- # end
+ # Returns a record hash with the column names as keys and column values
+ # as values.
+ def select_one(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation(arel, binds)
+ execute(to_sql(arel, binds), name).each(as: :hash) do |row|
+ @connection.next_result while @connection.more_results?
+ return row
+ end
+ end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
@@ -154,12 +136,6 @@ module ActiveRecord
alias exec_without_stmt exec_query
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
- super
- id_value || @connection.last_id
- end
- alias :create :insert_sql
-
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
execute to_sql(sql, binds), name
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 0e0c0e993a..11a151edd5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -52,8 +52,8 @@ module ActiveRecord
end
end
- def select_values(arel, name = nil)
- arel, binds = binds_from_relation arel, []
+ def select_values(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
execute_and_clear(sql, name, binds) do |result|
if result.nfields > 0
@@ -73,25 +73,13 @@ module ActiveRecord
end
# Executes an INSERT query and returns the new record's ID
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) # :nodoc:
unless pk
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
pk = primary_key(table_ref) if table_ref
end
-
- if pk && use_insert_returning?
- select_value("#{sql} RETURNING #{quote_column_name(pk)}")
- elsif pk
- super
- last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk))
- else
- super
- end
- end
-
- def create
- super.insert
+ super
end
# The internal PostgreSQL identifier of the money data type.
@@ -175,12 +163,6 @@ module ActiveRecord
alias :exec_update :exec_delete
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- unless pk
- # Extract the table from the insert sql. Yuck.
- table_ref = extract_table_ref_from_insert_sql(sql)
- pk = primary_key(table_ref) if table_ref
- end
-
if pk && use_insert_returning?
sql = "#{sql} RETURNING #{quote_column_name(pk)}"
end
@@ -202,11 +184,6 @@ module ActiveRecord
end
end
- # Executes an UPDATE query and returns the number of affected tuples.
- def update_sql(sql, name = nil)
- super.cmd_tuples
- end
-
# Begins a transaction.
def begin_db_transaction
execute "BEGIN"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index 25961a9869..87593ef704 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -50,6 +50,10 @@ module ActiveRecord
"[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
end
+ def map(value, &block)
+ value.map(&block)
+ end
+
private
def type_cast_array(value, method)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index fc201f8fb9..a8d2310035 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -6,6 +6,7 @@ module ActiveRecord
module OID # :nodoc:
class Range < Type::Value # :nodoc:
attr_reader :subtype, :type
+ delegate :user_input_in_time_zone, to: :subtype
def initialize(subtype, type = :range)
@subtype = subtype
@@ -18,7 +19,7 @@ module ActiveRecord
def cast_value(value)
return if value == 'empty'
- return value if value.is_a?(::Range)
+ return value unless value.is_a?(::String)
extracted = extract_bounds(value)
from = type_cast_single extracted[:from]
@@ -46,6 +47,12 @@ module ActiveRecord
other.type == type
end
+ def map(value) # :nodoc:
+ new_begin = yield(value.begin)
+ new_end = yield(value.end)
+ ::Range.new(new_begin, new_end, value.exclude_end?)
+ end
+
private
def type_cast_single(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index a1ec570042..e313a34c3a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -390,12 +390,12 @@ module ActiveRecord
"average" => "avg",
}
- protected
+ # Returns the version of the connected PostgreSQL server.
+ def postgresql_version
+ @connection.server_version
+ end
- # Returns the version of the connected PostgreSQL server.
- def postgresql_version
- @connection.server_version
- end
+ protected
# See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
FOREIGN_KEY_VIOLATION = "23503"
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 163cbb875f..d1893f35f5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -280,22 +280,6 @@ module ActiveRecord
log(sql, name) { @connection.execute(sql) }
end
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.changes
- end
-
- def delete_sql(sql, name = nil) #:nodoc:
- sql += " WHERE 1=1" unless sql =~ /WHERE/i
- super sql, name
- end
-
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
- super
- id_value || @connection.last_insert_row_id
- end
- alias :create :insert_sql
-
def select_rows(sql, name = nil, binds = [])
exec_query(sql, name, binds).rows
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 7ded96f8fb..04519f4dc3 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -124,7 +124,7 @@ module ActiveRecord
def deserialize(value)
return if value.nil?
- mapping.key(value.to_i)
+ mapping.key(value)
end
def serialize(value)
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
new file mode 100644
index 0000000000..2e962f4218
--- /dev/null
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -0,0 +1,47 @@
+require 'active_record/scoping/default'
+require 'active_record/scoping/named'
+
+module ActiveRecord
+ # This class is used to create a table that keeps track of values and keys such
+ # as which environment migrations were run in.
+ class InternalMetadata < ActiveRecord::Base # :nodoc:
+ class << self
+ def primary_key
+ "key"
+ end
+
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ end
+
+ def index_name
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
+ end
+
+ def []=(key, value)
+ first_or_initialize(key: key).update_attributes!(value: value)
+ end
+
+ def [](key)
+ where(key: key).pluck(:value).first
+ end
+
+ def table_exists?
+ ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
+ end
+
+ # Creates an internal metadata table with columns +key+ and +value+
+ def create_table
+ unless table_exists?
+ connection.create_table(table_name, primary_key: :key, id: false ) do |t|
+ t.column :key, :string
+ t.column :value, :string
+ t.timestamps
+ end
+
+ connection.add_index table_name, :key, unique: true, name: index_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index ba238ea142..f699e83ab3 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -126,9 +126,9 @@ module ActiveRecord
class PendingMigrationError < MigrationError#:nodoc:
def initialize(message = nil)
if !message && defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}.")
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}")
elsif !message
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate.")
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate")
else
super
end
@@ -143,6 +143,35 @@ module ActiveRecord
end
end
+ class NoEnvironmentInSchemaError < MigrationError #:nodoc:
+ def initialize
+ msg = "Environment data not found in the schema. To resolve this issue, run: \n\n\tbin/rake db:migrate"
+ if defined?(Rails.env)
+ super("#{msg} RAILS_ENV=#{::Rails.env}")
+ else
+ super(msg)
+ end
+ end
+ end
+
+ class ProtectedEnvironmentError < ActiveRecordError #:nodoc:
+ def initialize(env = "production")
+ msg = "You are attempting to run a destructive action against your '#{env}' database\n"
+ msg << "if you are sure you want to continue, run the same command with the environment variable\n"
+ msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
+ super(msg)
+ end
+ end
+
+ class EnvironmentMismatchError < ActiveRecordError
+ def initialize(current: nil, stored: nil)
+ msg = "You are attempting to modify a database that was last run in #{ stored } environment.\n"
+ msg << "You are running in #{ current } environment."
+ msg << "if you are sure you want to continue, run the same command with the environment variable\n"
+ msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
+ end
+ end
+
# = Active Record Migrations
#
# Migrations can manage the evolution of a schema used by several physical
@@ -1078,6 +1107,7 @@ module ActiveRecord
validate(@migrations)
Base.connection.initialize_schema_migrations_table
+ Base.connection.initialize_internal_metadata_table
end
def current_version
@@ -1202,10 +1232,28 @@ module ActiveRecord
ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all
else
migrated << version
- ActiveRecord::SchemaMigration.create!(:version => version.to_s)
+ ActiveRecord::SchemaMigration.create!(version: version.to_s)
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
end
end
+ def self.last_stored_environment
+ return nil if current_version == 0
+ raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
+
+ environment = ActiveRecord::InternalMetadata[:environment]
+ raise NoEnvironmentInSchemaError unless environment
+ environment
+ end
+
+ def self.current_environment
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
+
+ def self.protected_environment?
+ ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
+ end
+
def up?
@direction == :up
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 831bfa2df3..1b94573870 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -41,8 +41,9 @@ module ActiveRecord
end
def remove_index(table_name, options = {})
- index_name = index_name_for_remove(table_name, options)
- execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
+ options = { column: options } unless options.is_a?(Hash)
+ options[:name] = index_name_for_remove(table_name, options)
+ super(table_name, options)
end
private
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index a6a68f3d4b..f26c8471bc 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -44,6 +44,19 @@ module ActiveRecord
##
# :singleton-method:
+ # Accessor for the name of the internal metadata table. By default, the value is "active_record_internal_metadatas"
+ class_attribute :internal_metadata_table_name, instance_accessor: false
+ self.internal_metadata_table_name = "active_record_internal_metadatas"
+
+ ##
+ # :singleton-method:
+ # Accessor for an array of names of environments where destructive actions should be prohibited. By default,
+ # the value is ["production"]
+ class_attribute :protected_environments, instance_accessor: false
+ self.protected_environments = ["production"]
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index f5e69ec4fb..38916f7376 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -57,8 +57,10 @@ module ActiveRecord
console do |app|
require "active_record/railties/console_sandbox" if app.sandbox?
require "active_record/base"
- console = ActiveSupport::Logger.new(STDERR)
- Rails.logger.extend ActiveSupport::Logger.broadcast console
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
+ console = ActiveSupport::Logger.new(STDERR)
+ Rails.logger.extend ActiveSupport::Logger.broadcast console
+ end
end
runner do
@@ -69,6 +71,7 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) do
self.time_zone_aware_attributes = true
self.default_timezone = :utc
+ self.time_zone_aware_types = ActiveRecord::Base.time_zone_aware_types
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 9b59ee995a..ead9407391 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,6 +1,10 @@
require 'active_record'
db_namespace = namespace :db do
+ task :check_protected_environments => [:environment, :load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+
task :load_config do
ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
@@ -18,24 +22,28 @@ db_namespace = namespace :db do
end
namespace :drop do
- task :all => :load_config do
+ task :all => [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
end
desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.'
- task :drop => [:load_config] do
+ task :drop => [:load_config, :check_protected_environments] do
+ db_namespace["drop:_unsafe"].invoke
+ end
+
+ task "drop:_unsafe" => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
namespace :purge do
- task :all => :load_config do
+ task :all => [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
# desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
- task :purge => [:load_config] do
+ task :purge => [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_current
end
@@ -351,7 +359,7 @@ db_namespace = namespace :db do
task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => %w(environment load_config) do
+ task :purge => %w(environment load_config check_protected_environments) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index a549b28f16..37e18626b5 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -545,7 +545,7 @@ module ActiveRecord
end
end
- # returns either nil or the inverse association name that it finds.
+ # returns either false or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 316b0d6308..6aa490908b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -132,7 +132,7 @@ module ActiveRecord
# ==== Examples
#
# users = User.where(name: 'Oscar')
- # users.create # => #<User id: 3, name: "oscar", ...>
+ # users.create # => #<User id: 3, name: "Oscar", ...>
#
# users.create(name: 'fxn')
# users.create # => #<User id: 4, name: "fxn", ...>
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 19244bcf95..3cbb12a09d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -117,9 +117,9 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_nth_with_limit(offset_index, limit)
+ find_nth_with_limit_and_offset(0, limit, offset: offset_index)
else
- find_nth(0, offset_index)
+ find_nth 0
end
end
@@ -169,7 +169,7 @@ module ActiveRecord
# Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
# Person.where(["user_name = :u", { u: user_name }]).second
def second
- find_nth(1, offset_index)
+ find_nth 1
end
# Same as #second but raises ActiveRecord::RecordNotFound if no record
@@ -185,7 +185,7 @@ module ActiveRecord
# Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
# Person.where(["user_name = :u", { u: user_name }]).third
def third
- find_nth(2, offset_index)
+ find_nth 2
end
# Same as #third but raises ActiveRecord::RecordNotFound if no record
@@ -201,7 +201,7 @@ module ActiveRecord
# Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
# Person.where(["user_name = :u", { u: user_name }]).fourth
def fourth
- find_nth(3, offset_index)
+ find_nth 3
end
# Same as #fourth but raises ActiveRecord::RecordNotFound if no record
@@ -217,7 +217,7 @@ module ActiveRecord
# Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
# Person.where(["user_name = :u", { u: user_name }]).fifth
def fifth
- find_nth(4, offset_index)
+ find_nth 4
end
# Same as #fifth but raises ActiveRecord::RecordNotFound if no record
@@ -233,7 +233,7 @@ module ActiveRecord
# Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
# Person.where(["user_name = :u", { u: user_name }]).forty_two
def forty_two
- find_nth(41, offset_index)
+ find_nth 41
end
# Same as #forty_two but raises ActiveRecord::RecordNotFound if no record
@@ -488,27 +488,39 @@ module ActiveRecord
end
end
- def find_nth(index, offset)
+ def find_nth(index, offset = nil)
if loaded?
@records[index]
else
- offset += index
- @offsets[offset] ||= find_nth_with_limit(offset, 1).first
+ # TODO: once the offset argument is removed we rely on offset_index
+ # within find_nth_with_limit, rather than pass it in via
+ # find_nth_with_limit_and_offset
+ if offset
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing an offset argument to find_nth is deprecated,
+ please use Relation#offset instead.
+ MSG
+ else
+ offset = offset_index
+ end
+ @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first
end
end
def find_nth!(index)
- find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
- def find_nth_with_limit(offset, limit)
+ def find_nth_with_limit(index, limit)
+ # TODO: once the offset argument is removed from find_nth,
+ # find_nth_with_limit_and_offset can be merged into this method
relation = if order_values.empty? && primary_key
order(arel_table[primary_key].asc)
else
self
end
- relation = relation.offset(offset) unless offset.zero?
+ relation = relation.offset(index) unless index.zero?
relation.limit(limit).to_a
end
@@ -524,5 +536,16 @@ module ActiveRecord
end
end
end
+
+ private
+
+ def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc:
+ if loaded?
+ @records[index, limit]
+ else
+ index += offset
+ find_nth_with_limit(index, limit)
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb
index 92340216ed..8945cb0cc5 100644
--- a/activerecord/lib/active_record/relation/from_clause.rb
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -25,7 +25,7 @@ module ActiveRecord
end
def self.empty
- new(nil, nil)
+ @empty ||= new(nil, nil)
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 983bf019bc..5635b4215b 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -54,16 +54,17 @@ module ActiveRecord
end
end
+ FROZEN_EMPTY_ARRAY = [].freeze
Relation::MULTI_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_values # def select_values
- @values[:#{name}] || [] # @values[:select] || []
- end # end
- #
- def #{name}_values=(values) # def select_values=(values)
- assert_mutability! # assert_mutability!
- @values[:#{name}] = values # @values[:select] = values
- end # end
+ def #{name}_values
+ @values[:#{name}] || FROZEN_EMPTY_ARRAY
+ end
+
+ def #{name}_values=(values)
+ assert_mutability!
+ @values[:#{name}] = values
+ end
CODE
end
@@ -116,8 +117,9 @@ module ActiveRecord
result
end
+ FROZEN_EMPTY_HASH = {}.freeze
def create_with_value # :nodoc:
- @values[:create_with] || {}
+ @values[:create_with] || FROZEN_EMPTY_HASH
end
alias extensions extending_values
@@ -649,8 +651,8 @@ module ActiveRecord
# they must differ only by #where (if no #group has been defined) or #having (if a #group is
# present). Neither relation may have a #limit, #offset, or #distinct set.
#
- # Post.where("id = 1").or(Post.where("id = 2"))
- # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
+ # Post.where("id = 1").or(Post.where("author_id = 3"))
+ # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3'))
#
def or(other)
spawn.or!(other)
@@ -1190,7 +1192,7 @@ module ActiveRecord
def structurally_compatible_for_or?(other)
Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
(Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } &&
- (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
+ (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") == other.send("#{m}_clause") }
end
def new_where_clause
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
index 0a1814b3dd..dbd08811fa 100644
--- a/activerecord/lib/active_record/relation/record_fetch_warning.rb
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -24,9 +24,7 @@ module ActiveRecord
end
# :stopdoc:
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
- payload = args.last
-
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
QueryRegistry.queries << payload[:sql]
end
# :startdoc:
@@ -34,14 +32,14 @@ module ActiveRecord
class QueryRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
- attr_accessor :queries
+ attr_reader :queries
def initialize
- reset
+ @queries = []
end
def reset
- @queries = []
+ @queries.clear
end
end
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 1f000b3f0f..2c2d6cfa47 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -81,7 +81,7 @@ module ActiveRecord
end
def self.empty
- new([], [])
+ @empty ||= new([], [])
end
protected
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index a81ff98e49..dbf172a577 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -22,6 +22,7 @@ module ActiveRecord
parts = predicate_builder.build_from_hash(attributes)
when Arel::Nodes::Node
parts = [opts]
+ binds = other
else
raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 4e89ba4dd1..2bfc5ff7ae 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -213,7 +213,7 @@ module ActiveRecord
end
# TODO: Deprecate this
- def quoted_id
+ def quoted_id # :nodoc:
self.class.quote_value(@attributes[self.class.primary_key].value_for_database)
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index fdf9965a82..784a02d2c3 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -51,6 +51,9 @@ module ActiveRecord
initialize_schema_migrations_table
connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
+
+ ActiveRecord::InternalMetadata.create_table
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
end
private
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 2362dae9fc..65005bd44b 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -254,7 +254,7 @@ HEADER
end
def ignored?(table_name)
- [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored|
+ [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored|
ignored === remove_prefix_and_suffix(table_name)
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 51b9b17395..3017ee0bbb 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -9,7 +9,7 @@ module ActiveRecord
class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
def primary_key
- nil
+ "version"
end
def table_name
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index 8abda2ac49..f10f7a1515 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -20,14 +20,35 @@ module ActiveRecord
#
# <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely.
#
+ # A secure token can also be only created given a condition, for example if a user should only have an
+ # auto-generated invitation token if the user was invited:
+ #
+ # # Schema: User(token:string, invited:boolean)
+ # class User < ActiveRecord::Base
+ # has_secure_token if: :invited?
+ # end
+ #
+ # user = User.new(invited: true)
+ # user.save
+ # user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
+ #
+ # user = User.new(invited: false)
+ # user.save
+ # user.token # => nil
+ #
+ # The secure token creation supports all the options a `before_create` does - like +:if+ and +:unless+.
+ #
# Note that it's still possible to generate a race condition in the database in the same way that
# {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can.
# You're encouraged to add a unique index in the database to deal with this even more unlikely scenario.
- def has_secure_token(attribute = :token)
+ def has_secure_token(attribute = :token, **before_create_options)
# Load securerandom only when has_secure_token is used.
require 'active_support/core_ext/securerandom'
+
define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token }
- before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")}
+ before_create(before_create_options) do
+ self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")
+ end
end
def generate_unique_secure_token
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index b6fba0cf79..8f52e9068a 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -42,6 +42,22 @@ module ActiveRecord
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
+ def check_protected_environments!
+ unless ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK']
+ current = ActiveRecord::Migrator.current_environment
+ stored = ActiveRecord::Migrator.last_stored_environment
+
+ if ActiveRecord::Migrator.protected_environment?
+ raise ActiveRecord::ProtectedEnvironmentError.new(stored)
+ end
+
+ if stored && stored != current
+ raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
+ end
+ end
+ rescue ActiveRecord::NoDatabaseError
+ end
+
def register_task(pattern, task)
@tasks ||= {}
@tasks[pattern] = task
@@ -204,6 +220,8 @@ module ActiveRecord
else
raise ArgumentError, "unknown format #{format.inspect}"
end
+ ActiveRecord::InternalMetadata.create_table
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
end
def load_schema_for(*args)
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index edc1325b25..a376e2a17f 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -73,15 +73,18 @@ module ActiveRecord
value = value.to_s[0, column.limit]
end
- value = Arel::Nodes::Quoted.new(value)
-
comparison = if !options[:case_sensitive] && !value.nil?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
- klass.unscoped.where(comparison)
+ if value.nil?
+ klass.unscoped.where(comparison)
+ else
+ bind = Relation::QueryAttribute.new(attribute.to_s, value, Type::Value.new)
+ klass.unscoped.where(comparison, bind)
+ end
rescue RangeError
klass.none
end
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 15aecf28ca..7395839fca 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -45,7 +45,13 @@ module ActiveRecord
def determine_default_parent_class
application_record = nil
- in_root { application_record = File.exist?('app/models/application_record.rb') }
+ in_root do
+ application_record = if mountable_engine?
+ File.exist?("app/models/#{namespaced_path}/application_record.rb")
+ else
+ File.exist?('app/models/application_record.rb')
+ end
+ end
if application_record
"ApplicationRecord"