diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2005-09-25 17:56:03 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2005-09-25 17:56:03 +0000 |
commit | b3df95985a449fd155868b4ec04a556530a03e6c (patch) | |
tree | d5ebffef075e2dc6680c4a81adc6ad237835fbfa /activerecord/lib/active_record/connection_adapters/abstract | |
parent | ea654654226924f9b900e7981fdbdbd452ca15d8 (diff) | |
download | rails-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')
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 |