Skip to content

Commit e82b645

Browse files
authored
Merge pull request rails#54333 from jcoleman/eliminate-queries-when-loading-dumped-model-schema
Eliminate queries loading dumped model schema on Postgres
2 parents b1ec311 + 9ad36e0 commit e82b645

21 files changed

+164
-47
lines changed

activerecord/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Eliminate queries loading dumped schema cache on Postgres
2+
3+
Improve resiliency by avoiding needing to open a database connection to load the
4+
type map while defining attribute methods at boot when a schema cache file is
5+
configured on PostgreSQL databases.
6+
7+
*James Coleman*
8+
19
* `ActiveRecord::Coder::JSON` can be instantiated
210

311
Options can now be passed to `ActiveRecord::Coder::JSON` when instantiating the coder. This allows:

activerecord/lib/active_record/attributes.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,8 @@ def define_attribute(
241241

242242
def _default_attributes # :nodoc:
243243
@default_attributes ||= begin
244-
attributes_hash = with_connection do |connection|
245-
columns_hash.transform_values do |column|
246-
ActiveModel::Attribute.from_database(column.name, column.default, type_for_column(connection, column))
247-
end
244+
attributes_hash = columns_hash.transform_values do |column|
245+
ActiveModel::Attribute.from_database(column.name, column.default, type_for_column(column))
248246
end
249247

250248
attribute_set = ActiveModel::AttributeSet.new(attributes_hash)
@@ -299,7 +297,7 @@ def resolve_type_name(name, **options)
299297
Type.lookup(name, **options, adapter: Type.adapter_name_from(self))
300298
end
301299

302-
def type_for_column(connection, column)
300+
def type_for_column(column)
303301
hook_attribute_type(column.name, super)
304302
end
305303
end

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,8 +624,7 @@ def build_fixture_sql(fixtures, table_name)
624624

625625
columns.map do |name, column|
626626
if fixture.key?(name)
627-
type = lookup_cast_type_from_column(column)
628-
with_yaml_fallback(type.serialize(fixture[name]))
627+
with_yaml_fallback(column.cast_type.serialize(fixture[name]))
629628
else
630629
default_insert_value(column)
631630
end

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,6 @@ def cast_bound_value(value) # :nodoc:
114114
value
115115
end
116116

117-
# If you are having to call this function, you are likely doing something
118-
# wrong. The column does not have sufficient type information if the user
119-
# provided a custom type on the class level either explicitly (via
120-
# Attributes::ClassMethods#attribute) or implicitly (via
121-
# AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+).
122-
# In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to
123-
# represent the type doesn't sufficiently reflect the differences
124-
# (varchar vs binary) for example. The type used to get this primitive
125-
# should have been provided before reaching the connection adapter.
126-
def lookup_cast_type_from_column(column) # :nodoc:
127-
lookup_cast_type(column.sql_type)
128-
end
129-
130117
# Quotes a string, escaping any ' (single quote) and \ (backslash)
131118
# characters.
132119
def quote_string(s)
@@ -159,7 +146,8 @@ def quote_default_expression(value, column) # :nodoc:
159146
if value.is_a?(Proc)
160147
value.call
161148
else
162-
value = lookup_cast_type(column.sql_type).serialize(value)
149+
cast_type = column.respond_to?(:cast_type) ? column.cast_type : lookup_cast_type(column.sql_type)
150+
value = cast_type.serialize(value)
163151
quote(value)
164152
end
165153
end
@@ -232,6 +220,8 @@ def type_casted_binds(binds)
232220
end
233221
end
234222

223+
# In some adapters this executes queries; it should not be used in any
224+
# hot paths. Instead prefer using `column.cast_type`.
235225
def lookup_cast_type(sql_type)
236226
type_map.lookup(sql_type)
237227
end

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def schema_scale(column)
8585

8686
def schema_default(column)
8787
return unless column.has_default?
88-
type = @connection.lookup_cast_type_from_column(column)
88+
type = column.cast_type
8989
default = type.deserialize(column.default)
9090
if default.nil?
9191
schema_expression(column)

activerecord/lib/active_record/connection_adapters/column.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module ConnectionAdapters
77
class Column
88
include Deduplicable
99

10-
attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
10+
attr_reader :name, :cast_type, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
1111

1212
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
1313

@@ -17,8 +17,9 @@ class Column
1717
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
1818
# +sql_type_metadata+ is various information about the type of the column
1919
# +null+ determines if this column allows +NULL+ values.
20-
def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
20+
def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **)
2121
@name = name.freeze
22+
@cast_type = cast_type
2223
@sql_type_metadata = sql_type_metadata
2324
@null = null
2425
@default = default
@@ -45,6 +46,7 @@ def human_name
4546

