aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-09-25 17:56:03 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-09-25 17:56:03 +0000
commitb3df95985a449fd155868b4ec04a556530a03e6c (patch)
treed5ebffef075e2dc6680c4a81adc6ad237835fbfa /activerecord/lib/active_record/connection_adapters/abstract
parentea654654226924f9b900e7981fdbdbd452ca15d8 (diff)
downloadrails-b3df95985a449fd155868b4ec04a556530a03e6c.tar.gz
rails-b3df95985a449fd155868b4ec04a556530a03e6c.tar.bz2
rails-b3df95985a449fd155868b4ec04a556530a03e6c.zip
Refactored the AbstractAdapter to be a lot less scary. Cleaned up the docs and style for the OSS adapters
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2339 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract')
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb126
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb168
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb137
5 files changed, 546 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
new file mode 100644
index 0000000000..8b94d68717
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ class Base
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+ def initialize (config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+ end
+
+ # The class -> [adapter_method, config] map
+ @@defined_connections = {}
+
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the :adapter key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :dbfile => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from yaml for example):
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "dbfile" => "path/to/dbfile"
+ # )
+ #
+ # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # may be returned on an error.
+ def self.establish_connection(spec = nil)
+ case spec
+ when nil
+ raise AdapterNotSpecified unless defined? RAILS_ENV
+ establish_connection(RAILS_ENV)
+ when ConnectionSpecification
+ @@defined_connections[self] = spec
+ when Symbol, String
+ if configuration = configurations[spec.to_s]
+ establish_connection(configuration)
+ else
+ raise AdapterNotSpecified, "#{spec} database is not configured"
+ end
+ else
+ spec = spec.symbolize_keys
+ unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
+ adapter_method = "#{spec[:adapter]}_connection"
+ unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
+ remove_connection
+ establish_connection(ConnectionSpecification.new(spec, adapter_method))
+ end
+ end
+
+ def self.active_connections #:nodoc:
+ if threaded_connections
+ Thread.current['active_connections'] ||= {}
+ else
+ @@active_connections ||= {}
+ end
+ end
+
+ # Locate the connection of the nearest super class. This can be an
+ # active or defined connections: if it is the latter, it will be
+ # opened and set as the active connection for the class it was defined
+ # for (not necessarily the current class).
+ def self.retrieve_connection #:nodoc:
+ klass = self
+ ar_super = ActiveRecord::Base.superclass
+ until klass == ar_super
+ if conn = active_connections[klass]
+ return conn
+ elsif conn = @@defined_connections[klass]
+ klass.connection = conn
+ return self.connection
+ end
+ klass = klass.superclass
+ end
+ raise ConnectionNotEstablished
+ end
+
+ # Returns true if a connection that's accessible to this class have already been opened.
+ def self.connected?
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ if active_connections[klass]
+ return true
+ else
+ klass = klass.superclass
+ end
+ end
+ return false
+ end
+
+ # Remove the connection for this class. This will close the active
+ # connection and the defined connection (if they exist). The result
+ # can be used as argument for establish_connection, for easy
+ # re-establishing of the connection.
+ def self.remove_connection(klass=self)
+ conn = @@defined_connections[klass]
+ @@defined_connections.delete(klass)
+ active_connections[klass] = nil
+ conn.config if conn
+ end
+
+ # Set the connection for the class.
+ def self.connection=(spec)
+ raise ConnectionNotEstablished unless spec
+ conn = self.send(spec.adapter_method, spec.config)
+ active_connections[self] = conn
+ end
+
+ # Converts all strings in a hash to symbols.
+ def self.symbolize_strings_in_hash(hash) #:nodoc:
+ hash.symbolize_keys
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
new file mode 100644
index 0000000000..1e8dd045f6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -0,0 +1,73 @@
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ # TODO: Document me!
+ module DatabaseStatements
+ # Returns an array of record hashes with the column names as a keys and fields as values.
+ def select_all(sql, name = nil) end
+
+ # Returns a record hash with the column names as a keys and fields as values.
+ def select_one(sql, name = nil) end
+
+ # Returns a single value from a record
+ def select_value(sql, name = nil)
+ result = select_one(sql, name)
+ result.nil? ? nil : result.values.first
+ end
+
+ # Returns an array of the values of the first column in a select:
+ # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
+ def select_values(sql, name = nil)
+ result = select_all(sql, name)
+ result.map{ |v| v.values.first }
+ end
+
+ # Executes the statement
+ def execute(sql, name = nil) end
+
+ # Returns the last auto-generated ID from the affected table.
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) end
+
+ # Executes the update statement and returns the number of rows affected.
+ def update(sql, name = nil) end
+
+ # Executes the delete statement and returns the number of rows affected.
+ def delete(sql, name = nil) end
+
+ # Wrap a block in a transaction. Returns result of block.
+ def transaction(start_db_transaction = true)
+ begin
+ if block_given?
+ begin_db_transaction if start_db_transaction
+ result = yield
+ commit_db_transaction if start_db_transaction
+ result
+ end
+ rescue Exception => database_transaction_rollback
+ rollback_db_transaction if start_db_transaction
+ raise
+ end
+ end
+
+ # Begins the transaction (and turns off auto-committing).
+ def begin_db_transaction() end
+
+ # Commits the transaction (and turns on auto-committing).
+ def commit_db_transaction() end
+
+ # Rolls back the transaction (and turns on auto-committing). Must be done if the transaction block
+ # raises an exception or returns false.
+ def rollback_db_transaction() end
+
+ def add_limit!(sql, options) #:nodoc:
+ return unless options
+ add_limit_offset!(sql, options)
+ end
+
+ def add_limit_offset!(sql, options) #:nodoc:
+ return if options[:limit].nil?
+ sql << " LIMIT #{options[:limit]}"
+ sql << " OFFSET #{options[:offset]}" if options.has_key?(:offset) and !options[:offset].nil?
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
new file mode 100644
index 0000000000..7f7cc03c7a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -0,0 +1,42 @@
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ # TODO: Document me!
+ module Quoting
+ def quote(value, column = nil)
+ case value
+ when String
+ if column && column.type == :binary
+ "'#{quote_string(column.string_to_binary(value))}'" # ' (for ruby-mode)
+ elsif column && [:integer, :float].include?(column.type)
+ value.to_s
+ else
+ "'#{quote_string(value)}'" # ' (for ruby-mode)
+ end
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? quoted_true : "1")
+ when FalseClass then (column && column.type == :boolean ? quoted_false : "0")
+ when Float, Fixnum, Bignum then value.to_s
+ when Date then "'#{value.to_s}'"
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def quote_column_name(name)
+ name
+ end
+
+ def quoted_true
+ "'t'"
+ end
+
+ def quoted_false
+ "'f'"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
new file mode 100644
index 0000000000..f90ac0266a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -0,0 +1,168 @@
+module ActiveRecord
+ module ConnectionAdapters #:nodoc:
+ class Column #:nodoc:
+ attr_reader :name, :default, :type, :limit, :null
+ # The name should contain the name of the column, such as "name" in "name varchar(250)"
+ # The default should contain the type-casted default of the column, such as 1 in "count int(11) DEFAULT 1"
+ # The type parameter should either contain :integer, :float, :datetime, :date, :text, or :string
+ # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)"
+ def initialize(name, default, sql_type = nil, null = true)
+ @name, @type, @null = name, simplified_type(sql_type), null
+ # have to do this one separately because type_cast depends on #type
+ @default = type_cast(default)
+ @limit = extract_limit(sql_type) unless sql_type.nil?
+ end
+
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :datetime then Time
+ when :date then Date
+ when :timestamp then Time
+ when :time then Time
+ when :text, :string then String
+ when :binary then String
+ when :boolean then Object
+ end
+ end
+
+ def type_cast(value)
+ if value.nil? then return nil end
+ case type
+ when :string then value
+ when :text then value
+ when :integer then value.to_i rescue value ? 1 : 0
+ when :float then value.to_f
+ when :datetime then string_to_time(value)
+ when :timestamp then string_to_time(value)
+ when :time then string_to_dummy_time(value)
+ when :date then string_to_date(value)
+ when :binary then binary_to_string(value)
+ when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
+ else value
+ end
+ end
+
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ def string_to_binary(value)
+ value
+ end
+
+ def binary_to_string(value)
+ value
+ end
+
+ private
+ def string_to_date(string)
+ return string unless string.is_a?(String)
+ date_array = ParseDate.parsedate(string.to_s)
+ # treat 0000-00-00 as nil
+ Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
+ end
+
+ def string_to_time(string)
+ return string unless string.is_a?(String)
+ time_array = ParseDate.parsedate(string.to_s).compact
+ # treat 0000-00-00 00:00:00 as nil
+ Time.send(Base.default_timezone, *time_array) rescue nil
+ end
+
+ def string_to_dummy_time(string)
+ return string unless string.is_a?(String)
+ time_array = ParseDate.parsedate(string.to_s)
+ # pad the resulting array with dummy date information
+ time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
+ Time.send(Base.default_timezone, *time_array) rescue nil
+ end
+
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double|decimal|numeric/i
+ :float
+ when /datetime/i
+ :datetime
+ when /timestamp/i
+ :timestamp
+ when /time/i
+ :time
+ when /date/i
+ :date
+ when /clob/i, /text/i
+ :text
+ when /blob/i, /binary/i
+ :binary
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
+ end
+
+ class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) #:nodoc:
+ def to_sql
+ column_sql = "#{name} #{type_to_sql(type.to_sym, limit)}"
+ add_column_options!(column_sql, :null => null, :default => default)
+ column_sql
+ end
+ alias to_s :to_sql
+
+ private
+ def type_to_sql(name, limit)
+ base.type_to_sql(name, limit) rescue name
+ end
+
+ def add_column_options!(sql, options)
+ base.add_column_options!(sql, options.merge(:column => self))
+ end
+ end
+
+ class TableDefinition #:nodoc:
+ attr_accessor :columns
+
+ def initialize(base)
+ @columns = []
+ @base = base
+ end
+
+ def primary_key(name)
+ column(name, native[:primary_key])
+ end
+
+ def [](name)
+ @columns.find {|column| column.name == name}
+ end
+
+ def column(name, type, options = {})
+ column = self[name] || ColumnDefinition.new(@base, name, type)
+ column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
+ column.default = options[:default]
+ column.null = options[:null]
+ @columns << column unless @columns.include? column
+ self
+ end
+
+ def to_sql
+ @columns * ', '
+ end
+
+ private
+ def native
+ @base.native_database_types
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
new file mode 100644
index 0000000000..2ce1380f96
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -0,0 +1,137 @@
+module ActiveRecord
+ module ConnectionAdapters # :nodoc:
+ # TODO: Document me!
+ module SchemaStatements
+ def native_database_types #:nodoc:
+ {}
+ end
+
+ # def tables(name = nil) end
+
+ # Returns an array of indexes for the given table.
+ # def indexes(table_name, name = nil) end
+
+ # Returns an array of column objects for the table specified by +table_name+.
+ def columns(table_name, name = nil) end
+
+
+ def create_table(name, options = {})
+ table_definition = TableDefinition.new(self)
+ table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
+
+ yield table_definition
+ create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
+ create_sql << "#{name} ("
+ create_sql << table_definition.to_sql
+ create_sql << ") #{options[:options]}"
+ execute create_sql
+ end
+
+ def drop_table(name)
+ execute "DROP TABLE #{name}"
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}"
+ add_column_options!(add_column_sql, options)
+ execute(add_column_sql)
+ end
+
+ def remove_column(table_name, column_name)
+ execute "ALTER TABLE #{table_name} DROP #{column_name}"
+ end
+
+ def change_column(table_name, column_name, type, options = {})
+ raise NotImplementedError, "change_column is not implemented"
+ end
+
+ def change_column_default(table_name, column_name, default)
+ raise NotImplementedError, "change_column_default is not implemented"
+ end
+
+ def rename_column(table_name, column_name, new_column_name)
+ raise NotImplementedError, "rename_column is not implemented"
+ end
+
+ # Create a new index on the given table. By default, it will be named
+ # <code>"#{table_name}_#{column_name.to_a.first}_index"</code>, but you
+ # can explicitly name the index by passing <code>:name => "..."</code>
+ # as the last parameter. Unique indexes may be created by passing
+ # <code>:unique => true</code>.
+ def add_index(table_name, column_name, options = {})
+ index_name = "#{table_name}_#{column_name.to_a.first}_index"
+
+ if Hash === options # legacy support, since this param was a string
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_name = options[:name] || index_name
+ else
+ index_type = options
+ end
+
+ execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{column_name.to_a.join(", ")})"
+ end
+
+ # Remove the given index from the table.
+ #
+ # remove_index :my_table, :column => :foo
+ # remove_index :my_table, :name => :my_index_on_foo
+ #
+ # The first version will remove the index named
+ # <code>"#{my_table}_#{column}_index"</code> from the table. The
+ # second removes the named column from the table.
+ def remove_index(table_name, options = {})
+ if Hash === options # legacy support
+ if options[:column]
+ index_name = "#{table_name}_#{options[:column]}_index"
+ elsif options[:name]
+ index_name = options[:name]
+ else
+ raise ArgumentError, "You must specify the index name"
+ end
+ else
+ index_name = "#{table_name}_#{options}_index"
+ end
+
+ execute "DROP INDEX #{index_name} ON #{table_name}"
+ end
+
+
+ # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
+ def structure_dump #:nodoc:
+ end
+
+ def initialize_schema_information #:nodoc:
+ begin
+ execute "CREATE TABLE schema_info (version #{type_to_sql(:integer)})"
+ execute "INSERT INTO schema_info (version) VALUES(0)"
+ rescue ActiveRecord::StatementInvalid
+ # Schema has been intialized
+ end
+ end
+
+ def dump_schema_information #:nodoc:
+ begin
+ if (current_schema = ActiveRecord::Migrator.current_version) > 0
+ return "INSERT INTO schema_info (version) VALUES (#{current_schema});"
+ end
+ rescue ActiveRecord::StatementInvalid
+ # No Schema Info
+ end
+ end
+
+
+ def type_to_sql(type, limit = nil) #:nodoc:
+ native = native_database_types[type]
+ limit ||= native[:limit]
+ column_type_sql = native[:name]
+ column_type_sql << "(#{limit})" if limit
+ column_type_sql
+ end
+
+ def add_column_options!(sql, options) #:nodoc:
+ sql << " NOT NULL" if options[:null] == false
+ sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil?
+ end
+ end
+ end
+end \ No newline at end of file