Skip to content

Commit 65bf1c6

Browse files
kamipojeremy
authored andcommitted
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
1 parent c98e08d commit 65bf1c6

13 files changed

Lines changed: 170 additions & 28 deletions

File tree

activerecord/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+.
2+
3+
MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
4+
MariaDB virtual columns: https://mariadb.com/kb/en/mariadb/virtual-computed-columns/
5+
6+
Declare virtual columns with `t.virtual name, type: …, as: "expression"`.
7+
Pass `stored: true` to persist the generated value (false by default).
8+
9+
Example:
10+
11+
create_table :generated_columns do |t|
12+
t.string :name
13+
t.virtual :upper_name, type: :string, as: "UPPER(name)"
14+
t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true
15+
t.index :name_length # May be indexed, too!
16+
end
17+
18+
*Ryuta Kamizono*
19+
120
* Deprecate `initialize_schema_migrations_table` and `initialize_internal_metadata_table`.
221

322
*Ryuta Kamizono*

activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def column_options(o)
106106
column_options[:primary_key] = o.primary_key
107107
column_options[:collation] = o.collation
108108
column_options[:comment] = o.comment
109+
column_options[:as] = o.as
109110
column_options
110111
end
111112

activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module ConnectionAdapters #:nodoc:
99
# are typically created by methods in TableDefinition, and added to the
1010
# +columns+ attribute of said TableDefinition object, in order to be used
1111
# for generating a number of table creation or table changing SQL statements.
12-
ColumnDefinition = Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) do #:nodoc:
12+
ColumnDefinition = Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment, :as) do # :nodoc:
1313
def primary_key?
1414
primary_key || type.to_sym == :primary_key
1515
end
@@ -173,6 +173,7 @@ def primary_key(name, type = :primary_key, **options)
173173
:text,
174174
:time,
175175
:timestamp,
176+
:virtual,
176177
].each do |column_type|
177178
module_eval <<-CODE, __FILE__, __LINE__ + 1
178179
def #{column_type}(*args, **options)
@@ -374,6 +375,7 @@ def new_column_definition(name, type, options) # :nodoc:
374375
column.primary_key = type == :primary_key || options[:primary_key]
375376
column.collation = options[:collation]
376377
column.comment = options[:comment]
378+
column.as = options[:as]
377379
column
378380
end
379381

activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module ConnectionAdapters # :nodoc:
77
# Adapter level by over-writing this code inside the database specific adapters
88
module ColumnDumper
99
def column_spec(column)
10-
[schema_type(column), prepare_column_options(column)]
10+
[schema_type_with_virtual(column), prepare_column_options(column)]
1111
end
1212

1313
def column_spec_for_primary_key(column)
@@ -59,6 +59,14 @@ def default_primary_key?(column)
5959
schema_type(column) == :bigint
6060
end
6161

62+
def schema_type_with_virtual(column)
63+
if supports_virtual_columns? && column.virtual?
64+
:virtual
65+
else
66+
schema_type(column)
67+
end
68+
end
69+
6270
def schema_type(column)
6371
if column.bigint?
6472
:bigint

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ def supports_multi_insert?
346346
true
347347
end
348348

349+
# Does this adapter support virtual columns?
350+
def supports_virtual_columns?
351+
false
352+
end
353+
349354
# This is meant to be implemented by the adapters that support extensions
350355
def disable_extension(name)
351356
end

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ def supports_datetime_with_precision?
141141
end
142142
end
143143

144+
def supports_virtual_columns?
145+
if mariadb?
146+
version >= "5.2.0"
147+
else
148+
version >= "5.7.5"
149+
end
150+
end
151+
144152
def supports_advisory_locks?
145153
true
146154
end

activerecord/lib/active_record/connection_adapters/mysql/column.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def case_sensitive?
1515
def auto_increment?
1616
extra == "auto_increment"
1717
end
18+
19+
def virtual?
20+
/\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
21+
end
1822
end
1923
end
2024
end

activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ module ActiveRecord
22
module ConnectionAdapters
33
module MySQL
44
class SchemaCreation < AbstractAdapter::SchemaCreation
5-
delegate :add_sql_comment!, to: :@conn
6-
private :add_sql_comment!
5+
delegate :add_sql_comment!, :mariadb?, to: :@conn
6+
private :add_sql_comment!, :mariadb?
77

88
private
99

@@ -32,6 +32,7 @@ def add_table_options!(create_sql, options)
3232
def column_options(o)
3333
column_options = super
3434
column_options[:charset] = o.charset
35+
column_options[:stored] = o.stored
3536
column_options
3637
end
3738

@@ -44,6 +45,13 @@ def add_column_options!(sql, options)
4445
sql << " COLLATE #{collation}"
4546
end
4647

48+
if as = options[:as]
49+
sql << " AS (#{as})"
50+
if options[:stored]
51+
sql << (mariadb? ? " PERSISTENT" : " STORED")
52+
end
53+
end
54+
4755
add_sql_comment!(super, options[:comment])
4856
end
4957

activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ module ConnectionAdapters
33
module MySQL
44
module ColumnMethods
55
def primary_key(name, type = :primary_key, **options)
6-
options[:auto_increment] = true if [:primary_key, :integer, :bigint].include?(type) && !options.key?(:default)
7-
options[:limit] = 8 if [:primary_key].include?(type)
6+
options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
87
super
98
end
109

@@ -58,24 +57,29 @@ def unsigned_decimal(*args, **options)
5857
end
5958

6059
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
61-
attr_accessor :charset, :unsigned
60+
attr_accessor :charset, :unsigned, :stored
6261
end
6362

6463
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
6564
include ColumnMethods
6665

6766
def new_column_definition(name, type, options) # :nodoc:
68-
column = super
69-
case column.type
67+
case type
68+
when :virtual
69+
type = options[:type]
7070
when :primary_key
71-
column.type = :integer
72-
column.auto_increment = true
71+
type = :integer
72+
options[:limit] ||= 8
73+
options[:auto_increment] = true
74+
options[:primary_key] = true
7375
when /\Aunsigned_(?<type>.+)\z/
74-
column.type = $~[:type].to_sym
75-
column.unsigned = true
76+
type = $~[:type].to_sym
77+
options[:unsigned] = true
7678
end
77-
column.unsigned ||= options[:unsigned]
79+
column = super
80+
column.unsigned = options[:unsigned]
7881
column.charset = options[:charset]
82+
column.stored = options[:stored]
7983
column
8084
end
8185

activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ def column_spec_for_primary_key(column)
1414
def prepare_column_options(column)
1515
spec = super
1616
spec[:unsigned] = "true" if column.unsigned?
17+
18+
if supports_virtual_columns? && column.virtual?
19+
spec[:as] = extract_expression_for_virtual_column(column)
20+
spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
21+
spec = { type: schema_type(column).inspect }.merge!(spec)
22+
end
23+
1724
spec
1825
end
1926

@@ -46,6 +53,21 @@ def schema_collation(column)
4653
column.collation.inspect if column.collation != @table_collation_cache[table_name]
4754
end
4855
end
56+
57+
def extract_expression_for_virtual_column(column)
58+
if mariadb?
59+
create_table_info = create_table_info(column.table_name)
60+
if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info
61+
$~[:expression].inspect
62+
end
63+
else
64+
sql = "SELECT generation_expression FROM information_schema.columns" \
65+
" WHERE table_schema = #{quote(@config[:database])}" \
66+
" AND table_name = #{quote(column.table_name)}" \
67+
" AND column_name = #{quote(column.name)}"
68+
select_value(sql, "SCHEMA").inspect
69+
end
70+
end
4971
end
5072
end
5173
end

0 commit comments

Comments
 (0)