4647
def init_with(coder)
4748
@name = coder["name"]
49+
@cast_type = coder["cast_type"]
4850
@sql_type_metadata = coder["sql_type_metadata"]
4951
@null = coder["null"]
5052
@default = coder["default"]
@@ -55,6 +57,7 @@ def init_with(coder)
5557

5658
def encode_with(coder)
5759
coder["name"] = @name
60+
coder["cast_type"] = @cast_type
5861
coder["sql_type_metadata"] = @sql_type_metadata
5962
coder["null"] = @null
6063
coder["default"] = @default
@@ -75,6 +78,7 @@ def auto_populated?
7578
def ==(other)
7679
other.is_a?(Column) &&
7780
name == other.name &&
81+
cast_type == other.cast_type &&
7882
default == other.default &&
7983
sql_type_metadata == other.sql_type_metadata &&
8084
null == other.null &&
@@ -88,6 +92,7 @@ def hash
8892
Column.hash ^
8993
name.hash ^
9094
name.encoding.hash ^
95+
cast_type.hash ^
9196
default.hash ^
9297
sql_type_metadata.hash ^
9398
null.hash ^
@@ -114,7 +119,7 @@ def deduplicated
114119

115120
class NullColumn < Column
116121
def initialize(name, **)
117-
super(name, nil)
122+
super(name, nil, nil)
118123
end
119124
end
120125
end

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def new_column_from_field(table_name, field, _definitions)
209209

210210
MySQL::Column.new(
211211
field["Field"],
212+
lookup_cast_type(type_metadata.sql_type),
212213
default,
213214
type_metadata,
214215
field["Null"] == "YES",

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,14 @@ def quoted_binary(value) # :nodoc:
153153
"'#{escape_bytea(value.to_s)}'"
154154
end
155155

156+
# `column` may be either an instance of Column or ColumnDefinition.
156157
def quote_default_expression(value, column) # :nodoc:
157158
if value.is_a?(Proc)
158159
value.call
159160
elsif column.type == :uuid && value.is_a?(String) && value.include?("()")
160161
value # Does not quote function default values for UUID columns
161162
elsif column.respond_to?(:array?)
162-
type = lookup_cast_type_from_column(column)
163-
quote(type.serialize(value))
163+
quote(column.cast_type.serialize(value))
164164
else
165165
super
166166
end
@@ -186,11 +186,6 @@ def type_cast(value) # :nodoc:
186186
end
187187
end
188188

189-
def lookup_cast_type_from_column(column) # :nodoc:
190-
verify! if type_map.nil?
191-
type_map.lookup(column.oid, column.fmod, column.sql_type)
192-
end
193-
194189
private
195190
def lookup_cast_type(sql_type)
196191
super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,7 @@ def new_column_from_field(table_name, field, _definitions)
10011001

10021002
PostgreSQL::Column.new(
10031003
column_name,
1004+
get_oid_type(oid.to_i, fmod.to_i, column_name, type),
10041005
default_value,
10051006
type_metadata,
10061007
!notnull,

activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def new_column_from_field(table_name, field, definitions)
157157

158158
Column.new(
159159
field["name"],
160+
lookup_cast_type(field["type"]),
160161
default_value,
161162
type_metadata,
162163
field["notnull"].to_i == 0,

0 commit comments

Comments
 (0)