Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit 416385a09d61758ba8e2b2ff30dd64c8b9540883 1 parent 2402131
David Heinemeier Hansson dhh authored
2  activerecord/CHANGELOG
View
@@ -1,5 +1,7 @@
*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')
2  activerecord/Rakefile
View
@@ -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"
2  activerecord/lib/active_record.rb
View
@@ -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|
349 activerecord/lib/active_record/connection_adapters/openbase_adapter.rb
View
@@ -0,0 +1,349 @@
+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
22 activerecord/test/aaa_create_tables_test.rb
View
@@ -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
5 activerecord/test/adapter_test.rb
View
@@ -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
22 activerecord/test/connections/native_openbase/connection.rb
View
@@ -0,0 +1,22 @@
+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
+)
2  activerecord/test/fixtures/db_definitions/openbase.drop.sql
View
@@ -0,0 +1,2 @@
+DROP ALL
+go
282 activerecord/test/fixtures/db_definitions/openbase.sql
View
@@ -0,0 +1,282 @@
+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
2  activerecord/test/fixtures/db_definitions/openbase2.drop.sql
View
@@ -0,0 +1,2 @@
+DROP TABLE courses
+go
7 activerecord/test/fixtures/db_definitions/openbase2.sql
View
@@ -0,0 +1,7 @@
+CREATE TABLE courses (
+ id integer UNIQUE INDEX DEFAULT _rowid,
+ name text
+)
+go
+CREATE PRIMARY KEY courses (id)
+go
Please sign in to comment.
Something went wrong with that request. Please try again.