diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
19 files changed, 1763 insertions, 1206 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 347d794fa3..42bd16db80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -5,14 +5,11 @@ require 'active_support/core_ext/module/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection - # acquisition timeout period. + # acquisition timeout period: because max connections in pool + # are in use. class ConnectionTimeoutError < ConnectionNotEstablished end - # Raised when a connection pool is full and another connection is requested - class PoolFullError < ConnectionNotEstablished - end - module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -187,7 +184,11 @@ module ActiveRecord return remove if any? elapsed = Time.now - t0 - raise ConnectionTimeoutError if elapsed >= timeout + if elapsed >= timeout + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [timeout, elapsed] + raise ConnectionTimeoutError, msg + end end ensure @num_waiting -= 1 @@ -350,12 +351,12 @@ module ActiveRecord # # If all connections are leased and the pool is at capacity (meaning the # number of currently leased connections is greater than or equal to the - # size limit set), an ActiveRecord::PoolFullError exception will be raised. + # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - PoolFullError: no connection can be obtained from the pool. + # - ConnectionTimeoutError: no connection can be obtained from the pool. def checkout synchronize do conn = acquire_connection @@ -416,22 +417,14 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - PoolFullError if a connection could not be acquired (FIXME: - # why not ConnectionTimeoutError? + # - ConnectionTimeoutError if a connection could not be acquired def acquire_connection if conn = @available.poll conn elsif @connections.size < @size checkout_new_connection else - t0 = Time.now - begin - @available.poll(@checkout_timeout) - rescue ConnectionTimeoutError - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % - [@checkout_timeout, Time.now - t0] - raise PoolFullError, msg - end + @available.poll(@checkout_timeout) end end @@ -494,42 +487,42 @@ module ActiveRecord # # Normally there is only a single ConnectionHandler instance, accessible via # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine that connection pool that they should use. + # determine the connection pool that they should use. class ConnectionHandler - def initialize(pools = Hash.new { |h,k| h[k] = {} }) - @connection_pools = pools - @class_to_pool = Hash.new { |h,k| h[k] = {} } + def initialize + @owner_to_pool = Hash.new { |h,k| h[k] = {} } + @class_to_pool = Hash.new { |h,k| h[k] = {} } end def connection_pools - @connection_pools[Process.pid] + owner_to_pool.values.compact end - def establish_connection(name, spec) - set_pool_for_spec spec, ConnectionAdapters::ConnectionPool.new(spec) - set_class_to_pool name, connection_pools[spec] + def establish_connection(owner, spec) + @class_to_pool.clear + owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection # pools that the ConnectionHandler is managing. def active_connections? - connection_pools.values.any? { |pool| pool.active_connection? } + connection_pools.any?(&:active_connection?) end # Returns any connections in use by the current thread back to the pool, # and also returns connections to the pool cached by threads that are no # longer alive. def clear_active_connections! - connection_pools.each_value {|pool| pool.release_connection } + connection_pools.each(&:release_connection) end # Clears the cache which maps classes. def clear_reloadable_connections! - connection_pools.each_value {|pool| pool.clear_reloadable_connections! } + connection_pools.each(&:clear_reloadable_connections!) end def clear_all_connections! - connection_pools.each_value {|pool| pool.disconnect! } + connection_pools.each(&:disconnect!) end # Locate the connection of the nearest super class. This can be an @@ -552,56 +545,62 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for establish_connection, for easily # re-establishing the connection. - def remove_connection(klass) - pool = class_to_pool.delete(klass.name) - return nil unless pool - - connection_pools.delete pool.spec - pool.automatic_reconnect = false - pool.disconnect! - pool.spec.config + def remove_connection(owner) + if pool = owner_to_pool.delete(owner) + @class_to_pool.clear + pool.automatic_reconnect = false + pool.disconnect! + pool.spec.config + end end + # Retrieving the connection pool happens a lot so we cache it in @class_to_pool. + # This makes retrieving the connection pool O(1) once the process is warm. + # When a connection is established or removed, we invalidate the cache. + # + # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. + # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is + # significantly slower than #[]. So in the nil case, no caching will take place, + # but that's ok since the nil case is not the common one that we wish to optimise + # for. def retrieve_connection_pool(klass) - if !(klass < Model::Tag) - get_pool_for_class('ActiveRecord::Model') # default connection - else - pool = get_pool_for_class(klass.name) - pool || retrieve_connection_pool(klass.superclass) + class_to_pool[klass] ||= begin + until pool = pool_for(klass) + klass = klass.superclass + break unless klass < ActiveRecord::Tag + end + + class_to_pool[klass] = pool || pool_for(ActiveRecord::Model) end end private - def class_to_pool - @class_to_pool[Process.pid] + def owner_to_pool + @owner_to_pool[Process.pid] end - def set_pool_for_spec(spec, pool) - @connection_pools[Process.pid][spec] = pool - end - - def set_class_to_pool(name, pool) - @class_to_pool[Process.pid][name] = pool - pool + def class_to_pool + @class_to_pool[Process.pid] end - def get_pool_for_class(klass) - @class_to_pool[Process.pid].fetch(klass) { - c_to_p = @class_to_pool.values.find { |class_to_pool| - class_to_pool[klass] - } - - if c_to_p - pool = c_to_p[klass] - pool = ConnectionAdapters::ConnectionPool.new pool.spec - set_pool_for_spec pool.spec, pool - set_class_to_pool klass, pool + def pool_for(owner) + owner_to_pool.fetch(owner) { + if ancestor_pool = pool_from_any_process_for(owner) + # A connection was established in an ancestor process that must have + # subsequently forked. We can't reuse the connection, but we can copy + # the specification and establish a new connection with it. + establish_connection owner, ancestor_pool.spec else - set_class_to_pool klass, nil + owner_to_pool[owner] = nil end } end + + def pool_from_any_process_for(owner) + owner_to_pool = @owner_to_pool.values.find { |v| v[owner] } + owner_to_pool && owner_to_pool[owner] + end end class ConnectionManagement 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 b0b51f540c..793f58d4d3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,6 +1,11 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + def initialize + super + reset_transaction + end + # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) @@ -102,20 +107,6 @@ module ActiveRecord exec_delete(to_sql(arel, binds), name, binds) end - # Checks whether there is currently no transaction active. This is done - # by querying the database driver, and does not use the transaction - # house-keeping information recorded by #increment_open_transactions and - # friends. - # - # Returns true if there is no transaction active, false if there is a - # transaction active, and nil if this information is unknown. - # - # Not all adapters supports transaction state introspection. Currently, - # only the PostgreSQL adapter supports this. - def outside_transaction? - nil - end - # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -164,90 +155,119 @@ module ActiveRecord # # active_record_1 now automatically released # end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error! # end + # + # == Transaction isolation + # + # If your database supports setting the isolation level for a transaction, you can set + # it like so: + # + # Post.transaction(isolation: :serializable) do + # # ... + # end + # + # Valid isolation levels are: + # + # * <tt>:read_uncommitted</tt> + # * <tt>:read_committed</tt> + # * <tt>:repeatable_read</tt> + # * <tt>:serializable</tt> + # + # You should consult the documentation for your database to understand the + # semantics of these different levels: + # + # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction + # isolation level. However, support is disabled for mysql versions below 5, + # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] + # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable + options.assert_valid_keys :requires_new, :joinable, :isolation + + if !options[:requires_new] && current_transaction.joinable? + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" + end - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil - if options.has_key?(:joinable) - @transaction_joinable = options[:joinable] + yield else - @transaction_joinable = true + within_new_transaction(options) { yield } end - requires_new = options[:requires_new] || !last_transaction_joinable - - transaction_open = false - @_current_transaction_records ||= [] + rescue ActiveRecord::Rollback + # rollbacks are silently swallowed + end - begin - if block_given? - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) - end - yield - end - rescue Exception => database_transaction_rollback - if transaction_open && !outside_transaction? - transaction_open = false - decrement_open_transactions - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - end - raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) - end + def within_new_transaction(options = {}) #:nodoc: + transaction = begin_transaction(options) + yield + rescue Exception => error + rollback_transaction if transaction + raise ensure - @transaction_joinable = last_transaction_joinable - - if outside_transaction? - @open_transactions = 0 - elsif transaction_open - decrement_open_transactions - begin - if open_transactions == 0 - commit_db_transaction - commit_transaction_records - else - release_savepoint - save_point_records = @_current_transaction_records.pop - unless save_point_records.blank? - @_current_transaction_records.push([]) if @_current_transaction_records.empty? - @_current_transaction_records.last.concat(save_point_records) - end - end - rescue Exception => database_transaction_rollback - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - raise - end + begin + commit_transaction unless error + rescue Exception + rollback_transaction + raise end end + def current_transaction #:nodoc: + @transaction + end + + def transaction_open? + @transaction.open? + end + + def begin_transaction(options = {}) #:nodoc: + @transaction = @transaction.begin(options) + end + + def commit_transaction #:nodoc: + @transaction = @transaction.commit + end + + def rollback_transaction #:nodoc: + @transaction = @transaction.rollback + end + + def reset_transaction #:nodoc: + @transaction = ClosedTransaction.new(self) + end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - last_batch = @_current_transaction_records.last - last_batch << record if last_batch + @transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end + def transaction_isolation_levels + { + read_uncommitted: "READ UNCOMMITTED", + read_committed: "READ COMMITTED", + repeatable_read: "REPEATABLE READ", + serializable: "SERIALIZABLE" + } + end + + # Begins the transaction with the isolation level set. Raises an error by + # default; adapters that support setting the isolation level should implement + # this method. + def begin_isolated_db_transaction(isolation) + raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation" + end + # Commits the transaction (and turns on auto-committing). def commit_db_transaction() end @@ -356,42 +376,6 @@ module ActiveRecord update_sql(sql, name) end - # Send a rollback message to all records after they have been rolled back. If rollback - # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) - if rollback - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - else - records = @_current_transaction_records.pop - end - - unless records.blank? - records.uniq.each do |record| - begin - record.rolledback!(rollback) - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - - # Send a commit message to all records after they have been committed. - def commit_transaction_records - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - unless records.blank? - records.uniq.each do |record| - begin - record.committed! - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb new file mode 100644 index 0000000000..9d6111b51e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + # The goal of this module is to move Adapter specific column + # definitions to the Adapter instead of having it in the schema + # dumper itself. This code represents the normal case. + # We can then redefine how certain data types may be handled in the schema dumper on the + # Adapter level by over-writing this code inside the database spececific adapters + module ColumnDumper + def column_spec(column, types) + spec = prepare_column_options(column, types) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + spec + end + + # This can be overridden on a Adapter level basis to support other + # extended datatypes (Example: Adding an array option in the + # PostgreSQLAdapter) + def prepare_column_options(column, types) + spec = {} + spec[:name] = column.name.inspect + + # AR has an optimization which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type + 'decimal' + else + column.type.to_s + end + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null + spec[:default] = default_string(column.default) if column.has_default? + spec + end + + # Lists the valid migration options + def migration_keys + [:name, :limit, :precision, :scale, :default, :null] + end + + private + + def default_string(value) + case value + when BigDecimal + value.to_s + when Date, DateTime, Time + "'#{value.to_s(:db)}'" + else + value.inspect + end + 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 new file mode 100644 index 0000000000..4cca94e40b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -0,0 +1,165 @@ +module ActiveRecord + module ConnectionAdapters + class Transaction #:nodoc: + attr_reader :connection + + def initialize(connection) + @connection = connection + end + end + + class ClosedTransaction < Transaction #:nodoc: + def number + 0 + end + + def begin(options = {}) + RealTransaction.new(connection, self, options) + end + + def closed? + true + end + + def open? + false + end + + def joinable? + false + end + + # This is a noop when there are no open transactions + def add_record(record) + end + end + + class OpenTransaction < Transaction #:nodoc: + attr_reader :parent, :records + attr_writer :joinable + + def initialize(connection, parent, options = {}) + super connection + + @parent = parent + @records = [] + @finishing = false + @joinable = options.fetch(:joinable, true) + end + + # This state is necesarry so that we correctly handle stuff that might + # happen in a commit/rollback. But it's kinda distasteful. Maybe we can + # find a better way to structure it in the future. + def finishing? + @finishing + end + + def joinable? + @joinable && !finishing? + end + + def number + if finishing? + parent.number + else + parent.number + 1 + end + end + + def begin(options = {}) + if finishing? + parent.begin + else + SavepointTransaction.new(connection, self, options) + end + end + + def rollback + @finishing = true + perform_rollback + parent + end + + def commit + @finishing = true + perform_commit + parent + end + + def add_record(record) + records << record + end + + def rollback_records + records.uniq.each do |record| + begin + record.rolledback!(parent.closed?) + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def commit_records + records.uniq.each do |record| + begin + record.committed! + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def closed? + false + end + + def open? + true + end + end + + class RealTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent, options = {}) + super + + if options[:isolation] + connection.begin_isolated_db_transaction(options[:isolation]) + else + connection.begin_db_transaction + end + end + + def perform_rollback + connection.rollback_db_transaction + rollback_records + end + + def perform_commit + connection.commit_db_transaction + commit_records + end + end + + class SavepointTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent, options = {}) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + + super + connection.create_savepoint + end + + def perform_rollback + connection.rollback_to_savepoint + rollback_records + end + + def perform_commit + connection.release_savepoint + records.each { |r| parent.add_record(r) } + 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 b3f9187429..0cb219767b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,7 +3,9 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' +require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -33,6 +35,12 @@ module ActiveRecord autoload :QueryCache end + autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload :ClosedTransaction + autoload :RealTransaction + autoload :SavepointTransaction + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an @@ -52,6 +60,7 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include MonitorMixin + include ColumnDumper define_callbacks :checkout, :checkin @@ -62,13 +71,11 @@ module ActiveRecord def initialize(connection, logger = nil, pool = nil) #:nodoc: super() - @active = nil @connection = connection @in_use = false @instrumenter = ActiveSupport::Notifications.instrumenter @last_use = false @logger = logger - @open_transactions = 0 @pool = pool @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false @@ -160,6 +167,11 @@ module ActiveRecord false end + # Does this adapter support setting the isolation level for a transaction? + def supports_transaction_isolation? + false + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current @@ -181,19 +193,21 @@ module ActiveRecord # checking whether the database is actually capable of responding, i.e. whether # the connection isn't stale. def active? - @active != false end # Disconnects from the database if already connected, and establishes a - # new connection with the database. + # new connection with the database. Implementors should call super if they + # override the default implementation. def reconnect! - @active = true + clear_cache! + reset_transaction end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - @active = false + clear_cache! + reset_transaction end # Reset the state of this connection, directing the DBMS to clear @@ -236,18 +250,21 @@ module ActiveRecord @connection end - attr_reader :open_transactions + def open_transactions + @transaction.number + end def increment_open_transactions - @open_transactions += 1 + ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" end def decrement_open_transactions - @open_transactions -= 1 + ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" end def transaction_joinable=(joinable) - @transaction_joinable = joinable + ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + @transaction.joinable = joinable end def create_savepoint 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 1126fe7fce..1783b036a2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -169,6 +169,14 @@ module ActiveRecord true end + # MySQL 4 technically support transaction isolation, but it is affected by a bug + # where the transaction level gets persisted for the whole session: + # + # http://bugs.mysql.com/bug.php?id=39170 + def supports_transaction_isolation? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -269,6 +277,13 @@ module ActiveRecord # Transactions aren't supported end + def begin_isolated_db_transaction(isolation) + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + begin_db_transaction + rescue + # Transactions aren't supported + end + def commit_db_transaction #:nodoc: execute "COMMIT" rescue diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1445bb3b2f..816b5e17c1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -114,7 +114,7 @@ module ActiveRecord case type when :string, :text then var_name - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :integer then "(#{var_name}.to_i)" when :float then "#{var_name}.to_f" when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" @@ -124,6 +124,7 @@ module ActiveRecord when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" + when :json then "#{klass}.string_to_json(#{var_name})" else var_name end end @@ -178,7 +179,13 @@ module ActiveRecord return string unless string.is_a?(String) return nil if string.blank? - string_to_time "2000-01-01 #{string}" + dummy_time_string = "2000-01-01 #{string}" + + fast_string_to_time(dummy_time_string) || begin + time_hash = Date._parse(dummy_time_string) + return nil if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end end # convert something to a boolean diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index dd40351a38..b9a61f7d91 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -72,7 +72,7 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } - spec.reject!{ |_,value| !value } + spec.reject!{ |_,value| value.blank? } if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys spec.merge!(options) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8fc172f6e8..328d080687 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -74,6 +74,7 @@ module ActiveRecord end def reconnect! + super disconnect! connect end @@ -82,6 +83,7 @@ module ActiveRecord # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! + super unless @connection.nil? @connection.close @connection = nil diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 3b0353358a..0b936bbf39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -7,8 +7,6 @@ require 'mysql' class Mysql class Time - ### - # This monkey patch is for test_additional_columns_from_join_table def to_date Date.new(year, month, day) end @@ -191,14 +189,15 @@ module ActiveRecord end def reconnect! + super disconnect! - clear_cache! connect end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! + super @connection.close rescue nil end @@ -298,13 +297,6 @@ module ActiveRecord @connection.insert_id end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - module Fields class Type def type; end @@ -437,7 +429,7 @@ module ActiveRecord } end } - result_set = Result.new(types.keys, result.to_a, types) + result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) result.free else result_set = ActiveRecord::Result.new([], []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb new file mode 100644 index 0000000000..b7d24f2bb3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -0,0 +1,97 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module ArrayParser + private + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + def parse_pg_array(string) + parse_data(string, 0) + end + end + + def parse_data(string, index) + local_index = index + array = [] + while(local_index < string.length) + case string[local_index] + when '{' + local_index,array = parse_array_contents(array, string, local_index + 1) + when '}' + return array + end + local_index += 1 + end + + array + end + + def parse_array_contents(array, string, index) + is_escaping = false + is_quoted = false + was_quoted = false + current_item = '' + + local_index = index + while local_index + token = string[local_index] + if is_escaping + current_item << token + is_escaping = false + else + if is_quoted + case token + when '"' + is_quoted = false + was_quoted = true + when "\\" + is_escaping = true + else + current_item << token + end + else + case token + when "\\" + is_escaping = true + when ',' + add_item_to_array(array, current_item, was_quoted) + current_item = '' + was_quoted = false + when '"' + is_quoted = true + when '{' + internal_items = [] + local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) + array.push(internal_items) + when '}' + add_item_to_array(array, current_item, was_quoted) + return local_index,array + else + current_item << token + end + end + end + + local_index += 1 + end + return local_index,array + end + + def add_item_to_array(array, current_item, quoted) + if current_item.length == 0 + elsif !quoted && current_item == 'NULL' + array.push nil + else + array.push current_item + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb new file mode 100644 index 0000000000..62d091357d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -0,0 +1,124 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module Cast + def string_to_time(string) + return string unless String === string + + case string + when 'infinity'; 1.0 / 0.0 + when '-infinity'; -1.0 / 0.0 + else + super + end + end + + def hstore_to_string(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ',' + else + object + end + end + + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string + end + end + + def json_to_string(object) + if Hash === object + ActiveSupport::JSON.encode(object) + else + object + end + end + + def array_to_string(value, column, adapter, should_be_quoted = false) + casted_values = value.map do |val| + if String === val + if val == "NULL" + "\"#{val}\"" + else + quote_and_escape(adapter.type_cast(val, column, true)) + end + else + adapter.type_cast(val, column, true) + end + end + "{#{casted_values.join(',')}}" + end + + def string_to_json(string) + if String === string + ActiveSupport::JSON.decode(string) + else + string + end + end + + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + + def string_to_array(string, oid) + parse_pg_array(string).map{|val| oid.type_cast val} + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + + def quote_and_escape(value) + case value + when "NULL" + value + else + "\"#{value.gsub(/"/,"\\\"")}\"" + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb new file mode 100644 index 0000000000..553985bd1e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -0,0 +1,245 @@ +require 'active_support/deprecation' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module DatabaseStatements + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + + # Executes a SELECT query and returns an array of rows. Each row is an + # array of field values. + def select_rows(sql, name = nil) + select_raw(sql, name).last + end + + # Executes an INSERT query and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) + else + super + end + end + + def create + super.insert + end + + # create a 2D array representing the result set + def result_as_array(res) #:nodoc: + # check if we have any binary column and if they need escaping + ftypes = Array.new(res.nfields) do |i| + [i, res.ftype(i)] + end + + rows = res.values + return rows unless ftypes.any? { |_, x| + x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID + } + + typehash = ftypes.group_by { |_, type| type } + binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] + monies = typehash[MONEY_COLUMN_TYPE_OID] || [] + + rows.each do |row| + # unescape string passed BYTEA field (OID == 17) + binaries.each do |index, _| + row[index] = unescape_bytea(row[index]) + end + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + monies.each do |index, _| + data = row[index] + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + case data + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + end + end + end + + # Queries the database and returns the results in an Array-like object + def query(sql, name = nil) #:nodoc: + log(sql, name) do + result_as_array @connection.async_exec(sql) + end + end + + # Executes an SQL statement, returning a PGresult object on success + # or raising a PGError exception otherwise. + def execute(sql, name = nil) + log(sql, name) do + @connection.async_exec(sql) + end + end + + def substitute_at(column, index) + Arel::Nodes::BindParam.new "$#{index + 1}" + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = ActiveRecord::Result.new(result.fields, result.values, types) + result.clear + return ret + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + affected = result.cmd_tuples + result.clear + affected + end + end + alias :exec_update :exec_delete + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end + + [sql, binds] + end + + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + + # Executes an UPDATE query and returns the number of affected tuples. + def update_sql(sql, name = nil) + super.cmd_tuples + end + + # Begins a transaction. + def begin_db_transaction + execute "BEGIN" + end + + def begin_isolated_db_transaction(isolation) + begin_db_transaction + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + end + + # Commits a transaction. + def commit_db_transaction + execute "COMMIT" + end + + # Aborts a transaction. + def rollback_db_transaction + execute "ROLLBACK" + end + + def outside_transaction? + ActiveSupport::Deprecation.warn( + "#outside_transaction? is deprecated. This method was only really used " \ + "internally, but you can use #transaction_open? instead." + ) + @connection.transaction_status == PGconn::PQTRANS_IDLE + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 6657491c06..52344f61c0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -63,6 +63,21 @@ module ActiveRecord end end + class Array < Type + attr_reader :subtype + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + class Integer < Type def type_cast(value) return if value.nil? @@ -145,6 +160,14 @@ module ActiveRecord end end + class Json < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + end + class TypeMap def initialize @mapping = {} @@ -244,6 +267,7 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + register_type 'json', OID::Json.new register_type 'cidr', OID::Cidr.new alias_type 'inet', 'cidr' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb new file mode 100644 index 0000000000..37d43d891d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -0,0 +1,141 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module Quoting + # Escapes binary strings for bytea input to the database. + def escape_bytea(value) + PGconn.escape_bytea(value) if value + end + + # Unescapes bytea output from a database to the binary string it represents. + # NOTE: This is NOT an inverse of escape_bytea! This is only to be used + # on escaped binary output from database drive. + def unescape_bytea(value) + PGconn.unescape_bytea(value) if value + end + + # Quotes PostgreSQL-specific data types for SQL input. + def quote(value, column = nil) #:nodoc: + return super unless column + + case value + when Array + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + else + super + end + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + when 'json' then super(PostgreSQLColumn.json_to_string(value), column) + else super + end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end + when Float + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end + when Numeric + return super unless column.sql_type == 'money' + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + when String + case column.sql_type + when 'bytea' then "'#{escape_bytea(value)}'" + when 'xml' then "xml '#{quote_string(value)}'" + when /^bit/ + case value + when /^[01]*$/ then "B'#{value}'" # Bit-string notation + when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation + end + else + super + end + else + super + end + end + + def type_cast(value, column, array_member = false) + return super(value, column) unless column + + case value + when NilClass + if column.array && array_member + 'NULL' + elsif column.array + value + else + super(value, column) + end + when Array + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) + when String + return super(value, column) unless 'bytea' == column.sql_type + { :value => value, :format => 1 } + when Hash + case column.sql_type + when 'hstore' then PostgreSQLColumn.hstore_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else super(value, column) + end + when IPAddr + return super(value, column) unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super(value, column) + end + end + + # Quotes strings for use in SQL input. + def quote_string(s) #:nodoc: + @connection.escape(s) + end + + # Checks the following cases: + # + # - table_name + # - "table.name" + # - schema_name.table_name + # - schema_name."table.name" + # - "schema.name".table_name + # - "schema.name"."table.name" + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + + # Quotes column names for use in SQL queries. + def quote_column_name(name) #:nodoc: + PGconn.quote_ident(name.to_s) + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb new file mode 100644 index 0000000000..16da3ea732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module ReferentialIntegrity + def supports_disable_referential_integrity? #:nodoc: + true + end + + def disable_referential_integrity #:nodoc: + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + yield + ensure + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + 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 new file mode 100644 index 0000000000..8a073bf878 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -0,0 +1,446 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module SchemaStatements + # Drops the database specified on the +name+ attribute + # and creates it again using the provided +options+. + def recreate_database(name, options = {}) #:nodoc: + drop_database(name) + create_database(name, options) + end + + # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, + # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses + # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). + # + # Example: + # create_database config[:database], config + # create_database 'foo_development', :encoding => 'unicode' + def create_database(name, options = {}) + options = options.reverse_merge(:encoding => "utf8") + + option_string = options.symbolize_keys.sum do |key, value| + case key + when :owner + " OWNER = \"#{value}\"" + when :template + " TEMPLATE = \"#{value}\"" + when :encoding + " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" + when :tablespace + " TABLESPACE = \"#{value}\"" + when :connection_limit + " CONNECTION LIMIT = #{value}" + else + "" + end + end + + execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" + end + + # Drops a PostgreSQL database. + # + # Example: + # drop_database 'matt_development' + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + end + + # Returns the list of all tables in the schema search path or a specified schema. + def tables(name = nil) + query(<<-SQL, 'SCHEMA').map { |row| row[0] } + SELECT tablename + FROM pg_tables + WHERE schemaname = 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) + schema, table = Utils.extract_schema_and_table(name.to_s) + return false unless table + + binds = [[nil, table]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + SQL + end + + # Returns true if schema exists. + def schema_exists?(name) + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_namespace + WHERE nspname = '#{name}' + SQL + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[query(<<-SQL, "SCHEMA")] + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + + column_names = columns.values_at(*indkey).compact + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + end.compact + end + + # Returns the list of all column definitions for a table. + def columns(table_name) + # Limit, precision, and scale are all handled by the superclass. + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + end + end + + # Returns the current database name. + def current_database + query('select current_database()', 'SCHEMA')[0][0] + end + + # Returns the current schema name. + def current_schema + query('SELECT current_schema', 'SCHEMA')[0][0] + end + + # Returns the current database encoding format. + def encoding + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database + WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database collation. + def collation + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database ctype. + def ctype + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns an array of schema names. + def schema_names + query(<<-SQL, 'SCHEMA').flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + + # Sets the schema search path to a string of comma-separated schema names. + # Names beginning with $ have to be quoted (e.g. $user => '$user'). + # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html + # + # This should be not be called manually but set in database.yml. + def schema_search_path=(schema_csv) + if schema_csv + execute("SET search_path TO #{schema_csv}", 'SCHEMA') + @schema_search_path = schema_csv + end + end + + # Returns the active schema search path. + def schema_search_path + @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + end + + # Returns the current client message level. + def client_min_messages + query('SHOW client_min_messages', 'SCHEMA')[0][0] + end + + # Set the client message level. + def client_min_messages=(level) + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') + end + + # Returns the sequence name for a table's primary key or some other specified key. + def default_sequence_name(table_name, pk = nil) #:nodoc: + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') + eosql + result.rows.first.first + end + + # Resets the sequence of a table's primary key to the maximum value. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + + pk ||= default_pk + sequence ||= default_sequence + end + + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'SCHEMA' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql + end + end + + # Returns a table's primary key and belonging sequence. + 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] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = query(<<-end_sql, 'SCHEMA')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + + [result.first, result.last] + rescue + nil + end + + # Returns just a table's primary key + def primary_key(table) + row = exec_query(<<-end_sql, 'SCHEMA').rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr + INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + WHERE cons.contype = 'p' + AND dep.refobjid = '#{table}'::regclass + end_sql + + row && row.first + end + + # Renames a table. + # Also renames a table's primary key sequence if the sequence name matches the + # Active Record default. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + pk, seq = pk_and_sequence_for(new_name) + if seq == "#{name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + end + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + clear_cache! + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + + execute add_column_sql + end + + # Changes the column of a table. + def change_column(table_name, column_name, type, options = {}) + clear_cache! + quoted_table_name = quote_table_name(table_name) + + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + end + + # Changes the default value of a table column. + def change_column_default(table_name, column_name, default) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + clear_cache! + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + end + + # Renames a column in a table. + def rename_column(table_name, column_name, new_column_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def remove_index!(table_name, index_name) #:nodoc: + execute "DROP INDEX #{quote_table_name(index_name)}" + end + + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + 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) + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + when 'datetime' + return super unless precision + + case precision + when 0..6; "timestamp(#{precision})" + else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end + else + super + end + end + + # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + def distinct(columns, orders) #:nodoc: + return "DISTINCT #{columns}" if orders.empty? + + # Construct a clean list of column names from the ORDER BY clause, removing + # any ASC/DESC modifiers + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end + order_columns.delete_if { |c| c.blank? } + order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } + + "DISTINCT #{columns}, #{order_columns * ', '}" + end + end + end + 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 8e9ce80697..5e35f472c7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,6 +1,12 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' +require 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/array_parser' +require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/schema_statements' +require 'active_record/connection_adapters/postgresql/database_statements' +require 'active_record/connection_adapters/postgresql/referential_integrity' require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -36,80 +42,24 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type - super(name, self.class.extract_value_from_default(default), sql_type, null) + if sql_type =~ /\[\]$/ + @array = true + super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, self.class.extract_value_from_default(default), sql_type, null) + end end # :stopdoc: class << self + include ConnectionAdapters::PostgreSQLColumn::Cast + include ConnectionAdapters::PostgreSQLColumn::ArrayParser attr_accessor :money_precision - def string_to_time(string) - return string unless String === string - - case string - when 'infinity' then 1.0 / 0.0 - when '-infinity' then -1.0 / 0.0 - else - super - end - end - - def hstore_to_string(object) - if Hash === object - object.map { |k,v| - "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ',' - else - object - end - end - - def string_to_hstore(string) - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k,v| - v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - [k,v] - }] - else - string - end - end - - def string_to_cidr(string) - if string.nil? - nil - elsif String === string - IPAddr.new(string) - else - string - end - end - - def cidr_to_string(object) - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - end - end - - private - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - value.nil? ? 'NULL' - : value == "" ? '""' - : '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end end # :startdoc: @@ -164,6 +114,9 @@ module ActiveRecord # Hstore when /\A'(.*)'::hstore\z/ $1 + # JSON + when /\A'(.*)'::json\z/ + $1 # Object identifier types when /\A-?\d+\z/ $1 @@ -182,90 +135,94 @@ module ActiveRecord end private - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super + + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + when /^timestamp/i; nil + else super + end end - end - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ + else + super + end end - end - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore - # Network address types - when 'inet' - :inet - when 'cidr' - :cidr - when 'macaddr' - :macaddr - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :uuid - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. - else - super + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when /^interval(?:|\(\d+\))$/ + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :uuid + # JSON type + when 'json' + :json + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super + end end - end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -294,6 +251,10 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) options = args.extract_options! @@ -324,37 +285,77 @@ module ActiveRecord def uuid(name, options = {}) column(name, 'uuid', options) end + + def json(name, options = {}) + column(name, 'json', options) + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end end ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { - :primary_key => "serial primary key", - :string => { :name => "character varying", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "timestamp" }, - :timestamp => { :name => "timestamp" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "bytea" }, - :boolean => { :name => "boolean" }, - :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" }, - :inet => { :name => "inet" }, - :cidr => { :name => "cidr" }, - :macaddr => { :name => "macaddr" }, - :uuid => { :name => "uuid" } + primary_key: "serial primary key", + string: { name: "character varying", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "timestamp" }, + timestamp: { name: "timestamp" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "bytea" }, + boolean: { name: "boolean" }, + xml: { name: "xml" }, + tsvector: { name: "tsvector" }, + hstore: { name: "hstore" }, + inet: { name: "inet" }, + cidr: { name: "cidr" }, + macaddr: { name: "macaddr" }, + uuid: { name: "uuid" }, + json: { name: "json" } } + include Quoting + include ReferentialIntegrity + include SchemaStatements + include DatabaseStatements + # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name ADAPTER_NAME end + # Adds `:array` option to the default set provided by the + # AbstractAdapter + def prepare_column_options(column, types) + spec = super + spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec + end + + # Adds `:array` as a valid migration key + def migration_keys + super + [:array] + end + # Returns +true+, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -369,6 +370,10 @@ module ActiveRecord true end + def supports_transaction_isolation? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -406,19 +411,20 @@ module ActiveRecord end private - def cache - @cache[Process.pid] - end - def dealloc(key) - @connection.query "DEALLOCATE #{key}" if connection_active? - end + def cache + @cache[Process.pid] + end - def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError - false - end + def dealloc(key) + @connection.query "DEALLOCATE #{key}" if connection_active? + end + + def connection_active? + @connection.status == PGconn::CONNECTION_OK + rescue PGError + false + end end class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: @@ -471,9 +477,8 @@ module ActiveRecord # Close then reopen the connection. def reconnect! - clear_cache! + super @connection.reset - @open_transactions = 0 configure_connection end @@ -485,7 +490,7 @@ module ActiveRecord # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - clear_cache! + super @connection.close rescue nil end @@ -534,119 +539,11 @@ module ActiveRecord @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end - # QUOTING ================================================== - - # Escapes binary strings for bytea input to the database. - def escape_bytea(value) - PGconn.escape_bytea(value) if value - end - - # Unescapes bytea output from a database to the binary string it represents. - # NOTE: This is NOT an inverse of escape_bytea! This is only to be used - # on escaped binary output from database drive. - def unescape_bytea(value) - PGconn.unescape_bytea(value) if value - end - - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Hash - case column.sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - else super - end - when IPAddr - case column.sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end - when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? - "'#{value.to_s}'" - else - super - end - when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - when String - case column.sql_type - when 'bytea' then "'#{escape_bytea(value)}'" - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end - else - super - end - end - - def type_cast(value, column) - return super unless column - - case value - when String - return super unless 'bytea' == column.sql_type - { :value => value, :format => 1 } - when Hash - return super unless 'hstore' == column.sql_type - PostgreSQLColumn.hstore_to_string(value) - when IPAddr - return super unless ['inet','cidr'].includes? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super - end - end - - # Quotes strings for use in SQL input. - def quote_string(s) #:nodoc: - @connection.escape(s) - end - - # Checks the following cases: - # - # - table_name - # - "table.name" - # - schema_name.table_name - # - schema_name."table.name" - # - "schema.name".table_name - # - "schema.name"."table.name" - def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end - end - - # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' end + super end # Set the authorized user for this session @@ -655,698 +552,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - # REFERENTIAL INTEGRITY ==================================== - - def supports_disable_referential_integrity? #:nodoc: - true - end - - def disable_referential_integrity #:nodoc: - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - end - yield - ensure - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last - end - - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - alias :create :insert - - # create a 2D array representing the result set - def result_as_array(res) #:nodoc: - # check if we have any binary column and if they need escaping - ftypes = Array.new(res.nfields) do |i| - [i, res.ftype(i)] - end - - rows = res.values - return rows unless ftypes.any? { |_, x| - x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID - } - - typehash = ftypes.group_by { |_, type| type } - binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] - monies = typehash[MONEY_COLUMN_TYPE_OID] || [] - - rows.each do |row| - # unescape string passed BYTEA field (OID == 17) - binaries.each do |index, _| - row[index] = unescape_bytea(row[index]) - end - - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - monies.each do |index, _| - data = row[index] - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - case data - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - end - end - end - - - # Queries the database and returns the results in an Array-like object - def query(sql, name = nil) #:nodoc: - log(sql, name) do - result_as_array @connection.async_exec(sql) - end - end - - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - def execute(sql, name = nil) - log(sql, name) do - @connection.async_exec(sql) - end - end - - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - - def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - types = {} - result.fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } - end - - ret = Result.new(result.fields, result.values, types) - result.clear - return ret - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end - end - alias :exec_update :exec_delete - - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - sql = "#{sql} RETURNING #{quote_column_name(pk)}" - end - - [sql, binds] - end - - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - val = exec_query(sql, name, binds) - if !use_insert_returning? && pk - unless sequence_name - table_ref = extract_table_ref_from_insert_sql(sql) - sequence_name = default_sequence_name(table_ref, pk) - return val unless sequence_name - end - last_insert_id_result(sequence_name) - else - val - end - end - - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - - # Begins a transaction. - def begin_db_transaction - execute "BEGIN" - end - - # Commits a transaction. - def commit_db_transaction - execute "COMMIT" - end - - # Aborts a transaction. - def rollback_db_transaction - execute "ROLLBACK" - end - - def outside_transaction? - @connection.transaction_status == PGconn::PQTRANS_IDLE - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - # SCHEMA STATEMENTS ======================================== - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) #:nodoc: - drop_database(name) - create_database(name, options) - end - - # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, - # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses - # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). - # - # Example: - # create_database config[:database], config - # create_database 'foo_development', :encoding => 'unicode' - def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") - - option_string = options.symbolize_keys.sum do |key, value| - case key - when :owner - " OWNER = \"#{value}\"" - when :template - " TEMPLATE = \"#{value}\"" - when :encoding - " ENCODING = '#{value}'" - when :collation - " LC_COLLATE = '#{value}'" - when :ctype - " LC_CTYPE = '#{value}'" - when :tablespace - " TABLESPACE = \"#{value}\"" - when :connection_limit - " CONNECTION LIMIT = #{value}" - else - "" - end - end - - execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" - end - - # Drops a PostgreSQL database. - # - # Example: - # drop_database 'matt_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - end - - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = 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) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table - - binds = [[nil, table]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' - AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} - SQL - end - - # Returns true if schema exists. - def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = '#{name}' - SQL - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) - ORDER BY i.relname - SQL - - result.map do |row| - index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") - inddef = row[3] - oid = row[4] - - columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL - - column_names = columns.values_at(*indkey).compact - - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) - end.compact - end - - # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { - OID::Identity.new - } - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') - end - end - - # Returns the current database name. - def current_database - query('select current_database()', 'SCHEMA')[0][0] - end - - # Returns the current schema name. - def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] - end - - # Returns the current database encoding format. - def encoding - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns the current database collation. - def collation - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns the current database ctype. - def ctype - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns an array of schema names. - def schema_names - query(<<-SQL, 'SCHEMA').flatten - SELECT nspname - FROM pg_namespace - WHERE nspname !~ '^pg_.*' - AND nspname NOT IN ('information_schema') - ORDER by nspname; - SQL - end - - # Creates a schema for the given schema name. - def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" - end - - # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" - end - - # Sets the schema search path to a string of comma-separated schema names. - # Names beginning with $ have to be quoted (e.g. $user => '$user'). - # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html - # - # This should be not be called manually but set in database.yml. - def schema_search_path=(schema_csv) - if schema_csv - execute("SET search_path TO #{schema_csv}", 'SCHEMA') - @schema_search_path = schema_csv - end - end - - # Returns the active schema search path. - def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] - end - - # Returns the current client message level. - def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] - end - - # Set the client message level. - def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'", 'SCHEMA') - end - - # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - result = serial_sequence(table_name, pk || 'id') - return nil unless result - result.split('.').last - rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" - end - - def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') - SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql - result.rows.first.first - end - - # Resets the sequence of a table's primary key to the maximum value. - def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence - default_pk, default_sequence = pk_and_sequence_for(table) - - pk ||= default_pk - sequence ||= default_sequence - end - - if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" - end - - if pk && sequence - quoted_sequence = quote_table_name(sequence) - - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - end - end - - # Returns a table's primary key and belonging sequence. - 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, 'PK and serial sequence')[0] - SELECT attr.attname, seq.relname - FROM pg_class seq, - pg_attribute attr, - pg_depend dep, - pg_namespace name, - pg_constraint cons - WHERE seq.oid = dep.objid - AND seq.relkind = 'S' - AND attr.attrelid = dep.refobjid - AND attr.attnum = dep.refobjsubid - AND attr.attrelid = cons.conrelid - AND attr.attnum = cons.conkey[1] - AND cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass - end_sql - - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname, - CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) - END - FROM pg_class t - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{quote_table_name(table)}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - - [result.first, result.last] - rescue - nil - end - - # Returns just a table's primary key - def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT DISTINCT(attr.attname) - FROM pg_attribute attr - INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - WHERE cons.contype = 'p' - AND dep.refobjid = '#{table}'::regclass - end_sql - - row && row.first - end - - # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - pk, seq = pk_and_sequence_for(new_name) - if seq == "#{name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" - execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" - end - end - - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) - clear_cache! - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - - execute add_column_sql - end - - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) - clear_cache! - quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - end - - # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" - end - - def change_column_null(table_name, column_name, null, default = nil) - clear_cache! - unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") - end - - # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" - end - - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" - end - - def rename_index(table_name, old_name, new_name) - execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" - 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) - case type.to_s - when 'binary' - # PostgreSQL doesn't support limits on binary (bytea) columns. - # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. - case limit - when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") - end - when 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") - end - when 'datetime' - return super unless precision - - case precision - when 0..6; "timestamp(#{precision})" - else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") - end - else - super - end - end - - # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and - # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", "posts.created_at desc") - def distinct(columns, orders) #:nodoc: - return "DISTINCT #{columns}" if orders.empty? - - # Construct a clean list of column names from the ORDER BY clause, removing - # any ASC/DESC modifiers - order_columns = orders.collect do |s| - s = s.to_sql unless s.is_a?(String) - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') - end - order_columns.delete_if { |c| c.blank? } - order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - - "DISTINCT #{columns}, #{order_columns * ', '}" - end - module Utils extend self @@ -1371,6 +576,7 @@ module ActiveRecord end protected + # Returns the version of the connected PostgreSQL server. def postgresql_version @connection.server_version @@ -1392,21 +598,30 @@ module ActiveRecord end private - def initialize_type_map - result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') - leaves, nodes = result.partition { |row| row['typelem'] == '0' } - # populate the leaf nodes - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] - end + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } + + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end - # populate composite types - nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| - vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] - OID::TYPE_MAP[row['oid'].to_i] = vector + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end + + # populate array types + arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = array + end end - end FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: @@ -1548,12 +763,12 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass - AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + FROM pg_attribute a LEFT JOIN pg_attrdef d + ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum end_sql end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4fe0013f0f..4a48812807 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -104,6 +104,8 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + + @active = nil @statements = StatementPool.new(@connection, config.fetch(:statement_limit) { 1000 }) @config = config @@ -154,11 +156,15 @@ module ActiveRecord true end + def active? + @active != false + end + # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! super - clear_cache! + @active = false @connection.close rescue nil end @@ -397,7 +403,7 @@ module ActiveRecord table_name, row['name'], row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "Columns for index #{row['name']} on #{table_name}").map { |col| + exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| col['name'] }) end |