aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorRyuta Kamizono <kamipo@gmail.com>2015-12-15 05:26:14 +0900
committerJeremy Daer <jeremydaer@gmail.com>2017-02-01 22:13:46 -0700
commit65bf1c60053e727835e06392d27a2fb49665484c (patch)
tree02a5a5b0cead003b2d24c2419585748830b4416e /activerecord
parentc98e08df7a303f4c7d6d37aa638d4ce97bb1ec9c (diff)
downloadrails-65bf1c60053e727835e06392d27a2fb49665484c.tar.gz
rails-65bf1c60053e727835e06392d27a2fb49665484c.tar.bz2
rails-65bf1c60053e727835e06392d27a2fb49665484c.zip
Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+
MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html MariaDB virtual columns: https://mariadb.com/kb/en/mariadb/virtual-computed-columns/ Declare virtual columns with `t.virtual name, type: …, as: "expression"`. Pass `stored: true` to persist the generated value (false by default). Example: create_table :generated_columns do |t| t.string :name t.virtual :upper_name, type: :string, as: "UPPER(name)" t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true t.index :name_length # May be indexed, too! end Closes #22589
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb23
-rw-r--r--activerecord/test/cases/adapters/mysql2/virtual_column_test.rb59
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb7
13 files changed, 170 insertions, 28 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index acdf7d40f8..12a037465b 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,22 @@
+* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+.
+
+ MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
+ MariaDB virtual columns: https://mariadb.com/kb/en/mariadb/virtual-computed-columns/
+
+ Declare virtual columns with `t.virtual name, type: …, as: "expression"`.
+ Pass `stored: true` to persist the generated value (false by default).
+
+ Example:
+
+ create_table :generated_columns do |t|
+ t.string :name
+ t.virtual :upper_name, type: :string, as: "UPPER(name)"
+ t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true
+ t.index :name_length # May be indexed, too!
+ end
+
+ *Ryuta Kamizono*
+
* Deprecate `initialize_schema_migrations_table` and `initialize_internal_metadata_table`.
*Ryuta Kamizono*
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 807df2184a..81dec97bf7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -106,6 +106,7 @@ module ActiveRecord
column_options[:primary_key] = o.primary_key
column_options[:collation] = o.collation
column_options[:comment] = o.comment
+ column_options[:as] = o.as
column_options
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index b518ef760b..ecc6caa8f2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -9,7 +9,7 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- ColumnDefinition = Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) do #:nodoc:
+ ColumnDefinition = Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment, :as) do # :nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
end
@@ -173,6 +173,7 @@ module ActiveRecord
:text,
:time,
:timestamp,
+ :virtual,
].each do |column_type|
module_eval <<-CODE, __FILE__, __LINE__ + 1
def #{column_type}(*args, **options)
@@ -374,6 +375,7 @@ module ActiveRecord
column.primary_key = type == :primary_key || options[:primary_key]
column.collation = options[:collation]
column.comment = options[:comment]
+ column.as = options[:as]
column
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index b912d24626..d6c912a69e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -7,7 +7,7 @@ module ActiveRecord
# Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column)
- [schema_type(column), prepare_column_options(column)]
+ [schema_type_with_virtual(column), prepare_column_options(column)]
end
def column_spec_for_primary_key(column)
@@ -59,6 +59,14 @@ module ActiveRecord
schema_type(column) == :bigint
end
+ def schema_type_with_virtual(column)
+ if supports_virtual_columns? && column.virtual?
+ :virtual
+ else
+ schema_type(column)
+ end
+ end
+
def schema_type(column)
if column.bigint?
:bigint
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 348396de72..fd11cab5c0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -346,6 +346,11 @@ module ActiveRecord
true
end
+ # Does this adapter support virtual columns?
+ def supports_virtual_columns?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
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 aedf4581f5..89b4ec9473 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -141,6 +141,14 @@ module ActiveRecord
end
end
+ def supports_virtual_columns?
+ if mariadb?
+ version >= "5.2.0"
+ else
+ version >= "5.7.5"
+ end
+ end
+
def supports_advisory_locks?
true
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
index 1499c1681f..c9ad47c035 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
@@ -15,6 +15,10 @@ module ActiveRecord
def auto_increment?
extra == "auto_increment"
end
+
+ def virtual?
+ /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index d808b50332..39c2acbca9 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -2,8 +2,8 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation
- delegate :add_sql_comment!, to: :@conn
- private :add_sql_comment!
+ delegate :add_sql_comment!, :mariadb?, to: :@conn
+ private :add_sql_comment!, :mariadb?
private
@@ -32,6 +32,7 @@ module ActiveRecord
def column_options(o)
column_options = super
column_options[:charset] = o.charset
+ column_options[:stored] = o.stored
column_options
end
@@ -44,6 +45,13 @@ module ActiveRecord
sql << " COLLATE #{collation}"
end
+ if as = options[:as]
+ sql << " AS (#{as})"
+ if options[:stored]
+ sql << (mariadb? ? " PERSISTENT" : " STORED")
+ end
+ end
+
add_sql_comment!(super, options[:comment])
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 f1ba0cb708..76ebd0bf6c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -3,8 +3,7 @@ module ActiveRecord
module MySQL
module ColumnMethods
def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if [:primary_key, :integer, :bigint].include?(type) && !options.key?(:default)
- options[:limit] = 8 if [:primary_key].include?(type)
+ options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
super
end
@@ -58,24 +57,29 @@ module ActiveRecord
end
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset, :unsigned
+ attr_accessor :charset, :unsigned, :stored
end
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
def new_column_definition(name, type, options) # :nodoc:
- column = super
- case column.type
+ case type
+ when :virtual
+ type = options[:type]
when :primary_key
- column.type = :integer
- column.auto_increment = true
+ type = :integer
+ options[:limit] ||= 8
+ options[:auto_increment] = true
+ options[:primary_key] = true
when /\Aunsigned_(?<type>.+)\z/
- column.type = $~[:type].to_sym
- column.unsigned = true
+ type = $~[:type].to_sym
+ options[:unsigned] = true
end
- column.unsigned ||= options[:unsigned]
+ column = super
+ column.unsigned = options[:unsigned]
column.charset = options[:charset]
+ column.stored = options[:stored]
column
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index d44c35714f..7a277b8cfd 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -14,6 +14,13 @@ module ActiveRecord
def prepare_column_options(column)
spec = super
spec[:unsigned] = "true" if column.unsigned?
+
+ if supports_virtual_columns? && column.virtual?
+ spec[:as] = extract_expression_for_virtual_column(column)
+ spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
+ spec = { type: schema_type(column).inspect }.merge!(spec)
+ end
+
spec
end
@@ -46,6 +53,21 @@ module ActiveRecord
column.collation.inspect if column.collation != @table_collation_cache[table_name]
end
end
+
+ def extract_expression_for_virtual_column(column)
+ if mariadb?
+ create_table_info = create_table_info(column.table_name)
+ if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info
+ $~[:expression].inspect
+ end
+ else
+ sql = "SELECT generation_expression FROM information_schema.columns" \
+ " WHERE table_schema = #{quote(@config[:database])}" \
+ " AND table_name = #{quote(column.table_name)}" \
+ " AND column_name = #{quote(column.name)}"
+ select_value(sql, "SCHEMA").inspect
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
index 135789a57d..c131a5169c 100644
--- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -6,23 +6,27 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
end
test "microsecond precision for MySQL gte 5.6.4" do
- stub_version "5.6.4"
- assert_microsecond_precision
+ stub_version "5.6.4" do
+ assert_microsecond_precision
+ end
end
test "no microsecond precision for MySQL lt 5.6.4" do
- stub_version "5.6.3"
- assert_no_microsecond_precision
+ stub_version "5.6.3" do
+ assert_no_microsecond_precision
+ end
end
test "microsecond precision for MariaDB gte 5.3.0" do
- stub_version "5.5.5-10.1.8-MariaDB-log"
- assert_microsecond_precision
+ stub_version "5.5.5-10.1.8-MariaDB-log" do
+ assert_microsecond_precision
+ end
end
test "no microsecond precision for MariaDB lt 5.3.0" do
- stub_version "5.2.9-MariaDB"
- assert_no_microsecond_precision
+ stub_version "5.2.9-MariaDB" do
+ assert_no_microsecond_precision
+ end
end
private
@@ -41,5 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
def stub_version(full_version_string)
@connection.stubs(:full_version).returns(full_version_string)
@connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ yield
+ ensure
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
new file mode 100644
index 0000000000..442a4fb7b5
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
@@ -0,0 +1,59 @@
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+if ActiveRecord::Base.connection.supports_virtual_columns?
+ class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ class VirtualColumn < ActiveRecord::Base
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :virtual_columns, force: true do |t|
+ t.string :name
+ t.virtual :upper_name, type: :string, as: "UPPER(`name`)"
+ t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true
+ end
+ VirtualColumn.create(name: "Rails")
+ end
+
+ def teardown
+ @connection.drop_table :virtual_columns, if_exists: true
+ VirtualColumn.reset_column_information
+ end
+
+ def test_virtual_column
+ column = VirtualColumn.columns_hash["upper_name"]
+ assert_predicate column, :virtual?
+ assert_match %r{\bVIRTUAL\b}, column.extra
+ assert_equal "RAILS", VirtualColumn.take.upper_name
+ end
+
+ def test_stored_column
+ column = VirtualColumn.columns_hash["name_length"]
+ assert_predicate column, :virtual?
+ assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra
+ assert_equal 5, VirtualColumn.take.name_length
+ end
+
+ def test_change_table
+ @connection.change_table :virtual_columns do |t|
+ t.virtual :lower_name, type: :string, as: "LOWER(name)"
+ end
+ VirtualColumn.reset_column_information
+ column = VirtualColumn.columns_hash["lower_name"]
+ assert_predicate column, :virtual?
+ assert_match %r{\bVIRTUAL\b}, column.extra
+ assert_equal "rails", VirtualColumn.take.lower_name
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("virtual_columns")
+ assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output)
+ assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output)
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 8a4242cf1d..ec817a579b 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -101,12 +101,7 @@ module ActiveRecord
def test_primary_key_creates_primary_key_column
with_change_table do |t|
- if current_adapter?(:Mysql2Adapter)
- @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, { first: true, auto_increment: true, limit: 8, primary_key: true }]
- else
- @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true]
- end
-
+ @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true]
t.primary_key :id, first: true
end
end