diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
20 files changed, 637 insertions, 611 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 3f2e86a98d..53dbbd8c21 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -825,7 +825,7 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # establish_connection "library_db" + # establish_connection :library_db # end # # class ScaryBook < Book diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index e5a24b2aca..f0c0fbab6c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -61,17 +61,6 @@ module ActiveRecord lookup_cast_type(column.sql_type) end - def fetch_type_metadata(sql_type) - cast_type = lookup_cast_type(sql_type) - SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - end - # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) @@ -152,11 +141,18 @@ module ActiveRecord "'#{quote_string(value.to_s)}'" end - private - - def type_casted_binds(binds) + def type_casted_binds(binds) # :nodoc: + if binds.first.is_a?(Array) + binds.map { |column, value| type_cast(value, column) } + else binds.map { |attr| type_cast(attr.value_for_database) } end + end + + private + def lookup_cast_type(sql_type) + type_map.lookup(sql_type) + end def id_value_for_database(value) if primary_key = value.class.primary_key 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 c48a4acff8..a4fecc4a8e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -133,5 +133,6 @@ module ActiveRecord end end end + SchemaCreation = AbstractAdapter::SchemaCreation # :nodoc: end end 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 5eb7787226..46d7f84efd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -2,8 +2,33 @@ module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database - # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - IndexDefinition = Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: + # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes + class IndexDefinition # :nodoc: + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + + def initialize( + table, name, + unique = false, + columns = [], + lengths: {}, + orders: {}, + where: nil, + type: nil, + using: nil, + comment: nil + ) + @table = table + @name = name + @unique = unique + @columns = columns + @lengths = lengths + @orders = orders + @where = where + @type = type + @using = using + @comment = comment + end + end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the @@ -116,16 +141,12 @@ module ActiveRecord private - def as_options(value, default = {}) - if value.is_a?(Hash) - value - else - default - end + def as_options(value) + value.is_a?(Hash) ? value : {} end def polymorphic_options - as_options(polymorphic, options) + as_options(polymorphic) end def index_options 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 c44215cd43..13629dee7f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -31,6 +31,8 @@ module ActiveRecord # Returns the relation names useable to back Active Record models. # For most adapters this means all #tables and #views. def data_sources + select_values(data_source_sql, "SCHEMA") + rescue NotImplementedError tables | views end @@ -39,12 +41,14 @@ module ActiveRecord # data_source_exists?(:ebooks) # def data_source_exists?(name) + select_values(data_source_sql(name), "SCHEMA").any? if name.present? + rescue NotImplementedError data_sources.include?(name.to_s) end # Returns an array of table names defined in the database. def tables - raise NotImplementedError, "#tables is not implemented" + select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") end # Checks to see if the table +table_name+ exists on the database. @@ -52,12 +56,14 @@ module ActiveRecord # table_exists?(:developers) # def table_exists?(table_name) + select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? + rescue NotImplementedError tables.include?(table_name.to_s) end # Returns an array of view names defined in the database. def views - raise NotImplementedError, "#views is not implemented" + select_values(data_source_sql(type: "VIEW"), "SCHEMA") end # Checks to see if the view +view_name+ exists on the database. @@ -65,6 +71,8 @@ module ActiveRecord # view_exists?(:ebooks) # def view_exists?(view_name) + select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? + rescue NotImplementedError views.include?(view_name.to_s) end @@ -97,10 +105,12 @@ module ActiveRecord indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end - # Returns an array of Column objects for the table specified by +table_name+. - # See the concrete implementation for details on the expected parameter values. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) - raise NotImplementedError, "#columns is not implemented" + table_name = table_name.to_s + column_definitions(table_name).map do |field| + new_column_from_field(table_name, field) + end end # Checks to see if a column exists in a given table. @@ -334,18 +344,16 @@ module ActiveRecord # part_id int NOT NULL, # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # - def create_join_table(table_1, table_2, options = {}) + def create_join_table(table_1, table_2, column_options: {}, **options) join_table_name = find_join_table_name(table_1, table_2, options) - column_options = options.delete(:column_options) || {} - column_options.reverse_merge!(null: false) - type = column_options.delete(:type) || :integer + column_options.reverse_merge!(null: false, index: false) - t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } + t1_ref, t2_ref = [table_1, table_2].map { |t| t.to_s.singularize } create_table(join_table_name, options.merge!(id: false)) do |td| - td.send type, t1_column, column_options - td.send type, t2_column, column_options + td.references t1_ref, column_options + td.references t2_ref, column_options yield td if block_given? end end @@ -968,16 +976,6 @@ module ActiveRecord foreign_key_for(from_table, options_or_to_table).present? end - def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: - return unless supports_foreign_keys? - foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } - end - - def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: - foreign_key_for(from_table, options_or_to_table) || \ - raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") - end - def foreign_key_column_for(table_name) # :nodoc: prefix = Base.table_name_prefix suffix = Base.table_name_suffix @@ -993,23 +991,10 @@ module ActiveRecord end def dump_schema_information #:nodoc: - versions = ActiveRecord::SchemaMigration.order("version").pluck(:version) + versions = ActiveRecord::SchemaMigration.all_versions insert_versions_sql(versions) end - def insert_versions_sql(versions) # :nodoc: - sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - - if versions.is_a?(Array) - sql = "INSERT INTO #{sm_table} (version) VALUES\n" - sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") - sql << ";\n\n" - sql - else - "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});" - end - end - def initialize_schema_migrations_table # :nodoc: ActiveRecord::SchemaMigration.create_table end @@ -1027,12 +1012,11 @@ module ActiveRecord def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i - sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" } - versions = Dir[*paths].map do |filename| - filename.split("/").last.split("_").first.to_i + migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) + versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| + ActiveRecord::Migrator.parse_migration_filename(file).first.to_i end unless migrated.include?(version) @@ -1256,6 +1240,10 @@ module ActiveRecord end end + def schema_creation + SchemaCreation.new(self) + end + def create_table_definition(*args) TableDefinition.new(*args) end @@ -1264,6 +1252,17 @@ module ActiveRecord AlterTable.new create_table_definition(name) end + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + def index_column_names(column_names) if column_names.is_a?(String) && /\W/.match?(column_names) column_names @@ -1288,6 +1287,24 @@ module ActiveRecord end end + def foreign_key_for(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table) || \ + raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") + end + + def extract_foreign_key_action(specifier) + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify + when "RESTRICT"; :restrict + end + end + def validate_index_length!(table_name, new_name, internal = false) max_index_length = internal ? index_name_length : allowed_index_name_length @@ -1307,6 +1324,27 @@ module ActiveRecord def can_remove_index_by_name?(options) options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + + def insert_versions_sql(versions) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) + + if versions.is_a?(Array) + sql = "INSERT INTO #{sm_table} (version) VALUES\n" + sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") + sql << ";\n\n" + sql + else + "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});" + end + end + + def data_source_sql(name = nil, type: nil) + raise NotImplementedError + end + + def quoted_scope(name = nil, type: nil) + raise NotImplementedError + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 6bb072dd73..19b7821494 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -149,57 +149,67 @@ module ActiveRecord end def begin_transaction(options = {}) - run_commit_callbacks = !current_transaction.joinable? - transaction = - if @stack.empty? - RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) - else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, - run_commit_callbacks: run_commit_callbacks) - end + @connection.lock.synchronize do + run_commit_callbacks = !current_transaction.joinable? + transaction = + if @stack.empty? + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) + end - @stack.push(transaction) - transaction + @stack.push(transaction) + transaction + end end def commit_transaction - transaction = @stack.last + @connection.lock.synchronize do + transaction = @stack.last - begin - transaction.before_commit_records - ensure - @stack.pop - end + begin + transaction.before_commit_records + ensure + @stack.pop + end - transaction.commit - transaction.commit_records + transaction.commit + transaction.commit_records + end end def rollback_transaction(transaction = nil) - transaction ||= @stack.pop - transaction.rollback - transaction.rollback_records + @connection.lock.synchronize do + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records + end end def within_new_transaction(options = {}) - transaction = begin_transaction options - yield - rescue Exception => error - if transaction - rollback_transaction - after_failure_actions(transaction, error) - end - raise - ensure - unless error - if Thread.current.status == "aborting" - rollback_transaction if transaction - else - begin - commit_transaction - rescue Exception - rollback_transaction(transaction) unless transaction.state.completed? - raise + @connection.lock.synchronize do + begin + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + unless error + if Thread.current.status == "aborting" + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b31ce0a181..85d6fbe8b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -74,7 +74,7 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ attr_accessor :visitor, :pool - attr_reader :schema_cache, :owner, :logger + attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner def self.type_cast_config_to_integer(config) @@ -93,8 +93,6 @@ module ActiveRecord end end - attr_reader :prepared_statements - def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -142,32 +140,8 @@ module ActiveRecord end end - def collector - if prepared_statements - SQLString.new - else - BindCollector.new - end - end - - def arel_visitor # :nodoc: - Arel::Visitors::ToSql.new(self) - end - - def valid_type?(type) - false - end - - def schema_creation - SchemaCreation.new self - end - - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |field| - new_column_from_field(table_name, field) - end + def valid_type?(type) # :nodoc: + !native_database_types[type].nil? end # this method must only be called while holding connection pool's mutex @@ -232,10 +206,10 @@ module ActiveRecord self.class::ADAPTER_NAME end - # Does this adapter support migrations? - def supports_migrations? - false + def supports_migrations? # :nodoc: + true end + deprecate :supports_migrations? def supports_primary_key? # :nodoc: true @@ -439,12 +413,15 @@ module ActiveRecord # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) + if ignored.size > 0 + ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.") + end reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, - # and a PGconn object in case of PostgreSQLAdapter. + # and a PG::Connection object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. @@ -480,14 +457,6 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: - Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation) - end - - def lookup_cast_type(sql_type) # :nodoc: - type_map.lookup(sql_type) - end - def column_name_for_operation(operation, node) # :nodoc: visitor.accept(node, collector).value end @@ -634,6 +603,18 @@ module ActiveRecord columns(table_name).detect { |c| c.name == column_name } || raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + + def arel_visitor + Arel::Visitors::ToSql.new(self) + end end 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 5f86a11c2d..71fa7b929c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,7 @@ require "active_record/connection_adapters/mysql/quoting" require "active_record/connection_adapters/mysql/schema_creation" require "active_record/connection_adapters/mysql/schema_definitions" require "active_record/connection_adapters/mysql/schema_dumper" +require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" require "active_support/core_ext/string/strip" @@ -15,19 +16,12 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting include MySQL::ColumnDumper + include MySQL::SchemaStatements def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) end - def schema_creation # :nodoc: - MySQL::SchemaCreation.new(self) - end - - def arel_visitor # :nodoc: - Arel::Visitors::MySQL.new(self) - end - ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -54,9 +48,6 @@ module ActiveRecord json: { name: "json" }, } - INDEX_TYPES = [:fulltext, :spatial] - INDEX_USINGS = [:btree, :hash] - class StatementPool < ConnectionAdapters::StatementPool private def dealloc(stmt) stmt[:stmt].close @@ -89,11 +80,6 @@ module ActiveRecord /mariadb/i.match?(full_version) end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -104,10 +90,8 @@ module ActiveRecord true end - # Technically MySQL allows to create indexes with the sort order syntax - # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? - true + !mariadb? && version >= "8.0.1" end def supports_transaction_isolation? @@ -174,10 +158,6 @@ module ActiveRecord raise NotImplementedError end - def new_column(*args) #:nodoc: - MySQL::Column.new(*args) - end - # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: @@ -315,109 +295,18 @@ module ActiveRecord show_variable "collation_database" end - def tables # :nodoc: - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" - sql << " AND table_schema = #{quote(@config[:database])}" - - select_values(sql, "SCHEMA") - end - - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") - end - - def data_sources # :nodoc: - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(@config[:database])}" - - select_values(sql, "SCHEMA") - end - - def table_exists?(table_name) # :nodoc: - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - def data_source_exists?(table_name) # :nodoc: - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - schema, name = extract_schema_qualified_name(view_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - indexes = [] - current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| - each_hash(result) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == "PRIMARY" # skip the primary key - current_index = row[:Key_name] - - mysql_index_type = row[:Index_type].downcase.to_sym - index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil - index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], {}, nil, nil, index_type, index_using, row[:Index_comment].presence) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - end - end - - indexes - end - - def new_column_from_field(table_name, field) # :nodoc: - type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] - else - default, default_function = field[:Default], nil - end - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) - end - def table_comment(table_name) # :nodoc: - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables - WHERE table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + WHERE table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} SQL end @@ -517,7 +406,7 @@ module ActiveRecord def foreign_keys(table_name) raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', @@ -530,9 +419,9 @@ module ActiveRecord JOIN information_schema.referential_constraints rc USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL - AND fk.table_schema = #{quote(schema)} - AND fk.table_name = #{quote(name)} - AND rc.table_name = #{quote(name)} + AND fk.table_schema = #{scope[:schema]} + AND fk.table_name = #{scope[:name]} + AND rc.table_name = #{scope[:name]} SQL fk_info.map do |row| @@ -604,14 +493,14 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' - AND table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + AND table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} ORDER BY ordinal_position SQL end @@ -648,10 +537,6 @@ module ActiveRecord self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end - def valid_type?(type) - !native_database_types[type].nil? - end - def default_index_type?(index) # :nodoc: index.using == :btree || super end @@ -716,10 +601,6 @@ module ActiveRecord end end - def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra) - end - def add_index_length(quoted_columns, **options) if length = options[:length] case length @@ -786,11 +667,11 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) - unless options_include_default?(options) + unless options.key?(:default) options[:default] = column.default end - unless options.has_key?(:null) + unless options.key?(:null) options[:null] = column.null end @@ -922,19 +803,12 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "CASCADE"; :cascade - when "SET NULL"; :nullify - end - end - def create_table_info(table_name) # :nodoc: select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - def create_table_definition(*args) # :nodoc: - MySQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::MySQL.new(self) end def mismatched_foreign_key(message) @@ -949,12 +823,6 @@ module ActiveRecord ) end - def extract_schema_qualified_name(string) # :nodoc: - schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) - schema, name = @config[:database], schema unless name - [schema, name] - end - def integer_to_sql(limit) # :nodoc: case limit when 1; "tinyint" diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb new file mode 100644 index 0000000000..571edffec7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -0,0 +1,112 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| + each_hash(result) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == "PRIMARY" # skip the primary key + current_index = row[:Key_name] + + mysql_index_type = row[:Index_type].downcase.to_sym + case mysql_index_type + when :fulltext, :spatial + index_type = mysql_index_type + when :btree, :hash + index_using = mysql_index_type + end + + indexes << IndexDefinition.new( + row[:Table], + row[:Key_name], + row[:Non_unique].to_i == 0, + type: index_type, + using: index_using, + comment: row[:Index_comment].presence + ) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] + indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + end + end + + indexes + end + + private + def schema_creation + MySQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + MySQL::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil + end + + MySQL::Column.new( + field[:Field], + default, + type_metadata, + field[:Null] == "YES", + table_name, + default_function, + field[:Collation], + comment: field[:Comment].presence + ) + end + + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra) + end + + def extract_foreign_key_action(specifier) + super unless specifier == "RESTRICT" + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + + sql = "SELECT table_name FROM information_schema.tables" + sql << " WHERE table_schema = #{scope[:schema]}" + sql << " AND table_name = #{scope[:name]}" if scope[:name] + sql << " AND table_type = #{scope[:type]}" if scope[:type] + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + scope = {} + scope[:schema] = schema ? quote(schema) : "database()" + scope[:name] = quote(name) if name + scope[:type] = quote(type) if type + scope + end + + def extract_schema_qualified_name(string) + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = nil, schema unless name + [schema, name] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb index 24dcf852e1..9ad6a6c0d0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -2,6 +2,8 @@ module ActiveRecord module ConnectionAdapters module MySQL class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + attr_reader :extra def initialize(type_metadata, extra: "") 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 e1a75f8e5e..a73a8c1726 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -23,7 +23,7 @@ module ActiveRecord when ::String type_cast_array(@pg_decoder.decode(value), :deserialize) when Data - deserialize(value.values) + type_cast_array(value.values, :deserialize) else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 8f9d6e7f9b..702fa8175c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -6,7 +6,7 @@ module ActiveRecord def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) - PGconn.unescape_bytea(super) + PG::Connection.unescape_bytea(super) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 6663448a99..da8d0c6992 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -33,7 +33,7 @@ module ActiveRecord # Quotes schema names for use in SQL queries. def quote_schema_name(name) - PGconn.quote_ident(name) + PG::Connection.quote_ident(name) end def quote_table_name_for_assignment(table, attr) @@ -42,7 +42,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze + @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -77,6 +77,9 @@ module ActiveRecord end private + def lookup_cast_type(sql_type) + super(select_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) + end def _quote(value) case value @@ -105,7 +108,7 @@ module ActiveRecord case value when Type::Binary::Data # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } when OID::Xml::Data, OID::Bit::Data diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index 44a7338bf5..730e7c7137 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -6,8 +6,50 @@ module ActiveRecord true end - def disable_referential_integrity # :nodoc: + def disable_referential_integrity(&block) # :nodoc: if supports_disable_referential_integrity? + if supports_alter_constraint? + disable_referential_integrity_with_alter_constraint(&block) + else + disable_referential_integrity_with_disable_trigger(&block) + end + else + yield + end + end + + private + + def disable_referential_integrity_with_alter_constraint + tables_constraints = execute(<<-SQL).values + SELECT table_name, constraint_name + FROM information_schema.table_constraints + WHERE constraint_type = 'FOREIGN KEY' + AND is_deferrable = 'NO' + SQL + + execute( + tables_constraints.collect { |table, constraint| + "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} DEFERRABLE" + }.join(";") + ) + + begin + transaction do + execute("SET CONSTRAINTS ALL DEFERRED") + + yield + end + ensure + execute( + tables_constraints.collect { |table, constraint| + "ALTER TABLE #{quote_table_name(table)} ALTER CONSTRAINT #{constraint} NOT DEFERRABLE" + }.join(";") + ) + end + end + + def disable_referential_integrity_with_disable_trigger original_exception = nil begin @@ -39,10 +81,7 @@ Rails needs superuser privileges to disable referential integrity. end rescue ActiveRecord::ActiveRecordError end - else - yield end - end end end end 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 a61d920a73..5b483ad4ab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -54,81 +54,13 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path. - def tables - select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA") - end - - def data_sources # :nodoc - select_values(<<-SQL, "SCHEMA") - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) - SQL - end - - def views # :nodoc: - select_values(<<-SQL, "SCHEMA") - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # If the schema is not specified as part of +name+ then it will only find tables within - # the current schema search path (regardless of permissions to access tables in other schemas) - def table_exists?(name) - name = Utils.extract_schema_qualified_name(name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT tablename - FROM pg_tables - WHERE tablename = #{quote(name.identifier)} - AND schemaname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - - def data_source_exists?(name) # :nodoc: - name = Utils.extract_schema_qualified_name(name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = #{quote(name.identifier)} - AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - - def view_exists?(view_name) # :nodoc: - name = Utils.extract_schema_qualified_name(view_name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND c.relname = #{quote(name.identifier)} - AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0 + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. @@ -138,8 +70,8 @@ module ActiveRecord Passing default to #index_name_exists? is deprecated without replacement. MSG end - table = Utils.extract_schema_qualified_name(table_name.to_s) - index = Utils.extract_schema_qualified_name(index_name.to_s) + table = quoted_scope(table_name) + index = quoted_scope(index_name) select_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) @@ -148,9 +80,9 @@ module ActiveRecord INNER JOIN pg_class i ON d.indexrelid = i.oid LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index.identifier}' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} + AND i.relname = #{index[:name]} + AND t.relname = #{table[:name]} + AND n.nspname = #{index[:schema]} SQL end @@ -162,7 +94,7 @@ module ActiveRecord MSG end - table = Utils.extract_schema_qualified_name(table_name.to_s) + scope = quoted_scope(table_name) result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, @@ -176,8 +108,8 @@ module ActiveRecord LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' AND d.indisprimary = 'f' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} + AND t.relname = #{scope[:name]} + AND n.nspname = #{scope[:schema]} ORDER BY i.relname SQL @@ -208,27 +140,17 @@ module ActiveRecord ] end - IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) - end.compact - end - - def new_column_from_field(table_name, field) # :nondoc: - column_name, type, default, notnull, oid, fmod, collation, comment = field - oid = oid.to_i - fmod = fmod.to_i - type_metadata = fetch_type_metadata(column_name, type, oid, fmod) - default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) - PostgreSQLColumn.new( - column_name, - default_value, - type_metadata, - !notnull, - table_name, - default_function, - collation, - comment: comment.presence - ) + IndexDefinition.new( + table_name, + index_name, + unique, + columns, + orders: orders, + where: where, + using: using.to_sym, + comment: comment.presence + ) + end end def table_options(table_name) # :nodoc: @@ -239,22 +161,22 @@ module ActiveRecord # Returns a comment stored in database for given table def table_comment(table_name) # :nodoc: - name = Utils.extract_schema_qualified_name(table_name.to_s) - if name.identifier + scope = quoted_scope(table_name, type: "BASE TABLE") + if scope[:name] select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relname = #{quote(name.identifier)} - AND c.relkind IN ('r') -- (r)elation/table - AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + WHERE c.relname = #{scope[:name]} + AND c.relkind IN (#{scope[:type]}) + AND n.nspname = #{scope[:schema]} SQL end end # Returns the current database name. def current_database - select_value("select current_database()", "SCHEMA") + select_value("SELECT current_database()", "SCHEMA") end # Returns the current schema name. @@ -430,18 +352,18 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - name = Utils.extract_schema_qualified_name(table_name.to_s) select_values(<<-SQL.strip_heredoc, "SCHEMA") - SELECT column_name - FROM information_schema.key_column_usage kcu - JOIN information_schema.table_constraints tc - ON kcu.table_name = tc.table_name - AND kcu.table_schema = tc.table_schema - AND kcu.constraint_name = tc.constraint_name - WHERE constraint_type = 'PRIMARY KEY' - AND kcu.table_name = #{quote(name.identifier)} - AND kcu.table_schema = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - ORDER BY kcu.ordinal_position + SELECT a.attname + FROM ( + SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx + FROM pg_index + WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass + AND indisprimary + ) i + JOIN pg_attribute a + ON a.attrelid = i.indrelid + AND a.attnum = i.indkey[i.idx] + ORDER BY i.idx SQL end @@ -489,7 +411,7 @@ module ActiveRecord end execute sql - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end @@ -579,6 +501,7 @@ module ActiveRecord end def foreign_keys(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c @@ -588,8 +511,8 @@ module ActiveRecord JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid JOIN pg_namespace t3 ON c.connamespace = t3.oid WHERE c.contype = 'f' - AND t1.relname = #{quote(table_name)} - AND t3.nspname = ANY (current_schemas(false)) + AND t1.relname = #{scope[:name]} + AND t3.nspname = #{scope[:schema]} ORDER BY c.conname SQL @@ -607,18 +530,6 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "c"; :cascade - when "n"; :nullify - when "r"; :restrict - end - end - - def index_name_length - 63 - end - # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -666,17 +577,84 @@ module ActiveRecord [super, *order_columns].join(", ") end - def fetch_type_metadata(column_name, sql_type, oid, fmod) - cast_type = get_oid_type(oid, fmod, column_name, sql_type) - simple_type = SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) - end + private + def schema_creation + PostgreSQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + PostgreSQL::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + column_name, type, default, notnull, oid, fmod, collation, comment = field + type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + + PostgreSQLColumn.new( + column_name, + default_value, + type_metadata, + !notnull, + table_name, + default_function, + collation, + comment: comment.presence + ) + end + + def fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end + + def extract_foreign_key_action(specifier) + case specifier + when "c"; :cascade + when "n"; :nullify + when "r"; :restrict + end + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + + sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" + sql << " WHERE n.nspname = #{scope[:schema]}" + sql << " AND c.relname = #{scope[:name]}" if scope[:name] + sql << " AND c.relkind IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + type = \ + case type + when "BASE TABLE" + "'r'" + when "VIEW" + "'v','m'" + end + scope = {} + scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + + def extract_schema_qualified_name(string) + name = Utils.extract_schema_qualified_name(string.to_s) + [name.schema, name.identifier] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 311988625f..f57179ae59 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,8 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + undef to_yaml if method_defined?(:to_yaml) + attr_reader :oid, :fmod, :array def initialize(type_metadata, oid: nil, fmod: nil) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index a3f9ce6d64..aa7940188a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -19,9 +19,9 @@ module ActiveRecord def quoted if schema - PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + PG::Connection.quote_ident(schema) << SEPARATOR << PG::Connection.quote_ident(identifier) else - PGconn.quote_ident(identifier) + PG::Connection.quote_ident(identifier) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c89e29ba44..7ae9bd9a5f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -3,6 +3,7 @@ gem "pg", "~> 0.18" require "pg" require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" require "active_record/connection_adapters/postgresql/explain_pretty_printer" @@ -15,7 +16,6 @@ require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" -require "active_record/connection_adapters/statement_pool" module ActiveRecord module ConnectionHandling # :nodoc: @@ -29,11 +29,11 @@ module ActiveRecord conn_params[:user] = conn_params.delete(:username) if conn_params[:username] conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] - # Forward only valid config params to PGconn.connect. - valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + # Forward only valid config params to PG::Connection.connect. + 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 PGconn object, + # 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) end @@ -121,14 +121,6 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - def schema_creation # :nodoc: - PostgreSQL::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::PostgreSQL.new(self) - end - # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -201,8 +193,8 @@ module ActiveRecord end def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError + @connection.status == PG::CONNECTION_OK + rescue PG::Error false end end @@ -215,7 +207,7 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil - @table_alias_length = nil + @max_identifier_length = nil connect add_pg_encoders @@ -247,45 +239,48 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query "SELECT 1" + @lock.synchronize do + @connection.query "SELECT 1" + end true - rescue PGError + rescue PG::Error false end # Close then reopen the connection. def reconnect! - super - @connection.reset - configure_connection + @lock.synchronize do + super + @connection.reset + configure_connection + end end def reset! - clear_cache! - reset_transaction - unless @connection.transaction_status == ::PG::PQTRANS_IDLE - @connection.query "ROLLBACK" + @lock.synchronize do + clear_cache! + reset_transaction + unless @connection.transaction_status == ::PG::PQTRANS_IDLE + @connection.query "ROLLBACK" + end + @connection.query "DISCARD ALL" + configure_connection end - @connection.query "DISCARD ALL" - configure_connection end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - super - @connection.close rescue nil + @lock.synchronize do + super + @connection.close rescue nil + end end def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - def set_standard_conforming_strings execute("SET standard_conforming_strings = on", "SCHEMA") end @@ -306,8 +301,8 @@ module ActiveRecord true end - # Range datatypes weren't introduced until PostgreSQL 9.2 def supports_ranges? + # Range datatypes weren't introduced until PostgreSQL 9.2 postgresql_version >= 90200 end @@ -319,6 +314,12 @@ module ActiveRecord postgresql_version >= 90400 end + def supports_alter_constraint? + # PostgreSQL 9.4 introduces ALTER TABLE ... ALTER CONSTRAINT but it has a bug and fixed in 9.4.2 + # https://www.postgresql.org/docs/9.4/static/release-9-4-2.html + postgresql_version >= 90402 + end + def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") @@ -363,8 +364,9 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i + @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i end + alias index_name_length table_alias_length # Set the authorized user for this session def session_auth=(user) @@ -376,19 +378,10 @@ module ActiveRecord @use_insert_returning end - def valid_type?(type) - !native_database_types[type].nil? - end - def update_table_definition(table_name, base) #:nodoc: PostgreSQL::Table.new(table_name, base) end - def lookup_cast_type(sql_type) # :nodoc: - oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first["oid"].to_i - super(oid) - end - def column_name_for_operation(operation, node) # :nodoc: OPERATION_ALIASES.fetch(operation) { operation.downcase } end @@ -422,7 +415,7 @@ module ActiveRecord def translate_exception(exception, message) return exception unless exception.respond_to?(:result) - case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) + case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION @@ -659,7 +652,7 @@ module ActiveRecord CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze def is_cached_plan_failure?(e) pgerror = e.cause - code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC) rescue false @@ -698,7 +691,7 @@ module ActiveRecord # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect - @connection = PGconn.connect(@connection_parameters) + @connection = PG.connect(@connection_parameters) configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") @@ -785,8 +778,8 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(*args) - PostgreSQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::PostgreSQL.new(self) end def can_perform_case_insensitive_comparison_for?(column) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb new file mode 100644 index 0000000000..e02491edb6 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -0,0 +1,92 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + index_sql = select_value(<<-SQL, "SCHEMA") + SELECT sql + FROM sqlite_master + WHERE name = #{quote(row['name'])} AND type = 'index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name = #{quote(row['name'])} AND type = 'index' + SQL + + /\sWHERE\s+(?<where>.+)$/i =~ index_sql + + columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| + col["name"] + end + + IndexDefinition.new( + table_name, + row["name"], + row["unique"] != 0, + columns, + where: where + ) + end + end + + private + def schema_creation + SQLite3::SchemaCreation.new(self) + end + + def create_table_definition(*args) + SQLite3::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + default = \ + case field["dflt_value"] + when /^null$/i + nil + when /^'(.*)'$/m + $1.gsub("''", "'") + when /^"(.*)"$/m + $1.gsub('""', '"') + else + field["dflt_value"] + end + + type_metadata = fetch_type_metadata(field["type"]) + Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"]) + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'table','view'" + + sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" + sql << " AND name = #{scope[:name]}" if scope[:name] + sql << " AND type IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + type = \ + case type + when "BASE TABLE" + "'table'" + when "VIEW" + "'view'" + end + scope = {} + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 16ef195bfc..e2c05ccc4e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -5,6 +5,7 @@ require "active_record/connection_adapters/sqlite3/quoting" require "active_record/connection_adapters/sqlite3/schema_creation" require "active_record/connection_adapters/sqlite3/schema_definitions" require "active_record/connection_adapters/sqlite3/schema_dumper" +require "active_record/connection_adapters/sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -55,6 +56,7 @@ module ActiveRecord include SQLite3::Quoting include SQLite3::ColumnDumper + include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", @@ -82,14 +84,6 @@ module ActiveRecord SQLite3::Table.new(table_name, base) end - def schema_creation # :nodoc: - SQLite3::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::SQLite.new(self) - end - def initialize(connection, logger, connection_options, config) super(connection, logger, config) @@ -117,11 +111,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - def requires_reloading? true end @@ -163,10 +152,6 @@ module ActiveRecord true end - def valid_type?(type) - true - end - # Returns 62. SQLite supports index names up to 64 # characters. The rest is used by Rails internally to perform # temporary rename operations @@ -274,92 +259,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def data_sources # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def views # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def table_exists?(table_name) # :nodoc: - return false unless table_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(table_name)}" - - select_values(sql, "SCHEMA").any? - end - - def data_source_exists?(table_name) # :nodoc: - return false unless table_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(table_name)}" - - select_values(sql, "SCHEMA").any? - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(view_name)}" - - select_values(sql, "SCHEMA").any? - end - - def new_column_from_field(table_name, field) # :nondoc: - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/m - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/m - field["dflt_value"] = $1.gsub('""', '"') - end - - collation = field["collation"] - sql_type = field["type"] - type_metadata = fetch_type_metadata(sql_type) - new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - sql = <<-SQL - SELECT sql - FROM sqlite_master - WHERE name=#{quote(row['name'])} AND type='index' - UNION ALL - SELECT sql - FROM sqlite_temp_master - WHERE name=#{quote(row['name'])} AND type='index' - SQL - index_sql = exec_query(sql).first["sql"] - match = /\sWHERE\s+(.+)$/i.match(index_sql) - where = match[1] if match - IndexDefinition.new( - table_name, - row["name"], - row["unique"] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| - col["name"] - }, nil, nil, where) - end - end - def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f["pk"] > 0 } pks.sort_by { |f| f["pk"] }.map { |f| f["name"] } @@ -420,11 +319,10 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: alter_table(table_name) do |definition| - include_default = options_include_default?(options) definition[column_name].instance_eval do self.type = type self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default + self.default = options[:default] if options.include?(:default) self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) self.scale = options[:scale] if options.include?(:scale) @@ -545,7 +443,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)")) + @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)")) end def translate_exception(exception, message) @@ -606,16 +504,8 @@ module ActiveRecord end end - def create_table_definition(*args) - SQLite3::TableDefinition.new(*args) - end - - def extract_foreign_key_action(specifier) - case specifier - when "CASCADE"; :cascade - when "SET NULL"; :nullify - when "RESTRICT"; :restrict - end + def arel_visitor + Arel::Visitors::SQLite.new(self) end def configure_connection |