aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb107
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb38
26 files changed, 324 insertions, 139 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 0059f0b773..2299fc0214 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -98,6 +98,11 @@ module ActiveRecord
exec_query(sql, name).rows
end
+ # Determines whether the SQL statement is a write query.
+ def write_query?(sql)
+ raise NotImplementedError
+ end
+
# Executes the SQL statement in the context of this connection and returns
# the raw result from the connection adapter.
# Note: depending on your database connector, the result returned by this
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 9d9e8a4110..2cb0a2a4df 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -39,7 +39,9 @@ module ActiveRecord
end
def visit_TableDefinition(o)
- create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
+ create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE "
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
+ create_sql << "#{quote_table_name(o.name)} "
statements = o.columns.map { |c| accept c }
statements << accept(o.primary_keys) if o.primary_keys
@@ -119,6 +121,11 @@ module ActiveRecord
sql
end
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
+ def table_modifier_in_create(o)
+ " TEMPORARY" if o.temporary
+ end
+
def foreign_key_in_create(from_table, to_table, options)
options = foreign_key_options(from_table, to_table, options)
accept ForeignKeyDefinition.new(from_table, to_table, options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 015204c056..db489143af 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -256,15 +258,25 @@ module ActiveRecord
class TableDefinition
include ColumnMethods
- attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment
+ attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
+ attr_writer :indexes
+ deprecate :indexes=
- def initialize(name, temporary = false, options = nil, as = nil, comment: nil)
+ def initialize(
+ name,
+ temporary: false,
+ if_not_exists: false,
+ options: nil,
+ as: nil,
+ comment: nil,
+ **
+ )
@columns_hash = {}
@indexes = []
@foreign_keys = []
@primary_keys = nil
@temporary = temporary
+ @if_not_exists = if_not_exists
@options = options
@as = as
@name = name
@@ -348,8 +360,8 @@ module ActiveRecord
#
# create_table :taggings do |t|
# t.references :tag, index: { name: 'index_taggings_on_tag_id' }
- # t.references :tagger, polymorphic: true, index: true
- # t.references :taggable, polymorphic: { default: 'Photo' }
+ # t.references :tagger, polymorphic: true
+ # t.references :taggable, polymorphic: { default: 'Photo' }, index: false
# end
def column(name, type, options = {})
name = name.to_s
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 723d8c318d..38cfc3a241 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -205,6 +205,9 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Set to +:cascade+ to drop dependent objects as well.
# Defaults to false.
+ # [<tt>:if_not_exists</tt>]
+ # Set to true to avoid raising an error when the table already exists.
+ # Defaults to false.
# [<tt>:as</tt>]
# SQL to use to generate the table. When this option is used, the block is
# ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
@@ -287,8 +290,8 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, comment: nil, **options)
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment
+ def create_table(table_name, **options)
+ td = create_table_definition(table_name, options)
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
@@ -317,7 +320,9 @@ module ActiveRecord
end
if supports_comments? && !supports_comments_in_create?
- change_table_comment(table_name, comment) if comment.present?
+ if table_comment = options[:comment].presence
+ change_table_comment(table_name, table_comment)
+ end
td.columns.each do |column|
change_column_comment(table_name, column.name, column.comment) if column.comment.present?
@@ -846,17 +851,17 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id bigint column
+ # ====== Create a user_id bigint column without a index
#
- # add_reference(:products, :user)
+ # add_reference(:products, :user, index: false)
#
# ====== Create a user_id string column
#
# add_reference(:products, :user, type: :string)
#
- # ====== Create supplier_id, supplier_type columns and appropriate index
+ # ====== Create supplier_id, supplier_type columns
#
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ # add_reference(:products, :supplier, polymorphic: true)
#
# ====== Create a supplier_id column with a unique index
#
@@ -884,7 +889,7 @@ module ActiveRecord
#
# ====== Remove the reference
#
- # remove_reference(:products, :user, index: true)
+ # remove_reference(:products, :user, index: false)
#
# ====== Remove polymorphic reference
#
@@ -892,7 +897,7 @@ module ActiveRecord
#
# ====== Remove the reference with a foreign key
#
- # remove_reference(:products, :user, index: true, foreign_key: true)
+ # remove_reference(:products, :user, foreign_key: true)
#
def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options)
if foreign_key
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 564b226b39..0f2b1e85ff 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -137,7 +137,7 @@ module ActiveRecord
record.committed!
else
# if not running callbacks, only adds the record to the parent transaction
- record.add_to_transaction
+ connection.add_transaction_record(record)
end
end
ensure
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index fa10f18cb7..f7d75e6748 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -76,7 +76,7 @@ module ActiveRecord
SIMPLE_INT = /\A\d+\z/
- attr_accessor :visitor, :pool
+ attr_accessor :visitor, :pool, :prevent_writes
attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock
alias :in_use? :owner
@@ -100,6 +100,11 @@ module ActiveRecord
end
end
+ def self.build_read_query_regexp(*parts) # :nodoc:
+ parts = parts.map { |part| /\A\s*#{part}/i }
+ Regexp.union(*parts)
+ end
+
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@@ -125,12 +130,35 @@ module ActiveRecord
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
+
+ check_version
end
def replica?
@config[:replica] || false
end
+ # Determines whether writes are currently being prevents.
+ #
+ # Returns true if the connection is a replica, or if +prevent_writes+
+ # is set to true.
+ def preventing_writes?
+ replica? || prevent_writes
+ end
+
+ # Prevent writing to the database regardless of role.
+ #
+ # In some cases you may want to prevent writes to the database
+ # even if you are on a database that can write. `while_preventing_writes`
+ # will prevent writes to the database for the duration of the block.
+ def while_preventing_writes
+ original = self.prevent_writes
+ self.prevent_writes = true
+ yield
+ ensure
+ self.prevent_writes = original
+ end
+
def migrations_paths # :nodoc:
@config[:migrations_paths] || Migrator.migrations_paths
end
@@ -312,6 +340,11 @@ module ActiveRecord
false
end
+ # Does this adapter support materialized views?
+ def supports_materialized_views?
+ false
+ end
+
# Does this adapter support datetime with precision?
def supports_datetime_with_precision?
false
@@ -502,6 +535,9 @@ module ActiveRecord
end
private
+ def check_version
+ end
+
def type_map
@type_map ||= Type::TypeMap.new.tap do |mapping|
initialize_type_map(mapping)
@@ -575,14 +611,12 @@ module ActiveRecord
$1.to_i if sql_type =~ /\((.*)\)/
end
- def translate_exception_class(e, sql)
- begin
- message = "#{e.class.name}: #{e.message}: #{sql}"
- rescue Encoding::CompatibilityError
- message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}"
- end
+ def translate_exception_class(e, sql, binds)
+ message = "#{e.class.name}: #{e.message}"
- exception = translate_exception(e, message)
+ exception = translate_exception(
+ e, message: message, sql: sql, binds: binds
+ )
exception.set_backtrace e.backtrace
exception
end
@@ -595,24 +629,25 @@ module ActiveRecord
binds: binds,
type_casted_binds: type_casted_binds,
statement_name: statement_name,
- connection_id: object_id) do
+ connection_id: object_id,
+ connection: self) do
begin
@lock.synchronize do
yield
end
rescue => e
- raise translate_exception_class(e, sql)
+ raise translate_exception_class(e, sql, binds)
end
end
end
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
# override in derived class
case exception
when RuntimeError
exception
else
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index d40f38fb77..2d7f34ef24 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -54,10 +54,6 @@ module ActiveRecord
super(connection, logger, config)
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
-
- if version < "5.5.8"
- raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
- end
end
def version #:nodoc:
@@ -76,6 +72,10 @@ module ActiveRecord
!mariadb? && version >= "8.0.1"
end
+ def supports_expression_index?
+ !mariadb? && version >= "8.0.13"
+ end
+
def supports_transaction_isolation?
true
end
@@ -535,6 +535,12 @@ module ActiveRecord
end
private
+ def check_version
+ if version < "5.5.8"
+ raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8."
+ end
+ end
+
def combine_multi_statements(total_sql)
total_sql.each_with_object([]) do |sql, total_sql_chunks|
previous_packet = total_sql_chunks.last
@@ -622,6 +628,8 @@ module ActiveRecord
# See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
+ ER_NO_REFERENCED_ROW = 1216
+ ER_ROW_IS_REFERENCED = 1217
ER_DO_NOT_HAVE_DEFAULT = 1364
ER_ROW_IS_REFERENCED_2 = 1451
ER_NO_REFERENCED_ROW_2 = 1452
@@ -634,34 +642,34 @@ module ActiveRecord
ER_QUERY_INTERRUPTED = 1317
ER_QUERY_TIMEOUT = 3024
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
case error_number(exception)
when ER_DUP_ENTRY
- RecordNotUnique.new(message)
- when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
- InvalidForeignKey.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
+ when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
when ER_CANNOT_ADD_FOREIGN
- mismatched_foreign_key(message)
+ mismatched_foreign_key(message, sql: sql, binds: binds)
when ER_CANNOT_CREATE_TABLE
if message.include?("errno: 150")
- mismatched_foreign_key(message)
+ mismatched_foreign_key(message, sql: sql, binds: binds)
else
super
end
when ER_DATA_TOO_LONG
- ValueTooLong.new(message)
+ ValueTooLong.new(message, sql: sql, binds: binds)
when ER_OUT_OF_RANGE
- RangeError.new(message)
+ RangeError.new(message, sql: sql, binds: binds)
when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when ER_LOCK_DEADLOCK
- Deadlocked.new(message)
+ Deadlocked.new(message, sql: sql, binds: binds)
when ER_LOCK_WAIT_TIMEOUT
- LockWaitTimeout.new(message)
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_TIMEOUT
- StatementTimeout.new(message)
+ StatementTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_INTERRUPTED
- QueryCanceled.new(message)
+ QueryCanceled.new(message, sql: sql, binds: binds)
else
super
end
@@ -792,11 +800,13 @@ module ActiveRecord
Arel::Visitors::MySQL.new(self)
end
- def mismatched_foreign_key(message)
- parts = message.scan(/`(\w+)`[ $)]/).flatten
+ def mismatched_foreign_key(message, sql:, binds:)
+ parts = sql.scan(/`(\w+)`[ $)]/).flatten
MismatchedForeignKey.new(
self,
message: message,
+ sql: sql,
+ binds: binds,
table: parts[0],
foreign_key: parts[1],
target_table: parts[2],
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 2e7a78215a..f60d8469cc 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -174,12 +174,12 @@ module ActiveRecord
if e.path == path_to_adapter
# We can assume that a non-builtin adapter was specified, so it's
# either misspelled or missing from Gemfile.
- raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+ raise LoadError, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
# Bubbled up from the adapter require. Prefix the exception message
# with some guidance about how to address it and reraise.
else
- raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ raise LoadError, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
index 3dcb916d99..883747b84b 100644
--- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -10,7 +10,12 @@ module ActiveRecord
super
end
- def visit_Arel_Nodes_In(*)
+ def visit_Arel_Nodes_In(o, collector)
+ @preparable = false
+ super
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
@preparable = false
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 684c7042a7..56f777f3be 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -19,8 +19,19 @@ module ActiveRecord
execute(sql, name).to_a
end
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :select, :set, :show, :release, :savepoint) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
@@ -33,11 +44,19 @@ module ActiveRecord
if without_prepared_statement?(binds)
execute_and_free(sql, name) do |result|
- ActiveRecord::Result.new(result.fields, result.to_a) if result
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
end
else
exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
- ActiveRecord::Result.new(result.fields, result.to_a) if result
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index e167c01802..4894fd1c08 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -35,13 +35,39 @@ module ActiveRecord
]
end
- indexes.last[-2] << row[:Column_name]
- indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
- indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
+ if row[:Expression]
+ expression = row[:Expression]
+ expression = +"(#{expression})" unless expression.start_with?("(")
+ indexes.last[-2] << expression
+ indexes.last[-1][:expressions] ||= {}
+ indexes.last[-1][:expressions][expression] = expression
+ indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D"
+ else
+ indexes.last[-2] << row[:Column_name]
+ indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
+ indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D"
+ end
end
end
- indexes.map { |index| IndexDefinition.new(*index) }
+ indexes.map do |index|
+ options = index.last
+
+ if expressions = options.delete(:expressions)
+ orders = options.delete(:orders)
+ lengths = options.delete(:lengths)
+
+ columns = index[-2].map { |name|
+ [ name.to_sym, expressions[name] || +quote_column_name(name) ]
+ }.to_h
+
+ index[-2] = add_options_for_index_columns(
+ columns, order: orders, length: lengths
+ ).values.join(", ")
+ end
+
+ IndexDefinition.new(*index)
+ end
end
def remove_column(table_name, column_name, type = nil, options = {})
@@ -80,10 +106,13 @@ module ActiveRecord
def new_column_from_field(table_name, field)
type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
- if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default])
- default, default_function = nil, field[:Default]
- else
- default, default_function = field[:Default], nil
+ default, default_function = field[:Default], nil
+
+ if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
+ default, default_function = nil, default
+ elsif type_metadata.extra == "DEFAULT_GENERATED"
+ default = +"(#{default})" unless default.start_with?("(")
+ default, default_function = nil, default
end
MySQL::Column.new(
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 6bd6b67165..d93c1f449e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -67,11 +67,22 @@ module ActiveRecord
end
end
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:select, :show, :set) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
# Executes an SQL statement, returning a PG::Result object on success
# or raising a PG::Error exception otherwise.
# Note: the PG::Result object is manually memory managed; if you don't
# need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
def execute(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
materialize_transactions
log(sql, name) do
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
index 6fbeaa2b9e..b1dfbde86e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
Data = Struct.new(:encoder, :values) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
index aabe83b85d..7b42677101 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Hstore < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
def type
:hstore
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
index 7b057a8452..7f6adc351c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class LegacyPoint < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
def type
:point
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
index 02a9c506f6..8c74cecc4d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -7,7 +7,7 @@ module ActiveRecord
module PostgreSQL
module OID # :nodoc:
class Point < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
def type
:point
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
index 83c21ba6ea..203087bc36 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -36,7 +36,7 @@ module ActiveRecord
def query_conditions_for_initial_load
known_type_names = @store.keys.map { |n| "'#{n}'" }
known_type_types = %w('r' 'e' 'd')
- <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")]
+ <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index e75202b0be..0895d06356 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -93,11 +93,11 @@ module ActiveRecord
elsif value.hex?
"X'#{value}'"
end
- when Float
- if value.infinite? || value.nan?
- "'#{value}'"
- else
+ when Numeric
+ if value.finite?
super
+ else
+ "'#{value}'"
end
when OID::Array::Data
_quote(encode_array(value))
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index 8e381a92cf..ceb8b40bd9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -23,6 +23,17 @@ module ActiveRecord
end
super
end
+
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
+ def table_modifier_in_create(o)
+ # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY
+ # tables are already UNLOGGED.
+ if o.temporary
+ " TEMPORARY"
+ elsif o.unlogged
+ " UNLOGGED"
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index 206b855a18..dc4a0bb26e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -175,6 +175,13 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
+ attr_reader :unlogged
+
+ def initialize(*)
+ super
+ @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables
+ end
+
private
def integer_like_primary_key_type(type, options)
if type == :bigint || options[:limit] == 8
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index fae3ddbad4..16260fe565 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -68,7 +68,7 @@ module ActiveRecord
table = quoted_scope(table_name)
index = quoted_scope(index_name)
- query_value(<<-SQL, "SCHEMA").to_i > 0
+ query_value(<<~SQL, "SCHEMA").to_i > 0
SELECT COUNT(*)
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
@@ -85,7 +85,7 @@ module ActiveRecord
def indexes(table_name) # :nodoc:
scope = quoted_scope(table_name)
- result = query(<<-SQL, "SCHEMA")
+ result = query(<<~SQL, "SCHEMA")
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
@@ -124,7 +124,7 @@ module ActiveRecord
# add info on sort order (only desc order is explicitly specified, asc is the default)
# and non-default opclasses
- expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
opclasses[column] = opclass.to_sym if opclass
if nulls
orders[column] = [desc, nulls].compact.join(" ")
@@ -196,7 +196,7 @@ module ActiveRecord
# Returns an array of schema names.
def schema_names
- query_values(<<-SQL, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT nspname
FROM pg_namespace
WHERE nspname !~ '^pg_.*'
@@ -302,7 +302,7 @@ module ActiveRecord
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
- result = query(<<-end_sql, "SCHEMA")[0]
+ result = query(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@@ -319,10 +319,10 @@ module ActiveRecord
AND cons.contype = 'p'
AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
- end_sql
+ SQL
if result.nil? || result.empty?
- result = query(<<-end_sql, "SCHEMA")[0]
+ result = query(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
@@ -339,7 +339,7 @@ module ActiveRecord
WHERE t.oid = #{quote(quote_table_name(table))}::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
- end_sql
+ SQL
end
pk = result.shift
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
index bfd300723d..f2f4701500 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -68,7 +68,7 @@ module ActiveRecord
# * <tt>"schema_name".table_name</tt>
# * <tt>"schema.name"."table name"</tt>
def extract_schema_qualified_name(string)
- schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ schema, table = string.scan(/[^".]+|"[^"]*"/)
if table.nil?
table = schema
schema = nil
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index bc6eb11572..1c4e625ead 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -5,7 +5,7 @@ gem "pg", ">= 0.18", "< 2.0"
require "pg"
# Use async_exec instead of exec_params on pg versions before 1.1
-class ::PG::Connection
+class ::PG::Connection # :nodoc:
unless self.public_method_defined?(:async_exec_params)
remove_method :exec_params
alias exec_params async_exec
@@ -43,9 +43,14 @@ module ActiveRecord
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
conn_params.slice!(*valid_conn_param_keys)
- # The postgres drivers don't allow the creation of an unconnected PG::Connection object,
- # so just pass a nil connection object for the time being.
- ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
+ conn = PG.connect(conn_params)
+ ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError
+ else
+ raise
+ end
end
end
@@ -80,6 +85,19 @@ module ActiveRecord
class PostgreSQLAdapter < AbstractAdapter
ADAPTER_NAME = "PostgreSQL"
+ ##
+ # :singleton-method:
+ # PostgreSQL allows the creation of "unlogged" tables, which do not record
+ # data in the PostgreSQL Write-Ahead Log. This can make the tables faster,
+ # but significantly increases the risk of data loss if the database
+ # crashes. As a result, this should not be used in production
+ # environments. If you would like all created tables to be unlogged in
+ # the test environment you can add the following line to your test.rb
+ # file:
+ #
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
+ class_attribute :create_unlogged_tables, default: false
+
NATIVE_DATABASE_TYPES = {
primary_key: "bigserial primary key",
string: { name: "character varying" },
@@ -167,7 +185,7 @@ module ActiveRecord
end
def supports_json?
- postgresql_version >= 90200
+ true
end
def supports_comments?
@@ -220,15 +238,11 @@ module ActiveRecord
@local_tz = nil
@max_identifier_length = nil
- connect
+ configure_connection
add_pg_encoders
@statements = StatementPool.new @connection,
self.class.type_cast_config_to_integer(config[:statement_limit])
- if postgresql_version < 90100
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
- end
-
add_pg_decoders
@type_map = Type::HashLookupTypeMap.new
@@ -318,16 +332,16 @@ module ActiveRecord
end
def supports_ranges?
- # Range datatypes weren't introduced until PostgreSQL 9.2
- postgresql_version >= 90200
+ true
end
+ deprecate :supports_ranges?
def supports_materialized_views?
- postgresql_version >= 90300
+ true
end
def supports_foreign_tables?
- postgresql_version >= 90300
+ true
end
def supports_pgcrypto_uuid?
@@ -410,6 +424,12 @@ module ActiveRecord
end
private
+ def check_version
+ if postgresql_version < 90300
+ raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3."
+ end
+ end
+
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@@ -421,28 +441,28 @@ module ActiveRecord
LOCK_NOT_AVAILABLE = "55P03"
QUERY_CANCELED = "57014"
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
return exception unless exception.respond_to?(:result)
case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when FOREIGN_KEY_VIOLATION
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
when VALUE_LIMIT_VIOLATION
- ValueTooLong.new(message)
+ ValueTooLong.new(message, sql: sql, binds: binds)
when NUMERIC_VALUE_OUT_OF_RANGE
- RangeError.new(message)
+ RangeError.new(message, sql: sql, binds: binds)
when NOT_NULL_VIOLATION
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when SERIALIZATION_FAILURE
- SerializationFailure.new(message)
+ SerializationFailure.new(message, sql: sql, binds: binds)
when DEADLOCK_DETECTED
- Deadlocked.new(message)
+ Deadlocked.new(message, sql: sql, binds: binds)
when LOCK_NOT_AVAILABLE
- LockWaitTimeout.new(message)
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
when QUERY_CANCELED
- QueryCanceled.new(message)
+ QueryCanceled.new(message, sql: sql, binds: binds)
else
super
end
@@ -569,18 +589,11 @@ module ActiveRecord
def load_additional_types(oids = nil)
initializer = OID::TypeMapInitializer.new(type_map)
- if supports_ranges?
- query = <<-SQL
- SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
- FROM pg_type as t
- LEFT JOIN pg_range as r ON oid = rngtypid
- SQL
- else
- query = <<-SQL
- SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
- FROM pg_type as t
- SQL
- end
+ query = <<~SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
+ FROM pg_type as t
+ LEFT JOIN pg_range as r ON oid = rngtypid
+ SQL
if oids
query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
@@ -622,7 +635,7 @@ module ActiveRecord
def exec_cache(sql, name, binds)
materialize_transactions
- stmt_key = prepare_statement(sql)
+ stmt_key = prepare_statement(sql, binds)
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds, stmt_key) do
@@ -676,7 +689,7 @@ module ActiveRecord
# Prepare the statement if it hasn't been prepared, return
# the statement key.
- def prepare_statement(sql)
+ def prepare_statement(sql, binds)
@lock.synchronize do
sql_key = sql_key(sql)
unless @statements.key? sql_key
@@ -684,7 +697,7 @@ module ActiveRecord
begin
@connection.prepare nextkey, sql
rescue => e
- raise translate_exception_class(e, sql)
+ raise translate_exception_class(e, sql, binds)
end
# Clear the queue
@connection.get_last_result
@@ -699,12 +712,6 @@ module ActiveRecord
def connect
@connection = PG.connect(@connection_parameters)
configure_connection
- rescue ::PG::Error => error
- if error.message.include?("does not exist")
- raise ActiveRecord::NoDatabaseError
- else
- raise
- end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -762,7 +769,7 @@ module ActiveRecord
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name)
- query(<<-end_sql, "SCHEMA")
+ query(<<~SQL, "SCHEMA")
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
c.collname, col_description(a.attrelid, a.attnum) AS comment
@@ -773,7 +780,7 @@ module ActiveRecord
WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
- end_sql
+ SQL
end
def extract_table_ref_from_insert_sql(sql)
@@ -788,7 +795,7 @@ module ActiveRecord
def can_perform_case_insensitive_comparison_for?(column)
@case_insensitive_cache ||= {}
@case_insensitive_cache[column.sql_type] ||= begin
- sql = <<-end_sql
+ sql = <<~SQL
SELECT exists(
SELECT * FROM pg_proc
WHERE proname = 'lower'
@@ -800,7 +807,7 @@ module ActiveRecord
WHERE proname = 'lower'
AND castsource = #{quote column.sql_type}::regtype
)
- end_sql
+ SQL
execute_and_clear(sql, "SCHEMA", []) do |result|
result.getvalue(0, 0)
end
@@ -826,7 +833,7 @@ module ActiveRecord
"bool" => PG::TextDecoder::Boolean,
}
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
- query = <<-SQL % known_coder_types.join(", ")
+ query = <<~SQL % known_coder_types.join(", ")
SELECT t.oid, t.typname
FROM pg_type as t
WHERE t.typname IN (%s)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index b2dcdb5373..29f0e19a98 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -12,6 +12,10 @@ module ActiveRecord
quote_column_name(attr)
end
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
+ end
+
def quote_column_name(name)
@quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 48277f0ae2..8650c07bab 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# See https://www.sqlite.org/fileformat2.html#intschema
next if row["name"].starts_with?("sqlite_")
- index_sql = query_value(<<-SQL, "SCHEMA")
+ index_sql = query_value(<<~SQL, "SCHEMA")
SELECT sql
FROM sqlite_master
WHERE name = #{quote(row['name'])} AND type = 'index'
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 81882f6cc1..dcad230335 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -105,11 +105,6 @@ module ActiveRecord
@active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
-
- if sqlite_version < "3.8.0"
- raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
- end
-
configure_connection
end
@@ -214,6 +209,13 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:select) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
@@ -262,6 +264,10 @@ module ActiveRecord
end
def execute(sql, name = nil) #:nodoc:
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
materialize_transactions
log(sql, name) do
@@ -401,6 +407,18 @@ module ActiveRecord
end
private
+ # See https://www.sqlite.org/limits.html,
+ # the default value is 999 when not configured.
+ def bind_params_length
+ 999
+ end
+
+ def check_version
+ if sqlite_version < "3.8.0"
+ raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8."
+ end
+ end
+
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(int)i, SQLite3Integer
@@ -522,18 +540,18 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
end
- def translate_exception(exception, message)
+ def translate_exception(exception, message:, sql:, binds:)
case exception.message
# SQLite 3.8.2 returns a newly formatted error message:
# UNIQUE constraint failed: *table_name*.*column_name*
# Older versions of SQLite return:
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when /.* may not be NULL/, /NOT NULL constraint failed: .*/
- NotNullViolation.new(message)
+ NotNullViolation.new(message, sql: sql, binds: binds)
when /FOREIGN KEY constraint failed/i
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
else
super
end
@@ -543,7 +561,7 @@ module ActiveRecord
def table_structure_with_collation(table_name, basic_structure)
collation_hash = {}
- sql = <<-SQL
+ sql = <<~SQL
SELECT sql FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)