Added OpenBase database adapter that builds on top of the...

Added OpenBase database adapter that builds on top of the http://www.spice-of-life.net/ruby-openbase/ driver. All functionality except LIMIT/OFFSET is supported (closes #3528) [derrickspell@cdmplus.com]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3932 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
上级 24021317
*SVN*
* Added OpenBase database adapter that builds on top of the http://www.spice-of-life.net/ruby-openbase/ driver. All functionality except LIMIT/OFFSET is supported #3528 [derrickspell@cdmplus.com]
* Rework table aliasing to account for truncated table aliases. Add smarter table aliasing when doing eager loading of STI associations. This allows you to use the association name in the order/where clause. [Jonathan Viney / Rick Olson] #4108 Example (SpecialComment is using STI):
Author.find(:all, :include => { :posts => :special_comments }, :order => 'special_comments.body')
......
......@@ -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 )
for adapter in %w( mysql postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
Rake::TestTask.new("test_#{adapter}") { |t|
t.libs << "test" << "test/connections/native_#{adapter}"
t.pattern = "test/*_test{,_#{adapter}}.rb"
......
......@@ -68,7 +68,7 @@
end
unless defined?(RAILS_CONNECTION_ADAPTERS)
RAILS_CONNECTION_ADAPTERS = %w(mysql postgresql sqlite firebird sqlserver db2 oracle sybase)
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase )
end
RAILS_CONNECTION_ADAPTERS.each do |adapter|
......
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/
return :float if field_type.downcase == "money"
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!
#
# Maintainer: derrickspell@cdmplus.com
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" },
: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 ======================================
def add_limit_offset!(sql, options) #:nodoc
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
......@@ -32,11 +32,23 @@ def recreate(base, suffix = nil)
end
def execute_sql_file(path, connection)
File.read(path).split(';').each_with_index do |sql, i|
begin
connection.execute("\n\n-- statement ##{i}\n#{sql}\n") unless sql.blank?
rescue ActiveRecord::StatementInvalid
#$stderr.puts "warning: #{$!}"
# OpenBase has a different format for sql files
if current_adapter?(:OpenBaseAdapter) then
File.read(path).split("go").each_with_index do |sql, i|
begin
# OpenBase does not support comments embedded in sql
connection.execute(sql,"SQL statement ##{i}") unless sql.blank?
rescue ActiveRecord::StatementInvalid
#$stderr.puts "warning: #{$!}"
end
end
else
File.read(path).split(';').each_with_index do |sql, i|
begin
connection.execute("\n\n-- statement ##{i}\n#{sql}\n") unless sql.blank?
rescue ActiveRecord::StatementInvalid
#$stderr.puts "warning: #{$!}"
end
end
end
end
......
......@@ -27,7 +27,9 @@ def test_indexes
@connection.add_index :accounts, :firm_id, :name => idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
assert_equal idx_name, indexes.first.name
# OpenBase does not have the concept of a named index
# Indexes are merely properties of columns.
assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
assert !indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
else
......@@ -80,4 +82,5 @@ def test_reset_table_with_non_integer_pk
assert_nothing_raised { sub.save! }
end
end
end
print "Using native OpenBase\n"
require_dependency 'fixtures/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
db1 = 'activerecord_unittest'
db2 = 'activerecord_unittest2'
ActiveRecord::Base.establish_connection(
:adapter => "openbase",
:username => "admin",
:password => "",
:database => db1
)
Course.establish_connection(
:adapter => "openbase",
:username => "admin",
:password => "",
:database => db2
)
CREATE TABLE accounts (
id integer UNIQUE INDEX DEFAULT _rowid,
firm_id integer,
credit_limit integer
)
go
CREATE PRIMARY KEY accounts (id)
go
CREATE TABLE funny_jokes (
id integer UNIQUE INDEX DEFAULT _rowid,
name char(50) DEFAULT NULL
)
go
CREATE PRIMARY KEY funny_jokes (id)
go
CREATE TABLE companies (
id integer UNIQUE INDEX DEFAULT _rowid,
type char(50),
ruby_type char(50),
firm_id integer,
name char(50),
client_of integer,
rating integer default 1
)
go
CREATE PRIMARY KEY companies (id)
go
CREATE TABLE developers_projects (
developer_id integer NOT NULL,
project_id integer NOT NULL,
joined_on date,
access_level integer default 1
)
go
CREATE TABLE developers (
id integer UNIQUE INDEX DEFAULT _rowid,
name char(100),
salary integer DEFAULT 70000,
created_at datetime,
updated_at datetime
)
go
CREATE PRIMARY KEY developers (id)
go
CREATE TABLE projects (
id integer UNIQUE INDEX DEFAULT _rowid,
name char(100),
type char(255)
)
go
CREATE PRIMARY KEY projects (id)
go
CREATE TABLE topics (
id integer UNIQUE INDEX DEFAULT _rowid,
title char(255),
author_name char(255),
author_email_address char(255),
written_on datetime,
bonus_time time,
last_read date,
content char(4096),
approved boolean default true,
replies_count integer default 0,
parent_id integer,
type char(50)
)
go
CREATE PRIMARY KEY topics (id)
go
CREATE TABLE customers (
id integer UNIQUE INDEX DEFAULT _rowid,
name char,
balance integer default 0,
address_street char,
address_city char,
address_country char,
gps_location char
)
go
CREATE PRIMARY KEY customers (id)
go
CREATE TABLE orders (
id integer UNIQUE INDEX DEFAULT _rowid,
name char,
billing_customer_id integer,
shipping_customer_id integer
)
go
CREATE PRIMARY KEY orders (id)
go
CREATE TABLE movies (
movieid integer UNIQUE INDEX DEFAULT _rowid,
name text
)
go
CREATE PRIMARY KEY movies (movieid)
go
CREATE TABLE subscribers (
nick CHAR(100) NOT NULL DEFAULT _rowid,
name CHAR(100)
)
go
CREATE PRIMARY KEY subscribers (nick)
go
CREATE TABLE booleantests (
id integer UNIQUE INDEX DEFAULT _rowid,
value boolean
)
go
CREATE PRIMARY KEY booleantests (id)
go
CREATE TABLE defaults (
id integer UNIQUE INDEX ,
modified_date date default CURDATE(),
modified_date_function date default NOW(),
fixed_date date default '2004-01-01',
modified_time timestamp default NOW(),
modified_time_function timestamp default NOW(),
fixed_time timestamp default '2004-01-01 00:00:00.000000-00',
char1 char(1) default 'Y',
char2 char(50) default 'a char field',
char3 text default 'a text field'
)
go
CREATE TABLE auto_id_tests (
auto_id integer UNIQUE INDEX DEFAULT _rowid,
value integer
)
go
CREATE PRIMARY KEY auto_id_tests (auto_id)
go
CREATE TABLE entrants (
id integer UNIQUE INDEX ,
name text,
course_id integer
)
go
CREATE TABLE colnametests (
id integer UNIQUE INDEX ,
references integer NOT NULL
)
go
CREATE TABLE mixins (
id integer UNIQUE INDEX DEFAULT _rowid,
parent_id integer,
type char,
pos integer,
lft integer,
rgt integer,
root_id integer,
created_at timestamp,
updated_at timestamp
)
go
CREATE PRIMARY KEY mixins (id)
go
CREATE TABLE people (
id integer UNIQUE INDEX DEFAULT _rowid,
first_name text,
lock_version integer default 0
)
go
CREATE PRIMARY KEY people (id)
go
CREATE TABLE readers (
id integer UNIQUE INDEX DEFAULT _rowid,
post_id integer NOT NULL,
person_id integer NOT NULL
)
go
CREATE PRIMARY KEY readers (id)
go
CREATE TABLE binaries (
id integer UNIQUE INDEX DEFAULT _rowid,
data object
)
go
CREATE PRIMARY KEY binaries (id)
go
CREATE TABLE computers (
id integer UNIQUE INDEX ,
developer integer NOT NULL,
extendedWarranty integer NOT NULL
)
go
CREATE TABLE posts (
id integer UNIQUE INDEX ,
author_id integer,
title char(255),
type char(255),
body text
)
go
CREATE TABLE comments (
id integer UNIQUE INDEX ,
post_id integer,
type char(255),
body text
)
go
CREATE TABLE authors (
id integer UNIQUE INDEX ,
name char(255) default NULL
)
go
CREATE TABLE tasks (
id integer UNIQUE INDEX DEFAULT _rowid,
starting datetime,
ending datetime
)
go
CREATE PRIMARY KEY tasks (id)
go
CREATE TABLE categories (
id integer UNIQUE INDEX ,
name char(255),
type char(255)
)
go
CREATE TABLE categories_posts (
category_id integer NOT NULL,
post_id integer NOT NULL
)
go
CREATE TABLE fk_test_has_pk (
id INTEGER NOT NULL DEFAULT _rowid
)
go
CREATE PRIMARY KEY fk_test_has_pk (id)
go
CREATE TABLE fk_test_has_fk (
id INTEGER NOT NULL DEFAULT _rowid,
fk_id INTEGER NOT NULL REFERENCES fk_test_has_pk.id
)
go
CREATE PRIMARY KEY fk_test_has_fk (id)
go
CREATE TABLE keyboards (
key_number integer UNIQUE INDEX DEFAULT _rowid,
name char(50)
)
go
CREATE PRIMARY KEY keyboards (key_number)
go
CREATE TABLE legacy_things (
id INTEGER NOT NULL DEFAULT _rowid,
tps_report_number INTEGER default NULL,
version integer NOT NULL default 0
)
go
CREATE PRIMARY KEY legacy_things (id)
go
\ No newline at end of file
CREATE TABLE courses (
id integer UNIQUE INDEX DEFAULT _rowid,
name text
)
go
CREATE PRIMARY KEY courses (id)
go
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册