2007-01-22 14:43:50 +01:00
|
|
|
require 'active_record/connection_adapters/abstract_adapter'
|
|
|
|
|
|
|
|
module ActiveRecord
|
|
|
|
class Base
|
|
|
|
# Establishes a connection to the database that's used by all Active Record objects
|
|
|
|
def self.openbase_connection(config) # :nodoc:
|
|
|
|
require_library_or_gem 'openbase' unless self.class.const_defined?(:OpenBase)
|
|
|
|
|
|
|
|
config = config.symbolize_keys
|
|
|
|
host = config[:host]
|
|
|
|
username = config[:username].to_s
|
|
|
|
password = config[:password].to_s
|
|
|
|
|
|
|
|
|
|
|
|
if config.has_key?(:database)
|
|
|
|
database = config[:database]
|
|
|
|
else
|
|
|
|
raise ArgumentError, "No database specified. Missing argument: database."
|
|
|
|
end
|
|
|
|
|
|
|
|
oba = ConnectionAdapters::OpenBaseAdapter.new(
|
|
|
|
OpenBase.new(database, host, username, password), logger
|
|
|
|
)
|
|
|
|
|
|
|
|
oba
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
module ConnectionAdapters
|
|
|
|
class OpenBaseColumn < Column #:nodoc:
|
|
|
|
private
|
|
|
|
def simplified_type(field_type)
|
|
|
|
return :integer if field_type.downcase =~ /long/
|
2007-02-09 09:04:31 +01:00
|
|
|
return :decimal if field_type.downcase == "money"
|
2007-01-22 14:43:50 +01:00
|
|
|
return :binary if field_type.downcase == "object"
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
# The OpenBase adapter works with the Ruby/Openbase driver by Tetsuya Suzuki.
|
|
|
|
# http://www.spice-of-life.net/ruby-openbase/ (needs version 0.7.3+)
|
|
|
|
#
|
|
|
|
# Options:
|
|
|
|
#
|
|
|
|
# * <tt>:host</tt> -- Defaults to localhost
|
|
|
|
# * <tt>:username</tt> -- Defaults to nothing
|
|
|
|
# * <tt>:password</tt> -- Defaults to nothing
|
|
|
|
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
|
|
|
#
|
|
|
|
# The OpenBase adapter will make use of OpenBase's ability to generate unique ids
|
|
|
|
# for any column with an unique index applied. Thus, if the value of a primary
|
|
|
|
# key is not specified at the time an INSERT is performed, the adapter will prefetch
|
|
|
|
# a unique id for the primary key. This prefetching is also necessary in order
|
|
|
|
# to return the id after an insert.
|
|
|
|
#
|
|
|
|
# Caveat: Operations involving LIMIT and OFFSET do not yet work!
|
|
|
|
#
|
2007-02-09 09:04:31 +01:00
|
|
|
# Maintainer: derrick.spell@gmail.com
|
2007-01-22 14:43:50 +01:00
|
|
|
class OpenBaseAdapter < AbstractAdapter
|
|
|
|
def adapter_name
|
|
|
|
'OpenBase'
|
|
|
|
end
|
|
|
|
|
|
|
|
def native_database_types
|
|
|
|
{
|
|
|
|
:primary_key => "integer UNIQUE INDEX DEFAULT _rowid",
|
|
|
|
:string => { :name => "char", :limit => 4096 },
|
|
|
|
:text => { :name => "text" },
|
|
|
|
:integer => { :name => "integer" },
|
|
|
|
:float => { :name => "float" },
|
2007-02-09 09:04:31 +01:00
|
|
|
:decimal => { :name => "decimal" },
|
2007-01-22 14:43:50 +01:00
|
|
|
:datetime => { :name => "datetime" },
|
|
|
|
:timestamp => { :name => "timestamp" },
|
|
|
|
:time => { :name => "time" },
|
|
|
|
:date => { :name => "date" },
|
|
|
|
:binary => { :name => "object" },
|
|
|
|
:boolean => { :name => "boolean" }
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def supports_migrations?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def prefetch_primary_key?(table_name = nil)
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def default_sequence_name(table_name, primary_key) # :nodoc:
|
|
|
|
"#{table_name} #{primary_key}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def next_sequence_value(sequence_name)
|
|
|
|
ary = sequence_name.split(' ')
|
|
|
|
if (!ary[1]) then
|
|
|
|
ary[0] =~ /(\w+)_nonstd_seq/
|
|
|
|
ary[0] = $1
|
|
|
|
end
|
|
|
|
@connection.unique_row_id(ary[0], ary[1])
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# QUOTING ==================================================
|
|
|
|
|
|
|
|
def quote(value, column = nil)
|
|
|
|
if value.kind_of?(String) && column && column.type == :binary
|
|
|
|
"'#{@connection.insert_binary(value)}'"
|
|
|
|
else
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_true
|
|
|
|
"1"
|
|
|
|
end
|
|
|
|
|
|
|
|
def quoted_false
|
|
|
|
"0"
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# DATABASE STATEMENTS ======================================
|
|
|
|
|
2007-02-09 09:04:31 +01:00
|
|
|
def add_limit_offset!(sql, options) #:nodoc:
|
2007-01-22 14:43:50 +01:00
|
|
|
if limit = options[:limit]
|
|
|
|
unless offset = options[:offset]
|
|
|
|
sql << " RETURN RESULTS #{limit}"
|
|
|
|
else
|
|
|
|
limit = limit + offset
|
|
|
|
sql << " RETURN RESULTS #{offset} TO #{limit}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def select_all(sql, name = nil) #:nodoc:
|
|
|
|
select(sql, name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def select_one(sql, name = nil) #:nodoc:
|
|
|
|
add_limit_offset!(sql,{:limit => 1})
|
|
|
|
results = select(sql, name)
|
|
|
|
results.first if results
|
|
|
|
end
|
|
|
|
|
|
|
|
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
|
|
|
execute(sql, name)
|
|
|
|
update_nulls_after_insert(sql, name, pk, id_value, sequence_name)
|
|
|
|
id_value
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute(sql, name = nil) #:nodoc:
|
|
|
|
log(sql, name) { @connection.execute(sql) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def update(sql, name = nil) #:nodoc:
|
|
|
|
execute(sql, name).rows_affected
|
|
|
|
end
|
|
|
|
|
|
|
|
alias_method :delete, :update #:nodoc:
|
|
|
|
#=begin
|
|
|
|
def begin_db_transaction #:nodoc:
|
|
|
|
execute "START TRANSACTION"
|
|
|
|
rescue Exception
|
|
|
|
# Transactions aren't supported
|
|
|
|
end
|
|
|
|
|
|
|
|
def commit_db_transaction #:nodoc:
|
|
|
|
execute "COMMIT"
|
|
|
|
rescue Exception
|
|
|
|
# Transactions aren't supported
|
|
|
|
end
|
|
|
|
|
|
|
|
def rollback_db_transaction #:nodoc:
|
|
|
|
execute "ROLLBACK"
|
|
|
|
rescue Exception
|
|
|
|
# Transactions aren't supported
|
|
|
|
end
|
|
|
|
#=end
|
|
|
|
|
|
|
|
# SCHEMA STATEMENTS ========================================
|
|
|
|
|
|
|
|
# Return the list of all tables in the schema search path.
|
|
|
|
def tables(name = nil) #:nodoc:
|
|
|
|
tables = @connection.tables
|
|
|
|
tables.reject! { |t| /\A_SYS_/ === t }
|
|
|
|
tables
|
|
|
|
end
|
|
|
|
|
|
|
|
def columns(table_name, name = nil) #:nodoc:
|
|
|
|
sql = "SELECT * FROM _sys_tables "
|
|
|
|
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
|
|
|
|
sql << "ORDER BY columnNumber"
|
|
|
|
columns = []
|
|
|
|
select_all(sql, name).each do |row|
|
|
|
|
columns << OpenBaseColumn.new(row["fieldname"],
|
|
|
|
default_value(row["defaultvalue"]),
|
|
|
|
sql_type_name(row["typename"],row["length"]),
|
|
|
|
row["notnull"]
|
|
|
|
)
|
|
|
|
# breakpoint() if row["fieldname"] == "content"
|
|
|
|
end
|
|
|
|
columns
|
|
|
|
end
|
|
|
|
|
|
|
|
def indexes(table_name, name = nil)#:nodoc:
|
|
|
|
sql = "SELECT fieldname, notnull, searchindex, uniqueindex, clusteredindex FROM _sys_tables "
|
|
|
|
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
|
|
|
|
sql << "AND primarykey=0 "
|
|
|
|
sql << "AND (searchindex=1 OR uniqueindex=1 OR clusteredindex=1) "
|
|
|
|
sql << "ORDER BY columnNumber"
|
|
|
|
indexes = []
|
|
|
|
execute(sql, name).each do |row|
|
|
|
|
indexes << IndexDefinition.new(table_name,index_name(row),row[3]==1,[row[0]])
|
|
|
|
end
|
|
|
|
indexes
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
def select(sql, name = nil)
|
|
|
|
sql = translate_sql(sql)
|
|
|
|
results = execute(sql, name)
|
|
|
|
|
|
|
|
date_cols = []
|
|
|
|
col_names = []
|
|
|
|
results.column_infos.each do |info|
|
|
|
|
col_names << info.name
|
|
|
|
date_cols << info.name if info.type == "date"
|
|
|
|
end
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
if ( results.rows_affected )
|
|
|
|
results.each do |row| # loop through result rows
|
|
|
|
hashed_row = {}
|
|
|
|
row.each_index do |index|
|
|
|
|
hashed_row["#{col_names[index]}"] = row[index] unless col_names[index] == "_rowid"
|
|
|
|
end
|
|
|
|
date_cols.each do |name|
|
|
|
|
unless hashed_row["#{name}"].nil? or hashed_row["#{name}"].empty?
|
|
|
|
hashed_row["#{name}"] = Date.parse(hashed_row["#{name}"],false).to_s
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rows << hashed_row
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rows
|
|
|
|
end
|
|
|
|
|
|
|
|
def default_value(value)
|
|
|
|
# Boolean type values
|
|
|
|
return true if value =~ /true/
|
|
|
|
return false if value =~ /false/
|
|
|
|
|
|
|
|
# Date / Time magic values
|
|
|
|
return Time.now.to_s if value =~ /^now\(\)/i
|
|
|
|
|
|
|
|
# Empty strings should be set to null
|
|
|
|
return nil if value.empty?
|
|
|
|
|
|
|
|
# Otherwise return what we got from OpenBase
|
|
|
|
# and hope for the best...
|
|
|
|
return value
|
|
|
|
end
|
|
|
|
|
|
|
|
def sql_type_name(type_name, length)
|
|
|
|
return "#{type_name}(#{length})" if ( type_name =~ /char/ )
|
|
|
|
type_name
|
|
|
|
end
|
|
|
|
|
|
|
|
def index_name(row = [])
|
|
|
|
name = ""
|
|
|
|
name << "UNIQUE " if row[3]
|
|
|
|
name << "CLUSTERED " if row[4]
|
|
|
|
name << "INDEX"
|
|
|
|
name
|
|
|
|
end
|
|
|
|
|
|
|
|
def translate_sql(sql)
|
|
|
|
|
|
|
|
# Change table.* to list of columns in table
|
|
|
|
while (sql =~ /SELECT.*\s(\w+)\.\*/)
|
|
|
|
table = $1
|
|
|
|
cols = columns(table)
|
|
|
|
if ( cols.size == 0 ) then
|
|
|
|
# Maybe this is a table alias
|
|
|
|
sql =~ /FROM(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
|
|
|
|
$1 =~ /[\s|,](\w+)\s+#{table}[\s|,]/ # get the tablename for this alias
|
|
|
|
cols = columns($1)
|
|
|
|
end
|
|
|
|
select_columns = []
|
|
|
|
cols.each do |col|
|
|
|
|
select_columns << table + '.' + col.name
|
|
|
|
end
|
|
|
|
sql.gsub!(table + '.*',select_columns.join(", ")) if select_columns
|
|
|
|
end
|
|
|
|
|
|
|
|
# Change JOIN clause to table list and WHERE condition
|
|
|
|
while (sql =~ /JOIN/)
|
|
|
|
sql =~ /((LEFT )?(OUTER )?JOIN (\w+) ON )(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
|
|
|
|
join_clause = $1 + $5
|
|
|
|
is_outer_join = $3
|
|
|
|
join_table = $4
|
|
|
|
join_condition = $5
|
|
|
|
join_condition.gsub!(/=/,"*") if is_outer_join
|
|
|
|
if (sql =~ /WHERE/)
|
|
|
|
sql.gsub!(/WHERE/,"WHERE (#{join_condition}) AND")
|
|
|
|
else
|
|
|
|
sql.gsub!(join_clause,"#{join_clause} WHERE #{join_condition}")
|
|
|
|
end
|
|
|
|
sql =~ /(FROM .+?)(?:LEFT|OUTER|JOIN|WHERE|$)/
|
|
|
|
from_clause = $1
|
|
|
|
sql.gsub!(from_clause,"#{from_clause}, #{join_table} ")
|
|
|
|
sql.gsub!(join_clause,"")
|
|
|
|
end
|
|
|
|
|
|
|
|
# ORDER BY _rowid if no explicit ORDER BY
|
|
|
|
# This will ensure that find(:first) returns the first inserted row
|
|
|
|
if (sql !~ /(ORDER BY)|(GROUP BY)/)
|
|
|
|
if (sql =~ /RETURN RESULTS/)
|
|
|
|
sql.sub!(/RETURN RESULTS/,"ORDER BY _rowid RETURN RESULTS")
|
|
|
|
else
|
|
|
|
sql << " ORDER BY _rowid"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sql
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_nulls_after_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
|
|
|
sql =~ /INSERT INTO (\w+) \((.*)\) VALUES\s*\((.*)\)/m
|
|
|
|
table = $1
|
|
|
|
cols = $2
|
|
|
|
values = $3
|
|
|
|
cols = cols.split(',')
|
|
|
|
values.gsub!(/'[^']*'/,"''")
|
|
|
|
values.gsub!(/"[^"]*"/,"\"\"")
|
|
|
|
values = values.split(',')
|
|
|
|
update_cols = []
|
|
|
|
values.each_index { |index| update_cols << cols[index] if values[index] =~ /\s*NULL\s*/ }
|
|
|
|
update_sql = "UPDATE #{table} SET"
|
|
|
|
update_cols.each { |col| update_sql << " #{col}=NULL," unless col.empty? }
|
|
|
|
update_sql.chop!()
|
|
|
|
update_sql << " WHERE #{pk}=#{quote(id_value)}"
|
|
|
|
execute(update_sql, name + " NULL Correction") if update_cols.size > 0
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|