Skip to content

Commit

Permalink
Merge pull request rails#51174 from Shopify/connection-less-quoting
Browse files Browse the repository at this point in the history
Don't require an active connection for table and column quoting
  • Loading branch information
byroot authored Feb 27, 2024
2 parents 0a9ca01 + 0016280 commit af85f74
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def composite_primary_key? # :nodoc:
# Returns a quoted version of the primary key name, used to construct
# SQL statements.
def quoted_primary_key
@quoted_primary_key ||= connection.quote_column_name(primary_key)
@quoted_primary_key ||= adapter_class.quote_column_name(primary_key)
end

def reset_primary_key # :nodoc:
Expand Down
122 changes: 65 additions & 57 deletions activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,67 @@ module ActiveRecord
module ConnectionAdapters # :nodoc:
# = Active Record Connection Adapters \Quoting
module Quoting
extend ActiveSupport::Concern

module ClassMethods # :nodoc:
# Regexp for column names (with or without a table name prefix).
# Matches the following:
#
# "#{table_name}.#{column_name}"
# "#{column_name}"
def column_name_matcher
/
\A
(
(?:
# table_name.column_name | function(one or no argument)
((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
)
(?:(?:\s+AS)?\s+\w+)?
)
(?:\s*,\s*\g<1>)*
\z
/ix
end

# Regexp for column names with order (with or without a table name prefix,
# with or without various order modifiers). Matches the following:
#
# "#{table_name}.#{column_name}"
# "#{table_name}.#{column_name} #{direction}"
# "#{table_name}.#{column_name} #{direction} NULLS FIRST"
# "#{table_name}.#{column_name} NULLS LAST"
# "#{column_name}"
# "#{column_name} #{direction}"
# "#{column_name} #{direction} NULLS FIRST"
# "#{column_name} NULLS LAST"
def column_name_with_order_matcher
/
\A
(
(?:
# table_name.column_name | function(one or no argument)
((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
)
(?:\s+ASC|\s+DESC)?
(?:\s+NULLS\s+(?:FIRST|LAST))?
)
(?:\s*,\s*\g<1>)*
\z
/ix
end

# Quotes the column name. Must be implemented by subclasses
def quote_column_name(column_name)
raise NotImplementedError
end

# Quotes the table name. Defaults to column name quoting.
def quote_table_name(table_name)
quote_column_name(table_name)
end
end

# Quotes the column value to help prevent
# {SQL injection attacks}[https://en.wikipedia.org/wiki/SQL_injection].
def quote(value)
Expand Down Expand Up @@ -71,14 +132,14 @@ def quote_string(s)
s.gsub("\\", '\&\&').gsub("'", "''") # ' (for ruby-mode)
end

# Quotes the column name. Defaults to no quoting.
# Quotes the column name.
def quote_column_name(column_name)
column_name.to_s
self.class.quote_column_name(column_name)
end

# Quotes the table name. Defaults to column name quoting.
# Quotes the table name.
def quote_table_name(table_name)
quote_column_name(table_name)
self.class.quote_table_name(table_name)
end

# Override to return the quoted table name for assignment. Defaults to
Expand Down Expand Up @@ -159,59 +220,6 @@ def sanitize_as_sql_comment(value) # :nodoc:
comment
end

def column_name_matcher # :nodoc:
COLUMN_NAME
end

def column_name_with_order_matcher # :nodoc:
COLUMN_NAME_WITH_ORDER
end

# Regexp for column names (with or without a table name prefix).
# Matches the following:
#
# "#{table_name}.#{column_name}"
# "#{column_name}"
COLUMN_NAME = /
\A
(
(?:
# table_name.column_name | function(one or no argument)
((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
)
(?:(?:\s+AS)?\s+\w+)?
)
(?:\s*,\s*\g<1>)*
\z
/ix

# Regexp for column names with order (with or without a table name prefix,
# with or without various order modifiers). Matches the following:
#
# "#{table_name}.#{column_name}"
# "#{table_name}.#{column_name} #{direction}"
# "#{table_name}.#{column_name} #{direction} NULLS FIRST"
# "#{table_name}.#{column_name} NULLS LAST"
# "#{column_name}"
# "#{column_name} #{direction}"
# "#{column_name} #{direction} NULLS FIRST"
# "#{column_name} NULLS LAST"
COLUMN_NAME_WITH_ORDER = /
\A
(
(?:
# table_name.column_name | function(one or no argument)
((?:\w+\.)?\w+ | \w+\((?:|\g<2>)\))
)
(?:\s+ASC|\s+DESC)?
(?:\s+NULLS\s+(?:FIRST|LAST))?
)
(?:\s*,\s*\g<1>)*
\z
/ix

private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER

private
def type_casted_binds(binds)
binds.map do |value|
Expand Down
88 changes: 43 additions & 45 deletions activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,52 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module Quoting # :nodoc:
extend ActiveSupport::Concern

QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:

module ClassMethods # :nodoc:
def column_name_matcher
/
\A
(
(?:
# `table_name`.`column_name` | function(one or no argument)
((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
)
(?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
)
(?:\s*,\s*\g<1>)*
\z
/ix
end

def column_name_with_order_matcher
/
\A
(
(?:
# `table_name`.`column_name` | function(one or no argument)
((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
)
(?:\s+COLLATE\s+(?:\w+|"\w+"))?
(?:\s+ASC|\s+DESC)?
)
(?:\s*,\s*\g<1>)*
\z
/ix
end

def quote_column_name(name)
QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub('`', '``')}`".freeze
end

def quote_table_name(name)
QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub('`', '``').gsub(".", "`.`")}`".freeze
end
end

def cast_bound_value(value)
case value
when Rational
Expand All @@ -26,14 +69,6 @@ def cast_bound_value(value)
end
end

def quote_column_name(name)
QUOTED_COLUMN_NAMES[name] ||= "`#{super.gsub('`', '``')}`".freeze
end

def quote_table_name(name)
QUOTED_TABLE_NAMES[name] ||= -super.gsub(".", "`.`").freeze
end

def unquoted_true
1
end
Expand Down Expand Up @@ -81,43 +116,6 @@ def type_cast(value) # :nodoc:
super
end
end

def column_name_matcher
COLUMN_NAME
end

def column_name_with_order_matcher
COLUMN_NAME_WITH_ORDER
end

COLUMN_NAME = /
\A
(
(?:
# `table_name`.`column_name` | function(one or no argument)
((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
)
(?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
)
(?:\s*,\s*\g<1>)*
\z
/ix

COLUMN_NAME_WITH_ORDER = /
\A
(
(?:
# `table_name`.`column_name` | function(one or no argument)
((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
)
(?:\s+COLLATE\s+(?:\w+|"\w+"))?
(?:\s+ASC|\s+DESC)?
)
(?:\s*,\s*\g<1>)*
\z
/ix

private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
end
end
end
Expand Down
Loading

0 comments on commit af85f74

Please sign in to comment.