diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
16 files changed, 1581 insertions, 1012 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 b8f99adc22..46c7fc71ac 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -2,6 +2,7 @@ require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/deprecation' +require 'timeout' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -11,9 +12,6 @@ module ActiveRecord # Raised when a connection pool is full and another connection is requested class PoolFullError < ConnectionNotEstablished - def initialize size, timeout - super("Connection pool of size #{size} and timeout #{timeout}s is full") - end end module ConnectionAdapters @@ -94,6 +92,21 @@ module ActiveRecord attr_accessor :automatic_reconnect, :timeout attr_reader :spec, :connections, :size, :reaper + class Latch # :nodoc: + def initialize + @mutex = Mutex.new + @cond = ConditionVariable.new + end + + def release + @mutex.synchronize { @cond.broadcast } + end + + def await + @mutex.synchronize { @cond.wait @mutex } + end + end + # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, # host name, username, password, etc), as well as the maximum size for @@ -115,6 +128,7 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 + @latch = Latch.new @connections = [] @automatic_reconnect = true end @@ -128,18 +142,21 @@ module ActiveRecord @reserved_connections[current_connection_id] ||= checkout end - # Check to see if there is an active connection in this connection - # pool. + # Is there an open connection that is being used for the current thread? def active_connection? - active_connections.any? + @reserved_connections.fetch(current_connection_id) { + return false + }.in_use? end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. def release_connection(with_id = current_connection_id) - conn = @reserved_connections.delete(with_id) - checkin conn if conn + synchronize do + conn = @reserved_connections.delete(with_id) + checkin conn if conn + end end # If a connection already exists yield it to the block. If no connection @@ -184,19 +201,10 @@ module ActiveRecord end end - # Verify active connections and remove and disconnect connections - # associated with stale threads. - def verify_active_connections! #:nodoc: - synchronize do - @connections.each do |connection| - connection.verify! - end - end - end - def clear_stale_cached_connections! # :nodoc: + reap end - deprecate :clear_stale_cached_connections! + deprecate :clear_stale_cached_connections! => "Please use #reap instead" # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. @@ -213,23 +221,23 @@ module ActiveRecord # Raises: # - PoolFullError: no connection can be obtained from the pool. def checkout - # Checkout an available connection - synchronize do - # Try to find a connection that hasn't been leased, and lease it - conn = connections.find { |c| c.lease } - - # If all connections were leased, and we have room to expand, - # create a new connection and lease it. - if !conn && connections.size < size - conn = checkout_new_connection - conn.lease - end + loop do + # Checkout an available connection + synchronize do + # Try to find a connection that hasn't been leased, and lease it + conn = connections.find { |c| c.lease } + + # If all connections were leased, and we have room to expand, + # create a new connection and lease it. + if !conn && connections.size < size + conn = checkout_new_connection + conn.lease + end - if conn - checkout_and_verify conn - else - raise PoolFullError.new(size, timeout) + return checkout_and_verify(conn) if conn end + + Timeout.timeout(@timeout, PoolFullError) { @latch.await } end end @@ -243,7 +251,10 @@ module ActiveRecord conn.run_callbacks :checkin do conn.expire end + + release conn end + @latch.release end # Remove a connection from the connection pool. The connection will @@ -254,11 +265,9 @@ module ActiveRecord # FIXME: we might want to store the key on the connection so that removing # from the reserved hash will be a little easier. - thread_id = @reserved_connections.keys.find { |k| - @reserved_connections[k] == conn - } - @reserved_connections.delete thread_id if thread_id + release conn end + @latch.release end # Removes dead connections from the pool. A dead connection can occur @@ -271,10 +280,23 @@ module ActiveRecord remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end end + @latch.release end private + def release(conn) + thread_id = if @reserved_connections[current_connection_id] == conn + current_connection_id + else + @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + end + + @reserved_connections.delete thread_id if thread_id + end + def new_connection ActiveRecord::Base.send(spec.adapter_method, spec.config) end @@ -298,10 +320,6 @@ module ActiveRecord end c end - - def active_connections - @connections.find_all { |c| c.in_use? } - end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -328,16 +346,18 @@ module ActiveRecord # ActiveRecord::Base.connection_handler. Active Record models use this to # determine that connection pool that they should use. class ConnectionHandler - attr_reader :connection_pools - - def initialize(pools = {}) + def initialize(pools = Hash.new { |h,k| h[k] = {} }) @connection_pools = pools - @class_to_pool = {} + @class_to_pool = Hash.new { |h,k| h[k] = {} } + end + + def connection_pools + @connection_pools[Process.pid] end def establish_connection(name, spec) - @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec) - @class_to_pool[name] = @connection_pools[spec] + set_pool_for_spec spec, ConnectionAdapters::ConnectionPool.new(spec) + set_class_to_pool name, connection_pools[spec] end # Returns true if there are any active connections among the connection @@ -350,21 +370,16 @@ module ActiveRecord # and also returns connections to the pool cached by threads that are no # longer alive. def clear_active_connections! - @connection_pools.each_value {|pool| pool.release_connection } + connection_pools.each_value {|pool| pool.release_connection } end # Clears the cache which maps classes. def clear_reloadable_connections! - @connection_pools.each_value {|pool| pool.clear_reloadable_connections! } + connection_pools.each_value {|pool| pool.clear_reloadable_connections! } end def clear_all_connections! - @connection_pools.each_value {|pool| pool.disconnect! } - end - - # Verify active connections. - def verify_active_connections! #:nodoc: - @connection_pools.each_value {|pool| pool.verify_active_connections! } + connection_pools.each_value {|pool| pool.disconnect! } end # Locate the connection of the nearest super class. This can be an @@ -388,53 +403,56 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @class_to_pool.delete(klass.name) + pool = class_to_pool.delete(klass.name) return nil unless pool - @connection_pools.delete pool.spec + connection_pools.delete pool.spec pool.automatic_reconnect = false pool.disconnect! pool.spec.config end def retrieve_connection_pool(klass) - pool = @class_to_pool[klass.name] + pool = get_pool_for_class klass.name return pool if pool return nil if ActiveRecord::Model == klass retrieve_connection_pool klass.active_record_super end - end - - class ConnectionManagement - class Proxy # :nodoc: - attr_reader :body, :testing - def initialize(body, testing = false) - @body = body - @testing = testing - end + private - def method_missing(method_sym, *arguments, &block) - @body.send(method_sym, *arguments, &block) - end + def class_to_pool + @class_to_pool[Process.pid] + end - def respond_to?(method_sym, include_private = false) - super || @body.respond_to?(method_sym) - end + def set_pool_for_spec(spec, pool) + @connection_pools[Process.pid][spec] = pool + end - def each(&block) - body.each(&block) - end + def set_class_to_pool(name, pool) + @class_to_pool[Process.pid][name] = pool + pool + end - def close - body.close if body.respond_to?(:close) + def get_pool_for_class(klass) + @class_to_pool[Process.pid].fetch(klass) { + c_to_p = @class_to_pool.values.find { |class_to_pool| + class_to_pool[klass] + } - # Don't return connection (and perform implicit rollback) if - # this request is a part of integration test - ActiveRecord::Base.clear_active_connections! unless testing - end + if c_to_p + pool = c_to_p[klass] + pool = ConnectionAdapters::ConnectionPool.new pool.spec + set_pool_for_spec pool.spec, pool + set_class_to_pool klass, pool + else + set_class_to_pool klass, nil + end + } end + end + class ConnectionManagement def initialize(app) @app = app end @@ -442,9 +460,12 @@ module ActiveRecord def call(env) testing = env.key?('rack.test') - status, headers, body = @app.call(env) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + ActiveRecord::Base.clear_active_connections! unless testing + end - [status, headers, Proxy.new(body, testing)] + response rescue ActiveRecord::Base.clear_active_connections! unless testing raise 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 eb8cff9610..7b2961a04a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -2,9 +2,11 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements # Converts an arel AST to SQL - def to_sql(arel) + def to_sql(arel, binds = []) if arel.respond_to?(:ast) - visitor.accept(arel.ast) + visitor.accept(arel.ast) do + quote(*binds.shift.reverse) + end else arel end @@ -13,19 +15,19 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select_all(arel, name = nil, binds = []) - select(to_sql(arel), name, binds) + select(to_sql(arel, binds), name, binds) end # Returns a record hash with the column names as keys and column values # as values. - def select_one(arel, name = nil) - result = select_all(arel, name) + def select_one(arel, name = nil, binds = []) + result = select_all(arel, name, binds) result.first if result end # Returns a single value from a record - def select_value(arel, name = nil) - if result = select_one(arel, name) + def select_value(arel, name = nil, binds = []) + if result = select_one(arel, name, binds) result.values.first end end @@ -33,7 +35,7 @@ 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) - result = select_rows(to_sql(arel), name) + result = select_rows(to_sql(arel, []), name) result.map { |v| v[0] } end @@ -55,21 +57,21 @@ module ActiveRecord end # Executes insert +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_delete(sql, name, binds) exec_query(sql, name, binds) end # Executes update +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_update(sql, name, binds) exec_query(sql, name, binds) @@ -84,19 +86,19 @@ 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), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, 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) id_value || last_inserted_id(value) end # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) - exec_update(to_sql(arel), name, binds) + exec_update(to_sql(arel, binds), name, binds) end # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) - exec_delete(to_sql(arel), name, binds) + exec_delete(to_sql(arel, binds), name, binds) end # Checks whether there is currently no transaction active. This is done @@ -310,13 +312,27 @@ module ActiveRecord # 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: - subselect = select.clone - subselect.projections = [update.key] + key = update.key + subselect = subquery_for(key, select) + + update.where key.in(subselect) + end + + def join_to_delete(delete, select, key) #:nodoc: + subselect = subquery_for(key, select) - update.where update.key.in(subselect) + delete.where key.in(subselect) end protected + + # Return a subquery for the given key using the join information. + def subquery_for(key, select) + subselect = select.clone + subselect.projections = [key] + subselect + end + # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 27ff13ad89..17377bad96 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -57,7 +57,7 @@ module ActiveRecord def select_all(arel, name = nil, binds = []) if @query_cache_enabled - sql = to_sql(arel) + sql = to_sql(arel, binds) cache_sql(sql, binds) { super(sql, name, binds) } else super @@ -65,18 +65,24 @@ module ActiveRecord end private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end + def cache_sql(sql, binds) + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument("sql.active_record", + :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + # FIXME: we should guarantee that all cached items are Result + # objects. Then we can avoid this conditional + if ActiveRecord::Result === result + result.dup + else result.collect { |row| row.dup } end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index f93c7cd74a..44ac37c498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -71,7 +71,8 @@ module ActiveRecord when Date, Time then quoted_date(value) when Symbol then value.to_s else - YAML.dump(value) + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 132ca10f79..f0b6ae2b7d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,10 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc: + # Abstract representation of an index definition on a table. Instances of + # this type are typically created and returned by methods in database + # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end # Abstract representation of a column definition. Instances of this type @@ -62,11 +65,12 @@ module ActiveRecord class TableDefinition # An array of ColumnDefinition objects, representing the column changes # that have been defined. - attr_accessor :columns + attr_accessor :columns, :indexes def initialize(base) @columns = [] @columns_hash = {} + @indexes = {} @base = base end @@ -209,19 +213,22 @@ 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. So what can be written like this: + # 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: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id # t.string :tagger_type # t.string :taggable_type, :default => 'Photo' # end + # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id' + # add_index :taggings, [:tagger_id, :tagger_type] # # Can also be written as follows using references: # # create_table :taggings do |t| - # t.references :tag - # t.references :tagger, :polymorphic => true + # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' } + # t.references :tagger, :polymorphic => true, :index => true # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) @@ -252,6 +259,14 @@ module ActiveRecord end # end EOV end + + # Adds index options to the indexes hash, keyed by column name + # This is primarily used to track indexes that need to be created after the table + # === Examples + # index(:account_id, :name => 'index_projects_on_account_id') + def index(column_name, options = {}) + indexes[column_name] = options + end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. @@ -264,9 +279,11 @@ module ActiveRecord def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| column("#{col}_id", :integer, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references @@ -391,7 +408,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. @@ -432,9 +449,11 @@ module ActiveRecord def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| @base.add_column(@table_name, "#{col}_id", :integer, options) @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + @base.add_index(@table_name, polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references 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 bfd5121a70..e7a4f061fd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,8 +1,12 @@ require 'active_support/deprecation/reporting' +require 'active_record/schema_migration' +require 'active_record/migration/join_table' module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements + include ActiveRecord::Migration::JoinTable + # Returns a Hash of mappings from the abstract data types to the native # database types. See TableDefinition#column for details on the recognized # abstract data types. @@ -12,7 +16,7 @@ module ActiveRecord # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0...table_alias_length].gsub(/\./, '_') + table_name[0...table_alias_length].tr('.', '_') end # Checks to see if the table +table_name+ exists on the database. @@ -52,7 +56,7 @@ module ActiveRecord # Returns an array of Column objects for the table specified by +table_name+. # See the concrete implementation for details on the expected parameter values. - def columns(table_name, name = nil) end + def columns(table_name) end # Checks to see if a column exists in a given table. # @@ -159,7 +163,7 @@ module ActiveRecord yield td if block_given? if options[:force] && table_exists?(table_name) - drop_table(table_name) + drop_table(table_name, options) end create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " @@ -167,6 +171,46 @@ module ActiveRecord create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql + td.indexes.each_pair { |c,o| add_index table_name, c, o } + end + + # Creates a new join table with the name created using the lexical order of the first two + # arguments. These arguments can be a String or a Symbol. + # + # # Creates a table called 'assemblies_parts' with no id. + # create_join_table(:assemblies, :parts) + # + # You can pass a +options+ hash can include the following keys: + # [<tt>:table_name</tt>] + # Sets the table name overriding the default + # [<tt>:column_options</tt>] + # Any extra options you want appended to the columns definition. + # [<tt>:options</tt>] + # Any extra options you want appended to the table definition. + # [<tt>:temporary</tt>] + # Make a temporary table. + # [<tt>:force</tt>] + # Set to true to drop the table before creating it. + # Defaults to false. + # + # ===== Examples + # ====== Add a backend specific option to the generated SQL (MySQL) + # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # generates: + # CREATE TABLE assemblies_parts ( + # assembly_id int NOT NULL, + # part_id int NOT NULL, + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + def create_join_table(table_1, table_2, options = {}) + join_table_name = find_join_table_name(table_1, table_2, options) + + column_options = options.delete(:column_options) || {} + column_options.reverse_merge!({:null => false}) + + create_table(join_table_name, options.merge!(:id => false)) do |td| + td.integer :"#{table_1.to_s.singularize}_id", column_options + td.integer :"#{table_2.to_s.singularize}_id", column_options + end end # A block for changing columns in +table+. @@ -251,7 +295,7 @@ module ActiveRecord end # Drops a table from the database. - def drop_table(table_name) + def drop_table(table_name, options = {}) execute "DROP TABLE #{quote_table_name(table_name)}" end @@ -332,15 +376,22 @@ module ActiveRecord # Note: SQLite doesn't support index length # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc}) + # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc}) # generates # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) # + # ====== Creating a partial index + # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active") + # generates + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # + # Note: only supported by PostgreSQL + # def add_index(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" + index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end # Remove the given index from the table. @@ -404,22 +455,16 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version") - migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n") + + ActiveRecord::SchemaMigration.order('version').all.map { |sm| + "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" + }.join "\n\n" end # Should not be called normally, but this operation is non-destructive. # The migrations module handles this automatically. def initialize_schema_migrations_table - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - - unless table_exists?(sm_table) - create_table(sm_table, :id => false) do |schema_migrations_table| - schema_migrations_table.column :version, :string, :null => false - end - add_index sm_table, :version, :unique => true, - :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" - end + ActiveRecord::SchemaMigration.create_table end def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) @@ -495,8 +540,8 @@ module ActiveRecord # ===== Examples # add_timestamps(:suppliers) def add_timestamps(table_name) - add_column table_name, :created_at, :datetime, :null => false - add_column table_name, :updated_at, :datetime, :null => false + add_column table_name, :created_at, :datetime + add_column table_name, :updated_at, :datetime end # Removes the timestamp columns (created_at and updated_at) from the table definition. @@ -544,6 +589,9 @@ module ActiveRecord if Hash === options # legacy support, since this param was a string index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end else index_type = options end @@ -556,7 +604,7 @@ module ActiveRecord end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns] + [index_name, index_type, index_columns, index_options] end def index_name_for_remove(table_name, options = {}) @@ -570,8 +618,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index edea414db7..c6faae77cc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -13,21 +13,24 @@ module ActiveRecord autoload :Column autoload :ConnectionSpecification - autoload_under 'abstract' do - autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions' + autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do + autoload :IndexDefinition + autoload :ColumnDefinition + autoload :TableDefinition + autoload :Table + end + autoload_at 'active_record/connection_adapters/abstract/connection_pool' do + autoload :ConnectionHandler + autoload :ConnectionManagement + end + + autoload_under 'abstract' do autoload :SchemaStatements autoload :DatabaseStatements autoload :DatabaseLimits autoload :Quoting - autoload :ConnectionPool - autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' - autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' - autoload :QueryCache end @@ -54,7 +57,7 @@ module ActiveRecord define_callbacks :checkout, :checkin attr_accessor :visitor, :pool - attr_reader :schema_cache, :last_use, :in_use + attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use def initialize(connection, logger = nil, pool = nil) #:nodoc: @@ -83,6 +86,11 @@ module ActiveRecord end end + def schema_cache=(cache) + cache.connection = self + @schema_cache = cache + end + def expire @in_use = false end @@ -142,6 +150,11 @@ module ActiveRecord false end + # Does this adapter support partial indices? + def supports_partial_index? + false + end + # Does this adapter support explain? As of this writing sqlite3, # mysql2, and postgresql are the only ones that do. def supports_explain? @@ -150,15 +163,10 @@ module ActiveRecord # QUOTING ================================================== - # Override to return the quoted table name. Defaults to column quoting. - def quote_table_name(name) - quote_column_name(name) - end - # Returns a bind substitution value given a +column+ and list of current # +binds+ def substitute_at(column, index) - Arel.sql '?' + Arel::Nodes::BindParam.new '?' end # REFERENTIAL INTEGRITY ==================================== @@ -271,26 +279,25 @@ module ActiveRecord protected - def log(sql, name = "SQL", binds = []) - @instrumenter.instrument( - "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :binds => binds) { yield } - rescue Exception => e - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.debug message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception - end - - def translate_exception(e, message) - # override in derived class - ActiveRecord::StatementInvalid.new(message) - end - + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } + rescue Exception => e + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + raise exception + end + + def translate_exception(exception, message) + # override in derived class + ActiveRecord::StatementInvalid.new(message) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 560773ca86..a848838a4e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'arel/visitors/bind_visitor' module ActiveRecord module ConnectionAdapters @@ -122,12 +123,21 @@ module ActiveRecord :boolean => { :name => "tinyint", :limit => 1 } } + class BindSubstitution < Arel::Visitors::MySQL # :nodoc: + include Arel::Visitors::BindVisitor + end + # 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 @quoted_column_names, @quoted_table_names = {}, {} - @visitor = Arel::Visitors::MySQL.new self + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::MySQL.new self + else + @visitor = BindSubstitution.new self + end end def adapter_name #:nodoc: @@ -284,19 +294,10 @@ 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. However, MySQL is too stupid to create a - # temporary table for this automatically, so we have to give it some prompting - # in the form of a subsubquery. Ugh! + # these, we must use a subquery. def join_to_update(update, select) #:nodoc: if select.limit || select.offset || select.orders.any? - subsubselect = select.clone - subsubselect.projections = [update.key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(update.key.name) - subselect.from subsubselect.as('__active_record_temp') - - update.where update.key.in(subselect) + super else update.table select.source update.wheres = select.constraints @@ -365,7 +366,7 @@ module ActiveRecord def tables(name = nil, database = nil, like = nil) #:nodoc: sql = "SHOW TABLES " - sql << "IN #{database} " if database + sql << "IN #{quote_table_name(database)} " if database sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| @@ -409,7 +410,7 @@ module ActiveRecord end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name, name = nil)#: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| @@ -427,7 +428,7 @@ module ActiveRecord table, arguments = args.shift, args method = :"#{command}_sql" - if respond_to?(method) + if respond_to?(method, true) send(method, table, *arguments) else raise "Unknown method called : #{method}(#{arguments.inspect})" @@ -474,15 +475,26 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + case type.to_s + when 'integer' + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + when 'text' + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has character length #{limit}") + end + else + super end end @@ -504,8 +516,8 @@ module ActiveRecord 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+\((.+)\)/ - keys = $1.split(",").map { |key| key.gsub(/`/, "") } + 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 @@ -537,11 +549,22 @@ module ActiveRecord protected + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') + end + def add_index_length(option_strings, column_names, options = {}) if options.is_a?(Hash) && length = options[:length] case length when Hash - column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name)} + column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} when Fixnum column_names.each {|name| option_strings[name] += "(#{length})"} end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 2ecb198edb..9af8e46120 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,4 +1,5 @@ require 'set' +require 'active_support/deprecation' module ActiveRecord # :stopdoc: @@ -66,6 +67,25 @@ module ActiveRecord end end + def binary? + type == :binary + end + + # Casts a Ruby value to something appropriate for writing to the database. + def type_cast_for_write(value) + return value unless number? + + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end + # Casts value (which is a String) to an appropriate instance. def type_cast(value) return nil if value.nil? @@ -75,7 +95,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -83,12 +103,14 @@ module ActiveRecord when :date then klass.value_to_date(value) when :binary then klass.binary_to_string(value) when :boolean then klass.value_to_boolean(value) - when :hstore then klass.cast_hstore(value) else value end end def type_cast_code(var_name) + ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \ + "using Column#type_cast only, and it is going to be removed in future Rails versions.") + klass = self.class.name case type @@ -101,7 +123,7 @@ module ActiveRecord when :date then "#{klass}.value_to_date(#{var_name})" when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" - when :hstore then "#{klass}.cast_hstore(#{var_name})" + when :hstore then "#{klass}.string_to_hstore(#{var_name})" else var_name 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 6086c32dbe..92908d9599 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -32,6 +32,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super + @visitor = BindSubstitution.new self configure_connection end @@ -65,10 +66,6 @@ module ActiveRecord @connection.escape(string) end - def substitute_at(column, index) - Arel.sql "\0" - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -80,6 +77,7 @@ module ActiveRecord disconnect! connect end + alias :reset! :reconnect! # Disconnects from the database if already connected. # Otherwise, this method does nothing. @@ -90,15 +88,10 @@ module ActiveRecord end end - def reset! - disconnect! - connect - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel)}" + sql = "EXPLAIN #{to_sql(arel, binds.dup)}" start = Time.now result = exec_query(sql, 'EXPLAIN', binds) elapsed = Time.now - start @@ -224,8 +217,7 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) - binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a + exec_query(sql, name) end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -234,18 +226,12 @@ module ActiveRecord end alias :create :insert_sql - def exec_insert(sql, name, binds) - binds = binds.dup - - # Pretend to support bind parameters - execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + execute to_sql(sql, binds), name end def exec_delete(sql, name, binds) - binds = binds.dup - - # Pretend to support bind parameters - execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name + execute to_sql(sql, binds), name @connection.affected_rows end alias :exec_update :exec_delete diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e432c5af32..724dbff1f0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -119,7 +119,7 @@ module ActiveRecord private def cache - @cache[$$] + @cache[Process.pid] end end @@ -408,7 +408,7 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds).to_a + rows = exec_query(sql, name, binds) @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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb new file mode 100644 index 0000000000..c82afc232c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -0,0 +1,243 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module OID + class Type + def type; end + + def type_cast_for_write(value) + value + end + end + + class Identity < Type + def type_cast(value) + value + end + end + + class Bytea < Type + def type_cast(value) + PGconn.unescape_bytea value + end + end + + class Money < Type + def type_cast(value) + return if value.nil? + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Vector < Type + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + + class Integer < Type + def type_cast(value) + return if value.nil? + + value.to_i rescue value ? 1 : 0 + end + end + + class Boolean < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_boolean value + end + end + + class Timestamp < Type + def type; :timestamp; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::PostgreSQLColumn.string_to_time value + end + end + + class Date < Type + def type; :datetime; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.value_to_date value + end + end + + class Time < Type + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.string_to_dummy_time value + end + end + + class Float < Type + def type_cast(value) + return if value.nil? + + value.to_f + end + end + + class Decimal < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Hstore < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + end + + class TypeMap + def initialize + @mapping = {} + end + + def []=(oid, type) + @mapping[oid] = type + end + + def [](oid) + @mapping[oid] + end + + def key?(oid) + @mapping.key? oid + end + + def fetch(ftype, fmod) + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if ftype == 1700 && (fmod - 4 & 0xffff).zero? + ftype = 23 + end + + @mapping.fetch(ftype) { |oid| yield oid, fmod } + end + end + + TYPE_MAP = TypeMap.new # :nodoc: + + # When the PG adapter connects, the pg_type table is queried. The + # key of this hash maps to the `typname` column from the table. + # TYPE_MAP is then dynamically built with oids as the key and type + # objects as values. + NAMES = Hash.new { |h,k| # :nodoc: + h[k] = OID::Identity.new + } + + # Register an OID type named +name+ with a typcasting object in + # +type+. +name+ should correspond to the `typname` column in + # the `pg_type` table. + def self.register_type(name, type) + NAMES[name] = type + end + + # Alias the +old+ type to the +new+ type. + def self.alias_type(new, old) + NAMES[new] = NAMES[old] + end + + # Is +name+ a registered type? + def self.registered_type?(name) + NAMES.key? name + end + + register_type 'int2', OID::Integer.new + alias_type 'int4', 'int2' + alias_type 'int8', 'int2' + alias_type 'oid', 'int2' + + register_type 'numeric', OID::Decimal.new + register_type 'text', OID::Identity.new + alias_type 'varchar', 'text' + alias_type 'char', 'text' + alias_type 'bpchar', 'text' + alias_type 'xml', 'text' + + # FIXME: why are we keeping these types as strings? + alias_type 'tsvector', 'text' + alias_type 'interval', 'text' + alias_type 'cidr', 'text' + alias_type 'inet', 'text' + alias_type 'macaddr', 'text' + alias_type 'bit', 'text' + alias_type 'varbit', 'text' + + # FIXME: I don't think this is correct. We should probably be returning a parsed date, + # but the tests pass with a string returned. + register_type 'timestamptz', OID::Identity.new + + register_type 'money', OID::Money.new + register_type 'bytea', OID::Bytea.new + register_type 'bool', OID::Boolean.new + + register_type 'float4', OID::Float.new + alias_type 'float8', 'float4' + + register_type 'timestamp', OID::Timestamp.new + register_type 'date', OID::Date.new + register_type 'time', OID::Time.new + + register_type 'path', OID::Identity.new + register_type 'polygon', OID::Identity.new + register_type 'circle', OID::Identity.new + register_type 'hstore', OID::Hstore.new + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e6ddf8bf8f..68cf495025 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,6 +1,8 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/object/blank' require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/postgresql/oid' +require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' @@ -14,8 +16,8 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, - :reaping_frequency].each do |key| + :schema_order, :adapter, :pool, :wait_timeout, :template, + :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -34,7 +36,8 @@ module ActiveRecord # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: # Instantiates a new PostgreSQL column definition in a table. - def initialize(name, default, sql_type = nil, null = true) + def initialize(name, default, oid_type, sql_type = nil, null = true) + @oid_type = oid_type super(name, self.class.extract_value_from_default(default), sql_type, null) end @@ -52,180 +55,191 @@ module ActiveRecord end end - def cast_hstore(object) + def hstore_to_string(object) if Hash === object object.map { |k,v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ', ' + }.join ',' else - kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o| - unescape_hstore(o[1...-1]) - } - Hash[kvs.each_slice(2).to_a] + object end end - private - HSTORE_ESCAPE = { - ' ' => '\\ ', - '\\' => '\\\\', - '"' => '\\"', - '=' => '\\=', - } - HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys) - HSTORE_UNESCAPE = HSTORE_ESCAPE.invert - HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys) - - def unescape_hstore(value) - value.gsub(HSTORE_UNESCAPE_RE) do |match| - HSTORE_UNESCAPE[match] + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string end end + private + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + def escape_hstore(value) - value.gsub(HSTORE_ESCAPE_RE) do |match| - HSTORE_ESCAPE[match] - end + value.nil? ? 'NULL' + : value == "" ? '""' + : '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') end end # :startdoc: - private - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - else - super - end - end - - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore + # Extracts the value from a PostgreSQL column default definition. + def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + case default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + $1 # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string + when /\A'(.*)'::(?:character varying|bpchar|text)\z/m + $1 + # Character types (8.1 formatting) + when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m + $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } # Binary data types - when 'bytea' - :binary + when /\A'(.*)'::bytea\z/m + $1 # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string + when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ + $1 + when /\A'(.*)'::interval\z/ + $1 + # Boolean type + when 'true' + true + when 'false' + false # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string + when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ + $1 # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string + when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ + $1 + # Bit string types + when /\AB'(.*)'::"?bit(?: varying)?"?\z/ + $1 # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector + when /\A'(.*)'::xml\z/m + $1 # Arrays - when /^\D+\[\]$/ - :string + when /\A'(.*)'::"?\D+"?\[\]\z/ + $1 + # Hstore + when /\A'(.*)'::hstore\z/ + $1 # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :string - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. + when /\A-?\d+\z/ + $1 else - super - end + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end + end - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - case default - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - when NilClass - nil - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?)\z/ - $1 - # Character types - when /\A'(.*)'::(?:character varying|bpchar|text)\z/m - $1 - # Character types (8.1 formatting) - when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m - $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end + def type_cast(value) + return if value.nil? + return super if encoded? + + @oid_type.type_cast value + end + + private + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super end + end + + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end + + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + else + super + end + end + + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when 'interval' + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Network address types + when /^(?:cidr|inet|macaddr)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :string + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super + end + end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -244,6 +258,8 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # defaults to true. # # Any further options are used as connection parameters to libpq. See # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the @@ -284,7 +300,8 @@ module ActiveRecord :binary => { :name => "bytea" }, :boolean => { :name => "boolean" }, :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" } + :tsvector => { :name => "tsvector" }, + :hstore => { :name => "hstore" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -302,6 +319,10 @@ module ActiveRecord true end + def supports_partial_index? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -340,7 +361,7 @@ module ActiveRecord private def cache - @cache[$$] + @cache[Process.pid] end def dealloc(key) @@ -354,11 +375,23 @@ module ActiveRecord end end + class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: + include Arel::Visitors::BindVisitor + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::PostgreSQL.new self + else + @visitor = BindSubstitution.new self + end + + connection_parameters.delete :prepared_statements + @connection_parameters, @config = connection_parameters, config - @visitor = Arel::Visitors::PostgreSQL.new self # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -372,7 +405,9 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true end # Clears the prepared statements cache. @@ -455,14 +490,14 @@ module ActiveRecord # Escapes binary strings for bytea input to the database. def escape_bytea(value) - @connection.escape_bytea(value) if value + PGconn.escape_bytea(value) if value end # Unescapes bytea output from a database to the binary string it represents. # NOTE: This is NOT an inverse of escape_bytea! This is only to be used # on escaped binary output from database drive. def unescape_bytea(value) - @connection.unescape_bytea(value) if value + PGconn.unescape_bytea(value) if value end # Quotes PostgreSQL-specific data types for SQL input. @@ -470,6 +505,11 @@ module ActiveRecord return super unless column case value + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + else super + end when Float return super unless value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -501,6 +541,9 @@ module ActiveRecord when String return super unless 'bytea' == column.sql_type { :value => value, :format => 1 } + when Hash + return super unless 'hstore' == column.sql_type + PostgreSQLColumn.hstore_to_string(value) else super end @@ -571,7 +614,7 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel)}" + sql = "EXPLAIN #{to_sql(arel, binds)}" ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end @@ -626,8 +669,11 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - if pk + 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 @@ -693,7 +739,14 @@ module ActiveRecord end def substitute_at(column, index) - Arel.sql("$#{index + 1}") + Arel::Nodes::BindParam.new "$#{index + 1}" + end + + class Result < ActiveRecord::Result + def initialize(columns, rows, column_types) + super(columns, rows) + @column_types = column_types + end end def exec_query(sql, name = 'SQL', binds = []) @@ -701,7 +754,17 @@ module ActiveRecord result = binds.empty? ? exec_no_cache(sql, binds) : exec_cache(sql, binds) - ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = Result.new(result.fields, result.values, types) result.clear return ret end @@ -725,11 +788,27 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end [sql, binds] end + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -885,16 +964,20 @@ module ActiveRecord # add info on sort order for columns (only desc order is explicitly specified, asc is the default) desc_order_columns = inddef.scan(/(\w+) DESC/).flatten orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) end.compact end # Returns the list of all column definitions for a table. - def columns(table_name, name = nil) + def columns(table_name) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).collect do |column_name, type, default, notnull| - PostgreSQLColumn.new(column_name, default, type, notnull == 'f') + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') end end @@ -916,6 +999,27 @@ module ActiveRecord end_sql end + # Returns an array of schema names. + def schema_names + query(<<-SQL).flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + # Sets the schema search path to a string of comma-separated schema names. # Names beginning with $ have to be quoted (e.g. $user => '$user'). # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html @@ -923,7 +1027,7 @@ module ActiveRecord # This should be not be called manually but set in database.yml. def schema_search_path=(schema_csv) if schema_csv - execute "SET search_path TO #{schema_csv}" + execute("SET search_path TO #{schema_csv}", 'SCHEMA') @schema_search_path = schema_csv end end @@ -945,7 +1049,9 @@ module ActiveRecord # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: - serial_sequence(table_name, pk || 'id').split('.').last + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last rescue ActiveRecord::StatementInvalid "#{table_name}_#{pk || 'id'}_seq" end @@ -983,26 +1089,46 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT attr.attname, ns.nspname, seq.relname - FROM pg_class seq - INNER JOIN pg_depend dep ON seq.oid = dep.objid - INNER JOIN pg_attribute attr ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - INNER JOIN pg_namespace ns ON seq.relnamespace = ns.oid - WHERE seq.relkind = 'S' - AND cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass + result = query(<<-end_sql, 'PK and serial sequence')[0] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql - # [primary_key, sequence] - if result.second == 'public' then - sequence = result.last - else - sequence = result.second+'.'+result.last + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = query(<<-end_sql, 'PK and custom sequence')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql end - [result.first, sequence] + [result.first, result.last] rescue nil end @@ -1107,7 +1233,10 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') } + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } @@ -1133,6 +1262,10 @@ module ActiveRecord end end + def use_insert_returning? + @use_insert_returning + end + protected # Returns the version of the connected PostgreSQL server. def postgresql_version @@ -1151,6 +1284,22 @@ module ActiveRecord end private + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } + + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end + end + FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: def exec_no_cache(sql, binds) @@ -1173,7 +1322,11 @@ module ActiveRecord # prepared statements whose return value may have changed is # FEATURE_NOT_SUPPORTED. Check here for more details: # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + begin + code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + rescue + raise e + end if FEATURE_NOT_SUPPORTED == code @statements.delete sql_key(sql) retry @@ -1242,14 +1395,21 @@ module ActiveRecord # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) #:nodoc: - r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) - Integer(r.rows.first.first) + 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: + exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) end # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds).to_a + exec_query(sql, name, binds) end def select_raw(sql, name = nil) @@ -1280,7 +1440,7 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4e8932a695..aad1f9a7ef 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,26 +1,17 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :connection + attr_reader :columns, :columns_hash, :primary_keys, :tables, :version + attr_accessor :connection def initialize(conn) @connection = conn - @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = conn.columns(table_name, "#{table_name} Columns") - end - - @columns_hash = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col] - }] - end - - @primary_keys = Hash.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil - end + @columns = {} + @columns_hash = {} + @primary_keys = {} + @tables = {} + prepare_default_proc end # A cached lookup for table existence. @@ -30,12 +21,22 @@ module ActiveRecord @tables[name] = connection.table_exists?(name) end + # Add internal cache for table with +table_name+. + def add(table_name) + if table_exists?(table_name) + @primary_keys[table_name] + @columns[table_name] + @columns_hash[table_name] + end + end + # Clears out internal caches def clear! @columns.clear @columns_hash.clear @primary_keys.clear @tables.clear + @version = nil end # Clear out internal caches for table with +table_name+. @@ -45,6 +46,37 @@ module ActiveRecord @primary_keys.delete table_name @tables.delete table_name end + + 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].map do |val| + self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } + end + end + + def marshal_load(array) + @version, @columns, @columns_hash, @primary_keys, @tables = array + prepare_default_proc + end + + private + + def prepare_default_proc + @columns.default_proc = Proc.new do |h, table_name| + h[table_name] = connection.columns(table_name) + end + + @columns_hash.default_proc = Proc.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys.default_proc = Proc.new do |h, table_name| + h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil + 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 ee5d10859c..44e407a561 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,8 @@ -require 'active_record/connection_adapters/sqlite_adapter' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'arel/visitors/bind_visitor' -gem 'sqlite3', '~> 1.3.5' +gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord @@ -35,7 +37,184 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Adapter < SQLiteAdapter # :nodoc: + class SQLite3Column < Column #:nodoc: + class << self + def binary_to_string(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + end + + # The SQLite3 adapter works SQLite 3.6.16 or newer + # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # + # Options: + # + # * <tt>:database</tt> - Path to the database file. + class SQLite3Adapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map { |v| v.to_i } + end + + def <=>(version_string) + @version <=> version_string.split('.').map { |v| v.to_i } + end + end + + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + + class BindSubstitution < Arel::Visitors::SQLite # :nodoc: + include Arel::Visitors::BindVisitor + end + + def initialize(connection, logger, config) + super(connection, logger) + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) + @config = config + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::SQLite.new self + else + @visitor = BindSubstitution.new self + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + # Returns true + def supports_ddl_transactions? + true + end + + # Returns true if SQLite version is '3.6.8' or greater, false otherwise. + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? #:nodoc: + true + end + + # Returns true. + def supports_primary_key? #:nodoc: + true + end + + def requires_reloading? + true + end + + # Returns true + def supports_add_column? + true + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + super + clear_cache! + @connection.close rescue nil + end + + # Clears the prepared statements cache. + def clear_cache! + @statements.clear + end + + # Returns true + def supports_count_distinct? #:nodoc: + true + end + + # Returns true + def supports_autoincrement? #:nodoc: + true + end + + def supports_index_sort_order? + true + end + + def native_database_types #:nodoc: + { + :primary_key => default_primary_key_type, + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + # Returns the current database encoding format as a string, eg: 'UTF-8' + def encoding + @connection.encoding.to_s + end + + # Returns true. + def supports_explain? + true + end + + + # QUOTING ================================================== + def quote(value, column = nil) if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] @@ -45,10 +224,387 @@ module ActiveRecord end end - # Returns the current database encoding format as a string, eg: 'UTF-8' - def encoding - @connection.encoding.to_s + + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name.to_s.gsub('"', '""')}") + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value + + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger + value.encode! 'utf-8' + end + value + end + + # DATABASE STATEMENTS ====================================== + + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a 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) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + + def exec_query(sql, name = nil, binds = []) + log(sql, name, binds) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + type_cast(val, col) + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes end + alias :exec_update :exec_delete + + def last_inserted_id(result) + @connection.last_insert_row_id + end + + def execute(sql, name = nil) #:nodoc: + 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) + exec_query(sql, name).rows + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def begin_db_transaction #:nodoc: + log('begin transaction',nil) { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + log('commit transaction',nil) { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + log('rollback transaction',nil) { @connection.rollback } + end + + # SCHEMA STATEMENTS ======================================== + + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name + + exec_query(sql, name).map do |row| + row['name'] + end + end + + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + + # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + def columns(table_name) #:nodoc: + table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub('""', '"') + end + + SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + end + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + IndexDefinition.new( + table_name, + row['name'], + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')").map { |col| + col['name'] + }) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find { |field| + field['pk'] == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + exec_query "DROP INDEX #{quote_column_name(index_name)}" + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + end + + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds) + end + + def table_structure(table_name) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? + structure + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, + :null => column.null) + end + @definition.primary_key(primary_key(from)) if primary_key(from) + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + to_column_names = columns(to).map { |c| c.name } + columns = index.columns.map {|c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[columns.map {|name| [name, name]}] + rename.each { |a| column_mappings[a.last] = a.first } + from_columns = columns(from).collect {|col| col.name} + columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + + quoted_to = quote_table_name(to) + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| + sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + exec_query sql + end + end + + def sqlite_version + @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + end + + def default_primary_key_type + if supports_autoincrement? + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' + else + 'INTEGER PRIMARY KEY NOT NULL' + end + end + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + RecordNotUnique.new(message, exception) + else + super + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb deleted file mode 100644 index 69750a911d..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ /dev/null @@ -1,569 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/string/encoding' - -module ActiveRecord - module ConnectionAdapters #:nodoc: - class SQLiteColumn < Column #:nodoc: - class << self - def string_to_binary(value) - value.gsub(/\0|\%/n) do |b| - case b - when "\0" then "%00" - when "%" then "%25" - end - end - end - - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - - value.gsub(/%00|%25/n) do |b| - case b - when "%00" then "\0" - when "%25" then "%" - end - end - end - end - end - - # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby - # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/). - # - # Options: - # - # * <tt>:database</tt> - Path to the database file. - class SQLiteAdapter < AbstractAdapter - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } - end - - def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } - end - end - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.values.each do |hash| - dealloc hash[:stmt] - end - cache.clear - end - - private - def cache - @cache[$$] - end - - def dealloc(stmt) - stmt.close unless stmt.closed? - end - end - - def initialize(connection, logger, config) - super(connection, logger) - @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) - @config = config - @visitor = Arel::Visitors::SQLite.new self - end - - def adapter_name #:nodoc: - 'SQLite' - end - - # Returns true if SQLite version is '2.0.0' or greater, false otherwise. - def supports_ddl_transactions? - sqlite_version >= '2.0.0' - end - - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. - def supports_savepoints? - sqlite_version >= '3.6.8' - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - # Returns true. - def supports_primary_key? #:nodoc: - true - end - - # Returns true. - def supports_explain? - true - end - - def requires_reloading? - true - end - - # Returns true if SQLite version is '3.1.6' or greater, false otherwise. - def supports_add_column? - sqlite_version >= '3.1.6' - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - clear_cache! - @connection.close rescue nil - end - - # Clears the prepared statements cache. - def clear_cache! - @statements.clear - end - - # Returns true if SQLite version is '3.2.6' or greater, false otherwise. - def supports_count_distinct? #:nodoc: - sqlite_version >= '3.2.6' - end - - # Returns true if SQLite version is '3.1.0' or greater, false otherwise. - def supports_autoincrement? #:nodoc: - sqlite_version >= '3.1.0' - end - - def supports_index_sort_order? - sqlite_version >= '3.3.0' - end - - def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } - end - - - # QUOTING ================================================== - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a 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) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do - - # Don't cache statements without bind values - if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close - stmt = records - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } - end - - ActiveRecord::Result.new(cols, stmt.to_a) - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @connection.changes - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_insert_row_id - end - - def execute(sql, name = nil) #:nodoc: - 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) - exec_query(sql, name).rows - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - def begin_db_transaction #:nodoc: - log('begin transaction',nil) { @connection.transaction } - end - - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } - end - - def rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } - end - - # SCHEMA STATEMENTS ======================================== - - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, name).map do |row| - row['name'] - end - end - - def table_exists?(name) - name && tables('SCHEMA', name).any? - end - - # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name, name = nil) #:nodoc: - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/ - field["dflt_value"] = $1.gsub(/''/, "'") - when /^"(.*)"$/ - field["dflt_value"] = $1.gsub(/""/, '"') - end - - SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - IndexDefinition.new( - table_name, - row['name'], - row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')").map { |col| - col['name'] - }) - end - end - - def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] - end - - def remove_index!(table_name, index_name) #:nodoc: - exec_query "DROP INDEX #{quote_column_name(index_name)}" - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - end - - # See: http://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) - type.to_sym != :primary_key - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) - super(table_name, column_name, type, options) - else - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end - end - - def remove_column(table_name, *column_names) #:nodoc: - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end - end - alias :remove_columns :remove_column - - def change_column_default(table_name, column_name, default) #:nodoc: - alter_table(table_name) do |definition| - definition[column_name].default = default - end - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - alter_table(table_name) do |definition| - definition[column_name].null = null - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - alter_table(table_name) do |definition| - include_default = options_include_default?(options) - definition[column_name].instance_eval do - self.type = type - self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default - self.null = options[:null] if options.include?(:null) - self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) - end - end - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - end - - def empty_insert_statement_value - "VALUES(NULL)" - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds).to_a - end - - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure - end - - def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, - options.merge(:temporary => true)) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) - create_table(to, options) do |definition| - @definition = definition - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :precision => column.precision, :scale => column.scale, - :null => column.null) - end - @definition.primary_key(primary_key(from)) if primary_key(from) - yield @definition if block_given? - end - - copy_table_indexes(from, to, options[:rename] || {}) - copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, - options[:rename] || {}) - end - - def copy_table_indexes(from, to, rename = {}) #:nodoc: - indexes(from).each do |index| - name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - to_column_names = columns(to).map { |c| c.name } - columns = index.columns.map {|c| rename[c] || c }.select do |column| - to_column_names.include?(column) - end - - unless columns.empty? - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, columns, opts) - end - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] - rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} - quoted_columns = columns.map { |col| quote_column_name(col) } * ',' - - quoted_to = quote_table_name(to) - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' - sql << ')' - exec_query sql - end - end - - def sqlite_version - @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) - end - - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - - def translate_exception(exception, message) - case exception.message - when /column(s)? .* (is|are) not unique/ - RecordNotUnique.new(message, exception) - else - super - end - end - - end - end -end |