Skip to content

Commit dcfc7e1

Browse files
committed
Added support for computed columns
1 parent 8217d47 commit dcfc7e1

File tree

8 files changed

+236
-58
lines changed

8 files changed

+236
-58
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
- [#1301](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1301) Add support for `INDEX INCLUDE`.
66
- [#1312](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1312) Add support for `insert_all` and `upsert_all`.
7-
- [#1317](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1317) Reverse order of values when upserting.
7+
- [#](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/) Added support for computed columns.
88

99
#### Changed
1010

1111
- [#1273](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1273) TinyTDS v3+ is now required.
12+
- [#1317](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1317) Reverse order of values when upserting.
1213
- [#1343](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1343) Support more Azure services by changing language source.
1314

1415
#### Fixed

lib/active_record/connection_adapters/sqlserver/schema_creation.rb

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ def supports_index_using?
1212
false
1313
end
1414

15+
def visit_ColumnDefinition(o)
16+
column_sql = super
17+
column_sql = column_sql.sub(" #{o.sql_type}", "") if o.options[:as].present?
18+
column_sql
19+
end
20+
1521
def visit_TableDefinition(o)
1622
if_not_exists = o.if_not_exists
1723

@@ -58,18 +64,17 @@ def quoted_include_columns(o)
5864

5965
def add_column_options!(sql, options)
6066
sql << " DEFAULT #{quote_default_expression_for_column_definition(options[:default], options[:column])}" if options_include_default?(options)
61-
if options[:collation].present?
62-
sql << " COLLATE #{options[:collation]}"
63-
end
64-
if options[:null] == false
65-
sql << " NOT NULL"
66-
end
67-
if options[:is_identity] == true
68-
sql << " IDENTITY(1,1)"
69-
end
70-
if options[:primary_key] == true
71-
sql << " PRIMARY KEY"
67+
68+
sql << " COLLATE #{options[:collation]}" if options[:collation].present?
69+
sql << " NOT NULL" if options[:null] == false
70+
sql << " IDENTITY(1,1)" if options[:is_identity] == true
71+
sql << " PRIMARY KEY" if options[:primary_key] == true
72+
73+
if as = options[:as]
74+
sql << " AS #{as}"
75+
sql << " PERSISTED" if options[:stored]
7276
end
77+
7378
sql
7479
end
7580

lib/active_record/connection_adapters/sqlserver/schema_dumper.rb

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,32 @@ module ActiveRecord
44
module ConnectionAdapters
55
module SQLServer
66
class SchemaDumper < ConnectionAdapters::SchemaDumper
7-
SQLSEVER_NO_LIMIT_TYPES = [
8-
"text",
9-
"ntext",
10-
"varchar(max)",
11-
"nvarchar(max)",
12-
"varbinary(max)"
13-
].freeze
7+
SQLSERVER_NO_LIMIT_TYPES = %w[text ntext varchar(max) nvarchar(max) varbinary(max)].freeze
148

159
private
1610

11+
def prepare_column_options(column)
12+
spec = super
13+
14+
if @connection.supports_virtual_columns? && column.virtual?
15+
spec[:as] = extract_expression_for_virtual_column(column)
16+
spec[:stored] = column.virtual_stored?
17+
spec = { type: schema_type(column).inspect }.merge!(spec)
18+
end
19+
20+
spec
21+
end
22+
23+
def extract_expression_for_virtual_column(column)
24+
column.default_function.inspect
25+
end
26+
1727
def explicit_primary_key_default?(column)
1828
column.type == :integer && !column.is_identity?
1929
end
2030

2131
def schema_limit(column)
22-
return if SQLSEVER_NO_LIMIT_TYPES.include?(column.sql_type)
32+
return if SQLSERVER_NO_LIMIT_TYPES.include?(column.sql_type)
2333

2434
super
2535
end

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,41 +85,58 @@ def index_include_columns(table_name, index_name)
8585
select_all(sql, "SCHEMA").map { |row| row["column_name"] }
8686
end
8787

88-
def columns(table_name)
89-
return [] if table_name.blank?
90-
91-
column_definitions(table_name).map do |ci|
92-
sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :table_name
93-
sql_type_metadata = fetch_type_metadata ci[:type], sqlserver_options
94-
95-
new_column(
96-
ci[:name],
97-
lookup_cast_type(ci[:type]),
98-
ci[:default_value],
99-
sql_type_metadata,
100-
ci[:null],
101-
ci[:default_function],
102-
ci[:collation],
103-
nil,
104-
sqlserver_options
105-
)
88+
89+
# def columns(table_name)
90+
# return [] if table_name.blank?
91+
#
92+
# definitions = column_definitions(table_name)
93+
# definitions.map do |field|
94+
# new_column_from_field(table_name, field, definitions)
95+
# end
96+
# end
97+
98+
def new_column_from_field(table_name, field, definitions)
99+
sqlserver_options = field.slice(:ordinal_position, :is_primary, :is_identity, :table_name)
100+
sql_type_metadata = fetch_type_metadata(field[:type], sqlserver_options)
101+
generated_type = extract_generated_type(field)
102+
103+
104+
105+
106+
if generated_type.present?
107+
# binding.pry if field[:name] == "mutated_name"
108+
109+
110+
default_function = field[:computed_formula]
111+
else
112+
default_function = field[:default_function]
106113
end
107-
end
108114

109-
def new_column(name, cast_type, default, sql_type_metadata, null, default_function = nil, collation = nil, comment = nil, sqlserver_options = {})
115+
110116
SQLServer::Column.new(
111-
name,
112-
cast_type,
113-
default,
117+
field[:name],
118+
lookup_cast_type(field[:type]),
119+
field[:default_value],
114120
sql_type_metadata,
115-
null,
121+
field[:null],
116122
default_function,
117-
collation: collation,
118-
comment: comment,
123+
collation: field[:collation],
124+
comment: nil,
125+
generated_type: generated_type,
119126
**sqlserver_options
120127
)
121128
end
122129

130+
def extract_generated_type(field)
131+
if field[:is_computed]
132+
if field[:is_persisted]
133+
:stored
134+
else
135+
:virtual
136+
end
137+
end
138+
end
139+
123140
def primary_keys(table_name)
124141
primaries = primary_keys_select(table_name)
125142
primaries.present? ? primaries : identity_columns(table_name).map(&:name)
@@ -512,15 +529,19 @@ def column_definitions(table_name)
512529
raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if results.empty?
513530

514531
results.map do |ci|
515-
col = {
516-
name: ci["name"],
517-
numeric_scale: ci["numeric_scale"],
518-
numeric_precision: ci["numeric_precision"],
519-
datetime_precision: ci["datetime_precision"],
520-
collation: ci["collation"],
521-
ordinal_position: ci["ordinal_position"],
522-
length: ci["length"]
523-
}
532+
# col = {
533+
# name: ci["name"],
534+
# numeric_scale: ci["numeric_scale"],
535+
# numeric_precision: ci["numeric_precision"],
536+
# datetime_precision: ci["datetime_precision"],
537+
# collation: ci["collation"],
538+
# ordinal_position: ci["ordinal_position"],
539+
# length: ci["length"],
540+
# is_computed: ci["is_computed"],
541+
# is_persisted: ci["is_persisted"]
542+
# }
543+
544+
col = ci.slice("name", "numeric_scale", "numeric_precision", "datetime_precision", "collation", "ordinal_position", "length", "is_computed", "is_persisted", "computed_formula").symbolize_keys
524545

525546
col[:table_name] = view_exists ? view_table_name(table_name) : table_name
526547
col[:type] = column_type(ci: ci)
@@ -640,7 +661,10 @@ def column_definitions_sql(database, identifier)
640661
WHEN ic.object_id IS NOT NULL
641662
THEN 1
642663
END AS [is_primary],
643-
c.is_identity AS [is_identity]
664+
c.is_identity AS [is_identity],
665+
c.is_computed AS [is_computed],
666+
cc.is_persisted AS [is_persisted],
667+
cc.definition AS [computed_formula]
644668
FROM #{database}.sys.columns c
645669
INNER JOIN #{database}.sys.objects o
646670
ON c.object_id = o.object_id
@@ -659,6 +683,9 @@ def column_definitions_sql(database, identifier)
659683
ON k.parent_object_id = ic.object_id
660684
AND k.unique_index_id = ic.index_id
661685
AND c.column_id = ic.column_id
686+
LEFT OUTER JOIN #{database}.sys.computed_columns cc
687+
ON c.object_id = cc.object_id
688+
AND c.column_id = cc.column_id
662689
WHERE
663690
o.Object_ID = Object_ID(#{object_id_arg})
664691
AND s.name = #{schema_name}

lib/active_record/connection_adapters/sqlserver/table_definition.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def new_column_definition(name, type, **options)
109109
type = :datetime2 unless options[:precision].nil?
110110
when :primary_key
111111
options[:is_identity] = true
112+
when :virtual
113+
type = options[:type]
112114
end
113115

114116
super
@@ -117,7 +119,7 @@ def new_column_definition(name, type, **options)
117119
private
118120

119121
def valid_column_definition_options
120-
super + [:is_identity]
122+
super + [:is_identity, :as, :type, :stored]
121123
end
122124
end
123125

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ def supports_insert_conflict_target?
265265
false
266266
end
267267

268+
def supports_virtual_columns?
269+
true
270+
end
271+
268272
def return_value_after_insert?(column) # :nodoc:
269273
column.is_primary? || column.is_identity?
270274
end

lib/active_record/connection_adapters/sqlserver_column.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ module SQLServer
66
class Column < ConnectionAdapters::Column
77
delegate :is_identity, :is_primary, :table_name, :ordinal_position, to: :sql_type_metadata
88

9-
def initialize(*, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil, **)
9+
def initialize(*, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil, generated_type: nil, **)
1010
super
1111
@is_identity = is_identity
1212
@is_primary = is_primary
1313
@table_name = table_name
1414
@ordinal_position = ordinal_position
15+
@generated_type = generated_type
1516
end
1617

1718
def is_identity?
@@ -31,6 +32,21 @@ def case_sensitive?
3132
collation&.match(/_CS/)
3233
end
3334

35+
def virtual?
36+
37+
# binding.pry
38+
39+
@generated_type.present?
40+
end
41+
42+
def virtual_stored?
43+
@generated_type == :stored
44+
end
45+
46+
def has_default?
47+
super && !virtual?
48+
end
49+
3450
def init_with(coder)
3551
@is_identity = coder["is_identity"]
3652
@is_primary = coder["is_primary"]

0 commit comments

Comments
 (0)