Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #4093: FrontBaseAdapter.patch

File FrontBaseAdapter.patch, 38.6 kB (added by mlaster@metavillage.com, 3 years ago)
  • activerecord/test/connections/native_frontbase/connection.rb

    old new  
     1print "Using native Frontbase\n" 
     2require_dependency 'fixtures/course' 
     3require 'logger' 
     4 
     5ActiveRecord::Base.logger = Logger.new("debug.log") 
     6 
     7db1 = 'activerecord_unittest' 
     8db2 = 'activerecord_unittest2' 
     9 
     10ActiveRecord::Base.establish_connection( 
     11  :adapter      => "frontbase", 
     12  :host         => "localhost", 
     13  :username     => "rails", 
     14  :password     => "", 
     15  :database     => db1, 
     16  :session_name => "unittest-#{$$}" 
     17) 
     18 
     19Course.establish_connection( 
     20  :adapter      => "frontbase", 
     21  :host         => "localhost", 
     22  :username     => "rails", 
     23  :password     => "", 
     24  :database     => db2, 
     25  :session_name => "unittest-#{$$}" 
     26) 
  • activerecord/test/base_test.rb

    old new  
    10721072    end 
    10731073    assert_equal res, res3 
    10741074     
    1075     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" 
     1075    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" 
    10761076    res5 = nil 
    10771077    assert_nothing_raised do 
    1078       res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", 
    1079                         :joins => "p, comments c", 
     1078      res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", 
     1079                        :joins => "p, comments co", 
    10801080                        :select => "p.id") 
    10811081    end 
    10821082 
    10831083    assert_equal res4, res5  
    10841084     
    1085     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" 
     1085    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" 
    10861086    res7 = nil 
    10871087    assert_nothing_raised do 
    1088       res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=c.post_id", 
    1089                         :joins => "p, comments c", 
     1088      res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", 
     1089                        :joins => "p, comments co", 
    10901090                        :select => "p.id", 
    10911091                        :distinct => true) 
    10921092    end 
  • activerecord/test/adapter_test.rb

    old new  
    4141  if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) 
    4242    require 'fixtures/movie' 
    4343    require 'fixtures/subscriber' 
     44     
    4445    def test_reset_empty_table_with_custom_pk 
    4546      Movie.delete_all 
    4647      Movie.connection.reset_pk_sequence! 'movies' 
    4748      assert_equal 1, Movie.create(:name => 'fight club').id 
    4849    end 
    4950 
    50     def test_reset_table_with_non_integer_pk 
    51       Subscriber.delete_all 
    52       Subscriber.connection.reset_pk_sequence! 'subscribers' 
     51    if ActiveRecord::Base.connection.adapter_name != "FrontBase" 
     52      def test_reset_table_with_non_integer_pk 
     53        Subscriber.delete_all 
     54        Subscriber.connection.reset_pk_sequence! 'subscribers' 
    5355 
    54       sub = Subscriber.new(:name => 'robert drake') 
    55       sub.id = 'bob drake' 
    56       assert sub.save! 
     56        sub = Subscriber.new(:name => 'robert drake') 
     57        sub.id = 'bob drake' 
     58        assert sub.save! 
     59      end 
    5760    end 
    5861  end 
    5962end 
  • activerecord/test/threaded_connections_test.rb

    old new  
    11require 'abstract_unit' 
    22require 'fixtures/topic' 
    33 
    4 class ThreadedConnectionsTest < Test::Unit::TestCase 
    5   self.use_transactional_fixtures = false 
     4unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name 
     5  class ThreadedConnectionsTest < Test::Unit::TestCase 
     6    self.use_transactional_fixtures = false 
    67 
    7   fixtures :topics 
     8    fixtures :topics 
    89 
    9   def setup 
    10     @connection = ActiveRecord::Base.remove_connection 
    11     @connections = [] 
    12   end 
     10    def setup 
     11      @connection = ActiveRecord::Base.remove_connection 
     12      @connections = [] 
     13    end 
    1314   
    14   def gather_connections(use_threaded_connections) 
    15     ActiveRecord::Base.allow_concurrency = use_threaded_connections 
    16     ActiveRecord::Base.establish_connection(@connection) 
     15    def gather_connections(use_threaded_connections) 
     16      ActiveRecord::Base.allow_concurrency = use_threaded_connections 
     17      ActiveRecord::Base.establish_connection(@connection) 
    1718     
    18     5.times do 
    19       Thread.new do 
    20         Topic.find :first 
    21         @connections << ActiveRecord::Base.active_connections.values.first 
    22       end.join 
     19      5.times do 
     20        Thread.new do 
     21          Topic.find :first 
     22          @connections << ActiveRecord::Base.active_connections.values.first 
     23        end.join 
     24      end 
    2325    end 
    24   end 
    2526 
    26   def test_threaded_connections 
    27     gather_connections(true) 
    28     assert_equal @connections.uniq.length, 5 
    29   end 
     27    def test_threaded_connections 
     28      gather_connections(true) 
     29      assert_equal @connections.uniq.length, 5 
     30    end 
    3031 
    31   def test_unthreaded_connections 
    32     gather_connections(false) 
    33     assert_equal @connections.uniq.length, 1 
     32    def test_unthreaded_connections 
     33      gather_connections(false) 
     34      assert_equal @connections.uniq.length, 1 
     35    end 
    3436  end 
    3537end 
  • activerecord/test/fixtures/db_definitions/frontbase.sql

    old new  
     1CREATE TABLE accounts ( 
     2    id integer DEFAULT unique, 
     3    firm_id integer, 
     4    credit_limit integer, 
     5    PRIMARY KEY (id) 
     6); 
     7SET UNIQUE FOR accounts(id); 
     8 
     9CREATE TABLE funny_jokes ( 
     10  id integer DEFAULT unique, 
     11  firm_id integer default NULL, 
     12  name character varying(50), 
     13  PRIMARY KEY (id) 
     14); 
     15SET UNIQUE FOR funny_jokes(id); 
     16   
     17CREATE TABLE companies ( 
     18  id integer DEFAULT unique, 
     19    "type" character varying(50), 
     20    "ruby_type" character varying(50), 
     21    firm_id integer, 
     22    name character varying(50), 
     23    client_of integer, 
     24    rating integer default 1, 
     25    PRIMARY KEY (id) 
     26); 
     27SET UNIQUE FOR companies(id); 
     28 
     29CREATE TABLE topics ( 
     30  id integer DEFAULT unique, 
     31    title character varying(255), 
     32    author_name character varying(255), 
     33    author_email_address character varying(255), 
     34    written_on timestamp, 
     35    bonus_time time, 
     36    last_read date, 
     37    content varchar(65536), 
     38    approved boolean default true, 
     39    replies_count integer default 0, 
     40    parent_id integer, 
     41    "type" character varying(50), 
     42    PRIMARY KEY (id) 
     43); 
     44SET UNIQUE FOR topics(id); 
     45 
     46CREATE TABLE developers ( 
     47  id integer DEFAULT unique, 
     48    name character varying(100), 
     49    salary integer DEFAULT 70000, 
     50    created_at timestamp, 
     51    updated_at timestamp, 
     52    PRIMARY KEY (id) 
     53); 
     54SET UNIQUE FOR developers(id); 
     55 
     56CREATE TABLE projects ( 
     57  id integer DEFAULT unique, 
     58    name character varying(100), 
     59    type varchar(255), 
     60    PRIMARY KEY (id) 
     61); 
     62SET UNIQUE FOR projects(id); 
     63 
     64CREATE TABLE developers_projects ( 
     65    developer_id integer NOT NULL, 
     66    project_id integer NOT NULL, 
     67    joined_on date, 
     68    access_level integer default 1 
     69); 
     70 
     71CREATE TABLE orders ( 
     72  id integer DEFAULT unique, 
     73    name character varying(100), 
     74    billing_customer_id integer, 
     75    shipping_customer_id integer, 
     76    PRIMARY KEY (id) 
     77); 
     78SET UNIQUE FOR orders(id); 
     79 
     80CREATE TABLE customers ( 
     81  id integer DEFAULT unique, 
     82    name character varying(100), 
     83    balance integer default 0, 
     84    address_street character varying(100), 
     85    address_city character varying(100), 
     86    address_country character varying(100), 
     87    gps_location character varying(100), 
     88    PRIMARY KEY (id) 
     89); 
     90SET UNIQUE FOR customers(id); 
     91 
     92CREATE TABLE movies ( 
     93    movieid integer DEFAULT unique, 
     94    name varchar(65536), 
     95    PRIMARY KEY (movieid) 
     96); 
     97SET UNIQUE FOR movies(movieid); 
     98 
     99CREATE TABLE subscribers ( 
     100    nick varchar(65536) NOT NULL, 
     101    name varchar(65536), 
     102    PRIMARY KEY (nick) 
     103); 
     104 
     105CREATE TABLE booleantests ( 
     106  id integer DEFAULT unique, 
     107    value boolean, 
     108    PRIMARY KEY (id) 
     109); 
     110SET UNIQUE FOR booleantests(id); 
     111 
     112CREATE TABLE auto_id_tests ( 
     113  auto_id integer DEFAULT unique, 
     114    value integer, 
     115    PRIMARY KEY (auto_id) 
     116); 
     117SET UNIQUE FOR auto_id_tests(auto_id); 
     118 
     119CREATE TABLE entrants ( 
     120  id integer DEFAULT unique, 
     121  name varchar(65536), 
     122  course_id integer, 
     123  PRIMARY KEY (id) 
     124); 
     125SET UNIQUE FOR entrants(id); 
     126 
     127CREATE TABLE colnametests ( 
     128  id integer DEFAULT unique, 
     129  "references" integer NOT NULL, 
     130  PRIMARY KEY (id) 
     131); 
     132SET UNIQUE FOR colnametests(id); 
     133 
     134CREATE TABLE mixins ( 
     135  id integer DEFAULT unique, 
     136  parent_id integer, 
     137  type character varying(100),   
     138  pos integer, 
     139  lft integer, 
     140  rgt integer, 
     141  root_id integer,   
     142  created_at timestamp, 
     143  updated_at timestamp, 
     144  PRIMARY KEY (id) 
     145); 
     146SET UNIQUE FOR mixins(id); 
     147 
     148CREATE TABLE people ( 
     149  id integer DEFAULT unique, 
     150  first_name varchar(65536), 
     151  lock_version integer default 0, 
     152  PRIMARY KEY  (id) 
     153); 
     154SET UNIQUE FOR people(id); 
     155 
     156CREATE TABLE readers ( 
     157  id integer DEFAULT unique, 
     158  post_id INTEGER NOT NULL, 
     159  person_id INTEGER NOT NULL, 
     160  PRIMARY KEY  (id) 
     161); 
     162SET UNIQUE FOR readers(id); 
     163 
     164CREATE TABLE binaries (  
     165  id integer DEFAULT unique, 
     166  data BYTE VARYING(65536), 
     167  PRIMARY KEY (id) 
     168); 
     169SET UNIQUE FOR binaries(id); 
     170 
     171CREATE TABLE computers ( 
     172  id integer DEFAULT unique, 
     173  developer integer NOT NULL, 
     174  "extendedWarranty" integer NOT NULL, 
     175  PRIMARY KEY (id) 
     176); 
     177SET UNIQUE FOR computers(id); 
     178 
     179CREATE TABLE posts ( 
     180  id integer DEFAULT unique, 
     181  author_id integer, 
     182  title varchar(255), 
     183  type varchar(255), 
     184  body varchar(65536), 
     185  PRIMARY KEY (id) 
     186); 
     187SET UNIQUE FOR posts(id); 
     188 
     189CREATE TABLE comments ( 
     190  id integer DEFAULT unique, 
     191  post_id integer, 
     192  type varchar(255), 
     193  body varchar(65536), 
     194  PRIMARY KEY (id) 
     195); 
     196SET UNIQUE FOR comments(id); 
     197 
     198CREATE TABLE authors ( 
     199  id integer DEFAULT unique, 
     200  name varchar(255) default NULL, 
     201  PRIMARY KEY (id) 
     202); 
     203SET UNIQUE FOR authors(id); 
     204 
     205CREATE TABLE tasks ( 
     206  id integer DEFAULT unique, 
     207  starting timestamp, 
     208  ending timestamp, 
     209  PRIMARY KEY (id) 
     210); 
     211SET UNIQUE FOR tasks(id); 
     212 
     213CREATE TABLE categories ( 
     214  id integer DEFAULT unique, 
     215  name varchar(255), 
     216  type varchar(255), 
     217  PRIMARY KEY (id) 
     218); 
     219SET UNIQUE FOR categories(id); 
     220 
     221CREATE TABLE categories_posts ( 
     222  category_id integer NOT NULL, 
     223  post_id integer NOT NULL 
     224); 
     225 
     226CREATE TABLE fk_test_has_pk ( 
     227  id INTEGER NOT NULL PRIMARY KEY 
     228); 
     229SET UNIQUE FOR fk_test_has_pk(id); 
     230 
     231CREATE TABLE fk_test_has_fk ( 
     232  id    INTEGER NOT NULL PRIMARY KEY, 
     233  fk_id INTEGER NOT NULL REFERENCES fk_test_has_fk(id) 
     234); 
     235SET UNIQUE FOR fk_test_has_fk(id); 
     236 
     237CREATE TABLE keyboards ( 
     238  key_number integer DEFAULT unique, 
     239  "name" character varying(50), 
     240  PRIMARY KEY (key_number) 
     241); 
     242SET UNIQUE FOR keyboards(key_number); 
     243 
     244create table "legacy_things" 
     245( 
     246  "id" int, 
     247  "tps_report_number" int default NULL, 
     248  "version" int default 0 not null, 
     249  primary key ("id") 
     250); 
     251SET UNIQUE FOR legacy_things(id); 
  • activerecord/test/fixtures/db_definitions/frontbase.drop.sql

    old new  
     1DROP TABLE accounts CASCADE; 
     2DROP TABLE funny_jokes CASCADE; 
     3DROP TABLE companies CASCADE; 
     4DROP TABLE topics CASCADE; 
     5DROP TABLE developers CASCADE; 
     6DROP TABLE projects CASCADE; 
     7DROP TABLE developers_projects CASCADE; 
     8DROP TABLE orders CASCADE; 
     9DROP TABLE customers CASCADE; 
     10DROP TABLE movies CASCADE; 
     11DROP TABLE subscribers CASCADE; 
     12DROP TABLE booleantests CASCADE; 
     13DROP TABLE auto_id_tests CASCADE; 
     14DROP TABLE entrants CASCADE; 
     15DROP TABLE colnametests CASCADE; 
     16DROP TABLE mixins CASCADE; 
     17DROP TABLE people CASCADE; 
     18DROP TABLE readers CASCADE; 
     19DROP TABLE binaries CASCADE; 
     20DROP TABLE computers CASCADE; 
     21DROP TABLE posts CASCADE; 
     22DROP TABLE comments CASCADE; 
     23DROP TABLE authors CASCADE; 
     24DROP TABLE tasks CASCADE; 
     25DROP TABLE categories CASCADE; 
     26DROP TABLE categories_posts CASCADE; 
     27DROP TABLE fk_test_has_fk CASCADE; 
     28DROP TABLE fk_test_has_pk CASCADE; 
     29DROP TABLE keyboards CASCADE; 
     30DROP TABLE legacy_things CASCADE; 
  • activerecord/test/fixtures/db_definitions/frontbase2.sql

    old new  
     1CREATE TABLE courses ( 
     2  id integer DEFAULT unique, 
     3  name varchar(100) 
     4); 
  • activerecord/test/fixtures/db_definitions/frontbase2.drop.sql

    old new  
  • activerecord/Rakefile

    old new  
    2727 
    2828# Run the unit tests 
    2929 
    30 for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase
     30for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase frontbase
    3131  Rake::TestTask.new("test_#{adapter}") { |t| 
    3232    t.libs << "test" << "test/connections/native_#{adapter}" 
    3333    t.pattern = "test/*_test{,_#{adapter}}.rb" 
  • activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb

    old new  
     1require 'active_record/connection_adapters/abstract_adapter' 
     2require 'frontbase' 
     3 
     4module ActiveRecord 
     5     
     6  class Base 
     7 
     8    # FrontBase only supports one unnamed sequence per table 
     9    def self.set_sequence_name( value=nil, &block ) 
     10    end 
     11     
     12    # Establishes a connection to the database that's used by all Active Record objects. 
     13    def self.frontbase_connection(config) # :nodoc: 
     14      config = config.symbolize_keys 
     15      database     = config[:database] 
     16      port         = config[:port] 
     17      host         = config[:host] 
     18      username     = config[:username] 
     19      password     = config[:password] 
     20      dbpassword   = config[:dbpassword] 
     21      session_name = config[:session_name] 
     22 
     23      if dbpassword == nil 
     24        dbpassword = "" 
     25      end 
     26       
     27      # Turn off colorization since it make tail/less output difficult 
     28      self.colorize_logging = false; 
     29       
     30       
     31      connection = FBSQL_Connect.connect(host, port, database, username, password, dbpassword,session_name) 
     32      ConnectionAdapters::FrontBaseAdapter.new(connection, logger, [host, port, database, username, password, dbpassword, session_name], config) 
     33    end         
     34  end 
     35   
     36  module ConnectionAdapters 
     37 
     38    class FrontBaseColumn < Column #:nodoc: 
     39      attr_reader :fb_autogen 
     40       
     41      def initialize(base, name, type, typename, limit, precision, scale, default, nullable) 
     42         
     43        @base, @name = base, name 
     44        @type = simplified_type(type,typename,limit) 
     45        @limit = limit 
     46        @precision, @scale = precision, scale 
     47        @default = default 
     48        @null = nullable 
     49        @text    = [:string, :text].include? @type 
     50        @number  = [:float, :integer].include? @type 
     51        @fb_autogen = false 
     52        if @default 
     53          if @type == :boolean then 
     54            @default.downcase! 
     55          end 
     56          @default.gsub!(/^'(.*)'$/,'\1') if @text 
     57          @fb_autogen =  @default.include?("SELECT UNIQUE FROM") 
     58          @default = type_cast(@default) 
     59        end 
     60      end 
     61       
     62      def self.hex_encode(value) 
     63        retvalue = "" 
     64        value.each_byte do |b| 
     65          retvalue << sprintf("%02X", b) 
     66        end 
     67        retvalue 
     68      end 
     69 
     70      def self.hex_decode(value) 
     71        retvalue = "" 
     72        value.scan(/../) do |h| 
     73          c = h.hex 
     74          retvalue << c.chr 
     75        end 
     76        retvalue 
     77      end 
     78 
     79      private 
     80      def simplified_type(field_type, type_name,limit) 
     81        ret_type = :string 
     82#         puts "typecode: [#{field_type}] [#{type_name}]" 
     83        case field_type 
     84          when 1 then ret_type = :boolean # BOOLEAN 
     85          when 2 then ret_type = :integer # INTEGER 
     86          when 4 then ret_type = :float   # FLOAT 
     87          when 10 then ret_type = :string # CHARACTER VARYING 
     88          when 11 then ret_type = :binary 
     89          when 12 then ret_type = :binary 
     90          when 13 then ret_type = :date 
     91          when 14 then ret_type = :time 
     92          when 16 then ret_type = :timestamp 
     93          when 22 then ret_type = :integer # TINYINT 
     94          else 
     95            puts "ERROR: Unknown typecode: [#{field_type}] [#{type_name}]" 
     96        end 
     97        ret_type 
     98      end 
     99    end 
     100 
     101    class FrontBaseAdapter < AbstractAdapter 
     102       
     103      def initialize(connection, logger, connection_options, config) 
     104        super(connection, logger) 
     105        @connection_options, @config = connection_options, config 
     106        @transaction_mode = :pessimistic 
     107         
     108        # threaded_connections_test.rb will fail unless we set the session 
     109        # to optimistic locking mode 
     110        set_pessimistic_transactions 
     111#         execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC" 
     112      end 
     113 
     114      # Returns the human-readable name of the adapter.  Use mixed case - one 
     115      # can always use downcase if needed. 
     116      def adapter_name #:nodoc: 
     117        'FrontBase' 
     118      end 
     119 
     120      # Does this adapter support migrations?  Backend specific, as the 
     121      # abstract adapter always returns +false+. 
     122      def supports_migrations? #:nodoc: 
     123        true 
     124      end 
     125 
     126      def native_database_types #:nodoc 
     127        # !!! Lookup actual FB datatypes 
     128        { 
     129          :primary_key => "INTEGER DEFAULT UNIQUE PRIMARY KEY", 
     130          :string      => { :name => "VARCHAR", :limit => 255 }, 
     131          :text        => { :name => "VARCHAR", :limit => 1073741824 }, 
     132          :integer     => { :name => "INTEGER" }, 
     133          :float       => { :name => "FLOAT" }, 
     134          :datetime    => { :name => "TIMESTAMP" }, 
     135          :timestamp   => { :name => "TIMESTAMP" }, 
     136          :time        => { :name => "TIME" }, 
     137          :date        => { :name => "DATE" }, 
     138          :binary      => { :name => "BYTE VARYING" }, 
     139          :boolean     => { :name => "BOOLEAN" } 
     140        } 
     141      end 
     142 
     143 
     144      # QUOTING ================================================== 
     145 
     146      # Quotes the column value to help prevent 
     147      # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. 
     148      def quote(value, column = nil) 
     149        case value 
     150          when String 
     151            if column && column.type == :binary 
     152              s = column.class.string_to_binary(value).unpack("H*")[0] 
     153              "X'#{s}'" 
     154            elsif column && [:integer, :float].include?(column.type)  
     155              value.to_s 
     156            else 
     157              "'#{quote_string(value)}'" # ' (for ruby-mode) 
     158            end 
     159          when NilClass              then "NULL" 
     160          when TrueClass             then (column && column.type == :integer ? '1' : quoted_true) 
     161          when FalseClass            then (column && column.type == :integer ? '0' : quoted_false) 
     162          when Float, Fixnum, Bignum then value.to_s 
     163          when Time, Date, DateTime 
     164            if column 
     165              case column.type 
     166                when :date then "DATE '#{value.strftime("%Y-%m-%d")}'" 
     167                when :time then "TIME '#{value.strftime("%H:%M:%S")}'" 
     168                when :timestamp then "TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'" 
     169              else 
     170                raise NotImplementedError, "Unknown column type!" 
     171              end # case 
     172            else # Column wasn't passed in, so try to guess the right type 
     173              if value.kind_of? Date 
     174                "DATE '#{value.strftime("%Y-%m-%d")}'" 
     175              else 
     176                if value.hour == 0 && value.min == 0 && value.sec == 0 
     177                  "TIME '#{value.strftime("%H:%M:%S")}'" 
     178                else 
     179                  "TIMESTAMP '#{quoted_date(value)}'" 
     180                end 
     181              end  
     182            end #if column 
     183          else "'#{quote_string(value.to_yaml)}'" 
     184        end #case 
     185      end # def 
     186 
     187      # Quotes a string, escaping any ' (single quote) characters. 
     188      def quote_string(s) 
     189        s.gsub(/'/, "''") # ' (for ruby-mode) 
     190      end 
     191 
     192      def quote_column_name(name) #:nodoc: 
     193        "\"#{name}\"" 
     194      end 
     195 
     196      def quoted_true 
     197        "true" 
     198      end 
     199       
     200      def quoted_false 
     201        "false" 
     202      end 
     203 
     204 
     205      # CONNECTION MANAGEMENT ==================================== 
     206 
     207      def active? 
     208          retvalue = true 
     209          begin 
     210            retvalue = true if @connection.status == 1 
     211          rescue => e 
     212            retvalue = false 
     213          end 
     214          retvalue 
     215      end 
     216 
     217      def reconnect! 
     218        @connection.close rescue nil 
     219      
     220        @connection = FBSQL_Connect.connect(@connection_options[0], 
     221        @connection_options[1], 
     222        @connection_options[2], 
     223        @connection_options[3], 
     224        @connection_options[4], 
     225        @connection_options[5], 
     226        @connection_options[6])         
     227      end 
     228 
     229      # Close this connection 
     230      def disconnect! 
     231        @connection.close rescue nil 
     232        @active = false 
     233      end 
     234 
     235      # DATABASE STATEMENTS ====================================== 
     236 
     237      # Returns an array of record hashes with the column names as keys and 
     238      # column values as values. 
     239      def select_all(sql, name = nil) #:nodoc: 
     240        fbsql = cleanup_fb_sql(sql) 
     241        retValue = [] 
     242        fbresult = query(sql, name) 
     243#         puts "select_all SQL -> #{fbsql}" 
     244        columns = fbresult.columns 
     245        fbresult.each do |row| 
     246#           puts "SQL <- #{row.inspect}" 
     247          hashed_row = {}     
     248          colnum = 0 
     249          row.each do |col| 
     250            hashed_row[columns[colnum]] = col 
     251            colnum += 1 
     252          end 
     253          retValue << hashed_row 
     254        end 
     255        retValue 
     256      end 
     257 
     258      def select_one(sql, name = nil) #:nodoc: 
     259        fbsql = cleanup_fb_sql(sql) 
     260        retValue = [] 
     261        fbresult = query(fbsql, name) 
     262#         puts "SQL -> #{fbsql}" 
     263        columns = fbresult.columns 
     264         
     265        fbresult.each do |row| 
     266#           puts "SQL <- #{row.inspect}" 
     267          hashed_row = {}     
     268          colnum = 0 
     269          row.each do |col| 
     270            hashed_row[columns[colnum]] = col 
     271            colnum += 1 
     272          end 
     273          retValue << hashed_row 
     274          break 
     275        end 
     276        fbresult.clear 
     277        retValue = retValue.first 
     278      end 
     279 
     280      def query(sql, name = nil) #:nodoc: 
     281        fbsql = cleanup_fb_sql(sql) 
     282#         puts "SQL -> #{fbsql}" 
     283        log(fbsql, name) { @connection.query(fbsql) } 
     284      rescue => e 
     285#         puts "FB Exception: #{e.inspect}" 
     286        raise e 
     287      end 
     288 
     289      def execute(sql, name = nil) #:nodoc: 
     290        fbsql = cleanup_fb_sql(sql) 
     291#         puts "SQL -> #{fbsql}" 
     292        log(fbsql, name) { @connection.query(fbsql) } 
     293        rescue ActiveRecord::StatementInvalid => e 
     294          a = e.message.scan(/Table name - \w* - exists/) 
     295          raise e if a.length == 0 
     296          raise e 
     297      end 
     298       
     299      # Returns the last auto-generated ID from the affected table. 
     300      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: 
     301#         puts "SQL -> #{sql.inspect}" 
     302        result = execute(sql, name) 
     303        id_value || pk 
     304      end 
     305 
     306      # Executes the update statement and returns the number of rows affected. 
     307      def update(sql, name = nil) #:nodoc: 
     308#         puts "SQL -> #{sql.inspect}" 
     309        query(sql, name).num_rows 
     310        #@connection.affected_rows 
     311      end 
     312 
     313      alias_method :delete, :update #:nodoc: 
     314 
     315      def set_pessimistic_transactions 
     316        if @transactin_mode == :optimistic 
     317          execute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, LOCKING PESSIMISTIC, READ WRITE" 
     318          @transaction_mode = :pessimistic 
     319        end 
     320      end 
     321 
     322      def set_optimistic_transactions 
     323        if @transaction_mode == :pessimistic 
     324          execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC" 
     325          @transaction_mode = :optimistic 
     326        end 
     327      end 
     328 
     329      def begin_db_transaction #:nodoc: 
     330        execute "SET COMMIT FALSE" 
     331      end 
     332 
     333      def commit_db_transaction #:nodoc: 
     334        execute "COMMIT" 
     335      ensure 
     336        execute "SET COMMIT TRUE" 
     337      end 
     338 
     339      def rollback_db_transaction #:nodoc: 
     340        execute "ROLLBACK" 
     341        ensure 
     342          execute "SET COMMIT TRUE" 
     343      end 
     344 
     345      def add_limit_offset!(sql, options) #:nodoc 
     346        if limit = options[:limit] 
     347          offset = options[:offset] || 0 
     348         
     349# Here is the full syntax FrontBase supports: 
     350# (from gclem@frontbase.com) 
     351#  
     352#       TOP <limit - unsigned integer> 
     353#       TOP ( <offset expr>, <limit expr>) 
     354         
     355          # "TOP 0" is not allowed, so we have 
     356          # to use a cheap trick. 
     357          if limit == 0 
     358            if sql =~ /WHERE/i 
     359              sql.sub!(/WHERE/i, 'WHERE 0 = 1 AND ') 
     360            elsif 
     361              sql =~ /ORDER\s+BY/i 
     362              sql.sub!(/ORDER\s+BY/i, 'WHERE 0 = 1 ORDER BY') 
     363            else 
     364              sql << 'WHERE 0 = 1' 
     365            end 
     366          else 
     367            if offset == 0 
     368              sql.replace sql.gsub("SELECT ","SELECT TOP #{limit} ") 
     369            else 
     370              sql.replace sql.gsub("SELECT ","SELECT TOP(#{offset},#{limit}) ") 
     371          end 
     372        end 
     373           
     374        end 
     375      end 
     376 
     377      def prefetch_primary_key?(table_name = nil) 
     378        true 
     379      end 
     380 
     381      # Returns the next sequence value from a sequence generator. Not generally 
     382      # called directly; used by ActiveRecord to get the next primary key value 
     383      # when inserting a new database record (see #prefetch_primary_key?). 
     384      def next_sequence_value(sequence_name) 
     385        unique = select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") 
     386         
     387        # The test cases cannot handle a zero primary key 
     388        if unique == 0 then unique = select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") end 
     389        unique  
     390      end 
     391 
     392      def default_sequence_name(table, column) 
     393        table; 
     394      end 
     395 
     396      # Set the sequence to the max value of the table's column. 
     397      def reset_sequence!(table, column, sequence = nil) 
     398         
     399      end 
     400 
     401      def classes_for_table_name(table) 
     402        klasses = [] 
     403        # !!! Hack because subclasses is a protected method... 
     404        subclasses = ActiveRecord::Base.class_eval('@@subclasses') 
     405        subclasses.values.each do |a| 
     406          a.each {|n| klasses << n} 
     407        end 
     408        subset_klasses = klasses.select {|k| k.table_name == table} 
     409      end 
     410       
     411      def reset_pk_sequence!(table, pk = nil, sequence = nil) 
     412        klasses = classes_for_table_name(table) 
     413        klass = klasses.nil? ? nil : klasses[0] 
     414        pk = klass.primary_key if klass != nil 
     415        if pk && klass.columns_hash[pk].type == :integer 
     416          mpk = select_value("SELECT MAX(#{pk}) FROM #{table}") 
     417          execute("SET UNIQUE FOR #{klass.table_name}(#{pk})") 
     418        end 
     419         
     420      end 
     421 
     422      # SCHEMA STATEMENTS ======================================== 
     423 
     424      def structure_dump #:nodoc: 
     425        select_all("SHOW TABLES").inject("") do |structure, table| 
     426          structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n" 
     427        end 
     428      end 
     429 
     430      def recreate_database(name) #:nodoc: 
     431        drop_database(name) 
     432        create_database(name) 
     433      end 
     434 
     435      def create_database(name) #:nodoc: 
     436        execute "CREATE DATABASE #{name}" 
     437      end 
     438       
     439      def drop_database(name) #:nodoc: 
     440        puts "TRACE: drop_database" 
     441        execute "DROP DATABASE #{name}" 
     442      end 
     443 
     444 
     445      def tables(name = nil) #:nodoc: 
     446        sql = "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" 
     447         
     448        select_values(sql,nil) 
     449      end 
     450 
     451      def indexes(table_name, name = nil)#:nodoc: 
     452        indexes = [] 
     453        current_index = nil 
     454        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" 
     455 
     456        columns = [] 
     457        query(sql).each do |row| 
     458          index_name   = row[0] 
     459          ord_position = row[1] 
     460          ndx_colcount = row[2] 
     461          index_type   = row[3] 
     462          column_name  = row[4] 
     463           
     464          if index_type == 1 
     465            isUnique = true 
     466          else 
     467            isUnique = false 
     468          end 
     469           
     470          columns << column_name 
     471          if ord_position == ndx_colcount 
     472            indexes << IndexDefinition.new(table_name, index_name, isUnique , columns) 
     473            columns = [] 
     474          end 
     475        end 
     476        indexes 
     477      end 
     478 
     479      def columns(table_name, name = nil)#:nodoc: 
     480        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" 
     481        rawresults = query(sql,name) 
     482        columns = [] 
     483        rawresults.each do |field| 
     484          typestring = field[5] 
     485          typecode = field[6] 
     486          base = field[0] 
     487          name = field[1] 
     488          limit = field[7] 
     489          precision = field[8] 
     490          scale = field[9] 
     491          default = field[4] 
     492          nullable = field[3] 
     493          columns << FrontBaseColumn.new(base, name, typecode, typestring, limit, precision, scale, default, nullable) 
     494         end 
     495        columns 
     496      end 
     497       
     498      def create_table(name, options = {}) 
     499        table_definition = TableDefinition.new(self) 
     500        table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false 
     501 
     502        yield table_definition 
     503 
     504        if options[:force] 
     505          drop_table(name) rescue nil 
     506        end 
     507 
     508        create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " 
     509        create_sql << "#{name} (" 
     510        create_sql << table_definition.to_sql 
     511        create_sql << ") #{options[:options]}" 
     512        begin_db_transaction 
     513        execute create_sql 
     514        commit_db_transaction 
     515        rescue ActiveRecord::StatementInvalid => e 
     516          a = e.message.scan(/Table name - \w* - exists/) 
     517          raise e if a.length == 0 
     518      end 
     519       
     520      def rename_table(name, new_name) 
     521        columns = columns(name) 
     522        pkcol = columns.find {|c| c.fb_autogen} 
     523        execute "ALTER TABLE NAME #{name} TO #{new_name}" 
     524        if pkcol 
     525          change_column_default(new_name,pkcol.name,"UNIQUE") 
     526          begin_db_transaction 
     527          mpk = select_value("SELECT MAX(#{pkcol.name}) FROM #{new_name}") 
     528          mpk = 0 if mpk.nil? 
     529          execute "SET UNIQUE=#{mpk} FOR #{new_name}" 
     530          commit_db_transaction 
     531        end 
     532      end   
     533 
     534      # Drops a table from the database. 
     535      def drop_table(name) 
     536        execute "DROP TABLE #{name} RESTRICT" 
     537      rescue ActiveRecord::StatementInvalid => e 
     538        a = e.message.scan(/Referenced TABLE - \w* - does not exist/) 
     539        raise e if a.length == 0 
     540      end 
     541 
     542      # Adds a new column to the named table. 
     543      # See TableDefinition#column for details of the options you can use. 
     544      def add_column(table_name, column_name, type, options = {}) 
     545        add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}" 
     546        options[:type] = type 
     547        add_column_options!(add_column_sql, options) 
     548        execute(add_column_sql) 
     549      end 
     550 
     551      def add_column_options!(sql, options) #:nodoc: 
     552        default_value = quote(options[:default], options[:column]) 
     553        if options[:default] 
     554          if options[:type] == :boolean 
     555            default_value = options[:default] == 0 ? quoted_false : quoted_true 
     556          end 
     557        end 
     558        sql << " DEFAULT #{default_value}" unless options[:default].nil? 
     559        sql << " NOT NULL" if options[:null] == false 
     560      end 
     561 
     562      # Removes the column from the table definition. 
     563      # ===== Examples 
     564      #  remove_column(:suppliers, :qualification) 
     565      def remove_column(table_name, column_name) 
     566        execute "ALTER TABLE #{table_name} DROP #{column_name} RESTRICT" 
     567      end 
     568 
     569      def remove_index(table_name, options = {}) #:nodoc: 
     570        execute "DROP INDEX #{index_name(table_name, options)}" 
     571      end 
     572 
     573      def change_column_default(table_name, column_name, default) #:nodoc: 
     574        execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{default}" if default != "NULL" 
     575      end 
     576 
     577      def change_column(table_name, column_name, type, options = {}) #:nodoc: 
     578        change_column_sql = "ALTER COLUMN \"#{table_name}\".\"#{column_name}\" TO #{type_to_sql(type, options[:limit])}" 
     579        execute(change_column_sql) 
     580        change_column_sql = "ALTER TABLE \"#{table_name}\" ALTER COLUMN \"#{column_name}\"" 
     581 
     582        default_value = quote(options[:default], options[:column]) 
     583        if options[:default] 
     584          if type == :boolean 
     585            default_value = options[:default] == 0 ? quoted_false : quoted_true 
     586          end 
     587        end 
     588 
     589        if default_value != "NULL" 
     590          change_column_sql << " SET DEFAULT #{default_value}" 
     591          execute(change_column_sql) 
     592        end 
     593         
     594#         change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}" 
     595#         add_column_options!(change_column_sql, options) 
     596#         execute(change_column_sql) 
     597      end 
     598 
     599      def rename_column(table_name, column_name, new_column_name) #:nodoc: 
     600        execute "ALTER COLUMN NAME \"#{table_name}\".\"#{column_name}\" TO \"#{new_column_name}\"" 
     601      end 
     602             
     603      private 
     604       
     605      # Clean up sql to make it something FrontBase can digest 
     606      def cleanup_fb_sql(sql) #:nodoc: 
     607        cleansql = sql.gsub("!=","<>") # Turn non-standard != into standard <> 
     608        lines = cleansql.split("\n") 
     609        lines = lines.select { |l| l.length > 0} # Strip blank lines 
     610        lines = lines.select { |l| !(l =~ /^--/)} # Strip comments 
     611        cleansql = lines.join("\n") 
     612        cleansql 
     613      end 
     614    end 
     615  end 
     616end