Skip to content

Commit fd24e5b

Browse files
committed
Refactor Active Record adapters to have a similar internal interface
Adapters have very inconsistent internal APIs to perform queries. This refactoring tries to improve consistency with a common provite API for all of them. Abstract methods: - `raw_execute`: the only method where an adapter should perform a query. It returns a native, adapter specific result object. Does not apply query transformations. Does not check for writes. - `cast_result`: receives the native result object and returns a generic `ActiveRecord::Result`. - `affected_rows`: receives the native result object and returns the number of affected rows. By just implementing these 3 methods all adapters automatically get: - `raw_exec_query`: same as `raw_execute` but returns an `ActiveRecord::Result`. - `internal_exec_query`: same as `raw_exec_query` but check for writes and apply query transformations. - `internal_execute`: same as `internal_exec_query` but retuns the native, adapter specific, result object. With this increased conisistency, we can now reduce the ammount of duplicated code in every adapter. There's some room for futher improvments but I tried to not go too far all at once. Also previously some adapters had a block based query interface that allowed to eagerly clear the native result object. It may make sense to bring that capability back in a consistent way, but short term I opted for consistency.
1 parent 0b3320b commit fd24e5b

18 files changed

+298
-418
lines changed

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

Lines changed: 49 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,57 @@ 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+
raise NotImplementedError
549+
end
550+
551+
# Receive a native adapter result object and returns an ActiveRecord::Result object.
552+
def cast_result(raw_result)
553+
raise NotImplementedError
554+
end
555+
556+
def affected_rows(raw_result)
557+
raise NotImplementedError
558+
end
543559

560+
def preprocess_query(sql)
561+
check_if_write_query(sql)
544562
mark_transaction_written_if_write(sql)
545563

546-
raw_execute(sql, name, allow_retry: allow_retry, materialize_transactions: materialize_transactions)
564+
# We call tranformers after the write checks so we don't add extra parsing work.
565+
# This means we assume no transformer whille change a read for a write
566+
# but it would be insane to do such a thing.
567+
ActiveRecord.query_transformers.each do |transformer|
568+
sql = transformer.call(sql, self)
569+
end
570+
571+
sql
572+
end
573+
574+
# Same as #internal_exec_query, but yields a native adapter result
575+
def internal_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, &block)
576+
sql = preprocess_query(sql)
577+
raw_execute(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions, &block)
547578
end
548579

549580
def execute_batch(statements, name = nil)
550581
statements.each do |statement|
551-
internal_execute(statement, name)
582+
raw_execute(statement, name)
552583
end
553584
end
554585

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

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

667+
# We make sure to run query transformers on the orignal thread
668+
sql = preprocess_query(sql)
640669
future_result = async.new(
641670
pool,
642671
sql,
@@ -649,14 +678,14 @@ def select(sql, name = nil, binds = [], prepare: false, async: false, allow_retr
649678
else
650679
future_result.execute!(self)
651680
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)
681+
future_result
658682
else
659-
result
683+
result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
684+
if async
685+
FutureResult.wrap(result)
686+
else
687+
result
688+
end
660689
end
661690
end
662691

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: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,14 +1106,16 @@ 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

11191121
def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block) # :doc:
@@ -1134,13 +1136,6 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
11341136
raise ex.set_query(sql, binds)
11351137
end
11361138

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-
11441139
def translate_exception(exception, message:, sql:, binds:)
11451140
# override in derived class
11461141
case exception
@@ -1152,7 +1147,7 @@ def translate_exception(exception, message:, sql:, binds:)
11521147
end
11531148

11541149
def without_prepared_statement?(binds)
1155-
!prepared_statements || binds.empty?
1150+
!prepared_statements || binds.nil? || binds.empty?
11561151
end
11571152

11581153
def column_for(table_name, column_name)

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 2 additions & 21 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
@@ -961,13 +944,11 @@ def configure_connection
961944
end.join(", ")
962945

963946
# ...and send them all in one query
964-
internal_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}")
947+
raw_execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
965948
end
966949

967950
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
951+
internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA")
971952
end
972953

973954
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)