From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../connection_adapters/abstract_adapter.rb | 371 +++++++++++++++++++++ .../connection_adapters/mysql_adapter.rb | 131 ++++++++ .../connection_adapters/postgresql_adapter.rb | 170 ++++++++++ .../connection_adapters/sqlite_adapter.rb | 105 ++++++ .../connection_adapters/sqlserver_adapter.rb | 298 +++++++++++++++++ 5 files changed, 1075 insertions(+) create mode 100755 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb create mode 100755 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb (limited to 'activerecord/lib/active_record/connection_adapters') diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb new file mode 100755 index 0000000000..54fdfd25cd --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -0,0 +1,371 @@ +require 'benchmark' +require 'date' + +# Method that requires a library, ensuring that rubygems is loaded +# This is used in the database adaptors to require DB drivers. Reasons: +# (1) database drivers are the only third-party library that Rails depend upon +# (2) they are often installed as gems +def require_library_or_gem(library_name) + begin + require library_name + rescue LoadError => cannot_require + # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try. + begin + require 'rubygems' + rescue LoadError => rubygems_not_installed + raise cannot_require + end + # 2. Rubygems is installed and loaded. Try to load the library again + begin + require library_name + rescue LoadError => gem_not_installed + raise cannot_require + end + end +end + +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. + # + # == Connecting to another database for a single model + # + # To support different connections for different classes, you can + # simply call establish_connection with the classes you wish to have + # different connections for: + # + # class Courses < ActiveRecord::Base + # ... + # end + # + # Courses.establish_connection( ... ) + def self.establish_connection(spec) + if spec.instance_of? ConnectionSpecification + @@defined_connections[self] = spec + elsif spec.is_a?(Symbol) + establish_connection(configurations[spec.to_s]) + else + if spec.nil? then raise AdapterNotSpecified end + symbolize_strings_in_hash(spec) + unless spec.key?(:adapter) then raise AdapterNotSpecified end + + adapter_method = "#{spec[:adapter]}_connection" + unless methods.include?(adapter_method) then raise AdapterNotFound end + remove_connection + @@defined_connections[self] = ConnectionSpecification.new(spec, adapter_method) + 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 + until klass == ActiveRecord::Base.superclass + Thread.current['active_connections'] ||= {} + if Thread.current['active_connections'][klass] + return Thread.current['active_connections'][klass] + elsif @@defined_connections[klass] + klass.connection = @@defined_connections[klass] + 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 Thread.current['active_connections'].is_a?(Hash) && Thread.current['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) + Thread.current['active_connections'] ||= {} + Thread.current['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) + Thread.current['active_connections'] ||= {} + Thread.current['active_connections'][self] = conn + end + + # Converts all strings in a hash to symbols. + def self.symbolize_strings_in_hash(hash) + hash.each do |key, value| + if key.class == String + hash.delete key + hash[key.intern] = value + end + end + end + end + + module ConnectionAdapters # :nodoc: + class Column # :nodoc: + attr_reader :name, :default, :type, :limit + # 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) + @name, @default, @type = name, default, simplified_type(sql_type) + @limit = extract_limit(sql_type) unless sql_type.nil? + end + + def default + type_cast(@default) + end + + def klass + case type + when :integer then Fixnum + when :float then Float + when :datetime then Time + when :date then Date + when :text, :string 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 + when :float then value.to_f + when :datetime then string_to_time(value) + when :date then string_to_date(value) + when :boolean then (value == "t" or value == true ? true : false) + else value + end + end + + def human_name + Base.human_attribute_name(@name) + end + + private + def string_to_date(string) + return string if Date === string + date_array = ParseDate.parsedate(string) + # 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 if Time === string + time_array = ParseDate.parsedate(string).compact + # treat 0000-00-00 00:00:00 as nil + Time.local(*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 /time/i + :datetime + when /date/i + :date + when /(c|b)lob/i, /text/i + :text + when /char/i, /string/i + :string + when /boolean/i + :boolean + end + end + end + + # All the concrete database adapters follow the interface laid down in this class. + # You can use this interface directly by borrowing the database connection from the Base with + # Base.connection. + class AbstractAdapter + @@row_even = true + + include Benchmark + + def initialize(connection, logger = nil) # :nodoc: + @connection, @logger = connection, logger + @runtime = 0 + end + + # 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 an array of column objects for the table specified by +table_name+. + def columns(table_name, name = nil) end + + # Returns the last auto-generated ID from the affected table. + def insert(sql, name = nil, pk = nil, id_value = nil) end + + # Executes the update statement. + def update(sql, name = nil) end + + # Executes the delete statement. + def delete(sql, name = nil) end + + def reset_runtime # :nodoc: + rt = @runtime + @runtime = 0 + return rt + end + + # Wrap a block in a transaction. Returns result of block. + def transaction + begin + if block_given? + begin_db_transaction + result = yield + commit_db_transaction + result + end + rescue Exception => database_transaction_rollback + rollback_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 + + # Rollsback 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 quote(value, column = nil) + case value + when String then "'#{quote_string(value)}'" # ' (for ruby-mode) + when NilClass then "NULL" + when TrueClass then (column && column.type == :boolean ? "'t'" : "1") + when FalseClass then (column && column.type == :boolean ? "'f'" : "0") + when Float, Fixnum, Bignum, 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) + return name + end + + # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database. + def structure_dump() end + + protected + def log(sql, name, connection, &action) + begin + if @logger.nil? + action.call(connection) + else + result = nil + bm = measure { result = action.call(connection) } + @runtime += bm.real + log_info(sql, name, bm.real) + result + end + rescue => e + log_info("#{e.message}: #{sql}", name, 0) + raise ActiveRecord::StatementInvalid, "#{e.message}: #{sql}" + end + end + + def log_info(sql, name, runtime) + if @logger.nil? then return end + + @logger.info( + format_log_entry( + "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})", + sql.gsub(/ +/, " ") + ) + ) + end + + def format_log_entry(message, dump = nil) + if @@row_even then + @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37" + else + @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37" + end + + log_entry = " \e[#{message_color}m#{message}\e[m" + log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil? + log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil? + log_entry + end + end + + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb new file mode 100755 index 0000000000..5dcdded5bc --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -0,0 +1,131 @@ +require 'active_record/connection_adapters/abstract_adapter' +require 'parsedate' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.mysql_connection(config) # :nodoc: + unless self.class.const_defined?(:Mysql) + begin + # Only include the MySQL driver if one hasn't already been loaded + require_library_or_gem 'mysql' + rescue LoadError => cannot_require_mysql + # Only use the supplied backup Ruby/MySQL driver if no driver is already in place + begin + require 'active_record/vendor/mysql' + rescue LoadError + raise cannot_require_mysql + end + end + end + symbolize_strings_in_hash(config) + host = config[:host] + port = config[:port] + socket = config[:socket] + username = config[:username] ? config[:username].to_s : 'root' + password = config[:password].to_s + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + ConnectionAdapters::MysqlAdapter.new( + Mysql::real_connect(host, username, password, database, port, socket), logger + ) + end + end + + module ConnectionAdapters + class MysqlAdapter < AbstractAdapter # :nodoc: + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + sql = "SHOW FIELDS FROM #{table_name}" + result = nil + log(sql, name, @connection) { |connection| result = connection.query(sql) } + + columns = [] + result.each { |field| columns << Column.new(field[0], field[4], field[1]) } + columns + end + + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + return id_value || @connection.insert_id + end + + def execute(sql, name = nil) + log(sql, name, @connection) { |connection| connection.query(sql) } + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction + begin + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + end + + def commit_db_transaction + begin + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + end + + def rollback_db_transaction + begin + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + end + + def quote_column_name(name) + return "`#{name}`" + end + + def structure_dump + select_all("SHOW TABLES").inject("") do |structure, table| + structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name) + drop_database(name) + create_database(name) + end + + def drop_database(name) + execute "DROP DATABASE IF EXISTS #{name}" + end + + def create_database(name) + execute "CREATE DATABASE #{name}" + end + + private + def select(sql, name = nil) + result = nil + log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) } + rows = [] + all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields } + result.each_hash { |row| rows << all_fields_initialized.dup.update(row) } + rows + 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 new file mode 100644 index 0000000000..fb54642d3a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -0,0 +1,170 @@ + +# postgresql_adaptor.rb +# author: Luke Holden +# notes: Currently this adaptor does not pass the test_zero_date_fields +# and test_zero_datetime_fields unit tests in the BasicsTest test +# group. +# +# This is due to the fact that, in postgresql you can not have a +# totally zero timestamp. Instead null/nil should be used to +# represent no value. +# + +require 'active_record/connection_adapters/abstract_adapter' +require 'parsedate' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.postgresql_connection(config) # :nodoc: + require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn) + symbolize_strings_in_hash(config) + host = config[:host] + port = config[:port] || 5432 unless host.nil? + username = config[:username].to_s + password = config[:password].to_s + + if config.has_key?(:database) + database = config[:database] + else + raise ArgumentError, "No database specified. Missing argument: database." + end + + ConnectionAdapters::PostgreSQLAdapter.new( + PGconn.connect(host, port, "", "", database, username, password), logger + ) + end + end + + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter # :nodoc: + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + table_structure(table_name).inject([]) do |columns, field| + columns << Column.new(field[0], field[2], field[1]) + columns + end + end + + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + table = sql.split(" ", 4)[2] + return id_value || last_insert_id(table, pk) + end + + def execute(sql, name = nil) + log(sql, name, @connection) { |connection| connection.query(sql) } + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction() execute "BEGIN" end + def commit_db_transaction() execute "COMMIT" end + def rollback_db_transaction() execute "ROLLBACK" end + + def quote_column_name(name) + return "\"#{name}\"" + end + + private + def last_insert_id(table, column = "id") + sequence_name = "#{table}_#{column || 'id'}_seq" + @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i + end + + def select(sql, name = nil) + res = nil + log(sql, name, @connection) { |connection| res = connection.exec(sql) } + + results = res.result + rows = [] + if results.length > 0 + fields = res.fields + results.each do |row| + hashed_row = {} + row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] } + rows << hashed_row + end + end + return rows + end + + def split_table_schema(table_name) + schema_split = table_name.split('.') + schema_name = "public" + if schema_split.length > 1 + schema_name = schema_split.first.strip + table_name = schema_split.last.strip + end + return [schema_name, table_name] + end + + def table_structure(table_name) + database_name = @connection.db + schema_name, table_name = split_table_schema(table_name) + + # Grab a list of all the default values for the columns. + sql = "SELECT column_name, column_default, character_maximum_length, data_type " + sql << " FROM information_schema.columns " + sql << " WHERE table_catalog = '#{database_name}' " + sql << " AND table_schema = '#{schema_name}' " + sql << " AND table_name = '#{table_name}';" + + column_defaults = nil + log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) } + column_defaults.collect do |row| + field = row[0] + type = type_as_string(row[3], row[2]) + default = default_value(row[1]) + length = row[2] + + [field, type, default, length] + end + end + + def type_as_string(field_type, field_length) + type = case field_type + when 'numeric', 'real', 'money' then 'float' + when 'character varying', 'interval' then 'string' + when 'timestamp without time zone' then 'datetime' + else field_type + end + + size = field_length.nil? ? "" : "(#{field_length})" + + return type + size + end + + def default_value(value) + # Boolean types + return "t" if value =~ /true/i + return "f" if value =~ /false/i + + # Char/String type values + return $1 if value =~ /^'(.*)'::(bpchar|text|character varying)$/ + + # Numeric values + return value if value =~ /^[0-9]+(\.[0-9]*)?/ + + # Date / Time magic values + return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/ + + # Fixed dates / times + return $1 if value =~ /^'(.+)'::(date|timestamp)/ + + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + return nil + 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 new file mode 100644 index 0000000000..1f3845e6a8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -0,0 +1,105 @@ +# sqlite_adapter.rb +# author: Luke Holden + +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects + def self.sqlite_connection(config) # :nodoc: + require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite) + symbolize_strings_in_hash(config) + unless config.has_key?(:dbfile) + raise ArgumentError, "No database file specified. Missing argument: dbfile" + end + + db = SQLite::Database.new(config[:dbfile], 0) + + db.show_datatypes = "ON" if !defined? SQLite::Version + db.results_as_hash = true if defined? SQLite::Version + db.type_translation = false + + ConnectionAdapters::SQLiteAdapter.new(db, logger) + end + end + + module ConnectionAdapters + class SQLiteAdapter < AbstractAdapter # :nodoc: + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + table_structure(table_name).inject([]) do |columns, field| + columns << Column.new(field['name'], field['dflt_value'], field['type']) + columns + end + end + + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid ) + end + + def execute(sql, name = nil) + log(sql, name, @connection) do |connection| + if defined?( SQLite::Version ) + case sql + when "BEGIN" then connection.transaction + when "COMMIT" then connection.commit + when "ROLLBACK" then connection.rollback + else connection.execute(sql) + end + else + connection.execute( sql ) + end + end + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction() execute "BEGIN" end + def commit_db_transaction() execute "COMMIT" end + def rollback_db_transaction() execute "ROLLBACK" end + + def quote_string(s) + SQLite::Database.quote(s) + end + + def quote_column_name(name) + return "'#{name}'" + end + + private + def select(sql, name = nil) + results = nil + log(sql, name, @connection) { |connection| results = connection.execute(sql) } + + rows = [] + + results.each do |row| + hash_only_row = {} + row.each_key do |key| + hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum + end + rows << hash_only_row + end + + return rows + end + + def table_structure(table_name) + sql = "PRAGMA table_info(#{table_name});" + results = nil + log(sql, nil, @connection) { |connection| results = connection.execute(sql) } + return results + end + end + end +end \ No newline at end of file diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb new file mode 100644 index 0000000000..5cd5f5a0be --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -0,0 +1,298 @@ +require 'active_record/connection_adapters/abstract_adapter' + +# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server +# +# Author: Joey Gibson +# Date: 10/14/2004 +# +# REQUIREMENTS: +# +# This adapter will ONLY work on Windows systems, since it relies on Win32OLE, which, +# to my knowledge, is only available on Window. +# +# It relies on the ADO support in the DBI module. If you are using the +# one-click installer of Ruby, then you already have DBI installed, but +# the ADO module is *NOT* installed. You will need to get the latest +# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/ +# unzip it, and copy the file src/lib/dbd_ado/ADO.rb to +# X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb (you will need to create +# the ADO directory). Once you've installed that file, you are ready to go. +# +# This module uses the ADO-style DSNs for connection. For example: +# "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;" +# with User Id replaced with your proper login, and Password with your +# password. +# +# I have tested this code on a WindowsXP Pro SP1 system, +# ruby 1.8.2 (2004-07-29) [i386-mswin32], SQL Server 2000. +# +module ActiveRecord + class Base + def self.sqlserver_connection(config) + require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI) + class_eval { include ActiveRecord::SQLServerBaseExtensions } + + symbolize_strings_in_hash(config) + + if config.has_key? :dsn + dsn = config[:dsn] + else + raise ArgumentError, "No DSN specified" + end + + conn = DBI.connect(dsn) + conn["AutoCommit"] = true + + ConnectionAdapters::SQLServerAdapter.new(conn, logger) + end + end + + module SQLServerBaseExtensions #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods + def find_first(conditions = nil, orderings = nil) + sql = "SELECT TOP 1 * FROM #{table_name} " + add_conditions!(sql, conditions) + sql << "ORDER BY #{orderings} " unless orderings.nil? + + record = connection.select_one(sql, "#{name} Load First") + instantiate(record) unless record.nil? + end + + def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil) + sql = "SELECT " + sql << "TOP #{limit} " unless limit.nil? + sql << " * FROM #{table_name} " + sql << "#{joins} " if joins + add_conditions!(sql, conditions) + sql << "ORDER BY #{orderings} " unless orderings.nil? + + find_by_sql(sql) + end + end + + def attributes_with_quotes + columns_hash = self.class.columns_hash + + attrs = @attributes.dup + + attrs = attrs.reject do |name, value| + columns_hash[name].identity + end + + attrs.inject({}) do |attrs_quoted, pair| + attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first]) + attrs_quoted + end + end + end + + module ConnectionAdapters + class ColumnWithIdentity < Column + attr_reader :identity + + def initialize(name, default, sql_type = nil, is_identity = false) + super(name, default, sql_type) + + @identity = is_identity + end + end + + class SQLServerAdapter < AbstractAdapter # :nodoc: + def quote_column_name(name) + " [#{name}] " + end + + def select_all(sql, name = nil) + select(sql, name) + end + + def select_one(sql, name = nil) + result = select(sql, name) + result.nil? ? nil : result.first + end + + def columns(table_name, name = nil) + sql = < e + # Coulnd't turn on IDENTITY_INSERT + end + end + end + + log(sql, name, @connection) do |conn| + conn.execute(sql) + + select_one("SELECT @@IDENTITY AS Ident")["Ident"] + end + ensure + if ii_enabled + begin + execute enable_identity_insert(table_name, false) + + rescue Exception => e + # Couldn't turn off IDENTITY_INSERT + end + end + end + end + + def execute(sql, name = nil) + if sql =~ /^INSERT/i + insert(sql, name) + else + log(sql, name, @connection) do |conn| + conn.execute(sql) + end + end + end + + alias_method :update, :execute + alias_method :delete, :execute + + def begin_db_transaction + begin + @connection["AutoCommit"] = false + rescue Exception => e + @connection["AutoCommit"] = true + end + end + + def commit_db_transaction + begin + @connection.commit + ensure + @connection["AutoCommit"] = true + end + end + + def rollback_db_transaction + begin + @connection.rollback + ensure + @connection["AutoCommit"] = true + end + end + + def recreate_database(name) + drop_database(name) + create_database(name) + end + + def drop_database(name) + execute "DROP DATABASE #{name}" + end + + def create_database(name) + execute "CREATE DATABASE #{name}" + end + + private + def select(sql, name = nil) + rows = [] + + log(sql, name, @connection) do |conn| + conn.select_all(sql) do |row| + record = {} + + row.column_names.each do |col| + record[col] = row[col] + end + + rows << record + end + end + + rows + end + + def enable_identity_insert(table_name, enable = true) + if has_identity_column(table_name) + "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" + end + end + + def get_table_name(sql) + if sql =~ /into\s*([^\s]+)\s*/i or + sql =~ /update\s*([^\s]+)\s*/i + $1 + else + nil + end + end + + def has_identity_column(table_name) + return get_identity_column(table_name) != nil + end + + def get_identity_column(table_name) + if not @table_columns + @table_columns = {} + end + + if @table_columns[table_name] == nil + @table_columns[table_name] = columns(table_name) + end + + @table_columns[table_name].each do |col| + return col.name if col.identity + end + + return nil + end + + def query_contains_identity_column(sql, col) + return sql =~ /[\(\.\,]\s*#{col}/ + end + end + end +end \ No newline at end of file -- cgit v1.2.3