diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
40 files changed, 1646 insertions, 1537 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 6535121075..ccd2899489 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,5 +1,5 @@ require 'thread' -require 'thread_safe' +require 'concurrent/map' require 'monitor' module ActiveRecord @@ -10,8 +10,9 @@ module ActiveRecord end # Raised when a pool was unable to get ahold of all its connections - # to perform a "group" action such as +ConnectionPool#disconnect!+ - # or +ConnectionPool#clear_reloadable_connections!+. + # to perform a "group" action such as + # {ActiveRecord::Base.connection_pool.disconnect!}[rdoc-ref:ConnectionAdapters::ConnectionPool#disconnect!] + # or {ActiveRecord::Base.clear_reloadable_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_reloadable_connections!]. class ExclusiveConnectionTimeoutError < ConnectionTimeoutError end @@ -37,17 +38,18 @@ module ActiveRecord # Connections can be obtained and used from a connection pool in several # ways: # - # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and + # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection] + # as with Active Record 2.1 and # earlier (pre-connection-pooling). Eventually, when you're done with # the connection(s) and wish it to be returned to the pool, you call - # ActiveRecord::Base.clear_active_connections!. This will be the - # default behavior for Active Record when used in conjunction with + # {ActiveRecord::Base.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!]. + # This will be the default behavior for Active Record when used in conjunction with # Action Pack's request handling cycle. # 2. Manually check out a connection from the pool with - # ActiveRecord::Base.connection_pool.checkout. You are responsible for + # {ActiveRecord::Base.connection_pool.checkout}[rdoc-ref:#checkout]. You are responsible for # returning this connection to the pool when finished by calling - # ActiveRecord::Base.connection_pool.checkin(connection). - # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which + # {ActiveRecord::Base.connection_pool.checkin(connection)}[rdoc-ref:#checkin]. + # 3. Use {ActiveRecord::Base.connection_pool.with_connection(&block)}[rdoc-ref:#with_connection], which # obtains a connection, yields it as the sole argument to the block, # and returns it to the pool after the block completes. # @@ -140,7 +142,7 @@ module ActiveRecord # become available. # # Raises: - # - ConnectionTimeoutError if +timeout+ is given and no element + # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize { internal_poll(timeout) } @@ -195,7 +197,7 @@ module ActiveRecord elapsed = Time.now - t0 if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + msg = 'could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' % [timeout, elapsed] raise ConnectionTimeoutError, msg end @@ -331,18 +333,18 @@ module ActiveRecord # of the cache is to speed-up +connection+ method, it is not the authoritative # registry of which thread owns which connection, that is tracked by # +connection.owner+ attr on each +connection+ instance. - # The invariant works like this: if there is mapping of +thread => conn+, + # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, # then that +thread+ does indeed own that +conn+, however an absence of a such # mapping does not mean that the +thread+ doesn't own the said connection, in # that case +conn.owner+ attr should be consulted. # Access and modification of +@thread_cached_conns+ does not require # synchronization. - @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size) + @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size) @connections = [] @automatic_reconnect = true - # Connection pool allows for concurrent (outside the main `synchronize` section) + # Connection pool allows for concurrent (outside the main +synchronize+ section) # establishment of new connections. This variable tracks the number of threads # currently in the process of independently establishing connections to the DB. @now_connecting = 0 @@ -364,7 +366,7 @@ module ActiveRecord # Is there an open connection that is being used for the current thread? # - # This method only works for connections that have been abtained through + # This method only works for connections that have been obtained through # #connection or #with_connection methods, connections obtained through # #checkout will not be detected by #active_connection? def active_connection? @@ -406,9 +408,9 @@ module ActiveRecord # Disconnects all connections in the pool, and clears the pool. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def disconnect(raise_on_acquisition_timeout = true) with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @@ -426,8 +428,8 @@ module ActiveRecord # # The pool first tries to gain ownership of all connections, if unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool is forcefully - # disconneted wihout any regard for other connection owning threads. + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully + # disconnected without any regard for other connection owning threads. def disconnect! disconnect(false) end @@ -436,9 +438,9 @@ module ActiveRecord # require reloading. # # Raises: - # - +ExclusiveConnectionTimeoutError+ if unable to gain ownership of all + # - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all # connections in the pool within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds). + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def clear_reloadable_connections(raise_on_acquisition_timeout = true) num_new_conns_required = 0 @@ -474,7 +476,7 @@ module ActiveRecord # # The pool first tries to gain ownership of all connections, if unable to # do so within a timeout interval (default duration is - # +spec.config[:checkout_timeout] * 2+ seconds), the pool forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool forcefully # clears the cache and reloads connections without any regard for other # connection owning threads. def clear_reloadable_connections! @@ -494,7 +496,7 @@ module ActiveRecord # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool. + # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool. def checkout(checkout_timeout = @checkout_timeout) checkout_and_verify(acquire_connection(checkout_timeout)) end @@ -503,12 +505,12 @@ module ActiveRecord # no longer need this connection. # # +conn+: an AbstractAdapter object, which was obtained by earlier by - # calling +checkout+ on this pool. + # calling #checkout on this pool. def checkin(conn) synchronize do remove_connection_from_thread_cache conn - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -516,7 +518,7 @@ module ActiveRecord end end - # Remove a connection from the connection pool. The connection will + # Remove a connection from the connection pool. The connection will # remain open and active but will no longer be managed by this pool. def remove(conn) needs_new_connection = false @@ -547,7 +549,7 @@ module ActiveRecord bulk_make_new_connections(1) if needs_new_connection end - # Recover lost connections for the pool. A lost connection can occur if + # Recover lost connections for the pool. A lost connection can occur if # a programmer forgets to checkin a connection at the end of a thread # or a thread dies unexpectedly. def reap @@ -587,7 +589,7 @@ module ActiveRecord end #-- - # From the discussion on Github: + # From the discussion on GitHub: # https://github.com/rails/rails/pull/14938#commitcomment-6601951 # This hook-in method allows for easier monkey-patching fixes needed by # JRuby users that use Fibers. @@ -628,10 +630,10 @@ module ActiveRecord end end rescue ExclusiveConnectionTimeoutError - # `raise_on_acquisition_timeout == false` means we are directed to ignore any + # <tt>raise_on_acquisition_timeout == false</tt> means we are directed to ignore any # timeouts and are expected to just give up: we've obtained as many connections # as possible, note that in a case like that we don't return any of the - # `newly_checked_out` connections. + # +newly_checked_out+ connections. if raise_on_acquisition_timeout release_newly_checked_out = true @@ -688,18 +690,18 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - ConnectionTimeoutError if a connection could not be acquired + # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired # #-- # Implementation detail: the connection returned by +acquire_connection+ # will already be "+connection.lease+ -ed" to the current thread. def acquire_connection(checkout_timeout) - # NOTE: we rely on `@available.poll` and `try_to_checkout_new_connection` to - # `conn.lease` the returned connection (and to do this in a `synchronized` + # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # +conn.lease+ the returned connection (and to do this in a +synchronized+ # section), this is not the cleanest implementation, as ideally we would - # `synchronize { conn.lease }` in this method, but by leaving it to `@available.poll` - # and `try_to_checkout_new_connection` we can piggyback on `synchronize` sections - # of the said methods and avoid an additional `synchronize` overhead. + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections + # of the said methods and avoid an additional +synchronize+ overhead. if conn = @available.poll || try_to_checkout_new_connection conn else @@ -764,7 +766,7 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c @@ -824,11 +826,11 @@ module ActiveRecord # These caches are keyed by klass.name, NOT klass. Keying them by klass # alone would lead to memory leaks in development mode as all previous # instances of the class would stay in memory. - @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new(:initial_capacity => 2) + @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| + h[k] = Concurrent::Map.new(:initial_capacity => 2) end - @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k| - h[k] = ThreadSafe::Cache.new + @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k| + h[k] = Concurrent::Map.new end end @@ -857,6 +859,8 @@ module ActiveRecord end # Clears the cache which maps classes. + # + # See ConnectionPool#clear_reloadable_connections! for details. def clear_reloadable_connections! connection_pool_list.each(&:clear_reloadable_connections!) end @@ -956,12 +960,11 @@ module ActiveRecord def call(env) testing = env['rack.test'] - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do + status, headers, body = @app.call(env) + proxy = ::Rack::BodyProxy.new(body) do ActiveRecord::Base.clear_active_connections! unless testing end - - response + [status, headers, proxy] rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 30b2fca2ca..6711049588 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -19,8 +19,8 @@ module ActiveRecord # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to - # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal \Rails + # #index_name_length. The gap between + # #index_name_length is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length 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 38dd9578fe..7a2a1a0e33 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -29,7 +29,17 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) arel, binds = binds_from_relation arel, binds - select(to_sql(arel, binds), name, binds) + sql = to_sql(arel, binds) + if arel.is_a?(String) + preparable = false + else + preparable = visitor.preparable + end + if prepared_statements && preparable + select_prepared(sql, name, binds) + else + select(sql, name, binds) + end end # Returns a record hash with the column names as keys and column values @@ -48,8 +58,8 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select_rows(to_sql(arel, binds), name, binds).map(&:first) end @@ -67,7 +77,7 @@ module ActiveRecord # Executes +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) end # Executes insert +sql+ statement in the context of this connection using @@ -96,7 +106,7 @@ module ActiveRecord exec_query(sql, name, binds) end - # Returns the last auto-generated ID from the affected table. + # Executes an INSERT query and returns the new record's ID # # +id_value+ will be returned unless the value is nil, in # which case the database will attempt to calculate the last inserted @@ -105,20 +115,24 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) + sql, binds, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end + alias create insert + alias insert_sql insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) exec_update(to_sql(arel, binds), name, binds) end + alias update_sql update # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) exec_delete(to_sql(arel, binds), name, binds) end + alias delete_sql delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ @@ -137,7 +151,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -190,15 +204,15 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/current/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # - # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # An ActiveRecord::TransactionIsolationError 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 + # The 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. @@ -286,11 +300,16 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = schema_cache.columns_hash(table_name) + fixture = fixture.stringify_keys + columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| @@ -330,18 +349,12 @@ module ActiveRecord # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select) #:nodoc: - key = update.key + def join_to_update(update, select, key) # :nodoc: subselect = subquery_for(key, select) update.where key.in(subselect) end - - def join_to_delete(delete, select, key) #:nodoc: - subselect = subquery_for(key, select) - - delete.where key.in(subselect) - end + alias join_to_delete join_to_update protected @@ -354,28 +367,15 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - - - # Returns the last auto-generated ID from the affected table. - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - execute(sql, name) - id_value - end - - # Executes the update statement and returns the number of rows affected. - def update_sql(sql, name = nil) - execute(sql, name) + exec_query(sql, name, binds, prepare: false) end - # Executes the delete statement and returns the number of rows affected. - def delete_sql(sql, name = nil) - update_sql(sql, name) + def select_prepared(sql, name = nil, binds = []) + exec_query(sql, name, binds, prepare: true) end def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds] + [sql, binds, pk, sequence_name] end def last_inserted_id(result) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 2c7409b2dc..7e3760d34b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -43,9 +43,9 @@ module ActiveRecord # If you are having to call this function, you are likely doing something # wrong. The column does not have sufficient type information if the user # provided a custom type on the class level either explicitly (via - # `attribute`) or implicitly (via `serialize`, - # `time_zone_aware_attributes`). In almost all cases, the sql type should - # only be used to change quoting behavior, when the primitive to + # Attributes::ClassMethods#attribute) or implicitly (via + # AttributeMethods::Serialization::ClassMethods#serialize, +time_zone_aware_attributes+). + # In almost all cases, the sql type should only be used to change quoting behavior, when the primitive to # represent the type doesn't sufficiently reflect the differences # (varchar vs binary) for example. The type used to get this primitive # should have been provided before reaching the connection adapter. @@ -58,7 +58,7 @@ module ActiveRecord end end - # See docs for +type_cast_from_column+ + # See docs for #type_cast_from_column def lookup_cast_type_from_column(column) # :nodoc: lookup_cast_type(column.sql_type) end @@ -93,7 +93,7 @@ module ActiveRecord # Override to return the quoted table name for assignment. Defaults to # table quoting. # - # This works for mysql and mysql2 where table.column can be used to + # This works for mysql2 where table.column can be used to # resolve ambiguity. # # We override this in the sqlite3 and postgresql adapters to use only @@ -102,9 +102,13 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end - def quote_default_expression(value, column) #:nodoc: - value = lookup_cast_type(column.sql_type).serialize(value) - quote(value) + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + else + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end end def quoted_true diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 18d943f452..0ba4d94e3c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,8 +14,10 @@ module ActiveRecord send m, o end - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options private @@ -38,17 +40,32 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} " - create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + if supports_indexes_in_create? + statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) + end + + if supports_foreign_keys? + statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) + end + + create_sql << "(#{statements.join(', ')}) " if statements.present? create_sql << "#{o.options}" create_sql << " AS #{@conn.to_sql(o.as)}" if o.as create_sql end - def visit_AddForeignKey(o) + def visit_PrimaryKeyDefinition(o) + "PRIMARY KEY (#{o.name.join(', ')})" + end + + def visit_ForeignKeyDefinition(o) sql = <<-SQL.strip_heredoc - ADD CONSTRAINT #{quote_column_name(o.name)} + CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) SQL @@ -57,6 +74,10 @@ module ActiveRecord sql end + def visit_AddForeignKey(o) + "ADD #{accept(o)}" + end + def visit_DropForeignKey(name) "DROP CONSTRAINT #{quote_column_name(name)}" end @@ -89,8 +110,9 @@ module ActiveRecord sql end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) + def foreign_key_in_create(from_table, to_table, options) + options = foreign_key_options(from_table, to_table, options) + accept ForeignKeyDefinition.new(from_table, to_table, options) end def action_sql(action, dependency) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 158b773e11..1cda23dc1d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -23,6 +23,9 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end + class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: + end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: def name options[:name] @@ -120,23 +123,29 @@ module ActiveRecord end def foreign_key_options - as_options(foreign_key) + as_options(foreign_key).merge(column: column_name) end def columns - result = [["#{name}_id", type, options]] + result = [[column_name, type, options]] if polymorphic result.unshift(["#{name}_type", :string, polymorphic_options]) end result end + def column_name + "#{name}_id" + end + def column_names columns.map(&:first) end def foreign_table_name - Base.pluralize_table_names ? name.to_s.pluralize : name + foreign_key_options.fetch(:to_table) do + Base.pluralize_table_names ? name.to_s.pluralize : name + end end end @@ -178,10 +187,10 @@ module ActiveRecord # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # - # Inside migration files, the +t+ object in +create_table+ + # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table] # is actually of this type: # - # class SomeMigration < ActiveRecord::Migration + # class SomeMigration < ActiveRecord::Migration[5.0] # def up # create_table :foo do |t| # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" @@ -193,27 +202,29 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes attr_reader :name, :temporary, :options, :as, :foreign_keys - def initialize(types, name, temporary, options, as = nil) + def initialize(name, temporary, options, as = nil) @columns_hash = {} @indexes = {} @foreign_keys = {} - @native = types + @primary_keys = nil @temporary = temporary @options = options @as = as @name = name end + def primary_keys(name = nil) # :nodoc: + @primary_keys = PrimaryKeyDefinition.new(name) if name + @primary_keys + end + + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end # Returns a ColumnDefinition for the column with name +name+. @@ -222,90 +233,23 @@ module ActiveRecord end # Instantiates a new column for the table. - # The +type+ parameter is normally one of the migrations native types, - # which is one of the following: - # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, - # <tt>:binary</tt>, <tt>:boolean</tt>. - # - # You may use a type not in this list as long as it is supported by your - # database (for example, "polygon" in MySQL), but this will not be database - # agnostic and should usually be avoided. - # - # Available options are (none of these exists by default): - # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and - # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns. - # * <tt>:default</tt> - - # The column's default value. Use nil for NULL. - # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. - # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. - # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # See {connection.add_column}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_column] + # for available options. + # + # Additional options are: # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # Note: The precision is the total number of significant digits - # and the scale is the number of digits that can be stored following - # the decimal point. For example, the number 123.45 has a precision of 5 - # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can - # range from -999.99 to 999.99. - # - # Please be aware of different RDBMS implementations behavior with - # <tt>:decimal</tt> columns: - # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= - # <tt>:precision</tt>, and makes no comments about the requirements of - # <tt>:precision</tt>. - # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. - # Default is (10,0). - # * PostgreSQL: <tt>:precision</tt> [1..infinity], - # <tt>:scale</tt> [0..infinity]. No default. - # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. - # Internal storage as strings. No default. - # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, - # but the maximum supported <tt>:precision</tt> is 16. No default. - # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. - # Default is (38,0). - # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. - # Default unknown. - # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # # This method returns <tt>self</tt>. # # == Examples - # # Assuming +td+ is an instance of TableDefinition - # td.column(:granted, :boolean) - # # granted BOOLEAN - # - # td.column(:picture, :binary, limit: 2.megabytes) - # # => picture BLOB(2097152) # - # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false) - # # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL - # - # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2) - # # => bill_gates_money DECIMAL(15,2) - # - # td.column(:sensor_reading, :decimal, precision: 30, scale: 20) - # # => sensor_reading DECIMAL(30,20) - # - # # While <tt>:scale</tt> defaults to zero on most databases, it - # # probably wouldn't hurt to include it. - # td.column(:huge_integer, :decimal, precision: 30) - # # => huge_integer DECIMAL(30) - # - # # Defines a column with a database-specific type. - # td.column(:foo, 'polygon') - # # => foo polygon + # # Assuming +td+ is an instance of TableDefinition + # td.column(:granted, :boolean, index: true) # # == Short-hand examples # - # Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types. + # Instead of calling #column directly, you can also work with the short-hand definitions for the default types. # They use the type as the method name instead of as a parameter and allow for multiple columns to be defined # in a single statement. # @@ -337,7 +281,8 @@ module ActiveRecord # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option - # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: + # will also create an index, similar to calling {add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index]. + # So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id @@ -369,6 +314,8 @@ module ActiveRecord self end + # remove the column +name+ from the table. + # remove_column(:account_id) def remove_column(name) @columns_hash.delete name.to_s end @@ -386,7 +333,7 @@ module ActiveRecord end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] # # t.timestamps null: false def timestamps(*args) @@ -403,7 +350,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) args.each do |col| ReferenceDefinition.new(col, **options).add_to(self) @@ -414,11 +361,8 @@ module ActiveRecord def new_column_definition(name, type, options) # :nodoc: type = aliased_types(type.to_s, type) column = create_column_definition name, type - limit = options.fetch(:limit) do - native[type][:limit] if native[type].is_a?(Hash) - end - column.limit = limit + column.limit = options[:limit] column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] @@ -436,10 +380,6 @@ module ActiveRecord ColumnDefinition.new name, type end - def native - @native - end - def aliased_types(name, fallback) 'timestamp' == name ? :datetime : fallback end @@ -475,7 +415,7 @@ module ActiveRecord end # Represents an SQL table in an abstract way for updating a table. - # Also see TableDefinition and SchemaStatements#create_table + # Also see TableDefinition and {connection.create_table}[rdoc-ref:SchemaStatements#create_table] # # Available transformations are: # @@ -532,7 +472,7 @@ module ActiveRecord # # t.string(:name) unless t.column_exists?(:name, :string) # - # See SchemaStatements#column_exists? + # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end @@ -544,7 +484,7 @@ module ActiveRecord # t.index([:branch_id, :party_id], unique: true) # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') # - # See SchemaStatements#add_index for details of the options you can use. + # See {connection.add_index}[rdoc-ref:SchemaStatements#add_index] for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end @@ -555,7 +495,7 @@ module ActiveRecord # t.index(:branch_id) # end # - # See SchemaStatements#index_exists? + # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -564,7 +504,7 @@ module ActiveRecord # # t.rename_index(:user_id, :account_id) # - # See SchemaStatements#rename_index + # See {connection.rename_index}[rdoc-ref:SchemaStatements#rename_index] def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end @@ -573,7 +513,7 @@ module ActiveRecord # # t.timestamps(null: false) # - # See SchemaStatements#add_timestamps + # See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] def timestamps(options = {}) @base.add_timestamps(name, options) end @@ -592,10 +532,11 @@ module ActiveRecord # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # t.change_default(:status, from: nil, to: "draft") # - # See SchemaStatements#change_column_default - def change_default(column_name, default) - @base.change_column_default(name, column_name, default) + # See {connection.change_column_default}[rdoc-ref:SchemaStatements#change_column_default] + def change_default(column_name, default_or_changes) + @base.change_column_default(name, column_name, default_or_changes) end # Removes the column(s) from the table definition. @@ -603,7 +544,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) # - # See SchemaStatements#remove_columns + # See {connection.remove_columns}[rdoc-ref:SchemaStatements#remove_columns] def remove(*column_names) @base.remove_columns(name, *column_names) end @@ -614,7 +555,7 @@ module ActiveRecord # t.remove_index(column: [:branch_id, :party_id]) # t.remove_index(name: :by_branch_party) # - # See SchemaStatements#remove_index + # See {connection.remove_index}[rdoc-ref:SchemaStatements#remove_index] def remove_index(options = {}) @base.remove_index(name, options) end @@ -623,7 +564,7 @@ module ActiveRecord # # t.remove_timestamps # - # See SchemaStatements#remove_timestamps + # See {connection.remove_timestamps}[rdoc-ref:SchemaStatements#remove_timestamps] def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -632,7 +573,7 @@ module ActiveRecord # # t.rename(:description, :name) # - # See SchemaStatements#rename_column + # See {connection.rename_column}[rdoc-ref:SchemaStatements#rename_column] def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end @@ -642,7 +583,7 @@ module ActiveRecord # t.references(:user) # t.belongs_to(:supplier, foreign_key: true) # - # See SchemaStatements#add_reference for details of the options you can use. + # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args) options = args.extract_options! args.each do |ref_name| @@ -656,7 +597,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # - # See SchemaStatements#remove_reference + # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] def remove_references(*args) options = args.extract_options! args.each do |ref_name| @@ -669,7 +610,7 @@ module ActiveRecord # # t.foreign_key(:authors) # - # See SchemaStatements#add_foreign_key + # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end @@ -678,15 +619,10 @@ module ActiveRecord # # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors) # - # See SchemaStatements#foreign_key_exists? + # See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?] def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end - - private - def native - @base.native_database_types - end end end 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 index b944a8631c..a95109fdae 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -18,9 +18,9 @@ module ActiveRecord spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) end - # This can be overridden on a Adapter level basis to support other + # This can be overridden on an Adapter level basis to support other # extended datatypes (Example: Adding an array option in the - # PostgreSQLAdapter) + # PostgreSQL::ColumnDumper) def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect @@ -61,8 +61,8 @@ module ActiveRecord end def schema_limit(column) - limit = column.limit || native_database_types[column.type][:limit] - limit.inspect if limit + limit = column.limit + limit.inspect if limit && limit != native_database_types[column.type][:limit] end def schema_precision(column) @@ -76,11 +76,17 @@ module ActiveRecord def schema_default(column) type = lookup_cast_type_from_column(column) default = type.deserialize(column.default) - unless default.nil? + if default.nil? + schema_expression(column) + else type.type_cast_for_schema(default) end end + def schema_expression(column) + "-> { #{column.default_function.inspect} }" if column.default_function + end + def schema_collation(column) column.collation.inspect if column.collation end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ed19819d63..70868ebd03 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -23,6 +23,25 @@ module ActiveRecord table_name[0...table_alias_length].tr('.', '_') end + # Returns the relation names useable to back Active Record models. + # For most adapters this means all #tables and #views. + def data_sources + tables | views + end + + # Checks to see if the data source +name+ exists on the database. + # + # data_source_exists?(:ebooks) + # + def data_source_exists?(name) + data_sources.include?(name.to_s) + end + + # Returns an array of table names defined in the database. + def tables(name = nil) + raise NotImplementedError, "#tables is not implemented" + end + # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) @@ -31,6 +50,19 @@ module ActiveRecord tables.include?(table_name.to_s) end + # Returns an array of view names defined in the database. + def views + raise NotImplementedError, "#views is not implemented" + end + + # Checks to see if the view +view_name+ exists on the database. + # + # view_exists?(:ebooks) + # + def view_exists?(view_name) + views.include?(view_name.to_s) + end + # Returns an array of indexes for the given table. # def indexes(table_name, name = nil) end @@ -50,11 +82,10 @@ module ActiveRecord # def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) checks = [] - checks << lambda { |i| i.name == index_name } checks << lambda { |i| i.columns == column_names } checks << lambda { |i| i.unique } if options[:unique] + checks << lambda { |i| i.name == options[:name].to_s } if options[:name] indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end @@ -88,10 +119,16 @@ module ActiveRecord (!options.key?(:null) || c.null == options[:null]) } end + # Returns just a table's primary key + def primary_key(table_name) + pks = primary_keys(table_name) + pks.first if pks.one? + end + # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # - # There are two ways to work with +create_table+. You can use the block + # There are two ways to work with #create_table. You can use the block # form or the regular form, like this: # # === Block form @@ -123,7 +160,7 @@ module ActiveRecord # The +options+ hash can include the following keys: # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set it to false. + # Join tables for {ActiveRecord::Base.has_and_belongs_to_many}[rdoc-ref:Associations::ClassMethods#has_and_belongs_to_many] should set it to false. # # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] @@ -131,7 +168,8 @@ module ActiveRecord # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Note that Active Record models will automatically detect their - # primary key. This can be avoided by using +self.primary_key=+ on the model + # primary key. This can be avoided by using + # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model # to define the key explicitly. # # [<tt>:options</tt>] @@ -153,7 +191,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # id int auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -165,7 +203,7 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # guid int auto_increment PRIMARY KEY, # name varchar(80) # ) # @@ -215,12 +253,16 @@ module ActiveRecord Base.get_primary_key table_name.to_s.singularize end - td.primary_key pk, options.fetch(:id, :primary_key), options + if pk.is_a?(Array) + td.primary_keys pk + else + td.primary_key pk, options.fetch(:id, :primary_key), options + end end yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end @@ -232,10 +274,6 @@ module ActiveRecord end end - td.foreign_keys.each_pair do |other_table_name, foreign_key_options| - add_foreign_key(table_name, other_table_name, foreign_key_options) - end - result end @@ -258,7 +296,7 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # Note that +create_join_table+ does not create any indices by default; you can use + # Note that #create_join_table does not create any indices by default; you can use # its block form to do so yourself: # # create_join_table :products, :categories do |t| @@ -293,11 +331,11 @@ module ActiveRecord end # Drops the join table specified by the given arguments. - # See +create_join_table+ for details. + # See #create_join_table for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. - # In that case, the block will be used by create_join_table. + # In that case, the block will be used by #create_join_table. def drop_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) drop_table(join_table_name) @@ -315,7 +353,7 @@ module ActiveRecord # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as # - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ... # # Defaults to false. # @@ -402,17 +440,86 @@ module ActiveRecord # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. - # In that case, +options+ and the block will be used by create_table. + # In that case, +options+ and the block will be used by #create_table. def drop_table(table_name, options = {}) execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - # - # Note: Not all options will be available, generally this command should - # ignore most of them. In favor of doing a low-level call to simply - # create a column. + # Add a new +type+ column named +column_name+ to +table_name+. + # + # The +type+ parameter is normally one of the migrations native types, + # which is one of the following: + # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. + # + # You may use a type not in this list as long as it is supported by your + # database (for example, "polygon" in MySQL), but this will not be database + # agnostic and should usually be avoided. + # + # Available options are (none of these exists by default): + # * <tt>:limit</tt> - + # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. + # * <tt>:default</tt> - + # The column's default value. Use nil for NULL. + # * <tt>:null</tt> - + # Allows or disallows +NULL+ values in the column. This option could + # have been named <tt>:null_allowed</tt>. + # * <tt>:precision</tt> - + # Specifies the precision for a <tt>:decimal</tt> column. + # * <tt>:scale</tt> - + # Specifies the scale for a <tt>:decimal</tt> column. + # + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following + # the decimal point. For example, the number 123.45 has a precision of 5 + # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can + # range from -999.99 to 999.99. + # + # Please be aware of different RDBMS implementations behavior with + # <tt>:decimal</tt> columns: + # * The SQL standard says the default scale should be 0, <tt>:scale</tt> <= + # <tt>:precision</tt>, and makes no comments about the requirements of + # <tt>:precision</tt>. + # * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30]. + # Default is (10,0). + # * PostgreSQL: <tt>:precision</tt> [1..infinity], + # <tt>:scale</tt> [0..infinity]. No default. + # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. + # Internal storage as strings. No default. + # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, + # but the maximum supported <tt>:precision</tt> is 16. No default. + # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. + # Default is (38,0). + # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. + # Default unknown. + # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # Default (38,0). + # + # == Examples + # + # add_column(:users, :picture, :binary, limit: 2.megabytes) + # # ALTER TABLE "users" ADD "picture" blob(2097152) + # + # add_column(:articles, :status, :string, limit: 20, default: 'draft', null: false) + # # ALTER TABLE "articles" ADD "status" varchar(20) DEFAULT 'draft' NOT NULL + # + # add_column(:answers, :bill_gates_money, :decimal, precision: 15, scale: 2) + # # ALTER TABLE "answers" ADD "bill_gates_money" decimal(15,2) + # + # add_column(:measurements, :sensor_reading, :decimal, precision: 30, scale: 20) + # # ALTER TABLE "measurements" ADD "sensor_reading" decimal(30,20) + # + # # While :scale defaults to zero on most databases, it + # # probably wouldn't hurt to include it. + # add_column(:measurements, :huge_integer, :decimal, precision: 30) + # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30) + # + # # Defines a column with a database-specific type. + # add_column(:shapes, :triangle, 'polygon') + # # ALTER TABLE "shapes" ADD "triangle" polygon def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -460,11 +567,16 @@ module ActiveRecord # # change_column_default(:users, :email, nil) # - def change_column_default(table_name, column_name, default) + # Passing a hash containing +:from+ and +:to+ will make this change + # reversible in migration: + # + # change_column_default(:posts, :state, from: nil, to: "draft") + # + def change_column_default(table_name, column_name, default_or_changes) raise NotImplementedError, "change_column_default is not implemented" end - # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag # indicates whether the value can be +NULL+. For example # # change_column_null(:users, :nickname, false) @@ -476,7 +588,7 @@ module ActiveRecord # allows them to be +NULL+ (drops the constraint). # # The method accepts an optional fourth argument to replace existing - # +NULL+s with some other value. Use that one when enabling the + # <tt>NULL</tt>s with some other value. Use that one when enabling the # constraint if needed, since otherwise those rows would not be valid. # # Please note the fourth argument does not set a column's default. @@ -530,6 +642,8 @@ module ActiveRecord # # CREATE INDEX by_name ON accounts(name(10)) # + # ====== Creating an index with specific key lengths for multiple keys + # # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # # generates: @@ -585,15 +699,15 @@ module ActiveRecord # Removes the given index from the table. # - # Removes the +index_accounts_on_column+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # - # remove_index :accounts, :column + # remove_index :accounts, :branch_id # - # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: :branch_id # - # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: [:branch_id, :party_id] # @@ -602,10 +716,7 @@ module ActiveRecord # remove_index :accounts, name: :by_branch_party # def remove_index(table_name, options = {}) - remove_index!(table_name, index_name_for_remove(table_name, options)) - end - - def remove_index!(table_name, index_name) #:nodoc: + index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end @@ -652,7 +763,7 @@ module ActiveRecord # Adds a reference. The reference column is an integer by default, # the <tt>:type</tt> option can be used to specify a different type. # Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # #add_reference and #add_belongs_to are acceptable. # # The +options+ hash can include the following keys: # [<tt>:type</tt>] @@ -660,9 +771,11 @@ module ActiveRecord # [<tt>:index</tt>] # Add an appropriate index. Defaults to false. # [<tt>:foreign_key</tt>] - # Add an appropriate foreign key. Defaults to false. + # Add an appropriate foreign key constraint. Defaults to false. # [<tt>:polymorphic</tt>] - # Wether an additional +_type+ column should be added. Defaults to false. + # Whether an additional +_type+ column should be added. Defaults to false. + # [<tt>:null</tt>] + # Whether the column allows nulls. Defaults to true. # # ====== Create a user_id integer column # @@ -680,13 +793,17 @@ module ActiveRecord # # add_reference(:products, :supplier, foreign_key: true) # + # ====== Create a supplier_id column and a foreign key to the firms table + # + # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) + # def add_reference(table_name, *args) ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. + # #remove_reference and #remove_belongs_to are acceptable. # # ====== Remove the reference # @@ -712,7 +829,7 @@ module ActiveRecord alias :remove_belongs_to :remove_reference # Returns an array of foreign keys for the given table. - # The foreign keys are represented as +ForeignKeyDefinition+ objects. + # The foreign keys are represented as ForeignKeyDefinition objects. def foreign_keys(table_name) raise NotImplementedError, "foreign_keys is not implemented" end @@ -756,28 +873,23 @@ module ActiveRecord # [<tt>:name</tt>] # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. # [<tt>:on_delete</tt>] - # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] - # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? - options[:column] ||= foreign_key_column_for(to_table) - - options = { - column: options[:column], - primary_key: options[:primary_key], - name: foreign_key_name(from_table, options), - on_delete: options[:on_delete], - on_update: options[:on_update] - } + options = foreign_key_options(from_table, to_table, options) at = create_alter_table from_table at.add_foreign_key to_table, options execute schema_creation.accept(at) end - # Removes the given foreign key from the table. + # Removes the given foreign key from the table. Any option parameters provided + # will be used to re-add the foreign key in case of a migration rollback. + # It is recommended that you provide any options used when creating the foreign + # key so that the migration can be reverted properly. # # Removes the foreign key on +accounts.branch_id+. # @@ -791,6 +903,7 @@ module ActiveRecord # # remove_foreign_key :accounts, name: :special_fk_name # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -828,7 +941,17 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - "#{table_name.to_s.singularize}_id" + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + "#{name.singularize}_id" + end + + def foreign_key_options(from_table, to_table, options) # :nodoc: + options = options.dup + options[:column] ||= foreign_key_column_for(to_table) + options[:name] ||= foreign_key_name(from_table, options) + options end def dump_schema_information #:nodoc: @@ -845,7 +968,11 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end - def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + def initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table + end + + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) @@ -905,11 +1032,12 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) #:nodoc: + # + def columns_for_distinct(columns, orders) # :nodoc: columns end @@ -964,7 +1092,7 @@ module ActiveRecord if index_name.length > max_index_length raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -972,6 +1100,10 @@ module ActiveRecord [index_name, index_type, index_columns, index_options, algorithm, using] end + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + protected def add_index_sort_order(option_strings, column_names, options = {}) if options.is_a?(Hash) && order = options[:order] @@ -998,26 +1130,36 @@ module ActiveRecord column_names.map {|name| quote_column_name(name) + option_strings[name]} end - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + # if the adapter doesn't support the indexes call the best we can do + # is return the default index name for the options provided + return index_name(table_name, options) unless respond_to?(:indexes) - unless index_name_exists?(table_name, index_name, true) - if options.is_a?(Hash) && options.has_key?(:name) - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + checks = [] - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end + if options.is_a?(Hash) + checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name) + column_names = Array(options[:column]).map(&:to_s) + else + column_names = Array(options).map(&:to_s) + end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + if column_names.any? + checks << lambda { |i| i.columns.join('_and_') == column_names.join('_and_') } end - index_name + raise ArgumentError "No name or columns specified" if checks.none? + + matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } } + + if matching_indexes.count > 1 + raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \ + "Specify an index name from #{matching_indexes.map(&:name).join(', ')}" + elsif matching_indexes.none? + raise ArgumentError, "No indexes found on #{table_name} with the options provided." + else + matching_indexes.first.name + end end def rename_table_indexes(table_name, new_name) @@ -1044,7 +1186,7 @@ module ActiveRecord private def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + TableDefinition.new(name, temporary, options, as) end def create_alter_table(name) @@ -1064,6 +1206,14 @@ module ActiveRecord raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end + + def extract_new_default_value(default_or_changes) + if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) + default_or_changes[:to] + else + default_or_changes + 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 index 295a7bed87..14d04a6388 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -33,6 +33,7 @@ module ActiveRecord class NullTransaction #:nodoc: def initialize; end + def state; end def closed?; true; end def open?; false; end def joinable?; false; end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6d3a21a3dc..d9b42d4283 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/type' require 'active_support/core_ext/benchmark' +require 'active_record/connection_adapters/determine_if_preparable_visitor' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/sql_type_metadata' require 'active_record/connection_adapters/abstract/schema_dumper' @@ -22,6 +23,7 @@ module ActiveRecord autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :ReferenceDefinition end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do @@ -51,15 +53,15 @@ module ActiveRecord # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an # abstract interface for database-specific functionality such as establishing - # a connection, escaping values, building the right SQL fragments for ':offset' - # and ':limit' options, etc. + # a connection, escaping values, building the right SQL fragments for +:offset+ + # and +:limit+ options, etc. # # All the concrete database adapters follow the interface laid down in this class. - # ActiveRecord::Base.connection returns an AbstractAdapter object, which + # {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which # you can use. # # Most of the methods in the adapter are useful during migrations. Most - # notably, the instance methods provided by SchemaStatement are very useful. + # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter ADAPTER_NAME = 'Abstract'.freeze include Quoting, DatabaseStatements, SchemaStatements @@ -94,19 +96,32 @@ module ActiveRecord attr_reader :prepared_statements - def initialize(connection, logger = nil, pool = nil) #:nodoc: + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @connection = connection @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger - @pool = pool + @config = config + @pool = nil @schema_cache = SchemaCache.new self @visitor = nil @prepared_statements = false end + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end + end + class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) casted_binds = conn.prepare_binds_for_database(bvs) @@ -201,6 +216,11 @@ module ActiveRecord false end + # Does this adapter support application-enforced advisory locking? + def supports_advisory_locks? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -254,6 +274,11 @@ module ActiveRecord false end + # Does this adapter support json data type? + def supports_json? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -262,6 +287,20 @@ module ActiveRecord def enable_extension(name) end + # This is meant to be implemented by the adapters that support advisory + # locks + # + # Return true if we got the lock, otherwise false + def get_advisory_lock(lock_id) # :nodoc: + end + + # This is meant to be implemented by the adapters that support advisory + # locks. + # + # Return true if we released the lock, otherwise false + def release_advisory_lock(lock_id) # :nodoc: + end + # A list of extensions, to be filled in by adapters that support them. def extensions [] @@ -272,12 +311,6 @@ module ActiveRecord {} end - # Returns a bind substitution value given a bind +column+ - # NOTE: The column param is currently being used by the sqlserver-adapter - def substitute_at(column, _unused = 0) - Arel::Nodes::BindParam.new - end - # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -331,14 +364,14 @@ module ActiveRecord end # Checks whether the connection to the database is still active (i.e. not stale). - # This is done under the hood by calling <tt>active?</tt>. If the connection + # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For - # example, this method returns a Mysql object in case of MysqlAdapter, + # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, # and a PGconn object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as @@ -353,19 +386,17 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node, table_attribute) - node - end - def case_sensitive_comparison(table, attribute, column, value) - table_attr = table[attribute] - value = case_sensitive_modifier(value, table_attr) unless value.nil? - table_attr.eq(value) + if value.nil? + table[attribute].eq(value) + else + table[attribute].eq(Arel::Nodes::BindParam.new) + end end def case_insensitive_comparison(table, attribute, column, value) if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) else case_sensitive_comparison(table, attribute, column, value) end @@ -501,7 +532,7 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message, exception) + ActiveRecord::StatementInvalid.new(message) end def without_prepared_statement?(binds) 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 c3206c6045..3e84786be0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,235 +1,33 @@ +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/mysql/column' +require 'active_record/connection_adapters/mysql/schema_creation' +require 'active_record/connection_adapters/mysql/schema_definitions' +require 'active_record/connection_adapters/mysql/schema_dumper' +require 'active_record/connection_adapters/mysql/type_metadata' + require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include MySQL::ColumnDumper include Savepoints - module ColumnMethods - def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint - super - end - end - - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type - when :primary_key - column.type = :integer - column.auto_increment = true - end - column.charset = options[:charset] - column - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new(name, type) - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_DropForeignKey(name) - "DROP FOREIGN KEY #{name}" - end - - def visit_TableDefinition(o) - name = o.name - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " - - statements = o.columns.map { |c| accept c } - statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) }) - - create_sql << "(#{statements.join(', ')}) " if statements.present? - create_sql << "#{o.options}" - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as - create_sql - end - - def visit_AddColumnDefinition(o) - add_column_position!(super, column_options(o.column)) - end - - def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" - add_column_position!(change_column_sql, column_options(o.column)) - end - - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - - def add_column_options!(sql, options) - if options[:charset] - sql << " CHARACTER SET #{options[:charset]}" - end - if options[:collation] - sql << " COLLATE #{options[:collation]}" - end - super - end - - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - sql - end - - def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " - end - end - def update_table_definition(table_name, base) # :nodoc: - Table.new(table_name, base) + MySQL::Table.new(table_name, base) end def schema_creation - SchemaCreation.new self - end - - def column_spec_for_primary_key(column) - spec = {} - if column.auto_increment? - spec[:id] = ':bigint' if column.bigint? - return if spec.empty? - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec - end - - private - - def schema_limit(column) - super unless column.type == :boolean - end - - def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 - end - - def schema_collation(column) - if column.collation && table_name = column.instance_variable_get(:@table_name) - @collation_cache ||= {} - @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] - column.collation.inspect if column.collation != @collation_cache[table_name] - end - end - - public - - class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true - - def initialize(*) - super - assert_valid_default(default) - extract_default - end - - def extract_default - if blob_or_text_column? - @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(default) - @default = nil - end - end - - def has_default? - return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns - super - end - - def blob_or_text_column? - sql_type =~ /blob/i || type == :text - end - - def case_sensitive? - collation && !collation.match(/_ci$/) - end - - def auto_increment? - extra == 'auto_increment' - end - - private - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - - def assert_valid_default(default) - if blob_or_text_column? && default.present? - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - end - end - - class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :extra, :strict - - def initialize(type_metadata, extra: "", strict: false) - super(type_metadata) - @type_metadata = type_metadata - @extra = extra - @strict = strict - end - - def ==(other) - other.is_a?(MysqlTypeMetadata) && - attributes_for_hash == other.attributes_for_hash - end - alias eql? == - - def hash - attributes_for_hash.hash - end - - protected - - def attributes_for_hash - [self.class, @type_metadata, extra, strict] - end + MySQL::SchemaCreation.new(self) end ## # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line + # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line # to your application.rb file: # - # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false class_attribute :emulate_booleans self.emulate_booleans = true @@ -242,17 +40,18 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } + primary_key: "int auto_increment PRIMARY KEY", + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "int", limit: 4 }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "tinyint", limit: 1 }, + json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] @@ -260,14 +59,14 @@ module ActiveRecord # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config + super(connection, logger, config) @quoted_column_names, @quoted_table_names = {}, {} @visitor = Arel::Visitors::MySQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end @@ -283,6 +82,10 @@ module ActiveRecord end end + def version + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + end + # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -307,7 +110,11 @@ module ActiveRecord # # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version[0] >= 5 + version >= '5.0.0' + end + + def supports_explain? + true end def supports_indexes_in_create? @@ -319,11 +126,25 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + version >= '5.0.0' end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + version >= '5.6.4' + end + + # 5.0.0 definitely supports it, possibly supported by earlier versions but + # not sure + def supports_advisory_locks? + version >= '5.0.0' + end + + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' + end + + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' end def native_database_types @@ -343,7 +164,7 @@ module ActiveRecord end def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - Column.new(field, default, sql_type_metadata, null, default_function, collation) + MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation) end # Must return the MySQL error number from the exception, if the exception has an @@ -386,6 +207,14 @@ module ActiveRecord 0 end + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, '') + end + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -403,6 +232,80 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + ExplainPrettyPrinter.new.pp(result, elapsed) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + def clear_cache! super reload_type_map @@ -413,18 +316,13 @@ module ActiveRecord log(sql, name) { @connection.query(sql) } end - # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in an abstract way without concerning ourselves about whether it needs to be - # explicitly freed or not. - def execute_and_free(sql, name = nil) #:nodoc: + # Mysql2Adapter doesn't have to free a result after using it, but we use this method + # to write stuff in an abstract way without concerning ourselves about whether it + # needs to be explicitly freed or not. + def execute_and_free(sql, name = nil) # :nodoc: yield execute(sql, name) end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def begin_db_transaction execute "BEGIN" end @@ -445,7 +343,7 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. - def join_to_update(update, select) #:nodoc: + def join_to_update(update, select, key) # :nodoc: if select.limit || select.offset || select.orders.any? super else @@ -506,33 +404,70 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil, like = nil) #:nodoc: - sql = "SHOW TABLES " - sql << "IN #{quote_table_name(database)} " if database - sql << "LIKE #{quote(like)}" if like + def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG - execute_and_free(sql, 'SCHEMA') do |result| - result.collect(&:first) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG end + + data_sources + end + + def data_sources + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(@config[:database])}" + + select_values(sql, 'SCHEMA') end def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - def table_exists?(name) - return false unless name.present? - return true if tables(nil, nil, name).any? + def table_exists?(table_name) + # Update lib/active_record/internal_metadata.rb when this gets removed + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG - name = name.to_s - schema, table = name.split('.', 2) + data_source_exists?(table_name) + end - unless table # A table was provided without a schema - table = schema - schema = nil - end + def data_source_exists?(table_name) + return false unless table_name.present? - tables(nil, schema, table).any? + schema, name = table_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A table was provided without a schema + + sql = "SELECT table_name FROM information_schema.tables " + sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? + end + + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + schema, name = view_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A view was provided without a schema + + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" + + select_values(sql, 'SCHEMA').any? end # Returns an array of indexes for the given table. @@ -560,14 +495,16 @@ module ActiveRecord end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name)#:nodoc: + def columns(table_name) # :nodoc: sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - field_name = set_field_encoding(field[:Field]) - sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation]) + else + new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) + end end end end @@ -616,6 +553,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) + create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -629,7 +567,8 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -670,7 +609,7 @@ module ActiveRecord AND fk.table_name = '#{table_name}' SQL - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info = create_table_info(table_name) fk_info.map do |row| options = { @@ -687,7 +626,7 @@ module ActiveRecord end def table_options(table_name) - create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info = create_table_info(table_name) # strip create_definitions and partition_options raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip @@ -697,54 +636,57 @@ module ActiveRecord end # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s - when 'binary' - binary_to_sql(limit) + def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + sql = case type.to_s when 'integer' integer_to_sql(limit) when 'text' text_to_sql(limit) + when 'blob' + binary_to_sql(limit) + when 'binary' + if (0..0xfff) === limit + "varbinary(#{limit})" + else + binary_to_sql(limit) + end else - super + super(type, limit, precision, scale) end + + sql << ' unsigned' if unsigned && type != :primary_key + sql end # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') + variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') variables.first['Value'] unless variables.empty? + rescue ActiveRecord::StatementInvalid + nil end - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| - create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ - keys = $1.split(",").map { |key| key.delete('`"') } - keys.length == 1 ? [keys.first, nil] : nil - else - nil - end - end - end + def primary_keys(table_name) # :nodoc: + raise ArgumentError unless table_name.present? - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end + schema, name = table_name.to_s.split('.', 2) + schema, name = @config[:database], schema unless name # A table was provided without a schema - def case_sensitive_modifier(node, table_attribute) - node = Arel::Nodes.build_quoted node, table_attribute - Arel::Nodes::Bin.new(node) + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT column_name + FROM information_schema.key_column_usage + WHERE constraint_name = 'PRIMARY' + AND table_schema = #{quote(schema)} + AND table_name = #{quote(name)} + ORDER BY ordinal_position + SQL end def case_sensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - table[attribute].eq(value) - else + if value.nil? || column.case_sensitive? super + else + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) end end @@ -752,10 +694,25 @@ module ActiveRecord if column.case_sensitive? super else - table[attribute].eq(value) + table[attribute].eq(Arel::Nodes::BindParam.new) end end + # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use + # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for + # distinct queries, and requires that the ORDER BY include the distinct column. + # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html + def columns_for_distinct(columns, orders) # :nodoc: + order_columns = orders.reject(&:blank?).map { |s| + # Convert Arel node to string + s = s.to_sql unless s.is_a?(String) + # Remove any ASC/DESC modifiers + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super, *order_columns].join(', ') + end + def strict_mode? self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end @@ -781,6 +738,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type %r(^json)i, MysqlJson.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -789,7 +747,6 @@ module ActiveRecord register_integer_type m, %r(^tinyint)i, limit: 1 m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans - m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' @@ -798,11 +755,17 @@ module ActiveRecord .split(',').map{|enum| enum.strip.length - 2}.max MysqlString.new(limit: limit) end + + m.register_type(%r(^set)i) do |sql_type| + limit = sql_type[/^set\((.+)\)/i, 1] + .split(',').map{|set| set.strip.length - 1}.sum - 1 + MysqlString.new(limit: limit) + end end def register_integer_type(mapping, key, options) # :nodoc: mapping.register_type(key) do |sql_type| - if /unsigned/i =~ sql_type + if /\bunsigned\z/ === sql_type Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -819,7 +782,7 @@ module ActiveRecord end def fetch_type_metadata(sql_type, extra = "") - MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) + MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end def add_index_length(option_strings, column_names, options = {}) @@ -850,9 +813,9 @@ module ActiveRecord def translate_exception(exception, message) case error_number(exception) when 1062 - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when 1452 - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end @@ -929,15 +892,13 @@ module ActiveRecord subsubselect = select.clone subsubselect.projections = [key] - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct + # Materialize subquery by adding distinct # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') - end + subsubselect.distinct unless select.limit || select.offset || select.orders.any? - def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') end def mariadb? @@ -945,7 +906,7 @@ module ActiveRecord end def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + mariadb? ? false : version >= '5.7.6' end def configure_connection @@ -959,15 +920,17 @@ module ActiveRecord wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) + defaults = [':default', :default].to_set + # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - unless variables.has_key?('sql_mode') + unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict]) variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" @@ -977,7 +940,7 @@ module ActiveRecord # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| - if v == ':default' || v == :default + if defaults.include?(v) "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? "@@SESSION.#{k} = #{quote(v)}" @@ -998,17 +961,16 @@ module ActiveRecord end end - def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - TableDefinition.new(native_database_types, name, temporary, options, as) + def create_table_info_cache # :nodoc: + @create_table_info_cache ||= {} end - def binary_to_sql(limit) # :nodoc: - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") - end + def create_table_info(table_name) # :nodoc: + create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + end + + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: + MySQL::TableDefinition.new(name, temporary, options, as) end def integer_to_sql(limit) # :nodoc: @@ -1016,8 +978,9 @@ module ActiveRecord when 1; 'tinyint' when 2; 'smallint' when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when nil, 4; 'int' when 5..8; 'bigint' + when 11; 'int(11)' # backward compatibility with Rails 2.0 else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end @@ -1028,7 +991,25 @@ module ActiveRecord when nil, 0x100..0xffff; 'text' when 0x10000..0xffffff; 'mediumtext' when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") + else raise(ActiveRecordError, "No text type has byte length #{limit}") + end + end + + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinyblob' + when nil, 0x100..0xffff; 'blob' + when 0x10000..0xffffff; 'mediumblob' + when 0x1000000..0xffffffff; 'longblob' + else raise(ActiveRecordError, "No binary type has byte length #{limit}") + end + end + + class MysqlJson < Type::Internal::AbstractJson # :nodoc: + def changed_in_place?(raw_old_value, new_value) + # Normalization is required because MySQL JSON data format includes + # the space between the elements. + super(serialize(deserialize(raw_old_value)), new_value) end end @@ -1052,8 +1033,9 @@ module ActiveRecord end end - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) + ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 4b95b0681d..81de7c03fb 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -5,25 +5,18 @@ module ActiveRecord module ConnectionAdapters # An abstract definition of a column in a table. class Column - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true # Instantiates a new column in the table. # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. + # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) - @name = name + @name = name.freeze @sql_type_metadata = sql_type_metadata @null = null @default = default diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 08d46fca96..f633892dee 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -175,7 +175,7 @@ module ActiveRecord rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb new file mode 100644 index 0000000000..0fdc185c45 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + module DetermineIfPreparableVisitor + attr_reader :preparable + + def accept(*) + @preparable = true + super + end + + def visit_Arel_Nodes_In(*) + @preparable = false + super + end + + def visit_Arel_Nodes_SqlLiteral(*) + @preparable = false + super + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb new file mode 100644 index 0000000000..9c45fdd44a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class Column < ConnectionAdapters::Column # :nodoc: + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true + + def initialize(*) + super + assert_valid_default + extract_default + end + + def has_default? + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns + super + end + + def blob_or_text_column? + /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text + end + + def unsigned? + /\bunsigned\z/ === sql_type + end + + def case_sensitive? + collation && collation !~ /_ci\z/ + end + + def auto_increment? + extra == 'auto_increment' + end + + private + + def extract_default + if blob_or_text_column? + @default = null || strict ? nil : '' + end + end + + def assert_valid_default + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb new file mode 100644 index 0000000000..1e2c859af9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -0,0 +1,57 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + + def visit_ColumnDefinition(o) + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) + super + end + + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + + def visit_ChangeColumnDefinition(o) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) + end + + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + sql + end + + def index_in_create(table_name, column_name, options) + index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb new file mode 100644 index 0000000000..157e75dbf7 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -0,0 +1,93 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint && !options.key?(:default) + super + end + + def blob(*args, **options) + args.each { |name| column(name, :blob, options) } + end + + def tinyblob(*args, **options) + args.each { |name| column(name, :tinyblob, options) } + end + + def mediumblob(*args, **options) + args.each { |name| column(name, :mediumblob, options) } + end + + def longblob(*args, **options) + args.each { |name| column(name, :longblob, options) } + end + + def tinytext(*args, **options) + args.each { |name| column(name, :tinytext, options) } + end + + def mediumtext(*args, **options) + args.each { |name| column(name, :mediumtext, options) } + end + + def longtext(*args, **options) + args.each { |name| column(name, :longtext, options) } + end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end + + def unsigned_integer(*args, **options) + args.each { |name| column(name, :unsigned_integer, options) } + end + + def unsigned_bigint(*args, **options) + args.each { |name| column(name, :unsigned_bigint, options) } + end + + def unsigned_float(*args, **options) + args.each { |name| column(name, :unsigned_float, options) } + end + + def unsigned_decimal(*args, **options) + args.each { |name| column(name, :unsigned_decimal, options) } + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :unsigned + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + when /\Aunsigned_(?<type>.+)\z/ + column.type = $~[:type].to_sym + column.unsigned = true + end + column.unsigned ||= options[:unsigned] + column.charset = options[:charset] + column + end + + private + + def create_column_definition(name, type) + MySQL::ColumnDefinition.new(name, type) + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb new file mode 100644 index 0000000000..9dee3172f4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -0,0 +1,59 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = {} + if column.bigint? + spec[:id] = ':bigint' + spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? + spec[:unsigned] = 'true' if column.unsigned? + elsif column.auto_increment? + spec[:unsigned] = 'true' if column.unsigned? + return if spec.empty? + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + def prepare_column_options(column) + spec = super + spec[:unsigned] = 'true' if column.unsigned? + spec + end + + def migration_keys + super + [:unsigned] + end + + private + + def schema_type(column) + if column.sql_type == 'tinyblob' + 'blob' + else + super + end + end + + def schema_limit(column) + super unless column.type == :boolean + end + + def schema_precision(column) + super unless /time/ === column.sql_type && column.precision == 0 + end + + def schema_collation(column) + if column.collation && table_name = column.instance_variable_get(:@table_name) + @table_collation_cache ||= {} + @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + column.collation.inspect if column.collation != @table_collation_cache[table_name] + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb new file mode 100644 index 0000000000..e1e3f7b472 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :extra, :strict + + def initialize(type_metadata, extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MySQL::TypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, extra, strict] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..c3c5b660fd 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.18' +gem 'mysql2', '>= 0.3.18', '< 0.5' require 'mysql2' module ActiveRecord @@ -10,17 +10,21 @@ module ActiveRecord config = config.symbolize_keys config[:username] = 'root' if config[:username].nil? + config[:flags] ||= 0 if Mysql2::Client.const_defined? :FOUND_ROWS - config[:flags] = Mysql2::Client::FOUND_ROWS + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS + end end client = Mysql2::Client.new(config) - options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] - ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -37,8 +41,8 @@ module ActiveRecord configure_connection end - def supports_explain? - true + def supports_json? + version >= '5.7.8' end # HELPER METHODS =========================================== @@ -95,112 +99,22 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds.dup)}" - start = Time.now - result = exec_query(sql, 'EXPLAIN', binds) - elapsed = Time.now - start - - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(arel, name = nil, binds = []) + arel, binds = binds_from_relation(arel, binds) + execute(to_sql(arel, binds), name).each(as: :hash) do |row| + @connection.next_result while @connection.more_results? + return row end end - # FIXME: re-enable the following once a "better" query_cache solution is in core - # - # The overrides below perform much better than the originals in AbstractAdapter - # because we're able to take advantage of mysql2's lazy-loading capabilities - # - # # Returns a record hash with the column names as keys and column values - # # as values. - # def select_one(sql, name = nil) - # result = execute(sql, name) - # result.each(as: :hash) do |r| - # return r - # end - # end - # - # # Returns a single value from a record - # def select_value(sql, name = nil) - # result = execute(sql, name) - # if first = result.first - # first.first - # end - # end - # - # # Returns an array of the values of the first column in a select: - # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - # def select_values(sql, name = nil) - # execute(sql, name).map { |row| row.first } - # end - # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) - execute(sql, name).to_a + result = execute(sql, name) + @connection.next_result while @connection.more_results? + result.to_a end # Executes the SQL statement in the context of this connection. @@ -214,19 +128,14 @@ module ActiveRecord super end - def exec_query(sql, name = 'SQL', binds = []) + def exec_query(sql, name = 'SQL', binds = [], prepare: false) result = execute(sql, name) + @connection.next_result while @connection.more_results? ActiveRecord::Result.new(result.fields, result.to_a) end alias exec_without_stmt exec_query - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super - id_value || @connection.last_id - end - alias :create :insert_sql - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end @@ -254,11 +163,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] - end - - def set_field_encoding field_name - field_name + @full_version ||= @connection.server_info[:version] end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100644 index 2ae462d773..0000000000 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,468 +0,0 @@ -require 'active_record/connection_adapters/abstract_mysql_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/hash/keys' - -gem 'mysql', '~> 2.9' -require 'mysql' - -class Mysql # :nodoc: all - class Time - # Used for casting DateTime fields to a MySQL friendly Time. - # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e - def to_date - Date.new(year, month, day) - end - end - class Stmt; include Enumerable end - class Result; include Enumerable end -end - -module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) - config = config.symbolize_keys - host = config[:host] - port = config[:port] - socket = config[:socket] - username = config[:username] ? config[:username].to_s : 'root' - password = config[:password].to_s - database = config[:database] - - mysql = Mysql.init - mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] - - default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 - default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) - options = [host, username, password, database, port, socket, default_flags] - ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) - rescue Mysql::Error => error - if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) - else - raise - end - end - end - - module ConnectionAdapters - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - class StatementPool < ConnectionAdapters::StatementPool - private - - def dealloc(stmt) - stmt[:stmt].close - end - end - - def initialize(connection, logger, connection_options, config) - super - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @client_encoding = nil - connect - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # HELPER METHODS =========================================== - - def each_hash(result) # :nodoc: - if block_given? - result.each_hash do |row| - row.symbolize_keys! - yield row - end - else - to_enum(:each_hash, result) - end - end - - def error_number(exception) # :nodoc: - exception.errno if exception.respond_to?(:errno) - end - - # QUOTING ================================================== - - def quote_string(string) #:nodoc: - @connection.quote(string) - end - - #-- - # CONNECTION MANAGEMENT ==================================== - #++ - - def active? - if @connection.respond_to?(:stat) - @connection.stat - else - @connection.query 'select 1' - end - - # mysql-ruby doesn't raise an exception when stat fails. - if @connection.respond_to?(:errno) - @connection.errno.zero? - else - true - end - rescue Mysql::Error - false - end - - def reconnect! - super - disconnect! - connect - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - @connection.close rescue nil - end - - def reset! - if @connection.respond_to?(:change_user) - # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to - # reset the connection is to change the user to the same user. - @connection.change_user(@config[:username], @config[:password], @config[:database]) - configure_connection - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def select_rows(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).rows - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Clears the prepared statements cache. - def clear_cache! - super - @statements.clear - end - - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - - def exec_query(sql, name = 'SQL', binds = []) - if without_prepared_statement?(binds) - result_set, affected_rows = exec_without_stmt(sql, name) - else - result_set, affected_rows = exec_stmt(sql, name, binds) - end - - yield affected_rows if block_given? - - result_set - end - - def last_inserted_id(result) - @connection.insert_id - end - - module Fields # :nodoc: - class DateTime < Type::DateTime # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class Time < Type::Time # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - 2000, - 01, - 01, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class << self - TYPES = Type::HashLookupTypeMap.new # :nodoc: - - delegate :register_type, :alias_type, to: :TYPES - - def find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES.lookup(Mysql::Field::TYPE_LONG) - else - TYPES.lookup(field.type) - end - end - end - - register_type Mysql::Field::TYPE_TINY, Type::Boolean.new - register_type Mysql::Field::TYPE_LONG, Type::Integer.new - alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG - alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - - register_type Mysql::Field::TYPE_DATE, Type::Date.new - register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new - register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Type::Float.new - end - - def initialize_type_map(m) # :nodoc: - super - register_class_with_precision m, %r(datetime)i, Fields::DateTime - register_class_with_precision m, %r(time)i, Fields::Time - end - - def exec_without_stmt(sql, name = 'SQL') # :nodoc: - # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( - log(sql, name) do - result = @connection.query(sql) - affected_rows = @connection.affected_rows - - if result - types = {} - fields = [] - result.fetch_fields.each { |field| - field_name = field.name - fields << field_name - - if field.decimals > 0 - types[field_name] = Type::Decimal.new - else - types[field_name] = Fields.find_type field - end - } - - result_set = ActiveRecord::Result.new(fields, result.to_a, types) - result.free - else - result_set = ActiveRecord::Result.new([], []) - end - - [result_set, affected_rows] - end - end - - def execute_and_free(sql, name = nil) # :nodoc: - result = execute(sql, name) - ret = yield result - result.free - ret - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super sql, name - id_value || @connection.insert_id - end - alias :create :insert_sql - - def exec_delete(sql, name, binds) # :nodoc: - affected_rows = 0 - - exec_query(sql, name, binds) do |n| - affected_rows = n - end - - affected_rows - end - alias :exec_update :exec_delete - - def begin_db_transaction #:nodoc: - exec_query "BEGIN" - end - - private - - def exec_stmt(sql, name, binds) - cache = {} - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - begin - stmt.execute(*type_casted_binds) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older MySQL versions, we - # need to close the statement and delete the statement from the - # cache. - if binds.empty? - stmt.close - else - @statements.delete sql - end - raise e - end - - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) - metadata.free - end - - result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols - affected_rows = stmt.affected_rows - - stmt.free_result - stmt.close if binds.empty? - - [result_set, affected_rows] - end - end - - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - - @connection.real_connect(*@connection_options) - - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - - configure_connection - end - - # Many Rails applications monkey-patch a replacement of the configure_connection method - # and don't call 'super', so leave this here even though it looks superfluous. - def configure_connection - super - end - - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = super - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Returns the full version of the connected MySQL server. - def full_version - @full_version ||= @connection.server_info - end - - def set_field_encoding field_name - field_name.force_encoding(client_encoding) - if internal_enc = Encoding.default_internal - field_name = field_name.encode!(internal_enc) - end - field_name - 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 index 11d3f5301a..6c15facf3b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -8,7 +8,7 @@ module ActiveRecord end class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the # PostgreSQL shell: # # QUERY PLAN @@ -52,8 +52,8 @@ module ActiveRecord end end - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) execute_and_clear(sql, name, binds) do |result| if result.nfields > 0 @@ -72,28 +72,6 @@ module ActiveRecord end 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 - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -156,8 +134,8 @@ module ActiveRecord end end - def exec_query(sql, name = 'SQL', binds = []) - execute_and_clear(sql, name, binds) do |result| + def exec_query(sql, name = 'SQL', binds = [], prepare: false) + execute_and_clear(sql, name, binds, prepare: prepare) do |result| types = {} fields = result.fields fields.each_with_index do |fname, i| @@ -174,7 +152,7 @@ module ActiveRecord end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) + def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: unless pk # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) @@ -185,7 +163,7 @@ module ActiveRecord sql = "#{sql} RETURNING #{quote_column_name(pk)}" end - [sql, binds] + super end def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) @@ -202,11 +180,6 @@ module ActiveRecord 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" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 92349e2f9b..68752cdd80 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/postgresql/oid/json' require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/uuid' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 3de794f797..87593ef704 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -45,6 +45,15 @@ module ActiveRecord delimiter == other.delimiter end + def type_cast_for_schema(value) + return super unless value.is_a?(::Array) + "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" + end + + def map(value, &block) + value.map(&block) + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 2c04c46131..424769f765 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -4,18 +4,14 @@ module ActiveRecord module OID # :nodoc: class DateTime < Type::DateTime # :nodoc: def cast_value(value) - if value.is_a?(::String) - case value - when 'infinity' then ::Float::INFINITY - when '-infinity' then -::Float::INFINITY - when / BC$/ - astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) - super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) - else - super - end + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) else - value + super end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value # :nodoc: - include Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end + class Json < Type::Internal::AbstractJson end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index afc9383f91..87391b5dc7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -9,7 +9,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, new_value) # Postgres does not preserve insignificant whitespaces when - # roundtripping jsonb columns. This causes some false positives for + # round-tripping jsonb columns. This causes some false positives for # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are # consistent with our encoder's output. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb new file mode 100644 index 0000000000..7427a25ad5 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb @@ -0,0 +1,50 @@ +module ActiveRecord + Point = Struct.new(:x, :y) + + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Rails51Point < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :point + end + + def cast(value) + case value + when ::String + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + x, y = value.split(",") + build_point(x, y) + when ::Array + build_point(*value) + else + value + end + end + + def serialize(value) + if value.is_a?(ActiveRecord::Point) + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index fc201f8fb9..a8d2310035 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -6,6 +6,7 @@ module ActiveRecord module OID # :nodoc: class Range < Type::Value # :nodoc: attr_reader :subtype, :type + delegate :user_input_in_time_zone, to: :subtype def initialize(subtype, type = :range) @subtype = subtype @@ -18,7 +19,7 @@ module ActiveRecord def cast_value(value) return if value == 'empty' - return value if value.is_a?(::Range) + return value unless value.is_a?(::String) extracted = extract_bounds(value) from = type_cast_single extracted[:from] @@ -46,6 +47,12 @@ module ActiveRecord other.type == type end + def map(value) # :nodoc: + new_begin = yield(value.begin) + new_end = yield(value.end) + ::Range.new(new_begin, new_end, value.exclude_end?) + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 191c828e60..6155e53632 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -36,7 +36,7 @@ module ActiveRecord WHERE t.typname IN (%s) OR t.typtype IN (%s) - OR t.typinput::varchar = 'array_in' + OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure OR t.typelem != 0 SQL end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index f175730551..c1c77a967e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -31,6 +31,11 @@ module ActiveRecord Utils.extract_schema_qualified_name(name.to_s).quoted end + # Quotes schema names for use in SQL queries. + def quote_schema_name(name) + PGconn.quote_ident(name) + end + def quote_table_name_for_assignment(table, attr) quote_column_name(attr) end @@ -50,10 +55,11 @@ module ActiveRecord end end - # Does not quote function default values for UUID columns - def quote_default_expression(value, column) #:nodoc: - if column.type == :uuid && value =~ /\(\)/ - value + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + elsif column.type == :uuid && value =~ /\(\)/ + value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb new file mode 100644 index 0000000000..cc7721ddd8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnDumper + def column_spec_for_primary_key(column) + spec = {} + if column.serial? + return unless column.bigint? + spec[:id] = ':bigserial' + elsif column.type == :uuid + spec[:id] = ':uuid' + spec[:default] = schema_default(column) || 'nil' + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + + # Adds +:array+ option to the default set + def prepare_column_options(column) + spec = super + spec[:array] = 'true' if column.array? + spec + end + + # Adds +:array+ as a valid migration key + def migration_keys + super + [:array] + end + + private + + def schema_type(column) + return super unless column.serial? + + if column.bigint? + 'bigserial' + else + 'serial' + end + end + + def schema_expression(column) + super unless column.serial? + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 595c635fc0..67e727d8ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -68,15 +68,41 @@ module ActiveRecord 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. + # Returns the list of all tables in the schema search path. def tables(name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end + def data_sources # :nodoc + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + # 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) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(name) + end + + def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier @@ -90,6 +116,30 @@ module ActiveRecord SQL end + def views # :nodoc: + select_values(<<-SQL, 'SCHEMA') + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + + def view_exists?(view_name) # :nodoc: + name = Utils.extract_schema_qualified_name(view_name.to_s) + return false unless name.identifier + + select_values(<<-SQL, 'SCHEMA').any? + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + SQL + end + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -101,29 +151,36 @@ module ActiveRecord # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) + table = Utils.extract_schema_qualified_name(table_name.to_s) + index = Utils.extract_schema_qualified_name(index_name.to_s) + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index_name}' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + AND i.relname = '#{index.identifier}' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} 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)) ) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + 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 + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} ORDER BY i.relname SQL @@ -210,12 +267,12 @@ module ActiveRecord # Creates a schema for the given schema name. def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" + execute "CREATE SCHEMA #{quote_schema_name(schema_name)}" end # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" + def drop_schema(schema_name, options = {}) + execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE" end # Sets the schema search path to a string of comma-separated schema names. @@ -349,17 +406,19 @@ module ActiveRecord nil end - # Returns just a table's primary key - def primary_key(table) - pks = query(<<-end_sql, 'SCHEMA') - SELECT attr.attname - FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) - WHERE cons.contype = 'p' - AND cons.conrelid = '#{quote_table_name(table)}'::regclass - end_sql - return nil unless pks.count == 1 - pks[0][0] + def primary_keys(table_name) # :nodoc: + select_values(<<-SQL.strip_heredoc, 'SCHEMA') + WITH pk_constraint AS ( + SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint + WHERE contype = 'p' + AND conrelid = '#{quote_table_name(table_name)}'::regclass + ), cons AS ( + SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint + ) + SELECT attr.attname FROM pg_attribute attr + INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum + ORDER BY cons.rownum + SQL end # Renames a table. @@ -376,7 +435,7 @@ module ActiveRecord new_seq = "#{new_name}_#{pk}_seq" idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end @@ -410,11 +469,12 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) # :nodoc: + def change_column_default(table_name, column_name, default_or_changes) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column + default = extract_new_default_value(default_or_changes) alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" if default.nil? # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will @@ -446,8 +506,28 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" end - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" + def remove_index(table_name, options = {}) #:nodoc: + table = Utils.extract_schema_qualified_name(table_name.to_s) + + if options.is_a?(Hash) && options.key?(:name) + provided_index = Utils.extract_schema_qualified_name(options[:name].to_s) + + options[:name] = provided_index.identifier + table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present? + + if provided_index.schema.present? && table.schema != provided_index.schema + raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'") + end + end + + index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options)) + algorithm = + if options.is_a?(Hash) && options.key?(:algorithm) + index_algorithms.fetch(options[:algorithm]) do + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + end + end + execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}" end # Renames an index of a table. Raises error if length of new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 58715978f7..b2c49989a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -12,7 +12,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 93fa3984e6..2de6fbfaf0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -9,6 +9,7 @@ require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" require "active_record/connection_adapters/postgresql/schema_definitions" +require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" @@ -18,12 +19,6 @@ require 'ipaddr' module ActiveRecord module ConnectionHandling # :nodoc: - VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, - :client_encoding, :options, :application_name, :fallback_application_name, - :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, - :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] - # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) conn_params = config.symbolize_keys @@ -35,7 +30,8 @@ module ActiveRecord conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PGconn.connect. - conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + conn_params.slice!(*valid_conn_param_keys) # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. @@ -76,7 +72,6 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -93,7 +88,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -106,6 +100,12 @@ module ActiveRecord ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, + line: { name: "line" }, + lseg: { name: "lseg" }, + box: { name: "box" }, + path: { name: "path" }, + polygon: { name: "polygon" }, + circle: { name: "circle" }, bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, @@ -117,61 +117,14 @@ module ActiveRecord include PostgreSQL::ReferentialIntegrity include PostgreSQL::SchemaStatements include PostgreSQL::DatabaseStatements + include PostgreSQL::ColumnDumper include Savepoints def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - def column_spec_for_primary_key(column) - spec = {} - if column.serial? - return unless column.bigint? - spec[:id] = ':bigserial' - elsif column.type == :uuid - spec[:id] = ':uuid' - spec[:default] = column.default_function.inspect - else - spec[:id] = column.type.inspect - spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) - end - spec - end - - # Adds +:array+ option to the default set provided by the - # AbstractAdapter - def prepare_column_options(column) # :nodoc: - spec = super - spec[:array] = 'true' if column.array? - spec - end - - # Adds +:array+ as a valid migration key - def migration_keys - super + [:array] - end - - def schema_type(column) - return super unless column.serial? - - if column.bigint? - 'bigserial' - else - 'serial' - end - end - private :schema_type - - def schema_default(column) - if column.default_function - column.default_function.inspect unless column.serial? - else - super - end - end - private :schema_default - - # Returns +true+, since this connection adapter supports prepared statement + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true @@ -201,13 +154,18 @@ module ActiveRecord true end + def supports_json? + postgresql_version >= 90200 + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) - super + super(max) + @connection = connection @counter = 0 end @@ -234,16 +192,17 @@ module ActiveRecord # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) - super(connection, logger) + super(connection, logger, config) @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end - @connection_parameters, @config = connection_parameters, config + @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -321,18 +280,18 @@ module ActiveRecord true end - # Enable standard-conforming strings if available. def set_standard_conforming_strings - old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil - ensure - self.client_min_messages = old + execute('SET standard_conforming_strings = on', 'SCHEMA') end def supports_ddl_transactions? true end + def supports_advisory_locks? + true + end + def supports_explain? true end @@ -351,6 +310,20 @@ module ActiveRecord postgresql_version >= 90300 end + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{lock_id});") + end + + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{lock_id})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -417,12 +390,12 @@ module ActiveRecord "average" => "avg", } - protected + # Returns the version of the connected PostgreSQL server. + def postgresql_version + @connection.server_version + end - # Returns the version of the connected PostgreSQL server. - def postgresql_version - @connection.server_version - end + protected # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html FOREIGN_KEY_VIOLATION = "23503" @@ -433,9 +406,9 @@ module ActiveRecord case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end @@ -488,15 +461,15 @@ module ActiveRecord m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) m.register_type 'ltree', OID::SpecializedString.new(:ltree) + m.register_type 'line', OID::SpecializedString.new(:line) + m.register_type 'lseg', OID::SpecializedString.new(:lseg) + m.register_type 'box', OID::SpecializedString.new(:box) + m.register_type 'path', OID::SpecializedString.new(:path) + m.register_type 'polygon', OID::SpecializedString.new(:polygon) + m.register_type 'circle', OID::SpecializedString.new(:circle) # FIXME: why are we keeping these types as strings? m.alias_type 'interval', 'varchar' - m.alias_type 'path', 'varchar' - m.alias_type 'line', 'varchar' - m.alias_type 'polygon', 'varchar' - m.alias_type 'circle', 'varchar' - m.alias_type 'lseg', 'varchar' - m.alias_type 'box', 'varchar' register_class_with_precision m, 'time', Type::Time register_class_with_precision m, 'timestamp', OID::DateTime @@ -539,8 +512,13 @@ module ActiveRecord def extract_value_from_default(default) # :nodoc: case default # Quoted types - when /\A[\(B]?'(.*)'::/m - $1.gsub("''".freeze, "'".freeze) + when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m + # The default 'now'::date is CURRENT_DATE + if $1 == "now".freeze && $2 == "date".freeze + nil + else + $1.gsub("''".freeze, "'".freeze) + end # Boolean types when 'true'.freeze, 'false'.freeze default @@ -562,7 +540,7 @@ module ActiveRecord end def has_default_function?(default_value, default) # :nodoc: - !default_value && (%r{\w+\(.*\)} === default) + !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) end def load_additional_types(type_map, oids = nil) # :nodoc: @@ -594,16 +572,22 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: - def execute_and_clear(sql, name, binds) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) + def execute_and_clear(sql, name, binds, prepare: false) + if without_prepared_statement?(binds) + result = exec_no_cache(sql, name, []) + elsif !prepare + result = exec_no_cache(sql, name, binds) + else + result = exec_cache(sql, name, binds) + end ret = yield result result.clear ret end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql, []) } + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) } end def exec_cache(sql, name, binds) @@ -614,7 +598,7 @@ module ActiveRecord @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e - pgerror = e.original_exception + pgerror = e.cause # Get the PG code for the failure. Annoyingly, the code for # prepared statements whose return value may have changed is @@ -670,7 +654,7 @@ module ActiveRecord configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -685,7 +669,7 @@ module ActiveRecord self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] - # Use standard-conforming strings if available so we don't have to do the E'...' dance. + # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings # If using Active Record's time zone support configure the connection to return @@ -711,15 +695,7 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id(sequence_name) #:nodoc: - Integer(last_insert_id_value(sequence_name)) - end - - def last_insert_id_value(sequence_name) - last_insert_id_result(sequence_name).rows.first.first - end - - def last_insert_id_result(sequence_name) #:nodoc: + def last_insert_id_result(sequence_name) # :nodoc: exec_query("SELECT currval('#{sequence_name}')", 'SQL') end @@ -756,12 +732,12 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) # :nodoc: - sql[/into\s+([^\(]*).*values\s*\(/im] + sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as + PostgreSQL::TableDefinition.new(name, temporary, options, as) end def can_perform_case_insensitive_comparison_for?(column) @@ -837,7 +813,8 @@ module ActiveRecord ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) - ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Rails51Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 981d5d7a3c..eee142378c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -10,7 +10,7 @@ module ActiveRecord @columns = {} @columns_hash = {} @primary_keys = {} - @tables = {} + @data_sources = {} end def initialize_dup(other) @@ -18,33 +18,38 @@ module ActiveRecord @columns = @columns.dup @columns_hash = @columns_hash.dup @primary_keys = @primary_keys.dup - @tables = @tables.dup + @data_sources = @data_sources.dup end def primary_keys(table_name) - @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. - def table_exists?(name) - prepare_tables if @tables.empty? - return @tables[name] if @tables.key? name + def data_source_exists?(name) + prepare_data_sources if @data_sources.empty? + return @data_sources[name] if @data_sources.key? name - @tables[name] = connection.table_exists?(name) + @data_sources[name] = connection.data_source_exists?(name) end + alias table_exists? data_source_exists? + deprecate :table_exists? => "use #data_source_exists? instead" + # Add internal cache for table with +table_name+. def add(table_name) - if table_exists?(table_name) + if data_source_exists?(table_name) primary_keys(table_name) columns(table_name) columns_hash(table_name) end end - def tables(name) - @tables[name] + def data_sources(name) + @data_sources[name] end + alias tables data_sources + deprecate :tables => "use #data_sources instead" # Get the columns for a table def columns(table_name) @@ -64,36 +69,38 @@ module ActiveRecord @columns.clear @columns_hash.clear @primary_keys.clear - @tables.clear + @data_sources.clear @version = nil end def size - [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ + [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+ end - # Clear out internal caches for table with +table_name+. - def clear_table_cache!(table_name) - @columns.delete table_name - @columns_hash.delete table_name - @primary_keys.delete table_name - @tables.delete table_name + # Clear out internal caches for the data source +name+. + def clear_data_source_cache!(name) + @columns.delete name + @columns_hash.delete name + @primary_keys.delete name + @data_sources.delete name end + alias clear_table_cache! clear_data_source_cache! + deprecate :clear_table_cache! => "use #clear_data_source_cache! instead" def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version, @columns, @columns_hash, @primary_keys, @tables] + [@version, @columns, @columns_hash, @primary_keys, @data_sources] end def marshal_load(array) - @version, @columns, @columns_hash, @primary_keys, @tables = array + @version, @columns, @columns_hash, @primary_keys, @data_sources = array end private - def prepare_tables - connection.tables.each { |table| @tables[table] = true } + def prepare_data_sources + connection.data_sources.each { |source| @data_sources[source] = true } end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb new file mode 100644 index 0000000000..fe1dcbd710 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class SchemaCreation < AbstractAdapter::SchemaCreation + private + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7faca9cb70..d1893f35f5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/sqlite3/schema_creation' gem 'sqlite3', '~> 1.3.6' require 'sqlite3' @@ -32,7 +33,7 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -64,18 +65,6 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool private @@ -84,19 +73,22 @@ module ActiveRecord end end + def schema_creation # :nodoc: + SQLite3::SchemaCreation.new self + end + def initialize(connection, logger, connection_options, config) - super(connection, logger) + super(connection, logger, config) @active = nil - @statements = StatementPool.new(@connection, - self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @config = config + @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @visitor = Arel::Visitors::SQLite.new self @quoted_column_names = {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true + @visitor.extend(DetermineIfPreparableVisitor) else @prepared_statements = false end @@ -137,6 +129,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def active? @active != false end @@ -226,7 +222,7 @@ module ActiveRecord end class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles # the output of the SQLite shell: # # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) @@ -239,15 +235,18 @@ module ActiveRecord end end - def exec_query(sql, name = nil, binds = []) + def exec_query(sql, name = nil, binds = [], prepare: false) type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } log(sql, name, binds) do # Don't cache statements if they are not prepared - if without_prepared_statement?(binds) + unless prepare stmt = @connection.prepare(sql) begin cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end records = stmt.to_a ensure stmt.close @@ -260,7 +259,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params type_casted_binds + stmt.bind_params(type_casted_binds) end ActiveRecord::Result.new(cols, stmt.to_a) @@ -281,22 +280,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - def select_rows(sql, name = nil, binds = []) exec_query(sql, name, binds).rows end @@ -315,21 +298,56 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = nil, table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, 'SCHEMA').map do |row| - row['name'] + def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG end + + data_sources + end + + def data_sources + select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end def table_exists?(table_name) - table_name && tables(nil, table_name).any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) + return false unless table_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" + + select_values(sql, 'SCHEMA').any? + end + + def views # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') + end + + def view_exists?(view_name) # :nodoc: + return false unless view_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(view_name)}" + + select_values(sql, 'SCHEMA').any? end # Returns an array of +Column+ objects for the table specified by +table_name+. @@ -344,9 +362,10 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end + collation = field['collation'] sql_type = field['type'] type_metadata = fetch_type_metadata(sql_type) - new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0) + new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation) end end @@ -375,13 +394,13 @@ module ActiveRecord end end - def primary_key(table_name) #:nodoc: + def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f['pk'] > 0 } - return nil unless pks.count == 1 - pks[0]['name'] + pks.sort_by { |f| f['pk'] }.map { |f| f['name'] } end - def remove_index!(table_name, index_name) #:nodoc: + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name_for_remove(table_name, options) exec_query "DROP INDEX #{quote_column_name(index_name)}" end @@ -416,7 +435,9 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) + alter_table(table_name) do |definition| definition[column_name].default = default end @@ -441,6 +462,7 @@ module ActiveRecord self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) self.scale = options[:scale] if options.include?(:scale) + self.collation = options[:collation] if options.include?(:collation) end end end @@ -454,9 +476,9 @@ module ActiveRecord protected def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA') raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure + table_structure_with_collation(table_name, structure) end def alter_table(table_name, options = {}) #:nodoc: @@ -491,7 +513,7 @@ module ActiveRecord @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, :precision => column.precision, :scale => column.scale, - :null => column.null) + :null => column.null, collation: column.collation) end yield @definition if block_given? end @@ -548,11 +570,51 @@ module ActiveRecord # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) else super end end + + private + COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze + + def table_structure_with_collation(table_name, basic_structure) + collation_hash = {} + sql = "SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type='table' and name='#{ table_name }' \;" + + # Result will have following sample string + # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + # "password_digest" varchar COLLATE "NOCASE"); + result = exec_query(sql, 'SCHEMA').first + + if result + # Splitting with left parantheses and picking up last will return all + # columns separated with comma(,). + columns_string = result["sql"].split('(').last + + columns_string.split(',').each do |column_string| + # This regex will match the column name and collation type and will save + # the value in $1 and $2 respectively. + collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string) + end + + basic_structure.map! do |column| + column_name = column['name'] + + if collation_hash.has_key? column_name + column['collation'] = collation_hash[column_name] + end + + column + end + else + basic_structure.to_hash + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb index 82e9ef3d3d..57463dd749 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -3,10 +3,9 @@ module ActiveRecord class StatementPool include Enumerable - def initialize(connection, max = 1000) + def initialize(max = 1000) @cache = Hash.new { |h,pid| h[pid] = {} } - @connection = connection - @max = max + @max = max end def each(&block) |