diff options
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG | 2 | ||||
-rwxr-xr-x | activerecord/Rakefile | 2 | ||||
-rwxr-xr-x | activerecord/lib/active_record.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/openbase_adapter.rb | 349 | ||||
-rw-r--r-- | activerecord/test/aaa_create_tables_test.rb | 22 | ||||
-rw-r--r-- | activerecord/test/adapter_test.rb | 5 | ||||
-rw-r--r-- | activerecord/test/connections/native_openbase/connection.rb | 22 | ||||
-rw-r--r-- | activerecord/test/fixtures/db_definitions/openbase.drop.sql | 2 | ||||
-rw-r--r-- | activerecord/test/fixtures/db_definitions/openbase.sql | 282 | ||||
-rw-r--r-- | activerecord/test/fixtures/db_definitions/openbase2.drop.sql | 2 | ||||
-rw-r--r-- | activerecord/test/fixtures/db_definitions/openbase2.sql | 7 |
11 files changed, 689 insertions, 8 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a364b38f33..4e6ee6e216 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -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') diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 51117275bd..afb2a6d2cb 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -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" diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c0c81b335d..293c86f13b 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -68,7 +68,7 @@ ActiveRecord::Base.class_eval do 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| diff --git a/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb b/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb new file mode 100644 index 0000000000..e137278398 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/openbase_adapter.rb @@ -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 diff --git a/activerecord/test/aaa_create_tables_test.rb b/activerecord/test/aaa_create_tables_test.rb index fe83045c8f..8ff6c64e55 100644 --- a/activerecord/test/aaa_create_tables_test.rb +++ b/activerecord/test/aaa_create_tables_test.rb @@ -32,11 +32,23 @@ class AAACreateTablesTest < Test::Unit::TestCase 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 diff --git a/activerecord/test/adapter_test.rb b/activerecord/test/adapter_test.rb index af94904eee..93e1ca973b 100644 --- a/activerecord/test/adapter_test.rb +++ b/activerecord/test/adapter_test.rb @@ -27,7 +27,9 @@ class AdapterTest < Test::Unit::TestCase @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 @@ class AdapterTest < Test::Unit::TestCase assert_nothing_raised { sub.save! } end end + end diff --git a/activerecord/test/connections/native_openbase/connection.rb b/activerecord/test/connections/native_openbase/connection.rb new file mode 100644 index 0000000000..01a2ffe646 --- /dev/null +++ b/activerecord/test/connections/native_openbase/connection.rb @@ -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 +) diff --git a/activerecord/test/fixtures/db_definitions/openbase.drop.sql b/activerecord/test/fixtures/db_definitions/openbase.drop.sql new file mode 100644 index 0000000000..fb40e3f213 --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/openbase.drop.sql @@ -0,0 +1,2 @@ +DROP ALL +go
\ No newline at end of file diff --git a/activerecord/test/fixtures/db_definitions/openbase.sql b/activerecord/test/fixtures/db_definitions/openbase.sql new file mode 100644 index 0000000000..9ca1a7d06e --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/openbase.sql @@ -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
\ No newline at end of file diff --git a/activerecord/test/fixtures/db_definitions/openbase2.drop.sql b/activerecord/test/fixtures/db_definitions/openbase2.drop.sql new file mode 100644 index 0000000000..ea1571da0b --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/openbase2.drop.sql @@ -0,0 +1,2 @@ +DROP TABLE courses +go diff --git a/activerecord/test/fixtures/db_definitions/openbase2.sql b/activerecord/test/fixtures/db_definitions/openbase2.sql new file mode 100644 index 0000000000..a37c4f4c31 --- /dev/null +++ b/activerecord/test/fixtures/db_definitions/openbase2.sql @@ -0,0 +1,7 @@ +CREATE TABLE courses ( + id integer UNIQUE INDEX DEFAULT _rowid, + name text +) +go +CREATE PRIMARY KEY courses (id) +go
\ No newline at end of file |