diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d8b0e2a6ff..1aac586b04 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Add support for FrontBase (http://www.frontbase.com/) with a new adapter thanks to the hard work of one Mike Laster. Closes #4093. [mlaster@metavillage.com] + * Add warning about the proper way to validate the presence of a foreign key. Closes #4147. [Francois Beausoleil ] * Fix syntax error in documentation. Closes #4679. [mislav@nippur.irb.hr] diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 129cdd8a7f..623dd5aaa5 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -27,7 +27,7 @@ task :default => [ :test_mysql, :test_sqlite, :test_postgresql ] # Run the unit tests -for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase ) +for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase frontbase ) Rake::TestTask.new("test_#{adapter}") { |t| t.libs << "test" << "test/connections/native_#{adapter}" t.pattern = "test/*_test{,_#{adapter}}.rb" @@ -71,6 +71,45 @@ end desc 'Rebuild the PostgreSQL test databases' task :rebuild_postgresql_databases => [:drop_postgresql_databases, :build_postgresql_databases] +desc 'Build the FrontBase test databases' +task :build_frontbase_databases => :rebuild_frontbase_databases + +desc 'Rebuild the FrontBase test databases' +task :rebuild_frontbase_databases do + build_frontbase_database = Proc.new do |db_name, sql_definition_file| + %( + STOP DATABASE #{db_name}; + DELETE DATABASE #{db_name}; + CREATE DATABASE #{db_name}; + + CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM; + SET COMMIT FALSE; + + CREATE USER RAILS; + CREATE SCHEMA RAILS AUTHORIZATION RAILS; + COMMIT; + + SET SESSION AUTHORIZATION RAILS; + SCRIPT '#{sql_definition_file}'; + + COMMIT; + + DISCONNECT ALL; + ) + end + create_activerecord_unittest = build_frontbase_database['activerecord_unittest', File.join(SCHEMA_PATH, 'frontbase.sql')] + create_activerecord_unittest2 = build_frontbase_database['activerecord_unittest2', File.join(SCHEMA_PATH, 'frontbase2.sql')] + execute_frontbase_sql = Proc.new do |sql| + system(<<-SHELL) + /Library/FrontBase/bin/sql92 <<-SQL + #{sql} + SQL + SHELL + end + execute_frontbase_sql[create_activerecord_unittest] + execute_frontbase_sql[create_activerecord_unittest2] +end + # Generate the RDoc documentation Rake::RDocTask.new { |rdoc| diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 293c86f13b..d4b08da1f0 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -68,7 +68,7 @@ ActiveRecord::Base.class_eval do end unless defined?(RAILS_CONNECTION_ADAPTERS) - RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase ) + RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase ) end RAILS_CONNECTION_ADAPTERS.each do |adapter| diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c39689c01d..e7fa8434e6 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -755,8 +755,8 @@ module ActiveRecord #:nodoc: superclass == Base || !columns_hash.include?(inheritance_column) end - def quote(object) #:nodoc: - connection.quote(object) + def quote(value, column = nil) #:nodoc: + connection.quote(value,column) end # Used to sanitize objects before they're used in an SELECT SQL-statement. Delegates to connection.quote. @@ -947,7 +947,7 @@ module ActiveRecord #:nodoc: def find_one(id, options) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] - options.update :conditions => "#{table_name}.#{primary_key} = #{sanitize(id)}#{conditions}" + options.update :conditions => "#{table_name}.#{primary_key} = #{quote(id,columns_hash[primary_key])}#{conditions}" if result = find_initial(options) result @@ -958,7 +958,7 @@ module ActiveRecord #:nodoc: def find_some(ids, options) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] - ids_list = ids.map { |id| sanitize(id) }.join(',') + ids_list = ids.map { |id| quote(id,columns_hash[primary_key]) }.join(',') options.update :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}" result = find_every(options) diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 83bf279785..7a25bd4aa1 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -173,8 +173,18 @@ module ActiveRecord add_joins!(sql, options, scope) add_conditions!(sql, options[:conditions], scope) add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) - sql << " GROUP BY #{options[:group_field]} " if options[:group] - sql << " HAVING #{options[:having]} " if options[:group] && options[:having] + sql << " GROUP BY #{options[:group_alias]} " if options[:group] + + if options[:group] && options[:having] + # FrontBase requires identifiers in the HAVING clause and chokes on function calls + if Base.connection.adapter_name == 'FrontBase' + options[:having].downcase! + options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) + end + + sql << " HAVING #{options[:having]} " + end + sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) sql << ')' if use_workaround diff --git a/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb b/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb new file mode 100644 index 0000000000..6fa2ca3ffc --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb @@ -0,0 +1,837 @@ +# Requires FrontBase Ruby bindings from: +# svn://rubyforge.org/var/svn/frontbase-rails/trunk/ruby-frontbase + +require 'active_record/connection_adapters/abstract_adapter' +require 'frontbase' + +FB_TRACE = false + +module ActiveRecord + + class Base + + class << self + # FrontBase only supports one unnamed sequence per table + def set_sequence_name( value=nil, &block ) + end + + # Establishes a connection to the database that's used by all Active Record objects. + def frontbase_connection(config) # :nodoc: + config = config.symbolize_keys + database = config[:database] + port = config[:port] + host = config[:host] + username = config[:username] + password = config[:password] + dbpassword = config[:dbpassword] + session_name = config[:session_name] + + dbpassword = '' if dbpassword.nil? + + # Turn off colorization since it makes tail/less output difficult + self.colorize_logging = false + + + connection = FBSQL_Connect.connect(host, port, database, username, password, dbpassword, session_name) + ConnectionAdapters::FrontBaseAdapter.new(connection, logger, [host, port, database, username, password, dbpassword, session_name], config) + end + end + end + + module ConnectionAdapters + + # From EOF Documentation.... + # buffer should have space for EOUniqueBinaryKeyLength (12) bytes. + # Assigns a world-wide unique ID made up of: + # < Sequence [2], ProcessID [2] , Time [4], IP Addr [4] > + + class TwelveByteKey < String #:nodoc + @@mutex = Mutex.new + @@sequence_number = rand(65536) + @@key_cached_pid_component = nil + @@key_cached_ip_component = nil + + def initialize(string = nil) + # Generate a unique key + if string.nil? + new_key = replace('_' * 12) + + new_key[0..1] = self.class.key_sequence_component + new_key[2..3] = self.class.key_pid_component + new_key[4..7] = self.class.key_time_component + new_key[8..11] = self.class.key_ip_component + new_key + else + if string.size == 24 + string.gsub!(/[[:xdigit:]]{2}/) { |x| x.hex.chr } + end + raise "string is not 12 bytes long" unless string.size == 12 + super(string) + end + end + + def inspect + unpack("H*").first.upcase + end + + alias_method :to_s, :inspect + + private + + class << self + def key_sequence_component + seq = nil + @@mutex.synchronize do + seq = @@sequence_number + @@sequence_number = (@@sequence_number + 1) % 65536 + end + + sequence_component = "__" + sequence_component[0] = seq >> 8 + sequence_component[1] = seq + sequence_component + end + + def key_pid_component + if @@key_cached_pid_component.nil? + @@mutex.synchronize do + pid = $$ + pid_component = "__" + pid_component[0] = pid >> 8 + pid_component[1] = pid + @@key_cached_pid_component = pid_component + end + end + @@key_cached_pid_component + end + + def key_time_component + time = Time.new.to_i + time_component = "____" + time_component[0] = (time & 0xFF000000) >> 24 + time_component[1] = (time & 0x00FF0000) >> 16 + time_component[2] = (time & 0x0000FF00) >> 8 + time_component[3] = (time & 0x000000FF) + time_component + end + + def key_ip_component + if @@key_cached_ip_component.nil? + @@mutex.synchronize do + old_lookup_flag = BasicSocket.do_not_reverse_lookup + BasicSocket.do_not_reverse_lookup = true + udpsocket = UDPSocket.new + udpsocket.connect("17.112.152.32",1) + ip_string = udpsocket.addr[3] + BasicSocket.do_not_reverse_lookup = old_lookup_flag + packed = Socket.pack_sockaddr_in(0,ip_string) + addr_subset = packed[4..7] + ip = addr_subset[0] << 24 | addr_subset[1] << 16 | addr_subset[2] << 8 | addr_subset[3] + ip_component = "____" + ip_component[0] = (ip & 0xFF000000) >> 24 + ip_component[1] = (ip & 0x00FF0000) >> 16 + ip_component[2] = (ip & 0x0000FF00) >> 8 + ip_component[3] = (ip & 0x000000FF) + @@key_cached_ip_component = ip_component + end + end + @@key_cached_ip_component + end + end + end + + class FrontBaseColumn < Column #:nodoc: + attr_reader :fb_autogen + + def initialize(base, name, type, typename, limit, precision, scale, default, nullable) + + @base = base + @name = name + @type = simplified_type(type,typename,limit) + @limit = limit + @precision = precision + @scale = scale + @default = default + @null = nullable == "YES" + @text = [:string, :text].include? @type + @number = [:float, :integer].include? @type + @fb_autogen = false + + if @default + @default.gsub!(/^'(.*)'$/,'\1') if @text + @fb_autogen = @default.include?("SELECT UNIQUE FROM") + case @type + when :boolean + @default = @default == "TRUE" + when :binary + if @default != "X''" + buffer = "" + @default.scan(/../) { |h| buffer << h.hex.chr } + @default = buffer + else + @default = "" + end + else + @default = type_cast(@default) + end + end + end + + # Casts value (which is a String) to an appropriate instance. + def type_cast(value) + if type == :twelvebytekey + ActiveRecord::ConnectionAdapters::TwelveByteKey.new(value) + else + super(value) + end + end + + def type_cast_code(var_name) + if type == :twelvebytekey + "ActiveRecord::ConnectionAdapters::TwelveByteKey.new(#{var_name})" + else + super(var_name) + end + end + + private + def simplified_type(field_type, type_name,limit) + ret_type = :string + puts "typecode: [#{field_type}] [#{type_name}]" if FB_TRACE + + # 12 byte primary keys are a special case that Apple's EOF + # used heavily. Optimize for this case + if field_type == 11 && limit == 96 + ret_type = :twelvebytekey # BIT(96) + else + ret_type = case field_type + when 1 then :boolean # BOOLEAN + when 2 then :integer # INTEGER + when 4 then :float # FLOAT + when 10 then :string # CHARACTER VARYING + when 11 then :bitfield # BIT + when 13 then :date # DATE + when 14 then :time # TIME + when 16 then :timestamp # TIMESTAMP + when 20 then :text # CLOB + when 21 then :binary # BLOB + when 22 then :integer # TINYINT + else + puts "ERROR: Unknown typecode: [#{field_type}] [#{type_name}]" + end + end + puts "ret_type: #{ret_type.inspect}" if FB_TRACE + ret_type + end + end + + class FrontBaseAdapter < AbstractAdapter + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @transaction_mode = :pessimistic + + # threaded_connections_test.rb will fail unless we set the session + # to optimistic locking mode +# set_pessimistic_transactions +# execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC" + end + + # Returns the human-readable name of the adapter. Use mixed case - one + # can always use downcase if needed. + def adapter_name #:nodoc: + 'FrontBase' + end + + # Does this adapter support migrations? Backend specific, as the + # abstract adapter always returns +false+. + def supports_migrations? #:nodoc: + true + end + + def native_database_types #:nodoc + { + :primary_key => "INTEGER DEFAULT UNIQUE PRIMARY KEY", + :string => { :name => "VARCHAR", :limit => 255 }, + :text => { :name => "CLOB" }, + :integer => { :name => "INTEGER" }, + :float => { :name => "FLOAT" }, + :datetime => { :name => "TIMESTAMP" }, + :timestamp => { :name => "TIMESTAMP" }, + :time => { :name => "TIME" }, + :date => { :name => "DATE" }, + :binary => { :name => "BLOB" }, + :boolean => { :name => "BOOLEAN" }, + :twelvebytekey => { :name => "BYTE", :limit => 12} + } + end + + + # QUOTING ================================================== + + # Quotes the column value to help prevent + # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. + def quote(value, column = nil) + retvalue = "" + + puts "quote(#{value.inspect}(#{value.class}),#{column.type.inspect})" if FB_TRACE + # If a column was passed in, use column type information + unless value.nil? + if column + retvalue = case column.type + when :string + if value.kind_of?(String) + "'#{quote_string(value.to_s)}'" # ' (for ruby-mode) + else + "'#{quote_string(value.to_yaml)}'" + end + when :integer + if value.kind_of?(TrueClass) + '1' + elsif value.kind_of?(FalseClass) + '0' + else + value.to_i.to_s + end + when :float + value.to_f.to_s + when :datetime, :timestamp + "TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'" + when :time + "TIME '#{value.strftime("%H:%M:%S")}'" + when :date + "DATE '#{value.strftime("%Y-%m-%d")}'" + when :twelvebytekey + value = value.to_s.unpack("H*").first unless value.kind_of?(TwelveByteKey) + "X'#{value.to_s}'" + when :boolean + value = quoted_true if value.kind_of?(TrueClass) + value = quoted_false if value.kind_of?(FalseClass) + value + when :binary + blob_handle = @connection.create_blob(value.to_s) + puts "SQL -> Insert #{value.to_s.length} byte blob as #{retvalue}" if FB_TRACE + blob_handle.handle + when :text + if value.kind_of?(String) + clobdata = value.to_s # ' (for ruby-mode) + else + clobdata = value.to_yaml + end + clob_handle = @connection.create_clob(clobdata) + puts "SQL -> Insert #{value.to_s.length} byte clob as #{retvalue}" if FB_TRACE + clob_handle.handle + else + raise "*** UNKNOWN TYPE: #{column.type.inspect}" + end # case + # Since we don't have column type info, make a best guess based + # on the Ruby class of the value + else + retvalue = case value + when ActiveRecord::ConnectionAdapters::TwelveByteKey + s = value.unpack("H*").first + "X'#{s}'" + when String + if column && column.type == :binary + s = value.unpack("H*").first + "X'#{s}'" + elsif column && [:integer, :float].include?(column.type) + value.to_s + else + "'#{quote_string(value)}'" # ' (for ruby-mode) + end + when NilClass + "NULL" + when TrueClass + (column && column.type == :integer ? '1' : quoted_true) + when FalseClass + (column && column.type == :integer ? '0' : quoted_false) + when Float, Fixnum, Bignum + value.to_s + when Time, Date, DateTime + if column + case column.type + when :date + "DATE '#{value.strftime("%Y-%m-%d")}'" + when :time + "TIME '#{value.strftime("%H:%M:%S")}'" + when :timestamp + "TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'" + else + raise NotImplementedError, "Unknown column type!" + end # case + else # Column wasn't passed in, so try to guess the right type + if value.kind_of? Date + "DATE '#{value.strftime("%Y-%m-%d")}'" + else + if [:hour, :min, :sec].all? {|part| value.send(:part).zero? } + "TIME '#{value.strftime("%H:%M:%S")}'" + else + "TIMESTAMP '#{quoted_date(value)}'" + end + end + end #if column + else + "'#{quote_string(value.to_yaml)}'" + end #case + end + else + retvalue = "NULL" + end + + retvalue + end # def + + # Quotes a string, escaping any ' (single quote) characters. + def quote_string(s) + s.gsub(/'/, "''") # ' (for ruby-mode) + end + + def quote_column_name(name) #:nodoc: + %( "#{name}" ) + end + + def quoted_true + "true" + end + + def quoted_false + "false" + end + + + # CONNECTION MANAGEMENT ==================================== + + def active? + true if @connection.status == 1 + rescue => e + false + end + + def reconnect! + @connection.close rescue nil + @connection = FBSQL_Connect.connect(*@connection_options.first(7)) + end + + # Close this connection + def disconnect! + @connection.close rescue nil + @active = false + end + + # DATABASE STATEMENTS ====================================== + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select_all(sql, name = nil) #:nodoc: + fbsql = cleanup_fb_sql(sql) + return_value = [] + fbresult = execute(sql, name) + puts "select_all SQL -> #{fbsql}" if FB_TRACE + columns = fbresult.columns + + fbresult.each do |row| + puts "SQL <- #{row.inspect}" if FB_TRACE + hashed_row = {} + colnum = 0 + row.each do |col| + hashed_row[columns[colnum]] = col + if col.kind_of?(FBSQL_LOB) + hashed_row[columns[colnum]] = col.read + end + colnum += 1 + end + puts "raw row: #{hashed_row.inspect}" if FB_TRACE + return_value << hashed_row + end + return_value + end + + def select_one(sql, name = nil) #:nodoc: + fbsql = cleanup_fb_sql(sql) + return_value = [] + fbresult = execute(fbsql, name) + puts "SQL -> #{fbsql}" if FB_TRACE + columns = fbresult.columns + + fbresult.each do |row| + puts "SQL <- #{row.inspect}" if FB_TRACE + hashed_row = {} + colnum = 0 + row.each do |col| + hashed_row[columns[colnum]] = col + if col.kind_of?(FBSQL_LOB) + hashed_row[columns[colnum]] = col.read + end + colnum += 1 + end + return_value << hashed_row + break + end + fbresult.clear + return_value.first + end + + def query(sql, name = nil) #:nodoc: + fbsql = cleanup_fb_sql(sql) + puts "SQL(query) -> #{fbsql}" if FB_TRACE + log(fbsql, name) { @connection.query(fbsql) } + rescue => e + puts "FB Exception: #{e.inspect}" if FB_TRACE + raise e + end + + def execute(sql, name = nil) #:nodoc: + fbsql = cleanup_fb_sql(sql) + puts "SQL(execute) -> #{fbsql}" if FB_TRACE + log(fbsql, name) { @connection.query(fbsql) } + rescue ActiveRecord::StatementInvalid => e + if e.message.scan(/Table name - \w* - exists/).empty? + puts "FB Exception: #{e.inspect}" if FB_TRACE + raise e + end + end + + # Returns the last auto-generated ID from the affected table. + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + puts "SQL -> #{sql.inspect}" if FB_TRACE + execute(sql, name) + id_value || pk + end + + # Executes the update statement and returns the number of rows affected. + def update(sql, name = nil) #:nodoc: + puts "SQL -> #{sql.inspect}" if FB_TRACE + execute(sql, name).num_rows + end + + alias_method :delete, :update #:nodoc: + + def set_pessimistic_transactions + if @transaction_mode == :optimistic + execute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, LOCKING PESSIMISTIC, READ WRITE" + @transaction_mode = :pessimistic + end + end + + def set_optimistic_transactions + if @transaction_mode == :pessimistic + execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC" + @transaction_mode = :optimistic + end + end + + def begin_db_transaction #:nodoc: + execute "SET COMMIT FALSE" rescue nil + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + ensure + execute "SET COMMIT TRUE" + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + ensure + execute "SET COMMIT TRUE" + end + + def add_limit_offset!(sql, options) #:nodoc + if limit = options[:limit] + offset = options[:offset] || 0 + +# Here is the full syntax FrontBase supports: +# (from gclem@frontbase.com) +# +# TOP +# TOP ( , ) + + # "TOP 0" is not allowed, so we have + # to use a cheap trick. + if limit.zero? + case sql + when /WHERE/i + sql.sub!(/WHERE/i, 'WHERE 0 = 1 AND ') + when /ORDER\s+BY/i + sql.sub!(/ORDER\s+BY/i, 'WHERE 0 = 1 ORDER BY') + else + sql << 'WHERE 0 = 1' + end + else + if offset.zero? + sql.replace sql.gsub("SELECT ","SELECT TOP #{limit} ") + else + sql.replace sql.gsub("SELECT ","SELECT TOP(#{offset},#{limit}) ") + end + end + end + end + + def prefetch_primary_key?(table_name = nil) + true + end + + # Returns the next sequence value from a sequence generator. Not generally + # called directly; used by ActiveRecord to get the next primary key value + # when inserting a new database record (see #prefetch_primary_key?). + def next_sequence_value(sequence_name) + unique = select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") + # The test cases cannot handle a zero primary key + unique.zero? ? select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") : unique + end + + def default_sequence_name(table, column) + table + end + + # Set the sequence to the max value of the table's column. + def reset_sequence!(table, column, sequence = nil) + klasses = classes_for_table_name(table) + klass = klasses.nil? ? nil : klasses.first + pk = klass.primary_key unless klass.nil? + if pk && klass.columns_hash[pk].type == :integer + execute("SET UNIQUE FOR #{klass.table_name}(#{pk})") + end + end + + def classes_for_table_name(table) + ActiveRecord::Base.send(:subclasses).select {|klass| klass.table_name == table} + end + + def reset_pk_sequence!(table, pk = nil, sequence = nil) + klasses = classes_for_table_name(table) + klass = klasses.nil? ? nil : klasses.first + pk = klass.primary_key unless klass.nil? + if pk && klass.columns_hash[pk].type == :integer + mpk = select_value("SELECT MAX(#{pk}) FROM #{table}") + execute("SET UNIQUE FOR #{klass.table_name}(#{pk})") + end + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump #:nodoc: + 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) #:nodoc: + drop_database(name) + create_database(name) + end + + def create_database(name) #:nodoc: + execute "CREATE DATABASE #{name}" + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE #{name}" + end + + def current_database + select_value('SELECT "CATALOG_NAME" FROM INFORMATION_SCHEMA.CATALOGS').downcase + end + + def tables(name = nil) #:nodoc: + select_values(<<-SQL, nil) + SELECT "TABLE_NAME" + FROM INFORMATION_SCHEMA.TABLES AS T0, + INFORMATION_SCHEMA.SCHEMATA AS T1 + WHERE T0.SCHEMA_PK = T1.SCHEMA_PK + AND "SCHEMA_NAME" = CURRENT_SCHEMA + SQL + end + + def indexes(table_name, name = nil)#:nodoc: + indexes = [] + current_index = nil + sql = <<-SQL + SELECT INDEX_NAME, T2.ORDINAL_POSITION, INDEX_COLUMN_COUNT, INDEX_TYPE, + "COLUMN_NAME", IS_NULLABLE + FROM INFORMATION_SCHEMA.TABLES AS T0, + INFORMATION_SCHEMA.INDEXES AS T1, + INFORMATION_SCHEMA.INDEX_COLUMN_USAGE AS T2, + INFORMATION_SCHEMA.COLUMNS AS T3 + WHERE T0."TABLE_NAME" = '#{table_name}' + AND INDEX_TYPE <> 0 + AND T0.TABLE_PK = T1.TABLE_PK + AND T0.TABLE_PK = T2.TABLE_PK + AND T0.TABLE_PK = T3.TABLE_PK + AND T1.INDEXES_PK = T2.INDEX_PK + AND T2.COLUMN_PK = T3.COLUMN_PK + ORDER BY INDEX_NAME, T2.ORDINAL_POSITION + SQL + + columns = [] + query(sql).each do |row| + index_name = row[0] + ord_position = row[1] + ndx_colcount = row[2] + index_type = row[3] + column_name = row[4] + + is_unique = index_type == 1 + + columns << column_name + if ord_position == ndx_colcount + indexes << IndexDefinition.new(table_name, index_name, is_unique , columns) + columns = [] + end + end + indexes + end + + def columns(table_name, name = nil)#:nodoc: + sql = <<-SQL + SELECT "TABLE_NAME", "COLUMN_NAME", ORDINAL_POSITION, IS_NULLABLE, COLUMN_DEFAULT, + DATA_TYPE, DATA_TYPE_CODE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, + NUMERIC_PRECISION_RADIX, NUMERIC_SCALE, DATETIME_PRECISION, DATETIME_PRECISION_LEADING + FROM INFORMATION_SCHEMA.TABLES T0, + INFORMATION_SCHEMA.COLUMNS T1, + INFORMATION_SCHEMA.DATA_TYPE_DESCRIPTOR T3 + WHERE "TABLE_NAME" = '#{table_name}' + AND T0.TABLE_PK = T1.TABLE_PK + AND T0.TABLE_PK = T3.TABLE_OR_DOMAIN_PK + AND T1.COLUMN_PK = T3.COLUMN_NAME_PK + ORDER BY T1.ORDINAL_POSITION + SQL + + rawresults = query(sql,name) + columns = [] + rawresults.each do |field| + args = [base = field[0], + name = field[1], + typecode = field[6], + typestring = field[5], + limit = field[7], + precision = field[8], + scale = field[9], + default = field[4], + nullable = field[3]] + columns << FrontBaseColumn.new(*args) + end + columns + 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 + + if options[:force] + drop_table(name) rescue nil + end + + create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " + create_sql << "#{name} (" + create_sql << table_definition.to_sql + create_sql << ") #{options[:options]}" + begin_db_transaction + execute create_sql + commit_db_transaction + rescue ActiveRecord::StatementInvalid => e + raise e unless e.message.match(/Table name - \w* - exists/) + end + + def rename_table(name, new_name) + columns = columns(name) + pkcol = columns.find {|c| c.fb_autogen} + execute "ALTER TABLE NAME #{name} TO #{new_name}" + if pkcol + change_column_default(new_name,pkcol.name,"UNIQUE") + begin_db_transaction + mpk = select_value("SELECT MAX(#{pkcol.name}) FROM #{new_name}") + mpk = 0 if mpk.nil? + execute "SET UNIQUE=#{mpk} FOR #{new_name}" + commit_db_transaction + end + end + + # Drops a table from the database. + def drop_table(name) + execute "DROP TABLE #{name} RESTRICT" + rescue ActiveRecord::StatementInvalid => e + raise e unless e.message.match(/Referenced TABLE - \w* - does not exist/) + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + 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])}" + options[:type] = type + add_column_options!(add_column_sql, options) + execute(add_column_sql) + end + + def add_column_options!(sql, options) #:nodoc: + default_value = quote(options[:default], options[:column]) + if options[:default] + if options[:type] == :boolean + default_value = options[:default] == 0 ? quoted_false : quoted_true + end + end + sql << " DEFAULT #{default_value}" unless options[:default].nil? + sql << " NOT NULL" if options[:null] == false + end + + # Removes the column from the table definition. + # ===== Examples + # remove_column(:suppliers, :qualification) + def remove_column(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP #{column_name} RESTRICT" + end + + def remove_index(table_name, options = {}) #:nodoc: + if options[:unique] + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{quote_column_name(index_name(table_name, options))} RESTRICT" + else + execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" + end + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{default}" if default != "NULL" + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + change_column_sql = %( ALTER COLUMN "#{table_name}"."#{column_name}" TO #{type_to_sql(type, options[:limit])} ) + execute(change_column_sql) + change_column_sql = %( ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" ) + + default_value = quote(options[:default], options[:column]) + if options[:default] + if type == :boolean + default_value = options[:default] == 0 ? quoted_false : quoted_true + end + end + + if default_value != "NULL" + change_column_sql << " SET DEFAULT #{default_value}" + execute(change_column_sql) + end + +# change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}" +# add_column_options!(change_column_sql, options) +# execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute %( ALTER COLUMN NAME "#{table_name}"."#{column_name}" TO "#{new_column_name}" ) + end + + private + + # Clean up sql to make it something FrontBase can digest + def cleanup_fb_sql(sql) #:nodoc: + # Turn non-standard != into standard <> + cleansql = sql.gsub("!=", "<>") + # Strip blank lines and comments + cleansql.split("\n").reject { |line| line.match(/^(?:\s*|--.*)$/) } * "\n" + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index f83219f508..a1033c40e4 100755 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -392,7 +392,13 @@ class Fixture #:nodoc: end def value_list - @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ") + klass = @class_name.constantize rescue nil + + list = @fixture.inject([]) do |fixtures, (key, value)| + col = klass.columns_hash[key] unless klass.nil? + fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('\\n', "\n").gsub('\\r', "\r") + end + list * ', ' end def find diff --git a/activerecord/test/adapter_test.rb b/activerecord/test/adapter_test.rb index c4027b31cb..0c12c4d37e 100644 --- a/activerecord/test/adapter_test.rb +++ b/activerecord/test/adapter_test.rb @@ -66,19 +66,21 @@ class AdapterTest < Test::Unit::TestCase if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) require 'fixtures/movie' require 'fixtures/subscriber' + def test_reset_empty_table_with_custom_pk Movie.delete_all Movie.connection.reset_pk_sequence! 'movies' assert_equal 1, Movie.create(:name => 'fight club').id end - def test_reset_table_with_non_integer_pk - Subscriber.delete_all - Subscriber.connection.reset_pk_sequence! 'subscribers' - - sub = Subscriber.new(:name => 'robert drake') - sub.id = 'bob drake' - assert_nothing_raised { sub.save! } + if ActiveRecord::Base.connection.adapter_name != "FrontBase" + def test_reset_table_with_non_integer_pk + Subscriber.delete_all + Subscriber.connection.reset_pk_sequence! 'subscribers' + sub = Subscriber.new(:name => 'robert drake') + sub.id = 'bob drake' + assert_nothing_raised { sub.save! } + end end end diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb index c4c0742b7b..2f834772a6 100755 --- a/activerecord/test/base_test.rb +++ b/activerecord/test/base_test.rb @@ -1094,21 +1094,21 @@ class BasicsTest < Test::Unit::TestCase end assert_equal res, res3 - res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id" + res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res5 = nil assert_nothing_raised do - res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", - :joins => "p, comments c", + res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", + :joins => "p, comments co", :select => "p.id") end assert_equal res4, res5 - res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments c WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id" + res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", - :joins => "p, comments c", + res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", + :joins => "p, comments co", :select => "p.id", :distinct => true) end diff --git a/activerecord/test/connections/native_frontbase/connection.rb b/activerecord/test/connections/native_frontbase/connection.rb new file mode 100644 index 0000000000..e3d1d5b1e1 --- /dev/null +++ b/activerecord/test/connections/native_frontbase/connection.rb @@ -0,0 +1,26 @@ +puts 'Using native Frontbase' +require_dependency 'fixtures/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +db1 = 'activerecord_unittest' +db2 = 'activerecord_unittest2' + +ActiveRecord::Base.establish_connection( + :adapter => "frontbase", + :host => "localhost", + :username => "rails", + :password => "", + :database => db1, + :session_name => "unittest-#{$$}" +) + +Course.establish_connection( + :adapter => "frontbase", + :host => "localhost", + :username => "rails", + :password => "", + :database => db2, + :session_name => "unittest-#{$$}" +) diff --git a/activerecord/test/deprecated_finder_test.rb b/activerecord/test/deprecated_finder_test.rb index 35c8a2379a..c1065b0026 100755 --- a/activerecord/test/deprecated_finder_test.rb +++ b/activerecord/test/deprecated_finder_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'fixtures/company' require 'fixtures/topic' +require 'fixtures/reply' require 'fixtures/entrant' require 'fixtures/developer' diff --git a/activerecord/test/finder_test.rb b/activerecord/test/finder_test.rb index 98537ecad7..f23e383115 100644 --- a/activerecord/test/finder_test.rb +++ b/activerecord/test/finder_test.rb @@ -13,6 +13,12 @@ class FinderTest < Test::Unit::TestCase assert_equal(topics(:first).title, Topic.find(1).title) end + # find should handle strings that come from URLs + # (example: Category.find(params[:id])) + def test_find_with_string + assert_equal(Topic.find(1).title,Topic.find("1").title) + end + def test_exists assert (Topic.exists?(1)) assert !(Topic.exists?(45)) diff --git a/activerecord/test/fixtures/db_definitions/frontbase.drop.sql b/activerecord/test/fixtures/db_definitions/frontbase.drop.sql new file mode 100644 index 0000000000..231af7895c --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/frontbase.drop.sql @@ -0,0 +1,30 @@ +DROP TABLE accounts CASCADE; +DROP TABLE funny_jokes CASCADE; +DROP TABLE companies CASCADE; +DROP TABLE topics CASCADE; +DROP TABLE developers CASCADE; +DROP TABLE projects CASCADE; +DROP TABLE developers_projects CASCADE; +DROP TABLE orders CASCADE; +DROP TABLE customers CASCADE; +DROP TABLE movies CASCADE; +DROP TABLE subscribers CASCADE; +DROP TABLE booleantests CASCADE; +DROP TABLE auto_id_tests CASCADE; +DROP TABLE entrants CASCADE; +DROP TABLE colnametests CASCADE; +DROP TABLE mixins CASCADE; +DROP TABLE people CASCADE; +DROP TABLE readers CASCADE; +DROP TABLE binaries CASCADE; +DROP TABLE computers CASCADE; +DROP TABLE posts CASCADE; +DROP TABLE comments CASCADE; +DROP TABLE authors CASCADE; +DROP TABLE tasks CASCADE; +DROP TABLE categories CASCADE; +DROP TABLE categories_posts CASCADE; +DROP TABLE fk_test_has_fk CASCADE; +DROP TABLE fk_test_has_pk CASCADE; +DROP TABLE keyboards CASCADE; +DROP TABLE legacy_things CASCADE; diff --git a/activerecord/test/fixtures/db_definitions/frontbase.sql b/activerecord/test/fixtures/db_definitions/frontbase.sql new file mode 100644 index 0000000000..13ce65297b --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/frontbase.sql @@ -0,0 +1,251 @@ +CREATE TABLE accounts ( + id integer DEFAULT unique, + firm_id integer, + credit_limit integer, + PRIMARY KEY (id) +); +SET UNIQUE FOR accounts(id); + +CREATE TABLE funny_jokes ( + id integer DEFAULT unique, + firm_id integer default NULL, + name character varying(50), + PRIMARY KEY (id) +); +SET UNIQUE FOR funny_jokes(id); + +CREATE TABLE companies ( + id integer DEFAULT unique, + "type" character varying(50), + "ruby_type" character varying(50), + firm_id integer, + name character varying(50), + client_of integer, + rating integer default 1, + PRIMARY KEY (id) +); +SET UNIQUE FOR companies(id); + +CREATE TABLE topics ( + id integer DEFAULT unique, + title character varying(255), + author_name character varying(255), + author_email_address character varying(255), + written_on timestamp, + bonus_time time, + last_read date, + content varchar(65536), + approved boolean default true, + replies_count integer default 0, + parent_id integer, + "type" character varying(50), + PRIMARY KEY (id) +); +SET UNIQUE FOR topics(id); + +CREATE TABLE developers ( + id integer DEFAULT unique, + name character varying(100), + salary integer DEFAULT 70000, + created_at timestamp, + updated_at timestamp, + PRIMARY KEY (id) +); +SET UNIQUE FOR developers(id); + +CREATE TABLE projects ( + id integer DEFAULT unique, + name character varying(100), + type varchar(255), + PRIMARY KEY (id) +); +SET UNIQUE FOR projects(id); + +CREATE TABLE developers_projects ( + developer_id integer NOT NULL, + project_id integer NOT NULL, + joined_on date, + access_level integer default 1 +); + +CREATE TABLE orders ( + id integer DEFAULT unique, + name character varying(100), + billing_customer_id integer, + shipping_customer_id integer, + PRIMARY KEY (id) +); +SET UNIQUE FOR orders(id); + +CREATE TABLE customers ( + id integer DEFAULT unique, + name character varying(100), + balance integer default 0, + address_street character varying(100), + address_city character varying(100), + address_country character varying(100), + gps_location character varying(100), + PRIMARY KEY (id) +); +SET UNIQUE FOR customers(id); + +CREATE TABLE movies ( + movieid integer DEFAULT unique, + name varchar(65536), + PRIMARY KEY (movieid) +); +SET UNIQUE FOR movies(movieid); + +CREATE TABLE subscribers ( + nick varchar(65536) NOT NULL, + name varchar(65536), + PRIMARY KEY (nick) +); + +CREATE TABLE booleantests ( + id integer DEFAULT unique, + value boolean, + PRIMARY KEY (id) +); +SET UNIQUE FOR booleantests(id); + +CREATE TABLE auto_id_tests ( + auto_id integer DEFAULT unique, + value integer, + PRIMARY KEY (auto_id) +); +SET UNIQUE FOR auto_id_tests(auto_id); + +CREATE TABLE entrants ( + id integer DEFAULT unique, + name varchar(65536), + course_id integer, + PRIMARY KEY (id) +); +SET UNIQUE FOR entrants(id); + +CREATE TABLE colnametests ( + id integer DEFAULT unique, + "references" integer NOT NULL, + PRIMARY KEY (id) +); +SET UNIQUE FOR colnametests(id); + +CREATE TABLE mixins ( + id integer DEFAULT unique, + parent_id integer, + type character varying(100), + pos integer, + lft integer, + rgt integer, + root_id integer, + created_at timestamp, + updated_at timestamp, + PRIMARY KEY (id) +); +SET UNIQUE FOR mixins(id); + +CREATE TABLE people ( + id integer DEFAULT unique, + first_name varchar(65536), + lock_version integer default 0, + PRIMARY KEY (id) +); +SET UNIQUE FOR people(id); + +CREATE TABLE readers ( + id integer DEFAULT unique, + post_id INTEGER NOT NULL, + person_id INTEGER NOT NULL, + PRIMARY KEY (id) +); +SET UNIQUE FOR readers(id); + +CREATE TABLE binaries ( + id integer DEFAULT unique, + data BLOB, + PRIMARY KEY (id) +); +SET UNIQUE FOR binaries(id); + +CREATE TABLE computers ( + id integer DEFAULT unique, + developer integer NOT NULL, + "extendedWarranty" integer NOT NULL, + PRIMARY KEY (id) +); +SET UNIQUE FOR computers(id); + +CREATE TABLE posts ( + id integer DEFAULT unique, + author_id integer, + title varchar(255), + type varchar(255), + body varchar(65536), + PRIMARY KEY (id) +); +SET UNIQUE FOR posts(id); + +CREATE TABLE comments ( + id integer DEFAULT unique, + post_id integer, + type varchar(255), + body varchar(65536), + PRIMARY KEY (id) +); +SET UNIQUE FOR comments(id); + +CREATE TABLE authors ( + id integer DEFAULT unique, + name varchar(255) default NULL, + PRIMARY KEY (id) +); +SET UNIQUE FOR authors(id); + +CREATE TABLE tasks ( + id integer DEFAULT unique, + starting timestamp, + ending timestamp, + PRIMARY KEY (id) +); +SET UNIQUE FOR tasks(id); + +CREATE TABLE categories ( + id integer DEFAULT unique, + name varchar(255), + type varchar(255), + PRIMARY KEY (id) +); +SET UNIQUE FOR categories(id); + +CREATE TABLE categories_posts ( + category_id integer NOT NULL, + post_id integer NOT NULL +); + +CREATE TABLE fk_test_has_pk ( + id INTEGER NOT NULL PRIMARY KEY +); +SET UNIQUE FOR fk_test_has_pk(id); + +CREATE TABLE fk_test_has_fk ( + id INTEGER NOT NULL PRIMARY KEY, + fk_id INTEGER NOT NULL REFERENCES fk_test_has_fk(id) +); +SET UNIQUE FOR fk_test_has_fk(id); + +CREATE TABLE keyboards ( + key_number integer DEFAULT unique, + "name" character varying(50), + PRIMARY KEY (key_number) +); +SET UNIQUE FOR keyboards(key_number); + +create table "legacy_things" +( + "id" int, + "tps_report_number" int default NULL, + "version" int default 0 not null, + primary key ("id") +); +SET UNIQUE FOR legacy_things(id); diff --git a/activerecord/test/fixtures/db_definitions/frontbase2.drop.sql b/activerecord/test/fixtures/db_definitions/frontbase2.drop.sql new file mode 100644 index 0000000000..17b9ad46d7 --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/frontbase2.drop.sql @@ -0,0 +1 @@ +DROP TABLE courses CASCADE; diff --git a/activerecord/test/fixtures/db_definitions/frontbase2.sql b/activerecord/test/fixtures/db_definitions/frontbase2.sql new file mode 100644 index 0000000000..42f4bb78c9 --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/frontbase2.sql @@ -0,0 +1,4 @@ +CREATE TABLE courses ( + id integer DEFAULT unique, + name varchar(100) +); diff --git a/activerecord/test/migration_test.rb b/activerecord/test/migration_test.rb index c6a1d92481..e238eab1a2 100644 --- a/activerecord/test/migration_test.rb +++ b/activerecord/test/migration_test.rb @@ -58,7 +58,7 @@ if ActiveRecord::Base.connection.supports_migrations? # quoting assert_nothing_raised { Person.connection.add_index("people", ["key"], :name => "key", :unique => true) } - assert_nothing_raised { Person.connection.remove_index("people", :name => "key") } + assert_nothing_raised { Person.connection.remove_index("people", :name => "key", :unique => true) } # Sybase adapter does not support indexes on :boolean columns unless current_adapter?(:SybaseAdapter) @@ -461,27 +461,29 @@ if ActiveRecord::Base.connection.supports_migrations? Reminder.reset_sequence_name end - def test_create_table_with_binary_column - Person.connection.drop_table :binary_testings rescue nil +# FrontBase does not support default values on BLOB/CLOB columns + unless current_adapter?(:FrontBaseAdapter) + def test_create_table_with_binary_column + Person.connection.drop_table :binary_testings rescue nil - assert_nothing_raised { - Person.connection.create_table :binary_testings do |t| - t.column "data", :binary, :default => "", :null => false - end - } + assert_nothing_raised { + Person.connection.create_table :binary_testings do |t| + t.column "data", :binary, :default => "", :null => false + end + } - columns = Person.connection.columns(:binary_testings) - data_column = columns.detect { |c| c.name == "data" } + columns = Person.connection.columns(:binary_testings) + data_column = columns.detect { |c| c.name == "data" } - if current_adapter?(:OracleAdapter) - assert_equal "empty_blob()", data_column.default - else - assert_equal "", data_column.default + if current_adapter?(:OracleAdapter) + assert_equal "empty_blob()", data_column.default + else + assert_equal "", data_column.default + end + + Person.connection.drop_table :binary_testings rescue nil end - - Person.connection.drop_table :binary_testings rescue nil end - def test_migrator_with_duplicates assert_raises(ActiveRecord::DuplicateMigrationVersionError) do ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_duplicate/', nil) diff --git a/activerecord/test/pk_test.rb b/activerecord/test/pk_test.rb index 839a784e4a..604b263571 100644 --- a/activerecord/test/pk_test.rb +++ b/activerecord/test/pk_test.rb @@ -1,5 +1,6 @@ require "#{File.dirname(__FILE__)}/abstract_unit" require 'fixtures/topic' +require 'fixtures/reply' require 'fixtures/subscriber' require 'fixtures/movie' require 'fixtures/keyboard' diff --git a/activerecord/test/reflection_test.rb b/activerecord/test/reflection_test.rb index 4882ca246f..907d02193f 100644 --- a/activerecord/test/reflection_test.rb +++ b/activerecord/test/reflection_test.rb @@ -45,6 +45,12 @@ class ReflectionTest < Test::Unit::TestCase assert_equal :string, @first.column_for_attribute("title").type assert_equal 255, @first.column_for_attribute("title").limit end + + def test_column_null_not_null + subscriber = Subscriber.find(:first) + assert subscriber.column_for_attribute("name").null + assert !subscriber.column_for_attribute("nick").null + end def test_human_name_for_column assert_equal "Author name", @first.column_for_attribute("author_name").human_name diff --git a/activerecord/test/threaded_connections_test.rb b/activerecord/test/threaded_connections_test.rb index d9cc47eef3..a812ec642c 100644 --- a/activerecord/test/threaded_connections_test.rb +++ b/activerecord/test/threaded_connections_test.rb @@ -1,10 +1,11 @@ require 'abstract_unit' require 'fixtures/topic' -class ThreadedConnectionsTest < Test::Unit::TestCase - self.use_transactional_fixtures = false +unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name + class ThreadedConnectionsTest < Test::Unit::TestCase + self.use_transactional_fixtures = false - fixtures :topics + fixtures :topics def setup @connection = ActiveRecord::Base.remove_connection @@ -25,21 +26,22 @@ class ThreadedConnectionsTest < Test::Unit::TestCase ActiveRecord::Base.allow_concurrency = use_threaded_connections ActiveRecord::Base.establish_connection(@connection) - 5.times do - Thread.new do - Topic.find :first - @connections << ActiveRecord::Base.active_connections.values.first - end.join + 5.times do + Thread.new do + Topic.find :first + @connections << ActiveRecord::Base.active_connections.values.first + end.join + end + end + + def test_threaded_connections + gather_connections(true) + assert_equal @connections.uniq.length, 5 + end + + def test_unthreaded_connections + gather_connections(false) + assert_equal @connections.uniq.length, 1 end end - - def test_threaded_connections - gather_connections(true) - assert_equal @connections.uniq.length, 5 - end - - def test_unthreaded_connections - gather_connections(false) - assert_equal @connections.uniq.length, 1 - end end diff --git a/activerecord/test/transactions_test.rb b/activerecord/test/transactions_test.rb index 0048e24bc7..421da4d7a6 100644 --- a/activerecord/test/transactions_test.rb +++ b/activerecord/test/transactions_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'fixtures/topic' +require 'fixtures/reply' require 'fixtures/developer' class TransactionTest < Test::Unit::TestCase