Skip to content

Commit 4294d71

Browse files
authored
Merge pull request rails#52428 from Shopify/refactor-adapters
2 parents 0b3320b + 8078ebc commit 4294d71

20 files changed

+328
-465
lines changed

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

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,14 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, retu
163163
# +binds+ as the bind substitutes. +name+ is logged along with
164164
# the executed +sql+ statement.
165165
def exec_delete(sql, name = nil, binds = [])
166-
internal_exec_query(sql, name, binds)
166+
affected_rows(internal_execute(sql, name, binds))
167167
end
168168

169169
# Executes update +sql+ statement in the context of this connection using
170170
# +binds+ as the bind substitutes. +name+ is logged along with
171171
# the executed +sql+ statement.
172172
def exec_update(sql, name = nil, binds = [])
173-
internal_exec_query(sql, name, binds)
173+
affected_rows(internal_execute(sql, name, binds))
174174
end
175175

176176
def exec_insert_all(sql, name) # :nodoc:
@@ -532,30 +532,79 @@ def high_precision_current_timestamp
532532
HIGH_PRECISION_CURRENT_TIMESTAMP
533533
end
534534

535-
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
536-
raise NotImplementedError
535+
# Same as raw_execute but returns an ActiveRecord::Result object.
536+
def raw_exec_query(...) # :nodoc:
537+
cast_result(raw_execute(...))
538+
end
539+
540+
# Execute a query and returns an ActiveRecord::Result
541+
def internal_exec_query(...) # :nodoc:
542+
cast_result(internal_execute(...))
537543
end
538544

539545
private
540-
def internal_execute(sql, name = "SCHEMA", allow_retry: false, materialize_transactions: true)
541-
sql = transform_query(sql)
542-
check_if_write_query(sql)
546+
# Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
547+
def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true)
548+
type_casted_binds = type_casted_binds(binds)
549+
notification_payload = {
550+
sql: sql,
551+
name: name,
552+
binds: binds,
553+
type_casted_binds: type_casted_binds,
554+
async: async,
555+
connection: self,
556+
transaction: current_transaction.user_transaction.presence,
557+
statement_name: nil,
558+
row_count: 0,
559+
}
560+
@instrumenter.instrument("sql.active_record", notification_payload) do
561+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
562+
perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload)
563+
end
564+
rescue ActiveRecord::StatementInvalid => ex
565+
raise ex.set_query(sql, binds)
566+
end
567+
end
568+
569+
def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:)
570+
raise NotImplementedError
571+
end
572+
573+
# Receive a native adapter result object and returns an ActiveRecord::Result object.
574+
def cast_result(raw_result)
575+
raise NotImplementedError
576+
end
543577

578+
def affected_rows(raw_result)
579+
raise NotImplementedError
580+
end
581+
582+
def preprocess_query(sql)
583+
check_if_write_query(sql)
544584
mark_transaction_written_if_write(sql)
545585

546-
raw_execute(sql, name, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
586+
# We call tranformers after the write checks so we don't add extra parsing work.
587+
# This means we assume no transformer whille change a read for a write
588+
# but it would be insane to do such a thing.
589+
ActiveRecord.query_transformers.each do |transformer|
590+
sql = transformer.call(sql, self)
591+
end
592+
593+
sql
594+
end
595+
596+
# Same as #internal_exec_query, but yields a native adapter result
597+
def internal_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, &block)
598+
sql = preprocess_query(sql)
599+
raw_execute(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions, &block)
547600
end
548601

549602
def execute_batch(statements, name = nil)
550603
statements.each do |statement|
551-
internal_execute(statement, name)
604+
raw_execute(statement, name)
552605
end
553606
end
554607

555-
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
556-
raise NotImplementedError
557-
end
558-
559608
DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
560609
private_constant :DEFAULT_INSERT_VALUE
561610

