Skip to content

Commit e25dbca

Browse files
committed
Support virtual generated columns
1 parent 8b82841 commit e25dbca

File tree

5 files changed

+81
-22
lines changed

5 files changed

+81
-22
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,4 +820,4 @@ DEPENDENCIES
820820
websocket-client-simple
821821

822822
BUNDLED WITH
823-
2.6.2
823+
2.6.9

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ def auto_incremented_by_db?
2626
end
2727

2828
def virtual?
29-
# We assume every generated column is virtual, no matter the concrete type
30-
@generated.present?
29+
@generated == "s" || @generated == "v"
30+
end
31+
32+
def virtual_stored?
33+
@generated == "s"
3134
end
3235

3336
def has_default?

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module PostgreSQL
66
class SchemaCreation < SchemaCreation # :nodoc:
77
private
88
delegate :quoted_include_columns_for_index, to: :@conn
9+
delegate :database_version, to: :@conn
910

1011
def visit_AlterTable(o)
1112
sql = super
@@ -126,17 +127,23 @@ def add_column_options!(sql, options)
126127
end
127128

128129
if as = options[:as]
129-
sql << " GENERATED ALWAYS AS (#{as})"
130+
stored = options[:stored]
130131

131-
if options[:stored]
132-
sql << " STORED"
133-
else
132+
if (stored != true) && database_version < 180_000
134133
raise ArgumentError, <<~MSG
135-
PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
134+
PostgreSQL versions before 18 do not support VIRTUAL (not persisted) generated columns.
136135
Specify 'stored: true' option for '#{options[:column].name}'
137136
MSG
138137
end
138+
139+
sql << " GENERATED ALWAYS AS (#{as})"
140+
if stored == true
141+
sql << " STORED"
142+
elsif stored == false
143+
sql << " VIRTUAL"
144+
end
139145
end
146+
140147
super
141148
end
142149

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ def prepare_column_options(column)
7676

7777
if @connection.supports_virtual_columns? && column.virtual?
7878
spec[:as] = extract_expression_for_virtual_column(column)
79-
spec[:stored] = true
79+
if column.virtual_stored?
80+
spec[:stored] = true
81+
elsif column.virtual? and !column.virtual_stored?
82+
spec[:stored] = false
83+
end
8084
spec = { type: schema_type(column).inspect }.merge!(spec)
8185
end
8286

activerecord/test/cases/adapters/postgresql/virtual_column_test.rb

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ class VirtualColumn < ActiveRecord::Base
1212

1313
def setup
1414
@connection = ActiveRecord::Base.lease_connection
15-
@connection.create_table :virtual_columns, force: true do |t|
16-
t.string :name
17-
t.virtual :upper_name, type: :string, as: "UPPER(name)", stored: true
18-
t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true
19-
t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(name)", stored: true
20-
t.integer :column1
21-
t.virtual :column2, type: :integer, as: "column1 + 1", stored: true
15+
if @connection.database_version >= 18_00_00
16+
@connection.create_table :virtual_columns, force: true do |t|
17+
t.string :name
18+
t.virtual :upper_name, type: :string, as: "UPPER(name)", stored: true
19+
t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true
20+
t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(name)", stored: true
21+
t.integer :column1
22+
t.virtual :column2, type: :integer, as: "column1 + 1", stored: true
23+
t.virtual :column3, type: :integer, as: "column1 + 2", stored: false # only if PostgreSQL >= 18.0
24+
t.virtual :column4, type: :integer, as: "column1 + 3" # only if PostgreSQL >= 18.0
25+
end
26+
else
27+
@connection.create_table :virtual_columns, force: true do |t|
28+
t.string :name
29+
t.virtual :upper_name, type: :string, as: "UPPER(name)", stored: true
30+
t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true
31+
t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(name)", stored: true
32+
t.integer :column1
33+
t.virtual :column2, type: :integer, as: "column1 + 1", stored: true
34+
end
2235
end
36+
2337
VirtualColumn.create(name: "Rails")
2438
end
2539

@@ -47,6 +61,7 @@ def test_virtual_column
4761
def test_stored_column
4862
column = VirtualColumn.columns_hash["name_length"]
4963
assert_predicate column, :virtual?
64+
assert_predicate column, :virtual_stored? if ActiveRecord::Base.lease_connection.database_version >= 18_000
5065
assert_equal 5, VirtualColumn.take.name_length
5166
end
5267

@@ -57,18 +72,44 @@ def test_change_table
5772
VirtualColumn.reset_column_information
5873
column = VirtualColumn.columns_hash["lower_name"]
5974
assert_predicate column, :virtual?
75+
assert_predicate column, :virtual_stored? if ActiveRecord::Base.lease_connection.database_version >= 18_000
6076
assert_equal "rails", VirtualColumn.take.lower_name
6177
end
6278

63-
def test_non_persisted_column
64-
message = <<~MSG
79+
if ActiveRecord::Base.lease_connection.database_version >= 180_000
80+
def test_change_table_as_stored_false
81+
@connection.change_table :virtual_columns do |t|
82+
t.virtual :reversed_name, type: :string, as: "REVERSE(name)", stored: false
83+
end
84+
VirtualColumn.reset_column_information
85+
column = VirtualColumn.columns_hash["reversed_name"]
86+
assert_predicate column, :virtual?
87+
assert_not_predicate column, :virtual_stored?
88+
assert_equal "sliaR", VirtualColumn.take.reversed_name
89+
end
90+
91+
def test_change_table_without_stored_option
92+
@connection.change_table :virtual_columns do |t|
93+
t.virtual :ascii_name, type: :string, as: "ASCII(name)"
94+
end
95+
VirtualColumn.reset_column_information
96+
column = VirtualColumn.columns_hash["ascii_name"]
97+
assert_predicate column, :virtual?
98+
assert_not_predicate column, :virtual_stored?
99+
assert_equal "82", VirtualColumn.take.ascii_name
100+
end
101+
102+
else # ActiveRecord::Base.lease_connection.database_version < 18_000
103+
def test_non_persisted_column
104+
message = <<~MSG
65105
PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
66106
Specify 'stored: true' option for 'invalid_definition'
67-
MSG
107+
MSG
68108

69-
assert_raise ArgumentError, message do
70-
@connection.change_table :virtual_columns do |t|
71-
t.virtual :invalid_definition, type: :string, as: "LOWER(name)"
109+
assert_raise ArgumentError, message do
110+
@connection.change_table :virtual_columns do |t|
111+
t.virtual :invalid_definition, type: :string, as: "LOWER(name)"
112+
end
72113
end
73114
end
74115
end
@@ -79,6 +120,10 @@ def test_schema_dumping
79120
assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "length\(\(name\)::text\)", stored: true$/i, output)
80121
assert_match(/t\.virtual\s+"name_octet_length",\s+type: :integer,\s+as: "octet_length\(\(name\)::text\)", stored: true$/i, output)
81122
assert_match(/t\.virtual\s+"column2",\s+type: :integer,\s+as: "\(column1 \+ 1\)", stored: true$/i, output)
123+
if ActiveRecord::Base.lease_connection.database_version >= 18_00_00
124+
assert_match(/t\.virtual\s+"column3",\s+type: :integer,\s+as: "\(column1 \+ 2\)", stored: false$/i, output)
125+
assert_match(/t\.virtual\s+"column4",\s+type: :integer,\s+as: "\(column1 \+ 3\)", stored: false$/i, output)
126+
end
82127
end
83128

84129
def test_build_fixture_sql

0 commit comments

Comments
 (0)