aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters
diff options
context:
space:
mode:
authorMarcel Molina <marcel@vernix.org>2006-04-27 22:39:45 +0000
committerMarcel Molina <marcel@vernix.org>2006-04-27 22:39:45 +0000
commitb2c0ddf033fbf143539ad8a7630747a0e866e4fc (patch)
tree2aae48e00aa7d4951e711ebb667d8f37e0c8a6d0 /activerecord/lib/active_record/connection_adapters
parentdd5397a57c39da78f22e0fec517ff6e5ede7628a (diff)
downloadrails-b2c0ddf033fbf143539ad8a7630747a0e866e4fc.tar.gz
rails-b2c0ddf033fbf143539ad8a7630747a0e866e4fc.tar.bz2
rails-b2c0ddf033fbf143539ad8a7630747a0e866e4fc.zip
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]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4291 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
-rw-r--r--activerecord/lib/active_record/connection_adapters/frontbase_adapter.rb837
1 files changed, 837 insertions, 0 deletions
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 = "<INVALID>"
+
+ 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 <limit - unsigned integer>
+# TOP ( <offset expr>, <limit expr>)
+
+ # "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