@@ -637,6 +686,8 @@ def select(sql, name = nil, binds = [], prepare: false, async: false, allow_retr
637686
raise AsynchronousQueryInsideTransactionError, "Asynchronous queries are not allowed inside transactions"
638687
end
639688

689+
# We make sure to run query transformers on the orignal thread
690+
sql = preprocess_query(sql)
640691
future_result = async.new(
641692
pool,
642693
sql,
@@ -649,14 +700,14 @@ def select(sql, name = nil, binds = [], prepare: false, async: false, allow_retr
649700
else
650701
future_result.execute!(self)
651702
end
652-
return future_result
653-
end
654-
655-
result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
656-
if async
657-
FutureResult.wrap(result)
703+
future_result
658704
else
659-
result
705+
result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
706+
if async
707+
FutureResult.wrap(result)
708+
else
709+
result
710+
end
660711
end
661712
end
662713

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def sanitize_as_sql_comment(value) # :nodoc:
222222

223223
private
224224
def type_casted_binds(binds)
225-
binds.map do |value|
225+
binds&.map do |value|
226226
if ActiveModel::Attribute === value
227227
type_cast(value.value_for_database)
228228
else

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,24 +1106,25 @@ def type_map
11061106
end
11071107
end
11081108

1109-
def translate_exception_class(e, sql, binds)
1110-
message = "#{e.class.name}: #{e.message}"
1109+
def translate_exception_class(native_error, sql, binds)
1110+
return native_error if native_error.is_a?(ActiveRecordError)
11111111

1112-
exception = translate_exception(
1113-
e, message: message, sql: sql, binds: binds
1112+
message = "#{native_error.class.name}: #{native_error.message}"
1113+
1114+
active_record_error = translate_exception(
1115+
native_error, message: message, sql: sql, binds: binds
11141116
)
1115-
exception.set_backtrace e.backtrace
1116-
exception
1117+
active_record_error.set_backtrace(native_error.backtrace)
1118+
active_record_error
11171119
end
11181120

1119-
def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block) # :doc:
1121+
def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) # :doc:
11201122
@instrumenter.instrument(
11211123
"sql.active_record",
11221124
sql: sql,
11231125
name: name,
11241126
binds: binds,
11251127
type_casted_binds: type_casted_binds,
1126-
statement_name: statement_name,
11271128
async: async,
11281129
connection: self,
11291130
transaction: current_transaction.user_transaction.presence,
@@ -1134,13 +1135,6 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
11341135
raise ex.set_query(sql, binds)
11351136
end
11361137

1137-
def transform_query(sql)
1138-
ActiveRecord.query_transformers.each do |transformer|
1139-
sql = transformer.call(sql, self)
1140-
end
1141-
sql
1142-
end
1143-
11441138
def translate_exception(exception, message:, sql:, binds:)
11451139
# override in derived class
11461140
case exception
@@ -1152,7 +1146,7 @@ def translate_exception(exception, message:, sql:, binds:)
11521146
end
11531147

11541148
def without_prepared_statement?(binds)
1155-
!prepared_statements || binds.empty?
1149+
!prepared_statements || binds.nil? || binds.empty?
11561150
end
11571151

11581152
def column_for(table_name, column_name)

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,6 @@ def index_algorithms
197197

198198
# HELPER METHODS ===========================================
199199

200-
# The two drivers have slightly different ways of yielding hashes of results, so
201-
# this method must be implemented to provide a uniform interface.
202-
def each_hash(result) # :nodoc:
203-
raise NotImplementedError
204-
end
205-
206200
# Must return the MySQL error number from the exception, if the exception has an
207201
# error number.
208202
def error_number(exception) # :nodoc:
@@ -226,17 +220,6 @@ def disable_referential_integrity # :nodoc:
226220
# DATABASE STATEMENTS ======================================
227221
#++
228222

229-
# Mysql2Adapter doesn't have to free a result after using it, but we use this method
230-
# to write stuff in an abstract way without concerning ourselves about whether it
231-
# needs to be explicitly freed or not.
232-
def execute_and_free(sql, name = nil, async: false, allow_retry: false) # :nodoc:
233-
sql = transform_query(sql)
234-
check_if_write_query(sql)
235-
236-
mark_transaction_written_if_write(sql)
237-
yield raw_execute(sql, name, async: async, allow_retry: allow_retry)
238-
end
239-
240223
def begin_db_transaction # :nodoc:
241224
internal_execute("BEGIN", "TRANSACTION", allow_retry: true, materialize_transactions: false)
242225
end
@@ -787,11 +770,6 @@ def warning_ignored?(warning)
787770
warning.level == "Note" || super
788771
end
789772

790-
# Make sure we carry over any changes to ActiveRecord.default_timezone that have been
791-
# made since we established the connection
792-
def sync_timezone_changes(raw_connection)
793-
end
794-
795773
# See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
796774
ER_DB_CREATE_EXISTS = 1007
797775
ER_FILSORT_ABORT = 1028
@@ -961,13 +939,11 @@ def configure_connection
961939
end.join(", ")
962940

963941
# ...and send them all in one query
964-
internal_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}")
942+
raw_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
965943
end
966944

967945
def column_definitions(table_name) # :nodoc:
968-
execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
969-
each_hash(result)
970-
end
946+
internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
971947
end
972948

973949
def create_table_info(table_name) # :nodoc:

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

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,43 @@ module SchemaStatements # :nodoc:
88
def indexes(table_name)
99
indexes = []
1010
current_index = nil
11-
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
12-
each_hash(result) do |row|
13-
if current_index != row[:Key_name]
14-
next if row[:Key_name] == "PRIMARY" # skip the primary key
15-
current_index = row[:Key_name]
16-
17-
mysql_index_type = row[:Index_type].downcase.to_sym
18-
case mysql_index_type
19-
when :fulltext, :spatial
20-
index_type = mysql_index_type
21-
when :btree, :hash
22-
index_using = mysql_index_type
23-
end
24-
25-
indexes << [
26-
row[:Table],
27-
row[:Key_name],
28-
row[:Non_unique].to_i == 0,
29-
[],
30-
lengths: {},
31-
orders: {},
32-
type: index_type,
33-
using: index_using,
34-
comment: row[:Index_comment].presence
35-
]
11+
internal_exec_query("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA").each do |row|
12+
if current_index != row["Key_name"]
13+
next if row["Key_name"] == "PRIMARY" # skip the primary key
14+
current_index = row["Key_name"]
15+
16+
mysql_index_type = row["Index_type"].downcase.to_sym
17+
case mysql_index_type
18+
when :fulltext, :spatial
19+
index_type = mysql_index_type
20+
when :btree, :hash
21+
index_using = mysql_index_type
3622
end
3723

38-
if row[:Expression]
39-
expression = row[:Expression].gsub("\\'", "'")
40-
expression = +"(#{expression})" unless expression.start_with?("(")
41-
indexes.last[-2] << expression
42-
indexes.last[-1][:expressions] ||= {}
43-
indexes.last[-1][:expressions][expression] = expression
44-
indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D"
45-
else
46-
indexes.last[-2] << row[:Column_name]
47-
indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
48-
indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D"
49-
end
24+
indexes << [
25+
row["Table"],
26+
row["Key_name"],
27+
row["Non_unique"].to_i == 0,
28+
[],
29+
lengths: {},
30+
orders: {},
31+
type: index_type,
32+
using: index_using,
33+
comment: row["Index_comment"].presence
34+
]
35+
end
36+
37+
if expression = row["Expression"]
38+
expression = expression.gsub("\\'", "'")
39+
expression = +"(#{expression})" unless expression.start_with?("(")
40+
indexes.last[-2] << expression
41+
indexes.last[-1][:expressions] ||= {}
42+
indexes.last[-1][:expressions][expression] = expression
43+
indexes.last[-1][:orders][expression] = :desc if row["Collation"] == "D"
44+
else
45+
indexes.last[-2] << row["Column_name"]
46+
indexes.last[-1][:lengths][row["Column_name"]] = row["Sub_part"].to_i if row["Sub_part"]
47+
indexes.last[-1][:orders][row["Column_name"]] = :desc if row["Collation"] == "D"
5048
end
5149
end
5250

@@ -182,12 +180,12 @@ def default_type(table_name, field_name)
182180
end
183181

184182
def new_column_from_field(table_name, field, _definitions)
185-
field_name = field.fetch(:Field)
186-
type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
187-
default, default_function = field[:Default], nil
183+
field_name = field.fetch("Field")
184+
type_metadata = fetch_type_metadata(field["Type"], field["Extra"])
185+
default, default_function = field["Default"], nil
188186

189187
if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
190-
default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field[:Extra])
188+
default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field["Extra"])
191189
default, default_function = nil, default
192190
elsif type_metadata.extra == "DEFAULT_GENERATED"
193191
default = +"(#{default})" unless default.start_with?("(")
@@ -203,13 +201,13 @@ def new_column_from_field(table_name, field, _definitions)
203201
end
204202

205203
MySQL::Column.new(
206-
field[:Field],
204+
field["Field"],
207205
default,
208206
type_metadata,
209-
field[:Null] == "YES",
207+
field["Null"] == "YES",
210208
default_function,
211-
collation: field[:Collation],
212-
comment: field[:Comment].presence
209+
collation: field["Collation"],
210+
comment: field["Comment"].presence
213211
)
214212
end
215213

0 commit comments

Comments
 (0)